use std::ffi::OsString;
use std::fmt;
use std::path::PathBuf;
pub const USAGE: &str = "\
usage: nm [file] open file in the editor (no file: resume the last session)
nm eval evaluate stdin, print one JSON object per line
nm -h|--help show this help
Use `nm ./eval` to open a file named eval, and `nm -- -file` for a file
whose name starts with `-`.";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Command {
Edit(Option<PathBuf>),
Eval,
Help,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgsError {
UnknownOption(String),
UnexpectedArgument(String),
}
impl fmt::Display for ArgsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownOption(o) => write!(f, "unknown option: {o}"),
Self::UnexpectedArgument(a) => write!(f, "unexpected argument: {a}"),
}
}
}
impl std::error::Error for ArgsError {}
pub fn parse_args<I>(args: I) -> Result<Command, ArgsError>
where
I: IntoIterator<Item = OsString>,
{
let mut positional: Vec<OsString> = Vec::new();
let mut options_done = false;
for arg in args {
if !options_done && let Some(text) = arg.to_str() {
match text {
"--" => {
options_done = true;
continue;
}
"-h" | "--help" => return Ok(Command::Help),
_ if text.starts_with('-') && text != "-" => {
return Err(ArgsError::UnknownOption(text.to_string()));
}
_ => {}
}
}
positional.push(arg);
}
let mut rest = positional.into_iter();
let Some(first) = rest.next() else {
return Ok(Command::Edit(None));
};
if let Some(extra) = rest.next() {
return Err(ArgsError::UnexpectedArgument(
extra.to_string_lossy().into_owned(),
));
}
if !options_done && first == "eval" {
return Ok(Command::Eval);
}
Ok(Command::Edit(Some(PathBuf::from(first))))
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> Result<Command, ArgsError> {
parse_args(args.iter().map(OsString::from))
}
fn edit(path: &str) -> Result<Command, ArgsError> {
Ok(Command::Edit(Some(PathBuf::from(path))))
}
#[test]
fn no_arguments_is_session_mode() {
assert_eq!(parse(&[]), Ok(Command::Edit(None)));
}
#[test]
fn file_argument() {
assert_eq!(parse(&["budget.nm"]), edit("budget.nm"));
assert_eq!(parse(&["~/notes/a.nm"]), edit("~/notes/a.nm"));
assert_eq!(parse(&["-"]), edit("-"));
}
#[test]
fn eval_subcommand() {
assert_eq!(parse(&["eval"]), Ok(Command::Eval));
assert_eq!(parse(&["./eval"]), edit("./eval"));
assert_eq!(parse(&["--", "eval"]), edit("eval"));
}
#[test]
fn help() {
assert_eq!(parse(&["-h"]), Ok(Command::Help));
assert_eq!(parse(&["--help"]), Ok(Command::Help));
assert_eq!(parse(&["a.nm", "--help"]), Ok(Command::Help));
assert_eq!(parse(&["--", "--help"]), edit("--help"));
}
#[test]
fn errors() {
assert_eq!(
parse(&["--bogus"]),
Err(ArgsError::UnknownOption("--bogus".into()))
);
assert_eq!(
parse(&["a.nm", "b.nm"]),
Err(ArgsError::UnexpectedArgument("b.nm".into()))
);
assert_eq!(
parse(&["eval", "x"]),
Err(ArgsError::UnexpectedArgument("x".into()))
);
}
#[cfg(unix)]
#[test]
fn non_utf8_file_name() {
use std::os::unix::ffi::OsStringExt;
let name = OsString::from_vec(vec![b'a', 0xff, b'.', b'n', b'm']);
assert_eq!(
parse_args([name.clone()]),
Ok(Command::Edit(Some(PathBuf::from(name))))
);
}
}