First CLI Tools with os.Args
First CLI Tools with os.Args
Overview
Before flag, understand raw arguments: os.Args[0] is the program name, os.Args[1:] is everything the user typed. Many tiny tools need only this.
Hello CLI
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("args:", os.Args)
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: greeter <name>")
os.Exit(2)
}
fmt.Printf("hello, %s\n", os.Args[1])
}go run . Ada
# hello, Ada
go run .
# usage: greeter <name> (exit 2)Separate main from logic
package main
import (
"fmt"
"io"
"os"
)
func main() {
if err := run(os.Args[1:], os.Stdout); err != nil {
fmt.Fprintln(os.Stderr, "greeter:", err)
os.Exit(1)
}
}
func run(args []string, stdout io.Writer) error {
if len(args) < 1 {
return fmt.Errorf("usage: greeter <name>")
}
_, err := fmt.Fprintf(stdout, "hello, %s\n", args[0])
return err
}Now tests call run([]string{"Ada"}, &buf) without exiting the test process.
Positional args patterns
Required path
func run(args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: cat1 <file>")
}
b, err := os.ReadFile(args[0])
if err != nil {
return err
}
_, err = os.Stdout.Write(b)
return err
}Variadic files (like cat)
func run(args []string, stdout io.Writer) error {
if len(args) == 0 {
_, err := io.Copy(stdout, os.Stdin)
return err
}
for _, path := range args {
f, err := os.Open(path)
if err != nil {
return err
}
_, err = io.Copy(stdout, f)
f.Close()
if err != nil {
return err
}
}
return nil
}Options before flags package (-n style DIY)
func parse(args []string) (count int, rest []string, err error) {
count = 1
i := 0
for i < len(args) {
a := args[i]
if a == "--" {
i++
break
}
if len(a) > 1 && a[0] == '-' {
// fragile DIY — prefer flag package next chapter
return 0, nil, fmt.Errorf("unknown option %q", a)
}
break
}
return count, args[i:], nil
}Lesson: hand-parsing flags gets messy quickly. Use flag (or Cobra) for real options.
Program name for usage
name := filepath.Base(os.Args[0])
fmt.Fprintf(os.Stderr, "usage: %s <file>\n", name)Environment as “args”
if os.Getenv("DEBUG") != "" {
fmt.Fprintln(os.Stderr, "debug on")
}Env is global config; args are per-invocation. Chapter 305 covers layering.
Example: add calculator
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: add <a> <b>")
}
a, err := strconv.ParseFloat(args[0], 64)
if err != nil {
return fmt.Errorf("a: %w", err)
}
b, err := strconv.ParseFloat(args[1], 64)
if err != nil {
return fmt.Errorf("b: %w", err)
}
fmt.Println(a + b)
return nil
}Example: whichdir — print working directory variants
func run(args []string) error {
wd, err := os.Getwd()
if err != nil {
return err
}
fmt.Println(wd)
if len(args) > 0 && args[0] == "-base" {
fmt.Println(filepath.Base(wd))
}
return nil
}Rules of thumb
| Do | Don’t |
|---|---|
Put logic in run(...) |
Scatter os.Exit deep in packages |
| Usage on stderr, exit 2 for misuse | Print usage to stdout |
Validate len(args) early |
Index args[1] without checks |
Try next
- Write
echo1that joins args with spaces (likeecho). - Write
headnthat prints the first line of a file (no flags yet). - Add a unit test for
runwith abytes.Bufferas stdout.