shellish_parse/lib.rs
1//! This is a Rust crate to do "command line parsing". No, I'm not talking
2//! about parsing command line *arguments* that were passed to your program;
3//! for that purpose, I recommend the excellent [Clap][1] crate (with features
4//! `wrap_help` and `derive` enabled). What this crate does is take a *line of
5//! text* and parse it like a command line. In other words, it parses shellish.
6//!
7//! This is useful if you're implementing any kind of interactive system where
8//! a user needs to be able to input commands.
9//!
10//! [1]: https://crates.io/crates/clap
11//!
12//! # Usage
13//!
14//! Add `shellish_parse` to your `Cargo.toml`:
15//!
16//! ```toml
17//! shellish_parse = "2.2"
18//! ```
19//!
20//! Use `shellish_parse::parse` to parse some shellish:
21//!
22//! ```rust
23//! let line = "Hello World";
24//! assert_eq!(shellish_parse::parse(line, false).unwrap(), &[
25//! "Hello", "World"
26//! ]);
27//! ```
28//!
29//! The first parameter, a `&str`, is the line to parse. The second parameter,
30//! a can be a `bool`, indicating whether an unrecognized escape sequence
31//! should be an error:
32//!
33//! ```rust
34//! let line = r#"In\mvalid"#; // note: raw string
35//! assert_eq!(shellish_parse::parse(line, false).unwrap(), &[
36//! "In�valid"
37//! ]);
38//! assert_eq!(shellish_parse::parse(line, true).unwrap_err(),
39//! shellish_parse::ParseError::UnrecognizedEscape("\\m".to_string()));
40//! ```
41//!
42//! Or a [`ParseOptions`](struct.ParseOptions.html), giving you more control
43//! (see that struct's documentation for more details):
44//!
45//! ```rust
46//! # use shellish_parse::ParseOptions;
47//! let line = r#"In\mvalid"#; // note: raw string
48//! let options = ParseOptions::new().no_strict_escapes();
49//! assert_eq!(shellish_parse::parse(line, options).unwrap(), &[
50//! "In�valid"
51//! ]);
52//! let options = ParseOptions::new();
53//! assert_eq!(shellish_parse::parse(line, options).unwrap_err(),
54//! shellish_parse::ParseError::UnrecognizedEscape("\\m".to_string()));
55//! ```
56//!
57//! You may want to use an alias to make calling this function more convenient
58//! if you're using it in a lot of places:
59//!
60//! ```rust
61//! use shellish_parse::parse as parse_shellish;
62//! let line = "Hello World";
63//! assert_eq!(parse_shellish(line, false).unwrap(), &[
64//! "Hello", "World"
65//! ]);
66//! ```
67//!
68//! And putting your preferred `ParseOptions` into a `const` can save you some
69//! typing:
70//!
71//! ```rust
72//! # use shellish_parse::ParseOptions;
73//! const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new()
74//! .allow_comments_within_elements();
75//! use shellish_parse::parse as parse_shellish;
76//! let line = "This line contains a com#ment";
77//! assert_eq!(parse_shellish(line, SHELLISH_OPTIONS).unwrap(), &[
78//! "This", "line", "contains", "a", "com"
79//! ]);
80//! ```
81//!
82//! Regular parse is great and everything, but sometimes you want to be able
83//! to chain multiple commands on the same line. That's where `multiparse`
84//! comes in:
85//!
86//! ```rust
87//! # use shellish_parse::ParseOptions;
88//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
89//! let line = "Hello World; How are you?";
90//! assert_eq!(shellish_parse::multiparse(line, SHELLISH_OPTIONS, &[";"])
91//! .unwrap(), &[
92//! (vec!["Hello".to_string(), "World".to_string()], Some(0)),
93//! (vec!["How".to_string(), "are".to_string(), "you?".to_string()], None),
94//! ]);
95//! ```
96//!
97//! (Since it returns a vec of tuples, it's rather awkward to phrase in tests.)
98//!
99//! You pass the separators you want to use. A single semicolon is probably
100//! all you want. If you want to get really fancy, you can add arbitrarily many
101//! different separators. Each command returned comes with the index of the
102//! separator that terminated it:
103//!
104//! ```rust
105//! # use shellish_parse::ParseOptions;
106//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
107//! let line = "test -f foo && pv foo | bar || echo no foo & echo wat";
108//! assert_eq!(shellish_parse::multiparse(line, SHELLISH_OPTIONS,
109//! &["&&", "||", "&", "|", ";"])
110//! .unwrap(), &[
111//! (vec!["test".to_string(), "-f".to_string(), "foo".to_string()], Some(0)),
112//! (vec!["pv".to_string(), "foo".to_string()], Some(3)),
113//! (vec!["bar".to_string()], Some(1)),
114//! (vec!["echo".to_string(), "no".to_string(), "foo".to_string()], Some(2)),
115//! (vec!["echo".to_string(), "wat".to_string()], None),
116//! ]);
117//! ```
118//!
119//! Since the separators are checked in the order passed, put longer
120//! separators before shorter ones. If `"&"` preceded `"&&"` in the above call,
121//! `"&"` would always be recognized first, and `"&&"` would never be
122//! recognized.
123//!
124//! Extremely shellish things, like redirection or using parentheses to group
125//! commands, are out of scope of this crate. If you want those things, you
126//! might be writing an actual shell, and not just something shellish.
127//!
128//! # Syntax
129//!
130//! The syntax is heavily inspired by the UNIX Bourne shell. Quotation works
131//! exactly like in said shell. Backslashes can also be used for escaping (and
132//! more advanced usage, more like Rust strings than shellish). Unlike the real
133//! Bourne shell, `parse_shellish` contains no form of variable substitution.
134//!
135//! ## Whitespace
136//!
137//! Elements are separated by one or more whitespace characters.
138//!
139//! ```rust
140//! # use shellish_parse::ParseOptions;
141//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
142//! let line = "Hello there!";
143//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
144//! "Hello", "there!",
145//! ])
146//! ```
147//!
148//! Whitespace consists of spaces, tabs, or newlines. Whitespace before and
149//! after the command line is ignored. Any combination and quantity of
150//! whitespace between elements acts the same as a single space.
151//!
152//! ```rust
153//! # use shellish_parse::ParseOptions;
154//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
155//! let line = "\tHello\n\t there! \n\n";
156//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
157//! "Hello", "there!",
158//! ])
159//! ```
160//!
161//! ## Backslash escapes
162//!
163//! (All example input strings in this section are given as raw strings. The
164//! backslashes and quotation marks you see in them are literal.)
165//!
166//! You may escape any character with backslash.
167//!
168//! Backslash followed by an ASCII letter (26 letters `'A'` through `'Z'` and
169//! `'a'` through `'z'`) or digit (`'0'` through `'9'`) has a special meaning.
170//!
171//! - `'n'`: Newline (U+000A LINE FEED)
172//! - `'t'`: Tab (U+0009 CHARACTER TABULATION)
173//! - Any other letter (and any digit) will either insert a � (U+FFFD
174//! REPLACEMENT CHARACTER) or cause a parse error, depending on the value you
175//! pass as the second parameter to `parse`.
176//!
177//! ```rust
178//! # use shellish_parse::ParseOptions;
179//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
180//! let line = r#"General\t Kenobi\n"#;
181//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
182//! "General\t", "Kenobi\n",
183//! ])
184//! ```
185//!
186//! Backslash followed by a newline followed by any number of unescaped tabs or
187//! spaces will give nothing, just like in Rust strings. (i.e. you may continue
188//! a command line onto another line by preceding the linebreak with a
189//! backslash)
190//!
191//! ```rust
192//! # use shellish_parse::ParseOptions;
193//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
194//! let line = r#"You will die br\
195//! aver than most."#;
196//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
197//! "You", "will", "die", "braver", "than", "most."
198//! ])
199//! ```
200//!
201//! Backslash followed by anything else will give that character, ignoring any
202//! special meaning it might otherwise have had.
203//!
204//! ```rust
205//! # use shellish_parse::ParseOptions;
206//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
207//! let line = r#"Four\-score\ and\ seven \"years\" ago"#;
208//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
209//! "Four-score and seven", "\"years\"", "ago"
210//! ])
211//! ```
212//!
213//! Future versions may add more special characters. These will only be denoted
214//! by letter(s) or digit(s). For all other characters, the handling of
215//! backslash is guaranteed not to change.
216//!
217//! ## Quoting
218//!
219//! (All example input strings in this section are given as raw strings. The
220//! backslashes and quotation marks you see in them are literal.)
221//!
222//! You may quote parts of the command line. The quoted text will all go into
223//! the same element.
224//!
225//! ```rust
226//! # use shellish_parse::ParseOptions;
227//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
228//! let line = r#"cp "Quotation Mark Test" "Quotation Mark Test Backup""#;
229//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
230//! "cp", "Quotation Mark Test", "Quotation Mark Test Backup"
231//! ])
232//! ```
233//!
234//! Quoting will *not* create a new element on its own.
235//!
236//! ```rust
237//! # use shellish_parse::ParseOptions;
238//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
239//! let line = r#"I Probably Should Have"Added A Space!""#;
240//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
241//! "I", "Probably", "Should", "HaveAdded A Space!"
242//! ])
243//! ```
244//!
245//! There are two kinds of quotation. A double-quoted string will interpret
246//! backslash escapes, including `\"`.
247//!
248//! ```rust
249//! # use shellish_parse::ParseOptions;
250//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
251//! let line = r#"movie recommend "\"Swing it\" magistern""#;
252//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
253//! "movie", "recommend", "\"Swing it\" magistern"
254//! ])
255//! ```
256//!
257//! A single-quoted string **will not** interpret backslash escapes, not even
258//! `\'`!
259//!
260//! ```rust
261//! # use shellish_parse::ParseOptions;
262//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
263//! let line = r#"addendum 'and then he said "But I haven'\''t seen it, I \
264//! just searched for '\''movies with quotes in their titles'\'' on IMDB and \
265//! saw that it was popular"'"#;
266//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
267//! "addendum", "and then he said \"But I haven't seen it, I just \
268//! searched for 'movies with quotes in their titles' on IMDB and saw that it \
269//! was popular\""
270//! ])
271//! ```
272//!
273//! ## Continuation
274//!
275//! `parse` returns `Err(ParseResult::...)` on failure. There are three ways
276//! parsing can fail:
277//!
278//! 1. Dangling backslash: `like this\`
279//! 2. Unterminated string: `like "this`
280//! 3. Unrecognized escape sequence: `like this\m`
281//!
282//! In the first two cases, parsing could succeed if there were only more input
283//! to read. So you can handle these errors by prompting for more input, adding
284//! it onto the end of the string, and trying again. The `needs_continuation`
285//! method of `ParseResult` is here to help:
286//!
287//! ```rust
288//! # use shellish_parse::ParseOptions;
289//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
290//! // note: raw strings
291//! let input_lines = [r#"This is not a very \"#,
292//! r#"long line, so why did \"#,
293//! r#"we choose to 'force "#,
294//! r#"continuation'?"#];
295//! let mut input_iter = input_lines.into_iter();
296//! let mut buf = input_iter.next().unwrap().to_string();
297//! let result = loop {
298//! match shellish_parse::parse(&buf, SHELLISH_OPTIONS) {
299//! Err(x) if x.needs_continuation() => {
300//! buf.push('\n'); // don't forget this part!
301//! buf.push_str(input_iter.next().unwrap())
302//! },
303//! x => break x,
304//! }
305//! };
306//! assert_eq!(result.unwrap(), &[
307//! "This", "is", "not", "a", "very", "long", "line,", "so", "why", "did",
308//! "we", "choose", "to", "force \ncontinuation?"
309//! ]);
310//! ```
311//!
312//! ## Comments
313//!
314//! By default, comments are delimited by a `#` character.
315//!
316//! ```rust
317//! # use shellish_parse::ParseOptions;
318//! # const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
319//! let line = "Comment test. #comments #sayinghashtagoutloud";
320//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
321//! "Comment", "test."
322//! ])
323//! ```
324//!
325//! You can change this to any other character using
326//! [`ParseOptions`](struct.ParseOptions.html):
327//!
328//! ```rust
329//! # use shellish_parse::ParseOptions;
330//! const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new()
331//! .comment_char(Some('%'));
332//! let line = "bind lmbutton Interact % make left mouse button interact";
333//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
334//! "bind", "lmbutton", "Interact"
335//! ])
336//! ```
337//!
338//! You can also disable comment parsing entirely:
339//!
340//! ```rust
341//! # use shellish_parse::ParseOptions;
342//! const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new()
343//! .comment_char(None);
344//! let line = "Comment test. #comments #sayinghashtagoutloud";
345//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
346//! "Comment", "test.", "#comments", "#sayinghashtagoutloud"
347//! ])
348//! ```
349//!
350//! By default, comments are not allowed in the middle of an element. This
351//! behavior matches the Bourne shell. You can make it so that any comment
352//! character, found outside a string, will be accepted as the beginning of a
353//! comment:
354//!
355//! ```rust
356//! # use shellish_parse::ParseOptions;
357//! let line = "Comment that breaks an el#ement.";
358//! const SHELLISH_OPTIONS: ParseOptions = ParseOptions::new();
359//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS).unwrap(), &[
360//! "Comment", "that", "breaks", "an", "el#ement."
361//! ]);
362//! const SHELLISH_OPTIONS_2: ParseOptions = ParseOptions::new()
363//! .allow_comments_within_elements();
364//! assert_eq!(shellish_parse::parse(line, SHELLISH_OPTIONS_2).unwrap(), &[
365//! "Comment", "that", "breaks", "an", "el"
366//! ]);
367//! ```
368//!
369//! # Legalese
370//!
371//! `shellish_parse` is copyright 2022-2023, Solra Bizna, and licensed under
372//! either of:
373//!
374//! - Apache License, Version 2.0
375//! ([LICENSE-APACHE](LICENSE-APACHE) or
376//! <http://www.apache.org/licenses/LICENSE-2.0>)
377//! - MIT license
378//! ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
379//!
380//! at your option.
381//!
382//! Unless you explicitly state otherwise, any contribution intentionally
383//! submitted for inclusion in the `shellish_parse` crate by you, as defined
384//! in the Apache-2.0 license, shall be dual licensed as above, without any
385//! additional terms or conditions.
386
387use std::{
388 fmt::Display,
389 error::Error,
390};
391
392/// Options for configuring command-line parsing.
393///
394/// For backwards compatibility with 2.1, you can convert a `bool` into this
395/// type. `true` will be the default, and `false` will be `no_strict_escapes`.
396#[derive(Copy,Clone,Debug)]
397pub struct ParseOptions {
398 strict_escapes: bool,
399 allow_comments_within_elements: bool,
400 comment_char: Option<char>,
401}
402
403impl ParseOptions {
404 /// Create a new `ParseOptions` starting at the defaults.
405 ///
406 /// Equivalent to `ParseOptions::default()`, except that it's a `const fn`
407 /// so you can put it into a `const` variable if you like.
408 pub const fn new() -> ParseOptions {
409 ParseOptions {
410 strict_escapes: true,
411 allow_comments_within_elements: false,
412 comment_char: Some('#'),
413 }
414 }
415 /// The default is for bad escape sequences to result in a `ParseError`.
416 /// If `no_strict_escapes()` is used, then bad escape sequences will result
417 /// in '�' instead.
418 pub const fn no_strict_escapes(mut self) -> Self {
419 self.strict_escapes = false;
420 self
421 }
422 /// The default is for comments to be delimited by a `#` character. You can
423 /// specify another comment character, or disable comment delimiting,
424 /// using `comment_char()`.
425 pub const fn comment_char(mut self, comment_char: Option<char>) -> Self {
426 self.comment_char = comment_char;
427 self
428 }
429 /// The default is that comments will only count if they are preceded by
430 /// whitespace. Thus, by default, `foo bar#baz # bang` will parse as
431 /// `["foo", "bar#baz"]`. This matches the behavior of the Bourne shell.
432 /// You can override this behavior by calling
433 /// `allow_comments_within_elements()`, which would make that line parse
434 /// as `["foo", "bar"]` instead.
435 pub const fn allow_comments_within_elements(mut self) -> Self {
436 self.allow_comments_within_elements = true;
437 self
438 }
439}
440
441impl Default for ParseOptions {
442 fn default() -> ParseOptions {
443 ParseOptions::new()
444 }
445}
446
447impl From<bool> for ParseOptions {
448 fn from(i: bool) -> Self {
449 if i { ParseOptions::new() }
450 else { ParseOptions::new().no_strict_escapes() }
451 }
452}
453
454/// A result of a failed command line parse.
455///
456/// Most of these errors can be resolved with additional user input. The
457/// `needs_continuation` method is there to help. See
458/// [the module-level documentation](index.html) for more information.
459#[derive(Clone,Debug,PartialEq,Eq)]
460pub enum ParseError {
461 /// The command line ended with an unescaped backslash.
462 DanglingBackslash,
463 /// A string was still open when the command line ended.
464 DanglingString,
465 /// There was an unrecognized backslash escape sequence.
466 UnrecognizedEscape(String),
467}
468
469impl ParseError {
470 /// Returns `true` if this kind of ParseError will be resolved if the user
471 /// provides more input.
472 pub fn needs_continuation(&self) -> bool {
473 match self {
474 &ParseError::DanglingBackslash | &ParseError::DanglingString
475 => true,
476 _ => false,
477 }
478 }
479}
480
481impl Display for ParseError {
482 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483 match self {
484 &ParseError::DanglingBackslash
485 => write!(fmt, "dangling backslash"),
486 &ParseError::DanglingString
487 => write!(fmt, "dangling string"),
488 &ParseError::UnrecognizedEscape(ref seq)
489 => write!(fmt, "unrecognized escape sequence: {:?}", seq)
490 }
491 }
492}
493
494impl Error for ParseError {
495}
496
497/// Parse a shellish string into elements. This function will parse a single
498/// command. See [the module-level documentation](index.html) for more
499/// information.
500///
501/// - `input`: The string to parse.
502/// - `options`: A [`ParseOptions`](struct.ParseOptions.html) instance,
503/// describing the options in effect for this parse. For compatibility,
504/// may also be `true` as shorthand for `ParseOptions::new()`, and `false`
505/// as shorthand for `ParseOptions::new().no_strict_escapes()`.
506///
507/// When parsing is successful, returns a vector containing each individual
508/// element of the parsed command line.
509pub fn parse<T: Into<ParseOptions>>(input: &str, options: T)
510-> Result<Vec<String>, ParseError> {
511 match inner_parse(input, &options.into(), &[]) {
512 Ok(mut x) => {
513 assert!(x.len() <= 1);
514 if x.len() == 0 {
515 Ok(vec![])
516 }
517 else {
518 let (command, sep) = x.swap_remove(0);
519 assert_eq!(sep, None);
520 Ok(command)
521 }
522 },
523 Err(x) => Err(x),
524 }
525}
526
527/// Parse a shellish string into elements. This function can parse multiple
528/// commands on a single line, separated by any of the given list of
529/// separators. See [the module-level documentation](index.html) for more
530/// information.
531///
532/// - `input`: The string to parse.
533/// - `options`: A [`ParseOptions`](struct.ParseOptions.html) instance,
534/// describing the options in effect for this parse. For compatibility,
535/// may also be `true` as shorthand for `ParseOptions::new()`, and `false`
536/// as shorthand for `ParseOptions::new().no_strict_escapes()`.
537///
538/// When parsing is successful, returns a vector containing tuples of
539/// individual commands, along with the index of the separator that ended that
540/// command. The last command may not have a separator, in which case it was
541/// ended by the end of the string, rather than a separator.
542pub fn multiparse<T: Into<ParseOptions>>(
543 input: &str, options: T, separators: &[&str]
544) -> Result<Vec<(Vec<String>, Option<usize>)>, ParseError> {
545 inner_parse(input, &options.into(), separators)
546}
547
548fn inner_parse(input: &str, options: &ParseOptions, separators: &[&str])
549 -> Result<Vec<(Vec<String>, Option<usize>)>, ParseError> {
550 let mut utf8_buffer = [0u8; 5];
551 let comment_bytes = options.comment_char.map(|x| x.encode_utf8(&mut utf8_buffer));
552 let inbytes = input.as_bytes();
553 let mut ret = Vec::new();
554 let mut cur_line = Vec::new();
555 let mut pos = 0;
556 let mut cur_arg: Option<Vec<u8>> = None;
557 let mut cur_string = None;
558 'outer: while pos < input.len() {
559 if cur_string.is_none() {
560 // Check separators iff we're not in a string
561 let rem = &inbytes[pos..];
562 for (index, separator) in separators.iter().enumerate() {
563 if separator.len() > rem.len() { continue }
564 if separator.as_bytes() == &rem[..separator.len()] {
565 // Hit a separator. Skip it and ship it.
566 pos += separator.len();
567 if let Some(cur_arg) = cur_arg.take() {
568 cur_line.push(unsafe {String::from_utf8_unchecked(cur_arg) });
569 }
570 ret.push((cur_line, Some(index)));
571 cur_line = Vec::new();
572 continue 'outer;
573 }
574 }
575 // Also check comments, iff we're not in a string (and comments are
576 // a thing)
577 if let Some(comment_bytes) = comment_bytes.as_ref() {
578 if cur_arg.is_none() || options.allow_comments_within_elements {
579 if comment_bytes.as_bytes() == &rem[..comment_bytes.len()] {
580 break 'outer;
581 }
582 }
583 }
584 }
585 let nextb = inbytes[pos];
586 if cur_string.is_none() && (nextb == b'\n' || nextb == b' ' || nextb == b'\t') {
587 if let Some(cur_arg) = cur_arg.take() {
588 cur_line.push(unsafe {String::from_utf8_unchecked(cur_arg) });
589 }
590 pos += 1;
591 }
592 else if Some(nextb) == cur_string {
593 debug_assert!(cur_arg.is_some());
594 cur_string = None;
595 pos += 1;
596 }
597 else if nextb == b'\\' {
598 cur_arg.get_or_insert_with(Vec::new);
599 let cur_arg = cur_arg.as_mut().unwrap();
600 pos += 1;
601 if pos >= input.len() {
602 return Err(ParseError::DanglingBackslash);
603 }
604 let escb = inbytes[pos];
605 if escb.is_ascii_alphabetic() {
606 match escb {
607 b't' => cur_arg.push(b'\t'),
608 b'n' => cur_arg.push(b'\n'),
609 _ => {
610 if options.strict_escapes {
611 return Err(ParseError::UnrecognizedEscape(input[pos-1..=pos].to_string()))
612 }
613 else {
614 cur_arg.extend_from_slice("\u{FFFD}".as_bytes());
615 }
616 },
617 }
618 }
619 else if escb == b'\n' {
620 // eat any subsequent non-newline whitespace we find
621 while pos + 1 < inbytes.len()
622 && (inbytes[pos+1] == b' ' || inbytes[pos+1] == b'\t') {
623 pos += 1;
624 }
625 }
626 else {
627 cur_arg.push(escb);
628 }
629 pos += 1;
630 }
631 else if cur_string.is_none() && nextb == b'"' {
632 cur_arg.get_or_insert_with(Vec::new);
633 cur_string = Some(b'"');
634 pos += 1;
635 }
636 else if cur_string.is_none() && nextb == b'\'' {
637 cur_arg.get_or_insert_with(Vec::new);
638 cur_string = Some(b'\'');
639 pos += 1;
640 }
641 else {
642 cur_arg.get_or_insert_with(Vec::new).push(nextb);
643 pos += 1;
644 }
645 }
646 if cur_string.is_some() {
647 return Err(ParseError::DanglingString);
648 }
649 if let Some(cur_arg) = cur_arg.take() {
650 cur_line.push(unsafe {String::from_utf8_unchecked(cur_arg) });
651 }
652 if !cur_line.is_empty() {
653 ret.push((cur_line, None));
654 }
655 Ok(ret)
656}