Implemented subshells and improved error handling
This commit is contained in:
@@ -9,7 +9,7 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
||||
|
||||
let (argv,_) = setup_builtin(argv,job,None)?;
|
||||
|
||||
let new_dir = if let Some((arg,_)) = argv.into_iter().skip(1).next() {
|
||||
let new_dir = if let Some((arg,_)) = argv.into_iter().next() {
|
||||
PathBuf::from(arg)
|
||||
} else {
|
||||
PathBuf::from(env::var("HOME").unwrap())
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::{exec_input, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Span, Tk, TkFlags, TkRule}, Redir, RedirType}, prelude::*, procio::{IoBuf, IoFrame, IoMode}, state::{read_vars, write_meta, LogTab}};
|
||||
use crate::state::{read_vars, write_meta, LogTab};
|
||||
use crate::procio::{IoBuf, IoFrame, IoMode};
|
||||
use crate::prelude::*;
|
||||
use crate::parse::{Redir, RedirType};
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule};
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
|
||||
/// Variable substitution marker
|
||||
pub const VAR_SUB: char = '\u{fdd0}';
|
||||
@@ -10,6 +16,8 @@ pub const DUB_QUOTE: char = '\u{fdd1}';
|
||||
pub const SNG_QUOTE: char = '\u{fdd2}';
|
||||
/// Tilde sub marker
|
||||
pub const TILDE_SUB: char = '\u{fdd3}';
|
||||
/// Subshell marker
|
||||
pub const SUBSH: char = '\u{fdd4}';
|
||||
|
||||
impl Tk {
|
||||
/// Create a new expanded token
|
||||
@@ -18,7 +26,9 @@ impl Tk {
|
||||
/// tokens: A vector of raw tokens lexed from the expansion result
|
||||
/// span: The span of the original token that is being expanded
|
||||
/// flags: some TkFlags
|
||||
pub fn expand(self, span: Span, flags: TkFlags) -> ShResult<Self> {
|
||||
pub fn expand(self) -> ShResult<Self> {
|
||||
let flags = self.flags;
|
||||
let span = self.span.clone();
|
||||
let exp = Expander::new(self).expand()?;
|
||||
let class = TkRule::Expanded { exp };
|
||||
Ok(Self { class, span, flags, })
|
||||
@@ -57,7 +67,9 @@ impl Expander {
|
||||
|
||||
'outer: while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
DUB_QUOTE | SNG_QUOTE => {
|
||||
DUB_QUOTE |
|
||||
SNG_QUOTE |
|
||||
SUBSH => {
|
||||
while let Some(q_ch) = chars.next() {
|
||||
match q_ch {
|
||||
_ if q_ch == ch => continue 'outer, // Isn't rust cool
|
||||
@@ -119,7 +131,7 @@ impl Expander {
|
||||
var_name.clear();
|
||||
break
|
||||
}
|
||||
_ if is_hard_sep(ch) || ch == DUB_QUOTE => {
|
||||
_ if is_hard_sep(ch) || ch == DUB_QUOTE || ch == SUBSH => {
|
||||
let var_val = read_vars(|v| v.get_var(&var_name));
|
||||
result.push_str(&var_val);
|
||||
result.push(ch);
|
||||
@@ -211,6 +223,35 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
result.push(next_ch)
|
||||
}
|
||||
}
|
||||
'(' => {
|
||||
result.push(SUBSH);
|
||||
let mut paren_count = 1;
|
||||
while let Some(subsh_ch) = chars.next() {
|
||||
match subsh_ch {
|
||||
'\\' => {
|
||||
result.push(subsh_ch);
|
||||
if let Some(next_ch) = chars.next() {
|
||||
result.push(next_ch)
|
||||
}
|
||||
}
|
||||
'$' => result.push(VAR_SUB),
|
||||
'(' => {
|
||||
paren_count += 1;
|
||||
result.push(subsh_ch)
|
||||
}
|
||||
')' => {
|
||||
paren_count -= 1;
|
||||
if paren_count == 0 {
|
||||
result.push(SUBSH);
|
||||
} else {
|
||||
result.push(subsh_ch)
|
||||
}
|
||||
break
|
||||
}
|
||||
_ => result.push(subsh_ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
result.push(DUB_QUOTE);
|
||||
while let Some(q_ch) = chars.next() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
|
||||
|
||||
use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}};
|
||||
use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, get_snapshots, read_logic, read_vars, restore_snapshot, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}};
|
||||
|
||||
use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType};
|
||||
|
||||
@@ -97,6 +97,8 @@ impl Dispatcher {
|
||||
self.exec_builtin(node)
|
||||
} else if is_func(node.get_command().cloned()) {
|
||||
self.exec_func(node)
|
||||
} else if is_subsh(node.get_command().cloned()) {
|
||||
self.exec_subsh(node)
|
||||
} else {
|
||||
self.exec_cmd(node)
|
||||
}
|
||||
@@ -151,7 +153,29 @@ impl Dispatcher {
|
||||
write_logic(|l| l.insert_func(name, func)); // Store the AST
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_func(&mut self, func: Node) -> ShResult<()> {
|
||||
fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> {
|
||||
let NdRule::Command { assignments, argv } = subsh.class else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.set_assignments(assignments, AssignBehavior::Export);
|
||||
self.io_stack.append_to_frame(subsh.redirs);
|
||||
let mut argv = prepare_argv(argv)?;
|
||||
|
||||
let subsh = argv.remove(0);
|
||||
let subsh_body = subsh.0.to_string();
|
||||
flog!(DEBUG, subsh_body);
|
||||
let snapshot = get_snapshots();
|
||||
|
||||
if let Err(e) = exec_input(subsh_body) {
|
||||
restore_snapshot(snapshot);
|
||||
return Err(e.into())
|
||||
}
|
||||
|
||||
restore_snapshot(snapshot);
|
||||
Ok(())
|
||||
}
|
||||
fn exec_func(&mut self, func: Node) -> ShResult<()> {
|
||||
let blame = func.get_span().clone();
|
||||
let NdRule::Command { assignments, mut argv } = func.class else {
|
||||
unreachable!()
|
||||
@@ -163,7 +187,7 @@ impl Dispatcher {
|
||||
|
||||
let func_name = argv.remove(0).span.as_str().to_string();
|
||||
if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
|
||||
let scope_snapshot = read_vars(|v| v.clone());
|
||||
let snapshot = get_snapshots();
|
||||
// Set up the inner scope
|
||||
write_vars(|v| {
|
||||
**v = VarTab::new();
|
||||
@@ -174,19 +198,21 @@ impl Dispatcher {
|
||||
});
|
||||
|
||||
if let Err(e) = self.exec_brc_grp((*func).clone()) {
|
||||
write_vars(|v| **v = scope_snapshot);
|
||||
restore_snapshot(snapshot);
|
||||
match e.kind() {
|
||||
ShErrKind::FuncReturn(code) => {
|
||||
state::set_status(*code);
|
||||
return Ok(())
|
||||
}
|
||||
_ => return Err(e.into())
|
||||
_ => return {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return to the outer scope
|
||||
write_vars(|v| **v = scope_snapshot);
|
||||
restore_snapshot(snapshot);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(
|
||||
@@ -198,7 +224,7 @@ impl Dispatcher {
|
||||
)
|
||||
}
|
||||
}
|
||||
pub fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
|
||||
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
|
||||
let NdRule::BraceGrp { body } = brc_grp.class else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -213,7 +239,7 @@ impl Dispatcher {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> {
|
||||
fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> {
|
||||
let NdRule::CaseNode { pattern, case_blocks } = case_stmt.class else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -221,7 +247,7 @@ impl Dispatcher {
|
||||
self.io_stack.append_to_frame(case_stmt.redirs);
|
||||
|
||||
flog!(DEBUG,pattern.span.as_str());
|
||||
let exp_pattern = pattern.clone().expand(pattern.span.clone(), pattern.flags.clone())?;
|
||||
let exp_pattern = pattern.clone().expand()?;
|
||||
let pattern_raw = exp_pattern
|
||||
.get_words()
|
||||
.first()
|
||||
@@ -246,7 +272,7 @@ impl Dispatcher {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> {
|
||||
fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> {
|
||||
let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else {
|
||||
unreachable!();
|
||||
};
|
||||
@@ -297,7 +323,7 @@ impl Dispatcher {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
|
||||
fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
|
||||
let NdRule::IfNode { cond_nodes, else_block } = if_stmt.class else {
|
||||
unreachable!();
|
||||
};
|
||||
@@ -337,7 +363,7 @@ impl Dispatcher {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
|
||||
fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
|
||||
let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -362,7 +388,7 @@ impl Dispatcher {
|
||||
dispatch_job(job, is_bg)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
|
||||
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
|
||||
let NdRule::Command { ref mut assignments, argv: _ } = &mut cmd.class else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -402,7 +428,7 @@ impl Dispatcher {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
|
||||
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
|
||||
let NdRule::Command { assignments, argv } = cmd.class else {
|
||||
unreachable!()
|
||||
};
|
||||
@@ -438,7 +464,7 @@ impl Dispatcher {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> Vec<String> {
|
||||
fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> Vec<String> {
|
||||
let mut new_env_vars = vec![];
|
||||
match behavior {
|
||||
AssignBehavior::Export => {
|
||||
@@ -483,9 +509,8 @@ pub fn prepare_argv(argv: Vec<Tk>) -> ShResult<Vec<(String,Span)>> {
|
||||
let mut args = vec![];
|
||||
|
||||
for arg in argv {
|
||||
let flags = arg.flags;
|
||||
let span = arg.span.clone();
|
||||
let expanded = arg.expand(span.clone(), flags)?;
|
||||
let expanded = arg.expand()?;
|
||||
for exp in expanded.get_words() {
|
||||
args.push((exp,span.clone()))
|
||||
}
|
||||
@@ -602,3 +627,7 @@ pub fn is_func(tk: Option<Tk>) -> bool {
|
||||
};
|
||||
read_logic(|l| l.get_func(&tk.to_string())).is_some()
|
||||
}
|
||||
|
||||
pub fn is_subsh(tk: Option<Tk>) -> bool {
|
||||
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
|
||||
}
|
||||
|
||||
@@ -401,6 +401,7 @@ impl LexStream {
|
||||
}
|
||||
}
|
||||
if !paren_stack.is_empty() {
|
||||
self.cursor = pos;
|
||||
return Err(
|
||||
ShErr::full(
|
||||
ShErrKind::ParseErr,
|
||||
|
||||
@@ -39,18 +39,14 @@ impl ParsedSrc {
|
||||
}
|
||||
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
|
||||
let mut tokens = vec![];
|
||||
let mut errors = vec![];
|
||||
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
|
||||
match lex_result {
|
||||
Ok(token) => tokens.push(token),
|
||||
Err(error) => errors.push(error)
|
||||
Err(error) => return Err(vec![error])
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(errors)
|
||||
}
|
||||
|
||||
let mut errors = vec![];
|
||||
let mut nodes = vec![];
|
||||
for parse_result in ParseStream::new(tokens) {
|
||||
flog!(DEBUG, parse_result);
|
||||
@@ -59,7 +55,6 @@ impl ParsedSrc {
|
||||
Err(error) => errors.push(error)
|
||||
}
|
||||
}
|
||||
flog!(DEBUG, errors);
|
||||
|
||||
if !errors.is_empty() {
|
||||
return Err(errors)
|
||||
@@ -1108,7 +1103,7 @@ impl Iterator for ParseStream {
|
||||
flog!(DEBUG, "parsing");
|
||||
flog!(DEBUG, self.tokens);
|
||||
// Empty token vector or only SOI/EOI tokens, nothing to do
|
||||
if self.tokens.is_empty() || self.tokens.len() == 2 {
|
||||
if self.tokens.is_empty() || self.tokens.len() == 1 {
|
||||
return None
|
||||
}
|
||||
while let Some(tk) = self.tokens.first() {
|
||||
|
||||
29
src/state.rs
29
src/state.rs
@@ -184,6 +184,16 @@ impl VarTab {
|
||||
self.bpush_arg(arg);
|
||||
}
|
||||
}
|
||||
pub fn update_exports(&mut self) {
|
||||
for var_name in self.vars.keys() {
|
||||
let var = self.vars.get(var_name).unwrap();
|
||||
if var.export {
|
||||
env::set_var(var_name, &var.value);
|
||||
} else {
|
||||
env::set_var(var_name, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn sh_argv(&self) -> &VecDeque<String> {
|
||||
&self.sh_argv
|
||||
}
|
||||
@@ -379,6 +389,25 @@ pub fn set_status(code: i32) {
|
||||
write_vars(|v| v.set_param('?', &code.to_string()))
|
||||
}
|
||||
|
||||
/// Save the current state of the logic and variable table, and the working directory path
|
||||
pub fn get_snapshots() -> (LogTab, VarTab, String) {
|
||||
(
|
||||
read_logic(|l| l.clone()),
|
||||
read_vars(|v| v.clone()),
|
||||
env::var("PWD").unwrap_or_default()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) {
|
||||
write_logic(|l| **l = snapshot.0);
|
||||
write_vars(|v| {
|
||||
**v = snapshot.1;
|
||||
v.update_exports();
|
||||
});
|
||||
env::set_current_dir(&snapshot.2).unwrap();
|
||||
env::set_var("PWD", &snapshot.2);
|
||||
}
|
||||
|
||||
pub fn source_rc() -> ShResult<()> {
|
||||
let path = if let Ok(path) = env::var("FERN_RC") {
|
||||
PathBuf::from(&path)
|
||||
|
||||
@@ -10,6 +10,58 @@ fn cmd_not_found() {
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_subsh() {
|
||||
let input = "(foo";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!("{:?}",token);
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_dquote() {
|
||||
let input = "\"foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_squote() {
|
||||
let input = "'foo bar";
|
||||
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).skip(1).next().unwrap();
|
||||
let Err(err) = token else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unclosed_brc_grp() {
|
||||
let input = "{ foo bar";
|
||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let node = ParseStream::new(tokens).next().unwrap();
|
||||
let Err(err) = node else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
let err_fmt = format!("{err}");
|
||||
insta::assert_snapshot!(err_fmt)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn if_no_fi() {
|
||||
let input = "if foo; then bar;";
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::*;
|
||||
#[test]
|
||||
fn simple_expansion() {
|
||||
let varsub = "$foo";
|
||||
write_vars(|v| v.set_var("foo", "this is the value of the variable".into()));
|
||||
write_vars(|v| v.set_var("foo", "this is the value of the variable".into(), false));
|
||||
|
||||
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
||||
.map(|tk| tk.unwrap())
|
||||
@@ -14,7 +14,7 @@ fn simple_expansion() {
|
||||
let var_tk = tokens.pop().unwrap();
|
||||
|
||||
let var_span = var_tk.span.clone();
|
||||
let exp_tk = var_tk.expand(var_span, TkFlags::empty()).unwrap();
|
||||
let exp_tk = var_tk.expand().unwrap();
|
||||
write_vars(|v| v.vars_mut().clear());
|
||||
insta::assert_debug_snapshot!(exp_tk.get_words())
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ expression: err_fmt
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m case foo foo) bar;; bar) foo;; esac
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
|
||||
@@ -6,5 +6,5 @@ expression: err_fmt
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m if foo; bar; fi
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
|
||||
@@ -6,5 +6,5 @@ expression: err_fmt
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m while true; echo foo; done
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^^^^^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[31m[1mParse Error[0m - Expected a closing brace for this brace group
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m { foo bar
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
10
src/tests/snapshots/fern__tests__error__unclosed_dquote.snap
Normal file
10
src/tests/snapshots/fern__tests__error__unclosed_dquote.snap
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[31m[1mParse Error[0m - Unterminated quote
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m "foo bar
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
10
src/tests/snapshots/fern__tests__error__unclosed_squote.snap
Normal file
10
src/tests/snapshots/fern__tests__error__unclosed_squote.snap
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[31m[1mParse Error[0m - Unterminated quote
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m 'foo bar
|
||||
[36m[1m |[0m [31m[1m^^^^^^^^[0m
|
||||
[36m[1m |[0m
|
||||
10
src/tests/snapshots/fern__tests__error__unclosed_subsh.snap
Normal file
10
src/tests/snapshots/fern__tests__error__unclosed_subsh.snap
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
source: src/tests/error.rs
|
||||
expression: err_fmt
|
||||
---
|
||||
[31m[1mParse Error[0m - Unclosed subshell
|
||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
||||
[36m[1m |[0m
|
||||
[36m[1m1 |[0m (foo
|
||||
[36m[1m |[0m [31m[1m^[0m
|
||||
[36m[1m |[0m
|
||||
Reference in New Issue
Block a user