126

I want to format text as a table. I tried echoing with a '\t' separator, but it was misaligned.

Desired output:

a very long string..........     112232432      anotherfield
a smaller string                 123124343      anotherfield
Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
user1709294
  • 1,675
  • 5
  • 18
  • 21

11 Answers11

210

Use the column command:

column -t -s' ' filename
Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
P.P
  • 117,907
  • 20
  • 175
  • 238
  • This won't work for the example given in the question as there are spaces in the first column of data. – Burhan Ali Apr 03 '14 at 16:35
  • 3
    @BurhanAli Do I have to repeat my previous comment? All the answers assume some delimiter. OP hasn't said about the delimiter. So the same delimiter can be used in column as well. *as there are spaces in the first column of data* then how do you call it as *first* column? – P.P Apr 03 '14 at 16:42
  • 1
    No need to repeat. I read them. My comment is based on the desired output in the question. Using this answer on the given input does not produce the desired output. – Burhan Ali Apr 03 '14 at 16:44
  • @BurhanAli Even if you use printf, you need to know the delimiter. `%s` format specifier takes whitespace as delimiter. In that case, none of the answers here will work. I am amazed that you repeatedly talk about *desired* output when the parsing (using *any* tool) depends on the input's delimiter. – P.P Apr 03 '14 at 16:54
  • 3
    example for preparing the delimiter: `cat /etc/fstab | sed -r 's/\s+/ /g' | column -t -s' '` – untore Apr 29 '17 at 17:34
  • 3
    example for preparing the delimiter: `sed -r 's/\s+/ /g' /etc/fstab | column -t -s' '` – voices Jan 15 '18 at 15:43
  • `printf` doesn't use whitespace as a delimiter. It doesn't care about whitespace in the context that I provided the answer, nor does it care what is in the variables. – UtahJarhead Nov 25 '19 at 15:24
  • If someone is wondering, @voices avoided a [UUOC](http://catb.org/jargon/html/U/UUOC.html). – Bruno Saboia Jul 12 '23 at 05:56
132

printf is great, but people forget about it.

$ for num in 1 10 100 1000 10000 100000 1000000; do printf "%10s %s\n" $num "foobar"; done
         1 foobar
        10 foobar
       100 foobar
      1000 foobar
     10000 foobar
    100000 foobar
   1000000 foobar

$ for((i=0;i<array_size;i++));
do
    printf "%10s %10d %10s" stringarray[$i] numberarray[$i] anotherfieldarray[%i]
done

Notice I used %10s for strings. %s is the important part. It tells it to use a string. The 10 in the middle says how many columns it is to be. %d is for numerics (digits).

See man 1 printf for more info.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
UtahJarhead
  • 2,091
  • 1
  • 14
  • 21
  • 51
    just one advice which is useful when printing tables: `%-10s` wiil generate left-aligned strings of length 10 – steffen Oct 21 '15 at 13:45
  • 2
    @UtahJarhead the reference to variables stringarray[$i] should be replaced by ${stringarray[i]} and having the fist string spaces it has to be quoted "${stringarray[i]}" to avoid space char being interpreted as a delimiter. – Daniel Perez Apr 13 '20 at 14:41
26
function printTable()
{
    local -r delimiter="${1}"
    local -r data="$(removeEmptyLines "${2}")"

    if [[ "${delimiter}" != '' && "$(isEmptyString "${data}")" = 'false' ]]
    then
        local -r numberOfLines="$(wc -l <<< "${data}")"

        if [[ "${numberOfLines}" -gt '0' ]]
        then
            local table=''
            local i=1

            for ((i = 1; i <= "${numberOfLines}"; i = i + 1))
            do
                local line=''
                line="$(sed "${i}q;d" <<< "${data}")"

                local numberOfColumns='0'
                numberOfColumns="$(awk -F "${delimiter}" '{print NF}' <<< "${line}")"

                # Add Line Delimiter

                if [[ "${i}" -eq '1' ]]
                then
                    table="${table}$(printf '%s#+' "$(repeatString '#+' "${numberOfColumns}")")"
                fi

                # Add Header Or Body

                table="${table}\n"

                local j=1

                for ((j = 1; j <= "${numberOfColumns}"; j = j + 1))
                do
                    table="${table}$(printf '#| %s' "$(cut -d "${delimiter}" -f "${j}" <<< "${line}")")"
                done

                table="${table}#|\n"

                # Add Line Delimiter

                if [[ "${i}" -eq '1' ]] || [[ "${numberOfLines}" -gt '1' && "${i}" -eq "${numberOfLines}" ]]
                then
                    table="${table}$(printf '%s#+' "$(repeatString '#+' "${numberOfColumns}")")"
                fi
            done

            if [[ "$(isEmptyString "${table}")" = 'false' ]]
            then
                echo -e "${table}" | column -s '#' -t | awk '/^\+/{gsub(" ", "-", $0)}1'
            fi
        fi
    fi
}

function removeEmptyLines()
{
    local -r content="${1}"

    echo -e "${content}" | sed '/^\s*$/d'
}

function repeatString()
{
    local -r string="${1}"
    local -r numberToRepeat="${2}"

    if [[ "${string}" != '' && "${numberToRepeat}" =~ ^[1-9][0-9]*$ ]]
    then
        local -r result="$(printf "%${numberToRepeat}s")"
        echo -e "${result// /${string}}"
    fi
}

function isEmptyString()
{
    local -r string="${1}"

    if [[ "$(trimString "${string}")" = '' ]]
    then
        echo 'true' && return 0
    fi

    echo 'false' && return 1
}

function trimString()
{
    local -r string="${1}"

    sed 's,^[[:blank:]]*,,' <<< "${string}" | sed 's,[[:blank:]]*$,,'
}

SAMPLE RUNS

$ cat data-1.txt
HEADER 1,HEADER 2,HEADER 3

$ printTable ',' "$(cat data-1.txt)"
+-----------+-----------+-----------+
| HEADER 1  | HEADER 2  | HEADER 3  |
+-----------+-----------+-----------+

$ cat data-2.txt
HEADER 1,HEADER 2,HEADER 3
data 1,data 2,data 3

$ printTable ',' "$(cat data-2.txt)"
+-----------+-----------+-----------+
| HEADER 1  | HEADER 2  | HEADER 3  |
+-----------+-----------+-----------+
| data 1    | data 2    | data 3    |
+-----------+-----------+-----------+

$ cat data-3.txt
HEADER 1,HEADER 2,HEADER 3
data 1,data 2,data 3
data 4,data 5,data 6

$ printTable ',' "$(cat data-3.txt)"
+-----------+-----------+-----------+
| HEADER 1  | HEADER 2  | HEADER 3  |
+-----------+-----------+-----------+
| data 1    | data 2    | data 3    |
| data 4    | data 5    | data 6    |
+-----------+-----------+-----------+

$ cat data-4.txt
HEADER
data

$ printTable ',' "$(cat data-4.txt)"
+---------+
| HEADER  |
+---------+
| data    |
+---------+

$ cat data-5.txt
HEADER

data 1

data 2

$ printTable ',' "$(cat data-5.txt)"
+---------+
| HEADER  |
+---------+
| data 1  |
| data 2  |
+---------+

REF LIB at: https://github.com/gdbtek/linux-cookbooks/blob/master/libraries/util.bash

Nam Nguyen
  • 5,668
  • 14
  • 56
  • 70
24

To have the exact same output as you need, you need to format the file like this:

a very long string..........\t     112232432\t     anotherfield\n
a smaller string\t      123124343\t     anotherfield\n

And then using:

$ column -t -s $'\t' FILE
a very long string..........  112232432  anotherfield
a smaller string              123124343  anotherfield
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Gilles Quénot
  • 173,512
  • 41
  • 224
  • 223
  • 3
    What's the `$` in `$'\t'` doing? – rjmunro Dec 16 '14 at 13:40
  • Using tabstops becomes entirely unusable if 2 columns are more than about 5 characters different in size. – UtahJarhead Oct 22 '15 at 15:30
  • 1
    @rjmunro ["Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C standard."](https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html) – Vaelus Jan 08 '20 at 23:01
  • somehow it's not working for me. – CuriousNewbie Mar 17 '22 at 09:25
  • nevermind, mine working perfectly using this structure: `$ column -t -s $'@' FILE a very long string..........@112232432@anotherfield a smaller string@123124343@anotherfield` – CuriousNewbie Mar 18 '22 at 01:54
6

It's easier than you wonder.

If you are working with a separated-by-semicolon file and header too:

$ (head -n1 file.csv && sort file.csv | grep -v <header>) | column -s";" -t

If you are working with an array (using tab as separator):

for((i=0;i<array_size;i++));
do

   echo stringarray[$i] $'\t' numberarray[$i] $'\t' anotherfieldarray[$i] >> tmp_file.csv

done;

cat file.csv | column -t
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
4

awk solution that deals with stdin

Since column is not POSIX, maybe this is:

mycolumn() (
  file="${1:--}"
  if [ "$file" = - ]; then
    file="$(mktemp)"
    cat > "${file}"
  fi
  awk '
  FNR == 1 { if (NR == FNR) next }
  NR == FNR {
    for (i = 1; i <= NF; i++) {
      l = length($i)
      if (w[i] < l)
        w[i] = l
    }
    next
  }
  {
    for (i = 1; i <= NF; i++)
      printf "%*s", w[i] + (i > 1 ? 1 : 0), $i
    print ""
  }
  ' "$file" "$file"
  if [ "$1" = - ]; then
    rm "$file"
  fi
)

Test:

printf '12 1234 1
12345678 1 123
1234 123456 123456
' > file

Test commands:

mycolumn file
mycolumn <file
mycolumn - <file

Output for all:

      12   1234      1
12345678      1    123
    1234 123456 123456

See also:

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • 1
    The `if [ "$file" = - ]; then` at the end should be `if [ "$1" = - ]; then`. With the current code, you never clean up your temp files. – Camusensei Apr 17 '20 at 22:33
3

I am not sure where you were running this, but the code you posted would not produce the output you gave, at least not in the Bash version that I'm familiar with.

Try this instead:

stringarray=('test' 'some thing' 'very long long long string' 'blah')
numberarray=(1 22 7777 8888888888)
anotherfieldarray=('other' 'mixed' 456 'data')
array_size=4

for((i=0;i<array_size;i++))
do
    echo ${stringarray[$i]} $'\x1d' ${numberarray[$i]} $'\x1d' ${anotherfieldarray[$i]}
done | column -t -s$'\x1d'

Note that I'm using the group separator character (0x1D) instead of tab, because if you are getting these arrays from a file, they might contain tabs.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Benubird
  • 18,551
  • 27
  • 90
  • 141
0

Just in case someone wants to do that in PHP, I posted a gist on GitHub:

https://gist.github.com/redestructa/2a7691e7f3ae69ec5161220c99e2d1b3

Simply call:

$output = $tablePrinter->printLinesIntoArray($items, ['title', 'chilProp2']);

You may need to adapt the code if you are using a PHP version older than 7.2.

After that, call echo or writeLine depending on your environment.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
redestructa
  • 1,182
  • 1
  • 11
  • 11
0

The below code has been tested and does exactly what is requested in the original question.

Parameters:

%30s Column of 30 char and text right align.
%10d integer notation, %10s will also work. \

stringarray[0]="a very long string.........."
# 28Char (max length for this column)
numberarray[0]=1122324333
# 10digits (max length for this column)
anotherfield[0]="anotherfield"
# 12Char (max length for this column)
stringarray[1]="a smaller string....."
numberarray[1]=123124343
anotherfield[1]="anotherfield"

printf "%30s %10d %13s" "${stringarray[0]}" ${numberarray[0]} "${anotherfield[0]}"
printf "\n"
printf "%30s %10d %13s" "${stringarray[1]}" ${numberarray[1]} "${anotherfield[1]}"
# a var string with spaces has to be quoted
printf "\n Next line will fail \n"
printf "%30s %10d %13s" ${stringarray[0]} ${numberarray[0]} "${anotherfield[0]}"



  a very long string.......... 1122324333  anotherfield
         a smaller string.....  123124343  anotherfield
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Daniel Perez
  • 431
  • 5
  • 11
0

column -t skips empty fields when a line starts with a delimiter character or when there are two or more consecutive delimiter characters:

$ printf %s\\n a,b,c a,,c ,b,c|column -s, -t
a   b  c
a   c
b   c

Therefore I use this awk function instead (it requires gawk because it uses arrays of arrays):

$ tab(){ awk '{if(NF>m)m=NF;for(i=1;i<=NF;i++){a[NR][i]=$i;l=length($i);if(l>b[i])b[i]=l}}END{for(h in a){for(i=1;i<=m;i++)printf("%-"(b[i]+n)"s",a[h][i]);print""}}' n="${2-1}" "${1+FS=$1}"|sed 's/ *$//';}
$ printf %s\\n a,b,c a,,c ,b,c|tab ,
a b c
a   c
  b c
nisetama
  • 7,764
  • 1
  • 34
  • 21
0

if you data doesn't contain the equal sign ("=") anywhere in it, you can use that as a shell-friendly delimiter for column without having to escape anything -

  • by modifying FS to be either a tab ("\t") plus any amount of spaces (" ") or tabs ("\t") on either side of it, or a contiguous chunk of 2 or more spaces, it also allows the input data to have any amount of single space within each field

     echo "${inputdata2}" | 
    
 mawk NF=NF OFS== FS=' + |[ \t]*\t[ \t]*' |
 
 column -s= -t
a very long string..........  112232432  anotherfield
a smaller string              123124343  anotherfield

if the data does contain the equal sign, use a combo sep that's close to impossible to exist in typical data :

gawk -e NF=NF OFS='\301\372\5' FS=' + |[ \t]*\t[ \t]*' | 

LC_ALL=C column -s$'\301\372\5' -t
a very long string..........  112232432  anotherfield
a smaller string              123124343  anotherfield

and if ur data only has 2 columns, and you have ballpark sense of how wide the first field is, you can use this \r trick for nice on-screen formatting (but those don't become runs of spaces if u need to send it down the pipe) :

# each \t is 8-spaces at console terminal

mawk NF=2 FS=' + |[ \t]*\t[ \t]*' OFS='\r\t\t\t\t'
a very long string..........    112232432
a smaller string                123124343
RARE Kpop Manifesto
  • 2,453
  • 3
  • 11