Work on integrating error reporting using the ariadne crate

This commit is contained in:
2026-02-28 20:30:12 -05:00
parent 1b63eff783
commit ef0f66efaa
18 changed files with 763 additions and 540 deletions

259
Cargo.lock generated
View File

@@ -61,6 +61,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "ariadne" name = "ariadne"
version = "0.6.0" version = "0.6.0"
@@ -95,6 +101,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "clap" name = "clap"
version = "4.5.55" version = "4.5.55"
@@ -153,6 +170,15 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "diff" name = "diff"
version = "0.1.13" version = "0.1.13"
@@ -188,6 +214,12 @@ dependencies = [
"log", "log",
] ]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@@ -204,6 +236,12 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.3.4" version = "0.3.4"
@@ -216,18 +254,65 @@ dependencies = [
"wasip2", "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]] [[package]]
name = "glob" name = "glob"
version = "0.3.3" version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 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]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 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]] [[package]]
name = "insta" name = "insta"
version = "1.46.1" version = "1.46.1"
@@ -276,6 +361,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.180" version = "0.2.180"
@@ -349,6 +440,16 @@ dependencies = [
"yansi", "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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -373,6 +474,23 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 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]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.2"
@@ -415,6 +533,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -470,12 +594,14 @@ dependencies = [
"log", "log",
"nix", "nix",
"pretty_assertions", "pretty_assertions",
"rand",
"regex", "regex",
"serde_json", "serde_json",
"tempfile", "tempfile",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
"vte", "vte",
"yansi",
] ]
[[package]] [[package]]
@@ -508,7 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
dependencies = [ dependencies = [
"fastrand", "fastrand",
"getrandom", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -532,6 +658,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"
@@ -557,6 +689,49 @@ dependencies = [
"wit-bindgen", "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]] [[package]]
name = "windows-link" name = "windows-link"
version = "0.2.1" version = "0.2.1"
@@ -650,6 +825,88 @@ name = "wit-bindgen"
version = "0.51.0" version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" 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]] [[package]]
name = "yansi" name = "yansi"

View File

@@ -17,11 +17,13 @@ env_logger = "0.11.9"
glob = "0.3.2" glob = "0.3.2"
log = "0.4.29" log = "0.4.29"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } 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" regex = "1.11.1"
serde_json = "1.0.149" serde_json = "1.0.149"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"
vte = "0.15" vte = "0.15"
yansi = "1.0.1"
[dev-dependencies] [dev-dependencies]
insta = "1.42.2" insta = "1.42.2"

View File

@@ -1,6 +1,8 @@
use ariadne::{Fmt, Label, Span};
use crate::{ use crate::{
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
state::{self}, state::{self},
@@ -10,6 +12,7 @@ use super::setup_builtin;
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let src = span.source();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -17,22 +20,28 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
else { else {
unreachable!() unreachable!()
}; };
let cd_span = argv.first().unwrap().span.clone();
let (argv, _) = setup_builtin(Some(argv), job, None)?; let (argv, _) = setup_builtin(Some(argv), job, None)?;
let argv = argv.unwrap(); let argv = argv.unwrap();
let new_dir = if let Some((arg, _)) = argv.into_iter().next() { let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() {
PathBuf::from(arg) (PathBuf::from(arg),Some(span))
} else { } else {
PathBuf::from(env::var("HOME").unwrap()) (PathBuf::from(env::var("HOME").unwrap()),None)
}; };
if !new_dir.exists() { if !new_dir.exists() {
return Err(ShErr::full( let color = next_color();
ShErrKind::ExecFail, let mut err = ShErr::new(
format!("cd: No such file or directory '{}'", new_dir.display()), ShErrKind::ExecFail,
span, 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() { if !new_dir.is_dir() {

View File

@@ -1,3 +1,4 @@
use ariadne::Label;
use nix::{errno::Errno, unistd::execvpe}; use nix::{errno::Errno, unistd::execvpe};
use crate::{ 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 // execvpe only returns on error
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { 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)), _ => Err(ShErr::full(ShErrKind::Errno(e), format!("{e}"), span)),
} }
} }

View File

@@ -61,11 +61,10 @@ impl FromStr for UnaryOp {
"-t" => Ok(Self::Terminal), "-t" => Ok(Self::Terminal),
"-n" => Ok(Self::NonNull), "-n" => Ok(Self::NonNull),
"-z" => Ok(Self::Null), "-z" => Ok(Self::Null),
_ => Err(ShErr::Simple { _ => Err(ShErr::simple(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: "Invalid test operator".into(), "Invalid test operator",
notes: vec![], )),
}),
} }
} }
} }
@@ -98,11 +97,10 @@ impl FromStr for TestOp {
"-ge" => Ok(Self::IntGe), "-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe), "-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
_ => Err(ShErr::Simple { _ => Err(ShErr::simple(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: "Invalid test operator".into(), "Invalid test operator",
notes: vec![], )),
}),
} }
} }
} }
@@ -140,12 +138,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let operand = operand.expand()?.get_words().join(" "); let operand = operand.expand()?.get_words().join(" ");
conjunct_op = conjunct; conjunct_op = conjunct;
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
return Err(ShErr::Full { return Err(ShErr::full(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: "Invalid unary operator".into(), "Invalid unary operator",
notes: vec![], err_span,
span: err_span, ));
});
}; };
match op { match op {
UnaryOp::Exists => { UnaryOp::Exists => {
@@ -248,12 +245,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let test_op = operator.as_str().parse::<TestOp>()?; let test_op = operator.as_str().parse::<TestOp>()?;
match test_op { match test_op {
TestOp::Unary(_) => { TestOp::Unary(_) => {
return Err(ShErr::Full { return Err(ShErr::full(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: "Expected a binary operator in this test call; found a unary operator".into(), "Expected a binary operator in this test call; found a unary operator",
notes: vec![], err_span,
span: err_span, ));
});
} }
TestOp::StringEq => { TestOp::StringEq => {
let pattern = crate::expand::glob_to_regex(rhs.trim(), true); let pattern = crate::expand::glob_to_regex(rhs.trim(), true);
@@ -269,12 +265,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
| TestOp::IntGe | TestOp::IntGe
| TestOp::IntLe | TestOp::IntLe
| TestOp::IntEq => { | TestOp::IntEq => {
let err = ShErr::Full { let err = ShErr::full(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: format!("Expected an integer with '{}' operator", operator.as_str()), format!("Expected an integer with '{}' operator", operator),
notes: vec![], err_span.clone(),
span: err_span.clone(), );
};
let Ok(lhs) = lhs.trim().parse::<i32>() else { let Ok(lhs) = lhs.trim().parse::<i32>() else {
return Err(err); return Err(err);
}; };

View File

@@ -117,8 +117,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
"zoltraak: Attempted to destroy root directory '/'", "zoltraak: Attempted to destroy root directory '/'",
) )
.with_note( .with_note(
Note::new("If you really want to do this, you can use the --no-preserve-root flag") "If you really want to do this, you can use the --no-preserve-root flag"
.with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]),
), ),
); );
} }
@@ -181,8 +180,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
format!("zoltraak: '{path}' is a directory"), format!("zoltraak: '{path}' is a directory"),
) )
.with_note( .with_note(
Note::new("Use the '-r' flag to recursively shred directories") "Use the '-r' flag to recursively shred directories"
.with_sub_notes(vec!["Example: 'zoltraak -r directory'"]),
), ),
); );
} }

View File

@@ -688,11 +688,10 @@ impl ArithTk {
chars.next(); chars.next();
} }
_ => { _ => {
return Err(ShErr::Simple { return Err(ShErr::simple(
kind: ShErrKind::ParseErr, ShErrKind::ParseErr,
msg: "Invalid character in arithmetic substitution".into(), "Invalid character in arithmetic substitution",
notes: vec![], ));
});
} }
} }
} }
@@ -750,16 +749,14 @@ impl ArithTk {
match token { match token {
ArithTk::Num(n) => stack.push(n), ArithTk::Num(n) => stack.push(n),
ArithTk::Op(op) => { ArithTk::Op(op) => {
let rhs = stack.pop().ok_or(ShErr::Simple { let rhs = stack.pop().ok_or(ShErr::simple(
kind: ShErrKind::ParseErr, ShErrKind::ParseErr,
msg: "Missing right-hand operand".into(), "Missing right-hand operand",
notes: vec![], ))?;
})?; let lhs = stack.pop().ok_or(ShErr::simple(
let lhs = stack.pop().ok_or(ShErr::Simple { ShErrKind::ParseErr,
kind: ShErrKind::ParseErr, "Missing left-hand operand",
msg: "Missing left-hand operand".into(), ))?;
notes: vec![],
})?;
let result = match op { let result = match op {
ArithOp::Add => lhs + rhs, ArithOp::Add => lhs + rhs,
ArithOp::Sub => lhs - rhs, ArithOp::Sub => lhs - rhs,
@@ -770,21 +767,19 @@ impl ArithTk {
stack.push(result); stack.push(result);
} }
_ => { _ => {
return Err(ShErr::Simple { return Err(ShErr::simple(
kind: ShErrKind::ParseErr, ShErrKind::ParseErr,
msg: "Unexpected token during evaluation".into(), "Unexpected token during evaluation",
notes: vec![], ));
});
} }
} }
} }
if stack.len() != 1 { if stack.len() != 1 {
return Err(ShErr::Simple { return Err(ShErr::simple(
kind: ShErrKind::ParseErr, ShErrKind::ParseErr,
msg: "Invalid arithmetic expression".into(), "Invalid arithmetic expression",
notes: vec![], ));
});
} }
Ok(stack[0]) Ok(stack[0])
@@ -809,11 +804,10 @@ impl FromStr for ArithOp {
'*' => Ok(Self::Mul), '*' => Ok(Self::Mul),
'/' => Ok(Self::Div), '/' => Ok(Self::Div),
'%' => Ok(Self::Mod), '%' => Ok(Self::Mod),
_ => Err(ShErr::Simple { _ => Err(ShErr::simple(
kind: ShErrKind::ParseErr, ShErrKind::ParseErr,
msg: "Invalid arithmetic operator".into(), "Invalid arithmetic operator",
notes: vec![], )),
}),
} }
} }
} }
@@ -863,7 +857,7 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
io_stack.push_frame(io_frame); io_stack.push_frame(io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) {
eprintln!("{e}"); e.print_error();
exit(1); exit(1);
} }
exit(0); exit(0);
@@ -894,7 +888,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
ForkResult::Child => { ForkResult::Child => {
io_stack.push_frame(cmd_sub_io_frame); io_stack.push_frame(cmd_sub_io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { 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(1) };
} }
unsafe { libc::_exit(0) }; unsafe { libc::_exit(0) };
@@ -1275,11 +1269,10 @@ impl FromStr for ParamExp {
use ParamExp::*; use ParamExp::*;
let parse_err = || { let parse_err = || {
Err(ShErr::Simple { Err(ShErr::simple(
kind: ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
msg: "Invalid parameter expansion".into(), "Invalid parameter expansion",
notes: vec![], ) )
})
}; };
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
@@ -1437,11 +1430,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val), Some(val) => Ok(val),
None => { None => {
let expanded = expand_raw(&mut err.chars().peekable())?; let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::Simple { Err(ShErr::simple(
kind: ShErrKind::ExecFail, ShErrKind::ExecFail,
msg: expanded, expanded,
notes: vec![], ))
})
} }
} }
} }
@@ -1449,11 +1441,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val), Some(val) => Ok(val),
None => { None => {
let expanded = expand_raw(&mut err.chars().peekable())?; let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::Simple { Err(ShErr::simple(
kind: ShErrKind::ExecFail, ShErrKind::ExecFail,
msg: expanded, expanded,
notes: vec![], ))
})
} }
}, },
ParamExp::Substr(pos) => { ParamExp::Substr(pos) => {

View File

@@ -1,13 +1,63 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use ariadne::Color;
use ariadne::{Report, ReportKind};
use rand::{RngExt, TryRng};
use crate::{ use crate::{
libsh::term::{Style, Styled}, libsh::term::{Style, Styled},
parse::lex::Span, parse::lex::{Span, SpanSource},
prelude::*, prelude::*,
}; };
pub type ShResult<T> = Result<T, ShErr>; pub type ShResult<T> = Result<T, ShErr>;
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<Self::Item> {
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<ColorRng> = const { RefCell::new(ColorRng) };
}
pub fn next_color() -> Color {
COLOR_RNG.with(|rng| rng.borrow_mut().next().unwrap())
}
pub trait ShResultExt { pub trait ShResultExt {
fn blame(self, span: Span) -> Self; fn blame(self, span: Span) -> Self;
fn try_blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self;
@@ -16,39 +66,11 @@ pub trait ShResultExt {
impl<T> ShResultExt for Result<T, ShErr> { impl<T> ShResultExt for Result<T, ShErr> {
/// Blame a span for an error /// Blame a span for an error
fn blame(self, new_span: Span) -> Self { fn blame(self, new_span: Span) -> Self {
let Err(e) = self else { return self }; self.map_err(|e| e.blame(new_span))
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,
}),
}
} }
/// Blame a span if no blame has been assigned yet /// Blame a span if no blame has been assigned yet
fn try_blame(self, new_span: Span) -> Self { fn try_blame(self, new_span: Span) -> Self {
let Err(e) = &self else { return self }; self.map_err(|e| e.try_blame(new_span))
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,
}
} }
} }
@@ -107,270 +129,126 @@ impl Display for Note {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum ShErr { pub struct ShErr {
Simple { kind: ShErrKind,
kind: ShErrKind, src_span: Option<Span>,
msg: String, labels: Vec<ariadne::Label<Span>>,
notes: Vec<Note>, sources: Vec<SpanSource>,
}, notes: Vec<String>
Full {
kind: ShErrKind,
msg: String,
notes: Vec<Note>,
span: Span,
},
} }
impl ShErr { impl ShErr {
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self { pub fn new(kind: ShErrKind, span: Span) -> Self {
let msg = msg.into(); Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![] }
Self::Simple { }
kind, pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
msg, Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()] }
notes: vec![], }
} pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self {
} Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![msg.into()] }
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self { }
let msg = msg.into(); pub fn blame(self, span: Span) -> Self {
Self::Full { let ShErr { kind, src_span: _, labels, sources, notes } = self;
kind, Self { kind, src_span: Some(span), labels, sources, notes }
msg, }
span, pub fn try_blame(self, span: Span) -> Self {
notes: vec![], match self {
} ShErr { kind, src_span: None, labels, sources, notes } => Self { kind, src_span: Some(span), labels, sources, notes },
} _ => self
pub fn unpack(self) -> (ShErrKind, String, Vec<Note>, Option<Span>) { }
match self { }
ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None), pub fn kind(&self) -> &ShErrKind {
ShErr::Full { &self.kind
kind, }
msg, pub fn rename(mut self, name: impl Into<String>) -> Self {
notes, if let Some(span) = self.src_span.as_mut() {
span, span.rename(name.into());
} => (kind, msg, notes, Some(span)), }
} self
} }
pub fn with_note(self, note: Note) -> Self { pub fn with_label(self, source: SpanSource, label: ariadne::Label<Span>) -> Self {
let (kind, msg, mut notes, span) = self.unpack(); let ShErr { kind, src_span, mut labels, mut sources, notes } = self;
notes.push(note); sources.push(source);
if let Some(span) = span { labels.push(label);
Self::Full { Self { kind, src_span, labels, sources, notes }
kind, }
msg, pub fn with_context(self, ctx: Vec<(SpanSource, ariadne::Label<Span>)>) -> Self {
notes, let ShErr { kind, src_span, mut labels, mut sources, notes } = self;
span, for (src, label) in ctx {
} sources.push(src);
} else { labels.push(label);
Self::Simple { kind, msg, notes } }
} Self { kind, src_span, labels, sources, notes }
} }
pub fn with_span(sherr: ShErr, span: Span) -> Self { pub fn with_note(self, note: impl Into<String>) -> Self {
let (kind, msg, notes, _) = sherr.unpack(); let ShErr { kind, src_span, labels, sources, mut notes } = self;
Self::Full { notes.push(note.into());
kind, Self { kind, src_span, labels, sources, notes }
msg, }
notes, pub fn build_report(&self) -> Option<Report<'_, Span>> {
span, let span = self.src_span.as_ref()?;
} let mut report = Report::build(ReportKind::Error, span.clone())
} .with_config(ariadne::Config::default().with_color(true));
pub fn kind(&self) -> &ShErrKind { let msg = if self.notes.is_empty() {
match self { self.kind.to_string()
ShErr::Simple { } else {
kind, format!("{} - {}", self.kind, self.notes.first().unwrap())
msg: _, };
notes: _, report = report.with_message(msg);
}
| 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();
let src = span.get_source(); for label in self.labels.clone() {
let mut chars = src.chars(); report = report.with_label(label);
}
for note in &self.notes {
report = report.with_note(note);
}
while let Some(ch) = chars.next() { Some(report.finish())
total_len += ch.len_utf8(); }
cur_line.push(ch); fn collect_sources(&self) -> HashMap<SpanSource, String> {
if ch == '\n' { let mut source_map = HashMap::new();
if total_len > span.start { if let Some(span) = &self.src_span {
let line = (total_lines, mem::take(&mut cur_line)); let src = span.span_source().clone();
lines.push(line); source_map.entry(src.clone())
} .or_insert_with(|| src.content().to_string());
if total_len >= span.end { }
break; for src in &self.sources {
} source_map.entry(src.clone())
total_lines += 1; .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(); let sources = self.collect_sources();
} let cache = ariadne::FnCache::new(move |src: &SpanSource| {
} sources.get(src)
.cloned()
if !cur_line.is_empty() { .ok_or_else(|| format!("Failed to fetch source '{}'", src.name()))
let line = (total_lines, mem::take(&mut cur_line)); });
lines.push(line); if report.eprint(cache).is_err() {
} default();
}
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<Vec<String>> {
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)
}
}
}
} }
impl Display for ShErr { impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { if self.notes.is_empty() {
Self::Simple { write!(f, "{}", self.kind)
msg, } else {
kind: _, write!(f, "{} - {}", self.kind, self.notes.first().unwrap())
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(())
}
}
}
} }
impl From<std::io::Error> for ShErr { impl From<std::io::Error> for ShErr {
@@ -404,9 +282,8 @@ pub enum ShErrKind {
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno(Errno), Errno(Errno),
FileNotFound(String), FileNotFound,
CmdNotFound(String), CmdNotFound,
ReadlineIntr(String),
ReadlineErr, ReadlineErr,
// Not really errors, more like internal signals // Not really errors, more like internal signals
@@ -431,13 +308,12 @@ impl Display for ShErrKind {
Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::ResourceLimitExceeded => "Resource Limit Exceeded",
Self::BadPermission => "Bad Permissions", Self::BadPermission => "Bad Permissions",
Self::Errno(e) => &format!("Errno: {}", e.desc()), Self::Errno(e) => &format!("Errno: {}", e.desc()),
Self::FileNotFound(file) => &format!("File not found: {file}"), Self::FileNotFound => "File not found",
Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), Self::CmdNotFound => "Command not found",
Self::CleanExit(_) => "", Self::CleanExit(_) => "",
Self::FuncReturn(_) => "Syntax Error", Self::FuncReturn(_) => "Syntax Error",
Self::LoopContinue(_) => "Syntax Error", Self::LoopContinue(_) => "Syntax Error",
Self::LoopBreak(_) => "Syntax Error", Self::LoopBreak(_) => "Syntax Error",
Self::ReadlineIntr(_) => "",
Self::ReadlineErr => "Readline Error", Self::ReadlineErr => "Readline Error",
Self::ClearReadline => "", Self::ClearReadline => "",
Self::Null => "", Self::Null => "",

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, ops::BitOr}; use std::{cell::RefCell, fmt::Display, ops::BitOr};
pub trait Styled: Sized + Display { pub trait Styled: Sized + Display {
fn styled<S: Into<StyleSet>>(self, style: S) -> String { fn styled<S: Into<StyleSet>>(self, style: S) -> String {

View File

@@ -78,7 +78,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
if let Some(first_tk) = self.first() { if let Some(first_tk) = self.first() {
self self
.last() .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 { } else {
None None
} }

View File

@@ -1,7 +1,8 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator,
clippy::result_large_err
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -87,6 +88,7 @@ fn setup_panic_handler() {
} }
fn main() -> ExitCode { fn main() -> ExitCode {
yansi::enable();
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
setup_panic_handler(); setup_panic_handler();
@@ -162,7 +164,7 @@ fn shed_interactive() -> ShResult<()> {
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); e.print_error();
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
@@ -197,7 +199,7 @@ fn shed_interactive() -> ShResult<()> {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => e.print_error(),
} }
} }
} }
@@ -269,7 +271,7 @@ fn shed_interactive() -> ShResult<()> {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => e.print_error(),
} }
} }
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
@@ -295,7 +297,7 @@ fn shed_interactive() -> ShResult<()> {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => e.print_error(),
}, },
} }
} }

View File

@@ -1,16 +1,17 @@
use std::{ use std::{
collections::{HashSet, VecDeque}, cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt
os::unix::fs::PermissionsExt,
}; };
use ariadne::{Fmt, Label};
use crate::{ use crate::{
builtin::{ 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}, expand::{expand_aliases, glob_to_regex},
jobs::{ChildProc, JobStack, dispatch_job}, jobs::{ChildProc, JobStack, dispatch_job},
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt}, error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
prelude::*, prelude::*,
@@ -27,7 +28,7 @@ use super::{
}; };
thread_local! { thread_local! {
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) }; static RECURSE_DEPTH: Cell<usize> = const { Cell::new(0) };
} }
pub fn is_in_path(name: &str) -> bool { pub fn is_in_path(name: &str) -> bool {
@@ -160,7 +161,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags);
if let Err(errors) = parser.parse_src() { if let Err(errors) = parser.parse_src() {
for error in errors { for error in errors {
eprintln!("{error}"); error.print_error();
} }
return Ok(()); return Ok(());
} }
@@ -284,6 +285,7 @@ impl Dispatcher {
} }
pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> { pub fn exec_func_def(&mut self, func_def: Node) -> ShResult<()> {
let blame = func_def.get_span(); let blame = func_def.get_span();
let ctx = func_def.context.clone();
let NdRule::FuncDef { name, body } = func_def.class else { let NdRule::FuncDef { name, body } = func_def.class else {
unreachable!() 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() { if let Err(errors) = func_parser.parse_src() {
for error in errors { for error in errors {
eprintln!("{error}"); error.print_error();
} }
return Ok(()); return Ok(());
} }
@@ -318,14 +320,14 @@ impl Dispatcher {
self.run_fork("anonymous_subshell", |s| { self.run_fork("anonymous_subshell", |s| {
if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) {
eprintln!("{e}"); e.print_error();
return; return;
}; };
s.io_stack.append_to_frame(subsh.redirs); s.io_stack.append_to_frame(subsh.redirs);
let mut argv = match prepare_argv(argv) { let mut argv = match prepare_argv(argv) {
Ok(argv) => argv, Ok(argv) => argv,
Err(e) => { Err(e) => {
eprintln!("{e}"); e.print_error();
return; return;
} }
}; };
@@ -334,12 +336,12 @@ impl Dispatcher {
let subsh_body = subsh.0.to_string(); let subsh_body = subsh.0.to_string();
if let Err(e) = exec_input(subsh_body, None, s.interactive) { if let Err(e) = exec_input(subsh_body, None, s.interactive) {
eprintln!("{e}"); e.print_error();
}; };
}) })
} }
fn exec_func(&mut self, func: Node) -> ShResult<()> { fn exec_func(&mut self, func: Node) -> ShResult<()> {
let blame = func.get_span().clone(); let mut blame = func.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments, assignments,
mut argv, mut argv,
@@ -369,10 +371,11 @@ impl Dispatcher {
self.io_stack.append_to_frame(func.redirs); self.io_stack.append_to_frame(func.redirs);
let func_name = argv.remove(0).span.as_str().to_string(); let func_name = argv.remove(0).span.as_str().to_string();
blame.rename(func_name.clone());
let argv = prepare_argv(argv)?; let argv = prepare_argv(argv)?;
let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) {
let _guard = ScopeGuard::exclusive_scope(Some(argv)); let _guard = ScopeGuard::exclusive_scope(Some(argv));
func_body.body_mut().flags = func.flags; func_body.body_mut().flags = func.flags;
if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { if let Err(e) = self.exec_brc_grp(func_body.body().clone()) {
@@ -381,7 +384,7 @@ impl Dispatcher {
state::set_status(*code); state::set_status(*code);
Ok(()) Ok(())
} }
_ => Err(e).blame(blame), _ => Err(e),
} }
} else { } else {
Ok(()) Ok(())
@@ -418,7 +421,7 @@ impl Dispatcher {
log::trace!("Forking brace group"); log::trace!("Forking brace group");
self.run_fork("brace group", |s| { self.run_fork("brace group", |s| {
if let Err(e) = brc_grp_logic(s) { if let Err(e) = brc_grp_logic(s) {
eprintln!("{e}"); e.print_error();
} }
}) })
} else { } else {
@@ -472,7 +475,7 @@ impl Dispatcher {
log::trace!("Forking builtin: case"); log::trace!("Forking builtin: case");
self.run_fork("case", |s| { self.run_fork("case", |s| {
if let Err(e) = case_logic(s) { if let Err(e) = case_logic(s) {
eprintln!("{e}"); e.print_error();
} }
}) })
} else { } else {
@@ -535,7 +538,7 @@ impl Dispatcher {
log::trace!("Forking builtin: loop"); log::trace!("Forking builtin: loop");
self.run_fork("loop", |s| { self.run_fork("loop", |s| {
if let Err(e) = loop_logic(s) { if let Err(e) = loop_logic(s) {
eprintln!("{e}"); e.print_error();
} }
}) })
} else { } else {
@@ -613,7 +616,7 @@ impl Dispatcher {
log::trace!("Forking builtin: for"); log::trace!("Forking builtin: for");
self.run_fork("for", |s| { self.run_fork("for", |s| {
if let Err(e) = for_logic(s) { if let Err(e) = for_logic(s) {
eprintln!("{e}"); e.print_error();
} }
}) })
} else { } else {
@@ -669,7 +672,7 @@ impl Dispatcher {
log::trace!("Forking builtin: if"); log::trace!("Forking builtin: if");
self.run_fork("if", |s| { self.run_fork("if", |s| {
if let Err(e) = if_logic(s) { if let Err(e) = if_logic(s) {
eprintln!("{e}"); e.print_error();
state::set_status(1); state::set_status(1);
} }
}) })
@@ -726,7 +729,7 @@ impl Dispatcher {
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
self.run_fork(&cmd_raw, |s| { self.run_fork(&cmd_raw, |s| {
if let Err(e) = s.dispatch_builtin(cmd) { if let Err(e) = s.dispatch_builtin(cmd) {
eprintln!("{e}"); e.print_error();
} }
}) })
} else { } else {
@@ -819,6 +822,7 @@ impl Dispatcher {
} }
} }
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
let context = cmd.context.clone();
let NdRule::Command { assignments, argv } = cmd.class else { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
}; };
@@ -855,12 +859,22 @@ impl Dispatcher {
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => { Errno::ENOENT => {
let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); let source = span.span_source().clone();
eprintln!("{err}"); 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); ShErr::full(ShErrKind::Errno(e), format!("{e}"), span)
eprintln!("{err}"); .with_context(context)
.print_error();
} }
} }
exit(e as i32) exit(e as i32)

View File

@@ -64,24 +64,59 @@ impl QuoteState {
} }
} }
#[derive(Clone, PartialEq, Default, Debug, Eq, Hash)]
pub struct SpanSource {
name: String,
content: Arc<String>
}
impl SpanSource {
pub fn name(&self) -> &str {
&self.name
}
pub fn content(&self) -> Arc<String> {
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) /// Span::new(10..20)
#[derive(Clone, PartialEq, Default, Debug)] #[derive(Clone, PartialEq, Default, Debug)]
pub struct Span { pub struct Span {
range: Range<usize>, range: Range<usize>,
source: Arc<String>, source: SpanSource
} }
impl Span { impl Span {
/// New `Span`. Wraps a range and a string slice that it refers to. /// New `Span`. Wraps a range and a string slice that it refers to.
pub fn new(range: Range<usize>, source: Arc<String>) -> Self { pub fn new(range: Range<usize>, source: Arc<String>) -> Self {
let source = SpanSource { name: "<stdin>".into(), content: source };
Span { range, 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 /// Slice the source string at the wrapped range
pub fn as_str(&self) -> &str { 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<String> { pub fn get_source(&self) -> Arc<String> {
self.source.clone() self.source.content.clone()
}
pub fn span_source(&self) -> &SpanSource {
&self.source
} }
pub fn range(&self) -> Range<usize> { pub fn range(&self) -> Range<usize> {
self.range.clone() self.range.clone()
@@ -93,14 +128,23 @@ impl Span {
} }
} }
/// Allows simple access to the underlying range wrapped by the span impl ariadne::Span for Span {
impl Deref for Span { type SourceId = SpanSource;
type Target = Range<usize>;
fn deref(&self) -> &Self::Target { fn source(&self) -> &Self::SourceId {
&self.range &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)] #[derive(Clone, PartialEq, Debug)]
pub enum TkRule { pub enum TkRule {
Null, Null,
@@ -148,7 +192,7 @@ impl Tk {
self.span.as_str() self.span.as_str()
} }
pub fn source(&self) -> Arc<String> { pub fn source(&self) -> Arc<String> {
self.span.source.clone() self.span.source.content.clone()
} }
pub fn mark(&mut self, flag: TkFlags) { pub fn mark(&mut self, flag: TkFlags) {
self.flags |= flag; self.flags |= flag;
@@ -931,12 +975,12 @@ pub fn split_tk(tk: &Tk, pat: &str) -> Vec<Tk> {
let mut cursor = 0; let mut cursor = 0;
let mut splits = vec![]; let mut splits = vec![];
while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { 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)); splits.push(Tk::new(tk.class.clone(), before_span));
cursor += split.0.len() + pat.len(); cursor += split.0.len() + pat.len();
} }
if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { 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.push(Tk::new(tk.class.clone(), remaining_span));
} }
splits splits
@@ -957,8 +1001,8 @@ pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> {
} }
if slice[i..].starts_with(pat) { if slice[i..].starts_with(pat) {
let before_span = Span::new(tk.span.start..tk.span.start + i, 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.start + i + pat.len()..tk.span.end, 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 before_tk = Tk::new(tk.class.clone(), before_span);
let after_tk = Tk::new(tk.class.clone(), after_span); let after_tk = Tk::new(tk.class.clone(), after_span);
return Some((before_tk, after_tk)); return Some((before_tk, after_tk));

View File

@@ -1,12 +1,14 @@
use std::str::FromStr; use std::{fmt::Debug, str::FromStr, sync::Arc};
use ariadne::{Fmt, Label};
use bitflags::bitflags; use bitflags::bitflags;
use fmt::Display; 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::{ use crate::{
libsh::{ libsh::{
error::{Note, ShErr, ShErrKind, ShResult}, error::{Note, ShErr, ShErrKind, ShResult, next_color},
utils::TkVecUtils, utils::TkVecUtils,
}, },
prelude::*, prelude::*,
@@ -45,6 +47,7 @@ pub struct ParsedSrc {
pub src: Arc<String>, pub src: Arc<String>,
pub ast: Ast, pub ast: Ast,
pub lex_flags: LexFlags, pub lex_flags: LexFlags,
pub context: LabelCtx,
} }
impl ParsedSrc { impl ParsedSrc {
@@ -53,12 +56,17 @@ impl ParsedSrc {
src, src,
ast: Ast::new(vec![]), ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(), lex_flags: LexFlags::empty(),
context: vec![],
} }
} }
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self { pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags; self.lex_flags = flags;
self self
} }
pub fn with_context(mut self, ctx: LabelCtx) -> Self {
self.context = ctx;
self
}
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> { pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
let mut tokens = vec![]; let mut tokens = vec![];
for lex_result in LexStream::new(self.src.clone(), self.lex_flags) { for lex_result in LexStream::new(self.src.clone(), self.lex_flags) {
@@ -70,7 +78,7 @@ impl ParsedSrc {
let mut errors = vec![]; let mut errors = vec![];
let mut nodes = 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 { match parse_result {
Ok(node) => nodes.push(node), Ok(node) => nodes.push(node),
Err(error) => errors.push(error), Err(error) => errors.push(error),
@@ -104,12 +112,15 @@ impl Ast {
} }
} }
pub type LabelCtx = Vec<(SpanSource, Label<Span>)>;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Node { pub struct Node {
pub class: NdRule, pub class: NdRule,
pub flags: NdFlags, pub flags: NdFlags,
pub redirs: Vec<Redir>, pub redirs: Vec<Redir>,
pub tokens: Vec<Tk>, pub tokens: Vec<Tk>,
pub context: LabelCtx,
} }
impl Node { impl Node {
@@ -133,7 +144,7 @@ impl Node {
}; };
Span::new( 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(), first_tk.span.get_source(),
) )
} }
@@ -539,14 +550,25 @@ pub enum NdRule {
}, },
} }
#[derive(Debug)]
pub struct ParseStream { pub struct ParseStream {
pub tokens: Vec<Tk>, pub tokens: Vec<Tk>,
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 { impl ParseStream {
pub fn new(tokens: Vec<Tk>) -> Self { pub fn new(tokens: Vec<Tk>) -> Self {
Self { tokens } Self { tokens, context: vec![] }
}
pub fn with_context(tokens: Vec<Tk>, context: LabelCtx) -> Self {
Self { tokens, context }
} }
fn next_tk_class(&self) -> &TkRule { fn next_tk_class(&self) -> &TkRule {
if let Some(tk) = self.tokens.first() { if let Some(tk) = self.tokens.first() {
@@ -684,6 +706,7 @@ impl ParseStream {
class: NdRule::Conjunction { elements }, class: NdRule::Conjunction { elements },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs: vec![], redirs: vec![],
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
})) }))
} }
@@ -697,23 +720,47 @@ impl ParseStream {
} }
let name_tk = self.next_tk().unwrap(); let name_tk = self.next_tk().unwrap();
node_tks.push(name_tk.clone()); 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 { let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else {
self.context.pop();
return Err(parse_err_full( return Err(parse_err_full(
"Expected a brace group after function name", "Expected a brace group after function name",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
}; };
body = Box::new(brc_grp); 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 { let node = Node {
class: NdRule::FuncDef { name, body }, class: NdRule::FuncDef { name, body },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs: vec![], redirs: vec![],
tokens: node_tks, tokens: node_tks,
context: self.context.clone()
}; };
self.context.pop();
Ok(Some(node)) Ok(Some(node))
} }
fn panic_mode(&mut self, node_tks: &mut Vec<Tk>) { fn panic_mode(&mut self, node_tks: &mut Vec<Tk>) {
@@ -743,6 +790,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Malformed test call", "Malformed test call",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} else { } else {
break; break;
@@ -769,6 +817,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Invalid placement for logical operator in test", "Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
let op = match tk.class { let op = match tk.class {
@@ -784,6 +833,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Invalid placement for logical operator in test", "Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
} }
@@ -797,6 +847,7 @@ impl ParseStream {
class: NdRule::Test { cases }, class: NdRule::Test { cases },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs: vec![], redirs: vec![],
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(node)) Ok(Some(node))
@@ -828,6 +879,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected a closing brace for this brace group", "Expected a closing brace for this brace group",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
} }
@@ -840,6 +892,7 @@ impl ParseStream {
class: NdRule::BraceGrp { body }, class: NdRule::BraceGrp { body },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs, redirs,
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(node)) Ok(Some(node))
@@ -893,12 +946,10 @@ impl ParseStream {
let pat_err = parse_err_full( let pat_err = parse_err_full(
"Expected a pattern after 'case' keyword", "Expected a pattern after 'case' keyword",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
) )
.with_note( .with_note(
Note::new("Patterns can be raw text, or anything that gets substituted with raw text") "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)'",
]),
); );
let Some(pat_tk) = self.next_tk() else { let Some(pat_tk) = self.next_tk() else {
@@ -919,6 +970,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'in' after case variable name", "Expected 'in' after case variable name",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -931,6 +983,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected a case pattern here", "Expected a case pattern here",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
let case_pat_tk = self.next_tk().unwrap(); let case_pat_tk = self.next_tk().unwrap();
@@ -967,6 +1020,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'esac' after case block", "Expected 'esac' after case block",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
} }
@@ -978,10 +1032,17 @@ impl ParseStream {
}, },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs, redirs,
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(node)) Ok(Some(node))
} }
fn make_err(&self, span: lex::Span, label: Label<lex::Span>) -> 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<Option<Node>> { fn parse_if(&mut self) -> ShResult<Option<Node>> {
// Needs at last one 'if-then', // Needs at last one 'if-then',
// Any number of 'elif-then', // Any number of 'elif-then',
@@ -1000,10 +1061,14 @@ impl ParseStream {
let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" }; let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" };
let Some(cond) = self.parse_cmd_list()? else { let Some(cond) = self.parse_cmd_list()? else {
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
return Err(parse_err_full( let span = node_tks.get_span().unwrap();
&format!("Expected an expression after '{prefix_keywrd}'"), let color = next_color();
&node_tks.get_span().unwrap(), 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()); node_tks.extend(cond.tokens.clone());
@@ -1012,6 +1077,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
&format!("Expected 'then' after '{prefix_keywrd}' condition"), &format!("Expected 'then' after '{prefix_keywrd}' condition"),
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1027,6 +1093,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'then'", "Expected an expression after 'then'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
}; };
let cond_node = CondNode { let cond_node = CondNode {
@@ -1056,6 +1123,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'else'", "Expected an expression after 'else'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
} }
@@ -1066,6 +1134,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'fi' after if statement", "Expected 'fi' after if statement",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1081,6 +1150,7 @@ impl ParseStream {
}, },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs, redirs,
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(node)) Ok(Some(node))
@@ -1120,6 +1190,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"This for loop is missing a variable", "This for loop is missing a variable",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
if arr.is_empty() { if arr.is_empty() {
@@ -1127,6 +1198,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"This for loop is missing an array", "This for loop is missing an array",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
if !self.check_keyword("do") || !self.next_tk_is_some() { if !self.check_keyword("do") || !self.next_tk_is_some() {
@@ -1134,6 +1206,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Missing a 'do' for this for loop", "Missing a 'do' for this for loop",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1149,6 +1222,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Missing a 'done' after this for loop", "Missing a 'done' after this for loop",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1159,6 +1233,7 @@ impl ParseStream {
class: NdRule::ForNode { vars, arr, body }, class: NdRule::ForNode { vars, arr, body },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs, redirs,
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(node)) Ok(Some(node))
@@ -1188,6 +1263,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
&format!("Expected an expression after '{loop_kind}'"), // It also implements Display &format!("Expected an expression after '{loop_kind}'"), // It also implements Display
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
}; };
node_tks.extend(cond.tokens.clone()); node_tks.extend(cond.tokens.clone());
@@ -1197,6 +1273,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'do' after loop condition", "Expected 'do' after loop condition",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1212,6 +1289,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'do'", "Expected an expression after 'do'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
}; };
@@ -1221,6 +1299,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'done' after loop body", "Expected 'done' after loop body",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone()
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1240,6 +1319,7 @@ impl ParseStream {
}, },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs, redirs,
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
}; };
Ok(Some(loop_node)) Ok(Some(loop_node))
@@ -1277,6 +1357,7 @@ impl ParseStream {
}, },
flags, flags,
redirs: vec![], redirs: vec![],
context: self.context.clone(),
tokens: node_tks, tokens: node_tks,
})) }))
} }
@@ -1295,6 +1376,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Found case pattern in command", "Found case pattern in command",
&prefix_tk.span, &prefix_tk.span,
self.context.clone()
)); ));
} }
let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD); let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD);
@@ -1335,6 +1417,7 @@ impl ParseStream {
tokens: node_tks, tokens: node_tks,
flags, flags,
redirs, redirs,
context: self.context.clone(),
})); }));
} }
} }
@@ -1398,16 +1481,17 @@ impl ParseStream {
tokens: node_tks, tokens: node_tks,
flags, flags,
redirs, redirs,
context: self.context.clone(),
})) }))
} }
fn parse_assignment(&self, token: &Tk) -> Option<Node> { fn parse_assignment(&self, token: &Tk) -> Option<Node> {
let mut chars = token.span.as_str().chars(); let mut chars = token.span.as_str().chars();
let mut var_name = String::new(); 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 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 assign_kind = None;
let mut pos = token.span.start; let mut pos = token.span.range().start;
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
if assign_kind.is_some() { if assign_kind.is_some() {
@@ -1500,6 +1584,7 @@ impl ParseStream {
tokens: vec![token.clone()], tokens: vec![token.clone()],
flags, flags,
redirs: vec![], redirs: vec![],
context: self.context.clone(),
}) })
} else { } else {
None None
@@ -1556,8 +1641,14 @@ pub fn get_redir_file(class: RedirType, path: PathBuf) -> ShResult<File> {
Ok(result?) Ok(result?)
} }
fn parse_err_full(reason: &str, blame: &Span) -> ShErr { fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr {
ShErr::full(ShErrKind::ParseErr, reason, blame.clone()) 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 { fn is_func_name(tk: Option<&Tk>) -> bool {

View File

@@ -696,7 +696,7 @@ impl Completer {
tks tks
.iter() .iter()
.next() .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)) .map(|i| i.saturating_sub(1))
.unwrap_or(segments.len().saturating_sub(1)); .unwrap_or(segments.len().saturating_sub(1));
@@ -705,13 +705,13 @@ impl Completer {
let cword = if let Some(pos) = relevant let cword = if let Some(pos) = relevant
.iter() .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 pos
} else { } else {
let insert_pos = relevant let insert_pos = relevant
.iter() .iter()
.position(|tk| tk.span.start > cursor_pos) .position(|tk| tk.span.range().start > cursor_pos)
.unwrap_or(relevant.len()); .unwrap_or(relevant.len());
let mut new_tk = Tk::default(); let mut new_tk = Tk::default();
@@ -761,7 +761,7 @@ impl Completer {
// Set token_span from CompContext's current word // Set token_span from CompContext's current word
if let Some(cur) = ctx.words.get(ctx.cword) { 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 { } else {
self.token_span = (cursor_pos, cursor_pos); self.token_span = (cursor_pos, cursor_pos);
} }
@@ -799,7 +799,7 @@ impl Completer {
return Ok(CompResult::from_candidates(candidates)); 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 // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB
// inside a token) // inside a token)
@@ -812,7 +812,7 @@ impl Completer {
// If token contains '=', only complete after the '=' // If token contains '=', only complete after the '='
let token_str = cur_token.span.as_str(); let token_str = cur_token.span.as_str();
if let Some(eq_pos) = token_str.rfind('=') { 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 cur_token
.span .span
.set_range(self.token_span.0..self.token_span.1); .set_range(self.token_span.0..self.token_span.1);

View File

@@ -70,11 +70,10 @@ impl HistEntry {
impl FromStr for HistEntry { impl FromStr for HistEntry {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err(ShErr::Simple { let err = Err(ShErr::simple(
kind: ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on history entry '{s}'"), format!("Bad formatting on history entry '{s}'"),
notes: vec![], ));
});
//: 248972349;148;echo foo; echo bar //: 248972349;148;echo foo; echo bar
let Some(cleaned) = s.strip_prefix(": ") else { let Some(cleaned) = s.strip_prefix(": ") else {
@@ -133,11 +132,10 @@ impl FromStr for HistEntries {
while let Some((i, line)) = lines.next() { while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") { if !line.starts_with(": ") {
return Err(ShErr::Simple { return Err(ShErr::simple(
kind: ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on line {i}"), format!("Bad formatting on line {i}"),
notes: vec![], ));
});
} }
let mut chars = line.chars().peekable(); let mut chars = line.chars().peekable();
let mut feeding_lines = true; let mut feeding_lines = true;
@@ -163,11 +161,10 @@ impl FromStr for HistEntries {
} }
if feeding_lines { if feeding_lines {
let Some((_, line)) = lines.next() else { let Some((_, line)) = lines.next() else {
return Err(ShErr::Simple { return Err(ShErr::simple(
kind: ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on line {i}"), format!("Bad formatting on line {i}"),
notes: vec![], ));
});
}; };
chars = line.chars().peekable(); chars = line.chars().peekable();
} }

View File

@@ -975,7 +975,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
other => other, 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 { let Some(ctx) = stack.last() else {
return false; return false;
@@ -989,27 +989,27 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
if token.class != TkRule::Str if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class) && let Some(marker) = marker_for(&token.class)
{ {
insertions.push((token.span.end, markers::RESET)); insertions.push((token.span.range().end, markers::RESET));
insertions.push((token.span.start, marker)); insertions.push((token.span.range().start, marker));
return insertions; return insertions;
} else if token.flags.contains(TkFlags::IS_SUBSH) { } else if token.flags.contains(TkFlags::IS_SUBSH) {
let token_raw = token.span.as_str(); let token_raw = token.span.as_str();
if token_raw.ends_with(')') { 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; return insertions;
} else if token.class == TkRule::CasePattern { } else if token.class == TkRule::CasePattern {
insertions.push((token.span.end, markers::RESET)); insertions.push((token.span.range().end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT)); insertions.push((token.span.range().end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR)); insertions.push((token.span.range().start, markers::OPERATOR));
return insertions; return insertions;
} }
let token_raw = token.span.as_str(); let token_raw = token.span.as_str();
let mut token_chars = token_raw.char_indices().peekable(); 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 qt_state = QuoteState::default();
let mut cmd_sub_depth = 0; 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, (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() { while let Some((i, ch)) = token_chars.peek() {
let index = *i; // we have to dereference this here because rustc is a very pedantic program let index = *i; // we have to dereference this here because rustc is a very pedantic program

View File

@@ -109,10 +109,6 @@ impl ShOpts {
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'core' or 'prompt' in shopt key", "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, ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key", "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, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'"), 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, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'"), 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, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{opt}'"), 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, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{query}'"), 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",
]),
),
), ),
} }
} }