From ccb1f439153d8df043e589513932de71fbc5f428 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Thu, 26 Feb 2026 00:32:54 -0500 Subject: [PATCH] Implemented arrays and array indexing --- src/builtin/read.rs | 10 +- src/builtin/varcmds.rs | 12 +- src/expand.rs | 72 ++++-- src/parse/execute.rs | 22 +- src/parse/lex.rs | 31 ++- src/parse/mod.rs | 19 +- src/readline/complete.rs | 518 ++++++++++++++++++++++++--------------- src/state.rs | 85 ++++++- src/tests/expand.rs | 50 ++-- src/tests/state.rs | 60 ++--- 10 files changed, 570 insertions(+), 309 deletions(-) diff --git a/src/builtin/read.rs b/src/builtin/read.rs index ae6d844..d1a6efa 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -13,7 +13,7 @@ use crate::{ parse::{NdRule, Node}, procio::{borrow_fd, IoStack}, readline::term::RawModeGuard, - state::{self, read_vars, write_vars, VarFlags}, + state::{self, read_vars, write_vars, VarFlags, VarKind}, }; pub const READ_OPTS: [OptSpec; 7] = [ @@ -183,7 +183,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S if argv.is_empty() { write_vars(|v| { - v.set_var("REPLY", &input, VarFlags::NONE) + v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE) })?; } else { // get our field separator @@ -196,7 +196,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S for (i, arg) in argv.iter().enumerate() { if i == argv.len() - 1 { // Last arg, stuff the rest of the input into it - write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE))?; + write_vars(|v| v.set_var(&arg.0, VarKind::Str(remaining.clone()), VarFlags::NONE))?; break; } @@ -206,13 +206,13 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) { // We found a field separator, split at the char index let (field, rest) = trimmed.split_at(idx); - write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE))?; + write_vars(|v| v.set_var(&arg.0, VarKind::Str(field.to_string()), VarFlags::NONE))?; // note that this doesn't account for consecutive IFS characters, which is what // that trim above is for remaining = rest.to_string(); } else { - write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE))?; + write_vars(|v| v.set_var(&arg.0, VarKind::Str(trimmed.to_string()), VarFlags::NONE))?; remaining.clear(); } } diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index 14d12c4..106fe93 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -4,7 +4,7 @@ use crate::{ parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, - state::{self, VarFlags, read_vars, write_vars}, + state::{self, VarFlags, VarKind, read_vars, write_vars}, }; use super::setup_builtin; @@ -40,9 +40,9 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu } else { for (arg, _) in argv { if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, val, VarFlags::READONLY))?; + write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::READONLY))?; } else { - write_vars(|v| v.set_var(&arg, "", VarFlags::READONLY))?; + write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::READONLY))?; } } } @@ -111,7 +111,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult } else { for (arg, _) in argv { if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, val, VarFlags::EXPORT))?; + write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?; } else { write_vars(|v| v.export_var(&arg)); // Export an existing variable, if // any @@ -152,9 +152,9 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< } else { for (arg, _) in argv { if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, val, VarFlags::LOCAL))?; + write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::LOCAL))?; } else { - write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL))?; + write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?; } } } diff --git a/src/expand.rs b/src/expand.rs index fc1d44c..6958d4f 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -12,7 +12,7 @@ use crate::parse::{Redir, RedirType}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::readline::markers; use crate::state::{ - LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars + LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars }; use crate::{jobs, prelude::*}; @@ -516,7 +516,12 @@ pub fn expand_raw(chars: &mut Peekable>) -> ShResult { pub fn expand_var(chars: &mut Peekable>) -> ShResult { let mut var_name = String::new(); - let mut in_brace = false; + let mut brace_depth: i32 = 0; + let mut inner_brace_depth: i32 = 0; + let mut bracket_depth: i32 = 0; + let mut idx_brace_depth: i32 = 0; + let mut idx_raw = String::new(); + let mut idx = None; while let Some(&ch) = chars.peek() { match ch { markers::SUBSH if var_name.is_empty() => { @@ -538,17 +543,42 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { let expanded = expand_cmd_sub(&subsh_body)?; return Ok(expanded); } - '{' if var_name.is_empty() => { + '{' if var_name.is_empty() && brace_depth == 0 => { chars.next(); // consume the brace - in_brace = true; + brace_depth += 1; } - '}' if in_brace => { + '}' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { chars.next(); // consume the brace - let val = perform_param_expansion(&var_name)?; + log::debug!("expand_var closing brace, var_name: {:?}", var_name); + let val = if let Some(idx) = idx { + read_vars(|v| v.index_var(&var_name, idx))? + } else { + perform_param_expansion(&var_name)? + }; return Ok(val); } - ch if in_brace => { + '[' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { + chars.next(); // consume the bracket + bracket_depth += 1; + } + ']' if bracket_depth > 0 && idx_brace_depth == 0 => { + bracket_depth -= 1; + chars.next(); // consume the bracket + if bracket_depth == 0 { + let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?; + idx = Some(expanded_idx.parse::().map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("Array index must be a number, got '{expanded_idx}'")))?); + } + } + ch if bracket_depth > 0 => { + chars.next(); // safe to consume + if ch == '{' { idx_brace_depth += 1; } + if ch == '}' { idx_brace_depth -= 1; } + idx_raw.push(ch); + } + ch if brace_depth > 0 => { chars.next(); // safe to consume + if ch == '{' { inner_brace_depth += 1; } + if ch == '}' { inner_brace_depth -= 1; } var_name.push(ch); } ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { @@ -1151,6 +1181,7 @@ pub fn unescape_str(raw: &str) -> String { } first_char = false; } + result } @@ -1359,53 +1390,59 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { ParamExp::Len => unreachable!(), ParamExp::DefaultUnsetOrNull(default) => { if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - Ok(default) + log::debug!("DefaultUnsetOrNull default: {:?}", default); + let result = expand_raw(&mut default.chars().peekable()); + log::debug!("DefaultUnsetOrNull expanded: {:?}", result); + result } else { Ok(vars.get_var(&var_name)) } } ParamExp::DefaultUnset(default) => { if !vars.var_exists(&var_name) { - Ok(default) + expand_raw(&mut default.chars().peekable()) } else { Ok(vars.get_var(&var_name)) } } ParamExp::SetDefaultUnsetOrNull(default) => { if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE)); - Ok(default) + let expanded = expand_raw(&mut default.chars().peekable())?; + write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE)); + Ok(expanded) } else { Ok(vars.get_var(&var_name)) } } ParamExp::SetDefaultUnset(default) => { if !vars.var_exists(&var_name) { - write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE)); - Ok(default) + let expanded = expand_raw(&mut default.chars().peekable())?; + write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE)); + Ok(expanded) } else { Ok(vars.get_var(&var_name)) } } ParamExp::AltSetNotNull(alt) => { if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() { - Ok(alt) + expand_raw(&mut alt.chars().peekable()) } else { Ok("".into()) } } ParamExp::AltNotNull(alt) => { if vars.var_exists(&var_name) { - Ok(alt) + expand_raw(&mut alt.chars().peekable()) } else { Ok("".into()) } } ParamExp::ErrUnsetOrNull(err) => { if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + let expanded = expand_raw(&mut err.chars().peekable())?; Err(ShErr::Simple { kind: ShErrKind::ExecFail, - msg: err, + msg: expanded, notes: vec![], }) } else { @@ -1414,9 +1451,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } ParamExp::ErrUnset(err) => { if !vars.var_exists(&var_name) { + let expanded = expand_raw(&mut err.chars().peekable())?; Err(ShErr::Simple { kind: ShErrKind::ExecFail, - msg: err, + msg: expanded, notes: vec![], }) } else { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index d61b41d..44348d2 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -9,7 +9,7 @@ use crate::{ libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoMode, IoStack}, - state::{self, ShFunc, VarFlags, read_logic, read_shopts, write_jobs, write_logic, write_vars}, + state::{self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars}, }; use super::{ @@ -569,7 +569,7 @@ impl Dispatcher { .zip(chunk.iter().chain(std::iter::repeat(&empty))); for (var, val) in chunk_iter { - write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE))?; + write_vars(|v| v.set_var(&var.to_string(), VarKind::Str(val.to_string()), VarFlags::NONE))?; for_guard.vars.insert(var.to_string()); } @@ -899,13 +899,18 @@ impl Dispatcher { match behavior { AssignBehavior::Export => { for assign in assigns { + let is_arr = assign.flags.contains(NdFlags::ARR_ASSIGN); let NdRule::Assignment { kind, var, val } = assign.class else { unreachable!() }; let var = var.span.as_str(); - let val = val.expand()?.get_words().join(" "); + let val = if is_arr { + VarKind::arr_from_tk(val)? + } else { + VarKind::Str(val.expand()?.get_words().join(" ")) + }; match kind { - AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::EXPORT))?, + AssignKind::Eq => write_vars(|v| v.set_var(var, val, VarFlags::EXPORT))?, AssignKind::PlusEq => todo!(), AssignKind::MinusEq => todo!(), AssignKind::MultEq => todo!(), @@ -916,13 +921,18 @@ impl Dispatcher { } AssignBehavior::Set => { for assign in assigns { + let is_arr = assign.flags.contains(NdFlags::ARR_ASSIGN); let NdRule::Assignment { kind, var, val } = assign.class else { unreachable!() }; let var = var.span.as_str(); - let val = val.expand()?.get_words().join(" "); + let val = if is_arr { + VarKind::arr_from_tk(val)? + } else { + VarKind::Str(val.expand()?.get_words().join(" ")) + }; match kind { - AssignKind::Eq => write_vars(|v| v.set_var(var, &val, VarFlags::NONE))?, + AssignKind::Eq => write_vars(|v| v.set_var(var, val, VarFlags::NONE))?, AssignKind::PlusEq => todo!(), AssignKind::MinusEq => todo!(), AssignKind::MultEq => todo!(), diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 3823464..1b6a342 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -357,7 +357,7 @@ impl LexStream { '$' if chars.peek() == Some(&'{') => { pos += 2; chars.next(); - let mut brace_count = 0; + let mut brace_count = 1; while let Some(brc_ch) = chars.next() { match brc_ch { '\\' => { @@ -624,6 +624,35 @@ impl LexStream { } } } + '=' if chars.peek() == Some(&'(') => { + pos += 1; // '=' + let mut depth = 1; + chars.next(); + pos += 1; // '(' + // looks like an array + while let Some(arr_ch) = chars.next() { + match arr_ch { + '\\' => { + pos += 1; + if let Some(next_ch) = chars.next() { + pos += next_ch.len_utf8(); + } + } + '(' => { + depth += 1; + pos += 1; + } + ')' => { + depth -= 1; + pos += 1; + if depth == 0 { + break; + } + } + _ => pos += arr_ch.len_utf8(), + } + } + } _ if !self.in_quote && is_op(ch) => break, _ if is_hard_sep(ch) => break, _ => pos += ch.len_utf8(), diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 6543744..36b897f 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -144,6 +144,7 @@ bitflags! { const BACKGROUND = 0b000001; const FORK_BUILTINS = 0b000010; const NO_FORK = 0b000100; + const ARR_ASSIGN = 0b001000; } } @@ -1472,22 +1473,28 @@ impl ParseStream { } } } - if assign_kind.is_none() || var_name.is_empty() { - None - } else { + if let Some(assign_kind) = assign_kind && !var_name.is_empty() { let var = Tk::new(TkRule::Str, Span::new(name_range, token.source())); let val = Tk::new(TkRule::Str, Span::new(val_range, token.source())); + let flags = if var_val.starts_with('(') && var_val.ends_with(')') { + NdFlags::ARR_ASSIGN + } else { + NdFlags::empty() + }; + Some(Node { class: NdRule::Assignment { - kind: assign_kind.unwrap(), + kind: assign_kind, var, val, }, tokens: vec![token.clone()], - flags: NdFlags::empty(), + flags, redirs: vec![], }) - } + } else { + None + } } } diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 2b9e316..b4032e1 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -1,4 +1,4 @@ -use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; +use std::{env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; use crate::{ builtin::BUILTINS, @@ -11,6 +11,317 @@ use crate::{ state::{read_logic, read_vars}, }; +pub fn complete_users(start: &str) -> Vec { + let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else { + return vec![]; + }; + passwd + .lines() + .filter_map(|line| line.split(':').next()) + .filter(|username| username.starts_with(start)) + .map(|s| s.to_string()) + .collect() +} + +pub fn complete_vars(start: &str) -> Vec { + let Some((var_name, start, end)) = extract_var_name(start) else { + return vec![] + }; + if !read_vars(|v| v.get_var(&var_name)).is_empty() { + return vec![] + } + // if we are here, we have a variable substitution that isn't complete + // so let's try to complete it + read_vars(|v| { + v.flatten_vars() + .keys() + .filter(|k| k.starts_with(&var_name) && *k != &var_name) + .map(|k| k.to_string()) + .collect::>() + + }) +} + +pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> { + let mut chars = text.chars().peekable(); + let mut name = String::new(); + let mut reading_name = false; + let mut pos = 0; + let mut name_start = 0; + let mut name_end = 0; + + while let Some(ch) = chars.next() { + match ch { + '$' => { + if chars.peek() == Some(&'{') { + continue; + } + + reading_name = true; + name_start = pos + 1; // Start after the '$' + } + '{' if !reading_name => { + reading_name = true; + name_start = pos + 1; + } + ch if ch.is_alphanumeric() || ch == '_' => { + if reading_name { + name.push(ch); + } + } + _ => { + if reading_name { + name_end = pos; // End before the non-alphanumeric character + break; + } + } + } + pos += 1; + } + + if !reading_name { + return None; + } + + if name_end == 0 { + name_end = pos; + } + + Some((name, name_start, name_end)) +} + +fn complete_commands(start: &str) -> Vec { + let mut candidates = vec![]; + + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(':').map(PathBuf::from).collect::>(); + for path in paths { + // Skip directories that don't exist (common in PATH) + let Ok(entries) = std::fs::read_dir(path) else { + continue; + }; + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + let Ok(meta) = entry.metadata() else { + continue; + }; + + let file_name = entry.file_name().to_string_lossy().to_string(); + + if meta.is_file() + && (meta.permissions().mode() & 0o111) != 0 + && file_name.starts_with(start) + { + candidates.push(file_name); + } + } + } + + let builtin_candidates = BUILTINS + .iter() + .filter(|b| b.starts_with(start)) + .map(|s| s.to_string()); + + candidates.extend(builtin_candidates); + + read_logic(|l| { + let func_table = l.funcs(); + let matches = func_table + .keys() + .filter(|k| k.starts_with(start)) + .map(|k| k.to_string()); + + candidates.extend(matches); + + let aliases = l.aliases(); + let matches = aliases + .keys() + .filter(|k| k.starts_with(start)) + .map(|k| k.to_string()); + + candidates.extend(matches); + }); + + // Deduplicate (same command may appear in multiple PATH dirs) + candidates.sort(); + candidates.dedup(); + + candidates +} + +fn complete_dirs(start: &str) -> Vec { + let filenames = complete_filename(start); + filenames.into_iter().filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false)).collect() +} + +fn complete_filename(start: &str) -> Vec { + let mut candidates = vec![]; + let has_dotslash = start.starts_with("./"); + + // Split path into directory and filename parts + // Use "." if start is empty (e.g., after "foo=") + let path = PathBuf::from(if start.is_empty() { "." } else { start }); + let (dir, prefix) = if start.ends_with('/') || start.is_empty() { + // Completing inside a directory: "src/" → dir="src/", prefix="" + (path, "") + } else if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + // Has directory component: "src/ma" → dir="src", prefix="ma" + ( + parent.to_path_buf(), + path.file_name().unwrap().to_str().unwrap_or(""), + ) + } else { + // No directory: "fil" → dir=".", prefix="fil" + (PathBuf::from("."), start) + }; + + let Ok(entries) = std::fs::read_dir(&dir) else { + return candidates; + }; + + for entry in entries.flatten() { + let file_name = entry.file_name(); + let file_str = file_name.to_string_lossy(); + + // Skip hidden files unless explicitly requested + if !prefix.starts_with('.') && file_str.starts_with('.') { + continue; + } + + if file_str.starts_with(prefix) { + // Reconstruct full path + let mut full_path = dir.join(&file_name); + + // Add trailing slash for directories + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + full_path.push(""); // adds trailing / + } + + let mut path_raw = full_path.to_string_lossy().to_string(); + if path_raw.starts_with("./") && !has_dotslash { + path_raw = path_raw.trim_start_matches("./").to_string(); + } + + candidates.push(path_raw); + } + } + + candidates.sort(); + candidates +} + +#[derive(Default,Debug,Clone)] +pub struct BashCompSpec { + /// -F: The name of a function to generate the possible completions. + pub function: Option, + /// -W: The list of words + pub wordlist: Option>, + /// -f: complete file names + pub files: bool, + /// -d: complete directory names + pub dirs: bool, + /// -c: complete command names + pub commands: bool, + /// -u: complete user names + pub users: bool, + /// -v complete variable names + pub vars: bool, + /// -A signal: complete signal names + pub signals: bool +} + +impl BashCompSpec { + pub fn new() -> Self { + Self::default() + } + pub fn with_func(mut self, func: String) -> Self { + self.function = Some(func); + self + } + pub fn with_wordlist(mut self, wordlist: Vec) -> Self { + self.wordlist = Some(wordlist); + self + } + pub fn files(mut self, enable: bool) -> Self { + self.files = enable; + self + } + pub fn dirs(mut self, enable: bool) -> Self { + self.dirs = enable; + self + } + pub fn commands(mut self, enable: bool) -> Self { + self.commands = enable; + self + } + pub fn users(mut self, enable: bool) -> Self { + self.users = enable; + self + } + pub fn vars(mut self, enable: bool) -> Self { + self.vars = enable; + self + } + pub fn signals(mut self, enable: bool) -> Self { + self.signals = enable; + self + } + pub fn exec_comp_func(&self) -> Vec { + + todo!() + } +} + +impl CompSpec for BashCompSpec { + fn complete(&self, ctx: &CompContext) -> Vec { + let mut candidates = vec![]; + let prefix = &ctx.words[ctx.cword]; + + if self.files { + candidates.extend(complete_filename(prefix)); + } + if self.dirs { + candidates.extend(complete_dirs(prefix)); + } + if self.commands { + candidates.extend(complete_commands(prefix)); + } + if self.vars { + candidates.extend(complete_vars(prefix)); + } + if self.users { + candidates.extend(complete_users(prefix)); + } + if let Some(words) = &self.wordlist { + candidates.extend( + words + .iter() + .filter(|w| w.starts_with(prefix)) + .cloned(), + ); + } + if let Some(func) = &self.function { + } + + candidates + } +} + +pub trait CompSpec: Debug { + fn complete(&self, ctx: &CompContext) -> Vec; +} + +pub struct CompContext { + pub words: Vec, + pub cword: usize, + pub line: String, + pub cursor_pos: usize +} + pub enum CompCtx { CmdName, FileName, @@ -170,53 +481,6 @@ impl Completer { } } - pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> { - let mut chars = text.chars().peekable(); - let mut name = String::new(); - let mut reading_name = false; - let mut pos = 0; - let mut name_start = 0; - let mut name_end = 0; - - while let Some(ch) = chars.next() { - match ch { - '$' => { - if chars.peek() == Some(&'{') { - continue; - } - - reading_name = true; - name_start = pos + 1; // Start after the '$' - } - '{' if !reading_name => { - reading_name = true; - name_start = pos + 1; - } - ch if ch.is_alphanumeric() || ch == '_' => { - if reading_name { - name.push(ch); - } - } - _ => { - if reading_name { - name_end = pos; // End before the non-alphanumeric character - break; - } - } - } - pos += 1; - } - - if !reading_name { - return None; - } - - if name_end == 0 { - name_end = pos; - } - - Some((name, name_start, name_end)) - } pub fn get_completed_line(&self) -> String { if self.candidates.is_empty() { @@ -243,7 +507,7 @@ impl Completer { let end = tk.span.end; (start..=end).contains(&cursor_pos) }) else { - let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found + let candidates = complete_filename("./"); // Default to filename completion if no token is found let end_pos = line.len(); self.token_span = (end_pos, end_pos); return Ok(CompResult::from_candidates(candidates)); @@ -270,40 +534,6 @@ impl Completer { if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) { let var_sub = &cur_token.as_str(); - if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) { - if read_vars(|v| v.get_var(&var_name)).is_empty() { - // if we are here, we have a variable substitution that isn't complete - // so let's try to complete it - let ret: ShResult = read_vars(|v| { - let var_matches = v - .flatten_vars() - .keys() - .filter(|k| k.starts_with(&var_name) && *k != &var_name) - .map(|k| k.to_string()) - .collect::>(); - - if !var_matches.is_empty() { - let name_start = cur_token.span.start + start; - let name_end = cur_token.span.start + end; - self.token_span = (name_start, name_end); - cur_token - .span - .set_range(self.token_span.0..self.token_span.1); - Ok(CompResult::from_candidates(var_matches)) - } else { - Ok(CompResult::NoMatch) - } - }); - - if !matches!(ret, Ok(CompResult::NoMatch)) { - return ret; - } else { - ctx.pop(); - } - } else { - ctx.pop(); - } - } } let raw_tk = cur_token.as_str().to_string(); @@ -312,8 +542,8 @@ impl Completer { let expanded = expanded_words.join("\\ "); let mut candidates = match ctx.pop() { - Some(markers::COMMAND) => Self::complete_command(&expanded)?, - Some(markers::ARG) => Self::complete_filename(&expanded), + Some(markers::COMMAND) => complete_commands(&expanded), + Some(markers::ARG) => complete_filename(&expanded), Some(_) => { return Ok(CompResult::NoMatch); } @@ -343,124 +573,6 @@ impl Completer { Ok(CompResult::from_candidates(candidates)) } - fn complete_command(start: &str) -> ShResult> { - let mut candidates = vec![]; - - let path = env::var("PATH").unwrap_or_default(); - let paths = path.split(':').map(PathBuf::from).collect::>(); - for path in paths { - // Skip directories that don't exist (common in PATH) - let Ok(entries) = std::fs::read_dir(path) else { - continue; - }; - for entry in entries { - let Ok(entry) = entry else { - continue; - }; - let Ok(meta) = entry.metadata() else { - continue; - }; - - let file_name = entry.file_name().to_string_lossy().to_string(); - - if meta.is_file() - && (meta.permissions().mode() & 0o111) != 0 - && file_name.starts_with(start) - { - candidates.push(file_name); - } - } - } - - let builtin_candidates = BUILTINS - .iter() - .filter(|b| b.starts_with(start)) - .map(|s| s.to_string()); - - candidates.extend(builtin_candidates); - - read_logic(|l| { - let func_table = l.funcs(); - let matches = func_table - .keys() - .filter(|k| k.starts_with(start)) - .map(|k| k.to_string()); - - candidates.extend(matches); - - let aliases = l.aliases(); - let matches = aliases - .keys() - .filter(|k| k.starts_with(start)) - .map(|k| k.to_string()); - - candidates.extend(matches); - }); - - // Deduplicate (same command may appear in multiple PATH dirs) - candidates.sort(); - candidates.dedup(); - - Ok(candidates) - } - - fn complete_filename(start: &str) -> Vec { - let mut candidates = vec![]; - let has_dotslash = start.starts_with("./"); - - // Split path into directory and filename parts - // Use "." if start is empty (e.g., after "foo=") - let path = PathBuf::from(if start.is_empty() { "." } else { start }); - let (dir, prefix) = if start.ends_with('/') || start.is_empty() { - // Completing inside a directory: "src/" → dir="src/", prefix="" - (path, "") - } else if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() - { - // Has directory component: "src/ma" → dir="src", prefix="ma" - ( - parent.to_path_buf(), - path.file_name().unwrap().to_str().unwrap_or(""), - ) - } else { - // No directory: "fil" → dir=".", prefix="fil" - (PathBuf::from("."), start) - }; - - let Ok(entries) = std::fs::read_dir(&dir) else { - return candidates; - }; - - for entry in entries.flatten() { - let file_name = entry.file_name(); - let file_str = file_name.to_string_lossy(); - - // Skip hidden files unless explicitly requested - if !prefix.starts_with('.') && file_str.starts_with('.') { - continue; - } - - if file_str.starts_with(prefix) { - // Reconstruct full path - let mut full_path = dir.join(&file_name); - - // Add trailing slash for directories - if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - full_path.push(""); // adds trailing / - } - - let mut path_raw = full_path.to_string_lossy().to_string(); - if path_raw.starts_with("./") && !has_dotslash { - path_raw = path_raw.trim_start_matches("./").to_string(); - } - - candidates.push(path_raw); - } - } - - candidates.sort(); - candidates - } } impl Default for Completer { diff --git a/src/state.rs b/src/state.rs index 4bc401f..ae8109e 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,5 +1,5 @@ use std::{ - cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration + cell::RefCell, cmp::Ordering, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration }; use nix::unistd::{User, gethostname, getppid}; @@ -8,7 +8,7 @@ use crate::{ builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt, - }, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, readline::markers, shopt::ShOpts + }, parse::{ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Tk}}, prelude::*, readline::markers, shopt::ShOpts }; pub struct Shed { @@ -191,7 +191,7 @@ impl ScopeStack { flat_vars } - pub fn set_var(&mut self, var_name: &str, val: &str, 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) || is_local { self.set_var_local(var_name, val, flags) @@ -199,20 +199,61 @@ impl ScopeStack { self.set_var_global(var_name, val, flags) } } - fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> { + fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { if let Some(scope) = self.scopes.first_mut() { scope.set_var(var_name, val, flags) } else { Ok(()) } } - fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> { + fn set_var_local(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { if let Some(scope) = self.scopes.last_mut() { scope.set_var(var_name, val, flags) } else { Ok(()) } } + pub fn index_var(&self, var_name: &str, idx: isize) -> ShResult { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) + && let Some(var) = scope.vars().get(var_name) { + match var.kind() { + VarKind::Arr(items) => { + let idx = match idx.cmp(&0) { + Ordering::Less => { + if items.len() >= idx.unsigned_abs() { + items.len() - idx.unsigned_abs() + } else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Index {} out of bounds for array '{}'", idx, var_name) + )); + } + } + Ordering::Equal => idx as usize, + Ordering::Greater => idx as usize + }; + + if let Some(item) = items.get(idx) { + return Ok(item.clone()); + } else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Index {} out of bounds for array '{}'", idx, var_name) + )); + } + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name) + )); + } + } + } + } + Ok("".into()) + } pub fn get_var(&self, var_name: &str) -> String { if let Ok(param) = var_name.parse::() { return self.get_param(param); @@ -436,6 +477,30 @@ pub enum VarKind { AssocArr(Vec<(String, String)>), } +impl VarKind { + pub fn arr_from_tk(tk: Tk) -> ShResult { + let raw = tk.as_str(); + if !raw.starts_with('(') || !raw.ends_with(')') { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid array syntax: {}", raw), + )); + } + let raw = raw[1..raw.len() - 1].to_string(); + + let mut words = vec![]; + let tokens = LexStream::new(Arc::new(raw), LexFlags::empty()) + .collect::>>()?; + + for token in tokens { + let tk_words = token.expand()?.get_words(); + words.extend(tk_words); + } + + Ok(Self::Arr(words)) + } +} + impl Display for VarKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -676,7 +741,7 @@ impl VarTab { unsafe { env::remove_var(var_name) }; Ok(()) } - pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) -> ShResult<()> { + pub fn set_var(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { if let Some(var) = self.vars.get_mut(var_name) { if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) { return Err(ShErr::simple( @@ -684,16 +749,16 @@ impl VarTab { format!("Variable '{}' is readonly", var_name) )); } - var.kind = VarKind::Str(val.to_string()); + var.kind = val; var.flags |= flags; if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { var.mark_for_export(); } - unsafe { env::set_var(var_name, val) }; + unsafe { env::set_var(var_name, var.kind.to_string()) }; } } else { - let mut var = Var::new(VarKind::Str(val.to_string()), flags); + let mut var = Var::new(val, flags); if flags.contains(VarFlags::EXPORT) { var.mark_for_export(); unsafe { env::set_var(var_name, var.to_string()) }; @@ -771,7 +836,7 @@ impl MetaTab { return; } - log::debug!("Rehashing commands for PATH: '{}' and PWD: '{}'", path, cwd); + log::trace!("Rehashing commands for PATH: '{}' and PWD: '{}'", path, cwd); self.path_cache.clear(); self.old_path = Some(path.clone()); diff --git a/src/tests/expand.rs b/src/tests/expand.rs index 9ed5793..ead07f9 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -2,14 +2,14 @@ use std::collections::HashSet; use crate::expand::perform_param_expansion; use crate::prompt::readline::markers; -use crate::state::VarFlags; +use crate::state::{VarFlags, VarKind}; use super::*; #[test] fn simple_expansion() { let varsub = "$foo"; - write_vars(|v| v.set_var("foo", "this is the value of the variable", VarFlags::NONE)); + write_vars(|v| v.set_var("foo", VarKind::Str("this is the value of the variable".into()), VarFlags::NONE)); let mut tokens: Vec = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) .map(|tk| tk.unwrap()) @@ -132,8 +132,8 @@ fn test_infinite_recursive_alias() { #[test] fn param_expansion_defaultunsetornull() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("unset:-default").unwrap(); assert_eq!(result, "default"); @@ -142,8 +142,8 @@ fn param_expansion_defaultunsetornull() { #[test] fn param_expansion_defaultunset() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("unset-default").unwrap(); assert_eq!(result, "default"); @@ -152,8 +152,8 @@ fn param_expansion_defaultunset() { #[test] fn param_expansion_setdefaultunsetornull() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("unset:=assigned").unwrap(); assert_eq!(result, "assigned"); @@ -162,8 +162,8 @@ fn param_expansion_setdefaultunsetornull() { #[test] fn param_expansion_setdefaultunset() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("unset=assigned").unwrap(); assert_eq!(result, "assigned"); @@ -172,8 +172,8 @@ fn param_expansion_setdefaultunset() { #[test] fn param_expansion_altsetnotnull() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("set_var:+alt").unwrap(); assert_eq!(result, "alt"); @@ -182,8 +182,8 @@ fn param_expansion_altsetnotnull() { #[test] fn param_expansion_altnotnull() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); - v.set_var("set_var", "value", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); + v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE); }); let result = perform_param_expansion("set_var+alt").unwrap(); assert_eq!(result, "alt"); @@ -192,7 +192,7 @@ fn param_expansion_altnotnull() { #[test] fn param_expansion_len() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("#foo").unwrap(); assert_eq!(result, "3"); @@ -201,7 +201,7 @@ fn param_expansion_len() { #[test] fn param_expansion_substr() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo:1").unwrap(); assert_eq!(result, "oo"); @@ -210,7 +210,7 @@ fn param_expansion_substr() { #[test] fn param_expansion_substrlen() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo:0:2").unwrap(); assert_eq!(result, "fo"); @@ -219,7 +219,7 @@ fn param_expansion_substrlen() { #[test] fn param_expansion_remshortestprefix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo#f*").unwrap(); assert_eq!(result, "oo"); @@ -228,7 +228,7 @@ fn param_expansion_remshortestprefix() { #[test] fn param_expansion_remlongestprefix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo##f*").unwrap(); assert_eq!(result, ""); @@ -237,7 +237,7 @@ fn param_expansion_remlongestprefix() { #[test] fn param_expansion_remshortestsuffix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo%*o").unwrap(); assert_eq!(result, "fo"); @@ -246,7 +246,7 @@ fn param_expansion_remshortestsuffix() { #[test] fn param_expansion_remlongestsuffix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo%%*o").unwrap(); assert_eq!(result, ""); @@ -255,7 +255,7 @@ fn param_expansion_remlongestsuffix() { #[test] fn param_expansion_replacefirstmatch() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo/foo/X").unwrap(); assert_eq!(result, "X"); @@ -264,7 +264,7 @@ fn param_expansion_replacefirstmatch() { #[test] fn param_expansion_replaceallmatches() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo//o/X").unwrap(); assert_eq!(result, "fXX"); @@ -273,7 +273,7 @@ fn param_expansion_replaceallmatches() { #[test] fn param_expansion_replaceprefix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo/#f/X").unwrap(); assert_eq!(result, "Xoo"); @@ -282,7 +282,7 @@ fn param_expansion_replaceprefix() { #[test] fn param_expansion_replacesuffix() { write_vars(|v| { - v.set_var("foo", "foo", VarFlags::NONE); + v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE); }); let result = perform_param_expansion("foo/%o/X").unwrap(); assert_eq!(result, "foX"); diff --git a/src/tests/state.rs b/src/tests/state.rs index d5377cc..e87dc26 100644 --- a/src/tests/state.rs +++ b/src/tests/state.rs @@ -1,4 +1,4 @@ -use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarTab}; +use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarKind, VarTab}; use std::path::PathBuf; // ============================================================================ @@ -20,7 +20,7 @@ fn scopestack_descend_ascend() { let mut stack = ScopeStack::new(); // Set a global variable - stack.set_var("GLOBAL", "value1", VarFlags::NONE); + stack.set_var("GLOBAL", VarKind::Str("value1".into()), VarFlags::NONE); assert_eq!(stack.get_var("GLOBAL"), "value1"); // Descend into a new scope @@ -30,7 +30,7 @@ fn scopestack_descend_ascend() { assert_eq!(stack.get_var("GLOBAL"), "value1"); // Set a local variable - stack.set_var("LOCAL", "value2", VarFlags::LOCAL); + stack.set_var("LOCAL", VarKind::Str("value2".into()), VarFlags::LOCAL); assert_eq!(stack.get_var("LOCAL"), "value2"); // Ascend back to global scope @@ -48,14 +48,14 @@ fn scopestack_variable_shadowing() { let mut stack = ScopeStack::new(); // Set global variable - stack.set_var("VAR", "global", VarFlags::NONE); + stack.set_var("VAR", VarKind::Str("global".into()), VarFlags::NONE); assert_eq!(stack.get_var("VAR"), "global"); // Descend into local scope stack.descend(None); // Set local variable with same name - stack.set_var("VAR", "local", VarFlags::LOCAL); + stack.set_var("VAR", VarKind::Str("local".into()), VarFlags::LOCAL); assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global"); // Ascend back @@ -77,10 +77,10 @@ fn scopestack_local_vs_global_flag() { stack.descend(None); // Set with LOCAL flag - should go in current scope - stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL); + stack.set_var("LOCAL_VAR", VarKind::Str("local".into()), VarFlags::LOCAL); // Set without LOCAL flag - should go in global scope - stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE); + stack.set_var("GLOBAL_VAR", VarKind::Str("global".into()), VarFlags::NONE); // Both visible from local scope assert_eq!(stack.get_var("LOCAL_VAR"), "local"); @@ -98,15 +98,15 @@ fn scopestack_local_vs_global_flag() { fn scopestack_multiple_levels() { let mut stack = ScopeStack::new(); - stack.set_var("LEVEL0", "global", VarFlags::NONE); + stack.set_var("LEVEL0", VarKind::Str("global".into()), VarFlags::NONE); // Level 1 stack.descend(None); - stack.set_var("LEVEL1", "first", VarFlags::LOCAL); + stack.set_var("LEVEL1", VarKind::Str("first".into()), VarFlags::LOCAL); // Level 2 stack.descend(None); - stack.set_var("LEVEL2", "second", VarFlags::LOCAL); + stack.set_var("LEVEL2", VarKind::Str("second".into()), VarFlags::LOCAL); // All variables visible from deepest scope assert_eq!(stack.get_var("LEVEL0"), "global"); @@ -130,7 +130,7 @@ fn scopestack_multiple_levels() { fn scopestack_cannot_ascend_past_global() { let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); + stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE); // Try to ascend from global scope (should be no-op) stack.ascend(); @@ -202,7 +202,7 @@ fn scopestack_global_parameters() { fn scopestack_unset_var() { let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); + stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE); assert_eq!(stack.get_var("VAR"), "value"); stack.unset_var("VAR"); @@ -215,11 +215,11 @@ fn scopestack_unset_finds_innermost() { let mut stack = ScopeStack::new(); // Set global - stack.set_var("VAR", "global", VarFlags::NONE); + stack.set_var("VAR", VarKind::Str("global".into()), VarFlags::NONE); // Descend and shadow stack.descend(None); - stack.set_var("VAR", "local", VarFlags::LOCAL); + stack.set_var("VAR", VarKind::Str("local".into()), VarFlags::LOCAL); assert_eq!(stack.get_var("VAR"), "local"); // Unset should remove local, revealing global @@ -231,7 +231,7 @@ fn scopestack_unset_finds_innermost() { fn scopestack_export_var() { let mut stack = ScopeStack::new(); - stack.set_var("VAR", "value", VarFlags::NONE); + stack.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE); // Export the variable stack.export_var("VAR"); @@ -246,7 +246,7 @@ fn scopestack_var_exists() { assert!(!stack.var_exists("NONEXISTENT")); - stack.set_var("EXISTS", "yes", VarFlags::NONE); + stack.set_var("EXISTS", VarKind::Str("yes".into()), VarFlags::NONE); assert!(stack.var_exists("EXISTS")); stack.descend(None); @@ -255,7 +255,7 @@ fn scopestack_var_exists() { "Global var should be visible in local scope" ); - stack.set_var("LOCAL", "yes", VarFlags::LOCAL); + stack.set_var("LOCAL", VarKind::Str("yes".into()), VarFlags::LOCAL); assert!(stack.var_exists("LOCAL")); stack.ascend(); @@ -269,11 +269,11 @@ fn scopestack_var_exists() { fn scopestack_flatten_vars() { let mut stack = ScopeStack::new(); - stack.set_var("GLOBAL1", "g1", VarFlags::NONE); - stack.set_var("GLOBAL2", "g2", VarFlags::NONE); + stack.set_var("GLOBAL1", VarKind::Str("g1".into()), VarFlags::NONE); + stack.set_var("GLOBAL2", VarKind::Str("g2".into()), VarFlags::NONE); stack.descend(None); - stack.set_var("LOCAL1", "l1", VarFlags::LOCAL); + stack.set_var("LOCAL1", VarKind::Str("l1".into()), VarFlags::LOCAL); let flattened = stack.flatten_vars(); @@ -291,11 +291,11 @@ fn scopestack_local_var_mutation() { stack.descend(None); // `local foo="biz"` — create a local variable with initial value - stack.set_var("foo", "biz", VarFlags::LOCAL); + stack.set_var("foo", VarKind::Str("biz".into()), VarFlags::LOCAL); assert_eq!(stack.get_var("foo"), "biz"); // `foo="bar"` — reassign without LOCAL flag (plain assignment) - stack.set_var("foo", "bar", VarFlags::NONE); + stack.set_var("foo", VarKind::Str("bar".into()), VarFlags::NONE); assert_eq!( stack.get_var("foo"), "bar", @@ -321,11 +321,11 @@ fn scopestack_local_var_uninitialized() { stack.descend(None); // `local foo` — declare without a value - stack.set_var("foo", "", VarFlags::LOCAL); + stack.set_var("foo", VarKind::Str("".into()), VarFlags::LOCAL); assert_eq!(stack.get_var("foo"), ""); // `foo="bar"` — assign a value later - stack.set_var("foo", "bar", VarFlags::NONE); + stack.set_var("foo", VarKind::Str("bar".into()), VarFlags::NONE); assert_eq!( stack.get_var("foo"), "bar", @@ -441,7 +441,7 @@ fn vartab_new() { fn vartab_set_get_var() { let mut vartab = VarTab::new(); - vartab.set_var("TEST", "value", VarFlags::NONE); + vartab.set_var("TEST", VarKind::Str("value".into()), VarFlags::NONE); assert_eq!(vartab.get_var("TEST"), "value"); } @@ -449,10 +449,10 @@ fn vartab_set_get_var() { fn vartab_overwrite_var() { let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value1", VarFlags::NONE); + vartab.set_var("VAR", VarKind::Str("value1".into()), VarFlags::NONE); assert_eq!(vartab.get_var("VAR"), "value1"); - vartab.set_var("VAR", "value2", VarFlags::NONE); + vartab.set_var("VAR", VarKind::Str("value2".into()), VarFlags::NONE); assert_eq!(vartab.get_var("VAR"), "value2"); } @@ -462,7 +462,7 @@ fn vartab_var_exists() { assert!(!vartab.var_exists("TEST")); - vartab.set_var("TEST", "value", VarFlags::NONE); + vartab.set_var("TEST", VarKind::Str("value".into()), VarFlags::NONE); assert!(vartab.var_exists("TEST")); } @@ -470,7 +470,7 @@ fn vartab_var_exists() { fn vartab_unset_var() { let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value", VarFlags::NONE); + vartab.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE); assert!(vartab.var_exists("VAR")); vartab.unset_var("VAR"); @@ -482,7 +482,7 @@ fn vartab_unset_var() { fn vartab_export_var() { let mut vartab = VarTab::new(); - vartab.set_var("VAR", "value", VarFlags::NONE); + vartab.set_var("VAR", VarKind::Str("value".into()), VarFlags::NONE); vartab.export_var("VAR"); // Variable should still be accessible