docs: extract goget manpage to standalone troff source
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
Placeholder so the directory is tracked by git. The actual file
|
||||
generated here by `just build` is `goget.1`, copied from
|
||||
`../../../man/goget.1`.
|
||||
+13
-206
@@ -1,217 +1,24 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/core"
|
||||
)
|
||||
|
||||
// manPage holds the canonical goget(1) manual page in troff/groff format.
|
||||
// The source of truth is ../../man/goget.1; a copy is kept here so it can
|
||||
// be embedded into the binary at build time (//go:embed cannot reference
|
||||
// files outside the embedding package). The copy is refreshed by the
|
||||
// `cp` step in `just build`.
|
||||
//
|
||||
//go:embed man-page-data/goget.1
|
||||
var manPage string
|
||||
|
||||
// handleGenManPage implements the --generate-man-page flag. It writes the
|
||||
// embedded manpage to standard output and exits 0.
|
||||
func handleGenManPage() int {
|
||||
fmt.Printf(`.TH GOGET 1 "2026-06" "goget %s" "User Commands"
|
||||
.SH NAME
|
||||
goget \- modern IPv6-first command-line download utility
|
||||
.SH SYNOPSIS
|
||||
.B goget
|
||||
\fB\-\-url\fR \fIURL\fR [\fIOPTIONS\fR]
|
||||
.SH DESCRIPTION
|
||||
goget is a modern replacement for curl and wget built on Go stdlib.
|
||||
Features IPv6-first connections, parallel downloads, resume support,
|
||||
compression, checksum verification, and recursive mirroring.
|
||||
.SH OPTIONS
|
||||
.SS "Target"
|
||||
.TP
|
||||
\fB\-\-url\fR=\fIURL\fR
|
||||
URL to download (can be specified multiple times)
|
||||
.SS "Basic Options"
|
||||
.TP
|
||||
\fB\-\-output\fR=\fIFILE\fR
|
||||
Output filename (default: auto from URL)
|
||||
.TP
|
||||
\fB\-\-verbose\fR
|
||||
Verbose output with progress bar (default: enabled)
|
||||
.TP
|
||||
\fB\-\-no-progress\fR
|
||||
Hide progress bar (keep other output)
|
||||
.TP
|
||||
\fB\-\-quiet\fR
|
||||
Suppress all output except errors
|
||||
.TP
|
||||
\fB\-\-debug\fR
|
||||
Debug mode with detailed info
|
||||
.TP
|
||||
\fB\-\-resume\fR
|
||||
Resume interrupted download
|
||||
.TP
|
||||
\fB\-\-restart\fR
|
||||
Delete existing file and restart download
|
||||
.TP
|
||||
\fB\-\-overwrite\fR
|
||||
Overwrite existing file
|
||||
.TP
|
||||
\fB\-\-no-decompress\fR
|
||||
Disable automatic decompression
|
||||
.TP
|
||||
\fB\-\-parallel\fR=\fIN\fR
|
||||
Parallel connections (0=auto, 1=sequential)
|
||||
.TP
|
||||
\fB\-\-json\fR
|
||||
Output progress as JSON (for scripting)
|
||||
.TP
|
||||
\fB\-\-info\fR
|
||||
Show file information without downloading
|
||||
.TP
|
||||
\fB\-\-benchmark\fR=\fIURL\fR
|
||||
Benchmark download speed
|
||||
.SS "Network"
|
||||
.TP
|
||||
\fB\-\-timeout\fR=\fIDURATION\fR
|
||||
Connection timeout (0 = auto based on size)
|
||||
.TP
|
||||
\fB\-\-proxy\fR=\fIURL\fR
|
||||
Proxy server URL (http://, https://, socks5://, socks5h://)
|
||||
.TP
|
||||
\fB\-\-dns-servers\fR=\fIIPs\fR
|
||||
Custom DNS servers (comma-separated)
|
||||
.TP
|
||||
\fB\-\-max-retries\fR=\fIN\fR
|
||||
Maximum number of retries on failure
|
||||
.TP
|
||||
\fB\-\-no-ipv4\fR
|
||||
Disable IPv4 fallback
|
||||
.TP
|
||||
\fB\-\-no-redirect\fR
|
||||
Disable following HTTP redirects
|
||||
.TP
|
||||
\fB\-\-show-headers\fR
|
||||
Print HTTP response headers
|
||||
.TP
|
||||
\fB\-\-keep-timestamps\fR
|
||||
Set file modification time from Last-Modified header
|
||||
.TP
|
||||
\fB\-\-content-disposition\fR
|
||||
Use server-provided filename from Content-Disposition header
|
||||
.SS "Authentication"
|
||||
.TP
|
||||
Credentials are loaded automatically from \fB~/.netrc\fR when no \fB\-\-username\fR is given.
|
||||
.TP
|
||||
\fB\-\-username\fR=\fIUSER\fR
|
||||
Username for HTTP Basic/Digest Auth
|
||||
.TP
|
||||
\fB\-\-password\fR=\fIPASS\fR
|
||||
Password for HTTP Basic/Digest Auth
|
||||
.TP
|
||||
\fB\-\-password-file\fR=\fIFILE\fR
|
||||
Read password from file
|
||||
.TP
|
||||
\fB\-\-auth-type\fR=\fITYPE\fR
|
||||
Auth type: basic, digest, auto
|
||||
.TP
|
||||
\fB\-\-cookie-jar\fR=\fIFILE\fR
|
||||
File for loading/saving cookies
|
||||
.TP
|
||||
\fB\-\-cookie\fR=\fINAME=VALUE\fR
|
||||
Set cookie from CLI (repeatable, curl-compatible)
|
||||
.TP
|
||||
\fB\-\-retry-all-errors\fR
|
||||
Retry on any HTTP 4xx/5xx response
|
||||
.TP
|
||||
\fB\-\-ssl-key-log\fR=\fIFILE\fR
|
||||
Log TLS session keys to file (SSLKEYLOGFILE for Wireshark)
|
||||
.SS "Config"
|
||||
.TP
|
||||
\fB\-\-config\fR=\fIFILE\fR
|
||||
Path to config file
|
||||
.TP
|
||||
\fB\-\-config-get\fR=\fIKEY\fR
|
||||
Get a config value
|
||||
.TP
|
||||
\fB\-\-config-set\fR=\fIKEY\fR \fIVALUE\fR
|
||||
Set a config value
|
||||
.TP
|
||||
\fB\-\-config-list\fR
|
||||
List all config values
|
||||
.TP
|
||||
\fB\-\-config-unset\fR=\fIKEY\fR
|
||||
Unset a config value
|
||||
.TP
|
||||
\fB\-\-init-config\fR
|
||||
Generate default config file
|
||||
.SS "Mirror & Recursive"
|
||||
.TP
|
||||
\fB\-\-mirror\fR
|
||||
Mirror entire website (infinite depth)
|
||||
.TP
|
||||
\fB\-\-recursive\fR
|
||||
Enable recursive downloading
|
||||
.TP
|
||||
\fB\-\-max-depth\fR=\fIN\fR
|
||||
Maximum recursion depth
|
||||
.SS "Scheduling & Hooks"
|
||||
.TP
|
||||
\fB\-\-schedule\fR=\fITIME\fR
|
||||
Schedule download ("YYYY-MM-DD HH:MM" or cron "0 2 * * *")
|
||||
.TP
|
||||
\fB\-\-on-complete\fR=\fICMD\fR
|
||||
Run shell command after successful download
|
||||
.TP
|
||||
\fB\-\-bind-interface\fR=\fIIFACE\fR
|
||||
Bind to specific network interface
|
||||
.SS "Other"
|
||||
.TP
|
||||
\fB\-\-batch-file\fR=\fIFILE\fR
|
||||
Download multiple URLs from a file
|
||||
.TP
|
||||
\fB\-\-checksum-file\fR=\fIFILE\fR
|
||||
Read expected checksum from SHA256SUMS file
|
||||
.TP
|
||||
\fB\-\-generate-man-page\fR
|
||||
Generate this man page
|
||||
.TP
|
||||
\fB\-\-help\fR
|
||||
Show help
|
||||
.TP
|
||||
\fB\-\-version\fR
|
||||
Show version
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
Download a file:
|
||||
goget --url https://example.com/file.zip
|
||||
.TP
|
||||
Parallel download:
|
||||
goget --url https://example.com/large.iso --parallel 4
|
||||
.TP
|
||||
Resume interrupted download:
|
||||
goget --url https://example.com/file.zip --resume
|
||||
.TP
|
||||
Mirror website:
|
||||
goget --url https://example.com --mirror --output ./mirror
|
||||
.TP
|
||||
Recursive download:
|
||||
goget --url https://example.com --recursive --max-depth 2
|
||||
.TP
|
||||
JSON progress (for scripting):
|
||||
goget --url https://example.com/file.zip --json --no-progress > progress.jsonl
|
||||
.TP
|
||||
Scheduled download at 2 AM:
|
||||
goget --url https://example.com/daily.zip --schedule "0 2 * * *"
|
||||
.TP
|
||||
Run import script after download:
|
||||
goget --url https://example.com/data.csv --on-complete "import.sh"
|
||||
.SH FILES
|
||||
.TP
|
||||
~/.config/goget/config.json
|
||||
Configuration file
|
||||
.TP
|
||||
<file>.goget.meta
|
||||
Resume metadata file
|
||||
.SH BUGS
|
||||
Report bugs at https://codeberg.org/petrbalvin/goget/issues
|
||||
.SH AUTHOR
|
||||
Petr Balvin
|
||||
.SH LICENSE
|
||||
MIT License`, core.Version)
|
||||
fmt.Print(manPage)
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//go:build linux || freebsd
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"codeberg.org/petrbalvin/goget/internal/config"
|
||||
)
|
||||
|
||||
// manPageRequiredSections lists the conventional manpage sections that
|
||||
// goget(1) must include. Every section in this list must be present
|
||||
// in the manpage source as a ".SH <NAME>" header. Adding a new section
|
||||
// here forces the manpage author to document it.
|
||||
var manPageRequiredSections = []string{
|
||||
"NAME",
|
||||
"SYNOPSIS",
|
||||
"DESCRIPTION",
|
||||
"OPTIONS",
|
||||
"EXAMPLES",
|
||||
"EXIT STATUS",
|
||||
"FILES",
|
||||
"ENVIRONMENT",
|
||||
"SEE ALSO",
|
||||
"HISTORY",
|
||||
"AUTHORS",
|
||||
"REPORTING BUGS",
|
||||
"COPYRIGHT",
|
||||
}
|
||||
|
||||
// TestManPageEmbedded is the simplest smoke test: the embed succeeded
|
||||
// and the variable is not empty. Any first line beginning with `.`
|
||||
// (either a `.\"` comment or a macro such as `.TH`) is accepted as
|
||||
// proof the embedded blob is troff source.
|
||||
func TestManPageEmbedded(t *testing.T) {
|
||||
if manPage == "" {
|
||||
t.Fatal("embedded manPage is empty — //go:embed failed at build time")
|
||||
}
|
||||
if !strings.HasPrefix(manPage, ".") {
|
||||
t.Errorf("embedded manPage does not look like troff (got prefix %q)", firstLine(manPage))
|
||||
}
|
||||
}
|
||||
|
||||
// TestManPageHasTHHeader verifies the .TH macro is present with the
|
||||
// expected program name, section, and version. The .TH line is the
|
||||
// entry point that `man` uses to identify the page.
|
||||
func TestManPageHasTHHeader(t *testing.T) {
|
||||
if !strings.Contains(manPage, ".TH GOGET 1 ") {
|
||||
t.Error("manpage is missing the .TH GOGET 1 header")
|
||||
}
|
||||
}
|
||||
|
||||
// TestManPageHasMandatorySections asserts that every section in
|
||||
// manPageRequiredSections exists as a .SH heading. This keeps the page
|
||||
// discoverable in `man goget` and ensures consistency with project
|
||||
// documentation conventions.
|
||||
//
|
||||
// In troff, multi-word section names must be quoted to keep the
|
||||
// spaces inside a single argument: `.SH "SEE ALSO"`. The matcher
|
||||
// accepts both the bare form (`.SH SEE ALSO`) and the quoted form
|
||||
// (`.SH "SEE ALSO"`), so authors can use either convention.
|
||||
func TestManPageHasMandatorySections(t *testing.T) {
|
||||
for _, sec := range manPageRequiredSections {
|
||||
bare := "\n.SH " + sec + "\n"
|
||||
quoted := "\n.SH \"" + sec + "\"\n"
|
||||
if !strings.Contains(manPage, bare) && !strings.Contains(manPage, quoted) {
|
||||
t.Errorf("manpage is missing required section: .SH %s (or .SH \"%s\")", sec, sec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestManPageDocumentsEveryFlag is a guard against drift between the
|
||||
// CLI surface (defineFlags) and the manpage. For every flag
|
||||
// registered on the FlagSet, the manpage must contain the
|
||||
// corresponding option entry. The expected on-page form escapes
|
||||
// every hyphen in the flag name as `\-` (per the convention used
|
||||
// throughout the manpage) and prefixes the entry with `\-\-`.
|
||||
//
|
||||
// Adding a new flag in defineFlags but forgetting the manpage (or
|
||||
// vice versa) makes this test fail with a clear missing-flag list.
|
||||
func TestManPageDocumentsEveryFlag(t *testing.T) {
|
||||
cfg := config.DefaultConfig()
|
||||
fs := newTestFlagSet("goget-test-man-page")
|
||||
defineFlags(fs, cfg)
|
||||
|
||||
var missing []string
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
escaped := strings.ReplaceAll(f.Name, "-", `\-`)
|
||||
needle := `\-\-` + escaped
|
||||
if !strings.Contains(manPage, needle) {
|
||||
missing = append(missing, f.Name)
|
||||
}
|
||||
})
|
||||
if len(missing) > 0 {
|
||||
sort.Strings(missing)
|
||||
t.Errorf("manpage does not document %d flag(s):\n --%s",
|
||||
len(missing), strings.Join(missing, "\n --"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestManPageRendersWithGroff runs `groff -man -ww -Kutf-8 -z` on the
|
||||
// embedded manpage and fails if it reports any warnings or errors.
|
||||
// The `-z` flag suppresses output; `-ww` enables maximum warning
|
||||
// verbosity; `-Kutf-8` accepts UTF-8 source (em-dash, etc.).
|
||||
//
|
||||
// If `groff` is not installed on the test machine the test is
|
||||
// skipped — we still want it to pass on minimal CI images — but a
|
||||
// CI image with `groff` available will catch regressions in the
|
||||
// troff source.
|
||||
func TestManPageRendersWithGroff(t *testing.T) {
|
||||
if _, err := exec.LookPath("groff"); err != nil {
|
||||
t.Skip("groff not installed; skipping manpage render check")
|
||||
}
|
||||
cmd := exec.Command("groff", "-man", "-ww", "-Kutf-8", "-z")
|
||||
cmd.Stdin = strings.NewReader(manPage)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("groff rejected the manpage: %v\n%s", err, out)
|
||||
} else if len(out) > 0 {
|
||||
t.Fatalf("groff produced unexpected output:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// firstLine returns the first line of s, trimmed of the trailing
|
||||
// newline. Used only for error messages.
|
||||
func firstLine(s string) string {
|
||||
if i := strings.IndexByte(s, '\n'); i >= 0 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user