> Date: Thu, 4 Dec 2025 16:52:12 -0500
> From: Jan Schaumann <[email protected]>
> 
> How does '/bin/sh -c' with multiple args actually
> work?
> 
> The manual page says
> 
> sh -c command_string [command_name [argument ...]]
> 
> is valid, and should behave as such:
> 
> "Read commands from the command_string operand instead
> of, or in addition to, from the standard input.
> Special parameter 0 will be set from the command_name
> operand if given, and the positional parameters (1, 2,
> etc.)  set from the remaining argument operands, if
> any."

The important point is that command_string is interpreted as a shell
script, not an arbitrary path to a program to execute.

So try this:

$ sh -c 'echo 0=$0 1=$1 2=$2' foo bar baz quux

Exercise: Without running it, what is it going to print?

> So I would expect
> 
> sh -c /bin/sleep whatever 30
> 
> to lead to "/bin/sleep 30" being invoked with an argv0
> set to "whatever".
> 
> However:
> 
> $ sh -c /bin/sleep whatever 30
> usage: sleep seconds

ktrace it!

$ ktrace -t ac /bin/sh -c sleep whatever 30
usage: sleep seconds
$ kdump -t ac | grep -e ARG -e 'CALL.*execve'  
  1308   1308 ktrace   CALL  
execve(0x7f7ffff10d7c,0x7f7ffff10750,0x7f7ffff10780) 
  1308   1308 ktrace   ARG   "/bin/sh"
  1308   1308 ktrace   ARG   "-c"
  1308   1308 ktrace   ARG   "sleep"
  1308   1308 ktrace   ARG   "whatever"
  1308   1308 ktrace   ARG   "30"
  1308   1308 sh       CALL  execve(0x7e2e838,0x7e2e730,0x7e2e740)
  1308   1308 sh       ARG   "sleep"
$ 

(The ARG lines represent argv[0], argv[1], &c.)

> Now I know that I can do
> 
> $ sh -c "/bin/sleep 30"
> 
> but the manual page suggests I should be able to set
> argv0 of the command as described above, but I don't
> see a way of actually doing that.

I don't think sh has a way to do that.  It only has a way to set $0 in
a shell script you run.

There is only one call to execve in /bin/sh (under two #if branches):

226#ifdef SYSV
227     do {
228             execve(cmd, argv, envp);
229     } while (errno == EINTR);
230#else
231     execve(cmd, argv, envp);
232#endif

https://nxr.netbsd.org/xref/src/bin/sh/exec.c?r=1.59#226

The surrounding function tryexec has two callers:

1. When you write a path with a slash as a command name:

133     if (strchr(argv[0], '/') != NULL) {
134             tryexec(argv[0], argv, envp, vforked);

https://nxr.netbsd.org/xref/src/bin/sh/exec.c?r=1.59#133

2. When you don't write any slash in the command name:

140             while ((cmdname = padvance(&path, argv[0], 1)) != NULL) {
141                     if (--idx < 0 && pathopt == NULL) {
142                             /*
143                              * tryexec() does not return if it works.
144                              */
145                             tryexec(cmdname, argv, envp, vforked);

https://nxr.netbsd.org/xref/src/bin/sh/exec.c?r=1.59#140

Both of these paths pass the command name as written in the shell
script verbatim, so it has to be the same path by which the shell
script found the command.

(There's also a call to execl, but it is only for /bin/pwd:
https://nxr.netbsd.org/xref/src/bin/sh/cd.c?r=1.56#522)

Reply via email to