Skip to content

Commit 2eb21bf

Browse files
author
fmoko
authored
Merge pull request rust-lang#599 from AbdouSeck/improve-list-command
feat(cli): Improve the list command with options, and then some
2 parents b3aed52 + 90d83de commit 2eb21bf

File tree

4 files changed

+207
-20
lines changed

4 files changed

+207
-20
lines changed

src/exercise.rs

+10
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,16 @@ path = "{}.rs""#,
232232

233233
State::Pending(context)
234234
}
235+
236+
// Check that the exercise looks to be solved using self.state()
237+
// This is not the best way to check since
238+
// the user can just remove the "I AM NOT DONE" string fromm the file
239+
// without actually having solved anything.
240+
// The only other way to truly check this would to compile and run
241+
// the exercise; which would be both costly and counterintuitive
242+
pub fn looks_done(&self) -> bool {
243+
self.state() == State::Done
244+
}
235245
}
236246

237247
impl Display for Exercise {

src/main.rs

+112-20
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use notify::DebouncedEvent;
77
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
88
use std::ffi::OsStr;
99
use std::fs;
10-
use std::io;
10+
use std::io::{self, prelude::*};
1111
use std::path::Path;
1212
use std::process::{Command, Stdio};
1313
use std::sync::mpsc::channel;
@@ -58,6 +58,45 @@ fn main() {
5858
SubCommand::with_name("list")
5959
.alias("l")
6060
.about("Lists the exercises available in rustlings")
61+
.arg(
62+
Arg::with_name("paths")
63+
.long("paths")
64+
.short("p")
65+
.conflicts_with("names")
66+
.help("Show only the paths of the exercises")
67+
)
68+
.arg(
69+
Arg::with_name("names")
70+
.long("names")
71+
.short("n")
72+
.conflicts_with("paths")
73+
.help("Show only the names of the exercises")
74+
)
75+
.arg(
76+
Arg::with_name("filter")
77+
.long("filter")
78+
.short("f")
79+
.takes_value(true)
80+
.empty_values(false)
81+
.help(
82+
"Provide a string to match the exercise names.\
83+
\nComma separated patterns are acceptable."
84+
)
85+
)
86+
.arg(
87+
Arg::with_name("unsolved")
88+
.long("unsolved")
89+
.short("u")
90+
.conflicts_with("solved")
91+
.help("Display only exercises not yet solved")
92+
)
93+
.arg(
94+
Arg::with_name("solved")
95+
.long("solved")
96+
.short("s")
97+
.conflicts_with("unsolved")
98+
.help("Display only exercises that have been solved")
99+
)
61100
)
62101
.get_matches();
63102

@@ -93,9 +132,51 @@ fn main() {
93132
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
94133
let verbose = matches.is_present("nocapture");
95134

96-
if matches.subcommand_matches("list").is_some() {
97-
exercises.iter().for_each(|e| println!("{}", e.name));
135+
// Handle the list command
136+
if let Some(list_m) = matches.subcommand_matches("list") {
137+
if ["paths", "names"].iter().all(|k| !list_m.is_present(k)) {
138+
println!("{:<17}\t{:<46}\t{:<7}", "Name", "Path", "Status");
139+
}
140+
let filters = list_m.value_of("filter").unwrap_or_default().to_lowercase();
141+
exercises.iter().for_each(|e| {
142+
let fname = format!("{}", e.path.display());
143+
let filter_cond = filters
144+
.split(',')
145+
.filter(|f| f.trim().len() > 0)
146+
.any(|f| e.name.contains(&f) || fname.contains(&f));
147+
let status = if e.looks_done() { "Done" } else { "Pending" };
148+
let solve_cond = {
149+
(e.looks_done() && list_m.is_present("solved"))
150+
|| (!e.looks_done() && list_m.is_present("unsolved"))
151+
|| (!list_m.is_present("solved") && !list_m.is_present("unsolved"))
152+
};
153+
if solve_cond && (filter_cond || !list_m.is_present("filter")) {
154+
let line = if list_m.is_present("paths") {
155+
format!("{}\n", fname)
156+
} else if list_m.is_present("names") {
157+
format!("{}\n", e.name)
158+
} else {
159+
format!("{:<17}\t{:<46}\t{:<7}\n", e.name, fname, status)
160+
};
161+
// Somehow using println! leads to the binary panicking
162+
// when its output is piped.
163+
// So, we're handling a Broken Pipe error and exiting with 0 anyway
164+
let stdout = std::io::stdout();
165+
{
166+
let mut handle = stdout.lock();
167+
handle.write_all(line.as_bytes()).unwrap_or_else(|e| {
168+
match e.kind() {
169+
std::io::ErrorKind::BrokenPipe => std::process::exit(0),
170+
_ => std::process::exit(1),
171+
};
172+
});
173+
}
174+
}
175+
});
176+
std::process::exit(0);
98177
}
178+
179+
// Handle the run command
99180
if let Some(ref matches) = matches.subcommand_matches("run") {
100181
let name = matches.value_of("name").unwrap();
101182

@@ -123,13 +204,18 @@ fn main() {
123204
println!("{}", exercise.hint);
124205
}
125206

207+
// Handle the verify command
126208
if matches.subcommand_matches("verify").is_some() {
127209
verify(&exercises, verbose).unwrap_or_else(|_| std::process::exit(1));
128210
}
129211

212+
// Handle the watch command
130213
if matches.subcommand_matches("watch").is_some() {
131214
if let Err(e) = watch(&exercises, verbose) {
132-
println!("Error: Could not watch your progess. Error message was {:?}.", e);
215+
println!(
216+
"Error: Could not watch your progess. Error message was {:?}.",
217+
e
218+
);
133219
println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
134220
std::process::exit(1);
135221
}
@@ -138,24 +224,24 @@ fn main() {
138224
emoji = Emoji("🎉", "★")
139225
);
140226
println!();
141-
println!("+----------------------------------------------------+");
142-
println!("| You made it to the Fe-nish line! |");
143-
println!("+-------------------------- ------------------------+");
227+
println!("+----------------------------------------------------+");
228+
println!("| You made it to the Fe-nish line! |");
229+
println!("+-------------------------- ------------------------+");
144230
println!(" \\/ ");
145-
println!(" ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ ");
146-
println!(" ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ");
147-
println!(" ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ");
148-
println!(" ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ ");
149-
println!(" ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ ");
150-
println!(" ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ ");
151-
println!(" ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ ");
152-
println!(" ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ ");
231+
println!(" ▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒ ");
232+
println!(" ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ");
233+
println!(" ▒▒▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒▒▒ ");
234+
println!(" ░░▒▒▒▒░░▒▒ ▒▒ ▒▒ ▒▒ ▒▒░░▒▒▒▒ ");
235+
println!(" ▓▓▓▓▓▓▓▓ ▓▓ ▓▓██ ▓▓ ▓▓██ ▓▓ ▓▓▓▓▓▓▓▓ ");
236+
println!(" ▒▒▒▒ ▒▒ ████ ▒▒ ████ ▒▒░░ ▒▒▒▒ ");
237+
println!(" ▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒ ");
238+
println!(" ▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒ ");
153239
println!(" ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ");
154240
println!(" ▒▒▒▒▒▒▒▒▒▒██▒▒▒▒▒▒██▒▒▒▒▒▒▒▒▒▒ ");
155-
println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
156-
println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
157-
println!(" ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ ");
158-
println!(" ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ");
241+
println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒██████▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
242+
println!(" ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ");
243+
println!(" ▒▒ ▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ▒▒ ");
244+
println!(" ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ");
159245
println!(" ▒▒ ▒▒ ▒▒ ▒▒ ");
160246
println!();
161247
println!("We hope you enjoyed learning about the various aspects of Rust!");
@@ -223,7 +309,13 @@ fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
223309
let filepath = b.as_path().canonicalize().unwrap();
224310
let pending_exercises = exercises
225311
.iter()
226-
.skip_while(|e| !filepath.ends_with(&e.path));
312+
.skip_while(|e| !filepath.ends_with(&e.path))
313+
// .filter(|e| filepath.ends_with(&e.path))
314+
.chain(
315+
exercises
316+
.iter()
317+
.filter(|e| !e.looks_done() && !filepath.ends_with(&e.path))
318+
);
227319
clear_screen();
228320
match verify(pending_exercises, verbose) {
229321
Ok(_) => return Ok(()),

tests/fixture/state/info.toml

+7
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ name = "pending_test_exercise"
99
path = "pending_test_exercise.rs"
1010
mode = "test"
1111
hint = """"""
12+
13+
[[exercises]]
14+
name = "finished_exercise"
15+
path = "finished_exercise.rs"
16+
mode = "compile"
17+
hint = """"""
18+

tests/integration_tests.rs

+78
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,81 @@ fn run_single_test_success_without_output() {
181181
.code(0)
182182
.stdout(predicates::str::contains("THIS TEST TOO SHALL PAS").not());
183183
}
184+
185+
#[test]
186+
fn run_rustlings_list() {
187+
Command::cargo_bin("rustlings")
188+
.unwrap()
189+
.args(&["list"])
190+
.current_dir("tests/fixture/success")
191+
.assert()
192+
.success();
193+
}
194+
195+
#[test]
196+
fn run_rustlings_list_conflicting_display_options() {
197+
Command::cargo_bin("rustlings")
198+
.unwrap()
199+
.args(&["list", "--names", "--paths"])
200+
.current_dir("tests/fixture/success")
201+
.assert()
202+
.failure();
203+
}
204+
205+
#[test]
206+
fn run_rustlings_list_conflicting_solve_options() {
207+
Command::cargo_bin("rustlings")
208+
.unwrap()
209+
.args(&["list", "--solved", "--unsolved"])
210+
.current_dir("tests/fixture/success")
211+
.assert()
212+
.failure();
213+
}
214+
215+
#[test]
216+
fn run_rustlings_list_no_pending() {
217+
Command::cargo_bin("rustlings")
218+
.unwrap()
219+
.args(&["list"])
220+
.current_dir("tests/fixture/success")
221+
.assert()
222+
.success()
223+
.stdout(predicates::str::contains("Pending").not());
224+
}
225+
226+
#[test]
227+
fn run_rustlings_list_both_done_and_pending() {
228+
Command::cargo_bin("rustlings")
229+
.unwrap()
230+
.args(&["list"])
231+
.current_dir("tests/fixture/state")
232+
.assert()
233+
.success()
234+
.stdout(
235+
predicates::str::contains("Done")
236+
.and(predicates::str::contains("Pending"))
237+
);
238+
}
239+
240+
#[test]
241+
fn run_rustlings_list_without_pending() {
242+
Command::cargo_bin("rustlings")
243+
.unwrap()
244+
.args(&["list", "--solved"])
245+
.current_dir("tests/fixture/state")
246+
.assert()
247+
.success()
248+
.stdout(predicates::str::contains("Pending").not());
249+
}
250+
251+
#[test]
252+
fn run_rustlings_list_without_done() {
253+
Command::cargo_bin("rustlings")
254+
.unwrap()
255+
.args(&["list", "--unsolved"])
256+
.current_dir("tests/fixture/state")
257+
.assert()
258+
.success()
259+
.stdout(predicates::str::contains("Done").not());
260+
}
261+

0 commit comments

Comments
 (0)