Skip to content

Commit 3b5aa17

Browse files
CLI print warnings + errors and proper exit codes
Also use builtin parse errors from pest and general cleanup in CLI
1 parent d74ab56 commit 3b5aa17

File tree

5 files changed

+98
-66
lines changed

5 files changed

+98
-66
lines changed

src/cli.rs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
1-
use std::{error::Error, fs, path::PathBuf};
1+
use std::{
2+
error::Error,
3+
fs,
4+
io::{self, Read},
5+
path::PathBuf,
6+
};
27

38
use lazy_static::lazy_static;
49
use structopt::StructOpt;
510

611
use crate::parser;
712

813
lazy_static! {
9-
static ref STDOUT: PathBuf = PathBuf::from("-");
14+
static ref IO_SENTINEL: PathBuf = PathBuf::from("-");
1015
}
1116

1217
#[derive(Debug, StructOpt)]
1318
#[structopt(rename_all = "kebab")]
1419
/// Parse, assemble, and save Redcode files
1520
struct CliOptions {
16-
/// Input file
21+
/// Input file; use '-' to read from stdin
1722
#[structopt(parse(from_os_str))]
1823
input_file: PathBuf,
1924

@@ -26,8 +31,8 @@ enum Command {
2631
/// Save/print a program in 'load file' format
2732
#[structopt(name = "dump")]
2833
Dump {
29-
/// Output file - defaults to stdout ('-')
30-
#[structopt(long, short, parse(from_os_str), default_value = STDOUT.to_str().unwrap())]
34+
/// Output file; defaults to stdout ('-')
35+
#[structopt(long, short, parse(from_os_str), default_value = IO_SENTINEL.to_str().unwrap())]
3136
output_file: PathBuf,
3237

3338
/// Whether labels, expressions, macros, etc. should be resolved and
@@ -40,21 +45,32 @@ enum Command {
4045
pub fn run() -> Result<(), Box<dyn Error>> {
4146
let cli_options = CliOptions::from_args();
4247

43-
let input_program = fs::read_to_string(cli_options.input_file)?;
48+
let mut input = String::new();
4449

45-
let mut parsed_core = parser::parse(input_program.as_str())?;
50+
if cli_options.input_file == *IO_SENTINEL {
51+
io::stdin().read_to_string(&mut input)?;
52+
} else {
53+
input = fs::read_to_string(cli_options.input_file)?;
54+
}
55+
56+
let mut parsed_core = parser::parse(input.as_str())?;
57+
58+
// TODO colored output?
59+
for warning in parsed_core.warnings.iter() {
60+
eprintln!("Warning:\n {}", warning);
61+
}
4662

4763
match cli_options.command {
4864
Command::Dump {
4965
output_file,
5066
no_expand,
5167
} => {
5268
if !no_expand {
53-
parsed_core.resolve()?;
69+
parsed_core.result.resolve()?;
5470
}
5571

56-
if output_file == *STDOUT {
57-
println!("{}", parsed_core);
72+
if output_file == *IO_SENTINEL {
73+
print!("{}", parsed_core);
5874
} else {
5975
fs::write(output_file, format!("{}", parsed_core))?;
6076
};

src/load_file.rs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use std::{
2-
collections::{hash_map::Entry, HashMap},
3-
fmt,
4-
};
1+
use std::{collections::HashMap, fmt};
52

63
mod types;
74
pub use types::{AddressMode, Modifier, Opcode, Value};
@@ -169,12 +166,10 @@ impl Core {
169166
));
170167
}
171168

172-
match self.labels.entry(label) {
173-
Entry::Occupied(entry) => Err(format!("Label '{}' already exists", entry.key())),
174-
Entry::Vacant(entry) => {
175-
entry.insert(index);
176-
Ok(())
177-
}
169+
if self.labels.insert(label.clone(), index).is_some() {
170+
Err(format!("Label '{}' already exists", label))
171+
} else {
172+
Ok(())
178173
}
179174
}
180175

@@ -252,11 +247,13 @@ mod tests {
252247
core.add_label(256, "goblin".into())
253248
.expect_err("Should fail to add labels > 200");
254249
core.add_label(5, "baz".into())
255-
.expect_err("Should fail to add duplicate label");
250+
.expect_err("Should error duplicate label");
256251

257252
assert_eq!(core.label_address("foo").unwrap(), 0);
258253
assert_eq!(core.label_address("bar").unwrap(), 0);
259-
assert_eq!(core.label_address("baz").unwrap(), 123);
254+
255+
// The _last_ version of a label will be the one we use
256+
assert_eq!(core.label_address("baz").unwrap(), 5);
260257

261258
assert!(core.label_address("goblin").is_none());
262259
assert!(core.label_address("never_mentioned").is_none());

src/main.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
extern crate corewa_rs;
22

3-
use std::error::Error;
4-
53
use corewa_rs::cli;
64

7-
fn main() -> Result<(), Box<dyn Error>> {
8-
cli::run()
5+
fn main() {
6+
std::process::exit(
7+
// TODO use exitcode lib or something like that
8+
if let Err(err) = cli::run() {
9+
eprintln!("Error: {}", err);
10+
-1
11+
} else {
12+
// TODO use exit codes for warnings?
13+
0
14+
},
15+
)
916
}

src/parser.rs

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
use std::{error, fmt, str::FromStr};
22

33
use itertools::Itertools;
4-
use pest::{
5-
error::{Error as PestError, ErrorVariant, LineColLocation},
6-
iterators::{Pair, Pairs},
7-
};
4+
use pest::iterators::{Pair, Pairs};
85

96
use crate::load_file::{AddressMode, Core, Field, Instruction, Modifier, Opcode, Value};
107

118
mod grammar;
129

13-
#[derive(Debug)]
10+
#[derive(Debug, PartialEq)]
1411
pub struct Error {
1512
details: String,
1613
}
@@ -31,41 +28,39 @@ impl Error {
3128
}
3229
}
3330

34-
impl From<PestError<grammar::Rule>> for Error {
35-
fn from(pest_error: PestError<grammar::Rule>) -> Error {
31+
pub trait IntoError: fmt::Display {}
32+
33+
impl<T: pest::RuleType> IntoError for pest::error::Error<T> {}
34+
impl IntoError for String {}
35+
impl IntoError for &str {}
36+
37+
impl<T: IntoError> From<T> for Error {
38+
fn from(displayable_error: T) -> Self {
3639
Error {
37-
details: format!(
38-
"Parse error: {} {}",
39-
match pest_error.variant {
40-
ErrorVariant::ParsingError {
41-
positives,
42-
negatives,
43-
} => format!("expected one of {:?}, none of {:?}", positives, negatives),
44-
ErrorVariant::CustomError { message } => message,
45-
},
46-
match pest_error.line_col {
47-
LineColLocation::Pos((line, col)) => format!("at line {} column {}", line, col),
48-
LineColLocation::Span((start_line, start_col), (end_line, end_col)) => format!(
49-
"from line {} column {} to line {} column {}",
50-
start_line, start_col, end_line, end_col
51-
),
52-
}
53-
),
40+
details: format!("{}", displayable_error),
5441
}
5542
}
5643
}
5744

58-
impl From<String> for Error {
59-
fn from(details: String) -> Error {
60-
Error { details }
45+
#[derive(Debug)]
46+
pub struct ParsedCore {
47+
pub result: Core,
48+
pub warnings: Vec<Error>,
49+
}
50+
51+
impl fmt::Display for ParsedCore {
52+
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
53+
write!(formatter, "{}", self.result)
6154
}
6255
}
6356

64-
pub fn parse(file_contents: &str) -> Result<Core, Error> {
57+
pub fn parse(file_contents: &str) -> Result<ParsedCore, Error> {
6558
if file_contents.is_empty() {
6659
return Err(Error::no_input());
6760
}
6861

62+
let mut warnings = Vec::new();
63+
6964
let mut core = Core::default();
7065

7166
let parse_result = grammar::parse(grammar::Rule::File, file_contents)?
@@ -83,7 +78,9 @@ pub fn parse(file_contents: &str) -> Result<Core, Error> {
8378
.map(|pair| pair.as_str().to_owned());
8479

8580
for label in label_pairs {
86-
core.add_label(i, label.to_string())?;
81+
if let Err(failed_add) = core.add_label(i, label.to_string()) {
82+
warnings.push(failed_add.into())
83+
}
8784
}
8885

8986
if line_pair.peek().is_some() {
@@ -92,7 +89,10 @@ pub fn parse(file_contents: &str) -> Result<Core, Error> {
9289
}
9390
}
9491

95-
Ok(core)
92+
Ok(ParsedCore {
93+
result: core,
94+
warnings,
95+
})
9696
}
9797

9898
fn parse_instruction(mut instruction_pairs: Pairs<grammar::Rule>) -> Instruction {
@@ -188,10 +188,10 @@ mod tests {
188188

189189
#[test]
190190
fn parse_empty() {
191-
let result = parse("");
192-
assert!(result.is_err());
191+
let parsed = parse("");
192+
assert!(parsed.is_err());
193193

194-
assert_eq!(result.unwrap_err().details, "No input found");
194+
assert_eq!(parsed.unwrap_err().details, "No input found");
195195
}
196196

197197
#[test]
@@ -201,7 +201,14 @@ mod tests {
201201
label1 dat 0,0
202202
";
203203

204-
parse(simple_input).expect_err("Should fail for duplicate label");
204+
let parsed = parse(simple_input).expect("Failed to parse");
205+
206+
assert_eq!(
207+
parsed.warnings,
208+
vec![Error::from("Label 'label1' already exists")]
209+
);
210+
211+
assert_eq!(parsed.result.label_address("label1"), Some(1));
205212
}
206213

207214
#[test]
@@ -249,8 +256,10 @@ mod tests {
249256
.expect("Should resolve a core with no labels");
250257

251258
let mut parsed = parse(simple_input).expect("Should parse simple file");
252-
parsed.resolve().expect("Parsed file should resolve");
259+
parsed.result.resolve().expect("Parsed file should resolve");
260+
261+
assert!(parsed.warnings.is_empty());
253262

254-
assert_eq!(parsed, expected_core);
263+
assert_eq!(parsed.result, expected_core);
255264
}
256265
}

tests/dump_test.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ fn run_test(input: &str, expected_output: &str) {
44
eprintln!("Parsing warrior:");
55
eprintln!("{}", input);
66

7-
let mut core = corewa_rs::parse(input).expect("Failed to parse input");
8-
core.resolve().expect("Failed to resolve parsed input");
7+
let mut parsed_core = corewa_rs::parse(input).unwrap_or_else(|e| panic!("Parse error:\n{}", e));
8+
parsed_core
9+
.result
10+
.resolve()
11+
.unwrap_or_else(|e| panic!("{}", e));
912

1013
// TODO: dump and check output of pre-resolved core
1114

1215
eprintln!("Loaded core:");
13-
eprintln!("{:?}", core);
16+
dbg!(&parsed_core);
1417

15-
assert_eq!(format!("{}", core), expected_output);
18+
assert_eq!(format!("{}", parsed_core), expected_output);
1619
}
1720

1821
#[test]

0 commit comments

Comments
 (0)