如何逐行读取用户输入直到 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

为此,您必须逐字符阅读,而不是逐行阅读。

为什么? 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两次呢?

正如我所说,读取单个字符。当您使用readshell 内置命令时,它可能有一个输入缓冲区,并要求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

使用bashPOSIX 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

相关内容