From 77c4567091acefcc3354c4682d08ed13f3c832d2 Mon Sep 17 00:00:00 2001 From: robotomize Date: Thu, 11 May 2023 17:47:28 +0300 Subject: [PATCH] add the first version of the utility --- README.md | 47 ++++ cmd/goprintenv/main.go | 12 + cmd/goprintenv/root.go | 53 ++++ go.mod | 15 + go.sum | 18 ++ internal/analysis/analysis.go | 501 ++++++++++++++++++++++++++++++++++ internal/analysis/func.go | 26 ++ internal/parser/parser.go | 277 +++++++++++++++++++ internal/printer/print.go | 62 +++++ internal/slice/slice.go | 46 ++++ 10 files changed, 1057 insertions(+) create mode 100644 README.md create mode 100644 cmd/goprintenv/main.go create mode 100644 cmd/goprintenv/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/analysis/analysis.go create mode 100644 internal/analysis/func.go create mode 100644 internal/parser/parser.go create mode 100644 internal/printer/print.go create mode 100644 internal/slice/slice.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3bb886 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +## goprintenv + +A command line utility that displays the environment variables used by a Go project + +### Status + +The project is under development, it may not find all environment variables, keep this in mind when using it. + +```shell +goprintenv -p +``` + +### Install + +```shell +go install github.com/robotomize/go-printenv/cmd/goprintenv@latest +``` + +### Example + +Currently only processes env tags + +```go +type Nested struct { + Live bool `env:"LIVE,default=true"` +} + +type NestedStruct struct { + LastName string `env:"LAST_NAME,default=IVANOV" json:"last_name" bson:"lastName"` + Two Nested `env:",prefix=TWO_"` +} + +``` + +The result will be the following + +```shell +LAST_NAME=IVANOV +TWO_LIVE=true +``` + +### @TODO +* add ci, golangci-lint, Makefile, Dockerfile +* Implement parsing of built-in structures +* Should parse os.Getenv +* Support more env tags +* Refactor, simplify the code. Fix bugs and write unit tests \ No newline at end of file diff --git a/cmd/goprintenv/main.go b/cmd/goprintenv/main.go new file mode 100644 index 0000000..e4a9d30 --- /dev/null +++ b/cmd/goprintenv/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" +) + +func main() { + if err := rootCmd.Execute(); err != nil { + rootCmd.Println(err) + os.Exit(1) + } +} diff --git a/cmd/goprintenv/root.go b/cmd/goprintenv/root.go new file mode 100644 index 0000000..8fe90b8 --- /dev/null +++ b/cmd/goprintenv/root.go @@ -0,0 +1,53 @@ +package main + +import ( + "github.com/spf13/cobra" + "gituhb.com/robotomize/go-printenv/internal/analysis" + "gituhb.com/robotomize/go-printenv/internal/printer" +) + +var ( + verboseFlag bool + goProjectPath string +) + +func init() { + rootCmd.PersistentFlags().BoolVarP( + &verboseFlag, + "verbose", + "v", + false, + "more verbose", + ) + rootCmd.PersistentFlags().StringVarP( + &goProjectPath, + "path", + "p", + ".", + "go project path", + ) +} + +var rootCmd = &cobra.Command{ + Use: "goprintenv [-p project path -v verbose]", + Long: "Print env variables used in go project", + RunE: func(cmd *cobra.Command, args []string) error { + + ctx := cmd.Context() + var printOpts []printer.Option + + a := analysis.New( + goProjectPath, func(items ...analysis.OutputEntry) analysis.Printer { + return printer.New(items, printOpts...) + }, + ) + + if _, err := cmd.OutOrStdout().Write(a.Print()); err != nil { + return err + } + + _ = ctx + + return nil + }, +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4d4aae0 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module gituhb.com/robotomize/go-printenv + +go 1.19 + +require ( + github.com/spf13/cobra v1.6.1 + golang.org/x/tools v0.6.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/sys v0.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8641261 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +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/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/analysis/analysis.go b/internal/analysis/analysis.go new file mode 100644 index 0000000..5513085 --- /dev/null +++ b/internal/analysis/analysis.go @@ -0,0 +1,501 @@ +package analysis + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "gituhb.com/robotomize/go-printenv/internal/parser" + "gituhb.com/robotomize/go-printenv/internal/slice" + "golang.org/x/mod/modfile" +) + +var builtinTypes = []string{ + "string", "int", "int32", "uint32", "int64", "uint64", "bool", "uint16", "int16", "float64", "float32", + "complex64", "complex128", "byte", "nil", "uint8", "int8", "time.Time", "time.Duration", +} + +var _ fmt.Stringer = (*OutputEntry)(nil) + +type OutputEntry struct { + PackageName string + EntryName string + DefaultValue string +} + +func (o OutputEntry) String() string { + return fmt.Sprintf("%s=%s", o.EntryName, o.DefaultValue) +} + +type Option func(*analysis) + +type Printer interface { + Print() []byte +} + +type Analyzer interface { + Analyze() ([]OutputEntry, error) + Print() []byte +} + +type ExtractFunc func(t string) (tagDetail, bool) + +func New(pth string, printFunc func(items ...OutputEntry) Printer) Analyzer { + return &analysis{ + pth: pth, newPrinterFunc: printFunc, extractors: []ExtractFunc{ + extractFunc("env", "prefix", "default"), + }, + skipDirs: []string{}, + } +} + +type node struct { + refCounter int + struct1 *parser.Struct +} + +type tagDetail struct { + name string + isPrefix bool + prefix string + defaultValue string +} + +type analysis struct { + pth string + modPth string + skipDirs []string + newPrinterFunc func(items ...OutputEntry) Printer + extractors []ExtractFunc +} + +func (p *analysis) Print() []byte { + analyze, _ := p.Analyze() + + return p.newPrinterFunc(analyze...).Print() +} + +func (p *analysis) Analyze() ([]OutputEntry, error) { + var output []OutputEntry + structs, err := p.parse() + if err != nil { + return nil, fmt.Errorf("analyze parse: %w", err) + } + + structs = slice.Filter( + structs, func(v *parser.Struct) bool { + for _, field := range v.Fields { + var info *tagDetail + for _, f := range p.extractors { + detail, ok := f(field.Tag) + if !ok { + continue + } + info = &detail + break + } + + if info == nil { + return false + } + } + return true + }, + ) + + // if _, err = os.Stat(filepath.Join(dir, "vendor")); err != nil { + // if errors.Is(err, os.ErrNotExist) { + // goPath := os.Getenv("GOPATH") + // pthSegments := filepath.SplitList(goPath) + // pthSegments = append( + // pthSegments, append([]string{"pkg", "mod"}, filepath.SplitList(currGoMod.Mod.Path)...)..., + // ) + // } + // + // return nil, fmt.Errorf("os.Stat: %w", err) + // } + + // err = walkVendor( + // "", filepath.Join(p.pth, "vendor"), func(modPth, path string) error { + // if strings.HasSuffix(path, ".go") { + // parsed, err := parser.ParseVendorStruct(modPth, path) + // if err != nil { + // return fmt.Errorf("parser.ParseStruct: %w", err) + // } + // + // structs = append(structs, parsed...) + // } + // + // return nil + // }, + // ) + // if err != nil { + // return nil, err + // } + + nodes := make(map[string]*node) + for _, struct1 := range structs { + nodes[struct1.PackageName+":"+struct1.Name] = &node{ + refCounter: 0, + struct1: struct1, + } + } + + for _, struct1 := range structs { + FieldIter: + for _, field := range struct1.Fields { + for _, t := range builtinTypes { + if field.Type == t { + continue FieldIter + } + } + + // env.Config + var info *tagDetail + for _, f := range p.extractors { + detail, ok := f(field.Tag) + if !ok { + continue + } + info = &detail + } + + if info == nil { + continue + } + + if strings.Contains(field.Type, ".") { + continue + } + + nodPth := struct1.PackageName + ":" + field.Type + + nod, ok := nodes[nodPth] + if !ok { + continue + } + + nod.refCounter += 1 + } + } + + prepared := slice.Filter( + slice.MapValuesToSlice(nodes), func(n *node) bool { + return n.refCounter == 0 + }, + ) + + for _, n := range prepared { + rows, err := p.buildVar("", nodes, n.struct1) + if err != nil { + return nil, err + } + + output = append(output, rows...) + } + + return output, nil +} + +func walkVendor(modPth, dir string, f func(modPth, dir string) error) error { + dirs, err := os.ReadDir(dir) + if err != nil { + return err + } + + for _, entry := range dirs { + if entry.IsDir() { + if err = walkVendor( + filepath.Join(modPth, entry.Name()), filepath.Join(dir, entry.Name()), f, + ); err != nil { + return err + } + continue + } + if err = f(modPth, filepath.Join(dir, entry.Name())); err != nil { + return err + } + } + + return nil +} + +func (p *analysis) buildVar(prefix string, nodes map[string]*node, root *parser.Struct) ([]OutputEntry, error) { + var output []OutputEntry +FieldIter: + for _, field := range root.Fields { + for _, t := range builtinTypes { + if field.Type == t { + var info tagDetail + for _, f := range p.extractors { + detail, ok := f(field.Tag) + if !ok { + continue FieldIter + } + info = detail + break + } + + output = append( + output, OutputEntry{ + PackageName: root.PackageName, + EntryName: prefix + info.name, + DefaultValue: info.defaultValue, + }, + ) + continue FieldIter + } + } + + var info tagDetail + for _, f := range p.extractors { + detail, ok := f(field.Tag) + if !ok { + continue FieldIter + } + info = detail + } + + if !info.isPrefix { + output = append( + output, OutputEntry{ + PackageName: root.PackageName, + EntryName: prefix + info.name, + DefaultValue: info.defaultValue, + }, + ) + continue FieldIter + } + + if !strings.Contains(field.Type, ".") { + nodPth := root.PackageName + ":" + field.Type + nod, ok := nodes[nodPth] + if !ok { + d, _ := filepath.Split(root.FilePath) + dirs, err := os.ReadDir(d) + if err != nil { + return nil, err + } + + var structs []*parser.Struct + for _, entry := range dirs { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".go") { + parsed, err := parser.ParseVendorStruct(filepath.Join(d, entry.Name())) + if err != nil { + return nil, fmt.Errorf("parser.ParseVendorStruct: %w", err) + } + + structs = append(structs, parsed...) + } + } + + for _, s := range structs { + if s.Name == field.Type { + rows, err := p.buildVar(prefix+info.prefix, nodes, s) + if err != nil { + return nil, err + } + + output = append(output, rows...) + + continue FieldIter + } + } + + continue + } + + rows, err := p.buildVar(prefix+info.prefix, nodes, nod.struct1) + if err != nil { + return nil, err + } + output = append(output, rows...) + continue + } + + depStruct, exist, err := p.parseDepStructPth(nodes, field, root) + if err != nil { + return nil, err + } + + if !exist { + continue + } + + rows, err := p.buildVar(prefix+info.prefix, nodes, depStruct) + if err != nil { + return nil, err + } + output = append(output, rows...) + } + + return output, nil +} + +func extractFunc(key, prefixFlag, defaultFlag string) func(t string) (tagDetail, bool) { + return func(t string) (tagDetail, bool) { + var detail tagDetail + name, flags, ok := parser.LookupTagValue(t, key) + if !ok { + return detail, ok + } + + detail.name = name + + for _, flag := range flags { + switch { + case strings.Contains(flag, prefixFlag): + detail.isPrefix = true + tokens := strings.Split(flag, "=") + if len(tokens) > 1 { + detail.prefix = tokens[1] + } + case strings.Contains(flag, defaultFlag): + tokens := strings.Split(flag, "=") + if len(tokens) > 1 { + detail.defaultValue = tokens[1] + } + default: + } + } + + return detail, true + } +} + +var ErrGoPkgNotFound = errors.New("GOPATH not found") + +func (p *analysis) parseDepStructPth( + nodes map[string]*node, + field *parser.StructField, + struct1 *parser.Struct, +) (*parser.Struct, bool, error) { + pth, err := CurrentVendorRootPth(p.pth) + if err != nil { + return nil, false, fmt.Errorf("CurrentVendorRootPth: %w", err) + } + + tokens := strings.Split(field.Type, ".") + if len(tokens) > 1 { + pkgName := tokens[0] + structTyp := tokens[1] + + for _, pkg := range struct1.Imports { + if strings.Contains(pkg, pkgName) { + nodPth := pkg + ":" + structTyp + nod, ok := nodes[nodPth] + if ok { + return nod.struct1, true, nil + } + + var structs []*parser.Struct + + vendorPth := filepath.Join(append([]string{pth}, filepath.SplitList(pkg)...)...) + if _, err = os.Stat(filepath.Join(append([]string{pth}, filepath.SplitList(pkg)...)...)); err != nil { + if !errors.Is(err, os.ErrNotExist) { + return nil, false, err + } + + pkgSegments := filepath.SplitList(pkg) + vendorPth = filepath.Join(append([]string{pth}, pkgSegments[:len(pkgSegments)-1]...)...) + } + + dirs, err := os.ReadDir(vendorPth) + if err != nil { + return nil, false, err + } + + for _, d := range dirs { + if !d.IsDir() && strings.HasSuffix(d.Name(), ".go") { + parsed, err := parser.ParseVendorStruct(filepath.Join(vendorPth, d.Name())) + if err != nil { + return nil, false, fmt.Errorf("parser.ParseVendorStruct: %w", err) + } + + structs = append(structs, parsed...) + } + } + + for _, s := range structs { + if s.Name == structTyp { + return s, true, nil + } + } + + return nil, false, nil + } + } + } + + return nil, false, nil +} + +func (p *analysis) parse() ([]*parser.Struct, error) { + var structs []*parser.Struct + + modPth := filepath.Join(p.pth, "go.mod") + src, err := os.ReadFile(modPth) + if err != nil { + return nil, fmt.Errorf("os.ReadFile: %w", err) + } + + parse, err := modfile.Parse(modPth, src, nil) + if err != nil { + return nil, fmt.Errorf("modfile.Parse: %w", err) + } + + if parse.Module == nil { + return nil, ErrGoModNotFound + } + + p.modPth = parse.Module.Mod.Path + + if err = p.walkDir( + p.pth, func(modPth, rootPth, path string) error { + if strings.HasSuffix(path, ".go") { + parsed, err := parser.ParseStruct(modPth, rootPth, path) + if err != nil { + return fmt.Errorf("parser.ParseStruct: %w", err) + } + + structs = append(structs, parsed...) + } + + return nil + }, + ); err != nil { + return nil, fmt.Errorf("walkDir: %w", err) + } + + return structs, nil +} + +var ErrGoModNotFound = errors.New("go mod not found") + +func (p *analysis) walkDir(dir string, f func(modPth, rootPth, pth string) error) error { + dirs, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("os.ReadDir: %w", err) + } + + for _, entry := range dirs { + if strings.HasPrefix(entry.Name(), ".") || strings.HasPrefix(entry.Name(), "..") { + continue + } + + if entry.IsDir() && entry.Name() != "vendor" { + if err = p.walkDir(filepath.Join(dir, entry.Name()), f); err != nil { + return err + } + continue + } + + if err = f(p.modPth, p.pth, filepath.Join(dir, entry.Name())); err != nil { + return err + } + } + + return nil +} diff --git a/internal/analysis/func.go b/internal/analysis/func.go new file mode 100644 index 0000000..6f80da1 --- /dev/null +++ b/internal/analysis/func.go @@ -0,0 +1,26 @@ +package analysis + +import ( + "errors" + "os" + "path/filepath" +) + +func CurrentVendorRootPth(rootPth string) (string, error) { + pth := filepath.Join(rootPth, "vendor") + if _, err := os.Stat(pth); err != nil { + if errors.Is(err, os.ErrNotExist) { + goPath := os.Getenv("GOPATH") + pth = filepath.Join(append(filepath.SplitList(goPath), []string{"pkg", "mod"}...)...) + if _, err = os.Stat(pth); err != nil { + return "", ErrGoPkgNotFound + } + + return pth, nil + } + + return "", ErrGoModNotFound + } + + return pth, nil +} diff --git a/internal/parser/parser.go b/internal/parser/parser.go new file mode 100644 index 0000000..576d960 --- /dev/null +++ b/internal/parser/parser.go @@ -0,0 +1,277 @@ +package parser + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "strings" +) + +type File struct { + FilePath string + PackageName string + Imports []string + Fields []Field +} + +type Field struct { + StructName string + FieldName string + FieldType string + CommentName string + TagValue string +} + +type Struct struct { + FilePath string + PackageName string + Imports []string + Name string + Fields []*StructField +} + +type StructField struct { + Name string + Type string + Tag string +} + +func ParseVendorStruct(pth string) ([]*Struct, error) { + file, err := ParseVendor(pth) + if err != nil { + return nil, fmt.Errorf("ParseVendor: %w", err) + } + + var structs []*Struct +FieldIter: + for _, field := range file.Fields { + for _, struct1 := range structs { + if field.StructName == struct1.Name { + struct1.Fields = append( + struct1.Fields, &StructField{ + Name: field.FieldName, + Type: field.FieldType, + Tag: field.TagValue, + }, + ) + continue FieldIter + } + } + structs = append( + structs, &Struct{ + FilePath: file.FilePath, + PackageName: file.PackageName, + Imports: file.Imports, + Name: field.StructName, + Fields: []*StructField{ + { + Name: field.FieldName, + Type: field.FieldType, + Tag: field.TagValue, + }, + }, + }, + ) + } + + return structs, nil +} + +func ParseStruct(modPth, rootPth, pth string) ([]*Struct, error) { + file, err := Parse1(modPth, rootPth, pth) + if err != nil { + return nil, err + } + var structs []*Struct +FieldIter: + for _, field := range file.Fields { + for _, struct1 := range structs { + if field.StructName == struct1.Name { + struct1.Fields = append( + struct1.Fields, &StructField{ + Name: field.FieldName, + Type: field.FieldType, + Tag: field.TagValue, + }, + ) + continue FieldIter + } + } + structs = append( + structs, &Struct{ + FilePath: file.FilePath, + PackageName: file.PackageName, + Imports: file.Imports, + Name: field.StructName, + Fields: []*StructField{ + { + Name: field.FieldName, + Type: field.FieldType, + Tag: field.TagValue, + }, + }, + }, + ) + } + + return structs, nil +} + +// /root/vendor + +func ParseVendor(source string) (*File, error) { + fSet := token.NewFileSet() + src, err := os.ReadFile(source) + if err != nil { + return nil, fmt.Errorf("os.ReadFile: %w", err) + } + + file, err := parser.ParseFile(fSet, source, src, 0) + if err != nil { + return nil, err + } + + packageName := file.Name.Name + + pFile := &File{ + FilePath: source, + PackageName: packageName, + Fields: make([]Field, 0), + } + + ast.Inspect( + file, func(n ast.Node) bool { + switch t := n.(type) { + case *ast.TypeSpec: + e, ok := t.Type.(*ast.StructType) + if ok && token.IsExported(t.Name.Name) { + if e.Fields == nil || e.Fields.NumFields() < 1 { + // skip empty structs + return true + } + + for _, field := range e.Fields.List { + if len(field.Names) == 0 || !token.IsExported(field.Names[0].Name) || field.Tag == nil { + continue + } + + pFile.Fields = append( + pFile.Fields, Field{ + StructName: t.Name.Name, + FieldName: field.Names[0].Name, + CommentName: field.Names[0].Name, + FieldType: string(src[field.Type.Pos()-1 : field.Type.End()-1]), + TagValue: field.Tag.Value, + }, + ) + } + } + case *ast.ImportSpec: + pFile.Imports = append(pFile.Imports, strings.Trim(t.Path.Value, "\"")) + } + + return true + }, + ) + + return pFile, nil +} + +func Parse1(modPth, rootPth, source string) (*File, error) { + dir, _ := filepath.Split(source) + + pkg := filepath.Clean(strings.Replace(dir, rootPth, modPth, -1)) + + fSet := token.NewFileSet() + src, err := os.ReadFile(source) + if err != nil { + return nil, fmt.Errorf("os.ReadFile: %w", err) + } + + file, err := parser.ParseFile(fSet, source, src, 0) + if err != nil { + return nil, err + } + + packageName := file.Name.Name + if packageName == "main" { + packageName = modPth + } else { + packageName = pkg + } + + // tracing.Config + pFile := &File{ + FilePath: source, + PackageName: packageName, + Fields: make([]Field, 0), + } + + ast.Inspect( + file, func(n ast.Node) bool { + switch t := n.(type) { + case *ast.TypeSpec: + e, ok := t.Type.(*ast.StructType) + if ok && token.IsExported(t.Name.Name) { + if e.Fields == nil || e.Fields.NumFields() < 1 { + // skip empty structs + return true + } + + for _, field := range e.Fields.List { + if len(field.Names) == 0 || !token.IsExported(field.Names[0].Name) || field.Tag == nil { + continue + } + + pFile.Fields = append( + pFile.Fields, Field{ + StructName: t.Name.Name, + FieldName: field.Names[0].Name, + CommentName: field.Names[0].Name, + FieldType: string(src[field.Type.Pos()-1 : field.Type.End()-1]), + TagValue: field.Tag.Value, + }, + ) + } + } + case *ast.ImportSpec: + pFile.Imports = append(pFile.Imports, strings.Trim(t.Path.Value, "\"")) + } + + return true + }, + ) + + return pFile, nil +} + +func LookupTagValue(tag, key string) (name string, flags []string, ok bool) { + raw := strings.Trim(tag, "`") + + value, ok := reflect.StructTag(raw).Lookup(key) + if !ok { + return value, nil, ok + } + + values := strings.Split(value, ",") + + if len(values) < 1 { + return "", nil, true + } + + return values[0], values[1:], true +} + +func HasTagFlag(flags []string, query string) bool { + for _, flag := range flags { + if flag == query { + return true + } + } + + return false +} diff --git a/internal/printer/print.go b/internal/printer/print.go new file mode 100644 index 0000000..a63be8d --- /dev/null +++ b/internal/printer/print.go @@ -0,0 +1,62 @@ +package printer + +import ( + "bytes" + "fmt" + + "gituhb.com/robotomize/go-printenv/internal/analysis" +) + +type Printer interface { + Print() []byte +} + +type Option func(*Options) + +type Options struct { + groupByPkg bool + groupByModule bool + noDefault bool +} + +func WithGroup() Option { + return func(p *Options) { + p.groupByPkg = true + } +} + +func WithoutDefault() Option { + return func(options *Options) { + options.noDefault = true + } +} + +func WithGroupByModule() Option { + return func(options *Options) { + options.groupByModule = true + } +} + +func New(items []analysis.OutputEntry, opts ...Option) Printer { + p := &printer{items: items} + + for _, o := range opts { + o(&p.opts) + } + + return p +} + +type printer struct { + opts Options + items []analysis.OutputEntry +} + +func (p *printer) Print() []byte { + buf := bytes.NewBufferString("") + for _, e := range p.items { + buf.WriteString(fmt.Sprintf("%s\n", e.String())) + } + + return buf.Bytes() +} diff --git a/internal/slice/slice.go b/internal/slice/slice.go new file mode 100644 index 0000000..c772762 --- /dev/null +++ b/internal/slice/slice.go @@ -0,0 +1,46 @@ +package slice + +func MapValuesToSlice[K comparable, V any](m map[K]V) []V { + output := make([]V, 0, len(m)) + for _, v := range m { + output = append(output, v) + } + + return output +} + +func Find[T any](list []T, f func(t T) bool) (T, bool) { + var found T + for idx := range list { + if ok := f(list[idx]); ok { + return list[idx], true + } + } + + return found, false +} + +func Map[T, R any](list []T, f func(t T) R) []R { + if f == nil { + return make([]R, 0) + } + + output := make([]R, 0, len(list)) + + for idx := range list { + output = append(output, f(list[idx])) + } + + return output +} + +func Filter[T comparable](arr []T, filterFn func(v T) bool) []T { + output := make([]T, 0, len(arr)) + for _, v := range arr { + if filterFn == nil || filterFn(v) { + output = append(output, v) + } + } + + return output +}