Skip to content

Commit

Permalink
Redo CLI using clap's derive pattern (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
JoshKarpel committed Oct 14, 2023
1 parent 8c3baae commit 9546c06
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 115 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ repos:
- id: fmt
- id: cargo-check
- id: clippy
args: ["--fix", "--allow-staged", "--allow-dirty"]

ci:
skip: [ fmt, cargo-check, clippy ]
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.3.0

### Changed

- Reworked the CLI commands, especially for interacting with the bundled examples programs.

## 0.2.2

### Changed
Expand Down
21 changes: 20 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "fungoid"
version = "0.2.2"
description = "A Befunge interpreter and IDE"
version = "0.3.0"
description = "A Befunge interpreter and text-UI IDE written in Rust"
authors = ["Josh Karpel <[email protected]>"]
edition = '2018'
readme = "README.md"
Expand All @@ -11,7 +11,7 @@ homepage = "https://github.com/JoshKarpel/fungoid"
repository = "https://github.com/JoshKarpel/fungoid"

[dependencies]
clap = "4"
clap = { version = "4" , features = ["cargo", "derive"]}
crossterm = "0"
humantime = "2"
rand = "0"
Expand Down
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# fungoid

![Tests](https://github.com/JoshKarpel/fungoid/workflows/tests/badge.svg)

A Befunge interpreter written in Rust.
A Befunge interpreter and text-UI IDE written in Rust.

## Installation

Expand Down
238 changes: 132 additions & 106 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
use std::{
error::Error,
fmt,
fmt::Display,
io,
io::{Read, Write},
str::FromStr,
string::String,
time::Instant,
error::Error, ffi::OsString, fmt, fmt::Display, io, str::FromStr, string::String, time::Instant,
};

use clap::{
Arg,
ArgAction::{Set, SetTrue},
ArgMatches, Command,
};
use clap::{Args, Parser, Subcommand};
use fungoid::{examples::EXAMPLES, execution::ExecutionState, program::Program};
use humantime::format_duration;
use itertools::Itertools;
Expand All @@ -26,60 +15,122 @@ fn main() {
}
}

type GenericResult = Result<(), Box<dyn std::error::Error>>;

fn command() -> Command {
Command::new("fungoid")
.version("0.2.1")
.author("Josh Karpel <[email protected]>")
.about("A Befunge interpreter written in Rust")
.subcommand(
Command::new("run")
.about("Execute a program")
.arg(
Arg::new("profile")
.long("profile")
.action(SetTrue)
.help("Enable profiling"),
)
.arg(
Arg::new("trace")
.long("trace")
.action(SetTrue)
.help("Trace program execution"),
)
.arg(
Arg::new("FILE")
.action(Set)
.required(true)
.help("The file to read the program from"),
),
)
.subcommand(
Command::new("ide").about("Start a TUI IDE").arg(
Arg::new("FILE")
.action(Set)
.required(true)
.help("The file to read the program from"),
),
)
.subcommand(
Command::new("examples").about("Print the names of the bundled example programs."),
)
.arg_required_else_help(true)
type GenericResult<T> = Result<T, Box<dyn Error>>;

#[derive(Debug, Parser)]
#[command(name = "fungoid", author, version, about)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
fn cli() -> GenericResult {
let matches = command().get_matches();

if let Some(matches) = matches.subcommand_matches("ide") {
ide(matches)?;
} else if let Some(matches) = matches.subcommand_matches("run") {
run_program(matches)?;
} else if matches.subcommand_matches("examples").is_some() {
println!("{}", EXAMPLES.keys().sorted().join("\n"))
}

Ok(())
#[derive(Debug, Subcommand)]
enum Commands {
/// Run a program
#[command(arg_required_else_help = true)]
Run {
/// The path to the file to read the program from
file: OsString,
/// Enable execution tracing
#[arg(long)]
trace: bool,
/// Enable profiling
#[arg(long)]
profile: bool,
},
/// Start the TUI IDE
#[command(arg_required_else_help = true)]
Ide {
/// The path to the file to open
file: OsString,
},
/// Interact with the bundled example programs.
#[command(arg_required_else_help = true)]
Examples(ExamplesArgs),
}

#[derive(Debug, Args)]
struct ExamplesArgs {
#[command(subcommand)]
command: ExamplesCommands,
}

#[derive(Debug, Subcommand)]
enum ExamplesCommands {
/// Print the available bundled example programs
#[command(arg_required_else_help = true)]
List,
/// Print one of the example programs to stdout
#[command(arg_required_else_help = true)]
Print { example: String },
/// Run one of the example programs
#[command(arg_required_else_help = true)]
Run {
/// The name of the example to run
example: String,
/// Enable execution tracing
#[arg(long)]
trace: bool,
/// Enable profiling
#[arg(long)]
profile: bool,
},
}

fn cli() -> GenericResult<()> {
match Cli::parse().command {
Commands::Run {
file,
trace,
profile,
} => {
let program = Program::from_file(&file)?;

run_program(program, trace, profile)?;

Ok(())
}

Commands::Ide { file } => {
let program = Program::from_file(&file)?;

fungoid::ide::ide(program)?;

Ok(())
}

Commands::Examples(ExamplesArgs {
command: ExamplesCommands::List,
}) => {
println!("{}", EXAMPLES.keys().sorted().join("\n"));

Ok(())
}

Commands::Examples(ExamplesArgs {
command: ExamplesCommands::Print { example },
}) => {
let program = get_example(example.as_str())?;
println!("{}", program);

Ok(())
}

Commands::Examples(ExamplesArgs {
command:
ExamplesCommands::Run {
example,
trace,
profile,
},
}) => {
let program = Program::from_str(get_example(example.as_str())?).unwrap();

run_program(program, trace, profile)?;

Ok(())
}
}
}

#[derive(Debug)]
Expand All @@ -105,61 +156,34 @@ impl Error for NoExampleFound {
}
}

fn load_program(matches: &ArgMatches) -> Result<Program, Box<dyn Error>> {
let file = matches.get_one::<String>("FILE").unwrap();
if file.starts_with("example:") || file.starts_with("examples:") {
let (_, e) = file.split_once(':').unwrap();
if let Some(p) = EXAMPLES.get(e) {
Ok(Program::from_str(p)?)
} else {
Err(Box::new(NoExampleFound::new(format!(
"No example named '{}'.\nExamples: {:?}",
e,
EXAMPLES.keys()
))))
}
fn get_example(example: &str) -> GenericResult<&str> {
if let Some(program) = EXAMPLES.get(example) {
Ok(program)
} else {
Ok(Program::from_file(file)?)
Err(Box::new(NoExampleFound::new(format!(
"No example named '{}'.\nExamples:\n{}",
example,
EXAMPLES.keys().sorted().join("\n")
))))
}
}

fn ide(matches: &ArgMatches) -> GenericResult {
let program = load_program(matches)?;

fungoid::ide::ide(program)?;

Ok(())
}

fn run_program(matches: &ArgMatches) -> GenericResult {
let program = load_program(matches)?;

fn run_program(program: Program, trace: bool, profile: bool) -> GenericResult<()> {
let input = &mut io::stdin();
let output = &mut io::stdout();
let program_state =
fungoid::execution::ExecutionState::new(program, matches.get_flag("trace"), input, output);

run(program_state, matches.get_flag("profile"))?;
let mut program_state = ExecutionState::new(program, trace, input, output);

Ok(())
}

pub fn run<R: Read, O: Write>(
mut program_state: ExecutionState<R, O>,
profile: bool,
) -> GenericResult {
let start = Instant::now();
program_state.run()?;
let duration = start.elapsed();

let num_seconds = 1.0e-9 * (duration.as_nanos() as f64);

if profile {
eprintln!(
"Executed {} instructions in {} ({} instructions/second)",
program_state.instruction_count,
format_duration(duration),
((program_state.instruction_count as f64 / num_seconds) as u64).separated_string()
((program_state.instruction_count as f64 / duration.as_secs_f64()) as u64)
.separated_string()
);
}

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

#[cfg(test)]
mod tests {
use crate::command;
use clap::CommandFactory;

use crate::Cli;

#[test]
fn verify_command() {
command().debug_assert();
Cli::command().debug_assert()
}
}
4 changes: 2 additions & 2 deletions src/program.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashMap, fs::File, io, io::Read, str::FromStr};
use std::{collections::HashMap, ffi::OsString, fs::File, io, io::Read, str::FromStr};

use itertools::{Itertools, MinMaxResult};

Expand Down Expand Up @@ -33,7 +33,7 @@ impl Program {
self.0.insert(*pos, c);
}

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

0 comments on commit 9546c06

Please sign in to comment.