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

file()

an ordinary file; also "" (an anonymous scratch file), "clipboard" and "stdin"

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 http://, https://, ftp:// or file:// is passed to url().

url()

a file://, http://, https:// or ftp:// address

Read only. The "libcurl" method is the one used for everything except file:// (it is required on Windows since R 4.2.0, and the "internal" method no longer supports ftp:// since that release).

gzfile(), bzfile(), xzfile(), zstdfile()

a file compressed with gzip, bzip2, xz or zstd

zstdfile() was added in R 4.5.0 and exists only when R was built with the zstd library. gzfile() and file() detect the compression when reading.

unz()

one member of a zip archive

Read only, binary.

gzcon()

wraps another binary connection in gzip decompression (or compression)

Used for compressed data that arrives over a url() or a socket.

pipe()

the standard input or output of another program

The command runs in a shell when the connection is opened. Read or write, not append.

fifo()

a named pipe

Needs operating system support; capabilities("fifo") says whether it is there.

socketConnection(), serverSocket(), socketAccept()

a TCP socket, as client or server

capabilities("sockets"). serverSocket() and socketAccept() came in R 4.0.0.

textConnection()

a character vector (input) or a character vector being filled (output)

Always open. Copies the vector when it is created.

rawConnection()

a raw vector

A file in memory, for binary data; rawConnectionValue() returns the bytes written.

stdin(), stdout(), stderr()

the console and the two output streams

Always open and cannot be opened or closed. file("stdin") is the different, process-level standard input.

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

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

"r", "rt"

Read, text mode

The default when a connection is opened without a mode. The file must exist.

"w", "wt"

Write, text mode

Creates the file or empties it.

"a", "at"

Append, text mode

Creates the file if it is missing. For a compressed file it appends a new compressed stream.

"rb", "wb", "ab"

The same in binary mode

Required by readBin(), writeBin(), load() and save(). On Windows text mode translates line endings (LF and CRLF); binary mode does not.

"r+", "r+b"

Read and write

The write position starts at the beginning of the file.

"w+", "w+b"

Read and write, emptying the file first

File and socket connections only. R keeps a separate read position and write position.

"a+", "a+b"

Read and append

The default mode of socket 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"

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>)

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)

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)

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

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")

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

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" or endian = "big" (network byte order is big-endian), and endian = "swap" forces a swap. The help page: “endian-ness is relevant for size > 1, and should always be set for portable code”. Since R 4.0.0 an invalid endian value is an error (invalid 'endian' argument) where earlier versions silently swapped.

  • Size. size changes 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, so writeBin(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 = FALSE reads 1-byte and 2-byte integers as unsigned, as the 65535 in 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 pattern 00 00 00 80 on a little-endian machine, the smallest representable int, as the manual’s section on special values explains (R Data Import/Export: Special values).

  • Strings. readBin(what = "character") and writeBin("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 use readChar() and writeChar(); their nchars argument counts characters, not bytes, unless useBytes = TRUE (the script shows a letter that takes two bytes in UTF-8 but is one character), and writeChar() writes a terminating zero byte after each string unless eos = NULL, as the 78 79 7a 00 in the output shows.

  • Raw vectors and text. charToRaw() returns the bytes of a string and rawToChar() builds a string from bytes (a vector or, with multiple = TRUE, one string per byte). Embedded zero bytes are an error for rawToChar() (embedded nul in string) and a warning for readLines(). The bytes of a UTF-8 string are not valid text one at a time, which is why readChar(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() and writeBin() on a connection opened in text mode are errors, can only read from a binary connection and can 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 "

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] "/"

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

basename(path), dirname(path)

The last component and everything before it

dirname("books.csv") is ".", dirname("/") is "/"; basename("data/") ignores the trailing slash. Both are vectorised.

tools::file_ext(x)

The extension, without the dot

"" when there is none; only the part after the last dot ("gz" for backup.tar.gz); case is kept. A file whose name starts with a dot has no extension.

tools::file_path_sans_ext(x)

The path without its extension

compression = TRUE also strips .gz, .bz2 and .xz first.

path.expand(path)

Replaces a leading tilde by the home directory

Reads HOME on Unix; on Windows it reads R_USER (or HOME if that is unset) the first time it is called in a session. From R 4.1.0 it can also expand ~user on most Unix-alikes and tries harder when HOME is unset; from R 4.2.0 it warns when the expanded path would be too long.

normalizePath(path, winslash, mustWork)

An absolute, canonical path

Resolves ., .. and symbolic links (on Windows also short names to long names). mustWork = NA (the default) warns for a path that does not exist, TRUE makes it an error, FALSE is silent. winslash is "\\" by default and only matters on Windows.

nullfile()

The name of the null device

"/dev/null" or "nul:".

file.path(...)

Joins components

Never paste(..., sep = "/").

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)
  • 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 has all.files = TRUE.

  • Recursion. recursive = TRUE descends into directories and lists only files; add include.dirs = TRUE to keep the directories. With full.names = FALSE the names are relative to path, which is what the script prints; with full.names = TRUE they start with path, 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 = TRUE makes 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 = TRUE is new in R 4.6.0: the NEWS entry reads “list.files() now has optional argument fixed to 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 matches axb.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 error invalid 'pattern' regular expression; with fixed = TRUE all 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 as file.path(root, "*", "*.csv"), which no pattern can do; it returns character(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) and file_test("-d", path) are the POSIX test command. The R Introduction names file.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
  • Creating. dir.create() returns TRUE or FALSE invisibly, 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 unless recursive = TRUE. showWarnings = FALSE silences the warning; check the result or dir.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 warns cannot create file '<file>', reason 'No such file or directory' when the directory is missing.

  • Copying. file.copy(from, to) returns FALSE without a warning when the target exists and overwrite = FALSE (the default), which is the usual surprise. Its overwrite default is recursive, so a recursive copy overwrites. If to is an existing directory, the files are copied into it. Copying a directory needs recursive = TRUE, and to must then be one existing directory: otherwise the warnings are directories 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-recursive file.copy() and file.append(). copy.mode = TRUE (the default) copies the permission bits and copy.date = TRUE preserves the modification time (R 3.1.0), as the dated.txt line in the script shows. A missing source file warns with problem 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 of tempdir() often fails; the portable use is to rename within one directory, and the fallback elsewhere is file.copy() followed by file.remove(). The failure warning is cannot 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 warns cannot remove file '<file>', reason '...' when it cannot; for a non-empty directory the reason is Directory not empty. unlink() is the general tool: recursive = TRUE removes a directory tree, force = TRUE changes 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 is 0 for success and 1 for failure, invisibly. The help page states that being unable to delete a directory when recursive = FALSE is not a failure, yet in R 4.6.1 as run for this page unlink(dir) on a directory returned 1 and left the directory in place (the script shows the directory still exists), so test the result with dir.exists() and not by the status. Wildcards are expanded by Sys.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 called star*.tmp. R 4.5.0 checks arguments that must be a single logical more thoroughly (R NEWS names unlink(, recursive) and tempdir()), so unlink(x, recursive = NA) is the error invalid 'recursive' argument.

  • Information. file.info() returns a data frame with the size, the isdir flag, 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 give NA rows. file.size(), file.mtime() and file.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) returns 0 for success and -1 for failure, with mode 0 (exists), 1 (execute), 2 (write) and 4 (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 and file.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, and unlink(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, less or more on Unix), so its output is not part of the R output stream and there is nothing useful to run in a script. file.edit() and file.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())
  • Where it is. The per-session directory is a subdirectory of the first writable directory named by the environment variables TMPDIR, TMP and TEMP, in that order, or else a default location chosen by the system. Setting one of them inside a running session has no effect on tempdir(), 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 print basename() 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 and on.exit(unlink(dir, recursive = TRUE)) for a directory (as with_scratch() shows), or withr::local_tempfile() for those who use the withr package. Do not delete tempdir() itself.

  • Private directories. When several files belong together, tempfile("name-") plus dir.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 then writeLines(x, tempfile()) warns cannot open file '<file>': No such file or directory, as the script shows. tempdir(check = TRUE) (R 3.5.0) "recreates the tempdir() 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 the check argument more strictly, so tempdir(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")

The variables that matter for files and paths:

Variable Effect

HOME

The home directory used by tilde expansion on Unix, and one of the places where R looks for the user .Renviron and .Rprofile.

R_USER

The user’s home directory on Windows, which path.expand() reads there (it falls back to HOME when R_USER is unset).

TMPDIR, TMP, TEMP

Where the per-session temporary directory is created. Read at startup only.

R_ENVIRON, R_ENVIRON_USER

Which Renviron files are read at startup.

R_ZIPCMD, TAR and the option unzip

The external programs behind zip(), untar() and unzip(unzip = getOption("unzip")). tar() itself does not use TAR: its tar argument defaults to Sys.getenv("tar"), normally unset, which selects the internal implementation.

R_LIBS, R_LIBS_USER, R_LIBS_SITE

Library paths for installed packages (see Packages, Libraries and Namespaces).

PATH

Where Sys.which(), system() and system2() look for programs.

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

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"
  • 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 with shQuote(): this is the defence against both accidental spaces and shell injection.

  • Output. stdout = TRUE returns the output lines as a character vector, with the exit status in the attribute "status" when it is not zero (and a warning); stderr = TRUE merges standard error into it, stderr = FALSE or stdout = FALSE discards 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 prints as.vector() of it. The help page gives no guarantee that, when both streams go to the same place, they are interleaved in order. Without stdout = TRUE the value is the exit status, 0 for success, invisibly, so it must be printed explicitly. If the command could not be started at all the value is 127 with a warning (R 3.5.0 and later), and for a program that cannot be found with stdout = TRUE the result is an R error whose message starts with error in running command (the text after it is system-dependent).

  • Input and environment. input is a character vector sent to the command’s standard input one string per line; env is a character vector of NAME=value strings set for the command only (on Windows it works only for commands such as R and make that take variables on the command line); stdin can name a file.

  • Waiting. wait = FALSE starts the command and returns at once (the value is then 0); 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 status 124, as the sleep example shows; it works only with wait = 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 of stdout = TRUE; ignore.stdout and ignore.stderr discard streams. A shell is used on Unix-alikes, which is the reason to use system() when a pipeline or a redirection is the point, and shQuote() for every piece of data in it. On Windows system() does not use a shell and the separate function shell() does. Since R 3.5.0 both system(intern = FALSE) and system2() warn when the command cannot be executed.

  • Finding programs. Sys.which(c("zip", "tar")) returns the full path of each program on the PATH or "" for a program that is not there, a cross-platform way to ask "is it installed?" before relying on it (the script prints nzchar() 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
  • tar. tar(tarfile, files, compression = c("none", "gzip", "bzip2", "xz", "zstd")) records the paths given in files (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). The tar argument decides how: R’s internal implementation is used if tarfile is a connection or tar is "internal" or "", which is the default because the argument defaults to Sys.getenv("tar"), normally empty; an external program (GNU tar, or bsdtar on macOS and recent Windows) is used if you name it, and extra_flags is passed to it. The compression_level argument applies to the internal method only. The value is the return code, 0 for the internal version, invisibly. files = "." works with the internal method but usually not with an external tar, 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, exdir names the directory to extract into (created if needed), and files extracts only some members, named by their recorded paths. By default it uses the external program named by the TAR environment variable (which R sets) and detects the compression from the file; the compressed argument is deprecated in favour of that detection, and tar = "internal" selects R’s own reader. extras passes flags to an external tar; R 4.6.0 made untar(tar = "internal") support extras = "-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 external zip program, found through R_ZIPCMD (set at installation on Unix; on Windows it must be on the PATH, for example the one from Rtools). The default flags are those for zipping a directory tree portably (-r recurse, -9 best compression, -X leave out extra attributes); the script adds q so that zip itself prints nothing. The value is the status of the external command, invisibly. extras can add -x or -i patterns, and file names in it must be quoted with shQuote(). The zip program was installed in the container; where it is not, zip() fails.

  • unzip and unz. unzip(zipfile, list = TRUE) returns a data frame with Name, Length (the uncompressed size) and Date ("POSIXct"); the zip stores directories as entries of their own, ending with a slash, as the output shows. Extracting returns the paths written, invisibly, and exdir, files, overwrite and junkpaths (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 as unzip 6.00 does. 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, so unzip() does not restore them unless setTimes = 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"

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) or writeBin(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() and readChar() on such a connection gives incorrect results, and seek() 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 unopened file(path, encoding = "no-such-encoding") gives unsupported conversion from 'no-such-encoding' to '' (the script), while opening it at once with file(path, "r", encoding = ...) reports invalid 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"
Message Cause What to do

cannot open the connection (error), after the warning cannot open file 'nosuch.txt': No such file or directory

readLines(), read.csv(), scan(), file(open = ) on a path that does not exist, is not readable or is a directory. The warning before it carries the reason.

Read the warning, not the error. Check the path with file.exists(), getwd() and normalizePath(mustWork = TRUE); a relative path is relative to the working directory.

cannot open compressed file 'nosuch.gz', probable reason 'No such file or directory'

The same for gzfile(), bzfile(), xzfile() and zstdfile().

The same.

cannot open file '<path>': it is a directory, with 'raw = FALSE' but '<path>' is not a regular file

The path names a directory.

Use list.files() to list it.

invalid connection

A connection used after close(), or after closeAllConnections(), or after the garbage collector closed it.

Keep the connection open for as long as it is used and close it once; re-create it with file() when needed.

there is no connection 77

getConnection() with a number that is not a row of showConnections().

Look at showConnections(all = TRUE).

'con' is not a connection

A number or a NULL given where a connection is required.

Pass the connection object, or a file name for functions that accept one.

connection is not open, can only truncate an open connection

seek() or truncate() on a connection that is not open.

open() it first.

cannot read from this connection, cannot write to this connection

A read on a connection opened with "w" or "a", or a write on one opened with "r".

Open with the right mode, or "r+" or "w+" for both.

can only read from a binary connection, can only write to a binary connection

readBin() on a connection opened for reading in text mode, or writeBin() on one opened for writing in text mode.

Open with "rb" or "wb".

cannot close standard connections, cannot open standard connections

close(stdout()) or open(stdin()).

Leave the standard streams alone; use sink() to redirect output.

invalid 'description' argument

file() with a vector of length greater than one, or NA.

Pass a single string; iterate over the paths.

line 1 appears to contain an embedded nul (warning)

readLines() on a binary file.

Read binary files with readBin(), or use skipNul = TRUE if the NULs are noise.

embedded nul in string: 's\0h'

rawToChar() on bytes that contain a zero byte.

Drop the zero bytes, or split the vector at them.

all 128 connections are in use

Connections opened in a loop and never closed.

Close each one (on.exit(close(con))); check showConnections(); do not raise the limit as a fix.

localhost:11824 cannot be opened (warning) then cannot open the connection

socketConnection() to a port with nothing listening, after the timeout.

Check the host and port, that the server is running and the firewall; set timeout.

error reading from connection

readRDS() on a file that is not a serialization.

See Serialization: RDS, RData and JSON.

error 1 in extracting from zip file (warning)

unzip() on a file that does not exist or is not a zip file.

Check the path and file.exists(). unzip() warns, it does not stop, so a later step fails.

zero-length 'path' argument

dir.create("").

Check the value before calling.

cannot change working directory

A failed change of directory (setwd of a path that does not exist).

Check dir.exists() first, or do not change directory at all.

incomplete final line found on '<file>' (warning)

The last line of a text file has no newline.

readLines(warn = FALSE), or fix the writer.

closing unused connection 3 (<path>) (warning)

A connection was dropped without close().

Add on.exit(close(con)) after the open.

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 in commandArgs() is how Rscript tells 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, and file.copy() to make a backup first.

  • Not closing. Every file(), gzfile(), pipe() and socketConnection() needs a close(), ideally an on.exit(). The warning closing unused connection is 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 with writeBin() 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 after shQuote().

  • Ignoring return values. file.copy(), file.rename(), file.remove(), dir.create() and unlink() report failure by value (and file.copy() silently when the target exists). In a script that must not continue after a failure, wrap them in stopifnot().

  • Deleting too much. unlink(path, recursive = TRUE) with an empty or wrong path is a classic data-loss bug; with path built by file.path(dir, name) and name empty, it deletes dir. Check nzchar(name) and that the path is inside the intended directory.

  • Temporary paths in output. Printing tempfile() or tempdir() in a test makes it fail on the next run; compare basename() patterns or file.exists().

  • The shell on Windows. system() does not use a shell on Windows (use shell() or system2()), shQuote(type = "cmd") is the default there, and tools such as zip, tar and sed may 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

list.files() gained no.. to leave out "." and "..".

R 3.1.0

file.copy() gained copy.date; pushBack() gained encoding; file.path() removes a trailing separator on Windows.

R 3.2.0

file.info(extra_cols = FALSE) computes a minimal set of columns on Unix-alikes.

R 3.3.0

gzcon() gained text, which makes the connection text-oriented so that pushBack() works.

R 3.4.0

file.copy(copy.date = TRUE) sets timestamps with fractions of seconds where the platform and file system allow it.

R 3.5.0

tempdir(check = TRUE) recreates the session directory if it is no longer valid; system() and system2() accept a timeout; system(intern = FALSE) and system2() warn when the command cannot be executed, and system2() returns 127 in that case.

R 3.6.0

New nullfile(); file.copy() works with a to of length greater than one; untar() with an external command assumes it detects compression (the compressed argument is deprecated); zip() passes a long file list on standard input.

R 4.0.0

serverSocket() and socketAccept(); unlink(expand = ); readBin() and writeBin() give an error for an invalid endian; writeBin() allows long vectors where available; normalizePath() resolves symbolic links and normalises the case of long names in case-insensitive folders; file.path() supports paths not in the session encoding; most functions with file-path inputs signal an error for a marked-encoding path that cannot be translated; Sys.glob() supports the whole Unicode Basic Multilingual Plane.

R 4.1.0

textConnection() gained name; path.expand() can expand ~user on most Unix-alikes even without readline and tries harder when HOME is unset.

R 4.2.0

The native encoding is UTF-8 on recent Windows; the "internal" method of url() no longer supports ftp://; libcurl is required on Windows; path.expand() warns when an expanded path would be too long.

R 4.4.0

file.copy(recursive = FALSE) and file.append() omit directories.

R 4.4.3

dir.create() is protected against an empty-string path.

R 4.5.0

New connection type zstdfile() (R must be built with zstd); zstd in memCompress(), memDecompress(), tar() and untar(); stricter checks of single-logical arguments (unlink(recursive =), tempdir(check =)); file.info() on Windows reports the file owner.

R 4.5.1

The internal method of unzip() handles ../ in member paths as unzip 6.00 does.

R 4.6.0

list.files(fixed = ) for literal pattern matching; gzcon() can decompress a concatenation of compressed streams; untar(tar = "internal") supports extras = "-P".

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

paste(dir, "books.csv", sep = "/")

file.path(dir, "books.csv")

One separator rule for every platform; zero-length and trailing-slash cases handled

always

read.csv("C:\\data\\books.csv"), or a call to setwd with a Windows path at the top of every script

read.csv(file.path(base, "books.csv")), with base from an argument or an environment variable

A changed working directory is a hidden global; forward slashes are accepted on Windows (R FAQ 7.8)

always

list.files(pattern = "*.csv")

list.files(pattern = "\\.csv$"), or Sys.glob("*.csv") for a wildcard

pattern is a regular expression, not a shell wildcard

always

Escaping a regular expression by hand to find a literal name, list.files(pattern = "a\\.b\\.csv")

list.files(pattern = "a.b.csv", fixed = TRUE)

fixed does the literal match

R 4.6.0

system("rm -rf dir"), system("cp a b")

unlink(dir, recursive = TRUE), file.copy(a, b)

No shell, no quoting problems, a return value, and it works on Windows

always

system(paste("convert", file, "out.png"))

system2("convert", c(shQuote(file), "out.png"))

A file name with a space or a ; is a different command otherwise

always

sapply(files, function(f) readLines(f)) without closing anything, or file(path, "r") with no close()

readLines(path) for one shot; con <- file(path, "r"); on.exit(close(con)) for sequential reads

Leaked connections warn at garbage collection and can lose output

always

closeAllConnections() at the top of a script "to start clean"

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 paste0()

tempfile() and tempdir(), or file.path(tempdir(), name)

Portable, private to the session and removed at the end

always

Recreating tempdir() by hand with dir.create() after a cleaner removed it

tempdir(check = TRUE)

The check recreates a valid session directory

R 3.5.0

Advice that R can write only gzip, bzip2 and xz files

zstdfile() where the build supports it, for fast compression

zstd was added to the connections, to memCompress() and to tar()

R 4.5.0

readLines(url("ftp://..."))

https:// addresses

The internal method dropped ftp://; modern browsers and servers prefer HTTPS

R 4.2.0

socketConnection(server = TRUE) for every server

serverSocket() with socketAccept() for several clients

The listening socket stays open

R 4.0.0

make.socket(), read.socket()

socketConnection()

The manual suggests socket connections for new projects instead of the earlier low-level interface

old

Where to go next

References