fixed the $0 parameter not being populated correctly

This commit is contained in:
2026-02-19 14:24:55 -05:00
parent c8fe7b7978
commit 4ea08879a1
12 changed files with 210 additions and 99 deletions

View File

@@ -554,8 +554,12 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
pub fn expand_glob(raw: &str) -> ShResult<String> {
let mut words = vec![];
let opts = glob::MatchOptions {
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
..Default::default()
};
for entry in
glob::glob(raw).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
{
let entry =
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
@@ -1926,7 +1930,8 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
let pathbuf = PathBuf::from(&path);
let mut segments = pathbuf.iter().count();
let mut path_iter = pathbuf.iter();
while segments > 4 {
let max_segments = crate::state::read_shopts(|s| s.prompt.trunc_prompt_path);
while segments > max_segments {
path_iter.next();
segments -= 1;
}

View File

@@ -20,6 +20,10 @@ use super::{
ParsedSrc, Redir, RedirType,
};
thread_local! {
static RECURSE_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub struct ScopeGuard;
@@ -105,7 +109,12 @@ impl ExecArgs {
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> {
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));
let lex_flags = if interactive {
super::lex::LexFlags::INTERACTIVE
} else {
super::lex::LexFlags::empty()
};
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags);
if let Err(errors) = parser.parse_src() {
for error in errors {
eprintln!("{error}");
@@ -170,6 +179,10 @@ impl Dispatcher {
self.exec_builtin(node)
} else if is_subsh(node.get_command().cloned()) {
self.exec_subsh(node)
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() {
let dir = cmd.span.as_str().to_string();
let stack = IoStack { stack: self.io_stack.clone() };
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
} else {
self.exec_cmd(node)
}
@@ -266,6 +279,21 @@ impl Dispatcher {
unreachable!()
};
let max_depth = crate::state::read_shopts(|s| s.core.max_recurse_depth);
let depth = RECURSE_DEPTH.with(|d| {
let cur = d.get();
d.set(cur + 1);
cur + 1
});
if depth > max_depth {
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
return Err(ShErr::full(
ShErrKind::InternalErr,
format!("maximum recursion depth ({max_depth}) exceeded"),
blame,
));
}
let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?;
let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect());
@@ -273,27 +301,30 @@ impl Dispatcher {
let func_name = argv.remove(0).span.as_str().to_string();
let argv = prepare_argv(argv)?;
if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
let result = if let Some(func) = read_logic(|l| l.get_func(&func_name)) {
let _guard = ScopeGuard::exclusive_scope(Some(argv));
if let Err(e) = self.exec_brc_grp((*func).clone()) {
match e.kind() {
ShErrKind::FuncReturn(code) => {
state::set_status(*code);
return Ok(());
Ok(())
}
_ => return Err(e),
_ => Err(e),
}
} else {
Ok(())
}
Ok(())
} else {
Err(ShErr::full(
ShErrKind::InternalErr,
format!("Failed to find function '{}'", func_name),
blame,
))
}
};
RECURSE_DEPTH.with(|d| d.set(d.get() - 1));
result
}
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
let NdRule::BraceGrp { body } = brc_grp.class else {

View File

@@ -156,10 +156,10 @@ pub struct LexStream {
}
bitflags! {
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub struct LexFlags: u32 {
/// Return comment tokens
const LEX_COMMENTS = 0b000000001;
/// The lexer is operating in interactive mode
const INTERACTIVE = 0b000000001;
/// Allow unfinished input
const LEX_UNFINISHED = 0b000000010;
/// The next string-type token is a command name
@@ -740,7 +740,7 @@ impl Iterator for LexStream {
}
self.get_token(ch_idx..self.cursor, TkRule::Sep)
}
'#' => {
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
let ch_idx = self.cursor;
self.cursor += 1;

View File

@@ -44,6 +44,7 @@ macro_rules! try_match {
pub struct ParsedSrc {
pub src: Arc<String>,
pub ast: Ast,
pub lex_flags: LexFlags,
}
impl ParsedSrc {
@@ -51,11 +52,16 @@ impl ParsedSrc {
Self {
src,
ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(),
}
}
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags;
self
}
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
let mut tokens = vec![];
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
for lex_result in LexStream::new(self.src.clone(), self.lex_flags) {
match lex_result {
Ok(token) => tokens.push(token),
Err(error) => return Err(vec![error]),

View File

@@ -263,7 +263,7 @@ impl DerefMut for IoFrame {
/// redirection
#[derive(Debug, Default)]
pub struct IoStack {
stack: Vec<IoFrame>,
pub stack: Vec<IoFrame>,
}
impl IoStack {

View File

@@ -87,8 +87,8 @@ impl Completer {
ctx.push(markers::VAR_SUB);
}
}
markers::ARG => {
log::debug!("Found argument marker at position {}", pos);
markers::ARG | markers::ASSIGNMENT => {
log::debug!("Found argument/assignment marker at position {}", pos);
if last_priority < 1 {
ctx_start = pos;
ctx.push(markers::ARG);
@@ -328,6 +328,8 @@ impl Completer {
})
.collect();
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
candidates.truncate(limit);
Ok(CompResult::from_candidates(candidates))
}

View File

@@ -1,6 +1,6 @@
use std::{env, path::{Path, PathBuf}};
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::read_logic};
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
/// Syntax highlighter for shell input using Unicode marker-based annotation
///
@@ -214,16 +214,31 @@ impl Highlighter {
fn is_valid(command: &str) -> bool {
let path = env::var("PATH").unwrap_or_default();
let paths = path.split(':');
if PathBuf::from(&command).exists() {
return true;
let cmd_path = PathBuf::from(&command);
if cmd_path.exists() {
// the user has given us an absolute path
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled
return true;
} else {
let Ok(meta) = cmd_path.metadata() else { return false };
// this is a file that is executable by someone
return meta.permissions().mode() & 0o111 == 0
}
} else {
// they gave us a command name
// now we must traverse the PATH env var
// and see if we find any matches
for path in paths {
let path = PathBuf::from(path).join(command);
if path.exists() {
return true;
let Ok(meta) = path.metadata() else { continue };
return meta.permissions().mode() & 0o111 != 0;
}
}
// also check shell functions and aliases for any matches
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
if found {
return true;

View File

@@ -217,11 +217,17 @@ pub struct History {
impl History {
pub fn new() -> ShResult<Self> {
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
let max_hist = crate::state::read_shopts(|s| s.core.max_hist);
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
let home = env::var("HOME").unwrap();
format!("{home}/.fern_history")
}));
let mut entries = read_hist_file(&path)?;
// Enforce max_hist limit on loaded entries
if entries.len() > max_hist {
entries = entries.split_off(entries.len() - max_hist);
}
// Create pending entry for current input
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
entries.push(HistEntry {
@@ -238,8 +244,8 @@ impl History {
search_mask,
cursor,
search_direction: Direction::Backward,
ignore_dups: true,
max_size: None,
ignore_dups,
max_size: Some(max_hist as u32),
})
}

View File

@@ -212,7 +212,10 @@ impl FernVi {
self.editor.set_hint(hint);
}
None => {
self.writer.flush_write("\x07")?; // Bell character
match crate::state::read_shopts(|s| s.core.bell_style) {
crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; }
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
}
}
}
@@ -240,10 +243,12 @@ impl FernVi {
self.print_line()?;
self.writer.flush_write("\n")?;
let buf = self.editor.take_buf();
// Save command to history
self.history.push(buf.clone());
if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}");
// Save command to history if auto_hist is enabled
if crate::state::read_shopts(|s| s.core.auto_hist) {
self.history.push(buf.clone());
if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}");
}
}
return Ok(ReadlineEvent::Line(buf));
}
@@ -283,7 +288,8 @@ impl FernVi {
pub fn get_layout(&mut self, line: &str) -> Layout {
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(STDIN_FILENO);
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, line)
let tab_stop = crate::state::read_shopts(|s| s.prompt.tab_stop) as u16;
Layout::from_parts(tab_stop, cols, &self.prompt, to_cursor, line)
}
pub fn scroll_history(&mut self, cmd: ViCmd) {
/*
@@ -360,15 +366,16 @@ impl FernVi {
}
pub fn line_text(&mut self) -> String {
let start = Instant::now();
let line = self.editor.to_string();
self.highlighter.load_input(&line);
self.highlighter.highlight();
let highlighted = self.highlighter.take();
let hint = self.editor.get_hint_text();
let complete = format!("{highlighted}{hint}");
let end = start.elapsed();
complete
if crate::state::read_shopts(|s| s.prompt.highlight) {
self.highlighter.load_input(&line);
self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
} else {
format!("{line}{hint}")
}
}
pub fn print_line(&mut self) -> ShResult<()> {

View File

@@ -111,7 +111,7 @@ impl ShOpts {
return Err(
ShErr::simple(
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")
@@ -384,9 +384,8 @@ pub struct ShOptPrompt {
pub trunc_prompt_path: usize,
pub edit_mode: FernEditMode,
pub comp_limit: usize,
pub prompt_highlight: bool,
pub highlight: bool,
pub tab_stop: usize,
pub custom: HashMap<String, ShFunc>, // Contains functions for prompt modules
}
impl ShOptPrompt {
@@ -419,14 +418,14 @@ impl ShOptPrompt {
};
self.comp_limit = val;
}
"prompt_highlight" => {
"highlight" => {
let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for prompt_highlight value",
"shopt: expected 'true' or 'false' for highlight value",
));
};
self.prompt_highlight = val;
self.highlight = val;
}
"tab_stop" => {
let Ok(val) = val.parse::<usize>() else {
@@ -444,19 +443,17 @@ impl ShOptPrompt {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'"),
format!("shopt: Unexpected 'prompt' option '{opt}'"),
)
.with_note(Note::new("options can be accessed like 'core.option_name'"))
.with_note(Note::new("options can be accessed like 'prompt.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_style",
"max_recurse_depth",
Note::new("'prompt' contains the following options").with_sub_notes(vec![
"trunc_prompt_path",
"edit_mode",
"comp_limit",
"highlight",
"tab_stop",
"custom",
]),
),
)
@@ -489,10 +486,10 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.comp_limit));
Ok(Some(output))
}
"prompt_highlight" => {
"highlight" => {
let mut output =
String::from("Whether to enable or disable syntax highlighting on the prompt\n");
output.push_str(&format!("{}", self.prompt_highlight));
output.push_str(&format!("{}", self.highlight));
Ok(Some(output))
}
"tab_stop" => {
@@ -500,16 +497,6 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.tab_stop));
Ok(Some(output))
}
"custom" => {
let mut output = String::from(
"A table of custom 'modules' executed as shell functions for prompt scripting\n",
);
output.push_str("Current modules: \n");
for key in self.custom.keys() {
output.push_str(&format!(" - {key}\n"));
}
Ok(Some(output.trim().to_string()))
}
_ => Err(
ShErr::simple(
ShErrKind::SyntaxErr,
@@ -540,12 +527,8 @@ impl Display for ShOptPrompt {
output.push(format!("trunc_prompt_path = {}", self.trunc_prompt_path));
output.push(format!("edit_mode = {}", self.edit_mode));
output.push(format!("comp_limit = {}", self.comp_limit));
output.push(format!("prompt_highlight = {}", self.prompt_highlight));
output.push(format!("highlight = {}", self.highlight));
output.push(format!("tab_stop = {}", self.tab_stop));
output.push(String::from("prompt modules: "));
for key in self.custom.keys() {
output.push(format!(" - {key}"));
}
let final_output = output.join("\n");
@@ -559,9 +542,8 @@ impl Default for ShOptPrompt {
trunc_prompt_path: 4,
edit_mode: FernEditMode::Vi,
comp_limit: 100,
prompt_highlight: true,
highlight: true,
tab_stop: 4,
custom: HashMap::new(),
}
}
}

View File

@@ -121,6 +121,8 @@ impl ScopeStack {
pub fn new() -> Self {
let mut new = Self::default();
new.scopes.push(VarTab::new());
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
new
}
pub fn descend(&mut self, argv: Option<Vec<String>>) {
@@ -482,14 +484,6 @@ impl VarTab {
fn init_params() -> HashMap<ShellParam, String> {
let mut params = HashMap::new();
params.insert(ShellParam::ArgCount, "0".into()); // Number of positional parameters
params.insert(
ShellParam::Pos(0),
std::env::current_exe()
.unwrap()
.to_str()
.unwrap()
.to_string(),
); // Name of the shell
params.insert(ShellParam::ShPid, Pid::this().to_string()); // PID of the shell
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
params

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use tempfile::TempDir;
use crate::prompt::readline::complete::Completer;
use crate::prompt::readline::markers;
use crate::state::{write_logic, write_vars, VarFlags};
use super::*;
@@ -320,12 +321,12 @@ fn context_detection_command_position() {
let completer = Completer::new();
// At the beginning - command context
let (in_cmd, _) = completer.get_completion_context("ech", 3);
assert!(in_cmd, "Should be in command context at start");
let (ctx, _) = completer.get_completion_context("ech", 3);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
// After whitespace - still command if no command yet
let (in_cmd, _) = completer.get_completion_context(" ech", 5);
assert!(in_cmd, "Should be in command context after whitespace");
let (ctx, _) = completer.get_completion_context(" ech", 5);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
}
#[test]
@@ -333,11 +334,11 @@ fn context_detection_argument_position() {
let completer = Completer::new();
// After a complete command - argument context
let (in_cmd, _) = completer.get_completion_context("echo hello", 10);
assert!(!in_cmd, "Should be in argument context after command");
let (ctx, _) = completer.get_completion_context("echo hello", 10);
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11);
assert!(!in_cmd, "Should be in argument context");
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
}
#[test]
@@ -345,12 +346,12 @@ fn context_detection_nested_command_sub() {
let completer = Completer::new();
// Inside $() - should be command context
let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11);
assert!(in_cmd, "Should be in command context inside $()");
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
// After command in $() - argument context
let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17);
assert!(!in_cmd, "Should be in argument context inside $()");
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
}
#[test]
@@ -358,8 +359,8 @@ fn context_detection_pipe() {
let completer = Completer::new();
// After pipe - command context
let (in_cmd, _) = completer.get_completion_context("ls | gre", 8);
assert!(in_cmd, "Should be in command context after pipe");
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
}
#[test]
@@ -367,12 +368,74 @@ fn context_detection_command_sep() {
let completer = Completer::new();
// After semicolon - command context
let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11);
assert!(in_cmd, "Should be in command context after semicolon");
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
// After && - command context
let (in_cmd, _) = completer.get_completion_context("true && l", 9);
assert!(in_cmd, "Should be in command context after &&");
let (ctx, _) = completer.get_completion_context("true && l", 9);
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&");
}
#[test]
fn context_detection_variable_substitution() {
let completer = Completer::new();
// $VAR at argument position - VAR_SUB should take priority over ARG
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM");
// $VAR at command position - VAR_SUB should take priority over COMMAND
let (ctx, _) = completer.get_completion_context("$HOM", 4);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM");
}
#[test]
fn context_detection_variable_in_double_quotes() {
let completer = Completer::new();
// $VAR inside double quotes
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes");
}
#[test]
fn context_detection_stack_base_is_null() {
let completer = Completer::new();
// Empty input - only NULL on the stack
let (ctx, _) = completer.get_completion_context("", 0);
assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker");
}
#[test]
fn context_detection_context_start_position() {
let completer = Completer::new();
// Command at start - ctx_start should be 0
let (_, ctx_start) = completer.get_completion_context("ech", 3);
assert_eq!(ctx_start, 0, "Command at start should have ctx_start=0");
// Argument after command - ctx_start should be at arg position
let (_, ctx_start) = completer.get_completion_context("echo hel", 8);
assert_eq!(ctx_start, 5, "Argument ctx_start should point to arg start");
// Variable sub - ctx_start should point to the $
let (_, ctx_start) = completer.get_completion_context("echo $HOM", 9);
assert_eq!(ctx_start, 5, "Var sub ctx_start should point to the $");
}
#[test]
fn context_detection_priority_ordering() {
let completer = Completer::new();
// COMMAND (priority 2) should override ARG (priority 1)
// After a pipe, the next token is a command even though it looks like an arg
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe");
// VAR_SUB (priority 3) should override COMMAND (priority 2)
let (ctx, _) = completer.get_completion_context("$PA", 3);
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND");
}
// ============================================================================