diff --git a/go.mod b/go.mod index d20c398..7b70dc0 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,10 @@ go 1.20 require ( github.com/fatih/color v1.17.0 + github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/google/uuid v1.6.0 + github.com/spf13/cobra v1.8.1 ) require ( @@ -16,15 +18,17 @@ require ( github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/savioxavier/termlink v1.3.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/mod v0.12.0 // indirect diff --git a/go.sum b/go.sum index b1619b4..6f8e7c8 100644 --- a/go.sum +++ b/go.sum @@ -11,6 +11,7 @@ github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7N github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,6 +35,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4er github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= @@ -56,11 +59,18 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4= +github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/nft/nfctl/clone/clone.go b/nft/nfctl/clone/clone.go new file mode 100644 index 0000000..c975945 --- /dev/null +++ b/nft/nfctl/clone/clone.go @@ -0,0 +1,36 @@ +package clone + +import ( + "fmt" + "github.com/go-git/go-git/v5" + _ "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/loveuer/nf/nft/log" + "net/url" +) + +func Clone(pwd string, ins *url.URL) error { + uri := fmt.Sprintf("%s://%s%s", ins.Scheme, ins.Host, ins.Path) + opt := &git.CloneOptions{ + URL: uri, + Depth: 1, + InsecureSkipTLS: true, + SingleBranch: true, + } + + if ins.User != nil { + password, _ := ins.User.Password() + opt.Auth = &http.BasicAuth{ + Username: ins.User.Username(), + Password: password, + } + } + + log.Info("start clone %s", uri) + _, err := git.PlainClone(pwd, false, opt) + if err != nil { + return err + } + + return nil +} diff --git a/nft/nfctl/cmd/cmd.go b/nft/nfctl/cmd/cmd.go new file mode 100644 index 0000000..a13676a --- /dev/null +++ b/nft/nfctl/cmd/cmd.go @@ -0,0 +1,19 @@ +package cmd + +import "github.com/spf13/cobra" + +var ( + Root = &cobra.Command{ + Use: "nfctl", + Short: "nfctl: easy start your nf backend work", + } +) + +func init() { + initNew() + + Root.AddCommand( + versionCmd, + cmdNew, + ) +} diff --git a/nft/nfctl/cmd/new.go b/nft/nfctl/cmd/new.go new file mode 100644 index 0000000..bb2f064 --- /dev/null +++ b/nft/nfctl/cmd/new.go @@ -0,0 +1,131 @@ +package cmd + +import ( + "errors" + "fmt" + "github.com/loveuer/nf/nft/log" + "github.com/loveuer/nf/nft/nfctl/clone" + "github.com/loveuer/nf/nft/nfctl/opt" + "github.com/loveuer/nf/nft/nfctl/tp" + "github.com/spf13/cobra" + "net/url" + "os" + "path" +) + +var ( + cmdNew = &cobra.Command{ + Use: "new", + Short: "nfctl new: start new project", + Example: `nfctl new {project} -t ultone [recommend] +nfctl new {project} -t https://github.com/loveuer/ultone.git +nfctl new {project} --template http://username:token@my.gitlab.com/my-zone/my-repo.git +`, + SilenceUsage: true, + } + + template string + disableInit bool + + preTemplateMap = map[string]string{ + "ultone": "https://gitcode.com/loveuer/ultone.git", + } +) + +func initNew() { + cmdNew.Flags().BoolVar(&opt.Debug, "debug", false, "debug mode") + cmdNew.Flags().StringVarP(&template, "template", "t", "", "template name/url[example:ultone, https://github.com/xxx/yyy.git]") + cmdNew.Flags().BoolVar(&disableInit, "without-init", false, "don't run template init script") + + cmdNew.RunE = func(cmd *cobra.Command, args []string) error { + if opt.Debug { + log.SetLogLevel(log.LogLevelDebug) + } + + var ( + err error + urlIns *url.URL + pwd string + projectDir string + initBs []byte + renderBs []byte + scripts []tp.Cmd + ) + + if len(args) == 0 { + return fmt.Errorf("project name required") + } + + if pwd, err = os.Getwd(); err != nil { + return fmt.Errorf("get work dir err") + } + + projectDir = path.Join(pwd, args[0]) + + if _, err = os.Stat(projectDir); !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("project folder already exist") + } + + if err = os.MkdirAll(projectDir, 0750); err != nil { + return fmt.Errorf("create project dir err: %v", err) + } + + defer func() { + if err != nil { + _ = os.RemoveAll(projectDir) + } + }() + + if template == "" { + // todo no template new project + return fmt.Errorf("😥create basic project(without template) comming soon...") + } + + cloneUrl := template + if ptUrl, ok := preTemplateMap[cloneUrl]; ok { + cloneUrl = ptUrl + } + + if urlIns, err = url.Parse(cloneUrl); err != nil { + return fmt.Errorf("invalid clone url: %v", err) + } + + if err = clone.Clone(projectDir, urlIns); err != nil { + return fmt.Errorf("clone template err: %v", err) + } + + if initBs, err = os.ReadFile(path.Join(projectDir, ".nfctl")); err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } + + return fmt.Errorf("read nfctl script file err: %v", err) + } + + if renderBs, err = tp.RenderVar(initBs, map[string]any{ + "PROJECT_NAME": args[0], + }); err != nil { + return fmt.Errorf("render template init script err: %v", err) + } + + if scripts, err = tp.ParseCmd(projectDir, renderBs); err != nil { + return fmt.Errorf("parse template init script err: %v", err) + } + + for _, script := range scripts { + if opt.Debug { + log.Debug("start script:\n%s\n", script.String()) + } + + if err = script.Execute(); err != nil { + return fmt.Errorf("execute template init script err: %v", err) + } + } + + if err = os.RemoveAll(path.Join(projectDir, ".git")); err != nil { + log.Warn("remove .git folder err: %s", err.Error()) + } + + return nil + } +} diff --git a/nft/nfctl/cmd/version.go b/nft/nfctl/cmd/version.go new file mode 100644 index 0000000..1fde2ce --- /dev/null +++ b/nft/nfctl/cmd/version.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/loveuer/nf/nft/log" + "github.com/loveuer/nf/nft/nfctl/version" + "github.com/spf13/cobra" +) + +var ( + versionCmd = &cobra.Command{ + Use: "version", + Short: "print nfctl version and exit", + Run: func(cmd *cobra.Command, args []string) { + log.Info("version: %s", version.Version) + }, + } +) diff --git a/nft/nfctl/main.go b/nft/nfctl/main.go index 885b33d..55b704a 100644 --- a/nft/nfctl/main.go +++ b/nft/nfctl/main.go @@ -1,50 +1,19 @@ package main import ( - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - _ "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/storage/memory" - "github.com/loveuer/nf/nft/log" - "io" + "context" + "github.com/loveuer/nf/nft/nfctl/cmd" + "github.com/loveuer/nf/nft/nfctl/version" + "os/signal" + "syscall" ) func main() { - memo := memory.NewStorage() - fs := memfs.New() - repo, err := git.Clone(memo, fs, &git.CloneOptions{ - URL: "http://10.220.10.35/dev/template/ultone.git", - Auth: &http.BasicAuth{Username: "loveuer", Password: "uu_L6neSDseoWx55babJ"}, - Depth: 1, - SingleBranch: true, - InsecureSkipTLS: true, - }) - if err != nil { - panic(err) - } + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer cancel() - infos, err := fs.ReadDir(".") - if err != nil { - panic(err) - } + version.Check() + defer version.Fn() - for _, item := range infos { - log.Info("[fs.info] %s", item.Name()) - if item.Name() == "main.go" { - file, err := fs.Open(item.Name()) - if err != nil { - panic(err) - } - - bs, err := io.ReadAll(file) - if err != nil { - panic(err) - } - - log.Info("[fs.main]\n%s", string(bs)) - } - } - - _ = repo + _ = cmd.Root.ExecuteContext(ctx) } diff --git a/nft/nfctl/opt/var.go b/nft/nfctl/opt/var.go new file mode 100644 index 0000000..a3e7ac4 --- /dev/null +++ b/nft/nfctl/opt/var.go @@ -0,0 +1,5 @@ +package opt + +var ( + Debug bool +) diff --git a/nft/nfctl/readme.md b/nft/nfctl/readme.md new file mode 100644 index 0000000..e39ff0d --- /dev/null +++ b/nft/nfctl/readme.md @@ -0,0 +1,23 @@ +# nfctl + +# 通过 nfctl 快速开启后台项目 + +### 1. Installation + +- ① `go install github.com/loveuer/nf/nft/nfctl@latest` +- ② download prebuild binary [release](https://github.com/loveuer/nf/releases) + +### 2. Usage + +- `nfctl new {project}` +- `nfctl new project -t ultone` +- `nfctl new project -t https://github.com/xxx/yyy.git` +- `nfctl new project --template https://gitcode/loveuer/ultone.git` +- `nfctl new project --template https://{username}:{password/token}@my.gitlab.com/name/project.git` + +### 3. nfctl init script + +- `为方便模版的初始化, 可以采用 nfctl init script, 当 nfctl new project -t xxx 从模版开始项目时会自动执行` +- `具体的编写规则如下:` + * [init 脚本规则](https://github.com/loveuer/nf/nft/nfctl/script.md) 或者 + * [国内](https://gitcode.com/loveuer/nf/nft/nfctl/script.md) \ No newline at end of file diff --git a/nft/nfctl/tp/cmd_parse.go b/nft/nfctl/tp/parse.go similarity index 76% rename from nft/nfctl/tp/cmd_parse.go rename to nft/nfctl/tp/parse.go index 706c463..1536357 100644 --- a/nft/nfctl/tp/cmd_parse.go +++ b/nft/nfctl/tp/parse.go @@ -7,10 +7,10 @@ import ( "strings" ) -func ParseCmd(pwd string, content []byte) ([]TpCmd, error) { +func ParseCmd(pwd string, content []byte) ([]Cmd, error) { var ( err error - cmds = make([]TpCmd, 0) + cmds = make([]Cmd, 0) start = false ) @@ -24,7 +24,7 @@ func ParseCmd(pwd string, content []byte) ([]TpCmd, error) { continue } - if strings.HasPrefix(line, "#") { + if !start && strings.HasPrefix(line, "#") { continue } @@ -44,7 +44,7 @@ func ParseCmd(pwd string, content []byte) ([]TpCmd, error) { continue } - var cmd TpCmd + var cmd Cmd if cmd, err = ParseBlock(pwd, record); err != nil { return nil, err } @@ -66,10 +66,12 @@ func ParseCmd(pwd string, content []byte) ([]TpCmd, error) { return cmds, err } -func ParseBlock(pwd string, lines []string) (TpCmd, error) { +func ParseBlock(pwd string, lines []string) (Cmd, error) { switch lines[0] { - case "!replace": - return newReplace(pwd, lines[1:]) + case "!replace content": + return newReplaceContent(pwd, lines[1:]) + case "!replace name": + return newReplaceName(pwd, lines[1:]) case "!generate": return newGenerate(pwd, lines[1:]) } diff --git a/nft/nfctl/tp/parse_test.go b/nft/nfctl/tp/parse_test.go index 86d9c4f..7843f17 100644 --- a/nft/nfctl/tp/parse_test.go +++ b/nft/nfctl/tp/parse_test.go @@ -7,43 +7,16 @@ import ( ) func TestParseInitFile(t *testing.T) { - const init_bs = ` -!replace -content -reg -*.go -ultone => {{.PROJECT_NAME}} -EOF - -!replace -content -exact -go.mod -module ultone => module {{.PROJECT_NAME}} -EOF - -!replace -name -main.go => loveuer.go -EOF - -!generate -readme.md -# {{.PROJECT_NAME}} - -### run -- ` + "`" + `go run . --help` + "`" + ` -- ` + "`" + `go run .` + "`" + ` - -### build -- ` + "`" + `docker build -t {repo:tag} -f Dockerfile .` + "`" + ` -EOF -` - data := map[string]any{ - "PROJECT_NAME": "loveuer", + bs, err := os.ReadFile("xtest") + if err != nil { + log.Fatal(err.Error()) } - result, err := RenderVar([]byte(init_bs), data) + data := map[string]any{ + "PROJECT_NAME": "myproject", + } + + result, err := RenderVar(bs, data) if err != nil { log.Fatal(err.Error()) } @@ -57,5 +30,8 @@ EOF for _, item := range cmds { log.Info("one cmd => %s\n\n", item.String()) + if err = item.Execute(); err != nil { + log.Fatal(err.Error()) + } } } diff --git a/nft/nfctl/tp/template.go b/nft/nfctl/tp/render.go similarity index 100% rename from nft/nfctl/tp/template.go rename to nft/nfctl/tp/render.go diff --git a/nft/nfctl/tp/tp.go b/nft/nfctl/tp/tp.go index 5239d25..433a460 100644 --- a/nft/nfctl/tp/tp.go +++ b/nft/nfctl/tp/tp.go @@ -1,40 +1,48 @@ package tp import ( + "bufio" + "bytes" "fmt" + "github.com/loveuer/nf/nft/log" + "io/fs" "os" "path" + "path/filepath" + "regexp" "strings" ) -type TpCmd interface { +type Cmd interface { String() string Execute() error } var ( - _ TpCmd = (*TpGenerate)(nil) - _ TpCmd = (*TpReplace)(nil) + _ Cmd = (*Generate)(nil) + _ Cmd = (*ReplaceContent)(nil) + _ Cmd = (*ReplaceName)(nil) ) -type TpGenerate struct { +type Generate struct { pwd string filename string content []string } -func (t *TpGenerate) String() string { - //TODO implement me - panic("implement me") +func (t *Generate) String() string { + return fmt.Sprintf("!generate\n%s\n%s\n", t.filename, strings.Join(t.content, "\n")) } -func (t *TpGenerate) Execute() error { +func (t *Generate) Execute() error { var ( err error location = t.filename input *os.File ) + log.Debug("[Generate] generate[%s]", t.filename) + if !path.IsAbs(t.filename) { location = path.Join(t.pwd, t.filename) } @@ -44,7 +52,7 @@ func (t *TpGenerate) Execute() error { } if !strings.HasSuffix(location, "/") { - if input, err = os.OpenFile(location, os.O_CREATE|os.O_APPEND, 0744); err != nil { + if input, err = os.OpenFile(location, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0744); err != nil { return err } @@ -58,35 +66,251 @@ func (t *TpGenerate) Execute() error { return nil } -func newGenerate(pwd string, lines []string) (*TpGenerate, error) { +func newGenerate(pwd string, lines []string) (*Generate, error) { if len(lines) == 0 { return nil, fmt.Errorf("generate cmd require file/folder name") } - return &TpGenerate{ + return &Generate{ pwd: pwd, filename: lines[0], content: lines[1:], }, nil } -type TpReplace struct { - pwd string -} +type replaceNameMatchType int -func (t *TpReplace) String() string { - //TODO implement me - panic("implement me") -} +const ( + replaceNameMatchReg replaceNameMatchType = iota + 1 + replaceNameMatchExact + replaceNameMatchPrefix + replaceNameMatchSuffix +) -func (t *TpReplace) Execute() error { - //TODO implement me - panic("implement me") -} - -func newReplace(pwd string, lines []string) (*TpReplace, error) { - if len(lines) < 2 { - return nil, fmt.Errorf("invalid replace cmd") +func (rm replaceNameMatchType) Label() string { + switch rm { + case replaceNameMatchReg: + return "reg" + case replaceNameMatchExact: + return "exact" + case replaceNameMatchPrefix: + return "prefix" + case replaceNameMatchSuffix: + return "suffix" } - return &TpReplace{pwd: pwd}, nil + + log.Panic("unknown replace match type: %v", rm) + return "" +} + +type ReplaceContent struct { + pwd string + name string + content string + + targetName string + matchType replaceNameMatchType + fromContent string + targetEmpty bool + targetContent string +} + +func (t *ReplaceContent) String() string { + return fmt.Sprintf("!replace content\n%s\n%s\n", t.name, t.content) +} + +func (t *ReplaceContent) Execute() error { + var ( + fn filepath.WalkFunc + + handler = func(location string) error { + bs, err := os.ReadFile(location) + if err != nil { + return err + } + + log.Debug("[ReplaceContent] handle[%s] replace [%s] => [%s]", location, t.fromContent, t.targetContent) + newbs, err := t.executeFile(bs) + if err != nil { + return err + } + + return os.WriteFile(location, newbs, 0644) + } + ) + + switch t.matchType { + case replaceNameMatchExact: + fn = func(location string, info fs.FileInfo, err error) error { + if location == path.Join(t.pwd, t.targetName) { + log.Debug("[ReplaceContent] exact match: %s", location) + return handler(location) + } + + return nil + } + case replaceNameMatchPrefix: + fn = func(location string, info fs.FileInfo, err error) error { + if strings.HasPrefix(path.Base(location), t.targetName) { + log.Debug("[ReplaceContent] prefix match: %s", location) + return handler(location) + } + + return nil + } + case replaceNameMatchSuffix: + fn = func(location string, info fs.FileInfo, err error) error { + if strings.HasSuffix(location, t.targetName) { + log.Debug("[ReplaceContent] suffix match: %s", location) + return handler(location) + } + + return nil + } + case replaceNameMatchReg: + fn = func(location string, info fs.FileInfo, err error) error { + if match, err := regexp.MatchString(t.targetName, location); err == nil && match { + log.Debug("[ReplaceContent] reg match: %s", location) + return handler(location) + } + + return nil + } + } + + return filepath.Walk(t.pwd, fn) +} + +func (t *ReplaceContent) executeFile(raw []byte) ([]byte, error) { + scanner := bufio.NewScanner(bytes.NewReader(raw)) + scanner.Buffer(make([]byte, 1024), 1024*1024) + + lines := make([]string, 0) + for scanner.Scan() { + line := scanner.Text() + lines = append( + lines, + strings.ReplaceAll(line, t.fromContent, t.targetContent), + ) + } + + return []byte(strings.Join(lines, "\n")), nil +} + +func newReplaceContent(pwd string, lines []string) (*ReplaceContent, error) { + if len(lines) != 2 { + return nil, fmt.Errorf("invalid replace_content cmd: required 2 lines params") + } + + var ( + name = lines[0] + content = lines[1] + matchType replaceNameMatchType + ) + + names := strings.SplitN(name, " ", 2) + if len(names) != 2 { + return nil, fmt.Errorf("invalid replace_content cmd: name line, required: [reg/exact/prefix/shuffix] {filename}") + } + + switch names[0] { + case "exact": + matchType = replaceNameMatchExact + case "reg": + matchType = replaceNameMatchReg + case "prefix": + matchType = replaceNameMatchPrefix + case "suffix": + matchType = replaceNameMatchSuffix + default: + return nil, fmt.Errorf("invalid replace_content name match type, example: [reg *.go] [exact go.mod]") + } + + var ( + targetName string = names[1] + targetEmpty = false + targetContent string + ) + contents := strings.SplitN(content, "=>", 2) + fromContent := strings.TrimSpace(contents[0]) + if len(contents) == 1 { + targetEmpty = true + } else { + if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` { + targetEmpty = true + } + } + + return &ReplaceContent{ + pwd: pwd, + name: name, + content: content, + + matchType: matchType, + targetName: targetName, + fromContent: fromContent, + targetEmpty: targetEmpty, + targetContent: targetContent, + }, nil +} + +type ReplaceName struct { + pwd string + line string + + targetEmpty bool + fromContent string + targetContent string +} + +func (t *ReplaceName) String() string { + return fmt.Sprintf("!replace name\n%s\n", t.line) +} + +func (t *ReplaceName) Execute() error { + fullpath := path.Join(t.pwd, t.fromContent) + if t.targetEmpty { + return os.RemoveAll(fullpath) + } + + ftpath := path.Join(t.pwd, t.targetContent) + return os.Rename(fullpath, ftpath) +} + +func newReplaceName(pwd string, lines []string) (*ReplaceName, error) { + if len(lines) != 1 { + return nil, fmt.Errorf("replace_name need one line param, for example: mian.go => main.go") + } + + var ( + content = lines[0] + targetEmpty = false + fromContent string + targetContent string + ) + + contents := strings.SplitN(content, "=>", 2) + fromContent = strings.TrimSpace(contents[0]) + if len(contents) == 1 { + targetEmpty = true + } else { + if targetContent = strings.TrimSpace(contents[1]); targetContent == "" || targetContent == `""` || targetContent == `''` { + targetEmpty = true + } + } + + if !targetEmpty { + if (strings.HasPrefix(targetContent, `"`) && strings.HasSuffix(targetContent, `"`)) || (strings.HasPrefix(targetContent, `'`) && strings.HasSuffix(targetContent, `'`)) { + targetContent = targetContent[1 : len(targetContent)-1] + } + } + + return &ReplaceName{ + pwd: pwd, + line: content, + + targetEmpty: targetEmpty, + fromContent: fromContent, + targetContent: targetContent, + }, nil } diff --git a/nft/nfctl/version/version.go b/nft/nfctl/version/version.go new file mode 100644 index 0000000..7bc0f23 --- /dev/null +++ b/nft/nfctl/version/version.go @@ -0,0 +1,66 @@ +package version + +import ( + "bufio" + "fmt" + "github.com/fatih/color" + "github.com/loveuer/nf/nft/log" + "github.com/savioxavier/termlink" + "net/http" + "strings" + "sync" +) + +const Version = "v24.07.12-r1" + +var ( + lk = &sync.Mutex{} + empty = func() {} + upgrade = func(v string) func() { + return func() { + color.Green("\n🎉 🎉 🎉 [nfctl] New Version Found: %s", v) + color.Cyan("Upgrade it with: [go install github.com/loveuer/nf/nft/nfctl@latest]") + fmt.Print("Or Download by: ") + color.Cyan(termlink.Link("Releases", "https://github.com/loveuer/nf/releases")) + fmt.Println() + } + } + Fn = empty +) + +func Check() { + ready := make(chan struct{}) + go func() { + ready <- struct{}{} + uri := "https://raw.gitcode.com/loveuer/nf/raw/master/nft/nfctl/version/version.go" + prefix := "const Version = " + resp, err := http.Get(uri) + if err != nil { + log.Debug("[Check] http get[%s] err: %v", uri, err.Error()) + return + } + defer resp.Body.Close() + + scanner := bufio.NewScanner(resp.Body) + scanner.Buffer(make([]byte, 16*1024), 1024*1024) + + for scanner.Scan() { + line := scanner.Text() + log.Debug("[Check] version.go line: %s", line) + if strings.HasPrefix(line, prefix) { + v := strings.TrimPrefix(line, prefix) + if len(v) > 2 { + v = v[1 : len(v)-1] + } + + if v != "" && v > Version { + lk.Lock() + Fn = upgrade(v) + lk.Unlock() + return + } + } + } + }() + <-ready +}