web-dev-qa-db-ja.com

シェル:読み取り: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 ''

押すとき CtrlD

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

最後の2つのケースで出力が同じになるのはなぜですか?それらをどのように区別できますか?

POSIXシェルとbashでこれを行う別の方法はありますか?

7
Tom Hale

read -n "$n"(POSIX機能ではありません)を使用し、stdinが端末デバイスの場合、readは端末をicanonモードから外します。それ以外の場合、readは端末回線の規律の内部回線エディターと次に、$n文字または改行が読み取られるまで、一度に1バイトを読み取ります(無効な文字を入力すると、予期しない結果が表示される場合があります)。

1行から最大$n文字を読み取ります。また、入力からIFS文字を削除しないように、$IFSを空にする必要があります。

icanonモードを終了したため、^Dは特別ではなくなりました。だから押すと Ctrl+D^D文字が読み取られます。

端末が何らかの方法で切断されていない限り、端末デバイスからeofは表示されません。 stdinが別のタイプのファイルの場合、eofが表示される場合があります(: | IFS= read -rn 1; echo "$?"の場合、stdinは空のパイプ、または/dev/nullからstdinをリダイレクトする場合など)

readは、$n文字(有効な文字の一部ではないバイトが1文字としてカウントされる)または行全体が読み取られた場合、0を返します。

したがって、1文字だけが要求されるという特別な場合:

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"
}

完全な文字が読み取られた場合にのみ戻ることに注意してください。入力が間違ったエンコーディング(ロケールのエンコーディングとは異なる)である場合、たとえば、UTF-8(0xc3 0xa9)が予期されているときに、端末がiso8859-1(0xe9)でエンコードされたéを送信する場合、次のように入力できます。 éをいくつでも使用すると、関数は戻りません。 bashread -n1は、2番目の0xe9を返します(両方を変数に格納します)。これはやや優れた動作です。

^C文字も読みたい場合 Ctrl+C (スクリプトを強制終了させる代わりに、^Z^\...の場合も)、または^S/^Q Ctrl+S/Q (フロー制御の代わりに)stty行に-isig -ixonを追加できます。 bashread -n1もそれを行わないことに注意してください(オフの場合、isigも復元されます)。

スクリプトが強制終了された場合など、tty設定は復元されません。 Ctrl+Ctrapを追加することもできますが、スクリプト内の他のtrapsを上書きする可能性があります。

zshの代わりにbashを使用することもできます。ここで、read -kksh93またはbashread -n/-Nよりも前の日付)は端末から1文字を読み取り、^Dを単独で処理します(非その文字が入力された場合はゼロ)、改行を特別に扱いません。

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

f()%sから%q

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

出力、ユーザーがnewlineと入力した場合、 '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.
2
agc

実際、Bashでread -rn1を実行し、^Dを押すと、EOF条件ではなく、リテラル制御文字として扱われます。制御文字は、印刷時に表示されるため、printf "'%s'"と一緒に表示されません。出力をod -cなどにパイプすると、他の回答ですでに述べたprintf "%q"と同様に表示されます。

入力として実際には何もないため、結果は異なります。ここでは、printf "%q"でも空です。

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

ここでreadが改行を返さない理由は2つあります。まず、これは読み取りのデフォルトの行区切り文字であるため、出力として返されます。第2に、これはデフォルトのIFSの一部でもあり、readは、先頭と末尾の空白がIFSの一部である場合、それらを削除します。

したがって、区切り文字をデフォルトから変更するには、read -dが必要ですandIFSを空にします。

$ 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を返すため、$?をチェックしてそれを検出できます。

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

改行とCtrl-Dの場合は、ステータス変数によって区別されます。

改行の場合、ステータスはtrue(0)ですが、Ctrl-Dが指定されると、ステータスはfalse(1)になります。

0
user218374