Shell scripting - if statement difference - bash

This a question of an exercise:
What is the difference between the two "if" instructions?
#!/bin/bash
rm tmp
echo -n > tmp
for f in $*
do
if test ! -f $f
then
echo $f does not exist as a file
continue
fi
rm $f
if [ ! -f $f ]
then
echo $f has been deleted successfully
fi
ls $f >> tmp
done
x='cat tmp | grep -c ^.*$'
echo result: $x

The square brackets are a synonym for the test command, instead of if test ! -f $f we can use if [ ! -f $f ]. Note: test is a command which takes expression and test or evaluates.

No difference. test and [ are builtins in most (all?; definitely in dash, bash, yash, ksh, zsh, fish) shells now:
$ type [
[ is a shell builtin
$ type test
test is a shell builtin
There's also executable versions of them:
$ which [
/usr/bin/[
$ which test
/usr/bin/test
Unlike cd, test (or [) doesn't need to be a builtin (at least not for the common options -- some shells' extensions require it to be a builtin), but the fork+exec overhead of an external executable is too much for the little things that test tests.

Related

bash: "logical and" checks if file exists? [duplicate]

Both about -a and -e options in Bash documentation is said:
-a file
True if file exists.
-e file
True if file exists.
Trying to get what the difference is I ran the following script:
resin_dir=/Test/Resin_wheleph/Results
if [ -e ${resin_dir} ] ; then
echo "-e ";
fi
if [ ! -e ${resin_dir} ] ; then
echo "! -e";
fi
if [ -a ${resin_dir} ] ; then
echo "-a";
fi
if [ ! -a ${resin_dir} ] ; then
echo "! -a";
fi
/Test/Resin_wheleph/Results exists and is a directory. And this is what I get:
-e
-a
! -a
which seems to be a little strange (notice -a and ! -a). But when I use double brackets (e. g. if [[ -e ${resin_dir} ]]) in the similar script it gives reasonable output:
-e
-a
So:
What is a difference between -a and -e options?
Why -a produces a strange result when used inside single brackets?
I researched, and this is quite hairy:
-a is deprecated, thus isn't listed in the manpage for /usr/bin/test anymore, but still in the one for bash. Use -e . For single '[', the bash builtin behaves the same as the test bash builtin, which behaves the same as /usr/bin/[ and /usr/bin/test (the one is a symlink to the other). Note the effect of -a depends on its position: If it's at the start, it means file exists. If it's in the middle of two expressions, it means logical and.
[ ! -a /path ] && echo exists doesn't work, as the bash manual points out that -a is considered a binary operator there, and so the above isn't parsed as a negate -a .. but as a if '!' and '/path' is true (non-empty). Thus, your script always outputs "-a" (which actually tests for files), and "! -a" which actually is a binary and here.
For [[, -a isn't used as a binary and anymore (&& is used there), so its unique purpose is to check for a file there (although being deprecated). So, negation actually does what you expect.
The '-a' option to the test operator has one meaning as a unary operator and another as a binary operator. As a binary operator, it is the 'and' connective (and '-o' is the 'or' connective). As a unary operator, it apparently tests for a file's existence.
The autoconf system advises you to avoid using '-a' because it causes confusion; now I see why. Indeed, in portable shell programming, it is best to combine the conditions with '&&' or '||'.
I think #litb is on the right track. When you have '! -a ${resin_dir}', Bash may be interpreting it as "is the string '!' non-empty and is the string in '${resin_dir}' non-empty, to which the answer is yes. The Korn shell has a different view on this, and the Bourne shell yet another view - so stay away from '-a'.
On Solaris 10:
$ bash -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
Bad
$ ksh -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
OK
$ sh -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
sh: test: argument expected
$
The double bracket [[ exp ]] is a bash builtin. In bash -a and -e are the same, probably for some backwards compatibility.
The single bracket [ exp ] is an alias for the external command "test". In "test", -a is a logical AND. Although [ nothing AND $STRING ] looks like it should be false, test has some syntax quirks, which is why I recommend using the bash builtin [[ exp ]], which tends to be more sane.
Note:
bash really does call /bin/[ when you use "[".
$ [ $UNASIGNED_VAR == "bar" ]
bash: [: ==: unary operator expected
the error shows bash called [. An strace also shows "execve("/usr/bin/[", ..."

Shell script to iterate files. Got error on line 3

#!/bin/bash
for dir in /home/username/git/*/
do
for file in "$dir"/*
do
if [[ -f $file ]]
then
echo "$file"
fi
done
done
When I try to run it. I got
syntax error near unexpected toke' `do
'rocTest.sh: line 3: `do
Why?
Use "$file" (with quotes) consistently to deal with "problematic" file names; in particular if [[ -f $file ]] should be
if [[ -f "$file" ]] ...
Note that bash is not always in /bin (e.g. FreeBSD places it in /usr/local/bin); for wider portability, either use
#!/usr/bin/env bash
or #!/bin/sh and make sure to remove bash-isms (e.g. use checkbashisms on Debian/Ubuntu). E.g. write if test -f "$file" instead of [[ -f "$file" ]]

Bash For-Loop on Directories

Quick Background:
$ ls src
file1 file2 dir1 dir2 dir3
Script:
#!/bin/bash
for i in src/* ; do
if [ -d "$i" ]; then
echo "$i"
fi
done
Output:
src/dir1
src/dir2
src/dir3
However, I want it to read:
dir1
dir2
dir3
Now I realize I could sed/awk the output to remove "src/" however I am curious to know if there is a better way of going about this. Perhaps using a find + while-loop instead.
Do this instead for the echo line:
echo $(basename "$i")
No need for forking an external process:
echo "${i##*/}"
It uses the “remove the longest matching prefix” parameter expansion.
The */ is the pattern, so it will delete everything from the beginning of the string up to and including the last slash. If there is no slash in the value of $i, then it is the same as "$i".
This particular parameter expansion is specified in POSIX and is part of the legacy of the original Bourne shell. It is supported in all Bourne-like shells (sh, ash, dash, ksh, bash, zsh, etc.). Many of the feature-rich shells (e.g. ksh, bash, and zsh) have other expansions that can handle even more without involving external processes.
If you do a cd at the start of the script, it should be reverted when the script exits.
#!/bin/bash
cd src
for i in * ; do
if [ -d "$i" ]; then
echo "$i"
fi
done
Use basename as:
if [ -d "$i" ]; then
basename "$i"
fi

Using $# properly

I am trying to write a tiny script that accepts any number of command line arguments that prints out the rwx permissions for a file (not directory)
What I have is
file=$#
if [ -f $file ] ; then
ls -l $file
fi
This accepts only one command line argument however. Thanks for any help.
Here is a demonstration of the some of the differences between $* and $#, with and without quotes:
#/bin/bash
for i in $*; do
echo "\$*: ..${i}.."
done; echo
for i in "$*"; do
echo "\"\$*\": ..${i}.."
done; echo
for i in $#; do
echo "\$#: ..${i}.."
done; echo
for i in "$#"; do
echo "\"\$#\": ..${i}.."
done; echo
Running it:
user#host$ ./paramtest abc "space here"
$*: ..abc..
$*: ..space..
$*: ..here..
"$*": ..abc space here..
$#: ..abc..
$#: ..space..
$#: ..here..
"$#": ..abc..
"$#": ..space here..
How about this one:
for file
do
test -f "$file" && ls -l "$file"
done
The for loop by default will work on $#, so you don't have to mention it. Note that you will need to quote "$file" in case if the file name has embedded space. For example, if you save your script to 'myll.sh':
$ myll.sh "My Report.txt" file1 file2
Then "My Report.txt" will be passed in as a whole token instead of 2 separate tokens: "My" and "Report.txt"
The variable you want is indeed $# - this contains all command-line arguments as separate words, each passed on intact (no expansion). ($* treats all of them as a single word - good luck sorting it out if you have spaces in filenames).
You can loop, if you like. This is easily expanded to more complex actions than ls.
for file in "$#"; do
if [ -f "$file" ]; then
ls -l "$file"
fi
done
Note: you should quote $# to protect any special characters inside! You should also quote $file for the same reason - especially inside the test. If there is an empty string in $#, file will also be empty, and without quotes, -f will attempt to act on the ']'. Errors ensue.
Also, if all you need to do is ls (skipping your if) you can just do this:
ls -l "$#"
You could usefully loop over any files specified like this:
for file in "$#"; do
ls -l "$file"
done
If you want to double-check that the name specified is not a directory, you could do this:
for file in "$#"; do
if [ ! -d "$file" ]; then
ls -l "$file"
fi
done
the bash variable for all arguments passed to a script is "$*". Try:
for file in $*; do
if [ -f $file ] ; then
ls -l $file
fi
done
(not tested)

bash if -a vs -e option

Both about -a and -e options in Bash documentation is said:
-a file
True if file exists.
-e file
True if file exists.
Trying to get what the difference is I ran the following script:
resin_dir=/Test/Resin_wheleph/Results
if [ -e ${resin_dir} ] ; then
echo "-e ";
fi
if [ ! -e ${resin_dir} ] ; then
echo "! -e";
fi
if [ -a ${resin_dir} ] ; then
echo "-a";
fi
if [ ! -a ${resin_dir} ] ; then
echo "! -a";
fi
/Test/Resin_wheleph/Results exists and is a directory. And this is what I get:
-e
-a
! -a
which seems to be a little strange (notice -a and ! -a). But when I use double brackets (e. g. if [[ -e ${resin_dir} ]]) in the similar script it gives reasonable output:
-e
-a
So:
What is a difference between -a and -e options?
Why -a produces a strange result when used inside single brackets?
I researched, and this is quite hairy:
-a is deprecated, thus isn't listed in the manpage for /usr/bin/test anymore, but still in the one for bash. Use -e . For single '[', the bash builtin behaves the same as the test bash builtin, which behaves the same as /usr/bin/[ and /usr/bin/test (the one is a symlink to the other). Note the effect of -a depends on its position: If it's at the start, it means file exists. If it's in the middle of two expressions, it means logical and.
[ ! -a /path ] && echo exists doesn't work, as the bash manual points out that -a is considered a binary operator there, and so the above isn't parsed as a negate -a .. but as a if '!' and '/path' is true (non-empty). Thus, your script always outputs "-a" (which actually tests for files), and "! -a" which actually is a binary and here.
For [[, -a isn't used as a binary and anymore (&& is used there), so its unique purpose is to check for a file there (although being deprecated). So, negation actually does what you expect.
The '-a' option to the test operator has one meaning as a unary operator and another as a binary operator. As a binary operator, it is the 'and' connective (and '-o' is the 'or' connective). As a unary operator, it apparently tests for a file's existence.
The autoconf system advises you to avoid using '-a' because it causes confusion; now I see why. Indeed, in portable shell programming, it is best to combine the conditions with '&&' or '||'.
I think #litb is on the right track. When you have '! -a ${resin_dir}', Bash may be interpreting it as "is the string '!' non-empty and is the string in '${resin_dir}' non-empty, to which the answer is yes. The Korn shell has a different view on this, and the Bourne shell yet another view - so stay away from '-a'.
On Solaris 10:
$ bash -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
Bad
$ ksh -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
OK
$ sh -c 'x=""; if [ ! -a "$x" ] ; then echo OK ; else echo Bad; fi'
sh: test: argument expected
$
The double bracket [[ exp ]] is a bash builtin. In bash -a and -e are the same, probably for some backwards compatibility.
The single bracket [ exp ] is an alias for the external command "test". In "test", -a is a logical AND. Although [ nothing AND $STRING ] looks like it should be false, test has some syntax quirks, which is why I recommend using the bash builtin [[ exp ]], which tends to be more sane.
Note:
bash really does call /bin/[ when you use "[".
$ [ $UNASIGNED_VAR == "bar" ]
bash: [: ==: unary operator expected
the error shows bash called [. An strace also shows "execve("/usr/bin/[", ..."

Resources