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.
Related
I am trying to call an external bash script in an if condition in my main script.
The code of the external script IsArchive:
#!/bin/bash
STR="$1"
if [[ "$STR" == *".zip"* ]] || [[ "$STR" == *".iso"* ]] || [[ "$STR" == *".tar.gxz"* ]] || [[ "$STR" == *".tar.gx"* ]] || [[ "$STR" == *".tar.bz2"* ]] || \
[[ "$STR" == *".tar.gz"* ]] || [[ "$STR" == *".tar.xz"* ]] || [[ "$STR" == *".tgz"* ]] || [[ "$STR" == *".tbz2"* ]]
then
return 0
else
return 1
fi
and I try calling it in my main script as:
elif [[ $Option = "2" ]]
then
if IsArchive "$SourcePath";
then
less -1Ras "$SourcePath" | tee "$OutputFilePath"
#if file is not an archive
else
ls -1Rasl "$SourcePath" | tee "$OutputFilePath"
fi
when I execute the main script I receive the Error: ./script: line 61: IsArchive: command not found
You just need to make sure that the script is in your PATH. Either that, or reference it with either a full path or a relative path. Perhaps you just need to write:
if ./IsArchive "$SourcePath"; then ...
But there are several issues with IsArchive. You cannot return except from a function, so you probably want to use exit 0 and exit 1 instead of return. You probably don't want to consider a name like foo.zipadeedoodah to be an archive, but *".zip"* will match that, so you should probably remove the trailing *. It would be simpler to write it with a case statement:
#!/bin/bash
case "$1" in
*.zip|*.iso|*.tar.gxz|*.tar.gx|*.tar.bz2| \
*.tar.gz|*.tar.xz|*.tgz|*.tbz2) exit 0;;
*) exit 1;;
esac
I've the below code:
#!/bin/bash
. ~/.bash_profile
dt=$(date +"%Y.%m.%d")
if [[ "$#" -eq '0' ]] || [[ "$#" -eq '1' ]]
then
echo -e "Not correct usage"
exit 1
fi
tar zvxf $SHARE/*.gz -C $SHARE/landing/
sleep 3
ops () {
//
some sets of command
//
}
while read -r line
do
if [[ "$1" == "A" ]] || [[ "$1" == "B" ]] || [[ "$1" == "C" ]] && [[ "$2" =~ "$line" ]] || [[ "$2" == "ALL" ]]
then
ops
fi
done < $SHARE/landing/dir/ApprovedList."$1"
When i run the script, i can see that it's able to untar the .gz file and place it into the folder :$SHARE/landing/
But i guess it's not able to read the file and perform the operations inside the ops function, below is the last line of output i'm getting after running the script in interactive mode:
+ read -r line
Any help is most welcome!!!
Please show the input format, the argument format, and some explanation of those. In the meantime, tweaked for readability and error handling - edit to taste. YMMV.
#!/bin/bash
. ~/.bash_profile
ops () { # do some stuff
: some sets of commands ...
}
die() {
printf "%s\n\n Use: $0 x y z ...\n\n" "$1" >&2
kill -term $$ # exit in function behaves like return
}
# dt=$(date +"%Y.%m.%d") # unused
case "$#" in
0|1) die "Insufficient arguments" ;;
esac
tar zvxf "$SHARE"/*.gz -C "$SHARE/landing/" || die "tar failed"
sleep 3
infile="$SHARE/landing/dir/ApprovedList.$1"
[[ -e "$infile" ]] || die "$infile does not exist"
[[ -r "$infile" ]] || die "$infile is nor readable by $0 as $LOGNAME"
while read -r line
do case "$1" in
A|B|C) case "$2" in
*"$line"*|ALL) ops ;;
*) : data does not match, skipping ;;
esac ;;
*) die "Invalid arg1 '$1'";;
esac
done < "$infile"
Notes:
If you require exactly 2 arguments, then let's check exactly that:
die() {
printf "%s\n\n Use: $0 x y\n\n" "$1" >&2
kill -term $$ # exit in function behaves like return
}
and
case "$#" in
2) ;;
*) die "Incorrect # of arguments" ;;
esac
Also, better than the kill - add a trap at the top:
trap 'echo >&2 "ERROR in $0($BASH_SOURCE) at line $LINENO: [$BASH_COMMAND]"; exit $LINENO;' ERR
and use a literal error return in the function.
die() {
printf "%s\n\n Use: $0 x y\n\n" "$1" >&2
return 1
}
The check (except for [[ "$2" =~ "$line" ]])
if [[ "$1" == "A" ]] || [[ "$1" == "B" ]] || [[ "$1" == "C" ]] && [[ "$2" =~ "$line" ]] || [[ "$2" == "ALL" ]]
doesn't change inside the while-loop. So better check once before entering the while-loop.
So now you want to perform ops for each line of $SHARE/landing/dir/ApprovedList."$1".
You can use
xargs -a "$SHARE/landing/dir/ApprovedList.$1" -L 1 ops
EDIT:
When you have a simple check to perform for each line, you can move this check into the ops function.
First save $2 before entering the function:
to_be_checked="$2"
...
ops() {
[[ "$0" =~ "${to_be_checked}" ]] && return
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
Is it possible to override Bash's test builtin? So that
[[ $1 = 'a' ]]
not just does the test but also outputs which result was expected when it fails? Something like
echo "Expected $1 to be a.'
EDIT
I know this is bad :-).
The test expression compound command does real short-circuiting that affects all expansions.
$ set -x
$ [[ 0 -gt x=1+1 || ++x -eq $(tee /dev/fd/3 <<<$x) && $(echo 'nope' >&3) ]] 3>&1
+ [[ 0 -gt x=1+1 ]]
++ tee /dev/fd/2
2
+ [[ ++x -eq 2 ]]
So yes you could do anything in a single test expression. In reality it's quite rare to have a test produce a side-effect, and almost never used to produce output.
Also yes, reserved words can be overridden. Bash is more lenient with ksh-style function definitions than POSIX style (which still allows some invalid names).
function [[ { [ "${#:1:${##}-1}" ]; }; \[[ -a -o -a -o -a ]] || echo lulz
Yet another forky bomb.
if function function if function if if \function & then \if & fi && \if & then \function & fi && then \function fi
Something like this?
if [[ $1 == 'a' ]]; then
echo "all right";
else
echo 'Expected $1 to be "a"'
fi
Anyway, what's the point of the test if you only expect one answer? Or do you mean that for debugging purposes?
[[ 'a' = 'a' ]] || echo "failed"
[[ 'b' = 'a' ]] || echo "failed"
failed
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