9

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=$*".

2
  • Where is files=$* ever usual? That's plain wrong. Nov 18, 2013 at 12:21
  • Usual is relative, right. I meant a method which does not use arrays. What would you do then?
    – highsciguy
    Nov 18, 2013 at 12:28

2 Answers 2

11

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.

4

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 …

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .