Home / Alt manpages / perlfaq8(1)

  • perlfaq8(1)
  • User command
  • linux

Run External Commands Safely from Perl on Linux

You will finish with a small set of Perl patterns for running a Linux command, collecting its output, replacing the current process, or launching work in the background. The examples also show how to avoid handing untrusted arguments to a shell and how to inspect a failed child process.

Allow about 20 minutes. You need Perl and a shell on a Linux system. This guide uses Perl 5.38.2 from the Ubuntu perl and perl-doc packages installed here. The examples run ordinary commands as your current user. They do not need sudo.

1. Check the Perl you will run

Start by confirming the interpreter and the FAQ version installed on the machine. This is a read-only checkpoint:

$ perl -e 'printf "%s\n", $^V'
v5.38.2
$ dpkg-query -W -f='${Package} ${Version}\n' perl perl-doc
perl 5.38.2-3.2ubuntu0.6
perl-doc 5.38.2-3.2ubuntu0.6

The perlfaq8 document itself reports version 5.20210520. That is the version of this FAQ text, not the Perl interpreter. The operating-system value in a program is $^O; it identifies the platform Perl was built for, not its release number:

$ perl -e 'print "$^O\n"'
linux

Checkpoint: if these commands select a different Perl installation from the one used by your service, stop and fix the interpreter path before comparing results.

2. Use system when you need a command to run

system runs a program and returns status information. It does not return the program's standard output as a string. A simple list-form call keeps the command and its arguments separate:

use strict;
use warnings;

system('/usr/bin/printf', '%s\n', 'child ran') == 0
    or die "printf failed: $?\n";

Save this as run.pl, then execute it:

$ perl run.pl
child ran

The comparison checks that the child exited successfully. If it fails, $? contains encoded wait status, so do not treat every non-zero value as a simple application exit code. For a useful first diagnosis, print both the raw value and the usual exit-code and signal fields:

my $status = system('/usr/bin/false');
die "signal " . ($status & 127) . "\n"
    if $status & 127;
die "exit " . ($status >> 8) . "\n" if $status >> 8;

For a command whose output should stream directly to the terminal or service log, this is normally clearer than collecting it and printing it later. Avoid a void-context backtick expression such as `some command` when you do not want its output. It suggests that output matters and makes it easy to forget the exit status.

3. Capture standard output deliberately

Use backticks, also written as qx//, when the command's standard output is the data your program needs. This example captures one line and removes its trailing newline:

my $output = `/usr/bin/printf '%s\n' 'captured text'`;
chomp $output;
print "value: $output\n";
$ perl capture.pl
value: captured text

Backticks capture standard output only. Standard error still goes to the script's standard error unless you explicitly redirect it. If you need both streams as data, or need separate handling for them, use IPC::Open3 and read its handles carefully. A child that writes enough data to one pipe while the parent waits on another can deadlock, so do not invent a two-way protocol without reading the module documentation.

For fixed arguments and no shell features, prefer a multi-argument pipe when you need streamed output. The FAQ documents the older handle form:

open my $grep, '-|', 'grep', '-i', 'error', 'application.log'
    or die "cannot start grep: $!\n";
while (my $line = <$grep>) {
    print $line;
}
close $grep or die "grep failed: $?\n";

The '-|' form and separate arguments avoid shell expansion. The filename is still an ordinary argument, not a fragment of shell syntax. Check close, because a successful read does not by itself prove that the child finished successfully.

4. Treat shell parsing as an explicit boundary

A string form can invoke a shell when it contains shell metacharacters. That may be useful for a carefully controlled pipeline, but it is dangerous when any part of the string comes from a user, a file, an HTTP request or an environment variable. A value that looks like a filename can become command syntax.

Use list forms for system and exec when the command and its arguments are already separate values:

my $pattern = 'error';
my $file = '/var/log/application.log';

system('grep', '-i', $pattern, $file) == 0
    or die "grep failed: $?\n";

This does not make an unsafe program safe by itself. Validate paths and permitted values for your application, and avoid passing secrets as arguments because other users may see command lines through tools such as ps. If a shell is genuinely required, construct the smallest controlled command and document why it needs one. Do not concatenate untrusted text into it.

Checkpoint: replace the command in your own code with /usr/bin/printf and fixed literal arguments, run it, and confirm that the output is exactly what you expected before restoring the real command.

5. Know that exec replaces your process

exec does not start a child and return. It replaces the current Perl process with the requested program. Code after a successful exec is unreachable, so use it for a wrapper that has finished its setup and should become another program:

exec('/usr/bin/printf', '%s\n', 'Perl became printf')
    or die "exec failed: $!\n";
$ perl replace.pl
Perl became printf

Keep the failure handler. It runs only when the replacement could not be started. If you need Perl to continue after the external command, use system, a pipe, or a child created with fork instead.

6. Background work needs process cleanup

The FAQ describes system("command &") as a Unix option, but it also warns that the background process shares standard input, output and error with the parent. It can also leave a zombie unless the child is reaped. Use this only for a deliberately simple, short-lived task whose inherited filehandles are acceptable:

system('/usr/bin/sh', '-c', '/usr/bin/logger --tag my-job done &') == 0
    or die "could not start background shell: $?\n";

The shell in this example is explicit, and the string is entirely fixed. Do not substitute user input into it. For a real worker, prefer a service manager, a queue or a process-management module. If you use fork directly, arrange a SIGCHLD handler or call waitpid so finished children do not accumulate. Test background changes outside production first; they can affect logging, file locks and shutdown behaviour.

There is no useful undo for an already launched child. Stop it using the application-specific control method, or send a signal only after identifying the correct process. Never copy a broad kill command into a script as a substitute for process ownership.

7. Diagnose the environment without changing it

If a module cannot be found, print Perl's include path rather than guessing where packages were installed:

$ perl -e 'print join("\n", @INC), "\n"'
/etc/perl
/usr/local/lib/x86_64-linux-gnu/perl/5.38.2
...

Your list may differ. PERLLIB, PERL5LIB, the -I option and use lib can add directories. Prefer an explicit, reviewed path in a service rather than inheriting a surprising shell environment. To locate an installed module, use perldoc -l Module::Name if its documentation is present.

When an error message appears, remember that it may come from the shell, the external program or Perl. A misspelled shebang can cause the shell to interpret Perl source as shell commands. Check the first line, run the script explicitly with the intended interpreter, and use use warnings and use strict while investigating.

Done means

  • You confirmed the Perl interpreter and installed perlfaq8 version.
  • You choose system for streamed command execution, backticks or a pipe for captured standard output, and exec only when Perl should be replaced.
  • You use list forms for variable arguments and keep shell parsing out of untrusted data.
  • You check child exit status and understand that $? is encoded wait status.
  • You treat background work as a process-lifecycle problem, including inherited filehandles and child reaping.
  • You can inspect @INC and distinguish Perl errors from shell or child-program errors.