Stuff stored in maps can be eval'd on access by storing with the -F flag

This commit is contained in:
2026-02-28 01:41:16 -05:00
parent f0a000343b
commit 4cda68e635
5 changed files with 74 additions and 60 deletions

View File

@@ -136,7 +136,7 @@ bitflags! {
pub struct CompOptFlags: u32 { pub struct CompOptFlags: u32 {
const DEFAULT = 0b0000000001; const DEFAULT = 0b0000000001;
const DIRNAMES = 0b0000000010; const DIRNAMES = 0b0000000010;
const NOSPACE = 0b0000000100; const SPACE = 0b0000000100;
} }
} }
@@ -282,7 +282,7 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() { Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() {
"default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT,
"dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, "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( return Err(ShErr::full(
ShErrKind::InvalidOpt, ShErrKind::InvalidOpt,

View File

@@ -5,12 +5,13 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::{ 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)] #[derive(Debug, Clone)]
pub enum MapNode { pub enum MapNode {
Leaf(String), DynamicLeaf(String), // eval'd on access
StaticLeaf(String), // static value
Array(Vec<MapNode>), Array(Vec<MapNode>),
Branch(HashMap<String, MapNode>), Branch(HashMap<String, MapNode>),
} }
@@ -40,7 +41,7 @@ impl From<MapNode> for serde_json::Value {
.collect(); .collect();
Value::Array(arr) Value::Array(arr)
} }
MapNode::Leaf(leaf) => { MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => {
Value::String(leaf) Value::String(leaf)
} }
} }
@@ -66,8 +67,8 @@ impl From<Value> for MapNode {
.collect(); .collect();
MapNode::Array(nodes) MapNode::Array(nodes)
} }
Value::String(s) => MapNode::Leaf(s), Value::String(s) => MapNode::StaticLeaf(s),
v => MapNode::Leaf(v.to_string()) v => MapNode::StaticLeaf(v.to_string())
} }
} }
} }
@@ -77,7 +78,7 @@ impl MapNode {
match path { match path {
[] => Some(self), [] => Some(self),
[key, rest @ ..] => match self { [key, rest @ ..] => match self {
MapNode::Leaf(_) => None, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None,
MapNode::Array(map_nodes) => { MapNode::Array(map_nodes) => {
let idx: usize = key.parse().ok()?; let idx: usize = key.parse().ok()?;
map_nodes.get(idx)?.get(rest) map_nodes.get(idx)?.get(rest)
@@ -91,7 +92,7 @@ impl MapNode {
match path { match path {
[] => *self = value, [] => *self = value,
[key, rest @ ..] => { [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 // promote leaf to branch if we still have path left to traverse
*self = Self::default(); *self = Self::default();
} }
@@ -147,7 +148,7 @@ impl MapNode {
match self { match self {
MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), 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::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 { } else {
match self { match self {
MapNode::Leaf(leaf) => Ok(leaf.clone()), MapNode::StaticLeaf(leaf) => Ok(leaf.clone()),
MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd),
MapNode::Array(nodes) => { MapNode::Array(nodes) => {
let mut s = String::new(); let mut s = String::new();
for node in nodes { for node in nodes {
@@ -195,7 +197,7 @@ impl MapNode {
use super::setup_builtin; use super::setup_builtin;
fn map_opts_spec() -> [OptSpec; 5] { fn map_opts_spec() -> [OptSpec; 6] {
[ [
OptSpec { OptSpec {
opt: Opt::Short('r'), opt: Opt::Short('r'),
@@ -213,6 +215,10 @@ fn map_opts_spec() -> [OptSpec; 5] {
opt: Opt::Long("pretty".into()), opt: Opt::Long("pretty".into()),
takes_arg: false takes_arg: false
}, },
OptSpec {
opt: Opt::Short('F'),
takes_arg: false
},
OptSpec { OptSpec {
opt: Opt::Short('l'), opt: Opt::Short('l'),
takes_arg: false takes_arg: false
@@ -233,6 +239,7 @@ bitflags! {
const JSON = 0b000100; const JSON = 0b000100;
const LOCAL = 0b001000; const LOCAL = 0b001000;
const PRETTY = 0b010000; 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_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| { let found = write_vars(|v| {
if let Some(map) = v.get_map_mut(name) { if let Some(map) = v.get_map_mut(name) {
if is_json { if is_json {
if let Ok(parsed) = serde_json::from_str::<Value>(&rhs) { if let Ok(parsed) = serde_json::from_str::<Value>(&rhs) {
map.set(&path[1..], parsed.into()); map.set(&path[1..], parsed.into());
} else { } else {
map.set(&path[1..], MapNode::Leaf(rhs.clone())); map.set(&path[1..], make_leaf(rhs.clone()));
} }
} else { } else {
map.set(&path[1..], MapNode::Leaf(rhs.clone())); map.set(&path[1..], make_leaf(rhs.clone()));
} }
true true
} else { } else {
@@ -283,7 +294,7 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
let node: MapNode = parsed.into(); let node: MapNode = parsed.into();
new.set(&path[1..], node); new.set(&path[1..], node);
} else { } 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))); 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) format!("map not found: {}", name)
)); ));
} }
let Some(output) = read_vars(|v| { let Some(node) = read_vars(|v| {
v.get_map(name) v.get_map(name)
.and_then(|map| map.get(&path[1..]) .and_then(|map| map.get(&path[1..]).cloned())
.and_then(|n| {
if keys {
Some(n.keys().join(" "))
} else {
n.display(json, pretty).ok()
}
}))
}) else { }) else {
state::set_status(1); state::set_status(1);
continue; continue;
}; };
let output = if keys {
node.keys().join(" ")
} else {
node.display(json, pretty)?
};
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, output.as_bytes())?; write(stdout, output.as_bytes())?;
@@ -362,6 +371,7 @@ pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, Opt::Short('k') => map_opts.flags |= MapFlags::KEYS,
Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL,
Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY,
Opt::Short('F') => map_opts.flags |= MapFlags::FUNC,
_ => unreachable!() _ => unreachable!()
} }
} }

View File

@@ -228,7 +228,7 @@ pub enum CompSpecResult {
NoSpec, // No compspec registered NoSpec, // No compspec registered
NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns
* behavior flags */ * behavior flags */
Match(CompResult), // Compspec found and candidates returned Match { result: CompResult, flags: CompOptFlags }, // Compspec found and candidates returned
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@@ -433,6 +433,7 @@ impl CompSpec for BashCompSpec {
if self.function.is_some() { if self.function.is_some() {
candidates.extend(self.exec_comp_func(ctx)?); 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) Ok(candidates)
} }
@@ -511,7 +512,7 @@ pub struct Completer {
pub token_span: (usize, usize), pub token_span: (usize, usize),
pub active: bool, pub active: bool,
pub dirs_only: bool, pub dirs_only: bool,
pub no_space: bool, pub add_space: bool,
} }
impl Completer { impl Completer {
@@ -612,7 +613,7 @@ impl Completer {
} }
pub fn add_spaces(&mut self) { pub fn add_spaces(&mut self) {
if !self.no_space { if self.add_space {
self.candidates = std::mem::take(&mut self.candidates) self.candidates = std::mem::take(&mut self.candidates)
.into_iter() .into_iter()
.map(|c| { .map(|c| {
@@ -744,9 +745,10 @@ impl Completer {
flags: spec.get_flags(), flags: spec.get_flags(),
}) })
} else { } else {
Ok(CompSpecResult::Match(CompResult::from_candidates( Ok(CompSpecResult::Match {
candidates, result: CompResult::from_candidates(candidates),
))) flags: spec.get_flags(),
})
} }
} }
@@ -776,12 +778,15 @@ impl Completer {
return Ok(CompResult::NoMatch); return Ok(CompResult::NoMatch);
} }
if flags.contains(CompOptFlags::NOSPACE) { if flags.contains(CompOptFlags::SPACE) {
self.no_space = true; self.add_space = true;
} }
} }
CompSpecResult::Match(comp_result) => { CompSpecResult::Match { result, flags } => {
return Ok(comp_result); if flags.contains(CompOptFlags::SPACE) {
self.add_space = true;
}
return Ok(result);
} }
CompSpecResult::NoSpec => { /* carry on */ } CompSpecResult::NoSpec => { /* carry on */ }
} }

View File

@@ -780,27 +780,16 @@ impl AsFd for TermReader {
} }
} }
#[derive(Debug)]
pub struct Layout { pub struct Layout {
pub w_calc: Box<dyn WidthCalculator>,
pub prompt_end: Pos, pub prompt_end: Pos,
pub cursor: Pos, pub cursor: Pos,
pub end: 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 { impl Layout {
pub fn new() -> Self { pub fn new() -> Self {
let w_calc = width_calculator();
Self { Self {
w_calc,
prompt_end: Pos::default(), prompt_end: Pos::default(),
cursor: Pos::default(), cursor: Pos::default(),
end: 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 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); let end = Self::calc_pos(term_width, to_end, prompt_end, prompt_end.col);
Layout { Layout {
w_calc: width_calculator(),
prompt_end, prompt_end,
cursor, cursor,
end, end,

View File

@@ -211,12 +211,18 @@ impl ScopeStack {
flat_vars flat_vars
} }
pub fn set_var(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { 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) {
if flags.contains(VarFlags::LOCAL) || is_local { return self.set_var_local(var_name, val, flags);
self.set_var_local(var_name, val, flags)
} else {
self.set_var_global(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( pub fn set_var_indexed(
&mut self, &mut self,
@@ -225,18 +231,23 @@ impl ScopeStack {
val: String, val: String,
flags: VarFlags, flags: VarFlags,
) -> ShResult<()> { ) -> ShResult<()> {
let is_local = self.is_local_var(var_name); if flags.contains(VarFlags::LOCAL) {
if flags.contains(VarFlags::LOCAL) || is_local {
let Some(scope) = self.scopes.last_mut() else { let Some(scope) = self.scopes.last_mut() else {
return Ok(()); return Ok(());
}; };
scope.set_index(var_name, idx, val) return 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)
} }
// 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<()> { fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> {
let Some(scope) = self.scopes.first_mut() else { let Some(scope) = self.scopes.first_mut() else {