- @IT
- y-kawazの日記 / 2010-07-20: bashの配列変数に関するTips
- 68user's page / UNIXの部屋 コマンド検索: リダイレクト
- UNIX & Linux コマンド・シェルスクリプト リファレンス
- FVue / Bash: Passing variables by reference
- UEC - UNIXを深く理解し、シェルプログラミングを極めるコミュニティサイト
- 双六工場日誌 / 2013-08-15: bashのプロセス置換機能を活用して、シェル作業やスクリプト書きを効率化する
- stackoverflow / 2009-03-02: How do you echo a 4-digit Unicode character in Bash?
- Yakst
- 2015-09-28: Bashのよくある間違い
- 原文: Greg's Wiki / Bash Pitfalls
- 2013-08-21: 私が他人のシェルスクリプトから学んだこと
- 原文: Fizer Khan / 2013-08-14: What I learned from other's shell scripts
- 2015-09-28: Bashのよくある間違い
- GitHub / dylanaraps / pure-bash-bible
- Qiita / akinomyoga / 2018-08-04: Bash $((算術式)) のすべて - B 罠・バグ回避編
以下の記事によると、空白を含むファイル名を処理する際、while と read を利用する場合があるが、パイプを使うと別プロセスになるらしい。
つまり以下のようなことをすると exit 出来ない
exit | echo foo >&2 | exit | echo bar >&2結果
foo barpstree を数珠つなぎにしてみるとパイプの各段が独立したプロセスになっている様子が良く分かる。
$ pstree -p >&2 | pstree -p >&2 | pstree -p >&2結果
?(1)───mintty(1652)───bash(6816)─┬─pstree(136) ├─pstree(4804) └─pstree(5496) ?(1)───mintty(1652)───bash(6816)─┬─pstree(5496) └─pstree(4804) ?(1)───mintty(1652)───bash(6816)───pstree(5496)
例えば、以下のようにすると状況がだいたいのみこめると思う。
while の中でエラー発生時に exit で abort しようとしても出来なかったり、
while の中で環境変数を変更しようとしても、while の外に反映されなかったりしてはまる。
$ pstree -p; \ echo -e "loop outside : \$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}"; \ X=0; while [ $X -lt 1 ]; do # ここはパイプを使っていないため元プロセス echo -e "loop inside : \$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}"; pstree -p; X=$(( $X + 1 )); # exit 2; # while の外側の bash が終了する(while ループ以降の処理が実行されない) done; \ echo ${PIPESTATUS[@]}; \ echo -e "\$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}" | while read i; do # ここはパイプでリダイレクトするため fork されているので別プロセス echo -e "pipe input : ${i}"; echo -e "loop with pipe inside : \$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}"; pstree -p; exit 3; # パイプ用に fork した bash が終了する(while ループ以降の処理も実行される) done; \ echo ${PIPESTATUS[@]};出力結果は以下のようになる。
?(1)───mintty(1652)───bash(6816)───pstree(3152) loop outside : $$:6816 $BASHPID:6816 $PPID:1652 loop inside : $$:6816 $BASHPID:6816 $PPID:1652 ?(1)───mintty(1652)───bash(6816)───pstree(5008) 1 pipe input : $$:6816 $BASHPID:1044 $PPID:1652 loop with pipe inside : $$:6816 $BASHPID:6652 $PPID:1652 ?(1)───mintty(1652)───bash(6816)───bash(6652)───pstree(3300) 0 3while の前後にパイプが存在する事で、
while の中でエラー発生時に exit で abort しようとしても出来なかったり、
while の中で環境変数を変更しようとしても、while の外に反映されなかったりしてはまる。
やはり同様の問題が生じる。
ただし、substitution されるコマンドが1つだけの場合は問題ない模様。
ただし、substitution されるコマンドが1つだけの場合は問題ない模様。
$ echo "$(echo -e "\$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}")"; \ echo "$(pstree -p)"; \ echo "$(echo -e "\$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}"; pstree -p)";結果
$$:6816 $BASHPID:7072 $PPID:1652 ?(1)---mintty(1652)---bash(6816)---pstree(6356) $$:6816 $BASHPID:5476 $PPID:1652 ?(1)---mintty(1652)---bash(6816)---bash(5476)---pstree(3496)関数を使うと見た目上1つになるが、実際には複数コマンドになるので状況は改善しない。
$ function hoge() { echo -e "\$\$:${$} \$BASHPID:${BASHPID} \$PPID:${PPID}" pstree -p }; \ echo "$(hoge)";結果
$$:6816 $BASHPID:6920 $PPID:1652 ?(1)---mintty(1652)---bash(6816)---bash(6920)---pstree(4216)
上記と同様の理由により以下のようなやり方は fork 先の別プロセスで実行されてしまうため上手く行かない。
そこでこのような場合、名前付きパイプ(named pipe)を使ってやると、以下のようにすると上手く行く。
ls | readarray hoge echo "${hoge[@]}"上記の記事では、ファイルを介する方法が紹介されているが、ファイルを介すると削除とかも必要なので面倒くさい。
そこでこのような場合、名前付きパイプ(named pipe)を使ってやると、以下のようにすると上手く行く。
readarray hoge < <(ls) echo "${hoge[@]}"
find 等でファイル一覧を取得する際、行末の改行が入るので case 等でマッチングする際に不具合が生じる。
この場合 -t オプションで行末の改行を捨てるのが良いらしい。
この場合 -t オプションで行末の改行を捨てるのが良いらしい。
readarray -t hoge < <(find .)
${parameter:-word} の構文で colon(:) を省略して ${parameter-word} とすると unset の場合のみパラメータ展開で word に置換が行われるとの事。
例えば以下のようになる。
と思い良く読んでみたら、 ${parameter:-word}, ${parameter:=word}, ${parameter:?word}, ${parameter:+word} が説明されてる箇所の前段に以下のような説明がちゃんと付いていた。見落とすとか英語力なさ過ぎ(T T)。
例えば以下のようになる。
$ unset unset $ null= $ echo ${unset-UNSET} ${unset:-UNSETorNULL} UNSET UNSETorNULL $ echo ${null-UNSET} ${null:-UNSETorNULL} UNSETorNULLそんな構文 man 引いても Parameter Expansion の所に出てねぇよ!
と思い良く読んでみたら、 ${parameter:-word}, ${parameter:=word}, ${parameter:?word}, ${parameter:+word} が説明されてる箇所の前段に以下のような説明がちゃんと付いていた。見落とすとか英語力なさ過ぎ(T T)。
Omitting the colon results in a test only for a parameter that is unset.と言うことで以下のようにすれば $hoge について unset と null を判定出来る。
[ -z "$hoge" -a "${hoge-x}" = "x" ] && echo unset [ -z "$hoge" -a "${hoge-x}" = "" ] && echo null [ -z "$hoge" ] && echo unset or null参考:
- Bash Reference Manual # 3.5.3 Shell Parameter Expansion
- JM / bash(1) # パラメータの展開
- わからん / 2011-11-23: 変数が未定義かどうか判別する
- ほぷぅ(。・ω・) ノ PCまとめ Linux,Win,自宅サーバー 〜 / 2012-06-10: シェル Unix/Linux シェル変数 値がヌル、未設定の扱い
2>&1 とすると、標準エラー出力を標準出力に変更することが出来るが、
これは、「2 の出力先を、1 の出力先と同じものに設定する」という意味なんだそうな。
仕組みとしては、初期状態で
従って、
参考:
これは、「2 の出力先を、1 の出力先と同じものに設定する」という意味なんだそうな。
仕組みとしては、初期状態で
fd[1] = /dev/fd/1 fd[2] = /dev/fd/2となっているのを
fd[2] = fd[1];とすることで
fd[1] = /dev/fd/1 fd[2] = /dev/fd/1としている感じ。
従って、
foobar 2>&1 > hogeだと
fd[1] = hoge fd[2] = /dev/fd/1となり
foobar > hoge 2>&1だと
fd[1] = hoge fd[2] = hogeとなる。
参考:
- UNIXの部屋 コマンド検索: リダイレクト
test コマンドに -t オプションをつけて 0,1,2 番の file descriptor が開いているか否か調べることで、標準入出力がリダイレクトやパイプされているか否かを調べることが出来る。
例えば以下のような感じ
追記: 2015-02-09
test -p で /dev/{stdin,stdout,stderr} を調べるという方法もある模様。この場合、true/false が test -t とは逆になる模様。
参考:
追記: 2015-02-18
/dev/fd (実体は /proc/self/fd へのシンボリックリンク)以下を ls -l で見ると、例えば以下のようにシンボリックリンクとして file descriptor の割り当て状況が確認出来る。
追記: 2015-03-30
ちょっと違ってた。
正確には以下のようになる。
例えば以下のような感じ
$ { [ -t 0 ] ; echo $? >&2 ; } < /dev/null 1 $ { [ -t 0 ] ; echo $? >&2 ; } 0 $ { [ -t 1 ] ; echo $? >&2 ; } >/dev/null 1 $ { [ -t 1 ] ; echo $? >&2 ; } 0 $ { [ -t 2 ] ; echo $? ; } 2>/dev/null 1 $ { [ -t 2 ] ; echo $? ; } 0参考:
- stackoverflow / check isatty in bash
追記: 2015-02-09
test -p で /dev/{stdin,stdout,stderr} を調べるという方法もある模様。この場合、true/false が test -t とは逆になる模様。
参考:
- Qiita / b4b4r07 / シェルスクリプトでパイプを判断する
追記: 2015-02-18
/dev/fd (実体は /proc/self/fd へのシンボリックリンク)以下を ls -l で見ると、例えば以下のようにシンボリックリンクとして file descriptor の割り当て状況が確認出来る。
$ ls /proc/self/fd -l 4>&2 2>&1 合計 0 lrwxrwxrwx 1 kou なし 0 2月 18 10:10 0 -> /dev/pty1 lrwxrwxrwx 1 kou なし 0 2月 18 10:10 1 -> /dev/pty1 lrwxrwxrwx 1 kou なし 0 2月 18 10:10 2 -> /dev/pty1 lrwxrwxrwx 1 kou なし 0 2月 18 10:10 3 -> /proc/3052/fd/ lrwxrwxrwx 1 kou なし 0 2月 18 10:10 4 -> /dev/pty1
追記: 2015-03-30
ちょっと違ってた。
正確には以下のようになる。
$ { > ( > [ -t 1 ]; echo $?; [ -p /dev/stdout ]; echo $?; ls -l /proc/self/fd/1 /dev/fd/1 /dev/stdout > ) > ( > [ -t 1 ]; echo $?; [ -p /dev/stdout ]; echo $?; ls -l /proc/self/fd/1 /dev/fd/1 /dev/stdout > ) >/tmp/xxx; cat /tmp/xxx > ( > [ -t 1 ]; echo $?; [ -p /dev/stdout ]; echo $?; ls -l /proc/self/fd/1 /dev/fd/1 /dev/stdout > ) | cat > } 0 1 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /dev/fd/1 -> /dev/pty11 lrwxrwxrwx 1 kou None 15 12月 5 2013 /dev/stdout -> /proc/self/fd/1 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /proc/self/fd/1 -> /dev/pty11 1 1 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /dev/fd/1 -> /tmp/xxx lrwxrwxrwx 1 kou None 15 12月 5 2013 /dev/stdout -> /proc/self/fd/1 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /proc/self/fd/1 -> /tmp/xxx 1 0 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /dev/fd/1 -> pipe:[1008] lrwxrwxrwx 1 kou None 15 12月 5 2013 /dev/stdout -> /proc/self/fd/1 lrwxrwxrwx 1 kou None 0 3月 30 16:24 /proc/self/fd/1 -> pipe:[1008]つまり、まとめると以下のようになる。
状態 | test -t {0,1,2} | test -p /dev/std{in,out,err} |
---|---|---|
tty | 0 | 1 |
redirect | 1 | 1 |
pipe | 1 | 0 |
CentOS 6.6 で /etc/init.d/postfix のメッセージが日本語で出てきて調べてみたら $"Starting postfix: " みたいな書き方がされてて驚いた。
ただし、これは SJIS などでは2バイト目の"が問題になったりするので推奨されないやり方なんだそうな。
参考:
ただし、これは SJIS などでは2バイト目の"が問題になったりするので推奨されないやり方なんだそうな。
参考:
- Days of Speed / 2013-03-29: シェルスクリプト(bash)のメッセージを国際化する 2013年版
- 2001-01-13: bash スクリプト内メッセージの国際(カタログ)化
コマンド置換(command substitution)と何もしない組み込みコマンド(No effect)である : を利用して以下のようにすれば、
行継続の途中をコメントアウト出来そう。
行継続の途中をコメントアウト出来そう。
$ echo 1\ > 2\ > 3 123 $ echo 1\ > `: 2`\ > 3 13
man 見ると出てくるんだけど、/dev/{tcp,udp}/host/port で socket 通信が出来るらしいのだが、使い方にしばらく悩んだ。
参考に挙げてあるページを参考にして、
以下のような感じでやってみると見事に HTTP 接続出来た。
きちんと Connection: close を送ってやるのがコツのようだ。
流石に SSL までやろうとすると辛そうだが bash 恐ろしい子
参考:
参考に挙げてあるページを参考にして、
以下のような感じでやってみると見事に HTTP 接続出来た。
$ { echo -e "HEAD / HTTP/1.1\nConnection: close\n" >&3 ; cat <&3 ; } 3<>/dev/tcp/googl.com/httpstd{in,out,err} 以外のところに新しい fd に紐付けて開いてやるのと、
きちんと Connection: close を送ってやるのがコツのようだ。
流石に SSL までやろうとすると辛そうだが bash 恐ろしい子
参考:
- Qiita / k_ui / 2012-02-22: bash で TCP 通信
- Sarabande.jp / 2014-04-25: Bash: /dev/tcp、netcat、ncat を使って HTTP クライアントをつくる
read -N1 を使ってこんな感じかな?
REPLY=""; while [ "$REPLY" != "q" ]; do clear;{ banner Wait;banner "a q key."; }|head -n-1|head -c-1;read -N1;done;clear
以下の様にすると signal を捕捉して、process の終了前に特定の処理を実行出来る
参考:
trap finalize 0 1 2 3 15 # signal の捕捉 finalize はコマンドや関数等 trap 0 1 2 3 15 # signal の捕捉を解除上記の例では
- 0 通常の終了
- 1 HUP
- 2 INT
- 3 QUIT
- 15 TERM
参考:
- Bash Reference Manual # trap
- JM / kill.1 (section 1, procps)
shift # shift 1 shift n # shift n set -- "${@:1:$#-1}" # pop 1 (ただし 0 =< $# - 1) set -- "${@:1:$#-n}" # pop n (ただし 0 =< $# - n) set -- "$@" "${hoge[@]}" # push set -- "${hoge[@]}" "$@" # unshift参考:
- lists.gnu.org / Mail Archives / help-bash / 2012-02 / 2012-02-16: [Help-bash] opposite of 'shift'
Ignore case したい場合は shopt の -s オプションで nocaseglob を有効にしておくと良いらしい。
一時的に変えたいなら、( ) でくくってサブプロセスに放り込んでおくと良い。
例えば、以下のようにすると、一つ目の ls は case insensitive になるので大文字の JPG, PNG も拾うが、二つ目は元に戻るので小文字の jpg, png のみ拾う。
一時的に変えたいなら、( ) でくくってサブプロセスに放り込んでおくと良い。
例えば、以下のようにすると、一つ目の ls は case insensitive になるので大文字の JPG, PNG も拾うが、二つ目は元に戻るので小文字の jpg, png のみ拾う。
( shopt -s nocaseglob; ls *.{jpg,png}; ); ls *.{jpg,png}詳細は man bash して Pathname Expansion のところを参照。
いわゆる「間接参照」とか、
GNU Make だと「Computed Variable Names」や「計算された変数名とか、
PHP だと「Variable variables」や「可変変数」とか
のように呼ばれている機能そのものでないにしても近い機能に当たると思うんだが、
man bash の PARAMETERS と Parameter Expansion のところにしれっと書いてあったので気付いてなくて、
2018-03-13 にふと「bash 二重参照」でググったところ見つけた。
nameref は名前参照
indirect expansion は間接展開
とでも訳せばよいのだろうか?
nameref というのは、変数に入ってる値を変数名として、間接的に値を参照するための変数で、
declare や local に -n オプションを付けて代入すると nameref になる。
例えば、以下のように代入しておく。
indirect expansion というのは、同じく、変数に入ってる値を変数名として、間接的に値を展開する。
これは parameter expansion (パラメータ展開)で ${!parameter} のように paremeter の前に「!」を書く。
因みに、nameref に代入すると、以下のように参照先の変数が書き換わるので、
これはむしろ別名定義とか C 言語のポインタを用いた間接参照に近い機能と言えそうだ。
参考:
2018-11-21: 追記
bats-core/bin/bats 眺めてたら、どうも bash 組み込み関数版の printf に -v ってオプションがあって、これ使っても任意の変数名に対して代入出来るようだ。戻り値用に使うならこちらの方が短かそう。
こんな感じ。
GNU Make だと「Computed Variable Names」や「計算された変数名とか、
PHP だと「Variable variables」や「可変変数」とか
のように呼ばれている機能そのものでないにしても近い機能に当たると思うんだが、
man bash の PARAMETERS と Parameter Expansion のところにしれっと書いてあったので気付いてなくて、
2018-03-13 にふと「bash 二重参照」でググったところ見つけた。
nameref は名前参照
indirect expansion は間接展開
とでも訳せばよいのだろうか?
nameref というのは、変数に入ってる値を変数名として、間接的に値を参照するための変数で、
declare や local に -n オプションを付けて代入すると nameref になる。
例えば、以下のように代入しておく。
p=q q=r declare -n x=p # x=p declare -n y=$p # y=q declare -n z=x # z=pすると、以下のようになる。
echo ${x} # $p => q echo ${y} # $q => r echo ${z} # $p => q
indirect expansion というのは、同じく、変数に入ってる値を変数名として、間接的に値を展開する。
これは parameter expansion (パラメータ展開)で ${!parameter} のように paremeter の前に「!」を書く。
echo ${!p} # $q => rただし parameter が既に nameref の場合は、逆に変数に入っている値そのものに展開される。
echo ${!x} # x => p echo ${!y} # y => q echo ${!z} # z => p
因みに、nameref に代入すると、以下のように参照先の変数が書き換わるので、
これはむしろ別名定義とか C 言語のポインタを用いた間接参照に近い機能と言えそうだ。
x=hoge # p=hoge echo ${x} # $p => hoge echo ${y} # $q => r echo ${z} # $p => hoge echo ${!x} # x => p echo ${!y} # y => q echo ${!z} # z => p名前を振り直したい時は再度 declare -n すれば良い。
declare z=y # z=q echo ${x} # $p => hoge echo ${y} # $q => r echo ${z} # $q => r echo ${!x} # x => p echo ${!y} # y => q echo ${!z} # z => q
参考:
- 檜山正幸のキマイラ飼育記 / 2015-10-09: bashの間接参照変数
2018-11-21: 追記
bats-core/bin/bats 眺めてたら、どうも bash 組み込み関数版の printf に -v ってオプションがあって、これ使っても任意の変数名に対して代入出来るようだ。戻り値用に使うならこちらの方が短かそう。
こんな感じ。
$ echo $a $ printf -v a hoge $ echo $a hoge $ help printf ... Options: -v var assign the output to shell variable VAR rather than display it on the standard output ...
以下のような記事を見つけた
サブプロセスに入ってしまうので、元プロセスの変数等に介入できないのが厄介だが、使い方によっては面白いかもしれない。
- Qiita / some-nyan / 2017-04-05: bashでカッコを駆使して例外処理してみる
サブプロセスに入ってしまうので、元プロセスの変数等に介入できないのが厄介だが、使い方によっては面白いかもしれない。
以下のような関数を ~/.bashrc に作っておくと良い。
function path_to_head () # VARIABLE_NAME [VALUES ...] { local -n var="$1" local val for val in "${@:2}"; do var="${val}:${var/${val}:/}" done }
path_to_head PATH ~/local/binのようにすると、指定したパスを PATH の一番先頭に追加すると共に、既存の PATH に含まれる場合はそれを削除する。
source ~/.bashrcとかした際のパスの重複対策として有効。
shopt で extglob を enable にしておくと、Pathname expansion の Pattern Matching で [?*+@!](pattern-list) の構文が使えるようになる。
case ... esac 文の条件(pattern) は pathname expansion に基づくので、extglob が enable なら、以下のよう複数の条件を記述した変数を用いる事が出来る。
ただし標準設定では interactive mode だと extglob は enable になっているが、batch mode だと extglob は disable になっているようなので、shell script として使う場合は、extglob の値を要確認。
Gist / kou1okada / sort.sh で使ってみた例。
Here document で変数に放り込んで、eval で評価するって手もあるけど、$ をエスケープする必要があるし、エディタの編集サポートも使えないし、そもそも eval を使うこと自体が何となく気持ち悪いというのもあるので extglob を使う方がスマートだろう。
参考:
case ... esac 文の条件(pattern) は pathname expansion に基づくので、extglob が enable なら、以下のよう複数の条件を記述した変数を用いる事が出来る。
$ shopt -s extglob $ shopt extglob extglob on $ pat="@(a|b)" $ case "a" in $pat) echo MATCH ;; esac MATCH $ case "b" in $pat) echo MATCH ;; esac MATCH $ case "c" in $pat) echo MATCH ;; esac $ case "|" in $pat) echo MATCH ;; esac $ case "@(a|b)" in $pat) echo MATCH ;; esac $ pat="a|b" $ case "a" in $pat) echo MATCH ;; esac $ case "b" in $pat) echo MATCH ;; esac $ case "a|b" in $pat) echo MATCH ;; esac MATCH
$ shopt -u extglob $ shopt extglob extglob off $ pat="@(a|b)" $ case "a" in $pat) echo MATCH ;; esac $ case "b" in $pat) echo MATCH ;; esac $ case "c" in $pat) echo MATCH ;; esac $ case "|" in $pat) echo MATCH ;; esac $ case "@(a|b)" in $pat) echo MATCH ;; esac MATCH $ pat="a|b" $ case "a" in $pat) echo MATCH ;; esac $ case "b" in $pat) echo MATCH ;; esac $ case "a|b" in $pat) echo MATCH ;; esac MATCH
Gist / kou1okada / sort.sh で使ってみた例。
Here document で変数に放り込んで、eval で評価するって手もあるけど、$ をエスケープする必要があるし、エディタの編集サポートも使えないし、そもそも eval を使うこと自体が何となく気持ち悪いというのもあるので extglob を使う方がスマートだろう。
参考:
- StackExchange / Unix & Linux / 2015-10-06: How can I use a variable as a case condition?
以下のように PS4 に command substitution で時刻を埋め込んでおいて、set -x すればよいらしい。
参考:
PS4='+ $(date "+%s.%N")\011 ' set -xbuiltin の printf は %N 使えないし Cygwin だと fork のオーバーヘッドが辛い。
参考:
- stackoverflow / 2011-02-16: How to profile a bash shell script slow startup?
タグ
コメントをかく