Updated interaction between custom CLI syntax and Cobra flags

This commit is contained in:
2026-02-19 18:31:30 +01:00
parent cd77443a07
commit acab4333a7
2 changed files with 98 additions and 27 deletions
+47 -12
View File
@@ -15,6 +15,7 @@ type ParsedArgs struct {
Command string Command string
Filters []string Filters []string
Modifiers []string Modifiers []string
CmdArgIndex int // position of command in os.Args[1:], -1 if not found
} }
// Context key for parsed args // Context key for parsed args
@@ -84,28 +85,29 @@ func Execute() error {
if len(os.Args) > 1 { if len(os.Args) > 1 {
firstArg := os.Args[1] firstArg := os.Args[1]
if firstArg == "-h" || firstArg == "--help" || firstArg == "help" { if firstArg == "-h" || firstArg == "--help" || firstArg == "help" {
// Let Cobra handle help - skip preprocessing
return rootCmd.Execute() return rootCmd.Execute()
} }
} }
// Preprocess arguments BEFORE Cobra routing // Preprocess arguments (read-only scan — os.Args is never mutated)
if len(os.Args) > 1 { if len(os.Args) > 1 {
parsed := preprocessArgs(os.Args[1:]) parsed := preprocessArgs(os.Args[1:])
// Store in context for commands to use
ctx := context.WithValue(context.Background(), parsedArgsKey, parsed) ctx := context.WithValue(context.Background(), parsedArgsKey, parsed)
rootCmd.SetContext(ctx) rootCmd.SetContext(ctx)
// Rewrite os.Args for Cobra based on parsed command // Build clean args for Cobra via SetArgs (os.Args stays untouched).
// This allows Cobra to route to the correct command if parsed.CmdArgIndex >= 0 {
if parsed.Command != "list" || len(parsed.Filters) > 0 || len(parsed.Modifiers) > 0 { i := parsed.CmdArgIndex + 1 // offset for binary name in os.Args
// Reconstruct args: [command, ...filters, ...modifiers] cmdAndAfter := os.Args[i:] // command + subcommands + their flags
newArgs := []string{os.Args[0], parsed.Command} preCmdFlags := collectFlags(os.Args[1:i]) // persistent flags before command
newArgs = append(newArgs, parsed.Filters...)
newArgs = append(newArgs, parsed.Modifiers...) cobraArgs := make([]string, 0, len(cmdAndAfter)+len(preCmdFlags))
os.Args = newArgs cobraArgs = append(cobraArgs, cmdAndAfter...)
cobraArgs = append(cobraArgs, preCmdFlags...)
rootCmd.SetArgs(cobraArgs)
} }
// CmdArgIndex == -1: no command found, don't call SetArgs.
// Cobra processes os.Args naturally → root command → default report.
} }
return rootCmd.Execute() return rootCmd.Execute()
@@ -130,6 +132,7 @@ func preprocessArgs(args []string) *ParsedArgs {
Command: "list", // Default command Command: "list", // Default command
Filters: []string{}, Filters: []string{},
Modifiers: []string{}, Modifiers: []string{},
CmdArgIndex: -1,
} }
} }
@@ -170,6 +173,7 @@ func preprocessArgs(args []string) *ParsedArgs {
Command: "list", Command: "list",
Filters: stripFlags(args), Filters: stripFlags(args),
Modifiers: []string{}, Modifiers: []string{},
CmdArgIndex: -1,
} }
} }
@@ -186,6 +190,7 @@ func preprocessArgs(args []string) *ParsedArgs {
Command: cmdName, Command: cmdName,
Filters: leftArgs, Filters: leftArgs,
Modifiers: rightArgs, Modifiers: rightArgs,
CmdArgIndex: cmdIdx,
} }
} else { } else {
allFilters := append(leftArgs, rightArgs...) allFilters := append(leftArgs, rightArgs...)
@@ -193,6 +198,7 @@ func preprocessArgs(args []string) *ParsedArgs {
Command: cmdName, Command: cmdName,
Filters: allFilters, Filters: allFilters,
Modifiers: []string{}, Modifiers: []string{},
CmdArgIndex: cmdIdx,
} }
} }
} }
@@ -208,6 +214,35 @@ func stripFlags(args []string) []string {
return result return result
} }
// collectFlags extracts flag arguments (with their values) from a slice.
// Uses Cobra's persistent flag registry to determine if a flag takes a value.
func collectFlags(args []string) []string {
var flags []string
for i := 0; i < len(args); i++ {
if !strings.HasPrefix(args[i], "-") {
continue
}
flags = append(flags, args[i])
// If flag uses = syntax, value is already included
if strings.Contains(args[i], "=") {
continue
}
// Check if this flag takes a value argument
if i+1 < len(args) {
name := strings.TrimLeft(args[i], "-")
f := rootCmd.PersistentFlags().Lookup(name)
if f == nil && len(name) == 1 {
f = rootCmd.PersistentFlags().ShorthandLookup(name)
}
if f != nil && f.Value.Type() != "bool" {
i++
flags = append(flags, args[i])
}
}
}
return flags
}
func init() { func init() {
// Add persistent flags for directory overrides // Add persistent flags for directory overrides
rootCmd.PersistentFlags().StringVar(&configDirFlag, "config-dir", "", rootCmd.PersistentFlags().StringVar(&configDirFlag, "config-dir", "",
+36
View File
@@ -2,3 +2,39 @@
`Buy milk due:8d wait:5d` still showing up `Buy milk due:8d wait:5d` still showing up
# Missing uncomplete feat # Missing uncomplete feat
Undo / uncomplete fails in web-ui. task still has checked box and strikethrough.
# Cycling priority - Web
Trying to edit a task priority results in following console error:
GQLaRcBw.js:1 PUT https://opal.jnss.me/api/tasks/8814798c-97af-4134-9786-47e027d164c8 400 (Bad Request)
window.fetch @ GQLaRcBw.js:1
n @ nlPNz7OE.js:1
update @ nlPNz7OE.js:1
updateTask @ 2.qiXA3Mmu.js:1
X @ 2.qiXA3Mmu.js:3
Q @ 2.qiXA3Mmu.js:1
(anonymous) @ idxpmzXF.js:1
Qe @ jWcw5lls.js:1
n @ idxpmzXF.js:1
nlPNz7OE.js:1 API Error [/tasks/8814798c-97af-4134-9786-47e027d164c8]: Error: HTTP 400:
at n (nlPNz7OE.js:1:1556)
at async Object.updateTask (2.qiXA3Mmu.js:1:4502)
at async X (2.qiXA3Mmu.js:3:3519)
at async HTMLDivElement.Q (2.qiXA3Mmu.js:1:58579)
n @ nlPNz7OE.js:1
await in n
update @ nlPNz7OE.js:1
updateTask @ 2.qiXA3Mmu.js:1
X @ 2.qiXA3Mmu.js:3
Q @ 2.qiXA3Mmu.js:1
(anonymous) @ idxpmzXF.js:1
Qe @ jWcw5lls.js:1
n @ idxpmzXF.js:1
2.qiXA3Mmu.js:3 Failed to update task: Error: HTTP 400:
at n (nlPNz7OE.js:1:1556)
at async Object.updateTask (2.qiXA3Mmu.js:1:4502)
at async X (2.qiXA3Mmu.js:3:3519)
at async HTMLDivElement.Q (2.qiXA3Mmu.js:1:58579)
# Ambiguity complete and details tap.
Pressing the task description completes the task. only the checkbox click should complete task, otherwise open details view. to complete swipe left