diff --git a/README.md b/README.md index 190e232..47dec90 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Gotify-CLI is a command line client for pushing messages to [gotify/server][goti * initialization wizard * piping support (`echo message | gotify push`) * simple to use +* watch and push script result changes (`gotify watch "curl http://example.com/api | jq '.data'"`) ## Alternatives @@ -61,28 +62,29 @@ $ gotify push "my message" $ echo my message | gotify push $ gotify push < somefile $ gotify push -t "my title" -p 10 "my message" +$ gotify watch "curl http://example.com/api | jq '.data'" ``` ## Help -**Uses version `v1.2.0`** +**Uses version `v2.1.0`** ```bash -$ gotify help NAME: Gotify - The official Gotify-CLI USAGE: - gotify [global options] command [command options] [arguments...] + cli [global options] command [command options] [arguments...] VERSION: - 1.2.0 + 2.1.0 COMMANDS: init Initializes the Gotify-CLI version, v Shows the version config Shows the config push, p Pushes a message + watch watch the result of a command and pushes output difference help, h Shows a list of commands or help for one command GLOBAL OPTIONS: @@ -90,6 +92,25 @@ GLOBAL OPTIONS: --version, -v print the version ``` +### Watch help + +``` +NAME: + cli watch - watch the result of a command and pushes output difference + +USAGE: + cli watch [command options] + +OPTIONS: + --interval value, -n value watch interval (sec) (default: 2) + --priority value, -p value Set the priority (default: 0) + --exec value, -x value Pass command to exec (default to "sh -c") + --title value, -t value Set the title (empty for command) + --token value Override the app token + --url value Override the Gotify URL + --output value, -o value Output verbosity (short|default|long) (default: "default") +``` + ### Push help ```bash diff --git a/cli.go b/cli.go index 2f3ea52..67d962f 100644 --- a/cli.go +++ b/cli.go @@ -29,6 +29,7 @@ func main() { command.Version(), command.Config(), command.Push(), + command.Watch(), } err := app.Run(os.Args) if err != nil { diff --git a/command/push.go b/command/push.go index 53d3e14..7d05d11 100644 --- a/command/push.go +++ b/command/push.go @@ -16,7 +16,6 @@ import ( "gopkg.in/urfave/cli.v1" ) - func Push() cli.Command { return cli.Command{ Name: "push", diff --git a/command/watch.go b/command/watch.go new file mode 100644 index 0000000..e2cd047 --- /dev/null +++ b/command/watch.go @@ -0,0 +1,148 @@ +package command + +import ( + "bytes" + "errors" + "fmt" + "net/url" + "os/exec" + "strings" + "time" + + "github.com/gotify/cli/v2/config" + "github.com/gotify/cli/v2/utils" + "github.com/gotify/go-api-client/v2/models" + "gopkg.in/urfave/cli.v1" +) + +func Watch() cli.Command { + return cli.Command{ + Name: "watch", + Usage: "watch the result of a command and pushes output difference", + ArgsUsage: "", + Flags: []cli.Flag{ + cli.Float64Flag{Name: "interval,n", Usage: "watch interval (sec)", Value: 2}, + cli.IntFlag{Name: "priority,p", Usage: "Set the priority"}, + cli.StringFlag{Name: "exec,x", Usage: "Pass command to exec", Value: "sh -c"}, + cli.StringFlag{Name: "title,t", Usage: "Set the title (empty for command)"}, + cli.StringFlag{Name: "token", Usage: "Override the app token"}, + cli.StringFlag{Name: "url", Usage: "Override the Gotify URL"}, + cli.StringFlag{Name: "output,o", Usage: "Output verbosity (short|default|long)", Value: "default"}, + }, + Action: doWatch, + } +} + +func doWatch(ctx *cli.Context) { + conf, confErr := config.ReadConfig(config.GetLocations()) + + cmdArgs := ctx.Args() + cmdStringNotation := strings.Join(cmdArgs, " ") + execArgs := strings.Split(ctx.String("exec"), " ") + cmdArgs = append(execArgs[1:], cmdStringNotation) + execCmd := execArgs[0] + + outputMode := ctx.String("output") + if !(outputMode == "default" || outputMode == "long" || outputMode == "short") { + utils.Exit1With("output mode should be short|default|long") + return + } + interval := ctx.Float64("interval") + priority := ctx.Int("priority") + title := ctx.String("title") + if title == "" { + title = cmdStringNotation + } + token := ctx.String("token") + if token == "" { + if confErr != nil { + utils.Exit1With("token is not configured, run 'gotify init'") + return + } + token = conf.Token + } + stringURL := ctx.String("url") + if stringURL == "" { + if confErr != nil { + utils.Exit1With("url is not configured, run 'gotify init'") + return + } + stringURL = conf.URL + } + parsedURL, err := url.Parse(stringURL) + if err != nil { + utils.Exit1With("invalid url", stringURL) + return + } + + watchInterval := time.Duration(interval*1000) * time.Millisecond + + evalCmdOutput := func() (string, error) { + cmd := exec.Command(execCmd, cmdArgs...) + timeOut := time.After(watchInterval) + outputBuf := bytes.NewBuffer([]byte{}) + cmd.Stdout = outputBuf + cmd.Stderr = outputBuf + err := cmd.Start() + if err != nil { + return "", fmt.Errorf("command failed to invoke: %v", err) + } + done := make(chan error) + go func() { + err := cmd.Wait() + if err != nil { + done <- fmt.Errorf("command failed to invoke: %v", err) + } + done <- nil + }() + select { + case err := <-done: + return outputBuf.String(), err + case <-timeOut: + cmd.Process.Kill() + return outputBuf.String(), errors.New("command timed out") + } + } + + lastOutput, err := evalCmdOutput() + if err != nil { + utils.Exit1With("first run failed", err) + } + for range time.NewTicker(watchInterval).C { + output, err := evalCmdOutput() + if err != nil { + output += fmt.Sprintf("\n!== <%v> ==!", err) + } + if output != lastOutput { + msgData := bytes.NewBuffer([]byte{}) + + switch outputMode { + case "long": + fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation) + fmt.Fprintln(msgData, "== BEGIN OLD OUTPUT ==") + fmt.Fprint(msgData, lastOutput) + fmt.Fprintln(msgData, "== END OLD OUTPUT ==") + fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==") + fmt.Fprint(msgData, output) + fmt.Fprintln(msgData, "== END NEW OUTPUT ==") + case "default": + fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation) + fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==") + fmt.Fprint(msgData, output) + fmt.Fprintln(msgData, "== END NEW OUTPUT ==") + case "short": + fmt.Fprintf(msgData, output) + } + + msgString := msgData.String() + fmt.Println(msgString) + pushMessage(parsedURL, token, models.MessageExternal{ + Title: title, + Message: msgString, + Priority: priority, + }, true) + lastOutput = output + } + } + +}