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