Skip to content

Commit 41c1488

Browse files
committed
refactor: enhance argument parsing and command completion
- Introduced a shared argument parser in `lib/args.sh` to streamline command argument handling across various commands. - Updated command scripts to utilize the new parser, improving clarity and maintainability by reducing repetitive code. - Enhanced completion scripts for Bash, Zsh, and Fish to reflect updated command options and improve user experience. - Added integration tests for the argument parser to ensure functionality and reliability in command execution.
1 parent 289124b commit 41c1488

25 files changed

Lines changed: 1293 additions & 367 deletions

bin/gtr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ resolve_script_dir() {
2222

2323
# Source library files
2424
. "$GTR_DIR/lib/ui.sh"
25+
. "$GTR_DIR/lib/args.sh"
2526
. "$GTR_DIR/lib/config.sh"
2627
. "$GTR_DIR/lib/platform.sh"
2728
. "$GTR_DIR/lib/core.sh"

completions/_git-gtr

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#compdef _git-gtr git-gtr gtr
2+
# AUTO-GENERATED by scripts/generate-completions.sh — DO NOT EDIT MANUALLY
3+
# Re-generate with: ./scripts/generate-completions.sh
24
#
35
# Zsh completion for git gtr (Git Worktree Runner)
46
#
@@ -119,10 +121,10 @@ _git-gtr() {
119121
elif (( CURRENT >= 5 )); then
120122
case "$words[3]" in
121123
editor)
122-
_arguments '--editor[Editor to use]:editor:(cursor vscode zed idea pycharm webstorm vim nvim emacs sublime nano atom none)'
124+
_arguments '--editor[Editor to use]:editor:(atom cursor emacs idea nano none nvim pycharm sublime vim vscode webstorm zed)'
123125
;;
124126
ai)
125-
_arguments '--ai[AI tool to use]:tool:(aider auggie claude codex continue copilot cursor gemini opencode none)'
127+
_arguments '--ai[AI tool to use]:tool:(aider auggie claude codex continue copilot cursor gemini none opencode)'
126128
;;
127129
rm)
128130
_arguments \
@@ -172,15 +174,15 @@ _git-gtr() {
172174
'--local[Use local git config]' \
173175
'--global[Use global git config]' \
174176
'--system[Use system git config]' \
175-
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd)'
177+
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider)'
176178
;;
177179
set|add|unset)
178180
# Write operations only support --local and --global
179181
# (--system may require root or appropriate file permissions)
180182
_arguments \
181183
'--local[Use local git config]' \
182184
'--global[Use global git config]' \
183-
'*:config key:(gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd)'
185+
'*:config key:(gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider)'
184186
;;
185187
esac
186188
fi

completions/git-gtr.fish

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# AUTO-GENERATED by scripts/generate-completions.sh — DO NOT EDIT MANUALLY
2+
# Re-generate with: ./scripts/generate-completions.sh
3+
#
14
# Fish completion for git gtr
25
#
36
# This completion integrates with fish's completion system by registering completions
@@ -88,10 +91,10 @@ complete -c git -n '__fish_git_gtr_using_command list' -l porcelain -d 'Machine-
8891
complete -c git -n '__fish_git_gtr_using_command ls' -l porcelain -d 'Machine-readable output'
8992

9093
# Editor command options
91-
complete -c git -n '__fish_git_gtr_using_command editor' -l editor -d 'Editor to use' -r -a 'cursor vscode zed idea pycharm webstorm vim nvim emacs sublime nano atom none'
94+
complete -c git -n '__fish_git_gtr_using_command editor' -l editor -d 'Editor to use' -r -a 'atom cursor emacs idea nano none nvim pycharm sublime vim vscode webstorm zed'
9295

9396
# AI command options
94-
complete -c git -n '__fish_git_gtr_using_command ai' -l ai -d 'AI tool to use' -r -a 'aider auggie claude codex continue copilot cursor gemini opencode none'
97+
complete -c git -n '__fish_git_gtr_using_command ai' -l ai -d 'AI tool to use' -r -a 'aider auggie claude codex continue copilot cursor gemini none opencode'
9598

9699
# Clean command options
97100
complete -c git -n '__fish_git_gtr_using_command clean' -l merged -d 'Remove worktrees with merged PRs/MRs'
@@ -132,21 +135,21 @@ complete -f -c git -n '__fish_git_gtr_using_command config' -l global -d 'Use gl
132135
# --system only for read operations (list, get) - write requires root
133136
complete -f -c git -n '__fish_git_gtr_using_command config; and __fish_git_gtr_config_is_read' -l system -d 'Use system git config'
134137
complete -f -c git -n '__fish_git_gtr_using_command config' -a "
135-
gtr.worktrees.dir\t'Worktrees base directory'
136-
gtr.worktrees.prefix\t'Worktree folder prefix'
137-
gtr.defaultBranch\t'Default branch'
138-
gtr.editor.default\t'Default editor'
139-
gtr.editor.workspace\t'Path to workspace file (.code-workspace)'
140-
gtr.ai.default\t'Default AI tool'
141-
gtr.provider\t'Hosting provider (github, gitlab)'
142-
gtr.copy.include\t'Files to copy'
143-
gtr.copy.exclude\t'Files to exclude'
144-
gtr.copy.includeDirs\t'Directories to copy (e.g., node_modules)'
145-
gtr.copy.excludeDirs\t'Directories to exclude'
146-
gtr.hook.postCreate\t'Post-create hook'
147-
gtr.hook.preRemove\t'Pre-remove hook (abort on failure)'
148-
gtr.hook.postRemove\t'Post-remove hook'
149-
gtr.hook.postCd\t'Post-cd hook (shell integration only)'
138+
gtr.copy.include 'Files to copy'
139+
gtr.copy.exclude 'Files to exclude'
140+
gtr.copy.includeDirs 'Directories to copy (e.g., node_modules)'
141+
gtr.copy.excludeDirs 'Directories to exclude'
142+
gtr.hook.postCreate 'Post-create hook'
143+
gtr.hook.preRemove 'Pre-remove hook (abort on failure)'
144+
gtr.hook.postRemove 'Post-remove hook'
145+
gtr.hook.postCd 'Post-cd hook (shell integration only)'
146+
gtr.editor.default 'Default editor'
147+
gtr.editor.workspace 'Path to workspace file (.code-workspace)'
148+
gtr.ai.default 'Default AI tool'
149+
gtr.worktrees.dir 'Worktrees base directory'
150+
gtr.worktrees.prefix 'Worktree folder prefix'
151+
gtr.defaultBranch 'Default branch'
152+
gtr.provider 'Hosting provider (github, gitlab)'
150153
"
151154

152155
# Helper function to get branch names and special '1' for main repo

completions/gtr.bash

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/bin/bash
2+
# AUTO-GENERATED by scripts/generate-completions.sh — DO NOT EDIT MANUALLY
3+
# Re-generate with: ./scripts/generate-completions.sh
4+
#
25
# Bash completion for git gtr
36
#
47
# Prerequisites:
@@ -52,7 +55,7 @@ _git_gtr() {
5255
if [[ "$cur" == -* ]]; then
5356
COMPREPLY=($(compgen -W "--editor" -- "$cur"))
5457
elif [ "$prev" = "--editor" ]; then
55-
COMPREPLY=($(compgen -W "cursor vscode zed idea pycharm webstorm vim nvim emacs sublime nano atom none" -- "$cur"))
58+
COMPREPLY=($(compgen -W "atom cursor emacs idea nano none nvim pycharm sublime vim vscode webstorm zed" -- "$cur"))
5659
else
5760
local branches all_options
5861
branches=$(git branch --format='%(refname:short)' 2>/dev/null || true)
@@ -64,7 +67,7 @@ _git_gtr() {
6467
if [[ "$cur" == -* ]]; then
6568
COMPREPLY=($(compgen -W "--ai" -- "$cur"))
6669
elif [ "$prev" = "--ai" ]; then
67-
COMPREPLY=($(compgen -W "aider auggie claude codex continue copilot cursor gemini opencode none" -- "$cur"))
70+
COMPREPLY=($(compgen -W "aider auggie claude codex continue copilot cursor gemini none opencode" -- "$cur"))
6871
else
6972
local branches all_options
7073
branches=$(git branch --format='%(refname:short)' 2>/dev/null || true)
@@ -128,15 +131,15 @@ _git_gtr() {
128131
if [[ "$cur" == -* ]]; then
129132
COMPREPLY=($(compgen -W "--local --global --system" -- "$cur"))
130133
else
131-
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd" -- "$cur"))
134+
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider" -- "$cur"))
132135
fi
133136
;;
134137
set|add|unset)
135138
# Write operations only support --local and --global (--system requires root)
136139
if [[ "$cur" == -* ]]; then
137140
COMPREPLY=($(compgen -W "--local --global" -- "$cur"))
138141
else
139-
COMPREPLY=($(compgen -W "gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.provider gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd" -- "$cur"))
142+
COMPREPLY=($(compgen -W "gtr.copy.include gtr.copy.exclude gtr.copy.includeDirs gtr.copy.excludeDirs gtr.hook.postCreate gtr.hook.preRemove gtr.hook.postRemove gtr.hook.postCd gtr.editor.default gtr.editor.workspace gtr.ai.default gtr.worktrees.dir gtr.worktrees.prefix gtr.defaultBranch gtr.provider" -- "$cur"))
140143
fi
141144
;;
142145
esac

lib/adapters.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,10 @@ _load_from_ai_registry() {
9696
if [ -n "$info_lines_raw" ]; then
9797
local old_ifs="$IFS"
9898
IFS=';'
99+
set -f # Disable globbing during split
99100
# shellcheck disable=SC2086
100101
set -- $info_lines_raw
102+
set +f
101103
_AI_INFO_LINES=("$@")
102104
IFS="$old_ifs"
103105
fi

lib/args.sh

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
#!/usr/bin/env bash
2+
# Shared argument parser for gtr commands
3+
# Depends on: ui.sh (log_error, show_command_help)
4+
#
5+
# Usage:
6+
# parse_args "<spec>" "$@"
7+
#
8+
# Spec format (one flag per line):
9+
# --force # boolean flag → _arg_force=1
10+
# --from: value # value flag → _arg_from="<val>"
11+
# --dry-run|-n # aliases → _arg_dry_run=1
12+
# --editor|-e # short alias → _arg_editor=1
13+
#
14+
# Output variables:
15+
# _pa_positional[] — positional arguments (array)
16+
# _pa_passthrough[] — arguments after -- (array)
17+
# _arg_<name> — one per declared flag (hyphens → underscores)
18+
declare -a _pa_positional _pa_passthrough
19+
20+
# Try to match a flag against the spec. Sets _arg_<name> and _pa_shift_count.
21+
# Returns 0 if matched, 1 if no match.
22+
_pa_match_flag() {
23+
local spec="$1" flag="$2" next_val="${3:-}"
24+
25+
_pa_shift_count=0
26+
27+
local line
28+
while IFS= read -r line; do
29+
[ -z "$line" ] && continue
30+
31+
# Check if spec line ends with ": value"
32+
local has_value=0
33+
case "$line" in
34+
*": value")
35+
has_value=1
36+
line="${line%: value}"
37+
;;
38+
esac
39+
40+
# Check if $flag matches any alternative in the pattern
41+
local alt matched=0
42+
local IFS_save="$IFS"
43+
IFS="|"
44+
for alt in $line; do
45+
if [ "$flag" = "$alt" ]; then
46+
matched=1
47+
break
48+
fi
49+
done
50+
IFS="$IFS_save"
51+
52+
if [ "$matched" = "1" ]; then
53+
# Derive variable name from the first (canonical) pattern
54+
local canonical="${line%%|*}"
55+
canonical="${canonical#--}"
56+
canonical="${canonical#-}"
57+
canonical="${canonical//-/_}"
58+
59+
if [ "$has_value" = "1" ]; then
60+
if [ -z "$next_val" ]; then
61+
log_error "$flag requires a value"
62+
exit 1
63+
fi
64+
eval "_arg_${canonical}=\"\$next_val\""
65+
_pa_shift_count=2
66+
else
67+
eval "_arg_${canonical}=1"
68+
_pa_shift_count=1
69+
fi
70+
return 0
71+
fi
72+
done <<EOF
73+
$spec
74+
EOF
75+
76+
return 1
77+
}
78+
79+
# Main parser. Call from command functions as: parse_args "<spec>" "$@"
80+
parse_args() {
81+
local _pa_spec="$1"
82+
shift
83+
84+
_pa_positional=()
85+
_pa_passthrough=()
86+
87+
# Reset all _arg_ variables from the spec to empty
88+
local _pa_line
89+
while IFS= read -r _pa_line; do
90+
[ -z "$_pa_line" ] && continue
91+
local _pa_clean="${_pa_line%: value}"
92+
local _pa_canonical="${_pa_clean%%|*}"
93+
_pa_canonical="${_pa_canonical#--}"
94+
_pa_canonical="${_pa_canonical#-}"
95+
_pa_canonical="${_pa_canonical//-/_}"
96+
eval "_arg_${_pa_canonical}=''"
97+
done <<EOF
98+
$_pa_spec
99+
EOF
100+
101+
while [ $# -gt 0 ]; do
102+
case "$1" in
103+
-h|--help)
104+
show_command_help
105+
;;
106+
--)
107+
shift
108+
_pa_passthrough=("$@")
109+
break
110+
;;
111+
-*)
112+
if _pa_match_flag "$_pa_spec" "$@"; then
113+
shift "$_pa_shift_count"
114+
else
115+
log_error "Unknown flag: $1"
116+
exit 1
117+
fi
118+
;;
119+
*)
120+
_pa_positional+=("$1")
121+
shift
122+
;;
123+
esac
124+
done
125+
}

lib/commands/adapter.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ EOF
4545
}
4646

4747
cmd_adapter() {
48-
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
49-
show_command_help
50-
fi
48+
parse_args "" "$@"
5149

5250
echo "Available Adapters"
5351
echo ""

lib/commands/ai.sh

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,13 @@
11
#!/usr/bin/env bash
2+
# shellcheck disable=SC2154
23

34
# AI command
45
cmd_ai() {
5-
local identifier=""
6-
local ai_tool=""
7-
local -a ai_args=()
6+
parse_args "--ai: value" "$@"
87

9-
# Parse arguments
10-
while [ $# -gt 0 ]; do
11-
case "$1" in
12-
--ai)
13-
ai_tool="$2"
14-
shift 2
15-
;;
16-
--)
17-
shift
18-
ai_args=("$@")
19-
break
20-
;;
21-
-h|--help)
22-
show_command_help
23-
;;
24-
-*)
25-
log_error "Unknown flag: $1"
26-
exit 1
27-
;;
28-
*)
29-
if [ -z "$identifier" ]; then
30-
identifier="$1"
31-
fi
32-
shift
33-
;;
34-
esac
35-
done
8+
local identifier="${_pa_positional[0]:-}"
9+
local ai_tool="${_arg_ai:-}"
10+
local -a ai_args=("${_pa_passthrough[@]}")
3611

3712
if [ -z "$identifier" ]; then
3813
log_error "Usage: git gtr ai <id|branch> [--ai <name>] [-- args...]"
@@ -52,7 +27,7 @@ cmd_ai() {
5227
fi
5328

5429
resolve_repo_context || exit 1
55-
# shellcheck disable=SC2154
30+
5631
local repo_root="$_ctx_repo_root" base_dir="$_ctx_base_dir" prefix="$_ctx_prefix"
5732

5833
# Load AI adapter (after context — fail fast on bad repo first)
@@ -61,12 +36,12 @@ cmd_ai() {
6136
# Resolve target branch
6237
local worktree_path branch
6338
resolve_worktree "$identifier" "$repo_root" "$base_dir" "$prefix" || exit 1
64-
# shellcheck disable=SC2154
39+
6540
worktree_path="$_ctx_worktree_path" branch="$_ctx_branch"
6641

6742
log_step "Starting $ai_tool for: $branch"
68-
echo "Directory: $worktree_path"
69-
echo "Branch: $branch"
43+
log_info "Directory: $worktree_path"
44+
log_info "Branch: $branch"
7045

7146
ai_start "$worktree_path" "${ai_args[@]}"
7247
}

0 commit comments

Comments
 (0)