Subshells and PIDs: Why do $$ and \$\$ sometimes match under nested sh -c? - bash

I know this is an artificially complicated example, but why are both PIDs the same in the first line, while (as expected, to me at least) the two other lines yield different PIDs?
$ sh -c 'sh -c "echo $$ \$\$"'
4500 4500
$ sh -c 'sh -c "echo $$ \$\$"; true'
4596 5060
$ sh -c 'true; sh -c "echo $$ \$\$"'
4728 2868
Thanks!

For me in bash 4.1.5, the output of first line is:
sh -c 'sh -c "echo $$ \$\$"'
4063 4064
as expected - values are different.
Also tested on ash, sh, and zsh.
It must be some tricky optimization.
Update:
in bash 3.2 there is and "ONESHOT" feature:
see comment in the shell.c:1243:
#if defined (ONESHOT)
/* Run one command, given as the argument to the -c option. Tell
parse_and_execute not to fork for a simple command. */

Related

sh -c doesn't recognize heredoc syntax

I think the two commands below should be identical, but given the heredoc, the shell produces an error. Is it possible to pass a heredoc to the -c argument of sh?
heredoc example
/bin/sh -c <<EOF
echo 'hello'
EOF
# ERROR: /bin/sh: -c: option requires an argument
simple string example
/bin/sh -c "echo 'hello'"
# prints hello
The commands are not equivalent.
/bin/sh -c <<EOF
echo 'hello'
EOF
is equivalent to
echo "echo 'hello'" | /bin/sh -c
or, with here-string:
/bin/sh -c <<< "echo 'hello'"
but sh -c requires an argument. It would work with
echo "echo 'hello'" | /bin/sh
I tried to post this as a comment but formatting code doesn't work well in comments. Using the accepted answer by #Benjamin W. as a guide, I was able to get it to work with this snippet
( cat <<EOF
echo 'hello'
EOF
) | /bin/sh
The magic is in how cat handles inputs. From the man page:
If file is a single dash (`-') or absent, cat reads from the standard input.
So cat can redirect stdin to stdout and that can be piped to /bin/sh

Is there any difference between "sh -c 'some comand'" and directly run some command

let's say echo command, we can run that command by two ways:
# by 1
echo 'hello'
# or by 2
sh -c "echo 'hello'"
Is there any difference between the two ways? By the way, I can see the way 2 is very popular in yaml config files.
- name: init-mydb
image: busybox:1.28
command: ['sh', '-c', "sleep 2; done"]
The first way calls an inherited command interpreter, eg from a terminal running /bin/bash ; the second way exec sh (aka Bourne Shell) as the interpreter and instruct him ( -c ) to do something.
sh, ksh, csh, bash are all shell interpreters. They provide some features that are not always compatible between them. So, if you don't know the environment where your program will run, the best is to specify the interpreter you want, which is less error prone.
This is a single command:
foo 1 2 3
So is this
sh -c 'foo 1 2 3'
This is not a single command (but rather a pair of commands separated by a ;)
foo; bar
but this is
sh -c "foo; bar"
This does not specify a command using the name of a executable file
for x in 1 2 3; do echo "$x"; done
but this does
sh -c 'for x in 1 2 3; do echo "$x"; done'
sh -c is basically way to specify an arbitrary shell script as a single argument to a command that can be executed from a file.

Why doesn't nohup sh -c "..." store variable?

Why doesn't "nohup sh -c" stores variable?
$ nohup sh -c "RU=ru" &
[1] 17042
[1]+ Done nohup sh -c "RU=ru"
$ echo ${RU}
$ RU=ru
$ echo ${RU}
ru
How do I make it such that it store the variable value so that I can use in a loop later?
For now, it's not recording the value when I use RU=ru instead my bash loop, i.e.
nohup sh -c "RU=ru; for file in *.${RU}-en.${RU}; do some_command ${RU}.txt; done" &
It doesn't work within the sh -c "..." too, cat nohup.out outputs nothing for the echo:
$ nohup sh -c "FR=fr; echo ${FR}" &
[1] 17999
[1]+ Done nohup sh -c "FR=fr; echo ${FR}"
$ cat nohup.out
Why doesn't "nohup sh -c" stores variable?
Environment variables only live in a process, and potentially children of a process. When you run sh -c you are creating a new child process. Any environment variables in that child process cannot "move up" to the parent process. That's simply how shells and environment variables work.
It doesn't work within the sh -c "..." too, cat nohup.out outputs
nothing for the echo"
The reason for this is that you are using double quotes. When you use double quotes, the shell does variable expansion before running the command. If you switch to single quotes, the variable won't be expanded until the shell command runs:
nohup sh -c 'FR=fr; echo ${FR}'

compound command with bash -c in tmux vs screen

I need to run a command with bash -c within a tmux session, from within a shell script. In contrast to screen, tmux seems to require to quote the entire command, which leads to problems as bash -c also requires quoting for correct functioning with more complex command strings.
In the following I'm trying to demonstrate the behavior with a minimal example. What I'm trying to achieve involves more complex commands than ls of course. Also for my purpose it is necessary to expand the CMD variable, as it is built in the script before.
A minimal script for screen:
#!/bin/bash
set -x
CMD="ls -l; sleep 5"
screen -d -m bash -c "$CMD"
When executing this script you get (stdout due to -x)
+ CMD='ls -l; sleep 5'
+ screen -d -m bash -c 'ls -l; sleep 5'
The sleep command is for having time to attach to the screen session and see what happens. When attaching to the screen session after executing the above script one sees that the output of the ls command is in long list format, i.e. the command is executed properly.
In tmux it seems one has to quote the command to get it executed in the new session. I use the following script:
#!/bin/bash
set -x
CMD="ls -l; sleep 5"
tmux new -d "bash -c $CMD"
The stdout is
+ CMD='ls -l; sleep 5'
+ tmux new -d 'bash -c ls -l; sleep 5'
As one can see, the cmd sequence for bash -c is not quoted properly anymore. When attaching to the created tmux session one can see, that this results in ls being executed without the long list option recognized.
What can I do to get the proper quoting (i.e. single quotes around the expanded string) for the $CMD string passed to bash -c?
Update
Escaping, as Eric Renouf suggested, with \"$CMD\" produces
tmux new -d 'bash -c "ls -l; sleep 5"'
and escaping with '$CMD' produces
tmux new -d 'bash -c '\''ls -l; sleep 5'\'''
Both works for the provided minimal example, but is still not exactly what screen produces and does not work in my case.
Here are the exact call I'm making (see here for all the gory details):
$SCREEN -S "scalaris_$NODE_NAME" -d -m bash -x -f +B -c "$START_CMD; sleep 365d"
which produces (output of -x)
/usr/bin/screen -S scalaris_first#pvs-pc07.zib.de -d -m bash -x -f +B -c '"/usr/bin/erl" -setcookie "chocolate chip cookie" -pa /home/jvf/code/scalaris/contrib/yaws/ebin -pa /home/jvf/code/scalaris/contrib/log4erl/ebin -pa /home/jvf/code/scalaris/ebin -sasl sasl_error_logger false -yaws embedded true -scalaris log_path "\"/home/jvf/code/scalaris/log/first#pvs-pc07.zib.de\"" -scalaris docroot "\"/home/jvf/code/scalaris/docroot\"" -scalaris config "\"/home/jvf/code/scalaris/bin/scalaris.cfg\"" -scalaris local_config "\"/home/jvf/code/scalaris/bin/scalaris.local.cfg\"" -connect_all false -hidden -name first#pvs-pc07.zib.de -scalaris start_type first -scalaris port 14195 -scalaris yaws_port 8000 -scalaris join_at_list '\''[0]'\'' -scalaris start_mgmt_server true -scalaris nodes_per_vm "1" -s scalaris +sbt db +swt low +sbwt short'
I think the solutions suggested so far do not work because of the use of double quotes within the command, but I'm not 100% positive about that. How can I reproduce the exact quoting screen produces (single quotes around the complete command passed to bash -c) with tmux?
I'm trusting that you need to do this for tmux, but to get the extra quotes you want, you could just escape them in the outer quote like:
tmux new -d "bash -c \"$CMD\""
or you could put those in single quotes in most cases like
tmux new -d "bash -c '$CMD'"
since the $CMD will be expanded by the outer quotes

Why when I run sh -c [script], it won't accept any positional parameters?

I am confused; I run a script, with sh -c , but if I want to pass a parameter, it will be ignored.
example:
# script.sh
param=$1
echo "parameter is: " $param
If I run it as
sh -c ./script.sh hello
I get nothing in the output
Why is this happening? How can I avoid this?
This will work for you:
sh -c "./script.sh hello"
If you run it that way:
sh -c ./script.sh hello
than hello became sh's second parameter and ./script.sh is run with none parameters.
The -c switch accepts a single argument. The shell will be doing the parsing itself. E.g.
sh -c './script.sh hello'

Resources