Search code examples
stringshellmultiple-columnstabstop

Format columns in string nicely with bash tools


Let's say I have a file orders.txt which contains something like:

#   Description Amount  Price   Sum
1   Beermat 1000    0,01€   10€
2   Glass   100 1€  100€
3   Long description    1   10€ 10€
4   An even longer description  1   10€ 10€
5   An extra long description, for real!    1   10€ 10€
6   An extra long description, almost max. length   1   10€ 10€
7   Long description for some really fancy product and unfortunately this description is too long to fit into one line - bad luck!  1   10€ 10€
8   This line isn’t shown afterwards    1   1€  1€

Where the columns are separated with a tabstop a.k.a. \t

Usually I format these things with a little tool column -ts $'\t' order.txt which results in something like:

#  Description                                    Amount  Price  Sum
1  Beermat                                        1000    0,01€  10€
2  Glass                                          100     1€     100€
3  Long description                               1       10€    10€
4  An even longer description                     1       10€    10€
5  An extra long description, for real!           1       10€    10€
6  An extra long description, almost max. length  1       10€    10€

This works fine as long as one line doesn't exceed the line width of your terminal window. So in case of line #7 this tool outputs a column: line too long and exits.

What I'm looking for is a solution to generate me an output like this one:

#  Description                                    Amount  Price  Sum
1  Beermat                                        1000    0,01€  10€
2  Glass                                          100     1€     100€
3  Long description                               1       10€    10€
4  An even longer description                     1       10€    10€
5  An extra long description, for real!           1       10€    10€
6  An extra long description, almost max. length  1       10€    10€
7  Long description for some really fancy product 1       10€    10€
   and unfortunately this description is too long
   to fit into one line - bad luck!
8  This line isn’t shown afterwards               1       1€     1€

Solution

  • Well handling the long descriptions and printing the Amount, Price and Sum after a fixed width while splitting the string to print the remainder after the Amount, Price and Sum line is not trivial. There are more than a few ways to split strings, and more ways that are more elegant, but a brute-force example will give you an idea. You can tidy this up as you see fit. Just set your width by changing the dwidth variable or providing the desired width as a second argument after the filename.

    This assumes your input format is as you described, with fields separated by tabs for example: #\tDescription\tAmount\tPrice\tSum

    #!/bin/bash
    
    test -r "$1" || { 
        printf "Error: insufficient input, usage ${0//*\//} <orders file>\n\n"
        exit 1
    }
    
    oifs=$IFS           # set IFS to only break on tab or newline
    IFS=$'\t\n'
    
    dwidth=${2:-50}     # set the print width you want for description (default 50)
    i=0
    
    while read num desc amt price sum || test -n "$num"; do
    
        # test description > width, if so print only first 50 (or on word break)
        if test "${#desc}" -ge "$dwidth" ; then
            for ((i=$dwidth; i>0; i--)); do
                test "${desc:$i:1}" = ' ' && break
            done
    
            end=$i
            printf "%2s %-*s %-8s %-8s %-8s\n" $num $dwidth "${desc:0:end}" $amt $price $sum
    
            remain=$((${#desc}-$end))       # calculate remaining chars to print
    
            while test "$remain" -gt 0; do  # while characters remain
                strt=$((end+1))             # start printing at last end
                if test "$remain" -gt "$dwidth"; then   # test if more than width remain
                    for ((i=$dwidth; i>0; i--)); do     # if so, break on word
                        test "${desc:$((strt+i)):1}" = ' ' && break
                    done
                    end=$((strt+i))         # set end equal to start + chars in words
                    printf "   %-*s\n" $dwidth "${desc:$strt:$i}"   # print to width
                else
                    printf "   %-*s\n" $dwidth "${desc:$strt}"      # print rest and break
                    break
                fi
                remain=$((${#desc}-$end))   # calculate new remaining chars
            done
        else    # if description not > width, just print it
            printf "%2s %-*s %-8s %-8s %-8s\n" $num $dwidth $desc $amt $price $sum
        fi
    
    done < "$1"
    
    exit 0
    

    output: $ bash orders.sh orders.txt

    # Description                                        Amount   Price    Sum
    1 Beermat                                            1000     0,01€    10€
    2 Glass                                              100      1€       100€
    3 Long description                                   1        10€      10€
    4 An even longer description                         1        10€      10€
    5 An extra long description, for real!               1        10€      10€
    6 An extra long description, almost max. length      1        10€      10€
    7 Long description for some really fancy product and 1        10€      10€
      unfortunately this description is too long to fit
      into one line - bad luck!
    8 This line isn’t shown afterwards                   1        1€       1€
    

    output: $ bash orders.sh orders.txt 60

    # Description                                                  Amount   Price    Sum
    1 Beermat                                                      1000     0,01€    10€
    2 Glass                                                        100      1€       100€
    3 Long description                                             1        10€      10€
    4 An even longer description                                   1        10€      10€
    5 An extra long description, for real!                         1        10€      10€
    6 An extra long description, almost max. length                1        10€      10€
    7 Long description for some really fancy product and           1        10€      10€
      unfortunately this description is too long to fit into one
      line - bad luck!
    8 This line isn't shown afterwards                             1        1€       1€