Skip to content
academia.sh

Lesson 12 / 12

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.

Published

In this lesson

  • 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
Contents

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.

// 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.

// 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.

// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close