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

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

Related

Different output from $? running under bash -c

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

bash get exitcode of su script execution

I have a shell script when need to run as a particular user. So I call that script as below,
su - testuser -c "/root/check_package.sh | tee -a /var/log/check_package.log"
So after this when I check the last execution exitcode it returns always 0 only even if that script fails.
I tried something below also which didn't help,
su - testuser -c "/root/check_package.sh | tee -a /var/log/check_package.log && echo $? || echo $?"
Is there way to get the exitcode of command whatever running through su.
The problem here is not su, but tee: By default, the shell exits with the exit status of the last pipeline component; in your code, that component is not check_package.sh, but instead is tee.
If your /bin/sh is provided by bash (as opposed to ash, dash, or another POSIX-baseline shell), use set -o pipefail to cause the entirely pipeline to fail if any component of it does:
su - testuser -c "set -o pipefail; /root/check_package.sh | tee -a /var/log/check_package.log"
Alternately, you can do the tee out-of-band with redirection to a process substitution (though this requires your current user to have permission to write to check_package.log):
su - testuser -c "/root/check_package.sh" > >(tee -a /var/log/check_package.log
Both su and sudo exit with the exit status of the command they execute (if authentication succeeded):
$ sudo false; echo $?
1
$ su -c false; echo $?
1
Your problem is that the command pipeline that su runs is a pipeline. The exit status of your pipeline is that of the tee command (which succeeds), but what you really want is that of the first command in the pipeline.
If your shell is bash, you have a couple of options:
set -o pipefail before your pipeline, which will make it return the rightmost failure value of all the commands if any of them fail
Examine the specific member of the PIPESTATUS array variable - this can give you the exit status of the first command whether or not tee succeeds.
Examples:
$ sudo bash -c "false | tee -a /dev/null"; echo $?
0
$ sudo bash -c "set -o pipefail; false | tee -a /dev/null"; echo $?
1
$ sudo bash -c 'false | tee -a /dev/null; exit ${PIPESTATUS[0]}'; echo $?
1
You will get similar results using su -c, if your system shell (in /bin/sh) is Bash. If not, then you'd need to explicitly invoke bash, at which point sudo is clearly simpler.
I was facing a similar issue today, in case the topic is still open here my solution, otherwise just ignore it...
I wrote a bash script (let's say my_script.sh) which looks more or less like this:
### FUNCTIONS ###
<all functions listed in the main script which do what I want...>
### MAIN SCRIPT ### calls the functions defined in the section above
main_script() {
log_message "START" 0
check_env
check_data
create_package
tar_package
zip_package
log_message "END" 0
}
main_script |tee -a ${var_log} # executes script and writes info into log file
var_sta=${PIPESTATUS[0]} # captures status of pipeline
exit ${var_sta} # exits with value of status
It works when you call the script directly or in sudo mode

start-stop-daemon alway returns success

I have the following piece of code in a BASH script:
start-stop-daemon --start --quiet --background \
--startas /bin/bash -- -c "$CMD; exec echo $?> $FILE"
where CMD is some command string and FILE is an output file location. For some reason though, the output in FILE is always 0, even when it should give a different number (1 for error, etc).
Why is that, and how do I fix it?
Your $? gets expanded in the context of the current shell together with $CMD and $FILE.
To fix this you'll have to escape it like \$?, i.e.
... /bin/bash -c "$CMD; exec echo \$? >$FILE"
exec echo $? this will print 0 if your last command was run successfully. and that is why it prints 0 in file. what are you trying to achieve.

How to keep make log and check it online?

I want to redirect make's log and see what's doing on make. Here is the script
make |& tee make.log # bash syntax
# make 2>&1 | tee make.log # or, sh syntax
[ $? -ne 0 ] && echo "Error: stopped" && exit 1
echo "Done"
I found it won't execute the Error exit when make failed.
I guess it is caused by the pipe, but how to refine the build script?
Since you're already restricting yourself to using bash, not portable POSIX sh, you can just use bash's pipefail shell option; run set -o pipefail. The man page for bash says:
If pipefail is enabled, the pipeline's return status is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully.
eg.
#!/bin/bash
set -o pipefail
if make |& tee make.log ; then
echo "Done"
else
echo "Error: stopped"
exit 1
fi

How to check if another instance of my shell script is running

GNU bash, version 1.14.7(1)
I have a script is called "abc.sh"
I have to check this from abc.sh script only...
inside it I have written following statement
status=`ps -efww | grep -w "abc.sh" | grep -v grep | grep -v $$ | awk '{ print $2 }'`
if [ ! -z "$status" ]; then
echo "[`date`] : abc.sh : Process is already running"
exit 1;
fi
I know it's wrong because every time it exits as it found its own process in 'ps'
how to solve it?
how can I check that script is already running or not from that script only ?
An easier way to check for a process already executing is the pidof command.
if pidof -x "abc.sh" >/dev/null; then
echo "Process already running"
fi
Alternatively, have your script create a PID file when it executes. It's then a simple exercise of checking for the presence of the PID file to determine if the process is already running.
#!/bin/bash
# abc.sh
mypidfile=/var/run/abc.sh.pid
# Could add check for existence of mypidfile here if interlock is
# needed in the shell script itself.
# Ensure PID file is removed on program exit.
trap "rm -f -- '$mypidfile'" EXIT
# Create a file with current PID to indicate that process is running.
echo $$ > "$mypidfile"
...
Update:
The question has now changed to check from the script itself. In this case, we would expect to always see at least one abc.sh running. If there is more than one abc.sh, then we know that process is still running. I'd still suggest use of the pidof command which would return 2 PIDs if the process was already running. You could use grep to filter out the current PID, loop in the shell or even revert to just counting PIDs with wc to detect multiple processes.
Here's an example:
#!/bin/bash
for pid in $(pidof -x abc.sh); do
if [ $pid != $$ ]; then
echo "[$(date)] : abc.sh : Process is already running with PID $pid"
exit 1
fi
done
I you want the "pidof" method, here is the trick:
if pidof -o %PPID -x "abc.sh">/dev/null; then
echo "Process already running"
fi
Where the -o %PPID parameter tells to omit the pid of the calling shell or shell script. More info in the pidof man page.
Here's one trick you'll see in various places:
status=`ps -efww | grep -w "[a]bc.sh" | awk -vpid=$$ '$2 != pid { print $2 }'`
if [ ! -z "$status" ]; then
echo "[`date`] : abc.sh : Process is already running"
exit 1;
fi
The brackets around the [a] (or pick a different letter) prevent grep from finding itself. This makes the grep -v grep bit unnecessary. I also removed the grep -v $$ and fixed the awk part to accomplish the same thing.
Working solution:
if [[ `pgrep -f $0` != "$$" ]]; then
echo "Another instance of shell already exist! Exiting"
exit
fi
Edit: I checked out some comments lately, so I tried attempting same with some debugging. I will also will explain it.
Explanation:
$0 gives filename of your running script.
$$ gives PID of your running script.
pgrep searches for process by name and returns PID.
pgrep -f $0 searches by filename, $0 being the current bash script filename and returns its PID.
So, pgrep checks if your script PID ($0) is equal to current running script ($$). If yes, then the script runs normally. If no, that means there's another PID with same filename running, so it exits. The reason I used pgrep -f $0 instead of pgrep bash is that you could have multiple instances of bash running and thus returns multiple PIDs. By filename, its returns only single PID.
Exceptions:
Use bash script.sh not ./script.sh as it doesn't work unless you have shebang.
Fix: Use #!/bin/bash shebang at beginning.
The reason sudo doesn't work is that it returns pgrep returns PID of both bash and sudo, instead of returning of of bash.
Fix:
#!/bin/bash
pseudopid="`pgrep -f $0 -l`"
actualpid="$(echo "$pseudopid" | grep -v 'sudo' | awk -F ' ' '{print $1}')"
if [[ `echo $actualpid` != "$$" ]]; then
echo "Another instance of shell already exist! Exiting"
exit
fi
while true
do
echo "Running"
sleep 100
done
The script exits even if the script isn't running. That is because there's another process having that same filename. Try doing vim script.sh then running bash script.sh, it'll fail because of vim being opened with same filename
Fix: Use unique filename.
Someone please shoot me down if I'm wrong here
I understand that the mkdir operation is atomic, so you could create a lock directory
#!/bin/sh
lockdir=/tmp/AXgqg0lsoeykp9L9NZjIuaqvu7ANILL4foeqzpJcTs3YkwtiJ0
mkdir $lockdir || {
echo "lock directory exists. exiting"
exit 1
}
# take pains to remove lock directory when script terminates
trap "rmdir $lockdir" EXIT INT KILL TERM
# rest of script here
Here's how I do it in a bash script:
if ps ax | grep $0 | grep -v $$ | grep bash | grep -v grep
then
echo "The script is already running."
exit 1
fi
This allows me to use this snippet for any bash script. I needed to grep bash because when using with cron, it creates another process that executes it using /bin/sh.
I find the answer from #Austin Phillips is spot on. One small improvement I'd do is to add -o (to ignore the pid of the script itself) and match for the script with basename (ie same code can be put into any script):
if pidof -x "`basename $0`" -o $$ >/dev/null; then
echo "Process already running"
fi
pidof wasn't working for me so I searched some more and came across pgrep
for pid in $(pgrep -f my_script.sh); do
if [ $pid != $$ ]; then
echo "[$(date)] : my_script.sh : Process is already running with PID $pid"
exit 1
else
echo "Running with PID $pid"
fi
done
Taken in part from answers above and https://askubuntu.com/a/803106/802276
Use the PS command in a little different way to ignore child process as well:
ps -eaf | grep -v grep | grep $PROCESS | grep -v $$
I create a temporary file during execution.
This is how I do it:
#!/bin/sh
# check if lock file exists
if [ -e /tmp/script.lock ]; then
echo "script is already running"
else
# create a lock file
touch /tmp/script.lock
echo "run script..."
#remove lock file
rm /tmp/script.lock
fi
I have found that using backticks to capture command output into a variable, adversly, yeilds one too many ps aux results, e.g. for a single running instance of abc.sh:
ps aux | grep -w "abc.sh" | grep -v grep | wc -l
returns "1". However,
count=`ps aux | grep -w "abc.sh" | grep -v grep | wc -l`
echo $count
returns "2"
Seems like using the backtick construction somehow temporarily creates another process. Could be the reason why the topicstarter could not make this work. Just need to decrement the $count var.
I didn't want to hardcode abc.sh in the check, so I used the following:
MY_SCRIPT_NAME=`basename "$0"`
if pidof -o %PPID -x $MY_SCRIPT_NAME > /dev/null; then
echo "$MY_SCRIPT_NAME already running; exiting"
exit 1
fi
This is compact and universal
# exit if another instance of this script is running
for pid in $(pidof -x `basename $0`); do
[ $pid != $$ ] && { exit 1; }
done
The cleanest fastest way:
processAlreadyRunning () {
process="$(basename "${0}")"
pidof -x "${process}" -o $$ &>/dev/null
}
For other variants (like AIX) that don't have pidof or pgrep. Reliability is greatly improved by getting a "static" view of the process table as opposed to piping it directly to grep. Setting IFS to null will preserve the carriage returns when the ps output is assigned to a variable.
#!/bin/ksh93
IFS=""
script_name=$(basename $0)
PSOUT="$(ps ax)"
ANY_TEXT=$(echo $PSOUT | grep $script_name | grep -vw $$ | grep $(basename $SHELL))
if [[ $ANY_TEXT ]]; then
echo "Process is already running"
echo "$ANY_TEXT"
exit
fi
[ "$(pidof -x $(basename $0))" != $$ ] && exit
https://github.com/x-zhao/exit-if-bash-script-already-running/blob/master/script.sh

Resources