LoginSignup
172
152

More than 5 years have passed since last update.

bashで文字列分解する時、cutやawkもいいけど、setの方が早い、けどreadが最強

Last updated at Posted at 2017-03-09

スペースやタブで区切られた文字列から、特定の箇所を抽出する、
といえば、真っ先にcutやawkが浮かびますが、実はsetでも似たようなことができます。

お題

例えば、こんなhostsがあったとします。空白はすべてスペースであるとします。

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の場合

case_of_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の場合

case_of_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の場合

case_of_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}…たったこれだけで行けてしまいます…。

case_of_set_2
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 さんに教えて頂きました。感謝です。

case_of_read
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とかの中身がようやく取れます。

これ数か月後に自分が読んで理解できるかどうか…。

172
152
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
172
152