├── .gitignore ├── scripts ├── build.sh ├── install.sh ├── lint.sh └── format.sh ├── Makefile ├── .leaf.yml ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── go.mod ├── .golangci.yml ├── watcher.go ├── cmd ├── leaf │ └── main.go └── cmd.go ├── commander.go ├── filters.go ├── leaf.go ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "* Building as ./build/leaf" 6 | 7 | test -d build || mkdir build 8 | go build -o build/leaf cmd/leaf/main.go 9 | 10 | echo "+ Build complete!" 11 | 12 | exit 0 13 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "* Installing tools" 4 | 5 | # golangci-lint 6 | echo "* Installing golangci-lint" 7 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.22.2 8 | 9 | echo "+ Install complete!" 10 | 11 | exit 0 12 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | pkgs=$(go list ./... | grep -v vendor) 6 | 7 | echo "* Linting code for errors" 8 | 9 | # Vet first since golangci-lint returns unclear errors if packages don't build 10 | go vet ${pkgs} 11 | 12 | golangci-lint run 13 | 14 | echo "+ Your code is beautiful!" 15 | 16 | exit 0 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | @./scripts/build.sh 3 | 4 | format: 5 | @./scripts/format.sh 6 | 7 | help: 8 | @echo "Leaf Makefile: make []" 9 | @echo "Available commands:" 10 | @echo "build -- build leaf into ./build" 11 | @echo "format -- format code" 12 | @echo "help -- print this message" 13 | @echo "install -- install development tools" 14 | @echo "lint -- lint code for mistakes" 15 | 16 | install: 17 | @./scripts/install.sh 18 | 19 | lint: 20 | @./scripts/lint.sh 21 | 22 | .PHONY: build format help install lint 23 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pkgs=$(go list ./... | grep -v vendor) 4 | 5 | echo "* Formatting code" 6 | 7 | go vet ${pkgs} > /dev/null 2>&1 8 | 9 | vet_status="$?" 10 | 11 | if [ "$vet_status" -ne 0 ] 12 | then 13 | go fmt ${pkgs} 14 | echo "- Error while running golangci-lint formatters." 15 | echo " Run 'make lint' to fix errors." 16 | exit 1 17 | fi 18 | 19 | # Run --fix with exit code 0 and don't display output of command 20 | golangci-lint run --fix --issues-exit-code 0 > /dev/null 2>&1 21 | 22 | echo "+ Format complete!" 23 | 24 | exit 0 25 | -------------------------------------------------------------------------------- /.leaf.yml: -------------------------------------------------------------------------------- 1 | # Leaf configuration file. 2 | 3 | # Root directory to watch. 4 | # Defaults to current working directory. 5 | root: . 6 | 7 | # Exclude directories while watching. 8 | # If certain directories are not excluded, it might reach a 9 | # limitation where watcher doesn't start. 10 | exclude: 11 | - DEFAULTS # This includes the default ignored directories 12 | - build/ 13 | - scripts/ 14 | 15 | # Filters to apply on the watch. 16 | # Filters starting with '+' are includent and then with '-' 17 | # are excluded. This is not like exclude, these are still 18 | # being watched yet can be excluded from the execution. 19 | # These can include any regex supported by filepath.Match 20 | # method or even a directory. 21 | filters: 22 | - '+ go.mod' 23 | - '+ go.sum' 24 | - '+ *.go' 25 | - '+ cmd/' 26 | 27 | # Commands to be executed. These are run in the provided order. 28 | exec: 29 | - make format 30 | - make build 31 | 32 | # Stop the command chain when an error occurs 33 | exit_on_err: true 34 | 35 | # Delay after which commands are executed. 36 | delay: 1s 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vaibhav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ^1.14 19 | id: go 20 | - name: Check out code into the Go module directory 21 | uses: actions/checkout@v2 22 | - name: Build 23 | run: go build ./cmd/leaf 24 | 25 | lint: 26 | name: Lint 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Set up Go 1.x 30 | uses: actions/setup-go@v2 31 | with: 32 | go-version: ^1.14 33 | id: go 34 | - name: Check out code into the Go module directory 35 | uses: actions/checkout@v2 36 | - name: Install golangci-lint 37 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0 38 | - name: Lint 39 | run: $(go env GOPATH)/bin/golangci-lint run 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vrongmeal/leaf 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/golang/protobuf v1.3.2 // indirect 8 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 9 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 10 | github.com/kr/pretty v0.2.0 // indirect 11 | github.com/mattn/go-colorable v0.1.4 // indirect 12 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 13 | github.com/onsi/ginkgo v1.12.0 // indirect 14 | github.com/onsi/gomega v1.9.0 // indirect 15 | github.com/pelletier/go-toml v1.6.0 // indirect 16 | github.com/sirupsen/logrus v1.4.2 17 | github.com/spf13/afero v1.2.2 // indirect 18 | github.com/spf13/cast v1.3.1 // indirect 19 | github.com/spf13/cobra v0.0.6 20 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 21 | github.com/spf13/pflag v1.0.5 // indirect 22 | github.com/spf13/viper v1.6.2 23 | github.com/stretchr/testify v1.4.0 // indirect 24 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 25 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 // indirect 26 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 // indirect 27 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect 28 | golang.org/x/text v0.3.2 // indirect 29 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect 30 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 31 | gopkg.in/ini.v1 v1.52.0 // indirect 32 | gopkg.in/yaml.v2 v2.2.8 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-use-default: false 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - govet 8 | - errcheck 9 | - golint 10 | - staticcheck 11 | - gocritic 12 | - gosimple 13 | - gosec 14 | - interfacer 15 | - unconvert 16 | - ineffassign 17 | - goconst 18 | - gofmt 19 | - goimports 20 | - bodyclose 21 | - misspell 22 | - scopelint 23 | - lll 24 | - gocyclo 25 | - typecheck 26 | - whitespace 27 | 28 | linters-settings: 29 | govet: 30 | check-shadowing: true 31 | settings: 32 | printf: 33 | funcs: 34 | - (github.com/sirupsen/logrus).Infof 35 | - (github.com/sirupsen/logrus).Warnf 36 | - (github.com/sirupsen/logrus).Errorf 37 | - (github.com/sirupsen/logrus).Fatalf 38 | 39 | errcheck: 40 | check-type-assertions: true 41 | check-blank: true 42 | 43 | golint: 44 | min-confidence: 0.8 45 | 46 | gocritic: 47 | enabled-tags: 48 | - diagnostic 49 | - experimental 50 | - opinionated 51 | - performance 52 | - style 53 | disabled-checks: 54 | - dupImport # https://github.com/go-critic/go-critic/issues/845 55 | - ifElseChain 56 | - octalLiteral 57 | - whyNoLint 58 | - wrapperFunc 59 | 60 | goconst: 61 | min-len: 2 62 | min-occurrences: 4 63 | 64 | gofmt: 65 | simplify: true 66 | 67 | goimports: 68 | local-prefixes: github.com/vrongmeal/leaf 69 | 70 | misspell: 71 | locale: US 72 | ignore-words: [] 73 | 74 | lll: 75 | line-length: 140 76 | 77 | gocyclo: 78 | min-complexity: 20 79 | -------------------------------------------------------------------------------- /watcher.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/fsnotify/fsnotify" 11 | ) 12 | 13 | // WatchResult has the file changed or the error that occurred 14 | // during watching. 15 | type WatchResult struct { 16 | File string 17 | Err error 18 | } 19 | 20 | // Watcher watches a directory for changes and updates the 21 | // stream when a file change (valid by filters) is updated. 22 | type Watcher struct { 23 | root string 24 | paths []string 25 | exclude []string 26 | 27 | fc *FilterCollection 28 | notifier *fsnotify.Watcher 29 | 30 | res chan WatchResult 31 | } 32 | 33 | // NewWatcher returns a watcher from the given options. 34 | func NewWatcher(root string, exclude []string, fc *FilterCollection) (*Watcher, error) { 35 | w := &Watcher{ 36 | fc: fc, 37 | res: make(chan WatchResult), 38 | } 39 | 40 | isdir, err := isDir(root) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | if !isdir { 46 | return nil, fmt.Errorf( 47 | "path '%s' is not a directory", root) 48 | } 49 | 50 | w.root = filepath.Clean(root) 51 | w.paths, err = getAllDirs(w.root) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | w.exclude = []string{} 57 | for _, path := range exclude { 58 | var absPath string 59 | 60 | if _, err = isDir(path); err != nil { 61 | continue 62 | } 63 | 64 | absPath, err = filepath.Abs(path) 65 | if err != nil { 66 | continue 67 | } 68 | 69 | w.exclude = append(w.exclude, absPath) 70 | } 71 | 72 | w.notifier, err = fsnotify.NewWatcher() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | for _, f := range w.paths { 78 | exclude := false 79 | for _, e := range w.exclude { 80 | if strings.HasPrefix(f, e) { 81 | exclude = true 82 | break 83 | } 84 | } 85 | 86 | if exclude { 87 | continue 88 | } 89 | 90 | if err := w.notifier.Add(f); err != nil { 91 | return nil, err 92 | } 93 | } 94 | 95 | return w, nil 96 | } 97 | 98 | // Watch executes the watching of files. Exits on cancellation 99 | // of the context. 100 | func (w *Watcher) Watch(ctx context.Context) <-chan WatchResult { 101 | go w.startWatcher(ctx) 102 | return w.res 103 | } 104 | 105 | // startWatcher starts the fs.Notifier and watches for changes 106 | // in files in the root directory. 107 | func (w *Watcher) startWatcher(ctx context.Context) { 108 | defer w.notifier.Close() // nolint:errcheck 109 | for { 110 | select { 111 | case event := <-w.notifier.Events: 112 | if event.Op == fsnotify.Write { 113 | file := event.Name 114 | if w.fc.ShouldHandlePath(file) { 115 | w.res <- WatchResult{File: file} 116 | } 117 | } 118 | 119 | case err := <-w.notifier.Errors: 120 | if err != nil { 121 | w.res <- WatchResult{Err: err} 122 | } 123 | 124 | case <-ctx.Done(): 125 | close(w.res) 126 | return 127 | } 128 | } 129 | } 130 | 131 | // getAllDirs gets all the directories (including the root) 132 | // inside the given root directory. 133 | func getAllDirs(root string) ([]string, error) { 134 | paths := []string{} 135 | 136 | walkFn := func(path string, info os.FileInfo, err error) error { 137 | if err != nil { 138 | return err 139 | } 140 | 141 | if info.IsDir() { 142 | absPath, err := filepath.Abs(path) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | paths = append(paths, absPath) 148 | } 149 | 150 | return nil 151 | } 152 | 153 | err := filepath.Walk(root, walkFn) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | return paths, nil 159 | } 160 | -------------------------------------------------------------------------------- /cmd/leaf/main.go: -------------------------------------------------------------------------------- 1 | // Command leaf watches for changes in the working directory 2 | // and runs the specified set of commands whenever a file 3 | // updates. A set of filters can be applied to the watch and 4 | // directories can be excluded. 5 | // 6 | // 7 | // Usage: 8 | // 9 | // ❯ leaf -x 'make build' -x 'make run' 10 | // 11 | // The above command runs `make build` and `make run` commands 12 | // (in order). 13 | // 14 | // The CLI can be used as described by the help message: 15 | // 16 | // ❯ leaf help 17 | // 18 | // Command leaf watches for changes in the working directory and 19 | // runs the specified set of commands whenever a file updates. 20 | // A set of filters can be applied to the watch and directories 21 | // can be excluded. 22 | // 23 | // Usage: 24 | // leaf [flags] 25 | // leaf [command] 26 | // 27 | // Available Commands: 28 | // help Help about any command 29 | // version prints leaf version 30 | // 31 | // Flags: 32 | // -c, --config string config path for the configuration file (default "/.leaf.yml") 33 | // --debug run in development (debug) environment 34 | // -d, --delay duration delay after which commands are run on file change (default 500ms) 35 | // -e, --exclude strings paths to exclude from watching (default [.git/,node_modules/,vendor/,venv/]) 36 | // -x, --exec strings exec commands on file change 37 | // -z, --exit-on-err exit chain of commands on error 38 | // -f, --filters strings filters to apply to watch 39 | // -h, --help help for leaf 40 | // -o, --once run once and exit (no reload) 41 | // -r, --root string root directory to watch (default "") 42 | // 43 | // Use "leaf [command] --help" for more information about a command. 44 | // 45 | // In order to configure using a configuration file, create a 46 | // YAML or TOML or even a JSON file with the following structure 47 | // and pass it using the `-c` or `--config` flag. By default 48 | // a file named `.leaf.yml` in your working directory is taken 49 | // if no configuration file is found. 50 | // 51 | // # Leaf configuration file. 52 | // 53 | // # Root directory to watch. 54 | // # Defaults to current working directory. 55 | // root: . 56 | // 57 | // # Exclude directories while watching. 58 | // # If certain directories are not excluded, it might reach a 59 | // # limitation where watcher doesn't start. 60 | // exclude: 61 | // - DEFAULTS # This includes the default ignored directories 62 | // - build/ 63 | // - scripts/ 64 | // 65 | // # Filters to apply on the watch. 66 | // # Filters starting with '+' are includent and then with '-' 67 | // # are excluded. This is not like exclude, these are still 68 | // # being watched yet can be excluded from the execution. 69 | // # These can include any regex supported by filepath.Match 70 | // # method or even a directory. 71 | // filters: 72 | // - '+ go.mod' 73 | // - '+ go.sum' 74 | // - '+ *.go' 75 | // - '+ cmd/' 76 | // 77 | // # Commands to be executed. These are run in the provided order. 78 | // exec: 79 | // - make format 80 | // - make build 81 | // 82 | // # Stop the command chain when an error occurs 83 | // exit_on_err: true 84 | // 85 | // # Delay after which commands are executed. 86 | // delay: 1s 87 | // 88 | // The above config file is suitable to use with the current 89 | // project itself. It can also be translated into a command 90 | // as such: 91 | // 92 | // ❯ leaf -z -x 'make format' -x 'make build' -d '1s' \ 93 | // -e 'DEFAULTS' -e 'build' -e 'scripts' \ 94 | // -f '+ go.*' -f '+ *.go' -f '+ cmd/' 95 | // 96 | package main 97 | 98 | import "github.com/vrongmeal/leaf/cmd" 99 | 100 | func main() { 101 | cmd.Execute() 102 | } 103 | -------------------------------------------------------------------------------- /commander.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | 10 | "github.com/kballard/go-shellquote" 11 | ) 12 | 13 | var errEmptyCmd = fmt.Errorf("empty command") 14 | 15 | // Commander has a set of commands that run in order 16 | // and exit when the context is canceled. 17 | type Commander struct { 18 | Commands []string 19 | 20 | OnStart func(*Command) 21 | OnError func(error) 22 | OnExit func() 23 | 24 | ExitOnError bool 25 | 26 | done chan bool 27 | } 28 | 29 | // NewCommander creates a new commander. 30 | func NewCommander(commander Commander) *Commander { 31 | return &Commander{ 32 | Commands: commander.Commands, 33 | OnStart: commander.OnStart, 34 | OnError: commander.OnError, 35 | OnExit: commander.OnExit, 36 | ExitOnError: commander.ExitOnError, 37 | done: make(chan bool, 1), 38 | } 39 | } 40 | 41 | // Done signals that the commander is done running the commands. 42 | func (c *Commander) Done() <-chan bool { 43 | return c.done 44 | } 45 | 46 | // Run executes the commands in order. It stops when the 47 | // context is canceled. 48 | func (c *Commander) Run(ctx context.Context) { 49 | defer func() { 50 | // signal done when running commands is complete 51 | // or the function exits, eitherway. 52 | c.done <- true 53 | c.OnExit() 54 | }() 55 | 56 | for _, command := range c.Commands { 57 | cmd, err := NewCommand(command) 58 | if err != nil { 59 | c.OnError(err) 60 | return 61 | } 62 | 63 | select { 64 | case <-ctx.Done(): 65 | return 66 | 67 | default: 68 | if cmd == nil { 69 | continue 70 | } 71 | 72 | if c.OnStart != nil { 73 | c.OnStart(cmd) 74 | } 75 | 76 | if err := cmd.Execute(ctx); err != nil { 77 | if c.OnError != nil { 78 | c.OnError(err) 79 | } 80 | if c.ExitOnError { 81 | return 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | // Command is an external command that can be executed. 89 | type Command struct { 90 | Name string 91 | Args []string 92 | 93 | str string 94 | } 95 | 96 | // String returns the command in a human-readable format. 97 | func (c *Command) String() string { 98 | return c.str 99 | } 100 | 101 | // NewCommand creates a new command from the string. 102 | func NewCommand(cmd string) (*Command, error) { 103 | parsedCmd, err := shellquote.Split(cmd) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | if len(parsedCmd) == 0 { 109 | return nil, errEmptyCmd 110 | } 111 | 112 | name := parsedCmd[0] 113 | var args []string 114 | if len(parsedCmd) > 1 { 115 | args = parsedCmd[1:] 116 | } 117 | 118 | return &Command{ 119 | Name: name, 120 | Args: args, 121 | str: shellquote.Join(parsedCmd...), 122 | }, nil 123 | } 124 | 125 | // Execute runs the commands and exits elegantly when the 126 | // context is canceled. 127 | // 128 | // This doesn't use the exec.CommandContext because we just 129 | // don't want to kill the parent process but all the child 130 | // processes too. 131 | func (c *Command) Execute(ctx context.Context) error { 132 | stream := make(chan error) 133 | 134 | cmd := exec.Command(c.Name, c.Args...) // nolint:gosec 135 | cmd.Stdout = os.Stdout 136 | cmd.Stderr = os.Stderr 137 | cmd.Stdin = os.Stdin 138 | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 139 | 140 | if err := cmd.Start(); err != nil { 141 | return err 142 | } 143 | 144 | go func(ex *exec.Cmd, err chan<- error) { 145 | err <- ex.Wait() 146 | }(cmd, stream) 147 | 148 | select { 149 | case <-ctx.Done(): 150 | // Elegantly close the parent along-with the children. 151 | err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | return nil 157 | 158 | case err := <-stream: 159 | return err 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /filters.go: -------------------------------------------------------------------------------- 1 | package leaf 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // Filter can be used to Filter out watch results. 10 | type Filter struct { 11 | Include bool // whether to include pattern 12 | Pattern string 13 | } 14 | 15 | // NewFilter creates a filter from the pattern string. The 16 | // pattern either starts with '+' or '-' to include or 17 | // exclude the directory from results. 18 | func NewFilter(pattern string) (Filter, error) { 19 | f := Filter{} 20 | var err error 21 | 22 | cleanedPattern := strings.Trim(pattern, " ") 23 | if len(cleanedPattern) < 2 { 24 | return f, fmt.Errorf( 25 | "effective pattern '%s' invalid", cleanedPattern) 26 | } 27 | 28 | toInclude := cleanedPattern[0] 29 | if toInclude == '+' { 30 | f.Include = true 31 | } else if toInclude == '-' { 32 | f.Include = false 33 | } else { 34 | return f, fmt.Errorf( 35 | "should have first character as '+' or '-'") 36 | } 37 | 38 | onlyPath := strings.Trim(cleanedPattern[1:], " ") 39 | f.Pattern, err = filepath.Abs(onlyPath) 40 | if err != nil { 41 | return f, fmt.Errorf( 42 | "error making path absolute: %v", err) 43 | } 44 | 45 | return f, nil 46 | } 47 | 48 | // A FilterCollection contains a bunch of includes and excludes. 49 | type FilterCollection struct { 50 | Includes []string 51 | Excludes []string 52 | 53 | match FilterMatchFunc 54 | handle FilterHandleFunc 55 | } 56 | 57 | // NewFilterCollection creates a filter collection from a bunch 58 | // of filter patterns. 59 | func NewFilterCollection(filters []Filter, mf FilterMatchFunc, hf FilterHandleFunc) *FilterCollection { 60 | collection := &FilterCollection{ 61 | Includes: []string{}, 62 | Excludes: []string{}, 63 | match: mf, 64 | handle: hf, 65 | } 66 | 67 | if len(filters) == 0 { 68 | return collection 69 | } 70 | 71 | for _, f := range filters { 72 | if f.Include { 73 | collection.Includes = append(collection.Includes, f.Pattern) 74 | } else { 75 | collection.Excludes = append(collection.Excludes, f.Pattern) 76 | } 77 | } 78 | 79 | return collection 80 | } 81 | 82 | // NewFCFromPatterns creates a filter collection from a list of 83 | // string format filters, like `+ /path/to/some/dir`. 84 | func NewFCFromPatterns(patterns []string, mf FilterMatchFunc, hf FilterHandleFunc) (*FilterCollection, error) { 85 | filters := []Filter{} 86 | 87 | for _, p := range patterns { 88 | f, err := NewFilter(p) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | filters = append(filters, f) 94 | } 95 | 96 | return NewFilterCollection(filters, mf, hf), nil 97 | } 98 | 99 | // FilterMatchFunc compares the pattern with the path of 100 | // the file changed and returns true if the path resembles 101 | // the given pattern. 102 | type FilterMatchFunc func(pattern, path string) bool 103 | 104 | // StandardFilterMatcher matches the pattern with the path 105 | // and returns true if the path either starts with 106 | // (in absolute terms) or matches like the path regex. 107 | func StandardFilterMatcher(pattern, path string) bool { 108 | matched, err := filepath.Match(pattern, path) 109 | if err != nil { 110 | return false 111 | } 112 | 113 | if matched { 114 | return true 115 | } 116 | 117 | isDir, err := isDir(pattern) 118 | if err != nil || !isDir { 119 | return false 120 | } 121 | 122 | if len(path) < len(pattern) { 123 | return false 124 | } 125 | 126 | if path[:len(pattern)] == pattern { 127 | return true 128 | } 129 | 130 | return false 131 | } 132 | 133 | // HasInclude tells if the collection matches the path with 134 | // one of its includes. 135 | func (fc *FilterCollection) HasInclude(path string) bool { 136 | cleanedPath := filepath.Clean(path) 137 | 138 | for _, pattern := range fc.Includes { 139 | if fc.match(pattern, cleanedPath) { 140 | return true 141 | } 142 | } 143 | 144 | return false 145 | } 146 | 147 | // HasExclude tells if the collection matches the path with 148 | // one of its excludes. 149 | func (fc *FilterCollection) HasExclude(path string) bool { 150 | cleanedPath := filepath.Clean(path) 151 | 152 | for _, pattern := range fc.Excludes { 153 | if fc.match(pattern, cleanedPath) { 154 | return true 155 | } 156 | } 157 | 158 | return false 159 | } 160 | 161 | // ShouldHandlePath returns the result of the path handler 162 | // for the filter collection. 163 | func (fc *FilterCollection) ShouldHandlePath(path string) bool { 164 | handlerFunc := fc.handle 165 | return handlerFunc(fc, path) 166 | } 167 | 168 | // FilterHandleFunc is a function that checks if for the filter 169 | // collection, should the path be handled or not, i.e., should 170 | // the notifier tick for change in path or not. 171 | type FilterHandleFunc func(fc *FilterCollection, path string) bool 172 | 173 | // StandardFilterHandler returns true if the path should be included 174 | // and returns false if path should not be included in result. 175 | func StandardFilterHandler(fc *FilterCollection, path string) bool { 176 | handle := false 177 | 178 | // If there are no includes, path should be handled unless 179 | // it is in the excludes. 180 | if len(fc.Includes) == 0 || fc.HasInclude(path) { 181 | handle = true 182 | } 183 | 184 | if fc.HasExclude(path) { 185 | handle = false 186 | } 187 | 188 | return handle 189 | } 190 | -------------------------------------------------------------------------------- /leaf.go: -------------------------------------------------------------------------------- 1 | // Package leaf provides with utilities to create the leaf 2 | // CLI tool. It includes watcher, filters and commander which 3 | // watch files for changes, filter out required results and 4 | // execute external commands respectively. 5 | // 6 | // The package comes with utilities that can aid in creating 7 | // a reloader with a simple go program. 8 | // 9 | // Let's look at an example where the watcher watches the `src/` 10 | // directory for changes and for any changes builds the project. 11 | // 12 | // package main 13 | // 14 | // import ( 15 | // "log" 16 | // "os" 17 | // "path/filepath" 18 | // 19 | // "github.com/vrongmeal/leaf" 20 | // ) 21 | // 22 | // func main() { 23 | // // Use a context that cancels when program is interrupted. 24 | // ctx := leaf.NewCmdContext(func(os.Signal) { 25 | // log.Println("Shutting down.") 26 | // }) 27 | // 28 | // cwd, err := os.Getwd() 29 | // if err != nil { 30 | // log.Fatalln(err) 31 | // } 32 | // 33 | // // Root is /src 34 | // root := filepath.Join(cwd, "src") 35 | // 36 | // // Exclude "src/index.html" from results. 37 | // filters := []leaf.Filter{ 38 | // {Include: false, Pattern: "src/index.html"}, 39 | // } 40 | // 41 | // filterCollection := leaf.NewFilterCollection( 42 | // filters, 43 | // // Matches directory or filepath.Match expressions 44 | // leaf.StandardFilterMatcher, 45 | // // Definitely excludes and shows only includes (if any) 46 | // leaf.StandardFilterHandler) 47 | // 48 | // watcher, err := leaf.NewWatcher( 49 | // root, 50 | // // Standard paths to exclude, like vendor, .git, 51 | // // node_modules, venv etc. 52 | // leaf.DefaultExcludePaths, 53 | // filterCollection) 54 | // if err != nil { 55 | // log.Fatalln(err) 56 | // } 57 | // 58 | // cmd, err := leaf.NewCommand("npm run build") 59 | // if err != nil { 60 | // log.Fatalln(err) 61 | // } 62 | // 63 | // log.Printf("Watching: %s\n", root) 64 | // 65 | // for change := range watcher.Watch(ctx) { 66 | // if change.Err != nil { 67 | // log.Printf("ERROR: %v", change.Err) 68 | // continue 69 | // } 70 | // // If no error run the command 71 | // log.Printf("Running: %s\n", cmd.String()) 72 | // cmd.Execute(ctx) 73 | // } 74 | // } 75 | // 76 | package leaf 77 | 78 | import ( 79 | "context" 80 | "fmt" 81 | "os" 82 | "os/signal" 83 | "path/filepath" 84 | "runtime/debug" 85 | "time" 86 | 87 | "github.com/sirupsen/logrus" 88 | ) 89 | 90 | var ( 91 | // DefaultExcludePathsKeyword is used to include all 92 | // default excludes. 93 | DefaultExcludePathsKeyword = "DEFAULTS" 94 | 95 | // CWD is the current working directory or ".". 96 | CWD string 97 | 98 | // DefaultConfPath is the default path for app config. 99 | DefaultConfPath string 100 | 101 | // DefaultExcludePaths are the paths that should be 102 | // generally excluded while watching a project. 103 | DefaultExcludePaths = []string{ 104 | ".git/", 105 | "node_modules/", 106 | "vendor/", 107 | "venv/", 108 | } 109 | // ImportPath is the import path for leaf package. 110 | ImportPath = "github.com/vrongmeal/leaf" 111 | ) 112 | 113 | func init() { 114 | var err error 115 | CWD, err = os.Getwd() 116 | if err != nil { 117 | logrus.Fatalln(err) 118 | } 119 | 120 | DefaultConfPath = filepath.Join(CWD, ".leaf.yml") 121 | } 122 | 123 | // Config represents the conf file for the runner. 124 | type Config struct { 125 | // Root directory to watch. 126 | Root string `mapstructure:"root"` 127 | 128 | // Exclude these directories from watch. 129 | Exclude []string `mapstructure:"exclude"` 130 | 131 | // Filters to apply to the watch. 132 | Filters []string `mapstructure:"filters"` 133 | 134 | // Exec these commads after changes detected. 135 | Exec []string `mapstructure:"exec"` 136 | 137 | // ExitOnErr breaks the chain of command if any command returnns an error. 138 | ExitOnErr bool `mapstructure:"exit_on_err"` 139 | 140 | // Delay after which commands should be executed. 141 | Delay time.Duration `mapstructure:"delay"` 142 | } 143 | 144 | // NewCmdContext returns a context which cancels on an OS 145 | // interrupt, i.e., cancels when process is killed. 146 | func NewCmdContext(onInterrupt func(os.Signal)) context.Context { 147 | interrupt := make(chan os.Signal) 148 | signal.Notify(interrupt, os.Interrupt) 149 | ctx, cancel := context.WithCancel(context.Background()) 150 | 151 | go func(onSignal func(os.Signal), cancelProcess context.CancelFunc) { 152 | sig := <-interrupt 153 | onSignal(sig) 154 | cancelProcess() 155 | }(onInterrupt, cancel) 156 | 157 | return ctx 158 | } 159 | 160 | // GoModuleInfo returns the go module information which 161 | // includes the build info (version etc.). 162 | func GoModuleInfo() (*debug.Module, error) { 163 | buildInfo, ok := debug.ReadBuildInfo() 164 | if !ok { 165 | return nil, fmt.Errorf("unable to fetch build info") 166 | } 167 | 168 | for _, dep := range buildInfo.Deps { 169 | if dep.Path == ImportPath { 170 | return dep, nil 171 | } 172 | } 173 | 174 | return &buildInfo.Main, nil 175 | } 176 | 177 | // *** some helper functions *** 178 | 179 | // isDir checks if the given path is a directory or not. 180 | // Returns an error when path is invalid. 181 | func isDir(root string) (bool, error) { 182 | info, err := os.Stat(root) 183 | if err != nil { 184 | if os.IsNotExist(err) { 185 | return false, fmt.Errorf( 186 | "filepath does not exist") 187 | } 188 | 189 | return false, err 190 | } 191 | 192 | if info.IsDir() { 193 | return true, nil 194 | } 195 | 196 | return false, nil 197 | } 198 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # leaf 2 | 3 | > General purpose reloader for all projects. 4 | 5 | ![Continuous Integration](https://github.com/vrongmeal/leaf/workflows/Continuous%20Integration/badge.svg) 6 | 7 | Command leaf watches for changes in the working directory 8 | and runs the specified set of commands whenever a file 9 | updates. A set of filters can be applied to the watch and 10 | directories can be excluded. 11 | 12 | ## Contents 13 | 14 | 1. [Installation](#installation) 15 | 1. [Using `go get`](#using-go-get) 16 | 1. [Manual](#manual) 17 | 1. [Usage](#usage) 18 | 1. [Command line help](#command-line-help) 19 | 1. [Configuration file](#configuration-file) 20 | 1. [Custom reloader](#custom-reloader) 21 | 22 | ## Installation 23 | 24 | ### Using `go get` 25 | 26 | The following command will download and build Leaf in your 27 | `$GOPATH/bin`. 28 | 29 | ``` 30 | ❯ go get -u github.com/vrongmeal/leaf/cmd/leaf 31 | ``` 32 | 33 | ### Manual 34 | 35 | 1. Clone the repository and `cd` into it. 36 | 1. Run `make build` to build the leaf as `build/leaf`. 37 | 1. Move the binary somewhere in your `$PATH`. 38 | 39 | ## Usage 40 | 41 | ``` 42 | ❯ leaf -x 'make build' -x 'make run' 43 | ``` 44 | 45 | The above command runs `make build` and `make run` commands 46 | (in order). 47 | 48 | ### Command line help 49 | 50 | The CLI can be used as described by the help message: 51 | 52 | ``` 53 | ❯ leaf help 54 | 55 | Command leaf watches for changes in the working directory and 56 | runs the specified set of commands whenever a file updates. 57 | A set of filters can be applied to the watch and directories 58 | can be excluded. 59 | 60 | Usage: 61 | leaf [flags] 62 | leaf [command] 63 | 64 | Available Commands: 65 | help Help about any command 66 | version prints leaf version 67 | 68 | Flags: 69 | -c, --config string config path for the configuration file (default "/.leaf.yml") 70 | --debug run in development (debug) environment 71 | -d, --delay duration delay after which commands are run on file change (default 500ms) 72 | -e, --exclude strings paths to exclude from watching (default [.git/,node_modules/,vendor/,venv/]) 73 | -x, --exec strings exec commands on file change 74 | -z, --exit-on-err exit chain of commands on error 75 | -f, --filters strings filters to apply to watch 76 | -h, --help help for leaf 77 | -o, --once run once and exit (no reload) 78 | -r, --root string root directory to watch (default "") 79 | 80 | Use "leaf [command] --help" for more information about a command. 81 | ``` 82 | 83 | ### Configuration file 84 | 85 | In order to configure using a configuration file, create a 86 | YAML or TOML or even a JSON file with the following structure 87 | and pass it using the `-c` or `--config` flag. By default 88 | a file named `.leaf.yml` in your working directory is taken 89 | if no configuration file is found. 90 | 91 | ```yaml 92 | # Leaf configuration file. 93 | 94 | # Root directory to watch. 95 | # Defaults to current working directory. 96 | root: . 97 | 98 | # Exclude directories while watching. 99 | # If certain directories are not excluded, it might reach a 100 | # limitation where watcher doesn't start. 101 | exclude: 102 | - DEFAULTS # This includes the default ignored directories 103 | - build/ 104 | - scripts/ 105 | 106 | # Filters to apply on the watch. 107 | # Filters starting with '+' are includent and then with '-' 108 | # are excluded. This is not like exclude, these are still 109 | # being watched yet can be excluded from the execution. 110 | # These can include any regex supported by filepath.Match 111 | # method or even a directory. 112 | filters: 113 | - '+ go.mod' 114 | - '+ go.sum' 115 | - '+ *.go' 116 | - '+ cmd/' 117 | 118 | # Commands to be executed. These are run in the provided order. 119 | exec: 120 | - make format 121 | - make build 122 | 123 | # Stop the command chain when an error occurs 124 | exit_on_err: true 125 | 126 | # Delay after which commands are executed. 127 | delay: 1s 128 | ``` 129 | 130 | The above config file is suitable to use with the current 131 | project itself. It can also be translated into a command 132 | as such: 133 | 134 | ``` 135 | ❯ leaf -z -x 'make format' -x 'make build' -d '1s' \ 136 | -e 'DEFAULTS' -e 'build' -e 'scripts' \ 137 | -f '+ go.*' -f '+ *.go' -f '+ cmd/' 138 | ``` 139 | 140 | ## Custom reloader 141 | 142 | The package [github.com/vrongmeal/leaf](https://pkg.go.dev/github.com/vrongmeal/leaf) 143 | comes with utilities that can aid in creating a reloader 144 | with a simple go program. 145 | 146 | Let's look at an example where the watcher watches the `src/` 147 | directory for changes and for any changes builds the project. 148 | 149 | ```go 150 | package main 151 | 152 | import ( 153 | "fmt" 154 | "log" 155 | "os" 156 | "path/filepath" 157 | 158 | "github.com/vrongmeal/leaf" 159 | ) 160 | 161 | func main() { 162 | // Use a context that cancels when program is interrupted. 163 | ctx := leaf.NewCmdContext(func(os.Signal) { 164 | log.Println("Shutting down.") 165 | }) 166 | 167 | cwd, err := os.Getwd() 168 | if err != nil { 169 | log.Fatalln(err) 170 | } 171 | 172 | // Root is /src 173 | root := filepath.Join(cwd, "src") 174 | 175 | // Exclude "src/index.html" from results. 176 | filters := []leaf.Filter{ 177 | {Include: false, Pattern: "src/index.html"}, 178 | } 179 | 180 | filterCollection := leaf.NewFilterCollection( 181 | filters, 182 | // Matches directory or filepath.Match expressions 183 | leaf.StandardFilterMatcher, 184 | // Definitely excludes and shows only includes (if any) 185 | leaf.StandardFilterHandler) 186 | 187 | watcher, err := leaf.NewWatcher( 188 | root, 189 | // Standard paths to exclude, like vendor, .git, 190 | // node_modules, venv etc. 191 | leaf.DefaultExcludePaths, 192 | filterCollection) 193 | if err != nil { 194 | log.Fatalln(err) 195 | } 196 | 197 | cmd, err := leaf.NewCommand("npm run build") 198 | if err != nil { 199 | log.Fatalln(err) 200 | } 201 | 202 | log.Printf("Watching: %s\n", root) 203 | 204 | for change := range watcher.Watch(ctx) { 205 | if change.Err != nil { 206 | log.Printf("ERROR: %v", change.Err) 207 | continue 208 | } 209 | // If no error run the command 210 | fmt.Printf("Running: %s\n", cmd.String()) 211 | cmd.Execute(ctx) 212 | } 213 | } 214 | ``` 215 | 216 | --- 217 | 218 | Made with **khoon**, **paseena** and **love** `:-)` by 219 | 220 | Vaibhav ([vrongmeal](https://vrongmeal.github.io)) 221 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Package cmd implements the command-line interface for the 2 | // leaf command. It contains commands and their flags. 3 | package cmd 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "os" 9 | "time" 10 | 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | 15 | "github.com/vrongmeal/leaf" 16 | 17 | lpf "github.com/x-cray/logrus-prefixed-formatter" 18 | ) 19 | 20 | var ( 21 | confPath string 22 | debugEnv bool 23 | once bool 24 | exitOnErr bool 25 | 26 | conf leaf.Config 27 | ) 28 | 29 | var rootCmd = &cobra.Command{ 30 | Use: "leaf", 31 | Short: "general purpose hot-reloader for all projects", 32 | Long: ` 33 | Command leaf watches for changes in the working directory and 34 | runs the specified set of commands whenever a file updates. 35 | A set of filters can be applied to the watch and directories 36 | can be excluded.`, 37 | 38 | PersistentPreRun: func(*cobra.Command, []string) { 39 | // Logger is initialized depending upon the debug 40 | // flag and hence is not in the `init` function. 41 | initialiseLogger() 42 | }, 43 | 44 | PreRun: func(*cobra.Command, []string) { 45 | ferr, rerr := setupConfig() 46 | if rerr != nil { 47 | log.Fatalln(rerr) 48 | } else if ferr != nil { 49 | log.Warnf("config file not read: %v", ferr) 50 | } 51 | }, 52 | 53 | Run: func(*cobra.Command, []string) { 54 | log.Infof("watching '%s'", conf.Root) 55 | 56 | if err := runEngine(&conf); err != nil { 57 | log.Fatalln(err) 58 | } 59 | }, 60 | } 61 | 62 | var versionCmd = &cobra.Command{ 63 | Use: "version", 64 | Short: "prints leaf version", 65 | Long: ` 66 | Prints the build information for the leaf commnd line.`, 67 | 68 | Run: func(*cobra.Command, []string) { 69 | goModInfo, err := leaf.GoModuleInfo() 70 | if err != nil { 71 | log.Fatalf("error getting version: %v", err) 72 | } 73 | 74 | fmt.Printf("leaf version %s\n", goModInfo.Version) 75 | }, 76 | } 77 | 78 | func init() { 79 | initializeFlags() 80 | 81 | if err := bindFlagsToConfig(); err != nil { 82 | log.Fatalf("cannot bind flags with config: %v", err) 83 | } 84 | 85 | rootCmd.AddCommand(versionCmd) 86 | } 87 | 88 | // Execute starts the command line tool. 89 | func Execute() { 90 | if err := rootCmd.Execute(); err != nil { 91 | log.Fatalln(err) 92 | } 93 | } 94 | 95 | // initialiseLogger sets up the logger configuration. 96 | func initialiseLogger() { 97 | if debugEnv { 98 | log.SetLevel(log.DebugLevel) 99 | } else { 100 | log.SetLevel(log.InfoLevel) 101 | } 102 | 103 | log.SetFormatter(&lpf.TextFormatter{ 104 | DisableTimestamp: true, 105 | }) 106 | } 107 | 108 | // initializeFlags sets the initializes flags for the commands. 109 | func initializeFlags() { 110 | rootCmd.PersistentFlags().BoolVar( 111 | &debugEnv, "debug", false, 112 | "run in development (debug) environment") 113 | 114 | rootCmd.PersistentFlags().StringVarP( 115 | &confPath, "config", "c", leaf.DefaultConfPath, 116 | "config path for the configuration file") 117 | 118 | rootCmd.PersistentFlags().BoolVarP( 119 | &once, "once", "o", false, 120 | "run once and exit (no reload)") 121 | 122 | rootCmd.Flags().StringP( 123 | "root", "r", leaf.CWD, 124 | "root directory to watch") 125 | 126 | rootCmd.Flags().StringSliceP( 127 | "exclude", "e", leaf.DefaultExcludePaths, 128 | "paths to exclude from watching") 129 | 130 | rootCmd.Flags().StringSliceP( 131 | "filters", "f", []string{}, 132 | "filters to apply to watch") 133 | 134 | rootCmd.Flags().StringSliceP( 135 | "exec", "x", []string{}, 136 | "exec commands on file change") 137 | 138 | rootCmd.Flags().BoolP( 139 | "exit-on-err", "z", false, 140 | "exit chain of commands on error") 141 | 142 | rootCmd.Flags().DurationP( 143 | "delay", "d", 500*time.Millisecond, 144 | "delay after which commands are run on file change") 145 | } 146 | 147 | // bindFlagsToConfig binds the flags with viper config file. 148 | func bindFlagsToConfig() error { 149 | keyFlagMap := map[string]string{ 150 | "root": "root", 151 | "exclude": "exclude", 152 | "filters": "filters", 153 | "exec": "exec", 154 | "exit_on_err": "exit-on-err", 155 | "delay": "delay", 156 | } 157 | 158 | for key, flag := range keyFlagMap { 159 | err := viper.BindPFlag(key, rootCmd.Flags().Lookup(flag)) 160 | if err != nil { 161 | return err 162 | } 163 | } 164 | 165 | return nil 166 | } 167 | 168 | // setupConfig reads and unmarshals the config file into 169 | // the `conf` variable. 170 | func setupConfig() (fileErr, readErr error) { 171 | if confPath != "" { 172 | viper.SetConfigFile(confPath) 173 | } else { 174 | viper.SetConfigFile(leaf.DefaultConfPath) 175 | } 176 | 177 | viper.AutomaticEnv() 178 | 179 | var confFileErr error 180 | if err := viper.ReadInConfig(); err != nil { 181 | // Even if no config is provided we still unmarshal 182 | // the config because are flags are bound with conf. 183 | confFileErr = err 184 | } 185 | 186 | if err := viper.Unmarshal(&conf); err != nil { 187 | return confFileErr, fmt.Errorf("unable to read config: %v", err) 188 | } 189 | 190 | // By default the defaults are included in the excludes. 191 | // If the excludes are specified explicitly, defaults will 192 | // only be included if the `DEFAULTS` keyword is in the 193 | // excluded paths. 194 | if len(conf.Exclude) == 0 { 195 | conf.Exclude = leaf.DefaultExcludePaths 196 | } else { 197 | finalExcludes := []string{} 198 | for _, e := range conf.Exclude { 199 | if e == leaf.DefaultExcludePathsKeyword { 200 | finalExcludes = append(finalExcludes, 201 | leaf.DefaultExcludePaths...) 202 | continue 203 | } 204 | 205 | finalExcludes = append(finalExcludes, e) 206 | } 207 | 208 | conf.Exclude = finalExcludes 209 | } 210 | 211 | return confFileErr, nil 212 | } 213 | 214 | // runEngine runs the watcher and executes the commands from 215 | // the config on file change. 216 | func runEngine(conf *leaf.Config) error { 217 | ctx := leaf.NewCmdContext(func(s os.Signal) { 218 | log.Infof("closing: signal received: %s", s.String()) 219 | }) 220 | 221 | commander := leaf.NewCommander(leaf.Commander{ 222 | Commands: conf.Exec, 223 | OnStart: func(cmd *leaf.Command) { 224 | log.Infof("running: %s", cmd.String()) 225 | }, 226 | OnError: func(err error) { 227 | log.Errorln(err) 228 | }, 229 | OnExit: func() { 230 | log.Info("commands executed") 231 | }, 232 | ExitOnError: conf.ExitOnErr, 233 | }) 234 | 235 | fc, err := leaf.NewFCFromPatterns( 236 | conf.Filters, 237 | leaf.StandardFilterMatcher, 238 | leaf.StandardFilterHandler) 239 | if err != nil { 240 | log.Fatalf("error creating filters: %v", err) 241 | } 242 | 243 | watcher, err := leaf.NewWatcher( 244 | conf.Root, conf.Exclude, fc) 245 | if err != nil { 246 | log.Fatalf("error creating watcher: %v", err) 247 | } 248 | 249 | cmdCtx, killCmds := context.WithCancel(ctx) 250 | go commander.Run(cmdCtx) 251 | 252 | if !once { 253 | for wr := range watcher.Watch(ctx) { 254 | if wr.Err != nil { 255 | log.Errorf("error while watching: %v", err) 256 | continue 257 | } 258 | 259 | log.Infof("file '%s' changed, reloading...", wr.File) 260 | 261 | killCmds() // kill previous commands 262 | cmdCtx, killCmds = context.WithCancel(ctx) // new context 263 | time.Sleep(conf.Delay) // wait for 'delay' duration 264 | <-commander.Done() // wait more if required by commands 265 | go commander.Run(cmdCtx) // run commands 266 | } 267 | 268 | killCmds() 269 | } 270 | 271 | <-commander.Done() 272 | 273 | log.Infoln("shutdown successfully") 274 | return nil 275 | } 276 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 15 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 16 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 18 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 20 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 22 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 23 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 24 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 25 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 26 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 27 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 28 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 29 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 30 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 31 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 32 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 33 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 34 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 35 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 36 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 37 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 38 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= 40 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 42 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 43 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 44 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 45 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 46 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 47 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 48 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 49 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 50 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 51 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 52 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 53 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 54 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 55 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 56 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 57 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 58 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 59 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 60 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 61 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 62 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 63 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 64 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 65 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 66 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 67 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 68 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 69 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 70 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 71 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 72 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 73 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 74 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= 75 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 76 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 77 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 78 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 79 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 80 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 81 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 82 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 83 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= 84 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= 85 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 86 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 87 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 88 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 89 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 90 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 91 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 92 | github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= 93 | github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= 94 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 95 | github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= 96 | github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= 97 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 98 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 99 | github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= 100 | github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= 101 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 102 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 103 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 104 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 105 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 106 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 107 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 108 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 109 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 110 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 111 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 112 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 113 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 114 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 115 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 116 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 117 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 118 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 119 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 120 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 121 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 122 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 123 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 124 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 125 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 126 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 127 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 128 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 129 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 130 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 131 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 132 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 133 | github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= 134 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 135 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 136 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 137 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 138 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 139 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 140 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 141 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 142 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 143 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= 144 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 145 | github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= 146 | github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= 147 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 148 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 149 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 150 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 151 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 152 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 153 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 154 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 155 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 156 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 157 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= 158 | github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= 159 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 160 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 161 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 162 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 163 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 164 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 165 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 166 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 167 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= 168 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 169 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 170 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 171 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 172 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 173 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 174 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 175 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 176 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 177 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 178 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco= 179 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 180 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g= 181 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 182 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 183 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 184 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 185 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 186 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 187 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 188 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 189 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 190 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 191 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 192 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 193 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 194 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 195 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 196 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 197 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 199 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 200 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 201 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 202 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 203 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 204 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 205 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 206 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 207 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 208 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 209 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= 210 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 211 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 212 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 213 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 214 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 215 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 216 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 217 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 218 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 219 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 220 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 221 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 222 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 223 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 224 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 225 | gopkg.in/ini.v1 v1.52.0 h1:j+Lt/M1oPPejkniCg1TkWE2J3Eh1oZTsHSXzMTzUXn4= 226 | gopkg.in/ini.v1 v1.52.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 227 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 228 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 229 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 230 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 231 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 232 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 233 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 234 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 235 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 236 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 237 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 238 | --------------------------------------------------------------------------------