Using $# properly - bash

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)

Related

For files in directory Bash [duplicate]

I'm trying to loop through files in a directory, where the directory is passed through as an argument. I currently have the following script saved in test.sh:
#!/bin/bash
for filename in "$1"/*; do
echo "File:"
echo $filename
done
And I am running the above using:
sh test.sh path/to/loop/over
However, the above doesn't output the files at the directory path/to/loop/over, it instead outputs:
File:
path/to/loop/over/*
I'm guessing it's interpreting path/to/loop/over/* as a string and not a directory. My expected output is the following:
File:
foo.txt
File:
bar.txt
Where foo.txt and bar.txt are files in the path/to/loop/over/ directory. I found this answer which suggested to add a /* after the $1, however, this doesn't seem to help (neither do these suggestions)
Iterate over content of directory
Compatible answer (not only bash)
As this question is tagged shell, there is a POSIX compatible way:
#!/bin/sh
for file in "$1"/* ;do
[ -f "$file" ] && echo "Process '$file'."
done
Will be enough (work with filenames containing spaces):
$ myscript.sh /path/to/dir
Process '/path/to/dir/foo'.
Process '/path/to/dir/bar'.
Process '/path/to/dir/foo bar'.
This work well by using any posix shell. Tested with bash, ksh, dash, zsh and busybox sh.
#!/bin/sh
cd "$1" || exit 1
for file in * ;do
[ -f "$file" ] && echo "Process '$file'."
done
This version won't print path:
$ myscript.sh /path/to/dir
Process 'foo'.
Process 'bar'.
Process 'foo bar'.
Some bash ways
Introduction
I don't like to use shopt when not needed... (This change standard
bash behaviours and make script less readables).
There is an elegant way for doing this by using standard bash, without requirement of shopt.
Of course, previous answer work fine under bash, but. There are some
interresting way for making your script more powerfull, flexible, pretty, detailed...
Sample
#!/bin/bash
die() { echo >&2 "$0 ERROR: $#";exit 1;} # Emergency exit function
[ "$1" ] || die "Argument missing." # Exit unless argument submitted
[ -d "$1" ] || die "Arg '$1' is not a directory." # Exit if argument is not dir
cd "$1" || die "Can't access '$1'." # Exit unless access dir.
files=(*) # All files names in array $files
[ -f "$files" ] || die "No files found." # Exit if no files found
for file in "${files[#]}";do # foreach file:
echo Process "$file" # Process file
done
Explanation: considering globbing vs real files
When doing:
files=(/path/to/dir/*)
variable $files becomes an array containing all files contained under /path/to/dir/:
declare -p files
declare -a files=([0]="/path/to/dir/bar" [1]="/path/to/dir/baz" [2]="/path/to/dir/foo")
But if nothing match glob pattern, star won't be replaced and array become:
declare -p files
declare -a files=([0]="/path/to/dir/*")
From there. looking for $files is like looking for ${files[0]} ie: first field in array. So
[ -f "$files" ] || die "No files found."
will execute die function unless first field of array files is a file ([ -e "$files" ] to check for existing entry, [ -d "$files" ] to check for existing directory, ans so on... see man bash or help test).
But you could do replace this filesystem test by some string based test, like:
[ "$files" = "/path/to/dir/*" ] && die "No files found."
or, using array length:
((${#files[#]}==1)) && [ "${files##*/}" = "*" ] && die "No files found."
Dropping paths by using Parameter expansion:
For suppressing path from filenames, instead of cd $path you could do:
targetPath=/path/to/dir
files=($targetPath/*)
[ -f "$files" ] || die "No files found."
Then:
declare -p files
declare -a files=([0]="/path/to/dir/bar" [1]="/path/to/dir/baz" [2]="/path/to/dir/foo")
You could
printf 'File: %s\n' ${files[#]#$targetPath/}
File: bar
File: baz
File: foo
This would happen if the directory is empty, or misspelled. The shell (in its default configuration) simply doesn't expand a wildcard if it has no matches. (You can control this in Bash with shopt -s nullglob; with this option, wildcards which don't match anything are simply removed.)
You can verify this easily for yourself. In a directory with four files,
sh$ echo *
a file or two
sh$ echo [ot]*
or two
sh$ echo n*
n*
And in Bash,
bash$ echo n*
n*
bash$ shopt -s nullglob
bash$ echo n*
I'm guessing you are confused about how the current working directory affects the resolution of directory names; maybe read Difference between ./ and ~/

Binary operator expected error if partial file name exists [duplicate]

If I want to check for the existence of a single file, I can test for it using test -e filename or [ -e filename ].
Supposing I have a glob and I want to know whether any files exist whose names match the glob. The glob can match 0 files (in which case I need to do nothing), or it can match 1 or more files (in which case I need to do something). How can I test whether a glob has any matches? (I don't care how many matches there are, and it would be best if I could do this with one if statement and no loops (simply because I find that most readable).
(test -e glob* fails if the glob matches more than one file.)
Bash-specific solution:
compgen -G "<glob-pattern>"
Escape the pattern or it'll get pre-expanded into matches.
Exit status is:
1 for no-match,
0 for 'one or more matches'
stdout is a list of files matching the glob.
I think this is the best option in terms of conciseness and minimizing potential side effects.
Example:
if compgen -G "/tmp/someFiles*" > /dev/null; then
echo "Some files exist."
fi
The nullglob shell option is indeed a bashism.
To avoid the need for a tedious save and restore of the nullglob state, I'd only set it inside the subshell that expands the glob:
if test -n "$(shopt -s nullglob; echo glob*)"
then
echo found
else
echo not found
fi
For better portability and more flexible globbing, use find:
if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
echo found
else
echo not found
fi
Explicit -print -quit actions are used for find instead of the default implicit -print action so that find will quit as soon as it finds the first file matching the search criteria. Where lots of files match, this should run much faster than echo glob* or ls glob* and it also avoids the possibility of overstuffing the expanded command line (some shells have a 4K length limit).
If find feels like overkill and the number of files likely to match is small, use stat:
if stat -t glob* >/dev/null 2>&1
then
echo found
else
echo not found
fi
I like
exists() {
[ -e "$1" ]
}
if exists glob*; then
echo found
else
echo not found
fi
This is both readable and efficient (unless there are a huge number of files).
The main drawback is that it's much more subtle than it looks, and I sometimes feel compelled to add a long comment.
If there's a match, "glob*" is expanded by the shell and all the matches are passed to exists(), which checks the first one and ignores the rest.
If there's no match, "glob*" is passed to exists() and found not to exist there either.
Edit: there may be a false positive, see comment
#!/usr/bin/env bash
# If it is set, then an unmatched glob is swept away entirely --
# replaced with a set of zero words --
# instead of remaining in place as a single word.
shopt -s nullglob
M=(*px)
if [ "${#M[*]}" -ge 1 ]; then
echo "${#M[*]} matches."
else
echo "No such files."
fi
If you have globfail set you can use this crazy (which you really should not)
shopt -s failglob # exit if * does not match
( : * ) && echo 0 || echo 1
or
q=( * ) && echo 0 || echo 1
test -e has the unfortunate caveat that it considers broken symbolic links to not exist. So you may want to check for those, too.
function globexists {
test -e "$1" -o -L "$1"
}
if globexists glob*; then
echo found
else
echo not found
fi
I have yet another solution:
if [ "$(echo glob*)" != 'glob*' ]
This works nicely for me. There may be some corner cases I missed.
Based on flabdablet's answer, for me it looks like easiest (not necessarily fastest) is just to use find itself, while leaving glob expansion on shell, like:
find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"
Or in if like:
if find $yourGlob -quit &> /dev/null; then
echo "MATCH"
else
echo "NOT-FOUND"
fi
To simplify miku's answer somewhat, based on his idea:
M=(*py)
if [ -e ${M[0]} ]; then
echo Found
else
echo Not Found
fi
In Bash, you can glob to an array; if the glob didn't match, your array will contain a single entry that doesn't correspond to an existing file:
#!/bin/bash
shellglob='*.sh'
scripts=($shellglob)
if [ -e "${scripts[0]}" ]
then stat "${scripts[#]}"
fi
Note: if you have nullglob set, scripts will be an empty array, and you should test with [ "${scripts[*]}" ] or with [ "${#scripts[*]}" != 0 ] instead. If you're writing a library that must work with or without nullglob, you'll want
if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]
An advantage of this approach is that you then have the list of files you want to work with, rather than having to repeat the glob operation.
If you want to test if the files exist before iterating over them, you can use this pattern:
for F in glob*; do
if [[ ! -f $F ]]; then break; fi
...
done
if the glob does not does not match anything, $F will be the non-expanded glob ('glob*' in this case) and if a file with the same name does not exist, it will skip the rest of the loop.
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
echo "I found ${FOUND} matches"
else
echo "No matches found"
fi
set -- glob*
if [ -f "$1" ]; then
echo "It matched"
fi
Explanation
When there isn't a match for glob*, then $1 will contain 'glob*'. The test -f "$1" won't be true because the glob* file doesn't exist.
Why this is better than alternatives
This works with sh and derivates: KornShell and Bash. It doesn't create any sub-shell. $(..) and `...` commands create a sub-shell; they fork a process, and therefore are slower than this solution.
Like this in Bash (test files containing pattern):
shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
0) echo "only one file match" ;;
1) echo "more than one file match" ;;
2) echo "no file match" ;;
esac
It's far better than compgen -G: because we can discriminates more cases and more precisely.
It can work with only one wildcard *.
This abomination seems to work:
#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
echo "Glob matched"
else
echo "Glob did not match"
fi
It probably requires bash, not sh.
This works because the nullglob option causes the glob to evaluate to an empty string if there are no matches. Thus any non-empty output from the echo command indicates that the glob matched something.
A solution for extended globs (extglob) in Bash:
bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'
Exit status is 0 if there is at least one match, and non-zero (2) when there is no match. Standard output contains a newline-separated list of matching files (and file names containing spaces they are quoted).
Or, slightly different:
bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'
Differences to the ls-based solution: probably faster (not measured), file names with spaces not quoted in output, exit code 1 when there is no match (not 2 :shrug:).
Example usage:
No match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.foo|*.bar)'; echo "exit status: $?"
/bin/ls: cannot access '#(*.foo|*.bar)': No such file or directory
exit status: 2
At least one match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.ts|*.mp4)'; echo "exit status: $?"
'video1 with spaces.mp4'
video2.mp4
video3.mp4
exit status: 0
Concepts used:
ls' exit code behavior (adds -U for efficiency, and -1 for output control).
Does not enable extglob in current shell (often not desired).
Makes use of the $ prefix so that the \n is interpreted, so that the extended glob pattern is on a different line than the shopt -s extglob -- otherwise the extended glob pattern would be a syntax error!
Note 1: I worked towards this solution because the compgen -G "<glob-pattern>" approach suggested in other answers does not seem to work smoothly with brace expansion; and yet I needed some more advanced globbing features.
Note 2: lovely resource for the extended glob syntax: extglob
Both nullglob and compgen are useful only on some bash shells.
A (non-recursive) solution that works on most shells is:
set -- ./glob* # or /path/dir/glob*
[ -f "$1" ] || shift # remove the glob if present.
if [ "$#" -lt 1 ]
then echo "at least one file found"
fi
Assuming you may want to do something with the files if they exist:
mapfile -t exists < <(find "$dirName" -type f -iname '*.zip'); [[ ${#exists} -ne 0 ]] && { echo "Zip files found" ; } || { echo "Zip files not found" ; }
You can then loop through the exists array if you need to do something with the files.
(ls glob* &>/dev/null && echo Files found) || echo No file found
if ls -d $glob > /dev/null 2>&1; then
echo Found.
else
echo Not found.
fi
Note that this can be very time cosuming if there are a lot of matches or file access is slow.
ls | grep -q "glob.*"
Not the most efficient solution (if there's a ton of files in the directory it might be slowish), but it's simple, easy to read and also has the advantage that regexes are more powerful than plain Bash glob patterns.
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

How can I find the exist file in shell script with * argument [duplicate]

If I want to check for the existence of a single file, I can test for it using test -e filename or [ -e filename ].
Supposing I have a glob and I want to know whether any files exist whose names match the glob. The glob can match 0 files (in which case I need to do nothing), or it can match 1 or more files (in which case I need to do something). How can I test whether a glob has any matches? (I don't care how many matches there are, and it would be best if I could do this with one if statement and no loops (simply because I find that most readable).
(test -e glob* fails if the glob matches more than one file.)
Bash-specific solution:
compgen -G "<glob-pattern>"
Escape the pattern or it'll get pre-expanded into matches.
Exit status is:
1 for no-match,
0 for 'one or more matches'
stdout is a list of files matching the glob.
I think this is the best option in terms of conciseness and minimizing potential side effects.
Example:
if compgen -G "/tmp/someFiles*" > /dev/null; then
echo "Some files exist."
fi
The nullglob shell option is indeed a bashism.
To avoid the need for a tedious save and restore of the nullglob state, I'd only set it inside the subshell that expands the glob:
if test -n "$(shopt -s nullglob; echo glob*)"
then
echo found
else
echo not found
fi
For better portability and more flexible globbing, use find:
if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
echo found
else
echo not found
fi
Explicit -print -quit actions are used for find instead of the default implicit -print action so that find will quit as soon as it finds the first file matching the search criteria. Where lots of files match, this should run much faster than echo glob* or ls glob* and it also avoids the possibility of overstuffing the expanded command line (some shells have a 4K length limit).
If find feels like overkill and the number of files likely to match is small, use stat:
if stat -t glob* >/dev/null 2>&1
then
echo found
else
echo not found
fi
I like
exists() {
[ -e "$1" ]
}
if exists glob*; then
echo found
else
echo not found
fi
This is both readable and efficient (unless there are a huge number of files).
The main drawback is that it's much more subtle than it looks, and I sometimes feel compelled to add a long comment.
If there's a match, "glob*" is expanded by the shell and all the matches are passed to exists(), which checks the first one and ignores the rest.
If there's no match, "glob*" is passed to exists() and found not to exist there either.
Edit: there may be a false positive, see comment
#!/usr/bin/env bash
# If it is set, then an unmatched glob is swept away entirely --
# replaced with a set of zero words --
# instead of remaining in place as a single word.
shopt -s nullglob
M=(*px)
if [ "${#M[*]}" -ge 1 ]; then
echo "${#M[*]} matches."
else
echo "No such files."
fi
If you have globfail set you can use this crazy (which you really should not)
shopt -s failglob # exit if * does not match
( : * ) && echo 0 || echo 1
or
q=( * ) && echo 0 || echo 1
test -e has the unfortunate caveat that it considers broken symbolic links to not exist. So you may want to check for those, too.
function globexists {
test -e "$1" -o -L "$1"
}
if globexists glob*; then
echo found
else
echo not found
fi
I have yet another solution:
if [ "$(echo glob*)" != 'glob*' ]
This works nicely for me. There may be some corner cases I missed.
Based on flabdablet's answer, for me it looks like easiest (not necessarily fastest) is just to use find itself, while leaving glob expansion on shell, like:
find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"
Or in if like:
if find $yourGlob -quit &> /dev/null; then
echo "MATCH"
else
echo "NOT-FOUND"
fi
To simplify miku's answer somewhat, based on his idea:
M=(*py)
if [ -e ${M[0]} ]; then
echo Found
else
echo Not Found
fi
In Bash, you can glob to an array; if the glob didn't match, your array will contain a single entry that doesn't correspond to an existing file:
#!/bin/bash
shellglob='*.sh'
scripts=($shellglob)
if [ -e "${scripts[0]}" ]
then stat "${scripts[#]}"
fi
Note: if you have nullglob set, scripts will be an empty array, and you should test with [ "${scripts[*]}" ] or with [ "${#scripts[*]}" != 0 ] instead. If you're writing a library that must work with or without nullglob, you'll want
if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]
An advantage of this approach is that you then have the list of files you want to work with, rather than having to repeat the glob operation.
If you want to test if the files exist before iterating over them, you can use this pattern:
for F in glob*; do
if [[ ! -f $F ]]; then break; fi
...
done
if the glob does not does not match anything, $F will be the non-expanded glob ('glob*' in this case) and if a file with the same name does not exist, it will skip the rest of the loop.
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
echo "I found ${FOUND} matches"
else
echo "No matches found"
fi
set -- glob*
if [ -f "$1" ]; then
echo "It matched"
fi
Explanation
When there isn't a match for glob*, then $1 will contain 'glob*'. The test -f "$1" won't be true because the glob* file doesn't exist.
Why this is better than alternatives
This works with sh and derivates: KornShell and Bash. It doesn't create any sub-shell. $(..) and `...` commands create a sub-shell; they fork a process, and therefore are slower than this solution.
Like this in Bash (test files containing pattern):
shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
0) echo "only one file match" ;;
1) echo "more than one file match" ;;
2) echo "no file match" ;;
esac
It's far better than compgen -G: because we can discriminates more cases and more precisely.
It can work with only one wildcard *.
This abomination seems to work:
#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
echo "Glob matched"
else
echo "Glob did not match"
fi
It probably requires bash, not sh.
This works because the nullglob option causes the glob to evaluate to an empty string if there are no matches. Thus any non-empty output from the echo command indicates that the glob matched something.
A solution for extended globs (extglob) in Bash:
bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'
Exit status is 0 if there is at least one match, and non-zero (2) when there is no match. Standard output contains a newline-separated list of matching files (and file names containing spaces they are quoted).
Or, slightly different:
bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'
Differences to the ls-based solution: probably faster (not measured), file names with spaces not quoted in output, exit code 1 when there is no match (not 2 :shrug:).
Example usage:
No match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.foo|*.bar)'; echo "exit status: $?"
/bin/ls: cannot access '#(*.foo|*.bar)': No such file or directory
exit status: 2
At least one match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.ts|*.mp4)'; echo "exit status: $?"
'video1 with spaces.mp4'
video2.mp4
video3.mp4
exit status: 0
Concepts used:
ls' exit code behavior (adds -U for efficiency, and -1 for output control).
Does not enable extglob in current shell (often not desired).
Makes use of the $ prefix so that the \n is interpreted, so that the extended glob pattern is on a different line than the shopt -s extglob -- otherwise the extended glob pattern would be a syntax error!
Note 1: I worked towards this solution because the compgen -G "<glob-pattern>" approach suggested in other answers does not seem to work smoothly with brace expansion; and yet I needed some more advanced globbing features.
Note 2: lovely resource for the extended glob syntax: extglob
Both nullglob and compgen are useful only on some bash shells.
A (non-recursive) solution that works on most shells is:
set -- ./glob* # or /path/dir/glob*
[ -f "$1" ] || shift # remove the glob if present.
if [ "$#" -lt 1 ]
then echo "at least one file found"
fi
Assuming you may want to do something with the files if they exist:
mapfile -t exists < <(find "$dirName" -type f -iname '*.zip'); [[ ${#exists} -ne 0 ]] && { echo "Zip files found" ; } || { echo "Zip files not found" ; }
You can then loop through the exists array if you need to do something with the files.
(ls glob* &>/dev/null && echo Files found) || echo No file found
if ls -d $glob > /dev/null 2>&1; then
echo Found.
else
echo Not found.
fi
Note that this can be very time cosuming if there are a lot of matches or file access is slow.
ls | grep -q "glob.*"
Not the most efficient solution (if there's a ton of files in the directory it might be slowish), but it's simple, easy to read and also has the advantage that regexes are more powerful than plain Bash glob patterns.
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

Find multiple file formats in IF condition in Shell Script [duplicate]

If I want to check for the existence of a single file, I can test for it using test -e filename or [ -e filename ].
Supposing I have a glob and I want to know whether any files exist whose names match the glob. The glob can match 0 files (in which case I need to do nothing), or it can match 1 or more files (in which case I need to do something). How can I test whether a glob has any matches? (I don't care how many matches there are, and it would be best if I could do this with one if statement and no loops (simply because I find that most readable).
(test -e glob* fails if the glob matches more than one file.)
Bash-specific solution:
compgen -G "<glob-pattern>"
Escape the pattern or it'll get pre-expanded into matches.
Exit status is:
1 for no-match,
0 for 'one or more matches'
stdout is a list of files matching the glob.
I think this is the best option in terms of conciseness and minimizing potential side effects.
Example:
if compgen -G "/tmp/someFiles*" > /dev/null; then
echo "Some files exist."
fi
The nullglob shell option is indeed a bashism.
To avoid the need for a tedious save and restore of the nullglob state, I'd only set it inside the subshell that expands the glob:
if test -n "$(shopt -s nullglob; echo glob*)"
then
echo found
else
echo not found
fi
For better portability and more flexible globbing, use find:
if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
echo found
else
echo not found
fi
Explicit -print -quit actions are used for find instead of the default implicit -print action so that find will quit as soon as it finds the first file matching the search criteria. Where lots of files match, this should run much faster than echo glob* or ls glob* and it also avoids the possibility of overstuffing the expanded command line (some shells have a 4K length limit).
If find feels like overkill and the number of files likely to match is small, use stat:
if stat -t glob* >/dev/null 2>&1
then
echo found
else
echo not found
fi
I like
exists() {
[ -e "$1" ]
}
if exists glob*; then
echo found
else
echo not found
fi
This is both readable and efficient (unless there are a huge number of files).
The main drawback is that it's much more subtle than it looks, and I sometimes feel compelled to add a long comment.
If there's a match, "glob*" is expanded by the shell and all the matches are passed to exists(), which checks the first one and ignores the rest.
If there's no match, "glob*" is passed to exists() and found not to exist there either.
Edit: there may be a false positive, see comment
#!/usr/bin/env bash
# If it is set, then an unmatched glob is swept away entirely --
# replaced with a set of zero words --
# instead of remaining in place as a single word.
shopt -s nullglob
M=(*px)
if [ "${#M[*]}" -ge 1 ]; then
echo "${#M[*]} matches."
else
echo "No such files."
fi
If you have globfail set you can use this crazy (which you really should not)
shopt -s failglob # exit if * does not match
( : * ) && echo 0 || echo 1
or
q=( * ) && echo 0 || echo 1
test -e has the unfortunate caveat that it considers broken symbolic links to not exist. So you may want to check for those, too.
function globexists {
test -e "$1" -o -L "$1"
}
if globexists glob*; then
echo found
else
echo not found
fi
I have yet another solution:
if [ "$(echo glob*)" != 'glob*' ]
This works nicely for me. There may be some corner cases I missed.
Based on flabdablet's answer, for me it looks like easiest (not necessarily fastest) is just to use find itself, while leaving glob expansion on shell, like:
find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"
Or in if like:
if find $yourGlob -quit &> /dev/null; then
echo "MATCH"
else
echo "NOT-FOUND"
fi
To simplify miku's answer somewhat, based on his idea:
M=(*py)
if [ -e ${M[0]} ]; then
echo Found
else
echo Not Found
fi
In Bash, you can glob to an array; if the glob didn't match, your array will contain a single entry that doesn't correspond to an existing file:
#!/bin/bash
shellglob='*.sh'
scripts=($shellglob)
if [ -e "${scripts[0]}" ]
then stat "${scripts[#]}"
fi
Note: if you have nullglob set, scripts will be an empty array, and you should test with [ "${scripts[*]}" ] or with [ "${#scripts[*]}" != 0 ] instead. If you're writing a library that must work with or without nullglob, you'll want
if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]
An advantage of this approach is that you then have the list of files you want to work with, rather than having to repeat the glob operation.
If you want to test if the files exist before iterating over them, you can use this pattern:
for F in glob*; do
if [[ ! -f $F ]]; then break; fi
...
done
if the glob does not does not match anything, $F will be the non-expanded glob ('glob*' in this case) and if a file with the same name does not exist, it will skip the rest of the loop.
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
echo "I found ${FOUND} matches"
else
echo "No matches found"
fi
set -- glob*
if [ -f "$1" ]; then
echo "It matched"
fi
Explanation
When there isn't a match for glob*, then $1 will contain 'glob*'. The test -f "$1" won't be true because the glob* file doesn't exist.
Why this is better than alternatives
This works with sh and derivates: KornShell and Bash. It doesn't create any sub-shell. $(..) and `...` commands create a sub-shell; they fork a process, and therefore are slower than this solution.
Like this in Bash (test files containing pattern):
shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
0) echo "only one file match" ;;
1) echo "more than one file match" ;;
2) echo "no file match" ;;
esac
It's far better than compgen -G: because we can discriminates more cases and more precisely.
It can work with only one wildcard *.
This abomination seems to work:
#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
echo "Glob matched"
else
echo "Glob did not match"
fi
It probably requires bash, not sh.
This works because the nullglob option causes the glob to evaluate to an empty string if there are no matches. Thus any non-empty output from the echo command indicates that the glob matched something.
A solution for extended globs (extglob) in Bash:
bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'
Exit status is 0 if there is at least one match, and non-zero (2) when there is no match. Standard output contains a newline-separated list of matching files (and file names containing spaces they are quoted).
Or, slightly different:
bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'
Differences to the ls-based solution: probably faster (not measured), file names with spaces not quoted in output, exit code 1 when there is no match (not 2 :shrug:).
Example usage:
No match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.foo|*.bar)'; echo "exit status: $?"
/bin/ls: cannot access '#(*.foo|*.bar)': No such file or directory
exit status: 2
At least one match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.ts|*.mp4)'; echo "exit status: $?"
'video1 with spaces.mp4'
video2.mp4
video3.mp4
exit status: 0
Concepts used:
ls' exit code behavior (adds -U for efficiency, and -1 for output control).
Does not enable extglob in current shell (often not desired).
Makes use of the $ prefix so that the \n is interpreted, so that the extended glob pattern is on a different line than the shopt -s extglob -- otherwise the extended glob pattern would be a syntax error!
Note 1: I worked towards this solution because the compgen -G "<glob-pattern>" approach suggested in other answers does not seem to work smoothly with brace expansion; and yet I needed some more advanced globbing features.
Note 2: lovely resource for the extended glob syntax: extglob
Both nullglob and compgen are useful only on some bash shells.
A (non-recursive) solution that works on most shells is:
set -- ./glob* # or /path/dir/glob*
[ -f "$1" ] || shift # remove the glob if present.
if [ "$#" -lt 1 ]
then echo "at least one file found"
fi
Assuming you may want to do something with the files if they exist:
mapfile -t exists < <(find "$dirName" -type f -iname '*.zip'); [[ ${#exists} -ne 0 ]] && { echo "Zip files found" ; } || { echo "Zip files not found" ; }
You can then loop through the exists array if you need to do something with the files.
(ls glob* &>/dev/null && echo Files found) || echo No file found
if ls -d $glob > /dev/null 2>&1; then
echo Found.
else
echo Not found.
fi
Note that this can be very time cosuming if there are a lot of matches or file access is slow.
ls | grep -q "glob.*"
Not the most efficient solution (if there's a ton of files in the directory it might be slowish), but it's simple, easy to read and also has the advantage that regexes are more powerful than plain Bash glob patterns.
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

if condition to assign variable for file check with wildcard - shell script [duplicate]

If I want to check for the existence of a single file, I can test for it using test -e filename or [ -e filename ].
Supposing I have a glob and I want to know whether any files exist whose names match the glob. The glob can match 0 files (in which case I need to do nothing), or it can match 1 or more files (in which case I need to do something). How can I test whether a glob has any matches? (I don't care how many matches there are, and it would be best if I could do this with one if statement and no loops (simply because I find that most readable).
(test -e glob* fails if the glob matches more than one file.)
Bash-specific solution:
compgen -G "<glob-pattern>"
Escape the pattern or it'll get pre-expanded into matches.
Exit status is:
1 for no-match,
0 for 'one or more matches'
stdout is a list of files matching the glob.
I think this is the best option in terms of conciseness and minimizing potential side effects.
Example:
if compgen -G "/tmp/someFiles*" > /dev/null; then
echo "Some files exist."
fi
The nullglob shell option is indeed a bashism.
To avoid the need for a tedious save and restore of the nullglob state, I'd only set it inside the subshell that expands the glob:
if test -n "$(shopt -s nullglob; echo glob*)"
then
echo found
else
echo not found
fi
For better portability and more flexible globbing, use find:
if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
echo found
else
echo not found
fi
Explicit -print -quit actions are used for find instead of the default implicit -print action so that find will quit as soon as it finds the first file matching the search criteria. Where lots of files match, this should run much faster than echo glob* or ls glob* and it also avoids the possibility of overstuffing the expanded command line (some shells have a 4K length limit).
If find feels like overkill and the number of files likely to match is small, use stat:
if stat -t glob* >/dev/null 2>&1
then
echo found
else
echo not found
fi
I like
exists() {
[ -e "$1" ]
}
if exists glob*; then
echo found
else
echo not found
fi
This is both readable and efficient (unless there are a huge number of files).
The main drawback is that it's much more subtle than it looks, and I sometimes feel compelled to add a long comment.
If there's a match, "glob*" is expanded by the shell and all the matches are passed to exists(), which checks the first one and ignores the rest.
If there's no match, "glob*" is passed to exists() and found not to exist there either.
Edit: there may be a false positive, see comment
#!/usr/bin/env bash
# If it is set, then an unmatched glob is swept away entirely --
# replaced with a set of zero words --
# instead of remaining in place as a single word.
shopt -s nullglob
M=(*px)
if [ "${#M[*]}" -ge 1 ]; then
echo "${#M[*]} matches."
else
echo "No such files."
fi
If you have globfail set you can use this crazy (which you really should not)
shopt -s failglob # exit if * does not match
( : * ) && echo 0 || echo 1
or
q=( * ) && echo 0 || echo 1
test -e has the unfortunate caveat that it considers broken symbolic links to not exist. So you may want to check for those, too.
function globexists {
test -e "$1" -o -L "$1"
}
if globexists glob*; then
echo found
else
echo not found
fi
I have yet another solution:
if [ "$(echo glob*)" != 'glob*' ]
This works nicely for me. There may be some corner cases I missed.
Based on flabdablet's answer, for me it looks like easiest (not necessarily fastest) is just to use find itself, while leaving glob expansion on shell, like:
find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"
Or in if like:
if find $yourGlob -quit &> /dev/null; then
echo "MATCH"
else
echo "NOT-FOUND"
fi
To simplify miku's answer somewhat, based on his idea:
M=(*py)
if [ -e ${M[0]} ]; then
echo Found
else
echo Not Found
fi
In Bash, you can glob to an array; if the glob didn't match, your array will contain a single entry that doesn't correspond to an existing file:
#!/bin/bash
shellglob='*.sh'
scripts=($shellglob)
if [ -e "${scripts[0]}" ]
then stat "${scripts[#]}"
fi
Note: if you have nullglob set, scripts will be an empty array, and you should test with [ "${scripts[*]}" ] or with [ "${#scripts[*]}" != 0 ] instead. If you're writing a library that must work with or without nullglob, you'll want
if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]
An advantage of this approach is that you then have the list of files you want to work with, rather than having to repeat the glob operation.
If you want to test if the files exist before iterating over them, you can use this pattern:
for F in glob*; do
if [[ ! -f $F ]]; then break; fi
...
done
if the glob does not does not match anything, $F will be the non-expanded glob ('glob*' in this case) and if a file with the same name does not exist, it will skip the rest of the loop.
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
echo "I found ${FOUND} matches"
else
echo "No matches found"
fi
set -- glob*
if [ -f "$1" ]; then
echo "It matched"
fi
Explanation
When there isn't a match for glob*, then $1 will contain 'glob*'. The test -f "$1" won't be true because the glob* file doesn't exist.
Why this is better than alternatives
This works with sh and derivates: KornShell and Bash. It doesn't create any sub-shell. $(..) and `...` commands create a sub-shell; they fork a process, and therefore are slower than this solution.
Like this in Bash (test files containing pattern):
shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
0) echo "only one file match" ;;
1) echo "more than one file match" ;;
2) echo "no file match" ;;
esac
It's far better than compgen -G: because we can discriminates more cases and more precisely.
It can work with only one wildcard *.
This abomination seems to work:
#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
echo "Glob matched"
else
echo "Glob did not match"
fi
It probably requires bash, not sh.
This works because the nullglob option causes the glob to evaluate to an empty string if there are no matches. Thus any non-empty output from the echo command indicates that the glob matched something.
A solution for extended globs (extglob) in Bash:
bash -c $'shopt -s extglob \n /bin/ls -1U <ext-glob-pattern>'
Exit status is 0 if there is at least one match, and non-zero (2) when there is no match. Standard output contains a newline-separated list of matching files (and file names containing spaces they are quoted).
Or, slightly different:
bash -c $'shopt -s extglob \n compgen -G <ext-glob-pattern>'
Differences to the ls-based solution: probably faster (not measured), file names with spaces not quoted in output, exit code 1 when there is no match (not 2 :shrug:).
Example usage:
No match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.foo|*.bar)'; echo "exit status: $?"
/bin/ls: cannot access '#(*.foo|*.bar)': No such file or directory
exit status: 2
At least one match:
$ bash -c $'shopt -s extglob \n /bin/ls -1U #(*.ts|*.mp4)'; echo "exit status: $?"
'video1 with spaces.mp4'
video2.mp4
video3.mp4
exit status: 0
Concepts used:
ls' exit code behavior (adds -U for efficiency, and -1 for output control).
Does not enable extglob in current shell (often not desired).
Makes use of the $ prefix so that the \n is interpreted, so that the extended glob pattern is on a different line than the shopt -s extglob -- otherwise the extended glob pattern would be a syntax error!
Note 1: I worked towards this solution because the compgen -G "<glob-pattern>" approach suggested in other answers does not seem to work smoothly with brace expansion; and yet I needed some more advanced globbing features.
Note 2: lovely resource for the extended glob syntax: extglob
Both nullglob and compgen are useful only on some bash shells.
A (non-recursive) solution that works on most shells is:
set -- ./glob* # or /path/dir/glob*
[ -f "$1" ] || shift # remove the glob if present.
if [ "$#" -lt 1 ]
then echo "at least one file found"
fi
Assuming you may want to do something with the files if they exist:
mapfile -t exists < <(find "$dirName" -type f -iname '*.zip'); [[ ${#exists} -ne 0 ]] && { echo "Zip files found" ; } || { echo "Zip files not found" ; }
You can then loop through the exists array if you need to do something with the files.
(ls glob* &>/dev/null && echo Files found) || echo No file found
if ls -d $glob > /dev/null 2>&1; then
echo Found.
else
echo Not found.
fi
Note that this can be very time cosuming if there are a lot of matches or file access is slow.
ls | grep -q "glob.*"
Not the most efficient solution (if there's a ton of files in the directory it might be slowish), but it's simple, easy to read and also has the advantage that regexes are more powerful than plain Bash glob patterns.
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

Resources