Different output from $? running under bash -c - bash

Why do the following commands produce different output?
false ; echo $?
output: 1
bash -c "false ; echo $?"
output: 0
Both echo $SHELL and bash -c "echo $SHELL" return /bin/bash so I am not sure why the commands would differ in output.

Since the argument to bash -c is in double quotes, the original shell performs variable substitution in it. So you're actually executing
bash -c "false; echo 0"
Change it to single quotes and you'll get the output you expect.
bash -c 'false; echo $?'
See Difference between single and double quotes in Bash

Related

How to echo script invocation without variable expansion of its args

From within a bash script, I'd like to echo script invocation without expanding variables passed as arguments.
Echoing script invocation with expanded variables can be achieved with
echo "${BASH_SOURCE[0]} ${*}"
Echoing (the script's, or any other comand's) history using
echo "$(tail -n 1 ~/.bash_history)"
shows script invocation without variable expansions, as desired, however not for the running script (only for scripts completed).
How to echo script invocation without variable expansion of its arguments from within the running script?
If you can execute your script with bash -c script args, what you want is doable with the BASH_EXECUTION_STRING variable:
$ bash -c 'echo "$BASH_EXECUTION_STRING"'
echo "$BASH_EXECUTION_STRING"
This output is not easy to understand but you can see that the echo command, when executed, prints the unexpanded command. This is because the value of the BASH_EXECUTION_STRING variable is the literal: echo "$BASH_EXECUTION_STRING".
So, if your script is, for instance:
#!/usr/bin/env bash
script="$0"
cmd="$1"
shift
echo "script name: $script"
echo "command line: $cmd"
echo "parameter: $1"
you can execute it as:
$ a=42 bash -c './foo.sh "$BASH_EXECUTION_STRING" "$a"'
script name: ./foo.sh
command line: ./foo.sh "$BASH_EXECUTION_STRING" "$a"
parameter: 42

Why does `bash -c '...'` terminate early on some (but not all) errors?

What is going on here?
The following works as expected:
$ bash -c 'false; echo $?'
1
But trying to kill a nonexistent process with pkill makes bash terminate before the script is done.
$ bash -c 'pkill -f xyz_non_existent_process_xyz; echo $?'
[1] 21078 terminated bash -c 'pkill -f xyz_non_existent_process_xyz; echo $?'
If I run this command in the terminal, I see that pkill returns an error code of 1, just like the false command did:
$ pkill -f xyz_non_existing_process_xyz; echo $?
1
So the two commands are returning the same status code... so what's the difference!?
I tried wrapping the command in a number of ways. For example:
$ bash -c '(pkill -f xyz_non_existent_process_xyz || true); echo $?'
[1] 21309 terminated bash -c '(pkill -f xyz_non_existent_process_xyz || true); echo $?'
So it seems like whatever is causing bash to terminate early, it's not the exit status of any of the commands??
What's going on here?
It's simple: pkill find the bash command and stops its execution. Change the search pattern and it will function:
bash -c 'pkill -f "xyz_n""on_existent_process_xyz"; echo $?'
It's a little bit tricky: "xyz_n""on_existent_process_xyz" is the same as xyz_non_existent_process_xyz

Using /bin/bash -l -c with concatenated commands does not pass environment variables

I'm trying to execute a series of bash commands using /bin/bash -l -c as follows:
/bin/bash -l -c "cmd1 && cmd2 && cmd3..."
What I notice is that if cmd1 happens to export an environment variable, it is not seen by cmd2 and so on. If I run the same concatenated commands without the /bin/bash -l -c option, it just runs fine.
For example, this does not print the value of MYVAR:
$/bin/bash -l -c "export MYVAR="myvar" && echo $MYVAR"
whereas, the following works as expected
$export MYVAR="myvar" && echo $MYVAR
myvar
Can't find why using /bin/bash -l -c won't work here? Or are there any suggestion how to make this work with using /bin/bash -l -c?
variables are exported for child processes, parent process environment can't be changed by child process so consecutive commands (forking sub processes can't see variables exported from preceeding command), some commands are particular becuase don't fork new process but are run inside current shell process they are builtin commands only builtin commands can change current process environment export is a builtin, for more information about builtins man bash /^shell builtin.
Otherwise expansion is done during command parsing:
/bin/bash -l -c "export MYVAR="myvar" && echo $MYVAR"
is one command (note the nested quotes are not doing what you expect because are closing and opening quoted string, myvar is not quoted and may be split). So $MYVAR is expanded to current value before assigned to myvar.
export MYVAR="myvar" && echo $MYVAR
are two commands and $MYVAR because && is bash syntax (not literal like "&&"), is expanded after being assigned.
so that $ have a literal meaning (not expanded) use single quotes or escape with backslash.
/bin/bash -l -c 'export MYVAR="myvar" && echo "$MYVAR"'
or
/bin/bash -l -c "export MYVAR=\"myvar\" && echo \"\$MYVAR\""

How to save PID to variable in bash -c?

I'm having trouble with saving the PID into a variable when I'm using bash -c. For example:
bash -c "PID=$$; echo $PID"
In this case the output is empty. How to save the child PID (the PID of the command inside the double quotes) now in the variable PID
just use simple quotes or you expression is evaluated inside your current command line (too soon) and not in the child bash command, and PID isn't defined yet at this moment and you're actually passing
bash -c "PID=4353; echo"
(where 4353 is the pid of the current bash process)
Someone noted that it's not clear if you want to pass parent pid or child pid
to pass parent pid, fix it like this (only the part within double quotes is evaluated before calling bash child process):
bash -c "PID=$$; "'echo $PID'
to pass child pid, fix it like this (nothing is evaluated in the current shell, same trick used for awk scripts):
bash -c 'PID=$$; echo $PID'
set -x is useful for debugging and will show the actual command you end up running prefixed with +:
$ set -x
$ bash -c "PID=$$; echo $PID"
+ bash -c 'PID=1900; echo '
And indeed, in that command you would expect empty output. This happens because $$ and $PID are substituted before bash is called.
To avoid this, you can single quote the string or escape the "$"s:
$ set -x
$ bash -c 'PID=$$; echo $PID'
+ bash -c 'PID=$$; echo $PID'
1925
$ bash -c "PID=\$\$; echo \$PID"
+ bash -c 'PID=$$; echo $PID'
1929

Why does bash -c "false; echo $?" print 0?

I'm building a script that tries to run some commands on a server (over SSH), and writes on the screen whether they were successful.
I noticed a strange behaviour for $?, namely not being 0 when the previous command failed.
Initially, I had:
ssh <user>#<server> <<EOF
false
if [ $? -ne 0 ]; then
echo "It failed"
else
echo "It worked"
fi
EOF
If I copy and paste the script inside <<EOF and EOF, it prints It failed. If I run it with the ssh part, it prints It worked. To simplify, I then tried:
ssh <user>#<server> <<EOF
false
echo $?
EOF
Same thing happened. If I copy-paste or type the commands inside, it prints 1, but if I run all of it (including the ssh), it prints 0.
The same error happens if I directly use bash this way
bash <<EOF
false
echo $?
EOF
or
bash -c "false; echo $?"
Why does this happen? How can I check if the previous command failed in this context?
This is due to variable expansion. When you write bash -c "false; echo $?" the variable is expanded before the commands are ran. So your command is exactly like bash -c "false; echo 0;" if your previous command was successful.
To have the right result try bash -c 'false; echo $?'. This prevents variable expansion, it will be expanded when interpreted.
For the here document version do:
bash << 'EOF'
false
echo $?
'EOF'
In this case you need to quote the delimiter of the here document. But beware that the syntax you must use is the syntax for the shell you use to type the command. In the example, I was in tcsh , and it requires to use the exact same opening and closing delimiter. Under bash, the closing delimiter must be the opening one after quote removal.

Resources