gitコマンドのオプションは途中で省略してもOK【git commit --am --no-e】

# TL; DR

- `git` コマンドのオプションは先頭一致でマッチしている
  - **一意に特定できれば途中で切ってもok**

```bash
# よくあるオプション
$ git commit --amend --no-edit

# これでも可
$ git commit --am --no-e
```

# はじめに

開発も佳境を迎え筆が乗ってきた（~~キーボードだけど~~）。push前にこの修正もコミットに入れておこう。

```bash
$ git commit --amend --no-edi
```

おっと、 `--no-edit` の `t` が抜けてしまった。キーボードが俺に追いつけなかったようだ。

```bash
$ git commit --amend --no-edi
[dummy 9f0bed4] create a brand-new feature
 Date: Wed May 31 08:34:03 2023 +0900
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo
```

あれ、**普通に実行できてる？？**

# オプションはどこまで省略できるか

~~茶番はこのあたりにしておいて、~~ オプション引数がどの程度省略できるのか試してみます。

## git commit

まずは冒頭の `--amend`, `--no-edit` について。

```bash
$ git commit --amend --no-edit
[dummy 36d1015] create a brand-new feature
 Date: Wed May 31 08:34:03 2023 +0900
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo

# 一文字はok
$ git commit --amen --no-edi
[dummy 02a3c9a] create a brand-new feature
 Date: Wed May 31 08:34:03 2023 +0900
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo

# まだいける
$ git commit --am --no-e
[dummy ed5702d] create a brand-new feature
 Date: Wed May 31 08:34:03 2023 +0900
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 foo

 # ここでダメ
$ git commit --am --no-
error: ambiguous option: no- (could be --no-allow-empty or --no-allow-empty-message)

 # こっちもダメ
$ git commit --a --no-e
error: ambiguous option: a (could be --allow-empty or --allow-empty-message)
 ```

一意に特定できないとエラーになってしまうように見えます。

## git diff

`git diff` オプションで試してみるともっと分かりやすいです。こちらは一意に絞れなかった例。

```bash
$ git diff --no-colo
error: ambiguous option: no-colo (could be --no-color-moved or --no-color-moved-ws)
```

一方、一意であっても省略できないバターンもありました。

```bash
# --name-only
$ git diff --name-onl
error: invalid option: --name-onl
```

# 実装

どうやら部分一致でマッチしていそうですが、裏を取るために実装を読みます。

## git commit
`git commit` のオプションはここで定義されています。

```c:builtin/commit.c
int cmd_commit(int argc, const char **argv, const char *prefix)
{
	static struct wt_status s;
	static struct option builtin_commit_options[] = {
		// オプション一覧 (no-editが無い理由は後述)
        // ...
		OPT_BOOL(0, "amend", &amend, N_("amend previous commit")),
		OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")),
		{ OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
		// ...
		OPT_END()
	};
// ...
}
```

https://github.com/git/git/blob/79bdd48716a4c455bdc8ffd91d57a18d5cd55baa/builtin/commit.c#L1636

そして、コマンドライン引数のパーサーで定義されたオプションとマッチするものを探索します。
この際、完全一致しなくても、部分一致で一意に特定できればそのオプションを返しています。

```c:builtin/parse-options.c（関係ある部分だけ抜粋）
// 長いオプション(--hoge)のパーサー
static enum parse_opt_result parse_long_opt(
	struct parse_opt_ctx_t *p, const char *arg,
	const struct option *options)
{
	const struct option *all_opts = options;
	const char *arg_end = strchrnul(arg, '=');
	const struct option *abbrev_option = NULL, *ambiguous_option = NULL;
	enum opt_parsed abbrev_flags = OPT_LONG, ambiguous_flags = OPT_LONG;

    // オプション各候補についてマッチするか確認
	for (; options->type != OPTION_END; options++) {
		const char *rest, *long_name = options->long_name;
		enum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG;
again:
		if (!skip_prefix(arg, long_name, &rest))
			rest = NULL;
		if (!rest) {
			/* 引数が今見ているオプション候補のprefixと一致している？ */
			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
			    !strncmp(long_name, arg, arg_end - arg)) {
                // 「省略されたオプション」として今の候補を記録（※完全一致の場合もこの処理を通る）
				abbrev_option = options;
				abbrev_flags = flags ^ opt_flags;
				continue;
			}
        }
    }
    // 省略されたオプションとしてマッチしたものがあれば、それをオプションとして返す
	if (abbrev_option)
		return get_value(p, abbrev_option, all_opts, abbrev_flags);
	return PARSE_OPT_UNKNOWN;
}
```

https://github.com/git/git/blob/79bdd48716a4c455bdc8ffd91d57a18d5cd55baa/parse-options.c#L316

候補が一意に絞れなかった場合にはエラーになるようになっています。先ほどの検証で出てきた `ambiguous option` のエラーが定義されています。
また、`no-edit` のように `no-` で始まるオプションは、`no-` のprefixを取り除いてから再度パースしています。

```c:builtin/parse-options.c(先ほど省略した一意性チェックも記載)
static enum parse_opt_result parse_long_opt(
	struct parse_opt_ctx_t *p, const char *arg,
	const struct option *options)
{
	const struct option *all_opts = options;
	const char *arg_end = strchrnul(arg, '=');
	const struct option *abbrev_option = NULL, *ambiguous_option = NULL;
	enum opt_parsed abbrev_flags = OPT_LONG, ambiguous_flags = OPT_LONG;

    // オプション各候補についてマッチするか確認
	for (; options->type != OPTION_END; options++) {
		const char *rest, *long_name = options->long_name;
		enum opt_parsed flags = OPT_LONG, opt_flags = OPT_LONG;
again:
		if (!skip_prefix(arg, long_name, &rest))
			rest = NULL;
		if (!rest) {
			/* 引数が今見ているオプション候補のprefixと一致している？ */
			if (!(p->flags & PARSE_OPT_KEEP_UNKNOWN_OPT) &&
			    !strncmp(long_name, arg, arg_end - arg)) {
is_abbreviated:
                // すでに「省略されたオプション」の候補が存在する場合
				if (abbrev_option &&
				    !is_alias(p, abbrev_option, options)) {
					// 一意に定まらないため後でエラーを返す
					// 結果はあいまいオプションとして記録
					ambiguous_option = abbrev_option; 
					ambiguous_flags = abbrev_flags;
				}
				// 「省略されたオプション」として今の候補を記録（※完全一致の場合もこの処理を通る）
				abbrev_option = options;
				abbrev_flags = flags ^ opt_flags;
				continue;
			}
			// 否定で始まる場合、`no-` を取り除き結果を反転させる
			if (starts_with("no-", arg)) {
				flags |= OPT_UNSET;
                // `no-` 以降で改めて部分一致を試す
				goto is_abbreviated;
			}
		}
		if (*rest) {
			if (*rest != '=')
				continue;
			p->opt = rest + 1;
		}
		return get_value(p, options, all_opts, flags ^ opt_flags);
	}

    // 一意に定まらなかった場合はエラー
	if (ambiguous_option) {
		error(_("ambiguous option: %s "
			"(could be --%s%s or --%s%s)"),
			arg,
			(ambiguous_flags & OPT_UNSET) ?  "no-" : "",
			ambiguous_option->long_name,
			(abbrev_flags & OPT_UNSET) ?  "no-" : "",
			abbrev_option->long_name);
		return PARSE_OPT_HELP;
	}
	if (abbrev_option)
		return get_value(p, abbrev_option, all_opts, abbrev_flags);
	return PARSE_OPT_UNKNOWN;
}
```

# git diff
続いて部分一致不可だった `git diff` の実装を見ます。

別の関数でオプションのバリデーションチェックが行われているため、エラー扱いになっていました。

```c:builtin/diff.c
static int builtin_diff_files(struct rev_info *revs, int argc, const char **argv)
{
	unsigned int options = 0;

	while (1 < argc && argv[1][0] == '-') {
		if (!strcmp(argv[1], "--base"))
			revs->max_count = 1;
		else if (!strcmp(argv[1], "--ours"))
			revs->max_count = 2;
		else if (!strcmp(argv[1], "--theirs"))
			revs->max_count = 3;
		else if (!strcmp(argv[1], "-q"))
			options |= DIFF_SILENT_ON_REMOVED;
		else if (!strcmp(argv[1], "-h"))
			usage(builtin_diff_usage);
		else
            // 上記以外は一律エラー
			return error(_("invalid option: %s"), argv[1]);
		argv++; argc--;
	}
    // ...
}
```

https://github.com/git/git/blob/79bdd48716a4c455bdc8ffd91d57a18d5cd55baa/builtin/diff.c#L585

https://github.com/git/git/blob/79bdd48716a4c455bdc8ffd91d57a18d5cd55baa/builtin/diff.c#L272

省略オプションが使えるか使えないかは各サブコマンドの実装によるようです。~~私はここで力尽きてしまったので、気になる方は実装を読んでみてください。~~

# おわりに

以上、Gitのコマンドオプションが省略できるという紹介でした。急いでいるときにはどこまで削れるかチャレンジしてみてはいかがでしょうか？