diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 21:01:05 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-23 21:01:05 +0200 |
| commit | ffa4676a34851d667acbf8a460321ff93310b7ba (patch) | |
| tree | 69b686dcb235cd3473d9ad28f2c69748358a81f7 /internal/cli | |
| parent | f4e15c0c6e0f2e7a0a0d0654a63824834c9e4166 (diff) | |
| download | lectio-ffa4676a34851d667acbf8a460321ff93310b7ba.tar.gz lectio-ffa4676a34851d667acbf8a460321ff93310b7ba.zip | |
cli: flag-driven interface (short+long); -u maximal blip-proof harvest
Replace the today/date/show/compare/update subcommands with a single
flag-driven interface like the Python predecessor: lectio [DATE]
-a/-b/-c/-r/-w/-R/-o/-l/-g/-u, short and long names bound to the same
variable, DATE (YYYY-MM-DD) extracted from any position in the args.
Fix Harvest's "-u" to walk to the true maximal horizon without a
transient network error being mistaken for it: a fetch failure now
retries a few times with backoff and, if it still fails, is reported
as an error (after saving partial progress via writeSigla), while a
Parse failure (the real unpublished-horizon placeholder) still stops
cleanly with a nil error.
Diffstat (limited to 'internal/cli')
| -rw-r--r-- | internal/cli/cli.go | 374 | ||||
| -rw-r--r-- | internal/cli/cli_test.go | 114 |
2 files changed, 266 insertions, 222 deletions
diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 66f45a7..e7a2439 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -1,6 +1,6 @@ -// Package cli is lectio's subcommand dispatcher: it wires config, the -// readings router, liturgy.Harvest and render together into the `lectio` -// binary's Run entry point. +// Package cli is lectio's flag-driven command dispatcher: it wires config, +// the readings router, liturgy.Harvest and render together into the +// `lectio` binary's Run entry point. package cli import ( @@ -25,36 +25,35 @@ const versionString = "0.1.0" const helpText = `lectio — daily Catholic liturgy readings (Polish + 4 versions) Usage: - lectio [today] [--all] [--raw] [--width N] [--refresh] - lectio date D [--all] [--raw] [--width N] [--refresh] D = YYYY-MM-DD - lectio show VERSION [--date D] [--all] [--raw] [--width N] [--refresh] - one version's text - lectio compare LIST [--date D] [--all] [--width N] [--refresh] [--raw] - LIST = comma-separated versions (default: config "versions") - lectio update [--days N] [--from D] harvest future sigla to the TSV - lectio --version | lectio -v - lectio help | lectio -h | lectio <cmd> -h + lectio [DATE] [flags] DATE = YYYY-MM-DD (default: today), any position -Global flags (any subcommand, any position): - --offline skip the network, use cached/harvested data only - --lectionary new|traditional override config "lectionary" for this run - --lang pl|en override config "traditional_lang" for this run - -Per-command flags: - --all show every reading part (1st/2nd, psalm, acclamation, - gospel); default is the gospel only, unless config - all = true - --raw omit the banner/headings (for piping) - --width N wrap width; 0 = detect terminal, else a sane default - (80 normally, 120 for compare) - --refresh bypass the cache and re-fetch - --date D date as YYYY-MM-DD (show/compare; default: today) +Flags: + -a, --all all readings, not just the gospel + -b, --bible VER one version's text: pl,wuj,vul,grb,drb + -c, --compare LIST versions side by side (comma-separated) + -r, --raw text only, no banner/headings (for piping) + -w, --width N wrap width; 0 = detect terminal + -R, --refresh ignore cache, re-download + -o, --offline cache/sigla only, no network + -l, --lectionary WHICH new|trad (trad -> traditional) + -g, --lang LANG traditional lectionary language: pl|en + -u, --update harvest sigla maximally to the horizon (idempotent) + -v, --version print the version and exit + -h, --help this help Versions: pl (Polski/niedziela.pl) wuj (Wujek) vul (Wulgata) grb (Grecki) drb (Douay-Rheims) +Examples: + lectio today's gospel + lectio 2026-07-22 -a all readings for a date + lectio -c pl,wuj 2026-07-22 compare two versions for a date + lectio -b vul -a Vulgate text, all readings + lectio -l trad -a traditional lectionary, all readings + lectio -u harvest sigla to the horizon + Flags override config. Exit codes: 0 ok, 1 runtime error (fetch/parse), -2 usage error (unknown command/version, bad date, missing argument). +2 usage error (bad flag, bad date, bad version, bad --lectionary/--lang). ` const ( @@ -91,9 +90,52 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 0 } - offline, lectionary, lang, rest := extractGlobal(args) - if lectionary != "" && lectionary != "new" && lectionary != "traditional" { - fmt.Fprintf(stderr, "lectio: invalid --lectionary %q (want new|traditional)\n", lectionary) + date, rest, err := extractDate(args) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 2 + } + + var all, raw, refresh, offline, update bool + var bibleVer, compareList, lectionary, lang string + var width int + + fs := flag.NewFlagSet("lectio", flag.ContinueOnError) + fs.SetOutput(stderr) + fs.Usage = func() { fmt.Fprint(stderr, helpText) } + + fs.BoolVar(&all, "a", false, "all readings, not just the gospel") + fs.BoolVar(&all, "all", false, "all readings, not just the gospel") + fs.StringVar(&bibleVer, "b", "", "one version's text: pl,wuj,vul,grb,drb") + fs.StringVar(&bibleVer, "bible", "", "one version's text: pl,wuj,vul,grb,drb") + fs.StringVar(&compareList, "c", "", "versions side by side (comma list)") + fs.StringVar(&compareList, "compare", "", "versions side by side (comma list)") + fs.BoolVar(&raw, "r", false, "text only, no banner/headings") + fs.BoolVar(&raw, "raw", false, "text only, no banner/headings") + fs.IntVar(&width, "w", 0, "wrap width (0 = detect terminal)") + fs.IntVar(&width, "width", 0, "wrap width (0 = detect terminal)") + fs.BoolVar(&refresh, "R", false, "ignore cache, re-download") + fs.BoolVar(&refresh, "refresh", false, "ignore cache, re-download") + fs.BoolVar(&offline, "o", false, "cache/sigla only, no network") + fs.BoolVar(&offline, "offline", false, "cache/sigla only, no network") + fs.StringVar(&lectionary, "l", "", "new|trad") + fs.StringVar(&lectionary, "lectionary", "", "new|trad") + fs.StringVar(&lang, "g", "", "pl|en") + fs.StringVar(&lang, "lang", "", "pl|en") + fs.BoolVar(&update, "u", false, "harvest sigla maximally to the horizon") + fs.BoolVar(&update, "update", false, "harvest sigla maximally to the horizon") + + if err := fs.Parse(rest); err != nil { + return 2 + } + if fs.NArg() > 0 { + fmt.Fprintf(stderr, "lectio: unexpected argument %q; see 'lectio -h'\n", fs.Arg(0)) + return 2 + } + + lectionary, err = normalizeLectionary(lectionary) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) return 2 } if lang != "" && lang != "pl" && lang != "en" { @@ -101,46 +143,42 @@ func Run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return 2 } - cmd := "today" - cmdArgs := rest - if len(rest) > 0 && !strings.HasPrefix(rest[0], "-") { - cmd = rest[0] - cmdArgs = rest[1:] + if update { + return runHarvest(date, stdout, stderr) } - switch cmd { - case "today", "date", "show", "compare": - cfg, err := config.Load() - if err != nil { - fmt.Fprintln(stderr, "lectio:", err) - return 1 - } - if offline { - cfg.Offline = true - } - if lectionary != "" { - cfg.Lectionary = lectionary - } - if lang != "" { - cfg.TraditionalLang = lang - } - switch cmd { - case "today": - return runToday(cmdArgs, cfg, stdout, stderr) - case "date": - return runDate(cmdArgs, cfg, stdout, stderr) - case "show": - return runShow(cmdArgs, cfg, stdout, stderr) - case "compare": - return runCompare(cmdArgs, cfg, stdout, stderr) + cfg, err := config.Load() + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 1 + } + if offline { + cfg.Offline = true + } + if lectionary != "" { + cfg.Lectionary = lectionary + } + if lang != "" { + cfg.TraditionalLang = lang + } + + effAll := all || cfg.All + effWidth := width + if effWidth == 0 { + effWidth = cfg.Width + } + + if compareList != "" { + return renderCompare(cfg, compareList, date, effAll, raw, effWidth, refresh, stdout, stderr) + } + if bibleVer != "" { + if !validVersions[bibleVer] { + fmt.Fprintf(stderr, "lectio: unknown version %q (want one of pl, wuj, vul, grb, drb)\n", bibleVer) + return 2 } - return 1 // unreachable - case "update": - return runUpdate(cmdArgs, stdout, stderr) - default: - fmt.Fprintf(stderr, "lectio: unknown command %q; see 'lectio help'\n", cmd) - return 2 + return fetchAndPrint(cfg, bibleVer, date, effAll, raw, effWidth, refresh, stdout, stderr) } + return fetchAndPrint(cfg, cfg.DefaultVersion, date, effAll, raw, effWidth, refresh, stdout, stderr) } // wantsHelp reports whether -h/--help appears anywhere in args. @@ -153,107 +191,64 @@ func wantsHelp(args []string) bool { return false } -// extractGlobal pulls the global --offline/--lectionary/--lang flags out of -// args (they may appear anywhere, before or after the subcommand), leaving -// rest for subcommand dispatch and per-command flag parsing. -func extractGlobal(args []string) (offline bool, lectionary, lang string, rest []string) { - for i := 0; i < len(args); i++ { - a := args[i] - switch { - case a == "--offline": - offline = true - case a == "--lectionary" && i+1 < len(args): - lectionary = args[i+1] - i++ - case strings.HasPrefix(a, "--lectionary="): - lectionary = strings.TrimPrefix(a, "--lectionary=") - case a == "--lang" && i+1 < len(args): - lang = args[i+1] - i++ - case strings.HasPrefix(a, "--lang="): - lang = strings.TrimPrefix(a, "--lang=") - default: - rest = append(rest, a) +// extractDate pulls the single positional DATE token (YYYY-MM-DD, matching +// dateRe) out of args, wherever it appears, and returns it along with the +// remaining tokens for flag.FlagSet to parse. No flag's own value can match +// dateRe's shape (width is an int, versions/lists/lang/lectionary are short +// codes), so scanning raw tokens this way is unambiguous. Defaults to +// today() when no date token is present; errors if more than one is found. +func extractDate(args []string) (date string, rest []string, err error) { + found := false + for _, a := range args { + if dateRe.MatchString(a) { + if found { + return "", nil, fmt.Errorf("multiple dates given (%q and %q)", date, a) + } + date = a + found = true + continue } + rest = append(rest, a) } - return offline, lectionary, lang, rest -} - -// runToday handles the (default) "today" subcommand: --all --raw --width -// --refresh, date = today. -func runToday(args []string, cfg config.Config, stdout, stderr io.Writer) int { - fs := flag.NewFlagSet("today", flag.ContinueOnError) - fs.SetOutput(stderr) - all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel") - raw := fs.Bool("raw", false, "omit banner/headings (for piping)") - width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)") - refresh := fs.Bool("refresh", false, "bypass cache, re-fetch") - if err := fs.Parse(args); err != nil { - return 2 - } - if fs.NArg() > 0 { - fmt.Fprintf(stderr, "lectio: today takes no date argument (use: lectio date %s)\n", fs.Arg(0)) - return 2 + if !found { + date = today() } - return fetchAndPrint(cfg, cfg.DefaultVersion, today(), *all, *raw, *width, *refresh, stdout, stderr) + return date, rest, nil } -// runDate handles "date D": D is the required first positional argument, -// flags as in runToday. -func runDate(args []string, cfg config.Config, stdout, stderr io.Writer) int { - if len(args) == 0 { - fmt.Fprintln(stderr, "lectio: date requires an argument (YYYY-MM-DD)") - return 2 - } - date := args[0] - if !dateRe.MatchString(date) { - fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", date) - return 2 - } - fs := flag.NewFlagSet("date", flag.ContinueOnError) - fs.SetOutput(stderr) - all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel") - raw := fs.Bool("raw", false, "omit banner/headings (for piping)") - width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)") - refresh := fs.Bool("refresh", false, "bypass cache, re-fetch") - if err := fs.Parse(args[1:]); err != nil { - return 2 +// normalizeLectionary maps -l/--lectionary's accepted spellings ("new", +// "trad", "traditional") onto the canonical config.Config.Lectionary values +// ("new", "traditional"); "" (flag not given) passes through unchanged so +// the caller knows to leave config's own setting alone. +func normalizeLectionary(lectionary string) (string, error) { + switch lectionary { + case "": + return "", nil + case "trad": + return "traditional", nil + case "new", "traditional": + return lectionary, nil + default: + return "", fmt.Errorf("invalid --lectionary %q (want new|trad)", lectionary) } - return fetchAndPrint(cfg, cfg.DefaultVersion, date, *all, *raw, *width, *refresh, stdout, stderr) } -// runShow handles "show VERSION": same as today/date but the version is -// fixed by the caller instead of cfg.DefaultVersion. -func runShow(args []string, cfg config.Config, stdout, stderr io.Writer) int { - if len(args) == 0 { - fmt.Fprintln(stderr, "lectio: show requires a version argument (pl|wuj|vul|grb|drb)") - return 2 - } - ver := args[0] - if !validVersions[ver] { - fmt.Fprintf(stderr, "lectio: unknown version %q (want one of pl, wuj, vul, grb, drb)\n", ver) - return 2 - } - fs := flag.NewFlagSet("show", flag.ContinueOnError) - fs.SetOutput(stderr) - all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel") - raw := fs.Bool("raw", false, "omit banner/headings (for piping)") - width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal)") - refresh := fs.Bool("refresh", false, "bypass cache, re-fetch") - date := fs.String("date", today(), "date as YYYY-MM-DD") - if err := fs.Parse(args[1:]); err != nil { - return 2 - } - if !dateRe.MatchString(*date) { - fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *date) - return 2 +// runHarvest handles -u/--update: harvest sigla maximally (to the +// unpublished horizon) from date, printing the outcome or -- on a genuine +// interruption (see liturgy.Harvest) -- the error. +func runHarvest(date string, stdout, stderr io.Writer) int { + added, furthest, err := liturgy.Harvest(date, 0) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 1 } - return fetchAndPrint(cfg, ver, *date, *all, *raw, *width, *refresh, stdout, stderr) + fmt.Fprintf(stdout, "harvested %d day(s), furthest %s\n", added, furthest) + return 0 } -// fetchAndPrint is the shared today/date/show path: fetch via the readings -// router, apply the offline version swap, then render each section's -// heading and render.GatherVersion blocks. +// fetchAndPrint is the shared single-version render path (default version or +// -b/--bible): fetch via the readings router, apply the offline version +// swap, then render each section's heading and render.GatherVersion blocks. func fetchAndPrint(cfg config.Config, version, date string, all, raw bool, width int, refresh bool, stdout, stderr io.Writer) int { secs, err := readings.Load(cfg, readings.Options{ Date: date, @@ -320,31 +315,10 @@ func renderSection(sec liturgy.Section, version, lectionary string, width int, r return strings.TrimRight(strings.Join(lines, "\n"), "\n") } -// runCompare handles "compare LIST": LIST is the required first positional -// argument (falling back to cfg.Versions when omitted entirely). -func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int { - var list string - rest := args - if len(args) > 0 && !strings.HasPrefix(args[0], "-") { - list = args[0] - rest = args[1:] - } - - fs := flag.NewFlagSet("compare", flag.ContinueOnError) - fs.SetOutput(stderr) - all := fs.Bool("all", cfg.All, "show every reading part, not just the gospel") - raw := fs.Bool("raw", false, "omit banner/headings (for piping)") - width := fs.Int("width", cfg.Width, "wrap width (0 = detect terminal; compare default 120)") - refresh := fs.Bool("refresh", false, "bypass cache, re-fetch") - date := fs.String("date", today(), "date as YYYY-MM-DD") - if err := fs.Parse(rest); err != nil { - return 2 - } - if !dateRe.MatchString(*date) { - fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *date) - return 2 - } - +// renderCompare handles -c/--compare LIST: LIST is a comma-separated list of +// version codes (falling back to cfg.Versions when empty), rendered side by +// side via render.Compare. +func renderCompare(cfg config.Config, list, date string, all, raw bool, width int, refresh bool, stdout, stderr io.Writer) int { var versions []string if list == "" { versions = append(versions, cfg.Versions...) @@ -368,24 +342,24 @@ func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int } secs, err := readings.Load(cfg, readings.Options{ - Date: *date, - Refresh: *refresh, + Date: date, + Refresh: refresh, Offline: cfg.Offline, - All: *all, + All: all, }) if err != nil { fmt.Fprintln(stderr, "lectio:", err) return 1 } if len(secs) == 0 { - fmt.Fprintln(stderr, "lectio: no readings found for", *date) + fmt.Fprintln(stderr, "lectio: no readings found for", date) return 1 } - w := resolveWidth(*width, true, stdout) + w := resolveWidth(width, true, stdout) - if !*raw { - banner := bannerFor(*all, *date) + if !raw { + banner := bannerFor(all, date) fmt.Fprintln(stdout, banner) fmt.Fprintln(stdout, strings.Repeat("=", minInt(len(banner), w))) fmt.Fprintln(stdout) @@ -394,30 +368,6 @@ func runCompare(args []string, cfg config.Config, stdout, stderr io.Writer) int return 0 } -// runUpdate handles "update [--days N] [--from D]": it does not touch -// config -- liturgy.Harvest owns its own cache/sigla-store paths. -func runUpdate(args []string, stdout, stderr io.Writer) int { - fs := flag.NewFlagSet("update", flag.ContinueOnError) - fs.SetOutput(stderr) - days := fs.Int("days", 0, "max days to harvest (0 = until the unpublished horizon)") - from := fs.String("from", today(), "start date as YYYY-MM-DD (default: today)") - if err := fs.Parse(args); err != nil { - return 2 - } - if !dateRe.MatchString(*from) { - fmt.Fprintf(stderr, "lectio: invalid date %q (want YYYY-MM-DD)\n", *from) - return 2 - } - - added, furthest, err := liturgy.Harvest(*from, *days) - if err != nil { - fmt.Fprintln(stderr, "lectio:", err) - return 1 - } - fmt.Fprintf(stdout, "harvested %d day(s), furthest %s\n", added, furthest) - return 0 -} - // resolveWidth applies the --width 0 = detect-terminal rule: an explicit // positive width wins, then a real terminal width if stdout is a tty, then // a sane per-mode default. diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index 85097ed..de9220a 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -14,10 +14,18 @@ func TestHelp(t *testing.T) { } } -func TestUnknownCommand(t *testing.T) { +func TestHelpFlag(t *testing.T) { + var out, errb bytes.Buffer + code := Run([]string{"-h"}, nil, &out, &errb) + if code != 0 || !strings.Contains(out.String(), "lectio") { + t.Errorf("-h code=%d out=%q", code, out.String()) + } +} + +func TestUnexpectedPositional(t *testing.T) { var out, errb bytes.Buffer if code := Run([]string{"bogus"}, nil, &out, &errb); code != 2 { - t.Errorf("unknown cmd code=%d want 2", code) + t.Errorf("unexpected positional code=%d want 2 (stderr=%q)", code, errb.String()) } } @@ -28,24 +36,110 @@ func TestVersion(t *testing.T) { } } -// TestBadDate exercises the "date" subcommand's usage-error path (bad -// YYYY-MM-DD format) without touching the network: format validation -// happens before any fetch is attempted. +// TestBadDate exercises the usage-error path for a malformed date: a token +// that doesn't match dateRe's YYYY-MM-DD shape is left as an unrecognised +// positional argument once flag parsing is done, which is a usage error. func TestBadDate(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer - if code := Run([]string{"date", "13-13-13"}, nil, &out, &errb); code != 2 { + if code := Run([]string{"13-13-13"}, nil, &out, &errb); code != 2 { t.Errorf("bad date code=%d want 2 (stderr=%q)", code, errb.String()) } } -// TestUnknownVersion exercises "show"'s version-validation usage-error path -// without touching the network: the version code is checked against the -// five known codes before any fetch is attempted. +// TestTwoDatesIsAmbiguous exercises extractDate's ambiguity check: two +// dateRe-shaped tokens can't both be the positional date. +func TestTwoDatesIsAmbiguous(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"2026-07-22", "2026-07-23"}, nil, &out, &errb); code != 2 { + t.Errorf("two dates code=%d want 2 (stderr=%q)", code, errb.String()) + } +} + +// TestUnknownVersion exercises -b/--bible's version-validation usage-error +// path without touching the network: the version code is checked against +// the five known codes before any fetch is attempted. func TestUnknownVersion(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) var out, errb bytes.Buffer - if code := Run([]string{"show", "zzz"}, nil, &out, &errb); code != 2 { + if code := Run([]string{"-b", "zzz"}, nil, &out, &errb); code != 2 { t.Errorf("unknown version code=%d want 2 (stderr=%q)", code, errb.String()) } } + +// TestLectionaryTradMapsToTraditional exercises -l/--lectionary's trad -> +// traditional normalisation. It goes through extractDate/flag parsing and +// validation only (no network): a bogus date lets it exit before any fetch +// is attempted, but only after the -l value has already been validated and +// mapped, which is what this test checks indirectly via the exit code (a +// bad --lectionary would exit 2 for a different reason, so this alone +// wouldn't distinguish the two -- see TestLectionaryBogus for that side). +// Exercised directly against parseFlags below. +func TestLectionaryTradMapsToTraditional(t *testing.T) { + lectionary, err := normalizeLectionary("trad") + if err != nil { + t.Fatalf("normalizeLectionary(trad): %v", err) + } + if lectionary != "traditional" { + t.Errorf("normalizeLectionary(trad) = %q, want traditional", lectionary) + } +} + +func TestLectionaryBogus(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"-l", "bogus"}, nil, &out, &errb); code != 2 { + t.Errorf("bogus lectionary code=%d want 2 (stderr=%q)", code, errb.String()) + } + if _, err := normalizeLectionary("bogus"); err == nil { + t.Error("normalizeLectionary(bogus): want error, got nil") + } +} + +func TestLangBogus(t *testing.T) { + t.Setenv("XDG_CONFIG_HOME", t.TempDir()) + var out, errb bytes.Buffer + if code := Run([]string{"-g", "de"}, nil, &out, &errb); code != 2 { + t.Errorf("bogus lang code=%d want 2 (stderr=%q)", code, errb.String()) + } +} + +// TestDateTokenAnyPosition exercises extractDate directly: the date token +// is found regardless of where it appears among other flags. +func TestDateTokenAnyPosition(t *testing.T) { + cases := [][]string{ + {"-a", "2026-07-22"}, + {"2026-07-22", "-a"}, + {"-a", "2026-07-22", "-r"}, + } + for _, args := range cases { + date, rest, err := extractDate(args) + if err != nil { + t.Fatalf("extractDate(%v): %v", args, err) + } + if date != "2026-07-22" { + t.Errorf("extractDate(%v) date = %q, want 2026-07-22", args, date) + } + for _, r := range rest { + if r == "2026-07-22" { + t.Errorf("extractDate(%v) rest = %v still contains the date", args, rest) + } + } + } +} + +// TestExtractDateDefaultsToday checks that omitting a date token leaves +// today's date and passes every token through untouched. +func TestExtractDateDefaultsToday(t *testing.T) { + date, rest, err := extractDate([]string{"-a", "-r"}) + if err != nil { + t.Fatalf("extractDate: %v", err) + } + if date != today() { + t.Errorf("extractDate date = %q, want today() = %q", date, today()) + } + if len(rest) != 2 || rest[0] != "-a" || rest[1] != "-r" { + t.Errorf("extractDate rest = %v, want [-a -r]", rest) + } +} |
