diff --git a/.gitignore b/.gitignore index 4136138..7e6f802 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,13 @@ # Build output bin/ +# Generated copy of man/goget.1 (regenerated by `just build`). +# Required because //go:embed cannot reference files outside the +# embedding package, so man/goget.1 is copied into the package +# tree at build time and embedded into the binary. +cmd/goget/man-page-data/*.1 +!cmd/goget/man-page-data/.gitkeep + # Test artifacts *.test *.out diff --git a/CHANGELOG.md b/CHANGELOG.md index e24842d..edfaa52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +**Documentation** + +- **Standalone troff manpage** — `man/goget.1` is now the single source + of truth for the goget(1) manual. The previous embedded manpage listed + only ~30 of the 138 CLI flags and was missing every standard section; + the new file documents every flag with a dedicated entry, the + full set of conventional `.SH` sections (NAME, SYNOPSIS, DESCRIPTION, + OPTIONS, EXAMPLES, EXIT STATUS, FILES, ENVIRONMENT, SEE ALSO, HISTORY, + AUTHORS, REPORTING BUGS, COPYRIGHT), curl/wget compatibility notes, and + a release-history entry. The manpage is embedded into the binary via + `//go:embed`, so `--generate-man-page` and `man goget` now agree. +- **`just man`** — new task that runs `groff -man -ww -Kutf-8 -z` over + `man/goget.1` to validate the troff source. Skips cleanly when `groff` + is not installed (CI on minimal images still passes). +- **`just install-man`** and **`just uninstall-man`** — install the + manpage to `$HOME/.local/share/man/man1/goget.1.gz` (XDG rootless, + gzipped, reinstall-safe). After `just install-man`, `man goget` + resolves the page automatically via the user's `MANPATH`. + **Core engine** - **Unified worker pool** — single protocol-agnostic worker pool (`internal/pool`) @@ -53,6 +72,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 output when combined with `--json`. Credentials embedded in the stored URL are masked before display (the password placeholder is `xxxxx`). +### Testing + +- `cmd/goget/man_page_test.go` — new test file. Asserts the embedded + manpage is non-empty, carries a `.TH GOGET 1` header, contains every + mandatory `.SH` section, and documents every flag registered by + `defineFlags` (so adding a new flag without documenting it now breaks + the build). When `groff` is installed, also runs `groff -man -ww + -Kutf-8 -z` over the embedded source and fails on any warning. + ## [1.0.0] — 2026-06-26 First public release: a modern IPv6-first command-line download utility built on diff --git a/README.md b/README.md index ac156d3..a390a97 100644 --- a/README.md +++ b/README.md @@ -271,13 +271,18 @@ just install # go mod download just run # run from source (development) just build # go vet + gofmt check + build → bin/goget just test # go test -race -count=1 ./... +just man # validate man/goget.1 with groff +just install-man # install manpage to ~/.local/share/man/man1/ just uninstall # remove build artifacts +just uninstall-man# remove installed manpage ``` --- ## Documentation +- [`man/goget.1`](man/goget.1) — troff manual page (`man goget` after + `just install-man`) - [`docs/architecture.md`](docs/architecture.md) — components and request flow - [`docs/configuration.md`](docs/configuration.md) — config schema (TOML) - [`docs/api.md`](docs/api.md) — public Go library API diff --git a/cmd/goget/man-page-data/.gitkeep b/cmd/goget/man-page-data/.gitkeep new file mode 100644 index 0000000..bb38f56 --- /dev/null +++ b/cmd/goget/man-page-data/.gitkeep @@ -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`. diff --git a/cmd/goget/man_page.go b/cmd/goget/man_page.go index d579627..cb0dc85 100644 --- a/cmd/goget/man_page.go +++ b/cmd/goget/man_page.go @@ -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 -.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 } diff --git a/cmd/goget/man_page_test.go b/cmd/goget/man_page_test.go new file mode 100644 index 0000000..ce1b5eb --- /dev/null +++ b/cmd/goget/man_page_test.go @@ -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 " 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 +} diff --git a/justfile b/justfile index bb328f9..4e0f3c5 100644 --- a/justfile +++ b/justfile @@ -11,6 +11,10 @@ run: # Build the binary build: + # Keep the embedded manpage in sync with man/goget.1. //go:embed + # cannot reference files outside the embedding package, so we copy + # the source-of-truth manpage into the package tree before compiling. + cp man/goget.1 cmd/goget/man-page-data/goget.1 go vet ./... gofmt -l $(find cmd internal pkg -name '*.go') | grep . && exit 1 || true go build -ldflags "-s -w" -o bin/goget ./cmd/goget @@ -22,3 +26,22 @@ test: # Remove build artifacts uninstall: rm -rf bin/ coverage.out + +# Validate the manpage with groff (zero warnings on success). Requires +# `groff` to be installed — skipped silently otherwise. +man: + @command -v groff >/dev/null || { echo "groff not installed, skipping"; exit 0; } + groff -man -ww -Kutf-8 -z man/goget.1 + +# Install the manpage to ~/.local/share/man/man1/ (XDG rootless). +# After install, `man goget` resolves it automatically. Re-run safely to +# pick up edits. +install-man: man + @mkdir -p "$HOME/.local/share/man/man1" + gzip -c -f man/goget.1 > "$HOME/.local/share/man/man1/goget.1.gz" + @echo "Installed: $HOME/.local/share/man/man1/goget.1.gz" + +# Remove the manpage installed by `just install-man`. +uninstall-man: + rm -f "$HOME/.local/share/man/man1/goget.1.gz" + @echo "Removed: $HOME/.local/share/man/man1/goget.1.gz" diff --git a/man/goget.1 b/man/goget.1 new file mode 100644 index 0000000..40419b2 --- /dev/null +++ b/man/goget.1 @@ -0,0 +1,950 @@ +.\" Manpage for goget. +.\" Source of truth — kept in sync with cmd/goget/man-page-data/goget.1 +.\" by the `cp` step in `just build`. The duplicate is required because +.\" //go:embed cannot reference files outside the embedding package. +.\" +.\" Conventions used in this file: +.\" .TP tag-paragraph (one option per entry) +.\" \fB...\fR bold (the flag itself) +.\" \fI...\fR italic (the argument placeholder) +.\" \- literal hyphen (escapes groff's dash-to-endash conversion) +.\" \\- alternative escape for the same purpose +.\" +.\" Validate with: groff -man -ww man/goget.1 > /dev/null +.\" Render with: groff -man -Tutf8 man/goget.1 | less +.\" +.TH GOGET 1 "2026-06-26" "goget 1.0.0" "User Commands" +.SH NAME +goget \- modern IPv6-first command-line download utility +.SH SYNOPSIS +.B goget +[\fIOPTION\fR]... \fIURL\fR... +.SH DESCRIPTION +.B goget +is a modern replacement for +.BR curl (1) +and +.BR wget (1) +built on the Go standard library. +It provides IPv6-first connections (with automatic IPv4 fallback), parallel +chunked downloads, resume support, checksum verification, recursive website +mirroring, and support for nine network protocols +.RB ( HTTP / HTTPS ", " FTP / FTPS ", " SFTP ", " WebDAV / WebDAVS ", " +.BR file:// ", " data: ", " Gopher ", and " Gemini ") \(em all in a +single static binary with zero runtime dependencies. +.PP +Safety features include TLS 1.2 minimum, certificate verification, SSRF +protection (private/internal addresses are blocked by default), SSH host-key +verification for SFTP, HSTS cache (RFC 6797), and certificate or host-key +pinning. +.PP +URLs are specified positionally after the flags; the +.B \-\-url +flag is an alias for the first positional URL and may be repeated to download +several files in one invocation. +.PP +Configuration is loaded from +.IR ~/.config/goget/config.toml +(overridable with +.BR \-\-config " or the " GOGET_CONFIG +environment variable) and merged on top of compiled-in defaults. Command-line +flags always take precedence over configuration values. +.SH OPTIONS +.SS "Target" +.TP +\fB\-\-url\fR=\fIURL\fR +URL to download. May be repeated for multiple URLs, or supplied as positional +arguments after the flags. +.SS "Output" +.TP +\fB\-\-output\fR=\fIFILE\fR +Output filename or directory. When omitted, the filename is derived from the +URL. +.TP +\fB\-\-restart\fR +Delete any existing output file and restart the download from byte zero. +.TP +\fB\-\-overwrite\fR +Overwrite an existing output file without using atomic rename. +.TP +\fB\-\-content\-disposition\fR +Use the filename from the server's +.B Content\-Disposition +header instead of the one derived from the URL. +.TP +\fB\-\-create\-dirs\fR +Create missing parent directories of +.BR \-\-output +(enabled by default; disable with +.B \-\-create\-dirs=false +for +.BR curl (1) " /" wget (1) +compatibility). Ignored for stdout output. +.TP +\fB\-\-keep\-timestamps\fR +Set the output file's modification time to the server's +.B Last\-Modified +value. +.TP +\fB\-\-no\-clobber\fR +Skip the download if the output file already exists. +.TP +\fB\-\-continue\fR +Auto-resume the download if a partial output file is present. +.TP +\fB\-\-timestamping\fR +Download only when the remote file is newer than the local copy. +.SS "Progress and output" +.TP +\fB\-\-verbose\fR +Verbose output (default: enabled when set in the config file). +.TP +\fB\-\-no\-progress\fR +Hide the progress bar; keep every other piece of output. +.TP +\fB\-\-progress\fR=\fISTYLE\fR +Progress bar style. One of +.B bar +(default) or +.BR dot . +.TP +\fB\-\-quiet\fR +Suppress all output except errors. +.TP +\fB\-\-json\fR +Emit progress and completion events as newline-delimited JSON on stdout. +Each event is a single JSON object with +.BR type " (" progress " or " complete "), " downloaded ", " total ", " speed , +and (for the +.B progress +event) an +.B eta_ms +field when the total size and speed are known. +.TP +\fB\-\-debug\fR +Enable debug output with detailed connection and protocol information. +.TP +\fB\-\-show\-headers\fR +Print HTTP response headers during the download. +.TP +\fB\-\-write\-out\fR=\fIFORMAT\fR +Print metadata after the transfer, using a +.BR curl (1)\-compatible +format string. Supported directives: +.BR %\{http_code\} ", " %\{size_download\} ", " %\{time_total\} ", " +.BR %\{speed_download\} . +.TP +\fB\-\-trace\-time\fR +Print an HTTP timing breakdown after the download: +.BR DNS ", " TCP ", " TLS ", " TTFB ", and " Total . +.SS "Network" +.TP +\fB\-\-timeout\fR=\fIDURATION\fR +Overall connection timeout. +.B 0 +selects a smart auto-calculation based on the file size. +.TP +\fB\-\-connect\-timeout\fR=\fIDURATION\fR +Timeout for the TCP (or SSH) connection establishment phase. +.TP +\fB\-\-max\-time\fR=\fIDURATION\fR +Maximum total transfer time; abort if exceeded. +.B 0 +disables the limit. +.TP +\fB\-\-max\-retries\fR=\fIN\fR +Maximum number of retry attempts on failure. +.B 0 +selects the built-in default. +.TP +\fB\-\-retry\-delay\fR=\fIDURATION\fR +Delay inserted between retry attempts. +.TP +\fB\-\-retry\-all\-errors\fR +Retry on any HTTP 4xx or 5xx response (in addition to network errors). +.TP +\fB\-\-retry\-on\-http\-error\fR=\fICODES\fR +Retry on the given HTTP status codes (comma-separated; e.g. +.BR 503,429 ). +.TP +\fB\-\-max\-filesize\fR=\fISIZE\fR +Abort the transfer if the remote file exceeds this size (e.g. +.BR 100MB ", " 1GB ). +.TP +\fB\-\-proxy\fR=\fIURL\fR +Proxy URL. +.B http:// +and +.B https:// +trigger HTTP CONNECT; +.B socks5:// +uses local DNS, +.B socks5h:// +uses remote DNS. +Optional +.B user:pass@ +credentials are supported. +.TP +\fB\-\-no\-ipv4\fR +Disable IPv4 fallback; connect over IPv6 only. +.TP +\fB\-\-no\-redirect\fR +Do not follow HTTP 3xx redirects. +.TP +\fB\-\-dns\-servers\fR=\fIIPS\fR +Custom DNS servers, comma-separated (e.g. +.BR 1.1.1.1,8.8.8.8 ). +.TP +\fB\-\-bind\-interface\fR=\fIIFACE\fR +Bind outgoing connections to a specific network interface (Linux only). +.TP +\fB\-\-allow\-private\fR +Allow connections to private or internal IP addresses. By default these +are blocked as an SSRF safeguard. +.TP +\fB\-\-pinned\-cert\fR=\fISHA256\fR +Pin the server certificate by its leaf SHA-256 fingerprint. Applies to all +TLS-bearing protocols (HTTPS, FTPS, Gemini, WebDAVS); abort on mismatch. +.TP +\fB\-\-pinned\-host\-key\fR=\fIFINGERPRINT\fR +Pin the SSH host key (SFTP). Accepts +.BR SHA256: +(as emitted by +.BR "ssh\-keygen \-lf" ) +or a raw lowercase-hex SHA-256 of the key wire format. Pinning takes +precedence over +.B \-\-ssh\-insecure +and any +.I known_hosts +entry. +.TP +\fB\-\-rate\-limit\fR=\fIRATE\fR +Maximum download speed (e.g. +.BR 1MB/s ", " 500KB/s ). +.B 0 +disables the limit. +.TP +\fB\-\-max\-speed\fR=\fIRATE\fR +Alias for +.B \-\-rate\-limit +in the legacy "bytes per second" form. +.TP +\fB\-\-range\fR=\fIN\fB\-\fIN\fR +Download only a byte range (e.g. +.BR 0\-999 ). +.SS "HTTP and request shaping" +.TP +\fB\-\-header\fR=\fI"Key: Value"\fR +Custom HTTP header. May be repeated. +.TP +\fB\-\-insecure\fR +Skip TLS certificate verification. Use only for testing. +.TP +\fB\-\-cert\fR=\fIFILE\fR +Client certificate in PEM format (for mTLS). +.TP +\fB\-\-key\fR=\fIFILE\fR +Client private key in PEM format (for mTLS). +.TP +\fB\-\-ca\-certificate\fR=\fIFILE\fR +Path to a custom CA certificate bundle. +.TP +\fB\-\-ssl\-key\-log\fR=\fIFILE\fR +Log TLS session keys to +.I FILE +in the NSS Key Log format used by Wireshark +.RB ( SSLKEYLOGFILE ). +.TP +\fB\-\-data\fR=\fI"key=value"\fR +Send +.I key=value +as the request body. Implies HTTP POST. +.TP +\fB\-\-form\fR=\fI"field=value"\fR +Multipart form field; repeatable. A value of +.B field=@/path +attaches a file upload. +.TP +\fB\-\-post\-file\fR=\fIFILE\fR +POST the contents of +.I FILE +as the request body. +.TP +\fB\-\-referer\fR=\fIURL\fR +Set the +.B Referer +request header. +.TP +\fB\-\-compressed\fR +Send an +.B Accept\-Encoding +header so the server returns a compressed response. By default +.B goget +already handles compressed responses transparently; this flag is provided +for +.BR curl (1) +compatibility. +.TP +\fB\-\-spider\fR +Check whether the URL exists via a HEAD request; do not download the body. +.TP +\fB\-\-fail\fR +Exit with a non-zero status on HTTP 4xx or 5xx responses. +.TP +\fB\-\-no\-decompress\fR +Disable automatic decompression of compressed response bodies. +.SS "Recursive downloading and mirroring" +.TP +\fB\-\-recursive\fR +Follow links discovered in HTML pages. +.TP +\fB\-\-mirror\fR +Mirror an entire website. Shorthand for +.BR "\-\-recursive \-\-convert\-links \-\-page\-requisites" . +.TP +\fB\-\-max\-depth\fR=\fIN\fR +Maximum recursion depth. +.TP +\fB\-\-recursive\-parallel\fR=\fIN\fR +Number of concurrent file downloads in recursive mode across all protocols +(HTTP, WebDAV, FTP, SFTP). +.B 0 +runs sequentially (default). +.TP +\fB\-\-follow\-external\fR +Follow links to domains other than the starting domain. +.TP +\fB\-\-span\-hosts\fR +Alias for +.B \-\-follow\-external +in the +.BR wget (1) +style. +.TP +\fB\-\-domains\fR=\fILIST\fR +Restrict recursive crawling to the listed comma-separated domains. +.TP +\fB\-\-accept\fR=\fIPATTERN\fR +Accept patterns (filename glob or MIME type), comma-separated. Only +matching resources are downloaded. +.TP +\fB\-\-reject\fR=\fIPATTERN\fR +Reject patterns (glob), comma-separated. Matching resources are skipped. +.TP +\fB\-\-exclude\-pattern\fR=\fIPATTERN\fR +Alias for +.B \-\-reject +used by the recursive crawler. +.TP +\fB\-\-no\-parent\fR +Do not ascend to parent directories during recursive download. +.TP +\fB\-\-page\-requisites\fR +Download CSS, JavaScript, and images required to render the HTML pages. +.TP +\fB\-\-convert\-links\fR +Rewrite links in downloaded HTML for local offline viewing. +.TP +\fB\-\-no\-convert\-links\fR +Skip HTML link rewriting (faster; output is not offline-ready). +.TP +\fB\-\-backup\-converted\fR +Keep a +.B .orig +backup of each file before link conversion. +.TP +\fB\-\-no\-mirror\-assets\fR +Do not download CSS, JavaScript, and images during a mirror. +.TP +\fB\-\-no\-robots\fR +Do not honour +.I robots.txt +during mirroring. +.TP +\fB\-\-wait\fR=\fIDURATION\fR +Delay between requests in recursive or mirror mode (e.g. +.BR 1s ", " 500ms ). +.TP +\fB\-\-random\-wait\fR +Randomize the wait time between 0.5x and 1.5x of +.BR \-\-wait . +.TP +\fB\-\-adjust\-extension\fR +Append a +.B .html +extension to text/html files that lack one. +.TP +\fB\-\-cut\-dirs\fR=\fIN\fR +Ignore the first +.I N +path components when constructing local filenames. +.TP +\fB\-\-protocol\-directories\fR +Create protocol-prefixed subdirectories +.RB ( .http/ ", " .ftp/ ) +in recursive mode. +.TP +\fB\-\-dry\-run\fR +List the URLs that would be downloaded in recursive or mirror mode without +writing any files to disk. +.TP +\fB\-\-warc\-file\fR=\fIFILE\fR +Write a WARC (Web ARChive) record of the transfer to +.IR FILE . +.SS "Download management" +.TP +\fB\-\-batch\-file\fR=\fIFILE\fR +Read URLs from +.IR FILE , +one per line. Lines beginning with +.B # +are treated as comments and skipped. +.TP +\fB\-\-input\-file\fR=\fIFILE\fR +Alias for +.BR \-\-batch\-file . +A value of +.B \- +reads from standard input. +.TP +\fB\-\-glob\fR +Expand +.B [1-3] +and +.B {a,b,c} +patterns in the URL before issuing requests. +.TP +\fB\-\-queue\fR +Add the URL to the persistent download queue instead of downloading it +immediately. +.TP +\fB\-\-queue\-list\fR +List all items in the download queue and exit. +.TP +\fB\-\-queue\-process\fR +Process (download) every pending item in the queue and exit. +.TP +\fB\-\-queue\-clear\fR +Remove completed items from the queue. +.TP +\fB\-\-queue\-file\fR=\fIPATH\fR +Use a custom queue file (default +.IR ~/.config/goget/queue.json ). +.TP +\fB\-\-queue\-priority\fR=\fIN\fR +Priority for the queued item (1\(en10; higher values are processed first). +Default: 5. +.TP +\fB\-\-schedule\fR=\fITIME\fR +Schedule the download. Accepts an absolute timestamp +.RB ( " YYYY\-MM\-DD HH:MM ") +or a cron expression in the standard +.B MIN HOUR DOM MON DOW +format (e.g. +.BR "0 2 * * *" ). +.TP +\fB\-\-on\-complete\fR=\fICMD\fR +Run +.I CMD +after a successful download. The following environment variables are +exported into the hook: +.RS +.TP +.B GOGET_OUTPUT +Absolute path of the downloaded file. +.TP +.B GOGET_SIZE +Downloaded size in bytes. +.TP +.B GOGET_URL +The (credential-stripped) source URL. +.RE +.IP +The command is split into tokens with shell-like quoting rules but is +executed directly via +.BR exec (3) +\(em no shell is invoked, so metacharacters in arguments are not +re-interpreted. +.SS "Authentication" +.TP +\fB\-\-username\fR=\fIUSER\fR +Username for HTTP Basic or Digest authentication. When omitted, credentials +are auto-loaded from +.I ~/.netrc +if a matching machine entry is present. +.TP +\fB\-\-password\fR=\fIPASS\fR +Password for HTTP Basic or Digest authentication. Prefer +.B \-\-password\-file +to avoid exposing the secret on the command line. +.TP +\fB\-\-password\-file\fR=\fIFILE\fR +Read the password from +.I FILE +(the first line, with the trailing newline stripped). +.TP +\fB\-\-auth\-type\fR=\fITYPE\fR +Authentication scheme: +.BR basic ", " digest ", or " auto +(default). +.TP +\fB\-\-cookie\-jar\fR=\fIFILE\fR +Load cookies from and save cookies to +.I FILE +in Netscape format. +.TP +\fB\-\-cookie\fR=\fI"name=value"\fR +Set a cookie on the request. May be repeated. +.TP +\fB\-\-no\-private\fR +Disable +.I .netrc +and other credential-file auto-loading for this invocation. +.TP +\fB\-\-oauth\-client\-id\fR=\fIID\fR +OAuth 2.0 client ID. +.TP +\fB\-\-oauth\-client\-secret\fR=\fISECRET\fR +OAuth 2.0 client secret. +.TP +\fB\-\-oauth\-token\-url\fR=\fIURL\fR +OAuth 2.0 token endpoint. +.TP +\fB\-\-oauth\-auth\-url\fR=\fIURL\fR +OAuth 2.0 authorization endpoint (used by the +.B authorization_code +grant). +.TP +\fB\-\-oauth\-redirect\-uri\fR=\fIURI\fR +OAuth 2.0 redirect URI. +.TP +\fB\-\-oauth\-scopes\fR=\fISCOPES\fR +OAuth 2.0 scopes, comma-separated. +.TP +\fB\-\-oauth\-grant\-type\fR=\fITYPE\fR +OAuth 2.0 grant type: +.BR client_credentials ", " authorization_code ", " password ", or " +.BR refresh_token . +.TP +\fB\-\-oauth\-access\-token\fR=\fITOKEN\fR +OAuth 2.0 access token supplied directly. +.TP +\fB\-\-oauth\-refresh\-token\fR=\fITOKEN\fR +OAuth 2.0 refresh token used to obtain a new access token. +.SS "Upload" +.TP +\fB\-\-upload\fR +Upload the file in +.B \-\-upload\-file +instead of downloading. +.TP +\fB\-\-upload\-method\fR=\fIMETHOD\fR +HTTP method for the upload: +.B PUT +(default) or +.BR POST . +.TP +\fB\-\-upload\-file\fR=\fIFILE\fR +Path to the file to upload. +.TP +\fB\-\-form\-data\fR=\fI"k=v"\fR +Additional multipart form data, comma-separated. +.TP +\fB\-\-file\-field\fR=\fINAME\fR +Form field name used for the file part. Default: +.BR file . +.SS "Verification" +.TP +\fB\-\-checksum\fR=\fIHASH\fR +Expected checksum, as a hex string. +.TP +\fB\-\-checksum\-algo\fR=\fIALG\fR +Checksum algorithm: +.BR sha256 ", " sha512 ", " blake2b ", " sha3\-256 ", " sha3\-512 ", or " md5 . +Default: +.BR sha256 . +.TP +\fB\-\-checksum\-file\fR=\fIFILE\fR +Read expected checksums from +.I FILE +in the standard +.B SHA256SUMS +format. +.SS "PGP / OpenPGP" +.TP +\fB\-\-pgp\-verify\fR +Verify a PGP detached or inline signature after download. +.TP +\fB\-\-pgp\-decrypt\fR +Decrypt a PGP-encrypted file after download. +.TP +\fB\-\-pgp\-sig\fR=\fIFILE\fR +Path to a detached signature file (e.g. +.BR .asc ", " .sig ). +.TP +\fB\-\-pgp\-key\fR=\fIFILE\fR +Path to a PGP key (public or private, depending on the operation). +.TP +\fB\-\-pgp\-passphrase\fR=\fIPASS\fR +Passphrase for the private key. +.TP +\fB\-\-pgp\-passphrase\-file\fR=\fIFILE\fR +Read the private-key passphrase from +.IR FILE . +.SS "Metalink" +.TP +\fB\-\-metalink\fR +Treat the URL as a Metalink file (multi-source download). +.TP +\fB\-\-metalink\-file\fR=\fIPATH\fR +Read a Metalink description from +.IR PATH . +Accepts +.B .meta4 +and +.B .metalink +formats. +.SS "TLS and SSH" +.TP +\fB\-\-known\-hosts\fR=\fIFILE\fR +Use a custom +.I known_hosts +file for SFTP (default +.IR ~/.ssh/known_hosts ). +.TP +\fB\-\-ssh\-insecure\fR +Skip SSH host key verification for SFTP (accept any key). Mutually +incompatible with +.BR \-\-pinned\-host\-key . +.TP +\fB\-\-hsts\-file\fR=\fIFILE\fR +Use a custom HSTS cache file (default +.IR ~/.config/goget/hsts ). +.SS "Archive extraction" +.TP +\fB\-\-extract\fR +Automatically extract recognized archives (TAR, TAR.GZ, TAR.BZ2, ZIP) +after a successful download. +.TP +\fB\-\-extract\-dir\fR=\fIDIR\fR +Directory to extract into. Default: alongside the downloaded archive. +.TP +\fB\-\-strip\-components\fR=\fIN\fR +Strip the first +.I N +path components during archive extraction. +.SS "Configuration" +.TP +\fB\-\-config\fR=\fIFILE\fR +Path to the TOML configuration file. +.TP +\fB\-\-init\-config\fR +Write a default configuration file to +.I ~/.config/goget/config.toml +and exit. +.TP +\fB\-\-config\-get\fR=\fIKEY\fR +Print the value of a single configuration key (e.g. +.BR \-\-config\-get timeout ) +and exit. +.TP +\fB\-\-config\-set\fR=\fIKEY\fR \fIVALUE\fR +Persist +.I VALUE +under +.I KEY +in the configuration file and exit. +.TP +\fB\-\-config\-list\fR +Print every configuration key and value and exit. +.TP +\fB\-\-config\-unset\fR=\fIKEY\fR +Remove +.I KEY +from the configuration file and exit. +.SS "Information and shell integration" +.TP +\fB\-\-info\fR=\fIURL\fR +Show file metadata (size, content type, server, last-modified) without +downloading the body. +.TP +\fB\-\-benchmark\fR=\fIURL\fR +Benchmark the download speed by transferring the first 10 MB and reporting +the average throughput. +.TP +\fB\-\-show\-metadata\fR=\fIPATH\fR +Display the contents of a +.B .goget.meta +sidecar for debugging interrupted or resumed downloads. Accepts either a +path to the downloaded file (the sidecar is located next to it) or a direct +path to the sidecar. Renders a human-readable summary by default; combines +with +.B \-\-json +for raw output. Credentials embedded in the stored URL are masked. +.TP +\fB\-\-generate\-man\-page\fR +Print this manual page in troff format to standard output and exit. +.TP +\fB\-\-completion\fR=\fISHELL\fR +Print a shell completion script for +.IR SHELL \(lqbbash\(rqq, \(rqqzsh\(rqq, or \(rqqfish\(rqq\(en to standard output and exit. +.TP +\fB\-\-help\fR +Print the short help summary and exit. +.TP +\fB\-\-version\fR +Print the program name and version and exit. +.SH EXAMPLES +.SS "Simple download" +.PP +Download a single file to the current directory: +.PP +.RS +.nf +goget \-\-url https://example.com/file.zip +.fi +.RE +.SS "Parallel chunks and resume" +.PP +Files larger than 100 MB are automatically split into parallel chunks. To +force four connections explicitly and resume an interrupted transfer: +.PP +.RS +.nf +goget \-\-url https://example.com/large.iso \-\-parallel 4 +goget \-\-url https://example.com/file.zip \-\-resume +.fi +.RE +.SS "Verification" +.PP +Verify a download against a known checksum and PGP signature: +.PP +.RS +.nf +goget \-\-url https://example.com/release.tar.gz \\ + \-\-checksum\-algo sha256 \-\-checksum 5b9d...e2 \\ + \-\-pgp\-verify \-\-pgp\-sig release.tar.gz.asc \\ + \-\-pgp\-key maintainer.asc +.fi +.RE +.SS "Recursive mirror" +.PP +Mirror a documentation site, convert links for offline use, and respect +.IR robots.txt : +.PP +.RS +.nf +goget \-\-url https://example.com/docs/ \\ + \-\-mirror \-\-convert\-links \-\-output ./mirror +.fi +.RE +.SS "Scripting with JSON progress" +.PP +Stream machine-readable progress to a file for a downstream tool: +.PP +.RS +.nf +goget \-\-url https://example.com/data.csv \\ + \-\-json \-\-no\-progress > progress.jsonl +.fi +.RE +.SS "Scheduled download with a post-hook" +.PP +Fetch a daily report at 02:00 and feed it into a processor: +.PP +.RS +.nf +goget \-\-url https://example.com/daily.zip \\ + \-\-schedule "0 2 * * *" \\ + \-\-on\-complete "import.sh" +.fi +.RE +.SS "Batch file" +.PP +Read URLs from a file (one per line; lines starting with +.B # +are comments): +.PP +.RS +.nf +goget \-\-batch\-file urls.txt +.fi +.RE +.SH EXIT STATUS +.TP +.B 0 +Success. +.TP +.B 1 +Generic failure (network error, protocol error, invalid input). +.TP +.B 2 +Invalid command-line arguments or configuration. +.TP +.B 3 +File I/O error (cannot write output, cannot read source). +.TP +.B 4 +Authentication failure. +.TP +.B 5 +TLS or certificate error (including pinning mismatch). +.TP +.B 6 +Checksum or PGP verification failed. +.TP +.B 7 +Operation cancelled (SIGINT, SIGTERM, context cancellation). +.TP +.B 8 +Resource limit exceeded +.RB ( \-\-max\-filesize ", " \-\-max\-time ", etc.). +.PP +Only the generic +.B 0 +and +.B 1 +codes are guaranteed; the project reserves the other values and their +meaning is part of the stable interface. +.SH FILES +.TP +.I ~/.config/goget/config.toml +Default configuration file. Overridden by +.B \-\-config +or the +.B GOGET_CONFIG +environment variable. +.TP +.I ~/.config/goget/queue.json +Persistent download queue. Path can be overridden with +.BR \-\-queue\-file . +.TP +.I ~/.config/goget/hsts +HSTS cache (RFC 6797). Path can be overridden with +.BR \-\-hsts\-file . +.TP +.I ~/.ssh/known_hosts +Default SFTP host-key database. Path can be overridden with +.BR \-\-known\-hosts . +.TP +.I .goget.meta +Resume metadata sidecar written next to a partial output file. Deleted on +successful completion; consulted by +.B \-\-resume +or +.BR \-\-continue . +.TP +.I ~/.netrc +Auto-loaded credentials. Disabled for the current invocation by +.BR \-\-no\-private . +.SH ENVIRONMENT +.TP +.B GOGET_CONFIG +Path to the configuration file. Takes precedence over +.IR ~/.config/goget/config.toml +and +.BR \-\-config . +.TP +.B NO_COLOR +When set to a non-empty value, colour output is disabled regardless of the +.BR color " setting in the config file (see " https://no\-color.org )." +.TP +.B SSLKEYLOGFILE +Equivalent to passing +.B \-\-ssl\-key\-log +with the same value. Used by Wireshark to decrypt TLS traffic. +.TP +.B GOGET_OUTPUT +Exported into the environment of +.B \-\-on\-complete +hooks; contains the absolute path of the downloaded file. +.TP +.B GOGET_SIZE +Exported into +.B \-\-on\-complete +hooks; contains the downloaded size in bytes. +.TP +.B GOGET_URL +Exported into +.B \-\-on\-complete +hooks; contains the source URL with credentials stripped +.RB ( core.SafeURL ). +.TP +.B HOME +Used to locate +.I ~/.config/goget/ +and +.IR ~/.ssh/known_hosts . +.TP +.B HTTP_PROXY +.IR "https_proxy" ", " ALL_PROXY ", " NO_PROXY +Standard proxy environment variables; respected unless +.B \-\-proxy +is given explicitly. +.SH "SEE ALSO" +.BR curl (1), +.BR wget (1), +.BR sftp (1), +.BR ssh\-keygen (1), +.BR groff (1), +.BR man (1) +.PP +Project home page and issue tracker: +.RI < https://codeberg.org/petrbalvin/goget > +.PP +Full documentation set: +.RS +.TP +.I docs/configuration.md +Configuration schema. +.TP +.I docs/cli-reference.md +Full flag reference. +.TP +.I docs/protocols.md +Protocol handler details. +.TP +.I docs/recursive-mirror.md +Recursive and mirror behaviour. +.TP +.I docs/security.md +Threat model and security controls. +.TP +.I docs/api.md +Public Go library API. +.RE +.SH HISTORY +goget 1.0.0 (2026-06-26) is the first public release. +The project was created as a modern, IPv6-first, Go-stdlib-only replacement +for +.BR curl (1) +and +.BR wget (1). +.SH AUTHORS +Petr Balvín +.RI < opensource@petrbalvin.org > +\(em upstream maintainer. +.PP +See +.RI < https://codeberg.org/petrbalvin/goget > +for the full contributor list. +.SH "REPORTING BUGS" +Report bugs to the issue tracker at +.RI < https://codeberg.org/petrbalvin/goget/issues >. +.PP +For security issues, do not open a public issue; email +.RI < opensource@petrbalvin.org > +instead. +.SH COPYRIGHT +Copyright \(co 2026 Petr Balvín. +.PP +Licensed under the MIT License. +Permission is granted to use, copy, modify, and distribute this software +without restriction, provided that the above copyright notice and this +permission notice appear in all copies. +.PP +.B goget +is provided +.B "AS IS" +without any warranty of any kind. See the MIT License text distributed with +the source for the full disclaimer.