スペースやタブで区切られた文字列から、特定の箇所を抽出する、
といえば、真っ先にcutやawkが浮かびますが、実はsetでも似たようなことができます。
お題
例えば、こんなhostsがあったとします。空白はすべてスペースであるとします。
123.123.123.123 geeg1 # application server
123.123.123.124 geeg2 # web frontend server
123.123.123.125 geeg3 # super fabulous exciting backup server #1
これをループして、IP、ホスト名、コメント部分を別々の変数に格納し、表示します。
cutの場合
while read line
do
ip=$(cut -d' ' -f 1 <<<${line})
hostn=$(cut -d' ' -f 2 <<<${line})
comment=$(cut -d' ' -f 4- <<<${line})
echo "[ip]${ip} [hostname]${hostn} [comment]${comment}"
done < hosts
他にも書きようがあるかもしれませんが、ちょっとごちゃっとしてますね。
awkの場合
while read line
do
ip=$(awk '{print $1}' <<<${line})
hostn=$(awk '{print $2}' <<<${line})
comment=$(awk '{for(i=4;i<NF;i++){ printf("%s%s",$i,OFS=" ")}print $NF}' <<<${line})
echo "[ip]${ip} [hostname]${hostn} [comment]${comment}"
done < hosts
本当はawk内で完結させようと思ったのですが、知識及ばず…。
awkはフィールド範囲指定が弱いっぽいので、cutより複雑になってしまいますね。
setの場合
while read line
do
set ${line}
ip=${1}
hostn=${2}
comment=$(eval echo $(eval echo \\\$\{$(echo {4..${#}})\}))
echo "[ip]${ip} [hostname]${hostn} [comment]${comment}"
done < hosts
範囲指定部分がトリッキーですが…範囲指定ではない部分はいたってシンプルに書けます。
もしもちゃんと最後の要素の番号を判定せず、なんとなくの範囲指定をするなら、
comment=$(eval echo \$\{{4..99}\})
と書いてもOKです。
2017/10/15追記
@akinomyoga さんに教えて頂きました。感謝です。
$(eval echo $(eval echo \\\$\{$(echo {4..${#}})\}))
の部分ですが、なんと、
${*:4}
…たったこれだけで行けてしまいます…。
while read line
do
set ${line}
ip=${1}
hostn=${2}
comment=${*:4}
echo "[ip]${ip} [hostname]${hostn} [comment]${comment}"
done < hosts
まず、変数${*}は特殊パラメータであり、何を意味するかというと、全ての位置パラメータ($1とか$2とか)を意味します。
こいつに部分文字列展開${parameter:offset}を適用することで、位置パラメータの4番以降…という表現ができてしまうんですね。
素晴らしいです。
readの場合
@magicant さんに教えて頂きました。感謝です。
while read ip hostname n comment
do
echo "[ip]${ip} [hostname]${hostn} [comment]${comment}"
done < hosts
いらない部分を捨てる必要がありますが、捨てる部分がないか、少なければ、
ものすごくシンプルに書けますね。
実行速度比較
もはやタイトルでネタバレですが、上記のwhileを以下な感じで500回ほど回して、実行時間を計測しました。
time for i in {0..500}; do ./case_of_cut.sh >/dev/null; done
結果は以下です。
command | time(s) |
---|---|
cut | 23.692 |
awk | 28.268 |
set(tricky) | 13.493 |
set(4..99) | 8.421 |
read | 3.207 |
setがダブルスコアで圧勝です。
追記:readが更に早かったです!
setの仕組み
$1,$2等はPositional Parametersと言って、通常はシェル起動時に引数が代入されます。
代入分による代入はできませんが、setコマンドを使うことにより代入しなおすことが可能です。
この時、IFSで区切られてPositional Parametersに格納されるので、cutやawkの代替として使えるわけです。
また、setはシェルの組み込みコマンドのため、パフォーマンス的にも有利、かも。
尚、Positional Parametersが10以上の数字になる場合は、ブレース(これ→{})で括る必要があります。
結局どれを使うか
-
cut
誰が見ても分かりやすいけど遅い -
awk
少々トリッキーな上、遅い -
set
途中途中に捨てる部分が多い文字列であれば有用 -
read
文字列全部分解して使う際は勿論、途中で捨てるものがある場合でもOK
使いようによってはsetの真似事も可能
結論:setをpushしましたが、readが万能すぎました
おまけ setの範囲指定部分の簡単な解説
comment=$(eval echo $(eval echo \\\$\{$(echo {4..${#}})\}))
まず、$(echo {4..${#}})
で${#}を展開、{4..9}
みたいなのが取れます。これがブレース展開のネタ。
次に、evalで変数を評価してから表示します。{4..9}のブレース展開が行われます。
$は次のevalの評価で使うため、エスケープする必要がありますので、\\\$
という書き方になっています。
更に、10以上の数字になることを考慮して、ブレース展開される外側をブレースで括ります。
この時、外側ブレースはエスケープしないと評価されてしまうので、エスケープします。
ここまでで${4} ${5} ${6} ${7} ${8} ${9}
が取れます。
最後に、それら変数を再度evalで評価すると、$4とかの中身がようやく取れます。
これ数か月後に自分が読んで理解できるかどうか…。