Skip to content

Commit 708782e

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

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed

misc/cmd/goScrap/goScrap.go

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
"github.com/urfave/cli/v3"
11+
)
12+
13+
func main() {
14+
app := &cli.Command{
15+
Name: "goScrap",
16+
Usage: "Detects Go CLI programs and generates appropriate go build commands",
17+
Flags: []cli.Flag{
18+
&cli.BoolFlag{
19+
Name: "verbose",
20+
Usage: "Enable verbose output to stderr",
21+
},
22+
},
23+
Commands: []*cli.Command{
24+
{
25+
Name: "detect",
26+
Usage: "Detect Go CLI programs and output go build commands",
27+
Flags: []cli.Flag{
28+
&cli.StringFlag{
29+
Name: "output",
30+
Aliases: []string{"o"},
31+
Usage: "Specify output directory or file for go build commands",
32+
Value: "",
33+
},
34+
&cli.BoolFlag{
35+
Name: "relative",
36+
Usage: "Use relative paths in build commands (default: absolute paths)",
37+
Value: false,
38+
},
39+
},
40+
Action: detectAction,
41+
},
42+
},
43+
}
44+
45+
if err := app.Run(context.Background(), os.Args); err != nil {
46+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
47+
os.Exit(1)
48+
}
49+
}
50+
51+
func detectAction(ctx context.Context, c *cli.Command) error {
52+
verbose := c.Bool("verbose")
53+
output := c.String("output")
54+
useRelative := c.Bool("relative")
55+
rootDir := c.Args().First()
56+
if rootDir == "" {
57+
var err error
58+
rootDir, err = os.Getwd()
59+
if err != nil {
60+
return fmt.Errorf("failed to get current directory: %w", err)
61+
}
62+
}
63+
64+
absRootDir, err := filepath.Abs(rootDir)
65+
if err != nil {
66+
return fmt.Errorf("failed to get absolute path: %w", err)
67+
}
68+
69+
currentDir, err := os.Getwd()
70+
if err != nil {
71+
return fmt.Errorf("failed to get current working directory: %w", err)
72+
}
73+
74+
info, err := os.Stat(absRootDir)
75+
if err != nil {
76+
return fmt.Errorf("invalid root directory: %w", err)
77+
}
78+
if !info.IsDir() {
79+
return fmt.Errorf("%s is not a directory", absRootDir)
80+
}
81+
82+
var buildCommands []string
83+
hasGoFiles := false
84+
err = filepath.Walk(absRootDir, func(path string, info os.FileInfo, err error) error {
85+
if err != nil {
86+
return err
87+
}
88+
89+
if info.IsDir() {
90+
if isExcludedDir(info.Name()) {
91+
return filepath.SkipDir
92+
}
93+
if isValidGoCLIDir(path, verbose) {
94+
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
107+
}
108+
outputPath := generateOutputPath(path, output, info.Name(), absRootDir, useRelative)
109+
cmd := generateBuildCommand(cmdPath, outputPath, useRelative)
110+
buildCommands = append(buildCommands, cmd)
111+
}
112+
}
113+
return nil
114+
})
115+
116+
if err != nil {
117+
return fmt.Errorf("error walking directory: %w", err)
118+
}
119+
120+
if !hasGoFiles {
121+
return fmt.Errorf("no valid Go CLI programs found in %s", absRootDir)
122+
}
123+
124+
for _, cmd := range buildCommands {
125+
fmt.Println(cmd)
126+
}
127+
return nil
128+
}
129+
130+
func isExcludedDir(name string) bool {
131+
excluded := []string{".git", "vendor", "test", "tests", "example", "examples"}
132+
for _, dir := range excluded {
133+
if strings.EqualFold(name, dir) {
134+
return true
135+
}
136+
}
137+
return false
138+
}
139+
140+
func isValidGoCLIDir(dir string, verbose bool) bool {
141+
hasMain := false
142+
hasFuncMain := false
143+
hasValidGoFiles := false
144+
145+
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
146+
if err != nil {
147+
return err
148+
}
149+
// dont check subdirs
150+
if info.IsDir() && path != dir {
151+
return filepath.SkipDir
152+
}
153+
if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") && !strings.HasSuffix(info.Name(), "_test.go") {
154+
hasValidGoFiles = true
155+
content, err := os.ReadFile(path)
156+
if err != nil {
157+
if verbose {
158+
fmt.Fprintf(os.Stderr, "Warning: could not read %s: %v\n", path, err)
159+
}
160+
return nil
161+
}
162+
lines := strings.Split(string(content), "\n")
163+
for _, line := range lines {
164+
trimmed := strings.TrimSpace(line)
165+
if strings.HasPrefix(trimmed, "package main") {
166+
hasMain = true
167+
}
168+
// overkill? Maybe...
169+
if strings.Contains(trimmed, "func main()") {
170+
hasFuncMain = true
171+
}
172+
if hasMain && hasFuncMain {
173+
return filepath.SkipDir
174+
}
175+
}
176+
}
177+
return nil
178+
})
179+
180+
if err != nil && verbose {
181+
fmt.Fprintf(os.Stderr, "Warning: error scanning directory %s: %v\n", dir, err)
182+
}
183+
return hasMain && hasFuncMain && hasValidGoFiles
184+
}
185+
186+
func generateOutputPath(dir, output, dirName, rootDir string, useRelative bool) string {
187+
if output == "" {
188+
return filepath.Join(dir, dirName)
189+
}
190+
if isDir(output) {
191+
if useRelative {
192+
relPath, _ := filepath.Rel(rootDir, dir)
193+
return filepath.Join(output, relPath, dirName)
194+
}
195+
return filepath.Join(output, dirName)
196+
}
197+
return output
198+
}
199+
200+
func isDir(path string) bool {
201+
info, err := os.Stat(path)
202+
if os.IsNotExist(err) {
203+
return true
204+
}
205+
if err != nil {
206+
return false
207+
}
208+
return info.IsDir()
209+
}
210+
211+
func generateBuildCommand(cmdPath, outputPath string, useRelative bool) string {
212+
if useRelative && cmdPath == "" {
213+
return fmt.Sprintf("go build -o %s", outputPath)
214+
}
215+
return fmt.Sprintf("go build -C %s -o %s", cmdPath, outputPath)
216+
}

0 commit comments

Comments
 (0)