0

Suppose I have an associative array of ( [name]="Some description" ).

declare -A myItems=(
  [item1]='Item1 description'
  [item2]='Item2 description'
)

I now want to print a table of myItems with nice, even column lengths.

str=''
for n in $(echo "${!myItems[@]}" | tr " " "\n" | sort); do
  str+="$n\t${myItems[$n]}\n"
done

# $(printf '\t') was the simplest way I could find to capture a tab character
echo -e "$str" | column -t -s "$(printf '\t')"

### PRINTS ###
item1  Item1 description
item2  Item2 description

Cool. This works nicely.

Now suppose an item has a description that is multiple lines.

myItems=(
  [item1]='Item1 description'
  [item2]='Item2 description'
  [item3]='This item has
  multiple lines
  in it'
)

Now running my script prints

item1             Item1 description
item2             Item2 description
item3             This item has
  multiple lines
  in it

What I want is

item1  Item1 description
item2  Item2 description
item3  This item has
         multiple lines
         in it

Is this achievable with column? If not, can I achieve it through some other means?

dx_over_dt
  • 13,240
  • 17
  • 54
  • 102

3 Answers3

3

Assuming the array keys are always single lines, you can prefix all newline characters inside the values by a tab so that column aligns them correctly.

By the way: Bash supports so called C-Strings which allow you to specify tabs and newlines by their escape sequences. $'\n\t' is a newline followed by a tab.

for key in "${!myItems[@]}"; do
  printf '_%s\t%s\n' "$key" "${myItems[$key]//$'\n'/$'\n_\t'}"
done |
column -ts $'\t' |
sed 's/^_/ /'

If you also want to sort the keys as in your question, I'd suggest something more robust than for ... in $(echo ...), for instance

printf %s\\n "${!myItems[@]}" | sort |
while IFS= read -r key; do
   ...

And here is a general solution allowing for multi-line keys and values:

printf %s\\0 "${!myItems[@]}" | sort -z |
while IFS= read -rd '' key; do
   paste <(printf %s "$key") <(printf %s "${myItems[$key]}")
done |
sed 's/^/_/' | column -ts $'\t' | sed 's/^_//'
Socowi
  • 25,550
  • 3
  • 32
  • 54
  • 2
    This didn't work for me. I needed to add `$'\n'` to the end of the `printf` line, but it prints the same thing as my original. – dx_over_dt Aug 18 '20 at 19:57
  • @dx_over_dt Can you try again? It seems most versions of `column` ignore leading delimiters. I worked around this behavior by inserting a dummy `_` at the start of each line and removing it afterwards. – Socowi Aug 18 '20 at 20:05
1

You may use this 2 pass code:

# find max length of key
for i in "${!myItems[@]}"; do ((${#i} > max)) && max=${#i}; done

# create a variable filled with space using max+4; assuming 4 is tab space size
printf -v spcs "%*s" $((max+4)) " "

# finally print key and value using max and spcs filler
for i in "${!myItems[@]}"; do
   printf '%-*s\t%s\n' $max "$i" "${myItems[$i]//$'\n'/$'\n'$spcs}"
done
item1   Item1 description
item2   Item2 description
item3   This item has
          multiple lines
          in it
anubhava
  • 761,203
  • 64
  • 569
  • 643
  • 2
    This doesn't work any longer if the keys are longer than one tab width, and if there are items after the item with a multi-line description. – Benjamin W. Aug 18 '20 at 19:55
  • 1
    Note that this is not guaranteedto display each key next to its corresponding value. See [Do keys and values of associative arrays expand in the same order?](https://stackoverflow.com/q/54306133/6770384). However, for non critical programs I think the order is reliable enough. – Socowi Aug 18 '20 at 19:55
  • 1
    This doesn't quite work. When an item with multiple lines comes first, it aligns each of its lines with the next key. `item3 This item has` `item 2 multiple lines` `item 3 in it` – dx_over_dt Aug 18 '20 at 20:01
  • 1
    ok @dx_over_dt, looks like your data is different from what you've shown in question. Let me rework this and make it more generic – anubhava Aug 18 '20 at 20:02
  • OPs data is exactly as shown in the question. The difference is, that OP sorted their keys before listing them. Without sorting, `item3` is listed first and causes these problems. – Socowi Aug 18 '20 at 20:28
  • Assuming the keys are always single lines, you can fix their vertical alignment using a little trick. For each key, append the newlines from its corresponding value. Replace the first `<(...)` by `<(for key in "${!myItems[@]}"; do printf %s\\n "$key${myItems[$key]//[^$'\n']/}"; done)`. BTW: You might want to pipe `paste`'s output through `column -ts $'\t'`. – Socowi Aug 18 '20 at 20:32
  • ok final update. Got rid of awk and did all that in simpler bash code. – anubhava Aug 18 '20 at 20:51
0

Something tested

#!/usr/bin/env bash

declare -A myItems=(
  [item1]='Item1 description'
  [item2]='Item2 description'
  [item3]='This item has
multiple lines
in it'
)

# Iterate each item by key
for item in "${!myItems[@]}"; do
  
  # Transform lines of item name into an array
  IFS=$'\n' read -r -d '' -a itemName <<<"$item"

  # Transform lines of item description into an array
  IFS=$'\n' read -r -d '' -a itemDesc <<<"${myItems[$item]}"

  # Computes the biggest number of lines
  lines=$((${#itemName[@]}>${#itemDesc[@]}?${#itemName[@]}:${#itemDesc[@]}))

  # Print each line formated
  for (( l=0; l<lines; l++)); do
    printf '%-8s%s\n' "${itemName[$l]}" "${itemDesc[$l]}"
  done
done

Output:

item1   Item1 description
item2   Item2 description
item3   This item has
        multiple lines
        in it
Léa Gris
  • 17,497
  • 4
  • 32
  • 41