From 4cda68e635b0f5863a1cf506f34107efe214edaf Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 28 Feb 2026 01:41:16 -0500 Subject: [PATCH] Stuff stored in maps can be eval'd on access by storing with the -F flag --- src/builtin/complete.rs | 4 +-- src/builtin/map.rs | 54 ++++++++++++++++++++++++---------------- src/readline/complete.rs | 25 +++++++++++-------- src/readline/term.rs | 14 +---------- src/state.rs | 37 +++++++++++++++++---------- 5 files changed, 74 insertions(+), 60 deletions(-) diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index 17dfd93..41ae715 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -136,7 +136,7 @@ bitflags! { pub struct CompOptFlags: u32 { const DEFAULT = 0b0000000001; const DIRNAMES = 0b0000000010; - const NOSPACE = 0b0000000100; + const SPACE = 0b0000000100; } } @@ -282,7 +282,7 @@ pub fn get_comp_opts(opts: Vec) -> ShResult { Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() { "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, "dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, - "nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE, + "space" => comp_opts.opt_flags |= CompOptFlags::SPACE, _ => { return Err(ShErr::full( ShErrKind::InvalidOpt, diff --git a/src/builtin/map.rs b/src/builtin/map.rs index bfde9a9..8e3b810 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -5,12 +5,13 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use serde_json::{Map, Value}; use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_all_unescaped, split_at_unescaped}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars} + expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_all_unescaped, split_at_unescaped}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars} }; #[derive(Debug, Clone)] pub enum MapNode { - Leaf(String), + DynamicLeaf(String), // eval'd on access + StaticLeaf(String), // static value Array(Vec), Branch(HashMap), } @@ -40,7 +41,7 @@ impl From for serde_json::Value { .collect(); Value::Array(arr) } - MapNode::Leaf(leaf) => { + MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => { Value::String(leaf) } } @@ -66,8 +67,8 @@ impl From for MapNode { .collect(); MapNode::Array(nodes) } - Value::String(s) => MapNode::Leaf(s), - v => MapNode::Leaf(v.to_string()) + Value::String(s) => MapNode::StaticLeaf(s), + v => MapNode::StaticLeaf(v.to_string()) } } } @@ -77,7 +78,7 @@ impl MapNode { match path { [] => Some(self), [key, rest @ ..] => match self { - MapNode::Leaf(_) => None, + MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None, MapNode::Array(map_nodes) => { let idx: usize = key.parse().ok()?; map_nodes.get(idx)?.get(rest) @@ -91,7 +92,7 @@ impl MapNode { match path { [] => *self = value, [key, rest @ ..] => { - if matches!(self, MapNode::Leaf(_)) { + if matches!(self, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_)) { // promote leaf to branch if we still have path left to traverse *self = Self::default(); } @@ -147,7 +148,7 @@ impl MapNode { match self { MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), MapNode::Array(nodes) => nodes.iter().filter_map(|n| n.display(false, false).ok()).collect(), - MapNode::Leaf(s) => vec![], + MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![], } } @@ -173,7 +174,8 @@ impl MapNode { } } else { match self { - MapNode::Leaf(leaf) => Ok(leaf.clone()), + MapNode::StaticLeaf(leaf) => Ok(leaf.clone()), + MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd), MapNode::Array(nodes) => { let mut s = String::new(); for node in nodes { @@ -195,7 +197,7 @@ impl MapNode { use super::setup_builtin; -fn map_opts_spec() -> [OptSpec; 5] { +fn map_opts_spec() -> [OptSpec; 6] { [ OptSpec { opt: Opt::Short('r'), @@ -213,6 +215,10 @@ fn map_opts_spec() -> [OptSpec; 5] { opt: Opt::Long("pretty".into()), takes_arg: false }, + OptSpec { + opt: Opt::Short('F'), + takes_arg: false + }, OptSpec { opt: Opt::Short('l'), takes_arg: false @@ -233,6 +239,7 @@ bitflags! { const JSON = 0b000100; const LOCAL = 0b001000; const PRETTY = 0b010000; + const FUNC = 0b100000; } } @@ -260,16 +267,20 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() }; let is_json = map_opts.flags.contains(MapFlags::JSON); + let is_func = map_opts.flags.contains(MapFlags::FUNC); + let make_leaf = |s: String| { + if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) } + }; let found = write_vars(|v| { if let Some(map) = v.get_map_mut(name) { if is_json { if let Ok(parsed) = serde_json::from_str::(&rhs) { map.set(&path[1..], parsed.into()); } else { - map.set(&path[1..], MapNode::Leaf(rhs.clone())); + map.set(&path[1..], make_leaf(rhs.clone())); } } else { - map.set(&path[1..], MapNode::Leaf(rhs.clone())); + map.set(&path[1..], make_leaf(rhs.clone())); } true } else { @@ -283,7 +294,7 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() let node: MapNode = parsed.into(); new.set(&path[1..], node); } else { - new.set(&path[1..], MapNode::Leaf(rhs)); + new.set(&path[1..], make_leaf(rhs)); } write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); } @@ -325,20 +336,18 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() format!("map not found: {}", name) )); } - let Some(output) = read_vars(|v| { + let Some(node) = read_vars(|v| { v.get_map(name) - .and_then(|map| map.get(&path[1..]) - .and_then(|n| { - if keys { - Some(n.keys().join(" ")) - } else { - n.display(json, pretty).ok() - } - })) + .and_then(|map| map.get(&path[1..]).cloned()) }) else { state::set_status(1); continue; }; + let output = if keys { + node.keys().join(" ") + } else { + node.display(json, pretty)? + }; let stdout = borrow_fd(STDOUT_FILENO); write(stdout, output.as_bytes())?; @@ -362,6 +371,7 @@ pub fn get_map_opts(opts: Vec) -> MapOpts { Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, + Opt::Short('F') => map_opts.flags |= MapFlags::FUNC, _ => unreachable!() } } diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 12fbbe3..5b118c3 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -228,7 +228,7 @@ pub enum CompSpecResult { NoSpec, // No compspec registered NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns * behavior flags */ - Match(CompResult), // Compspec found and candidates returned + Match { result: CompResult, flags: CompOptFlags }, // Compspec found and candidates returned } #[derive(Default, Debug, Clone)] @@ -433,6 +433,7 @@ impl CompSpec for BashCompSpec { if self.function.is_some() { candidates.extend(self.exec_comp_func(ctx)?); } + candidates.sort_by_key(|c| c.len()); // sort by length to prioritize shorter completions, ties are then sorted alphabetically Ok(candidates) } @@ -511,7 +512,7 @@ pub struct Completer { pub token_span: (usize, usize), pub active: bool, pub dirs_only: bool, - pub no_space: bool, + pub add_space: bool, } impl Completer { @@ -612,7 +613,7 @@ impl Completer { } pub fn add_spaces(&mut self) { - if !self.no_space { + if self.add_space { self.candidates = std::mem::take(&mut self.candidates) .into_iter() .map(|c| { @@ -744,9 +745,10 @@ impl Completer { flags: spec.get_flags(), }) } else { - Ok(CompSpecResult::Match(CompResult::from_candidates( - candidates, - ))) + Ok(CompSpecResult::Match { + result: CompResult::from_candidates(candidates), + flags: spec.get_flags(), + }) } } @@ -776,12 +778,15 @@ impl Completer { return Ok(CompResult::NoMatch); } - if flags.contains(CompOptFlags::NOSPACE) { - self.no_space = true; + if flags.contains(CompOptFlags::SPACE) { + self.add_space = true; } } - CompSpecResult::Match(comp_result) => { - return Ok(comp_result); + CompSpecResult::Match { result, flags } => { + if flags.contains(CompOptFlags::SPACE) { + self.add_space = true; + } + return Ok(result); } CompSpecResult::NoSpec => { /* carry on */ } } diff --git a/src/readline/term.rs b/src/readline/term.rs index d0bebac..a820eb7 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -780,27 +780,16 @@ impl AsFd for TermReader { } } +#[derive(Debug)] pub struct Layout { - pub w_calc: Box, pub prompt_end: Pos, pub cursor: Pos, pub end: Pos, } -impl Debug for Layout { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "Layout: ")?; - writeln!(f, "\tPrompt End: {:?}", self.prompt_end)?; - writeln!(f, "\tCursor: {:?}", self.cursor)?; - writeln!(f, "\tEnd: {:?}", self.end) - } -} - impl Layout { pub fn new() -> Self { - let w_calc = width_calculator(); Self { - w_calc, prompt_end: Pos::default(), cursor: Pos::default(), end: Pos::default(), @@ -811,7 +800,6 @@ impl Layout { let cursor = Self::calc_pos(term_width, to_cursor, prompt_end, prompt_end.col); let end = Self::calc_pos(term_width, to_end, prompt_end, prompt_end.col); Layout { - w_calc: width_calculator(), prompt_end, cursor, end, diff --git a/src/state.rs b/src/state.rs index f610bff..ae1c3ac 100644 --- a/src/state.rs +++ b/src/state.rs @@ -211,12 +211,18 @@ impl ScopeStack { flat_vars } pub fn set_var(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { - let is_local = self.is_local_var(var_name); - if flags.contains(VarFlags::LOCAL) || is_local { - self.set_var_local(var_name, val, flags) - } else { - self.set_var_global(var_name, val, flags) + if flags.contains(VarFlags::LOCAL) { + return self.set_var_local(var_name, val, flags); } + // Dynamic scoping: walk scopes from innermost to outermost, + // update the nearest scope that already has this variable + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + return scope.set_var(var_name, val, flags); + } + } + // Not found in any scope — create in global scope + self.set_var_global(var_name, val, flags) } pub fn set_var_indexed( &mut self, @@ -225,18 +231,23 @@ impl ScopeStack { val: String, flags: VarFlags, ) -> ShResult<()> { - let is_local = self.is_local_var(var_name); - if flags.contains(VarFlags::LOCAL) || is_local { + if flags.contains(VarFlags::LOCAL) { let Some(scope) = self.scopes.last_mut() else { return Ok(()); }; - scope.set_index(var_name, idx, val) - } else { - let Some(scope) = self.scopes.first_mut() else { - return Ok(()); - }; - scope.set_index(var_name, idx, val) + return scope.set_index(var_name, idx, val); } + // Dynamic scoping: find nearest scope with this variable + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) { + return scope.set_index(var_name, idx, val); + } + } + // Not found — create in global scope + let Some(scope) = self.scopes.first_mut() else { + return Ok(()); + }; + scope.set_index(var_name, idx, val) } fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { let Some(scope) = self.scopes.first_mut() else {