Skip to content

Commit

Permalink
Retain trailing non-whitespace separators
Browse files Browse the repository at this point in the history
When there are more fields than variables, the last variable receives
all remaining fields, including the field separators, but not trailing
whitespace separators. This is the behavior required by POSIX.
  • Loading branch information
magicant committed Dec 10, 2023
1 parent 6823cca commit eb6940f
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 19 deletions.
4 changes: 2 additions & 2 deletions yash-builtin/src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
//! read to produce as many fields as there are variables. If there are fewer
//! fields than variables, the remaining variables are set to empty strings. If
//! there are more fields than variables, the last variable receives all
//! remaining fields, including the intermediate (but not trailing) field
//! separators.
//! remaining fields, including the field separators, but not trailing
//! whitespace separators.
//!
//! ## Escaping
//!
Expand Down
19 changes: 13 additions & 6 deletions yash-builtin/src/read/assigning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use yash_env::Env;
use yash_semantics::expansion::attr::AttrChar;
use yash_semantics::expansion::attr_strip::Strip as _;
use yash_semantics::expansion::quote_removal::skip_quotes;
use yash_semantics::expansion::split::Class;
use yash_semantics::expansion::split::Ifs;
use yash_syntax::source::pretty::Message;
use yash_syntax::source::pretty::MessageBase as _;
Expand All @@ -44,9 +45,9 @@ pub fn to_message(errors: &[Error]) -> Option<Message> {
///
/// This function performs field splitting on the text and assigns the resulting
/// fields to the variables. When there are more fields than variables, the last
/// variable receives all remaining fields, including the intermediate (but not
/// trailing) field separators. When there are fewer fields than variables, the
/// remaining variables are set to empty strings.
/// variable receives all remaining fields, including the field separators, but
/// not trailing whitespace separators. When there are fewer fields than
/// variables, the remaining variables are set to empty strings.
///
/// The return value is a vector of errors that occurred while assigning the
/// variables. The vector is empty if no error occurred.
Expand Down Expand Up @@ -79,9 +80,16 @@ pub fn assign(
// Assign the last
let range = match ranges.next() {
None => 0..0,
Some(range) => match ranges.last() {
Some(range) => match ranges.next() {
None => range,
Some(range_2) => range.start..range_2.end,
Some(_range) => {
let end = text
.iter()
.rposition(|&c| ifs.classify_attr(c) != Class::IfsWhitespace)
.unwrap()
+ 1;
range.start..end
}
},
};
let last_result = assign_one(env, last_variable, &text[range]);
Expand Down Expand Up @@ -291,7 +299,6 @@ mod tests {
}

#[test]
#[ignore = "to be implemented"]
fn non_default_ifs_delimiting_last_field_extra() {
let mut env = Env::new_virtual();
env.get_or_create_variable("IFS", Scope::Global)
Expand Down
12 changes: 12 additions & 0 deletions yash-semantics/src/expansion/split/ifs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

//! IFS parser
use crate::expansion::attr::AttrChar;
use crate::expansion::attr::Origin;
use std::borrow::Cow;

/// Type of characters that affect field splitting
Expand Down Expand Up @@ -185,6 +187,16 @@ impl Ifs<'_> {
Class::NonIfs
}
}

/// Returns the type of the attributed character.
#[must_use]
pub fn classify_attr(&self, c: AttrChar) -> Class {
if c.is_quoted || c.is_quoting || c.origin != Origin::SoftExpansion {
Class::NonIfs
} else {
self.classify(c.value)
}
}
}

#[cfg(test)]
Expand Down
13 changes: 2 additions & 11 deletions yash-semantics/src/expansion/split/ranges.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,11 @@
//! Extracting ranges of split fields
use super::super::attr::AttrChar;
use super::super::attr::Origin;
use super::ifs::Class;
use super::ifs::Class::*;
use super::ifs::Ifs;
use std::iter::FusedIterator;
use std::ops::Range;

fn classify(c: AttrChar, ifs: &Ifs) -> Class {
if c.is_quoted || c.is_quoting || c.origin != Origin::SoftExpansion {
NonIfs
} else {
ifs.classify(c.value)
}
}

/// State of a field-splitting iterator
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
enum State {
Expand Down Expand Up @@ -83,7 +73,7 @@ where
fn next(&mut self) -> Option<Range<usize>> {
while let Some(state) = self.state {
let index = self.next_index;
let class = self.inner.next().map(|c| classify(c, self.ifs));
let class = self.inner.next().map(|c| self.ifs.classify_attr(c));
self.next_index += 1;

let (next_state, field_range) = match (state, class) {
Expand Down Expand Up @@ -148,6 +138,7 @@ impl<I> FusedIterator for Ranges<'_, I> where I: Iterator<Item = AttrChar> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::expansion::attr::Origin;

fn attr_chars(s: &str) -> impl Iterator<Item = AttrChar> + '_ {
s.chars().map(|c| AttrChar {
Expand Down

0 comments on commit eb6940f

Please sign in to comment.