{"id":50706099,"url":"https://github.com/zemse/xls","last_synced_at":"2026-06-12T15:01:46.370Z","repository":{"id":362902335,"uuid":"1261133470","full_name":"zemse/xls","owner":"zemse","description":"A terminal spreadsheet: read/write XLS (BIFF8), XLSX and CSV with a built-in formula engine, CLI and TUI.","archived":false,"fork":false,"pushed_at":"2026-06-06T21:56:17.000Z","size":491,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-09T12:24:12.669Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zemse.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-06T09:26:27.000Z","updated_at":"2026-06-09T03:43:41.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/zemse/xls","commit_stats":null,"previous_names":["zemse/xls"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/zemse/xls","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zemse%2Fxls","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zemse%2Fxls/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zemse%2Fxls/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zemse%2Fxls/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zemse","download_url":"https://codeload.github.com/zemse/xls/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zemse%2Fxls/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34153483,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-09T12:01:15.049Z","updated_at":"2026-06-10T13:00:50.274Z","avatar_url":"https://github.com/zemse.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# xls\n\n**A spreadsheet for your terminal.** `xls` reads and writes Excel `.xls`\n(BIFF8), `.xlsx` (OOXML) and `.csv` files, evaluates Excel formulas with a\nbuilt-in engine covering all 522 worksheet functions (incl. dynamic-array spill\nand `LAMBDA`), and ships both a scriptable\nCLI and an interactive, mouse-aware TUI — all in a single Rust crate with no\nexternal runtime.\n\n```\n┌ Data ───────────────────────────────────────────────┐\n│     A          B          C          D               │\n│ 1  Region    Units      Price      Total             │\n│ 2  North       120      12.50    =B2*C2  → 1500.00   │\n│ 3  South        90      12.50    =B3*C3  → 1125.00   │\n│ 4  ────────────────────────────────────────          │\n│ 5  Total    =SUM(B2:B3)        =SUM(D2:D3) → 2625.00 │\n└──────────────────────────────────────────────────────┘\n NORMAL  D5  =SUM(D2:D3)  number                    *modified\n```\n\n\u003e Status: pre-release (`0.1`). The core (data model, formula engine, file I/O)\n\u003e is well-tested, including dynamic-array spill and `LAMBDA`.\n\n## Why\n\n- **It writes `.xls`.** No other pure-Rust crate writes the legacy BIFF8 binary\n  format. `xls` owns the format end-to-end (read *and* write).\n- **Real formulas.** A from-scratch engine — lexer, Pratt parser, dependency\n  graph, topological recalculation, circular-reference detection — not a\n  thin wrapper.\n- **Round-trip fidelity.** Styles, number formats, merged cells and frozen panes\n  are preserved; unrecognized parts (charts, drawings, pivots, VBA) are kept\n  verbatim so saving never destroys data.\n- **One crate, one binary.** `core` / `cli` / `tui` are internal modules behind\n  Cargo features, so `cargo install xls` ships exactly one artifact and the\n  library build never pulls UI dependencies.\n\n## Install\n\n```sh\n# from crates.io (once published)\ncargo install xls\n\n# from source\ngit clone https://github.com/zemse/xls \u0026\u0026 cd xls\ncargo install --path .\n```\n\n## CLI\n\n```sh\n# Read / inspect\nxls info report.xlsx                 # format, sheets, dimensions, date system\nxls get report.xlsx Sheet1!B2        # print a cell's displayed value\nxls get report.xlsx 'A1:J200' --format csv   # dump a range (table|csv|tsv|json|jsonl|md)\nxls get report.xlsx 'A1:J200' --format json --header  # array of objects keyed by header row\nxls get report.xlsx 'A1:J200' --raw --dates iso       # stored values; dates as ISO/serial\nxls eval data.csv '=AVERAGE(A1:A10)' # evaluate a formula against a file\nxls eval data.csv '=C2:C9' --format csv               # array results print as a grid\nxls format report.xlsx C2            # print a cell's number format (DATE/NUMBER/GENERAL)\nxls head report.xlsx -n 20           # first / last N rows (head / tail)\nxls grep report.xlsx ZANMAI          # matching cells with their addresses\nxls profile report.xlsx amount       # column stats + \"numbers stored as text\" warning\nxls diff before.xlsx after.xlsx --key date  # keyed row diff (omit --key for cell-by-cell)\n\n# Reshape / combine (read-only verbs print to stdout)\nxls pivot report.xlsx --rows category --values amount --agg sum   # group + aggregate\nxls filter report.xlsx 'amount\u003e1000' --format csv                 # rows matching a predicate\nxls join a.xlsx b.xlsx --on id       # inner-join two sheets on a key\nxls query report.xlsx \"SELECT category, SUM(amount) AS total FROM Sheet1 GROUP BY category ORDER BY total DESC\"\n                                     # read-only SQL: sheets are tables (row 0 = header), WHERE/GROUP BY/JOIN/ORDER BY/LIMIT\n\n# Write / mutate (each edits in place and saves)\nxls new book.xlsx                    # create an empty workbook\nxls set report.xlsx A1 '=SUM(B:B)'   # set a cell (formula/number/text), recalc, save\nxls batch report.xlsx --set A1=1 --set B2=hi   # many edits, one atomic open/save\nxls clear report.xlsx A1:B10         # clear a range\nxls fill report.xlsx A1:A10 0        # set a whole range to one value\nxls sort report.xlsx --by amount --desc        # stable multi-key sort (header kept)\nxls dedup report.xlsx --on id        # drop duplicate rows by key column(s)\nxls append base.xlsx new.xlsx        # append rows, aligned by header name\nxls to-number report.xlsx H1:H200    # force-convert any numeric text in a range\nxls to-date report.xlsx A2:A83 --format dd/mm/yyyy   # force-convert text dates to real dates\nxls format-set report.xlsx C2:C154 'dd/mm/yyyy'   # set a number format\nxls autofit report.xlsx              # fit column widths to content\nxls style report.xlsx A1:D1 --bold --bg FFFF00    # basic styling\nxls copy report.xlsx A1:B3 D1        # copy a range to an anchor (move = copy+clear)\nxls insert-row report.xlsx 3 -n 2    # insert 2 blank rows before row 3\nxls delete-col report.xlsx C         # delete column C (letter or number)\nxls add-sheet report.xlsx Summary    # add / delete / rename sheets\nxls rename-sheet report.xlsx Sheet1 Data\n\n# Safety flags on any mutating command\nxls set report.xlsx A1 x --dry-run   # print the diff, write nothing\nxls set report.xlsx A1 x --backup    # write report.xlsx.bak first\nxls set report.xlsx A1 x --output copy.xlsx        # write a copy, leave the input\n\n# Named ranges \u0026 Excel tables\nxls table add report.xlsx A1:C20 --name Sales      # define a table over a range\nxls get report.xlsx 'Sales[Amount]' --format csv   # read a table column\nxls eval report.xlsx '=SUM(Sales[Amount])'         # structured references in formulas\nxls name add report.xlsx TaxRate 'Sheet1!$E$1'     # define a named range\nxls info report.xlsx                               # lists named ranges + tables\n\n# Convert / import / interactive\nxls export old.xls --format xlsx     # convert between formats\nxls export report.xlsx -f csv -o -   # stream a text format to stdout\nxls export huge.xlsx -f csv -o out.csv --stream    # memory-bounded export of huge files\nxls import data.csv --into book.xlsx # add a CSV as a sheet\nxls open book.xlsx                    # launch the interactive TUI\n\ncat data.csv | xls eval - '=SUM(A:A)'              # read CSV from stdin with `-`\ncat data.csv | xls set - A1 x --output out.csv     # stdin write needs --output\n\n# Password-protected (encrypted) workbooks\nxls info statement.xlsx              # without -p: reports the encryption scheme\nxls get statement.xlsx B5 -p secret  # decrypt in-memory with --password / -p\n```\n\n`--password`/`-p` is a global flag accepted by any command. Without it, an\nencrypted file is reported with its scheme (e.g. *ECMA-376 agile encryption:\nAES-128-CBC, SHA1*) instead of opening. Mutating an encrypted file and saving\nwrites an **unencrypted** copy (re-encryption is not supported), and prints a\nwarning.\n\nRun `xls --help` for the full reference and `xls \u003ccmd\u003e --help` per subcommand.\n\n\u003e **Numbers stored as text:** numeric functions (`SUM`, `AVERAGE`, `MIN`, …)\n\u003e coerce numeric-looking text (e.g. `6,000.00`, `1,51,302.63` from bank/CSV\n\u003e exports) at evaluation time, so such cells contribute without rewriting your\n\u003e data — the cells stay text and the file is untouched. `COUNT` stays strict\n\u003e (so `COUNT` \u003c `COUNTA` flags text-stored numbers). To convert permanently,\n\u003e use `xls to-number \u003cfile\u003e \u003crange\u003e` or the TUI `:tonum` on a selection.\n\u003e\n\u003e **Dates stored as text:** likewise, exports often store dates as text\n\u003e (`\"04/04/2025\"`). `xls to-date \u003cfile\u003e \u003crange\u003e --format dd/mm/yyyy` parses them\n\u003e into real date serials and applies the format, so `get --raw --dates serial`\n\u003e (or `iso`) then emits machine-readable values. `xls profile` flags text-stored\n\u003e dates the same way it flags text-stored numbers.\n\n## TUI\n\n`xls open \u003cfile\u003e` (or just `xls \u003cfile\u003e`) launches a modal, vim-flavored editor:\n\n- **Navigate** — arrows, `Tab`/`Enter`, `Ctrl+Arrow` (jump to data edge),\n  `Home`/`End`, `Ctrl+Home`/`Ctrl+End`, `PgUp`/`PgDn`.\n- **Select** — `Shift+Arrow` for ranges; mouse click \u0026 drag.\n- **Edit** — type or `F2` to edit, `Esc` cancels, `Enter`/`Tab` commits and\n  recalculates. Formula bar with live address.\n- **Mouse** — click to select, drag to extend, wheel to scroll, click sheet tabs.\n- **Clipboard** — `Ctrl+C`/`X`/`V` (internal ranges + system clipboard).\n- **Undo/redo** — `Ctrl+Z` / `Ctrl+Y`.\n- **Dialogs** — `Ctrl+G`/`F5` go-to, `Ctrl+F` find, `:` command palette,\n  `Ctrl+S` save.\n\nVirtual scrolling renders only visible cells, with frozen rows/columns and\nnumber-format-aware display.\n\n## Library\n\n`default-features = false` gives a UI-free library exposing just `core`:\n\n```toml\n[dependencies]\nxls = { version = \"0.1\", default-features = false }\n```\n\n```rust\nuse xls::core::{Workbook, Cell};\nuse xls::core::formula::Engine;\n\nlet mut wb = Workbook::new();\nlet s = wb.sheet_mut(0).unwrap();\ns.set_a1(\"A1\", Cell::Number(2.0));\ns.set_a1(\"A2\", Cell::Number(3.0));\ns.set_a1(\"A3\", Cell::Formula { expr: \"=A1+A2\".into(), cached: Default::default() });\n\nEngine::new().recalc(\u0026mut wb);\nassert_eq!(wb.display_cell(0, 2, 0), \"5\");\n\nxls::core::save_path(\u0026wb, std::path::Path::new(\"out.xlsx\")).unwrap();\n```\n\n## Formula coverage\n\nAll of Excel's standard worksheet functions are implemented one-by-one, grouped\nby Microsoft category, each with known-answer tests (**all 522 implemented**).\n\n| Category | Examples |\n|----------|----------|\n| Logical | `IF` `IFS` `SWITCH` `AND` `OR` `XOR` `IFERROR` |\n| Math \u0026 Trig | `SUM` `SUMIFS` `SUMPRODUCT` `ROUND` `MOD` trig, `MDETERM` `MMULT` `SUBTOTAL` `AGGREGATE` |\n| Statistical | `AVERAGEIFS` `MEDIAN` `STDEV.S` `PERCENTILE.INC` `RANK.EQ` `NORM.DIST` `CHISQ.TEST` `FREQUENCY` |\n| Text | `LEFT` `MID` `SUBSTITUTE` `TEXT` `TEXTJOIN` `TEXTBEFORE` `REGEXEXTRACT` `TEXTSPLIT` |\n| Lookup \u0026 Reference | `VLOOKUP` `XLOOKUP` `INDEX` `MATCH` `OFFSET` `INDIRECT` `XMATCH` |\n| Dynamic arrays | `SORT` `SORTBY` `UNIQUE` `FILTER` `SEQUENCE` `VSTACK`/`HSTACK` `TAKE`/`DROP` (with cell **spill**) |\n| LAMBDA | `LAMBDA` `LET` `MAP` `REDUCE` `SCAN` `BYROW`/`BYCOL` `MAKEARRAY` |\n| Date \u0026 Time | `DATE` `EDATE` `EOMONTH` `NETWORKDAYS` `YEARFRAC` `WEEKNUM` |\n| Financial | `PMT` `NPV` `IRR` `XIRR` `PRICE` `YIELD` `DURATION` `MIRR` |\n| Engineering | base conversions, `BITAND`, `CONVERT`, `ERF`, complex `IM*` |\n| Information | `ISNUMBER` `ISERROR` `N` `TYPE` `CELL` `SHEET` |\n| Database | `DSUM` `DAVERAGE` `DGET` `DCOUNT` … |\n\nDynamic-array results **spill** into neighboring cells (with `#SPILL!` on\nobstruction), and `LAMBDA` values are first-class (callable via `LET` and the\nhigher-order helpers). Operators broadcast element-wise over ranges/arrays\n(`A1:A10\u003e5`); scalar *functions* don't auto-map over an array argument — use\n`MAP` for that (e.g. `MAP(SEQUENCE(10), LAMBDA(x, MOD(x,2)))`). **Stubbed** (documented `#N/A`): functions needing\nexternal data, a host app, or a cube/OLAP connection (`WEBSERVICE`,\n`STOCKHISTORY`, `CUBE*`, `RTD`, …).\n\n## Format support\n\n| Format | Read | Write | Notes |\n|--------|:----:|:-----:|-------|\n| XLSX (OOXML) | ✅ | ✅ | styles, shared strings, formulas, merges, frozen panes, named ranges, tables; opaque parts preserved; streaming reads for huge files |\n| XLS (BIFF8) | ✅ | ✅ | the only pure-Rust BIFF8 **writer**; formulas stored as cached values |\n| CSV / TSV | ✅ | ✅ | delimiter auto-detection, BOM handling, `encoding_rs` transcoding, type inference |\n\nPassword-protected files are decrypted with `--password` (ECMA-376 agile and\nstandard encryption — AES with SHA-1/256/384/512); legacy binary RC4/XOR and the\nrare \"extensible\" scheme are detected and named but not decrypted.\n\n## Building \u0026 testing\n\n```sh\ncargo test                          # full suite (CLI + TUI features)\ncargo test --no-default-features    # core only — fast, no UI deps\ncargo clippy --all-targets --all-features -- -D warnings\ncargo bench                         # recalc + xlsx round-trip timings\ncargo run --example gen_fixtures    # regenerate the binary test fixtures\n```\n\nThe data model uses sparse storage (`BTreeMap` keyed by cell position), so a\nsheet with a handful of cells in a million-row grid costs only those cells.\n\n## License\n\nLicensed under either of [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE) at\nyour option.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzemse%2Fxls","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzemse%2Fxls","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzemse%2Fxls/lists"}