텍스트가 터미널보다 넓은 경우(예: 여러 줄 줄 바꿈) bash 터미널 출력의 처음 몇 줄을 정확하게 포함합니까?

텍스트가 터미널보다 넓은 경우(예: 여러 줄 줄 바꿈) bash 터미널 출력의 처음 몇 줄을 정확하게 포함합니까?

TL;DR - 출력 줄을 정상적으로 덮어쓰는 방법을 알고 있지만 printf '\e[1A\e[2K'이전에 사용했거나(예를 들어) 온라인에서 찾은 방법 중 덮어쓰는 줄이 터미널 너비보다 길 때(예: 개행 문자가 실행될 때) 작동하지 않는 것 같습니다. ) . 줄 바꿈을 비활성화하면 표시된 텍스트가 효과적으로 잘립니다. 제가 놓치고 있는 이 상황을 처리할 수 있는 다른 팁이나 도구 또는 방법이 있습니까?

데스크톱(Fedora)과 모바일(Android의 Termux) 간에 공유할 수 있는 bash 스크립트가 있습니다. 기능에 관한 한 문제가 없으며 모든 것이 예상대로 작동합니다. 그러나 스크립트가 상당히 길고 터미널 출력이 엉망입니다. 최근에 나는 내가 사용할 수 있다는 것을 배웠다.ANSI 이스케이프 코드bash의 이전 출력 줄을 덮어쓰고 스크립트의 출력을 크게 정리하는 동시에 진행 상황을 인식하고 오류가 발생하면 확인합니다. 이에 대한 나의 이해는 여전히 매우 기본적이지만 테스트를 통해 ASCII 이스케이프의 시작을 printf인식한 다음 다음을 기반으로 하는 것 같습니다.\e이것, ESC[#A"커서를 #줄 위로 이동" 및 ESC[2K"전체 줄 삭제".

어쨌든, 내가 겪고 있는 한 가지 문제는 마지막 행을 제외한 모든 행을 덮어쓰고 다른 여러 행은 계속 표시된다는 것입니다. 처음에는 일부 Termux 버그 때문인 줄 알았으나 터미널의 크기(너비) 때문이라는 것을 확인했습니다( gnome-terminal창 크기를 조정하거나 텍스트 길이를 늘려 문제를 재현할 수 있었습니다). 기본적으로 내가 보는 것은 덮어쓰려는 출력 줄이 터미널 너비보다 길면 해당 줄이 남은 텍스트를 새 줄로 "줄바꿈"하는 것처럼 보이고 덮어쓰기는 줄바꿈된 텍스트의 일부만 대체한다는 것입니다.

다음은 내 스크립트에서 발생한 문제를 재현하는 스니펫입니다.

# create an array with variable-length texts to simulate status messages
arrStatusTexts=(  );
for i in {10..200..10}; do
    arrStatusTexts+=("$(printf '%*s\n' $i ' ' | tr ' ' X)");
done
 
# print out status at each step, overwriting output of each previous step as we go
echo "";
echo "--------------------";
echo "Steps of process"
echo "--------------------";
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    [[ 0 != "$i" ]] && printf '\e[1A\e[2K';
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done

위 명령을 실행하기 전에 데스크탑 터미널 창에서 다음을 얻습니다.

$ stty size
34 135

편집: 내 데스크톱 및 termux에서 내 TERM 변수는 다음과 같이 나타납니다.

$ echo "$TERM"
xterm-256color

위의 for 루프를 실행한 후 보고 싶은 최종 출력은 다음과 같습니다.

Step 20 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 루프가 완료된 후 실제로 표시되는 내용은 다음과 같습니다.

Step 13 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 14 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 15 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 16 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 17 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 18 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 19 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 20 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

기본적으로 1~12단계는 터미널 창 너비보다 작기 때문에 올바르게 다룹니다. 그러나 13단계 이후의 단계에서는 줄의 각 길이가 "줄바꿈"되고 줄바꿈된 부분만 지워집니다.


2022년 9월 1일에 편집됨: 더 이상한 점을 발견했습니다. 답변을 바탕으로여기그리고여기, 이 시퀀스를 사용하여 이것이 도움이 되는지 확인하기 위해 각 명령문 앞의 커서 위치를 \e[6n가져오려고 했습니다 .printf

위 내용을 다음과 같이 수정하세요.

# from: https://unix.stackexchange.com/a/183121/379297
function pos () {
    local CURPOS
    read -sdR -p $'\E[6n' CURPOS
    CURPOS=${CURPOS#*[} # Strip decoration characters <ESC>[
    echo "${CURPOS}"    # Return position in "row;col" format
}
export -f pos;
arrCursorPos=(  );
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    # get cursor position
    arrCursorPos+=("$(pos)");
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
for ((i=0; i < ${#arrCursorPos[@]}; i++)); do
    echo "cursor pos $(($i + 1)): '${arrCursorPos[$i]}'";
done

내가 받은 상태 메시지의 출력은 위와 동일하지만 이는 커서 위치를 유지하는 두 번째 배열의 출력입니다.

cursor pos 1: '19;1'
cursor pos 2: '20;1'
cursor pos 3: '21;1'
cursor pos 4: '22;1'
cursor pos 5: '23;1'
cursor pos 6: '24;1'
cursor pos 7: '25;1'
cursor pos 8: '26;1'
cursor pos 9: '27;1'
cursor pos 10: '28;1'
cursor pos 11: '29;1'
cursor pos 12: '30;1'
cursor pos 13: '31;1'
cursor pos 14: '33;1'
cursor pos 15: '34;1'
cursor pos 16: '34;1'
cursor pos 17: '34;1'
cursor pos 18: '34;1'
cursor pos 19: '34;1'
cursor pos 20: '34;1'

처음에는 인덱스 15~20이 같은 위치에 있어서 제대로 작동하지 않는 줄 알았습니다. 화면을 지우고( Ctrl+L) 몇 번 다시 실행한 후 다른 출력이 표시됩니다.

cursor pos 1: '11;1'
cursor pos 2: '12;1'
cursor pos 3: '13;1'
cursor pos 4: '14;1'
cursor pos 5: '15;1'
cursor pos 6: '16;1'
cursor pos 7: '17;1'
cursor pos 8: '18;1'
cursor pos 9: '19;1'
cursor pos 10: '20;1'
cursor pos 11: '21;1'
cursor pos 12: '22;1'
cursor pos 13: '23;1'
cursor pos 14: '25;1'
cursor pos 15: '27;1'
cursor pos 16: '29;1'
cursor pos 17: '31;1'
cursor pos 18: '33;1'
cursor pos 19: '34;1'
cursor pos 20: '34;1'

그리고 무슨 일이 일어나고 있는지 깨달았습니다. 마지막 몇 개의 배열 요소에 대해서는 마지막 행(내 경우에는 34 - col1에서 보고한 대로 stty size)에 도달합니다. 이 시점에서 새로운 출력 줄이 있으면 표시된 텍스트가 스크롤되지만 여전히 마지막 줄(34)에 머물러 있습니다. 그래서 이 방법은하다이는 초기 커서 위치를 추적하는 신뢰할 수 있는 방법인 것 같습니다.

나는 또한 다른 접근 방식을 시도했습니다(제안됨). 여기,여기, 그리고여기기능 관련 exec < /dev/tty및 사용stty여기코드 조각을 다음과 같이 수정합니다.

function extract_current_cursor_position () {
    export $1
    exec < /dev/tty
    oldstty=$(stty -g)
    stty raw -echo min 0
    echo -en "\033[6n" > /dev/tty
    IFS=';' read -r -d R -a pos
    stty $oldstty
    eval "$1[0]=$((${pos[0]:2} - 2))"
    eval "$1[1]=$((${pos[1]} - 1))"
}
export -f extract_current_cursor_position;
arrCursorPos=(  );
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    # get cursor position
    extract_current_cursor_position pos1
    arrCursorPos+=("${pos1[0]} ${pos1[1]}");
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
for ((i=0; i < ${#arrCursorPos[@]}; i++)); do
    echo "cursor pos $(($i + 1)): '${arrCursorPos[$i]}'";
done

화면을 지우고 이 명령을 다시 실행하면 다음과 같은 결과가 나타납니다.

cursor pos 1: '10 0'
cursor pos 2: '11 0'
cursor pos 3: '12 0'
cursor pos 4: '13 0'
cursor pos 5: '14 0'
cursor pos 6: '15 0'
cursor pos 7: '16 0'
cursor pos 8: '17 0'
cursor pos 9: '18 0'
cursor pos 10: '19 0'
cursor pos 11: '20 0'
cursor pos 12: '21 0'
cursor pos 13: '22 0'
cursor pos 14: '24 0'
cursor pos 15: '26 0'
cursor pos 16: '28 0'
cursor pos 17: '30 0'
cursor pos 18: '32 0'
cursor pos 19: '32 0'
cursor pos 20: '32 0'

이것도 효과가 있는 것 같습니다. extract_current_cursor_position함수가 y 값에서 2를 빼고 x 값에서 1을 빼는 이유는 확실하지 않습니다 . 그 부분을 조사하거나 빼기를 제거해야 할 수도 있습니다.

여전히 ncurses 옵션(예 tput: )을 조사해야 합니다. 적어도 Termux에서 ncurses 패키지가 사용 가능한지 확인했지만 더 테스트하면서 더 많은 정보를 채우기 위해 다시 돌아오겠습니다.


제가 할 수 있는 확실한 변화는 다음과 같습니다:

1. 지금까지 그랬듯이 대본을 바꾸지 말고, 여러 줄의 혼란을 참으세요. 하지만 너무 많은 작업이 되지 않는 한 출력물을 수정하고 싶습니다.

2. 모든 상태 텍스트를 줄이고 모든 것이 최소 화면 너비보다 작아질 때까지 출력의 인쇄 변수를 줄입니다( 예를 들어 stty size 모바일 보고서에서는 17 48모든 것을 한 줄에 48자로 제한합니다). 리팩토링을 하고 좀 더 노력을 기울이는 것은 괜찮지만 수백 개의 텍스트를 변경한다는 생각은 매우 지루해 보이고 실제로 무슨 일이 일어나고 있는지 알려주지 않습니다. 더 좋은 방법이 없다면 최후의 수단으로 이 방법을 사용하는 것이 좋습니다. 그것과 이것은 의미 있는 정보를 잃거나 사물이 표시되는 방식에 대해 다른 타협을 해야 할 수도 있습니다.

3. 2와 동일하지만 print -v msg출력을 변수에 넣은 다음 잘린 텍스트를 인쇄하는 데 사용됩니다(예: printf '%s\n' "${msg:0:48}". 2와 같은 문제이지만 의미 있는 정보가 누락되었을 수도 있습니다.

4. 위와 비슷하지만 잘리는 대신 이전 메시지의 길이를 추적하고 터미널 너비로 나누어 printf '\e[1A\e[2K';사용해야 할 명령문 수를 결정합니다. 아직 약간의 작업이 있지만 모든 메시지를 편집해야 하는 것보다는 덜하고 더 나은 최종 결과를 제공할 것입니다.

내가 놓친 부분을 해결하는 더 쉬운 방법이 있는지 궁금합니다. 현재 위치에 대한 특정 오프셋에서 인쇄되고 지워진 텍스트를 "grep"하는 방법이 있을 수 있습니다(각 상태 줄의 시작 부분에 유니코드 문자나 별표 등을 추가하면 검색/바꾸기가 매우 쉬워집니다). 아니면 내가 모르는 어떤 명령이나 내장 명령이 있습니까? 내 인터넷 검색에서는 위에 나열된 것 외에는 확실한 해결책을 찾지 못했습니다.

POSIX와 완전히 호환되는 솔루션은 필요하지 않습니다. bash(python/perl/awk)의 표준 도구와 함께 작동하는 솔루션만 있으면 됩니다.한 줄모두 공정한 게임이지만 전체 bash 스크립트를 그 중 하나로 다시 작성하는 것은 그렇지 않습니다. 이 도메인에는 일반적으로 다음이 포함된다는 점을 고려하면데스크탑질문, Termux에 익숙할 것으로 기대하지는 않지만 솔루션에 그래픽 세션(예: ssh에서는 작동하지 않음)이나 x86_64 아키텍처에서만 사용할 수 있는 도구(Termux는 ARM 버전 사용)가 필요한 경우 작동하지 않을 수 있습니다. . 가장 일반적인 bash/linux 도구는 여기에서 잘 작동하는 것 같습니다. 내 데스크탑에는 현재 bash 5.1.8(곧 Fedora 36으로 업그레이드할 예정)이 있고 Termux에는 bash 5.1.16의 ARM 버전이 있습니다.

답변1

귀하의 문제에 대한 솔루션을 작성하던 즈음에 저는 이것이 터미널 가상화를 저주하는 이유가 그토록 쉽다는 것을 깨달았습니다. 그것은 모든 불쾌한 터미널 관련 세부 사항(대부분 및 일부)을 숨깁니다. terminfo를 직접 사용하는 것은 고통스럽지만 원시 이스케이프 시퀀스보다 낫습니다.

bash코드 는 다음과 같습니다 . 다시 작성하는 것은 어렵지 않습니다 sh.

# Output a printf style format string and arguments and return the cursor
# to the beginning of the line. DO NOT use newline `\n`.
#
lineOut() {
    local rows cols len lines

    # Number of rows/columns on the terminal device
    rows=$(tput lines)
    cols=$(tput cols)

    # Output
    printf "$@"

    # How many lines we wrote
    len=$(printf "$@" | unexpand -a | wc -m)
    lines=$(( len / cols ))

    if tput am
    then
        # Cursor does not wrap when writing to the last column
        len=$(( len - (cols * lines) ))
        [[ $len -eq 0 ]] && (( lines-- ))
    fi

    # Move up the necessary number of lines to column 1
    printf '\r'
    for (( ; lines > 0; lines-- )); do tput cuu1; done
}


# Populate the arrStatusTexts (demo only) to simulate status messages
arrStatusTexts=()
for i in {10..200..10}; do
    arrStatusTexts+=("$(printf '%*s\n' "$i" ' ' | tr ' ' X)")
done

# Output
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    lineOut 'Step %s of %s: %s' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
    sleep 1
done
printf '.\n'

사용된 terminfo 코드는 다음 tput문서에 설명되어 있습니다.man 5 terminfo. 필요한 제어 코드 세트(예: try )가 없는 export TERM=dumb터미널 유형으로 실행하면 솔루션의 성능이 완전히 저하됩니다 .


해결책을 조사하던 중 u7( user7)를 발견했는데, 이는 우연히 "커서 위치는 어디에 있나요?” 터미널 문제:

# Magic to read the current cursor position (origin 1,1)
tput u7; read -t1 -srd'[' _; IFS=';' read -t1 -srd'R' y x

여기에 제안된 솔루션과 더 이상 관련이 없지만 보너스로 유용할 수 있습니다.

답변2

한 가지 방법은 커서가 있는 위치를 기록한 다음 필요한 경우 해당 지점으로 다시 이동하는 것입니다. 이는 이스케이프 시퀀스 인수를 통해 수동으로 수행할 수 있지만 더 많은 작업이 필요합니다. Curses에는 scrollok창 끝에 무언가가 인쇄될 때 발생하는 상황에 영향을 주는 호출이 있지만 이 메서드는 텍스트 스크롤을 제대로 처리하지 못할 수 있습니다. 아마도 스크롤백 창에서 보려는 내용에 따라 달라지거나 스크롤되지 않는 창에 텍스트를 표시할 수도 있습니다. 대체 화면을 사용하여 한 가지 항목을 표시한 다음 다른 항목을 표시해 보세요.

#!/usr/bin/env perl
use strict;
use warnings;
# or you could do it the hard way with XTerm Control Sequences: ask the
# terminal where the cursor is, manually parse the result, etc
use Curses;
initscr;

my ( $y, $x );    # where we started printing from to jump back to

move int rand(4), 0;    # put the cursor somewhere (start point)
getyx $y, $x;           # record this

my $i = 0;
while ( ++$i < 10 ) {
    clrtobot;       # maybe subsequent messages are shorter? clear all below
    addstring $i x ( $COLS + int rand 8 );    # noise that wraps a line
    refresh;
    sleep 1;
    move $y, $x;                              # back to where we started from
}

endwin;

답변3

오래된 질문이지만... 이미 언급한 \r을 printf의 %s 정밀도 옵션과 결합하면 필요에 따라 재정의할 수 있는 공백으로 채워진 고정 너비 필드가 제공됩니다.

이것은아니요어떻게 하시겠습니까? 하지만 코드에 많은 업데이트가 필요하지 않은 것을 구체적으로 원합니다. 실제로 저는 일반적으로 잘린 출력을 화면에 기록하고 잘리지 않은 출력을 로그에 기록하는 "msg()" 함수를 실행합니다. 그러나 논의를 위해 다음과 같이 하십시오.

cols=$( tput cols )
eol=$'\r'
printf() {
    command printf "%-${cols}.${cols}s${eol:-\n}" "$( command printf "${1%\\n}" "${@:2}" )"
}

# print out status at each step, overwriting output of each previous step as we go
echo "";
echo "--------------------";
echo "Steps of process"
echo "--------------------";
echo
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do

    eol=$'\r'
    (( i % 5 )) || eol=$'\n'
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
echo

화면 행과 열을 쉽게 얻을 수 있다면 $(( $rows - 2 )) 등으로 인쇄할 수도 있습니다.

#print_at [col] [row] [normal printf params...]
printf_at() {
    printf "\E[%d;%dH" "$1" "$2"
    shift 2
    eval "printf \"$@\""
}

보다 안전한 버전으로 문자열을 미리 형식화해야 합니다.

# printf_at [col] [row] "$( printf fmt vars.... )"
printf_at() {
    printf "\E[%d;%dH%b" "$1" "$2" "${@:2}"
}

관련 정보