Possible in bash? Turning command expansion into arguments

0

Are the only options for bash command expansion:

  • unquoted $(..) whose output is always parsed with dumb string splitting, or
  • quoted "$(..)" whose output is always passed along as a unit?

I'm trying to replicate in bash a fish shell function I created for use on Mac OS. My fish function selection https://superuser.com/a/1165855 takes the selection in the frontmost window and outputs the paths for use in command substitution like ls -l (selection). I was hoping to achieve the same thing in bash perhaps as ls -l $(selection).

I thought it was a matter of quoting and so tried passing linefeed-delimited paths to bash's printf "%q ". However I found that no matter what quoting I wrapped the command substitution output in, it was getting divided at whitespace.

Simplified example:

$ touch file\ a file\ b
$ ls $( echo "file\ a file\ b" ) # want expansion equiv to: ls 'file a' 'file b'
ls: a: No such file or directory
ls: b: No such file or directory
ls: file\: No such file or directory
ls: file\: No such file or directory

It wouldn't be the end of the world if I had to use quoted command substitution like ls -l "$(selection)" but doing that the command's output never gets split, nevermind observing my careful quoting. Is the old backtick syntax any different?

Funny, bash, you've got a lot of features. Has nobody though to allow cmda $(cmdb-that-generates-parameters-for-cmda)? Or does a bash user just avoid any spaces or symbols in filenames (like an animal) to make everything easy? Thanks for any answers.

Pierre Houston

Posted 2018-08-31T00:05:47.977

Reputation: 113

Answers

2

There're multiple ways to do what you want. By default bash using whitespaces as default separator, but you can easily override this using IFS (Internal Field Separator) or use different technique such as enumeration. Below are few examples that first come to mind.

Variant#1

Using special internal variable IFS (Internal Field Separator)

#!/bin/bash

touch 'file a' 'file b'

# Set IFS to new line as a separator
IFS='
'
ls -la $( echo 'file a'; echo 'file b' )

Variant#2

Using for loop

#!/bin/bash

touch 'file a' 'file b'
for variable in 'file a' 'file b';do
  ls -la "${variable}"
done

Variant#3

Using for loop from predefined values

#!/bin/bash

MultiArgs='
arg 0 1
arg 0  2
arg 0   3
arg 0    N
'

# Using only new line as a separator
IFS='
'

for variable in ${MultiArgs};do
  touch "${variable}"
  ls -la "${variable}"
done

Variant#4

Using ~ (tilda) as separator

#!/bin/bash

MultiArgs='arg 0 1~arg 0 2~arg0 3~ar g 0 N' # Arguments with spaces

IFS='~'
for file in ${MultiArgs};do
 touch "${file}"
 ls -la "${file}";
done

Is the old backtick syntax any different?

No, it is the same but backticks has some limitation that $() doesn't.

Or does a bash user just avoid any spaces or symbols in filenames (like an animal) to make everything easy?

No, it is Ok to use spaces in filenames as far as one would use correct quoting.

As about $(cmdb-that-generates-parameters-for-cmda)

#!/bin/bash

# first command generate text for `sed` command that replacing . to ###
echo $( for i in {1..5}; do echo "${i} space .";done | sed 's/\./###/g' )

#############################################
# Another example: $() inside of another $()
for i in {1..5}; do touch "${i} x.file";done # Prepare some files with spaces in filenames

IFS='
'
echo "$( ls -la $(for i in ls *.file; do echo "$i"; done))"

If you'd like to pass all parameters in one single line to feed your program transcode you can add to the end your ~/.bashrc file following function:

_tr() {
  local debug
  debug=1
  [ $debug -eq 1 ] && {
    echo "Number of Arguments: $#"
    echo "Arguments: $@"
  }

  transcode "$@"
}

and call this function from command line like that:

eval _tr $(echo "$out")

Where variable out must be like that: out="'file a' 'file b'".
If would type filenames manually then call to _tr function might looks like:

eval _tr $(echo "'file a' 'file b'")

If you would use some external script in place of $() than external script/program must return list of files quoted exactly like this:

"'file a' 'file b'"

Alex

Posted 2018-08-31T00:05:47.977

Reputation: 5 606

change from "${i}.file" to "${i} x.file" ... what were you saying about being ok to use spaces? ;^) – Pierre Houston – 2018-08-31T21:18:44.100

And I'm looking to do something like this as a convenience from the command line, not from a script, so boilerplate like setting IFS etc is not appropriate. But thanks anyway, especially for direct answers to my specific questions – Pierre Houston – 2018-08-31T21:26:05.503

@PierreHouston Bug fixed :))) – Alex – 2018-08-31T21:26:13.610

@PierreHouston For command line operations (without making script) use variant#2, just separate commands with ; character – Alex – 2018-08-31T21:28:48.800

Nice fast bug fix :thumbsup:. I don't see how #2 helps me at all. I (almost) literally want to type at a bash prompt: cmda $(cmdb-that-generates-parameters-for-cmda), a more literal example: transcode $(selection). It seems not possible, I'm going to just start using fish-shell on my work Macs. – Pierre Houston – 2018-08-31T21:33:46.600

@PierreHouston transcode "$(for i in '1 x.file' '2 x.file';do echo "${i}";done)" That what I meant how to employ variant#2, list your files in place of N x.file but make sure you enclosed it with single quotes. You can insert your selection between in and first ; as many files as you need separated by space. – Alex – 2018-09-01T12:47:42.643

Thanks @Alex but this is not in a script, this is me typing a command at the prompt and I'm looking for a convenience :^) If I have to type transcode "$(for i in ... whatever then fail because the syntax isn't convenient. If I have to type the names of the files, then fail because the point is to have that filled in for me. I very literally only want to type transcode "$(selection)" and have the selection function or script find the files I want (this part I can do) and output something that bash will accept as paths that can have inconvenient spaces or symbols (this bash cannot do). – Pierre Houston – 2018-09-04T22:36:42.433

@PierreHouston When you typing manually filenames that includes space in command line, all you have to do is to enclose those filenames in single quotes. As about populating filenames from function or script selection, - could you add some example of such function or script to your question, Im pretty sure we would find some solution, usually it is a matter of adding double quoiting like "'some file'" in those external scripts or functions that generates filenames. – Alex – 2018-09-05T19:30:40.673

Thanks for your continued interest Alex :^) If its a matter of quoting then in my original "file a" "file b" example with transcode $( echo "X" ), one could come up with some X that would end up with transcode getting the two parameters "file a" and "file b". It seems however that output from$( )is not fed back into bash's quote parsing but instead split at spaces. If X is'some file'thentranscodegets parameters'someandfile'`. I should've spoken to your prev example more, I don't think "$( for i.. echo "${i} – Pierre Houston – 2018-09-06T17:58:28.073

(forgive the incomplete edit in the previous comment) The suggestion in your previous comment, transcode "$( for i.. )", would demonstrate the 2nd option bash has, quoted "$( .. )". It seems that any output from that is passed to transcode as one parameter. If this is wrong, then one should be able to make a counter-example in the form transcode "$( echo X )" for some X, right? So sorry if my original Q was unclear, restating: was trying to find an X so that either transcode $( echo X ) or transcode "$( echo X )" will end up with transcode getting the parameters file a file b – Pierre Houston – 2018-09-06T18:14:27.307

@PierreHouston Sorry with delay answer, got a little busy last days. I added one more solution to my answer(in the end), I guess it is exactly what you are looking for. If you need some assistance to create external script that would feed correctly _tr function, then add to your question external script that you using to form list of files that includes space(s) in filenames, so we would try to correct it. – Alex – 2018-09-08T17:47:00.537

1

With xargs:

echo '"file a" "file b"' | xargs ls

Mind the quotes. Note xargs is not a Bash builtin (I'm mentioning this in a context of your "Bash, you've got a lot of features").

With eval which is a Bash builtin:

eval ls $( echo '"file a" "file b"' )

Again, mind the quotes. This may backfire easily. Methods based on changing the IFS (this other answer) seem safer. On the other hand with eval you can even do this:

eval ls $( echo '"file "{a,b}' )

Unfortunately none of these is as simple as the other shell's ls (selection) syntax you began with. However you can simplify the syntax of any(?) solution with a custom function, e.g.:

_(){ "$2" | xargs "$1"; }

This allows you to call _ ls selection, if only selection generates input for xargs right. Even better, this:

_(){ "${@:$#}" | xargs "${@:1:$(($#-1))}"; }

makes _ ls -l "file c" selection possible. I guess you could create a similar function that uses the "change the IFS" method from another answer.

Still, this is not as flexible and elegant as fish's ls (selection). I expect you can do ls (selection1) (selection2) in fish; the above _ function doesn't cover this case.

On the other hand eval ls $(selection1) $(selection2) may work if selection1 and selection2 return strings sanitized with proper quoting and/or escaping. If an attacker can make you select a file named ; rm -rf ~/; you'd better sanitize well; or not use eval in the first place.

Kamil Maciorowski

Posted 2018-08-31T00:05:47.977

Reputation: 38 429

Right, I wasn't speaking of just bash, but command-line usability when using bash shell in a terminal app on a unix-y system, specifically Mac OS in my case. So all signs point to No, bash just cannot do this. Thanks. – Pierre Houston – 2018-08-31T21:41:48.023

meant to say.. bash cannot do this without using eval. – Pierre Houston – 2018-09-10T18:06:49.547