Skip to content

Commit 7068779

Browse files
boneskullclaude
andcommitted
feat: add named positionals for help text display
Add optional 'name' property to PositionalBase interface that allows developers to specify meaningful names for positional arguments in help text. Without a name, positionals display as <arg0>, <arg1>, etc. Changes: - Add name?: string to PositionalBase in types.ts - Add formatPositionalUsage() and buildPositionalsUsage() helpers - Update generateHelp() to show positionals in usage line - Update generateCommandHelp() to show command positionals - Add 5 new tests for positional name display - Update examples to use named positionals Usage: bargs.stringPos({ name: 'source', required: true }) Shows: $ my-cli [options] <source> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent dbbb0d2 commit 7068779

5 files changed

Lines changed: 152 additions & 30 deletions

File tree

examples/greeter.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -34,36 +34,36 @@ const optionsDef = bargs.options({
3434
});
3535

3636
const positionalsDef = bargs.positionals(
37-
bargs.stringPos({ description: 'Name to greet', required: true }),
37+
bargs.stringPos({
38+
description: 'Name to greet',
39+
required: true,
40+
name: 'name',
41+
}),
3842
);
3943

40-
const main = async () => {
41-
const result = await bargs({
42-
name: 'greeter',
43-
version: '1.0.0',
44-
description: 'A friendly greeter CLI',
45-
options: optionsDef,
46-
positionals: positionalsDef,
47-
});
48-
49-
// Destructure the result
50-
const { positionals, values } = result;
51-
const [name] = positionals;
52-
const { greeting, shout, verbose } = values;
44+
const result = bargs({
45+
name: 'greeter',
46+
version: '1.0.0',
47+
description: 'A friendly greeter CLI',
48+
options: optionsDef,
49+
positionals: positionalsDef,
50+
});
5351

54-
// Build the message
55-
let message = `${greeting}, ${name}!`;
52+
// Destructure the result
53+
const { positionals, values } = result;
54+
const [name] = positionals;
55+
const { greeting, shout, verbose } = values;
5656

57-
if (shout) {
58-
message = message.toUpperCase();
59-
}
57+
// Build the message
58+
let message = `${greeting}, ${name}!`;
6059

61-
// Output
62-
if (verbose) {
63-
console.log('Configuration:', { greeting, name, shout });
64-
}
60+
if (shout) {
61+
message = message.toUpperCase();
62+
}
6563

66-
console.log(message);
67-
};
64+
// Output
65+
if (verbose) {
66+
console.log('Configuration:', { greeting, name, shout });
67+
}
6868

69-
void main();
69+
console.log(message);

examples/tasks.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@ await bargs({
6262
}),
6363
},
6464
positionals: [
65-
bargs.stringPos({ description: 'Task description', required: true }),
65+
bargs.stringPos({
66+
description: 'Task description',
67+
name: 'text',
68+
required: true,
69+
}),
6670
],
6771
handler: async ({ positionals, values }) => {
6872
const [text] = positionals;
@@ -126,7 +130,7 @@ await bargs({
126130
done: bargs.command({
127131
description: 'Mark a task as complete',
128132
positionals: [
129-
bargs.stringPos({ description: 'Task ID', required: true }),
133+
bargs.stringPos({ description: 'Task ID', name: 'id', required: true }),
130134
],
131135
handler: async ({ positionals, values }) => {
132136
const [idStr] = positionals;

src/help.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,38 @@ import type {
55
CommandConfigInput,
66
OptionDef,
77
OptionsSchema,
8+
PositionalDef,
89
PositionalsSchema,
910
} from './types.js';
1011

1112
import { bold, cyan, dim, yellow } from './ansi.js';
1213

14+
/**
15+
* Format a single positional for help usage line. Required positionals use
16+
* <name>, optional use [name]. Variadic positionals append "...".
17+
*/
18+
const formatPositionalUsage = (def: PositionalDef, index: number): string => {
19+
const name = def.name ?? `arg${index}`;
20+
const isRequired = def.required || 'default' in def;
21+
const isVariadic = def.type === 'variadic';
22+
const displayName = isVariadic ? `${name}...` : name;
23+
24+
return isRequired ? `<${displayName}>` : `[${displayName}]`;
25+
};
26+
27+
/**
28+
* Build the positionals usage string from a schema.
29+
*/
30+
const buildPositionalsUsage = (schema?: PositionalsSchema): string => {
31+
if (!schema || schema.length === 0) {
32+
return '';
33+
}
34+
35+
return schema
36+
.map((def, index) => formatPositionalUsage(def, index))
37+
.join(' ');
38+
};
39+
1340
/**
1441
* Get type label for help display.
1542
*/
@@ -100,7 +127,11 @@ export const generateHelp = <
100127
if (hasCommands(config)) {
101128
lines.push(` $ ${config.name} <command> [options]`);
102129
} else {
103-
lines.push(` $ ${config.name} [options]`);
130+
const positionalsPart = buildPositionalsUsage(config.positionals);
131+
const usageParts = [`$ ${config.name}`, '[options]', positionalsPart]
132+
.filter(Boolean)
133+
.join(' ');
134+
lines.push(` ${usageParts}`);
104135
}
105136
lines.push('');
106137

@@ -194,7 +225,15 @@ export const generateCommandHelp = <
194225

195226
// Usage
196227
lines.push(yellow('USAGE'));
197-
lines.push(` $ ${config.name} ${commandName} [options]`);
228+
const positionalsPart = buildPositionalsUsage(command.positionals);
229+
const usageParts = [
230+
`$ ${config.name} ${commandName}`,
231+
'[options]',
232+
positionalsPart,
233+
]
234+
.filter(Boolean)
235+
.join(' ');
236+
lines.push(` ${usageParts}`);
198237
lines.push('');
199238

200239
// Command options

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,5 +338,7 @@ interface OptionBase {
338338
*/
339339
interface PositionalBase {
340340
description?: string;
341+
/** Display name for help text (defaults to arg0, arg1, etc.) */
342+
name?: string;
341343
required?: boolean;
342344
}

test/help.test.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,59 @@ describe('generateHelp', () => {
133133
});
134134
});
135135

136+
describe('generateHelp positionals', () => {
137+
it('shows positionals in usage line with default names', () => {
138+
const help = stripAnsi(
139+
generateHelp({
140+
name: 'my-cli',
141+
positionals: [opt.stringPos({ required: true }), opt.stringPos()],
142+
}),
143+
);
144+
145+
assert.ok(help.includes('<arg0>'));
146+
assert.ok(help.includes('[arg1]'));
147+
});
148+
149+
it('shows positionals with custom names', () => {
150+
const help = stripAnsi(
151+
generateHelp({
152+
name: 'my-cli',
153+
positionals: [
154+
opt.stringPos({ name: 'source', required: true }),
155+
opt.stringPos({ name: 'dest' }),
156+
],
157+
}),
158+
);
159+
160+
assert.ok(help.includes('<source>'));
161+
assert.ok(help.includes('[dest]'));
162+
});
163+
164+
it('shows variadic positionals with ellipsis', () => {
165+
const help = stripAnsi(
166+
generateHelp({
167+
name: 'my-cli',
168+
positionals: [opt.variadic('string', { name: 'files' })],
169+
}),
170+
);
171+
172+
assert.ok(help.includes('[files...]'));
173+
});
174+
175+
it('shows positional with default as required (angle brackets)', () => {
176+
const help = stripAnsi(
177+
generateHelp({
178+
name: 'my-cli',
179+
positionals: [opt.stringPos({ default: 'foo', name: 'input' })],
180+
}),
181+
);
182+
183+
// Positionals with defaults are considered "required" in terms of display
184+
// because they always have a value
185+
assert.ok(help.includes('<input>'));
186+
});
187+
});
188+
136189
describe('generateCommandHelp', () => {
137190
it('generates help for a specific command', () => {
138191
const help = stripAnsi(
@@ -179,4 +232,28 @@ describe('generateCommandHelp', () => {
179232

180233
assert.ok(help.includes('Unknown command: unknown'));
181234
});
235+
236+
it('shows command positionals with custom names', () => {
237+
const help = stripAnsi(
238+
generateCommandHelp(
239+
{
240+
commands: {
241+
copy: opt.command({
242+
description: 'Copy files',
243+
handler: () => {},
244+
positionals: [
245+
opt.stringPos({ name: 'source', required: true }),
246+
opt.stringPos({ name: 'dest', required: true }),
247+
],
248+
}),
249+
},
250+
name: 'my-cli',
251+
},
252+
'copy',
253+
),
254+
);
255+
256+
assert.ok(help.includes('<source>'));
257+
assert.ok(help.includes('<dest>'));
258+
});
182259
});

0 commit comments

Comments
 (0)