Edited at

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越しに某サーバーのCentOS7のBashで動かしたところ3秒くらいで終わりました。

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


具体的に重かった部分

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

#!/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')


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


after

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


こう書けるんですね。


結果

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

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


結論

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

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


License

CC BY 4.0

CC-BY icon.svg