Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [ ] `validate()` — post-parse validation hook on the parsed struct
- [ ] `visit_subcommand()` — visitor dispatch over matched subcommands
- [ ] `range` / `choice` — built-in validation constraints
- [ ] pipeline `action` types — validate action types and show clear error messages
- [ ] env fallback — populate unprovided options from environment variables
- [ ] completion — shell completion script generation
- [x] short cluster / attached value — `-xvf`, `-ofile`
70 changes: 70 additions & 0 deletions docs/docs/examples/env-fallback.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
sidebar_position: 10
title: 9. Env Fallback
---

import {ExampleSource} from '@site/src/components/ExampleDoc';
import {ExamplePlayground} from '@site/src/components/ExamplePlayground';
import {setupEnvFallbackFs} from '@site/src/components/exampleSetups';

# Env Fallback

Options can be populated from environment variables when they are not provided
on the command line. The `.env` field on any option names the environment
variable to check. The same type conversion and validation pipeline runs
regardless of whether the value came from the CLI or the environment.

## Source

<ExampleSource
example={{
sourcePath: 'examples/env_fallback.cc',
}}
/>

## What This Example Shows

- **`--threads` / `NUM_THREADS`** — an integer option with `range<1, 32>`
validation. If `NUM_THREADS` is set in the environment, it is parsed and
validated exactly like a CLI argument. A value outside 1–32 is an error.
- **`--config` / `CONFIG_FILE`** — a string option with `matches<".*\\.toml">`
validation. The env value must end in `.toml` or parsing fails.
- **`--verbose` / `VERBOSE`** — a flag. When the env var is present (any
value), the flag is set to `true`.
- **CLI takes precedence** — if the option is supplied on the command line,
the environment variable is ignored.

## Try It

Edit the environment variables in the panel below, then run the program.

The virtual filesystem for this playground contains the following config files
you can reference as `CONFIG_FILE`:

| Path | Notes |
|---|---|
| `dev.toml` | local development |
| `test.toml` | test environment |
| `staging.toml` | staging environment |
| `prod.toml` | production |

<ExamplePlayground
name="env_fallback"
defaultArgs="build-job"
setupFs={setupEnvFallbackFs}
initialEnv={{NUM_THREADS: '4', CONFIG_FILE: 'dev.toml'}}
/>

Things to try:

- **Switch environment** — change `CONFIG_FILE` to `staging.toml` or `prod.toml`
- **CLI overrides env** — `--config test.toml build-job` while `CONFIG_FILE` is set
- **Validation via env** — set `NUM_THREADS=99` to see a range error
- **Bad config name** — set `CONFIG_FILE=settings.json` to see the regex fail
- **Verbose from env** — add `VERBOSE=1` and run `build-job`
- **Help** — `--help`

## Related Examples

- [Validation](/docs/examples/validation)
- [Simple API](/docs/examples/simple-api)
2 changes: 2 additions & 0 deletions docs/docs/examples/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Each one is small enough to copy into a project, but specific enough to answer
Short-cluster flags, negatable key-value options, and `std::map` storage with `insert_or_assign`.
8. [Distribution and Platform Examples](./distribution-and-platform)
Single-header usage, C++20 modules, and the Windows Unicode sample used by CI.
9. [Env Fallback](./env-fallback)
Populate options from environment variables with the same conversion and validation pipeline.

## Building

Expand Down
87 changes: 87 additions & 0 deletions docs/src/components/ExamplePlayground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@ function SpinnerIcon() {
}

type HistoryEntry = {args: string; output: string; exitCode: number};
type EnvRow = {id: number; key: string; value: string};

export function ExamplePlayground({
name,
defaultArgs = '--help',
setupFs,
initialEnv,
}: {
name: string;
defaultArgs?: string;
/** Optional callback to populate the Emscripten virtual FS before each run. */
setupFs?: (fs: any) => void;
/** If provided, show an env-var editor panel above the terminal. */
initialEnv?: Record<string, string>;
}) {
const wasmUrl = useBaseUrl(`/wasm/${name}.js`);
const factoryRef = useRef<Promise<(opts: object) => Promise<any>> | null>(null);
Expand All @@ -85,13 +89,45 @@ export function ExamplePlayground({
// Track cursor position so the block cursor renders at the right place.
const [cursorPos, setCursorPos] = useState(0);

// Env editor — active only when initialEnv prop is provided.
const showEnvEditor = initialEnv !== undefined;
const [envRows, setEnvRows] = useState<EnvRow[]>(() =>
initialEnv
? Object.entries(initialEnv).map(([key, value], i) => ({id: i, key, value}))
: [],
);
const nextIdRef = useRef(initialEnv ? Object.keys(initialEnv).length : 0);

const addEnvRow = () => {
setEnvRows((rows) => [
...rows,
{id: nextIdRef.current++, key: '', value: ''},
]);
};
const removeEnvRow = (id: number) => {
setEnvRows((rows) => rows.filter((r) => r.id !== id));
};
const updateEnvRow = (id: number, field: 'key' | 'value', val: string) => {
setEnvRows((rows) =>
rows.map((r) => (r.id === id ? {...r, [field]: val} : r)),
);
};
const buildEnv = (): Record<string, string> => {
const env: Record<string, string> = {};
for (const row of envRows) {
if (row.key.trim()) env[row.key.trim()] = row.value;
}
return env;
};

const run = async (overrideArgs?: string) => {
if (runningRef.current) return;
runningRef.current = true;
setRunning(true);
setLoadError(null);

const usedArgs = overrideArgs ?? args;
const usedEnv = buildEnv();
let out = '';
let exitCode = 0;

Expand All @@ -114,6 +150,13 @@ export function ExamplePlayground({
noExitRuntime: false,
});

// Inject env vars via setenv (requires _setenv + ccall in EXPORTED_FUNCTIONS/METHODS).
if (mod.ccall) {
for (const [k, v] of Object.entries(usedEnv)) {
mod.ccall('setenv', 'number', ['string', 'string', 'number'], [k, v, 1]);
}
}

// Populate the virtual FS after the module is fully initialised.
try {
if (setupFs) setupFs(mod.FS);
Expand Down Expand Up @@ -240,6 +283,50 @@ export function ExamplePlayground({

return (
<div className="pg-wrap">
{showEnvEditor && (
<div className="pg-env-panel">
<div className="pg-env-header">
<span className="pg-env-title">Environment</span>
<button className="pg-env-add" onClick={addEnvRow} type="button">
+ Add
</button>
</div>
{envRows.length === 0 ? (
<div className="pg-env-empty">No environment variables — click + Add to set one</div>
) : (
<div className="pg-env-rows">
{envRows.map((row) => (
<div key={row.id} className="pg-env-row">
<input
className="pg-env-input pg-env-key"
placeholder="KEY"
value={row.key}
onChange={(e) => updateEnvRow(row.id, 'key', e.target.value)}
spellCheck={false}
/>
<span className="pg-env-eq">=</span>
<input
className="pg-env-input pg-env-val"
placeholder="value"
value={row.value}
onChange={(e) =>
updateEnvRow(row.id, 'value', e.target.value)
}
spellCheck={false}
/>
<button
className="pg-env-remove"
onClick={() => removeEnvRow(row.id)}
type="button"
aria-label="Remove variable">
×
</button>
</div>
))}
</div>
)}
</div>
)}
<div
className="pg-terminal"
onClick={() => inputRef.current?.focus()}>
Expand Down
8 changes: 8 additions & 0 deletions docs/src/components/exampleSetups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ export function setupValidationFs(fs: any) {
// --output requires parent_exists. /tmp gives users a writable parent to try.
fs.mkdir('/tmp');
}

export function setupEnvFallbackFs(fs: any) {
// Per-environment config files — try CONFIG_FILE=prod.toml etc.
fs.writeFile('/dev.toml', '');
fs.writeFile('/test.toml', '');
fs.writeFile('/staging.toml', '');
fs.writeFile('/prod.toml', '');
}
136 changes: 129 additions & 7 deletions docs/src/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -146,24 +146,23 @@
.pg-cursor {
display: inline-block;
min-width: 0.55em;
background: #e6edf3;
color: #0d1117;
opacity: 0;
background: transparent;
color: #e6edf3;
line-height: 1;
}

.pg-cursor-blink {
opacity: 1;
animation: pg-blink 1.1s step-end infinite;
}

.pg-cursor-idle {
opacity: 1;
background: #e6edf3;
color: #0d1117;
}

@keyframes pg-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
0%, 100% { background: #e6edf3; color: #0d1117; }
50% { background: transparent; color: #e6edf3; }
}

/* transparent input overlay — keyboard capture only */
Expand Down Expand Up @@ -199,3 +198,126 @@
color: rgba(255, 255, 255, 0.5);
flex-shrink: 0;
}

/* ── Environment variable editor panel ──────────────────────────── */

.pg-wrap {
display: flex;
flex-direction: column;
}

.pg-env-panel {
background: #161b22;
border: 1px solid #30363d;
border-bottom: none;
border-radius: 10px 10px 0 0;
padding: 0.55rem 0.75rem;
font-family: var(--ifm-font-family-monospace);
font-size: 0.8rem;
color: #8b949e;
}

/* When env panel is present, flatten the terminal's top corners. */
.pg-env-panel + .pg-terminal {
border-radius: 0 0 10px 10px;
margin-top: 0;
}

.pg-env-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.45rem;
}

.pg-env-title {
color: #8b949e;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.07em;
}

.pg-env-add {
background: transparent;
border: 1px solid #30363d;
border-radius: 4px;
color: #8b949e;
cursor: pointer;
font-family: var(--ifm-font-family-monospace);
font-size: 0.75rem;
line-height: 1;
padding: 0.15rem 0.45rem;
transition: border-color 0.15s, color 0.15s;
}

.pg-env-add:hover {
border-color: #8b949e;
color: #e6edf3;
}

.pg-env-empty {
color: #484f58;
font-size: 0.75rem;
}

.pg-env-rows {
display: flex;
flex-direction: column;
gap: 0.3rem;
}

.pg-env-row {
display: flex;
align-items: center;
gap: 0.3rem;
}

.pg-env-input {
background: #0d1117;
border: 1px solid #30363d;
border-radius: 4px;
color: #e6edf3;
font-family: var(--ifm-font-family-monospace);
font-size: 0.8rem;
padding: 0.15rem 0.4rem;
outline: none;
transition: border-color 0.15s;
}

.pg-env-input:focus {
border-color: #6366f1;
}

.pg-env-key {
width: 10rem;
color: #ffa657;
}

.pg-env-key::placeholder { color: #484f58; }

.pg-env-val {
flex: 1;
}

.pg-env-val::placeholder { color: #484f58; }

.pg-env-eq {
color: #484f58;
user-select: none;
}

.pg-env-remove {
background: transparent;
border: none;
color: #484f58;
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
padding: 0 0.2rem;
transition: color 0.15s;
}

.pg-env-remove:hover {
color: #f85149;
}

Loading
Loading