44

I'm trying to write a bash script that allows the user to pass a directory path using wildcards.

For example,

bash show_files.sh *

when executed within this directory

drw-r--r--  2 root root  4.0K Sep 18 11:33 dir_a
-rw-r--r--  1 root root   223 Sep 18 11:33 file_b.txt
-rw-rw-r--  1 root root   106 Oct 18 15:48 file_c.sql

would output:

dir_a
file_b.txt
file_c.sql

The way it is right now, it outputs:

dir_a

contents of show_files.sh:

#!/bin/bash

dirs="$1"

for dir in $dirs
do
    echo $dir
done
eisaacson
  • 758
  • 2
  • 7
  • 21
  • 1
    Quoting the wildcard is fine, but the main difference in your script and those below is $@ instead of $1 – beroe Oct 22 '13 at 05:07

2 Answers2

64

The parent shell, the one invoking bash show_files.sh *, expands the * for you.

In your script, you need to use:

for dir in "$@"
do
    echo "$dir"
done

The double quotes ensure that multiple spaces etc in file names are handled correctly.

See also How to iterate over arguments in a bash shell script.


Potentially confusing addendum

If you're truly sure you want to get the script to expand the *, you have to make sure that * is passed to the script (enclosed in quotes, as in the other answers), and then make sure it is expanded at the right point in the processing (which is not trivial). At that point, I'd use an array.

names=( $@ )
for file in "${names[@]}"
do
    echo "$file"
done

I don't often use $@ without the double quotes, but this is one time when it is more or less the correct thing to do. The tricky part is that it won't handle wild cards with spaces in very well.

Consider:

$ > "double  space.c"
$ > "double  space.h"
$ echo double\ \ space.?
double  space.c double  space.h
$

That works fine. But try passing that as a wild-card to the script and ... well, let's just say it gets to be tricky at that point.

If you want to extract $2 separately, then you can use:

names=( $1 )
for file in "${names[@]}"
do
    echo "$file"
done
# ... use $2 ...
Community
  • 1
  • 1
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Great! Is there a way to do that which would allow for multiple parameters. For instance, `bash show_files.sh * second_param`. This application doesn't really need that. Just curious. – eisaacson Oct 18 '13 at 20:12
  • 5
    @eisaacson if second_param comes first you can do `sparam=$1; shift;` then use `$@`. – jordanm Oct 18 '13 at 20:24
  • @jordanm Your comment here is a great solution. Thank you. – eisaacson Oct 18 '13 at 21:28
  • @JonathanLeffler What you added to your answer makes it all very confusing. – eisaacson Oct 18 '13 at 21:29
  • 3
    Sorry; that wasn't the intention. Passing wild-cards around is not a good idea, in part because it makes everything very confusing. Let the shell deal with it naturally. In your design, pass the 'second' parameter first, and leave the 'list of files/directories' as the remainder of the arguments. This is the natural Unix design. I would recommend using it if at all possible. – Jonathan Leffler Oct 18 '13 at 21:52
  • `names=( $@ )` should be `names=( "$@" )` – Charles Duffy May 23 '16 at 16:34
  • @CharlesDuffy: wouldn't encoding it in quotes prevent meta characters being expanded, which is the object of the exercise? – Jonathan Leffler May 23 '16 at 16:55
  • 1
    Ahh. If the only intent is for globs to be expanded, you might want to clear IFS first, so we don't also get string-splitting, but `names=( $@ )` does make sense in that case. – Charles Duffy May 23 '16 at 16:57
12

Quote the wild-card:

bash show_files.sh '*'

or make your script accept a list of arguments, not just one:

for dir in "$@"
do
    echo "$dir"
done

It's better to iterate directly over "$@' rather than assigning it to another variable, in order to preserve its special ability to hold elements that themselves contain whitespace.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Great! The second option is perfect. Is there a way to do that which would allow for multiple parameters. For instance, `bash show_files.sh * second_param`. This application doesn't really need that. Just curious. – eisaacson Oct 18 '13 at 20:12
  • 1
    Kind of. Your script would receive a variable number of parameters, with what you call "second_param" really being the *last* parameter. `show_files.sh` never sees the `*`, only the list of files that the shell expands it to. It's a little tricky to get the last argument, although `bash` has some extensions to make it easier. `${@: -1}` (space required) would give you the last argument. `${@:0:$#}` seems to give you all *but* the last, but using an offset of 0 with `$@` (which is otherwise a 1-based array) seems a little fishy and that expression may be buggy. – chepner Oct 18 '13 at 20:25
  • 2
    For me, the first option is perfect. I needed to pass a parameter to another script that handles wildcards on its own. – mirelon Dec 27 '13 at 20:07