How to iterate over arguments in a Bash script
I have a complex command that I'd like to make a shell/bash script of. I can write it in terms of $1
easily:
foo $1 args -o $1.ext
I want to be able to pass multiple input names to the script. What's the right way to do it?
And, of course, I want to handle filenames with spaces in them.
Use "$@"
to represent all the arguments:
for var in "$@"
do
echo "$var"
done
This will iterate over each argument and print it out on a separate line. $@ behaves like $* except that when quoted the arguments are broken up properly if there are spaces in them:
sh test.sh 1 2 '3 4'
1
2
3 4
Rewrite of a now-deleted answer by VonC.
Robert Gamble's succinct answer deals directly with the question. This one amplifies on some issues with filenames containing spaces.
See also: ${1:+"$@"} in /bin/sh
Basic thesis: "$@"
is correct, and $*
(unquoted) is almost always wrong. This is because "$@"
works fine when arguments contain spaces, and works the same as $*
when they don't. In some circumstances, "$*"
is OK too, but "$@"
usually (but not always) works in the same places. Unquoted, $@
and $*
are equivalent (and almost always wrong).
So, what is the difference between $*
, $@
, "$*"
, and "$@"
? They are all related to 'all the arguments to the shell', but they do different things. When unquoted, $*
and $@
do the same thing. They treat each 'word' (sequence of non-whitespace) as a separate argument. The quoted forms are quite different, though: "$*"
treats the argument list as a single space-separated string, whereas "$@"
treats the arguments almost exactly as they were when specified on the command line. "$@"
expands to nothing at all when there are no positional arguments; "$*"
expands to an empty string — and yes, there's a difference, though it can be hard to perceive it. See more information below, after the introduction of the (non-standard) command al
.
Secondary thesis: if you need to process arguments with spaces and then pass them on to other commands, then you sometimes need non-standard tools to assist. (Or you should use arrays, carefully: "${array[@]}"
behaves analogously to "$@"
.)
Example:
$ mkdir "my dir" anotherdir
$ ls
anotherdir my dir
$ cp /dev/null "my dir/my file"
$ cp /dev/null "anotherdir/myfile"
$ ls -Fltr
total 0
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir/
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir/
$ ls -Fltr *
my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$ ls -Fltr "./my dir" "./anotherdir"
./my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
./anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$ var='"./my dir" "./anotherdir"' && echo $var
"./my dir" "./anotherdir"
$ ls -Fltr $var
ls: "./anotherdir": No such file or directory
ls: "./my: No such file or directory
ls: dir": No such file or directory
$
Why doesn't that work? It doesn't work because the shell processes quotes before it expands variables. So, to get the shell to pay attention to the quotes embedded in $var
, you have to use eval
:
$ eval ls -Fltr $var
./my dir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 my file
./anotherdir:
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 14:55 myfile
$
This gets really tricky when you have file names such as " He said, "Don't do this!"
" (with quotes and double quotes and spaces).
$ cp /dev/null "He said, "Don't do this!""
$ ls
He said, "Don't do this!" anotherdir my dir
$ ls -l
total 0
-rw-r--r-- 1 jleffler staff 0 Nov 1 15:54 He said, "Don't do this!"
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 anotherdir
drwxr-xr-x 3 jleffler staff 102 Nov 1 14:55 my dir
$
The shells (all of them) do not make it particularly easy to handle such stuff, so (funnily enough) many Unix programs do not do a good job of handling them. On Unix, a filename (single component) can contain any characters except slash and NUL '