|
| 1 | +--- |
| 2 | +title: "Go - Building CLI tools with goopts, a command-line argument parsing library" |
| 3 | +date: 2025-05-20T10:00:00Z |
| 4 | +description: "Presenting goopts, a cross-platform Go library for parsing command-line arguments with support for subcommands, argument groups, mutually exclusive options, and multiple value types." |
| 5 | +author: "Remi GASCOU (Podalirius)" |
| 6 | +tags: ["go", "cli", "library", "command-line"] |
| 7 | +draft: false |
| 8 | +--- |
| 9 | + |
| 10 | +When I started building security tools in Go for The Manticore Project, I quickly ran into a recurring problem. Every tool needed argument parsing: authentication flags, LDAP connection settings, debug options, positional arguments for modes and subcommands. Go's standard `flag` package handles simple cases, but it does not support argument groups, subcommands, mutually exclusive options, or even required arguments. |
| 11 | + |
| 12 | +I looked at existing libraries. Some were too minimal, others tried to do too much and imposed heavy conventions on how you structure your code. None of them felt right for the kind of tools I was building: security tools with multiple modes (like `audit`, `add`, `remove`), each with their own set of arguments, and common argument groups for authentication and LDAP settings. |
| 13 | + |
| 14 | +To support this, I wrote **goopts**. |
| 15 | + |
| 16 | +## What goopts provides |
| 17 | + |
| 18 | +goopts is a command-line argument parsing library for Go. It is designed to handle the argument parsing needs of complex CLI tools while keeping the API straightforward. The core features are: |
| 19 | + |
| 20 | +- **Eight argument types**: booleans, strings, integers, integer ranges, TCP ports, lists of strings, lists of integers, and maps of HTTP headers. |
| 21 | +- **Positional arguments**: string, integer, and boolean positionals that are consumed in order before named arguments. |
| 22 | +- **Argument groups**: logical groupings of related arguments (e.g. "Authentication", "LDAP Connection Settings") that appear together in the help output. |
| 23 | +- **Mutually exclusive groups**: groups where at most one argument (or exactly one, if required) can be provided. |
| 24 | +- **Dependent argument groups**: groups where if any argument is set, all arguments in the group must be set. |
| 25 | +- **Subparsers**: multi-level subcommand support, allowing patterns like `tool add constrained --flag value`. |
| 26 | +- **Automatic help generation**: `-h` and `--help` flags are handled automatically at every level. |
| 27 | + |
| 28 | +## Installation |
| 29 | + |
| 30 | +```bash |
| 31 | +$ go get github.com/TheManticoreProject/goopts |
| 32 | +``` |
| 33 | + |
| 34 | +## A basic example |
| 35 | + |
| 36 | +The simplest use case is a tool with a few flags and positional arguments. Here is a complete example: |
| 37 | + |
| 38 | +```go |
| 39 | +package main |
| 40 | + |
| 41 | +import ( |
| 42 | + "fmt" |
| 43 | + |
| 44 | + "github.com/TheManticoreProject/goopts/parser" |
| 45 | +) |
| 46 | + |
| 47 | +var ( |
| 48 | + filePath string |
| 49 | + verbose bool |
| 50 | + port int |
| 51 | +) |
| 52 | + |
| 53 | +func parseArgs() { |
| 54 | + ap := parser.NewParser("mytool v1.0 - by Remi GASCOU (Podalirius)") |
| 55 | + |
| 56 | + ap.NewStringPositionalArgument(&filePath, "filepath", "Path to the input file.") |
| 57 | + ap.NewBoolArgument(&verbose, "-v", "--verbose", false, "Enable verbose output.") |
| 58 | + ap.NewTcpPortArgument(&port, "-p", "--port", 8080, false, "Port number to listen on.") |
| 59 | + |
| 60 | + ap.Parse() |
| 61 | +} |
| 62 | + |
| 63 | +func main() { |
| 64 | + parseArgs() |
| 65 | + fmt.Printf("File: %s, Verbose: %t, Port: %d\n", filePath, verbose, port) |
| 66 | +} |
| 67 | +``` |
| 68 | + |
| 69 | +Running `./mytool --help` produces a formatted help message with the banner, positional arguments, and named arguments grouped together. |
| 70 | + |
| 71 | +## Argument types |
| 72 | + |
| 73 | +goopts supports eight argument types. Each type handles parsing, validation, and default values. |
| 74 | + |
| 75 | +| Type | Method | Description | |
| 76 | +|---|---|---| |
| 77 | +| Boolean | `NewBoolArgument` | Toggle flag, value is `!defaultValue` when present | |
| 78 | +| String | `NewStringArgument` | Single string value | |
| 79 | +| Integer | `NewIntArgument` | Integer with support for hex (`0x`), octal (`0o`), and binary (`0b`) prefixes | |
| 80 | +| Integer Range | `NewIntRangeArgument` | Integer validated against a `[min, max]` range | |
| 81 | +| TCP Port | `NewTcpPortArgument` | Integer validated against the `[0, 65535]` range | |
| 82 | +| List of Strings | `NewListOfStringsArgument` | Repeatable flag, each occurrence appends to a slice | |
| 83 | +| List of Integers | `NewListOfIntsArgument` | Repeatable flag for integer values | |
| 84 | +| Map of HTTP Headers | `NewMapOfHttpHeadersArgument` | Parses `Key: Value` format, splits on first colon | |
| 85 | + |
| 86 | +All argument types except booleans accept a `required` parameter. When an argument is required but not provided, the parser prints an error message and the usage. |
| 87 | + |
| 88 | +## Argument groups |
| 89 | + |
| 90 | +In security tools, arguments naturally fall into groups. Authentication options (`--domain`, `--username`, `--password`, `--hashes`) belong together. LDAP connection settings (`--dc-ip`, `--ldap-port`, `--use-ldaps`) belong together. goopts makes this explicit: |
| 91 | + |
| 92 | +```go |
| 93 | +ap := parser.NewParser("Delegations - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.0.0") |
| 94 | + |
| 95 | +groupAuth, _ := ap.NewArgumentGroup("Authentication") |
| 96 | +groupAuth.NewStringArgument(&authDomain, "-d", "--domain", "", true, "Active Directory domain to authenticate to.") |
| 97 | +groupAuth.NewStringArgument(&authUsername, "-u", "--username", "", true, "User to authenticate as.") |
| 98 | +groupAuth.NewStringArgument(&authPassword, "-p", "--password", "", false, "Password to authenticate with.") |
| 99 | +groupAuth.NewStringArgument(&authHashes, "-H", "--hashes", "", false, "NT/LM hashes, format is LMhash:NThash.") |
| 100 | + |
| 101 | +groupLdap, _ := ap.NewArgumentGroup("LDAP Connection Settings") |
| 102 | +groupLdap.NewStringArgument(&dcIp, "-dc", "--dc-ip", "", true, "IP Address of the domain controller.") |
| 103 | +groupLdap.NewTcpPortArgument(&ldapPort, "-lp", "--ldap-port", 389, false, "Port number to connect to LDAP server.") |
| 104 | +groupLdap.NewBoolArgument(&useLdaps, "-L", "--use-ldaps", false, "Use LDAPS instead of LDAP.") |
| 105 | +groupLdap.NewBoolArgument(&useKerberos, "-k", "--use-kerberos", false, "Use Kerberos instead of NTLM.") |
| 106 | +``` |
| 107 | + |
| 108 | +The help output displays each group with its own header, making it easy for users to understand which arguments are related. |
| 109 | + |
| 110 | +## Mutually exclusive and dependent groups |
| 111 | + |
| 112 | +Some argument combinations do not make sense together. For example, you might want the user to provide either a password or an NT hash, but not both. goopts supports this with mutually exclusive groups: |
| 113 | + |
| 114 | +```go |
| 115 | +groupCreds, _ := ap.NewRequiredMutuallyExclusiveArgumentGroup("Credentials") |
| 116 | +groupCreds.NewStringArgument(&password, "-p", "--password", "", false, "Password to authenticate with.") |
| 117 | +groupCreds.NewStringArgument(&hashes, "-H", "--hashes", "", false, "NT/LM hashes.") |
| 118 | +``` |
| 119 | + |
| 120 | +With `NewRequiredMutuallyExclusiveArgumentGroup`, the parser enforces that exactly one of the arguments in the group is provided. With `NewNotRequiredMutuallyExclusiveArgumentGroup`, at most one is allowed but none is also valid. |
| 121 | + |
| 122 | +Dependent groups work the other way: if any argument in the group is set, all arguments in the group must be set: |
| 123 | + |
| 124 | +```go |
| 125 | +groupProxy, _ := ap.NewDependentArgumentGroup("Proxy Settings") |
| 126 | +groupProxy.NewStringArgument(&proxyHost, "", "--proxy-host", "", false, "Proxy hostname.") |
| 127 | +groupProxy.NewTcpPortArgument(&proxyPort, "", "--proxy-port", 8080, false, "Proxy port.") |
| 128 | +``` |
| 129 | + |
| 130 | +## Subparsers for multi-command tools |
| 131 | + |
| 132 | +This is the feature that motivated goopts in the first place. Most of The Manticore Project's tools follow a multi-command pattern: `tool <mode> <submode> [options]`. For example, `Delegations add constrained --distinguished-name "..." --dc-ip "..."`. |
| 133 | + |
| 134 | +goopts supports this with nested subparsers: |
| 135 | + |
| 136 | +```go |
| 137 | +ap := parser.NewParser("Delegations - by Remi GASCOU (Podalirius) @ TheManticoreProject - v1.0.0") |
| 138 | +ap.SetupSubParsing("mode", &mode, true) |
| 139 | + |
| 140 | +subAdd := ap.AddSubParser("add", "Add a delegation to a computer, user or group.") |
| 141 | +subAdd.SetupSubParsing("delegationType", &delegationType, true) |
| 142 | + |
| 143 | +subAddConstrained := subAdd.AddSubParser("constrained", "Add a constrained delegation.") |
| 144 | +subAddConstrained.NewStringArgument(&distinguishedName, "-D", "--distinguished-name", "", true, "DN of the target object.") |
| 145 | + |
| 146 | +subAddUnconstrained := subAdd.AddSubParser("unconstrained", "Add an unconstrained delegation.") |
| 147 | +subAddUnconstrained.NewStringArgument(&distinguishedName, "-D", "--distinguished-name", "", true, "DN of the target object.") |
| 148 | + |
| 149 | +subAudit := ap.AddSubParser("audit", "Audit all delegations in Active Directory.") |
| 150 | +``` |
| 151 | + |
| 152 | +Each subparser is a full `ArgumentsParser` with its own arguments, groups, and even nested subparsers. The `SetupSubParsing` method configures which variable receives the selected subcommand name. The `caseInsensitive` parameter controls whether subcommand matching is case-sensitive. |
| 153 | + |
| 154 | +When the user runs `./Delegations add`, the parser routes to the `add` subparser and displays its available subcommands. When they run `./Delegations add constrained --help`, the parser routes two levels deep and displays the help for the `constrained` subparser. |
| 155 | + |
| 156 | +## How parsing works |
| 157 | + |
| 158 | +The parsing flow is: |
| 159 | + |
| 160 | +1. The parser first checks for `-h` or `--help` and displays usage if found. |
| 161 | +2. If subparsing is enabled, the first argument is consumed as the subcommand name and parsing is delegated to the matching subparser. |
| 162 | +3. Positional arguments are consumed in order from the remaining arguments. |
| 163 | +4. Named arguments are consumed by matching short or long names. Each argument type knows how to consume its value from the argument list. |
| 164 | +5. After all arguments are consumed, the parser validates required arguments, mutually exclusive groups, and dependent groups. |
| 165 | +6. If any validation fails, error messages are printed alongside the usage. |
| 166 | + |
| 167 | +Integer arguments support multiple notations: decimal (`42`), hexadecimal (`0xFF`), octal (`0o77`), and binary (`0b1010`). This is particularly useful for security tools that deal with flags and bitmasks. |
| 168 | + |
| 169 | +## Querying parsed arguments |
| 170 | + |
| 171 | +After parsing, you can check whether a specific argument was provided by the user: |
| 172 | + |
| 173 | +```go |
| 174 | +ap.Parse() |
| 175 | + |
| 176 | +if ap.ArgumentIsPresent("--server-port") { |
| 177 | + fmt.Printf("Server port was explicitly set to %d\n", serverPort) |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +This is useful when you need to distinguish between "the user provided the default value" and "the user did not provide this argument at all". |
| 182 | + |
| 183 | +## Real-world usage in The Manticore Project |
| 184 | + |
| 185 | +goopts is the argument parsing library used by every tool in The Manticore Project: |
| 186 | + |
| 187 | +- [Delegations](https://github.com/TheManticoreProject/Delegations) uses two levels of subparsers (`mode` and `delegationType`) with authentication and LDAP connection groups. |
| 188 | +- [FindGPPPasswords](https://github.com/TheManticoreProject/FindGPPPasswords) uses argument groups for authentication and output configuration. |
| 189 | +- [SIDTool](https://github.com/TheManticoreProject/SIDTool) uses positional arguments for the SID value and named arguments for output format options. |
| 190 | +- [keytab](https://github.com/TheManticoreProject/keytab) uses subparsers for different keytab operations. |
| 191 | + |
| 192 | +The library ensures a consistent user experience across all tools: the same authentication flags, the same help format, the same error messages. |
| 193 | + |
| 194 | +## References |
| 195 | + |
| 196 | +- [goopts on GitHub](https://github.com/TheManticoreProject/goopts) |
| 197 | +- [goopts documentation](https://github.com/TheManticoreProject/goopts#documentation) |
| 198 | +- [Manticore library](https://github.com/TheManticoreProject/Manticore) |
0 commit comments