1

I'm trying to automate creating certificates via a Perl script.

The command I want to run is:

easyrsa build-client-full $clientname nopass

The way I thought it should be done in Perl is:

   my $arguments = ("build-client-full $clientname nopass");       
   my $cmd = "$easyrsa_path/easyrsa"." "."$arguments";
   system("bash", $cmd);

However, this yields

"file not found"

on execution. I triple checked that the path is correct.

If I try it like this:

   my @arguments = ("bash", $easyrsa_path,"build-client-full $clientname nopass");
   system(@arguments);

Bash returns

"Unknown command 'build-client-full test nopass'. Run without commands for usage help."

Fang
  • 2,199
  • 4
  • 23
  • 44
  • 1
    Try spliting "all in one" argument => `@arguments = ('bash', $easyrsa_path,'build-client-full', $clientname, 'nopass');` – AnFi May 27 '18 at 14:24
  • @Anfi That works. Thank you. – Fang May 27 '18 at 14:33
  • 1
    1) If you were ever to actually invoke the shell you need `/path/shell-name -c` (2) Since `$clientname` is a variable that gets expanded you may need to ensure that everything in it gets quoted properly (like spaces). A good module for that is `String::ShellQuote`. But if you use `system LIST` form, as recommended, that is not a problem since the whole`$clientname` is passed as one argument, no matter what it expands to – zdim May 27 '18 at 20:02

2 Answers2

3

Background

When you use system(LIST) where LIST has more than one element, Perl will not call the shell, and instead directly invoke the program given by the first element in the LIST, and use the rest of the list as command line arguments to be passed verbatim, with no interpolation by the shell, including no splitting arguments on whitespace.

So in your first example, Perl is running the command bash and passing the string "$easyrsa_path/easyrsa build-client-full $clientname nopass", literally as one big long argument, and in your second example, it's running the command bash and passing the two arguments $easyrsa_path and "build-client-full $clientname nopass". However, I assume that easyrsa needs the three arguments as separate strings in its argument list, which the shell would normally split, but since both of your calls to system aren't using the shell, it's not working.

system (and exec) have four ways of interpreting their arguments, as per the documentation:

  1. If you pass a single string (including a LIST with only one element) that does not contain any shell metacharacters, it is split into words and passed directly to execvp(3) (meaning it bypasses the shell). Warning: This invocation is easily confused with the following - a single metacharacter will cause the shell to be invoked, which can be dangerous especially when unchecked variables are interpolated into the command string.

  2. If you pass a single string (including a LIST with only one element) that does contain shell metacharacters, the entire argument is passed to the system's command shell for parsing. Normally, that's /bin/sh -c on Unix platforms, but the idea of the "default shell" is problematic, and there is certainly no guarantee that it'll be bash (though it could be).

    Warning: In this invocation of system, you have the full power of the shell, which also means you're responsible for correctly quoting and escaping any shell metacharacters and/or whitespace. I recommend you only use this form if you explicitly want the power of the shell, and otherwise, it's usually best to use one of the following two.

  3. If there is more than one argument in LIST, this calls execvp(3) with the arguments in LIST, meaning the shell is avoided. (See below for caveats on Windows.)

  4. The form system {EXPR} LIST always runs the program named by EXPR and avoids the shell, no matter what's in LIST. (See below for caveats on Windows.)

The latter two are desirable if you want to pass special characters that the shell would normally interpret, and I'd actually always recommend doing this, since blindly passing user input into system can open up a security hole - I wrote a longer article about that over on PerlMonks.

Solutions

@Borodin and @AnFi have already pointed out: If you simply split up the elements of the LIST properly, it should work - it doesn't look like you need any features of bash or any shell here. And don't forget to check for errors!

system("$easyrsa_path/easyrsa","build-client-full",$clientname,"nopass") == 0
    or warn "system failed: \$? = $?";

Note that there are good modules that provide alternatives to system and qx, my go-to module is usually IPC::Run3. These modules are very helpful if you want to capture output from the external command. In this case, IPC::System::Simple might be easier since it provides a drop-in replacement for system with better error handling, as well as systemx which always avoids the shell. (That module is what autodie uses when you say use autodie ':all';.)

use IPC::System::Simple qw/systemx/;
systemx("$easyrsa_path/easyrsa","build-client-full",$clientname,"nopass");

Note that if you really wanted to call bash, you'd need to add the -c option and say system("bash","-c","--","$easyrsa_path/easyrsa build-client-full $clientname nopass"). But as I a said above, I strongly recommend against this, since if $easyrsa_path or $clientname contain any shell metacharacters or malicious content, you may end up having a huge problem.

Windows

Windows is more complicated than the above. The documentation says that the only "reliable" way to avoid calling the shell there is the system PROGRAM LIST form, but on Windows, command line arguments are not passed as a list, but a single big string, and it's up to the called command, not the shell, to interpret that string, and different commands may do that differently - see also. (I have heard good things about Win32::ShellQuote, though.)

Plus, there's the special system(1, @args) form documented in perlport.

haukex
  • 2,973
  • 9
  • 21
  • Yes, `system(1, @args)` is very poorly documented. Platform-specific functionality shouldn't be exiled to `perlport`. – Borodin May 27 '18 at 19:49
-1

If you pass multiple parameters to system then each one forms a separate parameter to the command line. So it is as though you had entered

easyrsa "build-client-full test nopass"

and you correctly get the error

Unknown command 'build-client-full test nopass'

You also don't need to add bash: perl will run the shell for you if necessary

You can either pass the whole command to system

system($cmd)

and perl will pass it to the shell to be processed as if you'd entered it at the command prompt. Or you can split the parameters properly

system("$easyrsa_path/easyrsa", "build-client-full", $clientname, "nopass")

which will make perl call easyrsa directly unless the command contains things that need the shell to process, like output redirection

zdim
  • 64,580
  • 5
  • 52
  • 81
Borodin
  • 126,100
  • 9
  • 70
  • 144
  • 1
    *"`system(...)` ... will make perl call `easyrsa` directly unless the command contains things that need the shell to process, like output redirection"* - Sorry, that's also incorrect, `system(LIST)` never calls the shell when `LIST` has more than one element. – haukex May 27 '18 at 15:58
  • (At least on *NIX / POSIX systems, Windows is more complicated.) – haukex May 27 '18 at 16:08
  • @haukex: If you want to help then I think it would be best if you wrote an answer of your own rather than sniping at other people's. I removed the superfluous `bash` because it was irrelevant to my point—it just introduces a second bash process—and the `Unknown command 'build-client-full test nopass'` error means that `build-client-full test nopass` reached `easyra`, so I imagine that it didn't come from the stated call. Please give citations for your second point. – Borodin May 27 '18 at 17:03
  • Regarding my first comment, I see now what you were talking about, I was mistaken, apologies and comment withdrawn. I don't think "sniping" is a fair characterization; I liked the first version of your comment better I've written a new answer though. That will hopefully answer your question about my comment reading `system(LIST)` (plus, just try running `perl -e 'system("echo","foo",">bar")==0 or die $?'`, with an `strace -f` to see it in action). – haukex May 27 '18 at 19:17
  • @haukex: *"I don't think "sniping" is a fair characterization"* I just don't think it's useful to pick holes in someone-else's answer without offering a correction. Thank you for elaborating. – Borodin May 27 '18 at 19:42
  • @haukex: *"just try running ... "* I don't have a command console near me, but I believe you. Most of my work is in Windows; what is the behaviour under Windows 10? – Borodin May 27 '18 at 19:47
  • Fair enough - I usually take any criticism as a chance to double-check myself, but I certainly didn't help my case by making a mistake with my first comment. Anyway, the output under Linux is `foo >bar`, and on Win7 it actually does create the file - not surprising though, as per the "Windows" section of [my answer](https://stackoverflow.com/a/50555635/9300627) and the links therein. However `perl -e "system(qw/perl -MData::Dumper -e print(Dumper(\@ARGV)) foo >bar/)==0 or die $?"` prints `$VAR1 = [ 'foo', '>bar' ];`. – haukex May 27 '18 at 20:02
  • It just downed on my that you never approved of my edit (I did leave a comment), and that it appears that you may have actually disliked mine touching your post. Apologies if that is so, I only tried to act constructively. So I'm rolling away my changes. – zdim Jun 24 '18 at 00:12