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로 하나씩 변환해 보겠습니다.

파이썬:

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

파이썬:

# 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

파이썬:

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

파이썬:

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의 나누기가 잘립니다. 따라서 offset, perfect_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에게 감사드립니다).
  • 두 번째 명령은 매개변수 #1에서 창 너비를 읽고 이를 변수에 넣습니다 width. 인수가 지정되지 않으면 실제 터미널 너비가 사용됩니다.
  • 세 번째 명령은 왼쪽 여백이 공백 문자열로 생성되어 변수에 배치되도록 message전체 를 보냅니다 . awkmargin
    • 각 입력 라인에 대해 awk의 첫 번째 라인을 실행합니다. 가장 긴 입력 라인의 길이를 계산합니다 max_len. (상한은 width)
    • 모든 입력 라인이 처리되면 두 번째 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 컴파일 옵션 간의 최대 호환성이 권장 코딩 스타일보다 더 중요하다고 생각합니다.

관련 정보