How to parametrize a function call in bash without using eval? - bash

I have the following code (simplified):
if ! sudo -u user command1 "$Options" -o1 -o2 2>>"$log" > "$Dir"/output;
However, in some cases (determined at run time, if a variable docker is set to true), I instead want to execute
if ! docker exec -t "$cont" command2 "$Options" -o1 -o2 2>>"$log" > "$Dir"/output;
What changes is the way to call the command (1 or 2). The rest of the parameters remain the same.
So I'd like to parametrize the call to command1 or command2.
Something like
if $docker then;
Command = docker exec -t "$cont" command2
else
Command = sudo -u user command1
if ! $Command "$Options" -o1 -o2 2>>"$log" > "$Dir"/output;
This does not work. Is it possible to achieve what I want without resorting to eval, which I understood to be a bad practice ?
Thanks

You can use an array to hold the command-specific part, and rely on word splitting to build the entire command line to run. Something like
#!/usr/bin/env bash
# Set up your assorted variables used below
if $docker; then
cmd=(docker exec -t "$cont" command2)
else
cmd=(sudo -u user command1)
fi
if ! "${cmd[#]}" "$Options" -o1 -o2 2>>"$log" > "$Dir"/output; then
# etc.
fi

Related

How to disable set -e and set -o

So I have this at the start of a bash script file (-e and -o).
However, in some functions, I would like for it to not exit out. Example
set -e
set -o pipefail
function check_status {
echo "Start Check"
docker exec mservice bash -c "echo 'Hello' | grep 'fail'"
echo "End check"
}
check_status
How can I prevent this from exiting out of the script - basically if I run this, it would printout "Start Check", but then exit because the next command returns a '1'.
I would like to be able to disable and enable the set -e/-o in multiple places or in different functions.
For all options, the opposite of set -š¯“§ is set +š¯“§ (note the plus sign).
So set +e will undo set -e, and set +o pipefail will undo set -o pipefail.
You can also leave the settings alone, and do something as simple as:
docker exec mservice bash -c "echo 'Hello' | grep 'fail'" || true
...and in so doing force a successful run of that command list. There would be no programmatic way to detect the failure of the docker command in this case, but if the command emits some error message, the human observer may notice.
Or you can handle the error in your function
#!/usr/bin/env bash
set -e
set -o pipefail
function check_status {
if docker exec mservice bash -c "echo 'Hello' | grep 'fail'"; then
echo good
else
echo bad
fi
}
status=$(check_status)
if [ "$status" = 'bad' ]; then
echo "check_status failed, but still running, handling the error..."
fi
Saving as testfail.sh and running it for me causes failure, since my docker installation does not include anything named mservice:
$ ./testfail.sh
Error response from daemon: No such container: mservice
check_status failed, but still running, handling the error...
$

testing a program in bash

I wrote a program in c++ and now I have a binary. I have also generated a bunch of tests for testing. Now I want to automate the process of testing with bash. I want to save three things in one execution of my binary:
execution time
exit code
output of the program
Right now I am stack up with a script that only tests that binary does its job and returns 0 and doesn't save any information that I mentioned above. My script looks like this
#!/bin/bash
if [ "$#" -ne 2 ]; then
echo "Usage: testScript <binary> <dir_with_tests>"
exit 1
fi
binary="$1"
testsDir="$2"
for test in $(find $testsDir -name '*.txt'); do
testname=$(basename $test)
encodedTmp=$(mktemp /tmp/encoded_$testname)
decodedTmp=$(mktemp /tmp/decoded_$testname)
printf 'testing on %s...\n' "$testname"
if ! "$binary" -c -f $test -o $encodedTmp > /dev/null; then
echo 'encoder failed'
rm "$encodedTmp"
rm "$decodedTmp"
continue
fi
if ! "$binary" -u -f $encodedTmp -o $decodedTmp > /dev/null; then
echo 'decoder failed'
rm "$encodedTmp"
rm "$decodedTmp"
continue
fi
if ! diff "$test" "$decodedTmp" > /dev/null ; then
echo "result differs with input"
else
echo "$testname passed"
fi
rm "$encodedTmp"
rm "$decodedTmp"
done
I want save output of $binary in a variable and not send it into /dev/null. I also want to save time using time bash function
As you asked for the output to be saved in a shell variable, I tried answering this without using output redirection ā€“ which saves output in (temporary) text files (which then have to be cleaned).
Saving the command output
You can replace this line
if ! "$binary" -c -f $test -o $encodedTmp > /dev/null; then
with
if ! output=$("$binary" -c -f $test -o $encodedTmp); then
Using command substitution saves the program output of $binary in the shell variable. Command substitution (combined with shell variable assignment) also allows exit codes of programs to be passed up to the calling shell so the conditional if statement will continue to check if $binary executed without error.
You can view the program output by running echo "$output".
Saving the time
Without a more sophisticated form of Inter-Process Communication, thereā€™s no way for a shell thatā€™s a sub-process of another shell to change the variables or the environment of its parent process so the only way that I could save both the time and the program output was to combine them in the one variable:
if ! time-output=$(time "$binary" -c -f $test -o $encodedTmp) 2>&1); then
Since time prints its profiling information to stderr, I use the parentheses operator to run the command in subshell whose stderr can be redirected to stdout. The programming output and the output of time can be viewed by running echo "$time-output" which should return something similar to:
<program output>
<blank line>
real 0m0.041s
user 0m0.000s
sys 0m0.046s
You can get the process status in bash by using $? and print it out by echo $?.
And to catch the output of time, you could use sth like that
{ time sleep 1 ; } 2> time.txt
Or you can save the output of the program and execution time at once
(time ls) > out.file 2>&1
You can save output to a file using output redirection. Just change first /dev/null line:
if ! "$binary" -c -f $test -o $encodedTmp > /dev/null; then
to
if ! "$binary" -c -f $test -o $encodedTmp > prog_output; then
then change second and third /dev/null lines respectively:
if ! "$binary" -u -f $encodedTmp -o $decodedTmp >> prog_output; then
if ! diff "$test" "$decodedTmp" >> prog_output; then
To measure program execution put
start=$(date +%s)
on the first line
then
end=$(date +%s)
echo "Execution time in seconds: " $((end-start)) >> prog_output
on the end.

How to execute arbitrary command under `bash -c`

What is a procedure to decorate an arbitrary bash command to execute it in a subshell? I cannot change the command, I have to decorate it on the outside.
the best I can think of is
>bash -c '<command>'
works on these:
>bash -c 'echo'
>bash -c 'echo foobar'
>bash -c 'echo \"'
but what about the commands such as
echo \'
and especially
echo \'\"
The decoration has to be always the same for all commands. It has to always work.
You say "subshell" - you can get one of those by just putting parentheses around the command:
x=outer
(x=inner; echo "x=$x"; exit)
echo "x=$x"
produces this:
x=inner
x=outer
You could (ab)use heredocs:
bash -c "$(cat <<-EOF
echo \'\"
EOF
)"
This is one way without using -c option:
bash <<EOF
echo \'\"
EOF
What you want to do is exactly the same as escapeshellcmd() in PHP (http://php.net/manual/fr/function.escapeshellcmd.php)
You just need to escape #&;`|*?~<>^()[]{}$\, \x0A and \xFF. ' and " are escaped only if they are not paired.
But beware of security issues...
Let bash take care of it this way:
1) prepare the command as an array:
astrCmd=(echo \'\");
2) export the array as a simple string:
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
3) restore the array and run it as a full command:
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}"
Create a function to make these steps more easy like:
FUNCbash(){
astrCmd=("$#");
export EXPORTEDastrCmd="`declare -p astrCmd| sed -r "s,[^=]*='(.*)',\1,"`";
bash -c "declare -a astrCmd='$EXPORTEDastrCmd';\${astrCmd[#]}";
}
FUNCbash echo \'\"

equivalent of pipefail in dash shell

Is there some similar option in dash shell corresponding to pipefail in bash?
Or any other way of getting a non-zero status if one of the commands in pipe fail (but not exiting on it which set -e would).
To make it clearer, here is an example of what I want to achieve:
In a sample debugging makefile, my rule looks like this:
set -o pipefail; gcc -Wall $$f.c -o $$f 2>&1 | tee err; if [ $$? -ne 0 ]; then vim -o $$f.c err; ./$$f; fi;
Basically it runs opens the error file and source file on error and runs the programs when there is no error. Saves me some typing. Above snippet works well on bash but my newer Ubunty system uses dash which doesn't seem to support pipefail option.
I basically want a FAILURE status if the first part of the below group of commands fail:
gcc -Wall $$f.c -o $$f 2>&1 | tee err
so that I can use that for the if statement.
Are there any alternate ways of achieving it?
Thanks!
I ran into this same issue and the bash options of set -o pipefail and ${PIPESTATUS[0]} both failed in the dash shell (/bin/sh) on the docker image I'm using. I'd rather not modify the image or install another package, but the good news is that using a named pipe worked perfectly for me =)
mkfifo named_pipe
tee err < named_pipe &
gcc -Wall $$f.c -o $$f > named_pipe 2>&1
echo $?
See this answer for where I found the info: https://stackoverflow.com/a/1221844/431296
The Q.'s sample problem requires:
I basically want a FAILURE status if the first part of the ... group of commands fail:
Install moreutils, and try the mispipe util, which returns the exit status of the first command in a pipe:
sudo apt install moreutils
Then:
if mispipe "gcc -Wall $$f.c -o $$f 2>&1" "tee err" ; then \
./$$f
else
vim -o $$f.c err
fi
While 'mispipe' does the job here, it is not an exact duplicate of the bash shell's pipefail; from man mispipe:
Note that some shells, notably bash, do offer a
pipefail option, however, that option does not
behave the same since it makes a failure of any
command in the pipeline be returned, not just the
exit status of the first.

Find out which shell PHP is using

I'm trying to execute a piped shell commands like this
set -o pipefail && command1 | command2 | command3
from a PHP script. The set -o pipefail part is to make the pipe break as soon as any of the commands fails. But the commands results in this:
sh: 1: set: Illegal option -o pipefail
whereas it runs fine from the terminal. Maybe explicitly specifying which shell PHP CLI should use (i.e. bin/bash) when executing shell commands could solve the problem or is there better way out?
You can always run bash -c 'set -o pipefail && command1 | command2 | command3' instead.
you can find it out by doing
echo `echo $SHELL`;

Resources