Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
85 changes: 74 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
174 changes: 174 additions & 0 deletions translator/bat2cat/translator.go
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
}
Loading
Loading