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

Используйте:

echo 3 | sudo tee /proc/sys/vm/drop_caches
8
24.01.2017, 11:24
3 ответа

Для этого вам нужно читать символ за символом, а не строку за строкой.

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

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

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

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

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

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

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

Короче говоря: Ниже приводится последний цикл, который ваш скрипт bash должен выполнить для чтения строки ввода, в то же время { {1}} позволяя пользователю в любой момент прервать ввод, нажав 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 , иначе мы не сможем обнаружить символы новой строки. Параметр -r для чтения позволяет нам правильно читать обратную косую черту.

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

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

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


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

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

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

# the loop goes here, with $'\n' replaced by $'\r'

stty "$stty_saved"

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

7
27.01.2020, 20:12

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

В моих тестах (в Linux, FreeBSD и Solaris) один read () всегда дает только одну строку, даже если пользователь ввел больше к моменту read () называется. Единственный случай, когда прочитанные данные могут содержать более одной строки, - это когда пользователь вводит новую строку как Ctrl + V Ctrl + J (буквальный следующий символ, за которым следует буквальный символ новой строки ( в отличие от возврата каретки, преобразованного в новую строку при нажатии 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
1
27.01.2020, 20:12

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

ПРОГРАММА.ш

#!/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
0
27.01.2020, 20:12

Теги

Похожие вопросы