hack のためのネタ帳, etc,,,

公式ページ等

マニュアル

参考になるページ

lint

Tips

以下の記事によると、空白を含むファイル名を処理する際、while と read を利用する場合があるが、パイプを使うと別プロセスになるらしい。 つまり以下のようなことをすると exit 出来ない
exit | echo foo >&2 | exit | echo bar >&2
結果
foo
bar
pstree を数珠つなぎにしてみるとパイプの各段が独立したプロセスになっている様子が良く分かる。
$ 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 文の場合
例えば、以下のようにすると状況がだいたいのみこめると思う。
$ 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 3
while の前後にパイプが存在する事で、
while の中でエラー発生時に exit で abort しようとしても出来なかったり、
while の中で環境変数を変更しようとしても、while の外に反映されなかったりしてはまる。
Command Substitution の場合
やはり同様の問題が生じる。
ただし、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)
readarray の場合
上記と同様の理由により以下のようなやり方は fork 先の別プロセスで実行されてしまうため上手く行かない。
ls | readarray hoge
echo "${hoge[@]}"
上記の記事では、ファイルを介する方法が紹介されているが、ファイルを介すると削除とかも必要なので面倒くさい。
そこでこのような場合、名前付きパイプ(named pipe)を使ってやると、以下のようにすると上手く行く。
readarray hoge < <(ls)
echo "${hoge[@]}"
find 等でファイル一覧を取得する際、行末の改行が入るので case 等でマッチングする際に不具合が生じる。
この場合 -t オプションで行末の改行を捨てるのが良いらしい。
readarray -t hoge < <(find .)
${parameter:-word} の構文で colon(:) を省略して ${parameter-word} とすると unset の場合のみパラメータ展開で word に置換が行われるとの事。
例えば以下のようになる。
$ 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
参考:
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
となる。
参考:
test コマンドに -t オプションをつけて 0,1,2 番の file descriptor が開いているか否か調べることで、標準入出力がリダイレクトやパイプされているか否かを調べることが出来る。
例えば以下のような感じ
$ { [ -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
参考:
追記: 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 の割り当て状況が確認出来る。
$ 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}
tty01
redirect11
pipe10
${PIPESTATUS[@]} を見ると、パイプ格段の終了値が取れる模様。

参考:
CentOS 6.6 で /etc/init.d/postfix のメッセージが日本語で出てきて調べてみたら $"Starting postfix: " みたいな書き方がされてて驚いた。
ただし、これは SJIS などでは2バイト目の"が問題になったりするので推奨されないやり方なんだそうな。

参考:
コマンド置換(command substitution)と何もしない組み込みコマンド(No effect)である : を利用して以下のようにすれば、
行継続の途中をコメントアウト出来そう。
$ echo 1\
> 2\
> 3
123
$ echo 1\
> `: 2`\
> 3
13
man 見ると出てくるんだけど、/dev/{tcp,udp}/host/port で socket 通信が出来るらしいのだが、使い方にしばらく悩んだ。
参考に挙げてあるページを参考にして、
以下のような感じでやってみると見事に HTTP 接続出来た。
$ { echo -e "HEAD / HTTP/1.1\nConnection: close\n" >&3 ; cat <&3 ; } 3<>/dev/tcp/googl.com/http
std{in,out,err} 以外のところに新しい fd に紐付けて開いてやるのと、
きちんと Connection: close を送ってやるのがコツのようだ。

流石に SSL までやろうとすると辛そうだが bash 恐ろしい子

参考:
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
を捕捉している

参考:
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
参考:
Ignore case したい場合は shopt の -s オプションで nocaseglob を有効にしておくと良いらしい。
一時的に変えたいなら、( ) でくくってサブプロセスに放り込んでおくと良い。
例えば、以下のようにすると、一つ目の 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 になる。

例えば、以下のように代入しておく。
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


参考:
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
...
以下のような記事を見つけた
サブプロセスに入ってしまうので、元プロセスの変数等に介入できないのが厄介だが、使い方によっては面白いかもしれない。

以下のような関数を ~/.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 なら、以下のよう複数の条件を記述した変数を用いる事が出来る。

extglob が on の場合

$ 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

extglob が off の場合

$ 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
ただし標準設定では interactive mode だと extglob は enable になっているが、batch mode だと extglob は disable になっているようなので、shell script として使う場合は、extglob の値を要確認。

Gist / kou1okada / sort.sh で使ってみた例
Here document で変数に放り込んで、eval で評価するって手もあるけど、$ をエスケープする必要があるし、エディタの編集サポートも使えないし、そもそも eval を使うこと自体が何となく気持ち悪いというのもあるので extglob を使う方がスマートだろう。

参考:
以下のように PS4 に command substitution で時刻を埋め込んでおいて、set -x すればよいらしい。
PS4='+ $(date "+%s.%N")\011 '
set -x
builtin の printf は %N 使えないし Cygwin だと fork のオーバーヘッドが辛い。

参考:

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Wiki内検索

フリーエリア

管理人/副管理人のみ編集できます