ユーザーが入力した行を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

これを行うには、1行ずつ読む必要がなく、文字ごとに読み取る必要があります。

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

Ctrl+D送る転送終了文字 (EOT、ASCII文字コード4、$'\04'in bash)を端末ドライバにコピーします。これはread()、シェルに送信する待機呼び出しを送信する効果があります。

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

それでは、2回入力するのをどのように防ぐことができますかCtrl+D

私が言ったように、単一の文字を読んでください。readシェル組み込みを使用している場合は、入力バッファーがある可能性があり、read()入力ストリームから最大文字数を読み取る必要があります(約16kb程度)。これは、シェルが16kbの入力チャンクを取得し、次に16kb未満のチャンク、0バイト(EOF)を取得します。入力の終わり(または改行文字または指定された区切り文字)が表示されると、制御はスクリプトに返されます。

単一文字を読み取るために使用する場合、read -n 1シェルは呼び出し時にシングルバイトバッファを使用します。read()つまり、緊密なループに入り、文字ごとに読み込み、各文字の後に制御をシェルスクリプトに返します。

唯一の問題read -nは、端末を「生モード」に設定することです。つまり、文字が解析なしでそのまま転送されるという意味です。たとえば、を押すと、Ctrl+D文字列にリテラルEOT文字が表示されます。だから私たちはそれを確認する必要があります。また、ユーザーがスクリプトに送信する前にを押すBackspaceCtrl+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_eot1に設定され、break内部ループから取り出されるステートメントが実行されます。改行文字()が検出されると、$'\n'内部ループから抜け出します。それ以外の場合は、line変数の末尾に文字を追加してください。

  • ループの後、行は標準出力として印刷されます。これがを使用して呼び出すことです"$line"。 EOTを検出してここに到達したら、最も外側のループを終了します。

1cat >file 1つの端末と別の端末で実行し、部分行を入力し、キーを押して出力で何が起こるかを確認してtail -f fileテストします。catCtrl+Dtail


ユーザーの場合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()通話中にユーザーがより多くの内容を入力しても、各行は1行だけ生成されました。read()読み取ったデータに複数の行を含めることができる唯一のケースは、ユーザーが改行Ctrl+VCtrl+J(リテラルの後の文字の後にリテラルの改行が続く場合(キャリッジリターンを時間に合わせて改行に変換するのとは反対Enter))を入力するときです。

ただし、組み込みシェルはread改行文字またはファイルの終わりが表示されるまで一度に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レコードごとに1つのレコードが返されると仮定し、それを1つのレコード(改行を含む)として扱う場合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シェルまたは他の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

答え3

何が必要かはわかりませんが、ユーザーが複数行を入力してからすべての行を完全に処理できるようにするには、mapfileEOFが表示されるまでユーザー入力を許可してからを使用できます。各行が配列の項目である配列を返します。

プログラム.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

関連情報