Connections and the File System
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Everything on this page is base R (the base, utils and tools packages that ship with R); no add-on package is run, and the lintr 3.4.0 default linters were used only to check the scripts. A connection is R’s answer to the question "where does this text or these bytes come from, and where do they go?": a file, a compressed file, a pipe to another program, a socket, a character vector or a block of memory are all objects that readLines(), writeLines(), scan(), readBin() and a dozen other functions accept in the same way. The second half of the page is about the file system around them: building and taking apart paths, listing and copying and deleting files, temporary files, running other programs, archives, and the environment variables that decide where R looks for things. It also collects the exact text of the errors and warnings you will meet, because "cannot open the connection" says very little by itself.
Every example is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)), and the lines that start with #> are what it printed. The scripts pass the default lintr linters. Errors and warnings that the page wants to show are caught with tryCatch() and printed with conditionMessage(), so the text is exactly what R says and the script still ends normally. The scripts run in a Linux container and never print a path that belongs to it: tempdir() is a per-session directory whose name changes on every run, so the examples print basename(), relative paths or the result of file.exists(), and where a message contains a path it is replaced by <path> in the code that prints it. A few results depend on the operating system, not on R (the order of bytes on the machine, the exit status of a failed command, which external programs are installed); the text says so where they appear. The container had the zip and unzip programs installed, so the zip() example ran; zip() cannot work without an external zip program. The scripts that read the books.csv file of the running library-shelf scenario (described on the section index) start from the shelf directory, so they say readLines("books.csv") with no directory.
This page is the second of the five data import and export pages. Reading and Writing Text Data covers what to do with the text once you can reach it (read.csv(), scan(), write.table()); Serialization: RDS, RData and JSON covers saveRDS(), load() and JSON, which also read and write through connections; HTTP and Web APIs covers download.file() and the url() connection in earnest. The encodings of text files are explained in Encodings, Locales and Internationalization; this page only shows where a connection fits in.
What a connection is
The R Data Import/Export manual introduces connections as a flexible interface to file-like objects that replaces the use of file names, and credits the idea to Chambers (1998) and Ripley (2001); the help page adds that R’s connections are modelled on those of S version 4 and go well beyond them with output text connections and URL, compressed and socket connections (R Data Import/Export: Connections). A connection object is a small integer with the classes "connection" and a more specific one ("file", "gzfile", "textConnection", "sockconn", and so on); the state (the file position, the buffer, the open mode) lives inside R, and the integer is only a handle into R’s table of connections. That is why a copy of the object is the same connection, why close() makes every copy invalid at once, and why a connection that is not closed is a resource leak, like a file descriptor in C.
The connections that R can create, and the functions that make them, are:
| Function | Reads or writes | Notes |
|---|---|---|
|
an ordinary file; also |
Opens for reading, writing, appending or both, in text or binary mode. For a text-mode read it recognises compressed files from the first bytes. A description that starts with |
|
a |
Read only. The |
|
a file compressed with gzip, bzip2, xz or zstd |
|
|
one member of a zip archive |
Read only, binary. |
|
wraps another binary connection in gzip decompression (or compression) |
Used for compressed data that arrives over a |
|
the standard input or output of another program |
The command runs in a shell when the connection is opened. Read or write, not append. |
|
a named pipe |
Needs operating system support; |
|
a TCP socket, as client or server |
|
|
a character vector (input) or a character vector being filled (output) |
Always open. Copies the vector when it is created. |
|
a raw vector |
A file in memory, for binary data; |
|
the console and the two output streams |
Always open and cannot be opened or closed. |
Source: R documentation: connections
The functions that accept a connection are the text-mode readLines(), writeLines(), cat(), sink(), scan(), parse(), read.dcf(), dput() and dump() (and so read.table() and source()), and the binary-mode readBin(), readChar(), writeBin(), writeChar(), load() and save(). Almost all of them also accept a character string, which they treat as the name of a file; that is the same thing as creating a file() connection, using it and closing it again inside the call.
Creating, opening and closing
By default a connection is not opened when it is created. The manual states the rule that every function using a connection follows: open the connection if it is not already open, close it after use only if the function opened it, and in the manual’s words "leave the connection in the state you found it in" (R Data Import/Export: Types of connections). Creating the connection touches nothing on disk; open() opens it, isOpen() asks, and close() closes it and destroys it.
# A connection starts out closed: creating it touches nothing on disk.
path <- tempfile(fileext = ".txt")
con <- file(path)
class(con)
#> [1] "file" "connection"
isOpen(con)
#> [1] FALSE
unlist(summary(con)[c("class", "mode", "text", "opened", "can read")])
#> class mode text opened can read
#> "file" "r" "text" "closed" "yes"
file.exists(path)
#> [1] FALSE
# Open it for writing, write three lines, close it.
open(con, "w")
isOpen(con)
#> [1] TRUE
isOpen(con, "write")
#> [1] TRUE
writeLines(c("alpha", "beta", "gamma"), con)
close(con)
file.exists(path)
#> [1] TRUE
# close() destroys the connection: the R object is only a dead handle now.
con
#> A connection, specifically, ‘file’, but invalid.
tryCatch(isOpen(con), error = \(e) conditionMessage(e))
#> [1] "invalid connection"
# Mode "a" appends; mode "w" truncates, even if nothing is written.
con <- file(path, "a")
writeLines("delta", con)
close(con)
readLines(path)
#> [1] "alpha" "beta" "gamma" "delta"
con <- file(path, "w")
close(con)
file.size(path)
#> [1] 0
Source: R documentation: connections
Three details are worth noticing in the output. The file did not exist until the connection was opened for writing. close() is not "pause": it destroys the connection, so the variable that held it is now only a dead handle, printed as "but invalid", and any further use is the error invalid connection. And mode "w" empties the file when the connection is opened, even if nothing is written afterwards, which is why a file(path, "w") that is created and closed leaves a file of size zero.
The open modes
The open argument of file(), and the open argument of open(), take a short string. The first letter is the direction, b means binary, and + means both directions.
| Mode | Meaning | Remarks |
|---|---|---|
|
Read, text mode |
The default when a connection is opened without a mode. The file must exist. |
|
Write, text mode |
Creates the file or empties it. |
|
Append, text mode |
Creates the file if it is missing. For a compressed file it appends a new compressed stream. |
|
The same in binary mode |
Required by |
|
Read and write |
The write position starts at the beginning of the file. |
|
Read and write, emptying the file first |
File and socket connections only. R keeps a separate read position and write position. |
|
Read and append |
The default mode of socket connections. |
Source: R documentation: connections
Not every mode applies to every connection: a url() can only be read, a pipe() is opened for either reading or writing, and only file and socket connections can be opened for both. An unsupported mode is "usually silently substituted", in the words of the help page, so test the connection (isOpen(), summary()) rather than assume. The compressed-file functions are an exception to the default: because the compressed file must always be opened in binary mode, gzfile(), bzfile(), xzfile() and zstdfile() read with "rb" when no mode is given.
Implicit and explicit opening
When a connection is not open, the first function that uses it opens it in the mode it needs and closes it again when it returns. That is convenient and has two consequences that the next script shows. Each call starts again at the top of the file, so two readLines(con, n = 1) calls on an unopened connection return the same first line, and a function that needs write access opens the connection in "wt" mode, which empties the file.
path <- tempfile(fileext = ".txt")
writeLines(c("alpha", "beta", "gamma"), path)
# An unopened connection is opened, used and closed by every call, so each
# call starts again at the top of the file.
con <- file(path)
readLines(con, n = 1)
#> [1] "alpha"
readLines(con, n = 1)
#> [1] "alpha"
isOpen(con)
#> [1] FALSE
# An opened connection keeps its position between calls.
open(con, "r")
readLines(con, n = 1)
#> [1] "alpha"
readLines(con, n = 1)
#> [1] "beta"
readLines(con)
#> [1] "gamma"
readLines(con)
#> character(0)
close(con)
# The mode of an unopened connection is decided by the first function that
# uses it: writeLines() opens it as "wt", which truncates the file.
con <- file(path)
writeLines("replaced", con)
readLines(path)
#> [1] "replaced"
close(con)
# Wrong-mode operations fail with a short message.
con <- file(path, "w")
tryCatch(readLines(con), error = \(e) conditionMessage(e))
#> [1] "cannot read from this connection"
close(con)
con <- file(path, "r")
tryCatch(writeLines("x", con), error = \(e) conditionMessage(e))
#> [1] "cannot write to this connection"
tryCatch(open(con, "r"), warning = \(w) conditionMessage(w))
#> [1] "connection is already open"
close(con)
The manual says it directly: scan() and readLines() given a file name open a file connection for the duration of the call, whereas opening the connection yourself lets a file be read sequentially in different formats. The same holds for output: when cat(), write(), write.table() or sink() is given a file name, the file is opened for writing or appending and closed at the end of the call, and to write repeatedly to the same file it is more efficient to open the connection once and pass it to each call (R Data Import/Export: Output to connections). The rule of thumb that follows: pass a file name for a one-shot read or write, and open a connection when the same file is written or read in several steps, when the position matters, or when the connection is not a file.
The warning for opening a connection twice is a plain warning (connection is already open), as the last line of the script shows; the two other messages come from asking for the wrong direction.
Closing connections reliably
A connection holds an operating system resource (a file descriptor, a socket, a child process) and R has a fixed number of connection slots. The help page says that a connection with no R object referring to it is closed automatically in due course, with a warning, and also that connections should be closed explicitly when finished with, to avoid wasting resources and to reduce the risk that buffered output is lost. The first is a safety net, not a plan; the second is the reason to close a connection that was written to even in a script that is about to end.
The idiom is on.exit(close(con)) straight after the connection is opened. It runs when the function ends, whether it ends normally or because of an error, and it keeps the close next to the open so that it is not forgotten when the function grows. When a function opens several connections, add = TRUE appends each handler instead of replacing the previous one (see Conditions and Error Handling for on.exit() and tryCatch()).
path <- tempfile(fileext = ".txt")
writeLines(c("alpha", "beta", "gamma"), path)
# Close in on.exit(): the connection is released even when the body fails.
first_line <- function(path) {
con <- file(path, "r")
on.exit(close(con))
readLines(con, n = 1)
}
first_line(path)
#> [1] "alpha"
failing <- function(path) {
con <- file(path, "r")
on.exit(close(con))
stop("could not parse the file")
}
tryCatch(failing(path), error = \(e) conditionMessage(e))
#> [1] "could not parse the file"
nrow(showConnections())
#> [1] 0
# Several resources: add = TRUE keeps the earlier handlers.
copy_lines <- function(from, to) {
input <- file(from, "r")
on.exit(close(input), add = TRUE)
output <- file(to, "w")
on.exit(close(output), add = TRUE)
writeLines(toupper(readLines(input)), output)
}
target <- tempfile(fileext = ".txt")
copy_lines(path, target)
readLines(target)
#> [1] "ALPHA" "BETA" "GAMMA"
nrow(showConnections())
#> [1] 0
# Forgetting to close: the connection stays open until it is garbage
# collected, and the R object is the only thing that keeps it alive.
leaked <- file(path, "r")
readLines(leaked, n = 1)
#> [1] "alpha"
showConnections()[, c("class", "mode", "isopen"), drop = FALSE]
#> class mode isopen
#> 3 "file" "r" "opened"
# Once the last reference is gone, gc() closes it and warns. The warning text
# names the file, so it is captured through a message sink here and the path
# is replaced.
rm(leaked)
captured <- character()
sink_con <- textConnection("captured", "w")
sink(sink_con, type = "message")
invisible(gc())
sink(type = "message")
close(sink_con)
sub("\\([^()]*\\)$", "(<path>)", captured)
#> [1] "Warning in .Internal(gc(verbose, reset, full)) :"
#> [2] " closing unused connection 3 (<path>)"
nrow(showConnections())
#> [1] 0
# A connection is also reachable by its number, as in showConnections().
con <- file(path, "r")
id <- as.integer(con)
id
#> [1] 3
same <- getConnection(id)
readLines(same, n = 1)
#> [1] "alpha"
close(con)
tryCatch(getConnection(77), error = \(e) conditionMessage(e))
#> [1] "there is no connection 77"
# closeAllConnections() closes every user connection, whoever opened it.
a <- file(path, "r")
b <- file(target, "r")
nrow(showConnections())
#> [1] 2
closeAllConnections()
nrow(showConnections())
#> [1] 0
tryCatch(isOpen(a), error = \(e) conditionMessage(e))
#> [1] "invalid connection"
showConnections(all = TRUE)[, c("class", "description")]
#> class description
#> 0 "terminal" "stdin"
#> 1 "terminal" "stdout"
#> 2 "terminal" "stderr"
Source: R documentation: showConnections
The script shows five things. on.exit(close(con)) leaves no open connection behind even when the function stops with an error (nrow(showConnections()) is 0). showConnections() lists the open connections that the user created, with the connection number as the row name; showConnections(all = TRUE) adds the closed ones and the three standard streams, numbered 0, 1 and 2. A connection that is no longer referenced is closed by the next garbage collection with the warning
closing unused connection 3 (<path>)
Source: R documentation: connections
where the number is the row of showConnections() and the text in parentheses is the description of the connection, here the path of the file (replaced by <path> in the output above). The warning is raised by the collector, from whatever code happens to be running, so tryCatch(warning = ) cannot catch it and the call it names (gc() here) is not the code that forgot the close; that is why the script captured it through a message sink. It is the sign of a missing close() or on.exit(), and it also means that the data written to an unclosed output file may not have reached the disk when the process stops. A text connection is the exception: an input text connection that is thrown away without being closed is garbage-collected without a warning.
getConnection() takes a row number of the showConnections() table (the same integer that as.integer(con) gives) and returns a connection object for it, so a connection whose variable was lost can still be closed as long as the collector has not run. closeAllConnections() closes and destroys all user connections, and restores all sink() diversions as it does so. It is a console command for cleaning up after an interactive session that went wrong, and it should not appear in a script or a package: it closes connections that belong to other code (a log file, the socket of a parallel cluster, a database driver), so the next use of those handles fails with invalid connection, and it silently cancels every sink(). Close the connections you opened.
The number of connections is limited. The help page gives the default as 128 including the three terminal connections, and says it can be raised at startup with the --max-connections=N option (to at most 4096). Connections that use files, pipes and fifos also use file descriptors, which the operating system limits per process (typically 256 on macOS and 1024 on Linux), and on Windows there is a limit of 512 open C file streams; the parallel package’s makeCluster() uses one socket connection per worker. Opening connections in a loop without closing them ends with the error all 128 connections are in use, shown in the section on common errors below.
Compressed files
R reads and writes gzip, bzip2, xz and (when built with the library) zstd compression through connections, and a plain file() can read all of them. For reading, the type of compression (if any) is determined from the first few bytes of the file, so readLines("books.csv.gz") works with no special call, as does read.csv() on a .gz, .bz2 or .xz path. For writing, you choose the format by choosing the function. The R Introduction summarises the trade-off: gzip is the long-supported "good general compromise", and bzip2 and xz generally achieve higher compression (much higher, depending on the file) at the expense of slower decompression and much slower compression (An Introduction to R: Compression and Archives). The help page puts typical numbers on it: bzip2 is about 15% better than gzip on text and xz at maximum compression about 30% better, while zstd typically compresses slightly worse than xz but is orders of magnitude faster at it.
# The same text through the four compressed connection types.
lines <- rep(readLines("books.csv"), 100)
plain_bytes <- sum(nchar(lines, type = "bytes") + 1L)
writers <- list(gzip = gzfile, bzip2 = bzfile, xz = xzfile, zstd = zstdfile)
compressed_bytes <- vapply(writers, \(open_fn) {
path <- tempfile()
on.exit(unlink(path))
con <- open_fn(path, "w")
writeLines(lines, con)
close(con)
file.size(path)
}, numeric(1))
compressed_bytes < plain_bytes
#> gzip bzip2 xz zstd
#> TRUE TRUE TRUE TRUE
# Reading detects the compression from the first bytes of the file.
path <- tempfile(fileext = ".gz")
con <- gzfile(path, "w")
writeLines(c("alpha", "beta"), con)
close(con)
readLines(path)
#> [1] "alpha" "beta"
readBin(path, "raw", n = 2)
#> [1] 1f 8b
# "rb" gives the compressed bytes themselves; gzfile() reads any mode.
con <- file(path, "rb")
readBin(con, "raw", n = 2)
#> [1] 1f 8b
close(con)
con <- gzfile(path)
readLines(con)
#> [1] "alpha" "beta"
close(con)
# Append mode adds a second compressed stream, which R reads as one file.
con <- gzfile(path, "a")
writeLines("gamma", con)
close(con)
readLines(path)
#> [1] "alpha" "beta" "gamma"
# gzcon() adds gzip decompression to an existing binary connection.
raw_con <- file(path, "rb")
gz <- gzcon(raw_con)
readLines(gz)
#> [1] "alpha" "beta" "gamma"
class(gz)
#> [1] "gzcon" "connection"
close(gz)
Source: R documentation: connections
The script prints only comparisons, not sizes, because the exact byte counts depend on the version of the compression libraries. Several points come out of it. Reading with readLines(path) or with a gzfile() connection decompresses; opening the same file with file(path, "rb") and reading raw bytes returns the compressed bytes, and the two bytes 1f 8b are the gzip magic number. The compression argument of the writers (0 to 9; 1 to 19 for zstd) trades time and memory for size, and the help page warns that xzfile(compress = 9) can need up to about 800 MB of memory. The type of compression is determined when the connection is created if no open is given and a file of that name exists; to write a different format over an existing file, open it with "w" at creation or unlink() it first.
Append mode needs care with compressed files: it does not produce a single compressed stream, but appends a new one, and readers may or may not read past the first. R does for gzfile(), bzfile(), xzfile(), zstdfile() and, since R 4.6.0, gzcon() (“gzcon() can now decompress a concatenation of compressed streams”, R NEWS 4.6.0); the last lines of the script show the three lines coming back from a file with two streams.
gzcon() takes an existing binary connection and adds gzip handling to it. Its most common use is to decompress data from a url() or a socket connection, which have no compression detection of their own. Two details are in the help page: closing the wrapper closes the underlying connection, and the original connection object now refers to the wrapper, so the new connection "needs to be closed explicitly". The text = TRUE argument (R 3.3.0) makes the wrapper text-oriented so that pushBack() works on it; the connection itself always stays binary.
For data already in memory, memCompress() and memDecompress() compress and decompress raw vectors with the same four algorithms (see the section on binary data below). To bundle several files into one, use tar() or zip(), covered under archives.
In-memory connections: text and raw
textConnection() and rawConnection() are connections whose "file" is an R object. They are the way to feed a function that wants a connection from a character vector, and to collect what a function writes without creating a file. They also exist so that a test of code that reads files does not need a file.
# Input: a character vector read as if it were a file.
csv_text <- c("title,pages", "Orchard Logic,210", "Rivers of Glass,310")
tc <- textConnection(csv_text)
read.csv(tc)
#> title pages
#> 1 Orchard Logic 210
#> 2 Rivers of Glass 310
close(tc)
read.csv(text = csv_text)
#> title pages
#> 1 Orchard Logic 210
#> 2 Rivers of Glass 310
# Output: complete lines are in the vector at all times; the last, unfinished
# line appears when the connection is closed.
tc <- textConnection("report", "w")
writeLines(c("Loans per member", "Ana Ruiz: 7"), tc)
cat("partial line", file = tc)
isIncomplete(tc)
#> [1] TRUE
report
#> [1] "Loans per member" "Ana Ruiz: 7"
close(tc)
report
#> [1] "Loans per member" "Ana Ruiz: 7" "partial line"
# With a NULL name nothing is created; textConnectionValue() reads the lines.
tc <- textConnection(NULL, "w")
writeLines(c("one", "two"), tc)
textConnectionValue(tc)
#> [1] "one" "two"
close(tc)
# local = TRUE creates the vector in the calling function, not the workspace.
collect <- function() {
tc <- textConnection("collected", "w", local = TRUE)
writeLines(c("a", "b"), tc)
close(tc)
collected # nolint: object_usage_linter.
}
collect()
#> [1] "a" "b"
exists("collected")
#> [1] FALSE
# capture.output() is built on an output text connection and sink().
capture.output(print(1:3), cat("done\n"))
#> [1] "[1] 1 2 3" "done"
# A raw connection is a file in memory: write binary data, take the bytes.
rc <- rawConnection(raw(0), "wb")
writeBin(c(1L, 2L, 3L), rc)
writeBin("shelf", rc)
bytes <- rawConnectionValue(rc)
close(rc)
bytes
#> [1] 01 00 00 00 02 00 00 00 03 00 00 00 73 68 65 6c 66 00
length(bytes)
#> [1] 18
# Read the bytes back through another raw connection.
rc <- rawConnection(bytes, "rb")
readBin(rc, "integer", n = 3)
#> [1] 1 2 3
readBin(rc, "character", n = 1)
#> [1] "shelf"
close(rc)
# In text mode a raw vector can be read line by line.
rc <- rawConnection(charToRaw("line one\nline two\n"))
readLines(rc)
#> [1] "line one" "line two"
close(rc)
Source: R documentation: textConnection
An input text connection copies the vector when it is created and is always open. read.csv(text = csv_text) is a shortcut for the first case: it builds the text connection itself. An output text connection keeps the variable of the name you gave in the workspace (or in the calling function with local = TRUE) up to date line by line; the vector has locked bindings until the connection is closed, closing the connection writes out the final line whether it is complete or not, and isIncomplete() tells you whether an unfinished line is pending. A NULL name creates no variable, and textConnectionValue() is the only way to read the output. Opening with mode "a" appends to an existing vector of that name. textConnection() has had an optional name argument since R 4.1.0 (the description shown by summary()), and the help page’s note is worth knowing: because output text connections keep the vector up to date line by line they are relatively expensive, and an anonymous file() connection is often a better way to collect output.
capture.output() is the everyday use of an output text connection. It is sink() into a textConnection(), followed by the lines; it is also why it leaves the connection numbers you see in showConnections() shifted by one while it runs.
A raw connection is the binary counterpart. Written to with writeBin(), it accumulates bytes that rawConnectionValue() returns, which is how to build a binary message without a file; read from, it lets readBin() consume a raw vector as a stream with a moving position (the shortcut readBin(raw_vector, ...) reads from a raw vector directly, but without a position that persists between calls). The integers 01 00 00 00 ... in the output are in little-endian order, the byte order of the machine that ran the script; the section on binary data shows how to control it. Writing compressed data to memory is the job of rawConnection() together with gzcon(); the help page of gzcon() notes that a textConnection() cannot be used for it because compressed output contains NUL bytes.
Pipes, fifos, sockets and URLs
These connections talk to other processes. All of them are created closed, opened on first use, and need close().
capabilities(c("fifo", "sockets", "libcurl"))
#> fifo sockets libcurl
#> TRUE TRUE TRUE
# Reading from a pipe: the command runs in a shell when the connection opens.
con <- pipe("echo shelf; echo catalogue")
readLines(con)
#> [1] "shelf" "catalogue"
close(con)
# Writing to a pipe: whatever R writes becomes the command's standard input.
sorted <- tempfile(fileext = ".txt")
con <- pipe(paste("sort >", shQuote(sorted)), "w")
writeLines(c("pear", "apple", "fig"), con)
status <- close(con)
status
#> [1] 0
readLines(sorted)
#> [1] "apple" "fig" "pear"
# close() returns the exit status of the command (zero means success).
con <- pipe("exit 3")
open(con, "r")
status <- close(con)
status != 0L
#> [1] TRUE
# A fifo (named pipe), opened for reading and writing in the same process.
if (capabilities("fifo")) {
fifo_path <- tempfile()
con <- fifo(fifo_path, "w+")
writeLines("through the fifo", con)
print(readLines(con))
close(con)
unlink(fifo_path)
}
#> [1] "through the fifo"
# A TCP server and client talking over the loopback interface. The port
# number is an arbitrary free one.
port <- 11823L
server <- serverSocket(port)
client <- socketConnection("localhost", port, blocking = TRUE, open = "r+")
peer <- socketAccept(server, blocking = TRUE, open = "r+")
writeLines("ping", client)
flush(client)
readLines(peer, n = 1)
#> [1] "ping"
writeLines("pong", peer)
flush(peer)
readLines(client, n = 1)
#> [1] "pong"
vapply(list(server, client, peer), \(con) class(con)[1L], character(1))
#> [1] "servsockconn" "sockconn" "sockconn"
close(client)
close(peer)
close(server)
nrow(showConnections())
#> [1] 0
Source: R documentation: connections
Pipes
pipe(command) opens a connection to a program. Opened for reading, it runs the command in a shell and makes its standard output the input of readLines() or scan(); opened for writing (it "makes no sense to append to a pipe"), it runs the command with its standard input connected to whatever R writes. The command line goes through a shell (sh on Unix, COMSPEC on Windows), so everything in the section on external commands about quoting applies: the script builds the redirection with shQuote(). close() on a pipe returns the exit status of the command, "typically zero values indicate success", invisibly; the exact number for a failure is system-dependent (on Linux it is the wait status, which is 256 times the exit code), so the script compares it with zero. The manual’s examples use pipes to run sed on the way in or out, which is still a fine use when a Unix tool does the job better than R.
Fifos
A fifo is a named pipe, a special file through which two processes talk. fifo(path, "w+") opens both directions in one R process, which is enough to see it work but not useful by itself; the useful form has another process on the other side. Fifos default to non-blocking, so, as the help page warns, opening one for writing only fails unless some other process is reading from it. Support is optional; capabilities("fifo") is TRUE on most Unix-alikes and on Windows. The help page’s own fifo example is marked "may hang even with OS support for fifos": the script above ran in the test container, but treat fifos as a tool for code that has both ends under its control.
Sockets
socketConnection() creates a TCP client or server. The R Data Import/Export manual gives the caveat: sockets are often blocked for security reasons or to force the use of Web caches, so they may be more useful on an intranet than externally (R Data Import/Export: Reading from sockets). socketConnection(server = TRUE) listens on a port and accepts one connection, after which the listening socket closes; serverSocket() (R 4.0.0) creates a listening socket that stays open and accepts any number of clients through socketAccept() until it is closed. The script runs both ends in one R process over the loopback interface: the client connects (the operating system queues the connection), the server accepts it, and the two exchange a line. Three details matter: use blocking = TRUE unless you are prepared to poll isIncomplete() (the default for sockets is non-blocking, and readLines() then returns whatever has arrived), call flush() after writing a message that the other side is waiting for, and set timeout (options(timeout) by default, or socketTimeout()) so that a missing peer ends in an error and not in a hang. capabilities("sockets") says whether the build supports them. Sockets are also what the parallel package uses to talk to its workers (see Parallel and Asynchronous Computing). For web servers and HTTP clients use the packages described in HTTP and Web APIs rather than writing the protocol on a raw socket; the old make.socket() family of functions is the "earlier low-level interface" that the manual says new projects should not use.
URLs
url() and file() with a URL as the description read from file://, http://, https:// and ftp:// addresses (the "internal" method dropped ftp:// in R 4.2.0, and the help page says modern browsers do not support it and https:// is much preferred). Redirects are followed; the timeout is options("timeout"), which "is a timeout for no response, not for the whole operation"; the headers argument of url() sends extra HTTP headers; and "server-side cached data is always accepted". A file:// URL has the form file:///path, with an empty host, and cannot be relative. None of this runs here, because the examples must not need a network: the function below is a fragment, not run, with a placeholder host. HTTPS needs the R build to have libcurl (capabilities("libcurl")); see the R FAQ entry on enabling secure downloads, referenced from HTTP and Web APIs.
# Fragment, not run: it needs network access (api.example.com is a placeholder).
read_remote_csv <- function(address) {
con <- url(address)
on.exit(close(con))
read.csv(con)
}
shelf_books <- read_remote_csv("https://api.example.com/shelf/books.csv")
Source: R documentation: connections
Reading and writing in pieces
An open connection keeps its position, which is what makes it possible to read a large file in chunks, to read a header in one way and the body in another, and to write a result a line at a time. readLines(con, n = 3) returns up to three lines and leaves the connection at the fourth; an empty result means the end of the file. The same is true of scan(con, n = , nlines = ), read.table(con, nrows = ) and readBin(con, n = ), which is the manual’s point about reading "sequentially in different formats".
# Read a file in pieces of three lines.
path <- tempfile(fileext = ".txt")
writeLines(sprintf("row %02d", 1:7), path)
con <- file(path, "r")
repeat {
chunk <- readLines(con, n = 3)
if (length(chunk) == 0L) break
cat(length(chunk), "lines, first:", chunk[1L], "\n")
}
#> 3 lines, first: row 01
#> 3 lines, first: row 04
#> 1 lines, first: row 07
close(con)
# Different readers on one open connection: the header as text, then rows.
con <- file("books.csv", "r")
header <- readLines(con, n = 1)
columns <- strsplit(gsub('"', "", header), ",")[[1L]]
columns
#> [1] "title" "author" "isbn" "year" "genre" "pages"
rows <- read.csv(con, header = FALSE, nrows = 3, col.names = columns)
rows[, c("title", "year", "pages")]
#> title year pages
#> 1 The Dispossessed 1974 387
#> 2 Pride and Prejudice 1813 432
#> 3 the quiet streets 2019 214
readLines(con, n = 1)
#> [1] "\"Loud Roofs\",\"Mara Quill\",\"978-0-00-200029-1\",2021,\"fiction\",198"
close(con)
# A file without a final newline: a warning, and the text is still read.
no_eol <- tempfile()
cat("line one\nline two", file = no_eol)
msg <- tryCatch(readLines(no_eol), warning = \(w) conditionMessage(w))
sub("'.*'", "'<file>'", msg)
#> [1] "incomplete final line found on '<file>'"
readLines(no_eol, warn = FALSE)
#> [1] "line one" "line two"
# pushBack() puts lines back; the most recently pushed lines are read first.
tc <- textConnection(LETTERS)
readLines(tc, 2)
#> [1] "A" "B"
scan(tc, "", 4, quiet = TRUE)
#> [1] "C" "D" "E" "F"
pushBack(c("aa", "bb"), tc)
pushBackLength(tc)
#> [1] 2
scan(tc, "", 4, quiet = TRUE)
#> [1] "aa" "bb" "G" "H"
pushBackLength(tc)
#> [1] 0
close(tc)
# Peek at a line without consuming it.
peek_line <- function(con) {
line <- readLines(con, n = 1)
pushBack(line, con)
line
}
tc <- textConnection(c("# comment", "data"))
peek_line(tc)
#> [1] "# comment"
readLines(tc, n = 2)
#> [1] "# comment" "data"
close(tc)
# seek() moves the file position (it returns the position before the move).
text_file <- tempfile()
writeLines(c("abc", "def", "ghi"), text_file)
con <- file(text_file, "rb")
isSeekable(con)
#> [1] TRUE
readChar(con, 3)
#> [1] "abc"
seek(con)
#> [1] 3
seek(con, 8)
#> [1] 3
readChar(con, 3)
#> [1] "ghi"
seek(con, 0)
#> [1] 11
readChar(con, 3)
#> [1] "abc"
close(con)
# truncate() cuts a file opened for writing at the current write position.
con <- file(text_file, "r+")
invisible(seek(con, 4, rw = "write"))
invisible(truncate(con))
close(con)
readLines(text_file)
#> [1] "abc"
# flush() pushes buffered output to the file without closing it.
out <- file(text_file, "w")
cat("buffered text", file = out)
flush(out)
file.size(text_file)
#> [1] 13
close(out)
# file() with no description is an anonymous scratch file.
scratch <- file()
cat("abc\ndef\n", file = scratch)
readLines(scratch)
#> [1] "abc" "def"
close(scratch)
Incomplete final lines
A text file should end with a newline. readLines() on one that does not returns the last line anyway and warns incomplete final line found on '<file>', with the file name in the message. Passing warn = FALSE silences it; with a non-blocking connection the behaviour differs, because an unfinished line may simply not have arrived yet (see isIncomplete() and the help page of readLines()).
Pushback
pushBack() puts lines of text back in front of a text-mode connection that is open for reading. The manual compares it to C’s ungetc, but more powerful because an essentially arbitrary number of lines can be pushed back; the pushbacks form a stack, and pushBackLength() gives the number pending (R Data Import/Export: Pushback). The peek_line() function in the script is the standard use: look at the next line to decide how to read the rest, then put it back. Pushback is only available for connections opened for input in text mode; seek() with a position discards pending pushbacks.
seek, truncate and flush
seek() returns the current byte position and, with a where argument, moves it; it returns the position before the move. A file open for both reading and writing has two positions, selected by rw = "read" or rw = "write". isSeekable() says whether a connection supports it (the help page: "currently only (possibly gz-compressed) file connections do"). Text connections, pipes, sockets and url() cannot seek, and it "should not be expected to work on text-mode connections with re-encoding selected". The R Data Import/Export manual and the help page warn that seek() depends on operating system facilities "which may be unreliable (e.g. with text files under Windows)"; the help page advises against it on Windows. truncate() cuts a file opened for writing at the current write position; it works only for file() connections and is "not implemented on all platforms". flush() empties the output buffer of a file, stdout() or stderr() connection into the operating system without closing it, which is what makes the new size visible in the script above; it needs an open connection (calling it on an unopened file connection is not meaningful and ended the R process with a segmentation fault in the container used for this page, so the script does not do it).
An anonymous file() (an empty description) is "a temporary file to write to and then read from" that is unlinked from the file system the moment it is opened, so it never leaves anything behind. The help page recommends it instead of an output text connection for collecting a lot of output.
Binary data
readBin() and writeBin() transfer vectors of bytes. The manual’s description of the arguments is the reference: what (an object of the mode wanted, or one of "numeric", "double", "integer", "int", "logical", "complex", "character", "raw"), n (the maximum number of items), size (bytes per item), signed, and endian; writeBin() takes an atomic vector without attributes, and a raw vector as con means "return the bytes" (R Data Import/Export: Binary connections). A file name or a raw vector can stand in for the connection.
# Write several types to a binary file, then read them back in order.
path <- tempfile(fileext = ".bin")
con <- file(path, "wb")
writeBin(c(1L, 256L, -1L), con)
writeBin(c(1.5, -2), con)
writeBin(TRUE, con)
writeBin("shelf", con)
writeBin(as.raw(c(0xde, 0xad)), con)
close(con)
file.size(path)
#> [1] 40
con <- file(path, "rb")
readBin(con, "integer", n = 3)
#> [1] 1 256 -1
readBin(con, "double", n = 2)
#> [1] 1.5 -2.0
readBin(con, "logical", n = 1)
#> [1] TRUE
readBin(con, "character", n = 1)
#> [1] "shelf"
readBin(con, "raw", n = 10)
#> [1] de ad
readBin(con, "integer", n = 5)
#> integer(0)
close(con)
# The first twelve bytes: three 4-byte integers (little-endian here).
readBin(path, "raw", n = 12)
#> [1] 01 00 00 00 00 01 00 00 ff ff ff ff
# endian and size make the byte stream independent of the machine.
writeBin(1L, raw(), endian = "little")
#> [1] 01 00 00 00
writeBin(1L, raw(), endian = "big")
#> [1] 00 00 00 01
writeBin(1L, raw(), size = 2, endian = "big")
#> [1] 00 01
writeBin(1L, raw(), size = 1)
#> [1] 01
writeBin(pi, raw(), size = 4, endian = "big")
#> [1] 40 49 0f db
readBin(writeBin(pi, raw(), size = 4), "double", size = 4)
#> [1] 3.141593
bytes <- as.raw(c(0xff, 0xff))
readBin(bytes, "integer", size = 2)
#> [1] -1
readBin(bytes, "integer", size = 2, signed = FALSE)
#> [1] 65535
.Platform$endian
#> [1] "little"
# Missing values are passed through at the natural size.
writeBin(NA_integer_, raw())
#> [1] 00 00 00 80
readBin(writeBin(NA_integer_, raw()), "integer")
#> [1] NA
# Fixed-length strings: writeChar() and readChar().
chars <- tempfile()
con <- file(chars, "wb")
writeChar("ABCDEF", con, eos = NULL)
writeChar("xyz", con)
close(con)
readBin(chars, "raw", n = 20)
#> [1] 41 42 43 44 45 46 78 79 7a 00
con <- file(chars, "rb")
readChar(con, 3)
#> [1] "ABC"
readChar(con, c(3, 3))
#> [1] "DEF" "xyz"
close(con)
readChar(chars, 6)
#> [1] "ABCDEF"
# Characters and bytes differ for non-ASCII text.
utf8 <- tempfile()
writeChar("é", utf8, eos = NULL)
file.size(utf8)
#> [1] 2
readChar(utf8, 1)
#> [1] "é"
readChar(utf8, 1, useBytes = TRUE)
#> [1] "\xc3"
# Raw vectors and strings.
r <- charToRaw("shelf é")
r
#> [1] 73 68 65 6c 66 20 c3 a9
rawToChar(r)
#> [1] "shelf é"
class(r)
#> [1] "raw"
rawToChar(r[1:5])
#> [1] "shelf"
# In-memory compression.
text <- paste(readLines("books.csv"), collapse = "\n")
packed <- memCompress(charToRaw(text), type = "gzip")
packed[1:2]
#> [1] 78 9c
length(packed) < nchar(text, type = "bytes")
#> [1] TRUE
identical(rawToChar(memDecompress(packed, type = "gzip")), text)
#> [1] TRUE
identical(rawToChar(memDecompress(packed, type = "unknown")), text)
#> [1] TRUE
Source: R documentation: readBin
The points to take from the script, each in the help page or the manual:
-
Byte order. By default the bytes are the machine’s own, so a file written with the defaults is portable only between machines of the same endianness. Everything that crosses a machine boundary should set
endian = "little"orendian = "big"(network byte order is big-endian), andendian = "swap"forces a swap. The help page: “endian-ness is relevant forsize > 1, and should always be set for portable code”. Since R 4.0.0 an invalidendianvalue is an error (invalid 'endian' argument) where earlier versions silently swapped. -
Size.
sizechanges the number of bytes per item (1, 2, 4 and possibly 8 for integers and logicals; 4, 8 and possibly 12 or 16 for numbers). Values are coerced on the way, sowriteBin(pi, raw(), size = 4)is a single-precision number and loses precision. Sizes the machine does not have are an error (size 3 is unknown on this machine).signed = FALSEreads 1-byte and 2-byte integers as unsigned, as the65535in the output shows. -
Missing values. They pass through at the natural size ("although this should not be attempted if a size change is involved"):
NA_integer_is the bit pattern00 00 00 80on a little-endian machine, the smallest representableint, as the manual’s section on special values explains (R Data Import/Export: Special values). -
Strings.
readBin(what = "character")andwriteBin("text", con)use C strings, a string of bytes ended by a zero byte; input strings are limited to 10000 characters. For fixed-length strings usereadChar()andwriteChar(); theirncharsargument counts characters, not bytes, unlessuseBytes = TRUE(the script shows a letter that takes two bytes in UTF-8 but is one character), andwriteChar()writes a terminating zero byte after each string unlesseos = NULL, as the78 79 7a 00in the output shows. -
Raw vectors and text.
charToRaw()returns the bytes of a string andrawToChar()builds a string from bytes (a vector or, withmultiple = TRUE, one string per byte). Embedded zero bytes are an error forrawToChar()(embedded nul in string) and a warning forreadLines(). The bytes of a UTF-8 string are not valid text one at a time, which is whyreadChar(utf8, 1, useBytes = TRUE)prints as"\xc3". -
Long vectors. R 4.0.0 made
writeBin()accept long vectors "where they are available" (R NEWS), while the help page still states that only 2^31 - 1 bytes can be written in a single call, so write very large vectors in pieces. -
Mode. Binary functions need a binary-mode connection.
readBin()andwriteBin()on a connection opened in text mode are errors,can only read from a binary connectionandcan only write to a binary connection;readChar()on one only warns,text connection used with readChar(), results may be incorrect. The other direction is tolerated:readLines()reads the lines of a connection opened with"rb".
memCompress() and memDecompress() apply gzip, bzip2, xz or zstd (R 4.5.0) to a raw vector. type = "unknown" in memDecompress() detects the compression from the header, which always succeeds for bzip2 and succeeds for the other forms when there is a suitable header. The gzip form is the zlib format (header 78 9c in the output), not the gzip file format with its 1f 8b header; memDecompress() reads both. This is the right tool to compress a serialized object or a response body in memory; for files use the connection functions above.
Standard input, output and error
stdin(), stdout() and stderr() are terminal connections: always open, read-only, write-only and write-only respectively, and they "cannot be opened or closed". Normal output goes to stdout() and messages to stderr(). The manual makes a distinction that is easy to miss: the connections cannot be redirected, but output can be sent to other connections, with sink() (and sink(type = "message") for stderr()).
# stdin(), stdout() and stderr() are always open and cannot be closed.
isOpen(stdout())
#> [1] TRUE
isOpen(stdin())
#> [1] TRUE
unlist(summary(stdout())[c("class", "mode", "opened")])
#> class mode opened
#> "terminal" "w" "opened"
tryCatch(close(stdout()), error = \(e) conditionMessage(e))
#> [1] "cannot close standard connections"
tryCatch(open(stdin()), error = \(e) conditionMessage(e))
#> [1] "cannot open standard connections"
# Writing to the two output streams; a message sink can capture stderr.
writeLines("to stdout", stdout())
#> to stdout
capture.output(writeLines("to stderr", stderr()), type = "message")
#> [1] "to stderr"
# What a script reads depends on the connection. Two throw-away scripts are
# run with Rscript and given three lines of standard input.
rscript <- file.path(R.home("bin"), "Rscript")
child <- tempfile(fileext = ".R")
writeLines(c(
"text <- readLines(file(\"stdin\"), n = 2)",
"cat(\"file(stdin) read:\", text, \"\\n\")"
), child)
system2(rscript, shQuote(child), input = c("one", "two", "three"),
stdout = TRUE)
#> [1] "file(stdin) read: one two "
# stdin() is the console; in a script run by Rscript the console is the
# script file, so the next line of the script is what gets read.
writeLines(c(
"text <- readLines(stdin(), n = 1)",
"this line is read as data and never evaluated",
"cat(\"stdin() read:\", text, \"\\n\")"
), child)
system2(rscript, shQuote(child), input = c("one", "two", "three"),
stdout = TRUE)
#> [1] "stdin() read: this line is read as data and never evaluated "
Source: R documentation: showConnections
The two child scripts at the end matter for command-line programs, covered in Scripts and the Command Line. stdin() "refers to the console and not to the C-level stdin of the process"; "when R is reading a script from a file, the file is the console", which the manual calls the traditional way of putting inline data after the code. So in a script run by Rscript script.R, readLines(stdin()) reads the next lines of the script itself, as the second child script shows, and the data piped into the process is read by file("stdin"). A script that wants its standard input says readLines(file("stdin")). nullfile() (R 3.6.0) is the portable name of the null device, /dev/null on Unix and nul: on Windows, for output that should vanish.
File paths
R leaves most path handling to the operating system. The R Introduction says it plainly: "with a few exceptions, R relies on the underlying OS functions to manipulate filepaths", and lists the issues that follow: POSIX file systems are case-sensitive while the defaults on Windows and macOS are not; a trailing slash is not valid on Windows and is best avoided everywhere; several slashes in a row are one slash on POSIX but may differ in a leading double slash; and Windows has drive letters, drive-relative paths and UNC paths, of which R supports the first only and "the use of absolute paths is safer" (An Introduction to R: Filepaths). The practical consequence is a short rule: build paths with file.path(), take them apart with basename() and dirname(), and let R expand and normalise them with path.expand() and normalizePath().
# Assemble paths with file.path(), never with paste().
file.path("data", "books.csv")
#> [1] "data/books.csv"
file.path("data", c("books.csv", "loans.csv"))
#> [1] "data/books.csv" "data/loans.csv"
file.path("data", character())
#> character(0)
file.path("data/", "books.csv")
#> [1] "data//books.csv"
paste("data", "books.csv", sep = "/")
#> [1] "data/books.csv"
# Split them with basename() and dirname().
files <- c("data/books.csv", "data/", "books.csv", "/", "")
basename(files)
#> [1] "books.csv" "data" "books.csv" "" ""
dirname(files)
#> [1] "data" "." "." "/" ""
# Extensions: tools::file_ext() and tools::file_path_sans_ext().
file_names <- c(
"books.csv", "backup.tar.gz", "README", ".gitignore", "LOANS.CSV"
)
tools::file_ext(file_names)
#> [1] "csv" "gz" "" "" "CSV"
tools::file_path_sans_ext(file_names)
#> [1] "books" "backup.tar" "README" ".gitignore" "LOANS"
tools::file_path_sans_ext("backup.tar.gz", compression = TRUE)
#> [1] "backup"
# The tilde is expanded by R itself, from HOME (or R_USER on Windows).
path.expand("~") == Sys.getenv("HOME")
#> [1] TRUE
identical(
path.expand("~/shelf/books.csv"),
file.path(Sys.getenv("HOME"), "shelf", "books.csv")
)
#> [1] TRUE
# normalizePath() makes a path absolute and resolves ".." and links.
normalizePath("books.csv") == file.path(getwd(), "books.csv")
#> [1] TRUE
basename(normalizePath("../shelf/./books.csv"))
#> [1] "books.csv"
# A path that does not exist: a warning by default, an error with mustWork.
msg <- tryCatch(normalizePath("nosuch.csv"), warning = \(w) conditionMessage(w))
msg
#> [1] "path[1]=\"nosuch.csv\": No such file or directory"
tryCatch(
normalizePath("nosuch.csv", mustWork = TRUE),
error = \(e) conditionMessage(e)
)
#> [1] "path[1]=\"nosuch.csv\": No such file or directory"
normalizePath("nosuch.csv", mustWork = FALSE)
#> [1] "nosuch.csv"
# winslash only matters on Windows: "\\" (the default) or "/".
normalizePath("books.csv", winslash = "/") == normalizePath("books.csv")
#> [1] TRUE
# On Windows file.path() still joins with "/", which Windows accepts.
file.path("C:", "data", "books.csv")
#> [1] "C:/data/books.csv"
.Platform$file.sep
#> [1] "/"
Source: R documentation: file.path
file.path, not paste
The R Introduction calls file.path() the recommended way to assemble a file path from components. It joins its arguments with the separator fsep, whose default is .Platform$file.sep: that is "/" on every platform R runs on, because Windows accepts it and R converts the few exceptions to the form Windows needs. It is vectorised, it is faster than paste() (the help page says it is used extensively in R itself), and it returns a zero-length result when any argument has zero length, where paste() would produce "data/". paste("data", "books.csv", sep = "/") builds the same string on a POSIX system, but it hard-codes the separator and does not know the rules of the platform: on Windows file.path() removes a trailing separator (since R 3.1.0), because such paths are invalid there. The case that shows the difference in the script is a component that already ends with a slash: file.path("data/", "books.csv") is "data//books.csv", which POSIX accepts as one slash. With fsep = .Platform$path.sep the same function builds PATH-style lists. Since R 4.0.0 file.path() marks the result as UTF-8 when its inputs are, which is when most file-path functions also began to signal an explicit error instead of silently using a different, escaped path when a path cannot be translated to the native encoding.
Windows paths are the first place a backslash shows up. The R FAQ answers "How do file names work in Windows?": the backslash is the escape character in R strings, so write "c:\\data\\money.dat" or, better, "c:/data/money.dat" (R FAQ 7.8: How do file names work in Windows?), and FAQ 7.37 explains why a backslash in a string behaves strangely (R FAQ 7.37: Why does backslash behave strangely inside strings?). Since R 4.0.0 there are also raw strings (r"(C:\data\books.csv)") for the cases where a backslash has to be literal. The FAQ’s own example of building file names in a loop (file.path("Results", paste0("result", i, ".rda"))) is entry 7.34 (R FAQ 7.34: How can I save the result of each iteration in a loop into a separate file?).
The path functions
| Function | What it does | Notes |
|---|---|---|
|
The last component and everything before it |
|
|
The extension, without the dot |
|
|
The path without its extension |
|
|
Replaces a leading tilde by the home directory |
Reads |
|
An absolute, canonical path |
Resolves |
|
The name of the null device |
|
|
Joins components |
Never |
Source: R documentation: normalizePath
The tilde needs one more remark: only R functions that expand it do. file.exists("~/books.csv") and readLines("~/books.csv") expand it themselves, but a path passed to an external program through system2() is not expanded by R, and in a shell command the shell may or may not expand it, so call path.expand() first. normalizePath() with mustWork = NA warns with the text path[1]="nosuch.csv": No such file or directory, which names the element number and the path as given; use mustWork = FALSE when a missing file is an expected state and TRUE when it is a bug.
Listing and finding files
list.files() (also called dir()) returns the names of the files in a directory. The function reference is the help page; the options that matter are all arguments of the same call.
# Build a small tree in a temporary directory (nothing is printed in full:
# the examples show paths relative to it).
root <- file.path(tempdir(), "shelf-export")
dir.create(file.path(root, "reports", "2026"), recursive = TRUE)
dir.create(file.path(root, "data"))
tree <- c(
"data/books.csv", "data/loans.csv", "data/notes.txt", "data/.hidden",
"reports/summary.md", "reports/2026/q1.csv", "reports/2026/q2.csv",
"top.R", "a.b.csv", "axb.csv", "book[1].csv"
)
invisible(file.create(file.path(root, tree)))
# list.files(): names only, hidden files off, not recursive.
list.files(root)
#> [1] "a.b.csv" "axb.csv" "book[1].csv" "data" "reports"
#> [6] "top.R"
list.files(root, all.files = TRUE)
#> [1] "." ".." "a.b.csv" "axb.csv" "book[1].csv"
#> [6] "data" "reports" "top.R"
list.files(root, all.files = TRUE, no.. = TRUE)
#> [1] "a.b.csv" "axb.csv" "book[1].csv" "data" "reports"
#> [6] "top.R"
list.files(file.path(root, "data"), all.files = TRUE, no.. = TRUE)
#> [1] ".hidden" "books.csv" "loans.csv" "notes.txt"
# recursive = TRUE lists files in subdirectories; include.dirs keeps the
# directories themselves.
list.files(root, recursive = TRUE)
#> [1] "a.b.csv" "axb.csv" "book[1].csv"
#> [4] "data/books.csv" "data/loans.csv" "data/notes.txt"
#> [7] "reports/2026/q1.csv" "reports/2026/q2.csv" "reports/summary.md"
#> [10] "top.R"
list.files(root, recursive = TRUE, include.dirs = TRUE)
#> [1] "a.b.csv" "axb.csv" "book[1].csv"
#> [4] "data" "data/books.csv" "data/loans.csv"
#> [7] "data/notes.txt" "reports" "reports/2026"
#> [10] "reports/2026/q1.csv" "reports/2026/q2.csv" "reports/summary.md"
#> [13] "top.R"
# pattern is a regular expression, matched against the file name only.
list.files(root, pattern = "\\.csv$", recursive = TRUE)
#> [1] "a.b.csv" "axb.csv" "book[1].csv"
#> [4] "data/books.csv" "data/loans.csv" "reports/2026/q1.csv"
#> [7] "reports/2026/q2.csv"
list.files(root, pattern = "^[Tt]", recursive = TRUE)
#> [1] "top.R"
# full.names = TRUE prepends the directory.
full <- list.files(root, pattern = "csv$", recursive = TRUE, full.names = TRUE)
substring(full, nchar(root) + 2L)
#> [1] "a.b.csv" "axb.csv" "book[1].csv"
#> [4] "data/books.csv" "data/loans.csv" "reports/2026/q1.csv"
#> [7] "reports/2026/q2.csv"
# fixed = TRUE (R 4.6.0): the pattern is literal text, not a regular expression.
list.files(root, pattern = "a.b.csv")
#> [1] "a.b.csv" "axb.csv"
list.files(root, pattern = "a.b.csv", fixed = TRUE)
#> [1] "a.b.csv"
list.files(root, pattern = "[1]", recursive = TRUE)
#> [1] "book[1].csv" "reports/2026/q1.csv"
list.files(root, pattern = "[1]", recursive = TRUE, fixed = TRUE)
#> [1] "book[1].csv"
tryCatch(
list.files(root, pattern = "book[1.csv"),
error = \(e) conditionMessage(e)
)
#> [1] "invalid 'pattern' regular expression"
# list.dirs() and wildcards.
list.dirs(root, full.names = FALSE)
#> [1] "" "data" "reports" "reports/2026"
list.dirs(root, full.names = FALSE, recursive = FALSE)
#> [1] "data" "reports"
basename(Sys.glob(file.path(root, "data", "*.csv")))
#> [1] "books.csv" "loans.csv"
substring(Sys.glob(file.path(root, "*", "*.csv")), nchar(root) + 2L)
#> [1] "data/books.csv" "data/loans.csv"
Sys.glob(file.path(root, "nosuch*"))
#> character(0)
glob2rx("a*.csv")
#> [1] "^a.*\\.csv$"
list.files(root, pattern = glob2rx("a*.csv"))
#> [1] "a.b.csv" "axb.csv"
# A directory that does not exist is an empty listing, not an error.
list.files(file.path(root, "nosuch"))
#> character(0)
# Existence tests.
file.exists(file.path(root, c("top.R", "data", "nosuch")))
#> [1] TRUE TRUE FALSE
dir.exists(file.path(root, c("top.R", "data", "nosuch")))
#> [1] FALSE TRUE FALSE
utils::file_test("-f", file.path(root, c("top.R", "data")))
#> [1] TRUE FALSE
unlink(root, recursive = TRUE)
Source: R documentation: list.files
-
Hidden files. Files whose name starts with a dot are left out unless
all.files = TRUE, which also adds"."and".."in non-recursive listings;no.. = TRUE(R 3.0.0) removes those two as well.list.dirs()implicitly hasall.files = TRUE. -
Recursion.
recursive = TRUEdescends into directories and lists only files; addinclude.dirs = TRUEto keep the directories. Withfull.names = FALSEthe names are relative topath, which is what the script prints; withfull.names = TRUEthey start withpath, and the listing is sorted alphabetically on the full path. -
The pattern is a regular expression matched against the file name only, never the directory part, and
ignore.case = TRUEmakes it case-insensitive. This catches people who expect a shell wildcard:pattern = "*.csv"is a regular expression, not the wildcard it would be in a shell, and"\\.csv$"is the usual form for "ends in .csv".glob2rx("a*.csv")translates a shell-style wildcard into a regular expression ("^a.*\\.csv$"). The pattern is described in Strings and Regular Expressions. -
fixed = TRUEis new in R 4.6.0: the NEWS entry reads “list.files()now has optional argumentfixedto allow literal pattern matching” (suggested and implemented by Duncan Murdoch, PR#18925). It matters whenever the name you are looking for contains a regex metacharacter, the dot above all. In the script,"a.b.csv"as a regular expression also matchesaxb.csv,"[1]"as a regular expression is the character class that matches a name containing a 1, and"book[1.csv"as a regular expression is an invalid pattern, which is the errorinvalid 'pattern' regular expression; withfixed = TRUEall of them are literal text. Code that must also run on R 4.5 and earlier cannot use the argument and should escape the pattern, or compare names with==or%in%after listing without a pattern. -
Wildcards.
Sys.glob()expands shell wildcards (*and?, character classes where the system supports them) in a path with several levels, such asfile.path(root, "*", "*.csv"), which nopatterncan do; it returnscharacter(0)when nothing matches. Since R 4.0.0 it supports all characters of the Unicode Basic Multilingual Plane. -
A directory that does not exist gives an empty result, not an error: "a path that does not exist, is not a directory or is unreadable is skipped". Test with
dir.exists()when a missing directory should be an error. -
Existence tests.
file.exists()is true for files and directories;dir.exists()is true only for directories;utils::file_test("-f", path)andfile_test("-d", path)are the POSIXtestcommand. The R Introduction namesfile.access()as the third way, and notes that "a file can exist on the filesystem and not be visible to the current user" (An Introduction to R: Files and directories).
File operations
The functions are small and their return values matter: most of them return a logical vector that says, element by element, whether the operation worked, and they warn rather than stop. A script that does not check the value does not know that the copy failed. The R Introduction lists the families: file.create(), dir.create(), file.remove(), unlink(), file.copy() (the analogue of cp), file.rename(), file.append(), file.info(), file.exists(), file.access(), file.link() and Sys.readlink(), file.show() and file.edit(), and file.choose() for interactive use.
# Work in a scratch directory under tempdir().
work <- file.path(tempdir(), "work")
dir.create(work)
# Creating it again is a warning and FALSE; showWarnings = FALSE hides it.
msg <- tryCatch(dir.create(work), warning = \(w) conditionMessage(w))
sub("^'.*' ", "'<dir>' ", msg)
#> [1] "'<dir>' already exists"
print(dir.create(work, showWarnings = FALSE))
#> [1] FALSE
# Nested directories need recursive = TRUE.
nested <- file.path(work, "a", "b")
msg <- tryCatch(dir.create(nested), warning = \(w) conditionMessage(w))
sub("'.*', reason", "'<dir>', reason", msg)
#> [1] "cannot create dir '<dir>', reason 'No such file or directory'"
dir.create(nested, recursive = TRUE)
dir.exists(nested)
#> [1] TRUE
# Create, copy, rename, append.
src <- file.path(work, "src.txt")
file.create(src)
#> [1] TRUE
writeLines("hello", src)
dst <- file.path(work, "dst.txt")
file.copy(src, dst)
#> [1] TRUE
file.copy(src, dst)
#> [1] FALSE
file.copy(src, dst, overwrite = TRUE)
#> [1] TRUE
file.copy(src, file.path(work, "a"))
#> [1] TRUE
list.files(work, recursive = TRUE)
#> [1] "a/src.txt" "dst.txt" "src.txt"
file.rename(dst, file.path(work, "renamed.txt"))
#> [1] TRUE
file.append(src, src)
#> [1] TRUE
readLines(src)
#> [1] "hello" "hello"
# Failures are warnings and FALSE; the paths in the messages are replaced here.
missing <- file.path(work, "nosuch.txt")
msg <- tryCatch(
file.rename(missing, file.path(work, "again.txt")),
warning = \(w) conditionMessage(w)
)
gsub("'[^']*/[^']*'", "'<path>'", msg)
#> [1] "cannot rename file '<path>' to '<path>', reason 'No such file or directory'"
msg <- tryCatch(
file.copy(c(src, missing), file.path(work, "a")),
warning = \(w) conditionMessage(w)
)
gsub("/[^ :]+", "<path>", msg)
#> [1] "problem copying <path> to <path>: No such file or directory"
# Copying a directory needs recursive = TRUE and an existing target directory.
dir.create(file.path(work, "z"))
tryCatch(
file.copy(file.path(work, "a"), file.path(work, "new")),
warning = \(w) conditionMessage(w)
)
#> [1] "directories are omitted unless 'recursive = TRUE'"
tryCatch(
file.copy(file.path(work, "a"), file.path(work, "new"), recursive = TRUE),
warning = \(w) conditionMessage(w)
)
#> [1] "'recursive' will be ignored as 'to' is not a single existing directory"
file.copy(file.path(work, "a"), file.path(work, "z"), recursive = TRUE)
#> [1] TRUE
list.files(file.path(work, "z"), recursive = TRUE, include.dirs = TRUE)
#> [1] "a" "a/b" "a/src.txt"
# file.remove() deletes files; unlink() also deletes directories and
# understands wildcards.
file.remove(file.path(work, "renamed.txt"))
#> [1] TRUE
msg <- tryCatch(
file.remove(file.path(work, "a")),
warning = \(w) conditionMessage(w)
)
sub("'.*', reason", "'<dir>', reason", msg)
#> [1] "cannot remove file '<dir>', reason 'Directory not empty'"
print(unlink(file.path(work, "a")))
#> [1] 1
dir.exists(file.path(work, "a"))
#> [1] TRUE
unlink(file.path(work, "a"), recursive = TRUE)
dir.exists(file.path(work, "a"))
#> [1] FALSE
invisible(file.create(file.path(work, c("x1.tmp", "x2.tmp", "keep.txt"))))
unlink(file.path(work, "*.tmp"))
list.files(work)
#> [1] "keep.txt" "src.txt" "z"
unlink(file.path(work, "nosuch.txt"))
# A name that contains a wildcard character needs expand = FALSE.
star <- file.path(work, "star*.tmp")
invisible(file.create(star, file.path(work, "starfish.tmp")))
unlink(star, expand = FALSE)
list.files(work, pattern = "star")
#> [1] "starfish.tmp"
# Information: file.info(), file.size(), file.mtime(), file.access().
info <- file.info(file.path(work, c("src.txt", "z", "nosuch.txt")))
basename(rownames(info))
#> [1] "src.txt" "z" "nosuch.txt"
info$isdir
#> [1] FALSE TRUE NA
info$size[1L]
#> [1] 12
names(info)
#> [1] "size" "isdir" "mode" "mtime" "ctime" "atime" "uid" "gid"
#> [9] "uname" "grname"
file.size(file.path(work, c("src.txt", "nosuch.txt")))
#> [1] 12 NA
class(file.mtime(src))
#> [1] "POSIXct" "POSIXt"
unname(file.access(src, 4))
#> [1] 0
unname(file.access(file.path(work, "nosuch.txt"), 0))
#> [1] -1
# Permissions are an octal mode; file.copy(copy.date = TRUE) keeps the time.
Sys.chmod(src, "640")
format(file.info(src)$mode)
#> [1] "640"
Sys.setFileTime(src, as.POSIXct("2026-06-30 12:00:00", tz = "UTC"))
dated <- file.path(work, "dated.txt")
file.copy(src, dated, copy.date = TRUE)
#> [1] TRUE
format(file.mtime(dated), tz = "UTC", usetz = TRUE)
#> [1] "2026-06-30 12:00:00 UTC"
# Links (not on every file system).
link <- file.path(work, "link.txt")
file.symlink(src, link)
#> [1] TRUE
Sys.readlink(link) == src
#> [1] TRUE
Sys.readlink(src)
#> [1] ""
file.exists(link)
#> [1] TRUE
basename(normalizePath(link))
#> [1] "src.txt"
unlink(work, recursive = TRUE)
dir.exists(work)
#> [1] FALSE
Source: R documentation: files
-
Creating.
dir.create()returnsTRUEorFALSEinvisibly, warns when the directory exists ('<dir>' already exists) or the parent is missing (cannot create dir '<dir>', reason 'No such file or directory'), and creates only one level unlessrecursive = TRUE.showWarnings = FALSEsilences the warning; check the result ordir.exists()when it matters.dir.create("")is an error,zero-length 'path' argument(a guard added in R 4.4.3).file.create()creates empty files, or truncates existing ones, and warnscannot create file '<file>', reason 'No such file or directory'when the directory is missing. -
Copying.
file.copy(from, to)returnsFALSEwithout a warning when the target exists andoverwrite = FALSE(the default), which is the usual surprise. Itsoverwritedefault isrecursive, so a recursive copy overwrites. Iftois an existing directory, the files are copied into it. Copying a directory needsrecursive = TRUE, andtomust then be one existing directory: otherwise the warnings aredirectories are omitted unless 'recursive = TRUE'and'recursive' will be ignored as 'to' is not a single existing directory, and nothing is copied. Since R 4.4.0 directories are omitted from a non-recursivefile.copy()andfile.append().copy.mode = TRUE(the default) copies the permission bits andcopy.date = TRUEpreserves the modification time (R 3.1.0), as thedated.txtline in the script shows. A missing source file warns withproblem copying <from> to <to>: No such file or directory. -
Renaming.
file.rename()moves a file or directory on the same file system, replacing the target. It is a system call, so it can fail across file systems, and the help page warns that the session temporary directory is commonly on a different file system from the working directory, so renaming a file out oftempdir()often fails; the portable use is to rename within one directory, and the fallback elsewhere isfile.copy()followed byfile.remove(). The failure warning iscannot rename file '<from>' to '<to>', reason 'No such file or directory'(or the system’s reason). -
Deleting: file.remove versus unlink.
file.remove()removes files (and empty directories on some systems) and warnscannot remove file '<file>', reason '...'when it cannot; for a non-empty directory the reason isDirectory not empty.unlink()is the general tool:recursive = TRUEremoves a directory tree,force = TRUEchanges permissions if needed to allow removal, it understands wildcards (unlink("*.tmp")), and it does not complain about a file that is not there. Its value is0for success and1for failure, invisibly. The help page states that being unable to delete a directory whenrecursive = FALSEis not a failure, yet in R 4.6.1 as run for this pageunlink(dir)on a directory returned1and left the directory in place (the script shows the directory still exists), so test the result withdir.exists()and not by the status. Wildcards are expanded bySys.glob(), never match a leading dot, and never delete.,..or~.expand = FALSE(R 4.0.0) switches the wildcard and tilde expansion off, which the script needs to delete a file that is really calledstar*.tmp. R 4.5.0 checks arguments that must be a single logical more thoroughly (R NEWS namesunlink(, recursive)andtempdir()), sounlink(x, recursive = NA)is the errorinvalid 'recursive' argument. -
Information.
file.info()returns a data frame with the size, theisdirflag, the permission mode (class"octmode"), the modification, status-change and access times ("POSIXct", see Dates and Times), and on Unix the owner and group; the row names are the paths that were asked for, and non-existent files giveNArows.file.size(),file.mtime()andfile.mode()are shortcuts.extra_cols = FALSE(R 3.2.0) skips the owner and group lookups, which can be slow on a large file list. What the three times mean depends on the system and file system (the creation time is only recorded on Windows), and the resolution is as fine as microseconds on modern file systems but whole seconds on HFS+ and two seconds on old FAT. -
Access.
file.access(path, mode)returns0for success and-1for failure, withmode0(exists),1(execute),2(write) and4(read); it asks the operating system on behalf of the user running R, so the answer can change with the user (a superuser can write a read-only file) and it is not a substitute for trying to open the file and handling the failure. -
Permissions and times.
Sys.chmod(paths, mode)takes an octal string;Sys.umask()shows the creation mask.Sys.setFileTime()sets the modification time. R supports the POSIX read, write and execute bits only, and the R Introduction notes that access control lists exist on several file systems without an agreed standard, so R has no facilities to control them. -
Links.
file.symlink(from, to)makes a symbolic link andfile.link()a hard link;Sys.readlink()returns the target of a symbolic link, and""for anything that is not one (the script shows both). Links need operating system support and, on Windows, privileges.file.info()follows symbolic links, andunlink(link, recursive = TRUE)removes only the link when it points to a directory. -
Showing.
file.show(path)displays a file with the pager of the R port (a console window in a GUI,lessormoreon Unix), so its output is not part of the R output stream and there is nothing useful to run in a script.file.edit()andfile.choose()are interactive.
Path arguments that carry a marked encoding which cannot be translated to the native encoding make most of these functions fail since R 4.0.0 (dir.exists(), file.exists(), file.access(), file.info(), list.files(), normalizePath() and path.expand() treat the path "like any other non-existent file, often with a warning"), where earlier versions silently used a different, escaped path.
Temporary files and directories
tempfile() returns a name that is very likely to be unique within a session and across simultaneous R sessions, and that is guaranteed not to be in use at the moment of the call; it creates nothing. The name is tmpdir, then pattern, then a random hexadecimal string, then fileext, and the three arguments are recycled to the longest, so a vector of patterns and extensions gives a vector of names. tempdir() is the per-session directory that is the default tmpdir; R creates it before the interpreter starts and removes it, with everything in it, when the session ends normally.
# tempfile() only invents a name: it creates nothing.
a <- tempfile()
b <- tempfile()
a != b
#> [1] TRUE
file.exists(a)
#> [1] FALSE
dirname(a) == tempdir()
#> [1] TRUE
# The pattern, the directory and the extension are all arguments.
p <- tempfile("shelf-", fileext = ".csv")
grepl("^shelf-.*\\.csv$", basename(p))
#> [1] TRUE
two <- tempfile(c("a", "b"), fileext = c(".x", ".y"))
sub("[0-9a-f]+", "<id>", basename(two))
#> [1] "<id>.x" "<id>.y"
# A private scratch directory, removed when the function ends.
with_scratch <- function(code) {
scratch <- tempfile("scratch-")
dir.create(scratch)
on.exit(unlink(scratch, recursive = TRUE))
code(scratch)
}
with_scratch(\(dir) {
writeLines("temporary", file.path(dir, "note.txt"))
list.files(dir)
})
#> [1] "note.txt"
# tempdir() is one directory per R session, deleted when the session ends.
dir.exists(tempdir())
#> [1] TRUE
tempdir() == tempdir()
#> [1] TRUE
Sys.getenv("TMPDIR", unset = "<unset>")
#> [1] "<unset>"
# If something deletes it, tempfile() names stop working; tempdir(check =
# TRUE) recreates a valid directory.
old <- tempdir()
unlink(old, recursive = TRUE)
dir.exists(old)
#> [1] FALSE
msg <- tryCatch(writeLines("x", tempfile()), warning = \(w) conditionMessage(w))
sub("'.*'", "'<file>'", msg)
#> [1] "cannot open file '<file>': No such file or directory"
dir.exists(tempdir(check = TRUE))
#> [1] TRUE
writeLines("x", tempfile())
Source: R documentation: tempfile
-
Where it is. The per-session directory is a subdirectory of the first writable directory named by the environment variables
TMPDIR,TMPandTEMP, in that order, or else a default location chosen by the system. Setting one of them inside a running session has no effect ontempdir(), because the directory is created before the interpreter starts, which the example in the section on environment variables confirms; set it before starting R (in the shell, or in.Renviron). The path must not contain spaces. The last component of the path is a random name made by the system; the names and the location are platform text, so a script that prints them is not reproducible, which is why the examples above printbasename()patterns and comparisons only. -
Cleaning up. The session directory disappears when R exits normally, not when it crashes or is killed, so a long-running process should remove its own scratch files. The pattern is
on.exit(unlink(path))for a file andon.exit(unlink(dir, recursive = TRUE))for a directory (aswith_scratch()shows), orwithr::local_tempfile()for those who use the withr package. Do not deletetempdir()itself. -
Private directories. When several files belong together,
tempfile("name-")plusdir.create()gives a directory of one’s own; file names inside it can be chosen freely (file.path(dir, "books.csv")). On a shared multi-user machine,tempfile()names are hard to guess, but a script that creates a predictable name in a shared directory (file.path("/tmp", "books.csv")) invites collisions and worse. -
When tempdir disappears. On a machine with a cleaner that deletes old files in the temporary directory, a long R session can find its
tempdir()gone, and thenwriteLines(x, tempfile())warnscannot open file '<file>': No such file or directory, as the script shows.tempdir(check = TRUE)(R 3.5.0) "recreates thetempdir()directory if it is no longer valid"; it may return a different path from the one that went missing, so do not cache the old value. R 4.5.0 also checks thecheckargument more strictly, sotempdir(check = c(TRUE, FALSE))is an error ('length = 2' in coercion to 'logical(1)'). -
Parallel code. Processes forked by
mclapply()share the parent’s per-session directory;tempfile()is arranged so that children try different names, but the "guaranteed not in use" promise holds only at the moment of asking (see Parallel and Asynchronous Computing).
The working directory and environment variables
Relative paths start at the working directory. getwd() returns it; the function that changes it (setwd, in the help page of getwd) changes it for the whole session, silently, which makes it a hazard in any code that other code calls: the next relative path means something else, and after an error the directory is not restored. In a script, prefer paths built from a known base (file.path(base, "data", "books.csv")) and let the caller choose the base. When a script must change directory for a while, restore it with on.exit(): the function that changes the directory returns the previous one invisibly, and an on.exit() handler that changes back to that value puts it back. A failed change is the error cannot change working directory. The sessions page explains how the working directory is chosen at startup and what the IDE does with it (Sessions, Workspaces and Startup). For the root of a project, community packages such as here and rprojroot find a directory by looking for a marker file (an .Rproj file, a DESCRIPTION, a .git directory) and build paths from it, which does not depend on where R was started; they are not run here.
Environment variables are the other place where paths are configured, and the way to keep a secret or a machine-specific path out of the code. Sys.getenv("NAME") returns "" for a variable that is not set; unset = NA makes the missing case visible, and unset = "default" supplies a default. Sys.setenv(NAME = "value") sets variables for the session and for every child process that R starts afterwards, Sys.unsetenv() removes them, and all arguments to Sys.setenv() must be named (the error is all arguments must be named).
# The working directory is where relative paths start.
basename(getwd())
#> [1] "shelf"
file.exists("books.csv")
#> [1] TRUE
identical(file.path(getwd(), "books.csv"), normalizePath("books.csv"))
#> [1] TRUE
# Environment variables: unset ones are "" unless unset= says otherwise.
Sys.getenv("SHELF_DATA_DIR")
#> [1] ""
Sys.getenv("SHELF_DATA_DIR", unset = NA)
#> [1] NA
Sys.getenv("SHELF_DATA_DIR", unset = "data")
#> [1] "data"
# Sys.setenv() takes named arguments and returns TRUE for each.
Sys.setenv(SHELF_DATA_DIR = "export", SHELF_FORMAT = "csv")
Sys.getenv(c("SHELF_DATA_DIR", "SHELF_FORMAT"))
#> SHELF_DATA_DIR SHELF_FORMAT
#> "export" "csv"
Sys.getenv("SHELF_DATA_DIR") |> file.path("books.csv")
#> [1] "export/books.csv"
Sys.unsetenv("SHELF_FORMAT")
Sys.getenv("SHELF_FORMAT", unset = NA)
#> [1] NA
tryCatch(Sys.setenv("SHELF_FORMAT"), error = \(e) conditionMessage(e))
#> [1] "all arguments must be named"
# Child processes inherit the variables set in the session.
system2("sh", c("-c", shQuote("echo $SHELF_DATA_DIR")), stdout = TRUE)
#> [1] "export"
# Tilde expansion reads HOME.
nzchar(Sys.getenv("HOME"))
#> [1] TRUE
old_home <- Sys.getenv("HOME")
Sys.setenv(HOME = "/srv/shelf")
path.expand("~/books.csv")
#> [1] "/srv/shelf/books.csv"
Sys.setenv(HOME = old_home)
# Changing TMPDIR in a running session does not move tempdir().
before <- tempdir()
Sys.setenv(TMPDIR = "/srv/scratch")
identical(before, tempdir())
#> [1] TRUE
Sys.unsetenv("TMPDIR")
Sys.unsetenv("SHELF_DATA_DIR")
Source: R documentation: Sys.getenv
The variables that matter for files and paths:
| Variable | Effect |
|---|---|
|
The home directory used by tilde expansion on Unix, and one of the places where R looks for the user |
|
The user’s home directory on Windows, which |
|
Where the per-session temporary directory is created. Read at startup only. |
|
Which |
|
The external programs behind |
|
Library paths for installed packages (see Packages, Libraries and Namespaces). |
|
Where |
Source: R documentation: Startup
The right place for values that belong to a machine or a person is a file named .Renviron, read at startup, with one NAME=value per line, in the project directory or the home directory; the order in which R reads them, and why a project .Renviron replaces the home one instead of adding to it, is explained in Sessions, Workspaces and Startup. A fragment such as the following keeps the data directory and the password out of the code. The password line is a placeholder, and the file must be kept out of version control.
# .Renviron: one NAME=value per line, read when R starts (illustrative, not run)
SHELF_DATA_DIR=export
SHELF_DB_PASSWORD=change-me
TMPDIR=/srv/scratch
Source: R documentation: Startup
Running other programs
system2() and system() start an operating system command. The R Introduction describes system2() as the slightly more general of the two, whose main advantage is that cross-platform code is easier to write with it, and the help page of system() calls its own interface "rather complicated" and recommends system2() for new code (An Introduction to R: System commands). The difference that matters is how the command line is built: system2(command, args) quotes command with shQuote() (so it must be a program name and nothing else) and passes args as a character vector, while system(command) takes one string that a shell parses, so a program name with spaces or an argument with a space, a quote or a ; changes the meaning.
# system2(): the command and its arguments are separate.
system2("echo", c("a", "b"), stdout = TRUE)
#> [1] "a b"
# Arguments go through a shell: quote anything with spaces or metacharacters.
system2("echo", "a b", stdout = TRUE)
#> [1] "a b"
system2("echo", shQuote("a b"), stdout = TRUE)
#> [1] "a b"
shQuote("a b")
#> [1] "'a b'"
shQuote("it's")
#> [1] "\"it's\""
shQuote("a b", type = "cmd")
#> [1] "\"a b\""
# Capture output: stdout = TRUE returns the lines; stderr = TRUE merges them.
script <- shQuote("echo out; echo err >&2; exit 4")
both <- suppressWarnings(
system2("sh", c("-c", script), stdout = TRUE, stderr = TRUE)
)
as.vector(both)
#> [1] "out" "err"
attr(both, "status")
#> [1] 4
only_out <- suppressWarnings(
system2("sh", c("-c", script), stdout = TRUE, stderr = FALSE)
)
as.vector(only_out)
#> [1] "out"
# Without stdout = TRUE the value is the exit status.
system2("sh", c("-c", shQuote("exit 5")))
system2("true")
# Environment variables for the child, and standard input from R.
system2("sh", c("-c", shQuote("echo $SHELF_DEMO")), env = "SHELF_DEMO=books",
stdout = TRUE)
#> [1] "books"
system2("sort", input = c("pear", "apple", "fig"), stdout = TRUE)
#> [1] "apple" "fig" "pear"
# A timeout: the command is killed and the status is 124.
res <- suppressWarnings(system2("sleep", "5", timeout = 1))
res
#> [1] 124
# system() takes one command line, which a shell parses.
system("echo from the shell", intern = TRUE)
#> [1] "from the shell"
files <- c("a b.txt", "c.txt")
system(paste("printf '%s\\n'", paste(shQuote(files), collapse = " ")),
intern = TRUE)
#> [1] "a b.txt" "c.txt"
# Sys.which() finds a program on the PATH ("" if there is none).
nzchar(Sys.which(c("sh", "tar", "no_such_program_xyz")))
#> [1] TRUE TRUE FALSE
names(Sys.which("sh"))
#> [1] "sh"
# A program that does not exist: system2() reports a missing command.
msg <- suppressWarnings(tryCatch(
system2("no_such_program_xyz", stdout = TRUE, stderr = FALSE),
error = \(e) conditionMessage(e)
))
sub(":.*$", "", msg)
#> [1] "error in running command"
Source: R documentation: system2
-
Quoting. Arguments are not quoted for you.
system2("echo", "a b")passes two words to the shell, which is why the output has lost its spaces;shQuote("a b")keeps them.shQuote()quotes for the Bourne shell by default (type = "sh"): a string without a single quote is surrounded by single quotes, otherwise by double quotes with the special characters backslash-escaped, and the same applies to a whole vector (one apostrophe anywhere changes the quoting of every element).type = "csh"quotes for the C shell, and"cmd"and"cmd2"for Windows ("cmd", the default there, follows Microsoft’s argument-parsing rules). Build every argument that contains a path or user data withshQuote(): this is the defence against both accidental spaces and shell injection. -
Output.
stdout = TRUEreturns the output lines as a character vector, with the exit status in the attribute"status"when it is not zero (and a warning);stderr = TRUEmerges standard error into it,stderr = FALSEorstdout = FALSEdiscards a stream, a character string names a file, and""(the default) sends it to the R console. The result may also carry an"errmsg"attribute whose text comes from the system, which is why the script printsas.vector()of it. The help page gives no guarantee that, when both streams go to the same place, they are interleaved in order. Withoutstdout = TRUEthe value is the exit status,0for success, invisibly, so it must be printed explicitly. If the command could not be started at all the value is127with a warning (R 3.5.0 and later), and for a program that cannot be found withstdout = TRUEthe result is an R error whose message starts witherror in running command(the text after it is system-dependent). -
Input and environment.
inputis a character vector sent to the command’s standard input one string per line;envis a character vector ofNAME=valuestrings set for the command only (on Windows it works only for commands such asRandmakethat take variables on the command line);stdincan name a file. -
Waiting.
wait = FALSEstarts the command and returns at once (the value is then0); it is ignored when the output is captured.timeout(seconds; R 3.5.0) kills the command after that time, with a warning and the exit status124, as thesleepexample shows; it works only withwait = TRUE, runs the command with standard input from the null device, and "is not guaranteed" for a program that ignores the signal. -
system.
system("cmd", intern = TRUE)is the old form ofstdout = TRUE;ignore.stdoutandignore.stderrdiscard streams. A shell is used on Unix-alikes, which is the reason to usesystem()when a pipeline or a redirection is the point, andshQuote()for every piece of data in it. On Windowssystem()does not use a shell and the separate functionshell()does. Since R 3.5.0 bothsystem(intern = FALSE)andsystem2()warn when the command cannot be executed. -
Finding programs.
Sys.which(c("zip", "tar"))returns the full path of each program on thePATHor""for a program that is not there, a cross-platform way to ask "is it installed?" before relying on it (the script printsnzchar()of the result so that no path appears). The R Introduction points to it for this purpose.
Prefer an R function to a shell command when there is one: file.copy() over cp, unlink() over rm, list.files() over ls, Sys.glob() over shell wildcards. They work on every platform, take paths as data, and report failure as a value. Reach for system2() for what R cannot do itself, such as calling git, a converter or a compiler, and check the returned status.
Archives: tar and zip
tar() and untar() create and unpack tar files; zip() and unzip() do the same for zip files. The R Introduction summarises it: R can list and unpack both kinds of archive with untar() and unzip() and create both, for zip with the help of an external program (An Introduction to R: Compression and Archives). The script does a complete round trip inside the working directory (so that the paths recorded in the archive are relative) and in tempdir(), and removes everything it created.
# Build a small export tree inside the working directory (removed at the end).
root <- "export-demo"
dir.create(file.path(root, "data"), recursive = TRUE)
invisible(file.copy(c("books.csv", "loans.csv"), file.path(root, "data")))
writeLines("shelf export", file.path(root, "README.txt"))
list.files(root, recursive = TRUE)
#> [1] "data/books.csv" "data/loans.csv" "README.txt"
# tar: files are recorded with the paths given; the archive lives in tempdir().
tarball <- tempfile(fileext = ".tar.gz")
tar(tarball, files = root, compression = "gzip")
untar(tarball, list = TRUE)
#> [1] "export-demo/data/" "export-demo/data/books.csv"
#> [3] "export-demo/data/loans.csv" "export-demo/README.txt"
# Extract everything, or only some files, into a directory (created if needed).
dest <- file.path(tempdir(), "from-tar")
untar(tarball, exdir = dest)
list.files(dest, recursive = TRUE)
#> [1] "export-demo/data/books.csv" "export-demo/data/loans.csv"
#> [3] "export-demo/README.txt"
one <- file.path(tempdir(), "one-file")
untar(tarball, files = "export-demo/README.txt", exdir = one)
list.files(one, recursive = TRUE)
#> [1] "export-demo/README.txt"
same <- tools::md5sum(c(
file.path(root, "data", "books.csv"),
file.path(dest, root, "data", "books.csv")
))
unname(same[1L] == same[2L])
#> [1] TRUE
# zip needs an external zip program to create an archive.
nzchar(Sys.which("zip"))
#> [1] TRUE
zipfile <- tempfile(fileext = ".zip")
zip(zipfile, files = root, flags = "-r9Xq")
listing <- unzip(zipfile, list = TRUE)
print(listing[order(listing$Name), c("Name", "Length")], row.names = FALSE)
#> Name Length
#> export-demo/ 0
#> export-demo/data/ 0
#> export-demo/data/books.csv 1187
#> export-demo/data/loans.csv 3694
#> export-demo/README.txt 13
# unzip() is built in; it returns the extracted paths invisibly.
unzipped <- unzip(zipfile, exdir = file.path(tempdir(), "from-zip"))
sort(basename(unzipped))
#> [1] "books.csv" "loans.csv" "README.txt"
unzipped <- unzip(zipfile, files = "export-demo/README.txt",
exdir = file.path(tempdir(), "from-zip-one"))
basename(unzipped)
#> [1] "README.txt"
# unz() reads one member of a zip file without extracting it.
con <- unz(zipfile, "export-demo/data/books.csv")
readLines(con, n = 2)
#> [1] "\"title\",\"author\",\"isbn\",\"year\",\"genre\",\"pages\""
#> [2] "\"The Dispossessed\",\"Ursula K. Le Guin\",\"978-0-306-40615-7\",1974,\"Science fiction\",387"
close(con)
unlink(root, recursive = TRUE)
dir.exists(root)
#> [1] FALSE
Source: R documentation: tar
-
tar.
tar(tarfile, files, compression = c("none", "gzip", "bzip2", "xz", "zstd"))records the paths given infiles(by default everything under the working directory; a path is relative to the working directory, so build archives from the directory that should be their root). Thetarargument decides how: R’s internal implementation is used iftarfileis a connection ortaris"internal"or"", which is the default because the argument defaults toSys.getenv("tar"), normally empty; an external program (GNUtar, orbsdtaron macOS and recent Windows) is used if you name it, andextra_flagsis passed to it. Thecompression_levelargument applies to the internal method only. The value is the return code,0for the internal version, invisibly.files = "."works with the internal method but usually not with an externaltar, which would expand it after creating the file. Zstandard compression of tarballs was added in R 4.5.0. -
untar.
untar(tarfile, list = TRUE)lists,exdirnames the directory to extract into (created if needed), andfilesextracts only some members, named by their recorded paths. By default it uses the external program named by theTARenvironment variable (which R sets) and detects the compression from the file; thecompressedargument is deprecated in favour of that detection, andtar = "internal"selects R’s own reader.extraspasses flags to an externaltar; R 4.6.0 madeuntar(tar = "internal")supportextras = "-P", to use the recorded paths unchanged. A tarball can contain absolute paths or..components, so never extract an archive from an untrusted source without listing it first and checking what it would write. -
zip.
zip(zipfile, files, flags = "-r9X")is a wrapper for an externalzipprogram, found throughR_ZIPCMD(set at installation on Unix; on Windows it must be on thePATH, for example the one from Rtools). The default flags are those for zipping a directory tree portably (-rrecurse,-9best compression,-Xleave out extra attributes); the script addsqso thatzipitself prints nothing. The value is the status of the external command, invisibly.extrascan add-xor-ipatterns, and file names in it must be quoted withshQuote(). Thezipprogram was installed in the container; where it is not,zip()fails. -
unzip and unz.
unzip(zipfile, list = TRUE)returns a data frame withName,Length(the uncompressed size) andDate("POSIXct"); the zip stores directories as entries of their own, ending with a slash, as the output shows. Extracting returns the paths written, invisibly, andexdir,files,overwriteandjunkpaths(drop the directory part) work as in the command line tool. The default method is R’s own minimal implementation, written for Windows users to unpack binary packages; since R 4.5.1 it handles../in member names asunzip 6.00does.unz(zipfile, member)reads a single member as a connection without extracting it. Dates in a zip file are stored in the style of MS-DOS, as local times to an accuracy of two seconds, sounzip()does not restore them unlesssetTimes = TRUE(internal method only).
Archives made by one tool and read by another are the usual source of surprises. The help page of tar() says that the tar format "no longer has an agreed standard" and that the behaviour of GNU tar changes between versions; --format=ustar (passed in extra_flags) is the more portable choice for GNU tar and for libarchive’s bsdtar. A checksum comparison after the round trip, as in the script (tools::md5sum()), is a cheap way to verify one. Since R 4.5.0 the package tools also has sha256sum(), which the NEWS describes as analogous to md5sum() and implementing the SHA-256 hashing algorithm; MD5 is enough to detect accidental corruption, and SHA-256 is the choice when a recorded checksum has to detect tampering (R documentation: sha256sum).
Encodings on connections
A text file has an encoding, and R’s functions assume the encoding of the session unless told otherwise. The connection is where that is told. The encoding argument of file() (and of url(), gzfile(), pipe() and the others) names the encoding of the stream, in the form that iconv() accepts, and the connection converts to the native encoding on input; on output it converts from the native encoding to the file’s. The fileEncoding argument of read.table() and write.table() does the same through a connection that those functions create. They are different from the encoding argument of readLines() and read.csv(), which only marks the strings and does not convert; Encodings, Locales and Internationalization has the comparison table and the decision rule.
# Write "cafe" with an acute accent in Latin-1: the connection converts.
path <- tempfile(fileext = ".txt")
con <- file(path, "w", encoding = "latin1")
writeLines("café", con)
close(con)
readBin(path, "raw", n = 5)
#> [1] 63 61 66 e9 0a
# Without the encoding the Latin-1 byte is not valid UTF-8.
x <- suppressWarnings(readLines(path))
x
#> [1] "caf\xe9"
validUTF8(x)
#> [1] FALSE
# Declaring the encoding of the strings does not convert the bytes ...
y <- readLines(path, encoding = "latin1")
Encoding(y)
#> [1] "latin1"
y
#> [1] "café"
# ... re-encoding is done by the connection, or by fileEncoding.
con <- file(path, encoding = "latin1")
z <- readLines(con)
close(con)
z
#> [1] "café"
Encoding(z)
#> [1] "UTF-8"
read.csv(path, header = FALSE, fileEncoding = "latin1")
#> V1
#> 1 café
# A byte order mark from a Windows tool. In a UTF-8 locale readLines() drops
# it by itself; the encoding name UTF-8-BOM makes the connection drop it.
bom <- tempfile(fileext = ".csv")
writeBin(c(as.raw(c(0xef, 0xbb, 0xbf)), charToRaw("title,pages\n")), bom)
readBin(bom, "raw", n = 4)
#> [1] ef bb bf 74
utf8ToInt(readLines(bom, n = 1L))[1L]
#> [1] 116
con <- file(bom, encoding = "UTF-8-BOM")
utf8ToInt(readLines(con, n = 1L))[1L]
#> [1] 116
close(con)
# An unsupported encoding name fails when the connection is used.
con <- file(path, encoding = "no-such-encoding")
tryCatch(readLines(con), error = \(e) conditionMessage(e))
#> [1] "unsupported conversion from 'no-such-encoding' to ''"
tryCatch(isOpen(con), error = \(e) conditionMessage(e))
#> [1] "invalid connection"
Source: R documentation: connections
The script shows the effect on bytes. The Latin-1 file holds the single byte e9 for the accented letter; read without an encoding, that is the string "caf\xe9", which is not valid UTF-8 (validUTF8() is FALSE); readLines(path, encoding = "latin1") marks the bytes as Latin-1 and R then displays them correctly, but the string stays Latin-1 and has not been converted; a connection created with encoding = "latin1" converts on the way in, and the result is a UTF-8 string. read.csv(fileEncoding = "latin1") does the same through the connection that read.table() makes. Points from the help page:
-
The names
"UTF-8"and"latin1"are portable;"utf8","mac"and"macroman"are not."UCS-2LE"and"UTF-16LE"are appropriate for Windows Unicode text files, and a byte order mark in those is removed."UTF-8-BOM"is accepted for reading and removes a UTF-8 byte order mark; in a UTF-8 locale, as the script shows,readLines()also dropped it by itself in the R 4.6.1 test, so the encoding name matters mainly for other locales and other readers. A BOM is not recommended on output: if you must write one,writeChar("\ufeff", con, eos = NULL)orwriteBin(as.raw(c(0xef, 0xbb, 0xbf)), con)does it. -
Output connections assume that the text handed to them is in the native encoding, and convert from it. Since R 4.2.0 the native encoding is UTF-8 on recent Windows as well, and
writeLines(useBytes = TRUE)skips the conversion, but "any attempt to convert encoding … will produce incorrect results" then. -
Re-encoding works only for text mode; mixing
readLines()andreadChar()on such a connection gives incorrect results, andseek()is not to be expected to work with it. -
A conversion that is not supported is an error reported when the connection is used. In R 4.6.1,
readLines()on an unopenedfile(path, encoding = "no-such-encoding")givesunsupported conversion from 'no-such-encoding' to ''(the script), while opening it at once withfile(path, "r", encoding = ...)reportsinvalid connection, and after either failure the connection no longer exists.
Common errors and warnings
The messages below were captured with the conditions() helper in the script: every warning and error that an expression signals, in the order they appear at the console. A file that cannot be opened is always two messages, a warning that says why and an error that says what failed.
# Record every warning and error that an expression signals, in order.
conditions <- function(expr) {
seen <- new.env()
seen$messages <- character()
note <- \(type, cond) {
text <- paste0(type, ": ", conditionMessage(cond))
seen$messages <- c(seen$messages, text)
}
withCallingHandlers(
tryCatch(expr, error = \(e) note("error", e)),
warning = \(w) {
note("warning", w)
invokeRestart("muffleWarning")
}
)
seen$messages
}
hide_path <- \(x) gsub("'[^']*/[^']*'", "'<path>'", x)
# A file that does not exist.
conditions(readLines("nosuch.txt"))
#> [1] "warning: cannot open file 'nosuch.txt': No such file or directory"
#> [2] "error: cannot open the connection"
conditions(read.csv("nosuch.csv"))
#> [1] "warning: cannot open file 'nosuch.csv': No such file or directory"
#> [2] "error: cannot open the connection"
conditions(gzfile("nosuch.gz", "r"))
#> [1] "warning: cannot open compressed file 'nosuch.gz', probable reason 'No such file or directory'"
#> [2] "error: cannot open the connection"
# A directory is not a file.
hide_path(conditions(readLines(tempdir())))
#> [1] "warning: 'raw = FALSE' but '<path>' is not a regular file"
#> [2] "warning: cannot open file '<path>': it is a directory"
#> [3] "error: cannot open the connection"
# A closed or unknown connection.
path <- tempfile()
writeLines(c("a", "b"), path)
con <- file(path, "r")
close(con)
conditions(readLines(con))
#> [1] "error: invalid connection"
conditions(close(con))
#> [1] "error: invalid connection"
conditions(getConnection(77))
#> [1] "error: there is no connection 77"
conditions(readLines(1))
#> [1] "error: 'con' is not a connection"
# Operations that need an open connection, or the other mode.
unopened <- file(path)
conditions(seek(unopened))
#> [1] "error: connection is not open"
conditions(truncate(unopened))
#> [1] "error: can only truncate an open connection"
close(unopened)
writer <- file(path, "w")
conditions(readLines(writer))
#> [1] "error: cannot read from this connection"
close(writer)
reader <- file(path, "r")
conditions(writeLines("x", reader))
#> [1] "error: cannot write to this connection"
conditions(readBin(reader, "integer", size = 3))
#> [1] "error: can only read from a binary connection"
close(reader)
conditions(close(stdout()))
#> [1] "error: cannot close standard connections"
conditions(file(c("a", "b")))
#> [1] "error: invalid 'description' argument"
# Binary data read as text.
bin <- tempfile()
writeBin(as.raw(c(0x41, 0x00, 0x42, 0x0a)), bin)
conditions(readLines(bin))
#> [1] "warning: line 1 appears to contain an embedded nul"
conditions(rawToChar(as.raw(c(0x73, 0x00, 0x68))))
#> [1] "error: embedded nul in string: 's\\0h'"
# Too many connections at once: the limit is 128 including the standard three.
leaked <- list()
conditions(repeat leaked[[length(leaked) + 1L]] <- file(path, "r"))
#> [1] "error: all 128 connections are in use"
length(leaked)
#> [1] 125
invisible(lapply(leaked, close))
nrow(showConnections())
#> [1] 0
# A socket nobody listens on.
conditions(socketConnection("localhost", 11824L, open = "r+", timeout = 2))
#> [1] "warning: localhost:11824 cannot be opened"
#> [2] "error: cannot open the connection"
# Files that are not what the reader expects.
conditions(readRDS(path))
#> [1] "error: error reading from connection"
conditions(unzip("nosuch.zip"))
#> [1] "warning: error 1 in extracting from zip file"
conditions(dir.create(""))
#> [1] "error: zero-length 'path' argument"
Source: R documentation: connections
| Message | Cause | What to do |
|---|---|---|
|
|
Read the warning, not the error. Check the path with |
|
The same for |
The same. |
|
The path names a directory. |
Use |
|
A connection used after |
Keep the connection open for as long as it is used and close it once; re-create it with |
|
|
Look at |
|
A number or a |
Pass the connection object, or a file name for functions that accept one. |
|
|
|
|
A read on a connection opened with |
Open with the right mode, or |
|
|
Open with |
|
|
Leave the standard streams alone; use |
|
|
Pass a single string; iterate over the paths. |
|
|
Read binary files with |
|
|
Drop the zero bytes, or split the vector at them. |
|
Connections opened in a loop and never closed. |
Close each one ( |
|
|
Check the host and port, that the server is running and the firewall; set |
|
|
|
|
|
Check the path and |
|
|
Check the value before calling. |
|
A failed change of directory ( |
Check |
|
The last line of a text file has no newline. |
|
|
A connection was dropped without |
Add |
Source: R documentation: connections
Pitfalls
-
Relative paths. A script that reads
"data/books.csv"works from one directory and fails from another. Make the base explicit, or find it from the script’s location (the--file=argument incommandArgs()is howRscripttells a script where it lives), and never rely on the IDE having set the working directory. The R FAQ 7.8 and 7.37 entries cover the Windows backslash. -
Opening in write mode by accident. A function that writes to an unopened connection truncates the file; use
"a"to append, andfile.copy()to make a backup first. -
Not closing. Every
file(),gzfile(),pipe()andsocketConnection()needs aclose(), ideally anon.exit(). The warningclosing unused connectionis the symptom, and data in an unflushed output file can be lost. -
Closing what is not yours. A function that receives an open connection must leave it open and positioned for the caller; only the function that opened it closes it. This is the manual’s rule: "leave the connection in the state you found it in".
-
Text versus binary. Reading a binary file with
readLines()and writing text withwriteBin()fail in different ways. Match the mode ("rb"and"wb") to the function. -
Pasted paths.
paste(dir, name, sep = "/")lets a trailing slash through and a missing one pass unnoticed;file.path()is for paths. A path built from user input is a file name, not a command: pass it to R functions, and to a shell only aftershQuote(). -
Ignoring return values.
file.copy(),file.rename(),file.remove(),dir.create()andunlink()report failure by value (andfile.copy()silently when the target exists). In a script that must not continue after a failure, wrap them instopifnot(). -
Deleting too much.
unlink(path, recursive = TRUE)with an empty or wrongpathis a classic data-loss bug; withpathbuilt byfile.path(dir, name)andnameempty, it deletesdir. Checknzchar(name)and that the path is inside the intended directory. -
Temporary paths in output. Printing
tempfile()ortempdir()in a test makes it fail on the next run; comparebasename()patterns orfile.exists(). -
The shell on Windows.
system()does not use a shell on Windows (useshell()orsystem2()),shQuote(type = "cmd")is the default there, and tools such aszip,tarandsedmay not be installed. -
Case.
"Books.csv"and"books.csv"are two files on Linux and one on macOS and Windows; an R script that works on a laptop can fail on a server. -
Encoding of file names. Non-ASCII file names depend on the locale; since R 4.0.0 a file name that cannot be translated is an error or a non-existent file, not a different file, and on Windows UTF-8 marked names are passed as Unicode.
-
Sockets and pipes in tests. A test that opens a port needs a free one and a timeout; a pipe needs the program to exist (
Sys.which()).
Version notes
The entries come from the R NEWS files (R NEWS); entries before R 4.0.0 are in the R 3.x NEWS (R 3.x NEWS).
| Version | Change |
|---|---|
R 3.0.0 |
|
R 3.1.0 |
|
R 3.2.0 |
|
R 3.3.0 |
|
R 3.4.0 |
|
R 3.5.0 |
|
R 3.6.0 |
New |
R 4.0.0 |
|
R 4.1.0 |
|
R 4.2.0 |
The native encoding is UTF-8 on recent Windows; the |
R 4.4.0 |
|
R 4.4.3 |
|
R 4.5.0 |
New connection type |
R 4.5.1 |
The internal method of |
R 4.6.0 |
|
The R-devel NEWS (the upcoming 4.7.0, never run here) says in its R-devel section that a gzcon() wrapper around a rawConnection() can now flush and be written (PR#19172), and that data() will read text files with the extension .zst or .zstd on builds with zstd support. Its section for the upcoming R 4.6.2 lists fixes: writeChar() in a multibyte locale counts characters and bytes consistently (PR#19112), path.expand("~") works on Windows for a user name that is not ASCII when neither HOME nor USER is set, and file.copy(date = TRUE) no longer opens files for exclusive access (PR#19187). Nothing on this page depends on them.
Where older tutorials differ
| Older tutorial writes | Current practice | Why | Since |
|---|---|---|---|
|
|
One separator rule for every platform; zero-length and trailing-slash cases handled |
always |
|
|
A changed working directory is a hidden global; forward slashes are accepted on Windows (R FAQ 7.8) |
always |
|
|
|
always |
Escaping a regular expression by hand to find a literal name, |
|
|
R 4.6.0 |
|
|
No shell, no quoting problems, a return value, and it works on Windows |
always |
|
|
A file name with a space or a |
always |
|
|
Leaked connections warn at garbage collection and can lose output |
always |
|
Nothing, or close the connections you opened |
It closes other code’s connections and sinks too |
always |
A hand-written path in a shared temporary folder, built with |
|
Portable, private to the session and removed at the end |
always |
Recreating |
|
The check recreates a valid session directory |
R 3.5.0 |
Advice that R can write only gzip, bzip2 and xz files |
|
zstd was added to the connections, to |
R 4.5.0 |
|
|
The internal method dropped |
R 4.2.0 |
|
|
The listening socket stays open |
R 4.0.0 |
|
|
The manual suggests socket connections for new projects instead of the earlier low-level interface |
old |
Where to go next
-
Reading and Writing Text Data for
read.csv(),write.csv(),scan()andread.fwf(), which all accept the connections on this page. -
Serialization: RDS, RData and JSON for
saveRDS(),load()and the binary connection behind them, and for whyload()of an untrusted file is dangerous. -
HTTP and Web APIs for
download.file()and theurl()connection against a real network. -
Encodings, Locales and Internationalization for
encoding =,fileEncoding =and the bytes behind them. -
Conditions and Error Handling for
tryCatch(),on.exit()and the warnings that file functions raise. -
Sessions, Workspaces and Startup for the working directory,
.Renvironand.Rprofile. -
Scripts and the Command Line for
Rscript,commandArgs()and reading standard input. -
Dates and Times for the
POSIXcttimes offile.info(). -
Strings and Regular Expressions for the regular expressions behind
list.files(pattern =). -
Parallel and Asynchronous Computing for the socket connections that clusters use.
-
Databases with DBI for connections to databases, which are a different kind of object (they are not R connections and
close()does not apply to them).
References
-
R Data Import/Export: 7 Connections; 7.1 Types of connections, 7.2 Output to connections, 7.3 Input from connections, 7.3.1 Pushback, 7.4 Listing and manipulating connections, 7.5 Binary connections, 7.5.1 Special values; 8 Network interfaces and 8.1 Reading from sockets
-
An Introduction to R: 14 OS facilities; 14.1 Files and directories, 14.2 Filepaths, 14.3 System commands, 14.4 Compression and Archives
-
R FAQ 7.8: How do file names work in Windows?; R FAQ 7.37: Why does backslash behave strangely inside strings?; R FAQ 7.34: How can I save the result of each iteration in a loop into a separate file?
-
R NEWS: Changes in R 4.0.0 to 4.6.1; R 3.x NEWS; R-devel NEWS
-
showConnections (stdin, stdout, stderr, getConnection, closeAllConnections, nullfile)