shell: read: 区分 EOF 和换行符

shell: read: 区分 EOF 和换行符

读取单个字符,如何区分 null<EOF>\n

例如:

f() { read -rn 1 -p "Enter a character: " char &&
      printf "\nYou entered '%s'\n" "$char"; }

带有可打印字符:

$ f
Enter a character: x
You entered 'x'

按下时Enter

$ f
Enter a character: 

You entered ''

当按Ctrl+时D

$ f
Enter a character: ^D
You entered ''
$ 

为什么最后两种情况的输出相同?我如何区分它们?

与 POSIX shell 相比,有不同的方法吗bash

答案1

使用read -n "$n"(不是 POSIX 功能),并且如果 stdin 是终端设备,则read使终端退出该icanon模式,否则read只能看到终端行规则内部行编辑器返回的整行,然后一次读取一个字节,直到$n已读取字符或换行符(如果输入无效字符,您可能会看到意外结果)。

$n它从一行读取最多一个字符。您还需要清空$IFS它,以免从输入中删除 IFS 字符。

既然我们离开了icanon模式,^D就不再特殊了。因此,如果您按,则会读取Ctrl+D该字符。^D

除非终端以某种方式断开连接,否则您不会从终端设备看到 eof。如果 stdin 是另一种类型的文件,您可能会看到 eof (例如: | IFS= read -rn 1; echo "$?"stdin 是空管道,或者从 重定向 stdin /dev/null

read$n如果已读取字符(不构成有效字符一部分的字节被计为 1 个字符)或整行,则将返回 0 。

因此,在仅请求一个字符的特殊情况下:

if IFS= read -rn 1 var; then
  if [ "${#var}" -eq 0 ]; then
    echo an empty line was read
  else
    printf %s "${#var} character "
    (export LC_ALL=C; printf '%s\n' "made of ${#var} byte(s) was read")
  fi
else
  echo "EOF found"
fi

POSIXly 做起来相当复杂。

这将类似于(假设一个基于 ASCII(而不是 EBCDIC)的系统):

readk() {
  REPLY= ret=1
  if [ -t 0 ]; then
    saved_settings=$(stty -g)
    stty -icanon min 1 time 0 icrnl
  fi
  while true; do
    code=$(dd bs=1 count=1 2> /dev/null | od -An -vto1 | tr -cd 0-7)
    [ -n "$code" ] || break
    case $code in
      000 | 012) ret=0; break;; # can't store NUL in variable anyway
      (*) REPLY=$REPLY$(printf "\\$code");;
    esac
    if expr " $REPLY" : ' .' > /dev/null; then
      ret=0
      break
    fi
  done
  if [ -t 0 ]; then
    stty "$saved_settings"
  fi
  return "$ret"
}

请注意,我们仅在读取完整字符后才返回。如果输入的编码错误(与语言环境的编码不同),例如,如果您的终端发送的是éiso8859-1 (0xe9) 编码,而我们期望的是 UTF-8 (0xc3 0xa9),那么您可以输入é任意数量的内容,该函数不会返回。bash'sread -n1将返回第二个 0xe9 (并将两者都存储在变量中),这是一个稍微好一点的行为。

如果您还想读取^Con 上的字符Ctrl+C(而不是让它杀死您的脚本;也适用于^Z, ^\...)或 ^S/ ^Qon Ctrl+S/Q(而不是流控制),您可以-isig -ixon在该stty行中添加 a 。请注意,bash'sread -n1也不会执行此操作(isig如果它关闭,它甚至会恢复)。

如果脚本被终止,这将不会恢复 tty 设置(例如,如果您按 )Ctrl+C。您可以添加trap,但这可能会覆盖trap脚本中的其他设置。

您还可以使用zsh代替bash, where read -k(早于ksh93or bash's read -n/-N)从终端读取一个字符并^D自行处理(如果输入该字符则返回非零)并且不特殊对待换行符。

if read -k k; then
  printf '1 character entered: %q\n' $k
fi

答案2

f()其更改%s%q

f() { read -rn 1 -p "Enter a character: " char && \
      printf "\nYou entered '%q'\n" "$char"; }
f;f

输出,如果用户输入新队, 然后 'Ctrl-D':

Enter a character: 

You entered ''''
Enter a character: ^D
You entered '$'\004''

来自`man printf:

 %q       ARGUMENT is printed in a format that can be reused as shell input, 
          escaping non-printable characters with the proposed POSIX $'' syntax.

答案3

实际上,如果您read -rn1在 Bash 中运行并点击^D,它会被视为文字控制字符,而不是 EOF 条件。控制字符在打印时不可见,因此它不会与 一起出现printf "'%s'"。将输出连接到类似的东西od -c会显示它,就像printf "%q"已经提到的其他答案一样。

实际上没有任何输入,结果是不同的,即使使用以下内容,这里也是空的printf "%q"

$ f()  { read -rn 1  x ; printf "%q\n" "$x"; }
$ printf "" | f
''

此处不返回换行符read有两个原因。首先,它是读取的默认行分隔符,因此作为输出返回。其次,它也是默认的一部分IFS,并且read删除前导和尾随空格(如果它们是 的一部分)IFS

因此,我们需要read -d更改默认的分隔符,清空IFS

$ g() { IFS= read -rn 1 -d '' x ; printf "%q\n" "$x"; }
$ printf "\n" | g
$'\n'

read -d ""使分隔符有效地成为 NUL 字节,这意味着这仍然无法区分无输入和 NUL 字节输入之间的区别:

$ printf "" | g
''
$ printf "\000" | g
''

尽管没有任何输入,但read返回 false,因此我们可以检查$?以检测到这一点。

答案4

read -r var
status=$?
echo "\$var='$var':\$?=$status"

换行符和 Ctrl-D 情况通过状态变量来区分。

如果是换行符,状态为 true (0),而当按下 Ctrl-D 时,状态为 false (1)

相关内容