Implemented arrays and array indexing
This commit is contained in:
@@ -13,7 +13,7 @@ use crate::{
|
|||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{borrow_fd, IoStack},
|
||||||
readline::term::RawModeGuard,
|
readline::term::RawModeGuard,
|
||||||
state::{self, read_vars, write_vars, VarFlags},
|
state::{self, read_vars, write_vars, VarFlags, VarKind},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const READ_OPTS: [OptSpec; 7] = [
|
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() {
|
if argv.is_empty() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("REPLY", &input, VarFlags::NONE)
|
v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE)
|
||||||
})?;
|
})?;
|
||||||
} else {
|
} else {
|
||||||
// get our field separator
|
// 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() {
|
for (i, arg) in argv.iter().enumerate() {
|
||||||
if i == argv.len() - 1 {
|
if i == argv.len() - 1 {
|
||||||
// Last arg, stuff the rest of the input into it
|
// 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;
|
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)) {
|
if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
|
||||||
// We found a field separator, split at the char index
|
// We found a field separator, split at the char index
|
||||||
let (field, rest) = trimmed.split_at(idx);
|
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
|
// note that this doesn't account for consecutive IFS characters, which is what
|
||||||
// that trim above is for
|
// that trim above is for
|
||||||
remaining = rest.to_string();
|
remaining = rest.to_string();
|
||||||
} else {
|
} 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();
|
remaining.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoStack, borrow_fd},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, VarFlags, read_vars, write_vars},
|
state::{self, VarFlags, VarKind, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::setup_builtin;
|
use super::setup_builtin;
|
||||||
@@ -40,9 +40,9 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
} else {
|
} else {
|
||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if let Some((var, val)) = arg.split_once('=') {
|
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 {
|
} 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 {
|
} else {
|
||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if let Some((var, val)) = arg.split_once('=') {
|
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 {
|
} else {
|
||||||
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
|
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
|
||||||
// any
|
// any
|
||||||
@@ -152,9 +152,9 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
} else {
|
} else {
|
||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if let Some((var, val)) = arg.split_once('=') {
|
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 {
|
} else {
|
||||||
write_vars(|v| v.set_var(&arg, "", VarFlags::LOCAL))?;
|
write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::parse::{Redir, RedirType};
|
|||||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||||
use crate::readline::markers;
|
use crate::readline::markers;
|
||||||
use crate::state::{
|
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::*};
|
use crate::{jobs, prelude::*};
|
||||||
|
|
||||||
@@ -516,7 +516,12 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
|
|
||||||
pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||||
let mut var_name = String::new();
|
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() {
|
while let Some(&ch) = chars.peek() {
|
||||||
match ch {
|
match ch {
|
||||||
markers::SUBSH if var_name.is_empty() => {
|
markers::SUBSH if var_name.is_empty() => {
|
||||||
@@ -538,17 +543,42 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
let expanded = expand_cmd_sub(&subsh_body)?;
|
let expanded = expand_cmd_sub(&subsh_body)?;
|
||||||
return Ok(expanded);
|
return Ok(expanded);
|
||||||
}
|
}
|
||||||
'{' if var_name.is_empty() => {
|
'{' if var_name.is_empty() && brace_depth == 0 => {
|
||||||
chars.next(); // consume the brace
|
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
|
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);
|
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::<isize>().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
|
chars.next(); // safe to consume
|
||||||
|
if ch == '{' { inner_brace_depth += 1; }
|
||||||
|
if ch == '}' { inner_brace_depth -= 1; }
|
||||||
var_name.push(ch);
|
var_name.push(ch);
|
||||||
}
|
}
|
||||||
ch if var_name.is_empty() && PARAMETERS.contains(&ch) => {
|
ch if var_name.is_empty() && PARAMETERS.contains(&ch) => {
|
||||||
@@ -1151,6 +1181,7 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
}
|
}
|
||||||
first_char = false;
|
first_char = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1359,53 +1390,59 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
ParamExp::Len => unreachable!(),
|
ParamExp::Len => unreachable!(),
|
||||||
ParamExp::DefaultUnsetOrNull(default) => {
|
ParamExp::DefaultUnsetOrNull(default) => {
|
||||||
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
|
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 {
|
} else {
|
||||||
Ok(vars.get_var(&var_name))
|
Ok(vars.get_var(&var_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::DefaultUnset(default) => {
|
ParamExp::DefaultUnset(default) => {
|
||||||
if !vars.var_exists(&var_name) {
|
if !vars.var_exists(&var_name) {
|
||||||
Ok(default)
|
expand_raw(&mut default.chars().peekable())
|
||||||
} else {
|
} else {
|
||||||
Ok(vars.get_var(&var_name))
|
Ok(vars.get_var(&var_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::SetDefaultUnsetOrNull(default) => {
|
ParamExp::SetDefaultUnsetOrNull(default) => {
|
||||||
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
|
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
|
||||||
write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE));
|
let expanded = expand_raw(&mut default.chars().peekable())?;
|
||||||
Ok(default)
|
write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE));
|
||||||
|
Ok(expanded)
|
||||||
} else {
|
} else {
|
||||||
Ok(vars.get_var(&var_name))
|
Ok(vars.get_var(&var_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::SetDefaultUnset(default) => {
|
ParamExp::SetDefaultUnset(default) => {
|
||||||
if !vars.var_exists(&var_name) {
|
if !vars.var_exists(&var_name) {
|
||||||
write_vars(|v| v.set_var(&var_name, &default, VarFlags::NONE));
|
let expanded = expand_raw(&mut default.chars().peekable())?;
|
||||||
Ok(default)
|
write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE));
|
||||||
|
Ok(expanded)
|
||||||
} else {
|
} else {
|
||||||
Ok(vars.get_var(&var_name))
|
Ok(vars.get_var(&var_name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::AltSetNotNull(alt) => {
|
ParamExp::AltSetNotNull(alt) => {
|
||||||
if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() {
|
if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() {
|
||||||
Ok(alt)
|
expand_raw(&mut alt.chars().peekable())
|
||||||
} else {
|
} else {
|
||||||
Ok("".into())
|
Ok("".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::AltNotNull(alt) => {
|
ParamExp::AltNotNull(alt) => {
|
||||||
if vars.var_exists(&var_name) {
|
if vars.var_exists(&var_name) {
|
||||||
Ok(alt)
|
expand_raw(&mut alt.chars().peekable())
|
||||||
} else {
|
} else {
|
||||||
Ok("".into())
|
Ok("".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::ErrUnsetOrNull(err) => {
|
ParamExp::ErrUnsetOrNull(err) => {
|
||||||
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
|
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
|
||||||
|
let expanded = expand_raw(&mut err.chars().peekable())?;
|
||||||
Err(ShErr::Simple {
|
Err(ShErr::Simple {
|
||||||
kind: ShErrKind::ExecFail,
|
kind: ShErrKind::ExecFail,
|
||||||
msg: err,
|
msg: expanded,
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -1414,9 +1451,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
}
|
}
|
||||||
ParamExp::ErrUnset(err) => {
|
ParamExp::ErrUnset(err) => {
|
||||||
if !vars.var_exists(&var_name) {
|
if !vars.var_exists(&var_name) {
|
||||||
|
let expanded = expand_raw(&mut err.chars().peekable())?;
|
||||||
Err(ShErr::Simple {
|
Err(ShErr::Simple {
|
||||||
kind: ShErrKind::ExecFail,
|
kind: ShErrKind::ExecFail,
|
||||||
msg: err,
|
msg: expanded,
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils},
|
libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoMode, IoStack},
|
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::{
|
use super::{
|
||||||
@@ -569,7 +569,7 @@ impl Dispatcher {
|
|||||||
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
||||||
|
|
||||||
for (var, val) in chunk_iter {
|
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());
|
for_guard.vars.insert(var.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,13 +899,18 @@ impl Dispatcher {
|
|||||||
match behavior {
|
match behavior {
|
||||||
AssignBehavior::Export => {
|
AssignBehavior::Export => {
|
||||||
for assign in assigns {
|
for assign in assigns {
|
||||||
|
let is_arr = assign.flags.contains(NdFlags::ARR_ASSIGN);
|
||||||
let NdRule::Assignment { kind, var, val } = assign.class else {
|
let NdRule::Assignment { kind, var, val } = assign.class else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let var = var.span.as_str();
|
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 {
|
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::PlusEq => todo!(),
|
||||||
AssignKind::MinusEq => todo!(),
|
AssignKind::MinusEq => todo!(),
|
||||||
AssignKind::MultEq => todo!(),
|
AssignKind::MultEq => todo!(),
|
||||||
@@ -916,13 +921,18 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
AssignBehavior::Set => {
|
AssignBehavior::Set => {
|
||||||
for assign in assigns {
|
for assign in assigns {
|
||||||
|
let is_arr = assign.flags.contains(NdFlags::ARR_ASSIGN);
|
||||||
let NdRule::Assignment { kind, var, val } = assign.class else {
|
let NdRule::Assignment { kind, var, val } = assign.class else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let var = var.span.as_str();
|
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 {
|
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::PlusEq => todo!(),
|
||||||
AssignKind::MinusEq => todo!(),
|
AssignKind::MinusEq => todo!(),
|
||||||
AssignKind::MultEq => todo!(),
|
AssignKind::MultEq => todo!(),
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ impl LexStream {
|
|||||||
'$' if chars.peek() == Some(&'{') => {
|
'$' if chars.peek() == Some(&'{') => {
|
||||||
pos += 2;
|
pos += 2;
|
||||||
chars.next();
|
chars.next();
|
||||||
let mut brace_count = 0;
|
let mut brace_count = 1;
|
||||||
while let Some(brc_ch) = chars.next() {
|
while let Some(brc_ch) = chars.next() {
|
||||||
match brc_ch {
|
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 !self.in_quote && is_op(ch) => break,
|
||||||
_ if is_hard_sep(ch) => break,
|
_ if is_hard_sep(ch) => break,
|
||||||
_ => pos += ch.len_utf8(),
|
_ => pos += ch.len_utf8(),
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ bitflags! {
|
|||||||
const BACKGROUND = 0b000001;
|
const BACKGROUND = 0b000001;
|
||||||
const FORK_BUILTINS = 0b000010;
|
const FORK_BUILTINS = 0b000010;
|
||||||
const NO_FORK = 0b000100;
|
const NO_FORK = 0b000100;
|
||||||
|
const ARR_ASSIGN = 0b001000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1472,22 +1473,28 @@ impl ParseStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if assign_kind.is_none() || var_name.is_empty() {
|
if let Some(assign_kind) = assign_kind && !var_name.is_empty() {
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let var = Tk::new(TkRule::Str, Span::new(name_range, token.source()));
|
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 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 {
|
Some(Node {
|
||||||
class: NdRule::Assignment {
|
class: NdRule::Assignment {
|
||||||
kind: assign_kind.unwrap(),
|
kind: assign_kind,
|
||||||
var,
|
var,
|
||||||
val,
|
val,
|
||||||
},
|
},
|
||||||
tokens: vec![token.clone()],
|
tokens: vec![token.clone()],
|
||||||
flags: NdFlags::empty(),
|
flags,
|
||||||
redirs: vec![],
|
redirs: vec![],
|
||||||
})
|
})
|
||||||
}
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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::{
|
use crate::{
|
||||||
builtin::BUILTINS,
|
builtin::BUILTINS,
|
||||||
@@ -11,6 +11,317 @@ use crate::{
|
|||||||
state::{read_logic, read_vars},
|
state::{read_logic, read_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn complete_users(start: &str) -> Vec<String> {
|
||||||
|
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<String> {
|
||||||
|
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::<Vec<_>>()
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
let mut candidates = vec![];
|
||||||
|
|
||||||
|
let path = env::var("PATH").unwrap_or_default();
|
||||||
|
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String>,
|
||||||
|
/// -W: The list of words
|
||||||
|
pub wordlist: Option<Vec<String>>,
|
||||||
|
/// -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<String>) -> 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<String> {
|
||||||
|
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompSpec for BashCompSpec {
|
||||||
|
fn complete(&self, ctx: &CompContext) -> Vec<String> {
|
||||||
|
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<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CompContext {
|
||||||
|
pub words: Vec<String>,
|
||||||
|
pub cword: usize,
|
||||||
|
pub line: String,
|
||||||
|
pub cursor_pos: usize
|
||||||
|
}
|
||||||
|
|
||||||
pub enum CompCtx {
|
pub enum CompCtx {
|
||||||
CmdName,
|
CmdName,
|
||||||
FileName,
|
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 {
|
pub fn get_completed_line(&self) -> String {
|
||||||
if self.candidates.is_empty() {
|
if self.candidates.is_empty() {
|
||||||
@@ -243,7 +507,7 @@ impl Completer {
|
|||||||
let end = tk.span.end;
|
let end = tk.span.end;
|
||||||
(start..=end).contains(&cursor_pos)
|
(start..=end).contains(&cursor_pos)
|
||||||
}) else {
|
}) 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();
|
let end_pos = line.len();
|
||||||
self.token_span = (end_pos, end_pos);
|
self.token_span = (end_pos, end_pos);
|
||||||
return Ok(CompResult::from_candidates(candidates));
|
return Ok(CompResult::from_candidates(candidates));
|
||||||
@@ -270,40 +534,6 @@ impl Completer {
|
|||||||
|
|
||||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||||
let var_sub = &cur_token.as_str();
|
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<CompResult> = 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::<Vec<_>>();
|
|
||||||
|
|
||||||
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();
|
let raw_tk = cur_token.as_str().to_string();
|
||||||
@@ -312,8 +542,8 @@ impl Completer {
|
|||||||
let expanded = expanded_words.join("\\ ");
|
let expanded = expanded_words.join("\\ ");
|
||||||
|
|
||||||
let mut candidates = match ctx.pop() {
|
let mut candidates = match ctx.pop() {
|
||||||
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
|
Some(markers::COMMAND) => complete_commands(&expanded),
|
||||||
Some(markers::ARG) => Self::complete_filename(&expanded),
|
Some(markers::ARG) => complete_filename(&expanded),
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
return Ok(CompResult::NoMatch);
|
return Ok(CompResult::NoMatch);
|
||||||
}
|
}
|
||||||
@@ -343,124 +573,6 @@ impl Completer {
|
|||||||
Ok(CompResult::from_candidates(candidates))
|
Ok(CompResult::from_candidates(candidates))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
|
||||||
let mut candidates = vec![];
|
|
||||||
|
|
||||||
let path = env::var("PATH").unwrap_or_default();
|
|
||||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
|
||||||
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<String> {
|
|
||||||
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 {
|
impl Default for Completer {
|
||||||
|
|||||||
85
src/state.rs
85
src/state.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::{
|
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};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
@@ -8,7 +8,7 @@ use crate::{
|
|||||||
builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{
|
builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::VecDequeExt,
|
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 {
|
pub struct Shed {
|
||||||
@@ -191,7 +191,7 @@ impl ScopeStack {
|
|||||||
|
|
||||||
flat_vars
|
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);
|
let is_local = self.is_local_var(var_name);
|
||||||
if flags.contains(VarFlags::LOCAL) || is_local {
|
if flags.contains(VarFlags::LOCAL) || is_local {
|
||||||
self.set_var_local(var_name, val, flags)
|
self.set_var_local(var_name, val, flags)
|
||||||
@@ -199,20 +199,61 @@ impl ScopeStack {
|
|||||||
self.set_var_global(var_name, val, flags)
|
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() {
|
if let Some(scope) = self.scopes.first_mut() {
|
||||||
scope.set_var(var_name, val, flags)
|
scope.set_var(var_name, val, flags)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
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() {
|
if let Some(scope) = self.scopes.last_mut() {
|
||||||
scope.set_var(var_name, val, flags)
|
scope.set_var(var_name, val, flags)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn index_var(&self, var_name: &str, idx: isize) -> ShResult<String> {
|
||||||
|
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 {
|
pub fn get_var(&self, var_name: &str) -> String {
|
||||||
if let Ok(param) = var_name.parse::<ShellParam>() {
|
if let Ok(param) = var_name.parse::<ShellParam>() {
|
||||||
return self.get_param(param);
|
return self.get_param(param);
|
||||||
@@ -436,6 +477,30 @@ pub enum VarKind {
|
|||||||
AssocArr(Vec<(String, String)>),
|
AssocArr(Vec<(String, String)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VarKind {
|
||||||
|
pub fn arr_from_tk(tk: Tk) -> ShResult<Self> {
|
||||||
|
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::<ShResult<Vec<Tk>>>()?;
|
||||||
|
|
||||||
|
for token in tokens {
|
||||||
|
let tk_words = token.expand()?.get_words();
|
||||||
|
words.extend(tk_words);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self::Arr(words))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Display for VarKind {
|
impl Display for VarKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
@@ -676,7 +741,7 @@ impl VarTab {
|
|||||||
unsafe { env::remove_var(var_name) };
|
unsafe { env::remove_var(var_name) };
|
||||||
Ok(())
|
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 let Some(var) = self.vars.get_mut(var_name) {
|
||||||
if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) {
|
if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
@@ -684,16 +749,16 @@ impl VarTab {
|
|||||||
format!("Variable '{}' is readonly", var_name)
|
format!("Variable '{}' is readonly", var_name)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
var.kind = VarKind::Str(val.to_string());
|
var.kind = val;
|
||||||
var.flags |= flags;
|
var.flags |= flags;
|
||||||
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
|
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
|
||||||
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
|
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
|
||||||
var.mark_for_export();
|
var.mark_for_export();
|
||||||
}
|
}
|
||||||
unsafe { env::set_var(var_name, val) };
|
unsafe { env::set_var(var_name, var.kind.to_string()) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut var = Var::new(VarKind::Str(val.to_string()), flags);
|
let mut var = Var::new(val, flags);
|
||||||
if flags.contains(VarFlags::EXPORT) {
|
if flags.contains(VarFlags::EXPORT) {
|
||||||
var.mark_for_export();
|
var.mark_for_export();
|
||||||
unsafe { env::set_var(var_name, var.to_string()) };
|
unsafe { env::set_var(var_name, var.to_string()) };
|
||||||
@@ -771,7 +836,7 @@ impl MetaTab {
|
|||||||
return;
|
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.path_cache.clear();
|
||||||
self.old_path = Some(path.clone());
|
self.old_path = Some(path.clone());
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use crate::expand::perform_param_expansion;
|
use crate::expand::perform_param_expansion;
|
||||||
use crate::prompt::readline::markers;
|
use crate::prompt::readline::markers;
|
||||||
use crate::state::VarFlags;
|
use crate::state::{VarFlags, VarKind};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn simple_expansion() {
|
fn simple_expansion() {
|
||||||
let varsub = "$foo";
|
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<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
||||||
.map(|tk| tk.unwrap())
|
.map(|tk| tk.unwrap())
|
||||||
@@ -132,8 +132,8 @@ fn test_infinite_recursive_alias() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_defaultunsetornull() {
|
fn param_expansion_defaultunsetornull() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("unset:-default").unwrap();
|
let result = perform_param_expansion("unset:-default").unwrap();
|
||||||
assert_eq!(result, "default");
|
assert_eq!(result, "default");
|
||||||
@@ -142,8 +142,8 @@ fn param_expansion_defaultunsetornull() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_defaultunset() {
|
fn param_expansion_defaultunset() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("unset-default").unwrap();
|
let result = perform_param_expansion("unset-default").unwrap();
|
||||||
assert_eq!(result, "default");
|
assert_eq!(result, "default");
|
||||||
@@ -152,8 +152,8 @@ fn param_expansion_defaultunset() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_setdefaultunsetornull() {
|
fn param_expansion_setdefaultunsetornull() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("unset:=assigned").unwrap();
|
let result = perform_param_expansion("unset:=assigned").unwrap();
|
||||||
assert_eq!(result, "assigned");
|
assert_eq!(result, "assigned");
|
||||||
@@ -162,8 +162,8 @@ fn param_expansion_setdefaultunsetornull() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_setdefaultunset() {
|
fn param_expansion_setdefaultunset() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("unset=assigned").unwrap();
|
let result = perform_param_expansion("unset=assigned").unwrap();
|
||||||
assert_eq!(result, "assigned");
|
assert_eq!(result, "assigned");
|
||||||
@@ -172,8 +172,8 @@ fn param_expansion_setdefaultunset() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_altsetnotnull() {
|
fn param_expansion_altsetnotnull() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("set_var:+alt").unwrap();
|
let result = perform_param_expansion("set_var:+alt").unwrap();
|
||||||
assert_eq!(result, "alt");
|
assert_eq!(result, "alt");
|
||||||
@@ -182,8 +182,8 @@ fn param_expansion_altsetnotnull() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_altnotnull() {
|
fn param_expansion_altnotnull() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("foo", "foo", VarFlags::NONE);
|
v.set_var("foo", VarKind::Str("foo".into()), VarFlags::NONE);
|
||||||
v.set_var("set_var", "value", VarFlags::NONE);
|
v.set_var("set_var", VarKind::Str("value".into()), VarFlags::NONE);
|
||||||
});
|
});
|
||||||
let result = perform_param_expansion("set_var+alt").unwrap();
|
let result = perform_param_expansion("set_var+alt").unwrap();
|
||||||
assert_eq!(result, "alt");
|
assert_eq!(result, "alt");
|
||||||
@@ -192,7 +192,7 @@ fn param_expansion_altnotnull() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_len() {
|
fn param_expansion_len() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("#foo").unwrap();
|
||||||
assert_eq!(result, "3");
|
assert_eq!(result, "3");
|
||||||
@@ -201,7 +201,7 @@ fn param_expansion_len() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_substr() {
|
fn param_expansion_substr() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo:1").unwrap();
|
||||||
assert_eq!(result, "oo");
|
assert_eq!(result, "oo");
|
||||||
@@ -210,7 +210,7 @@ fn param_expansion_substr() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_substrlen() {
|
fn param_expansion_substrlen() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo:0:2").unwrap();
|
||||||
assert_eq!(result, "fo");
|
assert_eq!(result, "fo");
|
||||||
@@ -219,7 +219,7 @@ fn param_expansion_substrlen() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_remshortestprefix() {
|
fn param_expansion_remshortestprefix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo#f*").unwrap();
|
||||||
assert_eq!(result, "oo");
|
assert_eq!(result, "oo");
|
||||||
@@ -228,7 +228,7 @@ fn param_expansion_remshortestprefix() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_remlongestprefix() {
|
fn param_expansion_remlongestprefix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo##f*").unwrap();
|
||||||
assert_eq!(result, "");
|
assert_eq!(result, "");
|
||||||
@@ -237,7 +237,7 @@ fn param_expansion_remlongestprefix() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_remshortestsuffix() {
|
fn param_expansion_remshortestsuffix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo%*o").unwrap();
|
||||||
assert_eq!(result, "fo");
|
assert_eq!(result, "fo");
|
||||||
@@ -246,7 +246,7 @@ fn param_expansion_remshortestsuffix() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_remlongestsuffix() {
|
fn param_expansion_remlongestsuffix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo%%*o").unwrap();
|
||||||
assert_eq!(result, "");
|
assert_eq!(result, "");
|
||||||
@@ -255,7 +255,7 @@ fn param_expansion_remlongestsuffix() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_replacefirstmatch() {
|
fn param_expansion_replacefirstmatch() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo/foo/X").unwrap();
|
||||||
assert_eq!(result, "X");
|
assert_eq!(result, "X");
|
||||||
@@ -264,7 +264,7 @@ fn param_expansion_replacefirstmatch() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_replaceallmatches() {
|
fn param_expansion_replaceallmatches() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo//o/X").unwrap();
|
||||||
assert_eq!(result, "fXX");
|
assert_eq!(result, "fXX");
|
||||||
@@ -273,7 +273,7 @@ fn param_expansion_replaceallmatches() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_replaceprefix() {
|
fn param_expansion_replaceprefix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo/#f/X").unwrap();
|
||||||
assert_eq!(result, "Xoo");
|
assert_eq!(result, "Xoo");
|
||||||
@@ -282,7 +282,7 @@ fn param_expansion_replaceprefix() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_expansion_replacesuffix() {
|
fn param_expansion_replacesuffix() {
|
||||||
write_vars(|v| {
|
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();
|
let result = perform_param_expansion("foo/%o/X").unwrap();
|
||||||
assert_eq!(result, "foX");
|
assert_eq!(result, "foX");
|
||||||
|
|||||||
@@ -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;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -20,7 +20,7 @@ fn scopestack_descend_ascend() {
|
|||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set a global variable
|
// 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");
|
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
||||||
|
|
||||||
// Descend into a new scope
|
// Descend into a new scope
|
||||||
@@ -30,7 +30,7 @@ fn scopestack_descend_ascend() {
|
|||||||
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
||||||
|
|
||||||
// Set a local variable
|
// 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");
|
assert_eq!(stack.get_var("LOCAL"), "value2");
|
||||||
|
|
||||||
// Ascend back to global scope
|
// Ascend back to global scope
|
||||||
@@ -48,14 +48,14 @@ fn scopestack_variable_shadowing() {
|
|||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set global variable
|
// 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");
|
assert_eq!(stack.get_var("VAR"), "global");
|
||||||
|
|
||||||
// Descend into local scope
|
// Descend into local scope
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Set local variable with same name
|
// 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");
|
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
|
||||||
|
|
||||||
// Ascend back
|
// Ascend back
|
||||||
@@ -77,10 +77,10 @@ fn scopestack_local_vs_global_flag() {
|
|||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Set with LOCAL flag - should go in current scope
|
// 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
|
// 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
|
// Both visible from local scope
|
||||||
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
|
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
|
||||||
@@ -98,15 +98,15 @@ fn scopestack_local_vs_global_flag() {
|
|||||||
fn scopestack_multiple_levels() {
|
fn scopestack_multiple_levels() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("LEVEL0", "global", VarFlags::NONE);
|
stack.set_var("LEVEL0", VarKind::Str("global".into()), VarFlags::NONE);
|
||||||
|
|
||||||
// Level 1
|
// Level 1
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
|
stack.set_var("LEVEL1", VarKind::Str("first".into()), VarFlags::LOCAL);
|
||||||
|
|
||||||
// Level 2
|
// Level 2
|
||||||
stack.descend(None);
|
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
|
// All variables visible from deepest scope
|
||||||
assert_eq!(stack.get_var("LEVEL0"), "global");
|
assert_eq!(stack.get_var("LEVEL0"), "global");
|
||||||
@@ -130,7 +130,7 @@ fn scopestack_multiple_levels() {
|
|||||||
fn scopestack_cannot_ascend_past_global() {
|
fn scopestack_cannot_ascend_past_global() {
|
||||||
let mut stack = ScopeStack::new();
|
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)
|
// Try to ascend from global scope (should be no-op)
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
@@ -202,7 +202,7 @@ fn scopestack_global_parameters() {
|
|||||||
fn scopestack_unset_var() {
|
fn scopestack_unset_var() {
|
||||||
let mut stack = ScopeStack::new();
|
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");
|
assert_eq!(stack.get_var("VAR"), "value");
|
||||||
|
|
||||||
stack.unset_var("VAR");
|
stack.unset_var("VAR");
|
||||||
@@ -215,11 +215,11 @@ fn scopestack_unset_finds_innermost() {
|
|||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set global
|
// Set global
|
||||||
stack.set_var("VAR", "global", VarFlags::NONE);
|
stack.set_var("VAR", VarKind::Str("global".into()), VarFlags::NONE);
|
||||||
|
|
||||||
// Descend and shadow
|
// Descend and shadow
|
||||||
stack.descend(None);
|
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");
|
assert_eq!(stack.get_var("VAR"), "local");
|
||||||
|
|
||||||
// Unset should remove local, revealing global
|
// Unset should remove local, revealing global
|
||||||
@@ -231,7 +231,7 @@ fn scopestack_unset_finds_innermost() {
|
|||||||
fn scopestack_export_var() {
|
fn scopestack_export_var() {
|
||||||
let mut stack = ScopeStack::new();
|
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
|
// Export the variable
|
||||||
stack.export_var("VAR");
|
stack.export_var("VAR");
|
||||||
@@ -246,7 +246,7 @@ fn scopestack_var_exists() {
|
|||||||
|
|
||||||
assert!(!stack.var_exists("NONEXISTENT"));
|
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"));
|
assert!(stack.var_exists("EXISTS"));
|
||||||
|
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
@@ -255,7 +255,7 @@ fn scopestack_var_exists() {
|
|||||||
"Global var should be visible in local scope"
|
"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"));
|
assert!(stack.var_exists("LOCAL"));
|
||||||
|
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
@@ -269,11 +269,11 @@ fn scopestack_var_exists() {
|
|||||||
fn scopestack_flatten_vars() {
|
fn scopestack_flatten_vars() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
|
stack.set_var("GLOBAL1", VarKind::Str("g1".into()), VarFlags::NONE);
|
||||||
stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
|
stack.set_var("GLOBAL2", VarKind::Str("g2".into()), VarFlags::NONE);
|
||||||
|
|
||||||
stack.descend(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();
|
let flattened = stack.flatten_vars();
|
||||||
|
|
||||||
@@ -291,11 +291,11 @@ fn scopestack_local_var_mutation() {
|
|||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// `local foo="biz"` — create a local variable with initial value
|
// `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");
|
assert_eq!(stack.get_var("foo"), "biz");
|
||||||
|
|
||||||
// `foo="bar"` — reassign without LOCAL flag (plain assignment)
|
// `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!(
|
assert_eq!(
|
||||||
stack.get_var("foo"),
|
stack.get_var("foo"),
|
||||||
"bar",
|
"bar",
|
||||||
@@ -321,11 +321,11 @@ fn scopestack_local_var_uninitialized() {
|
|||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// `local foo` — declare without a value
|
// `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"), "");
|
assert_eq!(stack.get_var("foo"), "");
|
||||||
|
|
||||||
// `foo="bar"` — assign a value later
|
// `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!(
|
assert_eq!(
|
||||||
stack.get_var("foo"),
|
stack.get_var("foo"),
|
||||||
"bar",
|
"bar",
|
||||||
@@ -441,7 +441,7 @@ fn vartab_new() {
|
|||||||
fn vartab_set_get_var() {
|
fn vartab_set_get_var() {
|
||||||
let mut vartab = VarTab::new();
|
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");
|
assert_eq!(vartab.get_var("TEST"), "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,10 +449,10 @@ fn vartab_set_get_var() {
|
|||||||
fn vartab_overwrite_var() {
|
fn vartab_overwrite_var() {
|
||||||
let mut vartab = VarTab::new();
|
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");
|
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");
|
assert_eq!(vartab.get_var("VAR"), "value2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ fn vartab_var_exists() {
|
|||||||
|
|
||||||
assert!(!vartab.var_exists("TEST"));
|
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"));
|
assert!(vartab.var_exists("TEST"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ fn vartab_var_exists() {
|
|||||||
fn vartab_unset_var() {
|
fn vartab_unset_var() {
|
||||||
let mut vartab = VarTab::new();
|
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"));
|
assert!(vartab.var_exists("VAR"));
|
||||||
|
|
||||||
vartab.unset_var("VAR");
|
vartab.unset_var("VAR");
|
||||||
@@ -482,7 +482,7 @@ fn vartab_unset_var() {
|
|||||||
fn vartab_export_var() {
|
fn vartab_export_var() {
|
||||||
let mut vartab = VarTab::new();
|
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");
|
vartab.export_var("VAR");
|
||||||
|
|
||||||
// Variable should still be accessible
|
// Variable should still be accessible
|
||||||
|
|||||||
Reference in New Issue
Block a user