Skip to content

Commit 9546c06

Browse files
authored
Redo CLI using clap's derive pattern (#67)
1 parent 8c3baae commit 9546c06

File tree

7 files changed

+165
-115
lines changed

7 files changed

+165
-115
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ repos:
1717
- id: fmt
1818
- id: cargo-check
1919
- id: clippy
20+
args: ["--fix", "--allow-staged", "--allow-dirty"]
2021

2122
ci:
2223
skip: [ fmt, cargo-check, clippy ]

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.3.0
4+
5+
### Changed
6+
7+
- Reworked the CLI commands, especially for interacting with the bundled examples programs.
8+
39
## 0.2.2
410

511
### Changed

Cargo.lock

Lines changed: 20 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "fungoid"
3-
version = "0.2.2"
4-
description = "A Befunge interpreter and IDE"
3+
version = "0.3.0"
4+
description = "A Befunge interpreter and text-UI IDE written in Rust"
55
authors = ["Josh Karpel <josh.karpel@gmail.com>"]
66
edition = '2018'
77
readme = "README.md"
@@ -11,7 +11,7 @@ homepage = "https://github.com/JoshKarpel/fungoid"
1111
repository = "https://github.com/JoshKarpel/fungoid"
1212

1313
[dependencies]
14-
clap = "4"
14+
clap = { version = "4" , features = ["cargo", "derive"]}
1515
crossterm = "0"
1616
humantime = "2"
1717
rand = "0"

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# fungoid
22

3-
![Tests](https://github.com/JoshKarpel/fungoid/workflows/tests/badge.svg)
4-
5-
A Befunge interpreter written in Rust.
3+
A Befunge interpreter and text-UI IDE written in Rust.
64

75
## Installation
86

src/main.rs

Lines changed: 132 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,8 @@
11
use std::{
2-
error::Error,
3-
fmt,
4-
fmt::Display,
5-
io,
6-
io::{Read, Write},
7-
str::FromStr,
8-
string::String,
9-
time::Instant,
2+
error::Error, ffi::OsString, fmt, fmt::Display, io, str::FromStr, string::String, time::Instant,
103
};
114

12-
use clap::{
13-
Arg,
14-
ArgAction::{Set, SetTrue},
15-
ArgMatches, Command,
16-
};
5+
use clap::{Args, Parser, Subcommand};
176
use fungoid::{examples::EXAMPLES, execution::ExecutionState, program::Program};
187
use humantime::format_duration;
198
use itertools::Itertools;
@@ -26,60 +15,122 @@ fn main() {
2615
}
2716
}
2817

29-
type GenericResult = Result<(), Box<dyn std::error::Error>>;
30-
31-
fn command() -> Command {
32-
Command::new("fungoid")
33-
.version("0.2.1")
34-
.author("Josh Karpel <josh.karpel@gmail.com>")
35-
.about("A Befunge interpreter written in Rust")
36-
.subcommand(
37-
Command::new("run")
38-
.about("Execute a program")
39-
.arg(
40-
Arg::new("profile")
41-
.long("profile")
42-
.action(SetTrue)
43-
.help("Enable profiling"),
44-
)
45-
.arg(
46-
Arg::new("trace")
47-
.long("trace")
48-
.action(SetTrue)
49-
.help("Trace program execution"),
50-
)
51-
.arg(
52-
Arg::new("FILE")
53-
.action(Set)
54-
.required(true)
55-
.help("The file to read the program from"),
56-
),
57-
)
58-
.subcommand(
59-
Command::new("ide").about("Start a TUI IDE").arg(
60-
Arg::new("FILE")
61-
.action(Set)
62-
.required(true)
63-
.help("The file to read the program from"),
64-
),
65-
)
66-
.subcommand(
67-
Command::new("examples").about("Print the names of the bundled example programs."),
68-
)
69-
.arg_required_else_help(true)
18+
type GenericResult<T> = Result<T, Box<dyn Error>>;
19+
20+
#[derive(Debug, Parser)]
21+
#[command(name = "fungoid", author, version, about)]
22+
struct Cli {
23+
#[command(subcommand)]
24+
command: Commands,
7025
}
71-
fn cli() -> GenericResult {
72-
let matches = command().get_matches();
73-
74-
if let Some(matches) = matches.subcommand_matches("ide") {
75-
ide(matches)?;
76-
} else if let Some(matches) = matches.subcommand_matches("run") {
77-
run_program(matches)?;
78-
} else if matches.subcommand_matches("examples").is_some() {
79-
println!("{}", EXAMPLES.keys().sorted().join("\n"))
80-
}
8126

82-
Ok(())
27+
#[derive(Debug, Subcommand)]
28+
enum Commands {
29+
/// Run a program
30+
#[command(arg_required_else_help = true)]
31+
Run {
32+
/// The path to the file to read the program from
33+
file: OsString,
34+
/// Enable execution tracing
35+
#[arg(long)]
36+
trace: bool,
37+
/// Enable profiling
38+
#[arg(long)]
39+
profile: bool,
40+
},
41+
/// Start the TUI IDE
42+
#[command(arg_required_else_help = true)]
43+
Ide {
44+
/// The path to the file to open
45+
file: OsString,
46+
},
47+
/// Interact with the bundled example programs.
48+
#[command(arg_required_else_help = true)]
49+
Examples(ExamplesArgs),
50+
}
51+
52+
#[derive(Debug, Args)]
53+
struct ExamplesArgs {
54+
#[command(subcommand)]
55+
command: ExamplesCommands,
56+
}
57+
58+
#[derive(Debug, Subcommand)]
59+
enum ExamplesCommands {
60+
/// Print the available bundled example programs
61+
#[command(arg_required_else_help = true)]
62+
List,
63+
/// Print one of the example programs to stdout
64+
#[command(arg_required_else_help = true)]
65+
Print { example: String },
66+
/// Run one of the example programs
67+
#[command(arg_required_else_help = true)]
68+
Run {
69+
/// The name of the example to run
70+
example: String,
71+
/// Enable execution tracing
72+
#[arg(long)]
73+
trace: bool,
74+
/// Enable profiling
75+
#[arg(long)]
76+
profile: bool,
77+
},
78+
}
79+
80+
fn cli() -> GenericResult<()> {
81+
match Cli::parse().command {
82+
Commands::Run {
83+
file,
84+
trace,
85+
profile,
86+
} => {
87+
let program = Program::from_file(&file)?;
88+
89+
run_program(program, trace, profile)?;
90+
91+
Ok(())
92+
}
93+
94+
Commands::Ide { file } => {
95+
let program = Program::from_file(&file)?;
96+
97+
fungoid::ide::ide(program)?;
98+
99+
Ok(())
100+
}
101+
102+
Commands::Examples(ExamplesArgs {
103+
command: ExamplesCommands::List,
104+
}) => {
105+
println!("{}", EXAMPLES.keys().sorted().join("\n"));
106+
107+
Ok(())
108+
}
109+
110+
Commands::Examples(ExamplesArgs {
111+
command: ExamplesCommands::Print { example },
112+
}) => {
113+
let program = get_example(example.as_str())?;
114+
println!("{}", program);
115+
116+
Ok(())
117+
}
118+
119+
Commands::Examples(ExamplesArgs {
120+
command:
121+
ExamplesCommands::Run {
122+
example,
123+
trace,
124+
profile,
125+
},
126+
}) => {
127+
let program = Program::from_str(get_example(example.as_str())?).unwrap();
128+
129+
run_program(program, trace, profile)?;
130+
131+
Ok(())
132+
}
133+
}
83134
}
84135

85136
#[derive(Debug)]
@@ -105,61 +156,34 @@ impl Error for NoExampleFound {
105156
}
106157
}
107158

108-
fn load_program(matches: &ArgMatches) -> Result<Program, Box<dyn Error>> {
109-
let file = matches.get_one::<String>("FILE").unwrap();
110-
if file.starts_with("example:") || file.starts_with("examples:") {
111-
let (_, e) = file.split_once(':').unwrap();
112-
if let Some(p) = EXAMPLES.get(e) {
113-
Ok(Program::from_str(p)?)
114-
} else {
115-
Err(Box::new(NoExampleFound::new(format!(
116-
"No example named '{}'.\nExamples: {:?}",
117-
e,
118-
EXAMPLES.keys()
119-
))))
120-
}
159+
fn get_example(example: &str) -> GenericResult<&str> {
160+
if let Some(program) = EXAMPLES.get(example) {
161+
Ok(program)
121162
} else {
122-
Ok(Program::from_file(file)?)
163+
Err(Box::new(NoExampleFound::new(format!(
164+
"No example named '{}'.\nExamples:\n{}",
165+
example,
166+
EXAMPLES.keys().sorted().join("\n")
167+
))))
123168
}
124169
}
125170

126-
fn ide(matches: &ArgMatches) -> GenericResult {
127-
let program = load_program(matches)?;
128-
129-
fungoid::ide::ide(program)?;
130-
131-
Ok(())
132-
}
133-
134-
fn run_program(matches: &ArgMatches) -> GenericResult {
135-
let program = load_program(matches)?;
136-
171+
fn run_program(program: Program, trace: bool, profile: bool) -> GenericResult<()> {
137172
let input = &mut io::stdin();
138173
let output = &mut io::stdout();
139-
let program_state =
140-
fungoid::execution::ExecutionState::new(program, matches.get_flag("trace"), input, output);
141-
142-
run(program_state, matches.get_flag("profile"))?;
174+
let mut program_state = ExecutionState::new(program, trace, input, output);
143175

144-
Ok(())
145-
}
146-
147-
pub fn run<R: Read, O: Write>(
148-
mut program_state: ExecutionState<R, O>,
149-
profile: bool,
150-
) -> GenericResult {
151176
let start = Instant::now();
152177
program_state.run()?;
153178
let duration = start.elapsed();
154179

155-
let num_seconds = 1.0e-9 * (duration.as_nanos() as f64);
156-
157180
if profile {
158181
eprintln!(
159182
"Executed {} instructions in {} ({} instructions/second)",
160183
program_state.instruction_count,
161184
format_duration(duration),
162-
((program_state.instruction_count as f64 / num_seconds) as u64).separated_string()
185+
((program_state.instruction_count as f64 / duration.as_secs_f64()) as u64)
186+
.separated_string()
163187
);
164188
}
165189

@@ -168,10 +192,12 @@ pub fn run<R: Read, O: Write>(
168192

169193
#[cfg(test)]
170194
mod tests {
171-
use crate::command;
195+
use clap::CommandFactory;
196+
197+
use crate::Cli;
172198

173199
#[test]
174200
fn verify_command() {
175-
command().debug_assert();
201+
Cli::command().debug_assert()
176202
}
177203
}

src/program.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, fs::File, io, io::Read, str::FromStr};
1+
use std::{collections::HashMap, ffi::OsString, fs::File, io, io::Read, str::FromStr};
22

33
use itertools::{Itertools, MinMaxResult};
44

@@ -33,7 +33,7 @@ impl Program {
3333
self.0.insert(*pos, c);
3434
}
3535

36-
pub fn from_file(path: &str) -> Result<Self, io::Error> {
36+
pub fn from_file(path: &OsString) -> Result<Self, io::Error> {
3737
let mut f = File::open(path)?;
3838
let mut contents = String::new();
3939
f.read_to_string(&mut contents)?;

0 commit comments

Comments
 (0)