cargo-expand: reading Rust macro output without guessing
Subcommand to show result of macro expansion
At a glance
- What is it?
- cargo-expand wraps rustc's -Zunpretty=expanded flag and formats the result, so you can see what a derive or macro_rules! actually produced. It is a debugging aid with a documented lossy edge.
- Who is it for?
- Adopt cargo-expand if you write or debug proc macros, derive macros, or macro_rules! and want the expansion as formatted text in the terminal. Do not adopt it as a build step or a source of truth about runtime behaviour: the README calls it a debugging aid and warns that expanded text is lossy.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 21 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What cargo-expand is for, and who ends up using it
Rust macros are invisible at the point of use. A #[derive(Debug)] line tells you nothing about the impl block it generates, and a macro_rules! arm can rewrite an expression in ways that are hard to trace by reading the definition. cargo-expand exists to close that gap: the README states that once installed, the command prints out the result of macro expansion and #[derive] expansion applied to the current crate.
The audience is narrow and specific. People writing proc macros need to check what their quote! output looks like after the compiler has processed it. People debugging a derive that behaves unexpectedly need to read the generated impl. People reading a codebase full of macro_rules! abstractions use it to see the concrete form underneath. If your crate has no macros beyond the standard prelude, the tool has little to show you.
The README frames the command as a wrapper around a more verbose compiler invocation, cargo rustc --profile=check -- -Zunpretty=expanded. That framing matters: cargo-expand is a convenience layer, not a separate expansion engine. Everything it displays comes from rustc.
The mechanism: rustc does the expansion, cargo-expand does the plumbing
The data flow is short. cargo-expand invokes cargo rustc with the check profile and passes -Zunpretty=expanded through to rustc. rustc performs the macro and derive expansion and prints the expanded crate as text. cargo-expand captures that text and, by default, formats it so it is readable rather than a single dense stream.
The Cargo.toml shows two formatting paths. The default feature is prettyplease, and the crate depends on prettyplease 0.3 with the verbatim feature. Separately, the README describes an optional rustfmt path: cargo-expand optionally uses rustfmt to format the expanded output, and if rustfmt is not available the expanded code is not formatted. Both statements are in the README, and they describe different mechanisms, so a reader should check which one their build actually uses rather than assuming.
The output is not just your macro. The README's example shows the expanded file beginning with #![feature(prelude_import)], a #[prelude_import] use of std::prelude::v1::*, and a #[macro_use] extern crate std. Those lines come from the compiler's view of the crate, not from your source, and they are part of why the result is described as lossy.
Installing cargo-expand and running a first expansion
The README gives one installation command. It installs the binary into your Cargo bin directory, which is what makes the subcommand available.
cargo install cargo-expandAfter that, run the command in the root of a crate. The README's example crate has a single struct with a derived Debug impl and a main that prints it.
cargo expandWhat you should see is the whole crate after expansion: the prelude import lines, the struct, an #[automatically_derived] impl of Debug with a generated fmt method, and the main function with its println! rewritten into a call to ::std::io::_print with a ::core::fmt::Arguments::new_v1 construction. That last part is the interesting one for most readers, because it shows how much work a single println! performs.
If rustfmt is part of your setup, the README says to add the component with rustup component add rustfmt. Without it, the README states the expanded code is not formatted.
Useful flags from the README: --ugly expands without formatting, --test test_something expands a particular test target, and passing a path such as path::to::module expands only that module, type, or function. The README points to cargo expand --help for the complete list and notes that most options are consistent with other Cargo subcommands.
Configuring theme, color, and pager in ~/.cargo/config.toml
cargo-expand reads an [expand] section from $CARGO_HOME/config.toml, usually ~/.cargo/config.toml. Three settings are documented.
The theme setting picks the syntax highlighting theme, and the README says to run cargo expand --themes or bat --list-themes to print the available list. Setting theme = "none" disables coloring, which is the right move when you are piping output into a file or another tool.
[expand]
theme = "TwoDark"The color setting changes the default coloring disposition, which the README says is normally auto. Setting it to always forces color on.
[expand]
color = "always"The pager setting enables paging of the output, which matters because expanded crates are long.
[expand]
pager = trueThese are the only configuration keys the README documents. There is no documented way to change the expansion itself, which is consistent with the tool being a thin wrapper: the expansion is rustc's, and rustc's behaviour is not configurable from this file.
The lossy expansion problem, stated by the project itself
The README has a disclaimer section, and it is blunt. Macro expansion to text is a lossy process. The README says it is a debugging aid only, and that there should be no expectation that the expanded code can be compiled successfully, nor that if it compiles then it behaves the same as the original code.
The README provides a concrete counterexample: a function that returns 3 when compiled ordinarily, but whose expanded code compiles and returns 4. The function declares x = 1, defines a macro_rules! first_x that expands to x, then declares x = 2, and returns x + first_x!(). The point is macro hygiene: the macro's reference to x resolves according to hygiene rules that plain text cannot represent. The README links to The Book's discussion of hygiene for the underlying rules.
This is the limitation that matters most, and it is easy to forget. If you are using cargo-expand to reason about what a program does at runtime, you are reasoning about a representation that the project explicitly says may not preserve behaviour. Use it to understand structure, generated impls, and token-level output. Do not use it to prove a semantic claim about the original code. A second practical constraint: the tool depends on rustc accepting -Zunpretty=expanded, which is a compiler-internal flag, and the README does not document a fallback for toolchains that reject it.
How it compares with rust-analyzer's expand macro command
The closest alternative for many users is the expand macro action in rust-analyzer, which editors expose through an IDE command. The difference in approach is where the expansion happens and what you get back.
rust-analyzer performs its own analysis and expands the macro inside the editor, so the result appears next to the code you are reading and is available without leaving the file. cargo-expand instead shells out to cargo rustc and asks the real compiler to print the expansion of a whole target, then formats it. That means cargo-expand shows you what rustc actually did, including the prelude and derive machinery, while an editor-side expansion is a view produced by a different implementation.
The trade-off runs both ways. cargo-expand gives a complete, compiler-authoritative dump that you can redirect to a file or page through, and it works anywhere Cargo runs, including a remote shell with no editor. rust-analyzer's command is faster to reach and scoped to the macro under the cursor, which is usually what you want when you are mid-edit. For inspecting a full crate's expansion or capturing output for a bug report, cargo-expand is the more direct tool; for a quick look at one macro while typing, the editor command wins.
Maintenance, upgrade cost, and licence terms
The repository is not archived, and the last push was on 2026-09-10. Recent releases are frequent: 1.0.126 and 1.0.125 both landed on 2026-08-19, and 1.0.124 on 2026-07-18. The version numbering is worth noting. The crate is at 1.0.126, so releases are patch-level increments rather than feature milestones, and the Cargo.toml pins rust-version = "1.90". If you build with an older toolchain, the manifest states the minimum supported Rust version, and that number moves as the crate tracks compiler internals.
That is the real upgrade cost. cargo-expand wraps a compiler-internal flag, and its dependencies include syn 3, prettyplease 0.3, and a pre-release pin on syn-select-next at =0.4.0-alpha.1. An exact pin on an alpha dependency means the dependency graph is deliberately constrained, and updating it is a project decision rather than something that floats. For most users this is invisible: install the binary and run it. For anyone vendoring or patching dependencies, the pinned alpha is the item to look at first.
The licence is dual, MIT OR Apache-2.0, which matches what the README states: licensed under either of Apache License, Version 2.0 or MIT license at your option. The README also notes that contributions intentionally submitted for inclusion are dual licensed on the same terms unless stated otherwise. That is a permissive arrangement, but whether it fits your organisation's policy is a question for your own review, not something this article can settle.
Editorial conclusion
Adopt cargo-expand if you write or debug proc macros, derive macros, or macro_rules! and want the expansion as formatted text in the terminal. Do not adopt it as a build step or a source of truth about runtime behaviour: the README calls it a debugging aid and warns that expanded text is lossy. Before relying on it, confirm that your toolchain accepts -Zunpretty=expanded on the stable channel you use, and decide whether you want prettyplease or rustfmt doing the formatting, since the default feature set differs from the rustfmt path described in the README.
Frequently asked questions
What is cargo-expand and what does it do?
It is a Cargo subcommand that prints the result of macro expansion and #[derive] expansion applied to the current crate. The README describes it as a wrapper around cargo rustc --profile=check -- -Zunpretty=expanded.
How do I install cargo-expand?
The README gives a single command: cargo install cargo-expand. It also notes that rustfmt is used optionally to format the output, and that you can add it with rustup component add rustfmt.
How do I use cargo expand?
Run cargo expand in the crate you want to inspect. The README also documents cargo expand --test test_something to expand a test target, cargo expand --ugly to expand without formatting, and cargo expand path::to::module to expand only a specific module, type, or function.
What is macro expansion?
In this context it is the step where the compiler replaces macro invocations and #[derive] attributes with the code they generate, which is what cargo-expand prints. The README warns that the text form of this process is lossy.
Can I use cargo expand in VS Code?
The README does not document a VS Code integration. It documents a command line tool, and separately the search results point to rust-analyzer's expand macro action, which is a different mechanism that expands inside the editor rather than through cargo rustc.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/dtolnay-cargo-expand)