138

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
0

11 Answers 11

227

Use the column command:

column -t -s' ' filename
Sign up to request clarification or add additional context in comments.

8 Comments

This won't work for the example given in the question as there are spaces in the first column of data.
@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?
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.
example for preparing the delimiter: cat /etc/fstab | sed -r 's/\s+/ /g' | column -t -s' '
example for preparing the delimiter: sed -r 's/\s+/ /g' /etc/fstab | column -t -s' '
|
144

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.

2 Comments

just one advice which is useful when printing tables: %-10s wiil generate left-aligned strings of length 10
@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.
27
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

6 Comments

Thanks! this is exactly what I was looking for. For mac users: you have to remove the -eparameter in the echo commands in order to get the dashes printed correctly.
Changing output colors really messes with the alignment. Not sure why... hmmm
@mattdevio have you found a fix for the colors?
@CarlosFlorêncio I did not, went another direction with it.
HEADS UP! The ref link have a more updated function.
|
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

5 Comments

What's the $ in $'\t' doing?
Using tabstops becomes entirely unusable if 2 columns are more than about 5 characters different in size.
somehow it's not working for me.
nevermind, mine working perfectly using this structure: $ column -t -s $'@' FILE a very long string..........@112232432@anotherfield a smaller string@123124343@anotherfield
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

Comments

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:

1 Comment

The if [ "$file" = - ]; then at the end should be if [ "$1" = - ]; then. With the current code, you never clean up your temp files.
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.

Comments

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.

Comments

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

1 Comment

as pointed by @steffen above, for left align use "-" symbol i.e printf "%-30s " "${stringarray[0]}"
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

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.