From 434d8443b35005ad0a7057bcb63eeb7e91e99646 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 6 Sep 2026 22:48:07 +0700 Subject: [PATCH] chore: drop the retired flags and give each command a real help header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shell pipeline's options were still accepted, so `sync -h` listed five entries reading only "retired". The scripts themselves are gone, so the migration hints have outlived what they pointed away from. Each command now states what it does and which options are required, instead of the flag package's bare "Usage of sync:". The top-level help lists the exit codes, including the new 4, and says which one is worth retrying — that is the part a driver script needs and the only place it was written down outside the README. Also fixes a real defect in the flag help: a backquoted word in a usage string is the flag package's placeholder syntax, so "as `tdl dl --takeout` did" rendered the option as "-takeout tdl dl --takeout". Backquotes are now used deliberately, giving -c CHAT, -r REMOTE:PATH, -m SIZE and friends. --- README.md | 10 --------- cmd/tgexport/doctor.go | 9 +++++--- cmd/tgexport/list.go | 9 +++++--- cmd/tgexport/main.go | 21 ++++++++++++++++++- cmd/tgexport/sync.go | 43 ++++++++++----------------------------- cmd/tgexport/sync_test.go | 25 ----------------------- cmd/tgexport/verify.go | 11 ++++++---- 7 files changed, 50 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index b85c8f6..f7d95fc 100644 --- a/README.md +++ b/README.md @@ -204,16 +204,6 @@ semaphore. Some hard-won details were worth keeping, and are: `RCLONE_MIN_SIZE` left over from another job would otherwise narrow the index and re-download everything it hid. -Flags that disappeared are recognised and explain what replaced them: - -| Old | Why it is gone | -|---|---| -| `-i` | no sweep interval; uploads start when a download finishes | -| `-a` | no `--min-age`; completion is observed, not inferred | -| `-f` | no export JSON; the chat is read live, so names cannot go stale | -| `-p` | no passes; one invocation converges | -| `-q` | renamed `--min-free` | - ## Notes - `tgexport` and the `tdl` CLI share one session store and cannot run against the diff --git a/cmd/tgexport/doctor.go b/cmd/tgexport/doctor.go index f39ceb4..9a4ebcb 100644 --- a/cmd/tgexport/doctor.go +++ b/cmd/tgexport/doctor.go @@ -27,10 +27,13 @@ import ( func doctorCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("doctor", flag.ContinueOnError) var ( - remoteArg = fs.String("r", "", "rclone destination to check, e.g. pikpak:archive") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + remoteArg = fs.String("r", "", "rclone destination `REMOTE:PATH` to check, e.g. pikpak:archive") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) + + commandUsage(fs, "tgexport doctor [-r REMOTE:PATH] [options]", + "Check the Telegram session and, when a remote is given, that it\nresolves and has free space. Run this before a long archive.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err // main maps this to a clean exit diff --git a/cmd/tgexport/list.go b/cmd/tgexport/list.go index 5cdccb0..e8cb7e7 100644 --- a/cmd/tgexport/list.go +++ b/cmd/tgexport/list.go @@ -28,10 +28,13 @@ import ( func listCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("list", flag.ContinueOnError) var ( - chat = fs.String("c", "", "chat id, username, or t.me link (required)") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + chat = fs.String("c", "", "`CHAT`: id, username, or t.me link (required)") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) + + commandUsage(fs, "tgexport list -c CHAT [options]", + "Print every media message in a chat as idsizename.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err diff --git a/cmd/tgexport/main.go b/cmd/tgexport/main.go index 582e3b6..2107d18 100644 --- a/cmd/tgexport/main.go +++ b/cmd/tgexport/main.go @@ -162,10 +162,29 @@ func usage() { Commands: sync Archive a chat to a remote, fetching only what is missing - list Print every media message in a chat as idsizename verify Report whether a chat is fully archived on a remote + list Print every media message in a chat as idsizename doctor Check the Telegram session, the destination remote, and free space +Exit codes: + 0 complete 2 usage error 4 stalled: nothing left is fetchable + 1 files remain 3 remote or Telegram failure + 130/143 interrupted + +Only 1 is worth retrying; a driver looping until 0 should stop on anything else. + Run 'tgexport -h' for command options. `) } + +// commandUsage gives a subcommand a header its flag list can hang off. +// +// The flag package's default is "Usage of sync:" and a bare list, which says +// neither what the command does nor which options are required. +func commandUsage(fs *flag.FlagSet, line, summary string) { + fs.Usage = func() { + out := fs.Output() + fmt.Fprintf(out, "Usage: %s\n\n%s\n\nOptions:\n", line, summary) + fs.PrintDefaults() + } +} diff --git a/cmd/tgexport/sync.go b/cmd/tgexport/sync.go index 5360b10..90bdd94 100644 --- a/cmd/tgexport/sync.go +++ b/cmd/tgexport/sync.go @@ -21,41 +21,28 @@ import ( "github.com/tiennm99dev/telegram-exporter/internal/verify" ) -// retiredFlags map options the shell pipeline had onto what replaced them. -// -// Recognising them beats "flag provided but not defined": these were in -// muscle memory and in wrapper scripts, and a bare parse error does not say -// whether the concept moved or disappeared. -var retiredFlags = map[string]string{ - "i": "the rclone sweep interval is gone; uploads start the moment a download finishes", - "a": "--min-age is gone; a file is only uploaded once the downloader reports it complete", - "f": "the export JSON is gone; the chat is read live, so names cannot go stale", - "p": "there are no passes; one invocation converges, and re-running resumes", - "q": "renamed to --min-free", -} - // syncCmd archives a chat to a remote: read the chat, skip what is already // there, download and upload the rest, then report on the result. func syncCmd(ctx context.Context, args []string) error { flags := flag.NewFlagSet("sync", flag.ContinueOnError) var ( - chat = flags.String("c", "", "chat id, username, or t.me link (required)") - remoteArg = flags.String("r", "", "rclone destination, e.g. pikpak:archive (required)") - staging = flags.String("d", "./staging", "staging directory for files in flight") - maxStaging = flags.String("m", "", "cap staging at this size, e.g. 40G (default: no cap)") + chat = flags.String("c", "", "`CHAT`: id, username, or t.me link (required)") + remoteArg = flags.String("r", "", "rclone destination `REMOTE:PATH`, e.g. pikpak:archive (required)") + staging = flags.String("d", "./staging", "staging `DIR` for files in flight") + maxStaging = flags.String("m", "", "cap staging at `SIZE`, e.g. 40G (default: no cap)") threads = flags.Int("threads", 4, "connections per file") limit = flags.Int("limit", 2, "files downloading at once") uploads = flags.Int("uploads", 2, "files uploading at once") - minFree = flags.Int64("min-free", 5, "stop if the remote has fewer than this many GiB free") + minFree = flags.Int64("min-free", 5, "stop when the remote has under this many `GiB` free") limitItems = flags.Int("limit-items", 0, "stop after this many files (0 means no limit)") confirm = flags.Bool("confirm", true, "re-state each uploaded file to prove its size") - takeout = flags.Bool("takeout", true, "use a takeout session, as `tdl dl --takeout` did") - ns = flags.String("n", "default", "tdl session namespace") - dataDir = flags.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + takeout = flags.Bool("takeout", true, "use a takeout session for higher rate limits") + ns = flags.String("n", "default", "tdl session `NAMESPACE`") + dataDir = flags.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") ) - for name, replacement := range retiredFlags { - flags.Var(retiredFlag{name, replacement}, name, "retired") - } + commandUsage(flags, "tgexport sync -c CHAT -r REMOTE:PATH [options]", + "Archive a chat's media to a remote, fetching only what is missing.\n"+ + "Re-running resumes: anything already on the remote is skipped.") if err := flags.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { @@ -353,14 +340,6 @@ func checkFree(ctx context.Context, dst fs.Fs, minGiB int64) error { return nil } -// retiredFlag reports a helpful error for an option that no longer exists. -type retiredFlag struct{ name, replacement string } - -func (r retiredFlag) String() string { return "" } -func (r retiredFlag) Set(string) error { - return fmt.Errorf("-%s no longer exists: %s", r.name, r.replacement) -} - // parseSize reads a binary size such as 40G, matching what run.sh -m accepted. func parseSize(s string) (int64, error) { if s == "" { diff --git a/cmd/tgexport/sync_test.go b/cmd/tgexport/sync_test.go index 9cd6d10..d9dd56c 100644 --- a/cmd/tgexport/sync_test.go +++ b/cmd/tgexport/sync_test.go @@ -112,28 +112,3 @@ func TestValidateBudgetRejectsCapBelowLargestFile(t *testing.T) { t.Errorf("an unset cap should accept anything, got: %v", err) } } - -// Options the shell pipeline had must produce an explanation, not "flag -// provided but not defined" — they are in wrapper scripts and muscle memory. -func TestRetiredFlagsExplainWhatReplacedThem(t *testing.T) { - for name, replacement := range retiredFlags { - err := retiredFlag{name, replacement}.Set("x") - if err == nil { - t.Errorf("-%s was accepted, want an explanation", name) - continue - } - if !strings.Contains(err.Error(), "-"+name) { - t.Errorf("error for -%s should name the flag, got: %v", name, err) - } - if !strings.Contains(err.Error(), replacement) { - t.Errorf("error for -%s should say what replaced it, got: %v", name, err) - } - } - - // The ones that mattered most in run.sh. - for _, name := range []string{"i", "a", "f", "p", "q"} { - if _, ok := retiredFlags[name]; !ok { - t.Errorf("-%s was a run.sh flag but is not recognised as retired", name) - } - } -} diff --git a/cmd/tgexport/verify.go b/cmd/tgexport/verify.go index 8239e47..b6b396d 100644 --- a/cmd/tgexport/verify.go +++ b/cmd/tgexport/verify.go @@ -29,13 +29,16 @@ import ( func verifyCmd(ctx context.Context, args []string) error { fs := flag.NewFlagSet("verify", flag.ContinueOnError) var ( - chat = fs.String("c", "", "chat id, username, or t.me link (required)") - remoteArg = fs.String("r", "", "rclone destination, e.g. pikpak:archive (required)") - ns = fs.String("n", "default", "tdl session namespace") - dataDir = fs.String("storage", tdlkv.DefaultDir(), "tdl bolt storage directory") + chat = fs.String("c", "", "`CHAT`: id, username, or t.me link (required)") + remoteArg = fs.String("r", "", "rclone destination `REMOTE:PATH`, e.g. pikpak:archive (required)") + ns = fs.String("n", "default", "tdl session `NAMESPACE`") + dataDir = fs.String("storage", tdlkv.DefaultDir(), "`DIR` holding the tdl session store") delStale = fs.Bool("delete-misnamed", false, "delete remote files stored under a superseded name") assumeYes = fs.Bool("y", false, "do not prompt before deleting") ) + + commandUsage(fs, "tgexport verify -c CHAT -r REMOTE:PATH [options]", + "Compare a chat against a remote and report what is missing, empty,\nthe wrong size, or stored under a name that cannot be written.") if err := fs.Parse(args); err != nil { if errors.Is(err, flag.ErrHelp) { return err