# [PATCH 3/6] ls-tree: convert to gitopt

13 messages from 2006-05-09 to 2006-05-09. Participants: Eric Wong, Timo Hirvonen, Junio C Hamano.
Thread: https://gitlist.dev/t/4108

## Eric Wong, 2006-05-09 05:06

Subject: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <11471512103526-git-send-email-normalperson@yhbt.net>
URL: https://gitlist.dev/e/11471512103526-git-send-email-normalperson%40yhbt.net
In-Reply-To: <1147151209168-git-send-email-normalperson@yhbt.net>

```
It was initially conceived as an addition to the git.c wrapper,
and not affect other programs.  But it turns out existing C
programs can use it pretty easily, too.

Features include:

 * getopt-style permuting (can easily be disabled for
   things like update-index)

 * command-line compatibile with existing usage:
   -S=pickaxe-arg-with-leading-equals is unchanged

 * printf-style rewriting (for front-ending shell scripts)

 * unbundling of short options: -uC20n20z => -u -C20 -n20 -z

 * automatically understands unambiguous abbreviations

 * optional argument handling (-C<num>, -M<num>)
   -C <num> (with a space between them) has not changed,
   however, <num> can be a sha1, or a path

Signed-off-by: Eric Wong <normalperson@yhbt.net>

---

 .gitignore           |    1 
 Makefile             |   10 +
 git.c                |   11 +
 gitopt.c             |  603 ++++++++++++++++++++++++++++++++++++++++++++++++++
 gitopt.h             |  142 ++++++++++++
 gitopt/git_wrapper.h |   45 ++++
 gitopt/sh.h          |   45 ++++
 t/t0200-gitopt.sh    |  280 +++++++++++++++++++++++
 test-gitopt.c        |  112 +++++++++
 9 files changed, 1244 insertions(+), 5 deletions(-)
 create mode 100644 gitopt.c
 create mode 100644 gitopt.h
 create mode 100644 gitopt/git_wrapper.h
 create mode 100644 gitopt/sh.h
 create mode 100755 t/t0200-gitopt.sh
 create mode 100644 test-gitopt.c

083777c55feab0b68b859f8cb0d5bcf105db4ef5
diff --git a/.gitignore b/.gitignore
index b5959d6..b2d8b06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -123,6 +123,7 @@ git-write-tree
 git-core-*/?*
 test-date
 test-delta
+test-gitopt
 common-cmds.h
 *.tar.gz
 *.dsc
diff --git a/Makefile b/Makefile
index 45484fc..46f2401 100644
--- a/Makefile
+++ b/Makefile
@@ -197,7 +197,7 @@ LIB_H = \
 	blob.h cache.h commit.h csum-file.h delta.h \
 	diff.h object.h pack.h pkt-line.h quote.h refs.h \
 	run-command.h strbuf.h tag.h tree.h git-compat-util.h revision.h \
-	tree-walk.h log-tree.h
+	tree-walk.h log-tree.h gitopt.h
 
 DIFF_OBJS = \
 	diff.o diff-lib.o diffcore-break.o diffcore-order.o \
@@ -212,6 +212,7 @@ LIB_OBJS = \
 	server-info.o setup.o sha1_file.o sha1_name.o strbuf.o \
 	tag.o tree.o usage.o config.o environment.o ctype.o copy.o \
 	fetch-clone.o revision.o pager.o tree-walk.o xdiff-interface.o \
+	gitopt.o \
 	$(DIFF_OBJS)
 
 BUILTIN_OBJS = \
@@ -470,6 +471,8 @@ all:
 strip: $(PROGRAMS) git$X
 	$(STRIP) $(STRIP_OPTS) $(PROGRAMS) git$X
 
+gitopt.o: gitopt.c gitopt.h gitopt/*.h
+
 git$X: git.c common-cmds.h $(BUILTIN_OBJS) $(GITLIBS)
 	$(CC) -DGIT_VERSION='"$(GIT_VERSION)"' \
 		$(ALL_CFLAGS) -o $@ $(filter %.c,$^) \
@@ -600,7 +603,7 @@ # with that.
 
 export NO_PYTHON
 
-test: all
+test: all test-gitopt$X
 	$(MAKE) -C t/ all
 
 test-date$X: test-date.c date.o ctype.o
@@ -609,6 +612,9 @@ test-date$X: test-date.c date.o ctype.o
 test-delta$X: test-delta.c diff-delta.o patch-delta.o
 	$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $^ -lz
 
+test-gitopt$X: test-gitopt.c gitopt.o ctype.o usage.o
+	$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $^
+
 check:
 	for i in *.c; do sparse $(ALL_CFLAGS) $(SPARSE_FLAGS) $$i || exit; done
 
diff --git a/git.c b/git.c
index 49ba518..785d97f 100644
--- a/git.c
+++ b/git.c
@@ -11,6 +11,7 @@ #include <stdarg.h>
 #include "git-compat-util.h"
 #include "exec_cmd.h"
 
+#include "gitopt/git_wrapper.h"
 #include "builtin.h"
 
 static void prepend_to_path(const char *dir, int len)
@@ -72,6 +73,7 @@ int main(int argc, const char **argv, ch
 	char *slash = strrchr(cmd, '/');
 	char git_command[PATH_MAX + 1];
 	const char *exec_path = NULL;
+	struct exec_args *ea;
 
 	/*
 	 * Take the basename of argv[0] as the command
@@ -99,7 +101,8 @@ int main(int argc, const char **argv, ch
 	if (!strncmp(cmd, "git-", 4)) {
 		cmd += 4;
 		argv[0] = cmd;
-		handle_internal_command(argc, argv, envp);
+		ea = gitopt_parse_git(argc, argv);
+		handle_internal_command(ea->argc, ea->argv, envp);
 		die("cannot handle %s internally", cmd);
 	}
 
@@ -144,6 +147,8 @@ int main(int argc, const char **argv, ch
 	}
 	argv[0] = cmd;
 
+	ea = gitopt_parse_git(argc, argv);
+
 	/*
 	 * We search for git commands in the following order:
 	 *  - git_exec_path()
@@ -157,10 +162,10 @@ int main(int argc, const char **argv, ch
 	prepend_to_path(exec_path, strlen(exec_path));
 
 	/* See if it's an internal command */
-	handle_internal_command(argc, argv, envp);
+	handle_internal_command(ea->argc, ea->argv, envp);
 
 	/* .. then try the external ones */
-	execv_git_cmd(argv);
+	execv_git_cmd(ea->argv);
 
 	if (errno == ENOENT)
 		cmd_usage(0, exec_path, "'%s' is not a git-command", cmd);
diff --git a/gitopt.c b/gitopt.c
new file mode 100644
index 0000000..056e163
--- /dev/null
+++ b/gitopt.c
@@ -0,0 +1,603 @@
+#include "git-compat-util.h"
+#include "gitopt.h"
+#include "cache.h"
+
+/* whether or not to pass-through unrecognized switches or to report an error.
+ * This is only intended to be set to true for use in the git.c wrapper */
+int gitopt_pass_through = 0;
+static int gitopt_errno = 0;
+int gitopt_dd_seen = 0;	/* double-dash seen flag */
+
+void (*gitopt_non_option_cb)(struct exec_args *b, const int argc,
+				const char **argv, int *argc_pos);
+
+void gitopt_default_non_option_cb(struct exec_args *b, const int argc,
+				const char **argv, int *argc_pos)
+{
+	b->argv[b->argc++] = argv[*argc_pos];
+}
+
+struct exec_args *new_exec_args(const int argc)
+{
+	struct exec_args *ea = xmalloc(sizeof(*ea));
+	ea->argc = argc;
+	ea->argv = xcalloc((argc+1), sizeof(char*));
+
+	return ea;
+}
+
+struct exec_args *nil_exec_args(struct exec_args *ea)
+{
+	int i;
+	for (i = 0; i < ea->argc; i++)
+		ea->argv[i] = NULL;
+	ea->argc = 0;
+	return ea;
+}
+
+static int combine_exec_args(struct exec_args *dest, struct exec_args *from)
+{
+	int i;
+
+	dest->argv = xrealloc(dest->argv,
+				(2 + dest->argc + from->argc)*sizeof(char*));
+
+	for (i = 0; i < from->argc; i++)
+		dest->argv[dest->argc++] = from->argv[i];
+
+	dest->argv[dest->argc] = NULL;
+
+	return i;
+}
+
+static struct exec_args *rewrite_args(const char *rewrite_fmt, const char *arg)
+{
+	struct exec_args *ea;
+	size_t len = strlen(rewrite_fmt) + (arg ? strlen(arg) : 0);
+	char *dest = xmalloc(len); /* don't free this */
+	int nr_ws = 0;
+	char *a, *b;
+
+	if (!arg) {
+		strcpy(dest, rewrite_fmt);
+		if ((a = strstr(dest,"=%s")) || (a = strstr(dest," %s")))
+			a[0] = a[1] = a[2] = '\0';
+		else if ((a = strstr(dest,"%s")))
+			a[0] = a[1] = '\0';
+	} else {
+		const char *c = rewrite_fmt;
+
+		do { if (isspace(*c++)) nr_ws++; } while (*c);
+		snprintf(dest, len, rewrite_fmt, arg);
+	}
+
+	ea = new_exec_args(1 + nr_ws);
+	a = b = dest;
+
+	for (ea->argc = 0; nr_ws && *b; b++) {
+		if (isspace(*b)) {
+			*b = '\0';
+			if (strlen(a))
+				ea->argv[ea->argc++] = a;
+			a = b + 1;
+		}
+	}
+	if (strlen(a))
+		ea->argv[ea->argc++] = a;
+	ea->argv[ea->argc] = NULL;
+
+	return ea;
+}
+
+struct exec_args *one_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	const char *c = argv[*argc_pos];
+	struct exec_args *ea = NULL;
+	const char *a = NULL;
+	size_t l_len = s->l ? strlen(s->l) : 0;
+	int so = 0; /* short option passed flag */
+
+	if (*c == '-')  {
+		if (s->s && c[1] != '-') {
+			if (isspace(s->s)) {
+				a = c + 1;
+				so = 1;
+			} else if (c[1] == s->s) {
+				a = c + 2;
+				so = 1;
+			}
+		} else if (s->l && c[1] == '-' && !strncmp(c+2, s->l, l_len))
+			a = c + 2 + l_len;
+	}
+
+	if (!a)
+		return NULL;
+
+	switch(a[0]) {
+	case '\0':
+		if (((*argc_pos + 1) < argc) && (a = argv[*argc_pos + 1])) {
+			/* optional arguments must be attached to the switch
+			 * so that there are no abiguities */
+			if (s->arg_fl & ARG_IS_OPT)
+				break;
+			*argc_pos += 1;
+			if (s->rewrite_fmt)
+				ea = rewrite_args(s->rewrite_fmt, a);
+			else {
+				ea = new_exec_args(2);
+				ea->argv[0] = c;
+				ea->argv[1] = a;
+			}
+		}
+		break;
+	case '=': /* only long options get to use "=" to denote an arg */
+		if (!so) {
+			if (s->rewrite_fmt)
+				ea = rewrite_args(s->rewrite_fmt, a + 1);
+			else {
+				ea = new_exec_args(1);
+				ea->argv[0] = c;
+			}
+			break;
+		}
+		/* fall-through, so that -S'= assigned_val;' still works */
+	default:
+		/* -s<a> (short opts only), long opts are ambiguous
+		 * w/o '=' or a space separating the opt */
+		if (!so)
+			break;
+		if (s->rewrite_fmt)
+			ea = rewrite_args(s->rewrite_fmt, a);
+		else {
+			ea = new_exec_args(1);
+			ea->argv[0] = c;
+		}
+	}
+
+	return ea;
+}
+
+static struct exec_args *optional_arg_common(struct exec_args *ea,
+			const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	if (!ea) {
+		if (s->rewrite_fmt)
+			ea = rewrite_args(s->rewrite_fmt, NULL);
+		else {
+			ea = new_exec_args(1);
+			ea->argv[0] = argv[*argc_pos];
+		}
+	}
+	return ea;
+}
+
+struct exec_args *optional_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	return optional_arg_common( one_arg(s, argc, argv, argc_pos),
+						s, argc, argv, argc_pos);
+}
+
+struct exec_args *int_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	struct exec_args *ea = one_arg(s, argc, argv, argc_pos);
+
+	if (ea) {
+		const char *c = ea->argv[ea->argc - 1];
+		char *endptr;
+		long int tmp;
+
+		/* -C<num>: */
+		if (ea->argc == 1) {
+			while (*c && !isdigit(*c)) c++;
+			if (!c) goto err;
+		}
+
+		endptr = (char *)c;
+		tmp = strtol(c, &endptr, 10);
+		if (endptr == c) {
+err:
+			if (s->arg_fl & ARG_IS_INT)
+				(*argc_pos)--;
+			free_exec_args(ea);
+			return NULL;
+		}
+	}
+	return ea;
+}
+
+struct exec_args *optional_int_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	return optional_arg_common( int_arg(s, argc, argv, argc_pos),
+						s, argc, argv, argc_pos);
+}
+
+static struct exec_args *run_proc(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	if (s->fn)
+		return s->fn(s, argc, argv, argc_pos);
+	switch (s->arg_fl) {
+	case ARG_ONE:
+		return one_arg(s, argc, argv, argc_pos);
+	case ARG_INT:
+		return int_arg(s, argc, argv, argc_pos);
+	case ARG_OPT:
+		return optional_arg(s, argc, argv, argc_pos);
+	case ARG_OPTINT:
+		return optional_int_arg(s, argc, argv, argc_pos);
+	default:
+		return NULL;
+	}
+}
+
+static const char * parse_bundled(struct exec_args *dest,
+			const struct opt_spec *s, const char *cur,
+			const int argc, const char **argv, int *arg_pos)
+{
+	struct exec_args *ea = NULL;
+	const char *orig = cur;
+	char *c = xmalloc(strlen(cur) + 2); /* don't free this */
+	const char *tmp_argv[] = { c };
+	int i = 0;
+
+	*c++ = '-';
+	*c++ = *cur++;
+	if (!s || (!s->arg_fl && !s->fn)) {
+		ea = new_exec_args(1);
+		if (s && s->rewrite_fmt)
+			ea->argv[0] = s->rewrite_fmt;
+		else {
+			ea->argv[0] = tmp_argv[0];
+			*c = '\0';
+		}
+	} else if (s->arg_fl || s->fn) {
+		if (*cur) {
+			/* no space between the arg and opt switch: */
+			if (s->arg_fl & ARG_IS_INT) {
+				/* we know to handle stuff like:
+				 * -h24w80 => -h=24 -w=80 */
+				char *endptr = (char *)cur;
+				strtol(cur, &endptr, 10);
+
+				while (cur < endptr)
+					*c++ = *cur++;
+			} else if (s->arg_fl & ARG_ONE) {
+				/* unfortunately, other args are less
+				 * clear-cut */
+				while (*cur)
+					*c++ = *cur++;
+			}
+			*c = '\0';
+			ea = run_proc(s, 1, tmp_argv, &i);
+		} else if ((*arg_pos + 1) < argc) {
+			int j = *arg_pos;
+			int x = argc - j;
+			const char **argv2 = xmalloc(x * sizeof(char *));
+
+			*c = '\0';
+			argv2[i++] = tmp_argv[0];
+			while (i <= x) argv2[i++] = argv[++j];
+
+			i = 0;
+			ea = run_proc(s, x, argv2, &i);
+			*arg_pos += i;
+			free(argv2);
+		}
+	}
+	if (ea) {
+		combine_exec_args(dest, ea);
+		free_exec_args(ea);
+	} else
+		gitopt_errno = error("Failed to parse bundled arguments in: %s",
+									orig);
+	return cur;
+}
+
+static void unbundle(struct exec_args *dest, const char *cur,
+		const struct opt_spec *ost,
+		const struct opt_spec *cur_spec,
+		const int argc, const char **argv, int *arg_pos)
+{
+	struct exec_args *ea = new_exec_args(strlen(cur));
+	const struct opt_spec *s;
+	const char *c = cur;
+
+	ea->argc = 0;
+
+	while (*c) {
+		int i;
+		for (i = 0; ost[i].s || ost[i].l; i++) {
+			s = ost + i;
+			if (!s->s || (!isspace(s->s) && s->s != *c))
+				continue;
+			c = parse_bundled(ea, s, c, argc, argv, arg_pos);
+			break;
+		}
+		if (ost[i].l || ost[i].s) continue;
+		/* pass-through while unbundling and creating switches:
+		 * this means that if we see -abc here, but we only
+		 * had -a defined in ost (-a defined to not accept args),
+		 * then we'd create switches
+		 * for -b and -c here (since we already knew -a)
+		 * and we're assuming -b and -c were just forgotten
+		 * in the ost.  If we had gotten -bac, that would
+		 * be passed through as -bac in gitopt_parse_ost()
+		 * as an unknown option if -b is undefined in the ost
+		 */
+		if (gitopt_pass_through)
+			c = parse_bundled(ea, NULL, c, argc, argv, arg_pos);
+		else {
+			/* continue for now, a higher-up function will work
+			 * with the error */
+			gitopt_errno = error("Unknown option '%s' in '%s'",
+							c,argv[*arg_pos]);
+			c++;
+		}
+	}
+	combine_exec_args(dest, ea);
+	free_exec_args(ea);
+}
+
+static void push_one_opt(struct exec_args *dest, const struct opt_spec *s,
+		const int argc, const char **argv, int *i)
+{
+	struct exec_args *ea;
+
+	if (!s->arg_fl && !s->fn) {
+		dest->argv[dest->argc++] = s->rewrite_fmt ? s->rewrite_fmt
+							: argv[*i];
+		return;
+	}
+
+	if ((ea = run_proc(s,argc,argv,i))) {
+		combine_exec_args(dest, ea);
+		free_exec_args(ea);
+	} else
+		gitopt_errno = error("Failed to parse arguments for: %s %s",
+					argv[*i], argv[*i+1]?argv[*i+1]:"");
+}
+
+/* look for a prefix abbreviation */
+static int opt_abbrev_match(const struct opt_spec *s, const char *p)
+{
+	const char *l = s->l;
+
+	while (*p) {
+		if (*l++ != *p++) return 0;
+		if (!*p || (s->arg_fl && *p == '=')) return 1;
+	}
+
+	return 0;
+}
+
+/* match a short option switch */
+static int opt_char_match(const struct opt_spec *s, const char *p)
+{
+	return ((s->s == p[0]) && ((!s->arg_fl && p[1] == '\0')
+				||
+			(s->arg_fl && (p[1] == '\0' || p[1] == '='))));
+}
+
+/* tokenize on '-' and look for a prefix abbreviation match */
+static int opt_token_match(const struct opt_spec *s, const char *p0)
+{
+	const char *l = s->l;
+	const char *p;
+
+	while ((l = strchr(l,'-'))) {
+		l++;
+		p = p0;
+		while (*p) {
+			if (*l++ != *p++) break;
+			if (!*p || (s->arg_fl && *p == '=')) return 1;
+		}
+	}
+
+	return 0;
+}
+
+/* look for unambigious abbreviated switches, if it can't be found,
+ * assume it's a non-option and pass it to b */
+static void fallback_long(const struct opt_spec *ost,
+			struct exec_args *a, struct exec_args *b,
+			const int argc, const char **argv,
+			const char *cur, int *argc_pos)
+{
+	const struct opt_spec *s;
+	int i, found = -1;
+
+	/* look for abbreviations: */
+	for (i = 0; ost[i].l || ost[i].s; i++) {
+		s = &(ost[i]);
+		/* maybe they wanted to use a short option
+		 * (normally a single-dash) but typed two dashes instead.
+		 * note: even if we find a short option here, we do not
+		 * attempt to unbundle in this case */
+		if ((s->s && opt_char_match(s, cur)) ||
+					(s->l && opt_abbrev_match(s,cur))) {
+			if (found >= 0)
+				goto pass_through;
+			found = i;
+		}
+	}
+
+	/* ok, try harder, based on tokenization on '-' */
+	if (found < 0) {
+		for (i = 0; ost[i].l || ost[i].s; i++) {
+			s = &(ost[i]);
+			if (s->l && opt_token_match(s,cur)) {
+				if (found >= 0)
+					goto pass_through;
+				found = i;
+			}
+		}
+	}
+	/* should I add a strstr() matcher here for the desperate? */
+
+	/* rewrite the abbreviated switch in it's unabbreviated form: */
+	if (found >= 0) {
+		char *tmp;
+		size_t len = 3 + strlen(cur); /* --cur=potential_args\0 */
+		size_t l_len;
+
+		s = &(ost[found]);
+		l_len = s->l ? strlen(s->l) : 0; /* favor long options */
+		tmp = xcalloc(len + l_len, 1); /* don't free this */
+		tmp[0] = '-';
+
+		if (s->l) {
+			tmp[1] = '-';
+			memcpy(tmp + 2, s->l, l_len);
+		} else
+			tmp[1] = s->s;
+		if (s->arg_fl) {
+			const char *c;
+			if ((c = strchr(cur,'='))) {
+				if (!l_len) c++; /* skip '=' for short opts */
+				strcpy(tmp + 2 + l_len, c);
+			}
+		}
+
+		argv[*argc_pos] = tmp;
+		push_one_opt(a, s, argc, argv, argc_pos);
+		return;
+	}
+
+pass_through:
+	gitopt_non_option_cb(b, argc, argv, argc_pos);
+	if (!gitopt_pass_through)
+		gitopt_errno = error("Unknown option: '%s'",argv[*argc_pos]);
+}
+
+static int opt_complete_match(const struct opt_spec *s, const char *p)
+{
+	if (s->arg_fl) {
+		size_t len = strlen(s->l);
+
+		return (!strncmp(s->l,p,len) &&
+				(p[len] == '\0' || (p[len] == '=')));
+	}
+	return !strcmp(s->l,p);
+}
+
+/* You should really only use this in git (wrapper) and test-gitopt: */
+struct exec_args *gitopt_parse_ost(const struct opt_spec *ost,
+			const int argc, const char **argv)
+{
+	struct exec_args *a = new_exec_args(argc); /* argv[0] and options: */
+	struct exec_args *b = new_exec_args(argc); /* non-option args */
+
+	gitopt_pass_through = 1;
+
+	if (gitopt_parse_ost_split(a, b, ost, argc, argv) < 0)
+		die("gitopt argument parsing failed");
+	combine_exec_args(a, b);
+	free_exec_args(b);
+
+	return a;
+}
+
+int gitopt_verify_b_args(const struct exec_args *b)
+{
+	const char **arg;
+
+	for (arg = b->argv; *arg; arg++) {
+		/* anything goes after a double dash */
+		if (!memcmp("--",*arg,3))
+			return 1;
+		if (*arg[0] == '-')
+			return 0;
+	}
+	return 1;
+}
+
+void gitopt_parse_one_opt(struct exec_args *a, struct exec_args *b,
+			const struct opt_spec *ost,
+			const int argc, const char **argv, int *argc_pos)
+{
+	const char *c = argv[*argc_pos];
+	const struct opt_spec *s;
+	int i;
+
+	if (!gitopt_dd_seen && !memcmp("--",c,3)) {
+		gitopt_dd_seen = 1;
+		if (!gitopt_pass_through)
+			return;
+	}
+	if (gitopt_dd_seen) {
+		gitopt_non_option_cb(b, argc, argv, argc_pos);
+		return;
+	}
+	if (!memcmp("--",c,2)) {	/* long options */
+		c += 2;
+		for (i = 0; ost[i].l || ost[i].s; i++) {
+			s = &(ost[i]);
+			if (!s->l || !opt_complete_match(s, c))
+				continue;
+			push_one_opt(a, s, argc, argv, argc_pos);
+			return;
+		}
+		if (ost[i].l || ost[i].s) return;
+		/* undefined --option: */
+		fallback_long(ost, a, b, argc, argv, c, argc_pos);
+		return;
+	}
+	if ((c[0] == '-') && (c[1] != '-')) { /* short option */
+		c++;
+		for (i = 0; ost[i].l || ost[i].s; i++) {
+			s = &(ost[i]);
+			if (!s->s) continue;
+			if (isspace(s->s) && (*c == '\0' || isdigit(*c))) {
+				/* special case for -<num> */
+				push_one_opt(a, s, argc, argv, argc_pos);
+				return;
+			}
+			if (s->s != *c) continue;
+			if ((c[1] != '\0') && (c[1] != '='))
+				unbundle(a, c, ost, s, argc, argv, argc_pos);
+			else
+				push_one_opt(a, s, argc, argv, argc_pos);
+			return;
+		}
+		if (ost[i].l || ost[i].s) return;
+		/* undefined: */
+		if (gitopt_pass_through)
+			gitopt_non_option_cb(b, argc, argv, argc_pos);
+		else
+			gitopt_errno = error("Unknown option: %s",
+							argv[*argc_pos]);
+		return;
+	}
+	gitopt_non_option_cb(b, argc, argv, argc_pos);
+}
+
+int gitopt_parse_ost_split(struct exec_args *a, struct exec_args *b,
+			const struct opt_spec *ost,
+			const int argc, const char **argv)
+{
+	int i;
+
+	if (!gitopt_non_option_cb)
+		gitopt_non_option_cb = gitopt_default_non_option_cb;
+
+	gitopt_dd_seen = 0;
+	b->argc = 0;
+	a->argv[0] = argv[0];
+	a->argc = 1;
+
+	for (i = 1; i < argc; i++)
+		gitopt_parse_one_opt(a, b, ost, argc, argv, &i);
+	return gitopt_errno;
+}
+
+void free_exec_args(struct exec_args *ea)
+{
+	free(ea->argv);
+	free(ea);
+}
+
diff --git a/gitopt.h b/gitopt.h
new file mode 100644
index 0000000..78c9e5d
--- /dev/null
+++ b/gitopt.h
@@ -0,0 +1,142 @@
+#ifndef GITOPT_H
+#define GITOPT_H
+
+/* gitopt_* functions will return this structure
+ * the elements in this struct can then be treated just
+ * like their counterparts from main(). */
+struct exec_args {
+	const char **argv;
+	int argc;
+};
+
+/* @l: long option string (without the leading "--")
+ *
+ * @s: single option char, ' ' has a special meaning for accepting a
+ * single '-' (dash), which can also accept an integer argument This is
+ * for things like "-5" => "--max-count=5" or "-" => "--stdin"
+ *
+ * @rewrite_fmt: rewrite the passed argument(s) (if any) into this
+ * (*printf) style string.  Only a single %s can be accepted and handled
+ * by the default fn() handlers included in gitopt.
+ *
+ * Do not use this if you need to use more than one "%s", you'll need to
+ * define and use a custom fn().  rewrite_fmt is only intended for
+ * the common argument rewriting cases.
+ *
+ * If rewrite_fmt has a "%s", " %s" or "=%s" in it, it will be stripped
+ * out if no arguments are passed to it (this can be the case where
+ * fn() (see below) is defined to optional_arg).
+ *
+ * Any single space between non-space characters will be interpreted as
+ * break in the option and the options will be split out into seperate
+ * elements in argv.
+ *
+ * @fn: define this to the function you wish to use for handling any
+ * arguments, or cases where rewrite_fmt is too limited.  You can define
+ * your own functions and use it here, of course.  The gitopt_eat*
+ * macros provide convenient ways to write functions for this member.
+ * Set this to 0/NULL if you have no args to parse.
+ */
+struct opt_spec {
+	const char *l;
+	const char s;
+	const char *rewrite_fmt;
+	unsigned int arg_fl;
+	struct exec_args *(*fn)(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos);
+};
+
+/* internal use: */
+#define ARG_IS_INT	0x08
+#define ARG_IS_OPT	0x10
+
+/* use these for opt_spec flags: */
+#define ARG_NIL		0x00
+#define ARG_ONE		0x01
+#define ARG_TWO		0x02	/* not really supported yet */
+#define ARG_THREE	0x04	/* not really supported yet */
+#define ARG_TRE		ARG_THREE
+#define ARG_INT		(ARG_ONE | ARG_IS_INT)
+#define ARG_OPT		(ARG_ONE | ARG_IS_OPT)
+#define ARG_OPTINT	(ARG_ONE | ARG_IS_OPT | ARG_IS_INT)
+
+extern int gitopt_pass_through;
+extern int gitopt_dd_seen;	/* double-dash seen flag */
+void (*gitopt_non_option_cb)(struct exec_args *b, const int argc,
+				const char **argv, int *argc_pos);
+void gitopt_default_non_option_cb(struct exec_args *b, const int argc,
+				const char **argv, int *argc_pos);
+struct exec_args *new_exec_args(const int argc);
+void free_exec_args(struct exec_args *ea);
+struct exec_args *nil_exec_args(struct exec_args *ea);
+
+/* You should really only use this in the git wrapper or tests: */
+struct exec_args *gitopt_parse_ost(const struct opt_spec *ost,
+			const int argc, const char **argv);
+
+/* Most C programs should use this internally.  Returns -1 on error */
+int gitopt_parse_ost_split(struct exec_args *a, struct exec_args *b,
+			const struct opt_spec *ost,
+			const int argc, const char **argv);
+
+# define gitopt_eat(func,code_block) \
+static struct exec_args *func(const struct opt_spec *s, \
+			const int argc, const char **argv, int *argc_pos) \
+{ \
+	code_block \
+	return new_exec_args(0); /* return non-NULL if no errors */ \
+}
+
+# define gitopt_eat_one_arg(func,code_block) \
+static struct exec_args *func(const struct opt_spec *s, \
+			const int argc, const char **argv, int *argc_pos) \
+{ \
+	struct exec_args *ea = one_arg(s, argc, argv, argc_pos); \
+	if (!ea) return NULL; \
+	code_block \
+	return nil_exec_args(ea); /* return non-NULL if no errors */ \
+}
+#define gitopt_eat_arg(func,code_block)	gitopt_eat_one_arg(func,code_block)
+
+# define gitopt_eat_int(func,code_block) \
+static struct exec_args *func(const struct opt_spec *s, \
+			const int argc, const char **argv, int *argc_pos) \
+{ \
+	struct exec_args *ea = one_arg(s, argc, argv, argc_pos); \
+	if (!ea) return NULL; \
+	code_block \
+	return nil_exec_args(ea); /* return non-NULL if no errors */ \
+}
+
+# define gitopt_eat_opt_int(func,code_block) \
+static struct exec_args *func(const struct opt_spec *s, \
+			const int argc, const char **argv, int *argc_pos) \
+{ \
+	struct exec_args *ea = optional_int_arg(s, argc, argv, argc_pos); \
+	code_block \
+	return nil_exec_args(ea); /* return non-NULL if no errors */ \
+}
+
+/* used for debugging */
+static inline void dump_ea(const char *pfx, struct exec_args *ea)
+{
+	const char **arg;
+	int i = 0;
+	for (arg = ea->argv; *arg; arg++)
+		fprintf(stderr,"[%d] %s: %s\n",i++,pfx,*arg);
+}
+
+void gitopt_parse_one_opt(struct exec_args *a, struct exec_args *b,
+			const struct opt_spec *ost,
+			const int argc, const char **argv, int *argc_pos);
+void gitopt_set_pass_through();	/* should only be used by git.c wrapper */
+
+struct exec_args *one_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos);
+struct exec_args *int_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos);
+struct exec_args *optional_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos);
+struct exec_args *optional_int_arg(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos);
+#endif /* GITOPT_H */
diff --git a/gitopt/git_wrapper.h b/gitopt/git_wrapper.h
new file mode 100644
index 0000000..5f27bf4
--- /dev/null
+++ b/gitopt/git_wrapper.h
@@ -0,0 +1,45 @@
+/* opt_spec table mappings for the git.c wrapper */
+
+#include "../gitopt.h"
+#include "../gitopt/sh.h"
+
+static const struct opt_spec ost_null[] = { { 0, 0 } };
+
+static const struct opt_spec_map {
+	const char *cmd;
+	const struct opt_spec *ost;
+} opt_specs[] = {
+	{ "checkout",		ost_checkout },
+	{ "commit",		ost_commit },
+	{ "am",			ost_am},
+};
+
+static const struct opt_spec *find_cmd_ost(const int argc, const char **argv)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(opt_specs); i++) {
+		const char *cmd = opt_specs[i].cmd;
+		if (strcmp(cmd, *argv))
+			continue;
+		return opt_specs[i].ost;
+	}
+	return NULL;
+}
+
+static struct exec_args *gitopt_parse_git(const int argc, const char **argv)
+{
+	const struct opt_spec *ost;
+
+	if (!strcmp(argv[0],"help") || !strcmp(argv[0],"version") ||
+				!(ost = find_cmd_ost(argc, argv))) {
+		struct exec_args *ea = new_exec_args(argc);
+		int i;
+
+		for (i = 0; i <= argc; i++) /* argv[argc] == NULL */
+			ea->argv[i] = argv[i];
+
+		return ea;
+	}
+	return gitopt_parse_ost(ost, argc, argv);
+}
diff --git a/gitopt/sh.h b/gitopt/sh.h
new file mode 100644
index 0000000..0ce6620
--- /dev/null
+++ b/gitopt/sh.h
@@ -0,0 +1,45 @@
+#ifndef GITOPT_SH_H
+#define GITOPT_SH_H
+
+/* opt_spec tables for some git programs written in shell that don't
+ * have too many options */
+
+static const struct opt_spec ost_am[] = {
+	{ "dotest",		'd',	0,		ARG_ONE,	0 },
+	{ "interactive",	'i',	0,		0,		0 },
+	{ "binary",		'b',	0,		0,		0 },
+	{ "3way",		'3',	0,		0,		0 },
+	{ "signoff",		's',	0,		0,		0 },
+	{ "utf8",		'u',	0,		0,		0 },
+	{ "keep",		'k',	0,		0,		0 },
+	{ "resolved",		'r',	0,		0,		0 },
+	{ "skip",		0,	0,		0,		0 },
+	{ "whitespace",		0,	0,		ARG_ONE,	0 },
+	{ 0, 0 }
+};
+
+static const struct opt_spec ost_checkout[] = {
+	{ 0,			'f',	0,		0,		0 },
+	{ 0,			'm',	0,		0,		0 },
+	{ 0,			'b',	"-b %s",	ARG_ONE,	0 },
+	{ 0, 0 }
+};
+
+static const struct opt_spec ost_commit[] = {
+	{ "file",		'F',	0,		ARG_ONE,	0 },
+	{ "all",		'a',	0,		0,		0 },
+	{ "author",		0,	0,		ARG_ONE,	0 },
+	{ "edit",		'e',	0,		0,		0 },
+	{ "include",		'i',	0,		0,		0 },
+	{ "only",		'o',	0,		0,		0 },
+	{ "message",		'm',	0,		ARG_ONE,	0 },
+	{ "no-verify",		'n',	0,		0,		0 },
+	{ "amend",		0,	0,		0,		0 },
+	{ "reedit",		'c',	0,		ARG_ONE,	0 },
+	{ "reuse-message",	'C',	0,		ARG_ONE,	0 },
+	{ "signoff",		's',	0,		0,		0 },
+	{ "verbose",		'v',	0,		0,		0 },
+	{ 0, 0 }
+};
+
+#endif /* GITOPT_SH_H */
diff --git a/t/t0200-gitopt.sh b/t/t0200-gitopt.sh
new file mode 100755
index 0000000..6da686c
--- /dev/null
+++ b/t/t0200-gitopt.sh
@@ -0,0 +1,280 @@
+#!/bin/sh
+#
+# Copyright (c) 2006 Eric Wong
+#
+
+test_description='gitopt command-line pa{ss,rs}er'
+
+. ./test-lib.sh
+
+cat > expect <<EOF
+00 'commit'
+01 '(null)'
+EOF
+test_expect_success 'single command' \
+	'test-gitopt commit > output && cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '(null)'
+EOF
+test_expect_success 'simple command with switches' \
+	'test-gitopt commit -a -s > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '(null)'
+EOF
+test_expect_success 'command with bundled switches' \
+	'test-gitopt commit -as > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '-mhello world'
+04 '(null)'
+EOF
+test_expect_success 'bundle with args for last (no space)' \
+	'test-gitopt commit -asm"hello world" > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '-m'
+04 'hello world'
+05 '(null)'
+EOF
+test_expect_success 'unbundle with args (space)' \
+	'test-gitopt commit -asm "hello world" > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '-m'
+04 'hello world'
+05 'file1'
+06 'file2'
+07 '(null)'
+EOF
+test_expect_success 'unbundle and reorder switches and command w/args' \
+	'test-gitopt commit file1 file2 -asm "hello world" > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '-a'
+02 '-s'
+03 '--edit'
+04 'file2'
+05 '--'
+06 'file1'
+07 '-as'
+08 '--all'
+09 '(null)'
+EOF
+test_expect_success 'reorder up to and pass-through "--"' \
+	'test-gitopt commit -as file2 --edit -- file1 -as --all > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '--find-copies-harder'
+02 '(null)'
+EOF
+test_expect_success 'abbreviation finder (prefix)' \
+	'test-gitopt whatchanged --find-c > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '--patch-with-raw'
+02 '(null)'
+EOF
+test_expect_success 'abbreviation finder (substring on "-" token)' \
+	'test-gitopt whatchanged --raw > output &&
+	 cmp expect output'
+
+# we assume unknown switches that cannot be resolved to a single known
+# switch to just be an new argument we do not know about, so we pass
+# it to the underlying command
+cat > expect <<EOF
+00 'whatchanged'
+01 '--name'
+02 '(null)'
+EOF
+test_expect_success 'ambiguous abbreviation (pass-through)' \
+	'test-gitopt whatchanged --name > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '--diff-filter=MCT'
+02 'file1'
+03 '(null)'
+EOF
+test_expect_success 'rewrite on long argument' \
+	'test-gitopt whatchanged file1 --diff-filter MCT > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-Shello'
+02 '(null)'
+EOF
+test_expect_success 'rewrite on short argument (#1)' \
+	'test-gitopt whatchanged -Shello > output &&
+	 cmp expect output'
+test_expect_success 'rewrite on short argument (#2)' \
+	'test-gitopt whatchanged -S hello > output &&
+	 cmp expect output'
+test_expect_success 'rewrite on --short argument (#3)' \
+	'test-gitopt whatchanged --S hello > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-Shello'
+02 '(null)'
+EOF
+test_expect_success 'rewrite on short argument (leading "=" arg) (#1)' \
+	'test-gitopt whatchanged --S=hello > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-C20'
+02 '-M'
+03 '(null)'
+EOF
+test_expect_success 'pass optional arg (#1)' \
+	'test-gitopt whatchanged -C20 -M > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-C'
+02 '--find-copies-harder'
+03 '(null)'
+EOF
+test_expect_success 'detect optional arg bogus (#1)' \
+	'test-gitopt whatchanged -C --find-copies-harder > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-C20'
+02 '(null)'
+EOF
+test_expect_success 'pass optional arg (#2)' \
+	'test-gitopt whatchanged -C20 > output &&
+	 cmp expect output'
+# test_expect_success 'pass optional arg (#3)' \
+	# 'test-gitopt whatchanged -C=20 > output &&
+	 # cmp expect output'
+
+cat > expect <<EOF
+00 'checkout'
+01 '-b'
+02 'newbranch'
+03 '(null)'
+EOF
+test_expect_success 'rewrite short split arg (#1)' \
+	'test-gitopt checkout -bnewbranch > output &&
+	 cmp expect output'
+# test_expect_success 'rewrite short split arg (#2)' \
+	# 'test-gitopt checkout --b=newbranch > output &&
+	 # cmp expect output'
+test_expect_success 'rewrite short sanity check' \
+	'test-gitopt checkout -b newbranch > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'log'
+01 '--default'
+02 'dunno'
+03 '--all'
+04 '(null)'
+EOF
+test_expect_success 'rewrite long split arg' \
+	'test-gitopt log --default=dunno --all > output &&
+	 cmp expect output'
+test_expect_success 'rewrite long sanity check' \
+	'test-gitopt log --default dunno --all > output &&
+	 cmp expect output'
+
+cat > expect <<EOF
+00 'log'
+01 '--max-count=56'
+02 '(null)'
+EOF
+test_expect_success 'rewrite -<num> => --max-count=<num>' \
+	'test-gitopt log -56 > output && cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-u'
+02 '-l20'
+03 '-p'
+04 '-Spicktoken'
+05 '(null)'
+EOF
+test_expect_success 'bundle options with integer args mixed in' \
+	'test-gitopt whatchanged -ul20pSpicktoken > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-u'
+02 '-C20'
+03 '-p'
+04 '(null)'
+EOF
+test_expect_success 'bundle options with optional integer args used' \
+	'test-gitopt whatchanged -uC20p > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-u'
+02 '-C'
+03 '-p'
+04 '(null)'
+EOF
+test_expect_success 'bundle options with optional integer args not used' \
+	'test-gitopt whatchanged -uCp > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'whatchanged'
+01 '-C'
+02 '20'
+03 '(null)'
+EOF
+test_expect_success 'optional integer arg switch must be attached' \
+	'test-gitopt whatchanged -C 20 > output &&
+	cmp expect output'
+
+cat > expect <<EOF
+00 'commit'
+01 '--message'
+02 'hello world'
+03 '-s'
+04 'file'
+05 '(null)'
+EOF
+test_expect_success 'long option argument parsing when short option can work' \
+	'test-gitopt commit --message "hello world" -s file > output &&
+	cmp expect output'
+
+test_done
diff --git a/test-gitopt.c b/test-gitopt.c
new file mode 100644
index 0000000..2692e2b
--- /dev/null
+++ b/test-gitopt.c
@@ -0,0 +1,112 @@
+
+#include "git-compat-util.h"
+#include "gitopt.h"
+#include "gitopt/sh.h"
+
+#define _rev \
+{ "max-count",		'n',	"--max-count=%s",	ARG_INT,	0 }, \
+{ 0,			' ',	"--max-count=%s",	ARG_INT,	0 }, \
+{ "max-age",		0,	"--max-age=%s",		ARG_INT,	0 }, \
+{ "min-age",		0,	"--min-age=%s",		ARG_INT,	0 }, \
+{ "since",		0,	"--since=%s",		ARG_ONE,	0 }, \
+{ "after",		0,	"--after=%s",		ARG_ONE,	0 }, \
+{ "before",		0,	"--before=%s",		ARG_ONE,	0 }, \
+{ "until",		0,	"--until=%s",		ARG_ONE,	0 }, \
+{ "all",		0,	0,			0,		0 }, \
+{ "not",		0,	0,			0,		0 }, \
+{ "default",		0,	"--default %s",		ARG_ONE,	0 }, \
+{ "topo-order",		0,	0,			0,		0 }, \
+{ "date-order",		0,	0,			0,		0 }, \
+{ "parents",		0,	0,			0,		0 }, \
+{ "dense",		0,	0,			0,		0 }, \
+{ "sparse",		0,	0,			0,		0 }, \
+{ "remove-empty",	0,	0,			0,		0 }, \
+{ "no-merges",		0,	0,			0,		0 }, \
+{ "boundary",		0,	0,			0,		0 }, \
+{ "objects",		0,	0,			0,		0 }, \
+{ "objects-edge",	0,	0,			0,		0 }, \
+{ "unpacked",		0,	0,			0,		0 }, \
+{ 0,			'r',	0,			0,		0 }, \
+{ 0,			't',	0,			0,		0 }, \
+{ 0,			'm',	0,			0,		0 }, \
+{ 0,			'c',	0,			0,		0 }, \
+{ "cc",			0,	0,			0,		0 }, \
+{ 0,			'v',	0,			0,		0 }, \
+{ "pretty",		0,	"--pretty=%s",		ARG_ONE,	0 }, \
+{ "root",		0,	0,			0,		0 }, \
+{ "no-commit-id",	0,	0,			0,		0 }, \
+{ "always",		0,	0,			0,		0 }, \
+{ "no-abbrev",		0,	0,			0,		0 }, \
+{ "abbrev",		0,	0,			0,		0 }, \
+{ "abbrev-commit",	0,	0,			0,		0 }, \
+{ "full-diff",		0,	0,			0,		0 }, \
+
+#define _diff \
+{ 0,			'u',	0,			0,		0 }, \
+{ 0,			'p',	0,			0,		0 }, \
+{ "patch-with-raw",	0,	0,			0,		0 }, \
+{ "stat",		0,	0,			0,		0 }, \
+{ "patch-with-stat",	0,	0,			0,		0 }, \
+{ 0,			'z',	0,			0,		0 }, \
+{ 0,			'l',	"-l%s",			ARG_INT,	0 }, \
+{ "full-index",		0,	0,			0,		0 }, \
+{ "name-only",		0,	0,			0,		0 }, \
+{ "name-status",	0,	0,			0,		0 }, \
+{ 0,			'R',	0,			0,		0 }, \
+{ 0,			'S',	"-S%s",			ARG_ONE,	0 }, \
+{ 0,			's',	0,			0,		0 }, \
+{ 0,			'O',	"-O%s",			ARG_ONE,	0 }, \
+{ "diff-filter",	0,	"--diff-filter=%s",	ARG_ONE,	0 }, \
+{ "pickaxe-all",	0,	0,			0,		0 }, \
+{ "pickaxe-regex",	0,	0,			0,		0 }, \
+{ 0,			'B',	"-B%s",			ARG_OPTINT,	0 }, \
+{ 0,			'M',	"-M%s",			ARG_OPTINT,	0 }, \
+{ 0,			'C',	"-C%s",			ARG_OPTINT,	0 }, \
+{ "find-copies-harder",	0,	0,			0,		0 }, \
+{ "abbrev",		0,	"--abbrev=%s",		ARG_OPT,	0 }, \
+
+#define end {0,0}
+
+static const struct opt_spec ost_log[] = { _rev end };
+static const struct opt_spec ost_rev_list[] = { _rev end };
+static const struct opt_spec ost_whatchanged[] = { _diff _rev end };
+static const struct opt_spec ost_show[] = { _diff _rev end };
+
+static const struct opt_spec_map {
+	const char *cmd;
+	const struct opt_spec *ost;
+} opt_specs[] = {
+	{ "checkout",		ost_checkout },
+	{ "commit",		ost_commit },
+	{ "log",		ost_log },
+	{ "rev-list",		ost_rev_list },
+	{ "show",		ost_show },
+	{ "whatchanged",	ost_whatchanged },
+};
+
+
+int main(int argc, const char **argv, char **envp)
+{
+	int i;
+	struct exec_args *ea;
+	const struct opt_spec *ost = NULL;
+
+	if (!argv[1])
+		usage("test-gitopt [<options>] <command> [<options>]");
+
+	for (i = 0; i < ARRAY_SIZE(opt_specs); i++) {
+		if (!strcmp(argv[1], opt_specs[i].cmd)) {
+			ost = opt_specs[i].ost;
+			break;
+		}
+	}
+	if (!ost)
+		usage("test-gitopt [<options>] <command> [<options>]");
+
+	ea = gitopt_parse_ost(ost, argc - 1, argv + 1);
+
+	for (i = 0; i <= ea->argc; i++)
+		printf("%02d '%s'\n", i, ea->argv[i] ? ea->argv[i] : "(null)");
+
+	return 0;
+}
-- 
1.3.2.g0a3ae

```

## Eric Wong, 2006-05-09 05:06

Subject: [PATCH 2/6] update-index: convert to using gitopt
Message-ID: <11471512101532-git-send-email-normalperson@yhbt.net>
URL: https://gitlist.dev/e/11471512101532-git-send-email-normalperson%40yhbt.net
In-Reply-To: <11471512103526-git-send-email-normalperson@yhbt.net>

```
This is an example how how to override gitopt_non_option_cb, as
well as disabling permuting of options to the front of the
command list.  This way, commands like:

	git-update-index --add new_file --remove old_file

will continue to work.

Signed-off-by: Eric Wong <normalperson@yhbt.net>

---

 update-index.c |  261 ++++++++++++++++++++++++++++----------------------------
 1 files changed, 130 insertions(+), 131 deletions(-)

50f33d232367e8d7a46bd014f5a5da4d0ad370b3
diff --git a/update-index.c b/update-index.c
index 00cde70..6205f85 100644
--- a/update-index.c
+++ b/update-index.c
@@ -7,6 +7,7 @@ #include "cache.h"
 #include "strbuf.h"
 #include "quote.h"
 #include "tree-walk.h"
+#include "gitopt.h"
 
 /*
  * Default to not allowing changes to the list of files. The
@@ -25,6 +26,14 @@ static int info_only;
 static int force_remove;
 static int verbose;
 static int mark_valid_only = 0;
+static int prefix_length = 0;
+static int line_termination = '\n';
+static const char *prefix = NULL;
+static char set_executable_bit = 0;
+static int read_from_stdin = 0;
+static int newfd = 0;
+static int has_errors = 0;
+
 #define MARK_VALID 1
 #define UNMARK_VALID 2
 
@@ -189,10 +198,9 @@ static struct cache_entry *refresh_entry
 	return updated;
 }
 
-static int refresh_cache(int really)
+static void refresh_cache(int really)
 {
 	int i;
-	int has_errors = 0;
 
 	for (i = 0; i < active_nr; i++) {
 		struct cache_entry *ce, *new;
@@ -205,7 +213,7 @@ static int refresh_cache(int really)
 			if (allow_unmerged)
 				continue;
 			printf("%s: needs merge\n", ce->name);
-			has_errors = 1;
+			has_errors |= 1;
 			continue;
 		}
 
@@ -225,7 +233,7 @@ static int refresh_cache(int really)
 			if (quiet)
 				continue;
 			printf("%s: needs update\n", ce->name);
-			has_errors = 1;
+			has_errors |= 1;
 			continue;
 		}
 		active_cache_changed = 1;
@@ -234,7 +242,7 @@ static int refresh_cache(int really)
 		 * from mmap(). */
 		active_cache[i] = new;
 	}
-	return has_errors;
+	return;
 }
 
 /*
@@ -600,15 +608,123 @@ static int do_unresolve(int ac, const ch
 	return err;
 }
 
+static void write_cache_if_changed ()
+{
+	if (active_cache_changed) {
+		if (write_cache(newfd, active_cache, active_nr) ||
+		    commit_index_file(&cache_file))
+			die("Unable to write new cachefile");
+	}
+}
+
+gitopt_eat(opt_q, quiet = 1;)
+gitopt_eat(opt_add, allow_add = 1;)
+gitopt_eat(opt_replace, allow_replace = 1;)
+gitopt_eat(opt_remove, allow_remove = 1;)
+gitopt_eat(opt_unmerged, allow_unmerged = 1;)
+gitopt_eat(opt_refresh, refresh_cache(0);)
+gitopt_eat(opt_really_refresh, refresh_cache(1);)
+
+static struct exec_args *opt_cacheinfo(const struct opt_spec *s,
+			const int argc, const char **argv, int *argc_pos)
+{
+	struct exec_args *ea = new_exec_args(4);
+	unsigned char sha1[20];
+	unsigned int mode;
+
+	ea->argc = 1;
+	ea->argv[0] = argv[*argc_pos];
+	while (*argc_pos < (argc-1) && ea->argc < 4)
+		ea->argv[ea->argc++] = argv[++(*argc_pos)];
+	if (ea->argc != 4)
+		die("git-update-index: --cacheinfo <mode> <sha1> <path>");
+
+	if ((sscanf(ea->argv[1], "%o", &mode) != 1) ||
+				get_sha1_hex(ea->argv[2], sha1) ||
+				add_cacheinfo(mode, sha1, ea->argv[3], 0))
+		die("git-update-index: --cacheinfo cannot add %s",ea->argv[3]);
+
+	return nil_exec_args(ea);
+}
+
+gitopt_eat_one_arg(opt_chmod,
+		if (*argc_pos + 1 >= argc)
+			die("git-update-index: --chmod=%s <path>",ea->argv[0]);
+		set_executable_bit = ea->argv[0][0];)
+
+gitopt_eat(opt_assume_unchanged, mark_valid_only = MARK_VALID;)
+gitopt_eat(opt_no_assume_unchanged, mark_valid_only = UNMARK_VALID;)
+gitopt_eat(opt_info_only, info_only = 1;)
+gitopt_eat(opt_force_remove, force_remove = 1;)
+gitopt_eat(opt_z, line_termination = 0;)
+gitopt_eat(opt_stdin,
+		if (*argc_pos != argc - 1)
+			die("--stdin must be at the end");
+		read_from_stdin = 1;)
+
+gitopt_eat(opt_index_info,
+		if (*argc_pos != argc - 1)
+			die("--index-info must be at the end");
+		allow_add = allow_replace = allow_remove = 1;
+		read_index_info(line_termination);)
+
+gitopt_eat(opt_unresolve,
+		has_errors = do_unresolve(argc - *argc_pos, argv + *argc_pos,
+					prefix, prefix_length);
+		if (has_errors)
+			active_cache_changed = 0;
+		write_cache_if_changed();
+		exit(has_errors ? 1 : 0);)
+
+gitopt_eat(opt_ignore_missing, not_new = 1;)
+gitopt_eat(opt_verbose, verbose = 1;)
+gitopt_eat(opt_h, usage(update_index_usage);)
+
+static const struct opt_spec update_index_ost[] = {
+	{ 0,			'q',	0,	0,	opt_q },
+	{ "add",		0,	0,	0,	opt_add },
+	{ "replace",		0,	0,	0,	opt_replace },
+	{ "remove",		0,	0,	0,	opt_remove },
+	{ "unmerged",		0,	0,	0,	opt_unmerged },
+	{ "refresh",		0,	0,	0,	opt_refresh },
+	{ "really-refresh",	0,	0,	0,	opt_really_refresh },
+	{ "cacheinfo",		0,	0,	ARG_TRE, opt_cacheinfo},
+	{ "chmod",		0,	"%s",	ARG_ONE, opt_chmod },
+	{ "assume-unchanged",	0,	0,	0,	opt_assume_unchanged },
+	{ "no-assume-unchanged",0,	0,	0,	opt_no_assume_unchanged},
+	{ "info-only",		0,	0,	0,	opt_info_only },
+	{ "force-remove",	0,	0,	0,	opt_force_remove },
+	{ 0,			'z',	0,	0,	opt_z },
+	{ "stdin",		0,	0,	0,	opt_stdin },
+	{ "index-info",		0,	0,	0,	opt_index_info },
+	{ "unresolve",		0,	0,	0,	opt_unresolve },
+	{ "ignore-missing",	0,	0,	0,	opt_ignore_missing },
+	{ "verbose",		0,	0,	0,	opt_verbose },
+	{ "help",		'h',	0,	0,	opt_h },
+	{ 0, 0 }
+};
+
+/* call this for every non-option (and everything after "--") we have */
+static void update_index_non_option_cb(struct exec_args *b, const int argc,
+					const char **argv, int *argc_pos)
+{
+	const char *arg = argv[*argc_pos];
+	update_one(arg, prefix, prefix_length);
+	if (set_executable_bit)
+		chmod_path(set_executable_bit, arg);
+}
+
 int main(int argc, const char **argv)
 {
-	int i, newfd, entries, has_errors = 0, line_termination = '\n';
-	int allow_options = 1;
-	int read_from_stdin = 0;
-	const char *prefix = setup_git_directory();
-	int prefix_length = prefix ? strlen(prefix) : 0;
-	char set_executable_bit = 0;
+	int entries;
+	struct exec_args *a;
+
+	prefix = setup_git_directory();
+	if (prefix)
+		prefix_length = strlen(prefix);
 
+	a = new_exec_args(argc);	/* argv[0] and options: */
+	gitopt_non_option_cb = update_index_non_option_cb;
 	git_config(git_default_config);
 
 	newfd = hold_index_file_for_update(&cache_file, get_index_file());
@@ -619,120 +735,9 @@ int main(int argc, const char **argv)
 	if (entries < 0)
 		die("cache corrupted");
 
-	for (i = 1 ; i < argc; i++) {
-		const char *path = argv[i];
+	if (gitopt_parse_ost_split(a, a, update_index_ost, argc, argv) < 0)
+		usage(update_index_usage);
 
-		if (allow_options && *path == '-') {
-			if (!strcmp(path, "--")) {
-				allow_options = 0;
-				continue;
-			}
-			if (!strcmp(path, "-q")) {
-				quiet = 1;
-				continue;
-			}
-			if (!strcmp(path, "--add")) {
-				allow_add = 1;
-				continue;
-			}
-			if (!strcmp(path, "--replace")) {
-				allow_replace = 1;
-				continue;
-			}
-			if (!strcmp(path, "--remove")) {
-				allow_remove = 1;
-				continue;
-			}
-			if (!strcmp(path, "--unmerged")) {
-				allow_unmerged = 1;
-				continue;
-			}
-			if (!strcmp(path, "--refresh")) {
-				has_errors |= refresh_cache(0);
-				continue;
-			}
-			if (!strcmp(path, "--really-refresh")) {
-				has_errors |= refresh_cache(1);
-				continue;
-			}
-			if (!strcmp(path, "--cacheinfo")) {
-				unsigned char sha1[20];
-				unsigned int mode;
-
-				if (i+3 >= argc)
-					die("git-update-index: --cacheinfo <mode> <sha1> <path>");
-
-				if ((sscanf(argv[i+1], "%o", &mode) != 1) ||
-				    get_sha1_hex(argv[i+2], sha1) ||
-				    add_cacheinfo(mode, sha1, argv[i+3], 0))
-					die("git-update-index: --cacheinfo"
-					    " cannot add %s", argv[i+3]);
-				i += 3;
-				continue;
-			}
-			if (!strcmp(path, "--chmod=-x") ||
-			    !strcmp(path, "--chmod=+x")) {
-				if (argc <= i+1)
-					die("git-update-index: %s <path>", path);
-				set_executable_bit = path[8];
-				continue;
-			}
-			if (!strcmp(path, "--assume-unchanged")) {
-				mark_valid_only = MARK_VALID;
-				continue;
-			}
-			if (!strcmp(path, "--no-assume-unchanged")) {
-				mark_valid_only = UNMARK_VALID;
-				continue;
-			}
-			if (!strcmp(path, "--info-only")) {
-				info_only = 1;
-				continue;
-			}
-			if (!strcmp(path, "--force-remove")) {
-				force_remove = 1;
-				continue;
-			}
-			if (!strcmp(path, "-z")) {
-				line_termination = 0;
-				continue;
-			}
-			if (!strcmp(path, "--stdin")) {
-				if (i != argc - 1)
-					die("--stdin must be at the end");
-				read_from_stdin = 1;
-				break;
-			}
-			if (!strcmp(path, "--index-info")) {
-				if (i != argc - 1)
-					die("--index-info must be at the end");
-				allow_add = allow_replace = allow_remove = 1;
-				read_index_info(line_termination);
-				break;
-			}
-			if (!strcmp(path, "--unresolve")) {
-				has_errors = do_unresolve(argc - i, argv + i,
-							  prefix, prefix_length);
-				if (has_errors)
-					active_cache_changed = 0;
-				goto finish;
-			}
-			if (!strcmp(path, "--ignore-missing")) {
-				not_new = 1;
-				continue;
-			}
-			if (!strcmp(path, "--verbose")) {
-				verbose = 1;
-				continue;
-			}
-			if (!strcmp(path, "-h") || !strcmp(path, "--help"))
-				usage(update_index_usage);
-			die("unknown option %s", path);
-		}
-		update_one(path, prefix, prefix_length);
-		if (set_executable_bit)
-			chmod_path(set_executable_bit, path);
-	}
 	if (read_from_stdin) {
 		struct strbuf buf;
 		strbuf_init(&buf);
@@ -757,12 +762,6 @@ int main(int argc, const char **argv)
 		}
 	}
 
- finish:
-	if (active_cache_changed) {
-		if (write_cache(newfd, active_cache, active_nr) ||
-		    commit_index_file(&cache_file))
-			die("Unable to write new cachefile");
-	}
-
+	write_cache_if_changed();
 	return has_errors ? 1 : 0;
 }
-- 
1.3.2.g0a3ae

```

## Eric Wong, 2006-05-09 05:06

Subject: [PATCH 3/6] ls-tree: convert to gitopt
Message-ID: <1147151211399-git-send-email-normalperson@yhbt.net>
URL: https://gitlist.dev/e/1147151211399-git-send-email-normalperson%40yhbt.net
In-Reply-To: <11471512101532-git-send-email-normalperson@yhbt.net>

```
A pretty simple and straight forward conversion, imho.

Signed-off-by: Eric Wong <normalperson@yhbt.net>

---

 gitopt/abbrev.h |   24 +++++++++++++++++
 ls-tree.c       |   79 +++++++++++++++++++++++--------------------------------
 2 files changed, 57 insertions(+), 46 deletions(-)
 create mode 100644 gitopt/abbrev.h

06d5dc3649e7407f7ff8df0d42a55a906b39cb39
diff --git a/gitopt/abbrev.h b/gitopt/abbrev.h
new file mode 100644
index 0000000..c3b2353
--- /dev/null
+++ b/gitopt/abbrev.h
@@ -0,0 +1,24 @@
+#include "../cache.h"
+
+/* we specify rewrite_fmt here to make opt_abbrev() simpler: */
+#define abbrev_ost_row \
+	{ "abbrev",		0,	"a %s",	ARG_OPTINT,	opt_abbrev }
+
+#define gitopt_opt_abbrev(dest) \
+static struct exec_args *opt_abbrev(const struct opt_spec *s, \
+			const int argc, const char **argv, int *argc_pos) \
+{ \
+	struct exec_args *ea = optional_int_arg(s, argc, argv, argc_pos); \
+	if (!ea) return NULL; \
+	if (ea->argc == 1) \
+		dest = DEFAULT_ABBREV; \
+	else { \
+		dest = strtoul(ea->argv[1], NULL, 10); \
+		if (dest && dest < MINIMUM_ABBREV) \
+			dest = MINIMUM_ABBREV; \
+		else if (dest > 40) \
+			dest = 40; \
+	} \
+	return nil_exec_args(ea); \
+}
+
diff --git a/ls-tree.c b/ls-tree.c
index f2b3bc1..4fb6343 100644
--- a/ls-tree.c
+++ b/ls-tree.c
@@ -7,6 +7,8 @@ #include "cache.h"
 #include "blob.h"
 #include "tree.h"
 #include "quote.h"
+#include "gitopt.h"
+#include "gitopt/abbrev.h"
 
 static int line_termination = '\n';
 #define LS_RECURSIVE 1
@@ -84,68 +86,53 @@ static int show_tree(unsigned char *sha1
 	return retval;
 }
 
+gitopt_eat(opt_d, ls_options |= LS_TREE_ONLY;)
+gitopt_eat(opt_t, ls_options |= LS_SHOW_TREES;)
+gitopt_eat(opt_z, line_termination = 0;)
+gitopt_eat(opt_r, ls_options |= LS_RECURSIVE;)
+gitopt_eat(opt_name_only_status, ls_options |= LS_NAME_ONLY;)
+gitopt_eat(opt_full_name, chomp_prefix = 0;)
+gitopt_opt_abbrev(abbrev)
+
+static const struct opt_spec ls_tree_ost[] = {
+	{ 0,			'z',	0,	0,	opt_z },
+	{ 0,			'r',	0,	0,	opt_r },
+	{ 0,			'd',	0,	0,	opt_d },
+	{ 0,			't',	0,	0,	opt_t },
+	{ "name-only",		0,	0,	0,	opt_name_only_status },
+	{ "name-status",	0,	0,	0,	opt_name_only_status },
+	{ "full-name",		0,	0,	0,	opt_full_name },
+	abbrev_ost_row,
+	{ 0, 0 }
+};
+
 int main(int argc, const char **argv)
 {
 	unsigned char sha1[20];
 	struct tree *tree;
+	struct exec_args *a = new_exec_args(argc); /* argv[0] and options: */
+	struct exec_args *b = new_exec_args(argc); /* non-option args */
 
 	prefix = setup_git_directory();
 	git_config(git_default_config);
 	if (prefix && *prefix)
 		chomp_prefix = strlen(prefix);
-	while (1 < argc && argv[1][0] == '-') {
-		switch (argv[1][1]) {
-		case 'z':
-			line_termination = 0;
-			break;
-		case 'r':
-			ls_options |= LS_RECURSIVE;
-			break;
-		case 'd':
-			ls_options |= LS_TREE_ONLY;
-			break;
-		case 't':
-			ls_options |= LS_SHOW_TREES;
-			break;
-		case '-':
-			if (!strcmp(argv[1]+2, "name-only") ||
-			    !strcmp(argv[1]+2, "name-status")) {
-				ls_options |= LS_NAME_ONLY;
-				break;
-			}
-			if (!strcmp(argv[1]+2, "full-name")) {
-				chomp_prefix = 0;
-				break;
-			}
-			if (!strncmp(argv[1]+2, "abbrev=",7)) {
-				abbrev = strtoul(argv[1]+9, NULL, 10);
-				if (abbrev && abbrev < MINIMUM_ABBREV)
-					abbrev = MINIMUM_ABBREV;
-				else if (abbrev > 40)
-					abbrev = 40;
-				break;
-			}
-			if (!strcmp(argv[1]+2, "abbrev")) {
-				abbrev = DEFAULT_ABBREV;
-				break;
-			}
-			/* otherwise fallthru */
-		default:
-			usage(ls_tree_usage);
-		}
-		argc--; argv++;
-	}
+
+	if (gitopt_parse_ost_split(a, b, ls_tree_ost, argc, argv) < 0)
+		usage(ls_tree_usage);
+	free_exec_args(a);
+
 	/* -d -r should imply -t, but -d by itself should not have to. */
 	if ( (LS_TREE_ONLY|LS_RECURSIVE) ==
 	    ((LS_TREE_ONLY|LS_RECURSIVE) & ls_options))
 		ls_options |= LS_SHOW_TREES;
 
-	if (argc < 2)
+	if (b->argc < 1)
 		usage(ls_tree_usage);
-	if (get_sha1(argv[1], sha1))
-		die("Not a valid object name %s", argv[1]);
+	if (get_sha1(b->argv[0], sha1))
+		die("Not a valid object name %s", b->argv[0]);
 
-	pathspec = get_pathspec(prefix, argv + 2);
+	pathspec = get_pathspec(prefix, b->argv + 1);
 	tree = parse_tree_indirect(sha1);
 	if (!tree)
 		die("not a tree object");
-- 
1.3.2.g0a3ae

```

## Eric Wong, 2006-05-09 05:06

Subject: [PATCH 4/6] ls-files: convert to using gitopt
Message-ID: <11471512121152-git-send-email-normalperson@yhbt.net>
URL: https://gitlist.dev/e/11471512121152-git-send-email-normalperson%40yhbt.net
In-Reply-To: <1147151211399-git-send-email-normalperson@yhbt.net>

```
Another simple and straight forward conversion, imho.

Signed-off-by: Eric Wong <normalperson@yhbt.net>

---

 ls-files.c |  187 ++++++++++++++++++++++--------------------------------------
 1 files changed, 69 insertions(+), 118 deletions(-)

6a65d6f1e59638185c846920d8c6f0dbd82f1bf7
diff --git a/ls-files.c b/ls-files.c
index 4a4af1c..62f9e10 100644
--- a/ls-files.c
+++ b/ls-files.c
@@ -10,6 +10,8 @@ #include <fnmatch.h>
 
 #include "cache.h"
 #include "quote.h"
+#include "gitopt.h"
+#include "gitopt/abbrev.h"
 
 static int abbrev = 0;
 static int show_deleted = 0;
@@ -648,133 +650,82 @@ static const char ls_files_usage[] =
 	"[ --exclude-per-directory=<filename> ] [--full-name] [--abbrev] "
 	"[--] [<file>]*";
 
+static void tag_pfx()
+{
+	tag_cached = "H ";
+	tag_unmerged = "M ";
+	tag_removed = "R ";
+	tag_modified = "C ";
+	tag_other = "? ";
+	tag_killed = "K ";
+}
+
+static int exc_given = 0;
+
+gitopt_eat(opt_z, line_terminator = 0;)
+gitopt_eat(opt_t, tag_pfx();)
+gitopt_eat(opt_v, tag_pfx(); show_valid_bit = 1;)
+gitopt_eat(opt_c, show_cached = 1;)
+gitopt_eat(opt_d, show_deleted = 1;)
+gitopt_eat(opt_m, show_modified = 1;)
+gitopt_eat(opt_o, show_others = 1;)
+gitopt_eat(opt_i, show_ignored = 1;)
+gitopt_eat(opt_s, show_stage = 1;)
+gitopt_eat(opt_k, show_killed = 1;)
+gitopt_eat(opt_directory, show_other_directories = 1;)
+gitopt_eat(opt_no_empty_directory, hide_empty_directories = 1;)
+gitopt_eat(opt_u, show_stage = 1; show_unmerged = 1;)
+gitopt_eat_one_arg(opt_x,
+		exc_given = 1;
+		add_exclude(ea->argv[0], "", 0, &exclude_list[EXC_CMDL]);)
+gitopt_eat_one_arg(opt_X,
+		exc_given = 1;
+		add_excludes_from_file(ea->argv[0]);)
+gitopt_eat_one_arg(opt_exclude_per_dir,
+		exc_given = 1;
+		exclude_per_dir = ea->argv[0];)
+gitopt_eat(opt_full_name, prefix_offset = 0;)
+gitopt_eat(opt_error_unmatch, error_unmatch = 1;)
+gitopt_opt_abbrev(abbrev)
+
+static const struct opt_spec ls_files_ost[] = {
+	{ 0,			'z',	0,	0,	opt_z },
+	{ 0,			'v',	0,	0,	opt_v },
+	{ 0,			't',	0,	0,	opt_t },
+	{ "cached",		'c',	0,	0,	opt_c },
+	{ "deleted",		'd',	0,	0,	opt_d },
+	{ "modified",		'm',	0,	0,	opt_m },
+	{ "others",		'o',	0,	0,	opt_o },
+	{ "ignored",		'i',	0,	0,	opt_i },
+	{ "stage",		's',	0,	0,	opt_s },
+	{ "killed",		'k',	0,	0,	opt_k },
+	{ "directory",		0,	0,	0,	opt_directory },
+	{ "no-empty-directory",	0,	0,	0,	opt_no_empty_directory},
+	{ "unmerged",		'u',	0,	0,	opt_u },
+	{ "exclude",		'x',	"%s",	ARG_ONE,	opt_x },
+	{ "exclude-from",	'X',	"%s",	ARG_ONE,	opt_X },
+	{ "exclude-per-directory",0,	"%s",	ARG_ONE, opt_exclude_per_dir },
+	{ "full-name",		0,	0,	0,	opt_full_name },
+	{ "error-unmatch",	0,	0,	0,	opt_error_unmatch },
+	abbrev_ost_row,
+	{ 0, 0 }
+};
+
 int main(int argc, const char **argv)
 {
-	int i;
-	int exc_given = 0;
+	struct exec_args *a = new_exec_args(argc); /* argv[0] and options: */
+	struct exec_args *b = new_exec_args(argc); /* non-option args */
 
 	prefix = setup_git_directory();
 	if (prefix)
 		prefix_offset = strlen(prefix);
 	git_config(git_default_config);
 
-	for (i = 1; i < argc; i++) {
-		const char *arg = argv[i];
-
-		if (!strcmp(arg, "--")) {
-			i++;
-			break;
-		}
-		if (!strcmp(arg, "-z")) {
-			line_terminator = 0;
-			continue;
-		}
-		if (!strcmp(arg, "-t") || !strcmp(arg, "-v")) {
-			tag_cached = "H ";
-			tag_unmerged = "M ";
-			tag_removed = "R ";
-			tag_modified = "C ";
-			tag_other = "? ";
-			tag_killed = "K ";
-			if (arg[1] == 'v')
-				show_valid_bit = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-c") || !strcmp(arg, "--cached")) {
-			show_cached = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-d") || !strcmp(arg, "--deleted")) {
-			show_deleted = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-m") || !strcmp(arg, "--modified")) {
-			show_modified = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-o") || !strcmp(arg, "--others")) {
-			show_others = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-i") || !strcmp(arg, "--ignored")) {
-			show_ignored = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-s") || !strcmp(arg, "--stage")) {
-			show_stage = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-k") || !strcmp(arg, "--killed")) {
-			show_killed = 1;
-			continue;
-		}
-		if (!strcmp(arg, "--directory")) {
-			show_other_directories = 1;
-			continue;
-		}
-		if (!strcmp(arg, "--no-empty-directory")) {
-			hide_empty_directories = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-u") || !strcmp(arg, "--unmerged")) {
-			/* There's no point in showing unmerged unless
-			 * you also show the stage information.
-			 */
-			show_stage = 1;
-			show_unmerged = 1;
-			continue;
-		}
-		if (!strcmp(arg, "-x") && i+1 < argc) {
-			exc_given = 1;
-			add_exclude(argv[++i], "", 0, &exclude_list[EXC_CMDL]);
-			continue;
-		}
-		if (!strncmp(arg, "--exclude=", 10)) {
-			exc_given = 1;
-			add_exclude(arg+10, "", 0, &exclude_list[EXC_CMDL]);
-			continue;
-		}
-		if (!strcmp(arg, "-X") && i+1 < argc) {
-			exc_given = 1;
-			add_excludes_from_file(argv[++i]);
-			continue;
-		}
-		if (!strncmp(arg, "--exclude-from=", 15)) {
-			exc_given = 1;
-			add_excludes_from_file(arg+15);
-			continue;
-		}
-		if (!strncmp(arg, "--exclude-per-directory=", 24)) {
-			exc_given = 1;
-			exclude_per_dir = arg + 24;
-			continue;
-		}
-		if (!strcmp(arg, "--full-name")) {
-			prefix_offset = 0;
-			continue;
-		}
-		if (!strcmp(arg, "--error-unmatch")) {
-			error_unmatch = 1;
-			continue;
-		}
-		if (!strncmp(arg, "--abbrev=", 9)) {
-			abbrev = strtoul(arg+9, NULL, 10);
-			if (abbrev && abbrev < MINIMUM_ABBREV)
-				abbrev = MINIMUM_ABBREV;
-			else if (abbrev > 40)
-				abbrev = 40;
-			continue;
-		}
-		if (!strcmp(arg, "--abbrev")) {
-			abbrev = DEFAULT_ABBREV;
-			continue;
-		}
-		if (*arg == '-')
-			usage(ls_files_usage);
-		break;
-	}
+	if (gitopt_parse_ost_split(a, b, ls_files_ost, argc, argv) < 0)
+		usage(ls_files_usage);
+	free_exec_args(a);
 
-	pathspec = get_pathspec(prefix, argv + i);
+	pathspec = get_pathspec(prefix,b->argv);
 
 	/* Verify that the pathspec matches the prefix */
 	if (pathspec)
-- 
1.3.2.g0a3ae

```

## Timo Hirvonen, 2006-05-09 09:08

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060509120809.4d9494b9.tihirvon@gmail.com>
URL: https://gitlist.dev/e/20060509120809.4d9494b9.tihirvon%40gmail.com
In-Reply-To: <11471512103526-git-send-email-normalperson@yhbt.net>

```
Eric Wong <normalperson@yhbt.net> wrote:

>  * unbundling of short options: -uC20n20z => -u -C20 -n20 -z

Does anyone ever use this?  I think this makes sense only for flags that
don't have parameters but that would create an ugly special case. Is it
too hard to type "-u -C=20 -n=20 -z"?

>  * optional argument handling (-C<num>, -M<num>)
>    -C <num> (with a space between them) has not changed,
>    however, <num> can be a sha1, or a path

IMO optional arguments are usually bad idea.

    -C 2 (is "2" argument?)
    -C2  (-C=2 or -C -2?)

Better to make it obvious there's an argument

    -C=2

or not support optional arguments at all and "-C 2" becomes unambiguous.

-- 
http://onion.dynserv.net/~timo/

```

## Junio C Hamano, 2006-05-09 12:58

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <7v8xpb73sq.fsf@assigned-by-dhcp.cox.net>
URL: https://gitlist.dev/e/7v8xpb73sq.fsf%40assigned-by-dhcp.cox.net
In-Reply-To: <20060509120809.4d9494b9.tihirvon@gmail.com>

```
Timo Hirvonen <tihirvon@gmail.com> writes:

> Eric Wong <normalperson@yhbt.net> wrote:
>
>>  * unbundling of short options: -uC20n20z => -u -C20 -n20 -z
>
> Does anyone ever use this?  I think this makes sense only for flags that
> don't have parameters but that would create an ugly special case. Is it
> too hard to type "-u -C=20 -n=20 -z"?

I can already hear in my head that people would start talking
about "git understands insane abbeviations of options".  It
might be unambiguous, but that does not change it is a bit on
the insane side.  People would probably expect -nuz can be split
into -n -u -z, and the current handcrafted mess (although it is
more obvious and easy to work with when reading and maintaining
the existing code) is not abbreviation friendly, which we would
want to do something about.  But I think squashing options with
parameters together is going a bit too far.

> --with-r => --patch-with-raw works great

I personally think this also is too much.

```

## Eric Wong, 2006-05-09 19:18

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060509191803.GA3676@localdomain>
URL: https://gitlist.dev/e/20060509191803.GA3676%40localdomain
In-Reply-To: <20060509120809.4d9494b9.tihirvon@gmail.com>

```
Timo Hirvonen <tihirvon@gmail.com> wrote:
> Eric Wong <normalperson@yhbt.net> wrote:
> 
> >  * unbundling of short options: -uC20n20z => -u -C20 -n20 -z
> 
> Does anyone ever use this?  I think this makes sense only for flags that
> don't have parameters but that would create an ugly special case. Is it
> too hard to type "-u -C=20 -n=20 -z"?

It is more for me.  Many programs that I use already accept bundled
switches, and '=' and '-' are relatively far away and requires me
to stretch my hand uncomfortably (I have very small hands, and have
limited mobility in several fingers).

> >  * optional argument handling (-C<num>, -M<num>)
> >    -C <num> (with a space between them) has not changed,
> >    however, <num> can be a sha1, or a path
> 
> IMO optional arguments are usually bad idea.
> 
>     -C 2 (is "2" argument?)
>     -C2  (-C=2 or -C -2?)
> 
> Better to make it obvious there's an argument
> 
>     -C=2
> 
> or not support optional arguments at all and "-C 2" becomes unambiguous.

git has always supported optional argument handling like this.

I'm striving for backwards compatibility with existing usage.  That
means as a diff option, -C alone works, as does -C20.  I've made -C 20
_not_ work because it breaks existing usage (where 20 could be a
filename, or a tree-ish).  -C=20 would mean something
else, since I wanted to make pickaxe work exactly as it did before:
-S=var would search for '=var', not 'var'.

-- 
Eric Wong

```

## Eric Wong, 2006-05-09 19:39

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060509193932.GB3676@localdomain>
URL: https://gitlist.dev/e/20060509193932.GB3676%40localdomain
In-Reply-To: <7v8xpb73sq.fsf@assigned-by-dhcp.cox.net>

```
Junio C Hamano <junkio@cox.net> wrote:
> Timo Hirvonen <tihirvon@gmail.com> writes:
> 
> > Eric Wong <normalperson@yhbt.net> wrote:
> >
> >>  * unbundling of short options: -uC20n20z => -u -C20 -n20 -z
> >
> > Does anyone ever use this?  I think this makes sense only for flags that
> > don't have parameters but that would create an ugly special case. Is it
> > too hard to type "-u -C=20 -n=20 -z"?
> 
> I can already hear in my head that people would start talking
> about "git understands insane abbeviations of options".  It
> might be unambiguous, but that does not change it is a bit on
> the insane side.  People would probably expect -nuz can be split
> into -n -u -z, and the current handcrafted mess (although it is
> more obvious and easy to work with when reading and maintaining
> the existing code) is not abbreviation friendly, which we would
> want to do something about.  But I think squashing options with
> parameters together is going a bit too far.

I think numeric parameters are unambiguous when bundled.
I'm used to things like `diff -ru10p` working, *shrug*

Non-numeric parameters can only be used if the option is at the end of the
bundled string:

git commit -sam'this is my commit message'  would work

git commit -m'say hello' would also work

but git commit -mas'this is my commit message' would not work as intended
(where user wanted -a -s, too)

> > --with-r => --patch-with-raw works great
> 
> I personally think this also is too much.

There are (currently) two types of abbreviations, one is the prefix one used
commonly in shell scripts:  -e|--e|--ed|--edi|--edit.  I think this should
always be supported as most of our shell scripts already do.

The other one tokenizes on '-' first and looks for a prefix match after
each '-'.  I'd like to make that at least optional:

diff --git a/gitopt.c b/gitopt.c
index 056e163..9ca6025 100644
--- a/gitopt.c
+++ b/gitopt.c
gitopt.c
@@ -427,7 +427,7 @@ static void fallback_long(const struct o
        }
 
        /* ok, try harder, based on tokenization on '-' */
-       if (found < 0) {
+       if (found < 0 && getenv("GIT_ABBREV_HARDER")) {
                for (i = 0; ost[i].l || ost[i].s; i++) {
                        s = &(ost[i]);
                        if (s->l && opt_token_match(s,cur)) {

-- 
Eric Wong

```

## Timo Hirvonen, 2006-05-09 20:10

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060509231031.b62576da.tihirvon@gmail.com>
URL: https://gitlist.dev/e/20060509231031.b62576da.tihirvon%40gmail.com
In-Reply-To: <20060509191803.GA3676@localdomain>

```
Eric Wong <normalperson@yhbt.net> wrote:

> I'm striving for backwards compatibility with existing usage.  That
> means as a diff option, -C alone works, as does -C20.  I've made -C 20
> _not_ work because it breaks existing usage (where 20 could be a
> filename, or a tree-ish).  -C=20 would mean something
> else,

I think optional arguments are still confusing.  We could support both
-C (no args) and -C=20 syntax.

> since I wanted to make pickaxe work exactly as it did before:
> -S=var would search for '=var', not 'var'.

Some other options use -x=y syntax so this would be confusing. Pickaxe's
-Stext syntax is a bit strange.  I think -S text or -S=text would be
more logical.

-- 
http://onion.dynserv.net/~timo/

```

## Junio C Hamano, 2006-05-09 20:35

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <7vlktb2ayy.fsf@assigned-by-dhcp.cox.net>
URL: https://gitlist.dev/e/7vlktb2ayy.fsf%40assigned-by-dhcp.cox.net
In-Reply-To: <20060509231031.b62576da.tihirvon@gmail.com>

```
Timo Hirvonen <tihirvon@gmail.com> writes:

> I think optional arguments are still confusing.  We could support both
> -C (no args) and -C=20 syntax.

Actually, optional numeric arguments are the norm not exception.
Think of "diff -u" vs "diff -u20" for example.  Also I think it
is conventional not to use = for single-letter single-dash
options, so -C (no args -- use the default number of the
implementation whatever it is) and -C20 (the same behaviour in
principle as -C, but use my number instead of the default) are
sane, while -C=20 _is_ odd.

Having said that, I think abbreviating -u20 -n -r to -u20nr
going too far (-nru20 would be palatable perhaps), even if the
implementation allows such, unless you are entering "obfusucated
command line parameters" contest.

```

## Timo Hirvonen, 2006-05-09 21:08

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060510000826.1a708c03.tihirvon@gmail.com>
URL: https://gitlist.dev/e/20060510000826.1a708c03.tihirvon%40gmail.com
In-Reply-To: <7vlktb2ayy.fsf@assigned-by-dhcp.cox.net>

```
Junio C Hamano <junkio@cox.net> wrote:

> Timo Hirvonen <tihirvon@gmail.com> writes:
> 
> > I think optional arguments are still confusing.  We could support both
> > -C (no args) and -C=20 syntax.
> 
> Actually, optional numeric arguments are the norm not exception.
> Think of "diff -u" vs "diff -u20" for example.  Also I think it
> is conventional not to use = for single-letter single-dash
> options, so -C (no args -- use the default number of the
> implementation whatever it is) and -C20 (the same behaviour in
> principle as -C, but use my number instead of the default) are
> sane, while -C=20 _is_ odd.

OK, if we don't support bundling flags at all then -x=y and -xy would do
the same thing and pickaxe's -Stext would work too.  But we could not
make option flag parsing global then (-S=value -> search "=value" or
"value"?).

Maybe we should just change -Stext to -S=text or -S text.

Better to support only -x=y or -x y, not both.

> Having said that, I think abbreviating -u20 -n -r to -u20nr
> going too far

Yes

> (-nru20 would be palatable perhaps),

No :)

-- 
http://onion.dynserv.net/~timo/

```

## Junio C Hamano, 2006-05-09 21:11

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <7v7j4u3nv8.fsf@assigned-by-dhcp.cox.net>
URL: https://gitlist.dev/e/7v7j4u3nv8.fsf%40assigned-by-dhcp.cox.net
In-Reply-To: <20060510000826.1a708c03.tihirvon@gmail.com>

```
Timo Hirvonen <tihirvon@gmail.com> writes:

> Better to support only -x=y or -x y, not both.

Didn't I just say -x=y where x is a single letter _is_ odd?
It is either -xy or -x y, not -x=y.

```

## Timo Hirvonen, 2006-05-09 21:31

Subject: Re: [PATCH 1/6] gitopt: a new command-line option parser for git
Message-ID: <20060510003141.0d0820ed.tihirvon@gmail.com>
URL: https://gitlist.dev/e/20060510003141.0d0820ed.tihirvon%40gmail.com
In-Reply-To: <7v7j4u3nv8.fsf@assigned-by-dhcp.cox.net>

```
Junio C Hamano <junkio@cox.net> wrote:

> Timo Hirvonen <tihirvon@gmail.com> writes:
> 
> > Better to support only -x=y or -x y, not both.
> 
> Didn't I just say -x=y where x is a single letter _is_ odd?
> It is either -xy or -x y, not -x=y.

Oh, I thought parameters would use same syntax for short and long
options.  For optional args -C2 would make sense but -C 2 would be
ambiguous ("-C -- 2" or "-C2"?).  Maybe I'm just too drunk to
understand.

-- 
http://onion.dynserv.net/~timo/

```
