Small command-line converters between plain-text source formats and their Microsoft Office (or Office-adjacent) counterparts. The plain-text side (Markdown, CSV) is the one you author and version; the other side is the export you hand to people who live in Word, Excel, PowerPoint, or LibreOffice Calc. The SQLite and Parquet converters run the other way, turning a database or data file into CSV or a spreadsheet someone can open, and the OpenDocument converters move Word and Excel files to and from LibreOffice's formats without using LibreOffice.
| Command | Direction | Engine |
|---|---|---|
md2docx |
Markdown → Word .docx |
pandoc |
docx2md |
Word .docx → Markdown |
pandoc |
csv2xlsx |
CSV → Excel .xlsx |
openpyxl |
xlsx2csv |
Excel .xlsx → CSV |
openpyxl |
sqlite2csv |
SQLite table(s) → CSV | stdlib sqlite3 |
sqlite2xlsx |
SQLite table(s) → Excel .xlsx |
openpyxl |
sqlite2ods |
SQLite table(s) → OpenDocument .ods |
odfpy |
parquet2csv |
Parquet → CSV | DuckDB CLI |
md2pptx |
Markdown → PowerPoint .pptx |
pandoc |
docx2odt |
Word .docx → OpenDocument .odt |
pandoc |
odt2docx |
OpenDocument .odt → Word .docx |
pandoc |
xlsx2ods |
Excel .xlsx → OpenDocument .ods |
openpyxl, odfpy |
ods2xlsx |
OpenDocument .ods → Excel .xlsx |
odfpy, openpyxl |
PowerPoint is export-only: pandoc writes .pptx but cannot read it, so there
is no pptx2md, and for the same reason no pptx2odp.
From the Excelano apt repository, which pulls in pandoc, openpyxl, and odfpy for you:
curl -fsSL https://excelano.com/apt/setup.sh | sudo sh # one-time
sudo apt install this2thatOr from a clone, which puts each script in ~/bin under its short name:
sudo apt install pandoc python3-openpyxl python3-odf
./install.shparquet2csv also needs the DuckDB CLI, which Debian does not package, so
neither route installs it; get it from
duckdb.org. The other converters
work without it.
Uninstall the clone-installed copies with ./uninstall.sh. To build the
package yourself, run packaging/build-deb.sh; the scripts are
architecture-independent, so one _all.deb serves every machine. A release
goes through the Excelano shipping apps: build-release bumps Version: in
packaging/control, builds the package with packaging/build-deb.sh, tags,
and creates the GitHub release, and apt-ship this2that v<version> publishes
that release's .deb to the apt repository.
Every command takes an input file and an optional output path; the output defaults to the input name with the new extension.
md2docx report.md # -> report.docx
md2docx report.md final.docx # explicit output
docx2md proposal.docx # -> proposal.md (images to ./media/)
csv2xlsx data.csv # -> data.xlsx (styled, typed)
csv2xlsx a.csv b.csv -o book.xlsx # -> one sheet per CSV, named by file stem
xlsx2csv workbook.xlsx --sheet Q3 # -> workbook.csv from the Q3 sheet
sqlite2csv app.db # -> app.csv, if app.db has exactly one table
sqlite2csv app.db --table users # -> app.csv from the users table
sqlite2csv app.db --all -o out/ # -> out/users.csv, out/orders.csv, ...
sqlite2xlsx app.db # -> app.xlsx, one sheet per table
sqlite2xlsx app.db --table users # -> app.xlsx, single sheet from users
sqlite2ods app.db # -> app.ods, one sheet per table
parquet2csv events.parquet # -> events.csv
md2pptx deck.md # -> deck.pptx
docx2odt proposal.docx # -> proposal.odt
odt2docx minutes.odt # -> minutes.docx
xlsx2ods budget.xlsx # -> budget.ods, formulas rewritten
ods2xlsx budget.ods # -> budget.xlsx, formulas rewrittenThe Python converters (csv2xlsx, xlsx2csv, xlsx2ods, ods2xlsx,
odt2docx, and the sqlite2* converters) report their version with --version
or -V. The number is stamped from
packaging/control when the package is built or install.sh runs, so a
converter run straight out of a clone reports 0.0.0+dev rather than
claiming a release it is not. The shell wrappers (md2docx, docx2md,
md2pptx, docx2odt, and parquet2csv) are short enough to sit inside the
one-page exemption in the CLI flag standard and carry no version flag.
md2docx, md2pptx, docx2odt, and odt2docx accept a pandoc reference
document as a style template. Set REFERENCE_DOC, or drop a reference file of
the output's type (reference.docx, reference.pptx, or reference.odt)
next to the input and it is picked up automatically.
REFERENCE_DOC=~/templates/excelano.docx md2docx report.mdcsv2xlsx conservatively types cells so Excel treats numbers as numbers and
dates as dates, but only when the conversion is lossless. Values Excel famously
mangles are kept as text: leading-zero codes (007, zip codes), phone-style
strings (+1 555...), and digit runs longer than 15. Pass --text to disable
inference entirely and write every cell verbatim.
Given several CSVs and an -o output, csv2xlsx writes one workbook with a
sheet per CSV, titled after each input's filename stem (sanitized and de-duped
for Excel's sheet-name rules), in the order given. A single input keeps the
plain csv2xlsx input [output] form; only two or more inputs need -o.
sqlite2csv uses only the Python standard library — no extra dependency.
It opens the database read-only, so a bad path errors out instead of
sqlite3 silently creating a new, empty file. With no --table, it exports
the database's only table if there is exactly one; with more than one it
errors and lists the table names, same as xlsx2csv does for sheets.
--all dumps every table to its own CSV in the output directory (-o,
default the current directory), and --list prints the table names and
exits. BLOB columns are written as hex strings.
Both write one workbook, one sheet per table, named after the table
(sanitized and de-duped for the format's sheet-name rules); --table writes
a single-sheet document from just that table instead. BLOB columns are
written as hex strings, same as sqlite2csv. sqlite2xlsx sheets get the
same bold frozen header, autofilter, and auto-fitted column widths as
csv2xlsx; sqlite2ods sheets get a bold header only, since odfpy has no
straightforward API for freeze panes or autofilter.
parquet2csv hands the reading and writing to DuckDB and only decides how
each column lands in a CSV cell. Scalar columns are written as DuckDB
formats them; nested columns (struct, map, union, list) are written as JSON
text, and BLOB columns as hex strings, same as the SQLite converters.
Both go through pandoc, so a document's structure comes across (headings,
lists, tables, images, links, footnotes) and its look does not: page layout,
headers and footers, section settings, tracked changes, and custom styles are
left behind, same as docx2md. pandoc's ODT reader drops a table's heading
rows and skips merged-away cells, which would lose the first row of most
Writer tables and shift the cells after a merge into the wrong column, so
odt2docx rewrites its input first: heading rows read as ordinary first rows
and each merged-away cell as an empty one. The text and column positions
survive; the heading styling and the merge do not.
Every sheet comes across with typed cells (number, percentage, date, time,
boolean, text), merged cells, named ranges, and formulas. Formulas are
rewritten between Excel's syntax and OpenFormula: references (Sheet!A1:B2
and [$Sheet.A1:.B2]), argument separators, array constants, union and
intersection operators, and function names, including the _xlfn. and
COM.MICROSOFT. forms the two use for newer functions. The function mapping
follows what LibreOffice reads and writes, and arguments OpenFormula lets a
call leave off that Excel requires (ROUND(x)) are filled in.
A formula with no equivalent on the other side keeps its last computed value,
and the converter names the cell on stderr. That covers structured table
references (Table1[Units]), links to other workbooks, spill references
(A1#), and functions one side lacks (TEXTBEFORE, LAMBDA, or
LibreOffice's own EASTERSUNDAY). Excel functions LibreOffice preserved
without evaluating them come back to Excel intact.
xlsx2ods stores each formula with the result Excel saved, and writes
Excel's matching rules into the document: criteria in COUNTIF, MATCH, and
the lookups use wildcards and ignore case, where an OpenDocument file that
does not say so uses case-sensitive regular expressions. ods2xlsx writes
formulas without a saved result, because openpyxl cannot store one; Excel and
LibreOffice compute them on open, but a reader that only looks at saved values
(xlsx2csv among them) sees those cells empty until a spreadsheet application
has saved the file. It warns when the source document matched criteria as
regular expressions. Styling, column widths, charts, conditional formatting,
and pivot tables are not carried in either direction.
Slides break on a heading at the slide level or a horizontal rule (---), the
same rule the Axe Markdown slide viewer uses — so one .md both presents in the
browser and exports to PowerPoint here.