3

Such 'identity' function should satisfy the following 2 properties :

identity $(identity a\ b c\ d)
# (Expected output:)
# a b
# c d

And, given the following 'argv_count' function:

argv_count () { echo "argv_count('$@'):$#"; }
argv_count $(identity a\ b c\ d)
# (Expected output:)
# argv_count('a b c d'):2

Additional quotes could be introduced in the tests if need.

A simple candidate such as the following fail to pass the second test:

identity () { for arg in "$@"; do echo "$arg"; done; }

cat is not a correct solution, as it is an identity function relative to stdin|stdout.

Lucas Cimon
  • 1,859
  • 2
  • 24
  • 33

2 Answers2

6

No, it is not possible. The reason is that when the shell parses the output of a command substitution (that is, $(somecommand)), it performs word splitting and wildcard expansion but no quote or escape evaluation. This means that if identity's output includes a space, the shell will treat that as a separator between "words" (i.e. arguments to the other program) no matter what quotes/escapes/whatever you add to try to avoid this. Worse, any wildcard-containing words in the output will be expanded into lists of matching files (if there are any). This means that $(identity 'foo * bar') is doubly doomed to failure.

With that said, there are ways to sort of fake it by changing the shell settings. For example, set -f will turn off wildcard expansion, solving that problem -- except that you have to set it in the parent shell before running identity, and then set it back to normal afterwards or lots of other things will break. Similarly, you could change IFS to prevent word splitting from treating spaces as separators -- but again you'd have to change it in the parent shell and set it back afterward, and it'd then cause trouble for whatever replacement separator character you chose. So you can fake it, but it's pretty bad fakery.

EDIT: As Michael Kropat pointed out, using eval is another way to "fake" it, and is more flexible if done carefully.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151
  • Thanks to both of you for those explanations ! To replace this in some practical context : does it means that no function can safely return a list of filenames (potentially containing spaces) as output ? Because unless using `eval` in the caller, you would lose the distinction between space & separator and hence could not loop through this list of files later on. – Lucas Cimon Feb 09 '14 at 00:59
  • @LucasCimon: a function could return a list delimited by null characters (as `find -print0` does), but this cannot be used with the `$( )` construct -- you have to pipe it to something like `xargs -0` or a `while IFS= read -rd '' argument` loop. The reason this is "safe" is the same reason you can't use it with `$( )`: C strings cannot contain nulls, so neither arguments nor `$( )` can handle them. – Gordon Davisson Feb 09 '14 at 01:10
2

If you're willing to use eval you can work around the word-splitting on returned values:

$ argv_count a\ b c\ d
argv_count('a b c d'):2
$ identity() { printf ' %q' "$@"; }
$ eval argv_count "$(identity a\ b c\ d)"
argv_count('a b c d'):2
$ eval argv_count "$(eval identity "$(identity a\ b c\ d)")"
argv_count('a b c d'):2

Or with Gordon Davisson's trickier case:

$ argv_count $'foo\t * bar'
argv_count('foo  * bar'):1
$ eval argv_count "$(eval identity "$(identity $'foo\t * bar')")"
argv_count('foo  * bar'):1
Michael Kropat
  • 14,557
  • 12
  • 70
  • 91
  • There are a few weird cases where this doesn't work -- try `eval argv_count $(identity $'foo\t * bar')` for example. I think if you add double-quotes around it like `"$(identity ....)"` it'll fix these cases. – Gordon Davisson Feb 08 '14 at 00:43
  • @GordonDavisson Fascinating example! I have no clue why `eval argv_count $(eval identity $(identity $'foo\t * bar'))` works fine, but adding the `\t` breaks the simpler: `eval argv_count $(identity $'foo\t * bar')`. As for double-quotes as a fix, it breaks nestability I believe. – Michael Kropat Feb 08 '14 at 03:24
  • @GordonDavisson I up-voted yours as the correct answer btw. I only added my answer as an intellectual curiosity. – Michael Kropat Feb 08 '14 at 03:25
  • Actually, I think yours is a better kluge than the ones I suggested. The more complex version fails for me just like the simpler version; not sure why it'd work for you, but double-quoting does seem to nest fine, you just have to quote at each level: `eval argv_count "$(eval identity "$(identity $'foo\t * bar')")"` works as expected. – Gordon Davisson Feb 08 '14 at 04:55
  • @GordonDavisson holy smokes it does work! I don't know what I was typing in wrong yesterday to get different results. – Michael Kropat Feb 08 '14 at 15:29