Linuxスペースを含む文字列の置換

シェルスクリプトで**文字列を書き換えて，足りない分はスペースで埋める**コードを書いたので，調べた内容やつまずいたところなどを備忘録として残しておく．

#課題
##背景
C言語の研修中の話．ソースコードを書くときに，ヘッダコメントを毎回書き換える必要があった．例えば

```training1-1.c
/*###################################################*/
/*#           File      :  training1-1.c            #*/
/*#           Date      :  2019/10/22               #*/
/*###################################################*/

int main(void){
    printf("Hello World\n");
    return 0;
}
```

みたいな，かんじ．
特に，研修中なんて「training1-1.c」のようにファイル名がほとんど同じなので，ファイル作成→ファイル名変更を繰り返していると**毎回打ち直すのが面倒**くさくなってくる．


##実現したいこと
1. **自動でヘッダコメントを編集し，コメントアウトのスペースの数もキレイに揃える**
2. **vimでソースコードを書いて保存したら，そのまま実行結果を出力してくれる**

Linux環境でファイルの作成からコンパイルまでを行っていたので，ちょうど勉強中のシェルスクリプトの知識を使って作ってみた．


#成果物
今回作った**ヘッダコメントの編集，コンパイル，実行をしてくれるシェルスクリプト（zikko.sh）**は，基になるテンプレート（template.c）と合わせて使うことになる．
実行方法とzikko.sh，template.cの中身は以下の通り．また，<ファイル名>は拡張子の前まででOK．

``` .sh
#実行方法
$ ./zikko.sh template <ファイル名>
```

```zikko.sh
#!/bin/bash

template=$1
newFile=$2
date=$(date "+%Y/%m%d")
convertSymbolFile=$"FILE_NAME"
convertSymbolDate=$"YYYY/MM/DD"

#ファイルが存在しない場合は，ヘッダファイルを編集
if [ ! -e $newFile.c ]; then
    #スペースを調整してファイルのタイトルを変更
    if [ ${#newFile} -le ${#convertSymbolFile} ]; then
        diffOfStr=$((${#convertSymbolFile}-${#newFile}))
        numOfSpace=$(seq -s" " $((diffOfNum+1)) | tr -d '[:digit:]')
        newFileTitle=$newFile$numOfSpace
        sed -e "s:$convertSymbolFile:$newFileTitle:g" $template.c > $newFile.c
    #else
    #変換後文字列の長さ > FILE_NAME の場合の処理も書こうとしたが，時間の都合上断念
    #convertSymbolFileの文字列を長くすれば問題ない気がする
    fi
    #日付を変更
    sed -i -e "s:$convertSymbolDate:$date:g" $newFile.c
fi

#main関数が書かれている行から編集開始
mainFuncLow=$(grep SOURCE_CODE -n $newFile.c | cut -d ":" -f 1)
vim -c $mainFuncLow $newFile.c

#コンパイルと実行
gcc -o $newFile.x $newFile.c
./$newFile.x

```
```template.c
/*###################################################*/
/*#           File      :  FILE_NAME                #*/
/*#           Date      :  YYYY/MM/DD               #*/
/*###################################################*/

int main(void){
    //SOURCE_CODE
    return 0;
}
```

テンプレートに変換前の文字列を追記して，zikko.shのファイルのタイトルを変える処理のところを少し弄れば，単純なヘッダコメントの追加には対応できるはず...
単純そうな作業の割には，コードが長い気もする．もっといい方法あったら教えてください．


#つまずいたポイント
##スペースの繰り返し
「Linux 文字列置換　繰り返し」とかで検索かけると，記号の繰り返しに関する記事はけっこうヒットするが，スペースを繰り返し挿入する記事はあまり見つけられなかった．見つけた中で`seq`コマンドの結果をパイプラインで`tr`コマンドに送る方法[^1]を用いた方法が一番簡単そうだった．

```つまずいたポイント1.sh
#diffOfNum=4

$ seq -s" " $((diffOfNum+1)) | tr -d '[:digit:]'
#->**** ※スペースの代わりに*で表示
```

- printコマンドの引数でNUM（任意の値）を与えても，画面には(NUM-1)回しか表示されないので注意が必要．**NUM回表示したいならば，引数に(NUM+1)を与える必要がある**．
- **スペースを繰り返し表示したい場合は，-sオプションの後を " " にする**必要がある．記号の場合は，-sオプションの後を繰り返し表示したい記号を入れればOK．


##文字列の結合
シェルスクリプトで文字列を結合するには，シェル変数を連続して書けばいいらしい[^2]．

```つまずいたポイント2.sh
#newFile=file1 ※<ファイル名>
#numOfSpace=****

$ newFileTitle=$newFile$numOfSpace
#->file1****
```

##sedコマンドの使い方
文字列の置換のために`sed`コマンドよく使ったので，使い方や躓いたポイントを簡単にメモしておく．まず，実行方法は以下の通り．

```
$ sed [オプション] <スクリプト> <ファイル名>
```
<スクリプト>には，アドレスとコマンドを組み合わせた文字列が入る．よく使われるコマンドは，

|コマンド  |実行結果
|-------   |---------
|s         |文字列の置換
|d         |行の削除
|p         |行の表示
がある．今回は文字列の置換しかしていないので，`s`コマンドについてのみ書いておく．

###変数展開
sコマンドを''(シングルクオート)で囲んで文字列の置換を行うと，**sedコマンドの処理は通るがFILE_NAMEという文字列が置換されていない**という問題が生じた．そのときのコードは以下の通り．

```つまずいたポイント3.sh
#convertSymbolFile="FILE_NAME"
#newFileTitle=file1****
#template=template
#newFile=file1

$ sed -e 's/$convertSymbolFile/$newFileTitle/g' $template.c > $newFile.c
#->実行はされるがFILE_NAMEは置換されていない
```
調べてみると，**置換できていない原因は，変数展開に失敗していた**ことが原因だった．この問題を**解決するには，変数の部分だけ引用符を解除する**必要があるらしい．[^3]
そんなわけで，変数展開の方法を修正したコードがこちら．

```つまずいたポイント3-2.sh
$ sed -e 's/'$convertSymbolFile'/'$newFileTitle'/g' $template.c > $newFile.c
#->sed: -e expression #1, char 16: sコマンドが終了していません
```
コードを修正したら，今度は別のエラーが出てしまった...


###スペースを含む文字列の置換
**エラー：「sコマンドが終了していません（unterminated `s' command）」が発生した原因は，"（ダブルクオート）と'（シングルクオート）で囲んだときのエスケープ処理の有無の違い**の模様．**'（シングルクオート）はbashによる展開等を行わずsedコマンドに文字列をそのまま渡してあげるイメージ**らしい[^4]．
ダブルクオートで囲って変数展開させるように修正[^5]したコードは以下の通り．

```つまずいたポイント3-3.sh
$ sed -e "s/$convertSymbolFile/$newFileTitle/g" $template.c > $newFile.c
#->FILE_NAMEの置換が上手く行われた
```

一応，fileの文字列からスペースを除いた状態で「つまずいたポイント3-2.sh」を実行してみたら問題なく実行できた．


###日付の置換
コメントヘッダの作成日時を自動で編集するために，**sedコマンドを使って日付の文字列の置換をしようとしたら，エラー：「sコマンドが未知です（unknown option to `s' command）」**が起きてしまった．

```つまずいたポイント3-4.sh
#newFile=file1
#date=2019/10/22
#convertSymbolDate=$"YYYY/MM/DD"

$ sed -i -e "s/$convertSymbolDate/$date/g" $newFile.c
#->sed: -e expression #1, char 34: unknown option to `s'
```

sedコマンドは、**sの次に書いた文字が自動的に区切り文字として認識れるのが原因**だった[^6]．区切り文字に/を使っていたため，`s/YYYY/MM/DD/2019/10/22/g`となってしまい，エラーを吐いたのだと考えられる．
区切り文字を修正したコードは，以下のようになる．

```つまずいたポイント3-5.sh
$ sed -i -e "s:$convertSymbolDate:$date:g" $newFile.c
#->YYYY/MM/DDの置換が上手く行われた
```


##検索文字の行番号だけを取得
vimを起動したときに自動的にmain関数のところにカーソルをもってくるために行番号をvimの-cオプションに渡したかったが，**grepコマンドだけでは出力が「行番号 : <検索対象の文字列>」になってしまう**という問題があった．
これは，**grepコマンドの標準出力をパイプラインでcutコマンドに渡すことで解決**できた[^7]．

```つまずいたポイント4.sh
#newFile=file1

$ grep SOURCE_CODE -n $newFile.c | cut -d ":" -f 1
#->7
```


#追記
これを使えばもっと簡単にできたかも？
参考[^8]


[^1]: [Linuxコンソール上で同じ文字列を繰り返して出力させる](https://orebibou.com/2016/06/linux%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E4%B8%8A%E3%81%A7%E5%90%8C%E3%81%98%E6%96%87%E5%AD%97%E5%88%97%E3%82%92%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97%E3%81%A6%E5%87%BA%E5%8A%9B%E3%81%95/)
[^2]: [シェルスクリプトで文字列と変数を連結する](https://hacknote.jp/archives/20987/)
[^3]: [sedコマンドの条件式に変数を使う場合に注意すること](https://hacknote.jp/archives/32540/)
[^4]: [あえていうほどでもないsed入門](https://www.techscore.com/blog/2016/03/14/%E3%81%82%E3%81%88%E3%81%A6%E3%81%84%E3%81%86%E3%81%BB%E3%81%A9%E3%81%A7%E3%82%82%E3%81%AA%E3%81%84sed%E5%85%A5%E9%96%80/)
[^5]: [sedの空白を含む変数の置換方法](https://teratail.com/questions/126701)
[^6]: [sedで置換がうまくいかなかった話](https://qiita.com/sublimer/items/b1f58ca60e69233d3602)
[^7]: [grepで検索文字の行番号を取得](https://ex1.m-yabe.com/archives/1681)
[^8]: [連続したスペースをシェルで扱うときはprintfを使う](https://www.greptips.com/posts/1115/)