R (4.6) Module 1 – Start here: R, the console and a working setup
Scripts, projects and file paths that travel
Organise an R analysis as a project folder, run scripts with source() and Rscript, pass arguments, build portable paths and list files with fixed patterns.
What you will learn
- Organise an analysis as a project folder with raw data, scripts and outputs kept apart
- Run a script with source() and with Rscript, and read its command-line arguments
- Build portable paths with file.path() and relative paths instead of setwd()
- List and test files with list.files(), file.exists() and fixed patterns
- Use Sys.getenv() and system2() to read settings and run other programs
Before you start
What you’ll learn · Before you start
What you will learn
- Organise an analysis as a project folder with raw data, scripts and outputs kept apart
- Run a script with source() and with Rscript, and read its command-line arguments
- Build portable paths with file.path() and relative paths instead of setwd()
- List and test files with list.files(), file.exists() and fixed patterns
- Use Sys.getenv() and system2() to read settings and run other programs
Before you start
On this page
A script that starts with setwd("C:/Users/asha/Desktop/survey") runs on one computer only. On every other one it
stops on its first line with “cannot change working directory”, before it has read a single row. The fix is a habit
rather than a tool: one folder per analysis, every path relative to that folder, and scripts that run from their
first line to their last on their own.
Below you lay out a project folder, run scripts with source() and with Rscript and give them arguments, build paths
that work on Windows, macOS and Linux, list files without being caught out by a regular expression, and read settings
from the environment instead of writing them into the code.
A project folder keeps raw data, scripts and output apart
Put everything one analysis needs in one folder, and give each kind of file its own place inside it. Follow the data of a small clinic-visits analysis through such a folder:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Raw data, untouched. The raw file goes into data/raw exactly as it arrived, and nothing ever edits it there. If a later step turns out wrong, you can always start again from it.
- A script reads it. R/01-clean.R reads the raw file. Scripts live in R/, numbered in the order they run.
- Cleaned data, by script. It writes the cleaned table to data/clean. A cleaned file is not edited by hand either; to change it, change the script and run it again.
- The analysis reads clean data. R/02-analyse.R reads only the clean data, so every number it computes leads back to the raw file through code.
- Output can be made again. Tables and figures go to output/. Delete output/ and data/clean/, and running the two scripts makes them again.
- Everything in one folder. README.txt says where the raw data came from and which script runs first. A colleague who copies the folder needs nothing from your computer.
The rule behind the layout is that data only ever changes by running code. Raw data stays as it arrived, cleaned data and output are made by scripts, and so anyone can delete them and make them again. This script creates the folders, with a raw file and a README, and lists what it made:
# One folder per analysis: raw data, cleaned data, scripts and output apart.
project <- "clinic-visits"
for (part in c("data/raw", "data/clean", "R", "output")) {
dir.create(file.path(project, part), recursive = TRUE)
}
writeLines(
c("site,visits", "north,12", "south,7"),
file.path(project, "data", "raw", "visits.csv")
)
writeLines("Where the data came from, and when.", file.path(project, "README.txt"))
# Every folder and file of the project, relative to its top folder.
list.files(project, recursive = TRUE, include.dirs = TRUE) from session import run, version
version()
run("""
Rscript project_skeleton.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ Rscript project_skeleton.R [1] "data" "data/clean" "data/raw" [4] "data/raw/visits.csv" "output" "R" [7] "README.txt"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 project_skeleton_session.py
list.files(project, recursive = TRUE, include.dirs = TRUE) lists every folder and file under clinic-visits, as
paths relative to it: data/raw/visits.csv is a path from the project’s top folder, which is the point of the
layout.
Scripts find these files from the working directory: The folder that relative paths start from. getwd() shows it; opening a project in RStudio, or starting R or Rscript in a folder, sets it., the folder that relative
paths start from. Opening a project in RStudio sets the working directory to the project’s folder and starts a new R
session; from a terminal, start R or Rscript in the project’s top folder. Two more habits keep an analysis honest:
- Restart R often, and do not save the workspace. At the end of a session R offers to save every object into a file
called
.RData, and the next R started in that folder loads it again by itself. Objects that come back that way, rather than from your scripts, make results that no script can reproduce. Answer no, or start R with--no-save. - Name files plainly. The tidyverse style guide asks for
.Rfile names in lower case, with words joined by-or_and no spaces or symbols, so that they work in every system’s paths.
Key idea
Keep raw data untouched and let scripts make everything else, inside one folder whose paths all start from its top: a colleague who copies the folder can then rerun the whole analysis.
Checkpoint
Check: A project folder for one analysis
source() runs a script in your session; Rscript runs it on its own
There are two ways to run a script, and they differ in where the results end up. source("file.R") runs every line
of the file in your current session, and what the script makes stays in your workspace. Rscript: The program that runs an R script from a terminal in a new R session, which ends with the script. Words after the file name reach commandArgs().
runs the file in a new R session, which ends when the script ends. This script reads the visits and keeps their total:
# Reads the raw visits and keeps their total in total_visits.
visits <- read.csv(file.path("data", "raw", "visits.csv"))
total_visits <- sum(visits$visits) The session below sources it at the console, then runs the same file with Rscript, then asks a new R whether the
total exists:
from session import run, version
version()
# source() inside a console session, then the same script run on its own with Rscript, then a new R that looks for
# what the script made.
run("""
R --vanilla --quiet -f source_demo.R
Rscript R/count_sites.R
Rscript -e 'exists("total_visits")'
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f source_demo.R
> # source() runs every line of a script in this session.
> source(file.path("R", "count_sites.R"))
> # What the script made stays in the workspace.
> total_visits
[1] 43
> ls()
[1] "total_visits" "visits"
$ Rscript R/count_sites.R
$ Rscript -e 'exists("total_visits")'
[1] FALSE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 source_session.py
After source() the workspace holds total_visits, 43, and visits. Rscript R/count_sites.R prints nothing,
because the script has only assignments, and leaves nothing behind: the new R answers FALSE. Use source() while you
work on an analysis, and Rscript for runs that must not depend on anything in your session, such as the final run
of a report.
A script run by Rscript can take command-line argument: A word typed after a script's name when it is run, such as 10 in Rscript summarise.R data.csv 10. R receives every argument as text., words after its
file name, which commandArgs(trailingOnly = TRUE) returns. Step through a run of summarise.R, which keeps the
sites with at least a given number of visits:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The command. You type Rscript summarise.R data/raw/visits.csv 10 in a terminal, in the project's top folder. Rscript starts a new R session with an empty workspace.
- The arguments are text. commandArgs(trailingOnly = TRUE) returns the words after the script's name, "data/raw/visits.csv" and "10". Both are text, as str(args) printed.
- Checked and converted. as.numeric() turns "10" into the number 10. The script stops at once if that fails, or if the file does not exist.
- Read the data. The script reads the four sites from the path it was given, which is relative to the working directory, the project's top folder.
- Write the result. It writes the two sites with at least 10 visits to output/busy-sites-10.csv and prints a line about it.
- Exit status 0. The session ends, and its workspace with it. Rscript exits with status 0, success. Only the files remain.
- A bad argument. With ten instead of 10, as.numeric() gives NA, so the script calls stop() with a message, and Rscript exits with status 1, which tells a scheduler or another script that the run failed.
# Usage: Rscript summarise.R <csv file> <minimum visits>
args <- commandArgs(trailingOnly = TRUE)
str(args)
if (length(args) != 2) {
stop("give a CSV file and a minimum number of visits")
}
path <- args[1]
minimum <- suppressWarnings(as.numeric(args[2]))
if (is.na(minimum)) stop("the minimum must be a number, not ", sQuote(args[2]))
if (!file.exists(path)) stop("there is no file ", path)
visits <- read.csv(path)
busy <- visits[visits$visits >= minimum, ]
dir.create("output", showWarnings = FALSE)
out <- file.path("output", paste0("busy-sites-", minimum, ".csv"))
write.csv(busy, out, row.names = FALSE)
cat("Read", nrow(visits), "sites; wrote", nrow(busy), "with at least", minimum, "visits to", out, "\n") from session import run, version
version()
# Two good runs, the file the first one wrote, then two runs that the script stops on purpose.
run("""
Rscript summarise.R data/raw/visits.csv 10
cat output/busy-sites-10.csv
Rscript summarise.R data/raw/visits.csv 8
!Rscript summarise.R data/raw/visits.csv ten
!Rscript summarise.R
""") Output
# R version 4.6.1 -- "Happy Hop" $ Rscript summarise.R data/raw/visits.csv 10 chr [1:2] "data/raw/visits.csv" "10" Read 4 sites; wrote 2 with at least 10 visits to output/busy-sites-10.csv $ cat output/busy-sites-10.csv "site","visits" "north",12 "east",15 $ Rscript summarise.R data/raw/visits.csv 8 chr [1:2] "data/raw/visits.csv" "8" Read 4 sites; wrote 3 with at least 8 visits to output/busy-sites-8.csv $ Rscript summarise.R data/raw/visits.csv ten chr [1:2] "data/raw/visits.csv" "ten" Error: the minimum must be a number, not ‘ten’ Execution halted [exit status 1] $ Rscript summarise.R chr(0) Error: give a CSV file and a minimum number of visits Execution halted [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 summarise_session.py
The first run kept the 2 sites with at least 10 visits, north and east, and the second the 3 with at least 8. With
ten, and with no arguments at all, the script stopped on purpose with a message that says what is wrong, and
Rscript exited with status 1, which tells a scheduler or another script that the run failed.
Comparing arguments while they are still text
Every command-line argument is text: str(args) printed chr [1:2]. Compared as text, "10" >= "9" is FALSE,
because the character 1 sorts before 9. Convert first, as.numeric(args[2]), check the result for NA, and only
then compare.
Checkpoint
Check: source(), Rscript and arguments
Paths that work on every computer
A path written as one piece of text, "data\\raw\\visits.csv" or "/Users/asha/survey/data.csv", carries the
habits of one system or the folders of one person. Build paths from their parts instead, relative to the project’s
top folder:
from session import run, version
version()
run("""
R --vanilla --quiet -f paths.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f paths.R
> # file.path() joins the parts with "/", which R also accepts on Windows.
> raw <- file.path("data", "raw", "visits.csv")
> raw
[1] "data/raw/visits.csv"
> # Take a path apart without cutting text by hand.
> basename(raw)
[1] "visits.csv"
> dirname(raw)
[1] "data/raw"
> tools::file_ext(raw)
[1] "csv"
> tools::file_path_sans_ext(basename(raw))
[1] "visits"
> # A relative path starts from the working directory.
> file.exists(raw)
[1] TRUE
> file.exists(file.path("data", "raw", "missing.csv"))
[1] FALSE
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 paths_session.py
file.path() joins the parts with /, and its help page notes that /, not \, is the separator it uses on
Windows too, so the same line works everywhere. The other functions take a path apart without any cutting of text:
| Function | On data/raw/visits.csv it gives |
|---|---|
basename() |
visits.csv, the last part |
dirname() |
data/raw, everything before it |
tools::file_ext() |
csv, the extension without its dot |
tools::file_path_sans_ext() |
the path without its extension |
file.exists() |
TRUE or FALSE for each path |
A relative path: A path that starts from the working directory, such as data/raw/visits.csv, rather than from the top of the disk. such as data/raw/visits.csv starts from the working directory, so
it works on any computer as long as R starts in the project’s top folder. The here package, on CRAN, goes one step
further: it builds paths from a project’s top folder wherever a script runs, and its documentation calls setwd()
“fragile and dependent on the way you organize your files”.
setwd() with a folder of your own computer
setwd("C:/Users/asha/Desktop/survey") at the top of a script fails on every other computer with “cannot change
working directory”, and on yours once the folder moves. Start R in the project’s folder instead, and write paths
as file.path("data", "raw", "visits.csv").
Checkpoint
Check: Paths that work on every computer
list.files() matches a regular expression unless you ask for fixed
list.files() lists the files of a folder, sorted alphabetically, and its pattern argument picks some of them.
pattern is a regular expression: A pattern language for text, in which . matches any character, ^ the start and $ the end. list.files() reads its pattern argument as one., not plain text, and that catches people out.
Here are five file names and four ways to ask for the CSV files:
from session import run, version
version()
run("""
R --vanilla --quiet -f list_files.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f list_files.R
> # Five files in a folder called inbox.
> files <- c("visits.csv", "visits.csv.bak", "visits_csv.txt", "old-visits.csv", "notes.txt")
> dir.create("inbox")
> invisible(file.create(file.path("inbox", files)))
> # pattern is a regular expression, in which "." matches any character.
> list.files("inbox", pattern = "visits.csv")
[1] "old-visits.csv" "visits_csv.txt" "visits.csv" "visits.csv.bak"
> # Escape the dot and anchor the end: only names that end in .csv.
> list.files("inbox", pattern = "\\.csv$")
[1] "old-visits.csv" "visits.csv"
> # fixed = TRUE, new in R 4.6.0, matches the text exactly as written.
> list.files("inbox", pattern = "visits.csv", fixed = TRUE)
[1] "old-visits.csv" "visits.csv" "visits.csv.bak"
> # full.names = TRUE gives paths you can open.
> list.files("inbox", pattern = "\\.csv$", full.names = TRUE)
[1] "inbox/old-visits.csv" "inbox/visits.csv"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 list_files_session.py
In a regular expression . matches any character, and a pattern may match anywhere in a name. So pattern = "visits.csv" also matched visits_csv.txt, through its underscore, and visits.csv.bak, which merely contains the
text. Two fixes, for two needs:
- A pattern that means what it says:
"\\.csv$", with the dot escaped as\\.and$for the end of the name, lists only the names that end in.csv. - Plain text:
fixed = TRUE, an argument new in R 4.6.0, matches the pattern exactly as written. It no longer matchedvisits_csv.txt, but it still matchedvisits.csv.bak, which contains the text.
full.names = TRUE adds the folder to each name, giving paths you can open, and recursive = TRUE looks inside
subfolders, as the project skeleton did.
Checkpoint
Check: Listing files with patterns
Settings and other programs: Sys.getenv() and system2()
A script should not change from one computer to the next, but some settings do: a folder outside the project, a
threshold that differs between runs, a password. Read them from environment variable: A named setting that the operating system passes to the programs it starts. R reads one with Sys.getenv() and always gets text., which the system or the person running the script sets, and run other programs with system2():
from session import run, version
version()
run("""
R --vanilla --quiet -f settings.R
""") Output
# R version 4.6.1 -- "Happy Hop"
$ R --vanilla --quiet -f settings.R
> # Sys.getenv() reads an environment variable; unset is the value when it is missing.
> Sys.getenv("CLINIC_MIN_VISITS", unset = "10")
[1] "10"
> # Set it for this session, then read it again.
> Sys.setenv(CLINIC_MIN_VISITS = "8")
> as.numeric(Sys.getenv("CLINIC_MIN_VISITS"))
[1] 8
> # system2() runs another program; stdout = TRUE returns what it printed.
> system2("Rscript", c("-e", shQuote("cat(6 * 7)")), stdout = TRUE)
[1] "42"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 settings_session.py
Sys.getenv("CLINIC_MIN_VISITS", unset = "10") returned "10" because the variable was not set; after
Sys.setenv() it returned "8", text again, which as.numeric() turns into a number. system2() ran a second R,
and with stdout = TRUE it returned what that program printed as text, "42".
Sys.setenv() lasts for the current R session and the programs it starts, and changes nothing outside them. To set
a variable for every session, write a line such as CLINIC_MIN_VISITS=8 in a file called .Renviron, which R reads
when it starts, from the current folder or else from your home folder. R --vanilla skips that file, and a
.Renviron that holds a secret stays out of version control.
Keep passwords and tokens out of scripts
A script is shared, copied and kept in version control, so a password written into it travels with it. Read it
with Sys.getenv("SURVEY_API_TOKEN"), which returns an empty string when it is not set, and stop the script with a
message when it is empty.
Checkpoint
Check: Environment variables and other programs
Practise: a project that runs anywhere
Build a small project on your own computer, in order:
- Make a folder called
clinic-visitswithdata/raw,data/clean,Randoutputinside it, and put a short CSV file of sites and visits intodata/raw. - Write
R/summarise.Rlike the script above, withfile.path()for every path and nosetwd(). - In a terminal in the project’s top folder, run
Rscript R/summarise.R data/raw/visits.csv 10, then withtenand check that it stops with your message. - Copy the folder somewhere else, run the same command there, and compare the files in
output.
Finally, the self-check covers projects, scripts, paths, file patterns and settings. It picks the idea you are least sure of, explains every answer as soon as you give it, and ends with a summary that links each idea to the part to reread:
Practice that adapts to you
Practise this lesson
16 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: why should an analysis script not call setwd() with an absolute path?
Because the path exists only on the computer where the script was written. setwd("C:/Users/asha/Desktop/survey")
stops with “cannot change working directory” on every other computer, before any work is done, and on the same
computer it breaks as soon as the folder moves or is renamed. It also hides an assumption: every relative path after it
depends on that one line. The portable way is to start R in the project’s top folder, which opening an RStudio project
does and a terminal does when you run Rscript there, and to build every path from the project’s top with
file.path(). The here package goes one step further and finds the project’s top folder wherever the script runs.
setwd() is still fine at the console for a quick look, as long as no script depends on it.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
An analysis script runs every time from your editor, but when a scheduler runs it at night with Rscript it stops with "cannot open the connection" and a warning that it cannot open data/raw/visits.csv. List the likely causes, how you would find the real one, and how you make the script run the same both ways.
-
Mid to senior
You are leaving a project, and next month a colleague must rerun your analysis from the raw data on another computer, without you. What do you put in the project folder, what do you leave out, and how does your colleague check that the rerun matches your results?
Key takeaways
- One folder per analysis keeps raw data untouched in
data/raw, scripts inR/and everything they make indata/cleanandoutput, so a copy of the folder can rerun the whole analysis. source()runs a script in your session and leaves its objects in your workspace;Rscriptruns it in a new session that keeps nothing, and exits with status 1 when the script stops with an error.commandArgs(trailingOnly = TRUE)returns a script’s arguments as text, which must be converted and checked before they are used.file.path()and relative paths from the project’s top folder work on every system, andbasename(),dirname()andtools::file_ext()take paths apart, wheresetwd()with an absolute path breaks on the next computer.list.files(pattern = )reads a regular expression, in which.matches any character; anchor and escape it, or usefixed = TRUEfrom R 4.6.0.Sys.getenv()reads settings and secrets from the environment, andsystem2()runs another program and can return what it printed.
Terms in this lesson
- command-line argument
- A word typed after a script's name when it is run, such as 10 in Rscript summarise.R data.csv 10. R receives every argument as text.
- environment variable
- A named setting that the operating system passes to the programs it starts. R reads one with Sys.getenv() and always gets text.
- regular expression
- A pattern language for text, in which . matches any character, ^ the start and $ the end. list.files() reads its pattern argument as one.
- relative path
- A path that starts from the working directory, such as data/raw/visits.csv, rather than from the top of the disk.
- Rscript
- The program that runs an R script from a terminal in a new R session, which ends with the script. Words after the file name reach commandArgs().
- working directory
- The folder that relative paths start from. getwd() shows it; opening a project in RStudio, or starting R or Rscript in a folder, sets it.
References
- An Introduction to R (sections 1.10 and 1.11, and appendix B, invoking R) (The R Core Team)
- Scripting Front-End for R (Rscript) (The R Core Team)
- Extract Command Line Arguments (commandArgs) (The R Core Team)
- Read R Code from a File, a Connection or Expressions (source) (The R Core Team)
- Get or Set Working Directory (getwd, setwd) (The R Core Team)
- Construct Path to File (file.path) (The R Core Team)
- Manipulate File Paths (basename, dirname) (The R Core Team)
- File Utilities (file_ext, file_path_sans_ext) (The R Core Team)
- File Manipulation (file.exists, file.create) (The R Core Team)
- List the Files in a Directory/Folder (list.files) (The R Core Team)
- Get Environment Variables (Sys.getenv) (The R Core Team)
- Set or Unset Environment Variables (Sys.setenv) (The R Core Team)
- Initialization at Start of an R Session (Startup, .Renviron) (The R Core Team)
- Invoke a System Command (system2) (The R Core Team)
- R NEWS (changes in R 4.6.0, list.files fixed) (The R Core Team)
- RStudio IDE user guide: projects (Posit)
- The tidyverse style guide: files (The tidyverse team (Posit))
- here (r-lib)
Report a problem with this lesson
Kept only in this browser. Your Learn progress