Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
768b218
Initialise project
zefir-git Sep 9, 2025
e402ef8
added CI
zefir-git Sep 9, 2025
32cc207
fix typo
zefir-git Sep 9, 2025
23dc74e
explicit method calls
zefir-git Sep 9, 2025
3123161
added licence
zefir-git Sep 9, 2025
26963fb
finished printHelp()
zefir-git Sep 9, 2025
78053b9
added a built-in `HelpCommand` for showing help w/ `help [command...]`
zefir-git Sep 9, 2025
9a42cdb
Remove default app.d
zefir-git Sep 10, 2025
e3c05a2
created skeleton for documentation
zefir-git Sep 10, 2025
549051a
show `[options]` instead of `[global options]` on the root cmd itself
zefir-git Sep 10, 2025
fb76e04
added methods to get the parsed args without running the command
zefir-git Sep 10, 2025
c0967fa
remove trailing full stops from CLI descriptions
zefir-git Sep 10, 2025
09a3cce
added examples and shields to readme
zefir-git Sep 10, 2025
54ac933
rename ci.yaml to build.yaml
zefir-git Sep 10, 2025
d05b74c
GitHub action to create docs with adrdox deploy to GitHub Pages
zefir-git Sep 10, 2025
f7ed109
improved help formatting
zefir-git Sep 10, 2025
689ca7b
document ANSI and document modules
zefir-git Sep 11, 2025
b958f48
removed trailing whitespace characters
zefir-git Sep 11, 2025
bf00c3a
documented `Command.chain`
zefir-git Sep 11, 2025
1fabf9c
fix typo
zefir-git Sep 21, 2025
a551811
fix ternary formatting (missing space)
zefir-git Sep 21, 2025
6e07df1
move magic string to documented compile-time constant
zefir-git Sep 21, 2025
f3946d2
replace `paddedName()` in favour of a parameter in `formattedName()`
zefir-git Sep 21, 2025
f06f924
fix order of args in doc for Option formattedName
zefir-git Sep 21, 2025
9fddc70
fix `Command.run()` not delegating to sub-commands
zefir-git Sep 21, 2025
193bb92
use `.empty()` instead of comparing `.length` with 0
zefir-git Sep 21, 2025
506b4a4
check if parsed variadic is null
zefir-git Sep 22, 2025
f2956b8
move assertion to top
zefir-git Sep 22, 2025
4ef9de2
improve formatting of string concat with ternary
zefir-git Sep 22, 2025
65e65aa
avoid underscore names
zefir-git Sep 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @zefir-git
8 changes: 8 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: "06:00"
25 changes: 25 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Build

on:
push:
branches: [main]
pull_request:
branches: [main]
release:
types: [published]

jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up D compiler
uses: dlang-community/setup-dlang@v2
with:
compiler: dmd

- name: Build with dub
run: dub build
37 changes: 37 additions & 0 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Docs

on:
release:
types: [published]

jobs:
docs:
name: Docs
runs-on: ubuntu-latest
if: "!github.event.release.prerelease"
permissions:
contents: read
id-token: write
pages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up D compiler
uses: dlang-community/setup-dlang@v2
with:
compiler: dmd

- name: Generate docs
run: dub run -y adrdox -- -i src -o docs

- name: Set up GitHub pages
uses: actions/configure-pages@v5

- name: Upload docs artifact to pages
uses: actions/upload-pages-artifact@v3
with:
path: docs

- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.dub
docs.json
__dummy.html
docs/
/cmd
libcmd.so
libcmd.dylib
libcmd.dll
libcmd.a
libcmd.lib
libcmd-test-*
*.exe
*.pdb
*.o
*.obj
*.lst
674 changes: 674 additions & 0 deletions COPYING

Large diffs are not rendered by default.

185 changes: 185 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,187 @@
# cmd

[![Documentation](https://img.shields.io/badge/Documentation-blue)](https://modpm.github.io/cmd)
[![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github)](https://github.com/modpm/cmd)
[![CI](https://github.com/modpm/cmd/actions/workflows/build.yaml/badge.svg)](https://github.com/modpm/cmd/actions/workflows/build.yaml)
[![Version](https://img.shields.io/dub/v/cmd)](https://code.dlang.org/packages/cmd)
[![Licence](https://img.shields.io/dub/l/cmd)](https://code.dlang.org/packages/cmd)
[![Score](https://img.shields.io/dub/score/cmd)](https://code.dlang.org/packages/cmd)
[![Downloads](https://img.shields.io/dub/dt/cmd)](https://code.dlang.org/packages/cmd)

Simple, intuitive library for building CLI applications in D.

Please see the [API Reference Documentation](https://modpm.github.io/cmd).

## Highlights

- Nested subcommands (commands may contain other commands).
- Required (`<>`) and optional (`[]`) arguments and options.
- Variadic arguments (`...`) for accepting multiple values.
- Built-in help and usage generation.
- Accepts `--option=value` and `--option value` forms.
- Repeated options are collected as arrays.

## Quick start

A minimal example that splits a string.

```d
import std.stdio;
import std.string;

import cmd.program;

void main(string[] argv)
{
auto args = new Program("split")
.description("Split a string")
.versionString("1.0.0")
.versionOption("--version", "Show version information")
.helpOption("-h, --help", "Show help for command")
.argument("<string>", "String to split")
.option("-s, --separator <char>", "Separator character")
.option("--first", "Return only the first element")
.parse(argv);

auto parts = args.argument("string").split(args.option("separator"));
if (args.flag("first"))
writeln(parts[0]);
else
writeln(parts);
}
```

> [!NOTE]
>
> `versionOption` enables a version flag for your program (e.g., `--version`), and helpOption enables a help flag
> (e.g., `-h` or `--help`). When specified, these flags are handled automatically:
> the library will print the version or help message and exit.

## Subcommands

Add subcommands with .command(); subcommands may themselves have nested subcommands. Example with a single subcommand:

```d
import std.stdio;
import cmd.program;

void main(string[] args)
{
new Program("example")
.description("An example CLI program using cmd")
.helpOption("-h, --help", "Show help for command")
.command(new Command("greet")
.description("Greet someone")
.option("--excited", "Add excitement to the greeting")
.argument("<name>", "Name of the person to greet")
.action((args) {
auto msg = "Hello, " ~ args.argument("name");
if (args.flag("excited"))
msg ~= "!!!";
writeln(msg);
return 0;
})
)
.run(args);
}
```

Invoke as: `example greet [options] <name>`.

> [!IMPORTANT]
>
> A command cannot have both subcommands and arguments.

## Flags and options

Use `.option()` to add options or flags to a command.

- Options require a parameter.
- Make an option required by wrapping the parameter name in `<>`, e.g. --target `<target>`.
- Make an option optional by wrapping it in `[]`. A default value can be provided for optional options.
- An option with no parameter is a flag; flags are boolean and indicate presence.
- Options and flags may have a short name (`-x`), a long name (`--example`), or both (`-x, --example`).

Example:

```d
auto args = new Program("example")
.option("-f, --foo <param>", "Option with a required parameterer")
.option("-b [bar]", "Option with an optional parameter and default value", "defaultValue")
.option("--flag", "A boolean flag")
.parse(argv);
```

Use `args.hasOption(name)` to check if an optional option without a default value is present before accessing it.
To get the first value of an option, use `args.option(name)`. To get all values, use `args.optionList(name)`.

To check a flag, use `args.flag(name)`.

> [!NOTE]
>
> `name` can be either the short or long name. For explicitness, you can prefix with a single or double dash,
> e.g. `args.option("-o")` or `args.option("--option")`.

## Arguments

Define positional arguments with `.argument()`.

- Required positional arguments are wrapped in `<>`.
- Optional positional arguments are wrapped in `[]`.
- A variadic (rest) argument is expressed with `...` and if used must be the final argument.
A required variadic argument requires at least one value; an optional variadic argument accepts zero or more.

Example:

```d
new Program("example")
.argument("<input>", "Input string or value")
.argument("[output]", "Optional output string or value")
.argument("<items...>", "One or more items to process")
```

## Custom command classes

Create modular command implementations by extending `Command`:

```d
module example.greet_command;

import std.stdio;
import cmd.command;

class GreetCommand : Command
{
public this()
{
super("greet")
.description("Greet someone")
.option("--excited", "Add excitement to the greeting")
.argument("<name>", "Name of the person to greet")
.action((args) {
auto msg = "Hello, " ~ args.argument("name");
if (args.flag("excited"))
msg ~= "!!!";
writeln(msg);
return 0;
});
}
}
```

To attach it as a subcommand: `.command(new GreetCommand())`.

# Built-in help command

`HelpCommand` is included to enable `help` as a subcommand:

```d
new Program("example")
.command(new HelpCommand())
```

Usage: `help [command...]`

Examples:
- `example help` — show help for the program.
- `example help greet` — show help for the `greet` subcommand.
11 changes: 11 additions & 0 deletions dub.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"authors": [
"Zefir Kirilov"
],
"copyright": "Copyright © 2025 Zefir Kirilov",
"description": "Simple, intuitive library for building CLI applications in D.",
"license": "GPL-3.0-only",
"name": "cmd",
"sourcePaths": ["src"],
"targetType": "library"
}
41 changes: 41 additions & 0 deletions skeleton.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<title>Documentation</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="style.css" />
<script type="text/javascript" src="script.js"></script>
<link rel="prefetch" href="search-results.html" />
</head>
<body>
<div id="page-header">
<div id="logotype">
<span>Documentation</span>
<nav>
<a href="https://modpm.github.io/cmd/">cmd</a>
</nav>
</div>

<form id="search" action="search-docs.html">
<input placeholder="Find a symbol name..." type="search" name="searchTerm" />
<input type="submit" value="Go" />
</form>
</div>
<div id="page-body">
<div id="page-content"></div>
<div id="page-nav"></div>
</div>
<div id="page-footer">
<p>
Copyright © 2025 Zefir Kirilov. Released under the
<a href="https://github.com/modpm/cmd/blob/main/COPYING" target="_blank">GPL-3.0</a>
licence.
</p>
<p>
Page generated with
<a href="https://github.com/adamdruppe/adrdox" target="_blank">adrdox</a>
</p>
</div>
</body>
</html>
31 changes: 31 additions & 0 deletions src/cmd/ansi.d
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module cmd.ansi;

import std.regex;

/** Removes ANSI escape sequences from a string, leaving only the text that would be displayed. */
public string stripAnsi(string input) @safe {
return input.replaceAll(
regex("\\x1B\\[[0-9;:?]*[A-Za-z]|\\x1B\\]8;;.*?\\x07(.*?)\\x1B\\]8;;\\x07|\\x1BO.|\\x1B.", "gs"),
"$1"
);
}

/** Applies foreground (text) SGR bold (increased intensity). */
public string bold(string text) nothrow @safe {
return "\x1B[1m" ~ text ~ "\x1B[0m";
}

/** Applies foreground (text) SGR dim (faint, decreased intensity). */
public string dim(string text) nothrow @safe {
return "\x1B[2m" ~ text ~ "\x1B[0m";
}

/** Applies foreground (text) SGR bright black. */
public string brightBlack(string text) nothrow @safe {
return "\x1B[90m" ~ text ~ "\x1B[0m";
}

/** Formats text as OSC 8 hyperlink. */
public string link(string text, string url) nothrow @safe {
return "\x1B]8;;" ~ url ~ "\x07" ~ text ~ "\x1B]8;;\x07";
}
Loading