From ef0f66efaa59ccd0a009db74674868a59c3a3d3d Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 28 Feb 2026 20:30:12 -0500 Subject: [PATCH] Work on integrating error reporting using the ariadne crate --- Cargo.lock | 259 +++++++++++++++++++++- Cargo.toml | 2 + src/builtin/cd.rs | 27 ++- src/builtin/exec.rs | 10 +- src/builtin/test.rs | 51 ++--- src/builtin/zoltraak.rs | 6 +- src/expand.rs | 85 ++++---- src/libsh/error.rs | 460 ++++++++++++++------------------------- src/libsh/term.rs | 2 +- src/libsh/utils.rs | 2 +- src/main.rs | 12 +- src/parse/execute.rs | 62 ++++-- src/parse/lex.rs | 72 ++++-- src/parse/mod.rs | 133 +++++++++-- src/readline/complete.rs | 12 +- src/readline/history.rs | 27 +-- src/readline/mod.rs | 20 +- src/shopt.rs | 61 ------ 18 files changed, 763 insertions(+), 540 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f76851e..57727d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "ariadne" version = "0.6.0" @@ -95,6 +101,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + [[package]] name = "clap" version = "4.5.55" @@ -153,6 +170,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "diff" version = "0.1.13" @@ -188,6 +214,12 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "errno" version = "0.3.14" @@ -204,6 +236,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "getrandom" version = "0.3.4" @@ -216,18 +254,65 @@ dependencies = [ "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + [[package]] name = "insta" version = "1.46.1" @@ -276,6 +361,12 @@ dependencies = [ "syn", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" version = "0.2.180" @@ -349,6 +440,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -373,6 +474,23 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "regex" version = "1.12.2" @@ -415,6 +533,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -470,12 +594,14 @@ dependencies = [ "log", "nix", "pretty_assertions", + "rand", "regex", "serde_json", "tempfile", "unicode-segmentation", "unicode-width", "vte", + "yansi", ] [[package]] @@ -508,7 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -532,6 +658,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "utf8parse" version = "0.2.2" @@ -557,6 +689,49 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "windows-link" version = "0.2.1" @@ -650,6 +825,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "yansi" diff --git a/Cargo.toml b/Cargo.toml index a42e787..6cfd2fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,13 @@ env_logger = "0.11.9" glob = "0.3.2" log = "0.4.29" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } +rand = "0.10.0" regex = "1.11.1" serde_json = "1.0.149" unicode-segmentation = "1.12.0" unicode-width = "0.2.0" vte = "0.15" +yansi = "1.0.1" [dev-dependencies] insta = "1.42.2" diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 3f19a93..0fcef5c 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -1,6 +1,8 @@ +use ariadne::{Fmt, Label, Span}; + use crate::{ jobs::JobBldr, - libsh::error::{ShErr, ShErrKind, ShResult}, + libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::{NdRule, Node}, prelude::*, state::{self}, @@ -10,6 +12,7 @@ use super::setup_builtin; pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { let span = node.get_span(); + let src = span.source(); let NdRule::Command { assignments: _, argv, @@ -17,22 +20,28 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { else { unreachable!() }; + let cd_span = argv.first().unwrap().span.clone(); let (argv, _) = setup_builtin(Some(argv), job, None)?; let argv = argv.unwrap(); - let new_dir = if let Some((arg, _)) = argv.into_iter().next() { - PathBuf::from(arg) + let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() { + (PathBuf::from(arg),Some(span)) } else { - PathBuf::from(env::var("HOME").unwrap()) + (PathBuf::from(env::var("HOME").unwrap()),None) }; if !new_dir.exists() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("cd: No such file or directory '{}'", new_dir.display()), - span, - )); + let color = next_color(); + let mut err = ShErr::new( + ShErrKind::ExecFail, + span.clone(), + ).with_label(src.clone(), Label::new(cd_span.clone()).with_color(color).with_message("Failed to change directory")); + if let Some(span) = arg_span { + let color = next_color(); + err = err.with_label(src.clone(), Label::new(span).with_color(color).with_message(format!("No such file or directory '{}'", new_dir.display().fg(color)))); + } + return Err(err); } if !new_dir.is_dir() { diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index 56de57a..949d2b1 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -1,3 +1,4 @@ +use ariadne::Label; use nix::{errno::Errno, unistd::execvpe}; use crate::{ @@ -41,7 +42,14 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh // execvpe only returns on error let cmd_str = cmd.to_str().unwrap().to_string(); match e { - Errno::ENOENT => Err(ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span)), + Errno::ENOENT => Err( + ShErr::full(ShErrKind::CmdNotFound, "", span.clone()) + .with_label( + span.span_source().clone(), + Label::new(span.clone()) + .with_message(format!("exec: command not found: {}", cmd_str)) + ) + ), _ => Err(ShErr::full(ShErrKind::Errno(e), format!("{e}"), span)), } } diff --git a/src/builtin/test.rs b/src/builtin/test.rs index 063268e..d64815f 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -61,11 +61,10 @@ impl FromStr for UnaryOp { "-t" => Ok(Self::Terminal), "-n" => Ok(Self::NonNull), "-z" => Ok(Self::Null), - _ => Err(ShErr::Simple { - kind: ShErrKind::SyntaxErr, - msg: "Invalid test operator".into(), - notes: vec![], - }), + _ => Err(ShErr::simple( + ShErrKind::SyntaxErr, + "Invalid test operator", + )), } } } @@ -98,11 +97,10 @@ impl FromStr for TestOp { "-ge" => Ok(Self::IntGe), "-le" => Ok(Self::IntLe), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::()?)), - _ => Err(ShErr::Simple { - kind: ShErrKind::SyntaxErr, - msg: "Invalid test operator".into(), - notes: vec![], - }), + _ => Err(ShErr::simple( + ShErrKind::SyntaxErr, + "Invalid test operator", + )), } } } @@ -140,12 +138,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { let operand = operand.expand()?.get_words().join(" "); conjunct_op = conjunct; let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { - return Err(ShErr::Full { - kind: ShErrKind::SyntaxErr, - msg: "Invalid unary operator".into(), - notes: vec![], - span: err_span, - }); + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "Invalid unary operator", + err_span, + )); }; match op { UnaryOp::Exists => { @@ -248,12 +245,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { let test_op = operator.as_str().parse::()?; match test_op { TestOp::Unary(_) => { - return Err(ShErr::Full { - kind: ShErrKind::SyntaxErr, - msg: "Expected a binary operator in this test call; found a unary operator".into(), - notes: vec![], - span: err_span, - }); + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "Expected a binary operator in this test call; found a unary operator", + err_span, + )); } TestOp::StringEq => { let pattern = crate::expand::glob_to_regex(rhs.trim(), true); @@ -269,12 +265,11 @@ pub fn double_bracket_test(node: Node) -> ShResult { | TestOp::IntGe | TestOp::IntLe | TestOp::IntEq => { - let err = ShErr::Full { - kind: ShErrKind::SyntaxErr, - msg: format!("Expected an integer with '{}' operator", operator.as_str()), - notes: vec![], - span: err_span.clone(), - }; + let err = ShErr::full( + ShErrKind::SyntaxErr, + format!("Expected an integer with '{}' operator", operator), + err_span.clone(), + ); let Ok(lhs) = lhs.trim().parse::() else { return Err(err); }; diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index c29e195..9d3cde7 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -117,8 +117,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu "zoltraak: Attempted to destroy root directory '/'", ) .with_note( - Note::new("If you really want to do this, you can use the --no-preserve-root flag") - .with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]), + "If you really want to do this, you can use the --no-preserve-root flag" ), ); } @@ -181,8 +180,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> { format!("zoltraak: '{path}' is a directory"), ) .with_note( - Note::new("Use the '-r' flag to recursively shred directories") - .with_sub_notes(vec!["Example: 'zoltraak -r directory'"]), + "Use the '-r' flag to recursively shred directories" ), ); } diff --git a/src/expand.rs b/src/expand.rs index 8608fb2..78e045f 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -688,11 +688,10 @@ impl ArithTk { chars.next(); } _ => { - return Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Invalid character in arithmetic substitution".into(), - notes: vec![], - }); + return Err(ShErr::simple( + ShErrKind::ParseErr, + "Invalid character in arithmetic substitution", + )); } } } @@ -750,16 +749,14 @@ impl ArithTk { match token { ArithTk::Num(n) => stack.push(n), ArithTk::Op(op) => { - let rhs = stack.pop().ok_or(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Missing right-hand operand".into(), - notes: vec![], - })?; - let lhs = stack.pop().ok_or(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Missing left-hand operand".into(), - notes: vec![], - })?; + let rhs = stack.pop().ok_or(ShErr::simple( + ShErrKind::ParseErr, + "Missing right-hand operand", + ))?; + let lhs = stack.pop().ok_or(ShErr::simple( + ShErrKind::ParseErr, + "Missing left-hand operand", + ))?; let result = match op { ArithOp::Add => lhs + rhs, ArithOp::Sub => lhs - rhs, @@ -770,21 +767,19 @@ impl ArithTk { stack.push(result); } _ => { - return Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Unexpected token during evaluation".into(), - notes: vec![], - }); + return Err(ShErr::simple( + ShErrKind::ParseErr, + "Unexpected token during evaluation", + )); } } } if stack.len() != 1 { - return Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Invalid arithmetic expression".into(), - notes: vec![], - }); + return Err(ShErr::simple( + ShErrKind::ParseErr, + "Invalid arithmetic expression", + )); } Ok(stack[0]) @@ -809,11 +804,10 @@ impl FromStr for ArithOp { '*' => Ok(Self::Mul), '/' => Ok(Self::Div), '%' => Ok(Self::Mod), - _ => Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Invalid arithmetic operator".into(), - notes: vec![], - }), + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + "Invalid arithmetic operator", + )), } } } @@ -863,7 +857,7 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult { io_stack.push_frame(io_frame); if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { - eprintln!("{e}"); + e.print_error(); exit(1); } exit(0); @@ -894,7 +888,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { ForkResult::Child => { io_stack.push_frame(cmd_sub_io_frame); if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { - eprintln!("{e}"); + e.print_error(); unsafe { libc::_exit(1) }; } unsafe { libc::_exit(0) }; @@ -1275,11 +1269,10 @@ impl FromStr for ParamExp { use ParamExp::*; let parse_err = || { - Err(ShErr::Simple { - kind: ShErrKind::SyntaxErr, - msg: "Invalid parameter expansion".into(), - notes: vec![], - }) + Err(ShErr::simple( + ShErrKind::SyntaxErr, + "Invalid parameter expansion", + ) ) }; // Handle indirect var expansion: ${!var} @@ -1437,11 +1430,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { Some(val) => Ok(val), None => { let expanded = expand_raw(&mut err.chars().peekable())?; - Err(ShErr::Simple { - kind: ShErrKind::ExecFail, - msg: expanded, - notes: vec![], - }) + Err(ShErr::simple( + ShErrKind::ExecFail, + expanded, + )) } } } @@ -1449,11 +1441,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { Some(val) => Ok(val), None => { let expanded = expand_raw(&mut err.chars().peekable())?; - Err(ShErr::Simple { - kind: ShErrKind::ExecFail, - msg: expanded, - notes: vec![], - }) + Err(ShErr::simple( + ShErrKind::ExecFail, + expanded, + )) } }, ParamExp::Substr(pos) => { diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 258ddc7..1853795 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,13 +1,63 @@ +use std::cell::RefCell; +use std::collections::HashMap; use std::fmt::Display; +use ariadne::Color; +use ariadne::{Report, ReportKind}; +use rand::{RngExt, TryRng}; use crate::{ libsh::term::{Style, Styled}, - parse::lex::Span, + parse::lex::{Span, SpanSource}, prelude::*, }; pub type ShResult = Result; +pub struct ColorRng; + +impl ColorRng { + fn get_colors() -> &'static [Color] { + &[ + Color::Red, + Color::Cyan, + Color::Blue, + Color::Green, + Color::Yellow, + Color::Magenta, + Color::Fixed(208), // orange + Color::Fixed(39), // deep sky blue + Color::Fixed(170), // orchid / magenta-pink + Color::Fixed(76), // chartreuse + Color::Fixed(51), // aqua + Color::Fixed(226), // bright yellow + Color::Fixed(99), // slate blue + Color::Fixed(214), // light orange + Color::Fixed(48), // spring green + Color::Fixed(201), // hot pink + Color::Fixed(81), // steel blue + Color::Fixed(220), // gold + Color::Fixed(105), // medium purple + ] + } +} + +impl Iterator for ColorRng { + type Item = Color; + fn next(&mut self) -> Option { + let colors = Self::get_colors(); + let idx = rand::rngs::SysRng.try_next_u32().ok()? as usize % colors.len(); + Some(colors[idx]) + } +} + +thread_local! { + static COLOR_RNG: RefCell = const { RefCell::new(ColorRng) }; +} + +pub fn next_color() -> Color { + COLOR_RNG.with(|rng| rng.borrow_mut().next().unwrap()) +} + pub trait ShResultExt { fn blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self; @@ -16,39 +66,11 @@ pub trait ShResultExt { impl ShResultExt for Result { /// Blame a span for an error fn blame(self, new_span: Span) -> Self { - let Err(e) = self else { return self }; - match e { - ShErr::Simple { kind, msg, notes } - | ShErr::Full { - kind, - msg, - notes, - span: _, - } => Err(ShErr::Full { - kind: kind.clone(), - msg: msg.clone(), - notes: notes.clone(), - span: new_span, - }), - } + self.map_err(|e| e.blame(new_span)) } /// Blame a span if no blame has been assigned yet fn try_blame(self, new_span: Span) -> Self { - let Err(e) = &self else { return self }; - match e { - ShErr::Simple { kind, msg, notes } => Err(ShErr::Full { - kind: kind.clone(), - msg: msg.clone(), - notes: notes.clone(), - span: new_span, - }), - ShErr::Full { - kind: _, - msg: _, - span: _, - notes: _, - } => self, - } + self.map_err(|e| e.try_blame(new_span)) } } @@ -107,270 +129,126 @@ impl Display for Note { } #[derive(Debug)] -pub enum ShErr { - Simple { - kind: ShErrKind, - msg: String, - notes: Vec, - }, - Full { - kind: ShErrKind, - msg: String, - notes: Vec, - span: Span, - }, +pub struct ShErr { + kind: ShErrKind, + src_span: Option, + labels: Vec>, + sources: Vec, + notes: Vec } impl ShErr { - pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { - let msg = msg.into(); - Self::Simple { - kind, - msg, - notes: vec![], - } - } - pub fn full(kind: ShErrKind, msg: impl Into, span: Span) -> Self { - let msg = msg.into(); - Self::Full { - kind, - msg, - span, - notes: vec![], - } - } - pub fn unpack(self) -> (ShErrKind, String, Vec, Option) { - match self { - ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None), - ShErr::Full { - kind, - msg, - notes, - span, - } => (kind, msg, notes, Some(span)), - } - } - pub fn with_note(self, note: Note) -> Self { - let (kind, msg, mut notes, span) = self.unpack(); - notes.push(note); - if let Some(span) = span { - Self::Full { - kind, - msg, - notes, - span, - } - } else { - Self::Simple { kind, msg, notes } - } - } - pub fn with_span(sherr: ShErr, span: Span) -> Self { - let (kind, msg, notes, _) = sherr.unpack(); - Self::Full { - kind, - msg, - notes, - span, - } - } - pub fn kind(&self) -> &ShErrKind { - match self { - ShErr::Simple { - kind, - msg: _, - notes: _, - } - | ShErr::Full { - kind, - msg: _, - notes: _, - span: _, - } => kind, - } - } - pub fn get_window(&self) -> Vec<(usize, String)> { - let ShErr::Full { - kind: _, - msg: _, - notes: _, - span, - } = self - else { - unreachable!() - }; - let mut total_len: usize = 0; - let mut total_lines: usize = 1; - let mut lines = vec![]; - let mut cur_line = String::new(); + pub fn new(kind: ShErrKind, span: Span) -> Self { + Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![] } + } + pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { + Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()] } + } + pub fn full(kind: ShErrKind, msg: impl Into, span: Span) -> Self { + Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![msg.into()] } + } + pub fn blame(self, span: Span) -> Self { + let ShErr { kind, src_span: _, labels, sources, notes } = self; + Self { kind, src_span: Some(span), labels, sources, notes } + } + pub fn try_blame(self, span: Span) -> Self { + match self { + ShErr { kind, src_span: None, labels, sources, notes } => Self { kind, src_span: Some(span), labels, sources, notes }, + _ => self + } + } + pub fn kind(&self) -> &ShErrKind { + &self.kind + } + pub fn rename(mut self, name: impl Into) -> Self { + if let Some(span) = self.src_span.as_mut() { + span.rename(name.into()); + } + self + } + pub fn with_label(self, source: SpanSource, label: ariadne::Label) -> Self { + let ShErr { kind, src_span, mut labels, mut sources, notes } = self; + sources.push(source); + labels.push(label); + Self { kind, src_span, labels, sources, notes } + } + pub fn with_context(self, ctx: Vec<(SpanSource, ariadne::Label)>) -> Self { + let ShErr { kind, src_span, mut labels, mut sources, notes } = self; + for (src, label) in ctx { + sources.push(src); + labels.push(label); + } + Self { kind, src_span, labels, sources, notes } + } + pub fn with_note(self, note: impl Into) -> Self { + let ShErr { kind, src_span, labels, sources, mut notes } = self; + notes.push(note.into()); + Self { kind, src_span, labels, sources, notes } + } + pub fn build_report(&self) -> Option> { + let span = self.src_span.as_ref()?; + let mut report = Report::build(ReportKind::Error, span.clone()) + .with_config(ariadne::Config::default().with_color(true)); + let msg = if self.notes.is_empty() { + self.kind.to_string() + } else { + format!("{} - {}", self.kind, self.notes.first().unwrap()) + }; + report = report.with_message(msg); - let src = span.get_source(); - let mut chars = src.chars(); + for label in self.labels.clone() { + report = report.with_label(label); + } + for note in &self.notes { + report = report.with_note(note); + } - while let Some(ch) = chars.next() { - total_len += ch.len_utf8(); - cur_line.push(ch); - if ch == '\n' { - if total_len > span.start { - let line = (total_lines, mem::take(&mut cur_line)); - lines.push(line); - } - if total_len >= span.end { - break; - } - total_lines += 1; + Some(report.finish()) + } + fn collect_sources(&self) -> HashMap { + let mut source_map = HashMap::new(); + if let Some(span) = &self.src_span { + let src = span.span_source().clone(); + source_map.entry(src.clone()) + .or_insert_with(|| src.content().to_string()); + } + for src in &self.sources { + source_map.entry(src.clone()) + .or_insert_with(|| src.content().to_string()); + } + source_map + } + pub fn print_error(&self) { + let default = || { + eprintln!("{}", self.kind); + for note in &self.notes { + eprintln!("note: {note}"); + } + }; + let Some(report) = self.build_report() else { + return default(); + }; - cur_line.clear(); - } - } - - if !cur_line.is_empty() { - let line = (total_lines, mem::take(&mut cur_line)); - lines.push(line); - } - - lines - } - pub fn get_line_col(&self) -> (usize, usize) { - let ShErr::Full { - kind: _, - msg: _, - notes: _, - span, - } = self - else { - unreachable!() - }; - - let mut lineno = 1; - let mut colno = 1; - let src = span.get_source(); - let mut chars = src.chars().enumerate(); - while let Some((pos, ch)) = chars.next() { - if pos >= span.start { - break; - } - if ch == '\n' { - lineno += 1; - colno = 1; - } else { - colno += 1; - } - } - (lineno, colno) - } - pub fn get_indicator_lines(&self) -> Option> { - match self { - ShErr::Simple { - kind: _, - msg: _, - notes: _, - } => None, - ShErr::Full { - kind: _, - msg: _, - notes: _, - span, - } => { - let text = span.as_str(); - let lines = text.lines(); - let mut indicator_lines = vec![]; - - for line in lines { - let indicator_line = "^" - .repeat(line.trim().len()) - .styled(Style::Red | Style::Bold); - indicator_lines.push(indicator_line); - } - - Some(indicator_lines) - } - } - } + let sources = self.collect_sources(); + let cache = ariadne::FnCache::new(move |src: &SpanSource| { + sources.get(src) + .cloned() + .ok_or_else(|| format!("Failed to fetch source '{}'", src.name())) + }); + if report.eprint(cache).is_err() { + default(); + } + } } impl Display for ShErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Simple { - msg, - kind: _, - notes, - } => { - let mut all_strings = vec![msg.to_string()]; - let mut notes_fmt = vec![]; - for note in notes { - let fmt = format!("{note}"); - notes_fmt.push(fmt); - } - all_strings.append(&mut notes_fmt); - let mut output = all_strings.join("\n"); - output.push('\n'); - - writeln!(f, "{}", output) - } - - Self::Full { - msg, - kind, - notes, - span: _, - } => { - let window = self.get_window(); - let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); - let mut lineno_pad_count = 0; - for (lineno, _) in window.clone() { - if lineno.to_string().len() > lineno_pad_count { - lineno_pad_count = lineno.to_string().len() + 1 - } - } - let padding = " ".repeat(lineno_pad_count); - writeln!(f)?; - - let (line, col) = self.get_line_col(); - let line_fmt = line.styled(Style::Cyan | Style::Bold); - let col_fmt = col.styled(Style::Cyan | Style::Bold); - let kind = kind.styled(Style::Red | Style::Bold); - let arrow = "->".styled(Style::Cyan | Style::Bold); - writeln!(f, "{kind} - {msg}",)?; - writeln!(f, "{padding}{arrow} [{line_fmt};{col_fmt}]",)?; - - let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold); - writeln!(f, "{bar}")?; - - let mut first_ind_ln = true; - for (lineno, line) in window { - let lineno = lineno.to_string(); - let line = line.trim(); - let mut prefix = format!("{padding}|"); - prefix.replace_range(0..lineno.len(), &lineno); - prefix = prefix.styled(Style::Cyan | Style::Bold); - writeln!(f, "{prefix} {line}")?; - - if let Some(ind_ln) = indicator_lines.next() { - if first_ind_ln { - let ind_ln_padding = " ".repeat(col); - let ind_ln = format!("{ind_ln_padding}{ind_ln}"); - writeln!(f, "{bar}{ind_ln}")?; - first_ind_ln = false; - } else { - writeln!(f, "{bar} {ind_ln}")?; - } - } - } - - write!(f, "{bar}")?; - - let bar_break = "-".styled(Style::Cyan | Style::Bold); - if !notes.is_empty() { - writeln!(f)?; - } - for note in notes { - write!(f, "{padding}{bar_break} {note}")?; - } - Ok(()) - } - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.notes.is_empty() { + write!(f, "{}", self.kind) + } else { + write!(f, "{} - {}", self.kind, self.notes.first().unwrap()) + } + } } impl From for ShErr { @@ -404,9 +282,8 @@ pub enum ShErrKind { ResourceLimitExceeded, BadPermission, Errno(Errno), - FileNotFound(String), - CmdNotFound(String), - ReadlineIntr(String), + FileNotFound, + CmdNotFound, ReadlineErr, // Not really errors, more like internal signals @@ -431,13 +308,12 @@ impl Display for ShErrKind { Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::BadPermission => "Bad Permissions", Self::Errno(e) => &format!("Errno: {}", e.desc()), - Self::FileNotFound(file) => &format!("File not found: {file}"), - Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), + Self::FileNotFound => "File not found", + Self::CmdNotFound => "Command not found", Self::CleanExit(_) => "", Self::FuncReturn(_) => "Syntax Error", Self::LoopContinue(_) => "Syntax Error", Self::LoopBreak(_) => "Syntax Error", - Self::ReadlineIntr(_) => "", Self::ReadlineErr => "Readline Error", Self::ClearReadline => "", Self::Null => "", diff --git a/src/libsh/term.rs b/src/libsh/term.rs index dec8148..49b860e 100644 --- a/src/libsh/term.rs +++ b/src/libsh/term.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, ops::BitOr}; +use std::{cell::RefCell, fmt::Display, ops::BitOr}; pub trait Styled: Sized + Display { fn styled>(self, style: S) -> String { diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 76a6d8b..fb77b3a 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -78,7 +78,7 @@ impl TkVecUtils for Vec { if let Some(first_tk) = self.first() { self .last() - .map(|last_tk| Span::new(first_tk.span.start..last_tk.span.end, first_tk.source())) + .map(|last_tk| Span::new(first_tk.span.range().start..last_tk.span.range().end, first_tk.source())) } else { None } diff --git a/src/main.rs b/src/main.rs index f91956e..fd213c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ #![allow( clippy::derivable_impls, clippy::tabs_in_doc_comments, - clippy::while_let_on_iterator + clippy::while_let_on_iterator, + clippy::result_large_err )] pub mod builtin; pub mod expand; @@ -87,6 +88,7 @@ fn setup_panic_handler() { } fn main() -> ExitCode { + yansi::enable(); env_logger::init(); kickstart_lazy_evals(); setup_panic_handler(); @@ -162,7 +164,7 @@ fn shed_interactive() -> ShResult<()> { sig_setup(); if let Err(e) = source_rc() { - eprintln!("{e}"); + e.print_error(); } // Create readline instance with initial prompt @@ -197,7 +199,7 @@ fn shed_interactive() -> ShResult<()> { QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(()); } - _ => eprintln!("{e}"), + _ => e.print_error(), } } } @@ -269,7 +271,7 @@ fn shed_interactive() -> ShResult<()> { QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(()); } - _ => eprintln!("{e}"), + _ => e.print_error(), } } let command_run_time = start.elapsed(); @@ -295,7 +297,7 @@ fn shed_interactive() -> ShResult<()> { QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(()); } - _ => eprintln!("{e}"), + _ => e.print_error(), }, } } diff --git a/src/parse/execute.rs b/src/parse/execute.rs index e7e711b..80e7cbb 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,16 +1,17 @@ use std::{ - collections::{HashSet, VecDeque}, - os::unix::fs::PermissionsExt, + cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt }; +use ariadne::{Fmt, Label}; + use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_pop, arr_fpop, arr_push, arr_fpush, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak + alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak }, expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, dispatch_job}, libsh::{ - error::{ShErr, ShErrKind, ShResult, ShResultExt}, + error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, utils::RedirVecUtils, }, prelude::*, @@ -27,7 +28,7 @@ use super::{ }; thread_local! { - static RECURSE_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; + static RECURSE_DEPTH: Cell = const { Cell::new(0) }; } pub fn is_in_path(name: &str) -> bool { @@ -160,7 +161,7 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); if let Err(errors) = parser.parse_src() { for error in errors { - eprintln!("{error}"); + error.print_error(); } return Ok(()); } @@ -284,6 +285,7 @@ impl Dispatcher { } pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { let blame = func_def.get_span(); + let ctx = func_def.context.clone(); let NdRule::FuncDef { name, body } = func_def.class else { unreachable!() }; @@ -299,10 +301,10 @@ impl Dispatcher { )); } - let mut func_parser = ParsedSrc::new(Arc::new(body)); + let mut func_parser = ParsedSrc::new(Arc::new(body)).with_context(ctx); if let Err(errors) = func_parser.parse_src() { for error in errors { - eprintln!("{error}"); + error.print_error(); } return Ok(()); } @@ -318,14 +320,14 @@ impl Dispatcher { self.run_fork("anonymous_subshell", |s| { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { - eprintln!("{e}"); + e.print_error(); return; }; s.io_stack.append_to_frame(subsh.redirs); let mut argv = match prepare_argv(argv) { Ok(argv) => argv, Err(e) => { - eprintln!("{e}"); + e.print_error(); return; } }; @@ -334,12 +336,12 @@ impl Dispatcher { let subsh_body = subsh.0.to_string(); if let Err(e) = exec_input(subsh_body, None, s.interactive) { - eprintln!("{e}"); + e.print_error(); }; }) } fn exec_func(&mut self, func: Node) -> ShResult<()> { - let blame = func.get_span().clone(); + let mut blame = func.get_span().clone(); let NdRule::Command { assignments, mut argv, @@ -369,10 +371,11 @@ impl Dispatcher { self.io_stack.append_to_frame(func.redirs); let func_name = argv.remove(0).span.as_str().to_string(); + blame.rename(func_name.clone()); + let argv = prepare_argv(argv)?; let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let _guard = ScopeGuard::exclusive_scope(Some(argv)); - func_body.body_mut().flags = func.flags; if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { @@ -381,7 +384,7 @@ impl Dispatcher { state::set_status(*code); Ok(()) } - _ => Err(e).blame(blame), + _ => Err(e), } } else { Ok(()) @@ -418,7 +421,7 @@ impl Dispatcher { log::trace!("Forking brace group"); self.run_fork("brace group", |s| { if let Err(e) = brc_grp_logic(s) { - eprintln!("{e}"); + e.print_error(); } }) } else { @@ -472,7 +475,7 @@ impl Dispatcher { log::trace!("Forking builtin: case"); self.run_fork("case", |s| { if let Err(e) = case_logic(s) { - eprintln!("{e}"); + e.print_error(); } }) } else { @@ -535,7 +538,7 @@ impl Dispatcher { log::trace!("Forking builtin: loop"); self.run_fork("loop", |s| { if let Err(e) = loop_logic(s) { - eprintln!("{e}"); + e.print_error(); } }) } else { @@ -613,7 +616,7 @@ impl Dispatcher { log::trace!("Forking builtin: for"); self.run_fork("for", |s| { if let Err(e) = for_logic(s) { - eprintln!("{e}"); + e.print_error(); } }) } else { @@ -669,7 +672,7 @@ impl Dispatcher { log::trace!("Forking builtin: if"); self.run_fork("if", |s| { if let Err(e) = if_logic(s) { - eprintln!("{e}"); + e.print_error(); state::set_status(1); } }) @@ -726,7 +729,7 @@ impl Dispatcher { let _guard = self.io_stack.pop_frame().redirect()?; self.run_fork(&cmd_raw, |s| { if let Err(e) = s.dispatch_builtin(cmd) { - eprintln!("{e}"); + e.print_error(); } }) } else { @@ -819,6 +822,7 @@ impl Dispatcher { } } fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { + let context = cmd.context.clone(); let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() }; @@ -855,12 +859,22 @@ impl Dispatcher { let cmd_str = cmd.to_str().unwrap().to_string(); match e { Errno::ENOENT => { - let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); - eprintln!("{err}"); + let source = span.span_source().clone(); + let color = next_color(); + ShErr::full(ShErrKind::CmdNotFound, "", span.clone()) + .with_label( + source, + Label::new(span) + .with_color(color) + .with_message(format!("{}: command not found", cmd_str.fg(color))) + ) + .with_context(context) + .print_error(); } _ => { - let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); - eprintln!("{err}"); + ShErr::full(ShErrKind::Errno(e), format!("{e}"), span) + .with_context(context) + .print_error(); } } exit(e as i32) diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 07b91a8..b29ef3d 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -64,24 +64,59 @@ impl QuoteState { } } +#[derive(Clone, PartialEq, Default, Debug, Eq, Hash)] +pub struct SpanSource { + name: String, + content: Arc +} + +impl SpanSource { + pub fn name(&self) -> &str { + &self.name + } + pub fn content(&self) -> Arc { + self.content.clone() + } + pub fn rename(&mut self, name: String) { + self.name = name; + } +} + +impl Display for SpanSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + /// Span::new(10..20) #[derive(Clone, PartialEq, Default, Debug)] pub struct Span { range: Range, - source: Arc, + source: SpanSource } impl Span { /// New `Span`. Wraps a range and a string slice that it refers to. pub fn new(range: Range, source: Arc) -> Self { + let source = SpanSource { name: "".into(), content: source }; Span { range, source } } + pub fn rename(&mut self, name: String) { + self.source.name = name; + } + pub fn with_name(mut self, name: String) -> Self { + self.source.name = name; + self + } /// Slice the source string at the wrapped range pub fn as_str(&self) -> &str { - &self.source[self.start..self.end] + &self.source.content[self.range().start..self.range().end] } pub fn get_source(&self) -> Arc { - self.source.clone() + self.source.content.clone() + } + pub fn span_source(&self) -> &SpanSource { + &self.source } pub fn range(&self) -> Range { self.range.clone() @@ -93,14 +128,23 @@ impl Span { } } -/// Allows simple access to the underlying range wrapped by the span -impl Deref for Span { - type Target = Range; - fn deref(&self) -> &Self::Target { - &self.range - } +impl ariadne::Span for Span { + type SourceId = SpanSource; + + fn source(&self) -> &Self::SourceId { + &self.source + } + + fn start(&self) -> usize { + self.range.start + } + + fn end(&self) -> usize { + self.range.end + } } +/// Allows simple access to the underlying range wrapped by the span #[derive(Clone, PartialEq, Debug)] pub enum TkRule { Null, @@ -148,7 +192,7 @@ impl Tk { self.span.as_str() } pub fn source(&self) -> Arc { - self.span.source.clone() + self.span.source.content.clone() } pub fn mark(&mut self, flag: TkFlags) { self.flags |= flag; @@ -931,12 +975,12 @@ pub fn split_tk(tk: &Tk, pat: &str) -> Vec { let mut cursor = 0; let mut splits = vec![]; while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { - let before_span = Span::new(tk.span.start + cursor..tk.span.start + cursor + split.0.len(), tk.source().clone()); + let before_span = Span::new(tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(), tk.source().clone()); splits.push(Tk::new(tk.class.clone(), before_span)); cursor += split.0.len() + pat.len(); } if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { - let remaining_span = Span::new(tk.span.start + cursor..tk.span.end, tk.source().clone()); + let remaining_span = Span::new(tk.span.range().start + cursor..tk.span.range().end, tk.source().clone()); splits.push(Tk::new(tk.class.clone(), remaining_span)); } splits @@ -957,8 +1001,8 @@ pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> { } if slice[i..].starts_with(pat) { - let before_span = Span::new(tk.span.start..tk.span.start + i, tk.source().clone()); - let after_span = Span::new(tk.span.start + i + pat.len()..tk.span.end, tk.source().clone()); + let before_span = Span::new(tk.span.range().start..tk.span.range().start + i, tk.source().clone()); + let after_span = Span::new(tk.span.range().start + i + pat.len()..tk.span.range().end, tk.source().clone()); let before_tk = Tk::new(tk.class.clone(), before_span); let after_tk = Tk::new(tk.class.clone(), after_span); return Some((before_tk, after_tk)); diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 937eb23..a9afde4 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1,12 +1,14 @@ -use std::str::FromStr; +use std::{fmt::Debug, str::FromStr, sync::Arc}; +use ariadne::{Fmt, Label}; use bitflags::bitflags; use fmt::Display; -use lex::{LexFlags, LexStream, Span, Tk, TkFlags, TkRule}; +use lex::{LexFlags, LexStream, Span, SpanSource, Tk, TkFlags, TkRule}; +use yansi::Color; use crate::{ libsh::{ - error::{Note, ShErr, ShErrKind, ShResult}, + error::{Note, ShErr, ShErrKind, ShResult, next_color}, utils::TkVecUtils, }, prelude::*, @@ -45,6 +47,7 @@ pub struct ParsedSrc { pub src: Arc, pub ast: Ast, pub lex_flags: LexFlags, + pub context: LabelCtx, } impl ParsedSrc { @@ -53,12 +56,17 @@ impl ParsedSrc { src, ast: Ast::new(vec![]), lex_flags: LexFlags::empty(), + context: vec![], } } pub fn with_lex_flags(mut self, flags: LexFlags) -> Self { self.lex_flags = flags; self } + pub fn with_context(mut self, ctx: LabelCtx) -> Self { + self.context = ctx; + self + } pub fn parse_src(&mut self) -> Result<(), Vec> { let mut tokens = vec![]; for lex_result in LexStream::new(self.src.clone(), self.lex_flags) { @@ -70,7 +78,7 @@ impl ParsedSrc { let mut errors = vec![]; let mut nodes = vec![]; - for parse_result in ParseStream::new(tokens) { + for parse_result in ParseStream::with_context(tokens, self.context.clone()) { match parse_result { Ok(node) => nodes.push(node), Err(error) => errors.push(error), @@ -104,12 +112,15 @@ impl Ast { } } +pub type LabelCtx = Vec<(SpanSource, Label)>; + #[derive(Clone, Debug)] pub struct Node { pub class: NdRule, pub flags: NdFlags, pub redirs: Vec, pub tokens: Vec, + pub context: LabelCtx, } impl Node { @@ -133,7 +144,7 @@ impl Node { }; Span::new( - first_tk.span.start..last_tk.span.end, + first_tk.span.range().start..last_tk.span.range().end, first_tk.span.get_source(), ) } @@ -539,14 +550,25 @@ pub enum NdRule { }, } -#[derive(Debug)] pub struct ParseStream { pub tokens: Vec, + pub context: LabelCtx +} + +impl Debug for ParseStream { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ParseStream") + .field("tokens", &self.tokens) + .finish() + } } impl ParseStream { pub fn new(tokens: Vec) -> Self { - Self { tokens } + Self { tokens, context: vec![] } + } + pub fn with_context(tokens: Vec, context: LabelCtx) -> Self { + Self { tokens, context } } fn next_tk_class(&self) -> &TkRule { if let Some(tk) = self.tokens.first() { @@ -684,6 +706,7 @@ impl ParseStream { class: NdRule::Conjunction { elements }, flags: NdFlags::empty(), redirs: vec![], + context: self.context.clone(), tokens: node_tks, })) } @@ -697,23 +720,47 @@ impl ParseStream { } let name_tk = self.next_tk().unwrap(); node_tks.push(name_tk.clone()); - let name = name_tk; + let name = name_tk.clone(); + let name_raw = name.to_string(); + let mut src = name_tk.span.span_source().clone(); + src.rename(name_raw.clone()); + let color = next_color(); + // Push a placeholder context so child nodes inherit it + self.context.push(( + src.clone(), + Label::new(name_tk.span.clone().with_name(name_raw.clone())) + .with_message(format!("in function '{}' defined here", name_raw.clone().fg(color))) + .with_color(color), + )); let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else { + self.context.pop(); return Err(parse_err_full( "Expected a brace group after function name", &node_tks.get_span().unwrap(), + self.context.clone() )); }; body = Box::new(brc_grp); + // Replace placeholder with full-span label + self.context.pop(); + let full_span = body.get_span(); + self.context.push(( + src, + Label::new(full_span.with_name(name_raw.clone())) + .with_message(format!("in function '{}' called here", name_raw.fg(color))) + .with_color(color), + )); let node = Node { class: NdRule::FuncDef { name, body }, flags: NdFlags::empty(), redirs: vec![], tokens: node_tks, + context: self.context.clone() }; + self.context.pop(); Ok(Some(node)) } fn panic_mode(&mut self, node_tks: &mut Vec) { @@ -743,6 +790,7 @@ impl ParseStream { return Err(parse_err_full( "Malformed test call", &node_tks.get_span().unwrap(), + self.context.clone() )); } else { break; @@ -769,6 +817,7 @@ impl ParseStream { return Err(parse_err_full( "Invalid placement for logical operator in test", &node_tks.get_span().unwrap(), + self.context.clone() )); } let op = match tk.class { @@ -784,6 +833,7 @@ impl ParseStream { return Err(parse_err_full( "Invalid placement for logical operator in test", &node_tks.get_span().unwrap(), + self.context.clone() )); } } @@ -797,6 +847,7 @@ impl ParseStream { class: NdRule::Test { cases }, flags: NdFlags::empty(), redirs: vec![], + context: self.context.clone(), tokens: node_tks, }; Ok(Some(node)) @@ -828,6 +879,7 @@ impl ParseStream { return Err(parse_err_full( "Expected a closing brace for this brace group", &node_tks.get_span().unwrap(), + self.context.clone() )); } } @@ -840,6 +892,7 @@ impl ParseStream { class: NdRule::BraceGrp { body }, flags: NdFlags::empty(), redirs, + context: self.context.clone(), tokens: node_tks, }; Ok(Some(node)) @@ -893,12 +946,10 @@ impl ParseStream { let pat_err = parse_err_full( "Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap(), + self.context.clone() ) .with_note( - Note::new("Patterns can be raw text, or anything that gets substituted with raw text") - .with_sub_notes(vec![ - "This includes variables like '$foo' or command substitutions like '$(echo foo)'", - ]), + "Patterns can be raw text, or anything that gets substituted with raw text" ); let Some(pat_tk) = self.next_tk() else { @@ -919,6 +970,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'in' after case variable name", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -931,6 +983,7 @@ impl ParseStream { return Err(parse_err_full( "Expected a case pattern here", &node_tks.get_span().unwrap(), + self.context.clone() )); } let case_pat_tk = self.next_tk().unwrap(); @@ -967,6 +1020,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'esac' after case block", &node_tks.get_span().unwrap(), + self.context.clone() )); } } @@ -978,10 +1032,17 @@ impl ParseStream { }, flags: NdFlags::empty(), redirs, + context: self.context.clone(), tokens: node_tks, }; Ok(Some(node)) } + fn make_err(&self, span: lex::Span, label: Label) -> ShErr { + let src = span.span_source().clone(); + ShErr::new(ShErrKind::ParseErr, span) + .with_label(src, label) + .with_context(self.context.clone()) + } fn parse_if(&mut self) -> ShResult> { // Needs at last one 'if-then', // Any number of 'elif-then', @@ -1000,10 +1061,14 @@ impl ParseStream { let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" }; let Some(cond) = self.parse_cmd_list()? else { self.panic_mode(&mut node_tks); - return Err(parse_err_full( - &format!("Expected an expression after '{prefix_keywrd}'"), - &node_tks.get_span().unwrap(), - )); + let span = node_tks.get_span().unwrap(); + let color = next_color(); + return Err(self.make_err(span.clone(), + Label::new(span) + .with_message(format!("Expected an expression after '{}'", prefix_keywrd.fg(color))) + .with_color(color) + )); + }; node_tks.extend(cond.tokens.clone()); @@ -1012,6 +1077,7 @@ impl ParseStream { return Err(parse_err_full( &format!("Expected 'then' after '{prefix_keywrd}' condition"), &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1027,6 +1093,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'then'", &node_tks.get_span().unwrap(), + self.context.clone() )); }; let cond_node = CondNode { @@ -1056,6 +1123,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'else'", &node_tks.get_span().unwrap(), + self.context.clone() )); } } @@ -1066,6 +1134,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'fi' after if statement", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1081,6 +1150,7 @@ impl ParseStream { }, flags: NdFlags::empty(), redirs, + context: self.context.clone(), tokens: node_tks, }; Ok(Some(node)) @@ -1120,6 +1190,7 @@ impl ParseStream { return Err(parse_err_full( "This for loop is missing a variable", &node_tks.get_span().unwrap(), + self.context.clone() )); } if arr.is_empty() { @@ -1127,6 +1198,7 @@ impl ParseStream { return Err(parse_err_full( "This for loop is missing an array", &node_tks.get_span().unwrap(), + self.context.clone() )); } if !self.check_keyword("do") || !self.next_tk_is_some() { @@ -1134,6 +1206,7 @@ impl ParseStream { return Err(parse_err_full( "Missing a 'do' for this for loop", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1149,6 +1222,7 @@ impl ParseStream { return Err(parse_err_full( "Missing a 'done' after this for loop", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1159,6 +1233,7 @@ impl ParseStream { class: NdRule::ForNode { vars, arr, body }, flags: NdFlags::empty(), redirs, + context: self.context.clone(), tokens: node_tks, }; Ok(Some(node)) @@ -1188,6 +1263,7 @@ impl ParseStream { return Err(parse_err_full( &format!("Expected an expression after '{loop_kind}'"), // It also implements Display &node_tks.get_span().unwrap(), + self.context.clone() )); }; node_tks.extend(cond.tokens.clone()); @@ -1197,6 +1273,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'do' after loop condition", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1212,6 +1289,7 @@ impl ParseStream { return Err(parse_err_full( "Expected an expression after 'do'", &node_tks.get_span().unwrap(), + self.context.clone() )); }; @@ -1221,6 +1299,7 @@ impl ParseStream { return Err(parse_err_full( "Expected 'done' after loop body", &node_tks.get_span().unwrap(), + self.context.clone() )); } node_tks.push(self.next_tk().unwrap()); @@ -1240,6 +1319,7 @@ impl ParseStream { }, flags: NdFlags::empty(), redirs, + context: self.context.clone(), tokens: node_tks, }; Ok(Some(loop_node)) @@ -1277,6 +1357,7 @@ impl ParseStream { }, flags, redirs: vec![], + context: self.context.clone(), tokens: node_tks, })) } @@ -1295,6 +1376,7 @@ impl ParseStream { return Err(parse_err_full( "Found case pattern in command", &prefix_tk.span, + self.context.clone() )); } let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD); @@ -1335,6 +1417,7 @@ impl ParseStream { tokens: node_tks, flags, redirs, + context: self.context.clone(), })); } } @@ -1398,16 +1481,17 @@ impl ParseStream { tokens: node_tks, flags, redirs, + context: self.context.clone(), })) } fn parse_assignment(&self, token: &Tk) -> Option { let mut chars = token.span.as_str().chars(); let mut var_name = String::new(); - let mut name_range = token.span.start..token.span.start; + let mut name_range = token.span.range().start..token.span.range().start; let mut var_val = String::new(); - let mut val_range = token.span.end..token.span.end; + let mut val_range = token.span.range().end..token.span.range().end; let mut assign_kind = None; - let mut pos = token.span.start; + let mut pos = token.span.range().start; while let Some(ch) = chars.next() { if assign_kind.is_some() { @@ -1500,6 +1584,7 @@ impl ParseStream { tokens: vec![token.clone()], flags, redirs: vec![], + context: self.context.clone(), }) } else { None @@ -1556,8 +1641,14 @@ pub fn get_redir_file(class: RedirType, path: PathBuf) -> ShResult { Ok(result?) } -fn parse_err_full(reason: &str, blame: &Span) -> ShErr { - ShErr::full(ShErrKind::ParseErr, reason, blame.clone()) +fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr { + let color = next_color(); + ShErr::new(ShErrKind::ParseErr, blame.clone()) + .with_label( + blame.span_source().clone(), + Label::new(blame.clone()).with_message(reason).with_color(color) + ) + .with_context(context) } fn is_func_name(tk: Option<&Tk>) -> bool { diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 5b118c3..7bb1a18 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -696,7 +696,7 @@ impl Completer { tks .iter() .next() - .is_some_and(|tk| tk.span.start > cursor_pos) + .is_some_and(|tk| tk.span.range().start > cursor_pos) }) .map(|i| i.saturating_sub(1)) .unwrap_or(segments.len().saturating_sub(1)); @@ -705,13 +705,13 @@ impl Completer { let cword = if let Some(pos) = relevant .iter() - .position(|tk| cursor_pos >= tk.span.start && cursor_pos <= tk.span.end) + .position(|tk| cursor_pos >= tk.span.range().start && cursor_pos <= tk.span.range().end) { pos } else { let insert_pos = relevant .iter() - .position(|tk| tk.span.start > cursor_pos) + .position(|tk| tk.span.range().start > cursor_pos) .unwrap_or(relevant.len()); let mut new_tk = Tk::default(); @@ -761,7 +761,7 @@ impl Completer { // Set token_span from CompContext's current word if let Some(cur) = ctx.words.get(ctx.cword) { - self.token_span = (cur.span.start, cur.span.end); + self.token_span = (cur.span.range().start, cur.span.range().end); } else { self.token_span = (cursor_pos, cursor_pos); } @@ -799,7 +799,7 @@ impl Completer { return Ok(CompResult::from_candidates(candidates)); }; - self.token_span = (cur_token.span.start, cur_token.span.end); + self.token_span = (cur_token.span.range().start, cur_token.span.range().end); // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB // inside a token) @@ -812,7 +812,7 @@ impl Completer { // If token contains '=', only complete after the '=' let token_str = cur_token.span.as_str(); if let Some(eq_pos) = token_str.rfind('=') { - self.token_span.0 = cur_token.span.start + eq_pos + 1; + self.token_span.0 = cur_token.span.range().start + eq_pos + 1; cur_token .span .set_range(self.token_span.0..self.token_span.1); diff --git a/src/readline/history.rs b/src/readline/history.rs index 0d6c1a5..befcbd9 100644 --- a/src/readline/history.rs +++ b/src/readline/history.rs @@ -70,11 +70,10 @@ impl HistEntry { impl FromStr for HistEntry { type Err = ShErr; fn from_str(s: &str) -> Result { - let err = Err(ShErr::Simple { - kind: ShErrKind::HistoryReadErr, - msg: format!("Bad formatting on history entry '{s}'"), - notes: vec![], - }); + let err = Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on history entry '{s}'"), + )); //: 248972349;148;echo foo; echo bar let Some(cleaned) = s.strip_prefix(": ") else { @@ -133,11 +132,10 @@ impl FromStr for HistEntries { while let Some((i, line)) = lines.next() { if !line.starts_with(": ") { - return Err(ShErr::Simple { - kind: ShErrKind::HistoryReadErr, - msg: format!("Bad formatting on line {i}"), - notes: vec![], - }); + return Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on line {i}"), + )); } let mut chars = line.chars().peekable(); let mut feeding_lines = true; @@ -163,11 +161,10 @@ impl FromStr for HistEntries { } if feeding_lines { let Some((_, line)) = lines.next() else { - return Err(ShErr::Simple { - kind: ShErrKind::HistoryReadErr, - msg: format!("Bad formatting on line {i}"), - notes: vec![], - }); + return Err(ShErr::simple( + ShErrKind::HistoryReadErr, + format!("Bad formatting on line {i}"), + )); }; chars = line.chars().peekable(); } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 34e8f37..df2119f 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -975,7 +975,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { other => other, } }); - stack.retain(|(i, m)| *i <= token.span.start && !markers::END_MARKERS.contains(m)); + stack.retain(|(i, m)| *i <= token.span.range().start && !markers::END_MARKERS.contains(m)); let Some(ctx) = stack.last() else { return false; @@ -989,27 +989,27 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { if token.class != TkRule::Str && let Some(marker) = marker_for(&token.class) { - insertions.push((token.span.end, markers::RESET)); - insertions.push((token.span.start, marker)); + insertions.push((token.span.range().end, markers::RESET)); + insertions.push((token.span.range().start, marker)); return insertions; } else if token.flags.contains(TkFlags::IS_SUBSH) { let token_raw = token.span.as_str(); if token_raw.ends_with(')') { - insertions.push((token.span.end, markers::SUBSH_END)); + insertions.push((token.span.range().end, markers::SUBSH_END)); } - insertions.push((token.span.start, markers::SUBSH)); + insertions.push((token.span.range().start, markers::SUBSH)); return insertions; } else if token.class == TkRule::CasePattern { - insertions.push((token.span.end, markers::RESET)); - insertions.push((token.span.end - 1, markers::CASE_PAT)); - insertions.push((token.span.start, markers::OPERATOR)); + insertions.push((token.span.range().end, markers::RESET)); + insertions.push((token.span.range().end - 1, markers::CASE_PAT)); + insertions.push((token.span.range().start, markers::OPERATOR)); return insertions; } let token_raw = token.span.as_str(); let mut token_chars = token_raw.char_indices().peekable(); - let span_start = token.span.start; + let span_start = token.span.range().start; let mut qt_state = QuoteState::default(); let mut cmd_sub_depth = 0; @@ -1031,7 +1031,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { insertions.insert(0, (span_start, markers::ASSIGNMENT)); } - insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token + insertions.insert(0, (token.span.range().end, markers::RESET)); // reset at the end of the token while let Some((i, ch)) = token_chars.peek() { let index = *i; // we have to dereference this here because rustc is a very pedantic program diff --git a/src/shopt.rs b/src/shopt.rs index 61a88a8..9e78779 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -109,10 +109,6 @@ impl ShOpts { ShErrKind::SyntaxErr, "shopt: expected 'core' or 'prompt' in shopt key", ) - .with_note( - Note::new("'shopt' takes arguments separated by periods to denote namespaces") - .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]), - ), ); } } @@ -138,10 +134,6 @@ impl ShOpts { ShErrKind::SyntaxErr, "shopt: Expected 'core' or 'prompt' in shopt key", ) - .with_note( - Note::new("'shopt' takes arguments separated by periods to denote namespaces") - .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]), - ), ), } } @@ -240,19 +232,6 @@ impl ShOptCore { ShErrKind::SyntaxErr, format!("shopt: Unexpected 'core' option '{opt}'"), ) - .with_note(Note::new("options can be accessed like 'core.option_name'")) - .with_note( - Note::new("'core' contains the following options").with_sub_notes(vec![ - "dotglob", - "autocd", - "hist_ignore_dupes", - "max_hist", - "interactive_comments", - "auto_hist", - "bell_enabled", - "max_recurse_depth", - ]), - ), ); } } @@ -315,19 +294,6 @@ impl ShOptCore { ShErrKind::SyntaxErr, format!("shopt: Unexpected 'core' option '{query}'"), ) - .with_note(Note::new("options can be accessed like 'core.option_name'")) - .with_note( - Note::new("'core' contains the following options").with_sub_notes(vec![ - "dotglob", - "autocd", - "hist_ignore_dupes", - "max_hist", - "interactive_comments", - "auto_hist", - "bell_enabled", - "max_recurse_depth", - ]), - ), ), } } @@ -445,20 +411,6 @@ impl ShOptPrompt { ShErrKind::SyntaxErr, format!("shopt: Unexpected 'prompt' option '{opt}'"), ) - .with_note(Note::new( - "options can be accessed like 'prompt.option_name'", - )) - .with_note( - Note::new("'prompt' contains the following options").with_sub_notes(vec![ - "trunc_prompt_path", - "edit_mode", - "comp_limit", - "highlight", - "auto_indent", - "linebreak_on_incomplete", - "custom", - ]), - ), ); } } @@ -512,19 +464,6 @@ impl ShOptPrompt { ShErrKind::SyntaxErr, format!("shopt: Unexpected 'prompt' option '{query}'"), ) - .with_note(Note::new( - "options can be accessed like 'prompt.option_name'", - )) - .with_note( - Note::new("'prompt' contains the following options").with_sub_notes(vec![ - "trunc_prompt_path", - "edit_mode", - "comp_limit", - "highlight", - "auto_indent", - "linebreak_on_incomplete", - ]), - ), ), } }