Bash globbing and argument passing

19,680

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 when files 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 …
Share:
19,680

Related videos on Youtube

Daniel Ezra
Author by

Daniel Ezra

Updated on September 18, 2022

Comments

  • Daniel Ezra
    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
      Admin over 10 years
      Where is files=$* ever usual? That's plain wrong.
    • Admin
      Admin over 10 years
      Usual is relative, right. I meant a method which does not use arrays. What would you do then?