Git エイリアスをUnix風のコマンドで設定する（alias.alias）

## はじめに

### Git エイリアスの紹介

Gitをコマンドラインで操作するときにしばしば感じる、タイプ数の多さ。 
別に大して長くはないコマンドであっても、可能な限りタイプ数を減らしたくなりませんか？

そんなときに活躍するのが[**Git エイリアス**](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E5%9F%BA%E6%9C%AC-Git-%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9)。 
定番の `co` だったらこんな風に設定できます。

```console
$ git config --global alias.co checkout
```

こうすると、例えば `git checkout feature/xxx` が `git co feature/xxx` と書けるようになる訳です。

### Git エイリアスの問題点

Gitの長いコマンドを短くできるGit エイリアス。 
Gitを駆使する上で多大な恩恵をもたらしてくれる訳ですが、一つ問題点に気付いてしまいました。

それは……

**コマンド自体が、微妙に、長くない？**

例えば、Unix系の `alias` コマンドは、

```console
$ alias ls='ls --color=auto'
```

というように使います。

両者を比較すると、こんな感じ。

```console
# Git エイリアス
$ git config --global alias.<key> <value>

# Unix系のaliasコマンド
$ alias <key>=<value>
```

**Git エイリアスの圧倒的長さ！！！**

もちろん、`alias` はGitとは全く関係ありません。 
ただ、そうは言っても記述量は少ないに越したことはないはず！

要するに、**コマンドを短くするためにエイリアスを設定したいのに、そのためには長いコマンドを打たなければならない**という訳です。

このジレンマを解消するために、Git エイリアスを `alias` っぽく使えるエイリアス `alias.alias` を作ってしまいましょう。 
（ “alias” がゲシュタルト崩壊しそう笑）

## alias.alias

前置きが長くなりましたが、後は一瞬。 
以下のコマンド（長い！）をコピペして、エンターキーを押すだけで完了です。

```console
$ git config --global alias.alias '!sh -c '\''[ $# -eq 0 ] && { git config --global --list | grep -E "^alias\." | grep -Ev "^alias\.alias=" | sed -E -e "s/ +/ /g" -e "s/\./ /"; exit 0; }; for a in "$@"; do [[ $a = *=* ]] && { git config --global "alias.${a%%=*}" "${a#*=}"; continue; }; s=`git config --global "alias.$a"`; [[ $s ]] && echo alias $a=$s; done'\'' --'
```

### 使い方

```
$ git alias [<key>[=<value>] ...]
```

原則、Unix系の `alias` と同じように記述できます。

#### 使用例

##### エイリアスの一覧表示

```console
$ git alias
alias co=checkout
alias br=branch
alias ci=commit
alias st=status
```

##### エイリアスの詳細表示

```console
$ git alias co br
alias co=checkout
alias br=branch
```

##### エイリアスの設定

```console
$ git alias co=checkout
$ git co feature/xxx
Switched to branch 'feature/xxx'
```

### 補足

`~/.gitconfig`（または `~/.config/git/config`）を開いて、以下のコードを直接入力しても大丈夫です。 
ちなみに、`git config --global --edit` というコマンドで、勝手にエディターが起動して開いてくれます。

```shell
[alias]
    alias = "!sh -c '[ $# -eq 0 ] && { git config --global --list | grep -E \"^alias\\.\" | grep -Ev \"^alias\\.alias=\" | sed -E -e \"s/ +/ /g\" -e \"s/\\./ /\"; exit 0; }; \
                     for a in \"$@\"; do [[ $a = *=* ]] && { git config --global \"alias.${a%%=*}\" \"${a#*=}\"; continue; }; \
                     s=`git config --global \"alias.$a\"`; [[ $s ]] && echo alias $a=$s; done' --"
```

### 解説

さて、やけに長いコマンドだと驚かれたかと思いますが、構造は非常に簡明です。
ややこしそうなシングルクォートの中身を省略して書くと、こんな感じ。

```shell
!sh -c '...' --
``` 

先頭の `!` は、Git エイリアスで外部コマンドの呼び出しを意味する「おまじない」です。 
これに続く `sh -c` では、`'...'` で与えられたシェルスクリプトをサブシェルとして実行しています。 
`--` は、以降の引数をサブシェルの引数と解釈するオプション**ではなく**、サブシェルに渡すダミーの第一引数です。

`sh -c` の仕様として、サブシェルに渡した引数は `$0` から順に代入されます。 
つまり、サブシェルに渡す引数がなければ `$0` は通例通りの `sh` 。 
ところが、例えば `abc` という文字列を渡したならば `$0` は `abc` となる訳です。 

更に厄介なのが、渡す引数の数と `$#` の数が**一致しません**。 
具体的には、

```console
$ sh -c 'echo $#: $0 $1 $2'
0: sh
$ sh -c 'echo $#: $0 $1 $2' a
0: a
$ sh -c 'echo $#: $0 $1 $2' a b
1: a b
$ sh -c 'echo $#: $0 $1 $2' a b c
2: a b c
```

と、引数を渡したときは第一引数が `$0` に代入される関係上、`$#` が「引数の数 - 1」となってしまうのです。

そこで、これら2点のややこしさを回避するために、予めダミーの引数 `--` を渡しておきます。 
これによって、ユーザーが与える引数は常に `$1` から始まり、`$#` の数もその数と一致します。 
なので、必ずしも `--` である必要はないです。

次はシングルクォートの中身についてですね。

```shell
[ $# -eq 0 ] && { git config --global --list | grep -E "^alias\.\" | grep -Ev "^alias\.alias=" | sed -E -e "s/ +/ /g" -e "s/\./ /"; exit 0; };
for a in "$@"; do [[ $a = *=* ]] && { git config --global "alias.${a%%=*}" "${a#*=}"; continue; };
s=`git config --global "alias.$a"`; [[ $s ]] && echo alias $a=$s; done
```

ふ、複雑……

**ではないです！**

要約すると、こんな構造になっています。

```shell
<引数なしなら、エイリアスの一覧表示>;
<ループ開始>
  <引数が<key>=<value>なら、エイリアスの設定>;
  <引数が<key>なので、エイリアスの詳細表示>;
<ループ終了>
```

ほら、シンプルですよね？

まずは一覧表示の部分から。

```shell
[ $# -eq 0 ] && { git config --global --list | grep -E "^alias\." | grep -Ev "^alias\.alias=" | sed -E -e "s/ +/ /g" -e "s/\./ /"; exit 0; }
```

Unix系の `alias` コマンドに準拠し、引数なしのときはエイリアスの一覧を表示するようにします。

`[ $# -eq 0 ] && ...` は、`test` コマンドを実行し、引数の数が `0` だったら正常終了して `&&` 以降に処理が移るという部分。 
`$1` が長さ0（空または空文字列）なら正常終了とする `[[ -z $1 ]]` の方が短くて良いかもしれないですが、`git alias ''` のように、明示的に空文字列が与えられた場合を弾けません。 
その回避策として、 `[[ -z ${1+.} ]]` のように `$1` が空のときにのみ空文字列を返す（空文字列または値ありのときは `.` を返します）ことも可能ですが、セマンティクス的にも合理的な上の通りにしました。

`{ ...; ...; }` の構文は、カレントシェルで一連のコマンドを実行するというもの。 
`&&` で処理を繫げると、エラー（異常終了）のときにそれ以降の処理がスキップされてしまい今回は不都合なので、`{}` を採用。

`git config --global --list` でグローバルの `.gitconfig` 内の全変数の一覧を取得し、`grep` でエイリアス（`alias.*`）だけをピックアップします。 
エイリアス以外の変数にも `alias.` が含まれていた場合を弾くため、正規表現（`-E`）を使いました。 
または `git config --global --get-regexp "alias\." | sed -E "s/ /=/"` でもOK。ただ、幾分長い。

後は好みの問題かもしれませんが、`grep -v` で `alias.alias` だけは表示しないように。 
また、`sed` の1つ目のパターンで `.gitconfig` ファイル内での改行・インデント等によって連続したホワイトスペースを空白1つ分に。 
更に`sed` の2つ目のパターンでは、`alias.*` の `.` を空白に。出力結果をUnix系の `alias` のものに寄せます。 
`echo` の出力や `grep` のパターンの一部等として、意図して連続スペースをコマンド内に書いている場合には、表示上ではそれが縮められてしまいますが、ファイル本体が変更される訳ではないので許容ということで。

ちなみに、`.bashrc` 等で予め `grep` に `--color` が指定されていたとしても、`grep -v` や `sed` を通ることでマッチ部分のハイライトが消えるので、Unix系の `alias` との見た目の違いは、コマンドがシングルクォートで囲まれていないことだけになります。

Perlの正規表現を用いて `grep -Px 'alias\.(?!alias).*'` とすると `grep` が1回だけで済みますが、`-P` に非対応の環境もあるので採用していません。

次に、エイリアスを設定する部分です。 
ここからは `for a in "$@"; do...done` を使い、各引数を `$a` に代入して処理を加えていきます。

```shell
[[ $a = *=* ]] && { git config --global "alias.${a%%=*}" "${a#*=}"; continue; }
```

`[[ $a = *=* ]]` で、`git alias` の 引数が `<key>=<value>` の形を取っていることを確かめ、従来通りのコマンドでGit エイリアスを設定します。

`${a%%=*}` では、後方一致の最長マッチを利用して最初の `=` 以降を削除し、`<key>` を取り出しています。 
反対に `${a#*=}` では、前方一致の最短マッチを利用して最初の `=` 以前を削除し、`<value>` を取り出しています。 
これらのテクニックについては、以下のサイトで丁寧にまとめられていますよ。

> shとbashでの変数内の文字列置換など - ろば電子が詰まつてゐる
> https://ozuma.hatenablog.jp/entry/20130928/1380380390

そして、詳細表示の部分で最後になります。

```shell
s=`git config --global "alias.$a"`; [[ $s ]] && echo alias $a=$s
```

Unix系の `alias` では、引数が `<key>` のときはそのエイリアスの詳細が表示されます。 
そこで、`git alias` の引数が `<key>=<value>` の形を取らなかったとき、引数は `<key>` だと考え、同じくエイリアスの詳細を出力するようにします。

`git config --global`（または `git config --global --get`）に変数名を与えると、その変数の**値**（`<value>` の部分）が取り出せることを利用します。 
``s=`...` `` の部分では、その出力結果をローカル変数 `$s` に代入しています。 
コマンドをバッククォート（`` ` ``）で囲むか、あるいは `$()` の括弧内に書くことで、出力が文字列になります。
存在しない変数名のときは空文字列です。 
ちなみに、この変換の時に連続した空白が1文字分に縮まるので、改めて `sed` で空白を削除する必要はありません。

`[[ $s ]]` は `[[ -n $s ]]` と等しいので、`$s` が長さ0でない、すなわち上のコマンドで出力があれば正常終了です。 
このとき `$s` には、今回知りたい変数の値が代入されている訳です。 
例えば `alias.co=checkout` について、`$s` は `checkout` となります。

そこで、 `echo` で `alias <key>=`（`co` のときは `alias co=`）と `$s` を繫ぐようにフォーマットを整えて出力すれば完成です。

## 最後に

長々と記事を書いてきましたが、伝えたいことはただ1つ。

`alias.alias` のおかげで、**長い**コマンドも**短い**エイリアスとして、それも**非常に短く**設定できるということです。

定番のテクニックですが、`alias g=git` としておけば、なんと `g alias <key>=<value>` と、一層短く書けますよ。

P.S. エイリアスにこだわらずとも、パスの通ったディレクトリにある `git-` から始まる実行ファイルが新しいサブコマンドになるというハックを利用し、`git-alias` を用意すれば良いことに気が付きました。また改めて記事にしたいと思います。

### 追記（2019.2.17）

`git-alias` を記事にしました！`git-unalias` も作ってます。 
☞ [Git エイリアスをUnix風のコマンドで設定する その２（git-alias, git-unalias）](https://qiita.com/Khios/items/751ebf02b5359842460c)

### 注意

Windows環境では、コード中に使用した `sh` や `grep` 等のUnix系固有のコマンドが使えませんので、[MSYS2](https://www.msys2.org)等を導入し、パスを通しておく必要があります。