Как прочитать пользовательский ввод построчно до нажатия Ctrl+D и включить строку, где было нажато Ctrl+D

Как прочитать пользовательский ввод построчно до нажатия Ctrl+D и включить строку, где было нажато Ctrl+D

Этот скрипт принимает ввод пользователя строка за строкой и выполняется myfunctionна каждой строке

#!/bin/bash
SENTENCE=""

while read word
do
    myfunction $word"
done
echo $SENTENCE

Чтобы остановить ввод, пользователь должен нажать [ENTER], а затем Ctrl+D.

Как мне перестроить свой скрипт так, чтобы он заканчивался только Ctrl+Dтой строкой, на которой Ctrl+Dбыло нажато, и обрабатывал ее?

решение1

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

Почему? Оболочка, скорее всего, использует стандартную функцию библиотеки C read() для чтения данных, которые вводит пользователь, и эта функция возвращает количество фактически прочитанных байтов. Если она возвращает ноль, это означает, что она столкнулась с EOF (см. руководство read(2); man 2 read). Обратите внимание, что EOF — это не символ, а условие, т. е. условие «больше нечего читать»,конец файла.

Ctrl+Dотправляетсимвол конца передачи (EOT, код символа ASCII 4, $'\04'в bash) драйверу терминала. Это приводит к отправке всего, что можно отправить, в ожидающий read()вызов оболочки.

Когда вы нажимаете Ctrl+Dна середине ввода текста в строку, все, что вы набрали до сих пор, отправляется в оболочку 1. Это означает, что если вы нажмете Ctrl+Dдважды после того, как напечатали что-то в строке, первый раз отправит некоторые данные, а второй отправитничего, и read()вызов вернет ноль, а оболочка интерпретирует это как EOF. Аналогично, если вы нажмете , Enterа затем Ctrl+D, оболочка сразу получит EOF, поскольку не было никаких данных для отправки.

Так как же избежать необходимости печатать Ctrl+Dдважды?

Как я уже сказал, считывайте отдельные символы. Когда вы используете readвстроенную команду оболочки, она, вероятно, имеет входной буфер и просит read()считать максимум столько символов из входного потока (может быть, 16 кб или около того). Это означает, что оболочка получит кучу кусков ввода по 16 кб, за которыми последует кусок, который может быть меньше 16 кб, за которым следуют нулевые байты (EOF). Как только будет обнаружен конец ввода (или новая строка, или указанный разделитель), управление возвращается скрипту.

Если вы используете read -n 1для чтения один символ, оболочка будет использовать буфер размером в один байт при вызове read(), т.е. она будет находиться в замкнутом цикле, считывая символ за символом, возвращая управление скрипту оболочки после каждого символа.

Единственная проблема заключается в read -nтом, что он устанавливает терминал в «сырой режим», что означает, что символы отправляются такими, какие они есть, без какой-либо интерпретации. Например, если вы нажмете Ctrl+D, вы получите буквальный символ EOT в вашей строке. Поэтому мы должны это проверить. Это также имеет побочный эффект, заключающийся в том, что пользователь не сможет редактировать строку перед отправкой ее в скрипт, например, нажав Backspace, или используя Ctrl+W(для удаления предыдущего слова) или Ctrl+U(для удаления до начала строки).

Короче говоря:Ниже приведен последний цикл, который bashдолжен выполнить ваш скрипт для считывания строки ввода, в то же время позволяя пользователю прервать ввод в любой момент, нажав Ctrl+D:

while true; do
    line=''

    while IFS= read -r -N 1 ch; do
        case "$ch" in
            $'\04') got_eot=1   ;&
            $'\n')  break       ;;
            *)      line="$line$ch" ;;
        esac
    done

    printf 'line: "%s"\n' "$line"

    if (( got_eot )); then
        break
    fi
done

Не вдаваясь в подробности:

  • IFS=очищает IFSпеременную. Без этого мы не смогли бы читать пробелы. Я использую read -Nвместо read -n, иначе мы не смогли бы определять новые строки. Параметр -rto readпозволяет нам правильно читать обратные косые черты.

  • Оператор caseдействует на каждый прочитанный символ ( ). Если обнаружен $chEOT ( ), он устанавливается в 1 и затем переходит к оператору, который выводит его из внутреннего цикла. Если обнаружен символ новой строки ( ), он просто выходит из внутреннего цикла. В противном случае он добавляет символ в конец переменной .$'\04'got_eotbreak$'\n'line

  • После цикла строка выводится на стандартный вывод. Это будет то место, где вы вызываете свой скрипт или функцию, которая использует "$line". Если мы попали сюда, обнаружив EOT, мы выходим из самого внешнего цикла.

1 Вы можете проверить это, запустив программу cat >fileв одном терминале и tail -f fileв другом, а затем ввести часть строки в catи нажать , Ctrl+Dчтобы увидеть, что произойдет в выводе tail.


Для ksh93пользователей: Цикл выше будет считывать символ возврата каретки, а не символ новой строки в ksh93, что означает, что тест для $'\n'нужно будет изменить на тест для $'\r'. Оболочка также отобразит их как ^M.

Чтобы обойти это:

stty_saved="$( stty -g )"
stty -echoctl

#цикл идет здесь, с заменой $'\n' на $'\r'

stty "$stty_saved"

Вы также можете захотеть явно вывести новую строку непосредственно перед , чтобы breakполучить точно такое же поведение, как в bash.

решение2

В режиме по умолчанию терминального устройства системный read()вызов (при вызове с достаточно большим буфером) будет начинать полные строки. Единственные случаи, когда считанные данные не будут заканчиваться символом новой строки, будут при нажатии Ctrl-D.

В моих тестах (на Linux, FreeBSD и Solaris) одиночный read()всегда выдает только одну строку, даже если пользователь ввел больше к моменту read()вызова. Единственный случай, когда считанные данные могут содержать более одной строки, — это когда пользователь вводит символ новой строки Ctrl+VCtrl+J(буквальный символ next, за которым следует буквальный символ новой строки (в отличие от возврата каретки, преобразованного в новую строку при нажатии Enter)).

Однако встроенная функция оболочки readсчитывает входные данные по одному байту за раз, пока не увидит символ новой строки или конца файла.конец файлабудет, когда read(0, buf, 1)возвращается 0, что может произойти только при нажатии Ctrl-Dна пустую строку.

Здесь вам нужно будет выполнять большие чтения и определять, Ctrl-Dкогда ввод не заканчивается символом новой строки.

С помощью встроенной функции этого сделать нельзя read, но можно с помощью sysreadвстроенной функции zsh.

Если вы хотите учесть ввод пользователем ^V^J:

#! /bin/zsh -
zmodload zsh/system # for sysread

myfunction() printf 'Got: <%s>\n' "$1"

lines=('')
while (($#lines)); do
  if (($#lines == 1)) && [[ $lines[1] == '' ]]; then
    sysread
    lines=("${(@f)REPLY}") # split on newline
    continue
  fi

  # pop one line
  line=$lines[1]
  lines[1]=()

  myfunction "$line"
done

Если вы хотите рассматривать foo^V^Jbarкак одну запись (со встроенным символом новой строки), то предположим, что каждая read()возвращает одну запись:

#! /bin/zsh -
zmodload zsh/system # for sysread

myfunction() printf 'Got: <%s>\n' "$1"

finished=false
while ! $finished && sysread line; do
  if [[ $line = *$'\n' ]]; then
    line=${line%?} # strip the newline
  else
    finished=true
  fi

  myfunction "$line"
done

В качестве альтернативы, с помощью zsh, вы можете использовать zshсобственный расширенный редактор строк для ввода данных и сопоставления ^Dих с виджетом, который сигнализирует об окончании ввода:

#! /bin/zsh -
myfunction() printf 'Got: <%s>\n' "$1"

finished=false
finish() {
  finished=true
  zle .accept-line
}

zle -N finish
bindkey '^D' finish

while ! $finished && line= && vared line; do
  myfunction "$line"
done

С помощью bashили других оболочек POSIX, для эквивалентного подхода sysread, вы можете сделать что-то похожее, используя ddдля выполнения read()системных вызовов:

#! /bin/sh -

sysread() {
  # add a . to preserve the trailing newlines
  REPLY=$(dd bs=8192 count=1 2> /dev/null; echo .)
  REPLY=${REPLY%?} # strip the .
  [ -n "$REPLY" ]
}

myfunction() { printf 'Got: <%s>\n' "$1"; }
nl='
'

finished=false
while ! "$finished" && sysread; do
  case $REPLY in
    (*"$nl") line=${REPLY%?};; # strip the newline
    (*) line=$REPLY finished=true
  esac

  myfunction "$line"
done

решение3

Я не совсем понимаю, о чем вы просите, но если вы хотите, чтобы пользователь мог ввести несколько строк, а затем обработать все строки как единое целое, вы можете использовать mapfile. Он принимает пользовательский ввод до тех пор, пока не встретится EOF, а затем возвращает массив, в котором каждая строка является элементом массива.

ПРОГРАММА.sh

#!/bin/bash

myfunction () {
    echo "$@"
}

SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input   #this takes user input until they terminate with ctrl+D
n=0
for line in "${input[@]}"
do
    ((n++))
    SENTANCE+="\n$n\t$(myfunction $line)"
done
echo -e "$SENTANCE"

Пример

$: bash PROGRAM.sh
Enter your input, press ctrl+D when finished
this is line 1
this is line 2
this is not line 10
# I pushed ctrl+d here

1   this is line 1
2   this is line 2
3   this is not line 10

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