Compare commits

...

3 Commits

6 changed files with 102 additions and 8 deletions

View File

@@ -250,7 +250,7 @@ mod tests {
"pre-mode-change", "post-mode-change", "pre-mode-change", "post-mode-change",
"on-history-open", "on-history-close", "on-history-select", "on-history-open", "on-history-close", "on-history-select",
"on-completion-start", "on-completion-cancel", "on-completion-select", "on-completion-start", "on-completion-cancel", "on-completion-select",
"on-exit", "on-exit"
]; ];
for kind in kinds { for kind in kinds {
test_input(format!("autocmd {kind} 'true'")).unwrap(); test_input(format!("autocmd {kind} 'true'")).unwrap();

View File

@@ -130,6 +130,15 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
// Increment SHLVL, or set to 1 if not present or invalid.
// This var represents how many nested shell instances we're in
if let Ok(var) = env::var("SHLVL")
&& let Ok(lvl) = var.parse::<u32>() {
unsafe { env::set_var("SHLVL", (lvl + 1).to_string()) };
} else {
unsafe { env::set_var("SHLVL", "1") };
}
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {

View File

@@ -258,6 +258,9 @@ impl Dispatcher {
Ok(()) Ok(())
} }
pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> {
let (line, _) = node.get_span().clone().line_and_col();
write_vars(|v| v.set_var("LINENO", VarKind::Str((line + 1).to_string()), VarFlags::NONE))?;
let Some(cmd) = node.get_command() else { let Some(cmd) = node.get_command() else {
return self.exec_cmd(node); // Argv is empty, probably an assignment return self.exec_cmd(node); // Argv is empty, probably an assignment
}; };
@@ -764,8 +767,16 @@ impl Dispatcher {
self.job_stack.new_job(); self.job_stack.new_job();
if cmds.len() == 1 { if cmds.len() == 1 {
self.fg_job = !is_bg && self.interactive; self.fg_job = !is_bg && self.interactive;
let cmd = cmds.into_iter().next().unwrap(); let mut cmd = cmds.into_iter().next().unwrap();
if is_bg && !matches!(cmd.class, NdRule::Command { .. }) {
self.run_fork(&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), |s| {
if let Err(e) = s.dispatch_node(cmd) {
e.print_error();
}
})?;
} else {
self.dispatch_node(cmd)?; self.dispatch_node(cmd)?;
}
// Give the pipeline terminal control as soon as the first child // Give the pipeline terminal control as soon as the first child
// establishes the PGID, so later children (e.g. nvim) don't get // establishes the PGID, so later children (e.g. nvim) don't get
@@ -1100,6 +1111,7 @@ impl Dispatcher {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0))); let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0)));
self.interactive = false;
f(self); f(self);
exit(state::get_status()) exit(state::get_status())
} }
@@ -1398,4 +1410,15 @@ mod tests {
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
assert_eq!(g.read_output(), "yes\n"); assert_eq!(g.read_output(), "yes\n");
} }
#[test]
fn empty_var_in_test() {
let _g = TestGuard::new();
// POSIX specifies that a quoted unset variable expands to an empty string, so the shell actually sees `[ -n "" ]`, which returns false
test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap();
assert_eq!(state::get_status(), 1);
// Without quotes, word splitting causes an empty var to be removed entirely, so the shell actually sees `[ -n ]`, testing the value of ']', which returns true
test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap();
assert_eq!(state::get_status(), 0);
}
} }

View File

@@ -3489,6 +3489,9 @@ impl LineBuf {
impl Display for LineBuf { impl Display for LineBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut full_buf = self.buffer.clone(); let mut full_buf = self.buffer.clone();
if full_buf.is_empty() {
return write!(f, "{}", full_buf);
}
if let Some((start, end)) = self.select_range() { if let Some((start, end)) = self.select_range() {
let mode = self.select_mode.unwrap(); let mode = self.select_mode.unwrap();
let start_byte = self.read_idx_byte_pos(start); let start_byte = self.read_idx_byte_pos(start);
@@ -3497,7 +3500,7 @@ impl Display for LineBuf {
match mode.anchor() { match mode.anchor() {
SelectAnchor::Start => { SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte; let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() { if *inclusive.end() >= full_buf.len() {
inclusive = start_byte..=end_byte.saturating_sub(1); inclusive = start_byte..=end_byte.saturating_sub(1);
} }
let selected = format!( let selected = format!(

View File

@@ -225,5 +225,6 @@ vi_test! {
vi_dollar_single : "h" => "$" => "h", 0; vi_dollar_single : "h" => "$" => "h", 0;
vi_caret_no_ws : "hello" => "$^" => "hello", 0; vi_caret_no_ws : "hello" => "$^" => "hello", 0;
vi_f_last_char : "hello" => "fo" => "hello", 4; vi_f_last_char : "hello" => "fo" => "hello", 4;
vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4 vi_r_on_space : "hello world" => "5|r-" => "hell- world", 4;
vi_vw_doesnt_crash : "" => "vw" => "", 0
} }

View File

@@ -315,6 +315,34 @@ impl ScopeStack {
}; };
scope.set_var(var_name, val, flags) scope.set_var(var_name, val, flags)
} }
pub fn get_magic_var(&self, var_name: &str) -> Option<String> {
match var_name {
"SECONDS" => {
let shell_time = read_meta(|m| m.shell_time());
let secs = Instant::now().duration_since(shell_time).as_secs();
Some(secs.to_string())
}
"EPOCHREALTIME" => {
let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs_f64();
Some(epoch.to_string())
}
"EPOCHSECONDS" => {
let epoch = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0))
.as_secs();
Some(epoch.to_string())
}
"RANDOM" => {
let random = rand::random_range(0..32768);
Some(random.to_string())
}
_ => None
}
}
pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> { pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> {
for scope in self.scopes.iter().rev() { for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) if scope.var_exists(var_name)
@@ -440,7 +468,9 @@ impl ScopeStack {
pub fn try_get_var(&self, var_name: &str) -> Option<String> { pub fn try_get_var(&self, var_name: &str) -> Option<String> {
// This version of get_var() is mainly used internally // This version of get_var() is mainly used internally
// so that we have access to Option methods // so that we have access to Option methods
if let Ok(param) = var_name.parse::<ShellParam>() { if let Some(magic) = self.get_magic_var(var_name) {
return Some(magic);
} else if let Ok(param) = var_name.parse::<ShellParam>() {
let val = self.get_param(param); let val = self.get_param(param);
if !val.is_empty() { if !val.is_empty() {
return Some(val); return Some(val);
@@ -463,6 +493,9 @@ impl ScopeStack {
var var
} }
pub fn get_var(&self, var_name: &str) -> String { pub fn get_var(&self, var_name: &str) -> String {
if let Some(magic) = self.get_magic_var(var_name) {
return magic;
}
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);
} }
@@ -1305,8 +1338,11 @@ impl VarTab {
} }
/// A table of metadata for the shell /// A table of metadata for the shell
#[derive(Clone, Default, Debug)] #[derive(Clone, Debug)]
pub struct MetaTab { pub struct MetaTab {
// Time when the shell was started, used for calculating shell uptime
shell_time: Instant,
// command running duration // command running duration
runtime_start: Option<Instant>, runtime_start: Option<Instant>,
runtime_stop: Option<Instant>, runtime_stop: Option<Instant>,
@@ -1331,6 +1367,25 @@ pub struct MetaTab {
pending_widget_keys: Vec<KeyEvent>, pending_widget_keys: Vec<KeyEvent>,
} }
impl Default for MetaTab {
fn default() -> Self {
Self {
shell_time: Instant::now(),
runtime_start: None,
runtime_stop: None,
system_msg: vec![],
dir_stack: VecDeque::new(),
getopts_offset: 0,
old_path: None,
old_pwd: None,
path_cache: HashSet::new(),
cwd_cache: HashSet::new(),
comp_specs: HashMap::new(),
pending_widget_keys: vec![],
}
}
}
impl MetaTab { impl MetaTab {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@@ -1338,6 +1393,9 @@ impl MetaTab {
..Default::default() ..Default::default()
} }
} }
pub fn shell_time(&self) -> Instant {
self.shell_time
}
pub fn set_pending_widget_keys(&mut self, keys: &str) { pub fn set_pending_widget_keys(&mut self, keys: &str) {
let exp = expand_keymap(keys); let exp = expand_keymap(keys);
self.pending_widget_keys = exp; self.pending_widget_keys = exp;