Delaying wildcard expansion in bash, while quoting for special characters

2.4k Views Asked by At

I've written a bash script that needs to do something later. It's called something like this:

later mv *.log /somewhere/else

However, when called like this *.log is expanded at call time, and the script is called as if I wrote

later mv 1.log 2.log 3.log /somewhere/else

I'm trying to get my script to expand wildcards later. I tried calling it like this

later mv '*.log' /somewhere/else

and also with \*, but these both result in the wildcard never getting expanded at all.

How do I expand the wildcards in the commandline? And is there a way to prevent expansion when the script is called, i.e. get the original parameters as they were typed?

This is the part of my scripts that prepares the call for later:

tmpfile=$(mktemp)

### Get a quoted commandline
line=
while (( "$#" > 0 )); do
        line="$line\"$1\" "
        shift
done

### Prepare a script to be run
echo '#!/bin/bash' > "$tmpfile"
echo "cd $(pwd)" >> "$tmpfile"
echo "trap 'rm \"$tmpfile\"' EXIT" >> "$tmpfile"
echo "$line" >> "$tmpfile"
chmod 777 "$tmpfile"

Note that I have to quote the commandline as some of my files and folders have spaces in their names; if I remove the quoting bit, even bits without wildcards stop working.

3

There are 3 best solutions below

8
On

The simplest way may be to simply pass the wildcard (glob) back to the shell for expansion,

for example:

 #!/bin/bash
 echo "For the wildcard $1"
 echo "I find these match(es): $(eval echo "$1")"

The $( … ) operator calls a sub-shell, and interpolates the results; the echo command simply returns them.

  • As @thb put in their answer, the eval is neccesary within a shell script (but not when keying it in on an interactive shell, in which case $(echo $1) works.

Note that you'll still have to escape spacing and the like, but it seems you've taken that into consideration. Naturally, the glob will need to be quoted when calling your script — this is why most Unix commands only take one list in the first or last position(s), although examples like find do exist that require escaped fileglobs, themselves.

PS: As for a program forcing the shell to not perform expansion: no, there's no built-in way to do so. The shell expands globs before (well, logically “before” if not necessarily chronologically) even identifying what file your program lives in, and is pretty well universally agnostic to what programs it runs.

6
On

Interesting question. I suspect that a better answer than mine is possible. Nevertheless, given

A='foo.*'

this seems to do what you want:

echo $(eval echo "$A")
0
On

Solution

In shell scripting there are bunch of tricky characters. Space and both quotes are the most difficult to handle. This suggested solution will address the problem of tricky characters by applying proper quoting.

later-script:

#!/bin/bash

if [ $# -lt 1 ]; then
        echo "Need argument!"
        exit 1
fi

### Get a quoted commandline
line=
files=($(echo "$1"))
for file in "${files[@]}"; do
        echo "Debug: Processing file [$file]"
        quoted_filename=$(printf '%q' "$file")
        if [ -n "$line" ]; then
                line="$line "
        fi
        line="$line$quoted_filename"
done
echo -e "Result:\n$line"

Testing the solution

Prep:

Preparing test environment with four matching entries having potential pitfalls:

mkdir 'foo.bar.*'
date > 'foo.bar test.dat'
touch 'foo."nickname".bar'
touch "foo.'single'.bar"

Run:

Using suggested wildcard from question.

./later.sh 'foo*'

Example output:

Debug: Processing file [foo.bar.*]
Debug: Processing file [foo.bar test.dat]
Debug: Processing file [foo."nickname".bar]
Debug: Processing file [foo.'single'.bar]
Result:
foo.bar.\* foo.bar\ test.dat foo.\"nickname\".bar foo.\'single\'.bar