Difference in executing a command line in a different ways

Just so I understand the difference between executing the command line in Bash Terminal macOS Mojave.

Let's take for example the installation of a package named: stackexchange.pkg

What are the differences between the two command line below? Is the result the same? Pros and Cons?

/usr/sbin/installer -pkg path_to_stackexchange.pkg -target /

sh -c /usr/sbin/installer -pkg 'path_to_stackexchange.pkg' -target '/'

Solution 1:

When you use sh with its -c option, the argument after -c is taken as the command string to run. Thus, when you run

sh -c /usr/sbin/installer -pkg 'path_to_stackexchange.pkg' -target '/'

sh will run the command "/usr/sbin/installer". It will not pass on the rest of its arguments, because they're not part of the command string. If you want to include arguments, you'd need to do something like this:

sh -c "/usr/sbin/installer -pkg 'path_to_stackexchange.pkg' -target '/'"

The double-quotes make that a single long argument to sh, so it'll execute the whole thing and pass -pkg etc as arguments to the installer command.

(BTW, in the shell, quotes don't nest. Nesting quoted strings inside quoted strings -- like the single-quoted arguments inside that double-quoted argument -- isn't really a thing. In this case it does work, because the inside a double-quoted string, single-quotes are just regular characters. In more complicated situations, things can get weird and confusing.)

So what's happening to those other arguments after the command? Well, the command string (the argument after -c) functions a little like a miniature shell script, and those are passed to that mini-script as arguments. But they're not passed to the commands in that mini-script, unless the mini-script explicitly passes them on:

sh -c '/usr/sbin/installer "$@"' installer-thingie -pkg 'path_to_stackexchange.pkg' -target '/'

Here, inside the mini-script, the "$@" expands to the arguments to the mini-script (i.e. -pkg, path_to_stackexchange.pkg, etc.), thus passing them on to the installer command.

But what's that installer-thingie argument? That's the name the mini-script is run under, so it'll show up as $0 inside the mini-script. Basically, it functions as a placeholder, and you could put pretty much anything there. "sh" is a common choice.

As for the pros and cons of using sh -c to run commands: unless there's some reason you really need to run another shell, just run the command directly. Running it with sh -c is more complicated and confusing, and (as you've experienced) easy to get wrong.

As @DavidAnderson pointed out, using sh -c is also less efficient, because it creates an additional shell process. If you look at the process tree for running installer directly (and assuming your interactive shell is zsh), it looks like this:

zsh───installer

But with sh -c, it's like this:

zsh───sh───installer

...it ran an sh process under your interactive shell, and then that ran installer as a subprocess.

On the other hand, if you prefer confusing complexity and inefficiency, there's no reason to stop at one unnecessary shell; why not add another sh -c to run the sh -c command?

sh -c "sh -c \"/usr/sbin/installer -pkg 'path_to_stackexchange.pkg' -target '/'\""

or even

sh -c "sh -c \"sh -c \\\"/usr/sbin/installer -pkg 'path_to_stackexchange.pkg' -target '/'\\\"\""

(did I mention that nesting quotes can get complicated?)