Skip to content

Latest commit

 

History

History
1397 lines (1081 loc) · 61.1 KB

File metadata and controls

1397 lines (1081 loc) · 61.1 KB

XL CLI Reference

The xl CLI provides a command-line interface for Excel operations, designed for LLM agents and automation.

Design Philosophy:

  • Stateless — Each command is self-contained
  • Explicit cell refs — Always use A1, B5:D10 notation
  • Global flags-f for file, -s for sheet, -o for output
  • LLM-optimized output — Markdown tables, token-efficient

Installation

git clone https://github.com/TJC-LP/xl.git
cd xl
make install

This builds a native binary (no JDK required, instant startup) and installs xl to ~/.local/bin/. Ensure it's in your PATH:

export PATH="$HOME/.local/bin:$PATH"

JAR install (requires JDK 17+): make install-jar

Uninstall: make uninstall

Update: After git pull, run make install (or make install-jar) again.


Quick Reference

# Global flags (used with all commands)
-f, --file <path>     # Input file (required for most commands)
-s, --sheet <name>    # Sheet to operate on (required for unqualified ranges)
-o, --output <path>   # Output file for mutations
-i, --in-place        # Edit file in place (same as -o matching -f)
--stream              # O(1) memory streaming for large files (search/stats/bounds/view + writes)
--max-size <MB>       # Max uncompressed size for in-memory load (default 100, 0 = unlimited)
--backend <name>      # XML backend: scalaxml (default) or saxstax (faster)
--no-recalc           # Write verbs: apply the edit, recalculate nothing (alias --preserve-caches)
--preserve-caches     # Same flag, spelled for the intent
--strict              # Write verbs: exit 1 when the write's recalculation reports problems

# Read-only operations
xl -f model.xlsx sheets                    # List all sheets
xl -f model.xlsx names                     # List defined names (named ranges)
xl -f model.xlsx -s "P&L" bounds           # Show used range
xl -f model.xlsx -s "P&L" view A1:D20      # View range as markdown
xl -f model.xlsx cell B5                   # Get single cell details (sheet auto-detected if unambiguous)
xl -f model.xlsx search "Revenue"          # Find cells by content (all sheets)
xl -f model.xlsx -s "P&L" stats B1:B100    # Numeric statistics for a range
xl -f model.xlsx -s "P&L" eval "=SUM(B1:B10)"   # Evaluate formula (what-if)
xl -f model.xlsx -s "P&L" evala "=TRANSPOSE(A1:C2)"  # Evaluate array formula (spill grid)

# Mutations (require -o or -i)
xl -f model.xlsx -s S1 -o output.xlsx put B5 1000000       # Write value
xl -f model.xlsx -s S1 -o output.xlsx putf C5 "=B5*1.1"    # Write formula

# What-if analysis with overrides
xl -f model.xlsx -s S1 eval "=B1*1.1" --with "B1=100"      # Evaluate with temporary values

# No file needed
xl new report.xlsx --sheet Data --sheet Summary   # Create a blank workbook
xl functions                                       # List all 108 supported functions
xl rasterizers                                     # List available PNG/PDF backends

Global flags must come before the verb: xl -f x.xlsx --strict recalc, never xl -f x.xlsx recalc --strict (decline reports Unexpected argument: recalc).

Cache posture on writes (--no-recalc / --preserve-caches)

Every write verb that changes cell content ends with a recalculation scoped to the edit's dirty dependency cone — the changed cells plus their transitive dependents (cross-sheet included) plus the always-dirty INDIRECT/OFFSET cells. Cached values outside that cone are never rewritten, so a book whose caches come from another engine keeps them.

--no-recalc (alias --preserve-caches) drops the recalculation entirely: the edit lands and no cached value is recomputed. Use it when an external calculator owns the numbers. Honored by put, putf, fill, copy, batch, insert-rows, insert-cols, delete-rows, delete-cols; recalc rejects it as a contradiction.

How completely caches survive depends on the verb:

  • Non-structural (put, putf, fill, copy, batch): every existing cached value survives byte-identical, cone included. The summary says so.

  • Structural (insert-rows, insert-cols, delete-rows, delete-cols): a structural edit moves cells, rewrites formula text and rewrites defined names, so xl invalidates the cache of every formula the edit could have changed and writes those cells without a <v>. It never re-asserts a pre-edit cache: whether a formula the edit did reach still has its old answer cannot be decided without recalculating, which is precisely what the flag refuses. (A formula reached through a shrunk defined name, or a static dependent of an INDIRECT cell, keeps its text byte-identical while its answer changes underneath it.)

    A cache survives only when the edit provably could not have changed it: the formula's text is byte-identical after the rewrite, it did not relocate, and nothing it transitively reads was moved or removed. So an edit below or beside your data preserves everything, while an edit inside a block preserves the rows above the cut and drops the rest. The summary counts both halves:

    Recalculation skipped (--no-recalc): 4 cached value(s) preserved, 7 formula(s) invalidated by
    the edit left uncached (recalculate externally)
    

    One consequence worth knowing: volatile formulas (TODAY(), NOW(), RAND()) above the cut keep their cached values too. The flag means "do not recalculate", and a volatile is no exception — Excel refreshes them on open regardless.

    Reopening the file in Excel, or a later xl recalc, fills those back in. A missing <v> is a visible gap that any recalculation repairs; a wrong one is silent and permanent, which is why xl never re-asserts a cache the edit invalidated. What it does not claim: a cache rides through only when the pre-edit dependency graph shows no path from it to the edited sheet, and a reference that graph cannot resolve — a multi-area or intersection defined name, a structured reference, an external link — can hide such a path. xl withdraws the caches of formulas that name a defined name it cannot parse, precisely because the graph is blind there; the remaining cases are tracked in #507 and affect a normal recalculating write too. If you need every formula cached after a structural edit, do not pass --no-recalc.

xl -f external-model.xlsx -s Data -o out.xlsx --no-recalc put B5 1000
xl -f external-model.xlsx -s Data -o out.xlsx --preserve-caches batch ops.json

Strict exit codes (--strict)

By default a write reports formula-evaluation errors, iterative non-convergence and data-table seed warnings in its summary and still exits 0. --strict promotes those to exit 1 while printing the same summary — for CI and scripted pipelines. Excel error values (#DIV/0!, #N/A) are data conditions, not failures, and never gate.

xl -f model.xlsx -o out.xlsx --strict recalc    # exit 1 if any formula failed to evaluate

With -o the output file is written even on a strict failure (the gate only sets the exit code). With -i the temp file is discarded and the input is left byte-identical; the summary then says NOT saved (--strict failure): <file> left untouched instead of Saved:. --strict is refused together with --stream (streaming writes never recalculate, so the gate could never fire). Verbs that produce no recalculation result — put/putf/fill/copy for the cell they authored, and every presentation-only verb — cannot gate today (issue #504).


Commands

Operation Categories

Category Commands Purpose
Info (no -f) functions, rasterizers, new Capability listing, blank workbook
Navigate sheets, bounds, names Find your way around
Explore view, cell, search, stats Read data incrementally
Analyze eval, evala What-if formula evaluation (scalar + array)
Mutate cells put, putf, style, fill, clear, copy, sort, merge, unmerge, comment, remove-comment, batch, import Make changes (require -o or -i)
Rows/columns row, col, autofit, insert-rows, delete-rows, insert-cols, delete-cols Sizing, visibility, structural editing
Sheets & view add-sheet, remove-sheet, rename-sheet, move-sheet, copy-sheet, sheets hide/show, freeze, unfreeze, name Workbook structure
Appearance & print sheet-view, tab-color, page-setup, header-footer Deliverable finish: gridlines, zoom, tab colors, print setup, footers
Conditional formatting cf add, cf list Highlight rules, color scales, data bars, top-N, text matches

Command Summary

Command Arguments Description
functions List all 108 supported Excel functions (no -f needed)
rasterizers List available SVG-to-raster backends (no -f needed)
new <output> [--sheet <name>]... Create a blank xlsx file (no -f needed)
sheets [list|hide <name> [--very]|show <name>] List sheets (default) or hide/show one
names List defined names (named ranges)
name add <name> <refers-to> | rm <name> Manage named ranges (requires -o)
bounds [--scan] Show used range of current sheet
view <range> [flags] Render range (markdown/json/csv/html/svg/png/jpeg/webp/pdf)
cell <ref> [--no-style] Get single cell details
search <pattern> [--limit n] [--sheets a,b] Find cells matching pattern (regex, all sheets by default)
stats <range> Statistics for numeric values in range
filter --where <pred> [--columns A,C:E] [--limit n] [--format md|csv|json] [--header] Show rows matching a predicate (read-only)
eval <formula> [--with overrides] Evaluate formula without modifying
evala <formula> [--at <ref>] [--with overrides] Evaluate array formula; display or spill result grid
put <ref|range> <value...> [--csv] [--no-detect] Write value(s) to cell or range (requires -o)
putf <ref|range> <formula...> Write formula(s); single formula + range drags with $ anchors (requires -o)
style <range> [options] Apply styling (requires -o)
row <n> [--height pt] [--hide|--show] Set row properties (requires -o)
col <letter|A:F> [--width n] [--auto-fit] [--hide|--show] Set column properties (requires -o)
autofit [--columns A:F] Auto-fit column widths from content (requires -o)
fill <source> <target> [--right] Fill cells with source value/formula (requires -o)
clear <range> [--all|--styles|--comments] Clear cell contents/styles/comments (requires -o)
copy <source> <target> [--values-only] Copy range with formula adjustment (requires -o)
sort <range> --by <col> [options] Sort rows by one or more columns (requires -o)
merge <range> Merge cells (requires -o)
unmerge <range> Unmerge cells (requires -o)
comment <ref> <text> [--author name] Add cell comment (requires -o)
remove-comment <ref> Remove cell comment (requires -o)
freeze <ref> Freeze panes at cell (requires -o)
unfreeze Remove freeze panes (requires -o)
sheet-view [--gridlines on|off] [--zoom n] [--tab-selected on|off] Set sheet view options (requires -o)
tab-color <color> | --clear Set/clear the sheet tab color (requires -o)
page-setup [--orientation portrait|landscape] [--scale n] [--fit-to-width n] [--fit-to-height n] [--fit-to-page on|off] Set print page setup (requires -o)
header-footer [--odd-header s] [--odd-footer s] [--even-\*] [--first-\*] [--different-odd-even] [--different-first] Set print header/footer text (requires -o)
cf add --range <range> --rule <dsl> [format flags] Add a conditional-formatting rule (requires -o)
cf list List conditional-formatting rules on the sheet (read-only)
chart add --type <t> --data <range> --at <ref> [options] Add a chart built from sheet ranges (requires -o)
add-image <image-file> --at <ref> [--size WxH] Embed an image (requires -o)
import <csv-file> [start-ref] [options] Import CSV with type detection (requires -o)
import-md <md-file|-> [--start ref] [options] Import GFM markdown table with type detection (requires -o)
add-sheet <name> [--after s] [--before s] Add new empty sheet (requires -o)
remove-sheet <name> Remove sheet (requires -o)
rename-sheet <name> <new-name> Rename sheet (requires -o)
move-sheet <name> [--to idx] [--after s] [--before s] Move sheet to new position (requires -o)
copy-sheet <name> <new-name> Copy sheet to new name (requires -o)
insert-rows <at-row> [count] Insert rows; shifts cells, rewrites formulas (requires -o)
delete-rows <at-row> [count] Delete rows; #REF! on lost references (requires -o)
insert-cols <at-col> [count] Insert columns; shifts cells, rewrites formulas (requires -o)
delete-cols <at-col> [count] Delete columns; #REF! on lost references (requires -o)
batch <file|-> [--dry-run] Apply multiple operations from JSON (requires -o; --dry-run validates without a file)
diff -g <file2> [--format markdown|json] Compare two workbooks; exit 0 identical, 1 differs, 2 error
lint [<file>] [--format text|json] Validate package structure (child order, r:id resolution, content-type coverage, over-max refs, data-table integrity, <f> canon); positional file or -f; exit 0 clean, 1 findings, 2 error

Command Details

xl sheets [list|hide|show]

Sheet operations. With no subcommand, defaults to list.

Subcommands:

Subcommand Arguments Description
list [--stats] List all sheets (--stats adds cell/formula counts; slower)
hide <sheet-name> [--very] Hide a sheet (--very = very hidden, VBA-only; requires -o)
show <sheet-name> Show a hidden sheet (requires -o)

Output (list --stats):

| # | Name        | Range    | Cells | Formulas |
|---|-------------|----------|-------|----------|
| 1 | Assumptions | A1:F50   | 234   | 12       |
| 2 | Revenue     | A1:M100  | 892   | 156      |
| 3 | P&L         | A1:N120  | 978   | 76       |

xl names / xl name add|rm

List and manage defined names (named ranges).

xl -f model.xlsx names                                       # List all defined names
xl -f model.xlsx -o out.xlsx name add Tax 'Sheet1!$A$1'      # Add or replace
xl -f model.xlsx -o out.xlsx name rm Tax                     # Remove

xl bounds [--scan]

Show the used range (bounding box of non-empty cells) for the sheet selected with -s. Instant by default (reads the worksheet's dimension element); --scan forces a full streaming scan for accurate bounds.

Output:

Sheet: Revenue
Used range: A1:M100
Rows: 1-100 (100 total)
Columns: A-M (13 total)
Non-empty: 892 cells

xl view <range>

View a rectangular range — markdown table by default, or JSON/CSV/HTML/SVG/PNG/JPEG/WebP/PDF.

Arguments:

Arg Type Required Default Description
range string Yes Cell range (e.g., "A1:D20")
--format string No markdown Output format: markdown, json, csv, html, svg, png, jpeg, webp, pdf
--formulas flag No false Show formulas instead of values
--eval flag No false Evaluate formulas (compute live values)
--strict flag No false Fail on formula evaluation errors (with --eval)
--limit int No 50 Max rows to display (0 = no limit). When output is clipped, a truncation marker is reported: markdown appends a "… showing X of Y rows" trailer; json adds truncated/totalRows fields (with --stream the notice goes to stderr instead); csv/svg note on stderr; html notes on stderr and appends an HTML comment; raster formats append the notice to the Exported: line
--skip-empty flag No false Skip empty cells (JSON) or empty rows/columns (tabular)
--skip-hidden flag No false Omit hidden rows/columns. Default renders them — a range you named never silently loses cells (GH-474)
--show-labels flag No false Include column letters and row numbers
--header-row int No Use values from this row as keys in JSON output (1-based)
--raster-output path For raster Output file (required for png/jpeg/webp/pdf)
--dpi int No 144 Resolution for raster output
--quality int No 90 JPEG quality 1-100
--rasterizer string No batik PNG/JPEG export uses Apache Batik by default (pure JVM, no external tools). Force another backend: cairosvg, rsvg-convert, resvg, imagemagick (explicit opt-in since 0.11.3). Native binaries need an external backend — see xl rasterizers
--gridlines flag No false Show cell gridlines in SVG output
--print-scale flag No false Apply print scaling (for PDF-like output)

Output:

|   | A           | B       | C          | D       |
|---|-------------|---------|------------|---------|
| 1 | Revenue     |         | $1,000,000 |         |
| 2 | COGS        |         | $400,000   |         |
| 4 | Gross Profit|         | =C1-C2     |         |

Hidden rows and columns (GH-474): a range you addressed explicitly renders in full — hidden lines included — and every data format carries a marker:

Format Marker
markdown trailing note: range includes hidden row(s) … and column(s) … line
csv the same note on stderr (stdout stays machine-parseable)
json top-level "hiddenRows": [5] / "hiddenCols": ["C"] fields (emitted only when the range holds hidden lines)

--skip-hidden restores the visible-only view, and the same marker then names what was dropped (note: omitted hidden …). html/svg/png/jpeg/webp/pdf are pictures of the sheet: they mirror Excel's display and always omit hidden lines. Streaming (--stream) never read row/column properties, so it has always rendered every addressed cell — and for the same reason it cannot honour --skip-hidden or emit the hidden-line marker: passing --skip-hidden with --stream prints note: --skip-hidden is ignored with --stream … on stderr and renders everything. Drop --stream when you need hidden lines elided or flagged.

Why the default: xl search finds a value in a hidden row and xl cell C5 reads it, so a view that silently elided the same cell read as file corruption.


xl rasterizers

List SVG-to-raster backends with live availability on this machine (no -f needed). PNG/JPEG/WebP/PDF export probes backends in this order: batikcairosvgrsvg-convertresvg; imagemagick is never probed automatically (fragile SVG delegate) and must be forced with --rasterizer imagemagick.

Platform matrix:

Distribution Rasterization
JAR (java -jar, make install-jar) Works out of the box — Batik is bundled (pure JVM, needs AWT)
Native binary (GitHub releases, make install) Batik cannot work (no AWT under native-image, by design) — one external tool is required

External tool installs (any one is enough):

pip install cairosvg          # Python, most portable
apt install librsvg2-bin      # rsvg-convert (Debian/Ubuntu); brew install librsvg (macOS)
cargo install resvg           # or a prebuilt binary: github.com/linebender/resvg/releases

When no backend is available, raster exports fail with an error naming the probed chain and pointing back at xl rasterizers. --format svg always works (pure vector, no backend needed).


xl cell <ref>

Get complete information about a single cell.

Arguments:

Arg Type Required Description
ref string Yes Cell reference (e.g., "A1", "B5")

Output (formula cell):

Cell: C4
Type: Formula
Formula: =C1-C2
Cached Value: 600000
Formatted: $600,000

Output (value cell):

Cell: A1
Type: Text
Value: Revenue

xl search <pattern>

Find cells containing text matching pattern. Searches all sheets by default (no -s needed).

Arguments:

Arg Type Required Default Description
pattern string Yes Search pattern (supports regex)
--sheets string No all Comma-separated list of sheets to search
--limit int No 50 Max results (0 = no limit). Reports the true total ("Found Y matches") and appends a "… showing X of Y matches" trailer when the hit list is clipped

Output:

Found 5 matches for "Revenue":

| Ref | Value           | Context (row)              |
|-----|-----------------|----------------------------|
| A1  | Revenue         | Revenue | | $1,000,000     |
| A10 | Revenue Growth  | Revenue Growth | | 5%      |

xl eval <formula> [--with overrides]

Evaluate a formula without modifying the file (what-if analysis). -f is optional for constant formulas (xl eval "=PI()*2").

Arguments:

Arg Type Required Description
formula string Yes Formula to evaluate
--with, -w string No Temporary cell overrides (e.g., "B1=100,B2=200"; repeatable)

Examples:

xl -f model.xlsx -s Sheet1 eval "=SUM(B1:B10)"
xl -f model.xlsx -s Sheet1 eval "=B1*1.1" --with "B1=100"

xl evala <formula> [--at <ref>]

Evaluate an array formula and display the result grid, or spill it into the sheet. Requires -f (array formulas need sheet context).

Arguments:

Arg Type Required Description
formula string Yes Array formula to evaluate
--at string No Target cell for array spill (default: display only)
--with, -w string No Temporary cell overrides (repeatable)

Examples:

xl -f data.xlsx -s Sheet1 evala "=TRANSPOSE(A1:C2)"          # Display result grid
xl -f data.xlsx -s Sheet1 evala "=SEQUENCE(5)" --at E1       # Spill starting at E1
xl -f data.xlsx -s Sheet1 evala "=A1:B2*10"                  # Array arithmetic with broadcasting

xl stats <range>

Calculate statistics (count, sum, min, max, average, ...) for numeric values in a range. Supports --stream for large files.

xl -f data.xlsx -s Sheet1 stats B2:B10000
xl -f huge.xlsx --stream stats A1:E100000

xl filter --where <predicate> [options]

Show rows of the used range matching a predicate. Read-only (no -o); phase 1 of GH-134 — predicate filtering only, no SQL-style SELECT/GROUP BY.

Arguments:

Arg Type Required Default Description
--where string Yes Filter predicate (grammar below)
--columns string No all used Output columns, e.g. A,C:E
--limit int No 50 Max matching rows to display
--format string No markdown markdown, csv, or json
--header flag No false First used row holds column names (excluded from matching)

Predicate grammar (keywords case-insensitive; NOT > AND > OR, parens allowed):

Form Example
Comparison (= != <> > >= < <=) B > 100, A = 'Widget', C = TRUE
Wildcard match (% only) A LIKE 'Widget%'
Inclusive range B BETWEEN 10 AND 100
Set membership A IN ('x', 'y', 'z')
Blank test A IS EMPTY, A IS NOT EMPTY

Semantics:

  • Column refs are letters (A, B) or, with --header, header names from the first used row (case-insensitive; header names win over letters on collision)
  • Numbers compare numerically, strings case-insensitively, booleans against TRUE/FALSE
  • Type mismatch (e.g. text cell vs number literal) means the row doesn't match — never an error
  • Formula cells compare by their cached value; IS EMPTY is true for missing cells, empty cells, and blank text
xl -f sales.xlsx -s Q1 filter --where "Revenue > 10000 AND Region = 'EMEA'" --header
xl -f data.xlsx -s Sheet1 filter --where "A LIKE 'Widget%'" --columns A,C:E --format csv
xl -f data.xlsx -s Sheet1 filter --where "B BETWEEN 10 AND 99" --format json

Output: matching rows keep their original row numbers. Markdown adds a Row column and a match-count footer; CSV starts with a row,<labels> header line; JSON is an array of {"row": n, "cells": {<label>: <typed value>}} objects (labels are header names with --header, letters otherwise).

Limitations: loads the workbook in memory (--max-size envelope applies); --stream is not supported. No date literals in predicates yet.


xl put <ref|range> <value...>

Write value(s) to a cell or range.

Modes:

Mode Example Behavior
Single put A1 100 Write 100 to A1
Fill put A1:A10 "TBD" Fill range with the same value
Batch put A1:C1 "X" "Y" "Z" One value per cell (row-major)
CSV split put A1:C1 "X,Y,Z" --csv Split one comma-separated value across the range

Type Inference:

  • Numbers and formatted numbers: 1000, $1,234.56, 50%
  • ISO dates: 2024-01-15
  • Booleans: true, false
  • Text: Everything else

Use --no-detect to preserve all positional values as text, including numbers and ISO date-like strings.

Negative numbers: use the --value flag (a leading - is parsed as a flag):

xl -f input.xlsx -s S1 -o output.xlsx put A1 --value "-500"

Example:

xl -f input.xlsx -s S1 -o output.xlsx put B5 1000000

xl putf <ref|range> <formula...>

Write formula(s) to a cell or range with Excel-style dragging.

Modes:

Mode Example Behavior
Single putf C1 "=A1+B1" One formula, one cell
Drag putf B2:B10 "=A2*1.1" Single formula + range: references shift per cell ($ anchors pin)
Batch putf D1:D3 "=A1+B1" "=A2*B2" "=A3-B3" One formula per cell, applied as-is (no dragging)

Anchor modes ($ controls shifting when dragging): $A$1 absolute, $A1 column-absolute, A$1 row-absolute, A1 fully relative.

Examples:

xl -f input.xlsx -s S1 -o output.xlsx putf C5 "=B5*1.1"
xl -f input.xlsx -s S1 -o output.xlsx putf C2:C10 "=SUM(\$B\$2:B2)"   # Running total

(The batch JSON putf op additionally accepts a "from" field to drag from an explicit source cell.)

Formula records (GH-430): legacy CSE array formulas ({=...}) and Data Table cells read from a file survive all rewrites — view --formulas and cell render them braced ({=SUM(A1:A3*10)}, {=TABLE(A1,A2)}) and JSON output carries an additive "formulaKind": "array" | "dataTable" field. putf rejects a top-level TABLE( expression: TABLE(...) is a data-table record's derived display text, not a real function (Excel would show #NAME?); data-table authoring is tracked in GH-419. Writing any value or formula onto a record cell replaces the record; copy of a data-table cell pastes its cached constant (Excel's paste behavior) and copy of an array anchor pastes a plain shifted formula.


xl style <range> [options]

Apply styling to cells.

Styles merge with existing formatting by default; --replace overwrites.

Arguments:

Arg Type Description
range string Cell/range reference
--bold flag Bold text
--italic flag Italic text
--underline flag Underlined text
--font-size double Font size in points
--font-name string Font family (e.g., "Arial")
--bg string Background color (name, #hex, or rgb(r,g,b))
--fg string Text color
--align string Horizontal alignment: left, center, right
--valign string Vertical alignment: top, middle, bottom
--wrap flag Enable text wrapping
--format string Number format: general, number, currency, percent, date, text
--border string Border style for all sides: none, thin, medium, thick
--border-top / --border-right / --border-bottom / --border-left string Per-side border style
--border-color string Border color (applies to all specified borders)
--replace flag Replace entire style instead of merging

Example:

xl -f input.xlsx -s S1 -o output.xlsx style A1:D1 --bold --bg yellow --align center

xl row <n> [options]

Set row properties (height, hide/show).

Arguments:

Arg Type Required Default Description
n int Yes Row number (1-based)
--height double No Row height in points
--hide flag No false Hide the row
--show flag No false Show (unhide) the row

Examples:

xl -f input.xlsx -o output.xlsx row 5 --height 30
xl -f input.xlsx -o output.xlsx row 10 --hide
xl -f input.xlsx -o output.xlsx row 10 --show

xl col <letter> [options]

Set column properties (width, hide/show, auto-fit).

Arguments:

Arg Type Required Default Description
letter string Yes Column letter or range (e.g., "A", "AA", "A:F")
--width double No Column width in character units (~8.43 default)
--auto-fit flag No false Auto-fit width based on cell content
--hide flag No false Hide the column
--show flag No false Show (unhide) the column

Behavior:

  • --auto-fit calculates optimal width based on longest cell content in the column
  • Adds 2 characters of padding to the calculated width
  • Minimum width is 8.43 (Excel default)
  • If both --width and --auto-fit are specified, --auto-fit takes precedence

Examples:

# Set explicit width
xl -f input.xlsx -o output.xlsx col B --width 20

# Auto-fit column width based on content
xl -f input.xlsx -o output.xlsx col A --auto-fit

# Hide/show columns
xl -f input.xlsx -o output.xlsx col C --hide
xl -f input.xlsx -o output.xlsx col C --show

xl clear <range>

Clear cell contents, styles, or comments from a range.

Arguments:

Arg Type Required Default Description
range string Yes Cell or range reference (e.g., "A1", "A1:D10")
--all flag No false Clear contents, styles, and comments
--styles flag No false Clear styles only (reset to default)
--comments flag No false Clear comments only

Behavior:

  • Default (no flags): Clears cell contents only
  • --all: Clears everything (contents, styles, comments)
  • --styles: Clears formatting but keeps contents and comments
  • --comments: Clears comments but keeps contents and styles
  • Flags can be combined: --styles --comments clears both
  • Merged regions overlapping the cleared range are automatically unmerged

Examples:

# Clear contents (default)
xl -f input.xlsx -o output.xlsx clear A1:D10

# Clear everything
xl -f input.xlsx -o output.xlsx clear A1:D10 --all

# Clear styles only (keep data)
xl -f input.xlsx -o output.xlsx clear A1:D10 --styles

# Clear comments only
xl -f input.xlsx -o output.xlsx clear B5 --comments

# Clear styles and comments, keep contents
xl -f input.xlsx -o output.xlsx clear A1:D10 --styles --comments

xl fill <source> <target> [--right]

Fill cells with source value/formula (Excel Ctrl+D/Ctrl+R equivalent).

Arguments:

Arg Type Required Default Description
source string Yes Source cell or range (e.g., "A1", "A1:C1")
target string Yes Target range to fill (e.g., "A1:A10", "A1:C10")
--right flag No false Fill rightward instead of downward

Behavior:

  • Fill Down (default): Source row(s) are repeated down through target range
    • Columns must match between source and target
    • Example: fill A1 A1:A10 copies A1 to A2:A10
    • Example: fill A1:C1 A1:C10 copies row 1 to rows 2-10
  • Fill Right (--right): Source column(s) are repeated right through target range
    • Rows must match between source and target
    • Example: fill A1 A1:E1 --right copies A1 to B1:E1
    • Example: fill A1:A5 A1:E5 --right copies column A to columns B-E
  • Formulas are shifted using Excel anchor rules ($ anchors are preserved)

Examples:

# Fill value down a column (Ctrl+D equivalent)
xl -f input.xlsx -o output.xlsx fill A1 A1:A100

# Fill multiple columns down together
xl -f input.xlsx -o output.xlsx fill A1:E1 A1:E100

# Fill value right across a row (Ctrl+R equivalent)
xl -f input.xlsx -o output.xlsx fill A1 A1:J1 --right

# Fill multiple rows right together
xl -f input.xlsx -o output.xlsx fill A1:A5 A1:J5 --right

# Formula shifting example: =A1*2 in B1 fills to =A2*2, =A3*2, etc.
xl -f input.xlsx -o output.xlsx fill B1 B1:B10

xl autofit [--columns A:F]

Auto-fit column widths based on content (defaults to all used columns).

xl -f input.xlsx -s S1 -o output.xlsx autofit
xl -f input.xlsx -s S1 -o output.xlsx autofit --columns A:F

xl copy <source> <target> [--values-only]

Copy a range to another location with Excel-style formula adjustment ($ anchors preserved). --values-only copies values without adjusting formulas.

xl -f input.xlsx -s S1 -o output.xlsx copy A1:C10 E1
xl -f input.xlsx -s S1 -o output.xlsx copy A1:C10 E1 --values-only

xl sort <range> --by <col> [options]

Sort rows in a range by one or more columns.

Arguments:

Arg Type Required Description
range string Yes Range to sort
--by, -b string Yes Primary sort column
--then-by string No Secondary sort column(s) (repeatable)
--desc flag No Sort descending (default: ascending)
--numeric flag No Force numeric comparison ("10" > "9")
--header flag No First row is header (exclude from sort)

Behavior: empty cells sort last; formulas sort by cached value; rows move together.

xl -f f.xlsx -s S1 -o o.xlsx sort A1:D100 --by B --desc --numeric
xl -f f.xlsx -s S1 -o o.xlsx sort A1:D100 --by B --then-by C --header

xl merge <range> / xl unmerge <range>

Merge or unmerge cells in a range.

xl -f input.xlsx -s S1 -o output.xlsx merge A1:D1
xl -f input.xlsx -s S1 -o output.xlsx unmerge A1:D1

xl comment <ref> <text> [--author name] / xl remove-comment <ref>

Add or remove a cell comment.

xl -f input.xlsx -s S1 -o output.xlsx comment A1 "Verify this figure" --author "Analyst"
xl -f input.xlsx -s S1 -o output.xlsx remove-comment A1

xl freeze <ref> / xl unfreeze

Freeze panes at a cell reference (rows above and columns to the left are locked), or remove them.

xl -f input.xlsx -s S1 -o output.xlsx freeze B2    # Freeze row 1 + column A
xl -f input.xlsx -s S1 -o output.xlsx unfreeze

Appearance & print setup: sheet-view, tab-color, page-setup, header-footer

The "deliverable finish" commands (GH-358). Each merges into the sheet's current settings: unspecified options are preserved. All require -o (or -i).

# Gridlines off + 85% zoom
xl -f in.xlsx -s Model -o out.xlsx sheet-view --gridlines off --zoom 85

# Tab colors: named, #hex, rgb(r,g,b), or theme:<slot>[:<tint>]
xl -f in.xlsx -s Model -o out.xlsx tab-color "#1F4E79"
xl -f in.xlsx -s Model -o out.xlsx tab-color theme:accent2:0.25
xl -f in.xlsx -s Model -o out.xlsx tab-color --clear     # clears a modeled color only

# Landscape, fit to one page wide and tall
xl -f in.xlsx -s Model -o out.xlsx page-setup --orientation landscape \
   --fit-to-width 1 --fit-to-height 1

# Confidential footer (&L/&C/&R sections; &P page, &N total, &D date, &F file, &A sheet)
xl -f in.xlsx -s Model -o out.xlsx header-footer \
   --odd-footer "&LProprietary & Confidential&RPage &P of &N"

Options:

Command Options
sheet-view --gridlines on|off, --zoom <10-400>, --tab-selected on|off
tab-color <color> or --clear
page-setup --orientation portrait|landscape, --scale <10-400>, --fit-to-width <n>, --fit-to-height <n>, --fit-to-page on|off
header-footer --odd-header/--odd-footer, --even-header/--even-footer, --first-header/--first-footer, --different-odd-even, --different-first

Notes:

  • Validation is up-front with clean errors (zoom/scale 10-400, orientation values, fit counts >= 1).
  • tab-color --clear removes the modeled color; a tab color already present in the source file's XML is preserved on write (preserve-if-None semantics) and cannot be stripped by the CLI.
  • page-setup --fit-to-page is tri-state: omitted derives the sheetPr fitToPage flag from --fit-to-width/--fit-to-height and preserves whatever the source carries; on forces the flag; off actively strips a preserved flag.
  • Even-page text sets different-odd-even automatically, first-page text sets different-first (Excel ignores the text while the corresponding flag is off).
  • Each command has a batch-op twin (sheet-view, tab-color, page-setup, header-footer) — the full deliverable finish is one batch file:
cat > finish.json <<'EOF'
[
  {"op": "sheet-view", "gridlines": false, "zoom": 85},
  {"op": "tab-color", "color": "#1F4E79"},
  {"op": "page-setup", "orientation": "landscape", "fitToWidth": 1, "fitToHeight": 1},
  {"op": "header-footer", "oddFooter": "&LProprietary & Confidential&RPage &P of &N"}
]
EOF
xl -f in.xlsx -s Model -o out.xlsx batch finish.json

xl cf add --range <range> --rule <dsl> / xl cf list

Author conditional formatting (GH-324). cf add appends one rule to a range (requires -o); cf list shows the sheet's rules (read-only). Priorities are auto-assigned in add order (lower priority wins in Excel) — the CLI never hand-stamps them.

Rule DSL (--rule):

Family Syntax Example
Cell value cellIs:<op>:<value> cellIs:greaterThan:100 (ops: lessThan/lt, lessThanOrEqual/lte, equal/eq, notEqual/ne, greaterThanOrEqual/gte, greaterThan/gt)
Range between:<lo>:<hi>, notBetween:<lo>:<hi> between:10:100
Formula expression:<formula> expression:MOD(ROW(),2)=0 (formula may contain :)
Color scale colorScale:<c1>:<c2>[:<c3>] colorScale:red:white:green (3-point mid at 50th percentile)
Data bar dataBar:<color> dataBar:#638EC6
Top/bottom N top10:<n>[:percent], bottom10:<n>[:percent] top10:5:percent
Text match text:<op>:<s> text:contains:overdue (ops: contains, notContains, beginsWith, endsWith; <s> may contain :)

Format flags (highlight rules — cellIs, between, notBetween, expression, top10, bottom10, text — require at least one; colorScale/dataBar carry inline colors and reject them): --bold, --italic, --underline, --strike, --bg <color>, --fg <color>. Flag colors accept the full color syntax including theme:accent1[:tint]; color tokens inside colorScale:/dataBar: rule strings accept named/#hex/rgb(r,g,b) only (the : separator conflicts with theme syntax).

# Red highlight for values over 100
xl -f f.xlsx -s S1 -o o.xlsx cf add --range A1:A10 \
   --rule 'cellIs:greaterThan:100' --bold --bg '#FFC7CE' --fg '#9C0006'

# 3-point color scale, then inspect
xl -f f.xlsx -s S1 -o o.xlsx cf add --range B2:B20 --rule 'colorScale:red:white:green'
xl -f o.xlsx -s S1 cf list

Batch op cf mirrors the command:

{"op": "cf", "range": "A1:A10", "rule": "cellIs:greaterThan:100", "bold": true, "bg": "#FFC7CE"}

xl chart add --type <t> --data <range> --at <ref> [options]

Add a typed chart built from sheet data ranges. Supported types: column, bar (horizontal), line, pie.

# Column chart: one series per data column, categories down column A
xl -f in.xlsx -s Data -o out.xlsx chart add --type column \
   --data B2:D10 --categories A2:A10 --series-names "Q1,Q2,Q3" \
   --title "Revenue" --at F2:K15

# Stacked horizontal bars, no legend, single-cell placement (default ~5.6x2.8cm size)
xl -f in.xlsx -s Data -o out.xlsx chart add --type bar --grouping stacked \
   --data B2:D10 --legend none --at F2

# Pie over one series
xl -f in.xlsx -s Data -o out.xlsx chart add --type pie \
   --data B2:B6 --categories A2:A6 --at E2:J12
Flag Description
--type, -t column, bar, line, pie (required)
--grouping clustered (default), stacked, percent-stacked — column/bar only
--data Values range; qualified refs (Data!B2:D10) accepted (required)
--categories Categories vector (one row or one column)
--series-names Comma-separated literal names, applied positionally
--series-colors Comma-separated colors (#307FE2,#005670), applied positionally; unset series cycle the theme accents. Pie: colors map per slice (c:dPt), slices past the list continue the accent cycle
--title Chart title
--legend right (default), left, top, bottom, top-right, none
--at Placement: a range (chart stretches over it) or a single cell (required)

Series split (deterministic): orientation follows the categories vector — column categories (A2:A10) make one series per data column, row categories (B1:D1) one per data row, absent categories default to per-column. A dimension mismatch between categories and data is an error, never a guess. Pie charts require exactly one series.


xl add-image <image-file> --at <ref> [--size WxH]

Embed an image. PNG/JPEG/GIF/BMP get natural-size sniffing for a single-cell --at; TIFF/EMF/WMF need an explicit --size. A range --at stretches the image over the range.

xl -f in.xlsx -s S1 -o out.xlsx add-image logo.png --at B2               # natural size
xl -f in.xlsx -s S1 -o out.xlsx add-image logo.png --at B2 --size 320x240
xl -f in.xlsx -s S1 -o out.xlsx add-image banner.jpeg --at A1:F4         # stretch over range

xl import <csv-file> [start-ref] [options]

Import CSV data with automatic type detection (numbers, booleans, ISO dates).

Arguments:

Arg Type Required Default Description
csv-file string Yes CSV file path
start-ref string No A1 Cell where import starts
--delimiter char No , Field separator
--encoding string No UTF-8 Input encoding
--skip-header flag No false Skip first row (do not import)
--new-sheet string No Create new sheet for imported data
--no-type-inference flag No false Treat all values as text
xl -f f.xlsx -s S1 -o o.xlsx import data.csv A1 --delimiter ";" --skip-header
xl -f f.xlsx -o o.xlsx import data.csv --new-sheet "Imported"

Limitations: entire CSV is loaded into memory (recommended <50k rows); dates must be ISO 8601 (YYYY-MM-DD).


xl import-md <md-file|-> [--start ref] [options]

Import a GFM (GitHub Flavored Markdown) pipe table with smart type detection. Use - to read from stdin — handy for LLM agents that generate tables inline.

Arguments:

Arg Type Required Default Description
md-file string Yes Markdown file path, or - for stdin
--start string No A1 Top-left cell for the imported table
--skip-header flag No false Skip the table's header row (do not import)
--new-sheet string No Create new sheet for imported data
--no-type-inference flag No false Treat all values as text
xl -f f.xlsx -s S1 -o o.xlsx import-md table.md --start B2
xl -f f.xlsx -o o.xlsx import-md table.md --new-sheet "Data"
printf '| A | B |\n|---|---|\n| 1 | 2 |\n' | xl -f f.xlsx -s S1 -o o.xlsx import-md -

Table format (GFM): header row, delimiter row (|---|---|), body rows. The first table found in the input is imported (preamble prose is skipped); the table ends at the first blank line. Outer pipes are optional, \| inside a cell is a literal pipe, and cell whitespace is trimmed. Body rows are padded/truncated to the delimiter row's column count.

Type detection (per cell, same smart detection as batch put): currency $1,234.56 → Number + Currency format, percent 45.5%0.455 + Percent format, ISO dates 2025-01-15 → date-formatted cell, plain numbers and true/false → typed values, everything else → text. Opt out with --no-type-inference.

Alignment: GFM markers map to cell horizontal alignment — :--- left, :---: center, ---: right (no marker leaves alignment unset).

Limitations: input is read as UTF-8 and parsed in memory; one table per import (first wins).


Sheet management: add-sheet, remove-sheet, rename-sheet, move-sheet, copy-sheet

xl -f f.xlsx -o o.xlsx add-sheet Summary --after Sheet1      # or --before <name>
xl -f f.xlsx -o o.xlsx remove-sheet Scratch
xl -f f.xlsx -o o.xlsx rename-sheet "Old Name" "New Name"
xl -f f.xlsx -o o.xlsx move-sheet Summary --to 0             # or --after/--before <name>
xl -f f.xlsx -o o.xlsx copy-sheet Template "Q2 Report"

Structural editing: insert-rows, delete-rows, insert-cols, delete-cols

Insert or delete rows/columns with full formula-reference rewriting: references at or past the cut shift, straddling ranges shrink, and references to deleted cells become #REF!. Cross-sheet references to the edited sheet are rewritten too.

Arguments: position (<at-row> 1-based, or <at-col> letter) and optional <count> (default 1). Column commands also accept an inclusive range (C:E), which overrides the count.

xl -f f.xlsx -s S1 -o o.xlsx insert-rows 5 2     # Insert 2 rows at row 5
xl -f f.xlsx -s S1 -o o.xlsx delete-rows 5       # Delete row 5
xl -f f.xlsx -s S1 -o o.xlsx insert-cols B 3     # Insert 3 columns at B
xl -f f.xlsx -s S1 -o o.xlsx delete-cols C:E     # Delete columns C through E

Example formula rewriting: deleting row 2 turns =A1+A3 into =A1+A2; =SUM(A1:A4) shrinks to =SUM(A1:A3); a direct reference to a deleted cell becomes #REF!.


xl batch <file|->

Apply multiple operations atomically from JSON input.

Arguments:

Arg Type Required Description
file string No (default -) JSON file path or - for stdin
--dry-run flag No Validate JSON and show a summary without writing (works without -f/-o)

JSON Schema:

[
  {"op": "put", "ref": "A1", "value": "Hello"},
  {"op": "putf", "ref": "B1", "value": "=A1*2"},
  {"op": "style", "range": "A1:B1", "bold": true},
  {"op": "merge", "range": "A1:D1"},
  {"op": "colwidth", "col": "A", "width": 15.5},
  {"op": "rowheight", "row": 1, "height": 30},
  {"op": "comment", "ref": "A1", "text": "Revenue figure", "author": "Analyst"},
  {"op": "autofit", "columns": "A:D"},
  {"op": "add-sheet", "name": "Summary", "after": "Sheet1"}
]

Supported Operations:

Operation Required Fields Optional Fields Description
put ref, value format, values, detect Write value to cell
putf ref, value from, values, format Write formula(s) to cell(s); format applies a number format to the formula cell(s)
style range styling options Apply cell styling
merge range Merge cells
unmerge range Unmerge cells
colwidth col, width Set column width
rowheight row, height Set row height
comment ref, text author Add cell comment
remove-comment ref Remove cell comment
hyperlink ref target Set cell hyperlink (omit target to clear)
clear range all, styles, comments Clear cell contents/styles/comments
col-hide col Hide column
col-show col Show column
row-hide row Hide row
row-show row Show row
autofit columns Auto-fit column widths
add-sheet name after Add new sheet
rename-sheet from, to Rename sheet
freeze ref Freeze panes at cell
unfreeze Remove freeze panes
copy source, target valuesOnly Copy range with formula adjustment
sheet-view gridlines, zoom, tabSelected Set sheet view options (operates on --sheet)
tab-color color, clear Set (color) or clear (clear: true) the sheet tab color
page-setup orientation, scale, fitToWidth, fitToHeight, fitToPage Set print page setup
header-footer oddHeader, oddFooter, evenHeader, evenFooter, firstHeader, firstFooter, differentOddEven, differentFirst Set print header/footer text
cf range, rule bold, italic, underline, strike, bg, fg Add a conditional-formatting rule (see cf add)
chart type, data, at categories, seriesNames, seriesColors, title, legend, grouping Add a chart (mirrors chart add)

Native JSON Types (recommended):

// Numbers are stored as numeric values (not text)
{"op": "put", "ref": "A1", "value": 99.0}

// Booleans
{"op": "put", "ref": "A2", "value": true}

// With explicit format
{"op": "put", "ref": "A3", "value": 99.0, "format": "currency"}
{"op": "put", "ref": "A4", "value": 0.594, "format": "percent"}

Format Options:

Format Name Description Example Output
general Default format 1234.5
integer Whole numbers 1235
decimal Two decimal places 1234.50
currency Currency with symbol $1,234.50
percent Percentage 59%
percent_decimal Percentage with decimals 59.4%
date Date format 11/10/25
datetime Date and time 11/10/25 14:30
time Time only 14:30:00
text Text format 1234.5
custom Any Excel format code See below

Custom Format Codes:

// MOIC/Multiple format (3.5x)
{"op": "put", "ref": "A1", "value": 3.5, "format": "0.0x"}

// Accounting format with negatives in parentheses
{"op": "put", "ref": "A2", "value": -1234, "format": "$#,##0;($#,##0)"}

// Basis points
{"op": "put", "ref": "A3", "value": 50, "format": "0 \"bps\""}

// Custom date format
{"op": "put", "ref": "A4", "value": "2025-11-10", "format": "yyyy-mm-dd"}

// Quoted-literal / semicolon-only codes are codes too (the 1/0 toggle-flag idiom)
{"op": "put", "ref": "A5", "value": 1, "format": "\"Yes \";;\"No \""}

Unrecognized format strings (GH-475): a string that is neither a known name nor Excel format-code-shaped is a typo far more often than a code.

  • On the put/putf format hint it is ignored, with a warning on stderr naming the string and listing the known names (format: "curency" → warning, cell stays General).
  • On the style op's numFormat it is still applied as a custom code (Excel, not xl, is the authority on codes) but warns the same way.

The --stream batch path applies exactly the same table, including custom codes — it used to know only six names and dropped everything else in silence.

Smart String Detection (enabled by default):

Strings are automatically detected and formatted:

// Currency detected from $ prefix
{"op": "put", "ref": "A1", "value": "$99.00"}  // → Number(99.0), Currency

// Percent detected from % suffix
{"op": "put", "ref": "A2", "value": "59.4%"}   // → Number(0.594), Percent

// ISO date detected
{"op": "put", "ref": "A3", "value": "2025-11-10"}  // → DateTime, Date format

// Plain text (no detection pattern)
{"op": "put", "ref": "A4", "value": "Hello"}       // → Text

Disable Detection: Set "detect": false to treat strings as plain text:

{"op": "put", "ref": "A1", "value": "$99.00", "detect": false}  // → Text "$99.00"
{"op": "put", "ref": "A2", "value": "59.4%", "detect": false}   // → Text "59.4%"

Formula Dragging (putf with range):

// Single formula dragged across range (uses Excel $ anchoring)
{"op": "putf", "ref": "B2:B10", "value": "=SUM($A$1:A2)", "from": "B2"}

// Explicit formulas for each cell (no dragging)
{"op": "putf", "ref": "B2:B4", "values": ["=A2*2", "=A3*2", "=A4*2"]}

Formula Number Formats (putf with format, parity with put):

The format field accepts the same named formats and custom codes as put and applies the number format to the formula cell(s) — no second style pass needed. Works with all three variants (single, dragging, explicit values):

{"op": "putf", "ref": "C1", "value": "=A1*2", "format": "#,##0.0"}
{"op": "putf", "ref": "B2:B10", "value": "=A2/A$1", "from": "B2", "format": "percent"}
{"op": "putf", "ref": "D1:D2", "values": ["=SUM(A:A)", "=SUM(B:B)"], "format": "currency"}

Style Options:

Option Type Description
bold boolean Bold text
italic boolean Italic text
underline boolean Underlined text
bg string Background color (hex: #FF0000)
fg string Font color (hex: #0000FF)
fontSize number Font size in points
fontName string Font family name
align string Horizontal alignment: left, center, right, justify
valign string Vertical alignment: top, middle, bottom
wrap boolean Enable text wrapping
numFormat string Number format (see Format Options above)
border string All borders: none, thin, medium, thick
borderTop string Top border style
borderRight string Right border style
borderBottom string Bottom border style
borderLeft string Left border style
borderColor string Border color (hex)
replace boolean Replace style instead of merge (default: false)

Note: Use align for horizontal alignment, not halign. Unknown properties are ignored with a warning.

Examples:

# From file
xl -f input.xlsx -o output.xlsx batch operations.json

# From stdin (pipe)
echo '[{"op": "put", "ref": "A1", "value": 100, "format": "currency"}]' | \
  xl -f input.xlsx -s Sheet1 -o output.xlsx batch -

# Complex workflow
cat <<'EOF' | xl -f input.xlsx -s Sheet1 -o output.xlsx batch -
[
  {"op": "put", "ref": "A1", "value": "Revenue", "format": "text"},
  {"op": "put", "ref": "B1", "value": 1000000, "format": "currency"},
  {"op": "style", "range": "A1:B1", "bold": true, "bg": "#FFFF00"},
  {"op": "merge", "range": "A1:B1"},
  {"op": "colwidth", "col": "A", "width": 20}
]
EOF

Common Gotchas

Scenario Behavior Workaround
Leading zeros: "00123" Smart detection converts to number 123 Use "detect": false to preserve as text
Mixed patterns: "50 (50%)" First pattern wins (treated as text) Use explicit "format" field
values array length mismatch Error raised if array length ≠ range cell count Ensure exact match
Percent as decimal "59.4%" stored as 0.594 Excel displays correctly with percent format
Invalid custom formats Accepted but may render incorrectly in Excel Test format codes in Excel first
--stream mode Supports formula dragging but not formula evaluation Use non-streaming for --eval

xl diff -g <file2> [--format markdown|json]

Compare two workbooks and report differences. The first file comes from the global -f, the second from -g/--file2. Optional global -s/--sheet restricts the comparison to one sheet.

Arguments:

Arg Type Required Default Description
-g, --file2 path Yes Second file to compare against
--format string No markdown markdown (human) or json (stable schema)

Exit codes (diff-tool convention): 0 identical, 1 differences found, 2 error.

What is compared (per sheet, refs in A1, row-major order):

  • Changed cells — value, formula text, and resolved style (styleChanged boolean). Formula cells compare by formula text; cached values are derived and ignored. Styles compare resolved formatting (style id lookup), so equal formatting under different ids is not a difference.
  • Added / removed cells — a cell with Empty value, default style, and no hyperlink counts as absent.
  • Sheets added / removed (by name).
  • Merged ranges, comments, hyperlinks — separate added/removed/changed deltas per sheet.
xl -f old.xlsx diff -g new.xlsx                      # Markdown report
xl -f old.xlsx -s Sheet1 diff -g new.xlsx            # One sheet only
xl -f old.xlsx diff -g new.xlsx --format json        # Machine-readable
xl -f old.xlsx diff -g new.xlsx && echo "unchanged"  # Exit-code driven

JSON schema (stable; sheets lists only sheets with differences):

{
  "identical": false,
  "sheetsAdded": [], "sheetsRemoved": [],
  "sheets": [{
    "name": "Sheet1",
    "added":   [{"ref": "D5", "value": "New", "formula": null}],
    "removed": [{"ref": "E5", "value": "Old", "formula": null}],
    "changed": [{"ref": "A5",
                 "before": {"value": "Revenue", "formula": null},
                 "after":  {"value": "Total Revenue", "formula": null},
                 "styleChanged": false}],
    "mergesAdded": [], "mergesRemoved": [],
    "commentsAdded": [], "commentsRemoved": [], "commentsChanged": [],
    "hyperlinksAdded": [], "hyperlinksRemoved": [], "hyperlinksChanged": []
  }]
}

Limitations: both workbooks load in memory (--max-size applies to each); no range-level filter yet.


xl lint [<file>] [--format text|json]

Validate the raw package structure against the Excel-repair classes — the defects Excel repairs loudly (repair dialog, content stripped) but every lenient reader, xl's own read included, accepts silently. Lint inspects the raw zip parts, never the parsed model, so nothing gets normalized before it's checked.

xl lint deliverable.xlsx                         # Positional file form
xl -f deliverable.xlsx lint                      # Flag form (equivalent)
xl -f deliverable.xlsx lint --format json        # Stable schema for pipelines
xl -f deliverable.xlsx lint && echo "safe to send"

What it flags (the complete LintCategory roster — a test pins this list against LintCategory.slug, so it cannot drift):

  • child-order — child elements of xl/workbook.xml (CT_Workbook) or a worksheet (CT_Worksheet) out of ECMA-376 schema sequence (e.g. <externalReferences> after <extLst>)
  • unresolved-rel-id — an r:id (sheet, externalReference, pivotCache, drawing, legacyDrawing, hyperlink, tablePart, …) with no entry in the paired .rels
  • wrong-rel-type — the r:id resolves, but to a relationship of the wrong type
  • missing-part — the relationship target part is absent from the package
  • missing-content-type — a present-and-referenced part has neither an <Override> nor a matching extension <Default> in [Content_Types].xml
  • ref-out-of-bounds — a ref/sqref/dimension token past row 1048576 or column XFD
  • data-table-torn — a <f t="dataTable"> record whose grid no longer holds together (missing record cell, inconsistent inputs, interior no longer matching the record)
  • data-table-unseeded — an uncached data-table interior in a calcMode="autoNoTable" book: Excel does not recompute data tables on open, so the grid opens BLANK
  • formula-leading-equals<f> text stored with the display form's leading = (non-spec; strict readers like openpyxl misread it — re-writing the file with xl heals it)

Exit codes (diff-tool convention): 0 no findings · 1 findings reported · 2 error (unreadable file, malformed core part).

xl lint is read-only — it never repairs or rewrites the file. xl's own output always lints clean; use it as a pre-send self-check in agent pipelines that splice or post-process workbooks.


Output Format

Cell Reference Convention

Always include explicit references in output:

# Good - References visible
|   | A        | B       |
|---|----------|---------|
| 1 | Revenue  | $1M     |
| 2 | COGS     | $400K   |

Error Format

Error: <ErrorType>
Location: <Context>
Details: <Human-readable explanation>
Suggestion: <How to fix>

Example:

Error: CircularReference
Location: B10
Details: Formula =A10+B10 creates cycle: B10 → A10 → B10
Suggestion: Use a different cell reference to break the cycle

See Also