66 "fmt"
77 "io"
88 "os"
9+ "strconv"
910 "strings"
1011
1112 "github.com/unraid/apprise-go/internal/notify"
@@ -17,34 +18,112 @@ const usageText = "" +
1718 " apprise [OPTIONS] [APPRISE_URL [APPRISE_URL2 [APPRISE_URL3]]]\n " +
1819 " apprise storage [OPTIONS] [ACTION] [UID1 [UID2 [UID3]]]\n "
1920
21+ type cliOptions struct {
22+ body string
23+ title string
24+ notificationType string
25+ inputFormat string
26+ disableAsync bool
27+ showVersion bool
28+ showHelp bool
29+ showSchema bool
30+ showDetails bool
31+ dryRun bool
32+ debug bool
33+ verbose int
34+ recursionDepth int
35+ interpretEscapes bool
36+ interpretEmojis bool
37+ theme string
38+ configPaths []string
39+ attachments []string
40+ pluginPaths []string
41+ tags []string
42+ storagePath string
43+ storagePruneDays int
44+ storageUIDLength int
45+ storageMode string
46+ }
47+
48+ type stringSliceFlag []string
49+
50+ func (s * stringSliceFlag ) String () string {
51+ return strings .Join (* s , "," )
52+ }
53+
54+ func (s * stringSliceFlag ) Set (value string ) error {
55+ * s = append (* s , value )
56+ return nil
57+ }
58+
59+ type countFlag int
60+
61+ func (c * countFlag ) String () string {
62+ return strconv .Itoa (int (* c ))
63+ }
64+
65+ func (c * countFlag ) Set (value string ) error {
66+ * c ++
67+ return nil
68+ }
69+
70+ func (c * countFlag ) IsBoolFlag () bool {
71+ return true
72+ }
73+
2074func Run (args []string , stdout , stderr io.Writer ) int {
75+ opts := defaultCliOptions ()
76+ args = normalizeArgs (args )
2177 fs := flag .NewFlagSet ("apprise" , flag .ContinueOnError )
2278 fs .SetOutput (stderr )
2379
24- var (
25- body string
26- title string
27- notificationType string
28- inputFormat string
29- disableAsync bool
30- showVersion bool
31- showHelp bool
32- )
33-
34- fs .StringVar (& body , "body" , "" , "Specify the message body." )
35- fs .StringVar (& body , "b" , "" , "Specify the message body." )
36- fs .StringVar (& title , "title" , "" , "Specify the message title." )
37- fs .StringVar (& title , "t" , "" , "Specify the message title." )
38- fs .StringVar (& notificationType , "notification-type" , string (notify .NotifyInfo ), "Specify the message type." )
39- fs .StringVar (& notificationType , "n" , string (notify .NotifyInfo ), "Specify the message type." )
40- fs .StringVar (& inputFormat , "input-format" , "text" , "Specify the message input format." )
41- fs .StringVar (& inputFormat , "i" , "text" , "Specify the message input format." )
42- fs .BoolVar (& disableAsync , "disable-async" , false , "Send all notifications sequentially." )
43- fs .BoolVar (& disableAsync , "Da" , false , "Send all notifications sequentially." )
44- fs .BoolVar (& showVersion , "version" , false , "Display the apprise version and exit." )
45- fs .BoolVar (& showVersion , "V" , false , "Display the apprise version and exit." )
46- fs .BoolVar (& showHelp , "help" , false , "Show help." )
47- fs .BoolVar (& showHelp , "h" , false , "Show help." )
80+ fs .StringVar (& opts .body , "body" , "" , "Specify the message body." )
81+ fs .StringVar (& opts .body , "b" , "" , "Specify the message body." )
82+ fs .StringVar (& opts .title , "title" , "" , "Specify the message title." )
83+ fs .StringVar (& opts .title , "t" , "" , "Specify the message title." )
84+ fs .StringVar (& opts .notificationType , "notification-type" , opts .notificationType , "Specify the message type." )
85+ fs .StringVar (& opts .notificationType , "n" , opts .notificationType , "Specify the message type." )
86+ fs .StringVar (& opts .inputFormat , "input-format" , opts .inputFormat , "Specify the message input format." )
87+ fs .StringVar (& opts .inputFormat , "i" , opts .inputFormat , "Specify the message input format." )
88+ fs .BoolVar (& opts .disableAsync , "disable-async" , false , "Send all notifications sequentially." )
89+ fs .BoolVar (& opts .disableAsync , "Da" , false , "Send all notifications sequentially." )
90+ fs .BoolVar (& opts .dryRun , "dry-run" , false , "Perform a trial run without sending notifications." )
91+ fs .BoolVar (& opts .dryRun , "d" , false , "Perform a trial run without sending notifications." )
92+ fs .BoolVar (& opts .showDetails , "details" , false , "Prints details about the current services supported by Apprise." )
93+ fs .BoolVar (& opts .showDetails , "l" , false , "Prints details about the current services supported by Apprise." )
94+ fs .BoolVar (& opts .showSchema , "schema" , false , "Prints Apprise schema JSON and exits." )
95+ fs .IntVar (& opts .recursionDepth , "recursion-depth" , opts .recursionDepth , "Specify the recursion depth when loading configs." )
96+ fs .IntVar (& opts .recursionDepth , "R" , opts .recursionDepth , "Specify the recursion depth when loading configs." )
97+ fs .Var ((* countFlag )(& opts .verbose ), "v" , "Increase verbosity." )
98+ fs .Var ((* countFlag )(& opts .verbose ), "verbose" , "Increase verbosity." )
99+ fs .BoolVar (& opts .interpretEscapes , "interpret-escapes" , false , "Enable interpretation of backslash escapes." )
100+ fs .BoolVar (& opts .interpretEscapes , "e" , false , "Enable interpretation of backslash escapes." )
101+ fs .BoolVar (& opts .interpretEmojis , "interpret-emojis" , false , "Enable interpretation of :emoji: definitions." )
102+ fs .BoolVar (& opts .interpretEmojis , "j" , false , "Enable interpretation of :emoji: definitions." )
103+ fs .BoolVar (& opts .debug , "debug" , false , "Debug mode." )
104+ fs .BoolVar (& opts .debug , "D" , false , "Debug mode." )
105+ fs .StringVar (& opts .theme , "theme" , opts .theme , "Specify the default theme." )
106+ fs .StringVar (& opts .theme , "T" , opts .theme , "Specify the default theme." )
107+ fs .Var ((* stringSliceFlag )(& opts .tags ), "tag" , "Specify tags used to filter which services to notify." )
108+ fs .Var ((* stringSliceFlag )(& opts .tags ), "g" , "Specify tags used to filter which services to notify." )
109+ fs .Var ((* stringSliceFlag )(& opts .configPaths ), "config" , "Specify one or more configuration locations." )
110+ fs .Var ((* stringSliceFlag )(& opts .configPaths ), "c" , "Specify one or more configuration locations." )
111+ fs .Var ((* stringSliceFlag )(& opts .attachments ), "attach" , "Specify one or more attachments." )
112+ fs .Var ((* stringSliceFlag )(& opts .attachments ), "a" , "Specify one or more attachments." )
113+ fs .Var ((* stringSliceFlag )(& opts .pluginPaths ), "plugin-path" , "Specify one or more plugin paths to scan." )
114+ fs .Var ((* stringSliceFlag )(& opts .pluginPaths ), "P" , "Specify one or more plugin paths to scan." )
115+ fs .StringVar (& opts .storagePath , "storage-path" , opts .storagePath , "Specify the path to the persistent storage location." )
116+ fs .StringVar (& opts .storagePath , "S" , opts .storagePath , "Specify the path to the persistent storage location." )
117+ fs .IntVar (& opts .storagePruneDays , "storage-prune-days" , opts .storagePruneDays , "Define the number of days the storage prune should run using." )
118+ fs .IntVar (& opts .storagePruneDays , "SPD" , opts .storagePruneDays , "Define the number of days the storage prune should run using." )
119+ fs .IntVar (& opts .storageUIDLength , "storage-uid-length" , opts .storageUIDLength , "Define the number of unique characters to store persistent cache in." )
120+ fs .IntVar (& opts .storageUIDLength , "SUL" , opts .storageUIDLength , "Define the number of unique characters to store persistent cache in." )
121+ fs .StringVar (& opts .storageMode , "storage-mode" , opts .storageMode , "Specify the persistent storage operational mode." )
122+ fs .StringVar (& opts .storageMode , "SM" , opts .storageMode , "Specify the persistent storage operational mode." )
123+ fs .BoolVar (& opts .showVersion , "version" , false , "Display the apprise version and exit." )
124+ fs .BoolVar (& opts .showVersion , "V" , false , "Display the apprise version and exit." )
125+ fs .BoolVar (& opts .showHelp , "help" , false , "Show help." )
126+ fs .BoolVar (& opts .showHelp , "h" , false , "Show help." )
48127
49128 if err := fs .Parse (args ); err != nil {
50129 if errors .Is (err , flag .ErrHelp ) {
@@ -56,41 +135,84 @@ func Run(args []string, stdout, stderr io.Writer) int {
56135 return 2
57136 }
58137
59- if showHelp {
138+ if opts . showHelp {
60139 printUsage (stdout )
61140 return 0
62141 }
63142
64- if showVersion {
143+ if opts . showVersion {
65144 fmt .Fprintln (stdout , version .Message ())
66145 return 0
67146 }
68147
69- nt , ok := notify .ParseNotifyType (notificationType )
70- if ! ok {
71- fmt .Fprintf (stderr , "unsupported notification type: %s\n " , notificationType )
72- return 2
148+ if opts .showSchema {
149+ schemaJSON , err := SchemaJSON ()
150+ if err != nil {
151+ fmt .Fprintln (stderr , err )
152+ return 1
153+ }
154+ if _ , err := stdout .Write (append (schemaJSON , '\n' )); err != nil {
155+ fmt .Fprintln (stderr , err )
156+ return 1
157+ }
158+ return 0
159+ }
160+
161+ if opts .showDetails {
162+ if err := PrintDetails (stdout ); err != nil {
163+ fmt .Fprintln (stderr , err )
164+ return 1
165+ }
166+ return 0
73167 }
74168
75169 urls := fs .Args ()
76- if len (urls ) == 0 {
77- printUsage (stdout )
170+ if isStorageAction (urls ) {
171+ return RunStorage (& opts , urls , stdout , stderr )
172+ }
173+
174+ tagged := resolveNotifyURLs (& opts , urls , stderr )
175+ if len (tagged ) == 0 {
176+ fmt .Fprintln (stdout , "You must specify at least one server URL or populated configuration file." )
177+ fmt .Fprintln (stdout , "Try 'apprise --help' for more information." )
78178 return 1
79179 }
80180
181+ nt , ok := notify .ParseNotifyType (opts .notificationType )
182+ if ! ok {
183+ fmt .Fprintf (stderr , "unsupported notification type: %s\n " , opts .notificationType )
184+ return 2
185+ }
186+
187+ body := opts .body
188+ title := opts .title
81189 if body == "" {
82190 data , err := io .ReadAll (os .Stdin )
83191 if err == nil {
84192 body = string (data )
85193 }
86194 }
87195
88- _ = inputFormat
89- _ = disableAsync
196+ // TODO: Wire these options into CLI behavior once the runtime supports them.
197+ _ = opts .inputFormat
198+ _ = opts .disableAsync
199+ _ = opts .attachments
200+ _ = opts .pluginPaths
201+ _ = opts .theme
202+ _ = opts .recursionDepth
203+ _ = opts .dryRun
204+ _ = opts .debug
205+ _ = opts .verbose
206+ _ = opts .interpretEscapes
207+ _ = opts .interpretEmojis
208+ _ = opts .storageMode
209+ _ = opts .storagePath
210+ _ = opts .storagePruneDays
211+ _ = opts .storageUIDLength
90212
91213 failed := false
92- for _ , rawURL := range urls {
93- parsed , err := notify .ParseURL (rawURL )
214+ for _ , entry := range tagged {
215+ parsed , err := notify .ParseURL (entry . URL )
94216 if err != nil {
95217 fmt .Fprintf (stderr , "invalid url: %s\n " , err )
96218 failed = true
@@ -1358,3 +1480,95 @@ func Run(args []string, stdout, stderr io.Writer) int {
13581480func printUsage (w io.Writer ) {
13591481 fmt .Fprint (w , usageText )
13601482}
1483+
1484+ func defaultCliOptions () cliOptions {
1485+ return cliOptions {
1486+ notificationType : string (notify .NotifyInfo ),
1487+ inputFormat : "text" ,
1488+ theme : "default" ,
1489+ recursionDepth : 1 ,
1490+ storagePath : defaultStoragePath ,
1491+ storageMode : defaultStorageMode ,
1492+ storagePruneDays : envInt ("APPRISE_STORAGE_PRUNE_DAYS" , defaultStoragePruneDays ),
1493+ storageUIDLength : envInt ("APPRISE_STORAGE_UID_LENGTH" , defaultStorageUIDLength ),
1494+ }
1495+ }
1496+
1497+ func envInt (name string , fallback int ) int {
1498+ if raw := strings .TrimSpace (os .Getenv (name )); raw != "" {
1499+ if value , err := strconv .Atoi (raw ); err == nil {
1500+ return value
1501+ }
1502+ }
1503+ return fallback
1504+ }
1505+
1506+ func normalizeArgs (args []string ) []string {
1507+ normalized := []string {}
1508+ for _ , arg := range args {
1509+ if isVerboseBundle (arg ) {
1510+ for range strings .TrimPrefix (arg , "-" ) {
1511+ normalized = append (normalized , "-v" )
1512+ }
1513+ continue
1514+ }
1515+ normalized = append (normalized , arg )
1516+ }
1517+ return normalized
1518+ }
1519+
1520+ func isVerboseBundle (arg string ) bool {
1521+ if len (arg ) < 3 || ! strings .HasPrefix (arg , "-" ) {
1522+ return false
1523+ }
1524+ trimmed := strings .TrimPrefix (arg , "-" )
1525+ for _ , r := range trimmed {
1526+ if r != 'v' {
1527+ return false
1528+ }
1529+ }
1530+ return true
1531+ }
1532+
1533+ func isStorageAction (args []string ) bool {
1534+ if len (args ) == 0 {
1535+ return false
1536+ }
1537+ return strings .HasPrefix ("storage" , strings .ToLower (args [0 ]))
1538+ }
1539+
1540+ func resolveNotifyURLs (opts * cliOptions , args []string , stderr io.Writer ) []taggedURL {
1541+ if len (args ) > 0 {
1542+ if len (opts .tags ) > 0 {
1543+ fmt .Fprintln (stderr , "--tag (-g) entries are ignored when using specified URLs" )
1544+ }
1545+ if len (opts .configPaths ) > 0 {
1546+ fmt .Fprintln (stderr , "You defined both URLs and a --config (-c) entry; Only the URLs will be referenced." )
1547+ }
1548+
1549+ var urls []taggedURL
1550+ for _ , arg := range args {
1551+ for _ , raw := range detectURLs (arg ) {
1552+ if strings .TrimSpace (raw ) == "" {
1553+ continue
1554+ }
1555+ urls = append (urls , taggedURL {URL : raw })
1556+ }
1557+ }
1558+ return urls
1559+ }
1560+
1561+ if len (opts .configPaths ) > 0 {
1562+ return filterTaggedURLs (loadTaggedURLs (loadConfigPaths (opts .configPaths )), parseTagFilters (opts .tags ))
1563+ }
1564+
1565+ if raw := strings .TrimSpace (os .Getenv (defaultEnvAppriseURLs )); raw != "" {
1566+ parsed := parseTaggedLine (raw )
1567+ if len (parsed ) == 0 {
1568+ return nil
1569+ }
1570+ return filterTaggedURLs (parsed , parseTagFilters (opts .tags ))
1571+ }
1572+
1573+ return filterTaggedURLs (loadTaggedURLs (loadConfigPaths (nil )), parseTagFilters (opts .tags ))
1574+ }
0 commit comments