curious case of shell exit values - bash

bash# sh -c "true && [[ $? = 0 ]] && echo XXX"
I expect this to echo XXX , as true should set the exit value as 0 .
But I figured out to my amazement that it depends on what the exit value was before we ran sh -c , i.e.
bash# true
bash# sh -c "true && [[ $? = 0 ]] && echo XXX"
returns XXX
but ,
bash# false
bash# sh -c "true && [[ $? = 0 ]] && echo XXX"
doesn't.
Which implies that any command we run inside sh doesn't set the exit value.
is it right behaviour , or am I misunderstanding something ?

$? is expanded inside double quotes, just like any other variable. So it is replaced with the exit code of the previous command before sh even sees it. You probably meant to use single quotes.

Try this:
$ false
$ sh -c 'true && [[ $? -eq 0 ]] && echo XXX'
XXX
Or:
$ false
$ sh -c 'true && [[ "$?" = "0" ]] && echo XXX'
XXX
Make sure that $? is not substituted before hand.

Related

Bash test variable and program exit in same statement

I am trying to test if certain variables are set and if ping exits with 0. When I do
var=1 #set for later
#"If" checks exit status correctly
if ping -c 1 an-inaccessible-thing
then
echo T
else
echo F
fi
#returns F
#"if" does not like the program in the [[ ]]
if [[ -n $var && ping -c 1 an-inaccessible-thing ]]
then
echo T
else
echo F
fi
#returns this error for obvious reasons
-bash: conditional binary operator expected
-bash: syntax error near `-c'
#if runs its test on the output of the shell, not its exit code.
if [[ -n $var && $(ping -c 1 an-inaccessible-thing) ]]
then
echo T
else
echo F
fi
#returns T, probably because it's being evaluated with -n and no the exit code
how can I test programs exit code inside the double square brackets?
Because [[ is a separate (bash builtin) program, like ping or any other program.
Do && outside of [[ ]].
[[ -n $var ]] && ping -c 1 an-inaccessible-thing

Exit always called - variable precedence

I'm new to bash and having an issue where exit is always called in my script. Consider this simple code:
if [[ "$x" -ge 1 && "$x" -le 4 ]]; then
/export/home/scripts/script1.sh \
"$x" \
|| echo "Error.. something went wrong." && exit 1
fi
How can I handle errors, considering && takes precedence over || ?
Using GNU bash, version 3.2.51(1).
Thanks
You can do it like this :
if [[ "$x" -ge 1 && "$x" -le 4 ]]; then
/export/home/scripts/script1.sh \
"$x" \
|| { echo "Error.. something went wrong." && exit 1 ; }
fi
Note : I used { ; }, instead of (), because () will open your command in a subshell, so it will not exit.
&& and || have the same precedence in shell; the implicit parenthesization is (a || b) && c, not a || (b && c). Mixing || and && in the same list is rarely a good idea; use an explicit if statement.
if [[ "$x" -ge 1 && "$x" -le 4 ]]; then
if ! /export/home/scripts/script1.sh "$x"; then
echo "Error.. something went wrong"
exit 1
fi
fi
For arithmetic comparisons, prefer the arithemetic command ((...)) over [[ ... ]] for readability.
if (( x >= 1 && x <= 4 )); then
You can use braces to regroup commands without creating a new subshell :
{ true || false; } && echo true || echo false # echoes true
{ false || false; } && echo true || echo false # echoes false
Its syntax is pretty annoying : the opening brace must be followed by a space (or another character of $IFS, such as a linefeed or a tab), and the closing brace must be preceded by a linefeed or a ;, denoting the end of the last command of the block.
Parenthesis don't have those difficulties, but they will execute their instructions in a subshell, which has multiple other effects :
calling exit will only exit the subshell, not the shell running your script : (exit) is a no-op
updating variables will only apply to the subshell and will have no effect on the values known to your script : a=0;( (( a++ )) ; echo $a) ; echo $a will echo 1 from the subshell, then 0 from the outer shell.
I prefer doing explicit tests on scripts using if so that I can clean up after myself if things go pear shaped. Helps keep the code looking cleaner, too.
if [[ "$x" -ge 1 && "$x" -le 4 ]]; then
if ! /export/home/scripts/script1.sh "$x"; then
err="Error.. something went wrong."
test -t 0 && echo "$err" >&2 # send errors to stderr if on terminal
logger -p local0.critical -t $(hostname -s) "$err" # send to syslog
# You could even add some code here to clean up after script1.sh.
exit 1
fi
fi

syntax error with using test -a in unix shell script

I am new to shell script. I tried to run the following but it gives me syntax error. What is wrong?
[[ $# -gt 0 -a $(file -b $1) != 'JPEG'* ]]
Well, let's see:
$ shellcheck myscript
In myscript line 1:
[[ $# -gt 0 -a $(file -b $1) != 'JPEG'* ]]
^-- SC2108: In [[..]], use && instead of -a.
There you go. Here's a complete example (bring your own test files):
#!/bin/bash
if [[ $# -gt 0 && $(file -b $1) != 'JPEG'* ]]
then
echo "true"
else
echo "false"
fi
in action:
$ chmod +x myscript
$ ./myscript
false
$ ./myscript test.jpg
false
$ ./myscript test.pdf
true

BASH: Why `printf` returns 1?

I have a problem that gaslights me.
Here comes my bash script "foo" reduced to the problem:
#!/bin/bash
function Args()
{
[[ "$1" == "-n" ]] && [[ -d "$2" ]] && printf "%s\n" "new ${3}"
[[ "$1" == "-p" ]] && [[ -d "$2" ]] && printf "%s\n" "page ${3}"
}
[[ $# -eq 3 ]] && Args "$#"
echo $?
Now when I execute this code, the following happens:
$ ./foo -n / bar
new bar
1
This, however, works:
$ ./foo -p / bar
page bar
0
Please, can anybody explain?
Sorry if this is a known "thing" and my googleing skills must be improved...
It is returning 1 in first case only because 2nd condition:
[[ "$1" == "-p" ]] && [[ -d "$2" ]] && printf "%s\n" "page ${3}"
won't match/apply when you call your script as:
./foo -n / bar
And due to non-matching of 2nd set of conditions it will return 1 to you since $? represents most recent command's exit status which is actually exit status of 2nd set of conditions.
When you call your script as:
./foo -p / bar
It returns status 0 to you since 2nd line gets executed and that is also the most recently executed one.

Bash remote files system directory test

the more I learn bash the more questions I have, and the more I understand why very few people do bash. Easy is something else, but I like it.
I have managed to figure out how to test directories and there writablity, but have a problem the minute I try to do this with a remote server over ssh. The first instance testing the /tmp directory works fine, but when the second part is called, I get line 0: [: missing]'`
Now if I replace the \" with a single quote, it works, but I thought that single quotes turn of variable referencing ?? Can someone explain this to me please ? Assuming that the tmp directory does exist and is writable, here the script so far
#!/bin/bash
SshHost="hostname"
SshRsa="~/.ssh/id_rsa"
SshUser="user"
SshPort="22"
Base="/tmp"
Sub="one space/another space"
BaseBashExist="bash -c \"[ -d \"$Base\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseBashExist} )
echo -n $Base
if [ $? -eq 0 ]
then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
BaseBashPerm="bash -c \"[ -w \"$Base\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseBashPerm} )
if [ $? -eq 0 ]
then
echo "...writeable"
else
echo "...not writeable"
fi
BaseAndSub="$Base/$Sub"
BaseAndSubBashExist="bash -c \"[ -d \"$BaseAndSub\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseAndSubExist=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseAndSubBashExist} )
echo -n $BaseAndSub
if [ $? -eq 0 ]
then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
BaseAndSubBashPerm="bash -c \"[ -w \"$BaseAndSub\" ] && echo 0 && exit 0 || echo 1 && exit 1\""
SSHBaseAndSubPerm=$( ssh -l $SshUser -i $SshRsa -p $SshPort $SshHost ${BaseAndSubBashPerm} )
if [ $? -eq 0 ]
then
echo -n "...writeable"
else
echo "...not writeable"
fi
exit 0
The first thing you should do is refactor your code with simplicity in mind, then the quoting error will go away as well. Try:
if ssh [flags] test -w "'$file'"; then
Encapsulate your SSH flags in a ssh config to facilitate re-use, and your script will shorten dramatically.
You are fine with single quotes in this context; by the time the script is seen by the remote bash, your local bash has already substituted in the variables you want to substitute.
However, your script is a total mess. You should put the repetitive code in functions if you cannot drastically simplify it.
#!/bin/bash
remote () {
# most of the parameters here are at their default values;
# why do you feel you need to specify them?
#ssh -l "user" -i ~/.ssh/id_rsa -p 22 hostname "$#"
ssh hostname "$#"
# —---------^
# if you really actually need to wrap the remote
# commands in bash -c "..." then add that here
}
exists_and_writable () {
echo -n "$1"
if remote test -d "$1"; then
echo -n "...OK..."
else
echo "...FAIL"
exit 1
fi
if remote test -w "$1"; then
echo "...writeable"
else
echo "...not writeable"
fi
}
Base="/tmp"
# Note the need for additional quoting here
Sub="one\\ space/another\\ space"
exists_and_writable "$Base"
BaseAndSub="$Base/$Sub"
exist_and_writable "$BaseAndSub"
exit 0
ssh -qnx "useraccount#hostname"
"test -f ${file absolute path} ||
echo ${file absolute path} no such file or directory"

Resources