0

An answer such as this does a good job explaining how to control passing all variables through to a command.

I wanted to explore how to do this on a per-argument basis. Observe (this was tested in zsh):

$ program() { echo 1: $1 2: $2 3: $3; }

$ run() { program "$@"; }

$ run2() { echo `run $1`; }

$ run2 'a b' c
1: a b 2: 3:

I want a way to pass a first arg which is a string that has spaces in it, and have that spliced $@style into another command. e.g. run2 "a b" c should produce 1: a 2: b 3:

At this point I have solved my immediate problem because although my test code breaks when I tested it in zsh, once implemented into an actual bash script, it works.

This does indicate that maybe this relies on some intricacies in string and argument handling that are not "safe". So this is more of a request for comment on a more robust way to achieve this behavior reliably.

Steven Lu
  • 2,282
  • actually this works in bash, but not in zsh. hmm. – Steven Lu Sep 23 '16 at 16:14
  • So if bash already does what you want, is this question moot? Interesting note about how it's different in zsh though – Eric Renouf Sep 23 '16 at 16:43
  • Well i'm not convinced this is the right or even a reasonable way to get the job done, it's very very error-prone... Such is life in shell scripting though I guess. – Steven Lu Sep 23 '16 at 17:42
  • Yeah, best be careful when trying to get word splitting to happen at only just the right moments, perhaps if you rephrase the question to better say what you're really trying to do we can help with other options though, but as it is your question is sort of "how do I get bash to do this thing that it does already" since your example input produces your example output – Eric Renouf Sep 23 '16 at 17:45
  • Yep I have updated the question a bit. – Steven Lu Sep 23 '16 at 17:56

2 Answers2

3

The difference between bash and zsh that matters here is in the way run2 calls run, specifically the effect of leaving $1 unquoted.

  • In zsh, run $1 applies the “remove-if-empty” operator to $1, i.e. calls run with the first argument that was passed to run2, except that if the first argument to run2 was empty (or if run2 was called with no argument), then run is called with no argument.
  • In other Bourne-style shells such as bash, run $1 applies the “split+glob” operator to $1, i.e. it splits the first argument to run2 into whitespace-separated chunks¹, interprets each piece as a wildcard pattern², and replaces each wildcard pattern that matches one or more file by the list of matches.

Thus run2 'a b' c calls run with the argument a b in zsh (the argument is passed unchanged), but calls run with the two arguments a and b in bash (split into whitespace-delimited pieces).

I want a way to pass a first arg which is a string that has spaces in it, and have that spliced $@style into another command. e.g. run2 "a b" c should produce 1: a 2: b 3:

Your description and your example say different things. If you want to pass the first argument to the other command, use run "$1" to make sure that the argument is not split. Passing arguments unchanged is the whole point of "$@".

It appears that what you actually want to do is break up the first argument to run2 into whitespace-delimited chunks. In bash, you can do this by turning off wildcard expansion and then (assuming that IFS isn't changed from the default) using an unquoted expansion.

run2 () ( set -f; run $1; )

(echo "$(somecommand)" is essentially equivalent to running somecommand in a subshell, and it appears that this is what you meant rather than echo $(somecommand) which applies split+glob on the output of the command, so I removed the redundant echo-command-substitution.)

In zsh, you can use the = character in a parameter substitution to perform world splitting (and no globbing) on the value.

run2 () { run $=1; }

The syntax of zsh is not compatible with that of plain sh. If you want to source an sh script from zsh, you can use the emulate builting:

emulate sh -c '. myscript.sh'

Use emulate ksh to emulate (some features of) ksh. This doesn't emulate all bash features, but it allows you to use arrays.

¹ More generally, based on the value of IFS.
² Unless this has been turned off with set -f.

  • Even on emulation zsh fails to act as other shells with unquoted expansions. Please analyze carefully run5 in my answer, compare dash output with zsh emulating ksh or sh. If you don't understand, please do ask!. –  Sep 25 '16 at 19:17
  • @sorontar There are corner cases where zsh's emulation isn't compatible with sh, but it's easy to avoid them. There's no run5 in your answer. I guess you meant run05? What of it? “Please analyze my obfuscated code carefully” — yeah, no. If you have a point, make it. – Gilles 'SO- stop being evil' Sep 25 '16 at 20:51
  • Keep your answers level, please. –  Sep 25 '16 at 20:56
  • I am not asking you to analize the code, analize the answers as printed. They are different in zsh (even with --shwordsplt) from all other shells. And, yes, I do mean run05 (as one example). All shells print 3 arguments, zsh (in emulation) prints 2. This is very far from a "corner case". –  Sep 25 '16 at 21:00
  • @sorontar I see, so you meant “zsh in sh emulation mode expands unquoted $@ differently from sh”. Indeed it does. That is a corner case. Unquoted variable expansion (except by mistake) isn't common in the first place and unquoted $* or $@ is even rarer. Given that the whole point of $@ is the double-quoted behavior, if you really mean unquoted $*, write $*. – Gilles 'SO- stop being evil' Sep 25 '16 at 21:21
  • The code in my answer has been greatly simplified. Note that zsh (even in emulation) has different results to all other shells. –  Sep 26 '16 at 02:34
0

Welcome to the world of quoting and quoting surprises.

The main issue is that zsh doesn't split on IFS characters by default.
In that: it is different from all other shells.

To test how quoting change the way shells work we need code that tests both quoted and unquoted versions of your code.

Your code (adding a couple of variables):

program() { printf '%02d-1: %6s   2: %6s    3: %6s' "$i" "$1" "$2" "$3"; }
runa()  { program  "$@" ; }
run1()  { echo "`runa  $1 $2 $3 `"; }
run1    'a b' c

Let me expand on the details.

Let's assume that you create a local sh link pointing to where zsh lives:

ln -s "/usr/bin/zsh" ./sh

And, also, let's assume you copy the following script to a so file.

An script repeating each function with quoted and unquoted variables:

program() { printf '%02d-1: %6s   2: %6s    3: %6s' "$i" "$1" "$2" "$3"; }
runa() { program "$@"; }
runb() { program  $@ ; }

run1() { echo "`runa  "$1" "$2" "$3"`"; }
run2() { echo "`runb  "$1" "$2" "$3"`"; }
run3() { echo "`runa  $1 $2 $3`"; }
run4() { echo "`runb  $1 $2 $3`"; }

for i in `seq 4`; do
    run"$i" 'a b' c
done

Then, On execution, we get this printed:

# Any shell (except zsh) results.
01-1:    a b   2:      c    3:
02-1:      a   2:      b    3:      c
03-1:      a   2:      b    3:      c
04-1:      a   2:      b    3:      c

Only the first run (run1), where all is quoted, keeps 'a b' joined.

However, zsh acts as if all has been quoted all the time:

# ZSH results.
01-1:    a b   2:      c    3:
02-1:    a b   2:      c    3:
03-1:    a b   2:      c    3:
04-1:    a b   2:      c    3:

zsh in emulation.

It is supposed that zsh will emulate older shells if called as sh or ksh.
But in practice this is not always true:

$ ./sh ./so   # emulated sh
01-1:    a b   2:      c    3:
02-1:    a b   2:      c    3:
03-1:      a   2:      b    3:      c
04-1:      a   2:      b    3:      c

The second line is different than the second line for any other shell.

It is a good idea to read the answers to this question

  • ITYM all other Bourne-like shell instead of all other shells. No sensible shell (rc, es, akagan, fish, zsh) splits by default. The splitting by default is a bug of other Bourne-like shells inherited from the Bourne shell (adminitedly, it's even worse in csh, and the origin of the problem probably lies in the Thomson shell (the ancestor of both) $1, $2... macro-like expansions). – Stéphane Chazelas Sep 29 '16 at 12:22