use std::{ffi::OsString, path::PathBuf};
use anyhow::{Context, Result, bail};
#[derive(Debug, PartialEq, Eq)]
pub enum Action {
Open,
Build,
Languages,
Check,
}
#[derive(Debug)]
pub struct Options {
pub action: Action,
pub language: Option<String>,
pub manifest_path: Option<PathBuf>,
pub package: Option<String>,
pub offline: bool,
pub locked: bool,
}
#[derive(Debug)]
pub enum Invocation {
Help,
Run(Options),
}
pub fn parse(args: impl IntoIterator<Item = OsString>) -> Result<Invocation> {
let mut args = args.into_iter().peekable();
if args.peek().is_some_and(|arg| arg == "textus") {
args.next();
}
let args: Vec<_> = args.collect();
if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
return Ok(Invocation::Help);
}
let mut args = args.into_iter();
let action = match args.next().as_deref().and_then(|arg| arg.to_str()) {
Some("open") => Action::Open,
Some("build") => Action::Build,
Some("languages") => Action::Languages,
Some("check") => Action::Check,
Some("i18n" | "render") => bail!(
"i18n/render commands were removed; use cargo textus build, open, check, or languages; see --help"
),
Some("list") => bail!("list was replaced by cargo textus languages; see --help"),
_ => bail!("expected build, open, check, or languages; see --help"),
};
let mut options = Options {
action,
language: None,
manifest_path: None,
package: None,
offline: false,
locked: false,
};
while let Some(arg) = args.next() {
match arg.to_str() {
Some("--lang") => {
let value = string_value(&mut args, "--lang")?;
if options.language.is_none() {
options.language = Some(value);
} else {
bail!("--lang may only be specified once");
}
}
Some("--manifest-path") => {
let value = args.next().context("--manifest-path requires a path")?;
if options.manifest_path.is_none() {
options.manifest_path = Some(value.into());
} else {
bail!("--manifest-path may only be specified once");
}
}
Some("--package" | "-p") => {
let value = string_value(&mut args, "--package")?;
if options.package.is_none() {
options.package = Some(value);
} else {
bail!("--package may only be specified once");
}
}
Some("--open") => bail!("--open was removed; use cargo textus open instead"),
Some("--offline") => options.offline = true,
Some("--locked") => options.locked = true,
_ => bail!("unknown argument {arg:?}; see --help"),
}
}
if options.action == Action::Languages && options.language.is_some() {
bail!("languages does not accept --lang; it lists registered languages");
}
if let Some(language) = &options.language {
textus_core::i18n::validate_language(language).map_err(anyhow::Error::msg)?;
}
Ok(Invocation::Run(options))
}
fn string_value(args: &mut impl Iterator<Item = OsString>, flag: &str) -> Result<String> {
args.next()
.with_context(|| format!("{flag} requires a value"))?
.into_string()
.map_err(|_| anyhow::anyhow!("{flag} requires a UTF-8 value"))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_words(words: &str) -> Result<Invocation> {
parse(words.split_whitespace().map(OsString::from))
}
#[test]
fn accepts_task_commands_with_optional_language() {
for prefix in ["", "textus "] {
for (command, expected) in [
("build", Action::Build),
("open", Action::Open),
("check", Action::Check),
] {
let Invocation::Run(options) = parse_words(&format!(
"{prefix}{command} --lang ko -p demo --offline --locked"
))
.unwrap() else {
panic!("expected command");
};
assert_eq!(options.action, expected);
assert_eq!(options.language.as_deref(), Some("ko"));
assert_eq!(options.package.as_deref(), Some("demo"));
assert!(options.offline && options.locked);
let Invocation::Run(options) = parse_words(&format!("{prefix}{command}")).unwrap()
else {
panic!("expected command");
};
assert!(options.language.is_none());
}
assert!(matches!(
parse_words(&format!("{prefix}languages")).unwrap(),
Invocation::Run(Options {
action: Action::Languages,
..
})
));
}
}
#[test]
fn rejects_removed_commands_and_invalid_options() {
for words in [
"i18n open --lang ko",
"render build",
"list",
"build --open",
"open --open",
"build --lang zz",
"build --lang ko-KR",
"build --lang",
"languages --lang ko",
"check --unknown",
"check --lang ko --lang en",
"build -p a -p b",
"build --manifest-path a --manifest-path b",
"build --manifest-path",
"build -p",
"doc",
] {
assert!(parse_words(words).is_err(), "{words}");
}
assert!(
parse_words("build --open")
.unwrap_err()
.to_string()
.contains("cargo textus open")
);
}
#[test]
fn help_does_not_require_a_project_or_valid_command() {
for words in ["", "textus", "--help", "build -h", "i18n --help"] {
assert!(matches!(parse_words(words).unwrap(), Invocation::Help));
}
}
}