rguides

system2

system2(command, args = character(), stdout = "", stderr = "", stdin = "", input = NULL, env = character(), wait = TRUE, timeout = 0)

Introduction

system2() runs an external command and gives you control over its output streams. Compared to the older system() function, it has cleaner semantics for capturing stdout and stderr separately, handling timeouts, and passing environment variables.

This function lives in base R, so you don’t need to install anything.

Parameters

command (required) The name or path of the command to execute. Must be a single character string.

# Good
system2("ls")

# Bad - will error
system2(c("ls", "ps"))

The command parameter specifies which program to run, while args controls how it is invoked. Unlike system(), where you pass a single shell command string, system2() separates the executable name from its arguments, which avoids shell injection risks and removes the need for manual quoting. Each element of the args vector becomes exactly one argument passed to the command — spaces inside an element are preserved, but spaces between elements are not interpreted as argument separators.

args A character vector of arguments. Each element becomes a separate argument. Do not pass a single space-separated string.

# Correct - three separate arguments
system2("grep", args = c("-r", "-n", "pattern", "."))

# Wrong - grep will look for a file literally named "-r -n pattern ."
system2("grep", args = "-r -n pattern .")

stdout Controls where stdout goes:

  • "" (default): capture and return as a character vector
  • FALSE: discard completely
  • TRUE: send to R’s stdout (prints directly, does not capture)
  • filename string: write to that file

stderr Same options as stdout. Note that on Unix systems, when you capture stdout with stdout="", stderr gets merged into it automatically.

stdin Controls stdin input:

  • "" (default): empty stdin
  • TRUE: read from R’s stdin
  • filename: read input from a file

input A single character string sent to the command’s stdin. For multi-line input, use stdin=filename instead.

env A named character vector of environment variables to set for the command.

system2("echo", args = "HOME is $HOME", env = c("HOME" = "/tmp"))
# [1] "HOME is /tmp"

wait

  • TRUE (default): wait for the command to finish before returning
  • FALSE: spawn the process and return immediately (requires R 3.4.0+)

When wait=FALSE, the return value is a process object. Query its exit status with parallel::mcquitest().

minimized and invisible Windows-only parameters. R silently ignores them on other platforms.

timeout Maximum seconds to wait. 0 means no timeout. On Unix, this uses the timeout(1) command internally. When a command times out, the exit status is 124.

Return value

The return type depends on your stdout and wait settings:

stdoutwaitReturn value
""TRUECharacter vector of stdout lines (or exit status if no output)
FALSETRUEExit status integer
anyFALSEProcess object
# Captures output
lines <- system2("ls", args = "-1", stdout = "")
class(lines)
# [1] "character"

# Gets only exit status
status <- system2("grep", args = c("-q", "foo", "bar.txt"), stdout = FALSE)
status
# [1] 1

# Asynchronous - returns immediately
p <- system2("sleep", args = "10", wait = FALSE)
# Process object returned right away

Once you understand the return type rules, the next step is handling actual command output. Capturing stdout as a character vector is the most frequent pattern: you run a command, get its output line by line, and then parse or use those lines in R. This replaces shell pipelines and temporary files with a single R expression, keeping the entire workflow inside one language and making it easier to handle errors, timeouts, and edge cases programmatically.

Capturing command output

The most common use case is running a command and getting its output back into R.

# List files with details
result <- system2("ls", args = c("-la", tempdir()), stdout = "")
cat(result, sep = "\n")
# [1] "total 32"
# [1] "drwxr-xr-x 6 user user 4096 Mar 24 10:00 ."
# [1] "drwxr-xr-x 5 user user 4096 Mar 24 09:30 ..."

When you only need to know whether a command succeeded or failed — not what it printed — capture the exit status by setting stdout = FALSE. An exit status of 0 means success, and any nonzero value signals an error. This pattern is essential for conditional logic: run a check, inspect the status, and branch accordingly. It avoids storing potentially large output in memory when the binary result is all you need, which matters when scanning large files or running checks across many inputs.

find_pattern <- function(pattern, file) {
  status <- system2("grep", args = c("-q", pattern, file), stdout = FALSE)
  status == 0
}

find_pattern("BUG", "README.md")
# [1] FALSE

External commands can hang indefinitely — a network call that never returns, a computation that spirals, or a pipeline waiting on a blocked resource. The timeout parameter puts a hard limit on how long system2() waits. On Unix systems, R delegates to GNU timeout(1), which sends SIGTERM first and then SIGKILL if the process refuses to die. A timed-out command returns exit status 124, which you can check to distinguish a genuine failure from a command that simply ran too long.

Handling timeouts

The timeout parameter kills commands that run too long. Exit status 124 means the timeout fired.

# This will be killed after 2 seconds
result <- system2("sleep", args = "10", timeout = 2, stdout = "")
result
# [1] 124

# Check for timeout
was_timeout <- function(result) {
  is.integer(result) && length(result) == 1 && result == 124
}

Be aware that on Unix systems, timeout(1) from GNU coreutils handles the timeout. If your system doesn’t have it, the command runs without a timeout even if you specify one.

Running commands asynchronously

Set wait=FALSE to spawn a process and continue immediately. The return value is a process object.

# Spawn a long-running process
p <- system2("python", args = c("-c", "import time; time.sleep(5)"), wait = FALSE)

# Later, check if it finished and get exit status
Sys.sleep(2)
parallel::mcquitest(p)
# Returns exit status when done

Running commands asynchronously is powerful but introduces coordination challenges — you need to track which processes are still running and collect their results. For simple fire-and-forget tasks, wait = FALSE is sufficient. For anything involving multiple concurrent processes with dependencies, consider the processx or callr packages, which provide richer process management primitives including standard stream redirection and proper cleanup of child processes on R exit.

Several common mistakes trip up even experienced R users when calling external commands. These mistakes are easy to make but straightforward to avoid once you recognise the patterns.

Common mistakes

Confusing stdout="" with stdout=FALSE

# Captures output
system2("echo", args = "hello", stdout = "")

# Discards output, returns exit status only
system2("echo", args = "hello", stdout = FALSE)

A second frequent mistake involves passing arguments as a single space-separated string instead of a character vector. When you write args = "-la ~", the command receives one literal argument containing spaces, not two separate flags. The correct form is args = c("-la", "~"). This distinction is the single most common source of “command not found” or “no such file” errors in system2() calls, because the called program sees a garbled filename instead of the intended flags and paths.

Passing args as a single string

# Doesn't work as intended
system2("ls", args = "-la ~")

# What you actually get: ls looks for a file literally named "-la ~"

# Correct
system2("ls", args = c("-la", "~"))

Cross-platform differences in how stdout and stderr are interleaved can produce surprising results. On Unix, capturing stdout with stdout = "" automatically merges stderr into the output, so diagnostic messages appear inline with normal output. On Windows, the two streams stay separate and stdout = "" only captures stdout — stderr is discarded by default. Code that relies on merged streams will silently lose error information when run on Windows, making debugging difficult if you only test on one platform.

Assuming cross-platform interleaving works the same way

# On Unix: both stdout and stderr are merged into result
result <- system2("some_command", stdout = "", stderr = "")

# On Windows: stdout and stderr are kept separate
# There is no simple one-liner to capture interleaved output on both platforms.

The input parameter accepts a single character string, not a vector of lines. If you need to send multiple lines to a command’s stdin, write them to a temporary file first and use stdin = "tempfile.txt" instead. Forgetting this limitation and passing a character vector to input silently concatenates the elements into one string with no line breaks, which produces different input than intended. Always use writeLines() to a temp file when the input spans multiple logical lines, and clean up the temporary file afterwards with unlink().

Using input for multi-line strings

# input expects a single string
system2("cat", input = "hello world")

# For multiple lines, write to a temp file
writeLines(c("line1", "line2"), "temp.txt")
system2("cat", stdin = "temp.txt", stdout = "")
# [1] "line1" "line2"
unlink("temp.txt")
  • system(), older interface with messier semantics
  • shell() — Windows-specific command execution

See also

  • cat, concatenate and print objects
  • readline, read a line from the terminal
  • print, print R objects