In my projects I use the pattern
if [ -f "$path" ] || [ -d "$path" ]; then
echo "$path exists"
fi
to check whether $path exists on the file system, regardless of whether it's a file or directory. Is this exactly equivalent to just doing
if [ -e "$path" ]; then
echo "$path exists"
fi
? What about for special files such as symbolic links or devices? Are there any platform dependent details to be aware of? I use Bash, but I would like to know about subtle differences between shells.
No, it's not. -e pathname only tests if pathname resolves to an existing directory entry. -f and -d, on the other hand, also tests if the entry is for a regular file and a directory, respectively.
For example, if pathname resolves to a FIFO (or a block special file, or a character device, or a socket, etc.), [ -e pathname ] returns true; but [ -f pathname ] || [ -d pathname ] returns false.
As for symbolic links, neither -e, -f, nor -d differentiates them from entries they resolve to. If pathname names a regular file, a directory, or a symbolic link to a regular file or a directory, both [ -e pathname ] and [ -f pathname ] || [ -d pathname ] return true. And if pathname names a symbolic link to an entry that doesn't exist (i.e. a broken symbolic link), both return false.
Related
I have created a macOS Service with Automator which actually will attach every file from the Finder to a new Thunderbird compose window and is just a simple bash script.
for f in "$#"
do
open -a /Applications/Thunderbird.app/ "$f"
done
This Service also would work for any folder, but for sure you can not attach a folder to a compose window. But my idea is now to let the script detect if the file is a document or a folder. If it is a document, attach it. If it is a folder, first zip compress it and than attach it. In the way:
if file is folder than
// zip compress folder
// attach *.zip to Thunderbird compose window
else // seems to be a document
// attach document to Thunderbird compose window
But how do I detect if the file is a folder and than compress it as zip file in the bash script?
if [[ -d "$file" ]]; then
# do your thing for the directory
else
# do the other thing for the file
fi
For more details, please see this related question: How do I tell if a regular file does not exist in Bash?
Code:
#!/bin/bash
if [ -d "$f" ]; then
upload_file="$f.zip"
# zip compress folder
zip "$f.zip" "$f"
elif [ -f "$f" ]; then # seems to be a document
upload_file="$f.zip"
else # Unknown file type
echo "Unknown file type." 1>&2
exit 1
fi
# attach file to Thunderbird compose window
open -a /Applications/Thunderbird.app/ "$upload_file"
exit 0
Explanation:
In bash "folders" are referred to as "directories." You should checkout the man page on test.
$ man test
The relevant section for you is:
NAME
test, [ -- condition evaluation utility
SYNOPSIS
test expression
[ expression ]
...
-d file True if file exists and is a directory.
-e file True if file exists (regardless of type).
-f file True if file exists and is a regular file.
To test if a file is a directory:
test -d "$f"
OR
[ -d "$f" ]
To test if a file is a regular file:
test -f "$f"
OR
[ -f "$f" ]
Edit: Quoted variables in sample code to avoid globbing and word splitting.
This command [ -f "$filename" ] will return true for files, while [ -d "$dirname" ] will return true for directories.
I would suggest using a check for file as well, because you could have things that are neither directories nor files.
I would approach it this way:
if [ -d "$fileDirectory" ]; then myCommandDirectories;
elif [ -f "$fileDirectory" ]; then myCommandFiles;
elif [ -z "$fileDirectory" ]; then myCommandEmptyArgument;
else myCommandNotFileDirectory; fi
On the code above, the syntax if [ -d ... ] would test if the argument is a directory, the syntax if [ -f ... ] would test if the argument is a file, the syntax if [ -z ... ] would test if the argument is unset or set to the empty string, and if the argument is none of those, you could still execute a certain command/script (in the exemple above myCommandNotFileDirectory).
Note: I included checking for an empty string, even if that was not asked on the question, because this is a "quality/error" control test I would normally do -- the variable "$fileDirectory" should never be empty on this context, and if it is, I would like to know that (it would show me that the script is not working properly), and thus I normally would redirect that command to a log file, like this:
elif [ -z "$fileDirectory" ]; then somecommand && echo "empty fileDirectory string ocurred" >> /var/log/mylog;
I currently have this bash script (which is located in my home directory, i.e., /home/username/ and I am running it as root as it's necessary for the icon copying lines):
cd /home/username/Pictures/Icon*
declare -a A={Arch,Debian,Fedora,Mageia,Manjaro,OpenSUSE}
declare -a B={Adwaita,Faenza,gnome,Humanity}
for i in $A; do
for j in $B; do
if test -e /usr/share/icons/$j/scalable ; else
mkdir /usr/share/icons/$j/scalable/
fi
if test -e /usr/share/icons/$j/scalable/$i.svg ; else
cp -a $i*.svg /usr/share/icons/$j/scalable/$i.svg
fi
done
done
What I want this script to do is to copy icons from my Pictures/Icons and logos directory to the scalable theme (specified in $B) subdirectories in /usr/share/icons. Before it does this, however, I'd like it to create a scalable directory in these theme subdirectories if it does not already exist. The problem is that the else part of the conditionals is not being read properly, as I keep receiving this error:
./copyicon.sh: line 8: syntax error near unexpected token `else'
./copyicon.sh: line 8: ` if test -e /usr/share/icons/$j/scalable ; else'
If you're wondering why the test -e ... in the conditional it's based on a textbook on bash scripting I've been following.
Checking file and/or directory existence
To check whether a file exists in bash, you use the -f operator. For directories, use -d. Example usage:
$ mkdir dir
$ [ -d dir ] && echo exists!
exists!
$ rmdir dir
$ [ -d dir ] && echo exists!
$ touch file
$ [ -f file ] || echo "doesn't exist..."
$ rm file
$ [ -f file ] || echo "doesn't exist..."
doesn't exist...
For more information simply execute man test.
A note on -e, this test operator checks whether a file exists. While this may seem like a good choice, it's better to use -f which will return false if the file isn't a regular file. /dev/null for example is a file but nor a regular file. Having the check return true is undesired in this case.
A note on variables
Be sure to quote variables too, once you have a space or any other special character contained in a variable it can have undesired side effects. So when you test for existence of files and directories, wrap the file/dir in double quotes. Something like [ -f "/path/to/some/${dir}/" ] will work while the following would fail if there is a space in dir: [ -f /path/to/some/${dir}/ ].
Fixing the syntax error
You are experiencing a syntax error in the control statements. A bash if clause is structured as following:
if ...; then
...
fi
Or optional with an else clause:
if ...; then
...
else
...
fi
You cannot omit the then clause. If you wish to only use the else clause you should negate the condition. Resulting in following code:
if [ ! -f "/usr/share/icons/$j/scalable" ]; then
mkdir "/usr/share/icons/$j/scalable/"
fi
Here we add an exclamation point (!) to flip the expression's evaluation. If the expression evaluates to true, the same expression preceded by ! will return false and the other way around.
You can't skip the then part of the if statement, easiest solution would be to just negate the test
if [[ ! -e /usr/share/icons/${j}/scalable ]] ; then
mkdir /usr/share/icons/${j}/scalable/
fi
if [[ ! -e /usr/share/icons/${j}/scalable/${i}.svg ]] ; then
cp -a ${i}*.svg /usr/share/icons/${j}/scalable/${i}.svg
fi
I left it with -e (exists), but you might consider using -d for directories or -f for files and some error handling to catch stuff (e.g. /usr/share/icons/$j/scalable/ exists, but is a file and not a directory for whatever reason.)
I also noticed that in your original code you are potentially trying to copy multiple files into one:
cp -a $i*.svg /usr/share/icons/$j/scalable/$i.svg
I left it that way in my example in case you are sure that it is always only one file and are intentionally renaming it. If not I'd suggest only specifying a target directory.
What does the following bash script mean:
if [ -d "directory name" -a ! -L "directory name" ]; then
# do something
fi
I can understand up to here:
if [ -d "directory name"
but I'm lost after that. Extra consideration if, in addition to explanation, docs that explain -a ! -L
The -L operator tests whether its argument is a symbolic link. It can also be written as -h.
The ! is the logical negation operator, and -a is logical "and".
So this:
if [ -d "directory name" -a ! -L "directory name" ]; then
means "if whatever is a directory and is not a symbolic link". (-d will return true if the target is a symbolic link to a directory.
The [ syntax is actually a synonym for the test command. Either man test or info test on your system should show you the documentation. [ is also a built-in command in bash, so info bash will also show you the documentation; search for
`test'
-d is "directory exists," but you seem to know that.
-a is "logical and."
! is "expression is false"
-L is "file exists and is a symbolic link (same as -h)"
So in english this would read
If "directory name" exists and is a directory and "directory name" exists and is not a symobolic link, then...
The documentation you want is man test.
I believe the second portion is:
...and (-a) "directory name" is not (!) a symbolic link (-L)
The -a is an extension to the POSIX standard version of test to serve as a boolean AND operator. Its use is discouraged by the standard itself (see the Application Usage section) in favor of two separate test commands using the regular shell && operator.
if [ -d "directory name" ] && [ ! -L "directory name" ]; then
# do something
fi
The linked page also defines the -d, ! and -L operators:
-d pathname
True if pathname resolves to an existing directory entry for a directory. False if pathname cannot be resolved, or if pathname resolves to an existing directory entry for a file that is not a directory.
! expression
True if expression is false. False if expression is true.
-L pathname
True if pathname resolves to an existing directory entry for a symbolic link. False if pathname cannot be resolved, or if pathname resolves to an existing directory entry for a file that is not a symbolic link. If the final component of pathname is a symbolic link, that symbolic link is not followed.
[ -d FILE ] - True if FILE exists and is a directory.
[ -a File] - True if FILE exists.
[ -L FILE ] - True if FILE exists and is a symbolic link.
[ ! EXPR ] - True if EXPR is false.
I got a recursive script which iterates a list of names, some of which are files and some are directories.
If it's a (non-empty) directory, I should call the script again with all of the files in the directory and check if they are legal.
The part of the code making the recursive call:
if [[ -d $var ]] ; then
if [ "$(ls -A $var)" ]; then
./validate `ls $var`
fi
fi
The part of code checking if the files are legal:
if [[ -f $var ]]; then
some code
fi
But, after making the recursive calls, I can no longer check any of the files inside that directory, because they are not in the same directory as the main script, the -f $var if cannot see them.
Any suggestion how can I still see them and use them?
Why not use find? Simple and easy solution to the problem.
Always quote variables, you never known when you will find a file or directory name with spaces
shopt -s nullglob
if [[ -d "$path" ]] ; then
contents=( "$path"/* )
if (( ${#contents[#]} > 0 )); then
"$0" "${contents[#]}"
fi
fi
you're re-inventing find
of course, var is a lousy variable name
if you're recursively calling the script, you don't need to hard-code the script name.
you should consider putting the logic into a function in the script, and the function can recursively call itself, instead of having to spawn an new process to invoke the shell script each time. If you do this, use $FUNCNAME instead of "$0"
A few people have mentioned how find might solve this problem, I just wanted to show how that might be done:
find /yourdirectory -type f -exec ./validate {} +;
This will find all regular files in yourdirectory and recursively in all its sub-directories, and return their paths as arguments to ./validate. The {} is expanded to the paths of the files that find locates within yourdirectory. The + at the end means that each call to validate will be on a large number of files, instead of calling it individually on each file (wherein the + is replaced with a \), this provides a huge speedup sometimes.
One option is to change directory (carefully) into the sub-directory:
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ./validate $(ls))
fi
fi
The outer parentheses start a new shell so the cd command does not affect the main shell. The exec replaces the original shell with (a new copy of) the validate script. Using $(...) instead of back-ticks is sensible. In general, it is sensible to enclose variable names in double quotes when they refer to file names that might contain spaces (but see below). The $(ls) will list the files in the directory.
Heaven help you with the ls commands if any file names or directory names contain spaces; you should probably be using * glob expansion instead. Note that a directory containing a single file with a name such as -n would trigger a syntax error in your script.
Corrigendum
As Jens noted in a comment, the location of the shell script (validate) has to be adjusted as you descend the directory hierarchy. The simplest mechanism is to have the script on your PATH, so you can write exec validate or even exec $0 instead of exec ./validate. Failing that, you need to adjust the value of $0 — assuming your shell leaves $0 as a relative path and doesn't mess around with converting it to an absolute path. So, a revised version of the code fragment might be:
# For validate on PATH or absolute name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec $0 $(ls))
fi
fi
or:
# For validate not on PATH and relative name in $0
if [[ -d "$var" ]] ; then
if [ "$(ls -A $var)" ]; then
(cd "$var"; exec ../$0 $(ls))
fi
fi
I am fairly new to shell scripting, so go easy on me please as I know this is most likely something real basic. My question is this, I need to write a script that will look at a directory and tell me if it finds a match for a string that I specify in the filenames. Here would be an example.
I have a directory named tmp. Inside that directory are files named tmp-a, temp-a, temporary-a, etc. If the script looks at the directory and sees that there is a filename with the string of 'tmp', or 'temp' it should continue with the script, but if it does not see any filenames matching a string specified in the shell script it should quit. I am basically looking for a conditional 'if [ -f filename ]' statement that can apply 'or'.
I hope that made sense and as always, thanks in advance.
Tim
The pattern tmp* expands to the list of files whose name begins with tmp, or to the single-word list consisting of the literal pattern itself if there is no matching file.
set --
for pattern in 'tmp*' 'temp*'; do
set -- $pattern "$#"
if [ ! -e "$1" ]; then shift; fi
done
if [ $# -eq 0 ]; then
echo "No matching file"
else
for x in "$#"; do …; done
fi
In bash, you can request the expansion of a pattern that matches no file to be the empty list, which simplifies matters a lot.
shopt -s nullglob
set -- tmp* temp*
if [ $# -eq 0 ]; then …
The same thing goes for zsh, which allows this to be set per-pattern.
set -- tmp*(N) temp*(N)
if [ $# -eq 0 ]; then …
If you wish to search recursively inside directories, you can use the find command.
if [ -n "$(find -name 'tmp*' -o -name 'temp*' | head -c 1)" ]; then
# there are matching files
fi