use nu_protocol::{ParseError, Span};
#[path = "delimiter_diagnostics.rs"]
mod delimiter_diagnostics;
use delimiter_diagnostics::{
closing_delimiter_str, quote_delimiter_str, unbalanced_closer, unclosed_from_open,
};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum TokenContents {
Item,
Comment,
Pipe,
PipePipe,
AssignmentOperator,
ErrGreaterPipe,
OutErrGreaterPipe,
Semicolon,
OutGreaterThan,
OutGreaterGreaterThan,
ErrGreaterThan,
ErrGreaterGreaterThan,
OutErrGreaterThan,
OutErrGreaterGreaterThan,
Eol,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Token {
pub contents: TokenContents,
pub span: Span,
}
impl Token {
pub fn new(contents: TokenContents, span: Span) -> Token {
Token { contents, span }
}
}
#[derive(Clone, Copy, Debug)]
pub enum BlockKind {
Paren,
CurlyBracket,
SquareBracket,
AngleBracket,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct OpenFrame {
pub kind: BlockKind,
pub open_span: Span,
}
fn is_item_terminator(
block_level: &[OpenFrame],
c: u8,
additional_whitespace: &[u8],
special_tokens: &[u8],
) -> bool {
block_level.is_empty()
&& (c == b' '
|| c == b'\t'
|| c == b'\n'
|| c == b'\r'
|| c == b'|'
|| c == b';'
|| additional_whitespace.contains(&c)
|| special_tokens.contains(&c))
}
pub fn is_assignment_operator(bytes: &[u8]) -> bool {
matches!(bytes, b"=" | b"+=" | b"++=" | b"-=" | b"*=" | b"/=")
}
fn is_special_item(block_level: &[OpenFrame], c: u8, special_tokens: &[u8]) -> bool {
block_level.is_empty() && special_tokens.contains(&c)
}
#[derive(Clone, Copy, Debug)]
struct CloserLabelHint {
open_span: Span,
expected_span: Span,
}
fn continues_onto_next_line(c: u8) -> bool {
matches!(
c,
b'|' | b'{' | b'(' | b'[' | b',' | b':' | b'+' | b'-' | b'*' | b'/' | b'=' | b'.'
)
}
pub(crate) fn interp_subexpr_step<T>(stack: &mut Vec<(u8, T)>, byte: u8, open: T) -> bool {
match stack.last() {
Some(&(expected, _)) if expected != b')' => {
if expected == b'"' && byte == b'\\' {
return true;
}
if byte == expected {
stack.pop();
}
}
_ => match byte {
b'\'' | b'"' | b'`' => stack.push((byte, open)),
b'(' => stack.push((b')', open)),
b')' => {
stack.pop();
}
_ => {}
},
}
false
}
pub fn lex_item(
input: &[u8],
curr_offset: &mut usize,
span_offset: usize,
additional_whitespace: &[u8],
special_tokens: &[u8],
in_signature: bool,
) -> (Token, Option<ParseError>) {
let mut quote_start: Option<(u8, Span)> = None;
let mut quote_is_interp = false;
let mut interp_expr_level: Vec<(u8, Span)> = vec![];
let mut in_comment = false;
let token_start = *curr_offset;
let mut block_level: Vec<OpenFrame> = vec![];
let mut closer_label_hint: Option<CloserLabelHint> = None;
let mut at_line_start = true;
let mut prev_line_continue = false;
let mut last_sig_char: Option<u8> = None;
let mut previous_char = None;
while let Some(c) = input.get(*curr_offset) {
let c = *c;
if let Some((start, open_span)) = quote_start {
if !interp_expr_level.is_empty() {
let open = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
if interp_subexpr_step(&mut interp_expr_level, c, open)
&& input.get(*curr_offset + 1).is_some()
{
*curr_offset += 2;
previous_char = Some(c);
at_line_start = false;
continue;
}
last_sig_char = Some(c);
at_line_start = false;
*curr_offset += 1;
previous_char = Some(c);
continue;
}
if c == b'\\' && start == b'"' {
if input.get(*curr_offset + 1).is_some() {
*curr_offset += 2;
previous_char = Some(c);
at_line_start = false;
continue;
} else {
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
let end_span = if span.end > span.start {
Span::new(span.end - 1, span.end)
} else {
span
};
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unclosed_from_open(
input,
span_offset,
quote_delimiter_str(start),
open_span,
end_span,
)),
);
}
}
if c == start {
quote_start = None;
} else if quote_is_interp && c == b'(' {
interp_expr_level.push((
b')',
Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1),
));
}
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'#' && !in_comment {
in_comment = previous_char
.map(char::from)
.map(char::is_whitespace)
.unwrap_or(true);
} else if c == b'\n' || c == b'\r' {
in_comment = false;
if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
break;
}
let is_newline_end = c == b'\n' || input.get(*curr_offset + 1) != Some(&b'\n');
if is_newline_end {
prev_line_continue = last_sig_char.is_some_and(continues_onto_next_line);
at_line_start = true;
last_sig_char = None;
}
} else if in_comment {
if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
break;
}
} else if is_special_item(&block_level, c, special_tokens) && token_start == *curr_offset {
*curr_offset += 1;
break;
} else if c == b'\'' || c == b'"' || c == b'`' {
let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
quote_start = Some((c, open_span));
quote_is_interp = c != b'`' && previous_char == Some(b'$');
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'[' {
let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
block_level.push(OpenFrame {
kind: BlockKind::SquareBracket,
open_span,
});
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'<' && in_signature {
let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
block_level.push(OpenFrame {
kind: BlockKind::AngleBracket,
open_span,
});
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'>' && in_signature {
if let Some(OpenFrame {
kind: BlockKind::AngleBracket,
..
}) = block_level.last()
{
let _ = block_level.pop();
}
last_sig_char = Some(c);
at_line_start = false;
} else if c == b']' {
if let Some(OpenFrame {
kind: BlockKind::SquareBracket,
..
}) = block_level.last()
{
let _ = block_level.pop();
} else if !block_level.is_empty() {
*curr_offset += 1;
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
let close_span = Span::new(span.end - 1, span.end);
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unbalanced_closer("]", "[", &block_level, close_span)),
);
}
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'{' {
if closer_label_hint.is_none()
&& let Some(frame) = block_level.last()
&& matches!(frame.kind, BlockKind::SquareBracket)
{
closer_label_hint = Some(CloserLabelHint {
open_span: frame.open_span,
expected_span: Span::new(
span_offset + *curr_offset,
span_offset + *curr_offset + 1,
),
});
}
let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
block_level.push(OpenFrame {
kind: BlockKind::CurlyBracket,
open_span,
});
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'}' {
if let Some(OpenFrame {
kind: BlockKind::CurlyBracket,
..
}) = block_level.last()
{
let _ = block_level.pop();
} else {
*curr_offset += 1;
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
let close_span = Span::new(span.end - 1, span.end);
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unbalanced_closer("}", "{", &block_level, close_span)),
);
}
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'(' {
let open_span = Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1);
block_level.push(OpenFrame {
kind: BlockKind::Paren,
open_span,
});
last_sig_char = Some(c);
at_line_start = false;
} else if c == b')' {
if let Some(OpenFrame {
kind: BlockKind::Paren,
..
}) = block_level.last()
{
let _ = block_level.pop();
} else {
*curr_offset += 1;
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
let close_span = Span::new(span.end - 1, span.end);
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unbalanced_closer(")", "(", &block_level, close_span)),
);
}
last_sig_char = Some(c);
at_line_start = false;
} else if c == b'r' && input.get(*curr_offset + 1) == Some(b'#').as_ref() {
let lex_result = lex_raw_string(input, curr_offset, span_offset);
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
if let Err(e) = lex_result {
return (
Token {
contents: TokenContents::Item,
span,
},
Some(e),
);
}
last_sig_char = Some(b'#');
at_line_start = false;
} else if c == b'|' && is_redirection(&input[token_start..*curr_offset]) {
*curr_offset += 1;
break;
} else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) {
break;
} else if !c.is_ascii_whitespace() {
if c == b'|'
&& at_line_start
&& !prev_line_continue
&& closer_label_hint.is_none()
&& let Some(frame) = block_level
.iter()
.rev()
.find(|f| matches!(f.kind, BlockKind::CurlyBracket))
{
closer_label_hint = Some(CloserLabelHint {
open_span: frame.open_span,
expected_span: Span::new(
span_offset + *curr_offset,
span_offset + *curr_offset + 1,
),
});
}
last_sig_char = Some(c);
at_line_start = false;
} else if at_line_start && (c == b' ' || c == b'\t') {
} else {
at_line_start = false;
}
*curr_offset += 1;
previous_char = Some(c);
}
let span = Span::new(span_offset + token_start, span_offset + *curr_offset);
let end_span = if span.end > span.start {
Span::new(span.end - 1, span.end)
} else {
span
};
if let Some((closer, open_span)) = interp_expr_level.first() {
let closer_str = match closer {
b')' => ")",
delim => quote_delimiter_str(*delim),
};
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unclosed_from_open(
input,
span_offset,
closer_str,
*open_span,
end_span,
)),
);
}
if let Some((delim, open_span)) = quote_start {
return (
Token {
contents: TokenContents::Item,
span,
},
Some(unclosed_from_open(
input,
span_offset,
quote_delimiter_str(delim),
open_span,
end_span,
)),
);
}
if let Some(frame) = block_level.last() {
let (label_open, label_end) = closer_label_hint
.filter(|h| h.open_span == frame.open_span)
.map(|h| (h.open_span, h.expected_span))
.unwrap_or((frame.open_span, end_span));
let cause = unclosed_from_open(
input,
span_offset,
closing_delimiter_str(frame.kind),
label_open,
label_end,
);
return (
Token {
contents: TokenContents::Item,
span,
},
Some(cause),
);
}
if *curr_offset - token_start == 0 {
return (
Token {
contents: TokenContents::Item,
span,
},
Some(ParseError::UnexpectedEof("command".to_string(), span)),
);
}
let mut err = None;
let output = match &input[(span.start - span_offset)..(span.end - span_offset)] {
bytes if is_assignment_operator(bytes) => Token {
contents: TokenContents::AssignmentOperator,
span,
},
b"out>" | b"o>" => Token {
contents: TokenContents::OutGreaterThan,
span,
},
b"out>>" | b"o>>" => Token {
contents: TokenContents::OutGreaterGreaterThan,
span,
},
b"out>|" | b"o>|" => {
err = Some(ParseError::Expected(
"`|`. Redirecting stdout to a pipe is the same as normal piping.",
span,
));
Token {
contents: TokenContents::Pipe,
span,
}
}
b"err>" | b"e>" => Token {
contents: TokenContents::ErrGreaterThan,
span,
},
b"err>>" | b"e>>" => Token {
contents: TokenContents::ErrGreaterGreaterThan,
span,
},
b"err>|" | b"e>|" => Token {
contents: TokenContents::ErrGreaterPipe,
span,
},
b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token {
contents: TokenContents::OutErrGreaterThan,
span,
},
b"out+err>>" | b"err+out>>" | b"o+e>>" | b"e+o>>" => Token {
contents: TokenContents::OutErrGreaterGreaterThan,
span,
},
b"out+err>|" | b"err+out>|" | b"o+e>|" | b"e+o>|" => Token {
contents: TokenContents::OutErrGreaterPipe,
span,
},
b"&&" => {
err = Some(ParseError::ShellAndAnd(span));
Token {
contents: TokenContents::Pipe,
span,
}
}
b"2>" => {
err = Some(ParseError::ShellErrRedirect(span));
Token {
contents: TokenContents::ErrGreaterThan,
span,
}
}
b"2>&1" => {
err = Some(ParseError::ShellOutErrRedirect(span));
Token {
contents: TokenContents::Pipe,
span,
}
}
_ => Token {
contents: TokenContents::Item,
span,
},
};
(output, err)
}
fn lex_raw_string(
input: &[u8],
curr_offset: &mut usize,
span_offset: usize,
) -> Result<(), ParseError> {
let mut prefix_sharp_cnt = 0;
let start = *curr_offset;
while let Some(b'#') = input.get(start + prefix_sharp_cnt + 1) {
prefix_sharp_cnt += 1;
}
*curr_offset += prefix_sharp_cnt + 1;
if input.get(*curr_offset) != Some(&b'\'') {
return Err(ParseError::Expected(
"'",
Span::new(span_offset + *curr_offset, span_offset + *curr_offset + 1),
));
}
*curr_offset += 1;
let mut matches = false;
while let Some(ch) = input.get(*curr_offset) {
if *ch == b'#' {
let start_ch = input[*curr_offset - prefix_sharp_cnt];
let postfix = &input[*curr_offset - prefix_sharp_cnt + 1..=*curr_offset];
if start_ch == b'\'' && postfix.iter().all(|x| *x == b'#') {
matches = true;
break;
}
}
*curr_offset += 1
}
if !matches {
let mut expected = '\''.to_string();
expected.push_str(&"#".repeat(prefix_sharp_cnt));
return Err(ParseError::UnexpectedEof(
expected,
Span::new(span_offset + *curr_offset - 1, span_offset + *curr_offset),
));
}
Ok(())
}
pub fn lex_signature(
input: &[u8],
span_offset: usize,
additional_whitespace: &[u8],
special_tokens: &[u8],
skip_comment: bool,
) -> (Vec<Token>, Option<ParseError>) {
let mut state = LexState {
input,
output: Vec::new(),
error: None,
span_offset,
};
lex_internal(
&mut state,
additional_whitespace,
special_tokens,
skip_comment,
true,
None,
);
(state.output, state.error)
}
#[derive(Debug)]
pub struct LexState<'a> {
pub input: &'a [u8],
pub output: Vec<Token>,
pub error: Option<ParseError>,
pub span_offset: usize,
}
pub fn lex_n_tokens(
state: &mut LexState,
additional_whitespace: &[u8],
special_tokens: &[u8],
skip_comment: bool,
max_tokens: usize,
) -> isize {
let n_tokens = state.output.len();
lex_internal(
state,
additional_whitespace,
special_tokens,
skip_comment,
false,
Some(max_tokens),
);
let tokens_n_diff = (state.output.len() as isize) - (n_tokens as isize);
let next_offset = state.output.last().map(|token| token.span.end);
if let Some(next_offset) = next_offset {
state.input = &state.input[next_offset - state.span_offset..];
state.span_offset = next_offset;
}
tokens_n_diff
}
pub fn lex(
input: &[u8],
span_offset: usize,
additional_whitespace: &[u8],
special_tokens: &[u8],
skip_comment: bool,
) -> (Vec<Token>, Option<ParseError>) {
let mut state = LexState {
input,
output: Vec::new(),
error: None,
span_offset,
};
lex_internal(
&mut state,
additional_whitespace,
special_tokens,
skip_comment,
false,
None,
);
(state.output, state.error)
}
fn lex_internal(
state: &mut LexState,
additional_whitespace: &[u8],
special_tokens: &[u8],
skip_comment: bool,
in_signature: bool,
max_tokens: Option<usize>,
) {
let initial_output_len = state.output.len();
let mut curr_offset = 0;
let mut is_complete = true;
while let Some(c) = state.input.get(curr_offset) {
if max_tokens
.is_some_and(|max_tokens| state.output.len() >= initial_output_len + max_tokens)
{
break;
}
let c = *c;
if c == b'|' {
let idx = curr_offset;
let prev_idx = idx;
curr_offset += 1;
if let Some(c) = state.input.get(curr_offset)
&& *c == b'|'
{
let idx = curr_offset;
curr_offset += 1;
state.output.push(Token::new(
TokenContents::PipePipe,
Span::new(state.span_offset + prev_idx, state.span_offset + idx + 1),
));
continue;
}
if let Some(prev) = state.output.last_mut() {
match prev.contents {
TokenContents::Eol => {
*prev = Token::new(
TokenContents::Pipe,
Span::new(state.span_offset + idx, state.span_offset + idx + 1),
);
let mut offset = 2;
while state.output.len() > offset {
let index = state.output.len() - offset;
if state.output[index].contents == TokenContents::Comment
&& state.output[index - 1].contents == TokenContents::Eol
{
state.output.remove(index - 1);
offset += 1;
} else {
break;
}
}
}
_ => {
state.output.push(Token::new(
TokenContents::Pipe,
Span::new(state.span_offset + idx, state.span_offset + idx + 1),
));
}
}
} else {
state.output.push(Token::new(
TokenContents::Pipe,
Span::new(state.span_offset + idx, state.span_offset + idx + 1),
));
}
is_complete = false;
} else if c == b';' {
if !is_complete && state.error.is_none() {
state.error = Some(ParseError::ExtraTokens(Span::new(
curr_offset,
curr_offset + 1,
)));
}
let idx = curr_offset;
curr_offset += 1;
state.output.push(Token::new(
TokenContents::Semicolon,
Span::new(state.span_offset + idx, state.span_offset + idx + 1),
));
} else if c == b'\r' {
curr_offset += 1;
} else if c == b'\n' {
let idx = curr_offset;
curr_offset += 1;
if !additional_whitespace.contains(&c) {
state.output.push(Token::new(
TokenContents::Eol,
Span::new(state.span_offset + idx, state.span_offset + idx + 1),
));
}
} else if c == b'#' {
let mut start = curr_offset;
while let Some(input) = state.input.get(curr_offset) {
if *input == b'\n' {
if !skip_comment {
state.output.push(Token::new(
TokenContents::Comment,
Span::new(state.span_offset + start, state.span_offset + curr_offset),
));
}
start = curr_offset;
break;
} else {
curr_offset += 1;
}
}
if start != curr_offset && !skip_comment {
state.output.push(Token::new(
TokenContents::Comment,
Span::new(state.span_offset + start, state.span_offset + curr_offset),
));
}
} else if c == b' ' || c == b'\t' || additional_whitespace.contains(&c) {
curr_offset += 1;
} else {
let (token, err) = lex_item(
state.input,
&mut curr_offset,
state.span_offset,
additional_whitespace,
special_tokens,
in_signature,
);
if state.error.is_none() {
state.error = err;
}
is_complete = true;
state.output.push(token);
}
}
}
fn is_redirection(token: &[u8]) -> bool {
matches!(
token,
b"o>" | b"out>" | b"e>" | b"err>" | b"o+e>" | b"e+o>" | b"out+err>" | b"err+out>"
)
}