関数を引数渡しでもパイプ渡しでも同様に処理できるようにするのと、実装時にハマったことのメモ。
忙しい人向けの結論
以下のように実装すれば良い。
#!/bin/bash
to_upper() {
# パラメータの受け取り
if [ -p /dev/stdin ]; then
if [ "`echo $@`" == "" ]; then
__str=`cat -`
else
__str=$@
fi
else
__str=$@
fi
# 処理
echo "${__str}" | tr '[a-z]' '[A-Z]'
}
# 通常の呼び出し方(引数渡し)はもちろんOK
to_upper hoge
# パイプ渡しでもOK
echo hoge | to_upper
パイプで渡せるようにする実装
ググった結果、パイプで渡されたときは標準入力 cat -
を読み出せば良いとわかった。
#!/bin/bash
to_upper() {
# パラメータの受け取り
if [ -p /dev/stdin ]; then
__str=`cat -`
else
__str=$@
fi
# 処理
echo "${__str}" | tr '[a-z]' '[A-Z]'
}
ハマったこと
テキストファイルを while read line
で読み、この関数を通したところ、引数渡しをすると最初の1行が処理されない問題が発覚。
# テキストファイルを読み出し関数に通す
echo -e "txt1\ntxt2\ntxt3" | while read line
do
to_upper $line
done
# 結果
# TXT2
# TXT3
なぜ?
原因は未だに分からないのですが、関数を呼び出す側でパイプ処理が通されている状態で引数渡しをすると、 /dev/stdin
が名前付きパイプとして認識され、それが関数側にも伝搬してしまっている模様。
#!/bin/bash
to_upper() {
if [ -p /dev/stdin ]; then
echo stdin
else
echo args
fi
}
##### 引数渡しで実装すると、呼び出し元側の影響を受ける
### case1
echo -e "txt1" | while read line
do
to_upper $line
done
# 結果
# stdin (本当はargsと出て欲しい)
### case2
while read line
do
to_upper $line
done < file.txt
# 結果
# args (想定通りargsと出る)
##### 元々パイプ渡し処理の場合は問題ない
### case3
echo -e "txt1" | while read line
do
echo $line | to_upper
done
# 結果
# stdin (想定通りstdinと出る)
### case4
while read line
do
echo $line | to_upper
done < file.txt
# 結果
# stdin (想定通りstdinと出る)
よって、前述Case1のような実装をすると、 read line
で1行目を while
文に読み出され、関数内で /dev/stdin
を名前付きパイプとして評価され、 cat -
で残りの標準入力をすべて奪い取られた結果、2周目の read line
のときには標準入力が空になっているので while
ループを終了される、という挙動となっていた。
どう実装すればよいか
前述Case1のとき、引数渡し to_upper $line
で関数を呼び出すので、関数内の引数 $@
に入ってるべき値(この場合 txt1
という文字列)が入っている。
ということで、冒頭結論の実装例のように、 /dev/stdin
を評価したあとに $@
に値が入っているかどうかを評価するロジックにすればよい。
参考
標準入力を受け取れるシェルスクリプト、関数の作成(パイプで繋げられるようにする) - Qiita
シェルスクリプトでパイプを判断する - Qiita