
Этот скрипт принимает ввод пользователя строка за строкой и выполняется 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
, иначе мы не смогли бы определять новые строки. Параметр-r
toread
позволяет нам правильно читать обратные косые черты.Оператор
case
действует на каждый прочитанный символ ( ). Если обнаружен$ch
EOT ( ), он устанавливается в 1 и затем переходит к оператору, который выводит его из внутреннего цикла. Если обнаружен символ новой строки ( ), он просто выходит из внутреннего цикла. В противном случае он добавляет символ в конец переменной .$'\04'
got_eot
break
$'\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