diff --git a/conf/block_posix.go b/conf/block_posix.go index 1e1b8f0..b0fdbae 100644 --- a/conf/block_posix.go +++ b/conf/block_posix.go @@ -1,9 +1,11 @@ -// +build !windows +//go:build !windows +// +build !windows package conf import ( "fmt" + "os" "syscall" ) @@ -12,7 +14,7 @@ func (b *Block) addDaemon(command string, options []string) error { b.Daemons = []Daemon{} } d := Daemon{ - Command: command, + Command: os.ExpandEnv(command), RestartSignal: syscall.SIGHUP, } for _, v := range options { diff --git a/conf/block_windows.go b/conf/block_windows.go index 08fb8eb..c1b668a 100644 --- a/conf/block_windows.go +++ b/conf/block_windows.go @@ -1,9 +1,11 @@ -// +build windows +//go:build windows +// +build windows package conf import ( "fmt" + "os" "syscall" ) @@ -12,7 +14,7 @@ func (b *Block) addDaemon(command string, options []string) error { b.Daemons = []Daemon{} } d := Daemon{ - Command: command, + Command: os.ExpandEnv(command), RestartSignal: syscall.SIGHUP, } for _, v := range options { diff --git a/conf/conf.go b/conf/conf.go index 6142f89..6409b28 100644 --- a/conf/conf.go +++ b/conf/conf.go @@ -24,6 +24,9 @@ type Block struct { Exclude []string NoCommonFilter bool InDir string + // todo: cumulative env with envEndIdx + // envEndIdx int + Env []string Daemons []Daemon Preps []Prep @@ -34,7 +37,7 @@ func (b *Block) addPrep(command string, options []string) error { b.Preps = []Prep{} } - var onchange = false + onchange := false for _, v := range options { switch v { case "+onchange": @@ -44,7 +47,7 @@ func (b *Block) addPrep(command string, options []string) error { } } - prep := Prep{command, onchange} + prep := Prep{os.ExpandEnv(command), onchange} b.Preps = append(b.Preps, prep) return nil diff --git a/conf/env.go b/conf/env.go new file mode 100644 index 0000000..fb99e54 --- /dev/null +++ b/conf/env.go @@ -0,0 +1,50 @@ +package conf + +import ( + "bufio" + "errors" + "fmt" + "os" + "regexp" + "strings" +) + +var validEnv = regexp.MustCompile(`^[a-zA-Z_][a-zA-Z_0-9]*$`) + +func processEnvFile(envFile string) ([]string, error) { + _, err := os.Stat(envFile) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf(" not found") + } + return nil, err + } + f, err := os.Open(envFile) + if err != nil { + return nil, err + } + defer f.Close() + + var envOut []string + lineNo := 0 + s := bufio.NewScanner(f) + for s.Scan() { + lineNo++ + line := strings.TrimSpace(s.Text()) + if line == "" || line[0] == '#' { + continue + } + p := strings.SplitN(line, `=`, 2) + if !validEnv.MatchString(p[0]) { + return nil, fmt.Errorf("%d: invalid environment variable %q", lineNo, p[0]) + } + if len(p) == 2 && p[1] == "" { + line = p[0] + } + envOut = append(envOut, line) + } + if s.Err() != nil { + return nil, fmt.Errorf("scan error: %v", err) + } + return envOut, nil +} diff --git a/conf/lex.go b/conf/lex.go index 22ce2a7..0890d24 100644 --- a/conf/lex.go +++ b/conf/lex.go @@ -12,10 +12,12 @@ import ( "unicode/utf8" ) -const spaces = " \t" -const whitespace = spaces + "\n" -const wordRunes = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ_" -const quotes = `'"` +const ( + spaces = " \t" + whitespace = spaces + "\n" + wordRunes = "abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ_" + quotes = `'"` +) // Characters we don't allow in bare strings const bareStringDisallowed = "{}#\n" + whitespace + quotes @@ -38,6 +40,8 @@ const ( itemSpace itemVarName itemEquals + itemEnvVar + itemEnvFile ) func (i itemType) String() string { @@ -70,6 +74,10 @@ func (i itemType) String() string { return "space" case itemVarName: return "var" + case itemEnvVar: + return "env" + case itemEnvFile: + return "envfile" default: panic("unreachable") } @@ -432,6 +440,12 @@ func lexInside(l *lexer) stateFn { case "prep": l.emit(itemPrep) return lexOptions + case "envfile": + l.emit(itemEnvFile) + return lexOptions + // case "env": + // l.emit(itemEnvVar) + // return lexOptions default: l.errorf("unknown directive: %s", l.current()) return nil diff --git a/conf/lex_test.go b/conf/lex_test.go index 7f9bcfa..bb3a565 100644 --- a/conf/lex_test.go +++ b/conf/lex_test.go @@ -263,6 +263,16 @@ var lexTests = []struct { {itemBareString, "b"}, }, }, + { + "one {\nenvfile: foo\n}", []itm{ + {itemBareString, "one"}, + {itemLeftParen, "{"}, + {itemEnvFile, "envfile"}, + {itemColon, ":"}, + {itemBareString, "foo\n"}, + {itemRightParen, "}"}, + }, + }, } func TestLex(t *testing.T) { diff --git a/conf/parse.go b/conf/parse.go index 214165c..4b81f87 100644 --- a/conf/parse.go +++ b/conf/parse.go @@ -8,6 +8,7 @@ package conf import ( "fmt" + "os" "path" "path/filepath" "runtime" @@ -248,11 +249,28 @@ Loop: dir = strings.Replace( dir, confVarName, p.config.variables[confVarName], -1, ) + // Expand any OS environment variables that might be in the path + dir = os.ExpandEnv(dir) dir, err := filepath.Abs(dir) if err != nil { p.errorf("%s", err) } block.InDir = dir + case itemEnvFile: + options := p.collectValues(itemBareString) + if len(options) > 0 { + p.errorf("envfile takes no options") + } + p.mustNext(itemColon) + envFile := prepValue(p.mustNext(itemBareString, itemQuotedString)) + cleanEnv := filepath.Clean(envFile) + envVars, err := processEnvFile(cleanEnv) + if err != nil { + p.errorf("envfile %s:%s", envFile, err) + } + block.Env = append(block.Env, envVars...) + // case itemEnvVar: + // options := p.collectValues(itemBareString, itemQuotedString, itemEquals) case itemDaemon: options := p.collectValues(itemBareString) p.mustNext(itemColon) diff --git a/conf/parse_test.go b/conf/parse_test.go index fc874cf..58dc8a2 100644 --- a/conf/parse_test.go +++ b/conf/parse_test.go @@ -159,7 +159,7 @@ var parseTests = []struct { Blocks: []Block{ { Include: []string{"foo"}, - Preps: []Prep{Prep{Command: "command"}}, + Preps: []Prep{{Command: "command"}}, }, }, }, @@ -171,7 +171,7 @@ var parseTests = []struct { Blocks: []Block{ { Include: []string{"foo"}, - Preps: []Prep{Prep{Command: "command", Onchange: true}}, + Preps: []Prep{{Command: "command", Onchange: true}}, }, }, }, @@ -183,7 +183,7 @@ var parseTests = []struct { Blocks: []Block{ { Include: []string{"foo"}, - Preps: []Prep{Prep{Command: "command\n-one\n-two"}}, + Preps: []Prep{{Command: "command\n-one\n-two"}}, }, }, }, @@ -195,7 +195,7 @@ var parseTests = []struct { Blocks: []Block{ { Include: []string{"foo", "bar"}, - Preps: []Prep{Prep{Command: "command"}}, + Preps: []Prep{{Command: "command"}}, }, }, }, @@ -299,6 +299,51 @@ var parseTests = []struct { }, }, }, + { + "", + "foo {\nenvfile:testdata/good.env\nprep: command\n}", + &Config{ + Blocks: []Block{ + { + Include: []string{"foo"}, + Env: []string{ + "FOO=BAR", + `BAR="FOO"`, + "BAZ", + "WUG", + }, + Preps: []Prep{ + { + Command: "command", + }, + }, + }, + }, + }, + }, + { + "", + "foo {\nenvfile:testdata/good.env\nenvfile:testdata/good2.env\nprep: command\n}", + &Config{ + Blocks: []Block{ + { + Include: []string{"foo"}, + Env: []string{ + "FOO=BAR", + `BAR="FOO"`, + "BAZ", + "WUG", + "ALSO=GOOD", + }, + Preps: []Prep{ + { + Command: "command", + }, + }, + }, + }, + }, + }, } var parseCmpOptions = []cmp.Option{ @@ -335,6 +380,8 @@ var parseErrorTests = []struct { {"@foo=bar\n@foo=bar {}", "test:2: variable @foo shadows previous declaration"}, {"{indir +foo: bar\n}", "test:1: indir takes no options"}, {"{indir: bar\nindir: voing\n}", "test:2: indir can only be used once per block"}, + {"{envfile: bar\n}", "test:1: envfile bar: not found"}, + {"{envfile: testdata/bad_var.env\n}", "test:1: envfile testdata/bad_var.env:1: invalid environment variable \"!NOPE\""}, } func TestErrorsParse(t *testing.T) { diff --git a/conf/testdata/bad_var.env b/conf/testdata/bad_var.env new file mode 100644 index 0000000..8d6e467 --- /dev/null +++ b/conf/testdata/bad_var.env @@ -0,0 +1 @@ +!NOPE=FOO \ No newline at end of file diff --git a/conf/testdata/good.env b/conf/testdata/good.env new file mode 100644 index 0000000..a70b844 --- /dev/null +++ b/conf/testdata/good.env @@ -0,0 +1,5 @@ +FOO=BAR +BAR="FOO" +#COMMENTED=not here +BAZ= +WUG \ No newline at end of file diff --git a/conf/testdata/good2.env b/conf/testdata/good2.env new file mode 100644 index 0000000..29adbb4 --- /dev/null +++ b/conf/testdata/good2.env @@ -0,0 +1 @@ +ALSO=GOOD \ No newline at end of file diff --git a/daemon.go b/daemon.go index 5f86c7f..67d83b7 100644 --- a/daemon.go +++ b/daemon.go @@ -25,7 +25,7 @@ const ( type daemon struct { conf conf.Daemon indir string - + env []string ex *shell.Executor log termlog.Stream shell string @@ -77,7 +77,7 @@ func (d *daemon) Restart() { d.Lock() defer d.Unlock() if d.ex == nil { - ex, err := shell.NewExecutor(d.shell, d.conf.Command, d.indir) + ex, err := shell.NewExecutor(d.shell, d.conf.Command, d.indir, d.env) if err != nil { d.log.Shout("Could not create executor: %s", err) } @@ -138,6 +138,7 @@ func NewDaemonPen(block conf.Block, vars map[string]string, log termlog.TermLog) log: log.Stream(niceHeader("daemon: ", dmn.Command)), shell: sh, indir: indir, + env: block.Env, } } return &DaemonPen{daemons: d}, nil diff --git a/go.mod b/go.mod index 9eba6a0..eafcac2 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,28 @@ module github.com/cortesi/modd -go 1.14 +go 1.19 require ( - github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect - github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/cortesi/moddwatch v0.0.0-20210323234936-df014e95c743 github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec - github.com/fatih/color v1.13.0 // indirect github.com/google/go-cmp v0.5.9 + gopkg.in/alecthomas/kingpin.v2 v2.2.6 + mvdan.cc/sh/v3 v3.6.0 +) + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/fatih/color v1.14.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/rjeczalik/notify v0.9.3 // indirect github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/crypto v0.4.0 // indirect - golang.org/x/net v0.4.0 // indirect - gopkg.in/alecthomas/kingpin.v2 v2.2.6 + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/net v0.6.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - mvdan.cc/sh/v3 v3.6.0 ) diff --git a/go.sum b/go.sum index 45d6f53..7b1ccb6 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -44,6 +46,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU= github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= +github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY= +github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -60,6 +64,8 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -69,6 +75,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= @@ -88,12 +96,16 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= diff --git a/prep.go b/prep.go index 7d9c83f..cf3fecd 100644 --- a/prep.go +++ b/prep.go @@ -22,9 +22,9 @@ func (p ProcError) Error() string { } // RunProc runs a process to completion, sending output to log -func RunProc(cmd string, shellMethod string, dir string, log termlog.Stream) error { +func RunProc(cmd string, shellMethod string, dir string, env []string, log termlog.Stream) error { log.Header() - ex, err := shell.NewExecutor(shellMethod, cmd, dir) + ex, err := shell.NewExecutor(shellMethod, cmd, dir, env) if err != nil { return err } @@ -69,7 +69,7 @@ func RunPreps( if err != nil { return err } - err = RunProc(cmd, sh, b.InDir, log.Stream(niceHeader("prep: ", cmd))) + err = RunProc(cmd, sh, b.InDir, b.Env, log.Stream(niceHeader("prep: ", cmd))) if err != nil { if pe, ok := err.(ProcError); ok { for _, n := range notifiers { diff --git a/shell/shell.go b/shell/shell.go index c0b6aec..086bb65 100644 --- a/shell/shell.go +++ b/shell/shell.go @@ -27,6 +27,7 @@ type Executor struct { Shell string Command string Dir string + Env []string cmd *exec.Cmd stdo io.ReadCloser @@ -50,7 +51,7 @@ func GetShellName(v string) (string, error) { return v, nil } -func NewExecutor(shell string, command string, dir string) (*Executor, error) { +func NewExecutor(shell string, command string, dir string, env []string) (*Executor, error) { _, err := makeCommand(shell, command, dir) if err != nil { return nil, err @@ -59,6 +60,7 @@ func NewExecutor(shell string, command string, dir string) (*Executor, error) { Shell: shell, Command: command, Dir: dir, + Env: env, }, nil } @@ -72,6 +74,9 @@ func (e *Executor) start( if err != nil { return nil, nil, nil, err } + if e.Env != nil { + cmd.Env = e.Env + } e.cmd = cmd stdo, err := cmd.StdoutPipe() diff --git a/shell/shell_test.go b/shell/shell_test.go index 34ce388..9ef9e15 100644 --- a/shell/shell_test.go +++ b/shell/shell_test.go @@ -42,7 +42,7 @@ func testCmd(t *testing.T, shell string, ct cmdTest) { } lt := termlog.NewLogTest() - exec, err := NewExecutor(shell, ct.cmd, "") + exec, err := NewExecutor(shell, ct.cmd, "", nil) if err != nil { t.Error(err) return