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.

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'"
  • change from "${i}.file" to "${i} x.file" ... what were you saying about being ok to use spaces? ;^) – Pierre Houston Aug 31 '18 at 21:18
  • 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 Aug 31 '18 at 21:26
  • @PierreHouston Bug fixed :))) – Alex Aug 31 '18 at 21:26
  • @PierreHouston For command line operations (without making script) use variant#2, just separate commands with ; character – Alex Aug 31 '18 at 21:28
  • 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 Aug 31 '18 at 21:33
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.

  • 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 Aug 31 '18 at 21:41
  • meant to say.. bash cannot do this without using eval. – Pierre Houston Sep 10 '18 at 18:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.