implemented globbing and tilde expansions
This commit is contained in:
@@ -8,6 +8,8 @@ pub const VAR_SUB: char = '\u{fdd0}';
|
||||
pub const DUB_QUOTE: char = '\u{fdd1}';
|
||||
/// Single quote '\\'' marker
|
||||
pub const SNG_QUOTE: char = '\u{fdd2}';
|
||||
/// Tilde sub marker
|
||||
pub const TILDE_SUB: char = '\u{fdd3}';
|
||||
|
||||
impl Tk {
|
||||
/// Create a new expanded token
|
||||
@@ -21,6 +23,7 @@ impl Tk {
|
||||
let class = TkRule::Expanded { exp };
|
||||
Ok(Self { class, span, flags, })
|
||||
}
|
||||
/// Perform word splitting
|
||||
pub fn get_words(&self) -> Vec<String> {
|
||||
match &self.class {
|
||||
TkRule::Expanded { exp } => exp.clone(),
|
||||
@@ -40,6 +43,11 @@ impl Expander {
|
||||
}
|
||||
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
||||
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())
|
||||
}
|
||||
pub fn split_words(&mut self) -> Vec<String> {
|
||||
@@ -76,6 +84,10 @@ impl Expander {
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
TILDE_SUB => {
|
||||
let home = env::var("HOME").unwrap_or_default();
|
||||
result.push_str(&home);
|
||||
}
|
||||
VAR_SUB => {
|
||||
while let Some(ch) = chars.next() {
|
||||
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
|
||||
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
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 {
|
||||
let mut chars = raw.chars();
|
||||
let mut result = String::new();
|
||||
let mut first_char = true;
|
||||
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'~' if first_char => {
|
||||
result.push(TILDE_SUB)
|
||||
}
|
||||
'\\' => {
|
||||
if let Some(next_ch) = chars.next() {
|
||||
result.push(next_ch)
|
||||
@@ -215,6 +245,7 @@ pub fn unescape_str(raw: &str) -> String {
|
||||
'$' => result.push(VAR_SUB),
|
||||
_ => result.push(ch)
|
||||
}
|
||||
first_char = false;
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -602,6 +633,8 @@ pub fn expand_prompt(raw: &str) -> ShResult<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 {
|
||||
let mut result = input.clone();
|
||||
let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect();
|
||||
|
||||
78
src/fern.rs
78
src/fern.rs
@@ -13,80 +13,13 @@ pub mod shopt;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::expand::expand_aliases;
|
||||
use libsh::error::ShResult;
|
||||
use parse::{execute::Dispatcher, ParsedSrc};
|
||||
use signal::sig_setup;
|
||||
use state::{read_logic, source_rc, write_meta};
|
||||
use termios::{LocalFlags, Termios};
|
||||
use crate::libsh::sys::{save_termios, set_termios};
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::signal::sig_setup;
|
||||
use crate::state::source_rc;
|
||||
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() {
|
||||
save_termios();
|
||||
@@ -97,7 +30,6 @@ fn main() {
|
||||
eprintln!("{e}");
|
||||
}
|
||||
|
||||
const MAX_READLINE_ERRORS: u32 = 5;
|
||||
let mut readline_err_count: u32 = 0;
|
||||
|
||||
loop { // Main loop
|
||||
@@ -109,7 +41,7 @@ fn main() {
|
||||
Err(e) => {
|
||||
eprintln!("{e}");
|
||||
readline_err_count += 1;
|
||||
if readline_err_count == MAX_READLINE_ERRORS {
|
||||
if readline_err_count == 5 {
|
||||
eprintln!("reached maximum readline error count, exiting");
|
||||
break
|
||||
} else {
|
||||
|
||||
@@ -45,7 +45,11 @@ pub struct Note {
|
||||
|
||||
impl Note {
|
||||
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 {
|
||||
@@ -192,7 +196,7 @@ impl ShErr {
|
||||
let mut indicator_lines = vec![];
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,55 @@
|
||||
use termios::{LocalFlags, Termios};
|
||||
|
||||
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) -> ! {
|
||||
write_jobs(|j| {
|
||||
@@ -6,7 +57,7 @@ pub fn sh_quit(code: i32) -> ! {
|
||||
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();
|
||||
}
|
||||
if code == 0 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -27,7 +27,9 @@ impl ExecArgs {
|
||||
Ok(Self { cmd, argv, envp })
|
||||
}
|
||||
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> {
|
||||
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 {
|
||||
nodes: VecDeque<Node>,
|
||||
pub io_stack: IoStack,
|
||||
@@ -430,7 +449,7 @@ impl Dispatcher {
|
||||
let var = var.span.as_str();
|
||||
let val = val.span.as_str();
|
||||
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::MinusEq => todo!(),
|
||||
AssignKind::MultEq => todo!(),
|
||||
|
||||
@@ -108,6 +108,9 @@ impl Tk {
|
||||
_ => self.span.as_str().to_string()
|
||||
}
|
||||
}
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.span.as_str()
|
||||
}
|
||||
pub fn source(&self) -> Arc<String> {
|
||||
self.span.source.clone()
|
||||
}
|
||||
|
||||
@@ -596,10 +596,7 @@ impl ParseStream {
|
||||
}
|
||||
node_tks.push(self.next_tk().unwrap());
|
||||
|
||||
let Some(pat_tk) = self.next_tk() else {
|
||||
self.panic_mode(&mut node_tks);
|
||||
return Err(
|
||||
parse_err_full(
|
||||
let pat_err = parse_err_full(
|
||||
"Expected a pattern after 'case' keyword", &node_tks.get_span().unwrap()
|
||||
)
|
||||
.with_note(
|
||||
@@ -607,10 +604,17 @@ impl ParseStream {
|
||||
.with_sub_notes(vec![
|
||||
"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;
|
||||
node_tks.push(pattern.clone());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user