Skip to main content

simpleargs/
arg.rs

1//! Low-level argument parsing.
2
3use std::ffi::{OsStr, OsString};
4
5/// Trait for string types that can be parsed as command-line arguments.
6pub trait ArgString: Sized {
7    /// Parse the string as a command-line argument.
8    ///
9    /// On failure, return the input.
10    fn parse_arg(self) -> Result<ParsedArg<Self>, Self>;
11
12    /// Convert the argument into a str if it is a valid Unicode string.
13    fn to_str(&self) -> Option<&str>;
14
15    /// Convert the argument into an OsStr.
16    fn to_osstr(&self) -> &OsStr;
17}
18
19fn is_arg_name(c: char) -> bool {
20    match c {
21        'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => true,
22        _ => false,
23    }
24}
25
26impl ArgString for String {
27    fn parse_arg(self) -> Result<ParsedArg<String>, String> {
28        let mut chars = self.chars();
29        match chars.next() {
30            Some('-') => (),
31            _ => return Ok(ParsedArg::Positional(self)),
32        }
33        let cur = chars.clone();
34        match chars.next() {
35            Some('-') => {
36                if chars.as_str().is_empty() {
37                    return Ok(ParsedArg::EndOfFlags);
38                }
39            }
40            Some(_) => chars = cur,
41            None => return Ok(ParsedArg::Positional(self)),
42        }
43        let body = chars.as_str();
44        let (name, value) = match body.find('=') {
45            Some(idx) => (&body[..idx], Some(&body[idx + 1..])),
46            None => (body, None),
47        };
48        if name.is_empty() || !name.chars().all(is_arg_name) {
49            return Err(self);
50        }
51        Ok(ParsedArg::Named(name.to_owned(), value.map(str::to_owned)))
52    }
53
54    fn to_str(&self) -> Option<&str> {
55        Some(self)
56    }
57
58    fn to_osstr(&self) -> &OsStr {
59        self.as_ref()
60    }
61}
62
63impl ArgString for OsString {
64    fn parse_arg(self) -> Result<ParsedArg<OsString>, OsString> {
65        use std::os::unix::ffi::{OsStrExt, OsStringExt};
66        let bytes = self.as_bytes();
67        if bytes.len() < 2 || bytes[0] != b'-' {
68            return Ok(ParsedArg::Positional(self));
69        }
70        let body = if bytes[1] != b'-' {
71            &bytes[1..]
72        } else if bytes.len() == 2 {
73            return Ok(ParsedArg::EndOfFlags);
74        } else {
75            &bytes[2..]
76        };
77        let (name, value) = match body.iter().position(|&c| c == b'=') {
78            None => (body, None),
79            Some(idx) => (&body[..idx], Some(&body[idx + 1..])),
80        };
81        if name.len() == 0
82            || name[0] == b'-'
83            || name[name.len() - 1] == b'-'
84            || !name.iter().all(|&c| is_arg_name(c as char))
85        {
86            return Err(self);
87        }
88        let name = Vec::from(name);
89        let name = unsafe { String::from_utf8_unchecked(name) };
90        let value = value.map(|v| OsString::from_vec(Vec::from(v)));
91        Ok(ParsedArg::Named(name, value))
92    }
93
94    fn to_str(&self) -> Option<&str> {
95        OsStr::to_str(self)
96    }
97
98    fn to_osstr(&self) -> &OsStr {
99        self
100    }
101}
102
103/// A single command-line argument which has been parsed.
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum ParsedArg<T> {
106    /// A positional argument.
107    Positional(T),
108    /// The "--" argument.
109    EndOfFlags,
110    /// A named option, such as "-opt" or "-opt=value".
111    ///
112    /// The leading dashes are removed from the name.
113    Named(String, Option<T>),
114}
115
116impl<T> ParsedArg<T> {
117    /// Map a `ParsedArg<T>` to a `ParsedArg<U>` by applying a function to the inner value.
118    pub fn map<U, F>(self, f: F) -> ParsedArg<U>
119    where
120        F: FnOnce(T) -> U,
121    {
122        match self {
123            ParsedArg::Positional(x) => ParsedArg::Positional(f(x)),
124            ParsedArg::EndOfFlags => ParsedArg::EndOfFlags,
125            ParsedArg::Named(x, y) => ParsedArg::Named(x, y.map(f)),
126        }
127    }
128}
129
130#[cfg(test)]
131mod test {
132    use super::*;
133    use std::ffi::OsStr;
134    use std::fmt::Debug;
135    use std::os::unix::ffi::OsStrExt;
136
137    fn osstr(s: &[u8]) -> OsString {
138        OsString::from(OsStr::from_bytes(s))
139    }
140
141    struct Case<T>(T, ParsedArg<T>);
142
143    impl<T> Case<T> {
144        fn map<F, U>(self, f: F) -> Case<U>
145        where
146            F: Fn(T) -> U,
147        {
148            let Case(input, output) = self;
149            Case(f(input), output.map(f))
150        }
151    }
152
153    impl<T: Debug + Clone + ArgString + PartialEq<T>> Case<T> {
154        fn test(&self) -> bool {
155            let Case(input, expected) = self;
156            match input.clone().parse_arg() {
157                Ok(arg) => {
158                    if &arg != expected {
159                        eprintln!(
160                            "{:?}.parse_arg(): got {:?}, expect {:?}",
161                            input, expected, arg
162                        );
163                        false
164                    } else {
165                        true
166                    }
167                }
168                Err(_) => {
169                    eprintln!("{:?}.parse_arg(): got error, expect {:?}", input, expected);
170                    false
171                }
172            }
173        }
174    }
175
176    fn success_cases() -> Vec<Case<String>> {
177        let mut cases = vec![
178            Case("abc", ParsedArg::Positional("abc")),
179            Case("", ParsedArg::Positional("")),
180            Case("-", ParsedArg::Positional("-")),
181            Case("--", ParsedArg::EndOfFlags),
182            Case("-a", ParsedArg::Named("a".to_owned(), None)),
183            Case("--a", ParsedArg::Named("a".to_owned(), None)),
184            Case("-a=", ParsedArg::Named("a".to_owned(), Some(""))),
185            Case("--a=", ParsedArg::Named("a".to_owned(), Some(""))),
186            Case("--arg-name", ParsedArg::Named("arg-name".to_owned(), None)),
187            Case("--ARG_NAME", ParsedArg::Named("ARG_NAME".to_owned(), None)),
188            Case(
189                "--opt=value",
190                ParsedArg::Named("opt".to_owned(), Some("value")),
191            ),
192        ];
193        cases.drain(..).map(|c| c.map(str::to_owned)).collect()
194    }
195
196    struct Fail<T>(T);
197
198    impl<T: Debug + Clone + ArgString + PartialEq<T>> Fail<T> {
199        fn test(&self) -> bool {
200            let Fail(input) = self;
201            match input.clone().parse_arg() {
202                Ok(arg) => {
203                    eprintln!("{:?}.parse_arg(): got {:?}, expect error", input, arg);
204                    false
205                }
206                Err(e) => {
207                    if &e != input {
208                        eprintln!(
209                            "{:?}.parse_arg(): got error {:?}, expect error {:?}",
210                            input, e, input
211                        );
212                        false
213                    } else {
214                        true
215                    }
216                }
217            }
218        }
219    }
220
221    const FAIL_CASES: &'static [&'static str] =
222        &["-\0", "--\n", "--\0=", "-=", "--=", "-=value", "--=xyz"];
223
224    #[test]
225    fn parse_string_success() {
226        let mut success = true;
227        for case in success_cases().drain(..) {
228            if !case.test() {
229                success = false;
230            }
231        }
232        if !success {
233            panic!("failed");
234        }
235    }
236
237    #[test]
238    fn parse_osstring_success() {
239        let mut success = true;
240        let mut cases: Vec<Case<OsString>> = success_cases()
241            .drain(..)
242            .map(|c| c.map(OsString::from))
243            .collect();
244        cases.push(Case(
245            osstr(b"\x80\xff"),
246            ParsedArg::Positional(osstr(b"\x80\xff")),
247        ));
248        cases.push(Case(
249            osstr(b"--opt=\xff"),
250            ParsedArg::Named("opt".to_owned(), Some(osstr(b"\xff"))),
251        ));
252        for case in cases.drain(..) {
253            if !case.test() {
254                success = false;
255            }
256        }
257        if !success {
258            panic!("failed");
259        }
260    }
261
262    #[test]
263    fn parse_string_failure() {
264        let mut success = true;
265        for &input in FAIL_CASES.iter() {
266            if !Fail(input.to_owned()).test() {
267                success = false;
268            }
269        }
270        if !success {
271            panic!("failed");
272        }
273    }
274
275    #[test]
276    fn parse_osstring_failure() {
277        let mut success = true;
278        let mut cases: Vec<OsString> = FAIL_CASES
279            .iter()
280            .map(|&s| OsString::from(s.to_owned()))
281            .collect();
282        for input in cases.drain(..) {
283            if !Fail(input).test() {
284                success = false;
285            }
286        }
287        if !success {
288            panic!("failed");
289        }
290    }
291}