-
Notifications
You must be signed in to change notification settings - Fork 0
/
args.rs
578 lines (501 loc) · 19.5 KB
/
args.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
//! Module for handling command line arguments.
use std::env;
use std::ffi::OsString;
use std::io;
use std::iter::IntoIterator;
use std::process::exit;
use std::str::FromStr;
use clap::{self, AppSettings, Arg, ArgMatches, ArgSettings, Shell, SubCommand};
use conv::TryFrom;
use conv::errors::Unrepresentable;
use exitcode;
use url;
use super::{gist, NAME, VERSION};
/// Parse command line arguments and return matches' object.
#[inline]
pub fn parse() -> Result<Options, ArgsError> {
parse_from_argv(env::args_os())
}
/// Parse application options from given array of arguments
/// (*all* arguments, including binary name).
#[inline]
pub fn parse_from_argv<I, T>(argv: I) -> Result<Options, ArgsError>
where I: IntoIterator<Item=T>, T: Clone + Into<OsString>
{
let argv: Vec<_> = argv.into_iter().collect();
// We allow `gisht JohnDoe/foo` to be an alias of `gisht run JohnDoe/foo`.
// To support this, some preprocessing on the arguments has to be done
// in order to pick the parser with or without subcommands.
let parser = {
// Determine whether the first non-flag argument is one of the gist commands.
let maybe_first_arg = argv.iter().skip(1)
.map(|arg| {
let arg: OsString = arg.clone().into();
arg.into_string().unwrap_or_else(|_| String::new())
})
.find(|arg| !arg.starts_with("-"));
// (That is, provided we got a positional argument at all).
let first_arg = maybe_first_arg.unwrap_or_else(|| "help".into());
if first_arg != "help" {
match Command::from_str(&first_arg) {
Ok(_) => create_full_parser(),
Err(_) => {
// If it's not a gist command, the parser we'll use
// will already have "run" command baked in.
let mut parser = create_parser_base();
parser = configure_run_gist_parser(parser);
parser
},
}
} else {
// If help was requested, use the full parser (with subcommands,
// though make them optional to account for the hack above).
// Al this ensures the correct help/usage instructions are shown.
create_full_parser()
.unset_setting(AppSettings::SubcommandRequiredElseHelp)
}
};
let matches = try!(get_matches_with_completion(parser, argv));
Options::try_from(matches)
}
/// Parse argv against given clap parser whilst handling the possible request
/// for generating autocompletion script for that parser.
fn get_matches_with_completion<'a, 'p, I, T>(parser: Parser<'p>, argv: I) -> clap::Result<ArgMatches<'a>>
where 'p: 'a, I: IntoIterator<Item=T>, T: Into<OsString> + Clone
{
const OPT_COMPLETION: &'static str = "completion";
let parser = parser
// Hidden flag that's used to generate shell completion scripts.
// It overrides the mandatory GIST arg.
.arg(Arg::with_name(OPT_COMPLETION)
.long("complete")
.required(false).conflicts_with(ARG_GIST)
.takes_value(true).number_of_values(1).multiple(false)
.possible_values(&Shell::variants())
.value_name("SHELL")
.help("Generate autocompletion script for given shell")
.set(ArgSettings::Hidden));
let matches = try!(parser.get_matches_from_safe(argv));
// If the completion flag was present, generate the scripts to stdout
// and quit immediately.
if let Some(shell) = matches.value_of(OPT_COMPLETION) {
let shell = shell.parse::<Shell>().unwrap();
trace!("Printing autocompletion script for {}...", shell);
create_full_parser().gen_completions_to(*NAME, shell, &mut io::stdout());
debug!("Autocompletion script for {} printed successuflly", shell);
// TODO: consider eliminating this exit(), most likely by converting
// clap::Result into Result<ArgsError> with a new ArgsError variant
exit(exitcode::OK);
}
Ok(matches)
}
/// Structure to hold options received from the command line.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Options {
/// Verbosity of the logging output.
///
/// Corresponds to the number of times the -v flag has been passed.
/// If -q has been used instead, this will be negative.
pub verbosity: isize,
/// Gist locality flag.
///
/// Depending on its value, this flag. may optionally
/// e.g. prohibit the app from downloading gists from remote hosts.
pub locality: Option<Locality>,
/// Gist command that's been issued.
pub command: Command,
/// Gist to operate on, if any.
pub gist: Option<GistArg>,
/// Arguments to the gist, if any.
/// This is only used if command == Command::Run.
pub gist_args: Option<Vec<String>>,
}
#[allow(dead_code)]
impl Options {
#[inline]
pub fn verbose(&self) -> bool { self.verbosity > 0 }
#[inline]
pub fn quiet(&self) -> bool { self.verbosity < 0 }
}
impl<'a> TryFrom<ArgMatches<'a>> for Options {
type Err = ArgsError;
fn try_from(matches: ArgMatches<'a>) -> Result<Self, Self::Err> {
let verbose_count = matches.occurrences_of(OPT_VERBOSE) as isize;
let quiet_count = matches.occurrences_of(OPT_QUIET) as isize;
let verbosity = verbose_count - quiet_count;
let locality = if matches.is_present(OPT_LOCAL) {
Some(Locality::Local)
} else if matches.is_present(OPT_REMOTE) {
Some(Locality::Remote)
} else {
None
};
// Command may be optionally provided.
// If it isn't, it means the "run" default was used, and so all the arguments
// are arguments to `gisht run`.
let (cmd, cmd_matches) = matches.subcommand();
let cmd_matches = cmd_matches.unwrap_or(&matches);
let command = Command::from_str(cmd).unwrap_or(Command::Run);
// Parse out the gist argument.
let gist = match cmd_matches.value_of(ARG_GIST) {
Some(g) => Some(try!(GistArg::from_str(g))),
None => None,
};
// For the "run" command, arguments may be provided.
let mut gist_args = cmd_matches.values_of(ARG_GIST_ARGV)
.map(|argv| argv.map(|v| v.to_owned()).collect());
if command == Command::Run && gist_args.is_none() {
gist_args = Some(vec![]);
}
Ok(Options{
verbosity: verbosity,
locality: locality,
command: command,
gist: gist,
gist_args: gist_args,
})
}
}
macro_attr! {
/// Error that can occur while parsing of command line arguments.
#[derive(Debug,
Error!("command line arguments error"), ErrorDisplay!, ErrorFrom!)]
pub enum ArgsError {
/// General when parsing the arguments.
Parse(clap::Error),
/// Error while parsing the gist URI.
Gist(GistError),
}
}
/// Type holding the value of the GIST argument.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum GistArg {
/// A gist URI, like "Octocat/hello" or "gh:Foo/bar"
Uri(gist::Uri),
/// A URL to a gist's browser page (that we hopefully recognize).
BrowserUrl(url::Url),
}
impl FromStr for GistArg {
type Err = GistError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
// This is kind of a crappy heuristic but it should suffice for now.
let s = input.trim().to_lowercase();
let is_browser_url = ["http://", "https://", "www."].iter()
.any(|p| s.starts_with(p));
if is_browser_url {
let gist_url = try!(url::Url::from_str(input));
Ok(GistArg::BrowserUrl(gist_url))
} else {
let uri = try!(gist::Uri::from_str(input));
Ok(GistArg::Uri(uri))
}
}
}
macro_attr! {
/// Error that can occur while parsing of the GIST argument.
#[derive(Debug, PartialEq,
Error!("gist argument error"), ErrorDisplay!, ErrorFrom!)]
pub enum GistError {
/// Error while parsing gist URI.
Uri(gist::UriError),
/// Error while parsing gist's browser URL.
BrowserUrl(url::ParseError),
}
}
macro_attr! {
/// Enum describing gist "locality" options.
#[derive(Clone, Debug, Eq, PartialEq, Hash,
IterVariants!(Localities))]
pub enum Locality {
/// Operate only on gists available locally
/// (do not fetch anything from remote gist hosts).
Local,
/// Always fetch the gists from a remote gist host.
Remote,
}
}
macro_attr! {
/// Gist command issued to the application, along with its arguments.
#[derive(Clone, Debug, Eq, PartialEq, Hash,
IterVariants!(Commands))]
pub enum Command {
/// Run the specified gist.
Run,
/// Output the path to gist's binary.
Which,
/// Print the complete source code of the gist's binary.
Print,
/// Open the gist's HTML page in the default web browser.
Open,
/// Display summary information about the gist.
Info,
/// List the information about available gist hosts.
Hosts,
}
}
impl Command {
/// Canonical name of this command.
/// This is the name that the command will be shown under in the usage/help text.
fn name(&self) -> &'static str {
match *self {
Command::Run => "run",
Command::Which => "which",
Command::Print => "print",
Command::Open => "open",
Command::Info => "info",
Command::Hosts => "hosts",
}
}
/// Aliases (alternative names) for this command.
/// These aliases are visible in the application's help message.
fn aliases(&self) -> &'static [&'static str] {
match *self {
Command::Run => &["exec"],
Command::Print => &["cat"],
Command::Open => &["show"],
Command::Info => &["stat"],
Command::Hosts => &["services"],
_ => &[],
}
}
/// Whether the command takes a gist as an argument.
pub fn takes_gist(&self) -> bool {
match *self {
Command::Hosts => false,
_ => true,
}
}
}
impl Default for Command {
fn default() -> Self { Command::Run }
}
impl FromStr for Command {
type Err = Unrepresentable<String>;
/// Create a Command from the result of clap::ArgMatches::subcommand_name().
fn from_str(s: &str) -> Result<Self, Self::Err> {
for command in Command::iter_variants() {
if command.name() == s || command.aliases().contains(&s) {
return Ok(command);
}
}
Err(Unrepresentable(s.to_owned()))
}
}
// Parser configuration
/// Type of the argument parser object
/// (which is called an "App" in clap's silly nomenclature).
type Parser<'p> = clap::App<'p, 'p>;
lazy_static! {
static ref ABOUT: &'static str = option_env!("CARGO_PKG_DESCRIPTION").unwrap_or("");
}
const ARG_GIST: &'static str = "gist";
const ARG_GIST_ARGV: &'static str = "argv";
const OPT_VERBOSE: &'static str = "verbose";
const OPT_QUIET: &'static str = "quiet";
const OPT_LOCAL: &'static str = "local";
const OPT_REMOTE: &'static str = "remote";
/// Create the full argument parser.
/// This parser accepts the entire gamut of the application's arguments and flags.
fn create_full_parser<'p>() -> Parser<'p> {
let parser = create_parser_base();
parser
.setting(AppSettings::SubcommandRequiredElseHelp)
.setting(AppSettings::VersionlessSubcommands)
.subcommand(configure_run_gist_parser(
subcommand_for(Command::Run)
.about("Run the specified gist")))
.subcommand(subcommand_for(Command::Which)
.about("Output the path to gist's binary")
.arg(gist_arg("Gist to locate")))
.subcommand(subcommand_for(Command::Print)
.about("Print the source code of gist's binary")
.arg(gist_arg("Gist to print")))
.subcommand(subcommand_for(Command::Open)
.about("Open the gist's webpage")
.arg(gist_arg("Gist to open")))
.subcommand(subcommand_for(Command::Info)
.about("Display summary information about the gist")
.arg(gist_arg("Gist to display info on")))
.subcommand(subcommand_for(Command::Hosts)
.about("List supported gist hosts (services)"))
.after_help(
"Hint: `gisht run GIST` can be shortened to just `gisht GIST`.\n\
If you want to pass arguments, put them after `--` (two dashes), like this:\n\n\
\tgisht Octocat/greet -- \"Hello world\" --cheerful")
}
/// Create the "base" argument parser object.
///
/// This base contains all the shared configuration (like the application name)
/// and the flags shared by all gist subcommands.
fn create_parser_base<'p>() -> Parser<'p> {
let mut parser = Parser::new(*NAME);
if let Some(version) = *VERSION {
parser = parser.version(version);
}
parser
.about(*ABOUT)
.setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::UnifiedHelpMessage)
.setting(AppSettings::DeriveDisplayOrder)
.setting(AppSettings::ColorNever)
// Gist locality flags (shared by all subcommands).
.arg(Arg::with_name(OPT_LOCAL)
.long("cached").short("c")
.conflicts_with(OPT_REMOTE)
.help("Operate only on gists available locally"))
.arg(Arg::with_name(OPT_REMOTE)
.long("fetch").short("f")
.conflicts_with(OPT_LOCAL)
.help("Always fetch the gist from a remote host"))
// Verbosity flags (shared by all subcommands).
.arg(Arg::with_name(OPT_VERBOSE)
.long("verbose").short("v")
.set(ArgSettings::Multiple)
.conflicts_with(OPT_QUIET)
.help("Increase logging verbosity"))
.arg(Arg::with_name(OPT_QUIET)
.long("quiet").short("q")
.set(ArgSettings::Multiple)
.conflicts_with(OPT_VERBOSE)
.help("Decrease logging verbosity"))
.help_short("H")
.version_short("V")
}
/// Create a clap subcommand Parser object for given gist Command.
fn subcommand_for<'p>(command: Command) -> Parser<'p> {
SubCommand::with_name(command.name())
.visible_aliases(command.aliases())
.help_short("H")
}
/// Configure a parser for the "run" command.
/// This is also used when there is no command given.
fn configure_run_gist_parser<'p>(parser: Parser<'p>) -> Parser<'p> {
parser
.arg(gist_arg("Gist to run"))
// This argument spec is capturing everything after the gist URI,
// allowing for the arguments to be passed to the gist itself.
.arg(Arg::with_name(ARG_GIST_ARGV)
.required(false)
.multiple(true)
.use_delimiter(false)
.help("Optional arguments passed to the gist")
.value_name("ARGS"))
.setting(AppSettings::TrailingVarArg)
}
/// Create the GIST argument to various gist subcommands.
fn gist_arg(help: &'static str) -> Arg {
Arg::with_name(ARG_GIST)
.required(true)
.help(help)
.value_name("GIST")
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use std::str::FromStr;
use super::{Command, create_full_parser, parse_from_argv};
#[test]
fn command_aliases_distinct_from_name() {
for cmd in Command::iter_variants() {
assert!(!cmd.aliases().contains(&cmd.name()),
"Aliases for '{}' command include its canonical name", cmd.name());
}
}
#[test]
fn command_aliases_not_duplicated() {
for cmd in Command::iter_variants() {
assert_eq!(cmd.aliases().len(),
cmd.aliases().iter().collect::<HashSet<_>>().len(),
"Command '{}' has duplicate aliases", cmd.name());
}
}
#[test]
fn command_from_name() {
for cmd in Command::iter_variants() {
assert_eq!(cmd, Command::from_str(cmd.name()).unwrap());
}
}
#[test]
fn command_from_alias() {
for cmd in Command::iter_variants() {
for alias in cmd.aliases() {
assert_eq!(cmd, Command::from_str(alias).unwrap());
}
}
}
/// Check if all gist subcommands are correctly used in the argparser.
#[test]
fn commands_in_usage() {
let mut usage = Vec::new();
create_full_parser().write_help(&mut usage).unwrap();
let usage = String::from_utf8(usage).unwrap();
for cmd in Command::iter_variants() {
assert!(usage.contains(cmd.name()),
"Usage string doesn't contain the '{}' command.", cmd.name());
for alias in cmd.aliases() {
assert!(usage.contains(alias),
"Usage string doesn't contain the '{}' alias of '{}' command", alias, cmd.name());
}
}
}
/// Verify that `run` subcommand is optional when running a gist without args.
#[test]
fn run_optional_no_args() {
let run_opts = parse_from_argv(vec!["gisht", "run", "test/test"]).unwrap();
let no_run_opts = parse_from_argv(vec!["gisht", "test/test"]).unwrap();
assert_eq!(run_opts, no_run_opts);
}
/// Verify that `run` subcommand is optional when running a gist with args.
#[test]
fn run_optional_with_args() {
let run_dashes_opts = parse_from_argv(vec![
"gisht", "run", "test/test", "--", "some", "arg"]).unwrap();
let no_run_dashes_opts = parse_from_argv(vec![
"gisht", "test/test", "--", "some", "arg"]).unwrap();
let run_no_dashes_opts = parse_from_argv(vec![
"gisht", "run", "test/test", "some", "arg"]).unwrap();
let no_run_no_dashes_opts = parse_from_argv(vec![
"gisht", "test/test", "--", "some", "arg"]).unwrap();
// Everything should be equal to each other.
let all_opts = vec![run_dashes_opts,
no_run_dashes_opts,
run_no_dashes_opts,
no_run_no_dashes_opts];
for opts1 in &all_opts {
for opts2 in &all_opts {
assert_eq!(opts1, opts2);
}
}
}
/// Verify that args can only be provided for the `run` subcommand.
#[test]
fn gist_args_only_for_run() {
for cmd in Command::iter_variants().filter(|cmd| *cmd != Command::Run) {
let args = vec!["gisht", cmd.name(), "test/test", "--", "some", "arg"];
assert!(parse_from_argv(args).is_err(),
"Command `{}` unexpectedly accepted arguments!", cmd.name());
}
}
/// Verify that passing an invalid gist spec will cause an error.
#[test]
fn invalid_gist() {
let gist_uri = "foo:foo:foo"; // Invalid.
let args = vec!["gisht", "run", gist_uri];
assert!(parse_from_argv(args).is_err(),
"Gist URI `{}` should cause a parse error but didn't", gist_uri);
}
// Verify that "help" command correctly shows help.
#[test]
fn help_works() {
let args = vec!["gisht", "help"];
assert!(parse_from_argv(args).is_err(),
"\"help\" command was incorrectly treated as gist command");
}
/// Verify that you can call the program with just the verbosity flags.
#[test]
fn just_verbosity_works() {
let args = vec!["gisht", "-v"];
assert!(parse_from_argv(args).is_ok(),
"Failed to parse command line with just the verbosity args");
}
}