Bypass missing option or value validation?

Hey, I'm new too Swift and to using Argument Parser but I'm really enjoying it so far. I've implemented a list @Flag that when used (or set to true) will list data contained in specific arrays depending on what command-line options and values exist when implemented (e.i. "gypsy -t --list" will list all tactics in the tactics array vs "gypsy -t persistence -m --list" will list all modules in the modules array for the specified tactic). The problem I'm running into is that when I try to implement either of those methods argument parser throws its error handling message telling me the option is missing or the value is missing and fails to list anything. Is there a way around this that will, regardless of missing options or values, allow me to list the contents of the arrays? I appreciate the help and understanding.

    mutating func run() throws {
        if list == true && CommandLine.arguments.count == 2 {
            print(tactics)
        }
    }

Welcome, @DefSecSentinel! Could you share more about how your command type is defined? What do the declarations of the -t and -m properties look like?

Thanks @nnnnnnnn ! Absolutely.

import ArgumentParser

let tactics = ["persistence"]

let subTechs = ["cron"]

@main
struct Gypsy: ParsableCommand {
    static let configuration = CommandConfiguration(abstract: "MacOS TTP Unit Testing Tool", version: "1.0.0")
    
    @Option(name: [.customShort("t"), .customLong("tactic")],help: "MITRE Tactic name, example: persistence")
    var inputTactic: String
    
    @Option(name: [.customShort("m"), .customLong("module")],help: "MITRE Sub-Technique name, example: cron")
    var inputModule: String
        
    @Flag(name: .shortAndLong, help: "List all available values for the specified tactic or module.")
    var list = false
    
    @Flag(name: .shortAndLong, help: "Invoke test using api call. Default is shell")
    var api = false
    
    func validate() throws {
        for tactic in tactics {
            if inputTactic != tactic {
                throw ValidationError("Invalid tactic name. Try again or use the --list flag with the -t option to show available tactics.")
            }
        }
        for subTech in subTechs {
            if inputModule != subTech {
                throw ValidationError("Invalid module name. Try again or use the --list flag after the -m option to show available modules for the specified tactic.")
            }
        }
    }
        
    mutating func run() throws {
        if list == true && CommandLine.arguments.count == 2 {
            print(tactics)
        }
    }
}
1 Like

Screen Shot 2022-04-11 at 11.24.14 AM

Both inputTactic and inputModule are declared as non-optional strings, which means that they are required from the user. If you change them to String? instead, then a user can omit them without getting an error.

However, a call like gypsy -t --list still won't work, because if a user provides -t they'll also need to provide a value. You might want to use two flags for listing instead, like --list-tactics and --list-inputs.

1 Like

Gotcha, ok that makes sense. I'll give the double list flags a shot with the optional string. I appreciate the help!

you could also special-case a tactic or module name of list if you know that won't conflict with legitimate values

gypsy -t list

Also I believe that your validation check doesn't do what you want it to do - it will fail as soon as it finds a tactic that doesn't match, rather than check all for matches. You probably want to use tactics.contains instead.

1 Like

@jayrhynas What do you mean by special-case a tactic or module name of list? The name list wouldn't conflict with legitimate values. Would I still be able to use that to list the other values in the specific array?

For the validation check how would you implement the tactic.contains? When I put:

if tactics.contains(inputTactic: String) 

or

if tactics.contains(inputTactic) 

I receive an error message. Thanks for the suggestions!

I'd probably do it something like this for the validate (assuming you made the inputs Optional):

func validate() throws {
    if let tactic = inputTactic, !tactics.contains(tactic) {
        throw ValidationError("Invalid tactic name. Try again or pass `list` to the `-t` option to show available tactics.")
    }
    if let subTech = inputModule, !subTechs.contains(subTech) {
        throw ValidationError("Invalid module name. Try again or pass `list` to the `-m` option to show available modules for the specified tactic.")
    }
}

and then this for the run logic with similar handling for the module/subTech:

mutating func run() throws {
    if inputTactic == "list" {
        print(tactics)
    }
}
1 Like