A modular code transformation framework. Each transformer handles one concern -- renaming files, normalising whitespace, converting identifier case, etc. -- and the pipeline system lets you compose them into multi-step workflows that run in a single invocation.
Every transformation is an independent module with its own options struct,
sensible defaults, and a consistent interface (process(path) -> Result).
Transformers can be used standalone via CLI subcommands, composed into
pipelines, or called directly as a Rust library from reformat-core.
| Transformer | CLI subcommand | What it does |
|---|---|---|
FileRenamer |
rename_files |
Case transforms, prefix/suffix operations, timestamps on filenames |
CaseConverter |
convert |
Convert identifiers between 6 case formats (camel, pascal, snake, screaming snake, kebab, screaming kebab) |
WhitespaceCleaner |
clean |
Strip trailing whitespace while preserving line endings |
EmojiTransformer |
emojis |
Replace task/status emojis with text alternatives, remove decorative emojis |
FileGrouper |
group |
Organise files by common prefix into subdirectories, detect and fix broken references |
EndingsNormalizer |
endings |
Normalise line endings to LF, CRLF, or CR (skips binary files automatically) |
IndentNormalizer |
indent |
Convert between tabs and spaces with configurable width, tab-stop-aware |
ContentReplacer |
replace |
Regex find-and-replace with capture group support, multiple sequential patterns |
HeaderManager |
header |
Insert or update file headers (license, copyright) with year templating |
All transformers share common behaviours: recursive directory traversal,
file extension filtering, dry-run mode, and automatic skipping of hidden files
and build directories (.git, node_modules, target, __pycache__, etc.).
Transformers become more useful when composed. The pipeline system chains any combination of the above steps and runs them in order on the same path.
There are two ways to define a pipeline, reflecting two different needs:
- Presets (
-p) -- Reusable, named pipelines stored inreformat.jsonat the project root. Version-controlled, shared across a team, run repeatedly. - Jobs (
--job) -- Ad-hoc, throwaway pipelines loaded from any file or stdin. No project config needed. Ideal for one-off migrations, scripted CI transforms, or quick multi-pattern replacements.
Both use the same JSON format (a steps array plus per-step config) and the
same execution engine. The only difference is where they are stored.
{
"steps": ["endings", "indent", "clean", "header"],
"endings": { "style": "lf" },
"indent": { "style": "spaces", "width": 4 },
"header": {
"text": "// Copyright {year} MyOrg. All rights reserved.",
"update_year": true,
"file_extensions": [".rs", ".go"]
}
}# As a reusable preset (stored in reformat.json under a name):
reformat -p normalize src/
# As a throwaway job (from a file):
reformat --job normalize.json src/
# As a throwaway job (piped from stdin):
cat normalize.json | reformat --job - src/For the common case of cleaning up a directory, reformat <path> runs three
transformations in a single optimised pass -- rename to lowercase, replace task
emojis, strip trailing whitespace -- without needing a config file.
The project is organised as a Cargo workspace:
- reformat-core -- All transformation logic. Every struct and option type is a public API. Use this crate directly if you want programmatic access.
- reformat-cli -- Thin CLI wrapper using clap. Parses arguments, loads config, calls into core.
- reformat-plugins -- Plugin system foundation (not yet active).
- Multi-level verbosity (
-v,-vv,-vvv), quiet mode (-q), file logging (--log-file) - Progress spinners, automatic operation timing, colour-coded output
- Dry-run mode on every transformer and every pipeline step
Install from crates.io:
cargo install reformatOr install from the workspace:
cargo install --path reformat-cliOr build from source:
cargo build --release -p reformatThe binary will be at ./target/release/reformat
Add to your Cargo.toml:
[dependencies]
reformat-core = "0.1.6"use reformat_core::{CaseConverter, CaseFormat};
let converter = CaseConverter::new(
CaseFormat::CamelCase, // from
CaseFormat::SnakeCase, // to
None, // file_extensions
false, // recursive
false, // dry_run
String::new(), // prefix
String::new(), // suffix
None, // strip_prefix
None, // strip_suffix
None, // replace_prefix_from
None, // replace_prefix_to
None, // replace_suffix_from
None, // replace_suffix_to
None, // glob_pattern
None, // word_filter
)?;
converter.process_directory(std::path::Path::new("src"))?;use reformat_core::{WhitespaceCleaner, WhitespaceOptions};
let mut options = WhitespaceOptions::default();
options.dry_run = false;
options.recursive = true;
let cleaner = WhitespaceCleaner::new(options);
let (files_cleaned, lines_cleaned) = cleaner.process(std::path::Path::new("src"))?;
println!("Cleaned {} lines in {} files", lines_cleaned, files_cleaned);use reformat_core::{CombinedProcessor, CombinedOptions};
let mut options = CombinedOptions::default();
options.recursive = true;
options.dry_run = false;
let processor = CombinedProcessor::new(options);
let stats = processor.process(std::path::Path::new("src"))?;
println!("Files renamed: {}", stats.files_renamed);
println!("Emojis transformed: {} files ({} changes)",
stats.files_emoji_transformed, stats.emoji_changes);
println!("Whitespace cleaned: {} files ({} lines)",
stats.files_whitespace_cleaned, stats.whitespace_lines_cleaned);use reformat_core::{EndingsNormalizer, EndingsOptions, LineEnding};
let options = EndingsOptions {
style: LineEnding::Lf,
recursive: true,
dry_run: false,
..Default::default()
};
let normalizer = EndingsNormalizer::new(options);
let (files, endings) = normalizer.process(std::path::Path::new("src"))?;
println!("Normalized {} endings in {} files", endings, files);use reformat_core::{IndentNormalizer, IndentOptions, IndentStyle};
let options = IndentOptions {
style: IndentStyle::Spaces,
width: 4,
recursive: true,
dry_run: false,
..Default::default()
};
let normalizer = IndentNormalizer::new(options);
let (files, lines) = normalizer.process(std::path::Path::new("src"))?;
println!("Normalized {} lines in {} files", lines, files);use reformat_core::{ContentReplacer, ReplaceOptions, ReplacePattern};
let options = ReplaceOptions {
patterns: vec![
ReplacePattern {
find: r"old_api\(".to_string(),
replace: "new_api(".to_string(),
},
],
recursive: true,
dry_run: false,
..Default::default()
};
let replacer = ContentReplacer::new(options)?;
let (files, replacements) = replacer.process(std::path::Path::new("src"))?;
println!("Made {} replacements in {} files", replacements, files);use reformat_core::{HeaderManager, HeaderOptions};
let options = HeaderOptions {
text: "// Copyright {year} MyOrg. All rights reserved.\n// SPDX-License-Identifier: MIT".to_string(),
update_year: true,
recursive: true,
dry_run: false,
..Default::default()
};
let manager = HeaderManager::new(options)?;
let (files, _) = manager.process(std::path::Path::new("src"))?;
println!("Updated headers in {} files", files);use reformat_core::{FileGrouper, GroupOptions};
let mut options = GroupOptions::default();
options.strip_prefix = true; // Remove prefix from filenames
options.from_suffix = false; // Set true to split at LAST separator
options.min_count = 2; // Require at least 2 files to create a group
options.dry_run = false;
let grouper = FileGrouper::new(options);
let stats = grouper.process(std::path::Path::new("templates"))?;
println!("Directories created: {}", stats.dirs_created);
println!("Files moved: {}", stats.files_moved);
println!("Files renamed: {}", stats.files_renamed);The fastest way to clean up your code:
# Process directory (non-recursive)
reformat <path>
# Process recursively
reformat -r <path>
# Preview changes without modifying files
reformat -d <path>What it does:
- Renames files to lowercase
- Transforms task emojis: β β [x], β β [ ]
- Removes trailing whitespace
Example:
# Clean up an entire project directory
reformat -r src/
# Preview changes first
reformat -d -r docs/
# Process a single file
reformat README.mdOutput:
Renamed '/tmp/TestFile.txt' -> '/tmp/testfile.txt'
Transformed emojis in '/tmp/testfile.txt'
Cleaned 2 lines in '/tmp/testfile.txt'
Processed files:
- Renamed: 1 file(s)
- Emoji transformations: 1 file(s) (1 changes)
- Whitespace cleaned: 1 file(s) (2 lines)
Basic conversion (using subcommand):
reformat convert --from-camel --to-snake myfile.pyRecursive directory conversion:
reformat convert --from-snake --to-camel -r src/Dry run (preview changes):
reformat convert --from-camel --to-kebab --dry-run mydir/Add prefix to all converted identifiers:
reformat convert --from-camel --to-snake --prefix "old_" myfile.pyFilter files by pattern:
reformat convert --from-camel --to-snake -r --glob "*test*.py" src/Only convert specific identifiers:
reformat convert --from-camel --to-snake --word-filter "^get.*" src/Clean all default file types in current directory:
reformat clean .Clean with dry-run to preview changes:
reformat clean --dry-run src/Clean only specific file types:
reformat clean -e .py -e .rs src/Clean a single file:
reformat clean myfile.pyReplace task emojis with text in markdown files:
reformat emojis docs/Process with dry-run to preview changes:
reformat emojis --dry-run README.mdOnly replace task emojis, keep other emojis:
reformat emojis --replace-task --no-remove-other docs/Process specific file types:
reformat emojis -e .md -e .txt project/Organize files by common prefix into subdirectories:
# Preview what groups would be created
reformat group --preview templates/
# Dry run to see what would happen
reformat group --dry-run templates/
# Group files (keep original filenames)
reformat group templates/
# Group files and strip prefix from filenames
reformat group --strip-prefix templates/
# Group by suffix (split at LAST separator) - for multi-part prefixes
reformat group --from-suffix templates/
# Process subdirectories recursively
reformat group -r templates/
# Use custom separator (e.g., hyphen)
reformat group -s '-' templates/
# Require at least 3 files to create a group
reformat group -m 3 templates/Example transformation with --strip-prefix (splits at FIRST separator):
Before: After:
templates/ templates/
βββ wbs_create.tmpl βββ wbs/
βββ wbs_delete.tmpl β βββ create.tmpl
βββ wbs_list.tmpl β βββ delete.tmpl
βββ work_package_create.tmpl β βββ list.tmpl
βββ work_package_delete.tmpl βββ work/
βββ other.txt β βββ package_create.tmpl
β βββ package_delete.tmpl
βββ other.txt
Example transformation with --from-suffix (splits at LAST separator):
Before: After:
templates/ templates/
βββ activity_relationships_list.tmpl βββ activity_relationships/
βββ activity_relationships_create.tmpl β βββ list.tmpl
βββ activity_relationships_delete.tmpl β βββ create.tmpl
βββ user_profile_edit.tmpl β βββ delete.tmpl
βββ user_profile_view.tmpl βββ user_profile/
βββ other.txt β βββ edit.tmpl
β βββ view.tmpl
βββ other.txt
After grouping files, reformat can scan your codebase for broken references:
# Interactive mode (default) - prompts for scanning
reformat group --strip-prefix templates/
# Output:
# Grouping complete:
# - Directories created: 2
# - Files moved: 5
#
# Changes recorded to: changes.json
#
# Would you like to scan for broken references? [y/N]: y
# Enter directories to scan: src
#
# Found 3 broken reference(s).
# Proposed fixes written to: fixes.json
#
# Review fixes.json and apply changes? [y/N]: y
# Fixed 3 reference(s) in 2 file(s).# Non-interactive mode with automatic scanning
reformat group --strip-prefix --no-interactive --scope src templates/
# Skip reference scanning entirely
reformat group --strip-prefix --no-interactive templates/Generated files:
changes.json- Record of all file operations (for auditing)fixes.json- Proposed reference fixes (review before applying)
Normalize line endings across files:
# Convert to Unix line endings (LF) - default
reformat endings src/
# Convert to Windows line endings (CRLF)
reformat endings --style crlf src/
# Preview changes
reformat endings --dry-run src/
# Process specific file types
reformat endings -e .py -e .rs src/Convert between tabs and spaces:
# Convert tabs to spaces (4-wide, default)
reformat indent src/
# Convert tabs to 2-space indentation
reformat indent --style spaces --width 2 src/
# Convert spaces to tabs
reformat indent --style tabs --width 4 src/
# Preview changes
reformat indent --dry-run src/Apply regex patterns across files:
# Simple text replacement
reformat replace --find "old_name" --replace-with "new_name" src/
# Regex with capture groups
reformat replace --find "func\((\w+), (\w+)\)" --replace-with "func(\$2, \$1)" src/
# Dry run
reformat replace --find "TODO" --replace-with "FIXME" --dry-run src/
# Filter by extension
reformat replace --find "2024" --replace-with "2025" -e .py src/For multiple patterns, use a preset (see Presets section below).
Insert or update file headers:
# Insert a license header
reformat header --text "// Copyright 2025 MyOrg\n// SPDX-License-Identifier: MIT" src/
# Insert header with automatic year
reformat header --text "// Copyright {year} MyOrg" --update-year src/
# Preview changes
reformat header --text "// Header" --dry-run src/
# Process specific file types
reformat header --text "# License" -e .py src/Define reusable transformation pipelines in a reformat.json file in your project root:
{
"code": {
"steps": ["rename", "emojis", "clean"],
"rename": {
"case_transform": "lowercase",
"space_replace": "hyphen"
},
"emojis": {
"replace_task_emojis": true,
"remove_other_emojis": false,
"file_extensions": [".md", ".txt"]
},
"clean": {
"remove_trailing": true,
"file_extensions": [".rs", ".py"]
}
},
"templates": {
"steps": ["group", "clean"],
"group": {
"separator": "_",
"min_count": 3,
"strip_prefix": true
}
}
}Run a preset:
reformat -p code src/
# Dry-run to preview changes
reformat -p code -d src/
# Run a different preset
reformat -p templates web/templates/Available step configuration options:
| Step | Options |
|---|---|
rename |
case_transform (lowercase/uppercase/capitalize), space_replace (underscore/hyphen), recursive, include_symlinks |
emojis |
replace_task_emojis, remove_other_emojis, file_extensions, recursive |
clean |
remove_trailing, file_extensions, recursive |
convert |
from_format, to_format, file_extensions, recursive, prefix, suffix, glob, word_filter |
group |
separator, min_count, strip_prefix, from_suffix, recursive |
endings |
style (lf/crlf/cr), file_extensions, recursive |
indent |
style (spaces/tabs), width, file_extensions, recursive |
replace |
patterns (array of {find, replace}), file_extensions, recursive |
header |
text, update_year, file_extensions, recursive |
Steps without explicit configuration use sensible defaults.
Example preset using new transformers:
{
"normalize": {
"steps": ["endings", "indent", "clean", "header"],
"endings": { "style": "lf" },
"indent": { "style": "spaces", "width": 4 },
"header": {
"text": "// Copyright {year} MyOrg. All rights reserved.\n// SPDX-License-Identifier: MIT",
"update_year": true,
"file_extensions": [".rs", ".go", ".js"]
}
},
"migrate-api": {
"steps": ["replace"],
"replace": {
"patterns": [
{ "find": "old_api\\(", "replace": "new_api(" },
{ "find": "Copyright 2024", "replace": "Copyright 2025" }
],
"file_extensions": [".rs", ".py"]
}
}
}Jobs are ad-hoc transformation pipelines for one-off tasks. A job file has the same
format as a single preset -- just a JSON object with steps and per-step config --
but is loaded from an arbitrary file (or stdin) instead of your project's reformat.json.
Run a job from a file:
reformat --job migrate.json src/Run a job from stdin:
echo '{"steps":["clean"]}' | reformat --job - src/Example job file for a multi-pattern replacement:
{
"steps": ["replace", "clean"],
"replace": {
"patterns": [
{"find": "old_api\\(", "replace": "new_api("},
{"find": "Copyright 2024", "replace": "Copyright 2025"}
],
"file_extensions": [".rs", ".py"]
}
}Jobs support dry-run mode:
reformat --job migrate.json --dry-run src/When to use presets vs. jobs:
Presets (-p) |
Jobs (--job) |
|
|---|---|---|
| Source | reformat.json in project root |
Any file or stdin |
| Lifecycle | Reusable, version-controlled | Throwaway, ad-hoc |
| Use case | Standard project workflows | One-off migrations, scripted transforms |
Control output verbosity:
# Info level output (-v)
reformat -v convert --from-camel --to-snake src/
# Debug level output (-vv)
reformat -vv clean src/
# Silent mode (errors only)
reformat -q convert --from-camel --to-snake src/
# Log to file
reformat --log-file debug.log -v convert --from-camel --to-snake src/Output example with -v:
2025-10-10T00:15:08.927Z [INFO] Converting from CamelCase to SnakeCase
2025-10-10T00:15:08.927Z [INFO] Target path: /tmp/test.py
2025-10-10T00:15:08.927Z [INFO] Recursive: false, Dry run: false
Converted '/tmp/test.py'
2025-10-10T00:15:08.931Z [INFO] Conversion completed successfully
2025-10-10T00:15:08.931Z [INFO] run_convert(), Elapsed=4.089125ms
--from-camel/--to-camel- camelCase (firstName, lastName)--from-pascal/--to-pascal- PascalCase (FirstName, LastName)--from-snake/--to-snake- snake_case (first_name, last_name)--from-screaming-snake/--to-screaming-snake- SCREAMING_SNAKE_CASE (FIRST_NAME, LAST_NAME)--from-kebab/--to-kebab- kebab-case (first-name, last-name)--from-screaming-kebab/--to-screaming-kebab- SCREAMING-KEBAB-CASE (FIRST-NAME, LAST-NAME)
Convert Python file from camelCase to snake_case:
reformat convert --from-camel --to-snake main.pyConvert C++ project from snake_case to PascalCase:
reformat convert --from-snake --to-pascal -r -e .cpp -e .hpp src/Preview converting JavaScript getters to snake_case:
reformat convert --from-camel --to-snake --word-filter "^get.*" -d src/Clean trailing whitespace from entire project:
reformat clean -r .Clean only Python files in src directory:
reformat clean -e .py src/Preview what would be cleaned without making changes:
reformat clean --dry-run .Transform task emojis in documentation:
reformat emojis -r docs/Example transformation:
Before:
- Task done β
- Task pending β
- Warning β issue
- π‘ In progress
- π’ Complete
- π΄ Blocked
After:
- Task done [x]
- Task pending [ ]
- Warning [!] issue
- [yellow] In progress
- [green] Complete
- [red] BlockedProcess only markdown files:
reformat emojis -e .md README.mdOrganize template files by prefix (split at first separator):
reformat group --strip-prefix web/templates/Organize files with multi-part prefixes (split at last separator):
# activity_relationships_list.tmpl -> activity_relationships/list.tmpl
reformat group --from-suffix web/templates/Preview groups without making changes:
reformat group --preview web/templates/Example output:
Found 2 potential group(s):
wbs (3 files):
- wbs_create.tmpl
- wbs_delete.tmpl
- wbs_list.tmpl
work (2 files):
- work_package_create.tmpl
- work_package_delete.tmpl
Group files with hyphen separator:
reformat group -s '-' --strip-prefix components/Recursively organize nested directories:
reformat group -r --strip-prefix src/Group files and automatically scan for broken references:
reformat group --strip-prefix --scope src templates/Example changes.json:
{
"operation": "group",
"timestamp": "2026-01-15T16:30:00+00:00",
"base_dir": "/project/templates",
"changes": [
{"type": "directory_created", "path": "wbs"},
{"type": "file_moved", "from": "wbs_create.tmpl", "to": "wbs/create.tmpl"}
]
}Example fixes.json:
{
"generated_from": "changes.json",
"fixes": [
{
"file": "src/handler.go",
"line": 15,
"context": "template.ParseFiles(\"wbs_create.tmpl\")",
"old_reference": "wbs_create.tmpl",
"new_reference": "wbs/create.tmpl"
}
]
}Normalize a cross-platform project to Unix endings:
reformat endings -r src/Convert to Windows line endings for distribution:
reformat endings --style crlf -r dist/Standardize a project to 4-space indentation:
reformat indent -r src/Convert to 2-space indentation for JavaScript:
reformat indent --width 2 -e .js -e .ts src/Convert to tabs:
reformat indent --style tabs --width 4 -e .go src/Update copyright year across all files:
reformat replace --find "Copyright 2024" --replace-with "Copyright 2025" -r .Swap function argument order using capture groups:
reformat replace --find "swap\((\w+), (\w+)\)" --replace-with "swap(\$2, \$1)" src/Add MIT license header to all Rust files:
reformat header -t "// Copyright {year} MyOrg\n// SPDX-License-Identifier: MIT" --update-year -e .rs src/Ensure all Python files have a header (preserves shebang):
reformat header -t "# Copyright {year} MyOrg" --update-year -e .py src/Run a multi-step cleanup preset:
# Define in reformat.json, then run:
reformat -p code src/
# Output:
# rename: 3 file(s) renamed
# emojis: 2 file(s), 5 change(s)
# clean: 4 file(s), 12 line(s) cleaned
# Preset 'code' complete.Preview preset changes without modifying files:
reformat -p code -d src/Case conversion preset:
{
"snake-to-camel": {
"steps": ["convert"],
"convert": {
"from_format": "snake",
"to_format": "camel",
"file_extensions": [".py"],
"recursive": true
}
}
}reformat -p snake-to-camel src/MIT License. See LICENSE for details.