Q: Bash script to center fortune/text from stdin/pipe I am using a little script in python3 to show centered fortunes in console, can you suggest me how to do this in pure bash?
file: center.python3
#!/usr/bin/env python3

import sys, os

linelist = list(sys.stdin)

# gets the biggest line
biggest_line_size = 0
for line in linelist:
    line_lenght = len(line.expandtabs())
    if line_lenght > biggest_line_size:
        biggest_line_size = line_lenght

columns = int(os.popen('tput cols', 'r').read())
offset = biggest_line_size / 2
perfect_center = columns / 2
padsize =  int(perfect_center - offset)
spacing = ' ' * padsize # space char

text = str()
for line in linelist:
    text += (spacing + line)

divider = spacing + ('─' * int(biggest_line_size)) # unicode 0x2500
text += divider

print(text, end="\n"*2)

Then in .bashrc
After making it executable chmod +x ~/center.python3:
fortune | ~/center.python3

EDIT: Later I will try to reply to this OP based on the comment I had, but for now I made it more literate.
EDIT 2: updating the python script to solve a bug as pointed out by @janos about tab expansion.


A: Let's translate from Python to Bash chunk-by-chunk.
Python:

#!/usr/bin/env python3

import sys, os

linelist = list(sys.stdin)


Bash:
#!/usr/bin/env bash

linelist=()
while IFS= read -r line; do
    linelist+=("$line")
done

Python:

# gets the biggest line
biggest_line_size = 0
for line in linelist:
    line_lenght = len(line)
    if line_lenght > biggest_line_size:
        biggest_line_size = line_lenght


Bash:
biggest_line_size=0
for line in "${linelist[@]}"; do
    # caveat alert: the length of a tab character is 1
    line_length=${#line}
    if ((line_length > biggest_line_size)); then
        biggest_line_size=$line_length
    fi
done

Python:

columns = int(os.popen('tput cols', 'r').read())
offset = biggest_line_size / 2
perfect_center = columns / 2
padsize =  int(perfect_center - offset)
spacing = ' ' * padsize # space char


Bash:
columns=$(tput cols)
# caveat alert: division truncates to integer value in Bash
((offset = biggest_line_size / 2))
((perfect_center = columns / 2))
((padsize = perfect_center - offset))
if ((padsize > 0)); then
    spacing=$(printf "%*s" $padsize "")
else
    spacing=
fi

Python:

text = str()
for line in linelist:
    text += (spacing + line)

divider = spacing + ('─' * int(biggest_line_size)) # unicode 0x2500
text += divider

print(text, end="\n"*2)


Bash:
for line in "${linelist[@]}"; do
    echo "$spacing$line"
done

printf $spacing 
for ((i = 0; i < biggest_line_size; i++)); do
    printf -- -
done
echo

The complete script for easier copy-pasting:
#!/usr/bin/env bash

linelist=()
while IFS= read -r line; do
    linelist+=("$line")
done

biggest_line_size=0
for line in "${linelist[@]}"; do
    line_length=${#line}
    if ((line_length > biggest_line_size)); then
        biggest_line_size=$line_length
    fi
done

columns=$(tput cols)
((offset = biggest_line_size / 2))
((perfect_center = columns / 2))
((padsize = perfect_center - offset))
spacing=$(printf "%*s" $padsize "")

for line in "${linelist[@]}"; do
    echo "$spacing$line"
done

printf "$spacing"
for ((i = 0; i < biggest_line_size; i++)); do
    printf ─  # unicode 0x2500
done
echo

Summary of caveats
Division in Bash truncates. So the values of offset, perfect_center and padsize might be slightly different.
There are some issues that exist in the original Python code too:


*

*The length of a tab character is 1. This will cause sometimes the divider line to look shorter than the longest line, like this:
                  Q:    Why did the tachyon cross the road?
                  A:    Because it was on the other side.
                  ──────────────────────────────────────


*If some lines are longer than columns, the divider line would be probably better using the length of columns instead of the longest line.

A: Here is my script center.sh:
#!/bin/bash

readarray message < <(expand)

width="${1:-$(tput cols)}"

margin=$(awk -v "width=$width" '
    { max_len = length > width ? width : length > max_len ? length : max_len }
    END { printf "%" int((width - max_len + 1) / 2) "s", "" }
' <<< "${message[@]}")

printf "%s" "${message[@]/#/$margin}"

How it works:


*

*the first command puts each line of stdin in array message after converting tabulations to spaces (thanks to @NominalAnimal)

*the second command reads the window width from parameter #1 and put it in variable width. If no parameter is given, the actual terminal width is used.

*the third command sends the whole message to awk in order to produce the left margin as a string of spaces which is put in variable margin.


*

*the first awk line is executed for each input line. It calculates max_len, the length of the longest input line (capped to width)

*the second awk line is executed when all input lines have been processed. It prints a string of (width - max_len) / 2 white space characters


*the last command prints every line of message after prepending margin to them


Test :
$ fortune | cowthink | center.sh
                    _______________________________________
                   ( English literature's performing flea. )
                   (                                       )
                   ( -- Sean O'Casey on P. G. Wodehouse    )
                    ---------------------------------------
                           o   ^__^
                            o  (oo)\_______
                               (__)\       )\/\
                                   ||----w |
                                   ||     ||

$ echo $'|\tTAB\t|' | center.sh 20
  |       TAB     |

$ echo "A line exceeding the maximum width" | center.sh 10
A line exceeding the maximum width

Finally, if you want to end the display with a separation line, like in your Python script, add this line before the last printf command:
message+=( $(IFS=''; sed s/./─/g <<< "${message[*]}" | sort | tail -n1)$'\n' )

What it does is replace every character in every line with a ─, select the longest with sort | tail -n1, and add it at the end of the message.
Test:
$ fortune | center.sh  60
     Tuesday is the Wednesday of the rest of your life.
     ──────────────────────────────────────────────────


A: #!/usr/bin/env bash

# Reads stdin and writes it to stdout centred.
#
# 1. Send stdin to a temporary file while keeping track of the maximum
#    line length that occurs in the input.  Tabs are expanded to eight
#    spaces.
# 2. When stdin is fully consumed, display the contents of the temporary
#    file, padded on the left with the apropriate number of spaces to
#    make the whole contents centred.
#
# Usage:
#
#  center [-c N] [-t dir] <data
#
# Options:
#
#   -c N    Assume a window width of N columns.
#           Defaults to the value of COLUMNS, or 80 if COLUMNS is not set.
#
#   -t dir  Use dir for temporary storage.
#           Defaults to the value of TMPDIR, or "/tmp" if TMPDIR is not set.

tmpdir="${TMPDIR:-/tmp}"
cols="${COLUMNS:-80}"

while getopts 'c:t:' opt; do
    case "$opt" in
        c)  cols="$OPTARG" ;;
        t)  tmpdir="$OPTARG" ;;
    esac
done

tmpfile="$tmpdir/center-$$.tmp"
trap 'rm -f "$tmpfile"' EXIT

while IFS= read -r line
do
    line="${line//$'\t'/        }"
    len="${#line}"
    maxlen="$(( maxlen < len ? len : maxlen ))"
    printf '%s\n' "$line"
done >"$tmpfile"

padlen="$(( maxlen < cols ? (cols - maxlen) / 2 : 0 ))"
padding="$( printf '%*s' "$padlen" "" )"

while IFS= read -r line
do
    printf '%s%s\n' "$padding" "$line"
done <"$tmpfile"

Testing:
$ fortune | cowsay | ./center
            ________________________________________
           / "There are two ways of constructing a  \
           | software design: One way is to make it |
           | so simple that there are obviously no  |
           | deficiencies, and the other way is to  |
           | make it so complicated that there are  |
           | no obvious deficiencies."              |
           |                                        |
           \ -- C. A. R. Hoare                      /
            ----------------------------------------
                   \   ^__^
                    \  (oo)\_______
                       (__)\       )\/\
                           ||----w |
                           ||     ||

$ fortune | cowsay -f bunny -W 15 | ./center -c 100
                                  _______________
                                 / It has just   \
                                 | been          |
                                 | discovered    |
                                 | that research |
                                 | causes cancer |
                                 \ in rats.      /
                                  ---------------
                                   \
                                    \   \
                                         \ /\
                                         ( )
                                       .( o ).


A: I would personally not strive for a pure Bash solution, but utilize tput and expand. However, a pure Bash solution is quite feasible:
#!/bin/bash

# Bash should populate LINES and COLUMNS
shopt -s checkwinsize

# LINES and COLUMNS are updated after each external command is executed.
# To ensure they are populated right now, we run an external command here.
# Because we don't want any other dependencies other than bash,
# we run bash. (In that child shell, run the 'true' built-in.)
bash -c true

# Tab character.
tab=$'\t'

# Timeout in seconds, for reading each input line.
timeout=5.0

# Read input lines into lines array:
lines=()
maxlen=0
while read -t $timeout LINE ; do

    # Expand each tab in LINE:
    while [ "${LINE#*$tab}" != "$LINE" ]; do
        # Beginning of LINE, replacing the tab with eight spaces
        prefix="${LINE%%$tab*}        "
        # Length of prefix
        length=${#prefix}
        # Round length down to nearest multiple of 8
        length=$[$length - ($length & 7)]
        # Combine prefix and the rest of the line
        LINE="${prefix:0:$length}${LINE#*$tab}"
    done

    # If LINE is longest thus far, update maxlen
    [ ${#LINE} -gt $maxlen ] && maxlen=${#LINE}

    # Add LINE to lines array.
    lines+=("$LINE")
done

# If the output is redirected to a file, COLUMNS will be undefined.
# So, use the following idiom to ensure we have an integer 'cols'.
cols=$[ $COLUMNS -0 ]

# Indentation needed to center the block
if [ $maxlen -lt $cols ]; then
    indent=$(printf '%*s' $[($cols-$maxlen)/2] '')
else
    indent=""
fi

# Display
for LINE in "${lines[@]}"; do
    printf '%s%s\n' "$indent" "$LINE"
done

The above script reads lines from standard input, and indents the output so that the longest line is centered on the terminal. It will fail gracefully (no indent) if the width of the terminal is not known to Bash.
I used the old-style conditional operators ([ ... ]) and shell arithmetic ($[..]) just because I wanted a maximum compatibility across older versions of Bash (and custom-compiled minimal Bashes, where the new-style operators are disabled at compile time). I do not normally recommend doing this either, but in this case, as we are striving for a pure-Bash solution, I thought that maximum compatibility across Bash compile options would be more important than recommended code style.
