3

In my script, how can I distinguish when the asterisk wildcard character was used instead of strongly typed parameters?

This

# myscript *

from this

# myscript p1 p2 p3 ... (where parameters are unknown number)
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
kofucii
  • 7,393
  • 12
  • 51
  • 79

6 Answers6

7

The shell expands the wildcard. By the time a script is run, the wildcard has been expanded, and there is no way a script can tell whether the arguments were a wildcard or an explicit list.

Which means that your script will need help from something else which is not a script. Specifically, something which is run before command-line processing. That something is an alias. This is your alias

alias myscript='set -f; globstopper /usr/bin/myscript'

What this does is set up an alias called 'myscript', so when someone types 'myscript', this is what gets run. The alias does two things: firstly, it turns off wildcard expansion with set -f, then it runs a function called globstopper, passing in the path to your script, and the rest of the command-line arguments.

So what's the globstopper function? This:

globstopper() {
  if [[ "$2" == "*" ]]
    then echo "You cannot use a wildcard"
    return
  fi
  set +f
  "$@";
}

This function does three things. Firstly, it checks to see if the argument to the script is a wildcard (caveat: it only checks the first argument, and it only checks to see if it's a simple star; extending this to cover more cases is left as an exercise to the reader). Secondly, it switches wildcard expansion back on. Lastly, it runs the original command.

For this to work, you do need to be able to set up the alias and the shell function in the user's shell, and require your users to use the alias, not the script. But if you can do that, it ought to work.

I should add that i am leaning heavily on the resplendent Simon Tatham's essay 'Magic Aliases: A Layering Loophole in the Bourne Shell' here.

Tom Anderson
  • 46,189
  • 17
  • 92
  • 133
  • Simon Tatham's essay is interesting, but starts from a faulty premise: that Bourne shells support aliases. Real Bourne shells do not support aliases. Only heavily extended Bourne shells do, and it is something of a moot point whether they're still Bourne shells or just POSIX shells or something else at that point. – Jonathan Leffler Sep 13 '10 at 16:26
  • 1
    I respectfully submit that this solution is not a general purpose solution. There are some limited circumstances under which it will seem to work, but `/usr/bin/myscript` still cannot tell whether it was originally invoked with wildcards or not. – Jonathan Leffler Sep 13 '10 at 16:45
  • 1
    @Jonathan: you're absolutely right, the script can't tell. But the alliance of the script and the alias can - i believe you can *always* use this approach to detect wildcards in the arguments; can you suggest a case where you can't? That said, if you do something like echo * | xargs myscript, then i don't think there's any way to detect that, even in principle. Or echo * >everything; echo "time passes"; xargs myscript – Tom Anderson Sep 13 '10 at 22:49
  • When using csh instead of the alias-enabled Bourne/Korn/Bash shell? Or when the script is invoked directly by absolute pathname. I'm not sure whether non-interactive shells necessarily read the control files where the alias is set up - it depends in part on where you put it. Perl scripts won't notice it. I wonder about `make` and similar programs. – Jonathan Leffler Sep 13 '10 at 23:06
7

I had a similar question, but rather than detecting when the user called the script using a wildcard, I simply wanted to prevent the use of the wildcard, and pass the string pre-expansion.

Tom's solution is great if you want to detect, but I'd rather prevent. In other words, if I had a script called findin that looked like

#!/bin/bash
echo "[${1}]"

and ran it using:

$ findin *

I would expect the output to be simply

[*]

To do this, you could just alias findin by

alias findin='set -f; /path/to/findin'

But then you would have the shell option set for the rest of your session. This will likely break many programs that don't expect this (e.g. ls -lh *.py). You could verify this by typing

echo $-

in console. If you see an f, that option is set.

You could manually clear the option by typing

set +f

after every instance of findin, but that would get tedious and annoying.

Since shell scripts spawn subshells and you cannot clear the flag from within the script (set +f), the solution I came up with was the following:

g(){ /usr/local/bin/findin "$@"; set +f; }
alias findin='set -f; g'

Note: 'g' might not be the best name for the function, so you'd be encouraged to change it.

Finally, you could generalize this by doing something like:

reset_expansion(){ CMD="$1"; shift; $CMD "$@"; set +f; }
alias findin='set -f; reset_expansion /usr/local/bin/findin'

That way another script where you would want expansion disabled would only require an additional alias, e.g.

alias newscript='set -f; reset_expansion /usr/local/bin/newscript'

and not an additional wrapper function.

For a much longer than necessary writeup, see my post here.


Community
  • 1
  • 1
jedwards
  • 29,432
  • 3
  • 65
  • 92
3

You can't.

It is one of the strengths (or, in some eyes, weaknesses) of Unix.

See the diatribe(s) in "The UNIX-HATERS Handbook".

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
1

Addendum

I found this post while looking for a workaround for my command line calculator:

alias c='set -f; call_clc'

where "call_clc" is the function: "function call_clc { clc "$*"; set +f; }"

and "clc" is the script
#!/bin/bash
echo "$*" | sed -e 's/ //g' >&1 | tee /dev/tty | bc

I need 'set -f' and 'set +f' in order to make inputs such as 'c 4 * 3' to work, therefore an asterix with white space before and after, in order to prevent globbing of the bash.

Update: the previous variant 'alias c='set -f; clc "$*"; set+f;'' did not work because for some reason the correct result was given after invoking the command "c 4 * 4' twice. Anyone an idea why this is so?

waldy
  • 11
  • 3
  • Regarding why your previous attempt didn't work: Aliases can't take arguments, so "$*" probably expanded to nothing when you used the alias. Functions seem to be the go-to solution - see [here](http://stackoverflow.com/questions/4060880/shell-bash-passing-argument-to-alias) for example. I still don't know about why it would work after calling it twice, though. Haven't tried it. – george Jul 16 '13 at 14:03
1
$arg_num == ***; // detects *(literally anything since it's a global wildcard)
$arg_num == *_*; // detects _

here is an example of it working with _

for i in $*
  do
    if [[ "$i" == *_* ]]; 
      then echo $i; 
    fi
  done

output of ./bash test * test2 _

_

output of ./bash test * test2 with ********* rather then ****

test
bash
pass.rtf
test2
_

NOTE: the * is so global in bash that it printed out files matching that description or in my case of the files on my oh-so-unused desktop. I wish I could give you a better answer but the best choice it to use something other then * or another scripting language.

rivenate247
  • 2,116
  • 2
  • 16
  • 18
0

If this is something you feel you must do, perhaps:

# if the number of parms is not the same as the number of files in cwd
# then user did not use *
dir_contents=(*)
if [[ "${#@}" -ne "${#dir_contents[@]}" ]]; then 
    used_star=false
else
    # if one of the params is not a file in cwd
    # then user did not use *
    used_star=true
    for f; do [[ ! -a "$f" ]] && { used_star=false; break; }; done
fi
unset dir_contents

$used_star && echo "used star" || echo "did not use star"

Pedantically, this will echo "used star" if the user actually used an asterisk or if the user manually entered the directory contents in any order.

glenn jackman
  • 238,783
  • 38
  • 220
  • 352