We have a script, with a return code. For example
#!/bin/bash
exit 42
which is works fine:
$ ./script ; echo $?
42
but if i go:
$ bash << EOF
./script ; echo $?
EOF
0
so it prints 0, while one would expect it to still print 42
Your $? is being expanded before executing the script. If you don't want your variables to expand in the heredoc (not a pipe) put single quotes around the name:
bash <<'EOF'
./script; echo $?
EOF
That wil prevent $? from being expanded while passing the string to the new bash command. It will, instead, be evaluated in the string which is what you seem to be going for.
Related
this is a bug I have found nothing about after a relentless search
I'm trying to run a bootstrap file in an EC2 instance, part of an EMR cluster v6.4.0. As the bootstrap action takes longer than 5 minutes, we execute it as a subprocess of the form
#!/bin/bash
var="var"
cat << EOF > ~/bootstrap.sh
intra="intra"
echo $var
echo $intra
EOF
/bin/bash ~/bootstrap.sh
exit 0
But the var "intra" is never set, and the bootstrap action returns the error line n: intra: unbound variable
If you execute that script the "intra" var is not printed.
Why can't I assign variables in a subprocess? Thank you!
When using that type of heredoc (<<WORD), you must escape literal $ characters using \$. Same goes for the backtick character (`):
#!/bin/bash
var="var"
cat << EOF > ~/bootstrap.sh
intra="intra"
echo $var
echo \$intra
EOF
/bin/bash ~/bootstrap.sh
exit 0
Another way of generating an equivalent bootstrap script is to use the literal heredoc form <<'WORD':
#!/bin/bash
var="var"
# This line will be inserted as-is without variable and subshell expansion:
cat << 'EOF1' > ~/bootstrap.sh
intra="intra"
EOF1
# We will allow this ONE line to expand.
cat << EOF2 >> ~/bootstrap.sh
echo $var
EOF2
# Back to literal insertions, no escaping necessary.
cat << 'EOF3' >> ~/bootstrap.sh
echo $intra
EOF3
/bin/bash ~/bootstrap.sh
exit 0
Inspecting the contents of ~/bootstrap.sh is a good place to start debugging.
I am very new to Bash scripting, can someone explain to me how the $# and $? work in the following code?
#!/bin/bash
ARGS=3 # Script requires 3 arguments.
E_BADARGS=85 # Wrong number of arguments passed to script.
if [ $# -ne "$ARGS" ]
then
echo "Usage: `basename $0` old-pattern new-pattern filename"
exit $E_BADARGS
fi
old_pattern=$1
new_pattern=$2
if [ -f "$3" ]
then
file_name=$3
else
echo "File \"$3\" does not exist."
exit $E_BADARGS
fi
exit $?
From Learn Bash in Y minutes:
# Builtin variables:
# There are some useful builtin variables, like
echo "Last program's return value: $?"
echo "Script's PID: $$"
echo "Number of arguments passed to script: $#"
echo "All arguments passed to script: $#"
echo "The script's name: $0"
echo "Script's arguments separated into different variables: $1 $2..."
From https://www.gnu.org/software/bash/manual/html_node/Special-Parameters.html
$# Expands to the number of positional parameters in decimal.
$? Expands to the exit status of the most recently executed foreground pipeline.
$# shows the number of the script's arguments
$? shows the last script's return value
about arguments: echo "ARG[$#]" before if and then execute the script like
script.sh 1
the ouput will be
ARG[1]
Usage: g old-pattern new-pattern filename
and so on
the ouput of $? could be also used on the command line:
#shell>ls
file1.txt g inpu nodes_list
#shell>echo $?
0
#shell>ls FileNameNotFound
ls: FileNameNotFound: No such file or directory
#shell> echo $?
1
In bash exist special variables... and i write you some of then.
$#- this is an special variable that content inside the number of command line (you can just count how many parameters were entered) you passed to the script. tis variable also represent the last command line but its better do this ${!#}
$?- this one is very special cause its represents is your script is fine this variable holds the exit status of the previosly command... its a littler confusing but it work perfectly... when you end you script you can positional this variable at the end and if she return 0 value you scrip is perfect is true, if she return 1 or others you must check out your lines.
Currently at work on the following version of Bash:
GNU bash, version 4.2.46(1)-release (x86_64-redhat-linux-gnu)
My current script:
#!/usr/bin/env bash
function main() {
local commands=$#
for command in ${commands[#]} ; do
echo "command arg: $command"
done
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
set -e
main $#
fi
In simple terms, this script will only exec main if it's the script being called, similar to Python's if __name__ == '__main__' convention.
In the main function, I'm simply looping over all the command variables, but quote escaping isn't happening as expected:
$ tests/simple /bin/bash -c 'echo true'
command arg: /bin/bash
command arg: -c
command arg: echo
command arg: true
The last argument here should get parsed by Bash as a single argument, nevertheless it is split into individual words.
What am I doing wrong? I want echo true to show up as a single argument.
You are getting the right output except for the 'echo true' part which is getting word split. You need to use double quotes in your code:
main "$#"
And in the function:
function main() {
local commands=("$#") # need () and double quotes here
for command in "${commands[#]}" ; do
echo "command arg: $command"
done
}
The function gets its own copy of $# and hence you don't really need to make a local copy of it.
With these changes, we get this output:
command arg: /bin/bash
command arg: -c
command arg: echo true
In general, it is not good to store shell commands in a variable. See BashFAQ/050.
See also:
How to copy an array in Bash?
You'll likely want to do something more like this:
function main() {
while [ $# -gt 0 ]
do
echo "$1"
shift
done
}
main /bin/bash -c "echo true"
The key really being $#, which counts the number of command line arguments, (not including the invocation name $0). The environment variable $# is automatically set to the number of command line arguments. If the function/script was called with the following command line:
$ main /bin/bash -c "echo true"
$# would have the value "3" for the arguments: "/bin/bash", "-c", and "echo true". The last one counts as one argument, because they are enclosed within quotes.
The shift command "shifts" all command line arguments one position to the left.
The leftmost argument is lost (main).
Quoting of # passed to main was your issue, but I thought I would mention that you also do not need to assign the value inside main to use it.
You could do the following:
main()
{
for command
do
...
done
}
main "$#"
when I use ssh to execute remote file in shell script, so that I could get the result executed on remote machine, however, I met a problem which the user defined variables could not be recognized.
I created a shell script run.sh like this
#!/bin/bash
ssh jenkins#10.122.214.55 << EOF
./test.sh && ret=1 || ret=0
echo "before"
echo ${ret}
echo "after"
echo ${HOME}
exit ${ret}
EOF
the content of test.sh which is called by run.sh :
#!/bin/bash
echo "lala"
exit 1
when I call ./run.sh
it print like this
lala
before
after
/home/jenkins
Why did not it echo ${ret}? After ./run.sh is called, echo $? is 0 which is unexpected, I thought it should echo 1 here.
Thanks!
Because the variables in the heredoc are being expanded by the local script before being sent to the standard input of ssh.
In other words, your heredoc behaves similarly to
#!/bin/bash
string="echo \"before\"
echo ${ret}
echo \"after\"
echo ${HOME}
exit ${ret}
./test.sh && ret=1 || ret=0"
echo "$string" | ssh jenkins#10.122.214.55
which makes it more obvious that the string has the variables interpolated.
There are a couple ways around this: either you can escape the $ symbols (e.g. echo \${ret}) so that the intended string is passed through, or you can use a verbatim heredoc
#!/bin/bash
ssh jenkins#10.122.214.55 <<'EOF'
./test.sh && ret=1 || ret=0
echo "before"
echo ${ret}
echo "after"
echo ${HOME}
exit ${ret}
EOF
By single-quoting the delimiter, we ensure that no expansion takes place inside the heredoc.
I want to write code like this:
command="some command"
safeRunCommand $command
safeRunCommand() {
cmnd=$1
$($cmnd)
if [ $? != 0 ]; then
printf "Error when executing command: '$command'"
exit $ERROR_CODE
fi
}
But this code does not work the way I want. Where did I make the mistake?
Below is the fixed code:
#!/bin/ksh
safeRunCommand() {
typeset cmnd="$*"
typeset ret_code
echo cmnd=$cmnd
eval $cmnd
ret_code=$?
if [ $ret_code != 0 ]; then
printf "Error: [%d] when executing command: '$cmnd'" $ret_code
exit $ret_code
fi
}
command="ls -l | grep p"
safeRunCommand "$command"
Now if you look into this code, the few things that I changed are:
use of typeset is not necessary, but it is a good practice. It makes cmnd and ret_code local to safeRunCommand
use of ret_code is not necessary, but it is a good practice to store the return code in some variable (and store it ASAP), so that you can use it later like I did in printf "Error: [%d] when executing command: '$command'" $ret_code
pass the command with quotes surrounding the command like safeRunCommand "$command". If you don’t then cmnd will get only the value ls and not ls -l. And it is even more important if your command contains pipes.
you can use typeset cmnd="$*" instead of typeset cmnd="$1" if you want to keep the spaces. You can try with both depending upon how complex is your command argument.
'eval' is used to evaluate so that a command containing pipes can work fine
Note: Do remember some commands give 1 as the return code even though there isn't any error like grep. If grep found something it will return 0, else 1.
I had tested with KornShell and Bash. And it worked fine. Let me know if you face issues running this.
Try
safeRunCommand() {
"$#"
if [ $? != 0 ]; then
printf "Error when executing command: '$1'"
exit $ERROR_CODE
fi
}
It should be $cmd instead of $($cmd). It works fine with that on my box.
Your script works only for one-word commands, like ls. It will not work for "ls cpp". For this to work, replace cmd="$1"; $cmd with "$#". And, do not run your script as command="some cmd"; safeRun command. Run it as safeRun some cmd.
Also, when you have to debug your Bash scripts, execute with '-x' flag. [bash -x s.sh].
There are several things wrong with your script.
Functions (subroutines) should be declared before attempting to call them. You probably want to return() but not exit() from your subroutine to allow the calling block to test the success or failure of a particular command. That aside, you don't capture 'ERROR_CODE' so that is always zero (undefined).
It's good practice to surround your variable references with curly braces, too. Your code might look like:
#!/bin/sh
command="/bin/date -u" #...Example Only
safeRunCommand() {
cmnd="$#" #...insure whitespace passed and preserved
$cmnd
ERROR_CODE=$? #...so we have it for the command we want
if [ ${ERROR_CODE} != 0 ]; then
printf "Error when executing command: '${command}'\n"
exit ${ERROR_CODE} #...consider 'return()' here
fi
}
safeRunCommand $command
command="cp"
safeRunCommand $command
The normal idea would be to run the command and then use $? to get the exit code. However, sometimes you have multiple cases in which you need to get the exit code. For example, you might need to hide its output, but still return the exit code, or print both the exit code and the output.
ec() { [[ "$1" == "-h" ]] && { shift && eval $* > /dev/null 2>&1; ec=$?; echo $ec; } || eval $*; ec=$?; }
This will give you the option to suppress the output of the command you want the exit code for. When the output is suppressed for the command, the exit code will directly be returned by the function.
I personally like to put this function in my .bashrc file.
Below I demonstrate a few ways in which you can use this:
# In this example, the output for the command will be
# normally displayed, and the exit code will be stored
# in the variable $ec.
$ ec echo test
test
$ echo $ec
0
# In this example, the exit code is output
# and the output of the command passed
# to the `ec` function is suppressed.
$ echo "Exit Code: $(ec -h echo test)"
Exit Code: 0
# In this example, the output of the command
# passed to the `ec` function is suppressed
# and the exit code is stored in `$ec`
$ ec -h echo test
$ echo $ec
0
Solution to your code using this function
#!/bin/bash
if [[ "$(ec -h 'ls -l | grep p')" != "0" ]]; then
echo "Error when executing command: 'grep p' [$ec]"
exit $ec;
fi
You should also note that the exit code you will be seeing will be for the grep command that's being run, as it is the last command being executed. Not the ls.