conch_runtime_pshaw/eval/
fields.rs

1use crate::env::{StringWrapper, VariableEnvironment};
2use crate::IFS_DEFAULT;
3use std::borrow::Borrow;
4use std::vec;
5
6lazy_static::lazy_static! {
7    static ref IFS: String = String::from("IFS");
8}
9
10/// Represents the types of fields that may result from evaluating a word.
11/// It is important to maintain such distinctions because evaluating parameters
12/// such as `$@` and `$*` have different behaviors in different contexts.
13#[derive(PartialEq, Eq, Clone, Debug)]
14pub enum Fields<T> {
15    /// No fields, distinct from present-but-null fields.
16    Zero,
17    /// A single field.
18    Single(T),
19    /// Any number of fields resulting from evaluating the `$@` special parameter.
20    At(Vec<T>),
21    /// Any number of fields resulting from evaluating the `$*` special parameter.
22    Star(Vec<T>),
23    /// A non-zero number of fields resulting from splitting, and which do not have
24    /// any special meaning.
25    Split(Vec<T>),
26}
27
28impl<T: StringWrapper> Fields<T> {
29    /// Indicates if a set of fields is considered null.
30    ///
31    /// A set of fields is null if every single string
32    /// it holds is the empty string.
33    pub fn is_null(&self) -> bool {
34        match *self {
35            Fields::Zero => true,
36
37            Fields::Single(ref s) => s.as_str().is_empty(),
38
39            Fields::At(ref v) | Fields::Star(ref v) | Fields::Split(ref v) => {
40                v.iter().all(|s| s.as_str().is_empty())
41            }
42        }
43    }
44
45    /// Joins all fields using a space.
46    ///
47    /// Note: `Zero` is treated as a empty-but-present field for simplicity.
48    pub fn join(self) -> T {
49        match self {
50            Fields::Zero => String::new().into(),
51            Fields::Single(s) => s,
52            Fields::At(v) | Fields::Star(v) | Fields::Split(v) => v
53                .iter()
54                .map(StringWrapper::as_str)
55                .filter(|s| !s.is_empty())
56                .collect::<Vec<&str>>()
57                .join(" ")
58                .into(),
59        }
60    }
61
62    /// Joins any field unconditionally with the first character of `$IFS`.
63    /// If `$IFS` is unset, fields are joined with a space, or concatenated
64    /// if `$IFS` is empty.
65    ///
66    /// Note: `Zero` is treated as a empty-but-present field for simplicity.
67    pub fn join_with_ifs<E: ?Sized>(self, env: &E) -> T
68    where
69        E: VariableEnvironment,
70        E::VarName: Borrow<String>,
71        E::Var: Borrow<String>,
72    {
73        match self {
74            Fields::Zero => String::new().into(),
75            Fields::Single(s) => s,
76            Fields::At(v) | Fields::Star(v) | Fields::Split(v) => {
77                let sep = env.var(&IFS).map(|s| s.borrow().as_str()).map_or(" ", |s| {
78                    if s.is_empty() {
79                        ""
80                    } else {
81                        &s[0..1]
82                    }
83                });
84
85                v.iter()
86                    .map(StringWrapper::as_str)
87                    .collect::<Vec<_>>()
88                    .join(sep)
89                    .into()
90            }
91        }
92    }
93
94    /// Splits a vector of fields further based on the contents of the `IFS`
95    /// variable (i.e. as long as it is non-empty). Any empty fields, original
96    /// or otherwise created will be discarded.
97    pub fn split<E: ?Sized>(self, env: &E) -> Fields<T>
98    where
99        E: VariableEnvironment,
100        E::VarName: Borrow<String>,
101        E::Var: Borrow<String>,
102    {
103        match self {
104            Fields::Zero => Fields::Zero,
105            Fields::Single(f) => split_fields_internal(vec![f], env).into(),
106            Fields::At(fs) => Fields::At(split_fields_internal(fs, env)),
107            Fields::Star(fs) => Fields::Star(split_fields_internal(fs, env)),
108            Fields::Split(fs) => Fields::Split(split_fields_internal(fs, env)),
109        }
110    }
111}
112
113// FIXME: with specialization can also implement From<IntoIterator<T>> but keep From<Vec<T>
114impl<T> From<Vec<T>> for Fields<T> {
115    fn from(mut fields: Vec<T>) -> Self {
116        if fields.is_empty() {
117            Fields::Zero
118        } else if fields.len() == 1 {
119            Fields::Single(fields.pop().unwrap())
120        } else {
121            Fields::Split(fields)
122        }
123    }
124}
125
126impl<T> From<T> for Fields<T> {
127    fn from(t: T) -> Self {
128        Fields::Single(t)
129    }
130}
131
132impl<T> IntoIterator for Fields<T> {
133    type Item = T;
134    type IntoIter = vec::IntoIter<Self::Item>;
135
136    fn into_iter(self) -> Self::IntoIter {
137        let vec = match self {
138            Fields::Zero => vec![],
139            Fields::Single(s) => vec![s],
140            Fields::At(v) | Fields::Star(v) | Fields::Split(v) => v,
141        };
142
143        vec.into_iter()
144    }
145}
146
147/// Actual implementation of `split_fields`.
148fn split_fields_internal<T, E: ?Sized>(words: Vec<T>, env: &E) -> Vec<T>
149where
150    T: StringWrapper,
151    E: VariableEnvironment,
152    E::VarName: Borrow<String>,
153    E::Var: Borrow<String>,
154{
155    // If IFS is set but null, there is nothing left to split
156    let ifs = env.var(&IFS).map_or(IFS_DEFAULT, |s| s.borrow().as_str());
157    if ifs.is_empty() {
158        return words;
159    }
160
161    let whitespace: Vec<char> = ifs.chars().filter(|c| c.is_whitespace()).collect();
162
163    let mut fields = Vec::with_capacity(words.len());
164    'word: for word in words.iter().map(StringWrapper::as_str) {
165        if word.is_empty() {
166            continue;
167        }
168
169        let mut iter = word.chars().enumerate().peekable();
170        loop {
171            let start;
172            loop {
173                match iter.next() {
174                    // If we are still skipping leading whitespace, and we hit the
175                    // end of the word there are no fields to create, even empty ones.
176                    None => continue 'word,
177                    Some((idx, c)) => {
178                        if whitespace.contains(&c) {
179                            continue;
180                        } else if ifs.contains(c) {
181                            // If we hit an IFS char here then we have encountered an
182                            // empty field, since the last iteration of this loop either
183                            // had just consumed an IFS char, or its the start of the word.
184                            // In either case the result should be the same.
185                            fields.push(String::new().into());
186                        } else {
187                            // Must have found a regular field character
188                            start = idx;
189                            break;
190                        }
191                    }
192                }
193            }
194
195            let end;
196            loop {
197                match iter.next() {
198                    None => {
199                        end = None;
200                        break;
201                    }
202                    Some((idx, c)) => {
203                        if ifs.contains(c) {
204                            end = Some(idx);
205                            break;
206                        }
207                    }
208                }
209            }
210
211            let field = match end {
212                Some(end) => &word[start..end],
213                None => &word[start..],
214            };
215
216            fields.push(String::from(field).into());
217
218            // Since now we've hit an IFS character, we need to also skip past
219            // any adjacent IFS whitespace as well. This also conveniently
220            // ignores any trailing IFS whitespace in the input as well.
221            loop {
222                match iter.peek() {
223                    Some(&(_, c)) if whitespace.contains(&c) => {
224                        iter.next();
225                    }
226                    Some(_) | None => break,
227                }
228            }
229        }
230    }
231
232    fields.shrink_to_fit();
233    fields
234}