Skip to main content

yash_syntax/parser/lex/
raw_param.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2021 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Part of the lexer that parses raw parameter expansion
18
19use super::core::Lexer;
20use crate::parser::core::Result;
21use crate::syntax::Param;
22use crate::syntax::ParamType;
23use crate::syntax::SpecialParam;
24use crate::syntax::TextUnit;
25
26/// Tests if a character can be part of a POSIXly-portable name.
27///
28/// Returns true if the character is an ASCII alphanumeric or underscore.
29///
30/// Note that a valid name cannot start with a digit, but this function
31/// returns true for digits as well.
32///
33/// Use [`is_portable_name`] to check if a string is a valid name.
34pub const fn is_portable_name_char(c: char) -> bool {
35    matches!(c, '0'..='9' | 'A'..='Z' | '_' | 'a'..='z')
36}
37
38/// Tests if a string is a valid POSIXly-portable name.
39///
40/// Returns true if the string is non-empty, the first character is not a digit,
41/// and all characters are ASCII alphanumeric or underscore.
42///
43/// Use [`is_portable_name_char`] to check each character.
44#[inline(always)]
45#[must_use]
46pub fn is_portable_name(s: &str) -> bool {
47    yash_env::variable::is_portable_variable_name(s)
48}
49
50/// Tests if a character names a special parameter.
51///
52/// A special parameter is one of: `@*#?-$!0`.
53pub const fn is_special_parameter_char(c: char) -> bool {
54    SpecialParam::from_char(c).is_some()
55}
56
57/// Tests if a character is a valid single-character raw parameter.
58///
59/// If this function returns true, the character is a valid parameter for a raw
60/// parameter expansion, but the next character is never treated as part of the
61/// parameter.
62///
63/// This function returns true for ASCII digits and special parameters.
64pub const fn is_single_char_name(c: char) -> bool {
65    c.is_ascii_digit() || is_special_parameter_char(c)
66}
67
68impl Lexer<'_> {
69    /// Parses a parameter expansion that is not enclosed in braces.
70    ///
71    /// The initial `$` must have been consumed before calling this function.
72    /// This functions checks if the next character is a valid POSIXly-portable
73    /// parameter name. If so, the name is consumed and returned. Otherwise, no
74    /// characters are consumed and the return value is `Ok(None)`.
75    ///
76    /// The `start_index` parameter should be the index for the initial `$`. It is
77    /// used to construct the result, but this function does not check if it
78    /// actually points to the `$`.
79    pub async fn raw_param(&mut self, start_index: usize) -> Result<Option<TextUnit>> {
80        let param = if let Some(c) = self.consume_char_if(is_special_parameter_char).await? {
81            Param {
82                id: c.value.to_string(),
83                r#type: SpecialParam::from_char(c.value).unwrap().into(),
84            }
85        } else if let Some(c) = self.consume_char_if(|c| c.is_ascii_digit()).await? {
86            Param {
87                id: c.value.to_string(),
88                r#type: ParamType::Positional(c.value.to_digit(10).unwrap() as usize),
89            }
90        } else if let Some(c) = self.consume_char_if(is_portable_name_char).await? {
91            let mut name = c.value.to_string();
92            while let Some(c) = self.consume_char_if(is_portable_name_char).await? {
93                name.push(c.value);
94            }
95            Param::variable(name)
96        } else {
97            return Ok(None);
98        };
99
100        let location = self.location_range(start_index..self.index());
101
102        Ok(Some(TextUnit::RawParam { param, location }))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::source::Source;
110    use crate::syntax::Param;
111    use assert_matches::assert_matches;
112    use futures_util::FutureExt as _;
113
114    #[test]
115    fn test_is_portable_name() {
116        assert!(!is_portable_name(""));
117        assert!(is_portable_name("valid_name"));
118        assert!(!is_portable_name("1invalid_name"));
119        assert!(is_portable_name("valid_name_123"));
120        assert!(is_portable_name("_VALID_NAME"));
121    }
122
123    #[test]
124    fn lexer_raw_param_special_parameter() {
125        let mut lexer = Lexer::with_code("$@;");
126        lexer.peek_char().now_or_never().unwrap().unwrap();
127        lexer.consume_char();
128
129        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
130        assert_matches!(result, TextUnit::RawParam { param, location } => {
131            assert_eq!(param, Param::from(SpecialParam::At));
132            assert_eq!(*location.code.value.borrow(), "$@;");
133            assert_eq!(location.code.start_line_number.get(), 1);
134            assert_eq!(*location.code.source, Source::Unknown);
135            assert_eq!(location.range, 0..2);
136        });
137
138        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
139    }
140
141    #[test]
142    fn lexer_raw_param_digit() {
143        let mut lexer = Lexer::with_code("$12");
144        lexer.peek_char().now_or_never().unwrap().unwrap();
145        lexer.consume_char();
146
147        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
148        assert_matches!(result, TextUnit::RawParam { param, location } => {
149            assert_eq!(param, Param::from(1));
150            assert_eq!(*location.code.value.borrow(), "$12");
151            assert_eq!(location.code.start_line_number.get(), 1);
152            assert_eq!(*location.code.source, Source::Unknown);
153            assert_eq!(location.range, 0..2);
154        });
155
156        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('2')));
157    }
158
159    #[test]
160    fn lexer_raw_param_posix_name() {
161        let mut lexer = Lexer::with_code("$az_AZ_019<");
162        lexer.peek_char().now_or_never().unwrap().unwrap();
163        lexer.consume_char();
164
165        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
166        assert_matches!(result, TextUnit::RawParam { param, location } => {
167            assert_eq!(param, Param::variable("az_AZ_019"));
168            assert_eq!(*location.code.value.borrow(), "$az_AZ_019<");
169            assert_eq!(location.code.start_line_number.get(), 1);
170            assert_eq!(*location.code.source, Source::Unknown);
171            assert_eq!(location.range, 0..10);
172        });
173
174        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('<')));
175    }
176
177    #[test]
178    fn lexer_raw_param_posix_name_line_continuations() {
179        let mut lexer = Lexer::with_code("$a\\\n\\\nb\\\n\\\nc\\\n>");
180        lexer.peek_char().now_or_never().unwrap().unwrap();
181        lexer.consume_char();
182
183        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
184        assert_matches!(result, TextUnit::RawParam { param, location } => {
185            assert_eq!(param, Param::variable("abc"));
186            assert_eq!(*location.code.value.borrow(), "$a\\\n\\\nb\\\n\\\nc\\\n>");
187            assert_eq!(location.code.start_line_number.get(), 1);
188            assert_eq!(*location.code.source, Source::Unknown);
189            assert_eq!(location.range, 0..14);
190        });
191
192        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('>')));
193    }
194
195    #[test]
196    fn lexer_raw_param_not_parameter() {
197        let mut lexer = Lexer::with_code("$;");
198        lexer.peek_char().now_or_never().unwrap().unwrap();
199        lexer.consume_char();
200        assert_eq!(lexer.raw_param(0).now_or_never().unwrap(), Ok(None));
201        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
202    }
203}