implemented globbing and tilde expansions

This commit is contained in:
2025-03-28 12:02:34 -04:00
parent 9989663c97
commit 1eb19092cc
10 changed files with 137 additions and 161 deletions

1
.gitignore vendored
View File

@@ -9,7 +9,6 @@ shell.nix
*~ *~
TODO.md TODO.md
rust-toolchain.toml rust-toolchain.toml
*src_old
# cachix tmp file # cachix tmp file
store-path-pre-build store-path-pre-build

76
Cargo.lock generated
View File

@@ -59,12 +59,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[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.10" version = "0.3.10"
@@ -97,19 +91,18 @@ name = "fern"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"glob",
"insta", "insta",
"nix", "nix",
"pretty_assertions", "pretty_assertions",
"rustyline", "rustyline",
"serde",
"serde_yaml",
] ]
[[package]] [[package]]
name = "hashbrown" name = "glob"
version = "0.15.2" version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]] [[package]]
name = "home" name = "home"
@@ -120,16 +113,6 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "indexmap"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "insta" name = "insta"
version = "1.42.2" version = "1.42.2"
@@ -143,12 +126,6 @@ dependencies = [
"similar", "similar",
] ]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.169" version = "0.2.169"
@@ -311,45 +288,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.7.0" version = "2.7.0"
@@ -391,12 +329,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "utf8parse" name = "utf8parse"
version = "0.2.2" version = "0.2.2"

View File

@@ -11,12 +11,11 @@ debug = true
[dependencies] [dependencies]
bitflags = "2.8.0" bitflags = "2.8.0"
glob = "0.3.2"
insta = "1.42.2" insta = "1.42.2"
nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] } nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl"] }
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
rustyline = { version = "15.0.0", features = [ "derive" ] } rustyline = { version = "15.0.0", features = [ "derive" ] }
serde = { version = "1.0.219", features = [ "derive" ] }
serde_yaml = "0.9.34"
[[bin]] [[bin]]
name = "fern" name = "fern"

View File

@@ -8,6 +8,8 @@ pub const VAR_SUB: char = '\u{fdd0}';
pub const DUB_QUOTE: char = '\u{fdd1}'; pub const DUB_QUOTE: char = '\u{fdd1}';
/// Single quote '\\'' marker /// Single quote '\\'' marker
pub const SNG_QUOTE: char = '\u{fdd2}'; pub const SNG_QUOTE: char = '\u{fdd2}';
/// Tilde sub marker
pub const TILDE_SUB: char = '\u{fdd3}';
impl Tk { impl Tk {
/// Create a new expanded token /// Create a new expanded token
@@ -21,6 +23,7 @@ impl Tk {
let class = TkRule::Expanded { exp }; let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags, }) Ok(Self { class, span, flags, })
} }
/// Perform word splitting
pub fn get_words(&self) -> Vec<String> { pub fn get_words(&self) -> Vec<String> {
match &self.class { match &self.class {
TkRule::Expanded { exp } => exp.clone(), TkRule::Expanded { exp } => exp.clone(),
@@ -40,6 +43,11 @@ impl Expander {
} }
pub fn expand(&mut self) -> ShResult<Vec<String>> { pub fn expand(&mut self) -> ShResult<Vec<String>> {
self.raw = self.expand_raw()?; self.raw = self.expand_raw()?;
if let Ok(glob_exp) = expand_glob(&self.raw) {
if !glob_exp.is_empty() {
self.raw = glob_exp;
}
}
Ok(self.split_words()) Ok(self.split_words())
} }
pub fn split_words(&mut self) -> Vec<String> { pub fn split_words(&mut self) -> Vec<String> {
@@ -76,6 +84,10 @@ impl Expander {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
TILDE_SUB => {
let home = env::var("HOME").unwrap_or_default();
result.push_str(&home);
}
VAR_SUB => { VAR_SUB => {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
@@ -130,6 +142,19 @@ impl Expander {
} }
} }
pub fn expand_glob(raw: &str) -> ShResult<String> {
let mut words = vec![];
for entry in glob::glob(raw)
.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? {
let entry = entry
.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
words.push(entry.to_str().unwrap().to_string())
}
Ok(words.join(" "))
}
/// Get the command output of a given command input as a String /// Get the command output of a given command input as a String
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> { pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
flog!(DEBUG, "in expand_cmd_sub"); flog!(DEBUG, "in expand_cmd_sub");
@@ -173,9 +198,14 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
pub fn unescape_str(raw: &str) -> String { pub fn unescape_str(raw: &str) -> String {
let mut chars = raw.chars(); let mut chars = raw.chars();
let mut result = String::new(); let mut result = String::new();
let mut first_char = true;
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'~' if first_char => {
result.push(TILDE_SUB)
}
'\\' => { '\\' => {
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch) result.push(next_ch)
@@ -215,6 +245,7 @@ pub fn unescape_str(raw: &str) -> String {
'$' => result.push(VAR_SUB), '$' => result.push(VAR_SUB),
_ => result.push(ch) _ => result.push(ch)
} }
first_char = false;
} }
result result
} }
@@ -602,6 +633,8 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
} }
/// Expand aliases in the given input string /// Expand aliases in the given input string
///
/// Recursively calls itself until all aliases are expanded
pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>, log_tab: &LogTab) -> String { pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>, log_tab: &LogTab) -> String {
let mut result = input.clone(); let mut result = input.clone();
let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect();

View File

@@ -13,80 +13,13 @@ pub mod shopt;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use std::collections::HashSet; use crate::libsh::sys::{save_termios, set_termios};
use crate::parse::execute::exec_input;
use crate::expand::expand_aliases; use crate::signal::sig_setup;
use libsh::error::ShResult; use crate::state::source_rc;
use parse::{execute::Dispatcher, ParsedSrc};
use signal::sig_setup;
use state::{read_logic, source_rc, write_meta};
use termios::{LocalFlags, Termios};
use crate::prelude::*; use crate::prelude::*;
/// The previous state of the terminal options.
///
/// This variable stores the terminal settings at the start of the program and restores them when the program exits.
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program.
/// It will not be mutated or accessed under any other circumstances.
///
/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky.
///
/// The possible states of this variable are:
/// - `None`: The terminal options have not been set yet (before initialization).
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved.
///
/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because:
/// - It is set once during program startup and accessed once during program exit.
/// - It is not mutated or accessed after the initial setup and final read.
///
/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() {
unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
Some(termios)
} else {
None
});
}
}
#[allow(static_mut_refs)]
pub unsafe fn get_saved_termios() -> Option<Termios> {
// SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program to reset the termios.
// Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten()
}
/// Set termios to not echo control characters, like ^Z for instance
fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
}
}
pub fn exec_input(input: String) -> ShResult<()> {
write_meta(|m| m.start_timer());
let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input, HashSet::new(), &log_tab);
let mut parser = ParsedSrc::new(Arc::new(input));
if let Err(errors) = parser.parse_src() {
for error in errors {
eprintln!("{error}");
}
return Ok(())
}
let mut dispatcher = Dispatcher::new(parser.extract_nodes());
dispatcher.begin_dispatch()
}
fn main() { fn main() {
save_termios(); save_termios();
@@ -97,7 +30,6 @@ fn main() {
eprintln!("{e}"); eprintln!("{e}");
} }
const MAX_READLINE_ERRORS: u32 = 5;
let mut readline_err_count: u32 = 0; let mut readline_err_count: u32 = 0;
loop { // Main loop loop { // Main loop
@@ -109,7 +41,7 @@ fn main() {
Err(e) => { Err(e) => {
eprintln!("{e}"); eprintln!("{e}");
readline_err_count += 1; readline_err_count += 1;
if readline_err_count == MAX_READLINE_ERRORS { if readline_err_count == 5 {
eprintln!("reached maximum readline error count, exiting"); eprintln!("reached maximum readline error count, exiting");
break break
} else { } else {

View File

@@ -45,7 +45,11 @@ pub struct Note {
impl Note { impl Note {
pub fn new(main: impl Into<String>) -> Self { pub fn new(main: impl Into<String>) -> Self {
Self { main: main.into(), sub_notes: vec![], depth: 0 } Self {
main: main.into(),
sub_notes: vec![],
depth: 0
}
} }
pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self { pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self {
@@ -192,7 +196,7 @@ impl ShErr {
let mut indicator_lines = vec![]; let mut indicator_lines = vec![];
for line in lines { for line in lines {
let indicator_line = "^".repeat(line.len()).styled(Style::Red | Style::Bold); let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold);
indicator_lines.push(indicator_line); indicator_lines.push(indicator_line);
} }

View File

@@ -1,4 +1,55 @@
use termios::{LocalFlags, Termios};
use crate::{prelude::*, state::write_jobs}; use crate::{prelude::*, state::write_jobs};
///
/// The previous state of the terminal options.
///
/// This variable stores the terminal settings at the start of the program and restores them when the program exits.
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program.
/// It will not be mutated or accessed under any other circumstances.
///
/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky.
///
/// The possible states of this variable are:
/// - `None`: The terminal options have not been set yet (before initialization).
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved.
///
/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because:
/// - It is set once during program startup and accessed once during program exit.
/// - It is not mutated or accessed after the initial setup and final read.
///
/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() {
unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
Some(termios)
} else {
None
});
}
}
#[allow(static_mut_refs)]
pub unsafe fn get_saved_termios() -> Option<Termios> {
// SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program to reset the termios.
// Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten()
}
/// Set termios to not echo control characters, like ^Z for instance
pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
}
}
pub fn sh_quit(code: i32) -> ! { pub fn sh_quit(code: i32) -> ! {
write_jobs(|j| { write_jobs(|j| {
@@ -6,7 +57,7 @@ pub fn sh_quit(code: i32) -> ! {
job.killpg(Signal::SIGTERM).ok(); job.killpg(Signal::SIGTERM).ok();
} }
}); });
if let Some(termios) = unsafe { crate::get_saved_termios() } { if let Some(termios) = unsafe { get_saved_termios() } {
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
} }
if code == 0 { if code == 0 {

View File

@@ -1,7 +1,7 @@
use std::collections::VecDeque; use std::collections::{HashSet, VecDeque};
use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_vars, ShFunc, VarTab}}; use crate::{builtin::{alias::alias, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, read_logic, read_vars, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}};
use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType}; use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType};
@@ -27,7 +27,9 @@ impl ExecArgs {
Ok(Self { cmd, argv, envp }) Ok(Self { cmd, argv, envp })
} }
pub fn get_cmd(argv: &[(String,Span)]) -> (CString,Span) { pub fn get_cmd(argv: &[(String,Span)]) -> (CString,Span) {
(CString::new(argv[0].0.as_str()).unwrap(),argv[0].1.clone()) let cmd = argv[0].0.as_str();
let span = argv[0].1.clone();
(CString::new(cmd).unwrap(),span)
} }
pub fn get_argv(argv: Vec<(String,Span)>) -> Vec<CString> { pub fn get_argv(argv: Vec<(String,Span)>) -> Vec<CString> {
argv.into_iter().map(|s| CString::new(s.0).unwrap()).collect() argv.into_iter().map(|s| CString::new(s.0).unwrap()).collect()
@@ -37,6 +39,23 @@ impl ExecArgs {
} }
} }
pub fn exec_input(input: String) -> ShResult<()> {
write_meta(|m| m.start_timer());
let log_tab = LOGIC_TABLE.read().unwrap();
let input = expand_aliases(input, HashSet::new(), &log_tab);
mem::drop(log_tab); // Release lock ASAP
let mut parser = ParsedSrc::new(Arc::new(input));
if let Err(errors) = parser.parse_src() {
for error in errors {
eprintln!("{error}");
}
return Ok(())
}
let mut dispatcher = Dispatcher::new(parser.extract_nodes());
dispatcher.begin_dispatch()
}
pub struct Dispatcher { pub struct Dispatcher {
nodes: VecDeque<Node>, nodes: VecDeque<Node>,
pub io_stack: IoStack, pub io_stack: IoStack,
@@ -430,7 +449,7 @@ impl Dispatcher {
let var = var.span.as_str(); let var = var.span.as_str();
let val = val.span.as_str(); let val = val.span.as_str();
match kind { match kind {
AssignKind::Eq => std::env::set_var(var, val), AssignKind::Eq => write_vars(|v| v.set_var(var, val, true)),
AssignKind::PlusEq => todo!(), AssignKind::PlusEq => todo!(),
AssignKind::MinusEq => todo!(), AssignKind::MinusEq => todo!(),
AssignKind::MultEq => todo!(), AssignKind::MultEq => todo!(),

View File

@@ -108,6 +108,9 @@ impl Tk {
_ => self.span.as_str().to_string() _ => self.span.as_str().to_string()
} }
} }
pub fn as_str(&self) -> &str {
self.span.as_str()
}
pub fn source(&self) -> Arc<String> { pub fn source(&self) -> Arc<String> {
self.span.source.clone() self.span.source.clone()
} }

View File

@@ -596,10 +596,7 @@ impl ParseStream {
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
let Some(pat_tk) = self.next_tk() else { let pat_err = parse_err_full(
self.panic_mode(&mut node_tks);
return Err(
parse_err_full(
"Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap() "Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap()
) )
.with_note( .with_note(
@@ -607,10 +604,17 @@ impl ParseStream {
.with_sub_notes(vec![ .with_sub_notes(vec![
"This includes variables like '$foo' or command substitutions like '$(echo foo)'" "This includes variables like '$foo' or command substitutions like '$(echo foo)'"
]) ])
) );
);
let Some(pat_tk) = self.next_tk() else {
self.panic_mode(&mut node_tks);
return Err(pat_err);
}; };
if pat_tk.span.as_str() == "in" {
return Err(pat_err)
}
pattern = pat_tk; pattern = pat_tk;
node_tks.push(pattern.clone()); node_tks.push(pattern.clone());