テキストが端末よりも広い場合(たとえば、複数行改行)、bash端末出力の最初の数行を正確に含めますか?

テキストが端末よりも広い場合(たとえば、複数行改行)、bash端末出力の最初の数行を正確に含めますか?

TL;DR - 出力行を正常に上書きする方法を知っていますが、printf '\e[1A\e[2K'以前に使用した場合(たとえば、オンラインで見つけた方法のうち、上書き行が端末の幅よりも長い場合(改行文字が実行されたときなど)は機能しません。同じです。 )。改行を無効にすると、表示されたテキストが効果的に切り捨てられます。私が見逃しているこの状況を処理する他のヒントやツール、または方法はありますか?

デスクトップ(Fedora)とモバイル(AndroidのTermux)間で共有できるbashスクリプトがあります。機能に関する限り問題はなく、すべてが期待どおりに機能します。しかし、スクリプトがかなり長く、端末出力が混乱しています。最近私は私が使用できることを学んだ。ANSIエスケープコードbashの前の出力行を上書きし、スクリプトの出力を大幅にクリーンアップすると同時に進行状況を認識し、エラーが発生した場合に確認します。これについての私の理解はまだ非常に基本的ですが、テストを通じてASCIIエスケープの始まりをprintf認識し、次のものに基づいているようです。\eこれESC[#A「カーソルを#行の上に移動」、ESC[2K「全行を削除」。

とにかく、私が経験している問題の1つは、最後の行を除くすべての行を上書きし、他のいくつかの行は引き続き表示されることです。最初は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

私が受け取ったステータスメッセージの出力は上記と同じですが、これはカーソル位置を維持する2番目の配列の出力です。

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を1行に48文字に制限します)。リファクタリングをしてもう少し努力することは大丈夫ですが、何百ものテキストを変更するという考えは非常に退屈に見え、実際に何が起こっているのか教えてくれません。より良い方法がない場合は、最後の手段としてこの方法を使用することをお勧めします。それと、これは意味のある情報を失ったり、物事がどのように表示されるかについて他の妥協をする必要があるかもしれません。

3.2と同じですが、print -v msg出力を変数に入れてから切り捨てられたテキストを印刷するために使用されますprintf '%s\n' "${msg:0:48}"

4. 上記と似ていますが、切り捨てではなく、前のメッセージの長さを追跡し、端末の幅で割ってprintf '\e[1A\e[2K';使用する文の数を決定します。まだ少しの作業がありますが、すべてのメッセージを編集する必要があるよりも少なく、より良い最終結果を提供します。

私が逃した部分を解決するより簡単な方法があるかどうか疑問に思います。現在の場所に対する特定のオフセットで印刷され、消去されたテキストを「grep」する方法があります(各ステータス行の先頭にUnicode文字やアスタリスクなどを追加すると、検索/置換が非常に簡単になります)。それとも私が知らないコマンドや組み込みコマンドはありますか?私のインターネット検索では、上記に記載されているもの以外は確実な解決策を見つけることができませんでした。

POSIXと完全に互換性のあるソリューションは必要ありません。 bash(python/perl/awk) の標準ツールと連携するソリューションだけが必要です。一行すべて公正なゲームですが、bashスクリプト全体をそのうちの1つに書き換えることはありません。このドメインには一般的に次のものが含まれることを考慮するとデスクトップ質問、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端末タイプで実行すると、ソリューションのパフォーマンスが完全に低下します。


解決策を調べていたところu7user7)が見つかりました。カーソル位置はどこにありますか?”端末の問題:

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

ここで提案された解決策とは関係ありませんが、ボーナスとして役に立ちます。

答え2

1つの方法は、カーソルがある場所を記録し、必要に応じてその点に戻すことです。これはエスケープシーケンス引数を介して手動で実行できますが、より多くの作業が必要です。 Cursesには、scrollokウィンドウの最後に何かが印刷されたときに発生する状況に影響を与える呼び出しがありますが、このメソッドはテキストスクロールを正しく処理できない可能性があります。おそらく、スクロールバックウィンドウで表示したい内容によって変わるか、スクロールされないウィンドウにテキストを表示することもできます。代替画面を使用して1つの項目を表示し、別の項目を表示してみてください。

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

関連情報