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",
"on-history-open", "on-history-close", "on-history-select",
"on-completion-start", "on-completion-cancel", "on-completion-select",
"on-exit",
"on-exit"
];
for kind in kinds {
test_input(format!("autocmd {kind} 'true'")).unwrap();

View File

@@ -130,6 +130,15 @@ fn main() -> ExitCode {
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 {
run_script(path, args.script_args)
} else if let Some(cmd) = args.command {
@@ -240,7 +249,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// may have moved it during resize/rewrap
readline.writer.update_t_cols();
readline.mark_dirty();
}
}
if JOB_DONE.swap(false, Ordering::SeqCst) {
// update the prompt so any job count escape sequences update dynamically

View File

@@ -258,6 +258,9 @@ impl Dispatcher {
Ok(())
}
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 {
return self.exec_cmd(node); // Argv is empty, probably an assignment
};
@@ -764,8 +767,16 @@ impl Dispatcher {
self.job_stack.new_job();
if cmds.len() == 1 {
self.fg_job = !is_bg && self.interactive;
let cmd = cmds.into_iter().next().unwrap();
self.dispatch_node(cmd)?;
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)?;
}
// Give the pipeline terminal control as soon as the first child
// establishes the PGID, so later children (e.g. nvim) don't get
@@ -1100,6 +1111,7 @@ impl Dispatcher {
match unsafe { fork()? } {
ForkResult::Child => {
let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0)));
self.interactive = false;
f(self);
exit(state::get_status())
}
@@ -1398,4 +1410,15 @@ mod tests {
assert_eq!(state::get_status(), 0);
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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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() {
let mode = self.select_mode.unwrap();
let start_byte = self.read_idx_byte_pos(start);
@@ -3497,7 +3500,7 @@ impl Display for LineBuf {
match mode.anchor() {
SelectAnchor::Start => {
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);
}
let selected = format!(

View File

@@ -225,5 +225,6 @@ vi_test! {
vi_dollar_single : "h" => "$" => "h", 0;
vi_caret_no_ws : "hello" => "$^" => "hello", 0;
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)
}
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>> {
for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name)
@@ -440,7 +468,9 @@ impl ScopeStack {
pub fn try_get_var(&self, var_name: &str) -> Option<String> {
// This version of get_var() is mainly used internally
// 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);
if !val.is_empty() {
return Some(val);
@@ -463,6 +493,9 @@ impl ScopeStack {
var
}
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>() {
return self.get_param(param);
}
@@ -1305,8 +1338,11 @@ impl VarTab {
}
/// A table of metadata for the shell
#[derive(Clone, Default, Debug)]
#[derive(Clone, Debug)]
pub struct MetaTab {
// Time when the shell was started, used for calculating shell uptime
shell_time: Instant,
// command running duration
runtime_start: Option<Instant>,
runtime_stop: Option<Instant>,
@@ -1331,6 +1367,25 @@ pub struct MetaTab {
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 {
pub fn new() -> Self {
Self {
@@ -1338,6 +1393,9 @@ impl MetaTab {
..Default::default()
}
}
pub fn shell_time(&self) -> Instant {
self.shell_time
}
pub fn set_pending_widget_keys(&mut self, keys: &str) {
let exp = expand_keymap(keys);
self.pending_widget_keys = exp;