LoginSignup
34
47

More than 5 years have passed since last update.

sedなんか投げ捨てて変数展開で高速に文字列を置換しよう

Last updated at Posted at 2017-01-14

はじめに

普段はC++の記事を投稿しています。
しかし
bashでもUTF-8 with BOMなファイルを作りたい - Qiita
http://qiita.com/yumetodo/items/38e17f6f5a8ef9860f5b
に続き2日連続投稿だ、おおっと・・・?

動機

bashでもUTF-8 with BOMなファイルを作りたい - Qiita
http://qiita.com/yumetodo/items/38e17f6f5a8ef9860f5b

の続き。どうにかそれらしいShellScriptを書いた。
しかし重い。それくらい重いかというと、実行になんとまさかの30分かかる。

私「いくらなんでも30分とか動画のエンコードじゃあるまいし待てるか!」

・・・あ、WindowsのMSYS2のBash上での話です。ssh越しに某サーバーのCentOS7のBashで動かしたところ3秒くらいで終わりました。

原因は、まあWindowsがfork()持ってないでいでしょ?ということにしておきます

具体的に重かった部分

#!/bin/bash
# @param input_file input file name
# @param prefix C-Preprocesser-Macro-Function name
# @param need_double_quote_index...
function convert_csv(){
  # argument
  local -r input_file=$1
  local -r prefix=$2
  shift 2
  local need_double_quote_index=($@)

  local -r need_double_quote_index_len=${#need_double_quote_index[@]}
  #write BOM
  echo -en '\xef\xbb\xbf'
  local line_string
  local is_first_line=1
  while read -r line_string; do
    if (( 1==is_first_line )); then
      is_first_line=0
      echo "//PREFIX,${line_string:0:-1},POSTFIX"
    else
        local IFS_BACKUP=$IFS
        IFS=','
        local elements=($line_string)
        IFS=$IFS_BACKUP
        local elements_len=${#elements[@]}
        local i=0
        local j
        echo -en "\rconverting ${input_file}... id ${elements[0]}" >&2
        # sleep 3s
        for (( j=0; j < elements_len; j++ )); do
          if (( i < need_double_quote_index_len && j == need_double_quote_index[i] )); then
            # ダブルクオートで囲う必要がある時
            elements[$j]="\"${elements[$j]}\""
            (( i++ ))
          else
            elements[$j]=$(echo "${elements[$j]}" | sed -e 's/\//./g')
          fi
        done
        echo "${prefix}(,$(IFS=,; echo "${elements[*]}"),)"
    fi
  done < <(iconv -f cp932 -t UTF-8 "${input_file}")
  echo -en "\rconverting ${input_file}...done.\n" >&2
}
echo "converting csv..."
convert_csv './ships.csv' 'SHIP' 1 > 'KCS_CUI/source/ships_test.csv'
convert_csv './slotitems.csv' 'WEAPON' 1 2 > 'KCS_CUI/source/slotitems_test.csv'
echo "done."

ShellScriptではあまり見ない[要出典]二重ループの中にある

            elements[$j]=$(echo "${elements[$j]}" | sed -e 's/\//./g')

の部分です。

外部コマンド呼び出しだし、$( )はサブシェル呼び出しだし、パイプ使っているし、こんなもんをfork()がないWindowsで実行すればそりゃ重いわな。

いやそうは言っても文字列置換といえば

やっぱり文字列置換といえばsedコマンドですよね?[要出典]ShellSciptなんかめったに書かないWindowsユーザーの私でも知ってるんだから間違いない。[独自研究]

それ、変数展開でできるよ

シェルスクリプト高速化のツボ - 新・日々録 by TRASH BOX@Eel
http://d.hatena.ne.jp/eel3/20141026/1414292281
どうしてもループ構文を使う場合は、ループ中で外部コマンドを使わず、内部コマンド(ビルトインコマンド)で実現できないか検討すること。

とのことなので、頑張って初心者なりに探したところ

bashで変数内文字列の一部を置換する - 元RX-7乗りの適当な日々
http://d.hatena.ne.jp/rx7/20100625/p2

bashで変数内文字列の一部を置換するAdd Star

こんなやり方もあった。

$ STR="I have a pen."
$ echo ${STR/pen/notebook}
I have a notebook.

ん?なんだそれ?

特殊な変数展開 - Miuran Business Systems
http://www.m-bsys.com/linux/variable_expansion

変数からの簡易文字列編集

パターンマッチングを使用した文字列編集も出来ます。ただ、一般的な正規表現が使えないため、私には使いづらいです…。単純な文字列パターンのときには有効です。「[0-9]」「[abc]」の等表現はOK、「*」は任意の文字列扱いになります。「任意の回数の繰り返し」は表現できません。

表現 説明
${変数名#パターン} 変数の先頭がパターンマッチした場合、最短マッチ部分を削除した文字列を返す。
${変数名##パターン} 変数の先頭がパターンマッチした場合、最長マッチ部分を削除した文字列を返す。
${変数名%パターン} 変数の末尾がパターンマッチした場合、最短マッチ部分を削除した文字列を返す。
${変数名%%パターン} 変数の末尾がパターンマッチした場合、最長マッチ部分を削除した文字列を返す。
${変数名/パターン/文字列} 最初にパターンマッチした部分を文字列で置換した文字列を返す。
${変数名//パターン/文字列} パターンマッチしたすべての部分を文字列で置換した文字列を返す。

これだ!

正規表現は使えませんが、今回の場合はそんな高等なものは必要ないのでこれで十分です。

つまり

before
            elements[$j]=$(echo "${elements[$j]}" | sed -e 's/\//./g')

after
            elements[$j]=${elements[$j]//\//.}

こう書けるんですね。

結果

実行時間が30分から1分に短縮した

私「それなら待てるわ。」

結論

猫も杓子もsed使うんじゃなくて、変数展開のことも思い出してあげてください。

それはそうとMSははようfork()実装しろ

License

CC BY 4.0

CC-BY icon.svg

34
47
2

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
34
47