Bashメモ

多重代入の実現

Bashから別のプログラムを呼んで複数の計算結果を返して変数に代入したい場合、以下の形式に沿っておくと簡潔に書ける。例えば、,で挟んで複数の計算結果を返す場合、

eval $(echo $result | sed -r 's/(.*),(.*),(.*)/one=\1;two=\2;three=\3/')

と書ける。 後方参照が二桁になってしまうような場合は、

eval $(echo $result | sed -r 's/([^,]+,){9}(.*)/ten=\2/')

と、量指定子でシフトさせて取り出す。

性能改善

シェルスクリプトの中で1行ずつ変数を分割する際には、cutとかawkとか余計なプロセスを起動せずsetを使って分割した方が効率的 - 双六工場日誌

という記事を見て、余計なプロセスを起動していることを認識。 以下、2つのスクリプトを作成してベンチマーク。(Cygwin環境)

a.sh

#!/bin/bash.exe
line="aaa,bbb,ccc,ddd,eee"
for i in $(seq 1 100); do
        eval $(echo $line | sed -r 's/^([a-z]+),([a-z]+),([a-z]+),([a-z]+),([a-z]+)$/a=\1;b=\2;c=\3;d=\4;e=\5/g')
done
echo $a;echo $b;echo $c;echo $d;echo $e;

b.sh

#!/bin/bash.exe
line="aaa,bbb,ccc,ddd,eee"
_IFS=$IFS
IFS=$','
for i in $(seq 1 100); do
        set -- $line
        a=$1;b=$2;c=$3;d=$4;e=$5
done
IFS=$_IFS
echo $a;echo $b;echo $c;echo $d;echo $e;
$ time bash a.sh
aaa
bbb
ccc
ddd
eee


real    0m3.193s
user    0m0.633s
sys     0m2.018s


$ time bash b.sh
aaa
bbb
ccc
ddd
eee


real    0m0.062s
user    0m0.015s
sys     0m0.030s

Bash 文字列操作覚え書き

それほど使うことがないものあるが${a:-}${a%.*}${a//}あたりは頻出。

$ echo ch{,annel,arset}
ch channel charset
$ echo ch{1..13}
ch1 ch2 ch3 ch4 ch5 ch6 ch7 ch8 ch9 ch10 ch11 ch12 ch13
$ echo ch{36,40,44,48}
ch36 ch40 ch44 ch48
$ chs=$(echo ch{36,40,44,48} | sed 's/ /,/g')
$ echo $chs
ch36,ch40,ch44,ch48
$ echo ${#chs}
19
$ echo ${chs/,/, }
ch36, ch40,ch44,ch48
$ echo ${chs//,/, }
ch36, ch40, ch44, ch48
$ echo ${chs::4}
ch36
$ echo ${chs:5:4}
ch40
$ echo ${chs: -4}
ch48
$ echo ${chs:5}
ch40,ch44,ch48
$ echo ${chs: -9:4}
ch44
$ echo ${chs#*,}
ch40,ch44,ch48
$ echo ${chs##*,}
ch48
$ echo ${chs%,*}
ch36,ch40,ch44
$ echo ${chs%%,*}
ch36
$ echo $cchs

$ echo ${chs:-Nothing}
ch36,ch40,ch44,ch48
$ echo ${cchs:-Nothing}
Nothing
$ echo ${chs:+Nothing}
Nothing
$ echo ${cchs:+Nothing}

$ echo ${cchs:-$chs}
ch36,ch40,ch44,ch48
$ echo $cchs

$ echo ${cchs:=$chs}
ch36,ch40,ch44,ch48
$ echo $cchs
ch36,ch40,ch44,ch48
$ echo ${chs/%?/}
ch36,ch40,ch44,ch4
$ echo ${chs/#??/}
36,ch40,ch44,ch48
$ echo {dir1,dir2}/log{1..5}{,.txt}
dir1/log1 dir1/log1.txt dir1/log2 dir1/log2.txt dir1/log3 dir1/log3.txt dir1/log4 dir1/log4.txt dir1/log5 dir1/log5.txt dir2/log1 dir2/log1.txt dir2/log2 dir2/log2.txt dir2/log3 dir2/log3.txt dir2/log4 dir2/log4.txt dir2/log5 dir2/log5.txt
  • 以下はbash 4.0以降
$ bash --version
GNU bash, version 4.2.53(1)-release (x86_64-redhat-linux-gnu)
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software; you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ Chs=$(echo Ch{36,40,44,48} | sed 's/ /,/g')
$ echo ${Chs}
Ch36,Ch40,Ch44,Ch48
$ echo ${Chs,}
ch36,Ch40,Ch44,Ch48
$ echo ${Chs,,}
ch36,ch40,ch44,ch48
$ echo ${Chs^^}
CH36,CH40,CH44,CH48
$ echo ${Chs~~}
cH36,cH40,cH44,cH48

改行入りの文字列を扱うとき

コマンドやプログラムの結果に改行が含まれていて、1行ずつ処理したい場合は、デリミタを示す環境変数であるIFSを改行に変えておく。デフォルトはスペース・タブ・改行になっている。行毎処理が終わったら、デフォルト値に戻しておく。

_IFS=$IFS # 単語の区切り、を一時保存。
IFS=$'\n' # 単語の区切りを、一時的に改行のみにする。ダブるクォートではなく、シングルクォートで。
res=(` ... `) # コマンド実行。

for line in ${res[*]}; do
    echo $line
    # 一行ずつ処理できる。
done

IFS=$_IFS