stdin/pipe でサブ/テキストを中央に配置する Bash スクリプト

stdin/pipe でサブ/テキストを中央に配置する Bash スクリプト

私はコンソールに集中した富を表示するためにPython3の小さなスクリプトを使用しています。純粋なbashでこれを行う方法を提案できますか?

ファイル: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)

それから.bashrc

実行可能にした後chmod +x ~/center.python3:

fortune | ~/center.python3

編集する:後で私の意見に基づいてこのOPに返信しますが、今はもう少し読みやすくしています。

編集2: タブ拡張に関して @janos が指摘したバグを修正するために Python スクリプトを更新しました。

ここに画像の説明を入力してください。

答え1

PythonからBashに1つずつ変換してみましょう。

Python:

#!/usr/bin/env python3

import sys, os

linelist = list(sys.stdin)

大きな打撃:

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

大きな打撃:

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

大きな打撃:

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)

大きな打撃:

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

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

より簡単なコピー - 貼り付けのための完全なスクリプト:

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

考慮事項の概要

Bashの分割が切り捨てられます。したがってoffsetperfect_center、 の値がpadsize少しずつ異なる場合があります。

元のPythonコードにはいくつかの問題があります。

  1. タブ文字の長さは1です。これにより、次のように、区切り線が最も長い線より短くなることがあります。

                      Q:    Why did the tachyon cross the road?
                      A:    Because it was on the other side.
                      ──────────────────────────────────────
    
  2. 一部の行が区切り文字の長さより長い場合は、長さの代わりに最長の行をcolumns使用する方が良い場合があります。columns

答え2

これは私のスクリプトです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}"

仕組み:

  • 最初のコマンドはタブを空白に変換し、各行をstdin配列に入れますmessage(@NominalAnimalに感謝します)。
  • 2番目のコマンドは、パラメータ#1からウィンドウの幅を読み取り、それを変数に入れますwidth。引数が指定されない場合、実際の端末幅が使用されます。
  • 3番目のコマンドは、左マージンが空白文字列として生成され、変数に配置されるようにmessage全体を送信します。 awkmargin
    • 各入力ラインに対して awk の最初のラインを実行します。最長入力ラインの長さを計算しますmax_len。 (上限はwidth
    • すべての入力ラインが処理されると、2番目のawkラインが実行されます。(width - max_len) / 2空白文字文字列を印刷します。
  • 最後のコマンドは、その後messageに追加された各行を印刷します。margin

テスト:

$ 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

最後に(Pythonスクリプトと同様に)区切り線でマークを終了するには、最後のprintfコマンドの前に次の行を追加します。

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

これが行うことは、各行のすべての文字をに置き換えて、最も長い文字を選択してsort | tail -n1メッセージの末尾に追加することです。

テスト:

$ fortune | center.sh  60
     Tuesday is the Wednesday of the rest of your life.
     ──────────────────────────────────────────────────

答え3

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

テスト:

$ 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 ).

答え4

個人的に私は純粋なBashソリューションを追求せずにtputむしろexpand。しかし、純粋なBashソリューションはかなり実現可能です。

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

上記のスクリプトは標準入力から行を読み取り、最長行が端末の中央に来るように出力をインデントします。 Bashが端末の幅を知らないと、正常に失敗します(インデントなし)。

私は以前のバージョンのBash(そしてコンパイル時に新しいスタイル演算子を無効にしたカスタムコンパイルされた最小Bash)との間の最大の互換性が欲しいので、古いスタイル条件演算子([ ... ])とシェル算術()を使用しています。$[..]私は一般的にこれをお勧めしませんが、この場合は純粋なBashソリューションに取り組んでいますので、Bashコンパイルオプション間の最大互換性が推奨コーディングスタイルよりも重要であると思います。

関連情報