2

This isn't all I'm doing in my real application, but it boils down to this:

I have a function that returns stuff with a space in it. I want to be able to call the function and have the contents be interpreted as a single argument, and I want to do this in the cleanest / shortest way possible.

For example:

sp() {
    echo "hello world"
}
echo `sp`         # two arguments to echo, BAD
echo $(sp)        # two arguments to echo, BAD
echo "$(sp)"      # one argument to echo, GOOD, but this takes a lot of characters

Is there any shorter / cleaner way to achieve what I want to do? I'm looking for something that's easier to type (4 extra characters is a lot). Perhaps some features that are not part of sh, but maybe bash or zsh? This sounds horrible, but because this is going to be typed into an interactive session, so perhaps some way to hook into the shell and modify the command line before it is executed?

1
  • 2
    In Bash and POSIX-compliant shells, the third alternative is the shortest reliable way of doing it. Commented Jul 26, 2014 at 5:32

3 Answers 3

3

zsh will do what you want out-of-the-box -- in zsh, foo $bar treats the expansion of $bar as a single argument by default, as opposed to string-splitting and glob-expanding unless it's double-quoted.

That said, it violates the POSIX sh standard by doing so, and you should be wary of learning bad habits: I personally used zsh until I discovered that it was making me sloppy when trying to write scripts for other shells.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks, but I'm trying to evaluate a function (that will return different things, so I can't just store the results in a variable). Is it possible to do anything similar with a function?
Sounds like the question you asked doesn't exactly match the one you wanted to ask. Anyhow -- yes, you can do that. One approach is to accept the place to store results as an argument. Another approach is to return a NUL-delimited stream, and read from that in the caller; yet another is to use hardcoded global variables for your output. If you want more details on any of these, ask in a separate question (if you can't find one that already answers it on the site).
@fwenom ...continuing re: said unrelated question asked as a followup (grumble), see also BashFAQ #006: mywiki.wooledge.org/BashFAQ/…
This is actually exactly what I was looking for, thanks! I missed that variables could actually be defined like x="$(run some function here)" and that they would be re-evaluated every time you wrote $x.
@fwenom, I'm glad this answer proved useful, but still a bit shaky in terms of understanding how you're using it; the example you just gave only runs the function once. See BashFAQ #50 to better understand caveats and limitations around storing code in scalar variables: mywiki.wooledge.org/BashFAQ/050
|
2

I want to be able to call the function and have the contents be interpreted as a single argument, and I want to do this in the cleanest / shortest way possible.

The cleanest and shorted way possible is to enclose your arguments around quotes to prevent word splitting and possible pathname expansion (now named filename expansion in 4.3) however there's a workaround if you only plan to run your scripts on Zsh or Bash.

In Bash, you can simply just disable word splitting with IFS=. You can also disable pathname expansion with shopt -s -o noglob or set -f. With sensible application this can actually be a good practice for advanced scripters if they know what they're doing.

Of course with word splitting disabled you can no longer do for x in $something; do. But that doesn't matter anyway since unless pathname expansion is disabled, that form is not commendable. You can however use IFS=<separators> read -rd '' -a array <<< "$something"; for x in "${array[@]}"; do as a better alternative.

With pathname expansion disabled you can still use compgen (applies extglob and dotglob as well when enabeld):

readarray -t files < <(compgen -G '*')

Or

IFS='\n' read -rd ''  -a files < <(compgen -G '*')

But that's not going to be perfect enough for filenames with newlines as conservative parties would say. So you can use a generator function:

function generate {
    local RESTORE=$(shopt -p -o noglob) IFS=
    shopt -u -o noglob
    __=($1)
    eval "$RESTORE"
}

generate '*'

for file in "${__[@]}"; do
    :
done

This is good if you don't do pathname expansion often.

And there's one last thing: "$*" and "${var[*]}" would no longer expand with space as a separator. So like when having a custom separator, merging arrays to a string is easy with eval:

IFS=' ' eval 'merged="${var[*]}"'

And all of these can be done safely by inserting the rules at the beginning of the script.

[ -n "$BASH_VERSION" ] || exit  ## You may use a block instead and insert a message if you like.
[[ BASH_VERSINFO -ge 4 ]] || exit  ## If you need `readarray` or other 4.0+ stuffs.
IFS=
shopt -s -o noglob

With that you can now safely use do_something $x.

Now watch how these ideas go to posing online wikis later ;)

Like I said, this is for advanced users. People who care much about promotion and denounce possibly misleading practices to newbies would probably not want this. So as explicitly noted, stay away from this if you have an opposing approach or perception. No need to bash with "obvious" remarks.

4 Comments

Since when does a "for experts only" line only give one free rein to promote code that would make handling the filename /tmp/$(rm -rf /)/foo hazardous? It's certainly possible to quote content for safe evaluation using printf '%q '; if you did so here, I'd have no qualm with the eval-based example.
@CharlesDuffy You have such a good comment that I don't follow you at all. Perhaps try to be more specific?
And I'm not promoting anything. I'm giving a solution as required. On the contrary you are the one promoting your unwarranted scripting philosophies.
I haven't contributed a single change to the Wooledge wiki in years; if anyone is appropriating your code without credit, that wouldn't be me. That said -- it appears that you're correct with respect to the usage of eval in this context. eval 'merged="${var[*]}"' is indeed safe, and my contrary comment mistaken.
1

Since this is for interactive use, perhaps you could use an auxiliary function to do the quoting for you. Something like:

$ func() { for arg; do echo "arg: $arg"; done; }
$ q() { cmd=$1; shift; $cmd "$*"; }
$ sp() { echo hello world; }
$ f="$(sp)"
$ echo $f
hello world
$ func $f
arg: hello
arg: world
$ q func $f
arg: hello world
$ q func `sp`
arg: hello world

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.