The bounty notice says
The answer should work on all shells,
for variables that do not have the same number of lines,
and should work for a dynamic number of variables
“work on all shells”, strictly speaking, is impossible.
It’s just about impossible to do anything non-trivial
that works in C shell-family shells
and also Bourne shell / POSIX-family shells.
I have tested the following in bash,
dash (which is a fairly rudimentary POSIX-compliant shell), ash and zsh.
Here are a few ways to set multi-line variables:
var17='The
quick
brown
fox'
var42=echo jumps; echo over; echo -e 'the\nlazy\ndog.'
another_var=$(printf '%s\n' And they lived happily ever after.)
yet_another=$'The\nend.'
The first method (literally typing Enter inside quotes)
should work in most, if not all, POSIX-compliant shells1.
The second method
(`echo word1 ; echo word2`
)
should work in most, if not all, POSIX-compliant shells2,
although the
echo -e 'word3\nword4'
)
part might not.
Some old shells might not recognize $(…)
,
and the $'…'
is (exclusively?) a bashism.
I deliberately used variable names without a consistent pattern
to demonstrate that the solution doesn’t depend on
the variable names following a pattern.
You could use varA
, varB
, varC
and varD
(or var1
, var2
, var3
and var4
) if you want.
Running the solution:
$ ./myscript1 "$var17" "$var42" "$another_var" "$yet_another"
The jumps And The
quick over they end.
brown the lived
fox lazy happily
dog. ever
after.
Note the obvious: the strings have different numbers of lines,
and the lines have different numbers of characters.
We don’t have to use the variables;
we can put a multi-line string value directly on the command line:
$ ./myscript1 "$var17" "$var42" "$another_var" $'The\nend.'
The jumps And The
quick over they end.
brown the lived
fox lazy happily
dog. ever
after.
Here’s myscript1
:
#!/bin/dash
first=1
for a in "$@"
do
if [ "$first" = 1 ]
then
set --
first=
fi
file=`mktemp`
set -- "$@" "$file"
printf '%s\n' "$a" > "$file"
done
pr -T -m "$@"
rm "$@"
Notes:
- Since many POSIX-compliant shells do not have named arrays,
we use the one (unnamed) array that they do all support: the argument list.
- Once we get into the
for a in "$@"
loop,
its value list is set, so we can change the shell’s argument list.
- First time through, clobber the shell’s argument list with
set --
.
- Each time through,
- Create a temporary file.
Use
`mktemp`
rather than $(mktemp)
for increased portability
(otherwise, $(…)
is generally preferred).
- Add the filename to the argument list.
- Write the current argument
(i.e., the multi-line string from the original argument list) to the file.
I don’t know whether
printf
is available
as a builtin command in all shells,
but it should be available
(as either a builtin command or an external executable program)
on all systems outside a museum.
If your system doesn’t have printf
, you can try using echo
,
but beware strings that begin with -
or contain \
.
- Invoke
pr
on the list of files.
- Use
-T
, as identified by steeldriver, to prevent pagination.
- Use
-m
to merge the files (i.e., in multi-column mode).
- Finally, delete all the files.
This looks like it separates the columns by a lot of space,
but it doesn’t always:
$ ./myscript1 "A very long string about a quick brown fox and a lazy dog" .
A very long string about a quick br .
What’s happening is:
pr
counts the number of files; i.e., the number of columns.
Let’s call it n.
- It divides the line width by n, rounding down.
(The line width defaults to 72.)
- It creates n columns,
each ⌊ 72/n ⌋ character positions wide.
Values narrower than that are padded; values wider than that are truncated.
You may prefer to avoid the blank space and the truncation.
We can do this,
since we can specify the line width to pr
with the -w
option:
$ ./myscript2 "A very long string about a quick brown fox and a lazy dog" .
A very long string about a quick brown fox and a lazy dog .
Here’s myscript2
:
#!/bin/dash
if [ "$#" = 0 ]
then
printf '%s\t%s\n' "Usage:" "$0 [string] ..."
exit 1
fi
first=1
maxwidth=0
for a
do
if [ "$first" = 1 ]
then
set --
first=
fi
file=`mktemp`
set -- "$@" "$file"
printf '%s\n' "$a" > "$file"
width=`printf '%s\n' "$a" | wc -L`
if [ "$width" -gt "$maxwidth" ]
then
maxwidth="$width"
fi
done
linewidth=`expr "$#" "*" "(" "$maxwidth" + 2 ")"`
pr -T -w "$linewidth" -m "$@"
rm "$@"
Notes:
for a
is short for for a in "$@"
.
This works in ash, bash, dash, zsh, and probably others.
wc -L
finds the maximum line length in the input.
Unfortunately, this is a GNU extension.
This can be done using POSIX tools, but not as simply.
- The loop finds the longest line in any of the inputs …
- … and then we calculate a line width that’s enough
to accommodate n columns of that width,
plus two spaces for column separation.
____________
1 But the C shell does not allow this
unless you type a \ before the Enter.
2 The C shell allows this,
but it converts the newlines into spaces.
(I’m probably missing something.)
<( ... )
is a bash construct not available in sh – Archemar Jan 19 '21 at 11:45