diff --git a/CLAUDE.md b/CLAUDE.md index 71cb65b..25e34d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -reflag is a Go CLI tool that translates command-line flags between traditional UNIX tools and their modern replacements. It supports 8 translators covering common tools like ls, find, grep, du, ps, dig, less, and more. +reflag is a Go CLI tool that translates command-line flags between traditional UNIX tools and their modern replacements. It supports 9 translators covering common tools like cat, ls, find, grep, du, ps, dig, less, and more. ## Build and Test Commands @@ -38,6 +38,7 @@ reflag/ │ ├── translator.go # Translator interface │ ├── registry.go # Global translator registry │ ├── translator_test.go # Registry tests +│ ├── cat2bat/ # cat → bat (plain mode) │ ├── ls2eza/ # ls → eza (BSD/GNU modes) │ ├── find2fd/ # find → fd (glob→regex conversion) │ ├── grep2rg/ # grep → ripgrep @@ -74,6 +75,7 @@ reflag/ | Translator | Source | Target | IncludeInInit | Notes | |------------|--------|--------|---------------|-------| +| cat2bat | cat | bat | true | Plain mode with auto colorization | | ls2eza | ls | eza | true | BSD/GNU mode detection | | find2fd | find | fd | true | Glob-to-regex conversion | | grep2rg | grep | ripgrep | true | Pattern handling | @@ -113,6 +115,7 @@ These flags have different meanings between BSD and GNU ls: ### Other Translators +- **cat2bat**: Converts cat commands to bat with plain mode flags (-p, --paging=never, --color=auto) to make bat behave like cat while preserving auto colorization - **find2fd**: Converts find expressions to fd syntax, including glob-to-regex pattern conversion - **grep2rg**: Translates grep flags to ripgrep, handles include/exclude patterns - **du2dust**: Converts du flags including unit/block size mappings diff --git a/README.md b/README.md index edd2d6e..98d24b2 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ A tool that translates command-line flags between different CLI tools. Currently supports: +- `cat` → [bat](https://github.com/sharkdp/bat) - `ls` → [eza](https://github.com/eza-community/eza) - `grep` → [ripgrep](https://github.com/BurntSushi/ripgrep) - `find` → [fd](https://github.com/sharkdp/fd) @@ -58,16 +59,16 @@ Install the tools you want to use. For example: ```bash # macOS -brew install eza fd ripgrep dust procs doggo moor duf +brew install bat eza fd ripgrep dust procs doggo moor duf # Linux (Ubuntu/Debian) -sudo apt install eza fd-find ripgrep dust procs +sudo apt install bat eza fd-find ripgrep dust procs # Linux (Fedora) -sudo dnf install eza fd-find ripgrep dust procs +sudo dnf install bat eza fd-find ripgrep dust procs # Arch Linux -sudo pacman -S eza fd ripgrep dust procs +sudo pacman -S bat eza fd ripgrep dust procs ``` **Note:** `moor` may need to be installed separately on some platforms. See the [moor installation guide](https://github.com/walles/moor#installing). @@ -94,14 +95,14 @@ echo 'reflag --init fish | source' >> ~/.config/fish/config.fish && source ~/.co ### 4. Start using your familiar commands ```bash -ls -ltr # Uses eza under the hood -grep -rni TODO # Uses ripgrep -find . -name '*.go' # Uses fd -df -h # Uses duf -du -h # Uses dust -ps aux # Uses procs +ls -ltr # Uses eza under the hood +grep -rni TODO # Uses ripgrep +find . -name '*.go' # Uses fd +df -h # Uses duf +du -h # Uses dust +ps aux # Uses procs dig example.com MX # Uses doggo -less -S file.txt # Uses moor +less -S file.txt # Uses moor ``` That's it! Your muscle memory still works, but you get modern tool output. @@ -315,6 +316,68 @@ ls and eza have opposite default sort orders for time and size sorting. reflag a | `-w` | Raw non-printable chars (ignored) | Output width (`-w COLS`) | | `-D` | Date format (`-D FORMAT`) | Dired mode (ignored) | +## cat2bat Translator + +The cat2bat translator converts `cat` commands to `bat` with flags that make bat behave like cat. + +### Key Features + +- **Plain output**: Always adds `-p` (plain style) to disable bat's decorations like line numbers, grid borders, and file headers +- **No paging**: Adds `--paging=never` to disable bat's automatic paging behavior +- **Smart colorization**: Uses `--color=auto` to enable syntax highlighting when output goes to a terminal, but disables it when piped or redirected + +### Supported Flags + +| cat flag | bat equivalent | Description | +|----------|----------------|-------------| +| `-n` | `-n` | Number all output lines | +| `--number` | `-n` | Number all output lines | +| `-s` | `-s` | Squeeze multiple blank lines into one | +| `--squeeze-blank` | `-s` | Squeeze multiple blank lines | +| `-A` | `-A` | Show non-printable characters | +| `--show-all` | `-A` | Show non-printable characters | +| `-u` | `-u` | Unbuffered output | +| `--unbuffered` | `-u` | Unbuffered output | + +### Ignored Flags + +All bat-specific flags are ignored since the goal is cat-like behavior: +- Syntax highlighting options (`-l`, `--language`, `--theme`) +- Line highlighting (`-H`, `--highlight-line`) +- Decorations (`--style`, `--decorations`) +- Git integration (`-d`, `--diff`) +- File metadata (`--file-name`) +- And other bat-specific features + +### Examples + +```bash +$ reflag cat bat README.md +bat -p --paging=never --color=auto README.md + +$ reflag cat bat -n file.txt +bat -p --paging=never --color=auto -n file.txt + +$ reflag cat bat -ns file1.txt file2.txt +bat -p --paging=never --color=auto -n -s file1.txt file2.txt + +# Color output when viewing in terminal +$ reflag cat bat file.rs +# Shows syntax-highlighted Rust code + +# No color when piping to other commands +$ reflag cat bat file.rs | grep "fn" +# Plain output suitable for grep +``` + +### Why Use This? + +While bat is designed to be a cat replacement with syntax highlighting, sometimes you want: +- The simplicity of cat's output +- Syntax highlighting only when appropriate (terminal vs pipe) +- To maintain muscle memory for cat flags +- A gradual transition from cat to bat + ## grep2rg Translator The grep2rg translator converts `grep` flags to `rg` (ripgrep) equivalents. diff --git a/main.go b/main.go index 6e9175a..80372af 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/kluzzebass/reflag/translator" + _ "github.com/kluzzebass/reflag/translator/bat2cat" // Register bat2cat translator _ "github.com/kluzzebass/reflag/translator/df2duf" // Register df2duf translator _ "github.com/kluzzebass/reflag/translator/dig2doggo" // Register dig2doggo translator _ "github.com/kluzzebass/reflag/translator/du2dust" // Register du2dust translator diff --git a/translator/bat2cat/translator.go b/translator/bat2cat/translator.go new file mode 100644 index 0000000..e3f7e4e --- /dev/null +++ b/translator/bat2cat/translator.go @@ -0,0 +1,174 @@ +package bat2cat + +import ( + "strings" + + "github.com/kluzzebass/reflag/translator" +) + +func init() { + translator.Register(&Translator{}) +} + +// Translator implements the cat to bat flag translation +type Translator struct{} + +func (t *Translator) Name() string { return "cat2bat" } +func (t *Translator) SourceTool() string { return "cat" } +func (t *Translator) TargetTool() string { return "bat" } +func (t *Translator) IncludeInInit() bool { return false } + +// Translate converts cat arguments to bat arguments to make bat behave like cat +func (t *Translator) Translate(args []string, mode string) []string { + return translateFlags(args) +} + +// Map of bat short flags to cat equivalents +var flagMap = map[rune]string{ + 'n': "-n", // --number → -n (line numbers) + 's': "-s", // --squeeze-blank → -s (squeeze blank lines) + 'u': "-u", // --unbuffered → -u (unbuffered, though bat ignores this) + 'A': "-A", // --show-all → approximates -A (show non-printable) +} + +func translateFlags(args []string) []string { + var result []string + skipNext := false + + // To make bat behave like cat, we need to: + // 1. Always add -p (plain style, no decorations) + // 2. Always add --paging=never (disable pager) + // 3. Allow default colorization with --color=auto + result = append(result, "-p", "--paging=never", "--color=auto") + + for i, arg := range args { + if skipNext { + skipNext = false + continue + } + + // Handle -- separator (everything after is files) + if arg == "--" { + result = append(result, args[i:]...) + break + } + + // Handle long options + if strings.HasPrefix(arg, "--") { + handleLongFlag(arg, args, i, &result, &skipNext) + continue + } + + // Handle short options + if strings.HasPrefix(arg, "-") && len(arg) > 1 && arg[1] != '-' { + handleShortFlags(arg, args, i, &result, &skipNext) + continue + } + + // Regular file argument + result = append(result, arg) + } + + return result +} + +func handleLongFlag(arg string, args []string, i int, result *[]string, skipNext *bool) { + // Handle --option=value format + if idx := strings.Index(arg, "="); idx != -1 { + opt := arg[:idx] + val := arg[idx+1:] + + switch opt { + case "--number": + *result = append(*result, "-n") + case "--squeeze-blank": + *result = append(*result, "-s") + case "--show-all": + *result = append(*result, "-A") + case "--file-name": + // cat doesn't have this, ignore + case "--language", "--highlight-line", "--diff-context", "--tabs", "--wrap", + "--terminal-width", "--color", "--italic-text", "--decorations", "--paging", + "--pager", "--map-syntax", "--ignored-suffix", "--theme", "--theme-light", + "--theme-dark", "--style", "--line-range", "--squeeze-limit", "--strip-ansi", + "--nonprintable-notation", "--binary": + // These are bat-specific features that cat doesn't have + // They're overridden by our plain mode settings + default: + // Unknown option, might be a file starting with -- + *result = append(*result, arg) + } + // Suppress unused variable warning + _ = val + return + } + + // Handle flags without =value + switch arg { + case "--number": + *result = append(*result, "-n") + case "--squeeze-blank": + *result = append(*result, "-s") + case "--show-all": + *result = append(*result, "-A") + case "--unbuffered": + *result = append(*result, "-u") + case "--plain", "--force-colorization", "--diff", "--list-themes", "--list-languages", + "--chop-long-lines", "--diagnostic", "--acknowledgements", "--set-terminal-title", + "--help", "--version": + // These are bat-specific, ignore or they're already handled + default: + // Check if next arg is a value for this flag + if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + switch arg { + case "--language", "--highlight-line", "--file-name", "--diff-context", + "--tabs", "--wrap", "--terminal-width", "--color", "--italic-text", + "--decorations", "--paging", "--pager", "--map-syntax", "--ignored-suffix", + "--theme", "--theme-light", "--theme-dark", "--style", "--line-range", + "--squeeze-limit", "--strip-ansi", "--nonprintable-notation", "--binary", + "--completion": + // These take values but are bat-specific, skip both flag and value + *skipNext = true + default: + // Unknown flag with potential value, keep it (might be a file) + *result = append(*result, arg) + } + } else { + // Unknown flag without value, might be a file + *result = append(*result, arg) + } + } +} + +func handleShortFlags(arg string, args []string, i int, result *[]string, skipNext *bool) { + flags := arg[1:] // Remove leading dash + + // Check for combined flags like -pp or -ns + for j, flag := range flags { + if mapped, ok := flagMap[flag]; ok { + *result = append(*result, mapped) + } else { + // Flags without direct mapping or bat-specific flags + switch flag { + case 'p': + // -p (plain) is already added by default, ignore + case 'l', 'H', 'm': + // These flags take values, skip the next argument + // Only skip if this is the last flag in a combined set + if j == len(flags)-1 && i+1 < len(args) { + *skipNext = true + } + case 'd', 'f', 'L', 'r', 'S': + // These are bat-specific flags that don't take values, ignore + case 'V': + // -V (version), ignore + case 'h': + // -h (help), ignore + default: + // Unknown single char flag + // Could be a typo or actual flag, preserve it + *result = append(*result, "-"+string(flag)) + } + } + } +} diff --git a/translator/bat2cat/translator_test.go b/translator/bat2cat/translator_test.go new file mode 100644 index 0000000..5067f4f --- /dev/null +++ b/translator/bat2cat/translator_test.go @@ -0,0 +1,234 @@ +package bat2cat + +import ( + "reflect" + "testing" +) + +func TestTranslateFlags(t *testing.T) { + tests := []struct { + name string + input []string + expected []string + }{ + // Basic usage - always adds plain mode flags + { + name: "simple file", + input: []string{"file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "multiple files", + input: []string{"file1.txt", "file2.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file1.txt", "file2.txt"}, + }, + { + name: "stdin dash", + input: []string{"-"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-"}, + }, + + // Flags that map to cat equivalents + { + name: "line numbers short", + input: []string{"-n", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "file.txt"}, + }, + { + name: "line numbers long", + input: []string{"--number", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "file.txt"}, + }, + { + name: "squeeze blank short", + input: []string{"-s", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-s", "file.txt"}, + }, + { + name: "squeeze blank long", + input: []string{"--squeeze-blank", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-s", "file.txt"}, + }, + { + name: "show all short", + input: []string{"-A", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-A", "file.txt"}, + }, + { + name: "show all long", + input: []string{"--show-all", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-A", "file.txt"}, + }, + { + name: "unbuffered short", + input: []string{"-u", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-u", "file.txt"}, + }, + { + name: "unbuffered long", + input: []string{"--unbuffered", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-u", "file.txt"}, + }, + + // Combined flags + { + name: "combined flags", + input: []string{"-ns", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "-s", "file.txt"}, + }, + { + name: "combined with number and squeeze", + input: []string{"-nsu", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "-s", "-u", "file.txt"}, + }, + + // bat-specific flags that are ignored or overridden + { + name: "plain flag ignored (already applied)", + input: []string{"-p", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "double plain ignored", + input: []string{"-pp", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "language ignored", + input: []string{"-l", "python", "file.py"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.py"}, + }, + { + name: "language long ignored", + input: []string{"--language=python", "file.py"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.py"}, + }, + { + name: "highlight ignored", + input: []string{"-H", "10:20", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "highlight long ignored", + input: []string{"--highlight-line=10:20", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "color ignored", + input: []string{"--color=always", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "theme ignored", + input: []string{"--theme=Monokai", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "style ignored", + input: []string{"--style=full", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "paging ignored", + input: []string{"--paging=always", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "line range ignored", + input: []string{"--line-range=10:20", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + + // Separator handling + { + name: "separator with files", + input: []string{"-n", "--", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "--", "file.txt"}, + }, + { + name: "separator with dash file", + input: []string{"--", "-", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "--", "-", "file.txt"}, + }, + + // Mixed valid and ignored flags + { + name: "mixed flags", + input: []string{"-n", "--color=always", "-s", "--theme=dark", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "-s", "file.txt"}, + }, + { + name: "real world example 1", + input: []string{"--style=plain", "--paging=never", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + { + name: "real world example 2", + input: []string{"-n", "--decorations=never", "--color=never", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "file.txt"}, + }, + + // Force colorization ignored + { + name: "force colorization", + input: []string{"-f", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + + // Diff mode ignored + { + name: "diff mode", + input: []string{"-d", "file.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "file.txt"}, + }, + + // Multiple files with various flags + { + name: "complex example", + input: []string{"-n", "-s", "file1.txt", "file2.txt", "file3.txt"}, + expected: []string{"-p", "--paging=never", "--color=auto", "-n", "-s", "file1.txt", "file2.txt", "file3.txt"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := translateFlags(tt.input) + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("translateFlags(%v) = %v, want %v", tt.input, result, tt.expected) + } + }) + } +} + +func TestTranslatorInterface(t *testing.T) { + tr := &Translator{} + + if tr.Name() != "cat2bat" { + t.Errorf("Name() = %v, want cat2bat", tr.Name()) + } + + if tr.SourceTool() != "cat" { + t.Errorf("SourceTool() = %v, want cat", tr.SourceTool()) + } + + if tr.TargetTool() != "bat" { + t.Errorf("TargetTool() = %v, want bat", tr.TargetTool()) + } + + if tr.IncludeInInit() { + t.Errorf("IncludeInInit() = true, want false") + } +} + +func TestTranslateMethod(t *testing.T) { + tr := &Translator{} + + // Test that Translate method calls translateFlags correctly + input := []string{"-n", "file.txt"} + expected := []string{"-p", "--paging=never", "--color=auto", "-n", "file.txt"} + + result := tr.Translate(input, "") + if !reflect.DeepEqual(result, expected) { + t.Errorf("Translate(%v, '') = %v, want %v", input, result, expected) + } +}