feat:support command line arguments when user not want use config file (#180)

Co-authored-by: jingdi.zhu <jingd.zhu@shopee.com>
This commit is contained in:
xiantang
2022-06-07 00:08:01 +08:00
committed by GitHub
parent 7b308c5d21
commit bb7fe28877
13 changed files with 337 additions and 44 deletions
+9
View File
@@ -24,6 +24,15 @@ NOTE: This tool has nothing to do with hot-deploy for production.
* Allow watching new directories after Air started
* Better building process
### ✨ beta feature
Support air config fields as arguments:
if you just want to config build command and run command, you can use like following command without config file:
`air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./api"`
## Installation
### Prefer install.sh
+1 -2
View File
@@ -7,13 +7,12 @@ require (
github.com/fsnotify/fsnotify v1.4.9
github.com/imdario/mergo v0.3.12
github.com/pelletier/go-toml v1.8.1
github.com/stretchr/testify v1.7.1
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
)
require github.com/creack/pty v1.1.11
require github.com/stretchr/testify v1.7.1
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
+15 -3
View File
@@ -15,6 +15,7 @@ var (
cfgPath string
debugMode bool
showVersion bool
cmdArgs map[string]runner.TomlInfo
runArgs []string
)
@@ -29,11 +30,17 @@ func helpMessage() {
}
func init() {
parseFlag(os.Args[1:])
}
func parseFlag(args []string) {
flag.Usage = helpMessage
flag.StringVar(&cfgPath, "c", "", "config path")
flag.BoolVar(&debugMode, "d", false, "debug mode")
flag.BoolVar(&showVersion, "v", false, "show version")
flag.Parse()
cmd := flag.CommandLine
cmdArgs = runner.ParseConfigFlag(cmd)
flag.CommandLine.Parse(args)
}
func main() {
@@ -51,12 +58,17 @@ func main() {
if debugMode {
fmt.Println("[debug] mode")
}
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
var err error
r, err := runner.NewEngine(cfgPath, debugMode)
cfg, err := runner.InitConfig(cfgPath)
if err != nil {
log.Fatal(err)
return
}
cfg.WithArgs(cmdArgs)
r, err := runner.NewEngineWithConfig(cfg, debugMode)
if err != nil {
log.Fatal(err)
return
+1
View File
@@ -0,0 +1 @@
package runner
+30 -17
View File
@@ -6,6 +6,7 @@ import (
"log"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"time"
@@ -20,7 +21,8 @@ const (
airWd = "air_wd"
)
type config struct {
// Config is the main configuration structure for Air.
type Config struct {
Root string `toml:"root"`
TmpDir string `toml:"tmp_dir"`
TestDataDir string `toml:"testdata_dir"`
@@ -85,7 +87,8 @@ type cfgScreen struct {
ClearOnRebuild bool `toml:"clear_on_rebuild"`
}
func initConfig(path string) (cfg *config, err error) {
// InitConfig initializes the configuration.
func InitConfig(path string) (cfg *Config, err error) {
if path == "" {
cfg, err = defaultPathConfig()
if err != nil {
@@ -140,7 +143,7 @@ func writeDefaultConfig() {
fmt.Printf("%s file created to the current directory with the default settings\n", dftTOML)
}
func defaultPathConfig() (*config, error) {
func defaultPathConfig() (*Config, error) {
// when path is blank, first find `.air.toml`, `.air.conf` in `air_wd` and current working directory, if not found, use defaults
for _, name := range []string{dftTOML, dftConf} {
cfg, err := readConfByName(name)
@@ -156,7 +159,7 @@ func defaultPathConfig() (*config, error) {
return &dftCfg, nil
}
func readConfByName(name string) (*config, error) {
func readConfByName(name string) (*Config, error) {
var path string
if wd := os.Getenv(airWd); wd != "" {
path = filepath.Join(wd, name)
@@ -171,7 +174,7 @@ func readConfByName(name string) (*config, error) {
return cfg, err
}
func defaultConfig() config {
func defaultConfig() Config {
build := cfgBuild{
Cmd: "go build -o ./tmp/main .",
Bin: "./tmp/main",
@@ -201,7 +204,7 @@ func defaultConfig() config {
misc := cfgMisc{
CleanOnExit: false,
}
return config{
return Config{
Root: ".",
TmpDir: "tmp",
TestDataDir: "testdata",
@@ -212,13 +215,13 @@ func defaultConfig() config {
}
}
func readConfig(path string) (*config, error) {
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
cfg := new(config)
cfg := new(Config)
if err = toml.Unmarshal(data, cfg); err != nil {
return nil, err
}
@@ -226,7 +229,7 @@ func readConfig(path string) (*config, error) {
return cfg, nil
}
func readConfigOrDefault(path string) (*config, error) {
func readConfigOrDefault(path string) (*Config, error) {
dftCfg := defaultConfig()
cfg, err := readConfig(path)
if err != nil {
@@ -236,7 +239,7 @@ func readConfigOrDefault(path string) (*config, error) {
return cfg, nil
}
func (c *config) preprocess() error {
func (c *Config) preprocess() error {
var err error
cwd := os.Getenv(airWd)
if cwd != "" {
@@ -278,7 +281,7 @@ func (c *config) preprocess() error {
return err
}
func (c *config) colorInfo() map[string]string {
func (c *Config) colorInfo() map[string]string {
return map[string]string{
"main": c.Color.Main,
"build": c.Color.Build,
@@ -287,30 +290,40 @@ func (c *config) colorInfo() map[string]string {
}
}
func (c *config) buildLogPath() string {
func (c *Config) buildLogPath() string {
return filepath.Join(c.tmpPath(), c.Build.Log)
}
func (c *config) buildDelay() time.Duration {
func (c *Config) buildDelay() time.Duration {
return time.Duration(c.Build.Delay) * time.Millisecond
}
func (c *config) binPath() string {
func (c *Config) binPath() string {
return filepath.Join(c.Root, c.Build.Bin)
}
func (c *config) tmpPath() string {
func (c *Config) tmpPath() string {
return filepath.Join(c.Root, c.TmpDir)
}
func (c *config) TestDataPath() string {
func (c *Config) testDataPath() string {
return filepath.Join(c.Root, c.TestDataDir)
}
func (c *config) rel(path string) string {
func (c *Config) rel(path string) string {
s, err := filepath.Rel(c.Root, path)
if err != nil {
return ""
}
return s
}
// WithArgs returns a new config with the given arguments added to the configuration.
func (c *Config) WithArgs(args map[string]TomlInfo) {
for _, value := range args {
if value.Value != nil && *value.Value != "" {
v := reflect.ValueOf(c)
setValue2Struct(v, value.fieldPath, *value.Value)
}
}
}
+3 -3
View File
@@ -12,7 +12,7 @@ const (
cmd = "go build -o ./tmp/main ."
)
func getWindowsConfig() config {
func getWindowsConfig() Config {
build := cfgBuild{
Cmd: "go build -o ./tmp/main .",
Bin: "./tmp/main",
@@ -28,7 +28,7 @@ func getWindowsConfig() config {
build.Cmd = cmd
}
return config{
return Config{
Root: ".",
TmpDir: "tmp",
TestDataDir: "testdata",
@@ -110,7 +110,7 @@ func TestReadConfByName(t *testing.T) {
_ = os.Unsetenv(airWd)
config, _ := readConfByName(dftTOML)
if config != nil {
t.Fatalf("expect config is nil,but get a not nil config")
t.Fatalf("expect Config is nil,but get a not nil Config")
}
}
+13 -9
View File
@@ -15,7 +15,7 @@ import (
// Engine ...
type Engine struct {
config *config
config *Config
logger *logger
watcher *fsnotify.Watcher
debugMode bool
@@ -37,14 +37,8 @@ type Engine struct {
ll sync.Mutex // lock for logger
}
// NewEngine ...
func NewEngine(cfgPath string, debugMode bool) (*Engine, error) {
var err error
cfg, err := initConfig(cfgPath)
if err != nil {
return nil, err
}
// NewEngineWithConfig ...
func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) {
logger := newLogger(cfg)
watcher, err := fsnotify.NewWatcher()
if err != nil {
@@ -70,6 +64,16 @@ func NewEngine(cfgPath string, debugMode bool) (*Engine, error) {
return &e, nil
}
// NewEngine ...
func NewEngine(cfgPath string, debugMode bool) (*Engine, error) {
var err error
cfg, err := InitConfig(cfgPath)
if err != nil {
return nil, err
}
return NewEngineWithConfig(cfg, debugMode)
}
// Run run run
func (e *Engine) Run() {
if len(os.Args) > 1 && os.Args[1] == "init" {
+2 -2
View File
@@ -25,7 +25,7 @@ func TestNewEngine(t *testing.T) {
t.Fatal("logger should not be nil")
}
if engine.config == nil {
t.Fatal("config should not be nil")
t.Fatal("Config should not be nil")
}
if engine.watcher == nil {
t.Fatal("watcher should not be nil")
@@ -200,7 +200,7 @@ func TestCtrlCWhenHaveKillDelay(t *testing.T) {
// fix https://github.com/cosmtrek/air/issues/278
// generate a random port
data := []byte("[build]\n kill_delay = \"2s\"")
c := config{}
c := Config{}
if err := toml.Unmarshal(data, &c); err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
+15
View File
@@ -0,0 +1,15 @@
package runner
import (
"flag"
)
// ParseConfigFlag parse toml information for flag
func ParseConfigFlag(f *flag.FlagSet) map[string]TomlInfo {
c := Config{}
m := flatConfig(c)
for k, v := range m {
f.StringVar(v.Value, k, "", "")
}
return m
}
+128
View File
@@ -0,0 +1,128 @@
package runner
import (
"flag"
"log"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestFlag(t *testing.T) {
// table driven tests
type testCase struct {
name string
args []string
expected string
key string
}
testCases := []testCase{
{
name: "test1",
args: []string{"--build.cmd", "go build -o ./tmp/main ."},
expected: "go build -o ./tmp/main .",
key: "build.cmd",
},
{
name: "tmp dir test",
args: []string{"--tmp_dir", "test"},
expected: "test",
key: "tmp_dir",
},
{
name: "check bool",
args: []string{"--build.exclude_unchanged", "true"},
expected: "true",
key: "build.exclude_unchanged",
},
{
name: "check int",
args: []string{"--build.kill_delay", "1000"},
expected: "1000",
key: "build.kill_delay",
},
{
name: "check exclude_regex",
args: []string{"--build.exclude_regex", `["_test.go"]`},
expected: `["_test.go"]`,
key: "build.exclude_regex",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
flag := flag.NewFlagSet(t.Name(), flag.ExitOnError)
cmdArgs := ParseConfigFlag(flag)
flag.Parse(tc.args)
assert.Equal(t, tc.expected, *cmdArgs[tc.key].Value)
})
}
}
func TestConfigRuntimeArgs(t *testing.T) {
// table driven tests
type testCase struct {
name string
args []string
key string
check func(t *testing.T, conf *Config)
}
testCases := []testCase{
{
name: "test1",
args: []string{"--build.cmd", "go build -o ./tmp/main ."},
key: "build.cmd",
check: func(t *testing.T, conf *Config) {
assert.Equal(t, "go build -o ./tmp/main .", conf.Build.Cmd)
},
},
{
name: "tmp dir test",
args: []string{"--tmp_dir", "test"},
key: "tmp_dir",
check: func(t *testing.T, conf *Config) {
assert.Equal(t, "test", conf.TmpDir)
},
},
{
name: "check int64",
args: []string{"--build.kill_delay", "1000"},
key: "build.kill_delay",
check: func(t *testing.T, conf *Config) {
assert.Equal(t, time.Duration(1000), conf.Build.KillDelay)
},
},
{
name: "check bool",
args: []string{"--build.exclude_unchanged", "true"},
key: "build.exclude_unchanged",
check: func(t *testing.T, conf *Config) {
assert.Equal(t, true, conf.Build.ExcludeUnchanged)
},
},
{
name: "check exclude_regex",
args: []string{"--build.exclude_regex", `["_test.go"]`},
check: func(t *testing.T, conf *Config) {
assert.Equal(t, []string{"_test.go"}, conf.Build.ExcludeRegex)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
os.Chdir(dir)
flag := flag.NewFlagSet(t.Name(), flag.ExitOnError)
cmdArgs := ParseConfigFlag(flag)
flag.Parse(tc.args)
cfg, err := InitConfig("")
if err != nil {
log.Fatal(err)
return
}
cfg.WithArgs(cmdArgs)
tc.check(t, cfg)
})
}
}
+2 -2
View File
@@ -26,12 +26,12 @@ var (
type logFunc func(string, ...interface{})
type logger struct {
config *config
config *Config
colors map[string]string
loggers map[string]logFunc
}
func newLogger(cfg *config) *logger {
func newLogger(cfg *Config) *logger {
if cfg == nil {
return nil
}
+89 -3
View File
@@ -4,9 +4,12 @@ import (
"crypto/sha256"
"encoding/hex"
"errors"
"log"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
@@ -54,7 +57,7 @@ func (e *Engine) isTmpDir(path string) bool {
}
func (e *Engine) isTestDataDir(path string) bool {
return path == e.config.TestDataPath()
return path == e.config.testDataPath()
}
func isHiddenDirectory(path string) bool {
@@ -199,7 +202,7 @@ func cmdPath(path string) string {
return strings.Split(path, " ")[0]
}
func adaptToVariousPlatforms(c *config) {
func adaptToVariousPlatforms(c *Config) {
// Fix the default configuration is not used in Windows
// Use the unix configuration on Windows
if runtime.GOOS == PlatformWindows {
@@ -207,7 +210,7 @@ func adaptToVariousPlatforms(c *config) {
runName := "start"
extName := ".exe"
originBin := c.Build.Bin
if 0 < len(c.Build.FullBin) {
if !strings.HasSuffix(c.Build.FullBin, extName) {
@@ -265,3 +268,86 @@ func (a *checksumMap) updateFileChecksum(filename, newChecksum string) (ok bool)
}
return false
}
// TomlInfo is a struct for toml config file
type TomlInfo struct {
fieldPath string
field reflect.StructField
Value *string
}
func setValue2Struct(v reflect.Value, fieldName string, value string) {
index := strings.Index(fieldName, ".")
if index == -1 && len(fieldName) == 0 {
return
}
fields := strings.Split(fieldName, ".")
var addressableVal reflect.Value
switch v.Type().String() {
case "*runner.Config":
addressableVal = v.Elem()
default:
addressableVal = v
}
if len(fields) == 1 {
// string slice int switch case
field := addressableVal.FieldByName(fieldName)
switch field.Kind() {
case reflect.String:
field.SetString(value)
case reflect.Slice:
if field.Len() == 0 {
field.Set(reflect.Append(field, reflect.ValueOf(value)))
}
case reflect.Int64:
i, _ := strconv.ParseInt(value, 10, 64)
field.SetInt(i)
case reflect.Int:
i, _ := strconv.Atoi(value)
field.SetInt(int64(i))
case reflect.Bool:
b, _ := strconv.ParseBool(value)
field.SetBool(b)
case reflect.Ptr:
field.SetString(value)
default:
log.Fatalf("unsupported type %s", v.FieldByName(fields[0]).Kind())
}
} else if len(fields) == 0 {
return
} else {
field := addressableVal.FieldByName(fields[0])
s2 := fieldName[index+1:]
setValue2Struct(field, s2, value)
}
}
// flatConfig ...
func flatConfig(stut interface{}) map[string]TomlInfo {
m := make(map[string]TomlInfo)
t := reflect.TypeOf(stut)
setTage2Map("", t, m, "")
return m
}
func setTage2Map(root string, t reflect.Type, m map[string]TomlInfo, fieldPath string) {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tomlVal := field.Tag.Get("toml")
switch field.Type.Kind() {
case reflect.Struct:
path := fieldPath + field.Name + "."
setTage2Map(root+tomlVal+".", field.Type, m, path)
default:
if tomlVal == "" {
continue
}
tomlPath := root + tomlVal
path := fieldPath + field.Name
var v *string
str := ""
v = &str
m[tomlPath] = TomlInfo{field: field, Value: v, fieldPath: path}
}
}
}
+29 -3
View File
@@ -2,16 +2,20 @@ package runner
import (
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestIsDirRootPath(t *testing.T) {
@@ -134,7 +138,7 @@ func TestChecksumMap(t *testing.T) {
}
func TestAdaptToVariousPlatforms(t *testing.T) {
config := &config{
config := &Config{
Build: cfgBuild{
Bin: "tmp\\main.exe -dev",
},
@@ -147,7 +151,7 @@ func TestAdaptToVariousPlatforms(t *testing.T) {
func Test_killCmd_no_process(t *testing.T) {
e := Engine{
config: &config{
config: &Config{
Build: cfgBuild{
SendInterrupt: false,
},
@@ -180,7 +184,7 @@ func Test_killCmd_SendInterrupt_false(t *testing.T) {
os.Remove("pid")
defer os.Remove("pid")
e := Engine{
config: &config{
config: &Config{
Build: cfgBuild{
SendInterrupt: false,
},
@@ -230,3 +234,25 @@ func Test_killCmd_SendInterrupt_false(t *testing.T) {
}
}
}
func TestGetStructureFieldTagMap(t *testing.T) {
c := Config{}
tagMap := flatConfig(c)
for _, i2 := range tagMap {
fmt.Printf("%v\n", i2.fieldPath)
}
}
func TestSetStructValue(t *testing.T) {
c := Config{}
v := reflect.ValueOf(&c)
setValue2Struct(v, "TmpDir", "asdasd")
assert.Equal(t, "asdasd", c.TmpDir)
}
func TestNestStructValue(t *testing.T) {
c := Config{}
v := reflect.ValueOf(&c)
setValue2Struct(v, "Build.Cmd", "asdasd")
assert.Equal(t, "asdasd", c.Build.Cmd)
}