该脚本逐行获取用户输入,并myfunction
在每一行执行
#!/bin/bash
SENTENCE=""
while read word
do
myfunction $word"
done
echo $SENTENCE
要停止输入,用户必须按[ENTER]
,然后按Ctrl+D
。
如何重建脚本以仅结束Ctrl+D
并处理按下的行Ctrl+D
。
答案1
为此,您必须逐字符阅读,而不是逐行阅读。
为什么? shell 很可能使用标准 C 库函数read()
来读取用户输入的数据,并且该函数返回实际读取的字节数。如果它返回零,则意味着它遇到了 EOF(请参阅手册read(2)
;man 2 read
)。注意,EOF不是一个字符,而是一个条件,即条件“没有更多内容可读”,文件结尾。
Ctrl+D发送一个传输结束字符
(EOT,ASCII 字符代码 4,$'\04'
在bash
) 到终端驱动程序。这具有发送任何要发送到read()
shell 的等待调用的效果。
当您Ctrl+D在一行中输入文本时按到一半,到目前为止您输入的任何内容都会发送到 shell 1。这意味着,如果您
Ctrl+D在一行中输入内容后输入两次,则第一个将发送一些数据,第二个将发送没有什么,调用read()
将返回零并且 shell 将其解释为 EOF。同样,如果您按Enter后跟Ctrl+D,shell 会立即收到 EOF,因为没有任何数据要发送。
那么如何避免输入Ctrl+D两次呢?
正如我所说,读取单个字符。当您使用read
shell 内置命令时,它可能有一个输入缓冲区,并要求read()
从输入流中读取最多那么多的字符(可能是 16 kb 左右)。这意味着 shell 将获得一堆 16 kb 的输入块,后面跟着一个可能小于 16 kb 的块,然后是零字节(EOF)。一旦遇到输入的结尾(或换行符或指定的分隔符),控制权就会返回给脚本。
如果您用来read -n 1
读取单个字符,shell 将在其对 的调用中使用单个字节的缓冲区read()
,即它将处于一个紧密循环中,逐个字符地读取,并在每个字符之后将控制权返回给 shell 脚本。
唯一的问题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
选项read
使我们能够正确读取反斜杠。该
case
语句作用于每个读取的字符 ($ch
)。如果$'\04'
检测到EOT ( ),它将设置got_eot
为 1,然后执行将break
其从内循环中取出的语句。如果检测到换行符 ($'\n'
),它就会跳出内循环。否则,它将字符添加到line
变量的末尾。循环之后,该行被打印到标准输出。这将是您调用使用
"$line"
.如果我们通过检测 EOT 到达这里,我们就会退出最外层循环。
1您可以通过cat >file
在一个终端和tail -f file
另一个终端中运行来测试这一点,然后在 中输入部分行
cat
并按Ctrl+D看看 的输出中会发生什么tail
。
对于ksh93
用户来说:上面的循环将读取 中的回车符而不是换行符ksh93
,这意味着 的测试$'\n'
将需要更改为 的测试$'\r'
。shell 也会将它们显示为^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(文字下一个字符后跟文字换行符(与按 时将回车符转换为换行符相反Enter)) 。
然而,内置的shellread
一次读取一个字节,直到看到换行符或文件结尾。那文件结尾会是当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 shell 或其他 POSIX shell,对于等效的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