0

Sorry if the title is bad (it is already the best one I can think of).

Anyway, here is the line of code:

printf "%s\n" "$(echo a b "c d")

Simple code. Of course, I will replace it with my own code later. This example is already enough for explanation.

The code above outputs:

a b c d

When the output I hoped to have is:

a
b
c d

by using a subshell.

To conclude, the "structure" of the code in the answer should be roughly similar to the first snippet of code in the question.

Is this even possible? Any help will be great.

P.S. I will edit my question to improve the question if requested from the comments.

sudoer
  • 45

2 Answers2

1

For something to print a list of arbitrary strings that you can then import as an array in bash, you can either use a NUL-delimited list (since bash variables can't contain NUL characters anyway) which you import using readarray -td '' (assuming bash-4.4+):

print0() { [ "$#" -eq 0 ] || printf '%s\0' "$@"; }

readarray -td '' list <(print0 a b "c d" $'e\nf')

Or get it to print the array definition with typeset -p and import with eval, which would also allow passing sparse arrays or more than one array:

eval "$( list=(a b "c d" $'e\nf'); typeset -p list)"

For which you could define a helper:

print_array() {
  eval "local $1"='( "${@:2}" )'
  typeset -p "$1"
}
bash-5.0$ eval "$(print_array list a b 'b c' $'e\nf')"
bash-5.0$ printf ' - <%s>\n' "${list[@]}"
 - <a>
 - <b>
 - <b c>
 - <e
f>

Beware that if called from within a function, the array will end up local to the function because typeset prepends declare -a to the array definition.

You could also get it to print set -- a b 'c d'... to import into the positional parameters:

set_argv() { printf 'set --'; [ "$#" -eq 0 ] || printf ' %q' "$@"; }
bash-5.0$ eval "$(set_argv a b 'b c' $'e\nf')"
bash-5.0$ printf ' - <%s>\n' "$@"
 - <a>
 - <b>
 - <b c>
 - <e
f>

Note that bash's typeset or printf %q don't always use the safest method of quoting, so beware of potential risks associated with evaluating that output.

0

For the given example you should use arrays. See Arrays and Parameter Expansion from the Bash manual page for more information about arrays.

args=(a b "c d")
printf '%s\n' "${args[@]}"

If you need to use the output of a command as arguments you could use the IFS parameter as argument separator and command substitution. Since globbing is performed after word splitting, you may want to disable globbing temporarily, also it's important to note empty items will be discarded during word splitting (thanks to Stéphane Chazelas for the reminders).

# save previous IFS value and change it to line feed
OLDIFS=$IFS IFS=$'\n'

temporarily disable globbing if needed

#set -o noglob

save command output to array, words are separated by IFS

args=($(printf 'arg 1\narg 2\narg 3\n'))

reenable globbing if needed

#set +o noglob

restore IFS

IFS=$OLDIFS

expand array as command arguments

printf '%s\n' "${args[@]}"

Using xargs is probably an easier option, but don't forget it's not a shell builtin and cannot call shell builtins directly, so in the following example we're not invoking the printf Bash builtin but the printf binary (usually located at /bin/printf or /usr/bin/printf).

# POSIX xargs requires quoting
printf '"arg 1" "arg 2" "arg 3"' | xargs printf '<arg>%s</arg>\n'

other xargs (like GNU) versions have a -d delimiter option

printf 'arg 1\narg 2\narg 3' | xargs -d'\n' printf '<arg>%s</arg>\n'

GNU and other xargs also have a (non-standard) -0 option which allows to separate arguments with a null characters (usually represented as '\0'), which is very convenient, since arguments cannot ever contain null bytes. Other commands take this into account, allowing to separate items with null bytes, like many find versions, which include a -print0 option to separate found files with '\0', although, again, it's not an standard option.

# find files under /bin and calculate their sha256 digests
find /bin/ -type f -print0 | xargs -0 sha256sum
don_aman
  • 1,373