ループを停止するには任意のキーを押してください

ループを停止するには任意のキーを押してください

以下のことをどうやって行うのでしょうか?

  • ループスクリプトがあります。
  • キーを押すと停止するようにしたいのですが、そうでない場合は 5 秒後に続行されます。

答え1

覚えておくべきことは、通常ターミナルで実行するシェルやアプリケーションは、キーボードや画面と対話しないということです。

これらは、stdin からの入力をバイト ストリームとして受け取ります。stdin が通常のファイルから来る場合、バイトはそこから来ます。パイプの場合、通常は別のプロセスから送られるデータです。デバイス ファイルの場合、コンピューターに接続された物理デバイスに送られるデータです。たとえば、tty キャラクタ デバイスの場合、通常は端末からシリアル ライン経由で送られるデータです。端末は、キーボード イベントをバイト シーケンスに変換する何らかの形式のデバイスです。

ターミナル アプリケーションの全能力はここにあります。ターミナル アプリケーションの入力メカニズムは抽象化されているため、対話型で使用したり、スクリプト内で自動的に使用したりできます。

ここで、このようなプロンプトを発行し、イベントを押す場合、アプリケーション (スクリプト) を対話型のみにしたい場合があります。stdin がターミナルであると想定するか、どの stdin が開いているかに関係なくターミナルから入力を取得します。

さて、上で見たように、アプリケーションが見るものはすべてバイトのストリームです。これは、押されたキーをバイトのシーケンスに変換する端末 (または端末エミュレータ) と tty デバイスのライン ディシプリンの役割です。いくつか例を挙げます。

  • キーを押すとa、ASCII端末は0x61バイトを送信します。
  • キーを押すと£、UTF-8 端末は 0xc2 と 0xa3 の 2 つのバイトを送信します。
  • キーを押すとEnter、ASCII端末は0x0dバイトを送信し、LinuxなどのASCIIベースのシステムのttyラインディシプリンは通常これを0x0aに変換します。
  • 単独で押すとCtrl端末は何も送信しませんが、 と一緒に押すとC端末は0x03バイトを送信し、ttyラインディシプリンがそれをインターセプトしてフォアグラウンドタスクにSIGINT信号を送信します。
  • を押すとLeft、端末は通常、バイト シーケンスを送信します (端末によって異なります。アプリケーションは terminfo データベースを照会してそれを変換できます)。最初のバイトは 0x1b です。たとえば、xtermASCII ベースのシステムでは、モードに応じて、0x1b 0x4f 0x44 または 0x1b 0x5b 0x44 (<ESC>[Aまたは<ESC>OA) が送信されます。

そこで私が尋ねる質問は次のようになります:

  1. stdinが端末でない場合でもユーザーに確認を求めますか?
  2. 1 の答えが「はい」の場合、ターミナルでユーザーにプロンプ​​トを表示しますか、それとも stdin/stdout を通じてプロンプトを表示しますか?
  3. 1 の答えが「いいえ」の場合、各反復の間に 5 秒間待機しますか?
  4. 2の答えがターミナルを通じて、制御端末を検出できない場合、スクリプトは中止するか、非端末モードにフォールバックする必要がありますか?
  5. プロンプトを発行した後に押されたキーのみを考慮しますか。つまり、プロンプトが発行される前にユーザーが誤ってキーを入力した場合です。
  6. 1 回のキー押下で発行されたバイトのみを確実に読み取るために、どの程度まで努力するつもりですか?

ここでは、スクリプトをターミナル対話型アプリケーションのみにして、stdin/stdout をそのままにして制御ターミナルを通じてのみ対話することを想定しています。

#! /bin/sh -

# ":" being a special builtin, POSIX requires it to exit if a
# redirection fails, which makes this a way to easily check if a
# controlling terminal is present and readable:
:</dev/tty

# if using bash however not in POSIX conformance mode, you'll need to
# change it to something like:
exec 3< /dev/tty 3<&- || exit

read_key_with_timeout() (
  timeout=$1 prompt=$2
  saved_tty_settings=$(stty -g) || exit

  # if we're killed, restore the tty settings, the convoluted part about
  # killing the subshell process is to work around a problem in shells
  # like bash that ignore a SIGINT if the current command being run handles
  # it.
  for sig in INT TERM QUIT; do
    trap '
      stty "$saved_tty_settings"
      trap - '"$sig"'
      pid=$(exec sh -c '\''echo "$PPID"'\'')
      kill -s '"$sig"' "$pid"

      # fall back if kill failed above
      exit 2' "$sig"
  done

  # drain the tty's buffer
  stty -icanon min 0 time 0; cat > /dev/null

  printf '%s\n' "$prompt"

  # use the tty line discipline features to say the next read()
  # should wait at most the given number of deciseconds (limited to 255)
  stty time "$((timeout * 10))" -echo

  # do one read and count the bytes returned
  count=$(dd 2> /dev/null count=1 | wc -c)

  # If the user pressed a key like the £ or Home ones described above
  # it's likely all the corresponding bytes will have been read by dd
  # above, but not guaranteed, so we may want to drain the tty buffer
  # again to make sure we don't leave part of the sequence sent by a
  # key press to be read by the next thing that reads from the tty device
  # thereafter. Here allowing the terminal to send bytes as slow as 10
  # per second. Doing so however, we may end up reading the bytes sent
  # upon subsequent key presses though.
  stty time 1; cat > /dev/null

  stty "$saved_tty_settings"

  # return whether at least one byte was read:
  [ "$(($count))" -gt 0 ]

) <> /dev/tty >&0 2>&0

until
  echo "Hello World"
  sleep 1
  echo "Done greeting the world"
  read_key_with_timeout 5 "Press any key to stop"
do
  continue
done

答え2

while true; do
    echo 'Looping, press Ctrl+C to exit'
    sleep 5
done

それ以上複雑にする必要はありません。

以下が必要ですbash:

while true; do
    echo 'Press any key to exit, or wait 5 seconds'
    if read -r -N 1 -t 5; then
        break
    fi
done

失敗した場合read(タイムアウトにより)、ループは継続されます。

関連情報