6

This question is based on a similar question on Ask Ubuntu, but instead of bash, I would like to have similar output in sh.

No issue in bash; it works as expected.

wolf@linux:~$ echo $SHELL
/usr/bin/bash
wolf@linux:~$ 
wolf@linux:~$ varA='Aug 01
> Aug 16
> Aug 26'
wolf@linux:~$ 
wolf@linux:~$ varB='04:25
> 03:39
> 10:06'
wolf@linux:~$ 
wolf@linux:~$ echo "$varA $varB"
Aug 01
Aug 16
Aug 26 04:25
03:39
10:06
wolf@linux:~$
wolf@linux:~$ paste <(printf %s "$varA") <(printf %s "$varB")
Aug 01  04:25
Aug 16  03:39
Aug 26  10:06
wolf@linux:~$

However, when I try similar command in sh, I'm getting the following error.

wolf@linux:~$ sh
$ 
$ varA='Aug 01
> Aug 16
> Aug 26'

$ varB='04:25
> 03:39
> 10:06'
$ 
$ echo "$varA $varB"
Aug 01
Aug 16
Aug 26 04:25
03:39
10:06
$ 
$ paste <(printf %s "$varA") <(printf %s "$varB")
sh: 22: Syntax error: "(" unexpected
$ 

Is it possible to get similar output in sh?

4

4 Answers 4

6

If your variables have the same number of lines then you could use the pr command to print standard output into two columns ex.

$ printf '%s\n' "$varA" "$varB" | pr -2 -Ts^I       
Aug 01  04:25
Aug 16  03:39
Aug 26  10:06

where ^I stands for the TAB character (to be consistent with the default separator of the paste command) and may be introduced using key combination Ctrl+V TAB, while -T turns off headers and footers.

1
  • If your shell is bash, you can write -s $'\t' Jan 19, 2021 at 23:09
1

You could always use mktemp but it's not specified by POSIX and there are some race conditions associated with it.

You can always use a while loop like that but remember that loops in shell are very slow:

counter=1
while true
do
    left="$(echo "$varA" | awk -v cnt="$counter" 'NR==cnt {printf $0}')"
    right="$(echo "$varB" | awk -v cnt="$counter" 'NR==cnt {printf $0}')"
    if [ -z "$left" ] && [ -z "$right" ]
    then
        break
    fi

    echo "$left $right"
    counter=$((counter + 1))
done

Output:

Aug 01 04:25
Aug 16 03:39
Aug 26 10:06

You can add more variables if you need to but it'll not satisfy the should work for a dynamic number of variables requirement because you have to add them manually. And you can of course implement everything within awk.

1
  • (1) The question says “… in side-by-side columns …”. And, no, it doesn’t explicitly define the term “columns”. But you seem to assume that each variable consists of a sequence of strings that are the same length. If that assumption is not true, this answer fails to satisfy the meaning that would be assumed by an ordinary English interpretation of the word “columns”. (2) Also, this solution can exhibit undesired behavior if the variables include blank lines. (3) If you know how to implement everything within awk, why not show such an answer? Mar 18, 2022 at 2:52
1
+100

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.)

0

1. Command paste

paste /dev/fd/3 3<<-EOF /dev/fd/4 4<<-EOF
> $varA
> EOF
> $varB
> EOF

Output :

Aug 01  04:25
Aug 16  03:39
Aug 26  10:06

alternative, you can use paste with temporary file too, but it lengthy process temp_file=$(mktemp)


2. Command pr

printf '%s\n' "$varA" "$varB" | pr -2 -t -s

Output:

Aug 01  04:25
Aug 16  03:39
Aug 26  10:06
5
  • This answer is partly correct, but it lacks detail and explanation.  Specifically, it doesn’t seem to honor the bounty comment: “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”.  (1) While it’s fairly obvious how to expand your paste solution to more than two variables, you should explain it.  (One sentence should be enough.) … (Cont’d) Mar 18, 2022 at 2:48
  • (Cont’d) …  And you should definitely point out that it might not work for more than seven variables, because not all shells understand 10<<.  (And why do you use <<- instead of plain <<?)  And your comment about temp_file=$(mktemp) is almost totally useless to somebody who doesn’t already know how to do it. … (Cont’d) Mar 18, 2022 at 2:48
  • (Cont’d) …  (2) The pr solution is basically an uncredited copy of steeldriver’s answer, and, as steeldriver pointed out, it does not work correctly for variables that do not have the same number of lines.  And, if it were an original, correct answer, you should explain that the -2 option should be adjusted to reflect the number of variables.   P.S.  There’s another solution using pr that does work correctly for variables that do not have the same number of lines — can you find it? Mar 18, 2022 at 2:48
  • I just noticed that your paste answer looks strikingly like Fritjof Larsson’s answer to the question Stéphane Chazelas linked to.  Yes, you certainly could have developed it independently; I won’t accuse you of plagiarism.  But be aware that Stack Exchange really dislikes plagiarism.  Please don’t copy somebody else’s work without identifying the source (ideally by name and link).   Even if somebody else’s work inspired your answer, you should acknowledge it. Mar 19, 2022 at 6:39
  • @G-ManSays'ReinstateMonica' aww man.. Did you have a grudge with me ? so you stalk all my activity ? (1). Detail of command, it up to my time, I'm not nanny to describe every command. (2). it just today that i know Fritjof Larsson answer indentical to mine. I searching on google like 20 tabs to get there. (3). Even "thankyou" word not recomended here, so it not wrong to simple answer, if it work uset it, if not change other
    – TyaCode
    Mar 19, 2022 at 9:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .