syntax error with using test -a in unix shell script - bash

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

Related

curious case of shell exit values

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.

Unexpected if statement behaviour

I was running a small bash script, but I couldn't figure out why it was entering a if block even when condition should be false.
$ cat script.sh
#!/bin/bash
if [[ "$#"="-h" || "$#"="--help" ]]
then
echo 'Show help'
exit
fi
echo 'Do stuff'
$ ./script.sh
Show help
$ bash -x script.sh
+ [[ -n =-h ]]
+ echo 'Show help'
Show help
+ exit
$ bash -x script.sh -option
+ [[ -n -option=-h ]]
+ echo 'Show help'
Show help
+ exit
So why is $# equal to -n when I didn't pass any arguments? Also even if it is, how does -n =-h evaluate to true? When I do pass an argument -option, why is it evaluated to true, too?
Whitespace is significant. Spaces between the arguments to [[ are mandatory.
if [[ "$#" = "-h" || "$#" = "--help" ]]
Also, "$#" means "all of the command-line arguments". It would be better to just check a single argument.
if [[ "$1" = "-h" || "$1" = "--help" ]]
And for what it's worth, variable expansions in [[ don't have to be quoted. It doesn't hurt, and quoting your variables actually a good habit to develop, but if you want you can remove the quotes.
if [[ $1 = -h || $1 = --help ]]
[[ string ]] return true if string is not empty, i.e. it's a shorcut for
[[ -n string ]]
In your case, the string was =-h, that's why you see
[[ -n =-h ]]
To test for string equiality, you have to use the = (or ==) operator, that must be preceded and followed by whitespace.
[[ "$#" = "-h" ]]
Note that "$#" means all the arguments:
set -- a b c
set -x
[[ "$#" == 'a b c' ]] && echo true
gives
+ [[ a b c == \a\ \b\ \c ]]
+ echo true
true
The other answers have already explained the problems with your code. This one shows that
bashisms such as [[ ... ]] are not needed,
you can gain flexibility by using a for loop to check whether at least one of the command-line argument matches -h or --help.
Script
#!/bin/sh
show_help=0
for arg in "$#"; do
shift
case "$arg" in
"--help")
show_help=1
;;
"-h")
show_help=1
;;
*)
;;
esac
done
if [ $show_help -eq 1 ]; then
printf "Show help\n"
exit
fi
Tests
After making the script (called "foo") executable, by running
chmod u+x foo
I get the following results
$ ./foo
$ ./foo -h
Show help
$ ./foo --help
Show help
$ ./foo bar
$ ./foo bar --help
Show help
$ ./foo bar --help baz -h
Show help

Bash, test for presence of two files

I found this answer which works fine, but I wanted to understand why the following code won't detect the presence of two files?
if [[ $(test -e ./file1 && test -e ./file2) ]]; then
echo "yep"
else
echo "nope"
fi
Running this directly from the shell works as expected:
test -e ./file1 && test -e ./file2 && echo yes
The output of test -e ./file1 && test -e ./file2 is an empty string, which causes [[ ]] to produce a non-zero exit code. You want
if [[ -e ./file1 && -e ./file2 ]]; then
echo "yep"
else
echo "nope"
fi
[[ ... ]] is a replacement for [ ... ] or test ..., not a wrapper around it.
if executes a program (or builtin, in the case of [[) and branches based on its return value. You need to omit either the [[ ]] or the tests:
if [[ -e ./file1 && -e ./file2 ]]; then
Or
if test -e ./file1 && test -e ./file2; then

Using sh -c if comparisons inside an SSH command

I'm having some trouble with the != section of the if statement. Essentially this statement is valid as far as I'm aware, however executing this gives [: 1: !=: unexpected operator. I've tried executing using -n but for whatever reason, even if the output is blank, using -n still runs the echo command.
Any help on this is appreciated. I've attached the code snippet below.
#!/bin/sh
HOST=$1
USER="/scripts/whoowns $HOST | tr -d '\r'"
ssh -t $HOST -p 22 -l deehem "sh -c 'if [ "" != "\`$USER\`" ]; then echo "Username for $HOST: \`$USER\`"; fi' ; bash -login"
As you realised, "bla "" bla" is just two strings concatenated ("bla bla").
You can escape the ": \", but the standard [ test tool has an option specifically for this task:
-n STRING
the length of STRING is nonzero
-z STRING
the length of STRING is zero
Note: why do you have \`? That way the string is never going to be empty....
In regards to your "however executing this gives [: 1: !=: unexpected operator" problem, here is a solution:
Use bash -c instead of sh -c. The bash program seems to be better than sh at handling square brackets. For example:
ubuntu#ubuntu:/$ echo "antipetalous" | if [[ "antipetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi
match
ubuntu#ubuntu:/$ echo "antepetalous" | if [[ "antepetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi
no
ubuntu#ubuntu:/$ sh -c 'echo "antipetalous" | if [[ "antipetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi'
sh: 1: [[: not found
no
ubuntu#ubuntu:/$ bash -c 'echo "antipetalous" | if [[ "antipetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi'
match
ubuntu#ubuntu:/$ bash -c 'echo "antepetalous" | if [[ "antepetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi'
no
ubuntu#ubuntu:/$ sh -c 'echo "antepetalous" | if [[ "antepetalous" =~ "ti" ]]; then echo "match"; else echo "no"; fi'
sh: 1: [[: not found
no
ubuntu#ubuntu:/$
So ssh -t $HOST -p 22 -l deehem "sh -c 'if [ "" != ... would become ssh -t $HOST -p 22 -l deehem "bash -c 'if [ "" != ....
Thanks to pLumo at https://askubuntu.com/questions/1310106/sh-c-sh-not-working-when-using-if-statement-and-having-in-the-filena ("command line - sh -c '...' sh {} not working when using if statement and having ' in the filename - Ask Ubuntu").

Test multiple file conditions in one swoop (BASH)?

Often when writing for the bash shell, one needs to test if a file (or Directory) exists (or doesn't exist) and take appropriate action. Most common amongst these test are...
-e - file exists, -f - file is a regular file (not a directory or device file), -s - file is not zero size, -d - file is a directory, -r - file has read permission, -w - file has write, or -x execute permission (for the user running the test)
This is easily confirmed as demonstrated on this user-writable directory....
#/bin/bash
if [ -f "/Library/Application Support" ]; then
echo 'YES SIR -f is fine'
else echo 'no -f for you'
fi
if [ -w "/Library/Application Support" ]; then
echo 'YES SIR -w is fine'
else echo 'no -w for you'
fi
if [ -d "/Library/Application Support" ]; then
echo 'YES SIR -d is fine'
else echo 'no -d for you'
fi
➝ no -f for you ✓
➝ YES SIR -w is fine ✓
➝ YES SIR -d is fine ✓
My question, although seemingly obvious, and unlikely to be impossible - is how to simply combine these tests, without having to perform them separately for each condition... Unfortunately...
if [ -wd "/Library/Application Support" ]
▶ -wd: unary operator expected
if [ -w | -d "/Library/Application Support" ]
▶ [: missing `]'
▶ -d: command not found
if [ -w [ -d "/Library.... ]] & if [ -w && -d "/Library.... ]
▶ [: missing `]'
➝ no -wd for you ✖
➝ no -w | -d for you ✖
➝ no [ -w [ -d .. ]] for you ✖
➝ no -w && -d for you ✖
What am I missing here?
You can use logical operators to multiple conditions, e.g. -a for AND:
MYFILE=/tmp/data.bin
if [ -f "$MYFILE" -a -r "$MYFILE" -a -w "$MYFILE" ]; then
#do stuff
fi
unset MYFILE
Of course, you need to use AND somehow as Kerrek(+1) and Ben(+1) pointed it out. You can do in in few different ways. Here is an ala-microbenchmark results for few methods:
Most portable and readable way:
$ time for i in $(seq 100000); do [ 1 = 1 ] && [ 2 = 2 ] && [ 3 = 3 ]; done
real 0m2.583s
still portable, less readable, faster:
$ time for i in $(seq 100000); do [ 1 = 1 -a 2 = 2 -a 3 = 3 ]; done
real 0m1.681s
bashism, but readable and faster
$ time for i in $(seq 100000); do [[ 1 = 1 ]] && [[ 2 = 2 ]] && [[ 3 = 3 ]]; done
real 0m1.285s
bashism, but quite readable, and fastest.
$ time for i in $(seq 100000); do [[ 1 = 1 && 2 = 2 && 3 = 3 ]]; done
real 0m0.934s
Note, that in bash, "[" is a builtin, so bash is using internal command not a symlink to /usr/bin/test exacutable. The "[[" is a bash keyword. So the slowest possible way will be:
time for i in $(seq 100000); do /usr/bin/\[ 1 = 1 ] && /usr/bin/\[ 2 = 2 ] && /usr/bin/\[ 3 = 3 ]; done
real 14m8.678s
You want -a as in -f foo -a -d foo (actually that test would be false, but you get the idea).
You were close with & you just needed && as in [ -f foo ] && [ -d foo ] although that runs multiple commands rather than one.
Here is a manual page for test which is the command that [ is a link to. Modern implementations of test have a lot more features (along with the shell-builtin version [[ which is documented in your shell's manpage).
check-file(){
while [[ ${#} -gt 0 ]]; do
case $1 in
fxrsw) [[ -f "$2" && -x "$2" && -r "$2" && -s "$2" && -w "$2" ]] || return 1 ;;
fxrs) [[ -f "$2" && -x "$2" && -r "$2" && -s "$2" ]] || return 1 ;;
fxr) [[ -f "$2" && -x "$2" && -r "$2" ]] || return 1 ;;
fr) [[ -f "$2" && -r "$2" ]] || return 1 ;;
fx) [[ -f "$2" && -x "$2" ]] || return 1 ;;
fe) [[ -f "$2" && -e "$2" ]] || return 1 ;;
hf) [[ -h "$2" && -f "$2" ]] || return 1 ;;
*) [[ -e "$1" ]] || return 1 ;;
esac
shift
done
}
check-file fxr "/path/file" && echo "is valid"
check-file hf "/path/folder/symlink" || { echo "Fatal error cant validate symlink"; exit 1; }
check-file fe "file.txt" || touch "file.txt" && ln -s "${HOME}/file.txt" "/docs/file.txt" && check-file hf "/docs/file.txt" || exit 1
if check-file fxrsw "${HOME}"; then
echo "Your home is your home from the looks of it."
else
echo "You infected your own home."
fi
Why not write a function to do it?
check_file () {
local FLAGS=$1
local PATH=$2
if [ -z "$PATH" ] ; then
if [ -z "$FLAGS" ] ; then
echo "check_file: must specify at least a path" >&2
exit 1
fi
PATH=$FLAGS
FLAGS=-e
fi
FLAGS=${FLAGS#-}
while [ -n "$FLAGS" ] ; do
local FLAG=`printf "%c" "$FLAGS"`
if [ ! -$FLAG $PATH ] ; then false; return; fi
FLAGS=${FLAGS#?}
done
true
}
Then just use it like:
for path in / /etc /etc/passwd /bin/bash
{
if check_file -dx $path ; then
echo "$path is a directory and executable"
else
echo "$path is not a directory or not executable"
fi
}
And you should get:
/ is a directory and executable
/etc is a directory and executable
/etc/passwd is not a directory or not executable
/bin/bash is not a directory or not executable
This seems to work (notice the double brackets):
#!/bin/bash
if [[ -fwd "/Library/Application Support" ]]
then
echo 'YES SIR -f -w -d are fine'
else
echo 'no -f or -w or -d for you'
fi

Resources