TL; DR
-
git
コマンドのオプションは先頭一致でマッチしている- 一意に特定できれば途中で切ってもok
# よくあるオプション
$ git commit --amend --no-edit
# これでも可
$ git commit --am --no-e
はじめに
開発も佳境を迎え筆が乗ってきた(キーボードだけど)。push前にこの修正もコミットに入れておこう。
$ git commit --amend --no-edi
おっと、 --no-edit
の t
が抜けてしまった。キーボードが俺に追いつけなかったようだ。
$ 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
について。
$ 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
オプションで試してみるともっと分かりやすいです。こちらは一意に絞れなかった例。
$ git diff --no-colo
error: ambiguous option: no-colo (could be --no-color-moved or --no-color-moved-ws)
一方、一意であっても省略できないバターンもありました。
# --name-only
$ git diff --name-onl
error: invalid option: --name-onl
実装
どうやら部分一致でマッチしていそうですが、裏を取るために実装を読みます。
git commit
git commit
のオプションはここで定義されています。
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()
};
// ...
}
そして、コマンドライン引数のパーサーで定義されたオプションとマッチするものを探索します。
この際、完全一致しなくても、部分一致で一意に特定できればそのオプションを返しています。
// 長いオプション(--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;
}
候補が一意に絞れなかった場合にはエラーになるようになっています。先ほどの検証で出てきた ambiguous option
のエラーが定義されています。
また、no-edit
のように no-
で始まるオプションは、no-
のprefixを取り除いてから再度パースしています。
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
の実装を見ます。
別の関数でオプションのバリデーションチェックが行われているため、エラー扱いになっていました。
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--;
}
// ...
}
省略オプションが使えるか使えないかは各サブコマンドの実装によるようです。私はここで力尽きてしまったので、気になる方は実装を読んでみてください。
おわりに
以上、Gitのコマンドオプションが省略できるという紹介でした。急いでいるときにはどこまで削れるかチャレンジしてみてはいかがでしょうか?