Intermediate

I/O Operations in R

Learn input and output in R - console formatting, reading and writing text files, and round-tripping tabular data with data frames and CSV

Input and output are where a program meets the outside world. In most languages that means printing strings and reading lines, and R does all of that. But R is a language built for statistics, so its I/O story has a distinctive centre of gravity: the data frame. Reading a CSV into a table and writing a table back out are first-class, one-line operations, and they are the I/O operations R programmers reach for most often.

R also gives you two everyday functions for writing to the console—print() and cat()—that behave quite differently, plus sprintf() for C-style formatted output. Understanding when to use each is the difference between clean, readable output and a screen full of [1] prefixes and stray quotes.

In this tutorial you will format console output precisely, write and read plain text files, handle input from standard input, round-trip a table through CSV, and deal gracefully with files that are missing or unreadable. Because R is vectorized, many of these operations work on whole vectors at once rather than one element at a time.

Formatted Console Output

The Hello World tutorial introduced print() and cat(). Here we look at how they differ and add sprintf() for precise numeric formatting. print() shows the [1] vector index and keeps quotes around strings; cat() strips both and joins its arguments; sprintf() returns a formatted string using C-style specifiers.

Create a file named formatted_output.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# Formatted output in R

# print() shows the [1] index prefix and quotes strings
print("Statistical computing")
print(c(1.5, 2.5, 3.5))

# cat() joins values with a space separator, no quotes or index
# fill = TRUE adds a trailing newline for us
cat("Values:", 1, 2, 3, fill = TRUE)

# sprintf() gives C-style format control
cat(sprintf("Integer: %d\n", 42L))
cat(sprintf("Float:   %.3f\n", 2 / 3))
cat(sprintf("Percent: %5.1f%%\n", 87.5))
cat(sprintf("Zero-pad: %08.2f\n", 3.14))

# paste() builds strings; paste0() uses no separator
label <- paste("R", "version", "4.4.2")
cat(label, fill = TRUE)
cat(paste0("file", "_", "name", ".csv"), fill = TRUE)

This produces:

[1] "Statistical computing"
[1] 1.5 2.5 3.5
Values: 1 2 3
Integer: 42
Float:   0.667
Percent:  87.5%
Zero-pad: 00003.14
R version 4.4.2
file_name.csv

Notice that print(c(1.5, 2.5, 3.5)) prints the whole vector on one line with a single [1] marker—R is vectorized, so a “single” value and a many-element vector are printed by the same machinery.

Writing and Reading Files

For plain text, writeLines() writes a character vector with one element per line, and readLines() reads a file back into a character vector. For appending, cat() accepts a file = argument together with append = TRUE. R’s real strength shows up with tabular data: write.csv() and read.csv() move an entire data frame to and from disk in a single call.

Create a file named file_io.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# File I/O in R

# --- Writing lines to a text file ---
notes <- c("First observation", "Second observation", "Third observation")
writeLines(notes, "notes.txt")
cat(sprintf("Wrote %d lines to notes.txt\n", length(notes)))

# --- Reading the file back ---
cat("\nContents of notes.txt:\n")
content <- readLines("notes.txt")
for (i in seq_along(content)) {
  cat(sprintf("%d: %s\n", i, content[i]))
}

# --- Appending to a file ---
cat("A later note\n", file = "notes.txt", append = TRUE)
cat(sprintf("\nAfter appending, the file has %d lines\n",
            length(readLines("notes.txt"))))

# --- Writing and reading tabular data (R's specialty) ---
cat("\nData frame round-trip:\n")
cities <- data.frame(
  city = c("Auckland", "Vienna", "Boston"),
  temp = c(19.5, 12.0, 8.3)
)
write.csv(cities, "cities.csv", row.names = FALSE)
loaded <- read.csv("cities.csv")
print(loaded)
cat(sprintf("Mean temperature: %.5f\n", mean(loaded$temp)))

The write.csv(..., row.names = FALSE) call suppresses R’s default habit of writing an unnamed row-number column. When the file is read back with read.csv(), R automatically infers that city is character data and temp is numeric, so mean(loaded$temp) works immediately on the restored column.

Reading from Standard Input

To read what a user (or a pipe) sends to standard input, open a connection with file("stdin") and read from it with readLines(). This is the idiom that works in scripts run with Rscript; the interactive-only readline() function returns an empty string in batch mode.

Create a file named read_input.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Reading from standard input
con <- file("stdin")

cat("Enter your name: ")
name <- readLines(con, n = 1)

cat("Enter your age: ")
age <- as.integer(readLines(con, n = 1))

close(con)

cat(sprintf("%s will be %d next year.\n", name, age + 1L))

When input is piped in rather than typed at a terminal, the two prompts print back-to-back before any value is consumed (there is no keyboard echo to separate them). Feeding it the two lines Ada and 36 produces:

Enter your name: Enter your age: Ada will be 37 next year.

Run interactively in an R session, the prompts would appear on separate lines as you type each answer. The as.integer() conversion turns the text "36" into the number 36 so the arithmetic age + 1L works.

Handling I/O Errors

Files go missing and permissions fail, so robust I/O code checks before it reads and traps errors when it must. Use file.exists() to test for a file, and tryCatch() to catch an error condition and continue instead of aborting the script.

Create a file named safe_read.R:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Graceful I/O error handling

read_safely <- function(path) {
  if (!file.exists(path)) {
    cat(sprintf("File '%s' does not exist.\n", path))
    return(invisible(NULL))
  }
  lines <- readLines(path)
  cat(sprintf("Read %d line(s) from %s\n", length(lines), path))
}

# tryCatch turns a fatal read error into a handled message
result <- tryCatch(
  read.csv("missing.csv"),
  error = function(e) {
    cat("Could not read missing.csv (handled gracefully)\n")
    NULL
  }
)

read_safely("missing.txt")

This produces:

Could not read missing.csv (handled gracefully)
File 'missing.txt' does not exist.

Because both the missing CSV and the missing text file are handled, the script runs to completion instead of stopping at the first error—exactly what you want in a data pipeline that processes many files.

Running with Docker

Docker gives you a consistent R environment without a local install. Use the same official R base image referenced throughout this series.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull r-base:4.4.2

# Run the formatted output example
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript formatted_output.R

# Run the file I/O example (this one is shown in Expected Output below)
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript file_io.R

# Run the standard input example, piping two lines of input in
printf "Ada\n36\n" | docker run --rm -i -v $(pwd):/app -w /app r-base:4.4.2 Rscript read_input.R

# Run the error handling example
docker run --rm -v $(pwd):/app -w /app r-base:4.4.2 Rscript safe_read.R

The -v $(pwd):/app mount is what lets file_io.R create notes.txt and cities.csv in your current directory—the files persist on your machine after the container exits. Note the extra -i flag on the read_input.R command, which keeps standard input open so the piped values reach the script.

Expected Output

Running file_io.R produces:

Wrote 3 lines to notes.txt

Contents of notes.txt:
1: First observation
2: Second observation
3: Third observation

After appending, the file has 4 lines

Data frame round-trip:
      city temp
1 Auckland 19.5
2   Vienna 12.0
3   Boston  8.3
Mean temperature: 13.26667

The aligned table under “Data frame round-trip” is R’s default printing for a data frame: the leftmost column holds row numbers, character columns are right-justified, and the numeric temp column is formatted with a consistent number of decimal places.

Key Concepts

  • print() vs cat(): print() adds the [1] index and keeps quotes—great for inspecting values; cat() produces clean, quote-free output for user-facing text. Use sprintf() when you need precise numeric formatting.
  • Vectorized I/O: writeLines() and readLines() operate on whole character vectors, one element per line, rather than requiring an explicit loop to write each line.
  • Data frames are R’s native I/O unit: write.csv() and read.csv() round-trip an entire table in one call, automatically inferring column types on read—this is the I/O operation R programmers use most.
  • row.names = FALSE: pass this to write.csv() to avoid an unwanted leading row-number column in your output file.
  • Standard input needs a connection: read piped or typed input with readLines(file("stdin"), n = 1); the interactive readline() does not work in Rscript batch mode.
  • Append with cat(..., append = TRUE): cat() writes to a file when given a file = argument, and append = TRUE adds to the end instead of overwriting.
  • Guard and trap: combine file.exists() for pre-checks with tryCatch() to keep a batch script running when an individual file is missing or malformed.

Running Today

All examples can be run using Docker:

docker pull r-base:4.4.2
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining