Skip to content

Commit 82892fa

Browse files
Merge pull request #10 from ian-h-chamberlain/cli-improvements
Cli improvements
2 parents a973065 + 3b5aa17 commit 82892fa

File tree

9 files changed

+869
-232
lines changed

9 files changed

+869
-232
lines changed

Cargo.lock

Lines changed: 584 additions & 117 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,10 @@ itertools = "0.8.0"
99
lazy_static = "1.2.0"
1010
pest_derive = "2.1.0"
1111
pest = "2.1.0"
12-
structopt = "0.2.15"
12+
structopt = "0.3.5"
13+
14+
[dev-dependencies]
15+
assert_cmd = "0.11.1"
16+
predicates = "1.0.2"
17+
assert_fs = "0.13.1"
18+
indoc = "0.3.4"

src/cli.rs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,80 @@
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

8+
use lazy_static::lazy_static;
39
use structopt::StructOpt;
410

511
use crate::parser;
612

13+
lazy_static! {
14+
static ref IO_SENTINEL: PathBuf = PathBuf::from("-");
15+
}
16+
717
#[derive(Debug, StructOpt)]
8-
/// Parse and save Redcode files
18+
#[structopt(rename_all = "kebab")]
19+
/// Parse, assemble, and save Redcode files
920
struct CliOptions {
10-
/// Input file
21+
/// Input file; use '-' to read from stdin
1122
#[structopt(parse(from_os_str))]
1223
input_file: PathBuf,
1324

14-
/// Output file; defaults to stdout
15-
#[structopt(long, short, parse(from_os_str))]
16-
output_file: Option<PathBuf>,
25+
#[structopt(subcommand)]
26+
command: Command,
27+
}
28+
29+
#[derive(Debug, StructOpt)]
30+
enum Command {
31+
/// Save/print a program in 'load file' format
32+
#[structopt(name = "dump")]
33+
Dump {
34+
/// Output file; defaults to stdout ('-')
35+
#[structopt(long, short, parse(from_os_str), default_value = IO_SENTINEL.to_str().unwrap())]
36+
output_file: PathBuf,
37+
38+
/// Whether labels, expressions, macros, etc. should be resolved and
39+
/// expanded in the output
40+
#[structopt(long, short = "E")]
41+
no_expand: bool,
42+
},
1743
}
1844

1945
pub fn run() -> Result<(), Box<dyn Error>> {
2046
let cli_options = CliOptions::from_args();
2147

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

24-
let parsed_input = parser::parse(input_program.as_str())?;
25-
let parse_output = parsed_input.dump();
26-
27-
if let Some(output_path) = cli_options.output_file {
28-
fs::write(output_path, parse_output)?;
50+
if cli_options.input_file == *IO_SENTINEL {
51+
io::stdin().read_to_string(&mut input)?;
2952
} else {
30-
println!("{}", parse_output);
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+
}
62+
63+
match cli_options.command {
64+
Command::Dump {
65+
output_file,
66+
no_expand,
67+
} => {
68+
if !no_expand {
69+
parsed_core.result.resolve()?;
70+
}
71+
72+
if output_file == *IO_SENTINEL {
73+
print!("{}", parsed_core);
74+
} else {
75+
fs::write(output_file, format!("{}", parsed_core))?;
76+
};
77+
}
3178
};
3279

3380
Ok(())

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ extern crate pest_derive;
44

55
// Extern crates
66
extern crate itertools;
7+
extern crate lazy_static;
78
extern crate pest;
89
extern crate structopt;
910

src/load_file.rs

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
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};
85

96
pub const DEFAULT_CORE_SIZE: usize = 8000;
107

11-
type InstructionSet = Box<[Option<Instruction>]>;
8+
type Instructions = Box<[Option<Instruction>]>;
129
type LabelMap = HashMap<String, usize>;
1310

1411
#[derive(Clone, Debug, PartialEq)]
@@ -109,14 +106,15 @@ impl Instruction {
109106
}
110107

111108
pub struct Core {
112-
instructions: InstructionSet,
109+
instructions: Instructions,
110+
resolved: Option<Instructions>,
113111
labels: LabelMap,
114112
}
115113

116114
impl PartialEq for Core {
117-
// TODO: should this impl resolve the instructions? Depends on the use case
118115
fn eq(&self, other: &Self) -> bool {
119-
self.instructions == other.instructions
116+
(self.resolved.is_some() && self.resolved == other.resolved)
117+
|| self.instructions == other.instructions
120118
}
121119
}
122120

@@ -128,35 +126,35 @@ impl Default for Core {
128126

129127
impl fmt::Debug for Core {
130128
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
131-
write!(
132-
formatter,
133-
"Labels: {:?}\nCore:\n{}",
134-
self.labels,
135-
self.dump()
136-
)
129+
if self.resolved.is_some() {
130+
write!(formatter, "{}", self)
131+
} else {
132+
write!(formatter, "<unresolved core>")
133+
}
137134
}
138135
}
139136

140137
impl Core {
141138
pub fn new(core_size: usize) -> Self {
142139
Core {
143140
instructions: vec![None; core_size].into_boxed_slice(),
144-
labels: HashMap::new(),
141+
resolved: None,
142+
labels: LabelMap::new(),
145143
}
146144
}
147145

148146
pub fn get(&self, index: usize) -> Option<Instruction> {
149-
self.instructions.get(index)?.clone()
150-
}
151-
152-
pub fn get_resolved(&self, index: usize) -> Result<Instruction, String> {
153-
self.get(index)
154-
.unwrap_or_default()
155-
.resolve(index, &self.labels)
147+
match &self.resolved {
148+
Some(instructions) => instructions,
149+
None => &self.instructions,
150+
}
151+
.get(index)?
152+
.clone()
156153
}
157154

158155
pub fn set(&mut self, index: usize, value: Instruction) {
159156
self.instructions[index] = Some(value);
157+
// TODO need to re-resolve here? Or make caller do it
160158
}
161159

162160
pub fn add_label(&mut self, index: usize, label: String) -> Result<(), String> {
@@ -168,21 +166,19 @@ impl Core {
168166
));
169167
}
170168

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

180176
pub fn label_address(&self, label: &str) -> Option<usize> {
181177
self.labels.get(label).copied()
182178
}
183179

184-
pub fn resolve(&self) -> Result<Self, String> {
185-
let instructions = self
180+
pub fn resolve(&mut self) -> Result<&mut Self, String> {
181+
let resolved = self
186182
.instructions
187183
.iter()
188184
.enumerate()
@@ -192,29 +188,29 @@ impl Core {
192188
.map(|instruction| instruction.resolve(i, &self.labels))
193189
.transpose()
194190
})
195-
.collect::<Result<InstructionSet, String>>()?;
191+
.collect::<Result<_, String>>()?;
196192

197-
Ok(Self {
198-
instructions,
199-
..Default::default()
200-
})
193+
self.resolved = Some(resolved);
194+
195+
Ok(self)
201196
}
197+
}
202198

203-
pub fn dump(&self) -> String {
204-
// TODO: convert to fmt::Display - this will require some upfront
205-
// validation that all labels are valid, etc
206-
// It may be desirable to have Debug be a dump() of the load file and
207-
// Display show the original parsed document (or something like that)
208-
match self.resolve() {
209-
Err(msg) => msg,
210-
Ok(core) => core
211-
.instructions
212-
.iter()
213-
.filter_map(Option::as_ref)
214-
.fold(String::new(), |result, instruction| {
215-
result + &instruction.to_string() + "\n"
216-
}),
217-
}
199+
impl fmt::Display for Core {
200+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
201+
write!(
202+
f,
203+
"{}",
204+
match &self.resolved {
205+
None => &self.instructions,
206+
Some(instructions) => instructions,
207+
}
208+
.iter()
209+
.filter_map(Option::as_ref)
210+
.fold(String::new(), |result, instruction| {
211+
result + &instruction.to_string() + "\n"
212+
})
213+
)
218214
}
219215
}
220216

@@ -251,11 +247,13 @@ mod tests {
251247
core.add_label(256, "goblin".into())
252248
.expect_err("Should fail to add labels > 200");
253249
core.add_label(5, "baz".into())
254-
.expect_err("Should fail to add duplicate label");
250+
.expect_err("Should error duplicate label");
255251

256252
assert_eq!(core.label_address("foo").unwrap(), 0);
257253
assert_eq!(core.label_address("bar").unwrap(), 0);
258-
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);
259257

260258
assert!(core.label_address("goblin").is_none());
261259
assert!(core.label_address("never_mentioned").is_none());
@@ -303,12 +301,10 @@ mod tests {
303301
},
304302
);
305303

306-
let resolved_core = core.resolve().expect("Should resolve all labels in core");
304+
core.resolve().expect("Should resolve all labels in core");
307305

308306
assert_eq!(
309-
resolved_core
310-
.get(3)
311-
.expect("Should have instruction at pos 5"),
307+
core.get(3).expect("Should have instruction at pos 5"),
312308
Instruction {
313309
field_a: Field {
314310
value: Value::Literal(4),

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
}

0 commit comments

Comments
 (0)