Skip to content

Commit d552c70

Browse files
committed
♻️ Synced dbin 📦 <-- misc/cmd/goScrap: add --json ⌚
1 parent 708782e commit d552c70

File tree

1 file changed

+197
-25
lines changed

1 file changed

+197
-25
lines changed

misc/cmd/goScrap/goScrap.go

Lines changed: 197 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,33 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"os"
78
"path/filepath"
89
"strings"
10+
"sort"
911

1012
"github.com/urfave/cli/v3"
1113
)
1214

15+
type DirectoryInfo struct {
16+
Suffix string `json:"suffix"`
17+
Path string `json:"path"`
18+
FullPath string `json:"full_path"`
19+
IsCommand bool `json:"is_command"`
20+
IsInternal bool `json:"is_internal"`
21+
}
22+
23+
type DetectionResult struct {
24+
RootDir string `json:"root_dir"`
25+
Directories []*DirectoryInfo `json:"directories"`
26+
}
27+
1328
func main() {
1429
app := &cli.Command{
1530
Name: "goScrap",
16-
Usage: "Detects Go CLI programs and generates appropriate go build commands",
31+
Usage: "Detects Go CLI programs and generates appropriate go build or install commands",
1732
Flags: []cli.Flag{
1833
&cli.BoolFlag{
1934
Name: "verbose",
@@ -31,6 +46,12 @@ func main() {
3146
Usage: "Specify output directory or file for go build commands",
3247
Value: "",
3348
},
49+
&cli.BoolFlag{
50+
Name: "json",
51+
Aliases: []string{"j"},
52+
Usage: "Output results in JSON format",
53+
Value: false,
54+
},
3455
&cli.BoolFlag{
3556
Name: "relative",
3657
Usage: "Use relative paths in build commands (default: absolute paths)",
@@ -39,6 +60,25 @@ func main() {
3960
},
4061
Action: detectAction,
4162
},
63+
{
64+
Name: "install",
65+
Usage: "Generate go install commands for detected CLI programs",
66+
Flags: []cli.Flag{
67+
&cli.StringFlag{
68+
Name: "target",
69+
Aliases: []string{"t"},
70+
Usage: "Specify target version or branch (latest, main, master)",
71+
Value: "latest",
72+
},
73+
&cli.BoolFlag{
74+
Name: "json",
75+
Aliases: []string{"j"},
76+
Usage: "Output results in JSON format",
77+
Value: false,
78+
},
79+
},
80+
Action: installAction,
81+
},
4282
},
4383
}
4484

@@ -52,6 +92,7 @@ func detectAction(ctx context.Context, c *cli.Command) error {
5292
verbose := c.Bool("verbose")
5393
output := c.String("output")
5494
useRelative := c.Bool("relative")
95+
useJSON := c.Bool("json")
5596
rootDir := c.Args().First()
5697
if rootDir == "" {
5798
var err error
@@ -79,9 +120,92 @@ func detectAction(ctx context.Context, c *cli.Command) error {
79120
return fmt.Errorf("%s is not a directory", absRootDir)
80121
}
81122

82-
var buildCommands []string
123+
result, err := detectGoCLIs(absRootDir, currentDir, output, useRelative, verbose)
124+
if err != nil {
125+
return err
126+
}
127+
128+
if len(result.Directories) == 0 {
129+
return fmt.Errorf("no valid Go CLI programs found in %s", absRootDir)
130+
}
131+
132+
if useJSON {
133+
outputJSON, err := json.MarshalIndent(result, "", " ")
134+
if err != nil {
135+
return fmt.Errorf("failed to marshal JSON: %w", err)
136+
}
137+
fmt.Println(string(outputJSON))
138+
return nil
139+
}
140+
141+
for _, dir := range result.Directories {
142+
outputPath := generateOutputPath(dir.FullPath, output, filepath.Base(dir.FullPath), absRootDir, useRelative)
143+
fmt.Println(generateBuildCommand(dir.FullPath, outputPath, useRelative))
144+
}
145+
return nil
146+
}
147+
148+
func installAction(ctx context.Context, c *cli.Command) error {
149+
verbose := c.Bool("verbose")
150+
target := c.String("target")
151+
useJSON := c.Bool("json")
152+
rootDir := c.Args().First()
153+
if rootDir == "" {
154+
var err error
155+
rootDir, err = os.Getwd()
156+
if err != nil {
157+
return fmt.Errorf("failed to get current directory: %w", err)
158+
}
159+
}
160+
161+
absRootDir, err := filepath.Abs(rootDir)
162+
if err != nil {
163+
return fmt.Errorf("failed to get absolute path: %w", err)
164+
}
165+
166+
info, err := os.Stat(absRootDir)
167+
if err != nil {
168+
return fmt.Errorf("invalid root directory: %w", err)
169+
}
170+
if !info.IsDir() {
171+
return fmt.Errorf("%s is not a directory", absRootDir)
172+
}
173+
174+
result, err := detectGoCLIs(absRootDir, absRootDir, "", false, verbose)
175+
if err != nil {
176+
return err
177+
}
178+
179+
if len(result.Directories) == 0 {
180+
return fmt.Errorf("no valid Go CLI programs found in %s", absRootDir)
181+
}
182+
183+
goModPath, err := findGoModPath(absRootDir)
184+
if err != nil {
185+
return fmt.Errorf("failed to find go.mod: %w", err)
186+
}
187+
188+
if useJSON {
189+
outputJSON, err := json.MarshalIndent(result, "", " ")
190+
if err != nil {
191+
return fmt.Errorf("failed to marshal JSON: %w", err)
192+
}
193+
fmt.Println(string(outputJSON))
194+
return nil
195+
}
196+
197+
for _, dir := range result.Directories {
198+
fmt.Println(generateInstallCommand(goModPath, dir.FullPath, target))
199+
}
200+
return nil
201+
}
202+
203+
func detectGoCLIs(rootDir, currentDir, output string, useRelative, verbose bool) (*DetectionResult, error) {
204+
var result DetectionResult
205+
result.RootDir = rootDir
83206
hasGoFiles := false
84-
err = filepath.Walk(absRootDir, func(path string, info os.FileInfo, err error) error {
207+
208+
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
85209
if err != nil {
86210
return err
87211
}
@@ -92,39 +216,47 @@ func detectAction(ctx context.Context, c *cli.Command) error {
92216
}
93217
if isValidGoCLIDir(path, verbose) {
94218
hasGoFiles = true
95-
// use relative path or not?
96-
var cmdPath string
97-
if useRelative {
98-
cmdPath, err = filepath.Rel(currentDir, path)
99-
if err != nil {
100-
return fmt.Errorf("failed to get relative path from %s to %s: %w", currentDir, path, err)
101-
}
102-
if cmdPath == "." {
103-
cmdPath = ""
104-
}
105-
} else {
106-
cmdPath = path
219+
dirInfo := &DirectoryInfo{
220+
FullPath: path,
221+
IsCommand: true,
222+
}
223+
if strings.Contains(path, "/internal/") || strings.HasPrefix(filepath.Base(path), "internal") {
224+
dirInfo.IsInternal = true
107225
}
108-
outputPath := generateOutputPath(path, output, info.Name(), absRootDir, useRelative)
109-
cmd := generateBuildCommand(cmdPath, outputPath, useRelative)
110-
buildCommands = append(buildCommands, cmd)
226+
relPath, err := filepath.Rel(rootDir, path)
227+
if err != nil {
228+
return fmt.Errorf("failed to get relative path from %s to %s: %w", rootDir, path, err)
229+
}
230+
dirInfo.Path = relPath
231+
dirInfo.Suffix = strings.TrimPrefix(relPath, string(os.PathSeparator))
232+
if dirInfo.Path == "." {
233+
dirInfo.Path = filepath.Base(path)
234+
dirInfo.Suffix = filepath.Base(path)
235+
}
236+
result.Directories = append(result.Directories, dirInfo)
111237
}
112238
}
113239
return nil
114240
})
115241

116242
if err != nil {
117-
return fmt.Errorf("error walking directory: %w", err)
243+
return nil, fmt.Errorf("error walking directory: %w", err)
118244
}
119245

120246
if !hasGoFiles {
121-
return fmt.Errorf("no valid Go CLI programs found in %s", absRootDir)
247+
return &result, nil
122248
}
123249

124-
for _, cmd := range buildCommands {
125-
fmt.Println(cmd)
250+
for i, dir := range result.Directories {
251+
if dir.Suffix == "." {
252+
result.Directories[i].Suffix = filepath.Base(dir.Path)
253+
}
126254
}
127-
return nil
255+
sort.Slice(result.Directories, func(i, j int) bool {
256+
return result.Directories[i].Suffix < result.Directories[j].Suffix
257+
})
258+
259+
return &result, nil
128260
}
129261

130262
func isExcludedDir(name string) bool {
@@ -146,7 +278,6 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
146278
if err != nil {
147279
return err
148280
}
149-
// dont check subdirs
150281
if info.IsDir() && path != dir {
151282
return filepath.SkipDir
152283
}
@@ -165,7 +296,6 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
165296
if strings.HasPrefix(trimmed, "package main") {
166297
hasMain = true
167298
}
168-
// overkill? Maybe...
169299
if strings.Contains(trimmed, "func main()") {
170300
hasFuncMain = true
171301
}
@@ -183,6 +313,38 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
183313
return hasMain && hasFuncMain && hasValidGoFiles
184314
}
185315

316+
func findGoModPath(rootDir string) (string, error) {
317+
var goModPath string
318+
err := filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
319+
if err != nil {
320+
return err
321+
}
322+
if !info.IsDir() && info.Name() == "go.mod" {
323+
goModPath = path
324+
return filepath.SkipDir
325+
}
326+
return nil
327+
})
328+
if err != nil {
329+
return "", fmt.Errorf("error searching for go.mod: %w", err)
330+
}
331+
if goModPath == "" {
332+
return "", fmt.Errorf("no go.mod file found in %s or its subdirectories", rootDir)
333+
}
334+
335+
content, err := os.ReadFile(goModPath)
336+
if err != nil {
337+
return "", fmt.Errorf("failed to read go.mod: %w", err)
338+
}
339+
lines := strings.Split(string(content), "\n")
340+
for _, line := range lines {
341+
if strings.HasPrefix(strings.TrimSpace(line), "module ") {
342+
return strings.TrimSpace(strings.TrimPrefix(line, "module ")), nil
343+
}
344+
}
345+
return "", fmt.Errorf("no module path found in go.mod")
346+
}
347+
186348
func generateOutputPath(dir, output, dirName, rootDir string, useRelative bool) string {
187349
if output == "" {
188350
return filepath.Join(dir, dirName)
@@ -214,3 +376,13 @@ func generateBuildCommand(cmdPath, outputPath string, useRelative bool) string {
214376
}
215377
return fmt.Sprintf("go build -C %s -o %s", cmdPath, outputPath)
216378
}
379+
380+
func generateInstallCommand(modulePath, cmdPath, target string) string {
381+
suffix := strings.TrimPrefix(cmdPath, filepath.Dir(modulePath))
382+
if suffix == "" || suffix == "." {
383+
suffix = "/cmd/" + filepath.Base(cmdPath)
384+
} else {
385+
suffix = "/cmd/" + strings.TrimPrefix(suffix, string(os.PathSeparator))
386+
}
387+
return fmt.Sprintf("go install %s%s@%s", modulePath, suffix, target)
388+
}

0 commit comments

Comments
 (0)