Skip to content

Commit 014c500

Browse files
authored
Merge pull request #17 from cgay/subcommands
Add support for subcommands
2 parents ab592a4 + 404721f commit 014c500

12 files changed

+1215
-776
lines changed

command-line-parser.dylan

Lines changed: 450 additions & 472 deletions
Large diffs are not rendered by default.

command-line-parser.lid

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ target-type: dll
33
files: library
44
command-line-parser
55
parsers
6+
help
67
macros

help.dylan

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
Module: command-line-parser
2+
Synopsis: Implements the --help flag and help subcommand
3+
4+
5+
// TODO(cgay): Wrap the descriptions nicely
6+
7+
define function program-name () => (name :: <string>)
8+
locator-base(as(<file-locator>, application-name()))
9+
end function;
10+
11+
define method command-help
12+
(cmd :: <command>) => (help :: <string>)
13+
let result = cmd.%command-help;
14+
for (subst in *pattern-substitutions*)
15+
let replacement = subst.substitution-function(result);
16+
if (replacement)
17+
result := replace-substrings(result, subst.substitution-pattern, replacement);
18+
end;
19+
end;
20+
result
21+
end method;
22+
23+
// make open generic?
24+
define function add-help-subcommand
25+
(parser :: <command-line-parser>) => ()
26+
add-subcommand(parser,
27+
make(<help-subcommand>,
28+
name: "help",
29+
help: "Display help for a subcommand.",
30+
options: list(make(<positional-option>,
31+
name: "subcommand",
32+
required?: #f,
33+
help: "A subcommand name."))))
34+
end function;
35+
36+
// TODO(cgay): we also have canonical-option-name, but I don't like it; it's
37+
// overkill. Need to have a look at format-option-usage...
38+
define function canonical-name
39+
(option :: <option>) => (name :: <string>)
40+
option.option-names[0]
41+
end function;
42+
43+
define method option-help
44+
(option :: <option>) => (help :: <string>)
45+
let result = option.%option-help;
46+
for (subst in *pattern-substitutions*)
47+
let replacement = subst.substitution-function(option);
48+
result := replace-substrings(result, subst.substitution-pattern, replacement);
49+
end;
50+
result
51+
end method;
52+
53+
define method option-variable
54+
(option :: <option>) => (variable-name :: <string>)
55+
option.%option-variable
56+
| uppercase(canonical-name(option))
57+
end;
58+
59+
define class <help-subcommand> (<subcommand>)
60+
keyword name = "help";
61+
keyword help = "Display help message for a subcommand.";
62+
end class;
63+
64+
define method execute-subcommand
65+
(parser :: <command-line-parser>, subcmd :: <help-subcommand>)
66+
=> (status :: false-or(<integer>));
67+
let name = get-option-value(subcmd, "subcommand");
68+
if (name)
69+
let subcmd = find-subcommand(parser, name);
70+
if (subcmd)
71+
print-synopsis(parser, subcmd);
72+
else
73+
usage-error("Subcommand %= not found.", name);
74+
end;
75+
else
76+
print-synopsis(parser, #f); // 'app help' same as 'app --help'
77+
end;
78+
end method;
79+
80+
// This has a class of its own so that the help option doesn't have to be
81+
// identified by name (which should be user settable).
82+
define open class <help-option> (<flag-option>)
83+
end;
84+
85+
// make open generic?
86+
define function add-help-option
87+
(parser :: <command-line-parser>) => ()
88+
add-option(parser, make(<help-option>,
89+
names: #("help", "h"),
90+
help: "Display this message."));
91+
end function;
92+
93+
define class <substitution> (<object>)
94+
constant slot substitution-pattern :: <string>, required-init-keyword: pattern:;
95+
constant slot substitution-function :: <function>, required-init-keyword: function:;
96+
end class;
97+
98+
// TODO(cgay): "%choices%"
99+
define variable *pattern-substitutions*
100+
= list(make(<substitution>,
101+
pattern: "%default%",
102+
function: method (option)
103+
if (instance?(option, <option>))
104+
// TODO(cgay): Make <boolean>s print as true/false in
105+
// %default% substitutions. There's some subtlety for
106+
// <flag-option> because of negative options.
107+
// Make a format-option-value generic?
108+
format-to-string("%s", option.option-default)
109+
end
110+
end),
111+
make(<substitution>,
112+
pattern: "%app%",
113+
function: always(program-name())));
114+
115+
// For use by extension modules.
116+
define function add-pattern-substitution
117+
(pattern :: <string>, fn :: <function>) => ()
118+
*pattern-substitutions*
119+
:= concatenate(*pattern-substitutions*,
120+
make(<substitution>, pattern: pattern, function: fn));
121+
end function;
122+
123+
define method visible-option-name
124+
(raw-name :: <string>) => (dash-name :: <string>)
125+
concatenate(if (raw-name.size = 1) "-" else "--" end, raw-name)
126+
end;
127+
128+
define method canonical-option-name
129+
(option :: <option>, #key plain?) => (dash-name :: <string>)
130+
if (plain?)
131+
option.option-names.first
132+
else
133+
option.option-names.first.visible-option-name
134+
end
135+
end;
136+
137+
// Return a string showing how this option is used on the command-line.
138+
// TODO(cgay): this is not called. I probably broke it at some point.
139+
// I think it should affect the way the option is displayed in the Options:
140+
// table. e.g., "--flag[=yes/no]"
141+
define open generic format-option-usage
142+
(option :: <option>) => (usage :: <string>);
143+
144+
define method format-option-usage
145+
(option :: <option>) => (usage :: <string>)
146+
option.canonical-option-name
147+
end;
148+
149+
define open generic print-synopsis
150+
(parser :: <command-line-parser>, subcmd :: false-or(<subcommand>), #key stream);
151+
152+
define method print-synopsis
153+
(parser :: <command-line-parser>, subcmd == #f,
154+
#key stream :: <stream> = *standard-output*)
155+
format(stream, "%s\n", parser.command-help);
156+
format(stream, "\n%s\n", generate-usage(parser));
157+
print-options(stream, parser,
158+
if (empty?(parser-subcommands(parser)))
159+
"Options:"
160+
else
161+
"Global options:"
162+
end);
163+
if (~empty?(parser-subcommands(parser)))
164+
format(stream, "\nSubcommands:\n");
165+
let (names, docs) = subcommand-columns(parser);
166+
if (~empty?(names))
167+
let name-width = reduce1(max, map(size, names));
168+
for (name in names, doc in docs)
169+
format(stream, "%s %s\n", pad-right(name, name-width), doc);
170+
end;
171+
end;
172+
let help-subcommand = find-subcommand(parser, <help-subcommand>);
173+
if (help-subcommand)
174+
format(stream, "\nUse '%s %s <subcommand>' to see subcommand options.\n",
175+
program-name(), subcommand-name(help-subcommand));
176+
end;
177+
end;
178+
end method;
179+
180+
define method print-synopsis
181+
(parser :: <command-line-parser>, subcmd :: <subcommand>,
182+
#key stream :: <stream> = *standard-output*)
183+
format(stream, "%s\n", subcmd.command-help);
184+
format(stream, "\n%s\n", generate-usage(subcmd));
185+
print-options(stream, subcmd, "Options:");
186+
let help-option = find-option(parser, <help-option>);
187+
if (help-option)
188+
format(stream, "\nUse '%s %s' to see global options.\n",
189+
program-name(), help-option.canonical-name.visible-option-name);
190+
end;
191+
end method;
192+
193+
define method print-options
194+
(stream :: <stream>, command :: <command>, header :: <string>) => ()
195+
let (names, docs) = option-columns(command);
196+
if (~empty?(names))
197+
format(stream, "\n%s\n", header);
198+
let name-width = reduce1(max, map(size, names));
199+
for (name in names, doc in docs)
200+
format(stream, " %s %s\n", pad-right(name, name-width), doc);
201+
end;
202+
end;
203+
let (names, docs) = positional-columns(command);
204+
if (~empty?(names))
205+
format(stream, "\nPositional arguments:\n");
206+
let name-width = reduce1(max, map(size, names));
207+
for (name in names, doc in docs)
208+
format(stream, " %s %s\n", pad-right(name, name-width), doc);
209+
end;
210+
end;
211+
end method;
212+
213+
define function positional-columns
214+
(cmd :: <command>) => (names :: <sequence>, docs :: <sequence>)
215+
let names = make(<stretchy-vector>);
216+
let docs = make(<stretchy-vector>);
217+
for (opt in cmd.positional-options)
218+
let name = opt.option-variable;
219+
if (opt.option-repeated?)
220+
name := concatenate(name, "...");
221+
end;
222+
add!(names, name);
223+
add!(docs, opt.option-help);
224+
end;
225+
values(names, docs)
226+
end function;
227+
228+
define function option-columns
229+
(parser :: <command>)
230+
=> (names :: <sequence>, docs :: <sequence>)
231+
let names = make(<stretchy-vector>);
232+
let docs = make(<stretchy-vector>);
233+
let any-shorts? = any?(method (opt) ~empty?(opt.short-names) end,
234+
parser.command-options);
235+
for (option in parser.pass-by-name-options)
236+
let longs = map(visible-option-name, option.long-names);
237+
let shorts = map(visible-option-name, option.short-names);
238+
let name = concatenate(join(concatenate(shorts, longs), ", "),
239+
" ",
240+
if (instance?(option, <flag-option>))
241+
""
242+
else
243+
option.option-variable | canonical-name(option);
244+
end);
245+
let indent = if (empty?(shorts) & any-shorts?)
246+
" " // Makes long options align (usually).
247+
else
248+
""
249+
end;
250+
add!(names, concatenate(indent, name));
251+
add!(docs, option.option-help);
252+
end for;
253+
values(names, docs)
254+
end function;
255+
256+
// There is much work to be done to make this better.
257+
define function subcommand-columns
258+
(parser :: <command-line-parser>)
259+
=> (names :: <sequence>, docs :: <sequence>)
260+
let names = make(<stretchy-vector>);
261+
let docs = make(<stretchy-vector>);
262+
for (subcmd in parser.parser-subcommands)
263+
add!(names, concatenate(" ", subcmd.subcommand-name));
264+
// TODO(cgay): Wrap doc text.
265+
add!(docs, subcmd.command-help);
266+
end for;
267+
values(names, docs)
268+
end function;
269+
270+
// Generate a one-line usage message showing the order of options and arguments.
271+
define generic generate-usage
272+
(cmd :: <command>) => (usage :: <string>);
273+
274+
define method generate-usage
275+
(cmd :: <command-line-parser>) => (usage :: <string>)
276+
with-output-to-string (stream)
277+
// Be careful to show where the two sets of options (global/sub) must go.
278+
let subs? = cmd.has-subcommands?;
279+
format(stream, "Usage: %s", program-name());
280+
if (cmd.pass-by-name-options.size > 0)
281+
format(stream, " [%soptions]", if (subs?) "global " else "" end);
282+
end;
283+
if (subs?)
284+
format(stream, " <subcommand> [sub options] args...")
285+
end;
286+
print-positional-args(stream, cmd);
287+
end
288+
end method;
289+
290+
define method generate-usage
291+
(subcmd :: <subcommand>) => (usage :: <string>)
292+
with-output-to-string (stream)
293+
format(stream, "Usage: %s %s%s", program-name(), subcommand-name(subcmd),
294+
if (subcmd.pass-by-name-options.size > 0)
295+
" [options]"
296+
else
297+
""
298+
end);
299+
print-positional-args(stream, subcmd);
300+
end;
301+
end method;
302+
303+
define function print-positional-args
304+
(stream :: <stream>, cmd :: <command>) => ()
305+
// When positional options are added to the command we verify that certain
306+
// constraints are met, like you can't add a repeated arg before a
307+
// non-repeated arg or add an optional arg before a required arg, so here we
308+
// assume those properties hold.
309+
for (option in cmd.positional-options)
310+
let var = option.option-variable;
311+
format(stream,
312+
if (option.option-required?) " %s%s" else " [%s]%s" end,
313+
var,
314+
if (option.option-repeated?) " ..." else "" end);
315+
end;
316+
end function;

0 commit comments

Comments
 (0)