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.
44pub fn is_portable_name(s: &str) -> bool {
45    s.starts_with(|c: char| !c.is_ascii_digit()) && s.chars().all(is_portable_name_char)
46}
47
48/// Tests if a character names a special parameter.
49///
50/// A special parameter is one of: `@*#?-$!0`.
51pub const fn is_special_parameter_char(c: char) -> bool {
52    SpecialParam::from_char(c).is_some()
53}
54
55/// Tests if a character is a valid single-character raw parameter.
56///
57/// If this function returns true, the character is a valid parameter for a raw
58/// parameter expansion, but the next character is never treated as part of the
59/// parameter.
60///
61/// This function returns true for ASCII digits and special parameters.
62pub const fn is_single_char_name(c: char) -> bool {
63    c.is_ascii_digit() || is_special_parameter_char(c)
64}
65
66impl Lexer<'_> {
67    /// Parses a parameter expansion that is not enclosed in braces.
68    ///
69    /// The initial `$` must have been consumed before calling this function.
70    /// This functions checks if the next character is a valid POSIXly-portable
71    /// parameter name. If so, the name is consumed and returned. Otherwise, no
72    /// characters are consumed and the return value is `Ok(None)`.
73    ///
74    /// The `start_index` parameter should be the index for the initial `$`. It is
75    /// used to construct the result, but this function does not check if it
76    /// actually points to the `$`.
77    pub async fn raw_param(&mut self, start_index: usize) -> Result<Option<TextUnit>> {
78        let param = if let Some(c) = self.consume_char_if(is_special_parameter_char).await? {
79            Param {
80                id: c.value.to_string(),
81                r#type: SpecialParam::from_char(c.value).unwrap().into(),
82            }
83        } else if let Some(c) = self.consume_char_if(|c| c.is_ascii_digit()).await? {
84            Param {
85                id: c.value.to_string(),
86                r#type: ParamType::Positional(c.value.to_digit(10).unwrap() as usize),
87            }
88        } else if let Some(c) = self.consume_char_if(is_portable_name_char).await? {
89            let mut name = c.value.to_string();
90            while let Some(c) = self.consume_char_if(is_portable_name_char).await? {
91                name.push(c.value);
92            }
93            Param::variable(name)
94        } else {
95            return Ok(None);
96        };
97
98        let location = self.location_range(start_index..self.index());
99
100        Ok(Some(TextUnit::RawParam { param, location }))
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::source::Source;
108    use crate::syntax::Param;
109    use assert_matches::assert_matches;
110    use futures_util::FutureExt;
111
112    #[test]
113    fn test_is_portable_name() {
114        assert!(!is_portable_name(""));
115        assert!(is_portable_name("valid_name"));
116        assert!(!is_portable_name("1invalid_name"));
117        assert!(is_portable_name("valid_name_123"));
118        assert!(is_portable_name("_VALID_NAME"));
119    }
120
121    #[test]
122    fn lexer_raw_param_special_parameter() {
123        let mut lexer = Lexer::with_code("$@;");
124        lexer.peek_char().now_or_never().unwrap().unwrap();
125        lexer.consume_char();
126
127        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
128        assert_matches!(result, TextUnit::RawParam { param, location } => {
129            assert_eq!(param, Param::from(SpecialParam::At));
130            assert_eq!(*location.code.value.borrow(), "$@;");
131            assert_eq!(location.code.start_line_number.get(), 1);
132            assert_eq!(*location.code.source, Source::Unknown);
133            assert_eq!(location.range, 0..2);
134        });
135
136        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
137    }
138
139    #[test]
140    fn lexer_raw_param_digit() {
141        let mut lexer = Lexer::with_code("$12");
142        lexer.peek_char().now_or_never().unwrap().unwrap();
143        lexer.consume_char();
144
145        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
146        assert_matches!(result, TextUnit::RawParam { param, location } => {
147            assert_eq!(param, Param::from(1));
148            assert_eq!(*location.code.value.borrow(), "$12");
149            assert_eq!(location.code.start_line_number.get(), 1);
150            assert_eq!(*location.code.source, Source::Unknown);
151            assert_eq!(location.range, 0..2);
152        });
153
154        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('2')));
155    }
156
157    #[test]
158    fn lexer_raw_param_posix_name() {
159        let mut lexer = Lexer::with_code("$az_AZ_019<");
160        lexer.peek_char().now_or_never().unwrap().unwrap();
161        lexer.consume_char();
162
163        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
164        assert_matches!(result, TextUnit::RawParam { param, location } => {
165            assert_eq!(param, Param::variable("az_AZ_019"));
166            assert_eq!(*location.code.value.borrow(), "$az_AZ_019<");
167            assert_eq!(location.code.start_line_number.get(), 1);
168            assert_eq!(*location.code.source, Source::Unknown);
169            assert_eq!(location.range, 0..10);
170        });
171
172        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('<')));
173    }
174
175    #[test]
176    fn lexer_raw_param_posix_name_line_continuations() {
177        let mut lexer = Lexer::with_code("$a\\\n\\\nb\\\n\\\nc\\\n>");
178        lexer.peek_char().now_or_never().unwrap().unwrap();
179        lexer.consume_char();
180
181        let result = lexer.raw_param(0).now_or_never().unwrap().unwrap().unwrap();
182        assert_matches!(result, TextUnit::RawParam { param, location } => {
183            assert_eq!(param, Param::variable("abc"));
184            assert_eq!(*location.code.value.borrow(), "$a\\\n\\\nb\\\n\\\nc\\\n>");
185            assert_eq!(location.code.start_line_number.get(), 1);
186            assert_eq!(*location.code.source, Source::Unknown);
187            assert_eq!(location.range, 0..14);
188        });
189
190        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some('>')));
191    }
192
193    #[test]
194    fn lexer_raw_param_not_parameter() {
195        let mut lexer = Lexer::with_code("$;");
196        lexer.peek_char().now_or_never().unwrap().unwrap();
197        lexer.consume_char();
198        assert_eq!(lexer.raw_param(0).now_or_never().unwrap(), Ok(None));
199        assert_eq!(lexer.peek_char().now_or_never().unwrap(), Ok(Some(';')));
200    }
201}