---
title: 'Command-Line Applications'
source: 'https://academia.sh/en/courses/go-applications/command-line-applications'
course: 'The Standard Library and Network Applications'
language: en
updated: '2026-09-14T10:59:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Measures by running that four types — integer, boolean, duration and string — are rebuilt from a string array by the flag parser'
    - 'Shows where an unrecognized flag stops parsing, and that the flags after the stopping point are never read'
    - "Proves by running that in a subcommand structure each subcommand carries its own flag set, and one subcommand's flag is not recognized in another"
    - 'Limits by showing that the parser accepts a valid but meaningless value without error, so that rebuilding a type is not rebuilding meaning'
---

# Command-Line Applications

The last lesson of the course and the topic. Integer, boolean, duration and string types being rebuilt from a hand-built string array given to a flag parser; an unrecognized flag stopping parsing; each subcommand carrying its own flag set — all measured by running. Limiting measurement: the parser rebuilds the expected type, not the value's meaning — a negative count passes with no error. The course wrap-up gathers the boundary crossings the twelve lessons measured into a single table.

The previous lesson rebuilt a database row from the driver's narrow type set through the target
variable's own `Scan` method, closing the topic's five lessons: a file and a buffer, a resource
embedded in a binary, a time value turning into text, a log record carrying field names, a
database row — in all five, the value crossing the boundary was not sufficient on its own; the
rebuild always relied on a schema held by the other side. This lesson carries the same question
to the program's own entry point: a command-line flag, too, enters the program as text. The
Shell Programming course (M03/K02) already covered how these arguments are built on the shell
side; this lesson continues only from where the argument reaches the program's hand — once the
string is held, who rebuilds its type, with what schema?

The answer sits in this course's standard-library flag parser, called just "flag" throughout: a
command-line option that arrives as text, added in front of a call with a `-`. The parser takes a
string array and, according to flag names declared in advance in the source, produces a value in
each flag's expected type — the schema again sits on the other side, this time as declarations
written in the source itself: `set.Int`, `set.Bool`, `set.String`. This lesson measures three
things: how the parser rebuilds four types (integer, boolean, duration, string) from a string,
where an unrecognized flag stops parsing, and how the subcommand structure gives each subcommand
its own flag set. **The real argument list (`os.Args`) is never read** — the flag set is given a
hand-built string array every time, just as the fake driver in the previous lesson was given a
hand-built row. As the closing lesson of both topic and course, it also gathers the twelve
lessons' boundary crossings into a single table.

## The Four Types the Flag Parser Rebuilds from a String

Alongside a single global flag set (`flag.CommandLine`), the flag package also supports building
your own: `flag.NewFlagSet` returns an independent set that can parse any string slice given to
it without ever touching `os.Args`. Only these independent sets are used throughout this lesson
— not a constraint, but a natural consequence of the ban on real sources: `os.Args` is a source
tied to the run's own environment, so the measurement never touches it.

**SD20.** — Four flags (integer, boolean, duration, string) are declared to a flag set; a
hand-built string array is parsed and the type of the value each flag rebuilds is read with `%T`.

```go
// types.go — the four types the flag parser rebuilds from a string
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)

	count := set.Int("count", 0, "count")
	keep := set.Bool("keep", false, "keep it")
	wait := set.Duration("wait", 0, "wait duration")
	name := set.String("name", "", "name")

	input := []string{"-count", "12", "-keep", "-wait", "1h30m", "-name", "bolt"}
	set.Parse(input)

	fmt.Println("-- four types rebuilt from a string --")
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "count", "12", *count, *count)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "keep", "(set)", *keep, *keep)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "wait", "1h30m", *wait, *wait)
	fmt.Printf("%-8s source=%-7q value=%-8v type=%T\n", "name", "bolt", *name, *name)
}
```

```
-- four types rebuilt from a string --
count    source="12"    value=12       type=int
keep     source="(set)" value=true     type=bool
wait     source="1h30m" value=1h30m0s  type=time.Duration
name     source="bolt"  value=bolt     type=string
```

All four flags sit in the source as text — every element in `input` is a `string`, nothing else.
`set.Int` parses `"12"` into the integer behind its `*int` pointer; `set.Bool` reads a flag's
mere presence as `true`; `set.Duration` converts `"1h30m"` into an hours-and-minutes
`time.Duration`; `set.String` returns the same string unconverted. All four confirm their type
with `%T` — the parser does exactly what `Scan` did in Database Access: it rebuilds data with no
type information at the source (there the driver's narrow set, here a bare string) into a Go
type at the target, with a schema declared in advance.

This rebuild is declaration-dependent, not automatic. `set.Int("count", 0, "count")` tells the
parser in advance that `count` will carry an integer; without it the parser has no way to know
whether `"12"` is an integer or a string. Just as a struct tag carries a JSON field's name, the
schema here sits not in the source, but in the code that builds the parser.

## What an Unrecognized Flag Does

A flag set only recognizes names declared to it in advance. When an undeclared name appears in
the string array, the parser does not silently ignore it — it **stops** parsing right there and
returns an error.

**SD21.** — After a known flag, an unrecognized flag appears, followed by a second known flag;
because parsing stops at the unrecognized flag, the second known flag is measured to never be
read.

```go
// unrecognized.go — an unrecognized flag stops parsing, nothing after it is read
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)

	count := set.Int("count", 0, "count")
	name := set.String("name", "initial", "name")

	input := []string{"-count", "5", "-unknown", "x", "-name", "bolt"}
	err := set.Parse(input)

	fmt.Println("-- what an unrecognized flag does --")
	fmt.Println("did parsing return an error:", err != nil)
	fmt.Println("count (read before the unrecognized flag):", *count)
	fmt.Println("name (after the unrecognized flag, never read):", *name)
}
```

```
-- what an unrecognized flag does --
did parsing return an error: true
count (read before the unrecognized flag): 5
name (after the unrecognized flag, never read): initial
```

Because `count` appears before `-unknown`, the parser reads it and assigns `5`. Reaching
`-unknown`, it finds no such flag declared in `set`, and `Parse` immediately returns an error —
the rest (`-name bolt`) is never processed; `name` stays at its default (`initial`). This is the
behavior of most of Go's own commands: parsing is never fully successful or wholly incomplete,
it stays **partial** — flags before the stopping point are read; the ones after are never seen.

`set.SetOutput(io.Discard)` is here for the same reason: in `flag.ContinueOnError` mode, the
parser normally prints its error text to a stream it writes itself, naming which flag was not
recognized. Only `err != nil` is measured; the error text itself is never written anywhere.

## Subcommand Structure: Each Subcommand's Own Flag Set

A single flag set suffices for one flat command, but a tool with more than one sub-function
generally uses a **subcommand** structure — the first argument is a function's name, the rest
are flags specific to it. This structure is not a language feature; it is a natural consequence
of `flag.NewFlagSet` returning independent sets: each subcommand gets its own set, and a flag
declared in one never exists in the other.

**SD22.** — Two subcommands (`add`, `list`) are called with their own flag set; then `add`'s own
flag (`-count`) is handed to `list`'s set and measured to not be recognized.

```go
// subcommand.go — each subcommand carries its own flag set, one is not recognized in the other
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	addSet := flag.NewFlagSet("add", flag.ContinueOnError)
	addSet.SetOutput(io.Discard)
	addName := addSet.String("name", "", "product name")
	addCount := addSet.Int("count", 1, "count")

	listSet := flag.NewFlagSet("list", flag.ContinueOnError)
	listSet.SetOutput(io.Discard)
	listFilter := listSet.String("filter", "", "filter")

	commands := map[string]*flag.FlagSet{"add": addSet, "list": listSet}

	input1 := []string{"add", "-name", "bolt", "-count", "20"}
	commands[input1[0]].Parse(input1[1:])
	fmt.Println("-- subcommand: add, its own flag set --")
	fmt.Printf("name=%s count=%d\n", *addName, *addCount)

	input2 := []string{"list", "-filter", "active"}
	commands[input2[0]].Parse(input2[1:])
	fmt.Println("-- subcommand: list, its own flag set --")
	fmt.Printf("filter=%s\n", *listFilter)

	input3 := []string{"list", "-count", "5"}
	err3 := commands[input3[0]].Parse(input3[1:])
	fmt.Println("-- list never recognizes the 'count' flag in its own set --")
	fmt.Println("did it give an error:", err3 != nil)
}
```

```
-- subcommand: add, its own flag set --
name=bolt count=20
-- subcommand: list, its own flag set --
filter=active
-- list never recognizes the 'count' flag in its own set --
did it give an error: true
```

The subcommand name (`add`, `list`) is not part of any flag set — the array's first element only
selects which set gets called, and is never handed to the parser. The `add` set recognizes
`-name` and `-count`, the `list` set only `-filter`. The third call confirms this: `-count` is
valid in `add`'s set, but handed to `list`'s set the parser never recognizes it. Even defined in
the same package and `main` function, the two sets are completely independent — neither inherits
a flag the other declares.

## Limiting Measurement: Type Is Rebuilt, Meaning Is Not

The previous three measurements showed the parser correctly rebuilding the type it expects from
a string. But "expected type" and "correct value" are not the same: the parser looks at whether
a string syntactically fits that type, never at whether the value is meaningful.

**SD23.** — The `count` flag is given a negative string (`"-3"`); the parser accepts it as a
valid integer and returns no error at all.

```go
// meaning.go — the parser rebuilds the expected type, not the value's meaning
package main

import (
	"flag"
	"fmt"
	"io"
)

func main() {
	set := flag.NewFlagSet("measurement", flag.ContinueOnError)
	set.SetOutput(io.Discard)
	count := set.Int("count", 0, "order count")

	input := []string{"-count", "-3"}
	err := set.Parse(input)

	fmt.Println("-- the parser rebuilds the expected type, not the value's meaning --")
	fmt.Println("did parsing give an error:", err != nil)
	fmt.Printf("count=%d type=%T\n", *count, *count)
}
```

```
-- the parser rebuilds the expected type, not the value's meaning --
did parsing give an error: false
count=-3 type=int
```

`"-3"` fits an integer's syntax exactly — a sign followed by digits — so `set.Int` accepts it,
and `count` takes `-3`, in `int` type, with no error. But a negative order count is invalid in
**meaning**; the parser has no knowledge of this, because its only job is converting the string
to the declared type, not enforcing a value-range rule. The same appeared in earlier lessons: a
struct tag carried a field's name but never checked its validity, a driver's scan rebuilt a
column's type but never asked whether the number inside it was meaningful. This lesson closes
that pattern — a valid-range check is the job of the code that uses the parsed value, not the
parser's.

## Course Wrap-Up

Across twelve lessons, this course asked one question: what happens to type information as a
value crosses a boundary? The shared definition's three claims gave three answers. First: type
information is lost as a rule — in the shared measurement, seven of thirteen crossings delivered
only bytes or text to the other side, a type inside the program reduced beyond the boundary to a
number array or a line. Second: lost information can be rebuilt, but the rebuilding was, every
time, done by the side holding the schema — a struct tag, the target type itself, the type the
parser expects, the type scanning reduces to; five crossings were rebuilt this way. Third,
specific to this course: the one crossing where type information was **preserved** was the one
where both sides shared the same schema — Contract-Based Service Communication showed this in
only one of the thirteen crossings.

The twelve lessons' table is not at the same resolution as the shared measurement's thirteen
crossings: JSON Serialization alone measured four sub-crossings, and this lesson likewise
measured four types in one lesson. The direction still holds — five of the twelve rows fell into
"lost," six into "rebuilt," one into "preserved"; the majority stayed on the side of loss or
schema-based rebuilding. The limiting reading repeated in every lesson: "rebuilt" does not mean
rebuilt **correctly**, or that a meaningful value arrived. A driver's type, a struct tag's field
name, a flag parser's integer — all gave the correct type, none guaranteed the meaning the
source intended.

The course closes without overreaching. The type system only works on this side of the boundary;
on the other side — network, disk, driver, string — there is no such thing as a type, only bytes
and text. Rebuilding the schema is always the holding side's job, and when that schema is wrong
or incomplete, no warning comes: a field with no counterpart silently drops, scanning into a
wrong target fails at run time, a negative count passes unquestioned. Preservation, the
strongest outcome, depends only on both sides sharing the same schema — and that sharing was
never automatic in any lesson, it always had to be built separately.

| Lesson | Crossing measured | Outcome | Limiting measurement |
|---|---|---|---|
| HTTP Server | Whether the request's path, header and body carry type information | lost | The Content-Type header is a hint, not a type — a wrong hint produces no error on the server |
| HTTP Client | The response body being read and closed on the client side | lost | The body is a reader, not a value — a second read comes back empty with no error |
| JSON Serialization | The JSON round trip of fields marked with a struct tag | rebuilt | "Rebuilt" does not mean rebuilt correctly — a field with no counterpart silently drops |
| Templates | The same struct's output passed through two templates | lost | The field name is looked up at run time — a missing-field error is born in the middle of the output |
| Contract-Based Service Communication | The fields of a resource produced with a schema shared by two processes | preserved | Preservation depends on the schema's distribution — if the schema diverges, the crossing silently falls into lost |
| Real-Time Communication | The body of a message pushed over a long-lived connection | lost | Framing gives where the message ends, not the type of the value inside it |
| File and I/O Interfaces | Four sources (a file, a buffer, a string, a network body) passing through the same reader | lost | A partial read and the end of file are separate steps — managing the distinction is the caller's job |
| Embedded Resources | A source file binding to three Go types at compile time | rebuilt | The binary still produces the same content after the source file is deleted — the link was built at compile time |
| Time Operations | A moment turning into text and coming back | rebuilt | The offset is rebuilt but the zone name is not carried — Equal is equal, == and String() are different |
| Structured Logging | The same event written as plain text and as key-value | rebuilt | The field name is preserved, the type is not — rebuilding depends on the reader's own schema |
| Database Access | Five columns from the driver scanned into the target type | rebuilt | Scanning looks at the target's type, not the source's — an incompatible target fails at run time |
| Command-Line Applications | The string given to a flag set parsed into four types | rebuilt | The parser rebuilds the expected type, not the value's meaning — a negative count passes with no error |

## Summary

- The flag parser rebuilt four types (integer, boolean, duration, string) from a hand-built
  string array according to previously declared flag definitions; `os.Args` was never read.
- An unrecognized flag stopped parsing right there: flags before the stopping point were read,
  the ones after were never processed.
- In the subcommand structure, each subcommand carried its own independent flag set; a flag
  declared in one was never recognized in the other.
- Limiting measurement: the parser checked whether a string syntactically fit the expected type,
  not whether the value was meaningful — a negative count passed with no error.
- Across twelve lessons, the course asked the same question at twelve boundaries and recorded
  one of three outcomes; the majority stayed lost or rebuilt, preservation appeared only once,
  where both sides shared the same schema.
- The course gathered the twelve lessons' answers into a single table; each row is read from its
  own source lesson.
