stdinから読み取らずにttyからカーソル位置を取得する(リダイレクトの助けが必要)

stdinから読み取らずにttyからカーソル位置を取得する(リダイレクトの助けが必要)

私は書いた美しいカスタムbashプロンプト、完全に動作します。改行で区切られた複数のコマンドを実行しようとすると問題が発生します。 (長さが長くて申し訳ありませんが、質問が明確になりますように)

重要な要約:カーソル位置を読むと、stdinそこにあるすべてのデータが破棄されます。データは破棄できません。アドバイスしてください。

予想される動作

stdinから未読の長期実行コマンド(例sleep 5:)を実行し、次のコマンド+入力(例ls -lah<enter>:)を入力すると、bashはstdinから読み取りを開始し、コマンド+ Enterを読み込み、実行を開始します(ls -lahここではケース)。私はこれを入力する前に、次のプロンプトが表示されるのを待つ「忍耐状況」ではなく、「頑張る状況」と呼びたいと思います。 (下記例)

基本bashプロンプトの例

PS1デフォルトのbashプロンプト(別名)などを使用すると、期待PS1='[\u@\h \W]\$ 'どおりに機能します。

基本的な患者プロファイル:

# wait for new prompt to show up and type new command
[me@machine ~]$ sleep 5; echo 'sleep done!'<enter>
sleep done!
[me@machine ~]$ ls -A /etc/skel<enter>
.bash_logout  .bash_profile  .bashrc
[me@machine ~]$ 

基本的な緊急事態:

# we immediately start typing our next command to be run after we typed the sleep+echo
[me@machine ~]$ sleep 5; echo 'sleep done!'<enter>
ls -A /etc/skel<enter>
sleep done!
# bash now auto-fills the prompt because it reads it from the tty/stdin
# and immediately runs it (because it ends with a newline):
[me@machine ~]$ ls -A /etc/skel
.bash_logout  .bash_profile  .bashrc
[me@machine ~]$ 

カスタムエラーメッセージ

カスタマイズされた患者プロファイル

患者は元気です。ここに問題はありません。

カスタマイズされた緊急事態

問題はprecmd()関数の先頭にあります。 (私は使用しています。この bash-preexec フック)

そこで、端末に現在のカーソル座標を要求する関数を呼び出して、最後のコマンド実行の出力が1で終わっていない場合は、追加の改行文字を印刷する必要があるかどうかを確認できます。他のSO投稿からこの機能を取得しました。https://stackoverflow.com/a/52944692

この機能を無効にすると問題は発生しません。

function precmd() {
        # must be 1st
        previous_command_exit="${?}"

        # saves cursore coordinates to 2 variables: _cursor_col, _cursor_row
        # If I comment-out the call to _fetch_cursor_position, I can use the `eager situation` as expected.
        _fetch_cursor_position

        # add extra newline if command did not end with a newline
        [[ "${_cursor_col}" -gt 1 ]] && printf "\n"

        # ...
}

私の場合は、次のように定義されます。

_fetch_cursor_position() {
  local -a pos
  IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo >&2 "failed with error: $? ; ${pos[*]}"
  _cursor_row="${pos[1]}"
  _cursor_col="${pos[2]}"
}

この機能のしくみは次のとおりです(より良い説明はこのSOの記事を参照してください)。

  1. 端末がカーソル位置を報告するように要求するエスケープシーケンスを^[[6n端末に印刷します。これはread(ab)usingフラグによって-p prompt印刷されます(参照help read)。
  2. 端末は^[[y;xR端末に「入力」してカーソル位置を報告します。ここで:
    ^[[: Esc^[文字の後のテキストが[ y: 上から行番号 (9 より大きい場合があります)
    x: 上から左下の列番号、1 から始まる (>9 であることがあります)
    R: リテラル文字R
  3. readその後、コマンドは値を解析してstdinから読み取り、配列に割り当てますpos
    pos[0]: ^[(私たちはこれを気にしません)
    pos2
    y位置値2: x の値

考えられる解決策

主な目的は、この_fetch_cursor_position関数がtty / stdinから入力データを読み取ることを防ぐことです。これは不可能な可能性があるため、現在のstdin値を保存してカーソル位置を読み取ってstdin値を復元する方法があります。私はbashが有望に見えたのでいくつかの調査をしましたが、coprocそれがどのように機能するかはわかりませんでした。

Bashリダイレクトがあるもの

私はbashのI / Oリダイレクトを使用して「高度な」タスクを実行するための解決策があるという強い感じを受けました。ただし、実際には>/dev/null、、、、などのファイル/サブシェルリダイレクトのみを使用し&>/dev/null、追加のFDを直接使用したことはありません。2>&1|&< <(echo abc)>myfile.txt

私の心の中にある過程はこうだ。

  1. 0fd を新しいファイル記述子にバックアップします (stdout/ 1do?)
  2. 同じターミナルで使用できるように、ターミナル(/dev/tty/ ?)で新しいstdin / stoutを開きます(もちろん、カーソルを移動せずに上記のすべての操作)。$(tty)
  3. 端末に位置を要求し、応答を解析します。
  4. 元の復元

他のFDへの出力位置

端末が0stdin /の代わりに別のFDに座標応答を書き、そこからread読み取るようにしてください。

すでに存在する標準入力データにアクセスできないサブシェルです。

_fetch_cursor_positionstdin/stout/stderrを継承するため、subshel​​lのわずかに編集された拡張バージョンを使用しても機能しません。

_echo_cursor_position() {
  local pos

  IFS='[;' read -p $'\e[6n' -d R -a pos -rs || echo >&2 "failed with error: $? ; ${pos[*]}"
  _cursor_row="${pos[1]}"
  _cursor_col="${pos[2]}"
  
  # this line is added, no other changes apart from the name
  printf "${_cursor_row} ${_cursor_col}"
}

function precmd() {
        # must be 1st
        previous_command_exit="${?}"

        #_fetch_cursor_position
        pos=( $(_echo_cursor_position) )
        _cursor_row="${pos[0]}"
        _cursor_col="${pos[1]}"

        # add extra newline if command did not end with a newline
        [[ "${_cursor_col}" -gt 1 ]] && printf "\n"

        # ...
}

この継承の問題が解決できる場合は、おそらくよりシンプルでエレガントなソリューションになります。

うまくいかない

1:readリダイレクトなし

カーソル位置は正しく保存されますが、一生懸命入力したコマンドは消えます。

IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row

2:read2つのリダイレクトを有効にします。/dev/tty

Bashを正しく理解している場合は、stdin / stdout(およびstderr)はデフォルトでttyに接続されているため、これは1と異なるべきではありませんが、そうではありません。

IFS='[;' read -r -s -p $'\e[6n' -d 'R' __garbage __cursor_col __cursor_row </dev/tty >/dev/tty

からman 1 bash

対話型シェルは、オプションではなく引数(-sが指定されていない限り)と-cオプションなしで始まるシェルであり、標準入力とエラーの両方がターミナルに接続されています(isatty(3)によって決まります)、または - 私はオプションシェルです。 Bashがインタラクティブである場合は、PS1を設定し、$-を含むシェルスクリプトまたは起動ファイルがこの状態をテストできるようにします。

3:2フェーズリダイレクトprintfread

1、2などの結果

printf $'\e[6n' >/dev/tty
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row </dev/tty

4:$(tty)代わりに値を使用してください/dev/tty

繰り返しますが、違いはありません

local tty="$(tty)"
printf $'\e[6n' >"${tty}"
IFS='[;' read -r -s -d 'R' __garbage __cursor_col __cursor_row <"${tty}"

何が起こるのか

0: シンプルなソリューション

このような愚かな努力をしないで、常に改行を印刷してください。

完璧主義的な観点から見ると、本当にそうしたくありません。

1: tmuxの使用

Kamil Machorovskyの返信を見る

私はいつもtmuxを使用しているので、必要に応じて追加の改行だけを印刷できる実用的で良いソリューションです。

tmuxを必要としないbashネイティブ/ターミナルネイティブソリューションがある場合は、これについて聞きたいです。

答え1

問題は次のとおりです。

端末は「入力」を通じてカーソル位置を報告します。

端末から読み取られるすべてのプログラムについて、ユーザーが入力するものと端末が「入力する」ものとの間に違いはありません。

このような場合は、別々のチャンネルを提供する端末を使用してください。使っていますtmuxその他の理由)。tmux次の作品から:

_fetch_cursor_position() {
  local pos
  pos="$(tmux display-message -p -F '#{cursor_x} #{cursor_y}')"
  _cursor_row="${pos#* }"
  _cursor_col="${pos% *}"
  ((_cursor_row++))
  ((_cursor_col++))
}

要点は、この関数が標準入力から読み取られないことです。別の言葉

tmux開始から行/列を計算します0。これらは現在のコードの残りの部分と互換性のある数値を作成するために私のコードにあります++(明らかに最初から計算が期待されます1)。最初から作成する場合は使用しません++。それに応じてテストを作成します(例:inの-gt 0代わりに)。-gt 1precmd()

答え2

簡単にするために、コードから配列を削除し、参照用のカーソル位置を追加しました。

_fetch_cursor_position() {
  echo -en "\e[25;10H"
  read -s -t 1 -d ' ' -p $'\e[6n ' >&2
  if [[ "$REPLY" ]]; then
    result=$(echo $REPLY | sed 's/^[/\\e/g')
    cat <<< "Cursor position in the terminal:
        $result"
  fi
}
_fetch_cursor_position

出力は次のとおりです

Cursor position in the terminal:  
\e[25;10R  

echoカーソルを置くステートメントに関連しています。

^[コマンドにはハード(実際の)Esc文字がありますsed。これはソフトEsc(つまりテキスト表現)ではありません。実際のEscを入力するには、

  • 存在する寸法/ウィムCtrl+入力VEsc
  • 存在するEmacsCtrl+ Q、と入力します。Esc

関連情報