env: support string args by "-S", "-vS" or "--split-strings"

This commit is contained in:
Ulrich Hornung
2024-03-14 19:38:28 +01:00
parent 6f95d058a2
commit c62ba559d0
14 changed files with 2587 additions and 207 deletions
+1
View File
@@ -25,6 +25,7 @@ sudoedit
tcsh
tzselect
urandom
VARNAME
wtmp
zsh
Generated
+25 -11
View File
@@ -547,7 +547,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"regex",
"syn 2.0.23",
"syn 2.0.32",
"unicode-xid",
]
@@ -559,7 +559,7 @@ checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1"
dependencies = [
"lazy_static",
"proc-macro2",
"syn 2.0.23",
"syn 2.0.32",
]
[[package]]
@@ -574,7 +574,7 @@ dependencies = [
"lazy_static",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.32",
]
[[package]]
@@ -942,7 +942,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.32",
]
[[package]]
@@ -1791,7 +1791,7 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
"syn 2.0.23",
"syn 2.0.32",
"unicode-ident",
]
@@ -1902,9 +1902,23 @@ checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
[[package]]
name = "serde"
version = "1.0.147"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]
[[package]]
name = "sha1"
@@ -2039,9 +2053,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.23"
version = "2.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
dependencies = [
"proc-macro2",
"quote",
@@ -3296,7 +3310,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.32",
"wasm-bindgen-shared",
]
@@ -3318,7 +3332,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
"syn 2.0.32",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
+357 -187
View File
File diff suppressed because it is too large Load Diff
+325
View File
@@ -0,0 +1,325 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// This module contains classes and functions for dealing with the differences
// between operating systems regarding the lossless processing of OsStr/OsString.
// In contrast to existing crates with similar purpose, this module does not use any
// `unsafe` features or functions.
// Due to a suboptimal design aspect of OsStr/OsString on windows, we need to
// encode/decode to wide chars on windows operating system.
// This prevents borrowing from OsStr on windows. Anyway, if optimally used,#
// this conversion needs to be done only once in the beginning and at the end.
use std::ffi::OsString;
#[cfg(not(target_os = "windows"))]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
#[cfg(target_os = "windows")]
use std::os::windows::prelude::*;
use std::{borrow::Cow, ffi::OsStr};
#[cfg(target_os = "windows")]
use u16 as NativeIntCharU;
#[cfg(not(target_os = "windows"))]
use u8 as NativeIntCharU;
pub type NativeCharInt = NativeIntCharU;
pub type NativeIntStr = [NativeCharInt];
pub type NativeIntString = Vec<NativeCharInt>;
pub struct NCvt;
pub trait Convert<From, To> {
fn convert(f: From) -> To;
}
// ================ str/String =================
impl<'a> Convert<&'a str, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: &'a str) -> Cow<'a, NativeIntStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(f.encode_utf16().collect())
}
#[cfg(not(target_os = "windows"))]
{
Cow::Borrowed(f.as_bytes())
}
}
}
impl<'a> Convert<&'a String, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: &'a String) -> Cow<'a, NativeIntStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(f.encode_utf16().collect())
}
#[cfg(not(target_os = "windows"))]
{
Cow::Borrowed(f.as_bytes())
}
}
}
impl<'a> Convert<String, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: String) -> Cow<'a, NativeIntStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(f.encode_utf16().collect())
}
#[cfg(not(target_os = "windows"))]
{
Cow::Owned(f.into_bytes())
}
}
}
// ================ OsStr/OsString =================
impl<'a> Convert<&'a OsStr, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: &'a OsStr) -> Cow<'a, NativeIntStr> {
to_native_int_representation(f)
}
}
impl<'a> Convert<&'a OsString, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: &'a OsString) -> Cow<'a, NativeIntStr> {
to_native_int_representation(f)
}
}
impl<'a> Convert<OsString, Cow<'a, NativeIntStr>> for NCvt {
fn convert(f: OsString) -> Cow<'a, NativeIntStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(f.encode_wide().collect())
}
#[cfg(not(target_os = "windows"))]
{
Cow::Owned(f.into_vec())
}
}
}
// ================ Vec<Str/String> =================
impl<'a> Convert<&'a Vec<&'a str>, Vec<Cow<'a, NativeIntStr>>> for NCvt {
fn convert(f: &'a Vec<&'a str>) -> Vec<Cow<'a, NativeIntStr>> {
f.iter().map(|x| Self::convert(*x)).collect()
}
}
impl<'a> Convert<Vec<&'a str>, Vec<Cow<'a, NativeIntStr>>> for NCvt {
fn convert(f: Vec<&'a str>) -> Vec<Cow<'a, NativeIntStr>> {
f.iter().map(|x| Self::convert(*x)).collect()
}
}
impl<'a> Convert<&'a Vec<String>, Vec<Cow<'a, NativeIntStr>>> for NCvt {
fn convert(f: &'a Vec<String>) -> Vec<Cow<'a, NativeIntStr>> {
f.iter().map(Self::convert).collect()
}
}
impl<'a> Convert<Vec<String>, Vec<Cow<'a, NativeIntStr>>> for NCvt {
fn convert(f: Vec<String>) -> Vec<Cow<'a, NativeIntStr>> {
f.into_iter().map(Self::convert).collect()
}
}
pub fn to_native_int_representation(input: &OsStr) -> Cow<'_, NativeIntStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(input.encode_wide().collect())
}
#[cfg(not(target_os = "windows"))]
{
Cow::Borrowed(input.as_bytes())
}
}
#[allow(clippy::needless_pass_by_value)] // needed on windows
pub fn from_native_int_representation(input: Cow<'_, NativeIntStr>) -> Cow<'_, OsStr> {
#[cfg(target_os = "windows")]
{
Cow::Owned(OsString::from_wide(&input))
}
#[cfg(not(target_os = "windows"))]
{
match input {
Cow::Borrowed(borrow) => Cow::Borrowed(OsStr::from_bytes(borrow)),
Cow::Owned(own) => Cow::Owned(OsString::from_vec(own)),
}
}
}
#[allow(clippy::needless_pass_by_value)] // needed on windows
pub fn from_native_int_representation_owned(input: NativeIntString) -> OsString {
#[cfg(target_os = "windows")]
{
OsString::from_wide(&input)
}
#[cfg(not(target_os = "windows"))]
{
OsString::from_vec(input)
}
}
pub fn get_single_native_int_value(c: &char) -> Option<NativeCharInt> {
#[cfg(target_os = "windows")]
{
let mut buf = [0u16, 0];
let s = c.encode_utf16(&mut buf);
if s.len() == 1 {
Some(buf[0])
} else {
None
}
}
#[cfg(not(target_os = "windows"))]
{
let mut buf = [0u8, 0, 0, 0];
let s = c.encode_utf8(&mut buf);
if s.len() == 1 {
Some(buf[0])
} else {
None
}
}
}
pub fn get_char_from_native_int(ni: NativeCharInt) -> Option<(char, NativeCharInt)> {
let c_opt;
#[cfg(target_os = "windows")]
{
c_opt = char::decode_utf16([ni; 1]).next().unwrap().ok();
};
#[cfg(not(target_os = "windows"))]
{
c_opt = std::str::from_utf8(&[ni; 1])
.ok()
.map(|x| x.chars().next().unwrap());
};
if let Some(c) = c_opt {
return Some((c, ni));
}
None
}
pub struct NativeStr<'a> {
native: Cow<'a, NativeIntStr>,
}
impl<'a> NativeStr<'a> {
pub fn new(str: &'a OsStr) -> Self {
Self {
native: to_native_int_representation(str),
}
}
pub fn native(&self) -> Cow<'a, NativeIntStr> {
self.native.clone()
}
pub fn into_native(self) -> Cow<'a, NativeIntStr> {
self.native
}
pub fn contains(&self, x: &char) -> Option<bool> {
let n_c = get_single_native_int_value(x)?;
Some(self.native.contains(&n_c))
}
pub fn slice(&self, from: usize, to: usize) -> Cow<'a, OsStr> {
let result = self.match_cow(|b| Ok::<_, ()>(&b[from..to]), |o| Ok(o[from..to].to_vec()));
result.unwrap()
}
pub fn split_once(&self, pred: &char) -> Option<(Cow<'a, OsStr>, Cow<'a, OsStr>)> {
let n_c = get_single_native_int_value(pred)?;
let p = self.native.iter().position(|&x| x == n_c)?;
let before = self.slice(0, p);
let after = self.slice(p + 1, self.native.len());
Some((before, after))
}
pub fn split_at(&self, pos: usize) -> (Cow<'a, OsStr>, Cow<'a, OsStr>) {
let before = self.slice(0, pos);
let after = self.slice(pos, self.native.len());
(before, after)
}
pub fn strip_prefix(&self, prefix: &OsStr) -> Option<Cow<'a, OsStr>> {
let n_prefix = to_native_int_representation(prefix);
let result = self.match_cow(
|b| b.strip_prefix(&*n_prefix).ok_or(()),
|o| o.strip_prefix(&*n_prefix).map(|x| x.to_vec()).ok_or(()),
);
result.ok()
}
pub fn strip_prefix_native(&self, prefix: &OsStr) -> Option<Cow<'a, NativeIntStr>> {
let n_prefix = to_native_int_representation(prefix);
let result = self.match_cow_native(
|b| b.strip_prefix(&*n_prefix).ok_or(()),
|o| o.strip_prefix(&*n_prefix).map(|x| x.to_vec()).ok_or(()),
);
result.ok()
}
fn match_cow<FnBorrow, FnOwned, Err>(
&self,
f_borrow: FnBorrow,
f_owned: FnOwned,
) -> Result<Cow<'a, OsStr>, Err>
where
FnBorrow: FnOnce(&'a [NativeCharInt]) -> Result<&'a [NativeCharInt], Err>,
FnOwned: FnOnce(&Vec<NativeCharInt>) -> Result<Vec<NativeCharInt>, Err>,
{
match &self.native {
Cow::Borrowed(b) => {
let slice = f_borrow(b);
let os_str = slice.map(|x| from_native_int_representation(Cow::Borrowed(x)));
os_str
}
Cow::Owned(o) => {
let slice = f_owned(o);
let os_str = slice.map(from_native_int_representation_owned);
os_str.map(Cow::Owned)
}
}
}
fn match_cow_native<FnBorrow, FnOwned, Err>(
&self,
f_borrow: FnBorrow,
f_owned: FnOwned,
) -> Result<Cow<'a, NativeIntStr>, Err>
where
FnBorrow: FnOnce(&'a [NativeCharInt]) -> Result<&'a [NativeCharInt], Err>,
FnOwned: FnOnce(&Vec<NativeCharInt>) -> Result<Vec<NativeCharInt>, Err>,
{
match &self.native {
Cow::Borrowed(b) => {
let slice = f_borrow(b);
slice.map(Cow::Borrowed)
}
Cow::Owned(o) => {
let slice = f_owned(o);
slice.map(Cow::Owned)
}
}
}
}
+55
View File
@@ -0,0 +1,55 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::fmt;
use crate::string_parser;
/// An error returned when string arg splitting fails.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ParseError {
MissingClosingQuote {
pos: usize,
c: char,
},
InvalidBackslashAtEndOfStringInMinusS {
pos: usize,
quoting: String,
},
BackslashCNotAllowedInDoubleQuotes {
pos: usize,
},
InvalidSequenceBackslashXInMinusS {
pos: usize,
c: char,
},
ParsingOfVariableNameFailed {
pos: usize,
msg: String,
},
InternalError {
pos: usize,
sub_err: string_parser::Error,
},
ReachedEnd,
ContinueWithDelimiter,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(format!("{:?}", self).as_str())
}
}
impl std::error::Error for ParseError {}
impl From<string_parser::Error> for ParseError {
fn from(value: string_parser::Error) -> Self {
Self::InternalError {
pos: value.peek_position,
sub_err: value,
}
}
}
+375
View File
@@ -0,0 +1,375 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// This file is based on work from Tomasz Miąsko who published it as "shell_words" crate,
// licensed under the Apache License, Version 2.0 <LICENSE-APACHE>
// or the MIT license <LICENSE-MIT>, at your option.
//
//! Process command line according to parsing rules of original GNU env.
//! Even though it looks quite like a POSIX syntax, the original
//! "shell_words" implementation had to be adapted significantly.
//!
//! Apart from the grammar differences, there is a new feature integrated: $VARIABLE expansion.
//!
//! [GNU env] <https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html#g_t_002dS_002f_002d_002dsplit_002dstring-syntax>
// spell-checker:ignore (words) Tomasz Miąsko rntfv FFFD varname
#![forbid(unsafe_code)]
use std::borrow::Cow;
use crate::native_int_str::from_native_int_representation;
use crate::native_int_str::NativeCharInt;
use crate::native_int_str::NativeIntStr;
use crate::native_int_str::NativeIntString;
use crate::parse_error::ParseError;
use crate::string_expander::StringExpander;
use crate::string_parser::StringParser;
use crate::variable_parser::VariableParser;
const BACKSLASH: char = '\\';
const DOUBLE_QUOTES: char = '\"';
const SINGLE_QUOTES: char = '\'';
const NEW_LINE: char = '\n';
const DOLLAR: char = '$';
const REPLACEMENTS: [(char, char); 9] = [
('r', '\r'),
('n', '\n'),
('t', '\t'),
('f', '\x0C'),
('v', '\x0B'),
('_', ' '),
('#', '#'),
('$', '$'),
('"', '"'),
];
const ASCII_WHITESPACE_CHARS: [char; 6] = [' ', '\t', '\r', '\n', '\x0B', '\x0C'];
pub struct SplitIterator<'a> {
expander: StringExpander<'a>,
words: Vec<Vec<NativeCharInt>>,
}
impl<'a> SplitIterator<'a> {
pub fn new(s: &'a NativeIntStr) -> Self {
Self {
expander: StringExpander::new(s),
words: Vec::new(),
}
}
fn skip_one(&mut self) -> Result<(), ParseError> {
self.expander
.get_parser_mut()
.consume_one_ascii_or_all_non_ascii()?;
Ok(())
}
fn take_one(&mut self) -> Result<(), ParseError> {
Ok(self.expander.take_one()?)
}
fn get_current_char(&self) -> Option<char> {
self.expander.peek().ok()
}
fn push_char_to_word(&mut self, c: char) {
self.expander.put_one_char(c);
}
fn push_word_to_words(&mut self) {
let word = self.expander.take_collected_output();
self.words.push(word);
}
fn get_parser(&self) -> &StringParser<'a> {
self.expander.get_parser()
}
fn get_parser_mut(&mut self) -> &mut StringParser<'a> {
self.expander.get_parser_mut()
}
fn substitute_variable<'x>(&'x mut self) -> Result<(), ParseError> {
let mut var_parse = VariableParser::<'a, '_> {
parser: self.get_parser_mut(),
};
let (name, default) = var_parse.parse_variable()?;
let varname_os_str_cow = from_native_int_representation(Cow::Borrowed(name));
let value = std::env::var_os(varname_os_str_cow);
match (&value, default) {
(None, None) => {} // do nothing, just replace it with ""
(Some(value), _) => {
self.expander.put_string(value);
}
(None, Some(default)) => {
self.expander.put_native_string(default);
}
};
Ok(())
}
fn check_and_replace_ascii_escape_code(&mut self, c: char) -> Result<bool, ParseError> {
if let Some(replace) = REPLACEMENTS.iter().find(|&x| x.0 == c) {
self.skip_one()?;
self.push_char_to_word(replace.1);
return Ok(true);
}
Ok(false)
}
fn make_invalid_sequence_backslash_xin_minus_s(&self, c: char) -> ParseError {
ParseError::InvalidSequenceBackslashXInMinusS {
pos: self.expander.get_parser().get_peek_position(),
c,
}
}
fn state_root(&mut self) -> Result<(), ParseError> {
loop {
match self.state_delimiter() {
Err(ParseError::ContinueWithDelimiter) => {}
Err(ParseError::ReachedEnd) => return Ok(()),
result => return result,
}
}
}
fn state_delimiter(&mut self) -> Result<(), ParseError> {
loop {
match self.get_current_char() {
None => return Ok(()),
Some('#') => {
self.skip_one()?;
self.state_comment()?;
}
Some(BACKSLASH) => {
self.skip_one()?;
self.state_delimiter_backslash()?;
}
Some(c) if ASCII_WHITESPACE_CHARS.contains(&c) => {
self.skip_one()?;
}
Some(_) => {
// Don't consume char. Will be done in unquoted state.
self.state_unquoted()?;
}
}
}
}
fn state_delimiter_backslash(&mut self) -> Result<(), ParseError> {
match self.get_current_char() {
None => Err(ParseError::InvalidBackslashAtEndOfStringInMinusS {
pos: self.get_parser().get_peek_position(),
quoting: "Delimiter".into(),
}),
Some('_') | Some(NEW_LINE) => {
self.skip_one()?;
Ok(())
}
Some(DOLLAR) | Some(BACKSLASH) | Some('#') | Some(SINGLE_QUOTES)
| Some(DOUBLE_QUOTES) => {
self.take_one()?;
self.state_unquoted()
}
Some('c') => Err(ParseError::ReachedEnd),
Some(c) if self.check_and_replace_ascii_escape_code(c)? => self.state_unquoted(),
Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)),
}
}
fn state_unquoted(&mut self) -> Result<(), ParseError> {
loop {
match self.get_current_char() {
None => {
self.push_word_to_words();
return Err(ParseError::ReachedEnd);
}
Some(DOLLAR) => {
self.substitute_variable()?;
}
Some(SINGLE_QUOTES) => {
self.skip_one()?;
self.state_single_quoted()?;
}
Some(DOUBLE_QUOTES) => {
self.skip_one()?;
self.state_double_quoted()?;
}
Some(BACKSLASH) => {
self.skip_one()?;
self.state_unquoted_backslash()?;
}
Some(c) if ASCII_WHITESPACE_CHARS.contains(&c) => {
self.push_word_to_words();
self.skip_one()?;
return Ok(());
}
Some(_) => {
self.take_one()?;
}
}
}
}
fn state_unquoted_backslash(&mut self) -> Result<(), ParseError> {
match self.get_current_char() {
None => Err(ParseError::InvalidBackslashAtEndOfStringInMinusS {
pos: self.get_parser().get_peek_position(),
quoting: "Unquoted".into(),
}),
Some(NEW_LINE) => {
self.skip_one()?;
Ok(())
}
Some('_') => {
self.skip_one()?;
self.push_word_to_words();
Err(ParseError::ContinueWithDelimiter)
}
Some('c') => {
self.push_word_to_words();
Err(ParseError::ReachedEnd)
}
Some(DOLLAR) | Some(BACKSLASH) | Some(SINGLE_QUOTES) | Some(DOUBLE_QUOTES) => {
self.take_one()?;
Ok(())
}
Some(c) if self.check_and_replace_ascii_escape_code(c)? => Ok(()),
Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)),
}
}
fn state_single_quoted(&mut self) -> Result<(), ParseError> {
loop {
match self.get_current_char() {
None => {
return Err(ParseError::MissingClosingQuote {
pos: self.get_parser().get_peek_position(),
c: '\'',
})
}
Some(SINGLE_QUOTES) => {
self.skip_one()?;
return Ok(());
}
Some(BACKSLASH) => {
self.skip_one()?;
self.split_single_quoted_backslash()?;
}
Some(_) => {
self.take_one()?;
}
}
}
}
fn split_single_quoted_backslash(&mut self) -> Result<(), ParseError> {
match self.get_current_char() {
None => Err(ParseError::MissingClosingQuote {
pos: self.get_parser().get_peek_position(),
c: '\'',
}),
Some(NEW_LINE) => {
self.skip_one()?;
Ok(())
}
Some(SINGLE_QUOTES) | Some(BACKSLASH) => {
self.take_one()?;
Ok(())
}
Some(c) if REPLACEMENTS.iter().any(|&x| x.0 == c) => {
// See GNU test-suite e11: In single quotes, \t remains as it is.
// Comparing with GNU behavior: \a is not accepted and issues an error.
// So apparently only known sequences are allowed, even though they are not expanded.... bug of GNU?
self.push_char_to_word(BACKSLASH);
self.take_one()?;
Ok(())
}
Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)),
}
}
fn state_double_quoted(&mut self) -> Result<(), ParseError> {
loop {
match self.get_current_char() {
None => {
return Err(ParseError::MissingClosingQuote {
pos: self.get_parser().get_peek_position(),
c: '"',
})
}
Some(DOLLAR) => {
self.substitute_variable()?;
}
Some(DOUBLE_QUOTES) => {
self.skip_one()?;
return Ok(());
}
Some(BACKSLASH) => {
self.skip_one()?;
self.state_double_quoted_backslash()?;
}
Some(_) => {
self.take_one()?;
}
}
}
}
fn state_double_quoted_backslash(&mut self) -> Result<(), ParseError> {
match self.get_current_char() {
None => Err(ParseError::MissingClosingQuote {
pos: self.get_parser().get_peek_position(),
c: '"',
}),
Some(NEW_LINE) => {
self.skip_one()?;
Ok(())
}
Some(DOUBLE_QUOTES) | Some(DOLLAR) | Some(BACKSLASH) => {
self.take_one()?;
Ok(())
}
Some('c') => Err(ParseError::BackslashCNotAllowedInDoubleQuotes {
pos: self.get_parser().get_peek_position(),
}),
Some(c) if self.check_and_replace_ascii_escape_code(c)? => Ok(()),
Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)),
}
}
fn state_comment(&mut self) -> Result<(), ParseError> {
loop {
match self.get_current_char() {
None => return Err(ParseError::ReachedEnd),
Some(NEW_LINE) => {
self.skip_one()?;
return Ok(());
}
Some(_) => {
self.get_parser_mut().skip_until_char_or_end(NEW_LINE);
}
}
}
}
pub fn split(mut self) -> Result<Vec<NativeIntString>, ParseError> {
self.state_root()?;
Ok(self.words)
}
}
pub fn split(s: &NativeIntStr) -> Result<Vec<NativeIntString>, ParseError> {
let splitted_args = SplitIterator::new(s).split()?;
Ok(splitted_args)
}
+92
View File
@@ -0,0 +1,92 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::{
ffi::{OsStr, OsString},
mem,
ops::Deref,
};
use crate::{
native_int_str::{to_native_int_representation, NativeCharInt, NativeIntStr},
string_parser::{Chunk, Error, StringParser},
};
/// This class makes parsing and word collection more convenient.
///
/// It manages an "output" buffer that is automatically filled.
/// It provides "skip_one" and "take_one" that focus on
/// working with ASCII separators. Thus they will skip or take
/// all consecutive non-ascii char sequences at once.
pub struct StringExpander<'a> {
parser: StringParser<'a>,
output: Vec<NativeCharInt>,
}
impl<'a> StringExpander<'a> {
pub fn new(input: &'a NativeIntStr) -> Self {
Self {
parser: StringParser::new(input),
output: Vec::default(),
}
}
pub fn new_at(input: &'a NativeIntStr, pos: usize) -> Self {
Self {
parser: StringParser::new_at(input, pos),
output: Vec::default(),
}
}
pub fn get_parser(&self) -> &StringParser<'a> {
&self.parser
}
pub fn get_parser_mut(&mut self) -> &mut StringParser<'a> {
&mut self.parser
}
pub fn peek(&self) -> Result<char, Error> {
self.parser.peek()
}
pub fn skip_one(&mut self) -> Result<(), Error> {
self.get_parser_mut().consume_one_ascii_or_all_non_ascii()?;
Ok(())
}
pub fn get_peek_position(&self) -> usize {
self.get_parser().get_peek_position()
}
pub fn take_one(&mut self) -> Result<(), Error> {
let chunks = self.parser.consume_one_ascii_or_all_non_ascii()?;
for chunk in chunks {
match chunk {
Chunk::InvalidEncoding(invalid) => self.output.extend(invalid),
Chunk::ValidSingleIntChar((_c, ni)) => self.output.push(ni),
}
}
Ok(())
}
pub fn put_one_char(&mut self, c: char) {
let os_str = OsString::from(c.to_string());
self.put_string(os_str);
}
pub fn put_string<S: AsRef<OsStr>>(&mut self, os_str: S) {
let native = to_native_int_representation(os_str.as_ref());
self.output.extend(native.deref());
}
pub fn put_native_string(&mut self, n_str: &NativeIntStr) {
self.output.extend(n_str);
}
pub fn take_collected_output(&mut self) -> Vec<NativeCharInt> {
mem::take(&mut self.output)
}
}
+182
View File
@@ -0,0 +1,182 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//
// spell-checker:ignore (words) splitted FFFD
#![forbid(unsafe_code)]
use std::{borrow::Cow, ffi::OsStr};
use crate::native_int_str::{
from_native_int_representation, get_char_from_native_int, get_single_native_int_value,
NativeCharInt, NativeIntStr,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Error {
pub peek_position: usize,
pub err_type: ErrorType,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ErrorType {
EndOfInput,
InternalError,
}
/// Provides a valid char or a invalid sequence of bytes.
///
/// Invalid byte sequences can't be splitted in any meaningful way.
/// Thus, they need to be consumed as one piece.
pub enum Chunk<'a> {
InvalidEncoding(&'a NativeIntStr),
ValidSingleIntChar((char, NativeCharInt)),
}
/// This class makes parsing a OsString char by char more convenient.
///
/// It also allows to capturing of intermediate positions for later splitting.
pub struct StringParser<'a> {
input: &'a NativeIntStr,
pointer: usize,
remaining: &'a NativeIntStr,
}
impl<'a> StringParser<'a> {
pub fn new(input: &'a NativeIntStr) -> Self {
let mut instance = Self {
input,
pointer: 0,
remaining: input,
};
instance.set_pointer(0);
instance
}
pub fn new_at(input: &'a NativeIntStr, pos: usize) -> Self {
let mut instance = Self::new(input);
instance.set_pointer(pos);
instance
}
pub fn get_input(&self) -> &'a NativeIntStr {
self.input
}
pub fn get_peek_position(&self) -> usize {
self.pointer
}
pub fn peek(&self) -> Result<char, Error> {
self.peek_char_at_pointer(self.pointer)
}
fn make_err(&self, err_type: ErrorType) -> Error {
Error {
peek_position: self.get_peek_position(),
err_type,
}
}
pub fn peek_char_at_pointer(&self, at_pointer: usize) -> Result<char, Error> {
let split = self.input.split_at(at_pointer).1;
if split.is_empty() {
return Err(self.make_err(ErrorType::EndOfInput));
}
if let Some((c, _ni)) = get_char_from_native_int(split[0]) {
Ok(c)
} else {
Ok('\u{FFFD}')
}
}
fn get_chunk_with_length_at(&self, pointer: usize) -> Result<(Chunk<'a>, usize), Error> {
let (_before, after) = self.input.split_at(pointer);
if after.is_empty() {
return Err(self.make_err(ErrorType::EndOfInput));
}
if let Some(c_ni) = get_char_from_native_int(after[0]) {
Ok((Chunk::ValidSingleIntChar(c_ni), 1))
} else {
let mut i = 1;
while i < after.len() {
if let Some(_c) = get_char_from_native_int(after[i]) {
break;
}
i += 1;
}
let chunk = &after[0..i];
Ok((Chunk::InvalidEncoding(chunk), chunk.len()))
}
}
pub fn peek_chunk(&self) -> Option<Chunk<'a>> {
return self
.get_chunk_with_length_at(self.pointer)
.ok()
.map(|(chunk, _)| chunk);
}
pub fn consume_chunk(&mut self) -> Result<Chunk<'a>, Error> {
let (chunk, len) = self.get_chunk_with_length_at(self.pointer)?;
self.set_pointer(self.pointer + len);
Ok(chunk)
}
pub fn consume_one_ascii_or_all_non_ascii(&mut self) -> Result<Vec<Chunk<'a>>, Error> {
let mut result = Vec::<Chunk<'a>>::new();
loop {
let data = self.consume_chunk()?;
let was_ascii = if let Chunk::ValidSingleIntChar((c, _ni)) = &data {
c.is_ascii()
} else {
false
};
result.push(data);
if was_ascii {
return Ok(result);
}
match self.peek_chunk() {
Some(Chunk::ValidSingleIntChar((c, _ni))) if c.is_ascii() => return Ok(result),
None => return Ok(result),
_ => {}
}
}
}
pub fn skip_multiple(&mut self, skip_byte_count: usize) {
let end_ptr = self.pointer + skip_byte_count;
self.set_pointer(end_ptr);
}
pub fn skip_until_char_or_end(&mut self, c: char) {
let native_rep = get_single_native_int_value(&c).unwrap();
let pos = self.remaining.iter().position(|x| *x == native_rep);
if let Some(pos) = pos {
self.set_pointer(self.pointer + pos);
} else {
self.set_pointer(self.input.len());
}
}
pub fn substring(&self, range: &std::ops::Range<usize>) -> &'a NativeIntStr {
let (_before1, after1) = self.input.split_at(range.start);
let (middle, _after2) = after1.split_at(range.end - range.start);
middle
}
pub fn peek_remaining(&self) -> Cow<'a, OsStr> {
from_native_int_representation(Cow::Borrowed(self.remaining))
}
pub fn set_pointer(&mut self, new_pointer: usize) {
self.pointer = new_pointer;
let (_before, after) = self.input.split_at(self.pointer);
self.remaining = after;
}
}
+158
View File
@@ -0,0 +1,158 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::ops::Range;
use crate::{native_int_str::NativeIntStr, parse_error::ParseError, string_parser::StringParser};
pub struct VariableParser<'a, 'b> {
pub parser: &'b mut StringParser<'a>,
}
impl<'a, 'b> VariableParser<'a, 'b> {
fn get_current_char(&self) -> Option<char> {
self.parser.peek().ok()
}
fn check_variable_name_start(&self) -> Result<(), ParseError> {
if let Some(c) = self.get_current_char() {
if c.is_ascii_digit() {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: self.parser.get_peek_position(),
msg: format!("Unexpected character: '{}', expected variable name must not start with 0..9", c) });
}
}
Ok(())
}
fn skip_one(&mut self) -> Result<(), ParseError> {
self.parser.consume_chunk()?;
Ok(())
}
fn parse_braced_variable_name(
&mut self,
) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), ParseError> {
let pos_start = self.parser.get_peek_position();
self.check_variable_name_start()?;
let (varname_end, default_end);
loop {
match self.get_current_char() {
None => {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: self.parser.get_peek_position(), msg: "Missing closing brace".into() })
},
Some(c) if !c.is_ascii() || c.is_ascii_alphanumeric() || c == '_' => {
self.skip_one()?;
}
Some(':') => {
varname_end = self.parser.get_peek_position();
loop {
match self.get_current_char() {
None => {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: self.parser.get_peek_position(),
msg: "Missing closing brace after default value".into() })
},
Some('}') => {
default_end = Some(self.parser.get_peek_position());
self.skip_one()?;
break
},
Some(_) => {
self.skip_one()?;
},
}
}
break;
},
Some('}') => {
varname_end = self.parser.get_peek_position();
default_end = None;
self.skip_one()?;
break;
},
Some(c) => {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: self.parser.get_peek_position(),
msg: format!("Unexpected character: '{}', expected a closing brace ('}}') or colon (':')", c)
})
},
};
}
let default_opt = if let Some(default_end) = default_end {
Some(self.parser.substring(&Range {
start: varname_end + 1,
end: default_end,
}))
} else {
None
};
let varname = self.parser.substring(&Range {
start: pos_start,
end: varname_end,
});
Ok((varname, default_opt))
}
fn parse_unbraced_variable_name(&mut self) -> Result<&'a NativeIntStr, ParseError> {
let pos_start = self.parser.get_peek_position();
self.check_variable_name_start()?;
loop {
match self.get_current_char() {
None => break,
Some(c) if c.is_ascii_alphanumeric() || c == '_' => {
self.skip_one()?;
}
Some(_) => break,
};
}
let pos_end = self.parser.get_peek_position();
if pos_end == pos_start {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: pos_start,
msg: "Missing variable name".into(),
});
}
let varname = self.parser.substring(&Range {
start: pos_start,
end: pos_end,
});
Ok(varname)
}
pub fn parse_variable(
&mut self,
) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), ParseError> {
self.skip_one()?;
let (name, default) = match self.get_current_char() {
None => {
return Err(ParseError::ParsingOfVariableNameFailed {
pos: self.parser.get_peek_position(),
msg: "missing variable name".into(),
})
}
Some('{') => {
self.skip_one()?;
self.parse_braced_variable_name()?
}
Some(_) => (self.parse_unbraced_variable_name()?, None),
};
Ok((name, default))
}
}
File diff suppressed because it is too large Load Diff
+17 -7
View File
@@ -15,7 +15,6 @@ use pretty_assertions::assert_eq;
use rlimit::setrlimit;
#[cfg(feature = "sleep")]
use rstest::rstest;
#[cfg(unix)]
use std::borrow::Cow;
use std::collections::VecDeque;
#[cfg(not(windows))]
@@ -352,6 +351,11 @@ impl CmdResult {
std::str::from_utf8(&self.stderr).unwrap()
}
/// Returns the program's standard error as a string slice, automatically handling invalid utf8
pub fn stderr_str_lossy(&self) -> Cow<'_, str> {
String::from_utf8_lossy(&self.stderr)
}
/// Returns the program's standard error as a string
/// consumes self
pub fn stderr_move_str(self) -> String {
@@ -372,6 +376,14 @@ impl CmdResult {
#[track_caller]
pub fn code_is(&self, expected_code: i32) -> &Self {
let fails = self.code() != expected_code;
if fails {
eprintln!(
"stdout:\n{}\nstderr:\n{}",
self.stdout_str(),
self.stderr_str()
);
}
assert_eq!(self.code(), expected_code);
self
}
@@ -395,10 +407,8 @@ impl CmdResult {
pub fn success(&self) -> &Self {
assert!(
self.succeeded(),
"Command was expected to succeed. Exit code: {}.\nstdout = {}\n stderr = {}",
self.exit_status()
.code()
.map_or("n/a".to_string(), |code| code.to_string()),
"Command was expected to succeed. code: {}\nstdout = {}\n stderr = {}",
self.code(),
self.stdout_str(),
self.stderr_str()
);
@@ -2674,7 +2684,7 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<
let (stdout, stderr): (String, String) = if cfg!(target_os = "linux") {
(
result.stdout_str().to_string(),
result.stderr_str().to_string(),
result.stderr_str_lossy().to_string(),
)
} else {
// `host_name_for` added prefix, strip 'g' prefix from results:
@@ -2682,7 +2692,7 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result<
let to = &from[1..];
(
result.stdout_str().replace(&from, to),
result.stderr_str().replace(&from, to),
result.stderr_str_lossy().replace(&from, to),
)
};
+1
View File
@@ -0,0 +1 @@
echo Hello Windows World!
+2
View File
@@ -221,6 +221,8 @@ grep -rlE '/usr/local/bin/\s?/usr/local/bin' init.cfg tests/* | xargs -r sed -Ei
# we should not regress our project just to match what GNU is going.
# So, do some changes on the fly
patch -N -r - -d "$path_GNU" -p 1 -i "`realpath \"$path_UUTILS/util/gnu-patches/tests_env_env-S.pl.patch\"`" || true
sed -i -e "s|rm: cannot remove 'e/slink'|rm: cannot remove 'e'|g" tests/rm/fail-eacces.sh
sed -i -e "s|rm: cannot remove 'a/b'|rm: cannot remove 'a'|g" tests/rm/fail-2eperm.sh
+47
View File
@@ -0,0 +1,47 @@
diff --git a/tests/env/env-S.pl b/tests/env/env-S.pl
index 710ca82cf..af7cf6efa 100755
--- a/tests/env/env-S.pl
+++ b/tests/env/env-S.pl
@@ -209,27 +209,28 @@ my @Tests =
{ERR=>"$prog: no terminating quote in -S string\n"}],
['err5', q[-S'A=B\\q'], {EXIT=>125},
{ERR=>"$prog: invalid sequence '\\q' in -S\n"}],
- ['err6', q[-S'A=$B'], {EXIT=>125},
- {ERR=>"$prog: only \${VARNAME} expansion is supported, error at: \$B\n"}],
+ ['err6', q[-S'A=$B echo hello'], {EXIT=>0},
+ {OUT=>"hello"}],
['err7', q[-S'A=${B'], {EXIT=>125},
- {ERR=>"$prog: only \${VARNAME} expansion is supported, " .
- "error at: \${B\n"}],
+ {ERR=>"$prog" . qq[: variable name issue (at 5): Missing closing brace\n]}],
['err8', q[-S'A=${B%B}'], {EXIT=>125},
- {ERR=>"$prog: only \${VARNAME} expansion is supported, " .
- "error at: \${B%B}\n"}],
+ {ERR=>"$prog" . qq[: variable name issue (at 5): Unexpected character: '%', expected a closing brace ('}') or colon (':')\n]}],
['err9', q[-S'A=${9B}'], {EXIT=>125},
- {ERR=>"$prog: only \${VARNAME} expansion is supported, " .
- "error at: \${9B}\n"}],
+ {ERR=>"$prog" . qq[: variable name issue (at 4): Unexpected character: '9', expected variable name must not start with 0..9\n]}],
# Test incorrect shebang usage (extraneous whitespace).
['err_sp2', q['-v -S cat -n'], {EXIT=>125},
- {ERR=>"env: invalid option -- ' '\n" .
- "env: use -[v]S to pass options in shebang lines\n" .
- "Try 'env --help' for more information.\n"}],
+ {ERR=>"$prog: error: unexpected argument '- ' found\n\n" .
+ " tip: to pass '- ' as a value, use '-- - '\n\n" .
+ "Usage: $prog [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n\n" .
+ "For more information, try '--help'.\n" .
+ "$prog: use -[v]S to pass options in shebang lines\n"}],
['err_sp3', q['-v -S cat -n'], {EXIT=>125}, # embedded tab after -v
- {ERR=>"env: invalid option -- '\t'\n" .
- "env: use -[v]S to pass options in shebang lines\n" .
- "Try 'env --help' for more information.\n"}],
+ {ERR=>"$prog: error: unexpected argument '-\t' found\n\n" .
+ " tip: to pass '-\t' as a value, use '-- -\t'\n\n" .
+ "Usage: $prog [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]\n\n" .
+ "For more information, try '--help'.\n" .
+ "$prog: use -[v]S to pass options in shebang lines\n"}],
# Also diagnose incorrect shebang usage when failing to exec.
# This typically happens with: