CSVでカンマセパレータだけを置換する(フィールド内カンマはスルー)・・・SED版

この記事でわかること。
・sedを使ってカンマセパレータを別文字に置換する方法

CSVでカンマセパレータだけを置換する(フィールド内カンマはスルー)」の記事で紹介した正規表現は、sedでは使えない。 sedは先読みとか後読みに対応していないためだ。 別の方法でCSVのカンマセパレータの置換方法を検討する。 ロジックを作り上げるプロセスを載せて備忘録とし、今後の応用に活かしたい。

まずはテストデータと期待結果を再掲載する。
 テストデータ:a,"b,c",d,"e,f,g",h
 期待する結果:a@"b,c"@d@"e,f,g"@h

問題を単純化するために、一旦テストデータをシンプルなものに変更する。
 テストデータ:a,"b,c",h
 期待する結果:a@"b,c"@h

最初に、左端のカンマを置換するsedを考えて、以下のように試してみた。

1$ echo -e 'a,"b,c",h' | sed -r 's/^.*,/@/'
2@h

先頭から h の直前のカンマまで置換されてしまった(失敗)。
メタキャラクタを使って、置換してはいけない部分の出力を試みる。

1$ echo -e 'a,"b,c",h' | sed -r 's/^(.*),/\1@/'
2a,"b,c"@h

よくなったが、左端のカンマが置換されていない(失敗)。
最短マッチさせるため、? を使ってみる。

1$ echo -e 'a,"b,c",h' | sed -r 's/^(.*)?,/\1@/'
2a,"b,c"@h

出力結果は変わらない(失敗)。
sedは最短マッチに ? を使えないことを思い出した。
否定の文字クラスを使って最短マッチさせる。

1$ echo -e 'a,"b,c",h' | sed -r 's/^([^,]*),/\1@/'
2a@"b,c",h

左端のカンマを置換できた(OK)。
次はダブルクォートで括られた中のカンマはスルーして、セパレータである h の左隣りのカンマの置換を目指す。
ここで、テストデータを上記出力結果 a@"b,c",h とすることで問題を簡単にする。以下を試してみた。

1$ echo -e 'a@"b,c",h' | sed -r 's/^(([^,"]*)("[^"]*")?),/\1@/'
2a@"b,c"@h

うまくいった(OK)。さらに冗長と思われる()を削除して試してみる。

1$ echo -e 'a@"b,c",h' | sed -r 's/^([^,"]*("[^"]*")?),/\1@/'
2a@"b,c"@h

同じ結果が得られたので、やはり冗長であった。
それではテストデータを元の a,"b,c",h に戻して、上記正規表現で試してみる。

1$ echo -e 'a,"b,c",h' | sed -r 's/^([^,"]*("[^"]*")?),/\1@/'
2a@"b,c",h

右端のカンマが置換されていないが、実は想定通り。 目論んでいるのは、このアウトプットに対し繰り返し同じパターンで置換を 実行することだ。 そうすれば上で試したように a@"b,c",ha@"b,c"@h と置換されるはずだ。

繰り返し置換するには、tコマンドを使ってスクリプトの先頭に制御をジャンプさせる。以下を試してみた。

1$ echo -e 'a,"b,c",h' | sed -e ':a' -re 's/^([^,"]*("[^"]*")?),/\1@/' -e 'ta'
2a@"b,c"@h

うまくいった(OK)。さて、それでは本来のテストデータで試してみよう。

1$ echo -e 'a,"b,c",d,"e,f,g",h' | sed -e ':a' -re 's/^([^,"]*("[^"]*")?),/\1@/' -e 'ta'
2a@"b,c"@d,"e,f,g",h

dの左側までしか置換されていない(失敗)。パターンの繰り返しが漏れていたので追加して試してみた。

1$ echo -e 'a,"b,c",d,"e,f,g",h' | sed -e ':a' -re 's/^(([^,"]*("[^"]*")?)*),/\1@/' -e 'ta'
2a@"b,c"@d@"e,f,g"@h

OK。これで完全に期待通りの結果が得られた。

上記のように繰り返し制御を使用すれば、先読み・後読みができなくても対応可能性が高まる。

追記)QAサイトで以下の質問が投稿された。

カンマが空白に置き換わっただけの類似問題。 以下のような sed と awk の合わせ技で可能。
sed -E '{:a;s/^(([^ "]*("[^"]*")?)*) /\1@/;ta}' | awk -F@ '{print NF}'

awk だけでも可能。正規表現を知っていれば応用が効く。
awk '{ while(sub(r,"@"))i++ print i+1 }' r='^(([^ "]*("[^"]*")?)*) '

関連ページ