Kernel#exec
exec(command) -> never returns The exec method replaces the current Ruby process with an external program. After calling exec, the Ruby interpreter stops and the new program runs in its place. The process never returns from exec.
How it works
When you call exec, the current process image is replaced with the new program. Unlike system or backticks, exec doesn’t create a subprocess—it becomes the subprocess.
Basic Usage
# Replace with a shell command
exec("ls -la")
# After this, Ruby is gone, ls runs
puts "This never prints"
That is the core idea behind exec: the current Ruby process is replaced, so anything after the call only matters if the replacement fails. It is a very direct tool for process replacement, and that directness is why it shows up in launcher code, wrapper scripts, and other execution paths where the current Ruby program is only a starting point.
Common Examples
Running commands
# Simple command
exec("echo hello")
# With arguments
exec("ruby", "-v")
# Via shell
exec("ls *.rb | head -5")
These command examples show the three common ways exec is used in practice. A simple string is easy to read, separate arguments avoid shell parsing surprises, and a shell command gives you expansions like globbing or pipes when you really want them. The right choice depends on how much control you need over the system call.
Environment variables
# Set environment for the new process
exec({ "HOME" => "/tmp" }, "ls")
Passing an environment hash is a small but useful reminder that exec can shape the new process before it begins. That is handy in scripts that need a temporary configuration, a different home directory, or a controlled runtime environment while still handing execution off completely.
Replace self
# Common pattern for process replacement
exec("ruby", "other_script.rb", *ARGV)
This pattern is common in bootstrap code because it lets one script become another without keeping two processes alive. The shell or launcher does its setup work, then the Ruby process hands control to the real program and disappears. That makes the flow easy to reason about when the goal is pure execution handoff.
Shell Expansion
# Without shell: separate arguments
exec("ls", "-la", "-h")
# With shell: glob expansion, pipes work
exec("ls -la *.rb")
The shell expansion example highlights the tradeoff between precision and convenience. Separate arguments are safer when you want exact control over the command, while a shell string is better when you need features like wildcard expansion or pipelines. Choosing one form deliberately avoids accidental behavior changes.
Practical Examples
Shebang scripts
#!/usr/bin/env ruby
# Common pattern to run another Ruby version
exec("ruby3.0", $0, *ARGV)
Shebang scripts often use exec as a clean handoff from the lightweight wrapper to the real interpreter. That keeps the wrapper short and avoids holding on to an extra Ruby process after the version switch. It is a tidy fit for small launcher files and environment shims.
Wrapper scripts
#!/usr/bin/env ruby
# Load environment, then exec
ENV.each { |k, v| ENV[k] = v } # Ensure env is set
exec("/path/to/real/binary")
Wrapper scripts are where the execution semantics become easiest to see. The Ruby code can prepare the environment, adjust a setting, or do a small amount of work, and then exec replaces the process with the actual binary. The result is still a single process from the operating system’s point of view.
Process interpreter
# Switch to different interpreter
exec("python", "script.py")
exec("node", "app.js")
Using exec as an interpreter switch is a practical way to keep a tiny launcher from lingering after it has done its job. It works especially well in tools that pick the right runtime and then hand off to another language or version without managing a long-lived child process.
Important notes
No Return
# Code after exec never runs (unless exec fails)
exec("false")
puts "Never prints"
# Use system if you need to continue
system("ls")
puts "This prints after ls"
The “no return” behavior is the part that most strongly defines exec. If the process replacement succeeds, the original Ruby code path is gone, which means cleanup logic must happen before the call. This is why exec is best used when the process handoff is the real goal, not just a step along the way.
Error handling
begin
exec("nonexistent_command")
rescue Errno::ENOENT => e
puts "Command not found: #{e}"
end
Error handling is still important even for a method that usually does not return. A failure to launch the new program is exactly the moment when a rescue block can help, because the Ruby process is still intact and can report the problem clearly. That makes missing binaries much easier to diagnose.
Comparison
# exec - replaces current process
exec("ls")
# system - runs in subprocess, returns after
system("ls")
puts "Runs after"
# spawn - runs in subprocess, returns immediately
spawn("ls")
puts "Runs immediately"
The comparison is useful because it places exec beside the other common process APIs. system and spawn are both about running something else, but they keep the Ruby process alive. exec is the one that fully hands over execution, so the surrounding code needs to be written with that in mind.
Exit Status
# The exit status is the program's exit status
# Use at_exit to handle
at_exit { exit! }
exec("ruby -e 'exit 42'")
Exit status matters when a launcher wants to forward the result of the replaced program. In that case, the wrapper should do as little as possible after the handoff and let the new process decide how to finish. That way, the calling shell or parent process sees the exit code from the real command, not from the launcher itself.
The exec method is used when you want to completely replace your Ruby process with another program, such as in wrapper scripts or when switching interpreters.
Practical Guidance
Because exec never returns on success, it fits best in places where the current process is only acting as a launcher. Wrapper scripts, small bootstraps, and process handoff code are all good examples. If the Ruby program still needs to clean up, log, or keep running, system or spawn is usually easier to work with.
It is also worth handling errors near the call site. If the target program cannot be found, exec raises before the process is replaced, so a small rescue block can make the failure mode much clearer.