Skip to content

Duplicate subcommands are silently ignored by ArgumentParser #976

Description

@true-false-maybe

Duplicate subcommands are silently ignored by ArgumentParser.

ArgumentParser version: 1.3.0 and main
Swift version: 6.3.3

Checklist

Steps to Reproduce

Replace this paragraph with an explanation of how to reproduce the incorrect behavior. This could include a code listing for a reduced version of your command, or a link to the code that is exhibiting the issue.

Have a command with two subcommands which have the same commandName in their CommandConfiguration.

Expected behavior

Describe what you expect to happen.

I would expect ArgumentParser to either print a warning or better yet throw an error (possibly even a fatalError) or call ParsableArguments.exit(withError:), whenever the application starts.

Actual behavior

Describe or copy/paste the behavior you observe.

Basically when there are two or more subcommands which have the same commandName only the first command can be invoked. The second one still exists and is listed in --help, but cannot be invoked, because calling the command only calls the first command.

A minimal reproducible example: https://github.com/true-false-maybe/argument-parser-duplicate-command

Activity

  1. true-false-maybe commented on Sep 24, 2026

    @true-false-maybe
    Author

    As a small work around it's possible to do something like this, though i think it makes more sense if Argument-Parser would do something like this on it's own:

    /// A `ParsableCommand` which can validate *all* descendants. Therefore it on the root command.
    /// If there are duplicate commands the command will early exit before doing anything.
    ///
    /// IMPORTANT: if you overwrite `validate` you should call `validateCommandSubtree(command: Self.self)`,
    /// otherwise this protocol is useless
    public protocol ValidatedRootCommand: ParsableCommand {}
    
    public extension ValidatedRootCommand {
        func validate() throws {
            validateCommandSubtree(command: Self.self)
        }
    
        func validateCommandSubtree(command: any ParsableCommand.Type, commandPath: [String?] = []) {
            let commandPath = commandPath + [command.configuration.commandName]
    
            let failingCommands: [String?: [any ParsableCommand.Type]] = command.configuration.subcommands
                .grouped { $0.configuration.commandName }
                .filter { $0.value.count > 1 }
    
            guard failingCommands.isEmpty else {
                let errorTable = failingCommands.map {
                    return "- '\($0.key, default: "<nil>")':\n    - \(String($0.value.map { String(describing: $0) }.joined(separator: ", ")))"
                }.joined(separator: "\n\n")
    
                // NOTE: throwing is not enough, we need to exit
                Self.exit(withError: ValidationError("At command '\(String(commandPath.compacted().joined(separator: " ")))' there are multiple possible subcommand-types:\n\(errorTable)"))
            }
    
            for subcommand in command.configuration.subcommands {
                validateCommandSubtree(command: subcommand, commandPath: commandPath)
            }
        }
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions