このスクリプトはユーザー入力を1行ずつ取得し、すべての行でmyfunction
を実行します
#!/bin/bash
SENTENCE=""
while read Word
do
myfunction $Word"
done
echo $SENTENCE
入力を停止するには、ユーザーは[ENTER]
を押してからCtrl+D
を押す必要があります。
Ctrl+D
だけで終わるようにスクリプトを再構築して、Ctrl+D
が押された行を処理するにはどうすればよいですか。
そのためには、行ごとではなく、文字ごとに読み取る必要があります。
どうして?シェルは、標準のCライブラリ関数read()
を使用してユーザーが入力しているデータを読み取る可能性が高く、その関数は実際に読み取られたバイト数を返します。ゼロを返す場合、EOF(read(2)
manual; man 2 read
を参照)が発生したことを意味します。EOFは文字以外の文字、つまり「これ以上読み取るものはない」という条件end-of-file。
Ctrl+D送信終了文字 (EOT、ASCII文字コード4、$'\04'
in bash
)をターミナルドライバーに送信します。これにより、送信するものはすべてシェルの待機中のread()
呼び出しに送信されます。
押すと Ctrl+D 行にテキストを入力する途中で、これまでに入力したものはすべてシェルに送信されます1。これは、 Ctrl+D 行に何かを入力した後2回、最初の1つはデータを送信し、2つ目はnothingを送信し、read()
呼び出しはゼロを返しますシェルはそれをEOFと解釈します。同様に、 Enter に続く Ctrl+D、送信するデータがなかったため、シェルはEOFを一度に取得します。
だからタイプすることを避ける方法 Ctrl+D 二回?
私が言ったように、単一の文字を読みます。 read
Shell組み込みコマンドを使用する場合、おそらく入力バッファーがあり、read()
に入力ストリームから最大でその数の文字を読み取るように要求します(おそらく16 kb程度)。 。これは、シェルが16 kbの入力チャンクの束を受け取り、その後に16 kb未満のチャンクが続き、その後にゼロバイト(EOF)が続くことを意味します。入力(または改行、または指定された区切り文字)の終わりに到達すると、制御がスクリプトに戻ります。
read -n 1
を使用して単一の文字を読み取る場合、シェルはread()
への呼び出しで単一バイトのバッファーを使用します。つまり、文字ごとに文字を読み取るタイトなループに座って、制御をシェルに返します。それぞれの後のスクリプト。
read -n
の唯一の問題は、端末が「rawモード」に設定されることです。つまり、文字は解釈されずにそのまま送信されます。たとえば、 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
を使用します。そうしないと、改行を検出できません。 read
の-r
オプションを使用すると、バックスラッシュを適切に読み取ることができます。
case
ステートメントは、各読み取り文字($ch
)に作用します。 EOT($'\04'
)が検出されると、got_eot
が1に設定され、次にbreak
ステートメントにフォールスルーして、内部ループから抜け出します。改行($'\n'
)が検出された場合は、内部ループから抜け出します。それ以外の場合は、line
変数の末尾に文字を追加します。
ループの後、行は標準出力に出力されます。これは、"$line"
を使用するスクリプトまたは関数を呼び出す場所です。 EOTを検出してここに到達した場合、最も外側のループを終了します。
1 これをテストするには、ある端末でcat >file
を実行し、別の端末でtail -f file
を実行してから、cat
に部分的な行を入力して、 Ctrl+Dtail
の出力で何が起こるかを確認します。
ksh93
ユーザーの場合:上記のループは、ksh93
の改行文字ではなく復帰文字を読み取ります。つまり、$'\n'
のテストを$'\r'
のテストに変更する必要があります。シェルはこれらも^M
として表示します。
これを回避するには:
stty_saved = "$(stty -g)" stty -echoctl # ループはここに進み、$ '\ n'は$ '\ r'に置き換えられます stty "$ stty_saved"
break
とまったく同じ動作を得るために、bash
の直前に明示的に改行を出力することもできます。
端末デバイスのデフォルトモードでは、read()
システムコール(十分な大きさのバッファーを指定して呼び出された場合)は行全体をリードします。読み取ったデータが改行文字で終了しない唯一の場合は、 Ctrl-D。
私のテスト(Linux、FreeBSD、およびSolaris)では、ユーザーがread()
を呼び出すまでにさらに入力した場合でも、単一のread()
は1行しか生成しません。読み込まれたデータに複数の行が含まれる可能性がある唯一のケースは、ユーザーが次のように改行を入力したときです。 Ctrl+VCtrl+J (リテラルの次の文字の後にリテラルの改行文字が続きます(押すと改行に改行が変換されるのとは対照的) Enter))。
ただし、read
Shellビルトインは、改行文字またはファイルの終わりを検出するまで、一度に1バイトずつ入力を読み取ります。そのファイルの終わりは、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()
が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
_
bash
または他のPOSIXシェルでは、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
_
私はあなたが何を求めているのかはっきりしていませんが、ユーザーが複数の行を入力してすべての行をまとめて処理できるようにしたい場合は、mapfile
を使用できます。 EOFが検出されるまでユーザー入力を受け取り、各行が配列内のアイテムである配列を返します。
SENTANCE=''
echo "Enter your input, press ctrl+D when finished"
mapfile input #this takes user input until they terminate with ctrl+D
for line in "${input[@]}
do
myfunction $line
done
echo $SENTANCE