feat: refactor to proper Go structure and add folder/recursive support

- Refactor to cmd/internal structure following Go best practices
- Add support for processing directories (files in folder)
- Add -r/--recursive flag for recursive directory traversal
- Split code into logical packages: types, processor, languages
- Fix --version flag to work properly with kong
- Update build path in GitHub Actions workflow
- Version bump to 0.1.0 for new major features
This commit is contained in:
carraes
2025-07-03 14:25:18 -03:00
parent 82b967c7c3
commit 37bbe92319
5 changed files with 203 additions and 123 deletions
+3 -1
View File
@@ -16,7 +16,9 @@
"Bash(gh repo edit:*)", "Bash(gh repo edit:*)",
"Bash(rm:*)", "Bash(rm:*)",
"Bash(gh run view:*)", "Bash(gh run view:*)",
"Bash(gemini:*)" "Bash(gemini:*)",
"Bash(gh release delete:*)",
"Bash(git reset:*)"
], ],
"deny": [] "deny": []
} }
+1 -1
View File
@@ -57,7 +57,7 @@ jobs:
CGO_ENABLED: 0 CGO_ENABLED: 0
CC: ${{ matrix.platform.arch == 'aarch64' && matrix.platform.os == 'linux' && 'aarch64-linux-gnu-gcc' || '' }} CC: ${{ matrix.platform.arch == 'aarch64' && matrix.platform.os == 'linux' && 'aarch64-linux-gnu-gcc' || '' }}
run: | run: |
go build -ldflags="-w -s" -o shush . go build -ldflags="-w -s" -o shush ./cmd/shush
- name: Prepare binary - name: Prepare binary
shell: bash shell: bash
+76
View File
@@ -0,0 +1,76 @@
package processor
import (
"fmt"
"path/filepath"
"strings"
"github.com/carlosarraes/shush/internal/types"
)
var languageMap = map[string]types.Language{
"lua": {LineComment: "--"},
"py": {LineComment: "#"},
"sh": {LineComment: "#"},
"js": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"ts": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"go": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"c": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"cpp": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"java": {LineComment: "//", BlockComment: &types.BlockComment{Start: "/*", End: "*/"}},
"rb": {LineComment: "#"},
"pl": {LineComment: "#"},
"yml": {LineComment: "#"},
"yaml": {LineComment: "#"},
}
func DetectLanguage(filename string) (types.Language, error) {
ext := strings.ToLower(filepath.Ext(filename))
if ext == "" {
return types.Language{}, fmt.Errorf("no file extension found")
}
ext = strings.TrimPrefix(ext, ".")
if language, ok := languageMap[ext]; ok {
return language, nil
}
return types.Language{}, fmt.Errorf("unsupported file extension: %s", ext)
}
func GetLanguageName(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
ext = strings.TrimPrefix(ext, ".")
names := map[string]string{
"lua": "Lua",
"py": "Python",
"sh": "Shell",
"js": "JavaScript",
"ts": "TypeScript",
"go": "Go",
"c": "C",
"cpp": "C++",
"java": "Java",
"rb": "Ruby",
"pl": "Perl",
"yml": "YAML",
"yaml": "YAML",
}
if name, ok := names[ext]; ok {
return name
}
return ext
}
func IsSupportedFile(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
if ext == "" {
return false
}
ext = strings.TrimPrefix(ext, ".")
_, ok := languageMap[ext]
return ok
}
+99 -121
View File
@@ -1,87 +1,106 @@
package main package processor
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/alecthomas/kong" "github.com/carlosarraes/shush/internal/types"
"github.com/fatih/color" "github.com/fatih/color"
) )
type CLI struct { type Processor struct {
File string `arg:"" name:"file" help:"Source code file to process"` cli types.CLI
Inline bool `help:"Remove only line comments"`
Block bool `help:"Remove only block comments"`
DryRun bool `help:"Show what would be removed without making changes"`
Backup bool `help:"Create backup file before modification"`
Verbose bool `help:"Show detailed output"`
Version bool `help:"Show version information"`
} }
type Language struct { func New(cli types.CLI) *Processor {
LineComment string return &Processor{cli: cli}
BlockComment *BlockComment
} }
type BlockComment struct { func (p *Processor) Process() error {
Start string info, err := os.Stat(p.cli.Path)
End string if os.IsNotExist(err) {
return fmt.Errorf("path not found: %s", p.cli.Path)
} }
var languageMap = map[string]Language{
"lua": {LineComment: "--"},
"py": {LineComment: "#"},
"sh": {LineComment: "#"},
"js": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"ts": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"go": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"c": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"cpp": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"java": {LineComment: "//", BlockComment: &BlockComment{Start: "/*", End: "*/"}},
"rb": {LineComment: "#"},
"pl": {LineComment: "#"},
"yml": {LineComment: "#"},
"yaml": {LineComment: "#"},
}
func main() {
var cli CLI
kong.Parse(&cli, kong.Description("Remove comments from source code files"))
if cli.Version {
fmt.Println("shush version 0.0.2")
return
}
if cli.Inline && cli.Block {
fmt.Fprintf(os.Stderr, "Error: --inline and --block flags are mutually exclusive\n")
os.Exit(1)
}
if err := processFile(cli); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
func processFile(cli CLI) error {
if _, err := os.Stat(cli.File); os.IsNotExist(err) {
return fmt.Errorf("file not found: %s", cli.File)
}
language, err := detectLanguage(cli.File)
if err != nil { if err != nil {
return err return err
} }
if cli.Verbose { if info.IsDir() {
fmt.Printf("Processing %s...\n", cli.File) return p.processDirectory(p.cli.Path)
fmt.Printf("Detected language: %s\n", getLanguageName(cli.File)) }
return p.processFile(p.cli.Path)
}
func (p *Processor) processDirectory(dirPath string) error {
var files []string
if p.cli.Recursive {
err := filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() && IsSupportedFile(path) {
files = append(files, path)
}
return nil
})
if err != nil {
return err
}
} else {
entries, err := os.ReadDir(dirPath)
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() {
fullPath := filepath.Join(dirPath, entry.Name())
if IsSupportedFile(fullPath) {
files = append(files, fullPath)
}
}
}
}
if len(files) == 0 {
return fmt.Errorf("no supported files found in directory: %s", dirPath)
}
if p.cli.Verbose {
fmt.Printf("Found %d supported files to process\n", len(files))
}
for _, file := range files {
if p.cli.Verbose {
fmt.Printf("Processing: %s\n", file)
}
if err := p.processFile(file); err != nil {
fmt.Fprintf(os.Stderr, "Error processing %s: %v\n", file, err)
continue
}
}
return nil
}
func (p *Processor) processFile(filename string) error {
language, err := DetectLanguage(filename)
if err != nil {
return err
}
if p.cli.Verbose {
fmt.Printf("Processing %s...\n", filename)
fmt.Printf("Detected language: %s\n", GetLanguageName(filename))
if language.BlockComment != nil { if language.BlockComment != nil {
fmt.Printf("Comment types: line (%s), block (%s %s)\n", fmt.Printf("Comment types: line (%s), block (%s %s)\n",
language.LineComment, language.BlockComment.Start, language.BlockComment.End) language.LineComment, language.BlockComment.Start, language.BlockComment.End)
@@ -90,88 +109,47 @@ func processFile(cli CLI) error {
} }
} }
if cli.DryRun { if p.cli.DryRun {
return showPreview(cli.File, language, cli) return p.showPreview(filename, language)
} }
sedCmd := buildSedCommand(language, cli) sedCmd := p.buildSedCommand(language)
if cli.Backup { if p.cli.Backup {
if err := createBackup(cli.File); err != nil { if err := p.createBackup(filename); err != nil {
return fmt.Errorf("failed to create backup: %v", err) return fmt.Errorf("failed to create backup: %v", err)
} }
if cli.Verbose { if p.cli.Verbose {
fmt.Printf("✓ Backup created: %s.bak\n", cli.File) fmt.Printf("✓ Backup created: %s.bak\n", filename)
} }
} }
if cli.Verbose { if p.cli.Verbose {
fmt.Printf("Executing: %s\n", sedCmd) fmt.Printf("Executing: %s\n", sedCmd)
} }
cmd := exec.Command("sed", "-i", sedCmd, cli.File) cmd := exec.Command("sed", "-i", sedCmd, filename)
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("sed command failed: %v", err) return fmt.Errorf("sed command failed: %v", err)
} }
if cli.Verbose { if p.cli.Verbose {
fmt.Printf("✓ Comments removed from %s\n", cli.File) fmt.Printf("✓ Comments removed from %s\n", filename)
} }
return nil return nil
} }
func detectLanguage(filename string) (Language, error) { func (p *Processor) buildSedCommand(language types.Language) string {
ext := strings.ToLower(filepath.Ext(filename))
if ext == "" {
return Language{}, fmt.Errorf("no file extension found")
}
ext = strings.TrimPrefix(ext, ".")
if language, ok := languageMap[ext]; ok {
return language, nil
}
return Language{}, fmt.Errorf("unsupported file extension: %s", ext)
}
func getLanguageName(filename string) string {
ext := strings.ToLower(filepath.Ext(filename))
ext = strings.TrimPrefix(ext, ".")
names := map[string]string{
"lua": "Lua",
"py": "Python",
"sh": "Shell",
"js": "JavaScript",
"ts": "TypeScript",
"go": "Go",
"c": "C",
"cpp": "C++",
"java": "Java",
"rb": "Ruby",
"pl": "Perl",
"yml": "YAML",
"yaml": "YAML",
}
if name, ok := names[ext]; ok {
return name
}
return ext
}
func buildSedCommand(language Language, cli CLI) string {
var commands []string var commands []string
if !cli.Block && language.LineComment != "" { if !p.cli.Block && language.LineComment != "" {
escaped := escapeForSed(language.LineComment) escaped := escapeForSed(language.LineComment)
commands = append(commands, fmt.Sprintf("/^[[:space:]]*%s/d", escaped)) commands = append(commands, fmt.Sprintf("/^[[:space:]]*%s/d", escaped))
commands = append(commands, fmt.Sprintf("s/%s.*//g", escaped)) commands = append(commands, fmt.Sprintf("s/%s.*//g", escaped))
} }
if !cli.Inline && language.BlockComment != nil { if !p.cli.Inline && language.BlockComment != nil {
startEscaped := escapeForSed(language.BlockComment.Start) startEscaped := escapeForSed(language.BlockComment.Start)
endEscaped := escapeForSed(language.BlockComment.End) endEscaped := escapeForSed(language.BlockComment.End)
commands = append(commands, fmt.Sprintf("s/%s.*%s//g", startEscaped, endEscaped)) commands = append(commands, fmt.Sprintf("s/%s.*%s//g", startEscaped, endEscaped))
@@ -197,7 +175,7 @@ func escapeForSed(pattern string) string {
return replacer.Replace(pattern) return replacer.Replace(pattern)
} }
func createBackup(filename string) error { func (p *Processor) createBackup(filename string) error {
backupName := filename + ".bak" backupName := filename + ".bak"
srcFile, err := os.Open(filename) srcFile, err := os.Open(filename)
@@ -230,7 +208,7 @@ func createBackup(filename string) error {
return nil return nil
} }
func showPreview(filename string, language Language, cli CLI) error { func (p *Processor) showPreview(filename string, language types.Language) error {
file, err := os.Open(filename) file, err := os.Open(filename)
if err != nil { if err != nil {
return err return err
@@ -249,11 +227,11 @@ func showPreview(filename string, language Language, cli CLI) error {
// Compile regex patterns once // Compile regex patterns once
var lineRegex, blockStartRegex, blockEndRegex *regexp.Regexp var lineRegex, blockStartRegex, blockEndRegex *regexp.Regexp
if language.LineComment != "" && !cli.Block { if language.LineComment != "" && !p.cli.Block {
escaped := regexp.QuoteMeta(language.LineComment) escaped := regexp.QuoteMeta(language.LineComment)
lineRegex = regexp.MustCompile(fmt.Sprintf(`^\s*%s|%s.*$`, escaped, escaped)) lineRegex = regexp.MustCompile(fmt.Sprintf(`^\s*%s|%s.*$`, escaped, escaped))
} }
if language.BlockComment != nil && !cli.Inline { if language.BlockComment != nil && !p.cli.Inline {
blockStartRegex = regexp.MustCompile(regexp.QuoteMeta(language.BlockComment.Start)) blockStartRegex = regexp.MustCompile(regexp.QuoteMeta(language.BlockComment.Start))
blockEndRegex = regexp.MustCompile(regexp.QuoteMeta(language.BlockComment.End)) blockEndRegex = regexp.MustCompile(regexp.QuoteMeta(language.BlockComment.End))
} }
+24
View File
@@ -0,0 +1,24 @@
package types
import "github.com/alecthomas/kong"
type CLI struct {
Path string `arg:"" name:"path" help:"Source code file or directory to process" optional:""`
Inline bool `help:"Remove only line comments"`
Block bool `help:"Remove only block comments"`
Recursive bool `short:"r" help:"Process directories recursively"`
DryRun bool `help:"Show what would be removed without making changes"`
Backup bool `help:"Create backup files before modification"`
Verbose bool `help:"Show detailed output"`
Version kong.VersionFlag `help:"Show version information"`
}
type Language struct {
LineComment string
BlockComment *BlockComment
}
type BlockComment struct {
Start string
End string
}