Read options after the path or URL and refuse a second argument (closes #196)
check / check (push) Waiting to run

Every command read options only up to its first argument, and check,
fetch, freshen, list and export dropped everything after it, so
`check DIR --require-signature FPR` passed an unsigned manifest. Options
are now read wherever they appear, as urfave/cli v3 does by default, and
`--` still ends them. Those five commands refuse a second argument,
naming it.

TestFlagAfterArgumentIsArgument asserted the defect (`gen DIR -v` read
`-v` as a path); a test that `gen -- -x` reads `-x` as a path replaces it.

Model: opus-5-5
This commit was merged in pull request #197.
This commit is contained in:
2026-10-13 14:21:56 +02:00
parent 0fbb3da0a5
commit 8f7170b77f
2 changed files with 132 additions and 31 deletions
+24 -22
View File
@@ -48,6 +48,10 @@ func manifestTempPath(out string) string {
// errUnknownCommand indicates an unrecognized command argument.
var errUnknownCommand = errors.New("unknown command")
// errUnexpectedArgument indicates a second argument to a command that takes
// one.
var errUnexpectedArgument = errors.New("unexpected argument")
// CLIApp is the main CLI application container. It holds configuration,
// I/O streams, and filesystem abstraction to enable testing and flexibility.
//
@@ -154,13 +158,14 @@ func commonFlags() []cli.Flag {
}
}
// stopOnFirstArg returns the StopOnNthArg setting every command uses: flags
// are read only before the command's first argument, and everything after it
// is an argument, as with urfave/cli v2. So `mfer gen d -v` names a path "-v".
func stopOnFirstArg() *int {
n := 1
// refuseExtraArgs is the ArgValidator of the commands that take one
// argument: it refuses a second one, naming it.
func refuseExtraArgs(_ context.Context, cmd *cli.Command) error {
if cmd.Args().Len() > 1 {
return fmt.Errorf("%w %q", errUnexpectedArgument, cmd.Args().Get(1))
}
return &n
return nil
}
// requireSignatureFlag returns the --require-signature flag taken by the
@@ -186,11 +191,10 @@ func includePermissionsFlag() *cli.BoolFlag {
func (mfa *CLIApp) generateCommand() *cli.Command {
return &cli.Command{
Name: cmdGenerate,
Aliases: []string{"gen"},
Usage: "Generate manifest file",
ArgsUsage: "[path ...]",
StopOnNthArg: stopOnFirstArg(),
Name: cmdGenerate,
Aliases: []string{"gen"},
Usage: "Generate manifest file",
ArgsUsage: "[path ...]",
Action: func(ctx context.Context, cmd *cli.Command) error {
mfa.setVerbosity(cmd)
mfa.printBanner()
@@ -252,7 +256,7 @@ func (mfa *CLIApp) checkCommand() *cli.Command {
Name: cmdCheck,
Usage: "Validate files using manifest file",
ArgsUsage: manifestArgsUsage,
StopOnNthArg: stopOnFirstArg(),
ArgValidator: refuseExtraArgs,
Action: func(ctx context.Context, cmd *cli.Command) error {
mfa.setVerbosity(cmd)
mfa.printBanner()
@@ -286,7 +290,7 @@ func (mfa *CLIApp) freshenCommand() *cli.Command {
Name: cmdFreshen,
Usage: "Update manifest with changed, new, and removed files",
ArgsUsage: manifestArgsUsage,
StopOnNthArg: stopOnFirstArg(),
ArgValidator: refuseExtraArgs,
Action: func(ctx context.Context, cmd *cli.Command) error {
mfa.setVerbosity(cmd)
mfa.printBanner()
@@ -337,7 +341,7 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
Name: cmdExport,
Usage: "Export manifest contents as JSON",
ArgsUsage: "[manifest file or URL]",
StopOnNthArg: stopOnFirstArg(),
ArgValidator: refuseExtraArgs,
Action: func(ctx context.Context, cmd *cli.Command) error {
mfa.setVerbosity(cmd)
@@ -348,9 +352,8 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
func (mfa *CLIApp) versionCommand() *cli.Command {
return &cli.Command{
Name: cmdVersion,
Usage: "Show version",
StopOnNthArg: stopOnFirstArg(),
Name: cmdVersion,
Usage: "Show version",
Action: func(context.Context, *cli.Command) error {
mfa.printVersion()
@@ -365,7 +368,7 @@ func (mfa *CLIApp) listCommand() *cli.Command {
Aliases: []string{"ls"},
Usage: "List files in manifest",
ArgsUsage: manifestArgsUsage,
StopOnNthArg: stopOnFirstArg(),
ArgValidator: refuseExtraArgs,
Action: mfa.listManifestOperation,
Flags: []cli.Flag{
&cli.BoolFlag{
@@ -386,7 +389,7 @@ func (mfa *CLIApp) fetchCommand() *cli.Command {
Name: cmdFetch,
Usage: "fetch manifest and referenced files",
ArgsUsage: "URL",
StopOnNthArg: stopOnFirstArg(),
ArgValidator: refuseExtraArgs,
Action: func(ctx context.Context, cmd *cli.Command) error {
mfa.setVerbosity(cmd)
mfa.printBanner()
@@ -445,9 +448,8 @@ func (mfa *CLIApp) run(args []string) {
Writer: mfa.Stdout,
// v3 writes its "Incorrect Usage" line to ErrWriter; v2 wrote it to
// stdout, before the help, so it stays on stdout.
ErrWriter: mfa.Stdout,
Flags: commonFlags(),
StopOnNthArg: stopOnFirstArg(),
ErrWriter: mfa.Stdout,
Flags: commonFlags(),
Action: func(_ context.Context, cmd *cli.Command) error {
if cmd.Args().Len() > 0 {
return fmt.Errorf("%w %q", errUnknownCommand, cmd.Args().First())