use super::*;
impl<'a> Parser<'a> {
pub(in crate::parser) fn pattern_from_word(&self, word: &Word) -> Pattern {
PatternParser::new(
self.input,
word,
self.zsh_glob_parse_features_at(word.span.start.offset),
)
.parse()
}
pub(in crate::parser) fn pattern_from_conditional_word(&self, word: &Word) -> Pattern {
let features = self.zsh_glob_parse_features_at(word.span.start.offset);
let source_backed_word = self
.conditional_prefixed_bare_group_source_word(word, features)
.unwrap_or_else(|| word.clone());
PatternParser::for_pattern_context(self.input, &source_backed_word, features).parse()
}
pub(in crate::parser) fn conditional_prefixed_bare_group_source_word(
&self,
word: &Word,
features: ZshGlobParseFeatures,
) -> Option<Word> {
if features.ksh_groups || !features.bare_groups {
return None;
}
let needs_reparse = self.word_is_safe_for_prefixed_bare_group_reparse(word)
&& Self::literal_text_has_prefixed_bare_group(word.span.slice(self.input));
needs_reparse.then(|| {
self.word_with_parts(
vec![WordPartNode::new(
WordPart::Literal(LiteralText::source()),
word.span,
)],
word.span,
)
})
}
pub(in crate::parser) fn word_is_safe_for_prefixed_bare_group_reparse(
&self,
word: &Word,
) -> bool {
word.parts.iter().all(|part| match &part.kind {
WordPart::Literal(text) => text.is_source_backed(),
WordPart::ZshQualifiedGlob(glob) => {
Self::zsh_glob_is_safe_for_prefixed_bare_group_reparse(glob)
}
_ => false,
})
}
pub(in crate::parser) fn zsh_glob_is_safe_for_prefixed_bare_group_reparse(
glob: &ZshQualifiedGlob,
) -> bool {
glob.segments
.iter()
.all(Self::zsh_glob_segment_is_safe_for_prefixed_bare_group_reparse)
&& glob.qualifiers.is_none()
}
pub(in crate::parser) fn zsh_glob_segment_is_safe_for_prefixed_bare_group_reparse(
segment: &ZshGlobSegment,
) -> bool {
match segment {
ZshGlobSegment::Pattern(pattern) => {
Self::pattern_is_safe_for_prefixed_bare_group_reparse(pattern)
}
ZshGlobSegment::InlineControl(_) => true,
}
}
pub(in crate::parser) fn pattern_is_safe_for_prefixed_bare_group_reparse(
pattern: &Pattern,
) -> bool {
pattern.parts.iter().all(|part| match &part.kind {
PatternPart::Literal(text) => text.is_source_backed(),
PatternPart::AnyString | PatternPart::AnyChar => true,
PatternPart::CharClass(text) => text.is_source_backed(),
PatternPart::Group { patterns, .. } => patterns
.iter()
.all(Self::pattern_is_safe_for_prefixed_bare_group_reparse),
PatternPart::Word(_) => false,
})
}
pub(in crate::parser) fn literal_text_has_prefixed_bare_group(text: &str) -> bool {
let mut escaped = false;
let mut bracket_depth = 0usize;
let mut previous_char = None;
for (index, ch) in text.char_indices() {
if escaped {
escaped = false;
previous_char = Some(ch);
continue;
}
if ch == '\\' {
escaped = true;
previous_char = Some(ch);
continue;
}
match ch {
'[' => bracket_depth = bracket_depth.saturating_add(1),
']' if bracket_depth > 0 => bracket_depth -= 1,
'(' if bracket_depth == 0
&& matches!(previous_char, Some('?' | '*' | '+' | '@' | '!'))
&& Self::literal_text_group_has_top_level_separator(text, index) =>
{
return true;
}
_ => {}
}
previous_char = Some(ch);
}
false
}
pub(in crate::parser) fn literal_text_group_has_top_level_separator(
text: &str,
open_index: usize,
) -> bool {
let mut escaped = false;
let mut paren_depth = 0usize;
for ch in text[open_index + '('.len_utf8()..].chars() {
if escaped {
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
match ch {
'(' => paren_depth = paren_depth.saturating_add(1),
')' if paren_depth == 0 => return false,
')' => paren_depth -= 1,
'|' if paren_depth == 0 => return true,
_ => {}
}
}
false
}
pub(in crate::parser) fn pattern_from_zsh_case_span(&mut self, span: Span) -> Pattern {
let text = span.slice(self.input);
let word = if Self::source_text_needs_quote_preserving_decode(text) {
self.decode_fragment_word_text(text, span, span.start, true)
} else {
self.decode_word_text(text, span, span.start, true)
};
let features = self.zsh_glob_parse_features_at(span.start.offset);
self.pattern_from_zsh_case_word_with_features(&word, features)
}
pub(in crate::parser) fn pattern_from_zsh_case_word_with_features(
&self,
word: &Word,
features: ZshGlobParseFeatures,
) -> Pattern {
PatternParser::for_pattern_context(self.input, word, features).parse()
}
pub(in crate::parser) fn pattern_from_source_text(&mut self, text: &SourceText) -> Pattern {
let span = text.span();
if self.source_text_pattern_depth >= SOURCE_TEXT_PATTERN_REPARSE_MAX_DEPTH {
return Pattern {
parts: vec![PatternPartNode::new(
PatternPart::Literal(if text.is_source_backed() {
LiteralText::source()
} else {
LiteralText::owned(text.slice(self.input).to_string())
}),
span,
)],
span,
};
}
self.source_text_pattern_depth += 1;
let mut parts = WordPartBuffer::new();
self.decode_word_parts_into_with_quote_fragments(
text.slice(self.input),
span.start,
text.is_source_backed(),
DecodeWordPartsOptions {
preserve_quote_fragments: true,
parse_dollar_quotes: true,
preserve_escaped_expansion_literals: text.is_source_backed(),
..DecodeWordPartsOptions::default()
},
&mut parts,
);
let pattern = PatternParser::from_word_parts(
self.input,
&parts,
span,
self.zsh_glob_parse_features_at(span.start.offset),
)
.parse();
self.source_text_pattern_depth -= 1;
pattern
}
pub(in crate::parser) fn single_literal_word_text<'b>(
&'b self,
word: &'b Word,
) -> Option<&'b str> {
if word.is_fully_quoted() || word.parts.len() != 1 {
return None;
}
let WordPart::Literal(text) = &word.parts[0].kind else {
return None;
};
Some(text.as_str(self.input, word.part_span(0)?))
}
pub(in crate::parser) fn literal_word_text(&self, word: &Word) -> Option<String> {
let mut text = String::new();
self.collect_literal_word_text(&word.parts, &mut text)?;
Some(text)
}
pub(in crate::parser) fn source_text_needs_quote_preserving_decode(text: &str) -> bool {
text.contains(['\'', '"'])
}
pub(in crate::parser) fn decode_word_text_preserving_quotes_if_needed(
&mut self,
s: &str,
span: Span,
base: Position,
source_backed: bool,
) -> Word {
self.decode_word_text_preserving_quotes_if_needed_with_escape_mode(
s,
span,
base,
source_backed,
source_backed,
)
}
pub(in crate::parser) fn decode_word_text_preserving_quotes_if_needed_with_escape_mode(
&mut self,
s: &str,
span: Span,
base: Position,
source_backed: bool,
preserve_escaped_expansion_literals: bool,
) -> Word {
if !Self::source_text_needs_quote_preserving_decode(s)
&& let Some(word) = self.maybe_parse_zsh_qualified_glob_word(s, span, source_backed)
{
return word;
}
let preserve_quote_fragments = Self::source_text_needs_quote_preserving_decode(s)
&& (!source_backed || self.source_matches(span, s));
if preserve_quote_fragments {
self.decode_fragment_word_text_with_escape_mode(
s,
span,
base,
source_backed,
preserve_escaped_expansion_literals,
)
} else {
self.decode_word_text_with_escape_mode(
s,
span,
base,
source_backed,
preserve_escaped_expansion_literals,
)
}
}
pub(in crate::parser) fn collect_literal_word_text(
&self,
parts: &[WordPartNode],
out: &mut String,
) -> Option<()> {
for part in parts {
match &part.kind {
WordPart::Literal(literal) => out.push_str(literal.as_str(self.input, part.span)),
WordPart::SingleQuoted { value, .. } => out.push_str(value.slice(self.input)),
WordPart::DoubleQuoted { parts, .. } => {
self.collect_literal_word_text(parts, out)?;
}
_ => return None,
}
}
Some(())
}
pub(in crate::parser) fn is_assignment(
word: &str,
allow_zsh_numeric_assignments: bool,
) -> Option<(&str, Option<&str>, &str, bool)> {
if !word.contains('=') {
return None;
}
let mut ident_end = 0;
let mut chars = word.char_indices();
let (_, first) = chars.next()?;
if first.is_ascii_digit() {
if !allow_zsh_numeric_assignments {
return None;
}
ident_end += first.len_utf8();
for (index, ch) in chars {
if ch.is_ascii_digit() {
ident_end = index + ch.len_utf8();
} else {
break;
}
}
} else {
if !first.is_ascii_alphabetic() && first != '_' {
return None;
}
ident_end += first.len_utf8();
for (index, ch) in chars {
if ch.is_ascii_alphanumeric() || ch == '_' {
ident_end = index + ch.len_utf8();
} else {
break;
}
}
}
let name = &word[..ident_end];
let mut cursor = ident_end;
let mut index = None;
if word[cursor..].starts_with('[') {
let mut close_index = None;
let mut bracket_depth = 0_i32;
let mut brace_depth = 0_i32;
let mut paren_depth = 0_i32;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for (relative, ch) in word[cursor + 1..].char_indices() {
let absolute = cursor + 1 + relative;
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'[' if !in_single && !in_double => bracket_depth += 1,
']' if !in_single && !in_double => {
if bracket_depth == 0 && brace_depth == 0 && paren_depth == 0 {
close_index = Some(absolute);
break;
}
bracket_depth -= 1;
}
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
_ => {}
}
}
let close_index = close_index?;
index = Some(&word[cursor + 1..close_index]);
cursor = close_index + 1;
}
let (is_append, value) = if word[cursor..].starts_with("+=") {
(true, &word[cursor + 2..])
} else if word[cursor..].starts_with('=') {
(false, &word[cursor + 1..])
} else {
return None;
};
Some((name, index, value, is_append))
}
pub(in crate::parser) fn scan_split_indexed_assignment(
&self,
start: Position,
) -> Option<(String, Position)> {
if start.offset >= self.input.len() {
return None;
}
let source = &self.input[start.offset..];
let mut chars = source.chars().peekable();
let mut cursor = start;
let mut text = String::new();
let first = *chars.peek()?;
if !first.is_ascii_alphabetic() && first != '_' {
return None;
}
text.push(Self::next_word_char_unwrap(&mut chars, &mut cursor));
text.push_str(&Self::read_word_while(&mut chars, &mut cursor, |c| {
c.is_ascii_alphanumeric() || c == '_'
}));
if chars.peek() != Some(&'[') {
return None;
}
text.push(Self::next_word_char_unwrap(&mut chars, &mut cursor));
let mut bracket_depth = 1_i32;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
while let Some(ch) = Self::next_word_char(&mut chars, &mut cursor) {
text.push(ch);
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'[' if !in_single && !in_double => bracket_depth += 1,
']' if !in_single && !in_double => {
bracket_depth -= 1;
if bracket_depth == 0 {
break;
}
}
_ => {}
}
}
if bracket_depth != 0 {
return None;
}
if chars.peek() == Some(&'+') {
text.push(Self::next_word_char_unwrap(&mut chars, &mut cursor));
}
if chars.peek() != Some(&'=') {
return None;
}
text.push(Self::next_word_char_unwrap(&mut chars, &mut cursor));
let mut paren_depth = 0_i32;
let mut brace_depth = 0_i32;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
while let Some(&ch) = chars.peek() {
if !in_single
&& !in_double
&& paren_depth == 0
&& brace_depth == 0
&& matches!(ch, ' ' | '\t' | '\n' | ';' | '|' | '&' | '>' | '<' | ')')
{
break;
}
let ch = Self::next_word_char_unwrap(&mut chars, &mut cursor);
text.push(ch);
if escaped {
escaped = false;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
_ => {}
}
}
Some((text, cursor))
}
pub(in crate::parser) fn try_parse_split_indexed_assignment_from_text(
&mut self,
) -> Option<Assignment> {
if !self.at(TokenKind::Word) {
return None;
}
let start = self.current_span.start;
let (text, end) = self.scan_split_indexed_assignment(start)?;
let span = Span::from_positions(start, end);
let assignment = self.parse_assignment_from_text(
&text,
span,
None,
SubscriptInterpretation::Contextual,
)?;
while self.current_token.is_some() && self.current_span.start.offset < end.offset {
self.advance();
}
Some(assignment)
}
pub(in crate::parser) fn infer_array_expr_kind(
explicit_kind: Option<ArrayKind>,
elements: &[ArrayElem],
) -> ArrayKind {
explicit_kind.unwrap_or_else(|| {
if elements
.iter()
.any(|element| !matches!(element, ArrayElem::Sequential(_)))
{
ArrayKind::Contextual
} else {
ArrayKind::Indexed
}
})
}
pub(in crate::parser) fn subscript_interpretation_from_array_kind(
explicit_kind: Option<ArrayKind>,
) -> SubscriptInterpretation {
match explicit_kind {
Some(ArrayKind::Indexed) => SubscriptInterpretation::Indexed,
Some(ArrayKind::Associative) => SubscriptInterpretation::Associative,
_ => SubscriptInterpretation::Contextual,
}
}
pub(in crate::parser) fn word_from_raw_text(&mut self, raw: &str, span: Span) -> Word {
if raw.is_empty() {
return Word::literal_with_span("", span);
}
self.parse_word_with_context(raw, span, span.start, self.source_matches(span, raw))
}
pub(in crate::parser) fn array_value_word_from_raw_text(
&mut self,
raw: &str,
span: Span,
) -> ArrayValueWord {
let word = self.word_from_raw_text(raw, span);
let has_top_level_unquoted_comma =
self.raw_text_has_top_level_unquoted_array_comma(raw, &word);
ArrayValueWord::new(word, has_top_level_unquoted_comma)
}
pub(in crate::parser) fn split_compound_array_elements(
&self,
inner: &str,
) -> Vec<(usize, usize)> {
let mut ranges = Vec::new();
let mut start: Option<usize> = None;
let mut bracket_depth = 0_i32;
let mut brace_depth = 0_i32;
let mut paren_depth = 0_i32;
let mut in_single = false;
let mut in_double = false;
let mut in_backtick = false;
let mut escaped = false;
let mut index = 0usize;
while index < inner.len() {
let Some(ch) = inner[index..].chars().next() else {
break;
};
let next_index = index + ch.len_utf8();
if start.is_none() {
if ch.is_whitespace() {
index = next_index;
continue;
}
if ch == '#' {
while index < inner.len() {
let Some(comment_ch) = inner[index..].chars().next() else {
break;
};
index += comment_ch.len_utf8();
if comment_ch == '\n' {
break;
}
}
continue;
}
start = Some(index);
}
if escaped {
escaped = false;
index = next_index;
continue;
}
if ch == '$'
&& !in_single
&& let Some(end) = Self::scan_raw_dollar_paren_substitution_end(inner, index)
{
index = end;
continue;
}
match ch {
'\\' if !in_single => escaped = true,
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'`' if !in_single => in_backtick = !in_backtick,
'[' if !in_single && !in_double => bracket_depth += 1,
']' if !in_single && !in_double && bracket_depth > 0 => bracket_depth -= 1,
'{' if !in_single && !in_double => brace_depth += 1,
'}' if !in_single && !in_double && brace_depth > 0 => brace_depth -= 1,
'(' if !in_single && !in_double => paren_depth += 1,
')' if !in_single && !in_double && paren_depth > 0 => paren_depth -= 1,
'#' if start == Some(index)
&& !in_single
&& !in_double
&& !in_backtick
&& bracket_depth == 0
&& brace_depth == 0
&& paren_depth == 0 =>
{
start = None;
while index < inner.len() {
let Some(comment_ch) = inner[index..].chars().next() else {
break;
};
index += comment_ch.len_utf8();
if comment_ch == '\n' {
break;
}
}
continue;
}
ch if ch.is_whitespace()
&& !in_single
&& !in_double
&& !in_backtick
&& bracket_depth == 0
&& brace_depth == 0
&& paren_depth == 0 =>
{
if let Some(start) = start.take() {
ranges.push((start, index));
}
}
_ => {}
}
index = next_index;
}
if let Some(start) = start {
ranges.push((start, inner.len()));
}
ranges
}
}