Bash globbing and argument passing
Solution 1
You're assigning files
as a scalar variable instead of an array variable.
In
files=$HOME/print/*.pdf
You're assigning some string like /home/highsciguy/print/*.pdf
to the $files
scalar (aka string) variable.
Use:
files=(~/print/*.pdf)
or
files=("$HOME"/print/*.pdf)
instead. The shell will expand that globbing pattern into a list of file paths, and assign each of them to elements of the $files
array.
The expansion of the glob is done at the time of the assignment.
You don't have to use non-standard sh features, and you could use your system's sh
instead of bash
here by writing it:
#!/bin/sh -
[ "$#" -gt 0 ] || set -- ~/print/*.pdf
for file do
ls -d -- "$file"
done
set
is to assign the "$@"
array of positional parameters.
Another approach could have been to store the globbing pattern in a scalar variable:
files=$HOME/print/*.pdf
And have the shell expand the glob at the time the $files
variable is expanded.
IFS= # disable word splitting
for file in $files; do ...
Here, because $files
is not quoted (which you shouldn't usually do), its expansion is subject to word splitting (which we've disabled here) and globbing/filename generation.
So the *.pdf
will be expanded to the list of matching files. However, if $HOME
contained wildcard characters, they could be expanded too, which is why it's still preferable to use an array variable.
Solution 2
You may have seen things like files=$*
and files=~/print/*.pdf
in older shells without arrays, and then ls $files
.
A variable substitution which is not within double quotes interprets the value of the variable as a whitespace-separated list of shell wildcard patterns which are replaced by matching file names if there are any. For example, after files=~/print/*.pdf
, ls $files
expands to something like ls
with the arguments /home/highsciguy/print/bar.pdf
, /home/highsciguy/print/foo.pdf
, etc. In the case files=$*
, this assignment concatenates the arguments passed to the script with spaces in between, and ls $files
splits them back out.
All of this breaks down if you have file names containing whitespace or globbing characters, which is why you shouldn't do things this way. Use arrays instead.
files=("$@")
if ((${#files[@]} == 0)); then
files=("$HOME"/print/*.pdf)
fi
Note that
- All array assignments require parentheses around the array values:
var=(…)
. - To test whether an array is empty, check its length.
"$files"
is empty whenfiles
is an array whose element of index 0 is unset or an empty string. Also[ "X$foo" = "X" ]
is an obsolete way to test whether$foo
is empty: all modern shells implement[ -n "$foo" ]
correctly. In bash, you can use[[ -n $foo ]]
.
In shells that don't support arrays, there is in fact one array: the positional parameters to the shell or current function. Here, you don't really need the files
array, in fact it would be easier to use the positional parameters.
#!/bin/sh
if [ "$#" -eq 0 ]; then
set -- ~/print/*.pdf
fi
for file do …
Related videos on Youtube
Daniel Ezra
Updated on September 18, 2022Comments
-
Daniel Ezra almost 2 years
I have the following simplified bash script
#!/bin/bash files=("$@") if [ "X$files" = "X" ]; then files=$HOME/print/*.pdf; fi for file in "${files[@]}"; do ls "$file"; done
If I pass arguments (file names) as parameters this script will print the proper file names. On the other hand, if I don't pass arguments, it will print
/home/user/print/*.pdf: No such file or directory
Why are the file names not expanded in this case, and how do I fix it? Note that I use the
files=("$@")
and"${files[@]}"
constructs because I read that it is to be preferred over the usual "files=$*".-
Admin over 10 yearsWhere is
files=$*
ever usual? That's plain wrong. -
Admin over 10 yearsUsual is relative, right. I meant a method which does not use arrays. What would you do then?
-