Для этого вам нужно читать символ за символом, а не строку за строкой.
Почему? Оболочка, скорее всего, использует стандартную библиотечную функцию 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
.
В режиме терминального устройства по умолчанию системный вызов 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
Я не совсем понимаю, о чем вы просите, но если вы хотите, чтобы пользователь мог вводить несколько строк, а затем обрабатывать все строки целиком, вы можете использовать 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