Command-line interfaces (CLIs) are a primary way to interact with Go programs. Robust CLIs often support subcommands (like git commit, docker run) and flags (like --verbose, -n).
Go's standard library os provides access to raw arguments via os.Args, and the flag package helps parse them.
os.Args is a slice of strings representing the arguments passed to the program.
os.Args[0]is the program name.os.Args[1:]are the user's arguments.
To implement subcommands, we inspect the first argument and then use a dedicated flag.FlagSet for that command.
switch os.Args[1] {
case "foo":
// Create a flag set for the "foo" command
fooCmd := flag.NewFlagSet("foo", flag.ExitOnError)
fooFlag := fooCmd.Bool("enable", false, "desc")
// Parse arguments specific to "foo" (skipping program name and subcommand)
fooCmd.Parse(os.Args[2:])
// ...
}- Go by Example: Command-Line Arguments
- Go by Example: Command-Line Flags
- Go by Example: Command-Line Subcommands
- pkg.go.dev/flag
Build a file-cli tool with two subcommands: counting lines and searching text in files.
Implement: func RunCLI(args []string) error
count — Count lines in a file
| Usage | file-cli count <filename> |
| Output | lines: <number> |
| Errors | Missing file or invalid format |
search — Find lines matching a pattern
| Usage | file-cli search [--case-insensitive] <pattern> <filename> |
| Flag | --case-insensitive (bool, default false) — ignores case when matching |
| Output | Print each matching line to stdout |
| Errors | Missing arguments or unreadable file |
General Errors
| Condition | Behaviour |
|---|---|
args length < 2 |
Return error |
| Unknown subcommand | Return error |
| Missing filename/pattern after flags | Return error |
args []string: The full argument slice (including program name).
- Return:
errorif any operation fails. - Stdout: The command results.
args := []string{"./app", "count", "test.txt"}
RunCLI(args)
// Output: lines: 5args := []string{"./app", "search", "--case-insensitive", "hello", "test.txt"}
RunCLI(args)
// Output:
// Hello World
// hello there- Use
os.Open(filename)to open a file. - Use
bufio.NewScanner(file)to read line by line. - Remember
flag.Parse()consumes flags.flag.Args()(orflagSet.Args()) returns the remaining non-flag arguments (like the filename). flagSet.Parseshould be called withargs[2:]becauseargs[0]is program name andargs[1]is the subcommand.
To run the tests:
go test -v ./quests/025.cliOr from the quest directory:
go test -vExpected output:
=== RUN TestRunCLI_Search/search_case-sensitive
=== RUN TestRunCLI_Search/search_case-insensitive
--- PASS: TestRunCLI_Search (0.00s)
--- PASS: TestRunCLI_Search/search_case-sensitive (0.00s)
--- PASS: TestRunCLI_Search/search_case-insensitive (0.00s)
=== RUN TestRunCLI_Errors
=== RUN TestRunCLI_Errors/no_args
=== RUN TestRunCLI_Errors/unknown_subcommand
=== RUN TestRunCLI_Errors/missing_file_arg_for_count
=== RUN TestRunCLI_Errors/missing_file_arg_for_serach
--- PASS: TestRunCLI_Errors (0.00s)
--- PASS: TestRunCLI_Errors/no_args (0.00s)
--- PASS: TestRunCLI_Errors/unknown_subcommand (0.00s)
--- PASS: TestRunCLI_Errors/missing_file_arg_for_count (0.00s)
--- PASS: TestRunCLI_Errors/missing_file_arg_for_serach (0.00s)
PASS