Skip to content

Commit 170fbab

Browse files
author
饺子w
authored
add watch command output (#20)
1 parent 80aad21 commit 170fbab

4 files changed

Lines changed: 174 additions & 5 deletions

File tree

README.md

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Gotify-CLI is a command line client for pushing messages to [gotify/server][goti
1212
* initialization wizard
1313
* piping support (`echo message | gotify push`)
1414
* simple to use
15+
* watch and push script result changes (`gotify watch "curl http://example.com/api | jq '.data'"`)
1516

1617
## Alternatives
1718

@@ -61,35 +62,55 @@ $ gotify push "my message"
6162
$ echo my message | gotify push
6263
$ gotify push < somefile
6364
$ gotify push -t "my title" -p 10 "my message"
65+
$ gotify watch "curl http://example.com/api | jq '.data'"
6466
```
6567

6668
## Help
6769

68-
**Uses version `v1.2.0`**
70+
**Uses version `v2.1.0`**
6971

7072
```bash
71-
$ gotify help
7273
NAME:
7374
Gotify - The official Gotify-CLI
7475

7576
USAGE:
76-
gotify [global options] command [command options] [arguments...]
77+
cli [global options] command [command options] [arguments...]
7778

7879
VERSION:
79-
1.2.0
80+
2.1.0
8081

8182
COMMANDS:
8283
init Initializes the Gotify-CLI
8384
version, v Shows the version
8485
config Shows the config
8586
push, p Pushes a message
87+
watch watch the result of a command and pushes output difference
8688
help, h Shows a list of commands or help for one command
8789

8890
GLOBAL OPTIONS:
8991
--help, -h show help
9092
--version, -v print the version
9193
```
9294

95+
### Watch help
96+
97+
```
98+
NAME:
99+
cli watch - watch the result of a command and pushes output difference
100+
101+
USAGE:
102+
cli watch [command options] <cmd>
103+
104+
OPTIONS:
105+
--interval value, -n value watch interval (sec) (default: 2)
106+
--priority value, -p value Set the priority (default: 0)
107+
--exec value, -x value Pass command to exec (default to "sh -c")
108+
--title value, -t value Set the title (empty for command)
109+
--token value Override the app token
110+
--url value Override the Gotify URL
111+
--output value, -o value Output verbosity (short|default|long) (default: "default")
112+
```
113+
93114
### Push help
94115

95116
```bash

cli.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func main() {
2929
command.Version(),
3030
command.Config(),
3131
command.Push(),
32+
command.Watch(),
3233
}
3334
err := app.Run(os.Args)
3435
if err != nil {

command/push.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"gopkg.in/urfave/cli.v1"
1717
)
1818

19-
2019
func Push() cli.Command {
2120
return cli.Command{
2221
Name: "push",

command/watch.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package command
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"net/url"
8+
"os/exec"
9+
"strings"
10+
"time"
11+
12+
"github.com/gotify/cli/v2/config"
13+
"github.com/gotify/cli/v2/utils"
14+
"github.com/gotify/go-api-client/v2/models"
15+
"gopkg.in/urfave/cli.v1"
16+
)
17+
18+
func Watch() cli.Command {
19+
return cli.Command{
20+
Name: "watch",
21+
Usage: "watch the result of a command and pushes output difference",
22+
ArgsUsage: "<cmd>",
23+
Flags: []cli.Flag{
24+
cli.Float64Flag{Name: "interval,n", Usage: "watch interval (sec)", Value: 2},
25+
cli.IntFlag{Name: "priority,p", Usage: "Set the priority"},
26+
cli.StringFlag{Name: "exec,x", Usage: "Pass command to exec", Value: "sh -c"},
27+
cli.StringFlag{Name: "title,t", Usage: "Set the title (empty for command)"},
28+
cli.StringFlag{Name: "token", Usage: "Override the app token"},
29+
cli.StringFlag{Name: "url", Usage: "Override the Gotify URL"},
30+
cli.StringFlag{Name: "output,o", Usage: "Output verbosity (short|default|long)", Value: "default"},
31+
},
32+
Action: doWatch,
33+
}
34+
}
35+
36+
func doWatch(ctx *cli.Context) {
37+
conf, confErr := config.ReadConfig(config.GetLocations())
38+
39+
cmdArgs := ctx.Args()
40+
cmdStringNotation := strings.Join(cmdArgs, " ")
41+
execArgs := strings.Split(ctx.String("exec"), " ")
42+
cmdArgs = append(execArgs[1:], cmdStringNotation)
43+
execCmd := execArgs[0]
44+
45+
outputMode := ctx.String("output")
46+
if !(outputMode == "default" || outputMode == "long" || outputMode == "short") {
47+
utils.Exit1With("output mode should be short|default|long")
48+
return
49+
}
50+
interval := ctx.Float64("interval")
51+
priority := ctx.Int("priority")
52+
title := ctx.String("title")
53+
if title == "" {
54+
title = cmdStringNotation
55+
}
56+
token := ctx.String("token")
57+
if token == "" {
58+
if confErr != nil {
59+
utils.Exit1With("token is not configured, run 'gotify init'")
60+
return
61+
}
62+
token = conf.Token
63+
}
64+
stringURL := ctx.String("url")
65+
if stringURL == "" {
66+
if confErr != nil {
67+
utils.Exit1With("url is not configured, run 'gotify init'")
68+
return
69+
}
70+
stringURL = conf.URL
71+
}
72+
parsedURL, err := url.Parse(stringURL)
73+
if err != nil {
74+
utils.Exit1With("invalid url", stringURL)
75+
return
76+
}
77+
78+
watchInterval := time.Duration(interval*1000) * time.Millisecond
79+
80+
evalCmdOutput := func() (string, error) {
81+
cmd := exec.Command(execCmd, cmdArgs...)
82+
timeOut := time.After(watchInterval)
83+
outputBuf := bytes.NewBuffer([]byte{})
84+
cmd.Stdout = outputBuf
85+
cmd.Stderr = outputBuf
86+
err := cmd.Start()
87+
if err != nil {
88+
return "", fmt.Errorf("command failed to invoke: %v", err)
89+
}
90+
done := make(chan error)
91+
go func() {
92+
err := cmd.Wait()
93+
if err != nil {
94+
done <- fmt.Errorf("command failed to invoke: %v", err)
95+
}
96+
done <- nil
97+
}()
98+
select {
99+
case err := <-done:
100+
return outputBuf.String(), err
101+
case <-timeOut:
102+
cmd.Process.Kill()
103+
return outputBuf.String(), errors.New("command timed out")
104+
}
105+
}
106+
107+
lastOutput, err := evalCmdOutput()
108+
if err != nil {
109+
utils.Exit1With("first run failed", err)
110+
}
111+
for range time.NewTicker(watchInterval).C {
112+
output, err := evalCmdOutput()
113+
if err != nil {
114+
output += fmt.Sprintf("\n!== <%v> ==!", err)
115+
}
116+
if output != lastOutput {
117+
msgData := bytes.NewBuffer([]byte{})
118+
119+
switch outputMode {
120+
case "long":
121+
fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation)
122+
fmt.Fprintln(msgData, "== BEGIN OLD OUTPUT ==")
123+
fmt.Fprint(msgData, lastOutput)
124+
fmt.Fprintln(msgData, "== END OLD OUTPUT ==")
125+
fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==")
126+
fmt.Fprint(msgData, output)
127+
fmt.Fprintln(msgData, "== END NEW OUTPUT ==")
128+
case "default":
129+
fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation)
130+
fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==")
131+
fmt.Fprint(msgData, output)
132+
fmt.Fprintln(msgData, "== END NEW OUTPUT ==")
133+
case "short":
134+
fmt.Fprintf(msgData, output)
135+
}
136+
137+
msgString := msgData.String()
138+
fmt.Println(msgString)
139+
pushMessage(parsedURL, token, models.MessageExternal{
140+
Title: title,
141+
Message: msgString,
142+
Priority: priority,
143+
}, true)
144+
lastOutput = output
145+
}
146+
}
147+
148+
}

0 commit comments

Comments
 (0)