From 9370f9c3ee04f63cf26be89c4680fd7f08e81875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Mon, 13 Jul 2026 19:50:38 +0200 Subject: [PATCH] feat(cli): standard --help and --version with per-command usage Assisted-by: Qwen 3.8 Max Preview --- cmd/gasm/main.go | 124 ++++++++++++++++++++++++++++++++++-------- cmd/gasm/main_test.go | 20 ++++++- justfile | 2 +- 3 files changed, 120 insertions(+), 26 deletions(-) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 0808f33..b7bba5f 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -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 print the lexical token stream - gasm parse 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 run static checks - gasm asm [-o out.bin] assemble to machine code (amd64, Phase 2) - gasm lsp run the language server over stdio - gasm version print the version + gasm [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 -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 ", ` +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 ") @@ -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 ", ` +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 ") @@ -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 ", ` +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] ", ` +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 { diff --git a/cmd/gasm/main_test.go b/cmd/gasm/main_test.go index 6d7799b..c14c6af 100644 --- a/cmd/gasm/main_test.go +++ b/cmd/gasm/main_test.go @@ -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) } } diff --git a/justfile b/justfile index d1817a2..e5f21b5 100644 --- a/justfile +++ b/justfile @@ -3,7 +3,7 @@ # gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm). -version := "0.7.0" +version := "0.8.0" default: @just --list