oxur-pretty

Crates.iooxur-pretty
lib.rsoxur-pretty
version0.2.0
created_at2026-01-04 08:33:53.997112+00
updated_at2026-01-17 00:36:16.511357+00
descriptionPretty-printer for S-expression formatted data
homepage
repositoryhttps://github.com/oxur/oxur
max_upload_size
id2021582
size155,325
Duncan McGreggor (oubiwann)

documentation

README

oxur-pretty

Pretty-printer for S-expression formatted data with rustfmt-style CLI.

Overview

oxur-pretty provides tools for formatting S-expressions in a human-readable way, with support for various formatting strategies and a command-line tool that follows rustfmt conventions.

Key features:

  • Zero dependencies on oxur-ast - Avoids cyclic dependencies
  • Idempotent formatting - format(format(x)) === format(x)
  • Multiple strategies - Automatic selection between inline, compact, and multiline formats
  • CLI tool - oxurfmt command-line tool matching rustfmt interface
  • Check mode - CI/CD integration with --check flag
  • Configurable - Control line width, indentation, and formatting rules
  • Atomic writes - Safe in-place file modification with permission preservation

CLI Tool: oxurfmt

The oxurfmt command-line tool formats S-expression files following rustfmt conventions.

Installation

# From workspace root
make build

# Or directly
cargo build --release -p oxur-pretty

The binary will be at ./bin/oxurfmt (via Makefile) or target/release/oxurfmt.

Quick Start

# Format file in-place
oxurfmt file.sexp

# Check if formatted (CI/CD)
oxurfmt --check file.sexp

# Format to stdout
oxurfmt --emit stdout file.sexp

# Stdin to stdout
cat file.sexp | oxurfmt
echo "(Span :lo 0 :hi 10)" | oxurfmt

# With custom config
oxurfmt --config max_width=120,tab_spaces=4 file.sexp

Commands and Flags

Basic Usage:

oxurfmt [OPTIONS] <FILE>...

Options:

Flag Description
--check Check if files are formatted (exit 1 if not)
--emit <MODE> What data to emit: files (default) or stdout
--backup Backup modified files (creates .bk files)
--config <key=val,...> Set config from command line
--color <MODE> Use colored output: always, never, or auto
-l, --files-with-diff Print names of files needing formatting
-v, --verbose Print verbose output
-q, --quiet Print less output

Configuration Keys:

  • max_width=<N> - Maximum line width (default: 100)
  • tab_spaces=<N> - Spaces per indent level (default: 2)
  • align_keywords=<bool> - Align keyword-value pairs (default: true)
  • compact_simple_values=<bool> - Keep simple values compact (default: true)
  • max_inline_items=<N> - Max items for inline formatting (default: 3)

Examples

Format Files In-Place

# Single file
oxurfmtmy-ast.sexp

# Multiple files
oxurfmtfile1.sexp file2.sexp file3.sexp

# With glob expansion
oxurfmt**/*.sexp

Check Mode (CI/CD)

# Check if files are formatted
oxurfmt--check src/*.sexp

# Exit code 0 if formatted, 1 if not, 2 on error
if oxurfmt--check file.sexp; then
    echo "Formatted correctly"
else
    echo "Needs formatting"
fi

# List files needing formatting
oxurfmt--check -l **/*.sexp

Stdin/Stdout Usage

# Read from stdin, write to stdout
cat input.sexp | oxurfmt> output.sexp

# Use '-' explicitly
oxurfmt- < input.sexp > output.sexp

# Format and pipe
echo "(Item :id 0 :name value)" | oxurfmt| less

# Format to stdout even with file input
oxurfmt--emit stdout my-ast.sexp > formatted.sexp

Backup Files

# Create backup before formatting
oxurfmt--backup important.sexp

# Creates important.sexp.bk before modifying important.sexp

Custom Configuration

# Single option
oxurfmt--config max_width=120 file.sexp

# Multiple options
oxurfmt--config max_width=80,tab_spaces=4,align_keywords=false file.sexp

# Disable keyword alignment
oxurfmt--config align_keywords=false file.sexp

Verbose Output

# Show what's being formatted
oxurfmt-v file1.sexp file2.sexp

# Quiet mode (errors only)
oxurfmt-q **/*.sexp

Formatting Strategies

oxurfmt automatically selects the best formatting strategy based on content:

Inline Format

Simple expressions that fit on one line:

(a b c)
(PathSegment :ident foo :id 0)

Compact Struct Format

Type-like structures with keyword-value pairs that fit the max width:

(Span :lo 0 :hi 10)
(Ident :name "use" :span (Span :lo 0 :hi 3))

Keyword-Aligned Format

Multiline with aligned keywords for readability:

(Item
  :attrs ()
  :id 0
  :span (Span :lo 0 :hi 0)
  :vis (Inherited)
  :ident (Ident :name "main" :span (Span :lo 0 :hi 0))
  :kind (Fn ...))

Smart Multiline Format

General multiline with proper indentation:

(Crate
  :attrs ()
  :items ((Item ...)
          (Item ...))
  :spans (ModSpans
           :inner-span (Span :lo 0 :hi 0)
           :inject-use-span (Span :lo 0 :hi 0)))

Exit Codes

  • 0 - Success (all files formatted correctly)
  • 1 - Files need formatting (only in --check mode)
  • 2 - Error occurred (parse error, I/O error, etc.)

Library API

Use oxur-pretty as a library for programmatic formatting.

Basic Usage

use oxur_pretty::format_sexp;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = "(Span :lo 0 :hi 10)";
    let formatted = format_sexp(input)?;

    println!("{}", formatted);
    // Output: (Span :lo 0 :hi 10)

    Ok(())
}

Custom Configuration

use oxur_pretty::{format_sexp_with_config, FormatConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let input = "(VeryLongTypeName :key1 value1 :key2 value2 :key3 value3)";

    let config = FormatConfig::default()
        .with_max_width(40)?
        .with_tab_spaces(4)?
        .with_align_keywords(true);

    let formatted = format_sexp_with_config(input, config)?;

    println!("{}", formatted);
    // Output will be multiline because it exceeds max_width

    Ok(())
}

Configuration Builder

use oxur_pretty::FormatConfig;

// Default configuration
let config = FormatConfig::default();
// max_width: 100
// tab_spaces: 2
// align_keywords: true
// compact_simple_values: true
// max_inline_items: 3

// Custom configuration
let config = FormatConfig::default()
    .with_max_width(120)?
    .with_tab_spaces(4)?
    .with_align_keywords(false)
    .with_compact_simple_values(false)
    .with_max_inline_items(5);

Error Handling

use oxur_pretty::{format_sexp, FormatterError};

match format_sexp("(unclosed") {
    Ok(formatted) => println!("{}", formatted),
    Err(FormatterError::UnmatchedOpen { pos }) => {
        eprintln!("Unmatched opening parenthesis at position {}", pos);
    }
    Err(FormatterError::UnterminatedString { pos }) => {
        eprintln!("Unterminated string at position {}", pos);
    }
    Err(e) => eprintln!("Error: {}", e),
}

Working with Parser and Formatter Directly

use oxur_pretty::{parse, Formatter, FormatConfig};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse S-expression
    let input = r#"(Ident :name "use" :span (Span :lo 0 :hi 3))"#;
    let expr = parse(input)?;

    // Format with custom config
    let config = FormatConfig::default().with_max_width(80)?;
    let formatter = Formatter::new(config);
    let output = formatter.format(&expr);

    println!("{}", output);

    Ok(())
}

Integration with oxur-ast

oxur-pretty is designed to work seamlessly with oxur-ast:

use oxur_ast::sexp::print_sexp;  // Built-in S-expression printer
use oxur_pretty::format_sexp;     // oxur-pretty formatter

// oxur-ast generates S-expressions (compact, no specific formatting)
let sexp = generate_sexp_from_ast(&ast);
let compact = print_sexp(&sexp);

// oxur-pretty formats for human readability
let formatted = format_sexp(&compact)?;

When to use which:

  • oxur-ast - For AST operations, round-trip preservation, programmatic S-expression generation
  • oxur-pretty - For human-readable output, code formatting, documentation examples

Testing

Run all tests:

cargo test -p oxur-pretty

Run specific test suites:

# Unit tests
cargo test --lib

# Integration tests
cargo test --test cli_integration

Test Coverage

The crate includes comprehensive tests:

  • Parser tests - All S-expression syntax (atoms, lists, strings, escapes, comments)
  • Formatter tests - Each formatting strategy, edge cases, idempotency
  • Config tests - Validation, builder pattern
  • Rules tests - Heuristics, decision logic
  • CLI tests - All flags, exit codes, file operations
  • Error tests - All error paths

Coverage target: 95%+

Design Documentation

See design document ODD-0037 for implementation details:

# From workspace root
./bin/oxd show 37

Formatting Philosophy

oxur-pretty follows these principles:

  1. Idempotency - Running the formatter multiple times produces the same result
  2. Readability - Optimizes for human readers, not machines
  3. Consistency - Same input always produces same output
  4. Preservation - Maintains semantic meaning, only changes whitespace
  5. Intelligence - Adapts formatting strategy based on content

Performance

The formatter is designed for speed:

  • Simple caching - Avoids redundant calculations
  • Efficient string building - Minimizes allocations
  • Lazy evaluation - Only calculates what's needed

Run benchmarks:

cargo bench -p oxur-pretty

Examples in the Wild

# Format generated AST files
./bin/aster to-ast examples/hello.rs | oxurfmt

# Format test fixtures
oxurfmt test-data/**/*.sexp

# Check formatting in CI
oxurfmt --check src/**/*.sexp || exit 1

# Format with project-specific config
oxurfmt --config max_width=120,tab_spaces=4 src/*.sexp

Comparison with Other Tools

Feature oxur-pretty oxur-ast printer rustfmt
S-expressions ✓ Optimized ✓ Basic
Rust code ✓ Optimized
Idempotent
Check mode
Config file ✗ CLI only
Stdin/stdout
Backup files
Multiple strategies ✓ 4 strategies ✗ Simple ✓ Many

Troubleshooting

"Parse error: Unmatched opening parenthesis"

Check that all parentheses are balanced:

# Use verbose mode to see where parsing fails
oxurfmt-v file.sexp

"Invalid config: max_width must be greater than 0"

Ensure config values are valid:

# Wrong
oxurfmt--config max_width=0 file.sexp

# Correct
oxurfmt--config max_width=80 file.sexp

File Not Modified

If using --emit stdout, output goes to stdout, not file:

# This DOES NOT modify file.sexp
oxurfmt--emit stdout file.sexp

# This modifies file.sexp in-place
oxurfmtfile.sexp

Permission Denied

Ensure write permissions:

chmod u+w file.sexp
oxurfmtfile.sexp

License

See the main repository for license information.

Contributing

Contributions are welcome! Please see the main repository for guidelines.

Commit count: 489

cargo fmt