p-ranav/argparse: a C++ argument parser whose badge and branch are both a decade behind its habits
Argument Parser for Modern C++
At a glance
- What is it?
- One header, C++17, MIT. The repository around it ships five build systems, a C++20 module, and commits that have not been tagged in twenty months.
- Who is it for?
- argparse earns its place in a C++17 codebase with one copied header and no dependency tree, and it is a better default than hand-rolling argv loops. The catch is packaging discipline: the badge still says 3.2 while the default branch is master and moving, so vendor the header at a known commit instead of tracking the tip.
- Can I use it commercially?
- Yes. MIT 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 4 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The whole API is a parser object and a chain
argparse describes itself as Argument Parser for Modern C++, and the highlights section lists three things: single header file, requires C++17, MIT License. The API matches that pitch. You include one header, construct a parser with your program name, add arguments, and read typed values back out.
#include <argparse/argparse.hpp>argparse::ArgumentParser program("program_name");Adding arguments is a chained call, and the chain is where the type information lives:
program.add_argument("square")
.help("display the square of a given integer")
.scan<'i', int>();Parsing wraps in a try block and takes argc and argv directly, which is the detail that keeps this from being a toy. Once parsed, the value comes back as a real C++ type rather than a string you convert yourself:
auto input = program.get<int>("square");
std::cout << (input * input) << std::endl;The constructor also takes optional second, third and fourth arguments, where the second is the program version and the third and fourth control which default arguments such as help are installed. That is a small decision with a visible effect: opting out of the built-in help is a constructor argument rather than a flag you discover later.
Optional arguments, implicit values, and is_used
The optional argument model is the part that has clearly been thought about, and it is where most competing parsers are thin. A flag with no value needs a declared false so the program can branch on it even when the user omits it, plus an implicit value for when the flag is present. Both are two chained calls:
program.add_argument("--verbose")
.help("increase output verbosity")
.default_value(false)
.implicit_value(true);And the runtime consequence is the ordinary one you would write by hand:
if (program["--verbose"] == true) {
std::cout << "Verbosity enabled" << std::endl;
}The README notes the distinction the parser is making, that a flag behaves more like a toggle than something requiring a value. It also points out that the program can be run with no argument at all without an error, because the default value is what gets read.
Beyond flags, the table of contents is longer than most single-header libraries manage. It lists requiring optional arguments, accessing optional arguments without defaults, deciding whether a value was given by the user, joining values of repeated optional arguments, repeating an argument to increase a value, mutually exclusive groups, storing values into variables, negative numbers, compound arguments, restricted choices, parent parsers, subcommands, custom prefix characters, custom assignment characters, the -- separator, and parse_known_args for tolerating unknown input.
That last one matters more than it sounds. parse_known_args is the difference between a parser that rejects a typo and a parser that lets you pass through flags you have not modelled yet, which is what you need when your program wraps another tool. Default branch is master, not main, worth knowing if you script anything against the raw URL.
Single header in the README, five build systems in the repository
Here is the first thing worth putting side by side. The highlights section says single header file, and consumption really is that simple, as the include line above shows. The repository around it tells a different story about effort. The tree at the root carries include/, module/, samples/, test/, tools/ and packaging/ directories, alongside BUILD.bazel, CMakeLists.txt, WORKSPACE.bazel, a .bazelrc, conanfile.py, xmake.lua and .travis.yml.
Both facts are true and they answer different questions. The README sentence is about what a consumer depends on, which is a header. The build files are about what the maintainer supports so that Bazel, CMake, Conan, xmake and Travis users can all consume it. Nobody is wrong, but a reader who only sees the highlights line will badly underestimate how much machinery is behind one include.
The samples directory makes the same point from a different angle. Eighteen example programs are listed there, covering positional arguments, compound arguments, custom assignment characters, custom prefix characters, description and epilog formatting, gathering remaining arguments, is_used, joining repeated optional arguments, list of arguments, negative numbers, optional flag arguments, parse_known_args, repeating an argument to increase a value, required optional arguments and subcommands, with their own BUILD.bazel, CMakeLists.txt and add_sample.bzl.
So the practical read on the single header claim is that this is a mature, well sampled library whose distribution surface is deliberately narrow. That is a good thing. It also means the README's three-bullet highlights undersell what is actually there, which is fine for a landing page and misleading if you stop reading.
C++17 in the topics, a C++20 module in the tree
The second contradiction is about the language standard, and it is sharper. The repository topics include cpp17, and the highlights section says Requires C++17. The root tree contains a module/ directory, the 3.0 release notes list Added C++20 module, and the 3.1 notes list a fix for C++23 standard library module usage.
Set those next to each other. The stated floor is C++17. The repository ships a C++20 module, and has been fixing behaviour against C++23 module builds since the previous release. Neither statement needs to be false for both to be printed: the library can require C++17 for its header interface while offering a module interface to people already on C++20.
The judgement you need is about your own build, not about which sentence is right. If your target is C++17, the header is your path and nothing in the module directory affects you. If you are on C++20 or later and want to compile the module, you are using a surface that the README's three bullets never mention, so check that directory yourself before you plan around it.
There is a smaller version of the same pattern in the tooling. A .travis.yml file sits at the repository root next to a .github/ directory, and release notes reference workflow files living under .github. That is a project mid-migration rather than confused, but it does mean part of the continuous integration story lives in a file from a platform that stopped being the default years ago.
A badge on 3.2 and a branch that moved twenty months past it
The third contradiction is the one with real consequences for how you depend on this. The README badge reads version 3.2. The release list tops out at 3.2, published 2025-01-25, whose notes are a quiet maintenance batch: a missed table of contents link, a min and max fix for a macro in minwindef.h, a choices range bug fix, and a change so store_into no longer overrides a default that was already set.
Now look at the push date: 2026-10-05. That is roughly twenty months of commits landing on the default branch after the last tag. The project is not dead. It is active, it is being worked on, and none of that work has produced a 3.3.
This is common in C++ libraries and it is worth naming rather than discovering later. Tagging costs nothing, but header-only libraries tend to get consumed by copying the file, and many projects never formalise a release because there is no install step to force the issue. The result is that main or master is the de facto distribution channel and the tag is a historical marker.
So the practical question is not whether 3.2 is good enough. It probably is. The question is which 3.2 you are running. Two options, both fine. Vendor the header at a specific commit you record in your own build notes, which gives you a byte-for-byte reproducible dependency with no network at build time. Or track master and accept that your build changes when you do not ask it to, in exchange for fixes you would otherwise not get. Choose deliberately, because the repository will not choose for you.
The topic tags support the same conclusion from a different angle. argument-parser, cpp17, cross-platform, header-only, library and mit-license describe a dependency meant to be consumed by other projects rather than run as a service, which is exactly the case where pinning discipline matters most and is most often skipped.
Editorial conclusion
argparse earns its place in a C++17 codebase with one copied header and no dependency tree, and it is a better default than hand-rolling argv loops. The catch is packaging discipline: the badge still says 3.2 while the default branch is master and moving, so vendor the header at a known commit instead of tracking the tip.
Frequently asked questions
How do you add an argument in p-ranav/argparse?
Create an argparse::ArgumentParser with your program name, then call add_argument and chain the configuration. A positional takes a name and a scan template such as .scan<'i', int>(), an optional takes names like -v and --verbose. Pass argc and argv to parse_args inside a try block, then read the typed value with get<int>("square").
Does argparse support C++20 modules?
The repository ships a module/ directory at the root, the 3.0 release notes list an added C++20 module, and 3.1 notes mention a fix for C++23 standard library module usage. The README highlights and repository topics say C++17, which describes the header interface floor. If you are on C++20 or later, look at the module directory directly rather than relying on the highlights section.
What is the latest release of p-ranav/argparse?
3.2, published 2025-01-25, containing bug fixes around choices ranges, min and max macros in minwindef.h, and store_into precedence. The default branch is master and was pushed on 2026-10-05, so there are roughly twenty months of untagged commits behind the tag. Pin a commit rather than tracking the branch tip.
Is argparse really a single header file?
For consumption, yes. You add one include of argparse/argparse.hpp and nothing else is needed at build time. Behind it the repository carries include/, module/, samples/, test/, tools/ and packaging/, plus BUILD.bazel, CMakeLists.txt, conanfile.py, xmake.lua and WORKSPACE.bazel, so the dependency is small while the surrounding project supports many build systems.
How do you tell if an optional flag was actually passed?
Declare a default_value for the argument so a defined value exists when the user omits it, and add an implicit_value for the case where the user supplies the flag without a value. The README notes this separates a toggle from an option that requires an argument, and it explains why running the program with no argument raises no error. There is also an is_used helper and a parse_known_args mode for tolerating unrecognised input.
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/p-ranav-argparse)