web-dev-qa-db-ja.com

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が押された行を処理するにはどうすればよいですか。

8
user123456

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

どうして?シェルは、標準の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の直前に明示的に改行を出力することもできます。

5
Kusalananda

端末デバイスのデフォルトモードでは、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ビルトインではこれを行うことはできませんが、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
_

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
_
1

私はあなたが何を求めているのかはっきりしていませんが、ユーザーが複数の行を入力してすべての行をまとめて処理できるようにしたい場合は、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
0
jeffpkamp