Failure of bash script called via command substitution does not stop parent script - linux

I have a bash script (exp1.sh)
#!/bin/bash
set -e
for row in $(./exp2.sh);
do
echo $?
echo outer=$row
done
echo "continuing"
which invokes another bash script exp2.sh.
#!/bin/bash
echo "A"
echo "B"
exit 1
I want the first script to fail fast when second script exits with error. (The second script actually reads rows from database to stdout, in case of database connectivity error it returns nonzero exit code.)
I expect the set -e option causes premature termination of first script exp1.sh. However from script output it seems that even the exitcode from second script is passed to first script the loop is performed and script continues beyond the loop:
1
outer=A
0
outer=B
continuing
I want neither the loop nor any command after loop to be executed. I understand the second script had passed some data to first script before it exited with error so loop processed them. I don't understand why loop didn't stop then and what's the correct fix.
The best thing I could figure out is to store result of command substitution into array, which works.
a=$(./exp2.sh)
# execution won't get here when error
echo $?
for row in $a
Is there a way to do this without anything being executed? I played with inherit_errexit as I found here but with no success.

Your idea for a solution is good but a=$(./exp2.sh) doesn't populate an array, it populates a string and then for row in $a is leaving the contents of that string unquoted and so open to the shell for interpretation. You can do this to make/use a as an array if the output of exp2.sh is as simple as you show:
a=( $( ./exp2.sh ) )
(( $? == 0 )) || exit 1
echo "$?"
for row in "${a[#]}"
but rather than a=( $(./exp2.sh) ) which has some caveats, it'd be more robust to do:
IFS=$'\n' read -r -d '' -a a < <( ./exp2.sh && printf '\0' )
or:
readarray -t a < <( ./exp2.sh )
See Reading output of a command into an array in Bash and How to split a string into an array in Bash?

Related

Bash command with pipe('|') alway return exit code of 0, even in error case [duplicate]

I want to execute a long running command in Bash, and both capture its exit status, and tee its output.
So I do this:
command | tee out.txt
ST=$?
The problem is that the variable ST captures the exit status of tee and not of command. How can I solve this?
Note that command is long running and redirecting the output to a file to view it later is not a good solution for me.
There is an internal Bash variable called $PIPESTATUS; it’s an array that holds the exit status of each command in your last foreground pipeline of commands.
<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0
Or another alternative which also works with other shells (like zsh) would be to enable pipefail:
set -o pipefail
...
The first option does not work with zsh due to a little bit different syntax.
Dumb solution: Connecting them through a named pipe (mkfifo). Then the command can be run second.
mkfifo pipe
tee out.txt < pipe &
command > pipe
echo $?
using bash's set -o pipefail is helpful
pipefail: the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero status
There's an array that gives you the exit status of each command in a pipe.
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1
This solution works without using bash specific features or temporary files. Bonus: in the end the exit status is actually an exit status and not some string in a file.
Situation:
someprog | filter
you want the exit status from someprog and the output from filter.
Here is my solution:
((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1
echo $?
See my answer for the same question on unix.stackexchange.com for a detailed explanation and an alternative without subshells and some caveats.
By combining PIPESTATUS[0] and the result of executing the exit command in a subshell, you can directly access the return value of your initial command:
command | tee ; ( exit ${PIPESTATUS[0]} )
Here's an example:
# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"
will give you:
return value: 1
So I wanted to contribute an answer like lesmana's, but I think mine is perhaps a little simpler and slightly more advantageous pure-Bourne-shell solution:
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
I think this is best explained from the inside out - command1 will execute and print its regular output on stdout (file descriptor 1), then once it's done, printf will execute and print icommand1's exit code on its stdout, but that stdout is redirected to file descriptor 3.
While command1 is running, its stdout is being piped to command2 (printf's output never makes it to command2 because we send it to file descriptor 3 instead of 1, which is what the pipe reads). Then we redirect command2's output to file descriptor 4, so that it also stays out of file descriptor 1 - because we want file descriptor 1 free for a little bit later, because we will bring the printf output on file descriptor 3 back down into file descriptor 1 - because that's what the command substitution (the backticks), will capture and that's what will get placed into the variable.
The final bit of magic is that first exec 4>&1 we did as a separate command - it opens file descriptor 4 as a copy of the external shell's stdout. Command substitution will capture whatever is written on standard out from the perspective of the commands inside it - but since command2's output is going to file descriptor 4 as far as the command substitution is concerned, the command substitution doesn't capture it - however once it gets "out" of the command substitution it is effectively still going to the script's overall file descriptor 1.
(The exec 4>&1 has to be a separate command because many common shells don't like it when you try to write to a file descriptor inside a command substitution, that is opened in the "external" command that is using the substitution. So this is the simplest portable way to do it.)
You can look at it in a less technical and more playful way, as if the outputs of the commands are leapfrogging each other: command1 pipes to command2, then the printf's output jumps over command 2 so that command2 doesn't catch it, and then command 2's output jumps over and out of the command substitution just as printf lands just in time to get captured by the substitution so that it ends up in the variable, and command2's output goes on its merry way being written to the standard output, just as in a normal pipe.
Also, as I understand it, $? will still contain the return code of the second command in the pipe, because variable assignments, command substitutions, and compound commands are all effectively transparent to the return code of the command inside them, so the return status of command2 should get propagated out - this, and not having to define an additional function, is why I think this might be a somewhat better solution than the one proposed by lesmana.
Per the caveats lesmana mentions, it's possible that command1 will at some point end up using file descriptors 3 or 4, so to be more robust, you would do:
exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
Note that I use compound commands in my example, but subshells (using ( ) instead of { } will also work, though may perhaps be less efficient.)
Commands inherit file descriptors from the process that launches them, so the entire second line will inherit file descriptor four, and the compound command followed by 3>&1 will inherit the file descriptor three. So the 4>&- makes sure that the inner compound command will not inherit file descriptor four, and the 3>&- will not inherit file descriptor three, so command1 gets a 'cleaner', more standard environment. You could also move the inner 4>&- next to the 3>&-, but I figure why not just limit its scope as much as possible.
I'm not sure how often things use file descriptor three and four directly - I think most of the time programs use syscalls that return not-used-at-the-moment file descriptors, but sometimes code writes to file descriptor 3 directly, I guess (I could imagine a program checking a file descriptor to see if it's open, and using it if it is, or behaving differently accordingly if it's not). So the latter is probably best to keep in mind and use for general-purpose cases.
(command | tee out.txt; exit ${PIPESTATUS[0]})
Unlike #cODAR's answer this returns the original exit code of the first command and not only 0 for success and 127 for failure. But as #Chaoran pointed out you can just call ${PIPESTATUS[0]}. It is important however that all is put into brackets.
In Ubuntu and Debian, you can apt-get install moreutils. This contains a utility called mispipe that returns the exit status of the first command in the pipe.
Outside of bash, you can do:
bash -o pipefail -c "command1 | tee output"
This is useful for example in ninja scripts where the shell is expected to be /bin/sh.
The simplest way to do this in plain bash is to use process substitution instead of a pipeline. There are several differences, but they probably don't matter very much for your use case:
When running a pipeline, bash waits until all processes complete.
Sending Ctrl-C to bash makes it kill all the processes of a pipeline, not just the main one.
The pipefail option and the PIPESTATUS variable are irrelevant to process substitution.
Possibly more
With process substitution, bash just starts the process and forgets about it, it's not even visible in jobs.
Mentioned differences aside, consumer < <(producer) and producer | consumer are essentially equivalent.
If you want to flip which one is the "main" process, you just flip the commands and the direction of the substitution to producer > >(consumer). In your case:
command > >(tee out.txt)
Example:
$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world
$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world
As I said, there are differences from the pipe expression. The process may never stop running, unless it is sensitive to the pipe closing. In particular, it may keep writing things to your stdout, which may be confusing.
PIPESTATUS[#] must be copied to an array immediately after the pipe command returns.
Any reads of PIPESTATUS[#] will erase the contents.
Copy it to another array if you plan on checking the status of all pipe commands.
"$?" is the same value as the last element of "${PIPESTATUS[#]}",
and reading it seems to destroy "${PIPESTATUS[#]}", but I haven't absolutely verified this.
declare -a PSA
cmd1 | cmd2 | cmd3
PSA=( "${PIPESTATUS[#]}" )
This will not work if the pipe is in a sub-shell. For a solution to that problem,
see bash pipestatus in backticked command?
Base on #brian-s-wilson 's answer; this bash helper function:
pipestatus() {
local S=("${PIPESTATUS[#]}")
if test -n "$*"
then test "$*" = "${S[*]}"
else ! [[ "${S[#]}" =~ [^0\ ] ]]
fi
}
used thus:
1: get_bad_things must succeed, but it should produce no output; but we want to see output that it does produce
get_bad_things | grep '^'
pipeinfo 0 1 || return
2: all pipeline must succeed
thing | something -q | thingy
pipeinfo || return
Pure shell solution:
% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag && (echo Some command failed: ; cat error.flag)
hello world
And now with the second cat replaced by false:
% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141
Please note the first cat fails as well, because it's stdout gets closed on it. The order of the failed commands in the log is correct in this example, but don't rely on it.
This method allows for capturing stdout and stderr for the individual commands so you can then dump that as well into a log file if an error occurs, or just delete it if no error (like the output of dd).
It may sometimes be simpler and clearer to use an external command, rather than digging into the details of bash. pipeline, from the minimal process scripting language execline, exits with the return code of the second command*, just like a sh pipeline does, but unlike sh, it allows reversing the direction of the pipe, so that we can capture the return code of the producer process (the below is all on the sh command line, but with execline installed):
$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world
$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt
hello world
$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world
$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1
$ pipeline -w tee out.txt "" true; echo $?
0
$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world
Using pipeline has the same differences to native bash pipelines as the bash process substitution used in answer #43972501.
* Actually pipeline doesn't exit at all unless there is an error. It executes into the second command, so it's the second command that does the returning.
Why not use stderr? Like so:
(
# Our long-running process that exits abnormally
( for i in {1..100} ; do echo ploop ; sleep 0.5 ; done ; exit 5 )
echo $? 1>&2 # We pass the exit status of our long-running process to stderr (fd 2).
) | tee ploop.out
So ploop.out receives the stdout. stderr receives the exit status of the long running process. This has the benefit of being completely POSIX-compatible.
(Well, with the exception of the range expression in the example long-running process, but that's not really relevant.)
Here's what this looks like:
...
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
ploop
5
Note that the return code 5 does not get output to the file ploop.out.

How to print a success message after the complete execution of a script?

#! /bin/sh
DB_USER='aaa';
DB_PASSWD='aaa1';
DB_NAME='data';
TABLE='datalog';
mysql --local-infile=1 --user=$DB_USER --password=$DB_PASSWD $DB_NAME -e "load data local infile '/home/demo/data1.csv' into table datalog fields terminated by ',' lines terminated by '\n';" -e echo "script executed successfully " | date "+%h %e"
My aim is to print a success message after the above script executes successfully. I have written the above command to do so but it is printing the date, not the echo statement.
The arguments to mysql -e should be SQL commands, not shell script.
The solution is much simpler than what you are trying.
#!/bin/sh
# Terminate immediately if a command fails
set -e
# Don't put useless semicolons at the end of each assignment
# Use lower case for your private variables
db_user='aaa'
db_passwd='aaa1'
db_name='data'
table='datalog'
# Quote strings
mysql --local-infile=1 --user="$db_user" --password="$db_passwd" "$db_name" \
-e "load data local infile '/home/demo/data1.csv' into table $table fields terminated by ',' lines terminated by '\n';"
# Just use date
# Print diagnostics to standard error, not standard output
date "+%h %e script executed successfully" >&2
The use of set -e is somewhat cumbersome, but looks like the simplest solution to your basic script. For a more complex script, maybe instead use something like
mysql -e "... stuff ..." || exit
to terminate on failure of this individual command, but allow other failures in the script. Perhaps see also What does set -e mean in a bash script?
If you want to preserve the exit code from mysql and always print a message to show what happened, probably take out the set -e and do something like
if mysql -e "... whatever ..."
then
date "+%h %e script completed successfully" >&2
else
rc=$?
date "+%h %e script failed: $rc" >&2
exit $rc
fi
As an aside, date does not read its standard input for anything, so you can't pipe echo to it. The script above simply uses only date, but here are a few different ways you could solve that.
# Merge this output with the next output
date "+%h %e" | tr '\n' ' ' >&2
echo script executed successfully >&2
or
# Use a command substitution to interpolate the output from date into
# the arguments for echo
echo "$(date "+%h %e") script executed successfully >&2
For more complex situations, maybe also look into xargs, though it's absolutely horribly overkill here.
date "+%h %e" | xargs -I {} echo script executed successfully {} >&2
If you use only date, you will need to be mindful of your use of % in any literal message; to print a literal per-cent sign from date, use %%.
As a further stylistic aside, you should avoid upper case for your private variables.

Bash Read CSV value from Variable in Bash

So in a script I have a variable that changes each time the script is run. The variable is IP_PING. It is basically a list of if address seperated by commas.
I want the script to take each ip addess in the csv variable and ping it. The example below works fine if I only have one ip address in the variable.
IP_PING="192.168.1.1"
echo "$IP_PING" | while read -r -d, ip1
do
ping -c 4 "$ip1" >/dev/null
if [ $? -ne 0 ]; then
echo "Ping fail"
else
echo "Ping success"
fi
done
But if I change the IP_Ping variable to = "192.168.1.1,192.168.100.1" The script only reads the first value 192.168.1.1. Is "while read" the wrong command to use for this. It is reading each value as a seperate column. I want all the values in a single column and then do the while on each value. If that makes sense.
"192.168.1.1,192.168.100.1"
Here is your issue. The problem is that when the while loop is reading, after the first iteration there is no more commas so it hits the end and quits. "192.168.1.1,192.168.100.1," should show the way you are intending.
As other's have suggested in the comments, I would go with a for loop myself, but to with closet to how you were trying to implement it, this would work for you:
IP_PING="192.168.1.1,192.168.1.2,192.168.1.3"
while read -r ip1 ; do
ping -c 4 "$ip1" >/dev/null
if [ $? -ne 0 ]; then
echo "Ping fail"
else
echo "Ping success"
fi
done < <(echo $IP_PING |tr ',' '\n')
< <(echo $IP_PING |tr ',' '\n') is process substitution. To put it simply it will "pipe" the output into the while loop. The difference from a normal pipe being that it creates one less process; the actual pipe creates a process itself.
Using process substitution will also keep the while loop in the current environment as well. (vs a while loop usually takes a 'snapshot' of the env and uses that during the loop, then it disappears when the loop ends) Meaning, with process substitution, that processes in the while loop will be able to see changes in env variables of the parent while it is looping, AND variables assigned within the loop will also still be available in the parent after it ends.

Filtering shell script output within itself, the script is not terminated

I want a Bash script to generate some output messages. The script is supposed to capture messages, do some filtering, transform, and then output them to the screen.
The filtered results are correct in the output, but the script is not terminated. I must press a return key to finish it. How do I fix it?
Demo script:
#!/bin/bash
exec &> >(
{
while read line; do
[ "$line" = "exit" ] && break
echo "`date +%H:%M:%S.%N` $line"
done
echo "while finish"
} )
for ((i=3;i--;)); do
echo "text $i"
done
echo "exit"
The script does terminate, but the script itself finishes before the background process that writes the output does, so the prompt is displayed first, then the output, leaving your terminal with a blank line that looks like the script is still running. You could type any command instead of hitting return, and that command would execute.
To avoid this, you need to run the while loop in an explicit background job that you can wait on before exiting your script.
mkfifo logpipe
trap 'wait $logger_pid; rm logpipe' EXIT
while read line; do
[ "$line" = "exit" ] && break
echo "$(date +%H:%M:%S.%N) $line"
done < logpipe &
logger_pid=$!
exec &> logpipe
# ==========
for ((i=3;i--;)); do
echo "text $i"
done
echo "exit"
The while loop runs in the background, reading its input from the named pipe logpipe. Once that is running, you can redirect all your output to the pipe and start your "main" script. The exit trap ensures that your script doesn't actually exit until the while loop completes; it also cleans up the named pipe for you.
You might not have noticed yet, but there is no guarantee that the while loop will receive the merged standard output and standard error in the exact order in which things are written to them. For instance,
echo out1
echo err1 >&2
echo out2
echo err2 >&2
may end up being read as
out1
err1
err2
out2
Each stream itself will remain in order, but the two could be arbitrarily merged.

check if file exist and wait for a sh script to run until the file is found

I'm working on a linux server which is sometimes very slow. So when i add some jobs to run for me i have to wait for a few hours just to run a simple calculation.
I was wondering if i am able to start the next analysis but let it wait until the output of the previous analysis is there. (the second analysis needs the first analysis output)
I tried to make except and other options working but still no success (found except and other options in previous question on stackoverflow):
expect {
'output/analysis_file1.txt'
}
Any ideas/hints are appreciated and will help me allot.
The only thing i want is to let the second scrip wait till the text file of the first script is given.
The 4 scripts:1.
#!/bin/bash
#$ -cwd
./script1.sh
. ./script2.sh $repla
. ./script3.sh $replac
2:
repla=''
for i in 'abcdefghijklmnopqrst'
do
repla=`echo $i | sed 's/'abc'/'xyz'/g'`
#echo $repla
done
3:
replac=''
for j in $1
do
replac=`echo $j | sed 's/'xyz'/'san'/g'`
#echo $replac
done
4:
replace=''
for h in $1
do
replace=`echo $h | sed 's/'san'/'sander'/g'`
#echo $replace
done
you can use below core with some modifications
#!/bin/bash
while [ ! -f FILE_NAME ]
do
sleep SOME_SECONDS
done
echo "file found"
You can use wait if you know the pid of the process running in background. Wait will also return the same exit code of the process it is waiting to stop.
firstProcess & # Running in background
firstPid=$!
otherProcess # Concurrent with firstProcess
wait $firstPid # Wait firstProcess finish
anotherProcess
Instead of executing multiple scripts independently you should create a master script runner like this:
#!/bin/bash
# sanity checks & parse arguments
./script1
ret=$?
# check for return value of script1 using $ret variable
./script2
ret=$?
# check for return value of script2 using $ret variable
./script3
ret=$?
# check for return value of script3 using $ret variable
...
# do cleanup and reporting

Resources