Skip to content

Commit 45ece1f

Browse files
committed
enable the CLI to run a single command and exit, without the REPL
1 parent 5771e8f commit 45ece1f

3 files changed

Lines changed: 138 additions & 99 deletions

File tree

src/config.rs

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -50,58 +50,56 @@ pub enum ConfigError {
5050
InvalidUrl
5151
}
5252

53-
impl ClientConfig {
54-
pub fn from_cli_args() -> Result<ClientConfig, ConfigError> {
55-
ClientConfig::from(|app| app.get_matches())
56-
}
53+
pub fn get_base_app<'a, 'b>() -> App<'a, 'b> {
54+
App::new("codedx-client")
55+
.version(crate_version!())
56+
.about("CLI client for the Code Dx REST API")
57+
.arg(Arg::with_name("base-url")
58+
.short("b")
59+
.long("base-url")
60+
.value_name("BASE URL")
61+
.help("Code Dx base url (e.g. 'https://localhost/codedx')")
62+
.takes_value(true)
63+
.required(true)
64+
.index(1)
65+
)
66+
.arg(Arg::with_name("username")
67+
.short("u")
68+
.long("username")
69+
.value_name("VALUE")
70+
.help("Username for basic auth")
71+
.takes_value(true)
72+
)
73+
.arg(Arg::with_name("password")
74+
.short("p")
75+
.long("password")
76+
.value_name("VALUE")
77+
.help("Password for basic auth")
78+
.takes_value(true)
79+
)
80+
.arg(Arg::with_name("api-key")
81+
.short("k")
82+
.long("api-key")
83+
.value_name("VALUE")
84+
.help("API Key for for key-based auth")
85+
.takes_value(true)
86+
)
87+
.arg(Arg::with_name("insecure")
88+
.long("insecure")
89+
.takes_value(false)
90+
.help("Ignore https certificate hostname validation")
91+
)
92+
.arg(Arg::with_name("no-prompt")
93+
.long("no-prompt")
94+
.takes_value(false)
95+
.help("Don't output REPL prompts to STDOUT")
96+
)
97+
}
5798

58-
pub fn from<'a, F>(get_matches: F) -> Result<ClientConfig, ConfigError>
59-
where F: for<'b> FnOnce(App<'a, 'b>) -> ArgMatches<'a>
60-
{
61-
let app = App::new("codedx-client")
62-
.version(crate_version!())
63-
.about("CLI client for the Code Dx REST API")
64-
.arg(Arg::with_name("base-url")
65-
.short("b")
66-
.long("base-url")
67-
.value_name("BASE URL")
68-
.help("Code Dx base url (e.g. 'https://localhost/codedx')")
69-
.takes_value(true)
70-
.required(true)
71-
.index(1)
72-
)
73-
.arg(Arg::with_name("username")
74-
.short("u")
75-
.long("username")
76-
.value_name("VALUE")
77-
.help("Username for basic auth")
78-
.takes_value(true)
79-
)
80-
.arg(Arg::with_name("password")
81-
.short("p")
82-
.long("password")
83-
.value_name("VALUE")
84-
.help("Password for basic auth")
85-
.takes_value(true)
86-
)
87-
.arg(Arg::with_name("api-key")
88-
.short("k")
89-
.long("api-key")
90-
.value_name("VALUE")
91-
.help("API Key for for key-based auth")
92-
.takes_value(true)
93-
)
94-
.arg(Arg::with_name("insecure")
95-
.long("insecure")
96-
.takes_value(false)
97-
.help("Ignore https certificate hostname validation")
98-
)
99-
.arg(Arg::with_name("no-prompt")
100-
.long("no-prompt")
101-
.takes_value(false)
102-
.help("Don't output REPL prompts to STDOUT")
103-
);
104-
let matches = get_matches(app);
99+
impl ClientConfig {
100+
/// Extract a `ClientConfig` from the given `ArgMatches`, which are expected to be derived
101+
/// from the `App` returned by `get_base_app`.
102+
pub fn from_matches<'a>(matches: &ArgMatches<'a>) -> Result<ClientConfig, ConfigError> {
105103

106104
// parse the base-url as a URI, then attempt to access the `path_segments_mut` to
107105
// ensure that will work once we pass the base url to the api client code.

src/main.rs

Lines changed: 89 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,77 @@ extern crate url;
99
#[macro_use] extern crate serde_json;
1010
#[macro_use] extern crate serde_derive;
1111

12+
mod client;
13+
mod commands;
14+
mod config;
15+
mod repl;
16+
1217
use clap::{ArgMatches, App, AppSettings};
1318
use std::io;
1419
use std::io::Write;
1520

16-
mod config;
1721
use config::*;
18-
19-
mod commands;
20-
21-
mod client;
2222
use client::*;
23-
24-
mod repl;
2523
use repl::CmdArgs;
2624

2725
fn main(){
28-
match ClientConfig::from_cli_args() {
26+
let app = {
27+
let mut base_app = config::get_base_app();
28+
for command in commands::all() {
29+
base_app = base_app.subcommand(command.as_subcommand());
30+
}
31+
base_app
32+
};
33+
let matches = app.get_matches();
34+
35+
match ClientConfig::from_matches(&matches) {
2936
Ok(config) => {
3037
let client = ApiClient::new(Box::new(config));
31-
run_repl(client);
38+
if let (_, Some(_)) = matches.subcommand() {
39+
run_oneoff(client, &matches);
40+
} else {
41+
if !client.get_config().no_prompt {
42+
println!("Welcome to the Code Dx CLI Client REPL.");
43+
println!("In the REPL, you can enter commands without having to provide the Code Dx base url or credentials each time.");
44+
println!("If this wasn't what you expected, make sure to include a command when running this program from the command line.");
45+
println!("For a list of commands, type 'help'. To exit, type 'exit'");
46+
println!();
47+
}
48+
run_repl(client);
49+
}
3250
},
33-
Err(ConfigError::MissingAuth) => println!("Authorization info missing or incomplete. Either an API Key or a Username + Password must be provided"),
34-
Err(ConfigError::MissingUrl) => println!("Missing the Base URL"),
35-
Err(ConfigError::InvalidUrl) => println!("Invalid Base URL. Did you forget 'http://' or 'https://' ?"),
51+
Err(ConfigError::MissingAuth) => eprintln!("Authorization info missing or incomplete. Either an API Key or a Username + Password must be provided"),
52+
Err(ConfigError::MissingUrl) => eprintln!("Missing the Base URL"),
53+
Err(ConfigError::InvalidUrl) => eprintln!("Invalid Base URL. Did you forget 'http://' or 'https://' ?"),
3654
}
3755
}
3856

39-
/// Main program loop.
57+
/// Run a single command based on the `arg_matches`.
4058
///
41-
/// Prompts for a command from stdin, then attempts to interpret it as a `ReplCommand` and execute it.
59+
/// This function will be called when the main `App` matches a subcommand.
60+
/// The subcommand will be run, and the program will exit immediately afterward.
61+
fn run_oneoff<'a>(client: ApiClient, arg_matches: &ArgMatches<'a>) -> ! {
62+
let command_runner = CommandRunner(commands::all());
63+
64+
let exit_code = match command_runner.maybe_run(&arg_matches, &client) {
65+
CommandRunnerResult::Done => 0,
66+
CommandRunnerResult::RequestedExit(code) => code,
67+
CommandRunnerResult::UnknownCommand => {
68+
eprintln!("Unknown command.");
69+
-1
70+
},
71+
CommandRunnerResult::InvalidArguments(msg) => {
72+
eprintln!("Invalid arguments for command: {}", msg);
73+
-2
74+
},
75+
};
76+
77+
std::process::exit(exit_code);
78+
}
79+
80+
/// Repeatedly prompt for- and execute- commands.
4281
///
43-
/// Repeats until an EOF or the "exit" command are encountered.
82+
/// The loop ends when the "exit" command is run, or when STDIN reaches an EOF.
4483
fn run_repl(client: ApiClient) {
4584
loop {
4685
// friendly prompt
@@ -76,49 +115,31 @@ fn run_repl(client: ApiClient) {
76115
match e.kind {
77116
clap::ErrorKind::HelpDisplayed => {
78117
// repl_app().print_help().unwrap();
79-
e.write_to(&mut io::stdout()).unwrap();
80-
println!("\n");
118+
e.write_to(&mut io::stderr()).unwrap();
119+
eprintln!("\n");
81120
},
82121
clap::ErrorKind::VersionDisplayed => {
83-
//repl_app().write_version(&mut io::stdout()).unwrap();
84-
println!("\n");
122+
eprintln!("\n");
85123
}
86124
_ => {
87-
e.write_to(&mut io::stdout()).unwrap();
88-
println!("\n");
125+
e.write_to(&mut io::stderr()).unwrap();
126+
eprintln!("\n");
89127
},
90128
}
91129
},
92130
Ok(arg_matches) => {
93131
let command_runner = CommandRunner(commands::all());
94-
let stuff = command_runner.maybe_run(&arg_matches, &client);
95-
// TODO: interpret the result (and give it a better name)
96132

97-
match stuff {
98-
None => {
99-
println!("Invalid command.");
100-
println!("Try again.");
101-
},
102-
Some(Err(msg)) => {
103-
println!("Invalid arguments for command: {}", msg);
104-
println!("Try again.");
105-
},
106-
Some(Ok(Err(commands::Exit(code)))) => {
107-
std::process::exit(code);
108-
},
109-
Some(Ok(Ok(()))) => {
110-
// command ran without issue
111-
}
133+
match command_runner.maybe_run(&arg_matches, &client) {
134+
CommandRunnerResult::UnknownCommand => eprintln!("Unknown command; try again."),
135+
CommandRunnerResult::InvalidArguments(msg) => eprintln!("Invalid arguments for command: {}\nTry again.", msg),
136+
CommandRunnerResult::RequestedExit(code) => std::process::exit(code),
137+
CommandRunnerResult::Done => (),
112138
}
113139
},
114140
};
115141
}
116142
}
117-
118-
}
119-
120-
if !client.get_config().no_prompt {
121-
println!("bye");
122143
}
123144
}
124145

@@ -139,13 +160,35 @@ fn repl_app() -> App<'static, 'static> {
139160
app
140161
}
141162

163+
/// Wrapper for a collection of `Command`s.
164+
///
165+
/// Its purpose is to run the first command that matches some given `arg_matches`, returning that command's result.
166+
/// It exposes the result as a friendly enum, `CommandRunnerResult`.
142167
struct CommandRunner<'a>(Vec<Box<commands::Command<'a>>>);
143168
impl <'a> CommandRunner<'a> {
144-
fn maybe_run<'b>(&self, arg_matches: &'a ArgMatches, client: &'b ApiClient) -> Option<Result<commands::CommandResult, &'a str>> {
145-
let foo = self.0.iter().filter_map(|command_box| {
169+
fn maybe_run<'b>(&self, arg_matches: &'a ArgMatches, client: &'b ApiClient) -> CommandRunnerResult<'a> {
170+
let raw_result = self.0.iter().filter_map(|command_box| {
146171
let cmd = command_box.as_ref();
147172
cmd.maybe_run(arg_matches, client)
148173
}).next();
149-
foo
174+
raw_result.into()
175+
}
176+
}
177+
178+
/// Result of attempting to run the first applicable command on some `ArgMatches`.
179+
enum CommandRunnerResult<'a> {
180+
Done,
181+
UnknownCommand,
182+
InvalidArguments(&'a str),
183+
RequestedExit(i32),
184+
}
185+
impl <'a> From<Option<Result<commands::CommandResult, &'a str>>> for CommandRunnerResult<'a> {
186+
fn from(result: Option<Result<commands::CommandResult, &'a str>>) -> Self {
187+
match result {
188+
Some(Ok(Ok(()))) => CommandRunnerResult::Done,
189+
Some(Ok(Err(commands::Exit(code)))) => CommandRunnerResult::RequestedExit(code),
190+
Some(Err(msg)) => CommandRunnerResult::InvalidArguments(msg),
191+
None => CommandRunnerResult::UnknownCommand,
192+
}
150193
}
151194
}

src/repl.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
//#[macro_use] extern crate nom;
2-
31
use std;
42
use std::str;
53
use std::str::FromStr;

0 commit comments

Comments
 (0)