feat: initial implementation of shush comment removal tool
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(go get:*)",
|
||||||
|
"Bash(go build:*)",
|
||||||
|
"Bash(./shush:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(mkdir:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(git add:*)"
|
||||||
|
],
|
||||||
|
"deny": []
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
name: Build and Release
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ matrix.platform.os }}-${{ matrix.platform.arch }}
|
||||||
|
runs-on: ${{ matrix.platform.os-type }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- os: linux
|
||||||
|
os-type: ubuntu-latest
|
||||||
|
arch: x86_64
|
||||||
|
target: x86_64-unknown-linux-musl
|
||||||
|
- os: darwin
|
||||||
|
os-type: macos-latest
|
||||||
|
arch: x86_64
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
- os: linux
|
||||||
|
os-type: ubuntu-latest
|
||||||
|
arch: aarch64
|
||||||
|
target: aarch64-unknown-linux-musl
|
||||||
|
- os: darwin
|
||||||
|
os-type: macos-latest
|
||||||
|
arch: aarch64
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Install cross-compilation tools
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.platform.os }}" = "linux" ]; then
|
||||||
|
if [ "${{ matrix.platform.arch }}" = "aarch64" ]; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y gcc-aarch64-linux-gnu
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build release binary
|
||||||
|
env:
|
||||||
|
GOOS: ${{ matrix.platform.os == 'darwin' && 'darwin' || 'linux' }}
|
||||||
|
GOARCH: ${{ matrix.platform.arch == 'x86_64' && 'amd64' || 'arm64' }}
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
CC: ${{ matrix.platform.arch == 'aarch64' && matrix.platform.os == 'linux' && 'aarch64-linux-gnu-gcc' || '' }}
|
||||||
|
run: |
|
||||||
|
go build -ldflags="-w -s" -o shush .
|
||||||
|
|
||||||
|
- name: Prepare binary
|
||||||
|
shell: bash
|
||||||
|
id: prepare_binary
|
||||||
|
run: |
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
# Set binary name based on OS and architecture
|
||||||
|
BINARY_NAME="shush-${{ matrix.platform.os }}-${{ matrix.platform.arch }}"
|
||||||
|
|
||||||
|
# Copy and rename binary
|
||||||
|
cp shush "dist/${BINARY_NAME}"
|
||||||
|
|
||||||
|
# For macOS x86_64, also provide a generic "shush" binary
|
||||||
|
if [ "${{ matrix.platform.os }}" = "darwin" ] && [ "${{ matrix.platform.arch }}" = "x86_64" ]; then
|
||||||
|
cp shush "dist/shush"
|
||||||
|
echo "asset_paths=dist/shush,dist/${BINARY_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "asset_paths=dist/${BINARY_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload binaries to release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: ${{ steps.prepare_binary.outputs.asset_paths }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Binaries
|
||||||
|
shush
|
||||||
|
*.exe
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
test.*
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Go build cache
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
vendor/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/carlosarraes/shush
|
||||||
|
|
||||||
|
go 1.24.4
|
||||||
|
|
||||||
|
require github.com/alecthomas/kong v1.12.0 // indirect
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/alecthomas/kong v1.12.0 h1:oKd/0fHSdajj5PfGDd3ScvEvpVJf9mT2mb5r9xYadYM=
|
||||||
|
github.com/alecthomas/kong v1.12.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
|
||||||
Executable
+118
@@ -0,0 +1,118 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# shush installer
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# curl -sSf https://raw.githubusercontent.com/carlosarraes/shush/main/install.sh | sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
REPO="carlosarraes/shush"
|
||||||
|
BINARY_NAME="shush"
|
||||||
|
BIN_DIR="${BIN_DIR:-$HOME/.local/bin}"
|
||||||
|
GITHUB_LATEST="https://api.github.com/repos/${REPO}/releases/latest"
|
||||||
|
|
||||||
|
get_arch() {
|
||||||
|
# detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case $ARCH in
|
||||||
|
x86_64) ARCH="x86_64" ;;
|
||||||
|
aarch64) ARCH="aarch64" ;;
|
||||||
|
arm64) ARCH="aarch64" ;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
get_os() {
|
||||||
|
# detect os
|
||||||
|
OS=$(uname -s)
|
||||||
|
case $OS in
|
||||||
|
Linux) OS="linux" ;;
|
||||||
|
Darwin) OS="darwin" ;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported OS: $OS"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
download_binary() {
|
||||||
|
# get latest release info
|
||||||
|
echo "Fetching latest release..."
|
||||||
|
VERSION=$(curl -s $GITHUB_LATEST | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4)
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Failed to fetch latest version"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Latest version: $VERSION"
|
||||||
|
|
||||||
|
# create temporary directory
|
||||||
|
TMP_DIR=$(mktemp -d)
|
||||||
|
# Ensure cleanup happens even if script fails or exits early
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
echo "Downloading ${BINARY_NAME} ${VERSION}..."
|
||||||
|
|
||||||
|
# Construct binary name based on OS and architecture
|
||||||
|
BINARY_SUFFIX="${BINARY_NAME}-${OS}-${ARCH}"
|
||||||
|
|
||||||
|
# For macOS x86_64, try generic "shush" first, then fallback to specific
|
||||||
|
if [ "$OS" = "darwin" ] && [ "$ARCH" = "x86_64" ]; then
|
||||||
|
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
|
echo "Downloading from: $DOWNLOAD_URL"
|
||||||
|
if ! curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${BINARY_NAME}" 2>/dev/null; then
|
||||||
|
echo "Generic binary not found, trying platform-specific binary..."
|
||||||
|
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_SUFFIX}"
|
||||||
|
echo "Downloading from: $DOWNLOAD_URL"
|
||||||
|
curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${BINARY_NAME}" || {
|
||||||
|
echo "Download failed. Check URL/permissions/network."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_SUFFIX}"
|
||||||
|
echo "Downloading from: $DOWNLOAD_URL"
|
||||||
|
curl -fsSL "$DOWNLOAD_URL" -o "${TMP_DIR}/${BINARY_NAME}" || {
|
||||||
|
echo "Download failed. Check URL/permissions/network."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# make it executable
|
||||||
|
chmod +x "${TMP_DIR}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
# Check if BIN_DIR exists and create if needed
|
||||||
|
CREATED_DIR_MSG=""
|
||||||
|
if [ ! -d "$BIN_DIR" ]; then
|
||||||
|
echo "Installation directory '$BIN_DIR' not found."
|
||||||
|
echo "Creating directory: $BIN_DIR"
|
||||||
|
mkdir -p "$BIN_DIR"
|
||||||
|
CREATED_DIR_MSG="Note: Created directory '$BIN_DIR'. You might need to add it to your system's PATH."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# install binary (no sudo needed for $HOME/.local/bin)
|
||||||
|
echo "Installing to $BIN_DIR..."
|
||||||
|
install -m 755 "${TMP_DIR}/${BINARY_NAME}" "$BIN_DIR"
|
||||||
|
|
||||||
|
# cleanup happens via trap
|
||||||
|
|
||||||
|
echo "${BINARY_NAME} ${VERSION} installed successfully to $BIN_DIR"
|
||||||
|
|
||||||
|
# Print the warning message if the directory was created
|
||||||
|
if [ -n "$CREATED_DIR_MSG" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "$CREATED_DIR_MSG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the installer
|
||||||
|
get_arch
|
||||||
|
get_os
|
||||||
|
download_binary
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Installation complete! Run '${BINARY_NAME} --help' to get started."
|
||||||
|
echo "Example usage: ${BINARY_NAME} file.py --backup --verbose"
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/kong"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CLI struct {
|
||||||
|
File string `arg:"" name:"file" help:"Source code file to process"`
|
||||||
|
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 {
|
||||||
|
LineComment string
|
||||||
|
BlockComment *BlockComment
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlockComment struct {
|
||||||
|
Start string
|
||||||
|
End string
|
||||||
|
}
|
||||||
|
|
||||||
|
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.1")
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.Verbose {
|
||||||
|
fmt.Printf("Processing %s...\n", cli.File)
|
||||||
|
fmt.Printf("Detected language: %s\n", getLanguageName(cli.File))
|
||||||
|
if language.BlockComment != nil {
|
||||||
|
fmt.Printf("Comment types: line (%s), block (%s %s)\n",
|
||||||
|
language.LineComment, language.BlockComment.Start, language.BlockComment.End)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Comment types: line (%s)\n", language.LineComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sedCmd := buildSedCommand(language, cli)
|
||||||
|
|
||||||
|
if cli.DryRun {
|
||||||
|
fmt.Printf("Would execute: %s\n", sedCmd)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.Backup {
|
||||||
|
if err := createBackup(cli.File); err != nil {
|
||||||
|
return fmt.Errorf("failed to create backup: %v", err)
|
||||||
|
}
|
||||||
|
if cli.Verbose {
|
||||||
|
fmt.Printf("✓ Backup created: %s.bak\n", cli.File)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.Verbose {
|
||||||
|
fmt.Printf("Executing: %s\n", sedCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("sed", "-i", sedCmd, cli.File)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("sed command failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cli.Verbose {
|
||||||
|
fmt.Printf("✓ Comments removed from %s\n", cli.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectLanguage(filename string) (Language, error) {
|
||||||
|
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
|
||||||
|
|
||||||
|
if !cli.Block && language.LineComment != "" {
|
||||||
|
escaped := escapeForSed(language.LineComment)
|
||||||
|
commands = append(commands, fmt.Sprintf("/^[[:space:]]*%s/d", escaped))
|
||||||
|
commands = append(commands, fmt.Sprintf("s/%s.*//g", escaped))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cli.Inline && language.BlockComment != nil {
|
||||||
|
startEscaped := escapeForSed(language.BlockComment.Start)
|
||||||
|
endEscaped := escapeForSed(language.BlockComment.End)
|
||||||
|
commands = append(commands, fmt.Sprintf("s/%s.*%s//g", startEscaped, endEscaped))
|
||||||
|
commands = append(commands, fmt.Sprintf("/%s/,/%s/d", startEscaped, endEscaped))
|
||||||
|
}
|
||||||
|
|
||||||
|
commands = append(commands, "/^[[:space:]]*$/d")
|
||||||
|
|
||||||
|
return strings.Join(commands, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeForSed(pattern string) string {
|
||||||
|
replacer := strings.NewReplacer(
|
||||||
|
"/", "\\/",
|
||||||
|
"*", "\\*",
|
||||||
|
".", "\\.",
|
||||||
|
"[", "\\[",
|
||||||
|
"]", "\\]",
|
||||||
|
"^", "\\^",
|
||||||
|
"$", "\\$",
|
||||||
|
"\\", "\\\\",
|
||||||
|
)
|
||||||
|
return replacer.Replace(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createBackup(filename string) error {
|
||||||
|
backupName := filename + ".bak"
|
||||||
|
|
||||||
|
srcFile, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
dstFile, err := os.Create(backupName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
buffer := make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := srcFile.Read(buffer)
|
||||||
|
if err != nil && err.Error() != "EOF" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := dstFile.Write(buffer[:n]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user