Skip to content

Commit fbed2ba

Browse files
authored
fix(cli): match apprise help and option errors (#62)
## What changed - Render full Apprise-style CLI help for `--help` and `-h`. - Suppress Go's default `flag` usage dump for unknown options. - Emit Python Apprise-style unknown-option errors for unsupported flags. - Add parity coverage for no-args, help, bad-option, and mixed parse-error CLI workflows. - Add built-binary E2E coverage to validate real process exit codes and stdout/stderr wiring. - Add regression coverage that older accepted flag forms still parse, including legacy single-dash long aliases. - Update the parse-error regression to compare Go against installed Python Apprise for the exact same arguments instead of using hand-written expected output. ## Why The CLI request-spec parity tests covered notification behavior, but not CLI help and parser output. As a result, `apprise-go` diverged from Python Apprise: `--help` showed only the short usage banner, and invalid flags dumped Go's full flag table. The parser change should not break existing `apprise-go` users who were relying on accepted Go flag spellings, so the PR also locks in compatibility for those older forms. For mixed parser failures, the test now uses the installed Python Apprise CLI as the source of truth. ## Validation - Built and manually ran `/tmp/apprise-cli-parity` from `./cmd/apprise`: - `--version` - no arguments - `--help` - `--blah` - legacy single-dash long aliases such as `-body`, `-title`, `-notification-type`, `-input-format`, and `-version` - `go test ./internal/cli -run TestCLIParseErrorsMatchPythonApprise -count=1` - `go test ./internal/cli` - `go test ./...` - Limetech CodeRabbit final check: `unresolved_coderabbit_threads 0` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Help flag now displays the full, user-friendly help text. * **Bug Fixes** * Unknown options produce a clear custom error message and proper exit code; parse errors are reported consistently. * **Tests** * Added cross-implementation tests validating help, unknown-option behavior, legacy flag parsing, and parse-error handling. * **Chores** * Updated internal run/notes document to reflect the new CLI parity run. <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/unraid/apprise-go/pull/62?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> #59
1 parent 0230c9d commit fbed2ba

3 files changed

Lines changed: 375 additions & 38 deletions

File tree

.codex/coderabbit-fixes-wip.md

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
## Context
44

55
- Repo: unraid/apprise-go
6-
- Branch: codex/telegram-formatting-followup
7-
- PR: #60
8-
- PR URL: https://github.com/unraid/apprise-go/pull/60
9-
- Generated at: 2026-05-21T00:36:00-04:00
6+
- Branch: codex/cli-help-parity
7+
- PR: #62
8+
- PR URL: https://github.com/unraid/apprise-go/pull/62
9+
- Generated at: 2026-05-21T14:29:52Z
1010

1111
## Inputs Pulled
1212

@@ -18,42 +18,19 @@
1818

1919
| Item ID | Type | File | Line | Summary | Status | Link | Evidence |
2020
| --- | --- | --- | --- | --- | --- | --- | --- |
21-
| CR-001 | thread | internal/notify/format_convert_test.go | 168 | Add Python parity to the cross-target corpus test. | BLOCKED | https://github.com/unraid/apprise-go/pull/60#discussion_r3278534673 | Skipped: this corpus intentionally validates target format conversion behavior that fixes Telegram behavior beyond current Python Apprise. Adding Python request parity would lock in the upstream bug this PR is fixing. |
22-
| CR-002 | thread | internal/notify/telegram_format_test.go | 140 | Route new Telegram format tests through Python-vs-Go request-sequence parity. | BLOCKED | https://github.com/unraid/apprise-go/pull/60#discussion_r3278534678 | Skipped: these tests assert corrected Telegram parse payloads that current Python Apprise does not emit. |
23-
| CR-003 | thread | internal/notify/live/telegram_live_test.go | 101 | Add Python-apprise request parity to the live Telegram test. | BLOCKED | https://github.com/unraid/apprise-go/pull/60#discussion_r3278534680 | Skipped: live test purpose is Bot API acceptance of corrected Go-generated Telegram parse payloads, not matching upstream Python's currently broken formatting. |
24-
| CR-004 | thread | internal/notify/live/telegram_live_test.go | n/a | Require explicit destination for the live suite. | DONE | https://github.com/unraid/apprise-go/pull/60#discussion_r3278534682 | Addressed in `a3dd48f`; GraphQL reports thread resolved/outdated. |
25-
| RVW-001 | review-body | internal/notify/live/telegram_live_test.go | 140-149 | Redact bot token from live test transport error logs. | DONE | https://github.com/unraid/apprise-go/pull/60#pullrequestreview-4320292462 | Added redaction helper and regression test; targeted and full Go tests passed. |
21+
| CR-001 | thread | internal/cli/cli.go | 212 | Unknown-option detection is over-applied and can misclassify non-unknown parse errors. | DONE | https://github.com/unraid/apprise-go/pull/62#discussion_r3281866028 | `go test ./internal/cli` passed; exact `-R not-an-int --blah` behavior is now compared against Python Apprise. |
22+
| RVW-001 | review-body | top-level | n/a | Review body reports one actionable comment, represented by CR-001. | DONE | https://github.com/unraid/apprise-go/pull/62#pullrequestreview-4337724219 | No separate top-level actionable item beyond CR-001. |
2623

2724
## Execution Log
2825

29-
### 1. Item: CR-004
30-
- Action: Required `APPRISE_GO_TELEGRAM_CHAT_ID` for live validation and removed auto-discovery/bot-ID fallback.
31-
- Validation: `go test ./internal/notify/live -run TestTelegramLiveFormattingAgainstBotAPI -count=1 -v` with explicit live env passed.
32-
- Result: DONE
33-
34-
### 2. Item: CR-001
35-
- Action: Verified this asks for parity against Python Apprise behavior that does not include the corrected Telegram conversions under test.
36-
- Validation: Code/test review.
37-
- Result: BLOCKED; skipped because it conflicts with the bug fix goal.
38-
39-
### 3. Item: CR-002
40-
- Action: Verified the requested parity would force the new Telegram unit tests back to current Python output.
41-
- Validation: Code/test review.
42-
- Result: BLOCKED; skipped because it conflicts with the bug fix goal.
43-
44-
### 4. Item: CR-003
45-
- Action: Verified live validation is intentionally testing Bot API acceptance of Go-generated corrected payloads.
46-
- Validation: Code/test review.
47-
- Result: BLOCKED; skipped because Python parity is not the purpose of this live suite.
48-
49-
### 5. Item: RVW-001
50-
- Action: Added token redaction before reporting request creation or transport errors.
51-
- Validation: `go test ./internal/notify/live -count=1`; `go test ./internal/notify ./internal/notify/live -run 'TestTelegram|TestTargetFormatConversionCorpusAcrossWorkflowTargets' -count=1`; live Bot API test with explicit env; `go test ./...`
26+
### 1. Item: CR-001
27+
- Action: Verified the exact installed Python Apprise behavior for `-R not-an-int --blah` and updated the regression test to compare Go against Python instead of asserting a hand-written expected result.
28+
- Validation: `go test ./internal/cli` passed.
5229
- Result: DONE
5330

5431
## Final Checks
5532

5633
- [x] Queue reviewed: no `TODO` left
5734
- [x] Remaining `BLOCKED` items documented with reason
58-
- [ ] Re-pulled CodeRabbit threads and reviews
59-
- [ ] No unhandled top-level review-body comment remains
35+
- [x] Re-pulled CodeRabbit threads and reviews
36+
- [x] No unhandled top-level review-body comment remains

internal/cli/cli.go

Lines changed: 113 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,81 @@ const usageText = "" +
1818
" apprise [OPTIONS] [APPRISE_URL [APPRISE_URL2 [APPRISE_URL3]]]\n" +
1919
" apprise storage [OPTIONS] [ACTION] [UID1 [UID2 [UID3]]]\n"
2020

21+
const helpText = usageText + `
22+
Send a notification to all of the specified servers identified by their URLs
23+
the content provided within the title, body and notification-type.
24+
25+
For a list of all of the supported services and information on how to use
26+
them, check out https://github.com/caronc/apprise
27+
28+
Options:
29+
-b, --body TEXT Specify the message body. If no body is
30+
specified then content is read from <stdin>.
31+
-t, --title TEXT Specify the message title. This field is
32+
completely optional.
33+
-P, --plugin-path PATH Specify one or more plugin paths to scan.
34+
-S, --storage-path PATH Specify the path to the persistent storage
35+
location
36+
(default=~/.local/share/apprise/cache).
37+
-SPD, --storage-prune-days INTEGER
38+
Define the number of days the storage prune
39+
should run using. Setting this to zero (0)
40+
will eliminate all accumulated content. By
41+
default this value is 30 days.
42+
-SUL, --storage-uid-length INTEGER
43+
Define the number of unique characters to
44+
store persistent cache in. By default this
45+
value is 8 characters.
46+
-SM, --storage-mode MODE Specify the persistent storage operational
47+
mode (default=auto). Possible values are:
48+
"auto", "flush", "memory".
49+
-c, --config CONFIG_URL Specify one or more configuration locations.
50+
-a, --attach ATTACHMENT_URL Specify one or more attachments.
51+
-n, --notification-type TYPE Specify the message type (default=info).
52+
Possible values are: "info", "success",
53+
"warning", "failure".
54+
-i, --input-format FORMAT Specify the message input format
55+
(default=text). Possible values are: "text",
56+
"markdown", "html".
57+
-T, --theme THEME Specify the default theme.
58+
-g, --tag TAG Specify one or more tags to filter which
59+
services to notify. Use multiple --tag (-g)
60+
entries to match ANY tag. Use comma
61+
separators to require ALL tags (strict
62+
match). Omit to notify untagged services
63+
only, or use "all" to notify everything.
64+
-Da, --disable-async Send all notifications sequentially
65+
-d, --dry-run Perform a trial run but only prints the
66+
notification services to-be triggered to
67+
stdout. Notifications are never sent using
68+
this mode.
69+
-l, --details Prints details about the current services
70+
supported by Apprise.
71+
-R, --recursion-depth INTEGER The number of recursive import entries that
72+
can be loaded from within Apprise
73+
configuration. By default this is set to 1.
74+
-v, --verbose Makes the operation more talkative. Use
75+
multiple v to increase the verbosity. I.e.:
76+
-vvvv
77+
-e, --interpret-escapes Enable interpretation of backslash escapes
78+
-j, --interpret-emojis Enable interpretation of :emoji: definitions
79+
-D, --debug Debug mode
80+
-V, --version Display the apprise version and exit.
81+
-h, --help Show this message and exit.
82+
83+
Actions:
84+
storage Access the persistent storage disk administration
85+
list List all URL IDs associated with detected URL(s). This
86+
is also the default action run if nothing is provided
87+
prune Eliminates stale entries found based on --storage-prune-
88+
days (-SPD)
89+
clean Removes any persistent data created by Apprise
90+
`
91+
92+
const commandErrorUsage = "" +
93+
"Usage: apprise [OPTIONS] SERVER_URL [SERVER_URL2 [SERVER_URL3]]\n" +
94+
"Try 'apprise --help' for help.\n"
95+
2196
type cliOptions struct {
2297
body string
2398
title string
@@ -75,7 +150,8 @@ func Run(args []string, stdout, stderr io.Writer) int {
75150
opts := defaultCliOptions()
76151
args = normalizeArgs(args)
77152
fs := flag.NewFlagSet("apprise", flag.ContinueOnError)
78-
fs.SetOutput(stderr)
153+
fs.SetOutput(io.Discard)
154+
fs.Usage = func() {}
79155

80156
fs.StringVar(&opts.body, "body", "", "Specify the message body.")
81157
fs.StringVar(&opts.body, "b", "", "Specify the message body.")
@@ -127,16 +203,19 @@ func Run(args []string, stdout, stderr io.Writer) int {
127203

128204
if err := fs.Parse(args); err != nil {
129205
if errors.Is(err, flag.ErrHelp) {
130-
printUsage(stdout)
206+
printHelp(stdout)
131207
return 0
132208
}
209+
if option := unknownOption(args, fs); option != "" {
210+
printUnknownOption(stderr, option)
211+
return 2
212+
}
133213
fmt.Fprintln(stderr, err)
134-
printUsage(stderr)
135214
return 2
136215
}
137216

138217
if opts.showHelp {
139-
printUsage(stdout)
218+
printHelp(stdout)
140219
return 0
141220
}
142221

@@ -253,6 +332,15 @@ func printUsage(w io.Writer) {
253332
fmt.Fprint(w, usageText)
254333
}
255334

335+
func printHelp(w io.Writer) {
336+
fmt.Fprint(w, helpText)
337+
}
338+
339+
func printUnknownOption(w io.Writer, option string) {
340+
fmt.Fprint(w, commandErrorUsage+"\n")
341+
fmt.Fprintf(w, "Error: No such option '%s'.\n", option)
342+
}
343+
256344
func defaultCliOptions() cliOptions {
257345
return cliOptions{
258346
notificationType: string(notify.NotifyInfo),
@@ -275,6 +363,27 @@ func envInt(name string, fallback int) int {
275363
return fallback
276364
}
277365

366+
func unknownOption(args []string, fs *flag.FlagSet) string {
367+
for _, arg := range args {
368+
if arg == "--" {
369+
return ""
370+
}
371+
if arg == "-" || !strings.HasPrefix(arg, "-") {
372+
continue
373+
}
374+
375+
option := strings.SplitN(arg, "=", 2)[0]
376+
name := strings.TrimLeft(option, "-")
377+
if name == "" {
378+
continue
379+
}
380+
if fs.Lookup(name) == nil {
381+
return option
382+
}
383+
}
384+
return ""
385+
}
386+
278387
func normalizeArgs(args []string) []string {
279388
normalized := []string{}
280389
for _, arg := range args {

0 commit comments

Comments
 (0)