VS Code のユーザースニペット作成をほぼ自動化するシェルスクリプトを書いた

[コードはここに置いてます](https://github.com/yousukeayada/general/blob/master/shell_scripts/make_snippet.sh)

## はじめに
最近VSCodeのユーザースニペットを使い始めてすごく便利だなーと思ったのですが、スニペット自体の作成がけっこう面倒で、どうにか楽をしようとシェルスクリプトを書いてみました。決して完璧なものではないので、ある程度省力化できる、くらいに考えてもらえればと。


## スニペット作成の面倒な点
- １行ごとに`""`で囲む必要がある。
- 行末に`,`
- タブ文字の追加
- 各種記号のエスケープ

個人的に以上のような作業が面倒なので、自動化したいです。


## スクリプトの流れ
以下のようなフローを想定しています。
1. スニペットにしたいコードを書いたファイルを作る
2. スクリプト実行時の引数にファイル名を指定すると、スニペットにコピペできる形でその中身が標準出力に表示される
3. スニペット登録用ファイル（json）の`body`にコピペする

### シェルスクリプト 
```bash:make_snippet.sh
#!/bin/bash

set -euo pipefail

# このスクリプト自身のディレクトリに移動する
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

SNIPPET=$1

while read line; do
    echo \"$line\",
done < <(sed -e 's/\"/\\\\\"/g' \
            -e 's/\(\\[0-9a-z]\)/\\\\\\\1/g' \
            -e 's/    /\\\\t/g' \
            -e 's/\$/\\\\\\\\$/g' \
            $SNIPPET)
```

`sed`のところだけ説明しておきます

```bash
's/\"/\\\\\"/g'
```
- `printf`などで使う`""`のエスケープ

```bash
's/\(\\[0-9a-z]\)/\\\\\\\1/g'
```
- `\n`など元々エスケープされていたものを`echo`すると`\`が消えてしまうので、無理やり増やす

```bash
's/    /\\\\t/g'
```
- 空白4文字をタブ文字に置換
- **インデント時の空白の数によっては適用されません**

```bash
's/\$/\\\\\\\\$/g'
```
- ユーザースニペットでは`$`が意味を持つのでエスケープする

## 試してみる
まずこちらのコードをスニペットにしてみます

```c:hello.c
#include <stdio.h>

int main() {
    printf("hello\n");

    return 0;
}
```

実行

```bash
$ ./make_snippet.sh hello.c 
"#include <stdio.h>",
"",
"int main() {",
"\tprintf(\"hello\\n\");",
"",
"\treturn 0;",
"}",
```
うまくいっているのがわかるかと思います。

次は[こちら](https://sousaku-memo.net/php-system/1817)を参考にして作ったシェルスクリプトのテンプレートをスニペットにしてみます。

```bash:template.sh
#!/bin/bash

set -euo pipefail

function usage() {
    echo "Usage:"
    echo "  $(basename "${BASH_SOURCE[0]}") [options] [--] arg1 arg2"
    echo
    echo "Options:"
    echo "  --help, -h"
    echo "    This help."
}

# 色付きの echo
function cecho() {
    local color_name color
    readonly color_name="$1"
    shift
    case $color_name in
        red) color=31 ;;
        green) color=32 ;;
        yellow) color=33 ;;
        blue) color=34 ;;
        cyan) color=36 ;;
        *) error_exit "An undefined color was specified." ;;
    esac
    printf "\033[${color}m%b\033[m\n" "$*"
}

# 引数を入れる配列を定義する
declare -a ARGS=()

# コマンドラインオプションをパースする
while (($# > 0)); do
    case "$1" in
        --help | -h)
            usage
            exit 0
            ;;
        --)
            # ダブルダッシュがあったら以降は引数とみなす
            shift
            while (($# > 0)); do
            ARGS+=("$1")
            shift
            done
            break
            ;;
        -*)
            cecho red "[ERROR] Illegal option: $1"
            exit 1
            ;;
        *)
            if [[ $1 != "" ]] && [[ ! $1 =~ ^-+ ]]; then
            ARGS+=("$1")
            fi
            shift
            ;;
    esac
done

# このスクリプト自身のディレクトリに移動する
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# スクリプト終了時に呼ばれるハンドラー関数
function on_exit() {
    local exit_code=$1
    # スクリプト終了時の後始末を行う場合はここに書く
    exit "$exit_code"
}

function main() {
    # スクリプト終了時のハンドラーを登録
    trap 'on_exit $?' EXIT

    cecho cyan "hello"
}

main
```

実行

```bash
$ ./make_snippet.sh template_large.sh 
"#!/bin/bash",
"",
"set -euo pipefail",
"",
"function usage() {",
"\techo \"Usage:\"",
"\techo \" \\$(basename \"\\${BASH_SOURCE[0]}\") [options] [--] arg1 arg2\"",
"\techo",
"\techo \"Options:\"",
"\techo \" --help, -h\"",
"\techo \"\tThis help.\"",
"}",
"",
"# 色付きの echo",
"function cecho() {",
"\tlocal color_name color",
"\treadonly color_name=\"\\$1\"",
"\tshift",
"\tcase \\$color_name in",
"\t\tred) color=31 ;;",
"\t\tgreen) color=32 ;;",
"\t\tyellow) color=33 ;;",
"\t\tblue) color=34 ;;",
"\t\tcyan) color=36 ;;",
"\t\t*) error_exit \"An undefined color was specified.\" ;;",
"\tesac",
"\tprintf \"\\033[\\${color}m%b\\033[m\\n\" \"\\$*\"",
"}",
"",
"# 引数を入れる配列を定義する",
"declare -a ARGS=()",
"",
"# コマンドラインオプションをパースする",
"while ((\\$# > 0)); do",
"\tcase \"\\$1\" in",
"\t\t--help | -h)",
"\t\t\tusage",
"\t\t\texit 0",
"\t\t\t;;",
"\t\t--)",
"\t\t\t# ダブルダッシュがあったら以降は引数とみなす",
"\t\t\tshift",
"\t\t\twhile ((\\$# > 0)); do",
"\t\t\tARGS+=(\"\\$1\")",
"\t\t\tshift",
"\t\t\tdone",
"\t\t\tbreak",
"\t\t\t;;",
"\t\t-*)",
"\t\t\tcecho red \"[ERROR] Illegal option: \\$1\"",
"\t\t\texit 1",
"\t\t\t;;",
"\t\t*)",
"\t\t\tif [[ \\$1 != \"\" ]] && [[ ! \\$1 =~ ^-+ ]]; then",
"\t\t\tARGS+=(\"\\$1\")",
"\t\t\tfi",
"\t\t\tshift",
"\t\t\t;;",
"\tesac",
"done",
"",
"# このスクリプト自身のディレクトリに移動する",
"readonly SCRIPT_DIR=\"\\$(cd \"\\$(dirname \"\\${BASH_SOURCE[0]}\")\" && pwd)\"",
"cd \"\\$SCRIPT_DIR\"",
"",
"# スクリプト終了時に呼ばれるハンドラー関数",
"function on_exit() {",
"\tlocal exit_code=\\$1",
"\t# スクリプト終了時の後始末を行う場合はここに書く",
"\texit \"\\$exit_code\"",
"}",
"",
"function main() {",
"\t# スクリプト終了時のハンドラーを登録",
"\ttrap 'on_exit \\$?' EXIT",
"",
"\tcecho cyan \"hello\"",
"}",
"",
"main",
```

こちらもエスケープ処理などしっかりできているのが確認できます。あとはコピペするだけですね。


## おわりに
今回はC/C++やシェルスクリプトの簡単なものぐらいしか試していないので、例外はたくさんあるでしょうが、けっこう楽できるようになったんじゃないかなと思います。シェルスクリプトは初心者なので、改善点などあればご指摘いただけると幸いです。

以上です。