Нажмите любую клавишу, чтобы остановить цикл.

Нажмите любую клавишу, чтобы остановить цикл.

Как сделать следующее?

  • У меня есть циклический сценарий.
  • Я хочу, чтобы он останавливался при нажатии клавиши, иначе он продолжится через 5 секунд.

решение1

Следует помнить, что оболочки или любые приложения, которые вы обычно запускаете в терминале, не взаимодействуют с клавиатурой и экраном.

Они принимают входные данные из своего stdin как поток байтов. Когда этот stdin поступает из обычного файла, байты поступают оттуда, когда это канал, это данные, обычно отправляемые другим процессом, когда это некий файл устройства, который может поступать на физические устройства, подключенные к компьютеру. Например, когда это символьное устройство tty, это данные, отправляемые по некоторой последовательной линии, обычно терминалом. Терминал — это некая форма устройства, которая преобразует события клавиатуры в последовательности байтов.

Вот где вся мощь терминальных приложений. Механизм ввода для них абстрагирован, так что их можно использовать интерактивно или автоматически в скриптах.

Здесь, если вы собираетесь выдать такой запрос и ожидаетеключсобытие нажатия, вы, вероятно, хотите, чтобы ваше приложение (ваш скрипт) было только интерактивным. Либо ожидайте, что stdin будет терминалом, либо принимайте ввод с терминала независимо от того, на чем открыт stdin.

Теперь, как показано выше, все приложения видят потоки байтов, это роль терминала (или эмулятора терминала) и дисциплины линии устройства tty по переводу нажатых клавиш в последовательности байтов. Несколько примеров:

  • при нажатии aклавиши ASCII-терминалы отправляют байт 0x61,
  • При нажатии £клавиши терминалы UTF-8 отправляют два байта 0xc2 и 0xa3.
  • При нажатии Enterклавиши ASCII-терминалы отправляют байт 0x0d, который дисциплины линии tty в системах на основе ASCII, таких как Linux, обычно преобразуют в 0x0a.
  • При нажатии Ctrlодной клавиши терминалы ничего не отправляют, но если нажать ее вместе с клавишей C, терминалы отправляют байт 0x03, который дисциплина линии tty перехватывает для отправки сигнала SIGINT в приоритетную задачу.
  • При нажатии Left, терминалы обычно отправляют последовательность байтов (зависит от терминала, приложения могут запрашивать базу данных terminfo для ее перевода), первый из которых — 0x1b. Например, в зависимости от режима, в котором он находится, , xtermв системах на основе ASCII, отправит либо 0x1b 0x4f 0x44, либо 0x1b 0x5b 0x44 ( <ESC>[Aили <ESC>OA).

Итак, вот какие вопросы я бы задал:

  1. Вы все еще хотите спрашивать пользователя, если stdin не является терминалом?
  2. Если ответ на вопрос 1 — да, то хотите ли вы вывести запрос пользователю на терминал или через stdin/stdout?
  3. Если ответ на вопрос 1 — нет, вы все равно хотите ждать 5 секунд между каждой итерацией?
  4. Если ответ на вопрос 2:через терминал, должен ли скрипт прервать работу, если он не может обнаружить управляющий терминал, или вернуться в нетерминальный режим?
  5. Хотите ли вы учитывать только те клавиши, которые были нажаты после выдачи подсказки? Например, если пользователь случайно нажал клавишу до выдачи подсказки.
  6. Насколько далеко вы готовы зайти, чтобы убедиться, что вы считываете только те байты, которые выдаются при нажатии одной клавиши?

Здесь я предполагаю, что вы хотите, чтобы ваш скрипт был исключительно терминальным интерактивным приложением и взаимодействовал только через управляющий терминал, оставив 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происходит сбой (по тайм-ауту), цикл продолжается.

Связанный контент