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 vectorFALSE: discard completelyTRUE: 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 stdinTRUE: 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 returningFALSE: 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:
| stdout | wait | Return value |
|---|---|---|
"" | TRUE | Character vector of stdout lines (or exit status if no output) |
FALSE | TRUE | Exit status integer |
| any | FALSE | Process 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")
Related functions
system(), older interface with messier semanticsshell()— Windows-specific command execution