mod args;
mod render;
use std::{
ffi::OsString,
io::{self, Write},
process::ExitCode,
};
use args::{Cli, Command};
use clap::{Parser, error::ErrorKind};
use render::{
RenderError, render_analysis, render_cluster, render_comparison_with_profile, render_inventory,
render_profiles, render_prosody,
};
pub fn run<I, T, W, E>(args: I, mut stdout: W, mut stderr: E) -> ExitCode
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
W: Write,
E: Write,
{
let cli = match Cli::try_parse_from(args) {
Ok(cli) => cli,
Err(error) => {
let code = if matches!(
error.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
0
} else {
2
};
let mut text = error.render().to_string();
if !text.ends_with('\n') {
text.push('\n');
}
let writer: &mut dyn Write = if error.use_stderr() {
&mut stderr
} else {
&mut stdout
};
return match writer.write_all(text.as_bytes()) {
Ok(()) => ExitCode::from(code),
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(_) => ExitCode::from(1),
};
}
};
match cli.command {
Command::Inventory { kind } => output_result(render_inventory(kind, &mut stdout), 1),
Command::Compare {
left,
right,
profile,
} => match render_comparison_with_profile(&left, &right, profile.into(), &mut stdout) {
Ok(()) => ExitCode::SUCCESS,
Err(RenderError::Io(error)) => output_result(Err(error), 1),
Err(RenderError::Parse(error)) => report_domain(&mut stderr, error),
Err(RenderError::Compare(error)) => report_domain(&mut stderr, error),
Err(RenderError::Analysis(error)) => report_domain(&mut stderr, error),
Err(RenderError::Cluster(error)) => report_domain(&mut stderr, error),
Err(RenderError::Stress(error)) => report_domain(&mut stderr, error),
Err(RenderError::Realization(error)) => report_domain(&mut stderr, error),
},
Command::Profiles => output_result(render_profiles(&mut stdout), 1),
Command::Analyze {
word,
kind,
syllabic_at,
} => match render_analysis(&word, kind, &syllabic_at, &mut stdout) {
Ok(()) => ExitCode::SUCCESS,
Err(RenderError::Io(error)) => output_result(Err(error), 1),
Err(RenderError::Analysis(error)) => report_domain(&mut stderr, error),
Err(RenderError::Cluster(error)) => report_domain(&mut stderr, error),
Err(RenderError::Parse(error)) => report_domain(&mut stderr, error),
Err(RenderError::Compare(error)) => report_domain(&mut stderr, error),
Err(RenderError::Stress(error)) => report_domain(&mut stderr, error),
Err(RenderError::Realization(error)) => report_domain(&mut stderr, error),
},
Command::Cluster {
cluster,
position,
kind,
} => match render_cluster(&cluster, position, kind, &mut stdout) {
Ok(()) => ExitCode::SUCCESS,
Err(RenderError::Io(error)) => output_result(Err(error), 1),
Err(RenderError::Cluster(error)) => report_domain(&mut stderr, error),
Err(RenderError::Analysis(error)) => report_domain(&mut stderr, error),
Err(RenderError::Parse(error)) => report_domain(&mut stderr, error),
Err(RenderError::Compare(error)) => report_domain(&mut stderr, error),
Err(RenderError::Stress(error)) => report_domain(&mut stderr, error),
Err(RenderError::Realization(error)) => report_domain(&mut stderr, error),
},
Command::Prosody {
word,
kind,
syllabic_at,
buffer_after,
syllables,
primary,
unstressed,
following,
} => match render_prosody(
render::ProsodyRequest {
input: &word,
kind,
syllabic_positions: &syllabic_at,
buffer_after: &buffer_after,
mode: syllables,
primary,
unstressed,
following,
},
&mut stdout,
) {
Ok(()) => ExitCode::SUCCESS,
Err(RenderError::Io(error)) => output_result(Err(error), 1),
Err(RenderError::Analysis(error)) => report_domain(&mut stderr, error),
Err(RenderError::Stress(error)) => report_domain(&mut stderr, error),
Err(RenderError::Realization(error)) => report_domain(&mut stderr, error),
Err(RenderError::Parse(error)) => report_domain(&mut stderr, error),
Err(RenderError::Compare(error)) => report_domain(&mut stderr, error),
Err(RenderError::Cluster(error)) => report_domain(&mut stderr, error),
},
}
}
fn output_result(result: io::Result<()>, failure_code: u8) -> ExitCode {
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(_) => ExitCode::from(failure_code),
}
}
fn report_domain<W: Write>(stderr: &mut W, error: impl std::fmt::Display) -> ExitCode {
match writeln!(stderr, "error: {error}") {
Ok(()) => ExitCode::from(2),
Err(error) if error.kind() == io::ErrorKind::BrokenPipe => ExitCode::SUCCESS,
Err(_) => ExitCode::from(1),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
struct BrokenWriter(io::ErrorKind);
impl Write for BrokenWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(self.0, "injected writer failure"))
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn non_broken_output_failure_returns_one() {
let mut stderr = Vec::new();
let code = run(
["bauske", "inventory"],
BrokenWriter(io::ErrorKind::Other),
&mut stderr,
);
assert_eq!(code, ExitCode::from(1));
}
#[test]
fn prosody_profiles_and_alternate_compare_writer_failures_return_one() {
for args in [
vec!["bauske", "prosody", "klama", "--kind", "brivla"],
vec!["bauske", "profiles"],
vec!["bauske", "compare", "e", "o", "--profile", "close-mid-e-v1"],
] {
let code = run(
args.iter().copied(),
BrokenWriter(io::ErrorKind::Other),
Vec::new(),
);
assert_eq!(code, ExitCode::from(1), "args={args:?}");
}
}
#[test]
fn broken_pipe_is_a_successful_exit() {
let code = run(
["bauske", "inventory"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(code, ExitCode::SUCCESS);
}
#[test]
fn help_writer_failure_is_handled() {
let code = run(
["bauske", "--help"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(code, ExitCode::SUCCESS);
}
#[test]
fn renderers_surface_io_failures_at_the_writer_boundary() {
let ordinary = crate::render::render_inventory(
crate::args::InventoryKind::Alphabet,
BrokenWriter(io::ErrorKind::Other),
)
.unwrap_err();
assert_eq!(ordinary.kind(), io::ErrorKind::Other);
let broken = crate::render::render_comparison_with_profile(
"p",
"b",
bauske_phonology::PronunciationProfile::RepresentativesV1,
BrokenWriter(io::ErrorKind::BrokenPipe),
)
.unwrap_err();
assert!(
matches!(broken, RenderError::Io(error) if error.kind() == io::ErrorKind::BrokenPipe)
);
}
#[test]
fn new_renderers_surface_io_failures_and_commands_handle_broken_pipe() {
let analysis = crate::render::render_analysis(
"ai",
crate::args::WordKindArg::Cmavo,
&[],
BrokenWriter(io::ErrorKind::Other),
)
.unwrap_err();
assert!(matches!(analysis, RenderError::Io(error) if error.kind() == io::ErrorKind::Other));
let cluster = crate::render::render_cluster(
"bf",
crate::args::ClusterPositionArg::Medial,
crate::args::WordKindArg::Brivla,
BrokenWriter(io::ErrorKind::BrokenPipe),
)
.unwrap_err();
assert!(
matches!(cluster, RenderError::Io(error) if error.kind() == io::ErrorKind::BrokenPipe)
);
let prosody = crate::render::render_prosody(
crate::render::ProsodyRequest {
input: "klama",
kind: crate::args::WordKindArg::Brivla,
syllabic_positions: &[],
buffer_after: &[],
mode: crate::args::SyllablesArg::Canonical,
primary: None,
unstressed: false,
following: crate::args::FollowingArg::Unspecified,
},
BrokenWriter(io::ErrorKind::Other),
)
.unwrap_err();
assert!(matches!(prosody, RenderError::Io(error) if error.kind() == io::ErrorKind::Other));
let profiles =
crate::render::render_profiles(BrokenWriter(io::ErrorKind::Other)).unwrap_err();
assert_eq!(profiles.kind(), io::ErrorKind::Other);
let comparison = crate::render::render_comparison_with_profile(
"p",
"b",
bauske_phonology::PronunciationProfile::RepresentativesV1,
BrokenWriter(io::ErrorKind::Other),
)
.unwrap_err();
assert!(
matches!(comparison, RenderError::Io(error) if error.kind() == io::ErrorKind::Other)
);
let analyze_code = run(
["bauske", "analyze", "ai", "--kind", "cmavo"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(analyze_code, ExitCode::SUCCESS);
let cluster_code = run(
[
"bauske",
"cluster",
"bf",
"--position",
"medial",
"--kind",
"brivla",
],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(cluster_code, ExitCode::SUCCESS);
let prosody_code = run(
["bauske", "prosody", "klama", "--kind", "brivla"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(prosody_code, ExitCode::SUCCESS);
let profiles_code = run(
["bauske", "profiles"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(profiles_code, ExitCode::SUCCESS);
let compare_code = run(
["bauske", "compare", "p", "b"],
BrokenWriter(io::ErrorKind::BrokenPipe),
Vec::new(),
);
assert_eq!(compare_code, ExitCode::SUCCESS);
}
#[test]
fn prosody_cli_reports_hint_resolution_and_realization_policy() {
let mut output = Vec::new();
let code = run(
[
"bauske",
"prosody",
"gas,tro",
"--kind",
"brivla",
"--syllables",
"hints",
],
&mut output,
Vec::new(),
);
assert_eq!(code, ExitCode::SUCCESS);
let output = String::from_utf8(output).unwrap();
assert!(output.contains("syllabification_resolved\ttrue"));
assert!(output.contains("realization_policy\tbuffer-plan-v1"));
for (word, expected) in [
("a,'i", "syllabification_resolved\ttrue"),
(
"a',i",
"syllable_limitation\tsyllables.hint-conflicts-with-apostrophe",
),
] {
let mut output = Vec::new();
let code = run(
[
"bauske",
"prosody",
word,
"--kind",
"cmavo",
"--syllables",
"hints",
],
&mut output,
Vec::new(),
);
assert_eq!(code, ExitCode::SUCCESS, "{word}");
assert!(
String::from_utf8(output).unwrap().contains(expected),
"{word}"
);
}
}
}