Ctrl+D までユーザー入力を 1 行ずつ読み取り、Ctrl+D が入力された行も含める方法

Ctrl+D までユーザー入力を 1 行ずつ読み取り、Ctrl+D が入力された行も含める方法

このスクリプトは、ユーザーの入力を1行ずつ受け取り、myfunction各行ごとに実行します。

#!/bin/bash
SENTENCE=""

while read word
do
    myfunction $word"
done
echo $SENTENCE

入力を停止するには、ユーザーは を押して[ENTER]から を押す必要がありますCtrl+D

Ctrl+D押された行のみを終了して処理するようにスクリプトを再構築するにはどうすればよいですかCtrl+D

答え1

そのためには、行ごとではなく、文字ごとに読む必要があります。

なぜでしょうか? シェルは、read() ユーザーが入力したデータを読み取るために標準 C ライブラリ関数を使用する可能性が高く、その関数は実際に読み取ったバイト数を返します。ゼロを返す場合は、EOF に遭遇したことを意味します (マニュアルを参照してくださいread(2)) man 2 read。EOF は文字ではなく条件、つまり「これ以上読み取るものがない」という条件であることに注意してください。ファイルの終わり

Ctrl+D送信する送信終了文字 (EOT、ASCII 文字コード 4、) を端末ドライバに送信します。これにより、シェルの待機呼び出しに送信するものがすべて送信され$'\04'ます。bashread()

Ctrl+D行にテキストを入力する途中でを押すと、それまでに入力した内容がシェルに送信されます1。つまり、 Ctrl+D行に何かを入力した後に を2回押すと、最初の入力でデータが送信され、2番目の入力でデータが送信されます。何もないを押すと、read()呼び出しはゼロを返し、シェルはそれを EOF として解釈します。同様に、Enterに続いて を押すとCtrl+D、送信するデータがないため、シェルはすぐに EOF を取得します。

では、2回入力しなくても済むようにするにはどうすればよいでしょうかCtrl+D?

前に述べたように、1 文字ずつ読み取ります。readシェルの組み込みコマンドを使用すると、おそらく入力バッファがあり、read()入力ストリームから最大でその文字数 (おそらく 16 KB 程度) の文字を読み取るように要求します。つまり、シェルは 16 KB の入力チャンクを取得し、その後に 16 KB 未満のチャンクが続き、その後にゼロ バイト (EOF) が続きます。入力の終わり (または改行、または指定された区切り文字) を検出すると、制御はスクリプトに戻ります。

を使用して 1 文字を読み取る場合read -n 1、シェルは の呼び出しで 1 バイトのバッファを使用しますread()。つまり、文字を 1 文字ずつ読み取りながらタイトなループを実行し、1 文字ごとに制御をシェル スクリプトに戻します。

の唯一の問題read -nは、端末を「raw モード」に設定することです。つまり、文字は解釈されずにそのまま送信されます。たとえば、 を押すと、文字列に EOT 文字がそのまま含まれます。そのため、これをチェックする必要があります。また、これには、 を押すか、 (前の単語を削除する) または (行の先頭まで削除する) を使用するCtrl+Dなどして、スクリプトに送信する前にユーザーが行を編集できなくなるという副作用もあります。BackspaceCtrl+WCtrl+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) に対して動作します。EOT ( $'\04') が検出されると、got_eot1 に設定され、break内部ループから抜け出すステートメントに進みます。改行 ( $'\n') が検出されると、内部ループから抜け出します。それ以外の場合は、変数の末尾に文字を追加しますline

  • ループの後、行は標準出力に印刷されます。これは、 を使用するスクリプトまたは関数を呼び出す場所です"$line"。EOT を検出してここに到達した場合は、最も外側のループを終了します。

1cat >fileこれをテストするには、 を1 つのターミナルで実行し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(リテラルの次の文字の後にリテラルの改行文字が続く ( を押したときにキャリッジ リターンが改行に変換されるのとは異なりますEnter)) として入力した場合です。

しかし、シェルreadの組み込み関数は、改行文字またはファイルの終わりが見つかるまで、入力を1バイトずつ読み取ります。ファイルの終わり空の行read(0, buf, 1)を押した場合にのみ発生する可能性がある0 が返されます。Ctrl-D

Ctrl-Dここでは、大規模な読み取りを実行し、入力が改行文字で終わらないことを検出する必要があります。

組み込み関数ではこれを行うことはできませんが、の組み込み関数readではこれを行うことができます。sysreadzsh

ユーザーの入力を考慮したい場合^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()1 つのレコードを返すと想定します。

#! /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

または他の POSIX シェルではbash、このアプローチと同等の方法として、システム コールを実行するために をsysread使用して、同様のことを行うことができます。ddread()

#! /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

関連情報