diff --git a/gumdrop_derive/src/lib.rs b/gumdrop_derive/src/lib.rs index 484cf3d..1c7e6ad 100644 --- a/gumdrop_derive/src/lib.rs +++ b/gumdrop_derive/src/lib.rs @@ -55,8 +55,13 @@ //! * `parse(from_str = "...")` for `fn(&str) -> T` //! * `parse(try_from_str = "...")` for //! `fn(&str) -> Result where E: Display` -//! * `parse(from_str)` uses `std::convert::From::from` -//! * `parse(try_from_str)` uses `std::str::FromStr::from_str` +//! * `parse(from_str)` uses `std::convert::From::<&str>::from` +//! * `parse(try_from_str)` uses `std::str::FromStr::from_str` (this is the +//! default when no parsing function is specified) +//! * `parse(from_os_str = "...")` for `fn(&OsStr) -> T` +//! * `parse(try_from_os_str = "...")` for +//! `fn(&OsStr) -> Result where E: Display` +//! * `parse(from_os_str)` uses `std::convert::From::<&OsStr>::from` //! //! The `options` attribute may also be added at the type level. //! @@ -229,13 +234,13 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum) Ok(quote!{ impl #impl_generics ::gumdrop::Options for #name #ty_generics #where_clause { - fn parse<__S: ::std::convert::AsRef>( + fn parse<__S: ::std::convert::AsRef<::std::ffi::OsStr>>( _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { let _arg = _parser.next_arg() .ok_or_else(::gumdrop::Error::missing_command)?; - Self::parse_command(_arg, _parser) + Self::parse_command(::gumdrop::to_str(_arg)?, _parser) } fn command(&self) -> ::std::option::Option<&dyn ::gumdrop::Options> { @@ -252,7 +257,7 @@ fn derive_options_enum(ast: &DeriveInput, data: &DataEnum) } } - fn parse_command<__S: ::std::convert::AsRef>(name: &str, + fn parse_command<__S: ::std::convert::AsRef<::std::ffi::OsStr>>(name: &str, _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { let cmd = match name { @@ -482,16 +487,18 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) if let Some(long) = &opt.long { let (pat, handle) = if let Some(n) = opt.action.tuple_len() { - (quote!{ ::gumdrop::Opt::LongWithArg(#long, _) }, + (quote!{ ::gumdrop::Opt::LongWithArg(_long, _) if _long == #long }, quote!{ return ::std::result::Result::Err( - ::gumdrop::Error::unexpected_single_argument(_opt, #n)) }) + ::gumdrop::Error::unexpected_single_argument(&_opt, #n)) }) } else if opt.action.takes_arg() { - (quote!{ ::gumdrop::Opt::LongWithArg(#long, _arg) }, - opt.make_action_arg()) + let mut handle = quote!{ let _arg: &::std::ffi::OsStr = _arg; }; + handle.extend(opt.make_action_arg()); + (quote!{ ::gumdrop::Opt::LongWithArg(_long, _arg) if _long == #long }, + handle) } else { - (quote!{ ::gumdrop::Opt::LongWithArg(#long, _) }, + (quote!{ ::gumdrop::Opt::LongWithArg(_long, _) if _long == #long }, quote!{ return ::std::result::Result::Err( - ::gumdrop::Error::unexpected_argument(_opt)) }) + ::gumdrop::Error::unexpected_argument(&_opt)) }) }; pattern.push(pat); @@ -577,7 +584,7 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) quote!{ #mark_used _result.#ident = ::std::option::Option::Some( - ::gumdrop::Options::parse_command(_free, _parser)?); + ::gumdrop::Options::parse_command(::gumdrop::to_str(_free)?, _parser)?); break; } } else { @@ -666,7 +673,7 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) Ok(quote!{ impl #impl_generics ::gumdrop::Options for #name #ty_generics #where_clause { - fn parse<__S: ::std::convert::AsRef>( + fn parse<__S: ::std::convert::AsRef<::std::ffi::OsStr>>( _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { #[derive(Default)] @@ -681,14 +688,15 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) let mut _used = _Used::default(); while let ::std::option::Option::Some(_opt) = _parser.next_opt() { - match _opt { + let _opt = _opt?; + match &_opt { #( #pattern => { #handle_opt } )* ::gumdrop::Opt::Free(_free) => { #handle_free } _ => { return ::std::result::Result::Err( - ::gumdrop::Error::unrecognized_option(_opt)); + ::gumdrop::Error::unrecognized_option(&_opt)); } } } @@ -712,7 +720,7 @@ fn derive_options_struct(ast: &DeriveInput, fields: &Fields) #help_requested_impl - fn parse_command<__S: ::std::convert::AsRef>(name: &str, + fn parse_command<__S: ::std::convert::AsRef<::std::ffi::OsStr>>(name: &str, _parser: &mut ::gumdrop::Parser<__S>) -> ::std::result::Result { ::std::result::Result::Err( @@ -838,6 +846,8 @@ enum ParseFn { Default, FromStr(Option), TryFromStr(Path), + FromOsStr(Option), + TryFromOsStr(Path), } struct ParseMethod { @@ -1474,6 +1484,7 @@ impl ParseFn { Some(ident) => match ident.to_string().as_str() { "from_str" => ParseFn::FromStr(None), "try_from_str" => ParseFn::Default, + "from_os_str" => ParseFn::FromOsStr(None), _ => return Err(unexpected_meta_item(ident.span())) } None => return Err(unexpected_meta_item(path.span())) @@ -1490,6 +1501,14 @@ impl ParseFn { let path = parse_str(&lit_str(&nv.lit)?)?; ParseFn::TryFromStr(path) } + "from_os_str" => { + let path = parse_str(&lit_str(&nv.lit)?)?; + ParseFn::FromOsStr(Some(path)) + } + "try_from_os_str" => { + let path = parse_str(&lit_str(&nv.lit)?)?; + ParseFn::TryFromOsStr(path) + } _ => return Err(unexpected_meta_item(nv.path.span())) } None => return Err(unexpected_meta_item(nv.path.span())) @@ -1512,17 +1531,28 @@ impl ParseFn { let res = match self { ParseFn::Default => quote!{ - ::std::str::FromStr::from_str(_arg) + ::std::str::FromStr::from_str(::gumdrop::to_str(_arg)?) .map_err(|e| ::gumdrop::Error::failed_parse_with_name( #name, ::std::string::ToString::to_string(&e)))? }, ParseFn::FromStr(None) => quote!{ - ::std::convert::From::from(_arg) + ::std::convert::From::from(::gumdrop::to_str(_arg)?) }, ParseFn::FromStr(Some(fun)) => quote!{ - #fun(_arg) + #fun(::gumdrop::to_str(_arg)?) }, ParseFn::TryFromStr(fun) => quote!{ + #fun(::gumdrop::to_str(_arg)?) + .map_err(|e| ::gumdrop::Error::failed_parse_with_name( + #name, ::std::string::ToString::to_string(&e)))? + }, + ParseFn::FromOsStr(None) => quote!{ + ::std::convert::From::from(_arg) + }, + ParseFn::FromOsStr(Some(fun)) => quote!{ + #fun(_arg) + }, + ParseFn::TryFromOsStr(fun) => quote!{ #fun(_arg) .map_err(|e| ::gumdrop::Error::failed_parse_with_name( #name, ::std::string::ToString::to_string(&e)))? @@ -1551,6 +1581,18 @@ impl ParseFn { .map_err(|e| ::gumdrop::Error::failed_parse_default( stringify!(#ident), #expr, ::std::string::ToString::to_string(&e)))? + }, + ParseFn::FromOsStr(None) => quote!{ + ::std::convert::From::from(::std::ffi::OsStr::new(#expr)) + }, + ParseFn::FromOsStr(Some(fun)) => quote!{ + #fun(::std::ffi::OsStr::new(#expr)) + }, + ParseFn::TryFromOsStr(fun) => quote!{ + #fun(::std::ffi::OsStr::new(#expr)) + .map_err(|e| ::gumdrop::Error::failed_parse_default( + stringify!(#ident), #expr, + ::std::string::ToString::to_string(&e)))? } }; @@ -1571,7 +1613,7 @@ impl ParseMethod { match self.tuple_len { None => quote!{ { let _arg = _parser.next_arg() - .ok_or_else(|| ::gumdrop::Error::missing_argument(_opt))?; + .ok_or_else(|| ::gumdrop::Error::missing_argument(&_opt))?; #parse } }, @@ -1585,7 +1627,7 @@ impl ParseMethod { let _found = #num; let _arg = _parser.next_arg() .ok_or_else(|| ::gumdrop::Error::insufficient_arguments( - _opt, #n, _found))?; + &_opt, #n, _found))?; #parse } , )* ) diff --git a/src/lib.rs b/src/lib.rs index 0627a9b..bf6bf29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -140,6 +140,8 @@ //! //! ``` //! use gumdrop::Options; +//! use std::path::PathBuf; +//! use std::fs::{self, Metadata}; //! //! #[derive(Debug, Options)] //! struct MyOptions { @@ -149,6 +151,13 @@ //! // `from_str = "..."` supplies a conversion function that always succeeds //! #[options(help = "a string that becomes uppercase", parse(from_str = "to_upper"))] //! upper: String, +//! // `from_os_str` can be used if the argument may contain invalid UTF-8 +//! #[options(help = "a path that may not be valid UTF-8", parse(from_os_str))] +//! path: PathBuf, +//! // `try_from_os_str = "..."` and `from_os_str = "..."` are akin to the named +//! // functions above but allow for arguments containing invalid UTF-8 +//! #[options(help = "metadata of a file", parse(try_from_os_str = "fs::metadata"))] +//! bytes: Option, //! } //! //! fn parse_hex(s: &str) -> Result { @@ -170,7 +179,9 @@ pub use gumdrop_derive::*; +use std::borrow::Cow; use std::error::Error as StdError; +use std::ffi::OsStr; use std::fmt; use std::slice::Iter; use std::str::Chars; @@ -194,6 +205,7 @@ enum ErrorKind { expected: usize, found: usize, }, + InvalidUtf8(String), MissingArgument(String), MissingCommand, MissingRequired(String), @@ -216,16 +228,16 @@ pub struct Parser<'a, S: 'a> { } /// Represents an option parsed from a `Parser` -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Opt<'a> { /// Short option, e.g. `-o` Short(char), /// Long option, e.g. `--option` Long(&'a str), /// Long option with argument, e.g. `--option=value` - LongWithArg(&'a str, &'a str), + LongWithArg(Cow<'a, str>, Cow<'a, OsStr>), /// Free argument - Free(&'a str), + Free(&'a OsStr), } /// Implements a set of options parsed from command line arguments. @@ -234,7 +246,7 @@ pub enum Opt<'a> { pub trait Options { /// Parses arguments until the given parser is exhausted or until /// an error is encountered. - fn parse>(parser: &mut Parser) -> Result where Self: Sized; + fn parse>(parser: &mut Parser) -> Result where Self: Sized; /// Returns the subcommand instance, if present. /// @@ -262,7 +274,7 @@ pub trait Options { /// Parses arguments received from the command line. /// /// The first argument (the program name) should be omitted. - fn parse_args>(args: &[S], style: ParsingStyle) -> Result + fn parse_args>(args: &[S], style: ParsingStyle) -> Result where Self: Sized { Self::parse(&mut Parser::new(args, style)) } @@ -277,13 +289,14 @@ pub trait Options { /// /// Otherwise, the parsed options are returned. fn parse_args_or_exit(style: ParsingStyle) -> Self where Self: Sized { - use std::env::args; + use std::env::args_os; use std::process::exit; - let args = args().collect::>(); + let args = args_os().collect::>(); + let program_name = args[0].to_string_lossy(); let opts = Self::parse_args(&args[1..], style).unwrap_or_else(|e| { - eprintln!("{}: {}", args[0], e); + eprintln!("{}: {}", program_name, e); exit(2); }); @@ -304,7 +317,7 @@ pub trait Options { } } - println!("Usage: {}{} [OPTIONS]", args[0], command_str); + println!("Usage: {}{} [OPTIONS]", program_name, command_str); println!(); println!("{}", command.self_usage()); @@ -338,12 +351,12 @@ pub trait Options { /// using the default [parsing style](enum.ParsingStyle.html). /// /// The first argument (the program name) should be omitted. - fn parse_args_default>(args: &[S]) -> Result where Self: Sized { + fn parse_args_default>(args: &[S]) -> Result where Self: Sized { Self::parse(&mut Parser::new(args, ParsingStyle::default())) } /// Parses options for the named command. - fn parse_command>(name: &str, parser: &mut Parser) -> Result where Self: Sized; + fn parse_command>(name: &str, parser: &mut Parser) -> Result where Self: Sized; /// Returns a string showing usage and help for each supported option. /// @@ -437,9 +450,30 @@ pub enum ParsingStyle { StopAtFirstFree, } +/// Argument after parsing from `OsStr`, similar to `Opt` but for parsing a +/// single argument which may contain multiple short options or only part of a +/// long option. +enum Arg<'a> { + DoubleDash, + Long(&'a str), + LongWithArg(Cow<'a, str>, Cow<'a, OsStr>), + Short(Chars<'a>), + Free(&'a OsStr), +} + +/// Extension of `OsStr` that uses platform-specific behavior to parse it as an +/// argument. +/// +/// Not on all platforms can `--valid-utf8=invalid-utf8` be transformed into +/// `Ok(Arg::Long(_, _))`; `Error::InvalidUtf8(_)` will be returned instead. +trait OsStrArg { + /// Parse `OsStr` as an argument + fn arg(&self) -> Result; +} + impl Error { /// Returns an error for a failed attempt at parsing an option value. - pub fn failed_parse(opt: Opt, err: String) -> Error { + pub fn failed_parse(opt: &Opt, err: String) -> Error { Error{kind: ErrorKind::FailedParse(opt.to_string(), err)} } @@ -456,7 +490,7 @@ impl Error { /// Returns an error for an option expecting two or more arguments not /// receiving the expected number of arguments. - pub fn insufficient_arguments(opt: Opt, expected: usize, found: usize) -> Error { + pub fn insufficient_arguments(opt: &Opt, expected: usize, found: usize) -> Error { Error{kind: ErrorKind::InsufficientArguments{ option: opt.to_string(), expected: expected, @@ -464,9 +498,14 @@ impl Error { }} } + /// Returns an error for an argument being invalid UTF-8. + pub fn invalid_utf8(invalid: String) -> Error { + Error{kind: ErrorKind::InvalidUtf8(invalid)} + } + /// Returns an error for an option receiving an unexpected argument value, /// e.g. `--option=value`. - pub fn unexpected_argument(opt: Opt) -> Error { + pub fn unexpected_argument(opt: &Opt) -> Error { Error{kind: ErrorKind::UnexpectedArgument(opt.to_string())} } @@ -474,12 +513,12 @@ impl Error { /// receiving only one in the long form, e.g. `--option=value`. /// /// These options must be passed as, e.g. `--option value second-value [...]`. - pub fn unexpected_single_argument(opt: Opt, n: usize) -> Error { + pub fn unexpected_single_argument(opt: &Opt, n: usize) -> Error { Error{kind: ErrorKind::UnexpectedSingleArgument(opt.to_string(), n)} } /// Returns an error for a missing required argument. - pub fn missing_argument(opt: Opt) -> Error { + pub fn missing_argument(opt: &Opt) -> Error { Error{kind: ErrorKind::MissingArgument(opt.to_string())} } @@ -505,8 +544,8 @@ impl Error { /// Returns an error when a free argument was encountered, but the options /// type does not support free arguments. - pub fn unexpected_free(arg: &str) -> Error { - Error{kind: ErrorKind::UnexpectedFree(arg.to_owned())} + pub fn unexpected_free(arg: &OsStr) -> Error { + Error{kind: ErrorKind::UnexpectedFree(arg.to_string_lossy().into_owned())} } /// Returns an error for an unrecognized command. @@ -515,11 +554,11 @@ impl Error { } /// Returns an error for an unrecognized option. - pub fn unrecognized_option(opt: Opt) -> Error { + pub fn unrecognized_option(opt: &Opt) -> Error { match opt { - Opt::Short(short) => Error::unrecognized_short(short), - Opt::Long(long) | Opt::LongWithArg(long, _) => - Error::unrecognized_long(long), + Opt::Short(short) => Error::unrecognized_short(*short), + Opt::Long(long) => Error::unrecognized_long(long), + Opt::LongWithArg(long, _) => Error::unrecognized_long(long), Opt::Free(_) => panic!("`Error::unrecognized_option` called with `Opt::Free` value") } } @@ -545,6 +584,7 @@ impl fmt::Display for Error { InsufficientArguments{option, expected, found} => write!(f, "insufficient arguments to option `{}`: expected {}; found {}", option, expected, found), + InvalidUtf8(s) => write!(f, "invalid UTF-8: {}", s), MissingArgument(opt) => write!(f, "missing argument to option `{}`", opt), MissingCommand => f.write_str("missing command name"), MissingRequired(opt) => write!(f, "missing required option `{}`", opt), @@ -567,7 +607,7 @@ impl StdError for Error { } } -impl<'a, S: 'a + AsRef> Parser<'a, S> { +impl<'a, S: 'a + AsRef> Parser<'a, S> { /// Returns a new parser for the given series of arguments. /// /// The given slice should **not** contain the program name as its first @@ -582,61 +622,49 @@ impl<'a, S: 'a + AsRef> Parser<'a, S> { } /// Returns the next option or `None` if no options remain. - pub fn next_opt(&mut self) -> Option> { + pub fn next_opt(&mut self) -> Option, Error>> { if let Some(mut cur) = self.cur.take() { if let Some(opt) = cur.next() { self.cur = Some(cur); - return Some(Opt::Short(opt)); + return Some(Ok(Opt::Short(opt))); } } if self.terminated { - return self.args.next().map(|s| Opt::Free(s.as_ref())); + return self.args.next().map(|s| Ok(Opt::Free(s.as_ref()))); } - match self.args.next().map(|s| s.as_ref()) { - Some(arg @ "-") => { - if self.style == ParsingStyle::StopAtFirstFree { - self.terminated = true; - } - Some(Opt::Free(arg)) - } - Some("--") => { + match self.args.next().map(|s| s.as_ref().arg()) { + Some(Ok(Arg::DoubleDash)) => { self.terminated = true; - self.args.next().map(|s| Opt::Free(s.as_ref())) - } - Some(long) if long.starts_with("--") => { - match long.find('=') { - Some(pos) => Some(Opt::LongWithArg( - &long[2..pos], &long[pos + 1..])), - None => Some(Opt::Long(&long[2..])) - } + self.args.next().map(|s| Ok(Opt::Free(s.as_ref()))) } - Some(short) if short.starts_with('-') => { - let mut chars = short[1..].chars(); - - let res = chars.next().map(Opt::Short); + Some(Ok(Arg::Long(opt))) => Some(Ok(Opt::Long(opt))), + Some(Ok(Arg::LongWithArg(opt, val))) => Some(Ok(Opt::LongWithArg(opt, val))), + Some(Ok(Arg::Short(mut chars))) => { + let res = chars.next().map(Opt::Short).map(Ok); self.cur = Some(chars); res } - Some(free) => { + Some(Ok(Arg::Free(free))) => { if self.style == ParsingStyle::StopAtFirstFree { self.terminated = true; } - Some(Opt::Free(free)) + Some(Ok(Opt::Free(free))) } + Some(Err(err)) => Some(Err(err)), None => None } } /// Returns the next argument to an option or `None` if none remain. - pub fn next_arg(&mut self) -> Option<&'a str> { + pub fn next_arg(&mut self) -> Option<&'a OsStr> { if let Some(cur) = self.cur.take() { let arg = cur.as_str(); if !arg.is_empty() { - return Some(arg); + return Some(arg.as_ref()); } } @@ -658,7 +686,7 @@ impl<'a, S: 'a> Clone for Parser<'a, S> { impl<'a> Opt<'a> { #[doc(hidden)] pub fn to_string(&self) -> String { - match *self { + match self { Opt::Short(ch) => format!("-{}", ch), Opt::Long(s) => format!("--{}", s), Opt::LongWithArg(opt, _) => format!("--{}", opt), @@ -674,6 +702,140 @@ impl Default for ParsingStyle { } } +#[cfg(any(unix, all(target_vendor = "fortanix", target_env = "sgx"), + target_os = "hermit", target_os = "redox", target_os = "vxworks", target_os = "wasi"))] +impl OsStrArg for OsStr { + fn arg(&self) -> Result { + /// Try to convert `&[u8]` (byte-representation of `&OsStr`) to `&str`. + fn bytes_to_str(s: &[u8]) -> Result<&str, Error> { + cow_to_str(String::from_utf8_lossy(s)) + } + + #[cfg(any(unix, target_os = "hermit", target_os = "redox", target_os = "vxworks"))] + use std::os::unix::ffi::OsStrExt; + #[cfg(all(target_vendor = "fortanix", target_env = "sgx"))] + use std::os::fortanix_sgx::ffi::OsStrExt; + #[cfg(target_os = "wasi")] + use std::os::wasi::ffi::OsStrExt; + + let bytes = self.as_bytes(); + match bytes { + b"-" => Ok(Arg::Free(self)), + b"--" => Ok(Arg::DoubleDash), + long if long.starts_with(b"--") => { + let bytes = &bytes[2..]; + match bytes.iter().position(|&x| x == b'=') { + Some(pos) => { + let (opt, arg) = bytes.split_at(pos); + Ok(Arg::LongWithArg(bytes_to_str(opt)?.into(), + OsStr::from_bytes(&arg[1..]).into())) + } + None => Ok(Arg::Long(bytes_to_str(bytes)?)), + } + }, + short if short.starts_with(b"-") => arg_short(self), + _free => Ok(Arg::Free(self)), + } + } +} + +#[cfg(windows)] +impl OsStrArg for OsStr { + fn arg(&self) -> Result { + use std::ffi::OsString; + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + let mut wide = self.encode_wide(); + match wide.next() { + // b'-' as u16 == 0x002D + Some(0x002D) => match wide.next() { + Some(0x002D) => match wide.next() { + Some(ch) => match self.to_string_lossy() { + Cow::Owned(s) => { + if s.contains('=') { + // b'=' as u16 == 0x003D + let opt = if ch == 0x003D { + "".into() + } else { + let mut opt = vec![ch]; + opt.extend(wide.by_ref().take_while(|&x| x != 0x003D)); + OsString::from_wide(&opt) + .into_string() + .map_err(|s| Error::invalid_utf8(s.to_string_lossy().into()))? + .into() + }; + let arg = OsString::from_wide(&wide.collect::>()); + Ok(Arg::LongWithArg(opt, arg.into())) + } else { + Err(Error::invalid_utf8(s)) + } + } + Cow::Borrowed(s) => { + match s.find('=') { + Some(pos) => Ok(Arg::LongWithArg( + s[2..pos].into(), OsStr::new(&s[pos + 1..]).into())), + None => Ok(Arg::Long(&s[2..])), + } + } + } + None => Ok(Arg::DoubleDash), + }, + Some(_) => arg_short(self), + None => Ok(Arg::Free(self)), + }, + _ => Ok(Arg::Free(self)), + } + } +} + +#[cfg(not(any(unix, windows, all(target_vendor = "fortanix", target_env = "sgx"), + target_os = "hermit", target_os = "redox", target_os = "vxworks", target_os = "wasi")))] +impl OsStrArg for OsStr { + /// For options like `--valid-utf8=invalid-utf8` `Error::InvalidUtf8` is + /// returned as `OsStr` cannot be split or truncated without a platform-specific + /// `OsStrExt`. + fn arg(&self) -> Result { + let lossy = self.to_string_lossy(); + match lossy.as_ref() { + "-" => Ok(Arg::Free(self)), + "--" => Ok(Arg::DoubleDash), + long if long.starts_with("--") => match lossy { + Cow::Owned(s) => Err(Error::invalid_utf8(s)), + Cow::Borrowed(s) => { + match s.find('=') { + Some(pos) => Ok(Arg::LongWithArg( + s[2..pos].into(), OsStr::new(&s[pos + 1..]).into())), + None => Ok(Arg::Long(&s[2..])), + } + } + } + short if short.starts_with('-') => arg_short(self), + _free => Ok(Arg::Free(self)), + } + } +} + +/// Parse short argument. +fn arg_short(s: &OsStr) -> Result { + Ok(Arg::Short(to_str(s)?[1..].chars())) +} + +/// Try to convert `&OsStr` to `&str`. +#[doc(hidden)] +pub fn to_str(s: &OsStr) -> Result<&str, Error> { + cow_to_str(s.to_string_lossy()) +} + +/// Try to convert `Cow` to `&str`, assuming that an owned `Cow` means that some +/// characters had to be replaced with the replacement character due to invalid +/// UTF-8. +fn cow_to_str(s: Cow) -> Result<&str, Error> { + match s { + Cow::Owned(s) => Err(Error::invalid_utf8(s)), + Cow::Borrowed(s) => Ok(s), + } +} + /// Parses arguments from the command line. /// /// The first argument (the program name) should be omitted. @@ -728,6 +890,7 @@ pub fn parse_args_default_or_exit() -> T { mod test { use super::{Opt, Parser, ParsingStyle}; use assert_matches::assert_matches; + use std::ffi::OsStr; #[test] fn test_parser() { @@ -736,18 +899,18 @@ mod test { let mut p = Parser::new(args, ParsingStyle::AllOptions); - assert_matches!(p.next_opt(), Some(Opt::Short('a'))); - assert_matches!(p.next_opt(), Some(Opt::Free("b"))); - assert_matches!(p.next_opt(), Some(Opt::Short('c'))); - assert_matches!(p.next_opt(), Some(Opt::Short('d'))); - assert_matches!(p.next_opt(), Some(Opt::Short('e'))); - assert_matches!(p.next_arg(), Some("arg")); - assert_matches!(p.next_opt(), Some(Opt::Short('x'))); - assert_matches!(p.next_arg(), Some("foo")); - assert_matches!(p.next_opt(), Some(Opt::Long("long"))); - assert_matches!(p.next_opt(), Some(Opt::LongWithArg("opt", "val"))); - assert_matches!(p.next_opt(), Some(Opt::Free("y"))); - assert_matches!(p.next_opt(), Some(Opt::Free("-z"))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('a')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(b))) if b == "b"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('c')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('d')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('e')))); + assert_matches!(p.next_arg(), Some(arg) if arg == "arg"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('x')))); + assert_matches!(p.next_arg(), Some(foo) if foo == "foo"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Long("long")))); + assert_matches!(p.next_opt(), Some(Ok(Opt::LongWithArg(ref opt, ref val))) if opt == "opt" && val == OsStr::new("val")); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(y))) if y == "y"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(z))) if z == "-z"); assert_matches!(p.next_opt(), None); } @@ -757,18 +920,69 @@ mod test { let mut p = Parser::new(args, ParsingStyle::AllOptions); - assert_matches!(p.next_opt(), Some(Opt::Short('a'))); - assert_matches!(p.next_opt(), Some(Opt::Free("b"))); - assert_matches!(p.next_opt(), Some(Opt::Short('c'))); - assert_matches!(p.next_opt(), Some(Opt::Long("d"))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('a')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(b))) if b == "b"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('c')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Long("d")))); assert_matches!(p.next_opt(), None); let mut p = Parser::new(args, ParsingStyle::StopAtFirstFree); - assert_matches!(p.next_opt(), Some(Opt::Short('a'))); - assert_matches!(p.next_opt(), Some(Opt::Free("b"))); - assert_matches!(p.next_opt(), Some(Opt::Free("-c"))); - assert_matches!(p.next_opt(), Some(Opt::Free("--d"))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Short('a')))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(b))) if b == "b"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(c))) if c == "-c"); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(d))) if d == "--d"); + assert_matches!(p.next_opt(), None); + } + + #[cfg(unix)] + #[test] + fn test_os_str() { + use super::{Error, ErrorKind}; + use std::os::unix::ffi::OsStrExt; + + let invalid = [b'-', b'-', b'f', b'o', 0x80, b'o']; + let invalid_long = OsStr::from_bytes(&invalid[..]); + let invalid_short = OsStr::from_bytes(&invalid[1..]); + let valid_free = OsStr::from_bytes(&invalid[2..]); + let invalid = [b'-', b'-', b'f', b'=', b'o', 0x80, b'o']; + let valid_long = OsStr::from_bytes(&invalid[..]); + let valid_arg = OsStr::from_bytes(&invalid[4..]); + + let args = &[invalid_long, invalid_short, valid_free, valid_long]; + + let mut p = Parser::new(args, ParsingStyle::AllOptions); + + assert_matches!(p.next_opt(), Some(Err(Error{kind: ErrorKind::InvalidUtf8(_)}))); + assert_matches!(p.next_opt(), Some(Err(Error{kind: ErrorKind::InvalidUtf8(_)}))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(free))) if free == valid_free); + assert_matches!(p.next_opt(), Some(Ok(Opt::LongWithArg(ref f, ref arg))) if f == "f" && arg == valid_arg); + assert_matches!(p.next_opt(), None); + } + + #[cfg(windows)] + #[test] + fn test_os_str() { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use super::{Error, ErrorKind}; + + let invalid = [0x002D, 0x002D, 0x0066, 0x006F, 0xD800, 0x006F]; + let invalid_long = OsString::from_wide(&invalid[..]); + let invalid_short = OsString::from_wide(&invalid[1..]); + let valid_free = OsString::from_wide(&invalid[2..]); + let invalid = [0x002D, 0x002D, 0x0066, 0x003D, 0x006F, 0xD800, 0x006F]; + let valid_long = OsString::from_wide(&invalid[..]); + let valid_arg = OsString::from_wide(&invalid[4..]); + + let args = &[&invalid_long, &invalid_short, &valid_free, &valid_long]; + + let mut p = Parser::new(args, ParsingStyle::AllOptions); + + assert_matches!(p.next_opt(), Some(Err(Error{kind: ErrorKind::InvalidUtf8(_)}))); + assert_matches!(p.next_opt(), Some(Err(Error{kind: ErrorKind::InvalidUtf8(_)}))); + assert_matches!(p.next_opt(), Some(Ok(Opt::Free(free))) if free == valid_free); + assert_matches!(p.next_opt(), Some(Ok(Opt::LongWithArg(ref f, ref arg))) if f == "f" && arg == &valid_arg); assert_matches!(p.next_opt(), None); } } diff --git a/tests/options.rs b/tests/options.rs index fb51ec4..7dc964f 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -1,4 +1,5 @@ use std::str::FromStr; +use std::ffi::OsString; use assert_matches::assert_matches; @@ -958,6 +959,9 @@ fn test_required_help() { #[test] fn test_parse() { + use std::ffi::OsStr; + use std::path::{Path, PathBuf}; + #[derive(Options)] struct Opts { #[options(help = "foo", parse(from_str = "parse_foo"))] @@ -968,6 +972,12 @@ fn test_parse() { baz: Option, #[options(help = "quux", parse(try_from_str))] quux: Option, + #[options(help = "foo-os", parse(from_os_str = "parse_foo_os"))] + foo_os: Option, + #[options(help = "bar-os", parse(try_from_os_str = "parse_bar_os"))] + bar_os: Option, + #[options(help = "baz-os", parse(from_os_str))] + baz_os: Option, } #[derive(Debug)] @@ -978,9 +988,20 @@ fn test_parse() { struct Baz(String); #[derive(Debug)] struct Quux(u32); + #[derive(Debug)] + struct FooOs(OsString); + #[derive(Debug)] + struct BarOs(PathBuf); + #[derive(Debug)] + struct BazOs(OsString); fn parse_foo(s: &str) -> Foo { Foo(s.to_owned()) } fn parse_bar(s: &str) -> Result::Err> { s.parse().map(Bar) } + fn parse_foo_os(s: &OsStr) -> FooOs { FooOs(s.to_owned()) } + fn parse_bar_os(s: &OsStr) -> Result { + let path = Path::new(s); + path.extension().map(|_| BarOs(path.to_owned())).ok_or("extension required") + } impl<'a> From<&'a str> for Baz { fn from(s: &str) -> Baz { @@ -996,17 +1017,30 @@ fn test_parse() { } } + impl<'a> From<&'a OsStr> for BazOs { + fn from(s: &OsStr) -> BazOs { + BazOs(s.to_owned()) + } + } + let opts = Opts::parse_args_default(&[ - "-ffoo", "--bar=123", "--baz", "sup", "-q", "456"]).unwrap(); + "-ffoo", "--bar=123", "--baz", "sup", "-q", "456", + "-Ffoo_os", "--bar-os=path.txt", "--baz-os", "supoz" + ]).unwrap(); assert_matches!(opts.foo, Some(Foo(ref s)) if s == "foo"); assert_matches!(opts.bar, Some(Bar(123))); assert_matches!(opts.baz, Some(Baz(ref s)) if s == "sup"); assert_matches!(opts.quux, Some(Quux(456))); + assert_matches!(opts.foo_os, Some(FooOs(ref s)) if s == "foo_os"); + assert_matches!(opts.bar_os, Some(BarOs(ref s)) if s.as_os_str() == "path.txt"); + assert_matches!(opts.baz_os, Some(BazOs(ref s)) if s == "supoz"); is_err!(Opts::parse_args_default(&["--bar", "xyz"]), |e| e.starts_with("invalid argument to option `--bar`: ")); is_err!(Opts::parse_args_default(&["--quux", "xyz"]), |e| e.starts_with("invalid argument to option `--quux`: ")); + is_err!(Opts::parse_args_default(&["--bar-os", "no_ext"]), + |e| e.starts_with("invalid argument to option `--bar-os`: ")); } #[test] @@ -1020,6 +1054,8 @@ fn test_default() { baz: Baz, #[options(count, default = "789")] count: u32, + #[options(default = "os", parse(from_os_str))] + foo_os: OsString, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -1038,12 +1074,14 @@ fn test_default() { assert_eq!(opts.bar, 123); assert_eq!(opts.baz, Baz(456)); assert_eq!(opts.count, 789); + assert_eq!(opts.foo_os, "os"); - let opts = Opts::parse_args_default(&["-b99", "--baz=4387", "-c", "-f1"]).unwrap(); + let opts = Opts::parse_args_default(&["-b99", "--baz=4387", "-c", "-f1", "-Fq"]).unwrap(); assert_eq!(opts.foo, 1); assert_eq!(opts.bar, 99); assert_eq!(opts.baz, Baz(4387)); assert_eq!(opts.count, 790); + assert_eq!(opts.foo_os, "q"); } #[test]