Skip to content

Commit 4599537

Browse files
committed
support stdin, error codes
1 parent ae3cd37 commit 4599537

File tree

11 files changed

+214
-45
lines changed

11 files changed

+214
-45
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [v0.14.0] - 2022-04-11
4+
5+
### Features
6+
7+
- Support stdin in CLI
8+
- Emit proper exit codes on specific errors
9+
310
## [v0.13.10] - 2022-03-11
411

512
### Fixes

Cargo.lock

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/melody_cli/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ colored = "2.0.0"
1717
thiserror = "1.0"
1818
anyhow = "1.0"
1919
melody_compiler = { version = "0.13.10", path = "../melody_compiler" }
20+
atty = "0.2"
21+
exitcode = "1.1.2"
2022

2123
[dev-dependencies]
2224
assert_cmd = "2.0.4"

crates/melody_cli/src/compile.rs

+22-7
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
1+
use crate::consts::STDIN_MARKER;
12
use crate::errors::CliError;
23
use crate::output::print_output;
34
use crate::utils::write_output_to_file;
45
use melody_compiler::compiler;
56
use std::fs::read_to_string;
7+
use std::io::{self, Read};
68

7-
pub fn compile_file(
8-
input_file_path: String,
9-
output_file_path: Option<String>,
10-
) -> anyhow::Result<()> {
11-
let source = read_to_string(input_file_path.clone())
12-
.map_err(|_| CliError::ReadFileError(input_file_path))?;
9+
fn read_stdin() -> anyhow::Result<String> {
10+
let mut buffer = String::new();
11+
io::stdin()
12+
.read_to_string(&mut buffer)
13+
.map_err(|_| CliError::ReadStdinError)?;
14+
Ok(buffer)
15+
}
16+
17+
fn read_file(path: &str) -> anyhow::Result<String> {
18+
let contents = read_to_string(path).map_err(|_| CliError::ReadFileError(path.to_owned()))?;
19+
Ok(contents)
20+
}
21+
22+
pub fn compile_file(input_file_path: &str, output_file_path: Option<String>) -> anyhow::Result<()> {
23+
let source = if input_file_path == STDIN_MARKER {
24+
read_stdin()?
25+
} else {
26+
read_file(input_file_path)?
27+
};
1328

1429
let compiler_output =
1530
compiler(&source).map_err(|error| CliError::ParseError(error.to_string()))?;
1631

1732
match output_file_path {
18-
Some(output_file_path) => write_output_to_file(output_file_path, compiler_output)?,
33+
Some(output_file_path) => write_output_to_file(&output_file_path, &compiler_output)?,
1934
None => print_output(&compiler_output),
2035
};
2136

crates/melody_cli/src/consts.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pub(crate) const COMMAND_MARKER: &str = ":";
2+
pub(crate) const STDIN_MARKER: &str = "-";

crates/melody_cli/src/errors.rs

+68-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
use crate::output::report_unhandled_error;
2+
use crate::{report_error, report_info, Args};
3+
use clap::CommandFactory;
4+
use std::process;
15
use thiserror::Error;
26

37
#[derive(Error, Debug)]
48
pub enum CliError {
5-
#[error("invalid arguments, please supply a path argument or use --repl")]
6-
MissingPath,
79
#[error("unable read file at path {0}")]
810
ReadFileError(String),
911
#[error("{0}")]
@@ -12,4 +14,68 @@ pub enum CliError {
1214
WriteFileError(String),
1315
#[error("unable to read input")]
1416
ReadInputError,
17+
#[error("unable to read stdin")]
18+
ReadStdinError,
19+
#[error("repl argument supplied with piped input or output")]
20+
ReplWithPipe,
21+
#[error("No input file supplied and no input piped.\nTry adding a path argument: 'melody ./file.mdy'")]
22+
StdinWithoutPipe,
23+
}
24+
25+
#[derive(Debug)]
26+
pub enum ErrorKind {
27+
Info,
28+
Error,
29+
}
30+
31+
impl CliError {
32+
pub fn kind(&self) -> ErrorKind {
33+
match self {
34+
CliError::StdinWithoutPipe => ErrorKind::Info,
35+
_ => ErrorKind::Error,
36+
}
37+
}
38+
39+
fn report(&self) {
40+
match self.kind() {
41+
ErrorKind::Info => {
42+
report_info(&self.to_string());
43+
println!();
44+
// silently ignoring an error when printing help
45+
// as we're already handling an error and have printed info
46+
let _print_result = Args::command().print_help();
47+
}
48+
ErrorKind::Error => {
49+
report_error(&self.to_string());
50+
}
51+
};
52+
}
53+
54+
fn to_exit_code(&self) -> exitcode::ExitCode {
55+
match self {
56+
CliError::WriteFileError(_)
57+
| CliError::ReadFileError(_)
58+
| CliError::ReadInputError
59+
| CliError::ReadStdinError => exitcode::IOERR,
60+
CliError::ParseError(_) => exitcode::DATAERR,
61+
CliError::ReplWithPipe => exitcode::USAGE,
62+
CliError::StdinWithoutPipe => exitcode::NOINPUT,
63+
}
64+
}
65+
}
66+
67+
pub fn handle_error(error: &anyhow::Error) -> ! {
68+
let cli_error = error.downcast_ref::<CliError>();
69+
70+
let cli_error = match cli_error {
71+
Some(cli_error) => cli_error,
72+
None => {
73+
report_unhandled_error(&error.to_string());
74+
process::exit(exitcode::SOFTWARE);
75+
}
76+
};
77+
78+
cli_error.report();
79+
80+
process::exit(cli_error.to_exit_code())
1581
}

crates/melody_cli/src/main.rs

+24-13
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,20 @@ mod utils;
1414
use clap::Parser;
1515
use colored::control::{ShouldColorize, SHOULD_COLORIZE};
1616
use compile::compile_file;
17-
use errors::CliError;
18-
use output::report_error;
17+
use consts::STDIN_MARKER;
18+
use errors::{handle_error, CliError};
19+
use output::{report_error, report_info};
1920
use repl::repl;
2021
use std::process;
21-
use types::{Args, ExitCode};
22+
use types::{Args, Streams};
2223

2324
fn main() {
2425
ShouldColorize::from_env();
2526

26-
let exit_code = match try_main() {
27-
Ok(_) => ExitCode::Ok,
28-
Err(error) => {
29-
report_error(&error.to_string());
30-
ExitCode::Error
31-
}
27+
match try_main() {
28+
Ok(_) => process::exit(exitcode::OK),
29+
Err(error) => handle_error(&error),
3230
};
33-
34-
process::exit(exit_code.into());
3531
}
3632

3733
fn try_main() -> anyhow::Result<()> {
@@ -46,13 +42,28 @@ fn try_main() -> anyhow::Result<()> {
4642
SHOULD_COLORIZE.set_override(false);
4743
}
4844

45+
let input_file_path = input_file_path.unwrap_or_else(|| STDIN_MARKER.to_owned());
46+
47+
argument_env_validation(start_repl, &input_file_path)?;
48+
4949
if start_repl {
5050
return repl();
5151
}
5252

53-
let input_file_path = input_file_path.ok_or(CliError::MissingPath)?;
53+
compile_file(&input_file_path, output_file_path)?;
5454

55-
compile_file(input_file_path, output_file_path)?;
55+
Ok(())
56+
}
57+
58+
fn argument_env_validation(start_repl: bool, input_file_path: &str) -> anyhow::Result<()> {
59+
let streams = Streams::new();
60+
61+
if streams.any_pipe() && start_repl {
62+
return Err(CliError::ReplWithPipe.into());
63+
}
64+
if !streams.stdin && input_file_path == STDIN_MARKER {
65+
return Err(CliError::StdinWithoutPipe.into());
66+
}
5667

5768
Ok(())
5869
}

crates/melody_cli/src/output.rs

+16-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,22 @@ pub fn report_source() {
9999
}
100100

101101
pub fn report_error(error: &str) {
102-
eprintln!("{}", format!("Error: {}", error).bright_red(),);
102+
eprintln!("{}", format!("Error: {}", error).bright_red());
103+
}
104+
105+
pub fn report_unhandled_error(error: &str) {
106+
eprintln!(
107+
"{}",
108+
format!(
109+
"An unhandled error occured.\nThis is a bug, please open an issue.\n\nCause: {}",
110+
error
111+
)
112+
.bright_red()
113+
);
114+
}
115+
116+
pub fn report_info(error: &str) {
117+
eprintln!("{}", error.bright_blue());
103118
}
104119

105120
pub fn report_repl_error(error: &str) {

crates/melody_cli/src/types.rs

+24-15
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
1+
use atty::Stream;
12
use clap::Parser;
23

3-
pub enum ExitCode {
4-
Ok,
5-
Error,
6-
}
7-
8-
impl From<ExitCode> for i32 {
9-
fn from(exit_code: ExitCode) -> Self {
10-
match exit_code {
11-
ExitCode::Ok => 0,
12-
ExitCode::Error => 1,
13-
}
14-
}
15-
}
16-
174
#[derive(Parser, Debug)]
185
#[clap(about, version, author)]
196
pub struct Args {
20-
#[clap(value_name = "INPUT_FILE_PATH", help = "Read from a file")]
7+
#[clap(
8+
value_name = "INPUT_FILE_PATH",
9+
help = "Read from a file. Use '-' and or pipe input to read from stdin"
10+
)]
2111
pub input_file_path: Option<String>,
2212
#[clap(
2313
short = 'o',
@@ -41,3 +31,22 @@ pub enum NextLoop {
4131
Continue,
4232
Exit,
4333
}
34+
35+
pub struct Streams {
36+
pub stdin: bool,
37+
pub stdout: bool,
38+
// pub stderr: bool,
39+
}
40+
41+
impl Streams {
42+
pub fn new() -> Self {
43+
Self {
44+
stdin: !atty::is(Stream::Stdin),
45+
stdout: !atty::is(Stream::Stdout),
46+
// stderr: !atty::is(Stream::Stderr),
47+
}
48+
}
49+
pub fn any_pipe(&self) -> bool {
50+
self.stdin || self.stdout
51+
}
52+
}

crates/melody_cli/src/utils.rs

+3-6
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ pub fn read_input() -> io::Result<String> {
1010
Ok(String::from(input.trim_end()))
1111
}
1212

13-
pub fn write_output_to_file(
14-
output_file_path: String,
15-
compiler_output: String,
16-
) -> anyhow::Result<()> {
17-
write(&output_file_path, compiler_output)
18-
.map_err(|_| CliError::WriteFileError(output_file_path))?;
13+
pub fn write_output_to_file(output_file_path: &str, compiler_output: &str) -> anyhow::Result<()> {
14+
write(output_file_path, compiler_output)
15+
.map_err(|_| CliError::WriteFileError(output_file_path.to_owned()))?;
1916

2017
Ok(())
2118
}

crates/melody_cli/tests/mod.rs

+39-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use unindent::unindent;
88

99
#[test]
1010
#[cfg_attr(miri, ignore)]
11-
fn cli_stdout_test() -> anyhow::Result<()> {
11+
fn cli_file_stdout_test() -> anyhow::Result<()> {
1212
let mut command = Command::cargo_bin("melody")?;
1313
let melody_file = NamedTempFile::new("test.mdy")?;
1414

@@ -48,6 +48,44 @@ fn cli_stdout_test() -> anyhow::Result<()> {
4848
Ok(())
4949
}
5050

51+
#[test]
52+
#[cfg_attr(miri, ignore)]
53+
fn cli_stdin_stdout_test() -> anyhow::Result<()> {
54+
let mut command = Command::cargo_bin("melody")?;
55+
56+
let source = r#"
57+
some of "a";
58+
some of "b";
59+
"#;
60+
61+
let expected_output = "a+b+";
62+
63+
command
64+
.write_stdin(source)
65+
.arg("-")
66+
.assert()
67+
.stdout(expected_output);
68+
69+
Ok(())
70+
}
71+
72+
#[test]
73+
#[cfg_attr(miri, ignore)]
74+
fn cli_stdin_stdout_no_hyphen_test() -> anyhow::Result<()> {
75+
let mut command = Command::cargo_bin("melody")?;
76+
77+
let source = r#"
78+
some of "a";
79+
some of "b";
80+
"#;
81+
82+
let expected_output = "a+b+";
83+
84+
command.write_stdin(source).assert().stdout(expected_output);
85+
86+
Ok(())
87+
}
88+
5189
#[test]
5290
#[cfg_attr(miri, ignore)]
5391
fn cli_file_test() -> anyhow::Result<()> {

0 commit comments

Comments
 (0)