feat(cli): standard --help and --version with per-command usage

Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
2026-07-13 19:50:38 +02:00
parent e98680597d
commit 9370f9c3ee
3 changed files with 120 additions and 26 deletions
+101 -23
View File
@@ -28,7 +28,7 @@ import (
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.7.0"
var version = "0.8.0"
func main() {
if len(os.Args) < 2 {
@@ -49,31 +49,71 @@ func main() {
case "lsp":
os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V":
fmt.Printf("gasm %s\n", version)
case "help", "-h", "--help":
os.Exit(cmdVersion())
case "help", "--help", "-h":
usage(os.Stdout)
default:
fmt.Fprintf(os.Stderr, "gasm: unknown command %q\n\n", os.Args[1])
usage(os.Stderr)
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
os.Exit(2)
}
}
// cmdVersion prints the release version.
func cmdVersion() int {
fmt.Printf("gasm %s\n", version)
return 0
}
func usage(w io.Writer) {
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler
fmt.Fprintf(w, `gasm %s — developer tooling for Go's Plan 9 assembler (GAsm)
gasm bundles a lexer, parser, formatter, linter, standalone assembler and
language server for Plan 9 assembly into one self-contained binary.
Usage:
gasm tokens <file> print the lexical token stream
gasm parse <file> parse and report syntax errors
gasm fmt [-w] [path...] canonicalise formatting (no path or a directory:
reformat every .s below it in place, like go fmt)
gasm lint <file...> run static checks
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2)
gasm lsp run the language server over stdio
gasm version print the version
gasm <command> [arguments]
gasm [flags]
Commands:
tokens print the lexical token stream
parse parse and report syntax errors
fmt canonicalise formatting (gofmt for assembly)
lint run static checks
asm assemble .s files to machine code (amd64)
lsp run the language server over stdio
version print the version (same as --version)
Flags:
-h, --help show this help
-V, --version print the version
Run "gasm <command> -h" for a command's usage and flags.
Examples:
gasm fmt reformat every .s below the current directory
gasm lint go-flac/*.s run static checks over the kernels
gasm asm -o k.bin kern_amd64.s
`, version)
}
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
// proper usage block: the one-line usage, the long description and the flag
// defaults. The flag package routes -h/--help to fs.Usage and exits 0.
func newCommand(name, usageLine, long string) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ExitOnError)
fs.Usage = func() {
w := fs.Output()
fmt.Fprintf(w, "Usage: %s\n\n%s\n", usageLine, strings.TrimSpace(long))
hasFlags := false
fs.VisitAll(func(*flag.Flag) { hasFlags = true })
if hasFlags {
fmt.Fprintln(w, "\nFlags:")
fs.PrintDefaults()
}
}
return fs
}
// readSource returns the contents of path, or stdin when path is "-".
func readSource(path string) (string, error) {
if path == "-" {
@@ -85,7 +125,10 @@ func readSource(path string) (string, error) {
}
func cmdTokens(args []string) int {
fs := flag.NewFlagSet("tokens", flag.ExitOnError)
fs := newCommand("tokens", "gasm tokens <file>", `
Print the lexical token stream of FILE: position, token kind and text, one
token per line. FILE may be "-" to read standard input.
`)
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm tokens <file>")
@@ -103,7 +146,11 @@ func cmdTokens(args []string) int {
}
func cmdParse(args []string) int {
fs := flag.NewFlagSet("parse", flag.ExitOnError)
fs := newCommand("parse", "gasm parse <file>", `
Parse FILE and report syntax errors on stderr. On success, print how many
declarations and TEXT functions the file contains. FILE may be "-" to read
standard input.
`)
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm parse <file>")
@@ -133,14 +180,24 @@ func cmdParse(args []string) int {
}
func cmdFmt(args []string) int {
fs_ := flag.NewFlagSet("fmt", flag.ExitOnError)
write := fs_.Bool("w", false, "write result to the source file")
fs_.Parse(args)
fs := newCommand("fmt", "gasm fmt [-w] [path...]", `
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
spacing, per-function mnemonic alignment and blank-line layout (exactly one
blank line before each label, TEXT and GLOBL block). Formatting is
idempotent and preserves every line, comments included.
With no paths — or a directory path — every .s file below it is reformatted
in place and the changed files are listed, the way go fmt does; "." and "_"
directories are skipped. Explicit file paths print to stdout unless -w is
given.
`)
write := fs.Bool("w", false, "write result to the source file")
fs.Parse(args)
// Like go fmt: with no arguments, or with a directory argument, every .s
// file below the directory is formatted in place and the names of the
// changed files are listed; explicit file arguments keep the -w / stdout
// behaviour.
paths := fs_.Args()
paths := fs.Args()
dirMode := len(paths) == 0
if dirMode {
paths = []string{"."}
@@ -215,7 +272,16 @@ func asmFiles(dir string) ([]string, error) {
}
func cmdLint(args []string) int {
fs := flag.NewFlagSet("lint", flag.ExitOnError)
fs := newCommand("lint", "gasm lint <file...>", `
Run the static checks over the given files and print diagnostics as
"file:line:col: severity: message [code]". The exit status is non-zero when
an error-severity diagnostic is found; warnings (e.g. the register-clobber
audit) do not affect it.
Rules include unknown-instruction, operand-count, undefined-label,
duplicate-label, missing-ret, missing-textflag-include, abi-argsize,
unreachable-code, register-clobber and funcdata-pcdata.
`)
disable := fs.String("disable", "", "comma-separated rule codes to disable")
fs.Parse(args)
if fs.NArg() == 0 {
@@ -256,7 +322,13 @@ func cmdLint(args []string) int {
}
func cmdLSP(args []string) int {
fs := flag.NewFlagSet("lsp", flag.ExitOnError)
fs := newCommand("lsp", "gasm lsp", `
Run the language server over standard input/output: JSON-RPC 2.0 with
Content-Length framing. Point an LSP-capable editor at the binary and
associate it with .s files; the target architecture is inferred from the file
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
hover, document symbols, diagnostics and semantic-token highlighting.
`)
fs.Parse(args)
srv := lsp.New(os.Stdin, os.Stdout)
if err := srv.Run(); err != nil {
@@ -267,7 +339,13 @@ func cmdLSP(args []string) int {
}
func cmdAsm(args []string) int {
fs := flag.NewFlagSet("asm", flag.ExitOnError)
fs := newCommand("asm", "gasm asm [-o out.bin] <file>", `
Assemble FILE (amd64) without the Go toolchain: every TEXT function is
encoded to machine code — scalar, VEX/AVX2 and EVEX/AVX-512 instructions,
FP/SP frame mapping, local labels and file-local static symbols (GLOBL/DATA)
resolved RIP-relative — and printed as a hex dump. With -o the concatenated
image (functions followed by the data section) is written to a file instead.
`)
out := fs.String("o", "", "write the concatenated machine code to this file")
fs.Parse(args)
if fs.NArg() != 1 {
+18 -2
View File
@@ -200,8 +200,24 @@ func TestCmdFmtWrite(t *testing.T) {
func TestUsage(t *testing.T) {
var b bytes.Buffer
usage(&b)
if !strings.Contains(b.String(), "gasm") {
t.Errorf("usage text unexpected:\n%s", b.String())
out := b.String()
for _, want := range []string{
"gasm", "Commands:", "Flags:", "--help", "--version",
"tokens", "parse", "fmt", "lint", "asm", "lsp", "version",
} {
if !strings.Contains(out, want) {
t.Errorf("usage text missing %q:\n%s", want, out)
}
}
}
func TestCmdVersion(t *testing.T) {
out, _, code := capture(func() int { return cmdVersion() })
if code != 0 {
t.Fatalf("code = %d", code)
}
if !strings.Contains(out, version) {
t.Errorf("version output %q does not mention %q", out, version)
}
}