rcmdnk's blog

入門UNIXシェルプログラミング―シェルの基礎から学ぶUNIXの世界

シェルスクリプトを使っていて、たまにコマンドラインで 入力文字が表示されなくなったりすることがあって、 その原因の1つがread-s(silent)で立ち上げた状態で ctrl-Cで止めてしまってた事だったことがわかったので その処理についてのまとめ。

Sponsored Links

問題が起きる条件

通常、シェルスクリプトで

1
2
3
4
5
#!/usr/bin/env bash

echo "Enter any key: "
read -s -n 1 c
echo "Input: $c"

みたいに読み込み文字を表示させないで入力を取り込もうとして、 このreadの待ちの状態でctrl-Cを押しても 特に問題は起こりません(ただエラー終了するだけです)。

ただし、このread部分を関数にして、さらにその関数をパイプ後に呼ぶと問題が 起こります。

1
2
3
4
5
6
7
8
#!/usr/bin/env bash

f_read () {
  echo "Enter any key: "
  read -s -n 1 c </dev/tty
  echo "Input: $c"
}
echo test|f_read

入力をパイプと分ける為に</dev/ttyを加えてあります。

これを実行すると、上の関数で無いものと同じ様に、一文字受け取ってそれを 表示するだけなんですが、 readの部分で止まっている時、ctrl-Cで止めてしまうと、 コマンドラインで打ち込んでも文字が表示されません。

実際にキーは入力されてるので適当にls <Enter>とかすれば表示されますが、 コマンドは表示されません。

ここで、stty echoを実行するときちんとコマンドも表示されるようなります。

サブシェルを呼ぶと問題なのか?と思ったんですが、

ret=$(f_read)

の様にした場合は途中で終了させても問題ないのでパイプにした時だけです。 パイプを使うと標準入力が切り替わったりするので、その辺で問題になるのかと。 (いまいち原理が理解できない。。。)

さらに組み合わせとして、

ret=$(echo test|f_read)

とした場合や

echo test|ret=$(f_read)

とした場合も問題が置きます。

さらに、trapでHUP信号等が来た時の動作を決めた場合でも様子が変わります。

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

f_read () {
  trap "return" 1 2 3 15
  echo "Enter any key: "
  read -s -n 1 c </dev/tty
  echo "Input: $c"
}
echo test|ret=$(f_read)

とすると、この場合は問題が起きません 1

ただし、パイプだけ:echo test |f_readや、中にパイプ:(echo test|f_read) の場合は上の様にしても駄目です。

解決法

解決法というほどではないですが、終了後にstty echoをすれば戻るので、 最後にかならずこれを呼んであげれば良い、というだけです。

上のスクリプトなら一番簡単な方法は

1
2
3
4
5
6
7
8
9
#!/usr/bin/env bash

f_read () {
  echo "Enter any key: "
  read -s -n 1 c </dev/tty
  echo "Input: $c"
}
echo test|f_read
stty echo

とすれば良いだけ2

ただし、このままだとこのスクリプト自体をパイプ後に入れると、 正しく終了した場合でもエラーが必ず出てしまいます。

stty echoを標準入力がパイプだったりターミナルと結びついてない状態で行うと、

$ echo test|stty echo
stty: stdin isn't a terminal

こんな感じのエラーに。 さらに、ctrl-Cで止めた場合には エラーが出る上に上の出力が止まる問題が発生します。

これを回避するために関数内でtrapをかけます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin/env bash

f_read () {
  trap "stty echo;return" 1 2 3 15
  echo "Enter any key: "
  read -s -n 1 c </dev/tty
  echo "Input: $c"
}
echo test|f_read
#f_read
#echo test|f_read
#ret=$(f_read)
#echo test|ret=$(f_read)
#ret=$(echo test|f_read)

これだと、上のコメントしてある場合も含めてすべての場合で 途中でctrl-Cで止めても、 その後でコマンドライン入力がきちんと表示されます。

ただし、この場合には、$()を使った場合、 ctrl-Cで止めると

stty: tcsetattr: Input/output error

というエラーが出ます。 この場合は、元々出力に問題がないものなので、終了後には コマンドもきちんと表示されますが、この処理がどうしても回避出来ませんでした。

差し当たり、問題が無いようなら、

trap "stty echo 2>/dev/null;return" 1 2 3 15

みたいにしてエラーメッセージを表示させない、ということまでは出来ます。

色々試してみた結果、今のところこんな感じ。

下のGistに色々試したスクリプトとメモが置いてあります。

memo & scripts for read test

Sponsored Links
  1. 上の関数でtrapを関数の中に書いてこれをサブシェル内で起動させる時、 HUP信号等に対してreturnを入れておかないと延々そこに留まってしまいます。

    trap "echo aaa" 1 2 3 15
    

    としておくと、ctrl-Cを押す度にaaaが表示されます。

    一方、trapを関数の外側で定義すると、 サブシェル内ではtrapがかからずctrl-Cで強制終了され、 そののち、親のシェルのtrapに入ります。

  2. パイプ処理の途中でctrl-Cが押された場合は 通常そのままスクリプトが進むので

Sponsored Links

« LogMeInのフリーアカウントが終了なのでTeamViewerに乗り換える シェルスクリプトで対話的な選択を出来るようにするスクリプトを作った:sentaku »