Script de bash para centralizar fortuna / texto de stdin / pipe

5

Eu estou usando um pequeno script em python3 para mostrar fortunas centralizadas no console, você pode me sugerir como fazer isso em puro bash?

arquivo: 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)

Então, em .bashrc

Depois de torná-lo executável chmod +x ~/center.python3 :

fortune | ~/center.python3

EDITAR : Mais tarde, tentarei responder a este OP com base no comentário que tive, mas por enquanto o tornei mais alfabetizado.

EDIT 2 : atualizando o script python para resolver um bug, como apontado por @janos sobre a expansão da guia.

    
por Iacchus 09.12.2016 / 11:00

4 respostas

3

Aqui está meu 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}"

Como funciona:

  • o primeiro comando coloca cada linha de stdin na matriz message depois de converter tabulações em espaços (graças a @NominalAnimal)
  • o segundo comando lê a largura da janela do parâmetro # 1 e a coloca na variável width . Se nenhum parâmetro for dado, a largura real do terminal é usada.
  • o terceiro comando envia todo o message para awk para produzir a margem esquerda como uma sequência de espaços que é colocada na variável margin .
    • a primeira linha do awk é executada para cada linha de entrada. Calcula max_len , o comprimento da linha de entrada mais longa (limitada a width )
    • a segunda linha do awk é executada quando todas as linhas de entrada foram processadas. Imprime uma string de (width - max_len) / 2 caracteres de espaço em branco
  • o último comando imprime todas as linhas de message depois de incluir margin nelas

Teste:

$ 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

Finalmente, se você quiser terminar a exibição com uma linha de separação, como em seu script Python, adicione essa linha antes do último comando printf :

message+=( $(IFS=''; sed s/./─/g <<< "${message[*]}" | sort | tail -n1)$'\n' )

O que ele faz é substituir todos os caracteres em todas as linhas por , selecionar o mais longo com sort | tail -n1 e adicioná-lo ao final da mensagem.

Teste:

$ fortune | center.sh  60
     Tuesday is the Wednesday of the rest of your life.
     ──────────────────────────────────────────────────
    
por 07.01.2017 / 11:42
6

Vamos traduzir do Python para o 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

O script completo para copiar e colar mais fácil:

#!/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

Resumo das advertências

A divisão em Bash trunca. Portanto, os valores de offset , perfect_center e padsize podem ser ligeiramente diferentes.

Existem alguns problemas que também existem no código Python original:

  1. O comprimento de um caractere de tabulação é 1. Isso fará com que, às vezes, a linha do divisor pareça mais curta do que a linha mais longa, assim:

                      Q:    Why did the tachyon cross the road?
                      A:    Because it was on the other side.
                      ──────────────────────────────────────
    
  2. Se algumas linhas forem maiores que columns , a linha divisória provavelmente seria melhor usando o comprimento de columns em vez da linha mais longa.

por 06.01.2017 / 08:01
2
#!/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"

Teste:

$ 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 ).
    
por 07.01.2017 / 13:27
1

Eu pessoalmente não me empenharia por uma solução Bash pura, mas utilizaria tput e expand . No entanto, uma solução Bash pura é bastante viável:

#!/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

O script acima lê as linhas da entrada padrão e recua a saída para que a linha mais longa seja centralizada no terminal. Ele falhará normalmente (sem recuo) se a largura do terminal não for conhecida pelo Bash.

Eu usei os operadores condicionais de estilo antigo ( [ ... ] ) e aritmética de shell ( $[..] ) só porque queria uma compatibilidade máxima em versões mais antigas do Bash (e Bashes minimalistas compilados sob encomenda, onde os operadores de estilo novo estão desabilitados no tempo de compilação). Eu normalmente não recomendo fazer isso, mas neste caso, como estamos nos esforçando para uma solução pura-Bash, eu pensei que a compatibilidade máxima através das opções de compilação do Bash seria mais importante do que o estilo de código recomendado.

    
por 07.01.2017 / 14:41

Tags