Kernel#spawn
spawn([env, ] command_line, options = {}) → Integer What spawn does
Kernel#spawn starts an external command in a new child process and returns immediately with that child’s process ID (an Integer). It does not wait for the child to finish, so it is the right primitive when you need a non-blocking subprocess or want to fan out work in parallel.
The same method is exposed at the module level as Process.spawn. Both forms share one implementation, so anything true for spawn(...) is also true for Process.spawn(...). Because Kernel is mixed into Object, every object responds to spawn at the instance level too.
Signatures
spawn([env, ] command_line, options = {}) # → Integer
spawn([env, ] exe_path, options = {}) # → Integer
spawn([env, ] exe_path, *args, options = {}) # → Integer
The leading env is an optional Hash. The trailing options is an optional Hash of execution options. Everything between them is the command: either a single string that goes through a shell, or a path plus zero or more string arguments that go straight to execve without a shell in the way.
Return value
spawn returns the child’s process ID (an Integer) on success. The pid is useful for two things: passing it to Process.wait so the child does not become a zombie, and passing it to Process.kill if you need to send the child a signal.
spawn does not tell you whether the child succeeded. The pid says nothing about exit status. To get the status, call Process.wait(pid).exitstatus or use Process.detach and read value.exitstatus.
pid = spawn('false') # => 27184 (some Integer)
status = Process.wait2(pid) # => [27184, #<Process::Status: exit 1>]
status.last.exitstatus # => 1
String form vs multi-arg form
A single string argument is sent to a shell (/bin/sh -c on Unix, ENV['COMSPEC'] on Windows) when it begins with a shell reserved word, contains shell metacharacters like * or |, or otherwise looks like shell syntax. Otherwise Ruby treats the string as a path and runs it directly, without a shell.
# Shell form — runs /bin/sh -c 'echo $USER'
spawn('echo $USER') # => Integer (child's pid; non-blocking)
# Exec form — runs /bin/echo with the single argument "$USER"
spawn('echo', '$USER')
Prefer the multi-arg form any time part of the command comes from user input. spawn("rm #{filename}") is a command-injection bug waiting to happen; spawn('rm', filename) is safe.
The env hash
The optional leading Hash sets environment variables for the child. A string value adds or overrides a variable; a nil value deletes it from the child’s environment. The parent’s ENV is never touched.
spawn({ 'GREETING' => 'hi', 'PATH' => nil }, 'env')
# => child runs with GREETING=hi and no PATH
:unsetenv_others => true starts the child with an empty environment before applying env. Without it, the child inherits the parent’s ENV and gets the env keys layered on top.
Execution options
The trailing options hash controls how the child starts:
spawn('build.sh', chdir: '/srv/app', umask: 0o077)
| Key | Purpose |
|---|---|
:chdir | Set the child’s working directory. The parent’s cwd is unchanged. |
:umask | File-creation mask for the new process. |
:unsetenv_others | Start the child with an empty ENV, then apply env. |
:close_others | Close inherited fds ≥ 3 that are not explicitly redirected. |
:pgroup | true makes the child a process-group leader; an Integer joins an existing group. Use :new_pgroup on Windows. |
:rlimit_<name> | Per-resource limit (e.g. :rlimit_cpu, :rlimit_nofile). Pass an Integer or [cur, max]. |
Redirecting file descriptors
:in, :out, and :err are shorthand for redirecting fds 0, 1, and 2. The value can be a path string, a [path, mode] or [path, mode, perm] array, an existing fd, [:child, fd] to wire two children together, or :close to drop the fd entirely.
spawn('long-job.sh',
out: '/var/log/job.out',
err: '/var/log/job.err',
in: :close)
For a one-shot pipe back into Ruby, IO.pipe is the standard trick. IO.pipe returns a pair of connected fds, r for reading and w for writing. You hand the write end to the child as :out, then close it in the parent after spawn so the read end sees EOF when the child exits. Forgetting w.close is the classic version of this bug: the child closes its copy on exit, but the parent’s copy stays open, and r.read blocks forever waiting for an EOF that never arrives:
r, w = IO.pipe
pid = spawn('ls', out: w)
w.close
output = r.read # => "file1\nfile2\n..."
r.close
Process.wait(pid)
Reaping children
A finished child whose parent has not called Process.wait or Process.detach lingers as a zombie until the parent exits or reaps it. In a long-running process that calls spawn many times, the zombie count grows until the OS pid table fills up.
Process.detach(pid) returns a Thread that reaps the child automatically. Join the thread and call .value to get a Process::Status:
thread = Process.detach(spawn('sleep', '2'))
status = thread.join.value # => #<Process::Status: exit 0>
status.exitstatus # => 0
status.success? # => true
Process.wait is the synchronous version: it blocks until the child exits and returns its pid (or a [pid, status] pair with Process.wait2).
The natural pattern when you have many children is to track the pids and wait on each one. Skipping the matching wait (or detach) is the most common way to leak zombies in a long-running process:
pids = %w[a.txt b.txt c.txt].map { |f| spawn('process.sh', f) }
pids.each { |pid| _, status = Process.wait2(pid) }
# status.exitstatus is 0 on a clean run, non-zero otherwise
spawn vs system, exec, and backticks
| Method | Returns | Waits? | Shell? | Use when |
|---|---|---|---|---|
`cmd` | captured stdout | yes | yes | You want output as a string. |
system(cmd) | true / false / nil | yes | yes | You want pass/fail and to keep terminal output. |
exec(cmd) | never returns | replaces | yes/no | You want the Ruby process to become the child. |
spawn(cmd, ...) | child pid | no | yes/no | You want async work or fine-grained control over env and fds. |
Common mistakes
- Forgetting to reap. Every
spawnneeds a matchingProcess.waitorProcess.detach, or the child sticks around as a zombie. - Reading the pid for status. The pid says nothing about success. Use
Process.waitorProcess.detachto get aProcess::Status. - Expecting
envto mutate the parent.spawn({'FOO' => 'bar'}, ...)setsFOOonly in the child. - Mixing shell and exec forms.
spawn('ls -l')runs through the shell;spawn('ls', '-l')does not. Pick the right one for the input you have. - Using
:pgroup => trueon Windows. It is a no-op there; use:new_pgroup => trueinstead. - Asserting side effects right after
spawn. The child has not necessarily run yet. Tests that check files written by a spawned child need a real wait.
See also
Kernel#execreplaces the current process with a child.Kernel#systemblocks and returns pass/fail.Kernel#forksplits the process without running a new program.