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

# はじめに

普段は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越しに[某サーバー](http://173210.github.io/proot)のCentOS7のBashで動かしたところ3秒くらいで終わりました。

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

# 具体的に重かった部分

https://github.com/YSRKEN/KanColleSimulator_KAI/blob/e1a97d29432a845e4834c5ff0dacd2d6fc94d5ad/csv_convert.sh

```bash
#!/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ではあまり見ない<sup>[[要出典](http://ansaikuropedia.org/wiki/%E8%A6%81%E5%87%BA%E5%85%B8)]</sup>二重ループの中にある

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

の部分です。

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

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

**やっぱり文字列置換といえば``sed``コマンド**ですよね？<sup>[[要出典](https://ja.wikipedia.org/wiki/Template:%E8%A6%81%E5%87%BA%E5%85%B8)]</sup>ShellSciptなんかめったに書かないWindowsユーザーの私でも知ってるんだから間違いない。<sup>[[独自研究](https://ja.wikipedia.org/wiki/Wikipedia:%E7%8B%AC%E8%87%AA%E7%A0%94%E7%A9%B6%E3%81%AF%E8%BC%89%E3%81%9B%E3%81%AA%E3%81%84)]</sup>

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

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

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

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

> # bashで変数内文字列の一部を置換する
>
> こんなやり方もあった。
>
> ```bash
> $ 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、「*」は任意の文字列扱いになります。「任意の回数の繰り返し」は表現できません。
>
> | 表現                       | 説明                                                                       |
> |----------------------------|----------------------------------------------------------------------------|
> | ${変数名#パターン}         | 変数の先頭がパターンマッチした場合、最短マッチ部分を削除した文字列を返す。 |
> | ${変数名##パターン}        | 変数の先頭がパターンマッチした場合、最長マッチ部分を削除した文字列を返す。 |
> | ${変数名%パターン}         | 変数の末尾がパターンマッチした場合、最短マッチ部分を削除した文字列を返す。 |
> | ${変数名%%パターン}        | 変数の末尾がパターンマッチした場合、最長マッチ部分を削除した文字列を返す。 |
> | **${変数名/パターン/文字列}**  | **最初にパターンマッチした部分を文字列で置換した文字列を返す。**               |
> | **${変数名//パターン/文字列}** | **パターンマッチしたすべての部分を文字列で置換した文字列を返す。**             |


これだ！

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


つまり


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

が

https://github.com/YSRKEN/KanColleSimulator_KAI/blob/14d402898172d8dc2742d88849794541c540a9e6/csv_convert.sh

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

こう書けるんですね。

# 結果

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

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

# 結論

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

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

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/yumetodo">@yumetodo</a> Win32/Win64 Subsystem は fork でない普通の CreateProcess の時点で既に遅いから 、fork が実装されたとしても factor 変わるだけで問題は解決しない気がする</p>&mdash; akinomyoga (@akinomyoga) <a href="https://twitter.com/akinomyoga/status/820312582854586368">2017年1月14日</a></blockquote>
<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/yumetodo">@yumetodo</a> 隠し API ZwCreateProcess で fork ができるという話もある (詳しくは知らないけれど)。 <a href="https://t.co/WcSCY57K8q">https://t.co/WcSCY57K8q</a></p>&mdash; akinomyoga (@akinomyoga) <a href="https://twitter.com/akinomyoga/status/820313320641024000">2017年1月14日</a></blockquote>
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/yumetodo">@yumetodo</a> 間違えた。パイプしない場合を聞きたかった。例えばこんな比較。 <a href="https://t.co/wQ5l4iuo9R">pic.twitter.com/wQ5l4iuo9R</a></p>&mdash; 173210.go (@173210) <a href="https://twitter.com/173210/status/820532462790930433">2017年1月15日</a></blockquote>
<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="und" dir="ltr"><a href="https://twitter.com/173210">@173210</a> <a href="https://t.co/tafOYztIo5">pic.twitter.com/tafOYztIo5</a></p>&mdash; yumetodo-C++erだけど化学科 (@yumetodo) <a href="https://twitter.com/yumetodo/status/820534965959999488">2017年1月15日</a></blockquote>
<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/yumetodo">@yumetodo</a> 決まりだな。MSYS環境でのパイプはクソ遅い。</p>&mdash; 173210.go (@173210) <a href="https://twitter.com/173210/status/820547844822089728">2017年1月15日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# License

[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.ja)

[![CC-BY アイコン](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg)](https://creativecommons.org/licenses/by/4.0/deed.ja)