Compare commits
18 Commits
a43f8a6dde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f6a3935bcb | |||
| 1f9d59b546 | |||
| 101d8434f8 | |||
| 9bd9c66b92 | |||
| 5173e1908d | |||
| 1f9c96f24e | |||
| 09024728f6 | |||
| 307386ffc6 | |||
| 13227943c6 | |||
| a46ebe6868 | |||
| 5500b081fe | |||
| f279159873 | |||
| bb3db444db | |||
| 85e5fc2875 | |||
| ac429cbdf4 | |||
| a464540fbe | |||
| 07d7015dd4 | |||
| 490ce4571d |
@@ -8,7 +8,7 @@ A Linux shell written in Rust. The name is a nod to the original Unix utilities
|
|||||||
|
|
||||||
### Line Editor
|
### Line Editor
|
||||||
|
|
||||||
`shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt.
|
`shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt than conventional `vi` mode implementations.
|
||||||
|
|
||||||
- **Normal mode** - motions (`w`, `b`, `e`, `f`, `t`, `%`, `0`, `$`, etc.), verbs (`d`, `c`, `y`, `p`, `r`, `x`, `~`, etc.), text objects (`iw`, `aw`, `i"`, `a{`, `is`, etc.), registers, `.` repeat, `;`/`,` repeat, and counts
|
- **Normal mode** - motions (`w`, `b`, `e`, `f`, `t`, `%`, `0`, `$`, etc.), verbs (`d`, `c`, `y`, `p`, `r`, `x`, `~`, etc.), text objects (`iw`, `aw`, `i"`, `a{`, `is`, etc.), registers, `.` repeat, `;`/`,` repeat, and counts
|
||||||
- **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo
|
- **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ pub fn alias(node: Node) -> ShResult<()> {
|
|||||||
write(stdout, alias_output.as_bytes())?; // Write it
|
write(stdout, alias_output.as_bytes())?; // Write it
|
||||||
} else {
|
} else {
|
||||||
for (arg, span) in argv {
|
for (arg, span) in argv {
|
||||||
|
|
||||||
let Some((name, body)) = arg.split_once('=') else {
|
let Some((name, body)) = arg.split_once('=') else {
|
||||||
let Some(alias) = read_logic(|l| l.get_alias(&arg)) else {
|
let Some(alias) = read_logic(|l| l.get_alias(&arg)) else {
|
||||||
return Err(ShErr::at(
|
return Err(ShErr::at(
|
||||||
@@ -59,7 +58,10 @@ pub fn alias(node: Node) -> ShResult<()> {
|
|||||||
return Err(ShErr::at(
|
return Err(ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span,
|
span,
|
||||||
format!("alias: Cannot assign alias to reserved name '{}'", name.fg(next_color())),
|
format!(
|
||||||
|
"alias: Cannot assign alias to reserved name '{}'",
|
||||||
|
name.fg(next_color())
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
write_logic(|l| l.insert_alias(name, body, span.clone()));
|
write_logic(|l| l.insert_alias(name, body, span.clone()));
|
||||||
|
|||||||
@@ -229,9 +229,9 @@ pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::VecDeque;
|
use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
|
||||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
fn set_arr(name: &str, elems: &[&str]) {
|
fn set_arr(name: &str, elems: &[&str]) {
|
||||||
let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string()));
|
let arr = VecDeque::from_iter(elems.iter().map(|s| s.to_string()));
|
||||||
|
|||||||
@@ -159,7 +159,10 @@ mod tests {
|
|||||||
test_input("autocmd post-cmd 'echo post'").unwrap();
|
test_input("autocmd post-cmd 'echo post'").unwrap();
|
||||||
|
|
||||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 1);
|
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 1);
|
||||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), 1);
|
assert_eq!(
|
||||||
|
read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Pattern =====================
|
// ===================== Pattern =====================
|
||||||
@@ -205,7 +208,10 @@ mod tests {
|
|||||||
|
|
||||||
test_input("autocmd -c pre-cmd").unwrap();
|
test_input("autocmd -c pre-cmd").unwrap();
|
||||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 0);
|
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)).len(), 0);
|
||||||
assert_eq!(read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(), 1);
|
assert_eq!(
|
||||||
|
read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)).len(),
|
||||||
|
1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -245,11 +251,21 @@ mod tests {
|
|||||||
fn all_kinds_parse() {
|
fn all_kinds_parse() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
let kinds = [
|
let kinds = [
|
||||||
"pre-cmd", "post-cmd", "pre-change-dir", "post-change-dir",
|
"pre-cmd",
|
||||||
"on-job-finish", "pre-prompt", "post-prompt",
|
"post-cmd",
|
||||||
"pre-mode-change", "post-mode-change",
|
"pre-change-dir",
|
||||||
"on-history-open", "on-history-close", "on-history-select",
|
"post-change-dir",
|
||||||
"on-completion-start", "on-completion-cancel", "on-completion-select",
|
"on-job-finish",
|
||||||
|
"pre-prompt",
|
||||||
|
"post-prompt",
|
||||||
|
"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 {
|
for kind in kinds {
|
||||||
|
|||||||
@@ -99,7 +99,10 @@ pub mod tests {
|
|||||||
let new_dir = env::current_dir().unwrap();
|
let new_dir = env::current_dir().unwrap();
|
||||||
assert_ne!(old_dir, new_dir);
|
assert_ne!(old_dir, new_dir);
|
||||||
|
|
||||||
assert_eq!(new_dir.display().to_string(), temp_dir.path().display().to_string());
|
assert_eq!(
|
||||||
|
new_dir.display().to_string(),
|
||||||
|
temp_dir.path().display().to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -111,7 +114,10 @@ pub mod tests {
|
|||||||
test_input("cd").unwrap();
|
test_input("cd").unwrap();
|
||||||
|
|
||||||
let cwd = env::current_dir().unwrap();
|
let cwd = env::current_dir().unwrap();
|
||||||
assert_eq!(cwd.display().to_string(), temp_dir.path().display().to_string());
|
assert_eq!(
|
||||||
|
cwd.display().to_string(),
|
||||||
|
temp_dir.path().display().to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -316,10 +316,10 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::state::{self, VarFlags, VarKind, read_meta, write_vars};
|
||||||
|
use crate::testutil::{TestGuard, test_input};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
use crate::state::{self, read_meta, write_vars, VarFlags, VarKind};
|
|
||||||
use crate::testutil::{TestGuard, test_input};
|
|
||||||
|
|
||||||
// ===================== complete: Registration =====================
|
// ===================== complete: Registration =====================
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use crate::{
|
|||||||
|
|
||||||
pub fn truncate_home_path(path: String) -> String {
|
pub fn truncate_home_path(path: String) -> String {
|
||||||
if let Ok(home) = env::var("HOME")
|
if let Ok(home) = env::var("HOME")
|
||||||
&& path.starts_with(&home) {
|
&& path.starts_with(&home)
|
||||||
|
{
|
||||||
let new = path.strip_prefix(&home).unwrap();
|
let new = path.strip_prefix(&home).unwrap();
|
||||||
return format!("~{new}");
|
return format!("~{new}");
|
||||||
}
|
}
|
||||||
@@ -376,8 +377,7 @@ pub fn dirs(node: Node) -> ShResult<()> {
|
|||||||
.map(|d| d.to_string_lossy().to_string());
|
.map(|d| d.to_string_lossy().to_string());
|
||||||
|
|
||||||
if abbreviate_home {
|
if abbreviate_home {
|
||||||
stack.map(truncate_home_path)
|
stack.map(truncate_home_path).collect()
|
||||||
.collect()
|
|
||||||
} else {
|
} else {
|
||||||
stack.collect()
|
stack.collect()
|
||||||
}
|
}
|
||||||
@@ -428,10 +428,13 @@ pub fn dirs(node: Node) -> ShResult<()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
|
use crate::{
|
||||||
|
state::{self, read_meta},
|
||||||
|
testutil::{TestGuard, test_input},
|
||||||
|
};
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne};
|
||||||
use std::{env, path::PathBuf};
|
use std::{env, path::PathBuf};
|
||||||
use crate::{state::{self, read_meta}, testutil::{TestGuard, test_input}};
|
use tempfile::TempDir;
|
||||||
use pretty_assertions::{assert_ne,assert_eq};
|
|
||||||
use tempfile::TempDir;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_pushd_interactive() {
|
fn test_pushd_interactive() {
|
||||||
@@ -580,8 +583,14 @@ use tempfile::TempDir;
|
|||||||
let out = g.read_output();
|
let out = g.read_output();
|
||||||
let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect();
|
let lines: Vec<&str> = out.split('\n').filter(|l| !l.is_empty()).collect();
|
||||||
assert_eq!(lines.len(), 2);
|
assert_eq!(lines.len(), 2);
|
||||||
assert_eq!(lines[0], super::truncate_home_path(path.to_string_lossy().to_string()));
|
assert_eq!(
|
||||||
assert_eq!(lines[1], super::truncate_home_path(original.to_string_lossy().to_string()));
|
lines[0],
|
||||||
|
super::truncate_home_path(path.to_string_lossy().to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lines[1],
|
||||||
|
super::truncate_home_path(original.to_string_lossy().to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ pub fn echo(node: Node) -> ShResult<()> {
|
|||||||
let output_channel = borrow_fd(STDOUT_FILENO);
|
let output_channel = borrow_fd(STDOUT_FILENO);
|
||||||
let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out
|
let xpg_echo = read_shopts(|o| o.core.xpg_echo); // If true, echo expands escape sequences by default, and -E opts out
|
||||||
|
|
||||||
let use_escape = (xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE);
|
let use_escape =
|
||||||
|
(xpg_echo && !flags.contains(EchoFlags::NO_ESCAPE)) || flags.contains(EchoFlags::USE_ESCAPE);
|
||||||
|
|
||||||
let mut echo_output = prepare_echo_args(
|
let mut echo_output = prepare_echo_args(
|
||||||
argv
|
argv
|
||||||
@@ -308,11 +309,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prepare_multiple_args() {
|
fn prepare_multiple_args() {
|
||||||
let result = prepare_echo_args(
|
let result = prepare_echo_args(vec!["hello".into(), "world".into()], false, false).unwrap();
|
||||||
vec!["hello".into(), "world".into()],
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
).unwrap();
|
|
||||||
assert_eq!(result, vec!["hello", "world"]);
|
assert_eq!(result, vec!["hello", "world"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub fn eval(node: Node) -> ShResult<()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
|
||||||
// ===================== Basic =====================
|
// ===================== Basic =====================
|
||||||
@@ -80,7 +80,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn eval_expands_variable() {
|
fn eval_expands_variable() {
|
||||||
let guard = TestGuard::new();
|
let guard = TestGuard::new();
|
||||||
write_vars(|v| v.set_var("CMD", VarKind::Str("echo evaluated".into()), VarFlags::NONE)).unwrap();
|
write_vars(|v| v.set_var("CMD", VarKind::Str("echo evaluated".into()), VarFlags::NONE))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_input("eval $CMD").unwrap();
|
test_input("eval $CMD").unwrap();
|
||||||
let out = guard.read_output();
|
let out = guard.read_output();
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn exec_nonexistent_command_fails() {
|
fn exec_nonexistent_command_fails() {
|
||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
let result = test_input("exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________");
|
let result = test_input(
|
||||||
|
"exec _____________no_such_______command_xyz_____________hopefully______this_doesnt______exist_____somewhere_in___your______PATH__________________",
|
||||||
|
);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,8 +180,8 @@ pub fn keymap(node: Node) -> ShResult<()> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::getopt::Opt;
|
|
||||||
use crate::expand::expand_keymap;
|
use crate::expand::expand_keymap;
|
||||||
|
use crate::getopt::Opt;
|
||||||
use crate::state::{self, read_logic};
|
use crate::state::{self, read_logic};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
|
||||||
@@ -217,7 +217,8 @@ mod tests {
|
|||||||
let opts = KeyMapOpts::from_opts(&[
|
let opts = KeyMapOpts::from_opts(&[
|
||||||
Opt::Short('n'),
|
Opt::Short('n'),
|
||||||
Opt::LongWithArg("remove".into(), "jk".into()),
|
Opt::LongWithArg("remove".into(), "jk".into()),
|
||||||
]).unwrap();
|
])
|
||||||
|
.unwrap();
|
||||||
assert_eq!(opts.remove, Some("jk".into()));
|
assert_eq!(opts.remove, Some("jk".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,10 +274,7 @@ mod tests {
|
|||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||||
|
|
||||||
let maps = read_logic(|l| l.keymaps_filtered(
|
let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
|
||||||
KeyMapFlags::NORMAL,
|
|
||||||
&expand_keymap("jk"),
|
|
||||||
));
|
|
||||||
assert!(!maps.is_empty());
|
assert!(!maps.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,10 +283,7 @@ mod tests {
|
|||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
test_input("keymap -i jk '<ESC>'").unwrap();
|
test_input("keymap -i jk '<ESC>'").unwrap();
|
||||||
|
|
||||||
let maps = read_logic(|l| l.keymaps_filtered(
|
let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::INSERT, &expand_keymap("jk")));
|
||||||
KeyMapFlags::INSERT,
|
|
||||||
&expand_keymap("jk"),
|
|
||||||
));
|
|
||||||
assert!(!maps.is_empty());
|
assert!(!maps.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +293,7 @@ mod tests {
|
|||||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||||
test_input("keymap -n jk 'dd'").unwrap();
|
test_input("keymap -n jk 'dd'").unwrap();
|
||||||
|
|
||||||
let maps = read_logic(|l| l.keymaps_filtered(
|
let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
|
||||||
KeyMapFlags::NORMAL,
|
|
||||||
&expand_keymap("jk"),
|
|
||||||
));
|
|
||||||
assert_eq!(maps.len(), 1);
|
assert_eq!(maps.len(), 1);
|
||||||
assert_eq!(maps[0].action, "dd");
|
assert_eq!(maps[0].action, "dd");
|
||||||
}
|
}
|
||||||
@@ -312,10 +304,7 @@ mod tests {
|
|||||||
test_input("keymap -n jk '<ESC>'").unwrap();
|
test_input("keymap -n jk '<ESC>'").unwrap();
|
||||||
test_input("keymap -n --remove jk").unwrap();
|
test_input("keymap -n --remove jk").unwrap();
|
||||||
|
|
||||||
let maps = read_logic(|l| l.keymaps_filtered(
|
let maps = read_logic(|l| l.keymaps_filtered(KeyMapFlags::NORMAL, &expand_keymap("jk")));
|
||||||
KeyMapFlags::NORMAL,
|
|
||||||
&expand_keymap("jk"),
|
|
||||||
));
|
|
||||||
assert!(maps.is_empty());
|
assert!(maps.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{MapNode, MapFlags, get_map_opts};
|
use super::{MapFlags, MapNode, get_map_opts};
|
||||||
use crate::getopt::Opt;
|
use crate::getopt::Opt;
|
||||||
use crate::state::{self, read_vars};
|
use crate::state::{self, read_vars};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
@@ -433,10 +433,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn mapnode_remove_nested() {
|
fn mapnode_remove_nested() {
|
||||||
let mut root = MapNode::default();
|
let mut root = MapNode::default();
|
||||||
root.set(
|
root.set(&["a".into(), "b".into()], MapNode::StaticLeaf("val".into()));
|
||||||
&["a".into(), "b".into()],
|
|
||||||
MapNode::StaticLeaf("val".into()),
|
|
||||||
);
|
|
||||||
root.remove(&["a".into(), "b".into()]);
|
root.remove(&["a".into(), "b".into()]);
|
||||||
assert!(root.get(&["a".into(), "b".into()]).is_none());
|
assert!(root.get(&["a".into(), "b".into()]).is_none());
|
||||||
// Parent branch should still exist
|
// Parent branch should still exist
|
||||||
|
|||||||
@@ -17,20 +17,21 @@ pub mod keymap;
|
|||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod pwd;
|
pub mod pwd;
|
||||||
pub mod read;
|
pub mod read;
|
||||||
|
pub mod resource;
|
||||||
|
pub mod seek;
|
||||||
pub mod shift;
|
pub mod shift;
|
||||||
pub mod shopt;
|
pub mod shopt;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
pub mod test; // [[ ]] thing
|
pub mod test; // [[ ]] thing
|
||||||
pub mod trap;
|
pub mod trap;
|
||||||
pub mod varcmds;
|
pub mod varcmds;
|
||||||
pub mod resource;
|
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 49] = [
|
pub const BUILTINS: [&str; 50] = [
|
||||||
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg", "disown",
|
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
|
||||||
"alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
|
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
|
||||||
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||||
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
|
||||||
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask"
|
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn true_builtin() -> ShResult<()> {
|
pub fn true_builtin() -> ShResult<()> {
|
||||||
@@ -50,7 +51,10 @@ pub fn noop_builtin() -> ShResult<()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use crate::{state, testutil::{TestGuard, test_input}};
|
use crate::{
|
||||||
|
state,
|
||||||
|
testutil::{TestGuard, test_input},
|
||||||
|
};
|
||||||
|
|
||||||
// You can never be too sure!!!!!!
|
// You can never be too sure!!!!!!
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ pub fn pwd(node: Node) -> ShResult<()> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::env;
|
|
||||||
use tempfile::TempDir;
|
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use std::env;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pwd_prints_cwd() {
|
fn pwd_prints_cwd() {
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::state::{self, read_vars, write_vars, VarFlags, VarKind};
|
use crate::state::{self, VarFlags, VarKind, read_vars, write_vars};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
|
||||||
// ===================== Basic read into REPLY =====================
|
// ===================== Basic read into REPLY =====================
|
||||||
|
|||||||
@@ -1,11 +1,22 @@
|
|||||||
use ariadne::Fmt;
|
use ariadne::Fmt;
|
||||||
use nix::{libc::STDOUT_FILENO, sys::{resource::{Resource, getrlimit, setrlimit}, stat::{Mode, umask}}, unistd::write};
|
use nix::{
|
||||||
|
libc::STDOUT_FILENO,
|
||||||
use crate::{
|
sys::{
|
||||||
getopt::{Opt, OptSpec, get_opts_from_tokens_strict}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node}, procio::borrow_fd, state::{self}
|
resource::{Resource, getrlimit, setrlimit},
|
||||||
|
stat::{Mode, umask},
|
||||||
|
},
|
||||||
|
unistd::write,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn ulimit_opt_spec() -> [OptSpec;5] {
|
use crate::{
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens_strict},
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::borrow_fd,
|
||||||
|
state::{self},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn ulimit_opt_spec() -> [OptSpec; 5] {
|
||||||
[
|
[
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('n'), // file descriptors
|
opt: Opt::Short('n'), // file descriptors
|
||||||
@@ -26,7 +37,7 @@ fn ulimit_opt_spec() -> [OptSpec;5] {
|
|||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('v'), // virtual memory
|
opt: Opt::Short('v'), // virtual memory
|
||||||
takes_arg: true,
|
takes_arg: true,
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,39 +61,51 @@ fn get_ulimit_opts(opt: &[Opt]) -> ShResult<UlimitOpts> {
|
|||||||
for o in opt {
|
for o in opt {
|
||||||
match o {
|
match o {
|
||||||
Opt::ShortWithArg('n', arg) => {
|
Opt::ShortWithArg('n', arg) => {
|
||||||
opts.fds = Some(arg.parse().map_err(|_| ShErr::simple(
|
opts.fds = Some(arg.parse().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid argument for -n: {}", arg.fg(next_color())),
|
format!("invalid argument for -n: {}", arg.fg(next_color())),
|
||||||
))?);
|
)
|
||||||
},
|
})?);
|
||||||
|
}
|
||||||
Opt::ShortWithArg('u', arg) => {
|
Opt::ShortWithArg('u', arg) => {
|
||||||
opts.procs = Some(arg.parse().map_err(|_| ShErr::simple(
|
opts.procs = Some(arg.parse().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid argument for -u: {}", arg.fg(next_color())),
|
format!("invalid argument for -u: {}", arg.fg(next_color())),
|
||||||
))?);
|
)
|
||||||
},
|
})?);
|
||||||
|
}
|
||||||
Opt::ShortWithArg('s', arg) => {
|
Opt::ShortWithArg('s', arg) => {
|
||||||
opts.stack = Some(arg.parse().map_err(|_| ShErr::simple(
|
opts.stack = Some(arg.parse().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid argument for -s: {}", arg.fg(next_color())),
|
format!("invalid argument for -s: {}", arg.fg(next_color())),
|
||||||
))?);
|
)
|
||||||
},
|
})?);
|
||||||
|
}
|
||||||
Opt::ShortWithArg('c', arg) => {
|
Opt::ShortWithArg('c', arg) => {
|
||||||
opts.core = Some(arg.parse().map_err(|_| ShErr::simple(
|
opts.core = Some(arg.parse().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid argument for -c: {}", arg.fg(next_color())),
|
format!("invalid argument for -c: {}", arg.fg(next_color())),
|
||||||
))?);
|
)
|
||||||
},
|
})?);
|
||||||
|
}
|
||||||
Opt::ShortWithArg('v', arg) => {
|
Opt::ShortWithArg('v', arg) => {
|
||||||
opts.vmem = Some(arg.parse().map_err(|_| ShErr::simple(
|
opts.vmem = Some(arg.parse().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid argument for -v: {}", arg.fg(next_color())),
|
format!("invalid argument for -v: {}", arg.fg(next_color())),
|
||||||
))?);
|
)
|
||||||
},
|
})?);
|
||||||
o => return Err(ShErr::simple(
|
}
|
||||||
|
o => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("invalid option: {}", o.fg(next_color())),
|
format!("invalid option: {}", o.fg(next_color())),
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,68 +122,89 @@ pub fn ulimit(node: Node) -> ShResult<()> {
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, opts) = get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?;
|
let (_, opts) =
|
||||||
|
get_opts_from_tokens_strict(argv, &ulimit_opt_spec()).promote_err(span.clone())?;
|
||||||
let ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?;
|
let ulimit_opts = get_ulimit_opts(&opts).promote_err(span.clone())?;
|
||||||
|
|
||||||
if let Some(fds) = ulimit_opts.fds {
|
if let Some(fds) = ulimit_opts.fds {
|
||||||
let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| ShErr::at(
|
let (_, hard) = getrlimit(Resource::RLIMIT_NOFILE).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to get file descriptor limit: {}", e),
|
format!("failed to get file descriptor limit: {}", e),
|
||||||
))?;
|
)
|
||||||
setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| ShErr::at(
|
})?;
|
||||||
|
setrlimit(Resource::RLIMIT_NOFILE, fds, hard).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to set file descriptor limit: {}", e),
|
format!("failed to set file descriptor limit: {}", e),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
if let Some(procs) = ulimit_opts.procs {
|
if let Some(procs) = ulimit_opts.procs {
|
||||||
let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| ShErr::at(
|
let (_, hard) = getrlimit(Resource::RLIMIT_NPROC).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to get process limit: {}", e),
|
format!("failed to get process limit: {}", e),
|
||||||
))?;
|
)
|
||||||
setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| ShErr::at(
|
})?;
|
||||||
|
setrlimit(Resource::RLIMIT_NPROC, procs, hard).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to set process limit: {}", e),
|
format!("failed to set process limit: {}", e),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
if let Some(stack) = ulimit_opts.stack {
|
if let Some(stack) = ulimit_opts.stack {
|
||||||
let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| ShErr::at(
|
let (_, hard) = getrlimit(Resource::RLIMIT_STACK).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to get stack size limit: {}", e),
|
format!("failed to get stack size limit: {}", e),
|
||||||
))?;
|
)
|
||||||
setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| ShErr::at(
|
})?;
|
||||||
|
setrlimit(Resource::RLIMIT_STACK, stack, hard).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to set stack size limit: {}", e),
|
format!("failed to set stack size limit: {}", e),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
if let Some(core) = ulimit_opts.core {
|
if let Some(core) = ulimit_opts.core {
|
||||||
let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| ShErr::at(
|
let (_, hard) = getrlimit(Resource::RLIMIT_CORE).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to get core dump size limit: {}", e),
|
format!("failed to get core dump size limit: {}", e),
|
||||||
))?;
|
)
|
||||||
setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| ShErr::at(
|
})?;
|
||||||
|
setrlimit(Resource::RLIMIT_CORE, core, hard).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to set core dump size limit: {}", e),
|
format!("failed to set core dump size limit: {}", e),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
if let Some(vmem) = ulimit_opts.vmem {
|
if let Some(vmem) = ulimit_opts.vmem {
|
||||||
let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| ShErr::at(
|
let (_, hard) = getrlimit(Resource::RLIMIT_AS).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to get virtual memory limit: {}", e),
|
format!("failed to get virtual memory limit: {}", e),
|
||||||
))?;
|
)
|
||||||
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| ShErr::at(
|
})?;
|
||||||
|
setrlimit(Resource::RLIMIT_AS, vmem, hard).map_err(|e| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("failed to set virtual memory limit: {}", e),
|
format!("failed to set virtual memory limit: {}", e),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
@@ -172,11 +216,17 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv,
|
argv,
|
||||||
} = node.class else { unreachable!() };
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
let (argv, opts) = get_opts_from_tokens_strict(
|
let (argv, opts) = get_opts_from_tokens_strict(
|
||||||
argv,
|
argv,
|
||||||
&[OptSpec { opt: Opt::Short('S'), takes_arg: false }],
|
&[OptSpec {
|
||||||
|
opt: Opt::Short('S'),
|
||||||
|
takes_arg: false,
|
||||||
|
}],
|
||||||
)?;
|
)?;
|
||||||
let argv = &argv[1..]; // skip command name
|
let argv = &argv[1..]; // skip command name
|
||||||
|
|
||||||
@@ -195,24 +245,28 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
let arg = argv[0].clone();
|
let arg = argv[0].clone();
|
||||||
let raw = arg.as_str();
|
let raw = arg.as_str();
|
||||||
if raw.chars().any(|c| c.is_ascii_digit()) {
|
if raw.chars().any(|c| c.is_ascii_digit()) {
|
||||||
let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| ShErr::at(
|
let mode_raw = u32::from_str_radix(raw, 8).map_err(|_| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("invalid numeric umask: {}", raw.fg(next_color())),
|
format!("invalid numeric umask: {}", raw.fg(next_color())),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mode = Mode::from_bits(mode_raw).ok_or_else(|| ShErr::at(
|
let mode = Mode::from_bits(mode_raw).ok_or_else(|| {
|
||||||
|
ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
span.clone(),
|
span.clone(),
|
||||||
format!("invalid umask value: {}", raw.fg(next_color())),
|
format!("invalid umask value: {}", raw.fg(next_color())),
|
||||||
))?;
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
umask(mode);
|
umask(mode);
|
||||||
} else {
|
} else {
|
||||||
let parts = raw.split(',');
|
let parts = raw.split(',');
|
||||||
|
|
||||||
for part in parts {
|
for part in parts {
|
||||||
if let Some((who,bits)) = part.split_once('=') {
|
if let Some((who, bits)) = part.split_once('=') {
|
||||||
let mut new_bits = 0;
|
let mut new_bits = 0;
|
||||||
if bits.contains('r') {
|
if bits.contains('r') {
|
||||||
new_bits |= 4;
|
new_bits |= 4;
|
||||||
@@ -253,7 +307,7 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
umask(Mode::from_bits_truncate(old_bits));
|
umask(Mode::from_bits_truncate(old_bits));
|
||||||
} else if let Some((who,bits)) = part.split_once('+') {
|
} else if let Some((who, bits)) = part.split_once('+') {
|
||||||
let mut new_bits = 0;
|
let mut new_bits = 0;
|
||||||
if bits.contains('r') {
|
if bits.contains('r') {
|
||||||
new_bits |= 4;
|
new_bits |= 4;
|
||||||
@@ -291,7 +345,7 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
umask(Mode::from_bits_truncate(old_bits));
|
umask(Mode::from_bits_truncate(old_bits));
|
||||||
} else if let Some((who,bits)) = part.split_once('-') {
|
} else if let Some((who, bits)) = part.split_once('-') {
|
||||||
let mut new_bits = 0;
|
let mut new_bits = 0;
|
||||||
if bits.contains('r') {
|
if bits.contains('r') {
|
||||||
new_bits |= 4;
|
new_bits |= 4;
|
||||||
@@ -337,7 +391,6 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if !opts.is_empty() {
|
} else if !opts.is_empty() {
|
||||||
let u = (old_bits >> 6) & 0o7;
|
let u = (old_bits >> 6) & 0o7;
|
||||||
let g = (old_bits >> 3) & 0o7;
|
let g = (old_bits >> 3) & 0o7;
|
||||||
@@ -345,11 +398,7 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
let mut u_str = String::from("u=");
|
let mut u_str = String::from("u=");
|
||||||
let mut g_str = String::from("g=");
|
let mut g_str = String::from("g=");
|
||||||
let mut o_str = String::from("o=");
|
let mut o_str = String::from("o=");
|
||||||
let stuff = [
|
let stuff = [(u, &mut u_str), (g, &mut g_str), (o, &mut o_str)];
|
||||||
(u, &mut u_str),
|
|
||||||
(g, &mut g_str),
|
|
||||||
(o, &mut o_str),
|
|
||||||
];
|
|
||||||
for (bits, out) in stuff.into_iter() {
|
for (bits, out) in stuff.into_iter() {
|
||||||
if bits & 4 == 0 {
|
if bits & 4 == 0 {
|
||||||
out.push('r');
|
out.push('r');
|
||||||
@@ -362,7 +411,7 @@ pub fn umask_builtin(node: Node) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg = [u_str,g_str,o_str].join(",");
|
let msg = [u_str, g_str, o_str].join(",");
|
||||||
let stdout = borrow_fd(STDOUT_FILENO);
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
write(stdout, msg.as_bytes())?;
|
write(stdout, msg.as_bytes())?;
|
||||||
write(stdout, b"\n")?;
|
write(stdout, b"\n")?;
|
||||||
@@ -423,7 +472,8 @@ mod tests {
|
|||||||
let opts = get_ulimit_opts(&[
|
let opts = get_ulimit_opts(&[
|
||||||
Opt::ShortWithArg('n', "256".into()),
|
Opt::ShortWithArg('n', "256".into()),
|
||||||
Opt::ShortWithArg('c', "0".into()),
|
Opt::ShortWithArg('c', "0".into()),
|
||||||
]).unwrap();
|
])
|
||||||
|
.unwrap();
|
||||||
assert_eq!(opts.fds, Some(256));
|
assert_eq!(opts.fds, Some(256));
|
||||||
assert_eq!(opts.core, Some(0));
|
assert_eq!(opts.core, Some(0));
|
||||||
assert!(opts.procs.is_none());
|
assert!(opts.procs.is_none());
|
||||||
|
|||||||
263
src/builtin/seek.rs
Normal file
263
src/builtin/seek.rs
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
use nix::{
|
||||||
|
libc::STDOUT_FILENO,
|
||||||
|
unistd::{Whence, lseek, write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node, execute::prepare_argv},
|
||||||
|
procio::borrow_fd,
|
||||||
|
state,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const LSEEK_OPTS: [OptSpec; 2] = [
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('c'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('e'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub struct LseekOpts {
|
||||||
|
cursor_rel: bool,
|
||||||
|
end_rel: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn seek(node: Node) -> ShResult<()> {
|
||||||
|
let NdRule::Command {
|
||||||
|
assignments: _,
|
||||||
|
argv,
|
||||||
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
|
||||||
|
let lseek_opts = get_lseek_opts(opts)?;
|
||||||
|
let mut argv = prepare_argv(argv)?.into_iter();
|
||||||
|
argv.next(); // drop 'seek'
|
||||||
|
|
||||||
|
let Some(fd) = argv.next() else {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
"lseek: Missing required argument 'fd'",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Ok(fd) = fd.0.parse::<u32>() else {
|
||||||
|
return Err(
|
||||||
|
ShErr::at(ShErrKind::ExecFail, fd.1, "Invalid file descriptor")
|
||||||
|
.with_note("file descriptors are integers"),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(offset) = argv.next() else {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
"lseek: Missing required argument 'offset'",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
let Ok(offset) = offset.0.parse::<i64>() else {
|
||||||
|
return Err(
|
||||||
|
ShErr::at(ShErrKind::ExecFail, offset.1, "Invalid offset")
|
||||||
|
.with_note("offset can be a positive or negative integer"),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let whence = if lseek_opts.cursor_rel {
|
||||||
|
Whence::SeekCur
|
||||||
|
} else if lseek_opts.end_rel {
|
||||||
|
Whence::SeekEnd
|
||||||
|
} else {
|
||||||
|
Whence::SeekSet
|
||||||
|
};
|
||||||
|
|
||||||
|
match lseek(fd as i32, offset, whence) {
|
||||||
|
Ok(new_offset) => {
|
||||||
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
|
let buf = new_offset.to_string() + "\n";
|
||||||
|
write(stdout, buf.as_bytes())?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
state::set_status(1);
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state::set_status(0);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
|
||||||
|
let mut lseek_opts = LseekOpts {
|
||||||
|
cursor_rel: false,
|
||||||
|
end_rel: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for opt in opts {
|
||||||
|
match opt {
|
||||||
|
Opt::Short('c') => lseek_opts.cursor_rel = true,
|
||||||
|
Opt::Short('e') => lseek_opts.end_rel = true,
|
||||||
|
_ => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
format!("lseek: Unexpected flag '{opt}'"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(lseek_opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_set_beginning() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello world\n").unwrap();
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek 9 0").unwrap();
|
||||||
|
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "0\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_set_offset() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello world\n").unwrap();
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek 9 6").unwrap();
|
||||||
|
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "6\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_then_read() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello world\n").unwrap();
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek 9 6").unwrap();
|
||||||
|
// Clear the seek output
|
||||||
|
g.read_output();
|
||||||
|
|
||||||
|
test_input("read line <&9").unwrap();
|
||||||
|
let val = crate::state::read_vars(|v| v.get_var("line"));
|
||||||
|
assert_eq!(val, "world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_cur_relative() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "abcdefghij\n").unwrap();
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek 9 3").unwrap();
|
||||||
|
test_input("seek -c 9 4").unwrap();
|
||||||
|
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "3\n7\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_end() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek -e 9 0").unwrap();
|
||||||
|
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "6\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_end_negative() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek -e 9 -2").unwrap();
|
||||||
|
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "4\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_write_overwrite() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "hello world\n").unwrap();
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
test_input("seek 9 6").unwrap();
|
||||||
|
test_input("echo -n 'WORLD' >&9").unwrap();
|
||||||
|
|
||||||
|
let contents = std::fs::read_to_string(&path).unwrap();
|
||||||
|
assert_eq!(contents, "hello WORLD\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_rewind_full_read() {
|
||||||
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("seek.txt");
|
||||||
|
std::fs::write(&path, "abc\n").unwrap();
|
||||||
|
let g = TestGuard::new();
|
||||||
|
|
||||||
|
test_input(format!("exec 9<> {}", path.display())).unwrap();
|
||||||
|
// Read moves cursor to EOF
|
||||||
|
test_input("read line <&9").unwrap();
|
||||||
|
// Rewind
|
||||||
|
test_input("seek 9 0").unwrap();
|
||||||
|
// Clear output from seek
|
||||||
|
g.read_output();
|
||||||
|
// Read again from beginning
|
||||||
|
test_input("read line <&9").unwrap();
|
||||||
|
|
||||||
|
let val = crate::state::read_vars(|v| v.get_var("line"));
|
||||||
|
assert_eq!(val, "abc");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_bad_fd() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
|
||||||
|
let result = test_input("seek 99 0");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn seek_missing_args() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
|
||||||
|
let result = test_input("seek");
|
||||||
|
assert!(result.is_err());
|
||||||
|
|
||||||
|
let result = test_input("seek 9");
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,9 +46,9 @@ pub fn source(node: Node) -> ShResult<()> {
|
|||||||
pub mod tests {
|
pub mod tests {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use tempfile::{NamedTempFile, TempDir};
|
|
||||||
use crate::state::{self, read_logic, read_vars};
|
use crate::state::{self, read_logic, read_vars};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use tempfile::{NamedTempFile, TempDir};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn source_simple() {
|
fn source_simple() {
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ impl FromStr for TestOp {
|
|||||||
"-ge" => Ok(Self::IntGe),
|
"-ge" => Ok(Self::IntGe),
|
||||||
"-le" => Ok(Self::IntLe),
|
"-le" => Ok(Self::IntLe),
|
||||||
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
|
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
|
||||||
_ => Err(ShErr::simple(ShErrKind::SyntaxErr, format!("Invalid test operator '{}'", s))),
|
_ => Err(ShErr::simple(
|
||||||
|
ShErrKind::SyntaxErr,
|
||||||
|
format!("Invalid test operator '{}'", s),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,10 +308,10 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs;
|
|
||||||
use tempfile::{TempDir, NamedTempFile};
|
|
||||||
use crate::state;
|
use crate::state;
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use std::fs;
|
||||||
|
use tempfile::{NamedTempFile, TempDir};
|
||||||
|
|
||||||
// ===================== Unary: file tests =====================
|
// ===================== Unary: file tests =====================
|
||||||
|
|
||||||
@@ -590,9 +593,10 @@ mod tests {
|
|||||||
fn parse_unary_ops() {
|
fn parse_unary_ops() {
|
||||||
use super::UnaryOp;
|
use super::UnaryOp;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
for op in ["-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s",
|
for op in [
|
||||||
"-p", "-S", "-b", "-c", "-k", "-O", "-G", "-N", "-u",
|
"-e", "-d", "-f", "-h", "-L", "-r", "-w", "-x", "-s", "-p", "-S", "-b", "-c", "-k", "-O",
|
||||||
"-g", "-t", "-n", "-z"] {
|
"-G", "-N", "-u", "-g", "-t", "-n", "-z",
|
||||||
|
] {
|
||||||
assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}");
|
assert!(UnaryOp::from_str(op).is_ok(), "failed to parse {op}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,10 +171,10 @@ pub fn trap(node: Node) -> ShResult<()> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::TrapTarget;
|
use super::TrapTarget;
|
||||||
use std::str::FromStr;
|
|
||||||
use nix::sys::signal::Signal;
|
|
||||||
use crate::state::{self, read_logic};
|
use crate::state::{self, read_logic};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use nix::sys::signal::Signal;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
// ===================== Pure: TrapTarget parsing =====================
|
// ===================== Pure: TrapTarget parsing =====================
|
||||||
|
|
||||||
@@ -231,7 +231,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn display_signal_roundtrip() {
|
fn display_signal_roundtrip() {
|
||||||
for name in &["INT", "QUIT", "TERM", "USR1", "USR2", "ALRM", "CHLD", "WINCH"] {
|
for name in &[
|
||||||
|
"INT", "QUIT", "TERM", "USR1", "USR2", "ALRM", "CHLD", "WINCH",
|
||||||
|
] {
|
||||||
let target = TrapTarget::from_str(name).unwrap();
|
let target = TrapTarget::from_str(name).unwrap();
|
||||||
assert_eq!(target.to_string(), *name);
|
assert_eq!(target.to_string(), *name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -245,8 +245,16 @@ mod tests {
|
|||||||
test_input("readonly a=1 b=2").unwrap();
|
test_input("readonly a=1 b=2").unwrap();
|
||||||
assert_eq!(read_vars(|v| v.get_var("a")), "1");
|
assert_eq!(read_vars(|v| v.get_var("a")), "1");
|
||||||
assert_eq!(read_vars(|v| v.get_var("b")), "2");
|
assert_eq!(read_vars(|v| v.get_var("b")), "2");
|
||||||
assert!(read_vars(|v| v.get_var_flags("a")).unwrap().contains(VarFlags::READONLY));
|
assert!(
|
||||||
assert!(read_vars(|v| v.get_var_flags("b")).unwrap().contains(VarFlags::READONLY));
|
read_vars(|v| v.get_var_flags("a"))
|
||||||
|
.unwrap()
|
||||||
|
.contains(VarFlags::READONLY)
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
read_vars(|v| v.get_var_flags("b"))
|
||||||
|
.unwrap()
|
||||||
|
.contains(VarFlags::READONLY)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -385,7 +393,11 @@ mod tests {
|
|||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
test_input("local mylocal").unwrap();
|
test_input("local mylocal").unwrap();
|
||||||
assert_eq!(read_vars(|v| v.get_var("mylocal")), "");
|
assert_eq!(read_vars(|v| v.get_var("mylocal")), "");
|
||||||
assert!(read_vars(|v| v.get_var_flags("mylocal")).unwrap().contains(VarFlags::LOCAL));
|
assert!(
|
||||||
|
read_vars(|v| v.get_var_flags("mylocal"))
|
||||||
|
.unwrap()
|
||||||
|
.contains(VarFlags::LOCAL)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
510
src/expand.rs
510
src/expand.rs
@@ -4,6 +4,7 @@ use std::str::{Chars, FromStr};
|
|||||||
|
|
||||||
use ariadne::Fmt;
|
use ariadne::Fmt;
|
||||||
use glob::Pattern;
|
use glob::Pattern;
|
||||||
|
use nix::unistd::{Uid, User};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color};
|
||||||
@@ -40,18 +41,26 @@ impl Tk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Expander {
|
pub struct Expander {
|
||||||
|
flags: TkFlags,
|
||||||
raw: String,
|
raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Expander {
|
impl Expander {
|
||||||
pub fn new(raw: Tk) -> ShResult<Self> {
|
pub fn new(raw: Tk) -> ShResult<Self> {
|
||||||
let raw = raw.span.as_str();
|
let tk_raw = raw.span.as_str();
|
||||||
Self::from_raw(raw)
|
Self::from_raw(tk_raw, raw.flags)
|
||||||
}
|
}
|
||||||
pub fn from_raw(raw: &str) -> ShResult<Self> {
|
pub fn from_raw(raw: &str, flags: TkFlags) -> ShResult<Self> {
|
||||||
let raw = expand_braces_full(raw)?.join(" ");
|
let raw = expand_braces_full(raw)?.join(" ");
|
||||||
let unescaped = unescape_str(&raw);
|
let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
Ok(Self { raw: unescaped })
|
unescape_heredoc(&raw)
|
||||||
|
} else {
|
||||||
|
unescape_str(&raw)
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
raw: unescaped,
|
||||||
|
flags,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
||||||
let mut chars = self.raw.chars().peekable();
|
let mut chars = self.raw.chars().peekable();
|
||||||
@@ -75,8 +84,12 @@ impl Expander {
|
|||||||
self.raw.insert_str(0, "./");
|
self.raw.insert_str(0, "./");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
|
Ok(vec![self.raw.clone()])
|
||||||
|
} else {
|
||||||
Ok(self.split_words())
|
Ok(self.split_words())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pub fn split_words(&mut self) -> Vec<String> {
|
pub fn split_words(&mut self) -> Vec<String> {
|
||||||
let mut words = vec![];
|
let mut words = vec![];
|
||||||
let mut chars = self.raw.chars();
|
let mut chars = self.raw.chars();
|
||||||
@@ -86,6 +99,11 @@ impl Expander {
|
|||||||
|
|
||||||
'outer: while let Some(ch) = chars.next() {
|
'outer: while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
|
markers::ESCAPE => {
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
cur_word.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => {
|
markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => {
|
||||||
while let Some(q_ch) = chars.next() {
|
while let Some(q_ch) = chars.next() {
|
||||||
match q_ch {
|
match q_ch {
|
||||||
@@ -456,7 +474,26 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
markers::TILDE_SUB => {
|
markers::TILDE_SUB => {
|
||||||
let home = env::var("HOME").unwrap_or_default();
|
let mut username = String::new();
|
||||||
|
while chars.peek().is_some_and(|ch| *ch != '/') {
|
||||||
|
let ch = chars.next().unwrap();
|
||||||
|
username.push(ch);
|
||||||
|
}
|
||||||
|
let home = if username.is_empty() {
|
||||||
|
env::var("HOME").unwrap_or_default()
|
||||||
|
}
|
||||||
|
else if let Ok(result) = User::from_name(&username)
|
||||||
|
&& let Some(user) = result {
|
||||||
|
user.dir.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
else if let Ok(id) = username.parse::<u32>()
|
||||||
|
&& let Ok(result) = User::from_uid(Uid::from_raw(id))
|
||||||
|
&& let Some(user) = result {
|
||||||
|
user.dir.to_string_lossy().to_string()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
format!("~{username}")
|
||||||
|
};
|
||||||
result.push_str(&home);
|
result.push_str(&home);
|
||||||
}
|
}
|
||||||
markers::PROC_SUB_OUT => {
|
markers::PROC_SUB_OUT => {
|
||||||
@@ -634,8 +671,12 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
|
|||||||
{
|
{
|
||||||
let entry =
|
let entry =
|
||||||
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
||||||
|
let entry_raw = entry
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| ShErr::simple(ShErrKind::SyntaxErr, "Non-UTF8 filename found in glob"))?;
|
||||||
|
let escaped = escape_str(entry_raw, true);
|
||||||
|
|
||||||
words.push(entry.to_str().unwrap().to_string())
|
words.push(escaped)
|
||||||
}
|
}
|
||||||
Ok(words.join(" "))
|
Ok(words.join(" "))
|
||||||
}
|
}
|
||||||
@@ -973,6 +1014,11 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strip ESCAPE markers from a string, leaving the characters they protect intact.
|
||||||
|
fn strip_escape_markers(s: &str) -> String {
|
||||||
|
s.replace(markers::ESCAPE, "")
|
||||||
|
}
|
||||||
|
|
||||||
/// Processes strings into intermediate representations that are more readable
|
/// Processes strings into intermediate representations that are more readable
|
||||||
/// by the program
|
/// by the program
|
||||||
///
|
///
|
||||||
@@ -989,6 +1035,7 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
'~' if first_char => result.push(markers::TILDE_SUB),
|
'~' if first_char => result.push(markers::TILDE_SUB),
|
||||||
'\\' => {
|
'\\' => {
|
||||||
if let Some(next_ch) = chars.next() {
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(markers::ESCAPE);
|
||||||
result.push(next_ch)
|
result.push(next_ch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1139,6 +1186,25 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::VAR_SUB);
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
while let Some(bt_ch) = chars.next() {
|
||||||
|
match bt_ch {
|
||||||
|
'\\' => {
|
||||||
|
result.push(bt_ch);
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => result.push(bt_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
'"' => {
|
'"' => {
|
||||||
result.push(markers::DUB_QUOTE);
|
result.push(markers::DUB_QUOTE);
|
||||||
break;
|
break;
|
||||||
@@ -1303,6 +1369,25 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
result.push('$');
|
result.push('$');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::VAR_SUB);
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
while let Some(bt_ch) = chars.next() {
|
||||||
|
match bt_ch {
|
||||||
|
'\\' => {
|
||||||
|
result.push(bt_ch);
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => result.push(bt_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => result.push(ch),
|
_ => result.push(ch),
|
||||||
}
|
}
|
||||||
first_char = false;
|
first_char = false;
|
||||||
@@ -1311,6 +1396,133 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like unescape_str but for heredoc bodies. Only processes:
|
||||||
|
/// - $var / ${var} / $(cmd) substitution markers
|
||||||
|
/// - Backslash escapes (only before $, `, \, and newline)
|
||||||
|
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
|
||||||
|
pub fn unescape_heredoc(raw: &str) -> String {
|
||||||
|
let mut chars = raw.chars().peekable();
|
||||||
|
let mut result = String::new();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
match chars.peek() {
|
||||||
|
Some('$') | Some('`') | Some('\\') | Some('\n') => {
|
||||||
|
let next_ch = chars.next().unwrap();
|
||||||
|
if next_ch == '\n' {
|
||||||
|
// line continuation — discard both backslash and newline
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push(markers::ESCAPE);
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// backslash is literal
|
||||||
|
result.push('\\');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'$' if chars.peek() == Some(&'(') => {
|
||||||
|
result.push(markers::VAR_SUB);
|
||||||
|
chars.next(); // consume '('
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
let mut paren_count = 1;
|
||||||
|
while let Some(subsh_ch) = chars.next() {
|
||||||
|
match subsh_ch {
|
||||||
|
'\\' => {
|
||||||
|
result.push(subsh_ch);
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'(' => {
|
||||||
|
paren_count += 1;
|
||||||
|
result.push(subsh_ch);
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
paren_count -= 1;
|
||||||
|
if paren_count == 0 {
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
result.push(subsh_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => result.push(subsh_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'$' => {
|
||||||
|
result.push(markers::VAR_SUB);
|
||||||
|
if chars.peek() == Some(&'$') {
|
||||||
|
chars.next();
|
||||||
|
result.push('$');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::VAR_SUB);
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
while let Some(bt_ch) = chars.next() {
|
||||||
|
match bt_ch {
|
||||||
|
'\\' => {
|
||||||
|
result.push(bt_ch);
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'`' => {
|
||||||
|
result.push(markers::SUBSH);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => result.push(bt_ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => result.push(ch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opposite of unescape_str - escapes a string to be executed as literal text
|
||||||
|
/// Used for completion results, and glob filename matches.
|
||||||
|
pub fn escape_str(raw: &str, use_marker: bool) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut chars = raw.chars();
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\'' | '"' | '\\' | '|' | '&' | ';' | '(' | ')' | '<' | '>' | '$' | '*' | '!' | '`' | '{'
|
||||||
|
| '?' | '[' | '#' | ' ' | '\t' | '\n' => {
|
||||||
|
if use_marker {
|
||||||
|
result.push(markers::ESCAPE);
|
||||||
|
} else {
|
||||||
|
result.push('\\');
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
'~' if result.is_empty() => {
|
||||||
|
if use_marker {
|
||||||
|
result.push(markers::ESCAPE);
|
||||||
|
} else {
|
||||||
|
result.push('\\');
|
||||||
|
}
|
||||||
|
result.push(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
result.push(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unescape_math(raw: &str) -> String {
|
pub fn unescape_math(raw: &str) -> String {
|
||||||
let mut chars = raw.chars().peekable();
|
let mut chars = raw.chars().peekable();
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
@@ -1588,7 +1800,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
ParamExp::RemShortestPrefix(prefix) => {
|
ParamExp::RemShortestPrefix(prefix) => {
|
||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let unescaped = unescape_str(&prefix);
|
let unescaped = unescape_str(&prefix);
|
||||||
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix);
|
let expanded =
|
||||||
|
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix));
|
||||||
let pattern = Pattern::new(&expanded).unwrap();
|
let pattern = Pattern::new(&expanded).unwrap();
|
||||||
for i in 0..=value.len() {
|
for i in 0..=value.len() {
|
||||||
let sliced = &value[..i];
|
let sliced = &value[..i];
|
||||||
@@ -1601,7 +1814,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
ParamExp::RemLongestPrefix(prefix) => {
|
ParamExp::RemLongestPrefix(prefix) => {
|
||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let unescaped = unescape_str(&prefix);
|
let unescaped = unescape_str(&prefix);
|
||||||
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix);
|
let expanded =
|
||||||
|
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(prefix));
|
||||||
let pattern = Pattern::new(&expanded).unwrap();
|
let pattern = Pattern::new(&expanded).unwrap();
|
||||||
for i in (0..=value.len()).rev() {
|
for i in (0..=value.len()).rev() {
|
||||||
let sliced = &value[..i];
|
let sliced = &value[..i];
|
||||||
@@ -1614,7 +1828,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
ParamExp::RemShortestSuffix(suffix) => {
|
ParamExp::RemShortestSuffix(suffix) => {
|
||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let unescaped = unescape_str(&suffix);
|
let unescaped = unescape_str(&suffix);
|
||||||
let expanded = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix);
|
let expanded =
|
||||||
|
strip_escape_markers(&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix));
|
||||||
let pattern = Pattern::new(&expanded).unwrap();
|
let pattern = Pattern::new(&expanded).unwrap();
|
||||||
for i in (0..=value.len()).rev() {
|
for i in (0..=value.len()).rev() {
|
||||||
let sliced = &value[i..];
|
let sliced = &value[i..];
|
||||||
@@ -1627,8 +1842,9 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
ParamExp::RemLongestSuffix(suffix) => {
|
ParamExp::RemLongestSuffix(suffix) => {
|
||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let unescaped = unescape_str(&suffix);
|
let unescaped = unescape_str(&suffix);
|
||||||
let expanded_suffix =
|
let expanded_suffix = strip_escape_markers(
|
||||||
expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone());
|
&expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()),
|
||||||
|
);
|
||||||
let pattern = Pattern::new(&expanded_suffix).unwrap();
|
let pattern = Pattern::new(&expanded_suffix).unwrap();
|
||||||
for i in 0..=value.len() {
|
for i in 0..=value.len() {
|
||||||
let sliced = &value[i..];
|
let sliced = &value[i..];
|
||||||
@@ -1642,8 +1858,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let search = unescape_str(&search);
|
let search = unescape_str(&search);
|
||||||
let replace = unescape_str(&replace);
|
let replace = unescape_str(&replace);
|
||||||
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
|
let expanded_search =
|
||||||
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
|
strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
|
||||||
|
let expanded_replace =
|
||||||
|
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
|
||||||
let regex = glob_to_regex(&expanded_search, false); // unanchored pattern
|
let regex = glob_to_regex(&expanded_search, false); // unanchored pattern
|
||||||
|
|
||||||
if let Some(mat) = regex.find(&value) {
|
if let Some(mat) = regex.find(&value) {
|
||||||
@@ -1659,8 +1877,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let search = unescape_str(&search);
|
let search = unescape_str(&search);
|
||||||
let replace = unescape_str(&replace);
|
let replace = unescape_str(&replace);
|
||||||
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
|
let expanded_search =
|
||||||
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
|
strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
|
||||||
|
let expanded_replace =
|
||||||
|
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
|
||||||
let regex = glob_to_regex(&expanded_search, false);
|
let regex = glob_to_regex(&expanded_search, false);
|
||||||
let mut result = String::new();
|
let mut result = String::new();
|
||||||
let mut last_match_end = 0;
|
let mut last_match_end = 0;
|
||||||
@@ -1679,8 +1899,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let search = unescape_str(&search);
|
let search = unescape_str(&search);
|
||||||
let replace = unescape_str(&replace);
|
let replace = unescape_str(&replace);
|
||||||
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
|
let expanded_search =
|
||||||
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
|
strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
|
||||||
|
let expanded_replace =
|
||||||
|
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
|
||||||
let pattern = Pattern::new(&expanded_search).unwrap();
|
let pattern = Pattern::new(&expanded_search).unwrap();
|
||||||
for i in (0..=value.len()).rev() {
|
for i in (0..=value.len()).rev() {
|
||||||
let sliced = &value[..i];
|
let sliced = &value[..i];
|
||||||
@@ -1694,8 +1916,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
let search = unescape_str(&search);
|
let search = unescape_str(&search);
|
||||||
let replace = unescape_str(&replace);
|
let replace = unescape_str(&replace);
|
||||||
let expanded_search = expand_raw(&mut search.chars().peekable()).unwrap_or(search);
|
let expanded_search =
|
||||||
let expanded_replace = expand_raw(&mut replace.chars().peekable()).unwrap_or(replace);
|
strip_escape_markers(&expand_raw(&mut search.chars().peekable()).unwrap_or(search));
|
||||||
|
let expanded_replace =
|
||||||
|
strip_escape_markers(&expand_raw(&mut replace.chars().peekable()).unwrap_or(replace));
|
||||||
let pattern = Pattern::new(&expanded_search).unwrap();
|
let pattern = Pattern::new(&expanded_search).unwrap();
|
||||||
for i in (0..=value.len()).rev() {
|
for i in (0..=value.len()).rev() {
|
||||||
let sliced = &value[i..];
|
let sliced = &value[i..];
|
||||||
@@ -1740,6 +1964,11 @@ pub fn expand_case_pattern(raw: &str) -> ShResult<String> {
|
|||||||
markers::DUB_QUOTE | markers::SNG_QUOTE => {
|
markers::DUB_QUOTE | markers::SNG_QUOTE => {
|
||||||
in_quote = !in_quote;
|
in_quote = !in_quote;
|
||||||
}
|
}
|
||||||
|
markers::ESCAPE => {
|
||||||
|
if let Some(next_ch) = chars.next() {
|
||||||
|
result.push(next_ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
'*' | '?' | '[' | ']' if in_quote => {
|
'*' | '?' | '[' | ']' if in_quote => {
|
||||||
result.push('\\');
|
result.push('\\');
|
||||||
result.push(ch);
|
result.push(ch);
|
||||||
@@ -2381,11 +2610,11 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::time::Duration;
|
|
||||||
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
|
|
||||||
use crate::state::{write_vars, read_vars, ArrIndex, VarKind, VarFlags};
|
|
||||||
use crate::parse::lex::Span;
|
use crate::parse::lex::Span;
|
||||||
|
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
|
||||||
|
use crate::state::{ArrIndex, VarFlags, VarKind, read_vars, write_vars};
|
||||||
use crate::testutil::{TestGuard, test_input};
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
// ===================== has_braces =====================
|
// ===================== has_braces =====================
|
||||||
|
|
||||||
@@ -2525,10 +2754,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn braces_simple_list() {
|
fn braces_simple_list() {
|
||||||
assert_eq!(
|
assert_eq!(expand_braces_full("{a,b,c}").unwrap(), vec!["a", "b", "c"]);
|
||||||
expand_braces_full("{a,b,c}").unwrap(),
|
|
||||||
vec!["a", "b", "c"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -2617,7 +2843,19 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn braces_cursed() {
|
fn braces_cursed() {
|
||||||
let result = expand_braces_full("foo{a,{1,2,3,{1..4},5},c}{5..1}bar").unwrap();
|
let result = expand_braces_full("foo{a,{1,2,3,{1..4},5},c}{5..1}bar").unwrap();
|
||||||
assert_eq!(result, vec![ "fooa5bar", "fooa4bar", "fooa3bar", "fooa2bar", "fooa1bar", "foo15bar", "foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo15bar", "foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo45bar", "foo44bar", "foo43bar", "foo42bar", "foo41bar", "foo55bar", "foo54bar", "foo53bar", "foo52bar", "foo51bar", "fooc5bar", "fooc4bar", "fooc3bar", "fooc2bar", "fooc1bar", ])
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![
|
||||||
|
"fooa5bar", "fooa4bar", "fooa3bar", "fooa2bar", "fooa1bar", "foo15bar", "foo14bar",
|
||||||
|
"foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar", "foo22bar",
|
||||||
|
"foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar", "foo15bar",
|
||||||
|
"foo14bar", "foo13bar", "foo12bar", "foo11bar", "foo25bar", "foo24bar", "foo23bar",
|
||||||
|
"foo22bar", "foo21bar", "foo35bar", "foo34bar", "foo33bar", "foo32bar", "foo31bar",
|
||||||
|
"foo45bar", "foo44bar", "foo43bar", "foo42bar", "foo41bar", "foo55bar", "foo54bar",
|
||||||
|
"foo53bar", "foo52bar", "foo51bar", "fooc5bar", "fooc4bar", "fooc3bar", "fooc2bar",
|
||||||
|
"fooc1bar",
|
||||||
|
]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Arithmetic =====================
|
// ===================== Arithmetic =====================
|
||||||
@@ -2858,7 +3096,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unescape_backslash() {
|
fn unescape_backslash() {
|
||||||
let result = unescape_str("hello\\nworld");
|
let result = unescape_str("hello\\nworld");
|
||||||
assert_eq!(result, "hellonworld");
|
let expected = format!("hello{}nworld", markers::ESCAPE);
|
||||||
|
assert_eq!(result, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3089,10 +3328,22 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn key_alias_arrows() {
|
fn key_alias_arrows() {
|
||||||
assert_eq!(parse_key_alias("UP").unwrap(), KeyEvent(KeyCode::Up, ModKeys::NONE));
|
assert_eq!(
|
||||||
assert_eq!(parse_key_alias("DOWN").unwrap(), KeyEvent(KeyCode::Down, ModKeys::NONE));
|
parse_key_alias("UP").unwrap(),
|
||||||
assert_eq!(parse_key_alias("LEFT").unwrap(), KeyEvent(KeyCode::Left, ModKeys::NONE));
|
KeyEvent(KeyCode::Up, ModKeys::NONE)
|
||||||
assert_eq!(parse_key_alias("RIGHT").unwrap(), KeyEvent(KeyCode::Right, ModKeys::NONE));
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_key_alias("DOWN").unwrap(),
|
||||||
|
KeyEvent(KeyCode::Down, ModKeys::NONE)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_key_alias("LEFT").unwrap(),
|
||||||
|
KeyEvent(KeyCode::Left, ModKeys::NONE)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_key_alias("RIGHT").unwrap(),
|
||||||
|
KeyEvent(KeyCode::Right, ModKeys::NONE)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3104,7 +3355,13 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn key_alias_ctrl_shift_alt_modifier() {
|
fn key_alias_ctrl_shift_alt_modifier() {
|
||||||
let key = parse_key_alias("C-S-A-b").unwrap();
|
let key = parse_key_alias("C-S-A-b").unwrap();
|
||||||
assert_eq!(key, KeyEvent(KeyCode::Char('B'), ModKeys::CTRL | ModKeys::SHIFT | ModKeys::ALT));
|
assert_eq!(
|
||||||
|
key,
|
||||||
|
KeyEvent(
|
||||||
|
KeyCode::Char('B'),
|
||||||
|
ModKeys::CTRL | ModKeys::SHIFT | ModKeys::ALT
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3296,7 +3553,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_remove_shortest_prefix() {
|
fn param_remove_shortest_prefix() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| v.set_var("PATH", VarKind::Str("/usr/local/bin".into()), VarFlags::NONE)).unwrap();
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"PATH",
|
||||||
|
VarKind::Str("/usr/local/bin".into()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let result = perform_param_expansion("PATH#*/").unwrap();
|
let result = perform_param_expansion("PATH#*/").unwrap();
|
||||||
assert_eq!(result, "usr/local/bin");
|
assert_eq!(result, "usr/local/bin");
|
||||||
@@ -3305,7 +3569,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn param_remove_longest_prefix() {
|
fn param_remove_longest_prefix() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| v.set_var("PATH", VarKind::Str("/usr/local/bin".into()), VarFlags::NONE)).unwrap();
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"PATH",
|
||||||
|
VarKind::Str("/usr/local/bin".into()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let result = perform_param_expansion("PATH##*/").unwrap();
|
let result = perform_param_expansion("PATH##*/").unwrap();
|
||||||
assert_eq!(result, "bin");
|
assert_eq!(result, "bin");
|
||||||
@@ -3419,7 +3690,10 @@ mod tests {
|
|||||||
fn word_split_default_ifs() {
|
fn word_split_default_ifs() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
|
|
||||||
let mut exp = Expander { raw: "hello world\tfoo".to_string() };
|
let mut exp = Expander {
|
||||||
|
raw: "hello world\tfoo".to_string(),
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
let words = exp.split_words();
|
let words = exp.split_words();
|
||||||
assert_eq!(words, vec!["hello", "world", "foo"]);
|
assert_eq!(words, vec!["hello", "world", "foo"]);
|
||||||
}
|
}
|
||||||
@@ -3427,9 +3701,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn word_split_custom_ifs() {
|
fn word_split_custom_ifs() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
unsafe { std::env::set_var("IFS", ":"); }
|
unsafe {
|
||||||
|
std::env::set_var("IFS", ":");
|
||||||
|
}
|
||||||
|
|
||||||
let mut exp = Expander { raw: "a:b:c".to_string() };
|
let mut exp = Expander {
|
||||||
|
raw: "a:b:c".to_string(),
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
let words = exp.split_words();
|
let words = exp.split_words();
|
||||||
assert_eq!(words, vec!["a", "b", "c"]);
|
assert_eq!(words, vec!["a", "b", "c"]);
|
||||||
}
|
}
|
||||||
@@ -3437,9 +3716,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn word_split_empty_ifs() {
|
fn word_split_empty_ifs() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
unsafe { std::env::set_var("IFS", ""); }
|
unsafe {
|
||||||
|
std::env::set_var("IFS", "");
|
||||||
|
}
|
||||||
|
|
||||||
let mut exp = Expander { raw: "hello world".to_string() };
|
let mut exp = Expander {
|
||||||
|
raw: "hello world".to_string(),
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
let words = exp.split_words();
|
let words = exp.split_words();
|
||||||
assert_eq!(words, vec!["hello world"]);
|
assert_eq!(words, vec!["hello world"]);
|
||||||
}
|
}
|
||||||
@@ -3449,11 +3733,82 @@ mod tests {
|
|||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
|
|
||||||
let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE);
|
let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE);
|
||||||
let mut exp = Expander { raw };
|
let mut exp = Expander {
|
||||||
|
raw,
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
let words = exp.split_words();
|
let words = exp.split_words();
|
||||||
assert_eq!(words, vec!["hello world"]);
|
assert_eq!(words, vec!["hello world"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== Escaped Word Splitting =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_split_escaped_space() {
|
||||||
|
let _guard = TestGuard::new();
|
||||||
|
|
||||||
|
let raw = format!("hello{}world", unescape_str("\\ "));
|
||||||
|
let mut exp = Expander {
|
||||||
|
raw,
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
|
let words = exp.split_words();
|
||||||
|
assert_eq!(words, vec!["hello world"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_split_escaped_tab() {
|
||||||
|
let _guard = TestGuard::new();
|
||||||
|
|
||||||
|
let raw = format!("hello{}world", unescape_str("\\\t"));
|
||||||
|
let mut exp = Expander {
|
||||||
|
raw,
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
|
let words = exp.split_words();
|
||||||
|
assert_eq!(words, vec!["hello\tworld"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_split_escaped_custom_ifs() {
|
||||||
|
let _guard = TestGuard::new();
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("IFS", ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = format!("a{}b:c", unescape_str("\\:"));
|
||||||
|
let mut exp = Expander {
|
||||||
|
raw,
|
||||||
|
flags: TkFlags::empty(),
|
||||||
|
};
|
||||||
|
let words = exp.split_words();
|
||||||
|
assert_eq!(words, vec!["a:b", "c"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Parameter Expansion with Escapes (TestGuard) =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_exp_prefix_removal_escaped() {
|
||||||
|
let guard = TestGuard::new();
|
||||||
|
write_vars(|v| v.set_var("branch", VarKind::Str("## main".into()), VarFlags::NONE)).unwrap();
|
||||||
|
|
||||||
|
test_input("echo \"${branch#\\#\\# }\"").unwrap();
|
||||||
|
|
||||||
|
let out = guard.read_output();
|
||||||
|
assert_eq!(out, "main\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn param_exp_suffix_removal_escaped() {
|
||||||
|
let guard = TestGuard::new();
|
||||||
|
write_vars(|v| v.set_var("val", VarKind::Str("hello world!!".into()), VarFlags::NONE)).unwrap();
|
||||||
|
|
||||||
|
test_input("echo \"${val%\\!\\!}\"").unwrap();
|
||||||
|
|
||||||
|
let out = guard.read_output();
|
||||||
|
assert_eq!(out, "hello world\n");
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== Arithmetic with Variables (TestGuard) =====================
|
// ===================== Arithmetic with Variables (TestGuard) =====================
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3478,8 +3833,13 @@ mod tests {
|
|||||||
fn array_index_first() {
|
fn array_index_first() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE)
|
v.set_var(
|
||||||
}).unwrap();
|
"arr",
|
||||||
|
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(0))).unwrap();
|
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(0))).unwrap();
|
||||||
assert_eq!(val, "a");
|
assert_eq!(val, "a");
|
||||||
@@ -3489,8 +3849,13 @@ mod tests {
|
|||||||
fn array_index_second() {
|
fn array_index_second() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("arr", VarKind::arr_from_vec(vec!["x".into(), "y".into(), "z".into()]), VarFlags::NONE)
|
v.set_var(
|
||||||
}).unwrap();
|
"arr",
|
||||||
|
VarKind::arr_from_vec(vec!["x".into(), "y".into(), "z".into()]),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(1))).unwrap();
|
let val = read_vars(|v| v.index_var("arr", ArrIndex::Literal(1))).unwrap();
|
||||||
assert_eq!(val, "y");
|
assert_eq!(val, "y");
|
||||||
@@ -3500,8 +3865,13 @@ mod tests {
|
|||||||
fn array_all_elems() {
|
fn array_all_elems() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE)
|
v.set_var(
|
||||||
}).unwrap();
|
"arr",
|
||||||
|
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
|
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
|
||||||
assert_eq!(elems, vec!["a", "b", "c"]);
|
assert_eq!(elems, vec!["a", "b", "c"]);
|
||||||
@@ -3511,8 +3881,13 @@ mod tests {
|
|||||||
fn array_elem_count() {
|
fn array_elem_count() {
|
||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("arr", VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]), VarFlags::NONE)
|
v.set_var(
|
||||||
}).unwrap();
|
"arr",
|
||||||
|
VarKind::arr_from_vec(vec!["a".into(), "b".into(), "c".into()]),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
|
let elems = read_vars(|v| v.get_arr_elems("arr")).unwrap();
|
||||||
assert_eq!(elems.len(), 3);
|
assert_eq!(elems.len(), 3);
|
||||||
@@ -3525,7 +3900,9 @@ mod tests {
|
|||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
let dummy_span = Span::default();
|
let dummy_span = Span::default();
|
||||||
crate::state::SHED.with(|s| {
|
crate::state::SHED.with(|s| {
|
||||||
s.logic.borrow_mut().insert_alias("ll", "ls -la", dummy_span.clone());
|
s.logic
|
||||||
|
.borrow_mut()
|
||||||
|
.insert_alias("ll", "ls -la", dummy_span.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
|
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
|
||||||
@@ -3538,7 +3915,9 @@ mod tests {
|
|||||||
let _guard = TestGuard::new();
|
let _guard = TestGuard::new();
|
||||||
let dummy_span = Span::default();
|
let dummy_span = Span::default();
|
||||||
crate::state::SHED.with(|s| {
|
crate::state::SHED.with(|s| {
|
||||||
s.logic.borrow_mut().insert_alias("foo", "foo --verbose", dummy_span.clone());
|
s.logic
|
||||||
|
.borrow_mut()
|
||||||
|
.insert_alias("foo", "foo --verbose", dummy_span.clone());
|
||||||
});
|
});
|
||||||
|
|
||||||
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
|
let log_tab = crate::state::SHED.with(|s| s.logic.borrow().clone());
|
||||||
@@ -3553,7 +3932,14 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn index_simple() {
|
fn index_simple() {
|
||||||
let guard = TestGuard::new();
|
let guard = TestGuard::new();
|
||||||
write_vars(|v| v.set_var("arr", VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])), VarFlags::NONE)).unwrap();
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"arr",
|
||||||
|
VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_input("echo $arr").unwrap();
|
test_input("echo $arr").unwrap();
|
||||||
|
|
||||||
@@ -3564,8 +3950,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn index_cursed() {
|
fn index_cursed() {
|
||||||
let guard = TestGuard::new();
|
let guard = TestGuard::new();
|
||||||
write_vars(|v| v.set_var("arr", VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])), VarFlags::NONE)).unwrap();
|
write_vars(|v| {
|
||||||
write_vars(|v| v.set_var("i", VarKind::Arr(VecDeque::from(["0".into(), "1".into(), "2".into()])), VarFlags::NONE)).unwrap();
|
v.set_var(
|
||||||
|
"arr",
|
||||||
|
VarKind::Arr(VecDeque::from(["foo".into(), "bar".into(), "biz".into()])),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"i",
|
||||||
|
VarKind::Arr(VecDeque::from(["0".into(), "1".into(), "2".into()])),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
test_input("echo $echo ${var:-${arr[$(($(echo ${i[0]}) + 1))]}}").unwrap();
|
test_input("echo $echo ${var:-${arr[$(($(echo ${i[0]}) + 1))]}}").unwrap();
|
||||||
|
|
||||||
|
|||||||
131
src/getopt.rs
131
src/getopt.rs
@@ -3,7 +3,11 @@ use std::sync::Arc;
|
|||||||
use ariadne::Fmt;
|
use ariadne::Fmt;
|
||||||
use fmt::Display;
|
use fmt::Display;
|
||||||
|
|
||||||
use crate::{libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::lex::Tk, prelude::*};
|
use crate::{
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
|
||||||
|
parse::lex::Tk,
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
pub type OptSet = Arc<[Opt]>;
|
pub type OptSet = Arc<[Opt]>;
|
||||||
|
|
||||||
@@ -82,17 +86,23 @@ pub fn get_opts_from_tokens(
|
|||||||
sort_tks(tokens, opt_specs, false)
|
sort_tks(tokens, opt_specs, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
pub fn sort_tks(
|
||||||
|
tokens: Vec<Tk>,
|
||||||
|
opt_specs: &[OptSpec],
|
||||||
|
strict: bool,
|
||||||
|
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||||
let mut tokens_iter = tokens
|
let mut tokens_iter = tokens
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| t.expand())
|
.map(|t| t.expand())
|
||||||
.collect::<ShResult<Vec<_>>>()?
|
.collect::<ShResult<Vec<_>>>()?
|
||||||
.into_iter();
|
.into_iter()
|
||||||
|
.peekable();
|
||||||
let mut opts = vec![];
|
let mut opts = vec![];
|
||||||
let mut non_opts = vec![];
|
let mut non_opts = vec![];
|
||||||
|
|
||||||
while let Some(token) = tokens_iter.next() {
|
while let Some(token) = tokens_iter.next() {
|
||||||
if &token.to_string() == "--" {
|
if &token.to_string() == "--" {
|
||||||
|
non_opts.push(token);
|
||||||
non_opts.extend(tokens_iter);
|
non_opts.extend(tokens_iter);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -140,12 +150,11 @@ pub fn sort_tks(tokens: Vec<Tk>, opt_specs: &[OptSpec], strict: bool) -> ShResul
|
|||||||
Ok((non_opts, opts))
|
Ok((non_opts, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::parse::lex::{LexFlags, LexStream};
|
use crate::parse::lex::{LexFlags, LexStream};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_short_single() {
|
fn parse_short_single() {
|
||||||
@@ -156,7 +165,10 @@ use super::*;
|
|||||||
#[test]
|
#[test]
|
||||||
fn parse_short_combined() {
|
fn parse_short_combined() {
|
||||||
let opts = Opt::parse("-abc");
|
let opts = Opt::parse("-abc");
|
||||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
assert_eq!(
|
||||||
|
opts,
|
||||||
|
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -173,7 +185,12 @@ use super::*;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_opts_basic() {
|
fn get_opts_basic() {
|
||||||
let words = vec!["file.txt".into(), "-v".into(), "--help".into(), "arg".into()];
|
let words = vec![
|
||||||
|
"file.txt".into(),
|
||||||
|
"-v".into(),
|
||||||
|
"--help".into(),
|
||||||
|
"arg".into(),
|
||||||
|
];
|
||||||
let (non_opts, opts) = get_opts(words);
|
let (non_opts, opts) = get_opts(words);
|
||||||
assert_eq!(non_opts, vec!["file.txt", "arg"]);
|
assert_eq!(non_opts, vec!["file.txt", "arg"]);
|
||||||
assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]);
|
assert_eq!(opts, vec![Opt::Short('v'), Opt::Long("help".into())]);
|
||||||
@@ -191,7 +208,10 @@ use super::*;
|
|||||||
fn get_opts_combined_short() {
|
fn get_opts_combined_short() {
|
||||||
let words = vec!["-abc".into(), "file".into()];
|
let words = vec!["-abc".into(), "file".into()];
|
||||||
let (non_opts, opts) = get_opts(words);
|
let (non_opts, opts) = get_opts(words);
|
||||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
assert_eq!(
|
||||||
|
opts,
|
||||||
|
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
|
||||||
|
);
|
||||||
assert_eq!(non_opts, vec!["file"]);
|
assert_eq!(non_opts, vec!["file"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +235,10 @@ use super::*;
|
|||||||
assert_eq!(Opt::Short('v').to_string(), "-v");
|
assert_eq!(Opt::Short('v').to_string(), "-v");
|
||||||
assert_eq!(Opt::Long("help".into()).to_string(), "--help");
|
assert_eq!(Opt::Long("help".into()).to_string(), "--help");
|
||||||
assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file");
|
assert_eq!(Opt::ShortWithArg('o', "file".into()).to_string(), "-o file");
|
||||||
assert_eq!(Opt::LongWithArg("output".into(), "file".into()).to_string(), "--output file");
|
assert_eq!(
|
||||||
|
Opt::LongWithArg("output".into(), "file".into()).to_string(),
|
||||||
|
"--output file"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lex(input: &str) -> Vec<Tk> {
|
fn lex(input: &str) -> Vec<Tk> {
|
||||||
@@ -229,8 +252,14 @@ use super::*;
|
|||||||
let tokens = lex("file.txt --help -v arg");
|
let tokens = lex("file.txt --help -v arg");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![
|
||||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Long("help".into()), takes_arg: false },
|
opt: Opt::Short('v'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Long("help".into()),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
@@ -248,9 +277,10 @@ use super::*;
|
|||||||
fn tks_short_with_arg() {
|
fn tks_short_with_arg() {
|
||||||
let tokens = lex("-o output.txt file.txt");
|
let tokens = lex("-o output.txt file.txt");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![OptSpec {
|
||||||
OptSpec { opt: Opt::Short('o'), takes_arg: true },
|
opt: Opt::Short('o'),
|
||||||
];
|
takes_arg: true,
|
||||||
|
}];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
|
|
||||||
@@ -263,13 +293,17 @@ use super::*;
|
|||||||
fn tks_long_with_arg() {
|
fn tks_long_with_arg() {
|
||||||
let tokens = lex("--output result.txt input.txt");
|
let tokens = lex("--output result.txt input.txt");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![OptSpec {
|
||||||
OptSpec { opt: Opt::Long("output".into()), takes_arg: true },
|
opt: Opt::Long("output".into()),
|
||||||
];
|
takes_arg: true,
|
||||||
|
}];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
|
|
||||||
assert_eq!(opts, vec![Opt::LongWithArg("output".into(), "result.txt".into())]);
|
assert_eq!(
|
||||||
|
opts,
|
||||||
|
vec![Opt::LongWithArg("output".into(), "result.txt".into())]
|
||||||
|
);
|
||||||
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
||||||
assert!(non_opts.contains(&"input.txt".to_string()));
|
assert!(non_opts.contains(&"input.txt".to_string()));
|
||||||
}
|
}
|
||||||
@@ -279,8 +313,14 @@ use super::*;
|
|||||||
let tokens = lex("-v -- -a --foo");
|
let tokens = lex("-v -- -a --foo");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![
|
||||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('a'), takes_arg: false },
|
opt: Opt::Short('v'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('a'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
@@ -296,29 +336,47 @@ use super::*;
|
|||||||
let tokens = lex("-abc");
|
let tokens = lex("-abc");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![
|
||||||
OptSpec { opt: Opt::Short('a'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('b'), takes_arg: false },
|
opt: Opt::Short('a'),
|
||||||
OptSpec { opt: Opt::Short('c'), takes_arg: false },
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('b'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('c'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (_non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
|
|
||||||
assert_eq!(opts, vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]);
|
assert_eq!(
|
||||||
|
opts,
|
||||||
|
vec![Opt::Short('a'), Opt::Short('b'), Opt::Short('c')]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tks_unknown_opt_becomes_non_opt() {
|
fn tks_unknown_opt_becomes_non_opt() {
|
||||||
let tokens = lex("-v -x file");
|
let tokens = lex("-v -x file");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![OptSpec {
|
||||||
OptSpec { opt: Opt::Short('v'), takes_arg: false },
|
opt: Opt::Short('v'),
|
||||||
];
|
takes_arg: false,
|
||||||
|
}];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
|
|
||||||
assert_eq!(opts, vec![Opt::Short('v')]);
|
assert_eq!(opts, vec![Opt::Short('v')]);
|
||||||
// -x is not in spec, so its token goes to non_opts
|
// -x is not in spec, so its token goes to non_opts
|
||||||
assert!(non_opts.into_iter().map(|s| s.to_string()).any(|s| s == "-x" || s == "file"));
|
assert!(
|
||||||
|
non_opts
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.any(|s| s == "-x" || s == "file")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -326,16 +384,25 @@ use super::*;
|
|||||||
let tokens = lex("-n 5 --output file.txt input");
|
let tokens = lex("-n 5 --output file.txt input");
|
||||||
|
|
||||||
let opt_spec = vec![
|
let opt_spec = vec![
|
||||||
OptSpec { opt: Opt::Short('n'), takes_arg: true },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Long("output".into()), takes_arg: true },
|
opt: Opt::Short('n'),
|
||||||
|
takes_arg: true,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Long("output".into()),
|
||||||
|
takes_arg: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
let (non_opts, opts) = get_opts_from_tokens(tokens, &opt_spec).unwrap();
|
||||||
|
|
||||||
assert_eq!(opts, vec![
|
assert_eq!(
|
||||||
|
opts,
|
||||||
|
vec![
|
||||||
Opt::ShortWithArg('n', "5".into()),
|
Opt::ShortWithArg('n', "5".into()),
|
||||||
Opt::LongWithArg("output".into(), "file.txt".into()),
|
Opt::LongWithArg("output".into(), "file.txt".into()),
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
let non_opts: Vec<String> = non_opts.into_iter().map(|s| s.to_string()).collect();
|
||||||
assert!(non_opts.contains(&"input".to_string()));
|
assert!(non_opts.contains(&"input".to_string()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use ariadne::{Color, Fmt};
|
use ariadne::{Color, Fmt};
|
||||||
use ariadne::{Report, ReportKind};
|
use ariadne::{Report, ReportKind};
|
||||||
use rand::TryRng;
|
use rand::TryRng;
|
||||||
use yansi::Paint;
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use yansi::Paint;
|
||||||
|
|
||||||
use crate::procio::RedirGuard;
|
use crate::procio::RedirGuard;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -201,6 +201,7 @@ impl ShErr {
|
|||||||
pub fn is_flow_control(&self) -> bool {
|
pub fn is_flow_control(&self) -> bool {
|
||||||
self.kind.is_flow_control()
|
self.kind.is_flow_control()
|
||||||
}
|
}
|
||||||
|
/// Promotes a shell error from a simple error to an error that blames a span
|
||||||
pub fn promote(mut self, span: Span) -> Self {
|
pub fn promote(mut self, span: Span) -> Self {
|
||||||
if self.notes.is_empty() {
|
if self.notes.is_empty() {
|
||||||
return self;
|
return self;
|
||||||
@@ -208,6 +209,8 @@ impl ShErr {
|
|||||||
let first = self.notes[0].clone();
|
let first = self.notes[0].clone();
|
||||||
if self.notes.len() > 1 {
|
if self.notes.len() > 1 {
|
||||||
self.notes = self.notes[1..].to_vec();
|
self.notes = self.notes[1..].to_vec();
|
||||||
|
} else {
|
||||||
|
self.notes = vec![];
|
||||||
}
|
}
|
||||||
|
|
||||||
self.labeled(span, first)
|
self.labeled(span, first)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::collections::HashSet;
|
|||||||
use std::os::fd::{BorrowedFd, RawFd};
|
use std::os::fd::{BorrowedFd, RawFd};
|
||||||
|
|
||||||
use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr};
|
use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr};
|
||||||
use nix::unistd::isatty;
|
use nix::unistd::{isatty, write};
|
||||||
use scopeguard::guard;
|
use scopeguard::guard;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
@@ -147,11 +147,10 @@ impl RawModeGuard {
|
|||||||
let orig = ORIG_TERMIOS
|
let orig = ORIG_TERMIOS
|
||||||
.with(|cell| cell.borrow().clone())
|
.with(|cell| cell.borrow().clone())
|
||||||
.expect("with_cooked_mode called before raw_mode()");
|
.expect("with_cooked_mode called before raw_mode()");
|
||||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig)
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
|
||||||
.expect("Failed to restore cooked mode");
|
|
||||||
let res = f();
|
let res = f();
|
||||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t)
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t).ok();
|
||||||
.expect("Failed to restore raw mode");
|
unsafe { write(BorrowedFd::borrow_raw(*TTY_FILENO), b"\x1b[?1l\x1b>").ok() };
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,11 +158,12 @@ impl RawModeGuard {
|
|||||||
impl Drop for RawModeGuard {
|
impl Drop for RawModeGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let _ = termios::tcsetattr(
|
termios::tcsetattr(
|
||||||
BorrowedFd::borrow_raw(self.fd),
|
BorrowedFd::borrow_raw(self.fd),
|
||||||
termios::SetArg::TCSANOW,
|
termios::SetArg::TCSANOW,
|
||||||
&self.orig,
|
&self.orig,
|
||||||
);
|
)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ use std::sync::LazyLock;
|
|||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
/// Minimum fd number for shell-internal file descriptors.
|
||||||
|
const MIN_INTERNAL_FD: RawFd = 10;
|
||||||
|
|
||||||
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
|
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
|
||||||
open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty")
|
let fd = open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty");
|
||||||
|
// Move the tty fd above the user-accessible range so that
|
||||||
|
// `exec 3>&-` and friends don't collide with shell internals.
|
||||||
|
let high =
|
||||||
|
fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).expect("Failed to dup /dev/tty high");
|
||||||
|
close(fd).ok();
|
||||||
|
high
|
||||||
});
|
});
|
||||||
|
|||||||
58
src/main.rs
58
src/main.rs
@@ -39,9 +39,11 @@ use crate::procio::borrow_fd;
|
|||||||
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
|
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
|
||||||
use crate::readline::{Prompt, ReadlineEvent, ShedVi};
|
use crate::readline::{Prompt, ReadlineEvent, ShedVi};
|
||||||
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
|
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
|
||||||
use crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta};
|
use crate::state::{
|
||||||
|
AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts,
|
||||||
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use state::{read_vars, write_vars};
|
use state::write_vars;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct ShedArgs {
|
struct ShedArgs {
|
||||||
@@ -63,20 +65,6 @@ struct ShedArgs {
|
|||||||
login_shell: bool,
|
login_shell: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force evaluation of lazily-initialized values early in shell startup.
|
|
||||||
///
|
|
||||||
/// In particular, this ensures that the variable table is initialized, which
|
|
||||||
/// populates environment variables from the system. If this initialization is
|
|
||||||
/// deferred too long, features like prompt expansion may fail due to missing
|
|
||||||
/// environment variables.
|
|
||||||
///
|
|
||||||
/// This function triggers initialization by calling `read_vars` with a no-op
|
|
||||||
/// closure, which forces access to the variable table and causes its `LazyLock`
|
|
||||||
/// constructor to run.
|
|
||||||
fn kickstart_lazy_evals() {
|
|
||||||
read_vars(|_| {});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We need to make sure that even if we panic, our child processes get sighup
|
/// We need to make sure that even if we panic, our child processes get sighup
|
||||||
fn setup_panic_handler() {
|
fn setup_panic_handler() {
|
||||||
let default_panic_hook = std::panic::take_hook();
|
let default_panic_hook = std::panic::take_hook();
|
||||||
@@ -111,7 +99,6 @@ fn setup_panic_handler() {
|
|||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
yansi::enable();
|
yansi::enable();
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
kickstart_lazy_evals();
|
|
||||||
setup_panic_handler();
|
setup_panic_handler();
|
||||||
|
|
||||||
let mut args = ShedArgs::parse();
|
let mut args = ShedArgs::parse();
|
||||||
@@ -130,6 +117,16 @@ 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 {
|
||||||
@@ -255,14 +252,38 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
|
|||||||
PollFlags::POLLIN,
|
PollFlags::POLLIN,
|
||||||
)];
|
)];
|
||||||
|
|
||||||
|
let mut exec_if_timeout = None;
|
||||||
|
|
||||||
let timeout = if readline.pending_keymap.is_empty() {
|
let timeout = if readline.pending_keymap.is_empty() {
|
||||||
|
let screensaver_cmd = read_shopts(|o| o.prompt.screensaver_cmd.clone());
|
||||||
|
let screensaver_idle_time = read_shopts(|o| o.prompt.screensaver_idle_time);
|
||||||
|
if screensaver_idle_time > 0 && !screensaver_cmd.is_empty() {
|
||||||
|
exec_if_timeout = Some(screensaver_cmd);
|
||||||
|
PollTimeout::from((screensaver_idle_time * 1000) as u16)
|
||||||
|
} else {
|
||||||
PollTimeout::MAX
|
PollTimeout::MAX
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
PollTimeout::from(1000u16)
|
PollTimeout::from(1000u16)
|
||||||
};
|
};
|
||||||
|
|
||||||
match poll(&mut fds, timeout) {
|
match poll(&mut fds, timeout) {
|
||||||
Ok(_) => {}
|
Ok(0) => {
|
||||||
|
// We timed out.
|
||||||
|
if let Some(cmd) = exec_if_timeout {
|
||||||
|
let prepared = ReadlineEvent::Line(cmd);
|
||||||
|
let saved_hist_opt = read_shopts(|o| o.core.auto_hist);
|
||||||
|
let _guard = scopeguard::guard(saved_hist_opt, |opt| {
|
||||||
|
write_shopts(|o| o.core.auto_hist = opt);
|
||||||
|
});
|
||||||
|
write_shopts(|o| o.core.auto_hist = false); // don't save screensaver command to history
|
||||||
|
|
||||||
|
match handle_readline_event(&mut readline, Ok(prepared))? {
|
||||||
|
true => return Ok(()),
|
||||||
|
false => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(Errno::EINTR) => {
|
Err(Errno::EINTR) => {
|
||||||
// Interrupted by signal, loop back to handle it
|
// Interrupted by signal, loop back to handle it
|
||||||
continue;
|
continue;
|
||||||
@@ -271,6 +292,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
|
|||||||
eprintln!("poll error: {e}");
|
eprintln!("poll error: {e}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeout — resolve pending keymap ambiguity
|
// Timeout — resolve pending keymap ambiguity
|
||||||
|
|||||||
@@ -8,7 +8,29 @@ use ariadne::Fmt;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}
|
alias::{alias, unalias},
|
||||||
|
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
|
||||||
|
autocmd::autocmd,
|
||||||
|
cd::cd,
|
||||||
|
complete::{compgen_builtin, complete_builtin},
|
||||||
|
dirstack::{dirs, popd, pushd},
|
||||||
|
echo::echo,
|
||||||
|
eval, exec,
|
||||||
|
flowctl::flowctl,
|
||||||
|
getopts::getopts,
|
||||||
|
intro,
|
||||||
|
jobctl::{self, JobBehavior, continue_job, disown, jobs},
|
||||||
|
keymap, map,
|
||||||
|
pwd::pwd,
|
||||||
|
read::{self, read_builtin},
|
||||||
|
resource::{ulimit, umask_builtin},
|
||||||
|
seek::seek,
|
||||||
|
shift::shift,
|
||||||
|
shopt::shopt,
|
||||||
|
source::source,
|
||||||
|
test::double_bracket_test,
|
||||||
|
trap::{TrapTarget, trap},
|
||||||
|
varcmds::{export, local, readonly, unset},
|
||||||
},
|
},
|
||||||
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
|
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
|
||||||
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
|
||||||
@@ -136,10 +158,15 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
|
|||||||
if nodes.len() == 1 {
|
if nodes.len() == 1 {
|
||||||
let is_single_cmd = match &nodes[0].class {
|
let is_single_cmd = match &nodes[0].class {
|
||||||
NdRule::Command { .. } => true,
|
NdRule::Command { .. } => true,
|
||||||
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }),
|
NdRule::Pipeline { cmds } => {
|
||||||
|
cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
|
||||||
|
}
|
||||||
NdRule::Conjunction { elements } => {
|
NdRule::Conjunction { elements } => {
|
||||||
elements.len() == 1 && match &elements[0].cmd.class {
|
elements.len() == 1
|
||||||
NdRule::Pipeline { cmds } => cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. }),
|
&& match &elements[0].cmd.class {
|
||||||
|
NdRule::Pipeline { cmds } => {
|
||||||
|
cmds.len() == 1 && matches!(cmds[0].class, NdRule::Command { .. })
|
||||||
|
}
|
||||||
NdRule::Command { .. } => true,
|
NdRule::Command { .. } => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -151,8 +178,12 @@ pub fn exec_dash_c(input: String) -> ShResult<()> {
|
|||||||
let mut node = nodes.remove(0);
|
let mut node = nodes.remove(0);
|
||||||
loop {
|
loop {
|
||||||
match node.class {
|
match node.class {
|
||||||
NdRule::Conjunction { mut elements } => { node = *elements.remove(0).cmd; }
|
NdRule::Conjunction { mut elements } => {
|
||||||
NdRule::Pipeline { mut cmds } => { node = cmds.remove(0); }
|
node = *elements.remove(0).cmd;
|
||||||
|
}
|
||||||
|
NdRule::Pipeline { mut cmds } => {
|
||||||
|
node = cmds.remove(0);
|
||||||
|
}
|
||||||
NdRule::Command { .. } => break,
|
NdRule::Command { .. } => break,
|
||||||
_ => break,
|
_ => break,
|
||||||
}
|
}
|
||||||
@@ -250,6 +281,7 @@ impl Dispatcher {
|
|||||||
NdRule::CaseNode { .. } => self.exec_case(node)?,
|
NdRule::CaseNode { .. } => self.exec_case(node)?,
|
||||||
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
|
NdRule::BraceGrp { .. } => self.exec_brc_grp(node)?,
|
||||||
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
|
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
|
||||||
|
NdRule::Negate { .. } => self.exec_negated(node)?,
|
||||||
NdRule::Command { .. } => self.dispatch_cmd(node)?,
|
NdRule::Command { .. } => self.dispatch_cmd(node)?,
|
||||||
NdRule::Test { .. } => self.exec_test(node)?,
|
NdRule::Test { .. } => self.exec_test(node)?,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -257,6 +289,15 @@ 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
|
||||||
};
|
};
|
||||||
@@ -284,30 +325,35 @@ impl Dispatcher {
|
|||||||
self.exec_cmd(node)
|
self.exec_cmd(node)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn exec_negated(&mut self, node: Node) -> ShResult<()> {
|
||||||
|
let NdRule::Negate { cmd } = node.class else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
self.dispatch_node(*cmd)?;
|
||||||
|
let status = state::get_status();
|
||||||
|
state::set_status(if status == 0 { 1 } else { 0 });
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> {
|
pub fn exec_conjunction(&mut self, conjunction: Node) -> ShResult<()> {
|
||||||
let NdRule::Conjunction { elements } = conjunction.class else {
|
let NdRule::Conjunction { elements } = conjunction.class else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut elem_iter = elements.into_iter();
|
let mut elem_iter = elements.into_iter();
|
||||||
|
let mut skip = false;
|
||||||
while let Some(element) = elem_iter.next() {
|
while let Some(element) = elem_iter.next() {
|
||||||
let ConjunctNode { cmd, operator } = element;
|
let ConjunctNode { cmd, operator } = element;
|
||||||
|
if !skip {
|
||||||
self.dispatch_node(*cmd)?;
|
self.dispatch_node(*cmd)?;
|
||||||
|
}
|
||||||
|
|
||||||
let status = state::get_status();
|
let status = state::get_status();
|
||||||
match operator {
|
skip = match operator {
|
||||||
ConjunctOp::And => {
|
ConjunctOp::And => status != 0,
|
||||||
if status != 0 {
|
ConjunctOp::Or => status == 0,
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConjunctOp::Or => {
|
|
||||||
if status == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ConjunctOp::Null => break,
|
ConjunctOp::Null => break,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -327,7 +373,11 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
let body_span = body.get_span();
|
let body_span = body.get_span();
|
||||||
let body = body_span.as_str().to_string();
|
let body = body_span.as_str().to_string();
|
||||||
let name = name.span.as_str().strip_suffix("()").unwrap();
|
let name = name
|
||||||
|
.span
|
||||||
|
.as_str()
|
||||||
|
.strip_suffix("()")
|
||||||
|
.unwrap_or(name.span.as_str());
|
||||||
|
|
||||||
if KEYWORDS.contains(&name) {
|
if KEYWORDS.contains(&name) {
|
||||||
return Err(ShErr::at(
|
return Err(ShErr::at(
|
||||||
@@ -578,6 +628,7 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
state::set_status(0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -714,10 +765,14 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !matched && !else_block.is_empty() {
|
if !matched {
|
||||||
|
if !else_block.is_empty() {
|
||||||
for node in else_block {
|
for node in else_block {
|
||||||
s.dispatch_node(node)?;
|
s.dispatch_node(node)?;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
state::set_status(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -749,7 +804,18 @@ impl Dispatcher {
|
|||||||
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 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
|
||||||
@@ -822,7 +888,10 @@ impl Dispatcher {
|
|||||||
|
|
||||||
if fork_builtins {
|
if fork_builtins {
|
||||||
log::trace!("Forking builtin: {}", cmd_raw);
|
log::trace!("Forking builtin: {}", cmd_raw);
|
||||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
let guard = self.io_stack.pop_frame().redirect()?;
|
||||||
|
if cmd_raw.as_str() == "exec" {
|
||||||
|
guard.persist();
|
||||||
|
}
|
||||||
self.run_fork(&cmd_raw, |s| {
|
self.run_fork(&cmd_raw, |s| {
|
||||||
if let Err(e) = s.dispatch_builtin(cmd) {
|
if let Err(e) = s.dispatch_builtin(cmd) {
|
||||||
e.print_error();
|
e.print_error();
|
||||||
@@ -947,6 +1016,7 @@ impl Dispatcher {
|
|||||||
"autocmd" => autocmd(cmd),
|
"autocmd" => autocmd(cmd),
|
||||||
"ulimit" => ulimit(cmd),
|
"ulimit" => ulimit(cmd),
|
||||||
"umask" => umask_builtin(cmd),
|
"umask" => umask_builtin(cmd),
|
||||||
|
"seek" => seek(cmd),
|
||||||
"true" | ":" => {
|
"true" | ":" => {
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1084,6 +1154,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())
|
||||||
}
|
}
|
||||||
@@ -1195,3 +1266,202 @@ pub fn is_func(tk: Option<Tk>) -> bool {
|
|||||||
pub fn is_subsh(tk: Option<Tk>) -> bool {
|
pub fn is_subsh(tk: Option<Tk>) -> bool {
|
||||||
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
|
tk.is_some_and(|tk| tk.flags.contains(TkFlags::IS_SUBSH))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::state;
|
||||||
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
|
||||||
|
// ===================== while/until status =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_loop_status_zero_after_completion() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("while false; do :; done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_loop_status_zero_after_iterations() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("X=0; while [[ $X -lt 3 ]]; do X=$((X+1)); done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn until_loop_status_zero_after_completion() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("until true; do :; done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn until_loop_status_zero_after_iterations() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("X=3; until [[ $X -le 0 ]]; do X=$((X-1)); done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_break_preserves_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("while true; do break; done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn while_body_status_propagates() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("X=0; while [[ $X -lt 1 ]]; do X=$((X+1)); false; done").unwrap();
|
||||||
|
// Loop body ended with `false` (status 1), but the loop itself
|
||||||
|
// completed normally when the condition failed, so status should be 0
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== if/elif/else status =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_true_body_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("if true; then echo ok; fi").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_false_no_else_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("if false; then echo ok; fi").unwrap();
|
||||||
|
// No branch taken, POSIX says status is 0
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_else_branch_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("if false; then true; else false; fi").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== for loop status =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_empty_list_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("for x in; do echo $x; done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_body_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("for x in a b c; do true; done").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== case status =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn case_match_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("case foo in foo) true;; esac").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn case_no_match_status() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("case foo in bar) true;; esac").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== other stuff =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_var_zip() {
|
||||||
|
let g = TestGuard::new();
|
||||||
|
test_input("for a b in 1 2 3 4 5 6; do echo $a $b; done").unwrap();
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "1 2\n3 4\n5 6\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn for_loop_unsets_zipped() {
|
||||||
|
let g = TestGuard::new();
|
||||||
|
test_input("for a b c d in 1 2 3 4 5 6; do echo $a $b $c $d; done").unwrap();
|
||||||
|
let out = g.read_output();
|
||||||
|
assert_eq!(out, "1 2 3 4\n5 6\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== negation (!) status =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_true() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("! true").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_false() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("! false").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_negate_true() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("! ! true").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_negate_false() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
test_input("! ! false").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_pipeline_last_cmd() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// pipeline status = last cmd (false) = 1, negated → 0
|
||||||
|
test_input("! true | false").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_pipeline_last_cmd_true() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// pipeline status = last cmd (true) = 0, negated → 1
|
||||||
|
test_input("! false | true").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_in_conjunction() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// ! binds to pipeline, not conjunction: (! (true && false)) && true
|
||||||
|
test_input("! (true && false) && true").unwrap();
|
||||||
|
assert_eq!(state::get_status(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negate_in_if_condition() {
|
||||||
|
let g = TestGuard::new();
|
||||||
|
test_input("if ! false; then echo yes; fi").unwrap();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
330
src/parse/lex.rs
330
src/parse/lex.rs
@@ -17,9 +17,9 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const KEYWORDS: [&str; 16] = [
|
pub const KEYWORDS: [&str; 17] = [
|
||||||
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
|
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
|
||||||
"case", "esac", "[[", "]]",
|
"case", "esac", "[[", "]]", "!",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
|
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
|
||||||
@@ -166,6 +166,7 @@ pub enum TkRule {
|
|||||||
ErrPipe,
|
ErrPipe,
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
|
Bang,
|
||||||
Bg,
|
Bg,
|
||||||
Sep,
|
Sep,
|
||||||
Redir,
|
Redir,
|
||||||
@@ -216,6 +217,31 @@ impl Tk {
|
|||||||
};
|
};
|
||||||
self.span.as_str().trim() == ";;"
|
self.span.as_str().trim() == ";;"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_opener(&self) -> bool {
|
||||||
|
OPENERS.contains(&self.as_str())
|
||||||
|
|| matches!(self.class, TkRule::BraceGrpStart)
|
||||||
|
|| matches!(self.class, TkRule::CasePattern)
|
||||||
|
}
|
||||||
|
pub fn is_closer(&self) -> bool {
|
||||||
|
matches!(self.as_str(), "fi" | "done" | "esac")
|
||||||
|
|| self.has_double_semi()
|
||||||
|
|| matches!(self.class, TkRule::BraceGrpEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_closer_for(&self, other: &Tk) -> bool {
|
||||||
|
if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd))
|
||||||
|
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match other.as_str() {
|
||||||
|
"for" | "while" | "until" => matches!(self.as_str(), "done"),
|
||||||
|
"if" => matches!(self.as_str(), "fi"),
|
||||||
|
"case" => matches!(self.as_str(), "esac"),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Tk {
|
impl Display for Tk {
|
||||||
@@ -240,19 +266,12 @@ bitflags! {
|
|||||||
const ASSIGN = 0b0000000001000000;
|
const ASSIGN = 0b0000000001000000;
|
||||||
const BUILTIN = 0b0000000010000000;
|
const BUILTIN = 0b0000000010000000;
|
||||||
const IS_PROCSUB = 0b0000000100000000;
|
const IS_PROCSUB = 0b0000000100000000;
|
||||||
|
const IS_HEREDOC = 0b0000001000000000;
|
||||||
|
const LIT_HEREDOC = 0b0000010000000000;
|
||||||
|
const TAB_HEREDOC = 0b0000100000000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LexStream {
|
|
||||||
source: Arc<String>,
|
|
||||||
pub cursor: usize,
|
|
||||||
pub name: String,
|
|
||||||
quote_state: QuoteState,
|
|
||||||
brc_grp_depth: usize,
|
|
||||||
brc_grp_start: Option<usize>,
|
|
||||||
flags: LexFlags,
|
|
||||||
}
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct LexFlags: u32 {
|
pub struct LexFlags: u32 {
|
||||||
@@ -271,7 +290,6 @@ bitflags! {
|
|||||||
/// The lexer has no more tokens to produce
|
/// The lexer has no more tokens to produce
|
||||||
const STALE = 0b0001000000;
|
const STALE = 0b0001000000;
|
||||||
const EXPECTING_IN = 0b0010000000;
|
const EXPECTING_IN = 0b0010000000;
|
||||||
const IN_CASE = 0b0100000000;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +313,18 @@ pub fn clean_input(input: &str) -> String {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LexStream {
|
||||||
|
source: Arc<String>,
|
||||||
|
pub cursor: usize,
|
||||||
|
pub name: String,
|
||||||
|
quote_state: QuoteState,
|
||||||
|
brc_grp_depth: usize,
|
||||||
|
brc_grp_start: Option<usize>,
|
||||||
|
case_depth: usize,
|
||||||
|
heredoc_skip: Option<usize>,
|
||||||
|
flags: LexFlags,
|
||||||
|
}
|
||||||
|
|
||||||
impl LexStream {
|
impl LexStream {
|
||||||
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
||||||
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
||||||
@@ -306,6 +336,8 @@ impl LexStream {
|
|||||||
quote_state: QuoteState::default(),
|
quote_state: QuoteState::default(),
|
||||||
brc_grp_depth: 0,
|
brc_grp_depth: 0,
|
||||||
brc_grp_start: None,
|
brc_grp_start: None,
|
||||||
|
heredoc_skip: None,
|
||||||
|
case_depth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Returns a slice of the source input using the given range
|
/// Returns a slice of the source input using the given range
|
||||||
@@ -365,7 +397,7 @@ impl LexStream {
|
|||||||
}
|
}
|
||||||
pub fn read_redir(&mut self) -> Option<ShResult<Tk>> {
|
pub fn read_redir(&mut self) -> Option<ShResult<Tk>> {
|
||||||
assert!(self.cursor <= self.source.len());
|
assert!(self.cursor <= self.source.len());
|
||||||
let slice = self.slice(self.cursor..)?;
|
let slice = self.slice(self.cursor..)?.to_string();
|
||||||
let mut pos = self.cursor;
|
let mut pos = self.cursor;
|
||||||
let mut chars = slice.chars().peekable();
|
let mut chars = slice.chars().peekable();
|
||||||
let mut tk = Tk::default();
|
let mut tk = Tk::default();
|
||||||
@@ -377,20 +409,38 @@ impl LexStream {
|
|||||||
return None; // It's a process sub
|
return None; // It's a process sub
|
||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
if let Some('|') = chars.peek() {
|
||||||
|
// noclobber force '>|'
|
||||||
|
chars.next();
|
||||||
|
pos += 1;
|
||||||
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some('>') = chars.peek() {
|
if let Some('>') = chars.peek() {
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
if let Some('&') = chars.peek() {
|
let Some('&') = chars.peek() else {
|
||||||
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
let mut found_fd = false;
|
let mut found_fd = false;
|
||||||
|
if chars.peek().is_some_and(|ch| *ch == '-') {
|
||||||
|
chars.next();
|
||||||
|
found_fd = true;
|
||||||
|
pos += 1;
|
||||||
|
} else {
|
||||||
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
|
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
|
||||||
chars.next();
|
chars.next();
|
||||||
found_fd = true;
|
found_fd = true;
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
let span_start = self.cursor;
|
let span_start = self.cursor;
|
||||||
@@ -404,10 +454,6 @@ impl LexStream {
|
|||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
'<' => {
|
'<' => {
|
||||||
if chars.peek() == Some(&'(') {
|
if chars.peek() == Some(&'(') {
|
||||||
@@ -415,14 +461,94 @@ impl LexStream {
|
|||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
for _ in 0..2 {
|
match chars.peek() {
|
||||||
if let Some('<') = chars.peek() {
|
Some('<') => {
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
|
match chars.peek() {
|
||||||
|
Some('<') => {
|
||||||
|
chars.next();
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ch) => {
|
||||||
|
let mut ch = *ch;
|
||||||
|
while is_field_sep(ch) {
|
||||||
|
let Some(next_ch) = chars.next() else {
|
||||||
|
// Incomplete input — fall through to emit << as Redir
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
pos += next_ch.len_utf8();
|
||||||
|
ch = next_ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_field_sep(ch) {
|
||||||
|
// Ran out of input while skipping whitespace — fall through
|
||||||
} else {
|
} else {
|
||||||
|
let saved_cursor = self.cursor;
|
||||||
|
match self.read_heredoc(pos) {
|
||||||
|
Ok(Some(heredoc_tk)) => {
|
||||||
|
// cursor is set to after the delimiter word;
|
||||||
|
// heredoc_skip is set to after the body
|
||||||
|
pos = self.cursor;
|
||||||
|
self.cursor = saved_cursor;
|
||||||
|
tk = heredoc_tk;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Incomplete heredoc — restore cursor and fall through
|
||||||
|
self.cursor = saved_cursor;
|
||||||
|
}
|
||||||
|
Err(e) => return Some(Err(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// No delimiter yet — input is incomplete
|
||||||
|
// Fall through to emit the << as a Redir token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some('>') => {
|
||||||
|
chars.next();
|
||||||
|
pos += 1;
|
||||||
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some('&') => {
|
||||||
|
chars.next();
|
||||||
|
pos += 1;
|
||||||
|
|
||||||
|
let mut found_fd = false;
|
||||||
|
if chars.peek().is_some_and(|ch| *ch == '-') {
|
||||||
|
chars.next();
|
||||||
|
found_fd = true;
|
||||||
|
pos += 1;
|
||||||
|
} else {
|
||||||
|
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
|
||||||
|
chars.next();
|
||||||
|
found_fd = true;
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
|
let span_start = self.cursor;
|
||||||
|
self.cursor = pos;
|
||||||
|
return Some(Err(ShErr::at(
|
||||||
|
ShErrKind::ParseErr,
|
||||||
|
Span::new(span_start..pos, self.source.clone()),
|
||||||
|
"Invalid redirection",
|
||||||
|
)));
|
||||||
|
} else {
|
||||||
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -446,6 +572,133 @@ impl LexStream {
|
|||||||
self.cursor = pos;
|
self.cursor = pos;
|
||||||
Some(Ok(tk))
|
Some(Ok(tk))
|
||||||
}
|
}
|
||||||
|
pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult<Option<Tk>> {
|
||||||
|
let slice = self.slice(pos..).unwrap_or_default().to_string();
|
||||||
|
let mut chars = slice.chars();
|
||||||
|
let mut delim = String::new();
|
||||||
|
let mut flags = TkFlags::empty();
|
||||||
|
let mut first_char = true;
|
||||||
|
// Parse the delimiter word, stripping quotes
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'-' if first_char => {
|
||||||
|
pos += 1;
|
||||||
|
flags |= TkFlags::TAB_HEREDOC;
|
||||||
|
}
|
||||||
|
'\"' => {
|
||||||
|
pos += 1;
|
||||||
|
self.quote_state.toggle_double();
|
||||||
|
flags |= TkFlags::LIT_HEREDOC;
|
||||||
|
}
|
||||||
|
'\'' => {
|
||||||
|
pos += 1;
|
||||||
|
self.quote_state.toggle_single();
|
||||||
|
flags |= TkFlags::LIT_HEREDOC;
|
||||||
|
}
|
||||||
|
_ if self.quote_state.in_quote() => {
|
||||||
|
pos += ch.len_utf8();
|
||||||
|
delim.push(ch);
|
||||||
|
}
|
||||||
|
ch if is_hard_sep(ch) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ch => {
|
||||||
|
pos += ch.len_utf8();
|
||||||
|
delim.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first_char = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pos is now right after the delimiter word — this is where
|
||||||
|
// the cursor should return so the rest of the line gets lexed
|
||||||
|
let cursor_after_delim = pos;
|
||||||
|
|
||||||
|
// Re-slice from cursor_after_delim so iterator and pos are in sync
|
||||||
|
// (the old chars iterator consumed the hard_sep without advancing pos)
|
||||||
|
let rest = self
|
||||||
|
.slice(cursor_after_delim..)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
|
let mut chars = rest.chars();
|
||||||
|
|
||||||
|
// Scan forward to the newline (or use heredoc_skip from a previous heredoc)
|
||||||
|
let body_start = if let Some(skip) = self.heredoc_skip {
|
||||||
|
// A previous heredoc on this line already read its body;
|
||||||
|
// our body starts where that one ended
|
||||||
|
let skip_offset = skip - cursor_after_delim;
|
||||||
|
for _ in 0..skip_offset {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
skip
|
||||||
|
} else {
|
||||||
|
// Skip the rest of the current line to find where the body begins
|
||||||
|
let mut scan = pos;
|
||||||
|
let mut found_newline = false;
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
scan += ch.len_utf8();
|
||||||
|
if ch == '\n' {
|
||||||
|
found_newline = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found_newline {
|
||||||
|
if self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
|
return Ok(None);
|
||||||
|
} else {
|
||||||
|
return Err(ShErr::at(
|
||||||
|
ShErrKind::ParseErr,
|
||||||
|
Span::new(pos..pos, self.source.clone()),
|
||||||
|
"Heredoc delimiter not found",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scan
|
||||||
|
};
|
||||||
|
|
||||||
|
pos = body_start;
|
||||||
|
let start = pos;
|
||||||
|
|
||||||
|
// Read lines until we find one that matches the delimiter exactly
|
||||||
|
let mut line = String::new();
|
||||||
|
let mut line_start = pos;
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
pos += ch.len_utf8();
|
||||||
|
if ch == '\n' {
|
||||||
|
let trimmed = line.trim_end_matches('\r');
|
||||||
|
if trimmed == delim {
|
||||||
|
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
||||||
|
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
||||||
|
self.heredoc_skip = Some(pos);
|
||||||
|
self.cursor = cursor_after_delim;
|
||||||
|
return Ok(Some(tk));
|
||||||
|
}
|
||||||
|
line.clear();
|
||||||
|
line_start = pos;
|
||||||
|
} else {
|
||||||
|
line.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check the last line (no trailing newline)
|
||||||
|
let trimmed = line.trim_end_matches('\r');
|
||||||
|
if trimmed == delim {
|
||||||
|
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
||||||
|
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
||||||
|
self.heredoc_skip = Some(pos);
|
||||||
|
self.cursor = cursor_after_delim;
|
||||||
|
return Ok(Some(tk));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
|
Err(ShErr::at(
|
||||||
|
ShErrKind::ParseErr,
|
||||||
|
Span::new(start..pos, self.source.clone()),
|
||||||
|
format!("Heredoc delimiter '{}' not found", delim),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn read_string(&mut self) -> ShResult<Tk> {
|
pub fn read_string(&mut self) -> ShResult<Tk> {
|
||||||
assert!(self.cursor <= self.source.len());
|
assert!(self.cursor <= self.source.len());
|
||||||
let slice = self.slice_from_cursor().unwrap().to_string();
|
let slice = self.slice_from_cursor().unwrap().to_string();
|
||||||
@@ -453,7 +706,7 @@ impl LexStream {
|
|||||||
let mut chars = slice.chars().peekable();
|
let mut chars = slice.chars().peekable();
|
||||||
let can_be_subshell = chars.peek() == Some(&'(');
|
let can_be_subshell = chars.peek() == Some(&'(');
|
||||||
|
|
||||||
if self.flags.contains(LexFlags::IN_CASE)
|
if self.case_depth > 0
|
||||||
&& let Some(count) = case_pat_lookahead(chars.clone())
|
&& let Some(count) = case_pat_lookahead(chars.clone())
|
||||||
{
|
{
|
||||||
pos += count;
|
pos += count;
|
||||||
@@ -623,6 +876,16 @@ impl LexStream {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'(' if can_be_subshell && chars.peek() == Some(&')') => {
|
||||||
|
// standalone "()" — function definition marker
|
||||||
|
pos += 2;
|
||||||
|
chars.next();
|
||||||
|
let mut tk = self.get_token(self.cursor..pos, TkRule::Str);
|
||||||
|
tk.mark(TkFlags::KEYWORD);
|
||||||
|
self.cursor = pos;
|
||||||
|
self.set_next_is_cmd(true);
|
||||||
|
return Ok(tk);
|
||||||
|
}
|
||||||
'(' if self.next_is_cmd() && can_be_subshell => {
|
'(' if self.next_is_cmd() && can_be_subshell => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
let mut paren_count = 1;
|
let mut paren_count = 1;
|
||||||
@@ -731,7 +994,7 @@ impl LexStream {
|
|||||||
"case" | "select" | "for" => {
|
"case" | "select" | "for" => {
|
||||||
new_tk.mark(TkFlags::KEYWORD);
|
new_tk.mark(TkFlags::KEYWORD);
|
||||||
self.flags |= LexFlags::EXPECTING_IN;
|
self.flags |= LexFlags::EXPECTING_IN;
|
||||||
self.flags |= LexFlags::IN_CASE;
|
self.case_depth += 1;
|
||||||
self.set_next_is_cmd(false);
|
self.set_next_is_cmd(false);
|
||||||
}
|
}
|
||||||
"in" if self.flags.contains(LexFlags::EXPECTING_IN) => {
|
"in" if self.flags.contains(LexFlags::EXPECTING_IN) => {
|
||||||
@@ -739,8 +1002,8 @@ impl LexStream {
|
|||||||
self.flags &= !LexFlags::EXPECTING_IN;
|
self.flags &= !LexFlags::EXPECTING_IN;
|
||||||
}
|
}
|
||||||
_ if is_keyword(text) => {
|
_ if is_keyword(text) => {
|
||||||
if text == "esac" && self.flags.contains(LexFlags::IN_CASE) {
|
if text == "esac" && self.case_depth > 0 {
|
||||||
self.flags &= !LexFlags::IN_CASE;
|
self.case_depth -= 1;
|
||||||
}
|
}
|
||||||
new_tk.mark(TkFlags::KEYWORD);
|
new_tk.mark(TkFlags::KEYWORD);
|
||||||
}
|
}
|
||||||
@@ -843,10 +1106,19 @@ impl Iterator for LexStream {
|
|||||||
|
|
||||||
let token = match get_char(&self.source, self.cursor).unwrap() {
|
let token = match get_char(&self.source, self.cursor).unwrap() {
|
||||||
'\r' | '\n' | ';' => {
|
'\r' | '\n' | ';' => {
|
||||||
|
let ch = get_char(&self.source, self.cursor).unwrap();
|
||||||
let ch_idx = self.cursor;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
self.set_next_is_cmd(true);
|
self.set_next_is_cmd(true);
|
||||||
|
|
||||||
|
// If a heredoc was parsed on this line, skip past the body
|
||||||
|
// Only on newline — ';' is a command separator within the same line
|
||||||
|
if (ch == '\n' || ch == '\r')
|
||||||
|
&& let Some(skip) = self.heredoc_skip.take()
|
||||||
|
{
|
||||||
|
self.cursor = skip;
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(ch) = get_char(&self.source, self.cursor) {
|
while let Some(ch) = get_char(&self.source, self.cursor) {
|
||||||
match ch {
|
match ch {
|
||||||
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {
|
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {
|
||||||
@@ -881,6 +1153,14 @@ impl Iterator for LexStream {
|
|||||||
return self.next();
|
return self.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
'!' if self.next_is_cmd() => {
|
||||||
|
self.cursor += 1;
|
||||||
|
let tk_type = TkRule::Bang;
|
||||||
|
|
||||||
|
let mut tk = self.get_token((self.cursor - 1)..self.cursor, tk_type);
|
||||||
|
tk.flags |= TkFlags::KEYWORD;
|
||||||
|
tk
|
||||||
|
}
|
||||||
'|' => {
|
'|' => {
|
||||||
let ch_idx = self.cursor;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
|||||||
1219
src/parse/mod.rs
1219
src/parse/mod.rs
File diff suppressed because one or more lines are too long
@@ -19,7 +19,7 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
|||||||
pub use bitflags::bitflags;
|
pub use bitflags::bitflags;
|
||||||
pub use nix::{
|
pub use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
fcntl::{OFlag, open},
|
fcntl::{FcntlArg, OFlag, fcntl, open},
|
||||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||||
sys::{
|
sys::{
|
||||||
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||||
@@ -33,5 +33,4 @@ pub use nix::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Additional utilities, if needed, can be added here
|
// Additional utilities, if needed, can be added here
|
||||||
|
|||||||
180
src/procio.rs
180
src/procio.rs
@@ -8,15 +8,27 @@ use crate::{
|
|||||||
expand::Expander,
|
expand::Expander,
|
||||||
libsh::{
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
|
sys::TTY_FILENO,
|
||||||
utils::RedirVecUtils,
|
utils::RedirVecUtils,
|
||||||
},
|
},
|
||||||
parse::{Redir, RedirType, get_redir_file},
|
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Credit to fish-shell for many of the implementation ideas present in this
|
// Credit to fish-shell for many of the implementation ideas present in this
|
||||||
// module https://fishshell.com/
|
// module https://fishshell.com/
|
||||||
|
|
||||||
|
/// Minimum fd number for shell-internal file descriptors.
|
||||||
|
/// User-visible fds (0-9) are kept clear so `exec 3>&-` etc. work as expected.
|
||||||
|
const MIN_INTERNAL_FD: RawFd = 10;
|
||||||
|
|
||||||
|
/// Like `dup()`, but places the new fd at `MIN_INTERNAL_FD` or above so it
|
||||||
|
/// doesn't collide with user-managed fds.
|
||||||
|
fn dup_high(fd: RawFd) -> nix::Result<RawFd> {
|
||||||
|
fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum IoMode {
|
pub enum IoMode {
|
||||||
Fd {
|
Fd {
|
||||||
@@ -37,8 +49,9 @@ pub enum IoMode {
|
|||||||
pipe: Arc<OwnedFd>,
|
pipe: Arc<OwnedFd>,
|
||||||
},
|
},
|
||||||
Buffer {
|
Buffer {
|
||||||
|
tgt_fd: RawFd,
|
||||||
buf: String,
|
buf: String,
|
||||||
pipe: Arc<OwnedFd>,
|
flags: TkFlags, // so we can see if its a heredoc or not
|
||||||
},
|
},
|
||||||
Close {
|
Close {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
@@ -79,19 +92,29 @@ impl IoMode {
|
|||||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||||
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
||||||
|
|
||||||
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
|
let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())?
|
||||||
// multiple
|
.expand()?
|
||||||
|
.join(" "); // should just be one string, will have to find some way to handle a return of multiple paths
|
||||||
|
|
||||||
let expanded_pathbuf = PathBuf::from(expanded_path);
|
let expanded_pathbuf = PathBuf::from(expanded_path);
|
||||||
|
|
||||||
let file = get_redir_file(mode, expanded_pathbuf)?;
|
let file = get_redir_file(mode, expanded_pathbuf)?;
|
||||||
|
// Move the opened fd above the user-accessible range so it never
|
||||||
|
// collides with the target fd (e.g. `3>/tmp/foo` where open() returns 3,
|
||||||
|
// causing dup2(3,3) to be a no-op and then OwnedFd drop closes it).
|
||||||
|
let raw = file.as_raw_fd();
|
||||||
|
let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).map_err(ShErr::from)?;
|
||||||
|
drop(file); // closes the original low fd
|
||||||
self = IoMode::OpenedFile {
|
self = IoMode::OpenedFile {
|
||||||
tgt_fd,
|
tgt_fd,
|
||||||
file: Arc::new(OwnedFd::from(file)),
|
file: Arc::new(unsafe { OwnedFd::from_raw_fd(high) }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
|
||||||
|
Ok(Self::Buffer { tgt_fd, buf, flags })
|
||||||
|
}
|
||||||
pub fn get_pipes() -> (Self, Self) {
|
pub fn get_pipes() -> (Self, Self) {
|
||||||
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
|
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
|
||||||
(
|
(
|
||||||
@@ -206,24 +229,107 @@ impl<'e> IoFrame {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
pub fn save(&'e mut self) {
|
pub fn save(&'e mut self) {
|
||||||
let saved_in = dup(STDIN_FILENO).unwrap();
|
let saved_in = dup_high(STDIN_FILENO).unwrap();
|
||||||
let saved_out = dup(STDOUT_FILENO).unwrap();
|
let saved_out = dup_high(STDOUT_FILENO).unwrap();
|
||||||
let saved_err = dup(STDERR_FILENO).unwrap();
|
let saved_err = dup_high(STDERR_FILENO).unwrap();
|
||||||
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
|
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
|
||||||
}
|
}
|
||||||
pub fn redirect(mut self) -> ShResult<RedirGuard> {
|
pub fn redirect(mut self) -> ShResult<RedirGuard> {
|
||||||
self.save();
|
self.save();
|
||||||
for redir in &mut self.redirs {
|
if let Err(e) = self.apply_redirs() {
|
||||||
let io_mode = &mut redir.io_mode;
|
// Restore saved fds before propagating the error so they don't leak.
|
||||||
if let IoMode::File { .. } = io_mode {
|
self.restore().ok();
|
||||||
*io_mode = io_mode.clone().open_file()?;
|
return Err(e);
|
||||||
};
|
|
||||||
let tgt_fd = io_mode.tgt_fd();
|
|
||||||
let src_fd = io_mode.src_fd();
|
|
||||||
dup2(src_fd, tgt_fd)?;
|
|
||||||
}
|
}
|
||||||
Ok(RedirGuard::new(self))
|
Ok(RedirGuard::new(self))
|
||||||
}
|
}
|
||||||
|
fn apply_redirs(&mut self) -> ShResult<()> {
|
||||||
|
for redir in &mut self.redirs {
|
||||||
|
let io_mode = &mut redir.io_mode;
|
||||||
|
match io_mode {
|
||||||
|
IoMode::Close { tgt_fd } => {
|
||||||
|
if *tgt_fd == *TTY_FILENO {
|
||||||
|
// Don't let user close the shell's tty fd.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
close(*tgt_fd).ok();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
IoMode::File { .. } => match io_mode.clone().open_file() {
|
||||||
|
Ok(file) => *io_mode = file,
|
||||||
|
Err(e) => {
|
||||||
|
if let Some(span) = redir.span.as_ref() {
|
||||||
|
return Err(e.promote(span.clone()));
|
||||||
|
}
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IoMode::Buffer { tgt_fd, buf, flags } => {
|
||||||
|
let (rpipe, wpipe) = nix::unistd::pipe()?;
|
||||||
|
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
|
||||||
|
buf.clone()
|
||||||
|
} else {
|
||||||
|
let words = Expander::from_raw(buf, *flags)?.expand()?;
|
||||||
|
if flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
|
words.into_iter().next().unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
let ifs = state::get_separator();
|
||||||
|
words.join(&ifs).trim().to_string() + "\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if flags.contains(TkFlags::TAB_HEREDOC) {
|
||||||
|
let lines = text.lines();
|
||||||
|
let mut min_tabs = usize::MAX;
|
||||||
|
for line in lines {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let line_len = line.len();
|
||||||
|
let after_strip = line.trim_start_matches('\t').len();
|
||||||
|
let delta = line_len - after_strip;
|
||||||
|
min_tabs = min_tabs.min(delta);
|
||||||
|
}
|
||||||
|
if min_tabs == usize::MAX {
|
||||||
|
// let's avoid possibly allocating a string with 18 quintillion tabs
|
||||||
|
min_tabs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if min_tabs > 0 {
|
||||||
|
let stripped = text
|
||||||
|
.lines()
|
||||||
|
.fold(vec![], |mut acc, ln| {
|
||||||
|
if ln.is_empty() {
|
||||||
|
acc.push("");
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
|
||||||
|
acc.push(stripped_ln);
|
||||||
|
acc
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
text = stripped + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write(wpipe, text.as_bytes())?;
|
||||||
|
*io_mode = IoMode::Pipe {
|
||||||
|
tgt_fd: *tgt_fd,
|
||||||
|
pipe: rpipe.into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
let tgt_fd = io_mode.tgt_fd();
|
||||||
|
let src_fd = io_mode.src_fd();
|
||||||
|
if let Err(e) = dup2(src_fd, tgt_fd) {
|
||||||
|
if let Some(span) = redir.span.as_ref() {
|
||||||
|
return Err(ShErr::from(e).promote(span.clone()));
|
||||||
|
} else {
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
pub fn restore(&mut self) -> ShResult<()> {
|
pub fn restore(&mut self) -> ShResult<()> {
|
||||||
if let Some(saved) = self.saved_io.take() {
|
if let Some(saved) = self.saved_io.take() {
|
||||||
dup2(saved.0, STDIN_FILENO)?;
|
dup2(saved.0, STDIN_FILENO)?;
|
||||||
@@ -334,6 +440,8 @@ pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PipeFrames = Map<PipeGenerator, fn((Option<Redir>, Option<Redir>)) -> IoFrame>;
|
type PipeFrames = Map<PipeGenerator, fn((Option<Redir>, Option<Redir>)) -> IoFrame>;
|
||||||
|
|
||||||
|
/// An iterator that lazily creates a specific number of pipes.
|
||||||
pub struct PipeGenerator {
|
pub struct PipeGenerator {
|
||||||
num_cmds: usize,
|
num_cmds: usize,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
@@ -394,7 +502,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pipeline_simple() {
|
fn pipeline_simple() {
|
||||||
if !has_cmd("sed") { return };
|
if !has_cmd("sed") {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let g = TestGuard::new();
|
let g = TestGuard::new();
|
||||||
|
|
||||||
test_input("echo foo | sed 's/foo/bar/'").unwrap();
|
test_input("echo foo | sed 's/foo/bar/'").unwrap();
|
||||||
@@ -405,10 +515,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pipeline_multi() {
|
fn pipeline_multi() {
|
||||||
if !has_cmds(&[
|
if !has_cmds(&["cut", "sed"]) {
|
||||||
"cut",
|
return;
|
||||||
"sed"
|
}
|
||||||
]) { return; }
|
|
||||||
let g = TestGuard::new();
|
let g = TestGuard::new();
|
||||||
|
|
||||||
test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap();
|
test_input("echo foo bar baz | cut -d ' ' -f 2 | sed 's/a/A/'").unwrap();
|
||||||
@@ -419,10 +528,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rube_goldberg_pipeline() {
|
fn rube_goldberg_pipeline() {
|
||||||
if !has_cmds(&[
|
if !has_cmds(&["sed", "cat"]) {
|
||||||
"sed",
|
return;
|
||||||
"cat",
|
}
|
||||||
]) { return }
|
|
||||||
let g = TestGuard::new();
|
let g = TestGuard::new();
|
||||||
|
|
||||||
test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap();
|
test_input("{ echo foo; echo bar } | if cat; then :; else echo failed; fi | (read line && echo $line | sed 's/foo/baz/'; sed 's/bar/buzz/')").unwrap();
|
||||||
@@ -437,7 +545,9 @@ pub mod tests {
|
|||||||
|
|
||||||
test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap();
|
test_input("echo this is in a file > /tmp/simple_file_redir.txt").unwrap();
|
||||||
|
|
||||||
g.add_cleanup(|| { std::fs::remove_file("/tmp/simple_file_redir.txt").ok(); });
|
g.add_cleanup(|| {
|
||||||
|
std::fs::remove_file("/tmp/simple_file_redir.txt").ok();
|
||||||
|
});
|
||||||
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap();
|
let contents = std::fs::read_to_string("/tmp/simple_file_redir.txt").unwrap();
|
||||||
|
|
||||||
assert_eq!(contents, "this is in a file\n");
|
assert_eq!(contents, "this is in a file\n");
|
||||||
@@ -458,7 +568,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn input_redir() {
|
fn input_redir() {
|
||||||
if !has_cmd("cat") { return; }
|
if !has_cmd("cat") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let dir = tempfile::TempDir::new().unwrap();
|
let dir = tempfile::TempDir::new().unwrap();
|
||||||
let path = dir.path().join("input.txt");
|
let path = dir.path().join("input.txt");
|
||||||
std::fs::write(&path, "hello from file\n").unwrap();
|
std::fs::write(&path, "hello from file\n").unwrap();
|
||||||
@@ -487,7 +599,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pipe_and_stderr() {
|
fn pipe_and_stderr() {
|
||||||
if !has_cmd("cat") { return; }
|
if !has_cmd("cat") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let g = TestGuard::new();
|
let g = TestGuard::new();
|
||||||
|
|
||||||
test_input("echo on stderr >&2 |& cat").unwrap();
|
test_input("echo on stderr >&2 |& cat").unwrap();
|
||||||
@@ -511,7 +625,9 @@ pub mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pipeline_preserves_exit_status() {
|
fn pipeline_preserves_exit_status() {
|
||||||
if !has_cmd("cat") { return; }
|
if !has_cmd("cat") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
|
|
||||||
test_input("false | cat").unwrap();
|
test_input("false | cat").unwrap();
|
||||||
@@ -533,7 +649,11 @@ pub mod tests {
|
|||||||
let _g = TestGuard::new();
|
let _g = TestGuard::new();
|
||||||
|
|
||||||
// Redirect stdout to file, then dup stderr to stdout — both should go to file
|
// Redirect stdout to file, then dup stderr to stdout — both should go to file
|
||||||
test_input(format!("{{ echo out; echo err >&2 }} > {} 2>&1", path.display())).unwrap();
|
test_input(format!(
|
||||||
|
"{{ echo out; echo err >&2 }} > {} 2>&1",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let contents = std::fs::read_to_string(&path).unwrap();
|
let contents = std::fs::read_to_string(&path).unwrap();
|
||||||
assert!(contents.contains("out"));
|
assert!(contents.contains("out"));
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use nix::sys::signal::Signal;
|
use nix::sys::signal::Signal;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::complete::{CompFlags, CompOptFlags, CompOpts},
|
builtin::complete::{CompFlags, CompOptFlags, CompOpts},
|
||||||
|
expand::escape_str,
|
||||||
libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils},
|
libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils},
|
||||||
parse::{
|
parse::{
|
||||||
execute::exec_input,
|
execute::exec_input,
|
||||||
@@ -22,7 +24,9 @@ use crate::{
|
|||||||
term::{LineWriter, TermWriter, calc_str_width, get_win_size},
|
term::{LineWriter, TermWriter, calc_str_width, get_win_size},
|
||||||
vimode::{ViInsert, ViMode},
|
vimode::{ViInsert, ViMode},
|
||||||
},
|
},
|
||||||
state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars},
|
state::{
|
||||||
|
VarFlags, VarKind, read_jobs, read_logic, read_meta, read_shopts, read_vars, write_vars,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn complete_signals(start: &str) -> Vec<String> {
|
pub fn complete_signals(start: &str) -> Vec<String> {
|
||||||
@@ -173,6 +177,11 @@ fn complete_commands(start: &str) -> Vec<String> {
|
|||||||
.collect()
|
.collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if read_shopts(|o| o.core.autocd) {
|
||||||
|
let dirs = complete_dirs(start);
|
||||||
|
candidates.extend(dirs);
|
||||||
|
}
|
||||||
|
|
||||||
candidates.sort();
|
candidates.sort();
|
||||||
candidates
|
candidates
|
||||||
}
|
}
|
||||||
@@ -559,13 +568,15 @@ pub trait Completer {
|
|||||||
fn reset(&mut self);
|
fn reset(&mut self);
|
||||||
fn reset_stay_active(&mut self);
|
fn reset_stay_active(&mut self);
|
||||||
fn is_active(&self) -> bool;
|
fn is_active(&self) -> bool;
|
||||||
fn all_candidates(&self) -> Vec<String> { vec![] }
|
fn all_candidates(&self) -> Vec<String> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
fn selected_candidate(&self) -> Option<String>;
|
fn selected_candidate(&self) -> Option<String>;
|
||||||
fn token_span(&self) -> (usize, usize);
|
fn token_span(&self) -> (usize, usize);
|
||||||
fn original_input(&self) -> &str;
|
fn original_input(&self) -> &str;
|
||||||
fn token(&self) -> &str {
|
fn token(&self) -> &str {
|
||||||
let orig = self.original_input();
|
let orig = self.original_input();
|
||||||
let (s,e) = self.token_span();
|
let (s, e) = self.token_span();
|
||||||
orig.get(s..e).unwrap_or(orig)
|
orig.get(s..e).unwrap_or(orig)
|
||||||
}
|
}
|
||||||
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
|
||||||
@@ -1171,13 +1182,19 @@ impl Completer for FuzzyCompleter {
|
|||||||
log::debug!("Getting completed line for candidate: {}", _candidate);
|
log::debug!("Getting completed line for candidate: {}", _candidate);
|
||||||
|
|
||||||
let selected = self.selector.selected_candidate().unwrap_or_default();
|
let selected = self.selector.selected_candidate().unwrap_or_default();
|
||||||
log::debug!("Selected candidate: {}", selected);
|
let (mut start, end) = self.completer.token_span;
|
||||||
let (start, end) = self.completer.token_span;
|
let slice = self
|
||||||
log::debug!("Token span: ({}, {})", start, end);
|
.completer
|
||||||
|
.original_input
|
||||||
|
.get(start..end)
|
||||||
|
.unwrap_or_default();
|
||||||
|
start += slice.width();
|
||||||
|
let completion = selected.strip_prefix(slice).unwrap_or(&selected);
|
||||||
|
let escaped = escape_str(completion, false);
|
||||||
let ret = format!(
|
let ret = format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&self.completer.original_input[..start],
|
&self.completer.original_input[..start],
|
||||||
selected,
|
escaped,
|
||||||
&self.completer.original_input[end..]
|
&self.completer.original_input[end..]
|
||||||
);
|
);
|
||||||
log::debug!("Completed line: {}", ret);
|
log::debug!("Completed line: {}", ret);
|
||||||
@@ -1430,11 +1447,15 @@ impl SimpleCompleter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selected = &self.candidates[self.selected_idx];
|
let selected = &self.candidates[self.selected_idx];
|
||||||
let (start, end) = self.token_span;
|
let (mut start, end) = self.token_span;
|
||||||
|
let slice = self.original_input.get(start..end).unwrap_or("");
|
||||||
|
start += slice.width();
|
||||||
|
let completion = selected.strip_prefix(slice).unwrap_or(selected);
|
||||||
|
let escaped = escape_str(completion, false);
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&self.original_input[..start],
|
&self.original_input[..start],
|
||||||
selected,
|
escaped,
|
||||||
&self.original_input[end..]
|
&self.original_input[end..]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1593,10 +1614,12 @@ impl SimpleCompleter {
|
|||||||
.set_range(self.token_span.0..self.token_span.1);
|
.set_range(self.token_span.0..self.token_span.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If token contains '=', only complete after the '='
|
// If token contains any COMP_WORDBREAKS, break the word
|
||||||
let token_str = cur_token.span.as_str();
|
let token_str = cur_token.span.as_str();
|
||||||
if let Some(eq_pos) = token_str.rfind('=') {
|
|
||||||
self.token_span.0 = cur_token.span.range().start + eq_pos + 1;
|
let word_breaks = read_vars(|v| v.try_get_var("COMP_WORDBREAKS")).unwrap_or("=".into());
|
||||||
|
if let Some(break_pos) = token_str.rfind(|c: char| word_breaks.contains(c)) {
|
||||||
|
self.token_span.0 = cur_token.span.range().start + break_pos + 1;
|
||||||
cur_token
|
cur_token
|
||||||
.span
|
.span
|
||||||
.set_range(self.token_span.0..self.token_span.1);
|
.set_range(self.token_span.0..self.token_span.1);
|
||||||
@@ -1642,3 +1665,462 @@ impl SimpleCompleter {
|
|||||||
Ok(CompResult::from_candidates(candidates))
|
Ok(CompResult::from_candidates(candidates))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
readline::{Prompt, ShedVi},
|
||||||
|
state::{VarFlags, VarKind, write_vars},
|
||||||
|
testutil::TestGuard,
|
||||||
|
};
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
|
||||||
|
let g = TestGuard::new();
|
||||||
|
let prompt = Prompt::default();
|
||||||
|
let vi = ShedVi::new_no_hist(prompt, g.pty_slave().as_raw_fd())
|
||||||
|
.unwrap()
|
||||||
|
.with_initial(initial);
|
||||||
|
(vi, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== extract_var_name =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_var_simple() {
|
||||||
|
let (name, start, end) = extract_var_name("$HOME").unwrap();
|
||||||
|
assert_eq!(name, "HOME");
|
||||||
|
assert_eq!(start, 1);
|
||||||
|
assert_eq!(end, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_var_braced() {
|
||||||
|
let (name, start, end) = extract_var_name("${PATH}").unwrap();
|
||||||
|
assert_eq!(name, "PATH");
|
||||||
|
// '$' hits continue (no pos++), '{' is at pos=0, so name_start = 1
|
||||||
|
assert_eq!(start, 1);
|
||||||
|
assert_eq!(end, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_var_partial() {
|
||||||
|
let (name, start, _end) = extract_var_name("$HO").unwrap();
|
||||||
|
assert_eq!(name, "HO");
|
||||||
|
assert_eq!(start, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_var_none() {
|
||||||
|
assert!(extract_var_name("hello").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== ScoredCandidate::fuzzy_score =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_exact_match() {
|
||||||
|
let mut c = ScoredCandidate::new("hello".into());
|
||||||
|
let score = c.fuzzy_score("hello");
|
||||||
|
assert!(score > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_prefix_match() {
|
||||||
|
let mut c = ScoredCandidate::new("hello_world".into());
|
||||||
|
let score = c.fuzzy_score("hello");
|
||||||
|
assert!(score > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_no_match() {
|
||||||
|
let mut c = ScoredCandidate::new("abc".into());
|
||||||
|
let score = c.fuzzy_score("xyz");
|
||||||
|
assert_eq!(score, i32::MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_empty_query() {
|
||||||
|
let mut c = ScoredCandidate::new("anything".into());
|
||||||
|
let score = c.fuzzy_score("");
|
||||||
|
assert_eq!(score, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fuzzy_boundary_bonus() {
|
||||||
|
let mut a = ScoredCandidate::new("foo_bar".into());
|
||||||
|
let mut b = ScoredCandidate::new("fxxxbxr".into());
|
||||||
|
let score_a = a.fuzzy_score("fbr");
|
||||||
|
let score_b = b.fuzzy_score("fbr");
|
||||||
|
// word-boundary match should score higher
|
||||||
|
assert!(score_a > score_b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== CompResult::from_candidates =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comp_result_no_match() {
|
||||||
|
let result = CompResult::from_candidates(vec![]);
|
||||||
|
assert!(matches!(result, CompResult::NoMatch));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comp_result_single() {
|
||||||
|
let result = CompResult::from_candidates(vec!["foo".into()]);
|
||||||
|
assert!(matches!(result, CompResult::Single { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comp_result_many() {
|
||||||
|
let result = CompResult::from_candidates(vec!["foo".into(), "bar".into()]);
|
||||||
|
assert!(matches!(result, CompResult::Many { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== complete_signals =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_signals_int() {
|
||||||
|
let results = complete_signals("INT");
|
||||||
|
assert!(results.contains(&"INT".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_signals_empty() {
|
||||||
|
let results = complete_signals("");
|
||||||
|
assert!(!results.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complete_signals_no_match() {
|
||||||
|
let results = complete_signals("ZZZZZZZ");
|
||||||
|
assert!(results.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== COMP_WORDBREAKS =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wordbreak_equals_default() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
let mut comp = SimpleCompleter::new();
|
||||||
|
|
||||||
|
let line = "cmd --foo=bar".to_string();
|
||||||
|
let cursor = line.len();
|
||||||
|
let _ = comp.get_candidates(line.clone(), cursor);
|
||||||
|
|
||||||
|
let eq_idx = line.find('=').unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
comp.token_span.0,
|
||||||
|
eq_idx + 1,
|
||||||
|
"token_span.0 ({}) should be right after '=' ({})",
|
||||||
|
comp.token_span.0,
|
||||||
|
eq_idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wordbreak_colon_when_set() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut comp = SimpleCompleter::new();
|
||||||
|
let line = "scp host:foo".to_string();
|
||||||
|
let cursor = line.len();
|
||||||
|
let _ = comp.get_candidates(line.clone(), cursor);
|
||||||
|
|
||||||
|
let colon_idx = line.find(':').unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
comp.token_span.0,
|
||||||
|
colon_idx + 1,
|
||||||
|
"token_span.0 ({}) should be right after ':' ({})",
|
||||||
|
comp.token_span.0,
|
||||||
|
colon_idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wordbreak_rightmost_wins() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
write_vars(|v| v.set_var("COMP_WORDBREAKS", VarKind::Str("=:".into()), VarFlags::NONE))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut comp = SimpleCompleter::new();
|
||||||
|
let line = "cmd --opt=host:val".to_string();
|
||||||
|
let cursor = line.len();
|
||||||
|
let _ = comp.get_candidates(line.clone(), cursor);
|
||||||
|
|
||||||
|
let colon_idx = line.rfind(':').unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
comp.token_span.0,
|
||||||
|
colon_idx + 1,
|
||||||
|
"should break at rightmost wordbreak char"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== SimpleCompleter cycling =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_wraps_forward() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
let mut comp = SimpleCompleter {
|
||||||
|
candidates: vec!["aaa".into(), "bbb".into(), "ccc".into()],
|
||||||
|
selected_idx: 2,
|
||||||
|
original_input: "".into(),
|
||||||
|
token_span: (0, 0),
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
comp.cycle_completion(1);
|
||||||
|
assert_eq!(comp.selected_idx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_wraps_backward() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
let mut comp = SimpleCompleter {
|
||||||
|
candidates: vec!["aaa".into(), "bbb".into(), "ccc".into()],
|
||||||
|
selected_idx: 0,
|
||||||
|
original_input: "".into(),
|
||||||
|
token_span: (0, 0),
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
comp.cycle_completion(-1);
|
||||||
|
assert_eq!(comp.selected_idx, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Completion escaping =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_str_special_chars() {
|
||||||
|
use crate::expand::escape_str;
|
||||||
|
let escaped = escape_str("hello world", false);
|
||||||
|
assert_eq!(escaped, "hello\\ world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_str_multiple_specials() {
|
||||||
|
use crate::expand::escape_str;
|
||||||
|
let escaped = escape_str("a&b|c", false);
|
||||||
|
assert_eq!(escaped, "a\\&b\\|c");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_str_no_specials() {
|
||||||
|
use crate::expand::escape_str;
|
||||||
|
let escaped = escape_str("hello", false);
|
||||||
|
assert_eq!(escaped, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_str_all_shell_metacharacters() {
|
||||||
|
use crate::expand::escape_str;
|
||||||
|
for ch in [
|
||||||
|
'\'', '"', '\\', '|', '&', ';', '(', ')', '<', '>', '$', '*', '!', '`', '{', '?', '[', '#',
|
||||||
|
' ', '\t', '\n',
|
||||||
|
] {
|
||||||
|
let input = format!("a{ch}b");
|
||||||
|
let escaped = escape_str(&input, false);
|
||||||
|
let expected = format!("a\\{ch}b");
|
||||||
|
assert_eq!(escaped, expected, "failed to escape {:?}", ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_str_kitchen_sink() {
|
||||||
|
use crate::expand::escape_str;
|
||||||
|
let input = "f$le (with) 'spaces' & {braces} | pipes; #hash ~tilde `backtick` !bang";
|
||||||
|
let escaped = escape_str(input, false);
|
||||||
|
assert_eq!(
|
||||||
|
escaped,
|
||||||
|
"f\\$le\\ \\(with\\)\\ \\'spaces\\'\\ \\&\\ \\{braces}\\ \\|\\ pipes\\;\\ \\#hash\\ ~tilde\\ \\`backtick\\`\\ \\!bang"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_line_only_escapes_new_text() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// Simulate: user typed "echo hel", completion candidate is "hello world"
|
||||||
|
let comp = SimpleCompleter {
|
||||||
|
candidates: vec!["hello world".into()],
|
||||||
|
selected_idx: 0,
|
||||||
|
original_input: "echo hel".into(),
|
||||||
|
token_span: (5, 8), // "hel" spans bytes 5..8
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
let result = comp.get_completed_line();
|
||||||
|
// "hel" is the user's text (not escaped), "lo world" is new (escaped)
|
||||||
|
assert_eq!(result, "echo hello\\ world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_line_no_new_text() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// User typed the full token, nothing new to escape
|
||||||
|
let comp = SimpleCompleter {
|
||||||
|
candidates: vec!["hello".into()],
|
||||||
|
selected_idx: 0,
|
||||||
|
original_input: "echo hello".into(),
|
||||||
|
token_span: (5, 10),
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
let result = comp.get_completed_line();
|
||||||
|
assert_eq!(result, "echo hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_line_appends_suffix_with_escape() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// User typed "echo hel", candidate is "hello world" (from filesystem)
|
||||||
|
// strip_prefix("hel") => "lo world", which gets escaped
|
||||||
|
let comp = SimpleCompleter {
|
||||||
|
candidates: vec!["hello world".into()],
|
||||||
|
selected_idx: 0,
|
||||||
|
original_input: "echo hel".into(),
|
||||||
|
token_span: (5, 8),
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
let result = comp.get_completed_line();
|
||||||
|
assert_eq!(result, "echo hello\\ world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_line_suffix_only_escapes_new_part() {
|
||||||
|
let _g = TestGuard::new();
|
||||||
|
// User typed "echo hello", candidate is "hello world&done"
|
||||||
|
// strip_prefix("hello") => " world&done", only that gets escaped
|
||||||
|
let comp = SimpleCompleter {
|
||||||
|
candidates: vec!["hello world&done".into()],
|
||||||
|
selected_idx: 0,
|
||||||
|
original_input: "echo hello".into(),
|
||||||
|
token_span: (5, 10),
|
||||||
|
active: true,
|
||||||
|
dirs_only: false,
|
||||||
|
add_space: false,
|
||||||
|
};
|
||||||
|
let result = comp.get_completed_line();
|
||||||
|
// "hello" is preserved as-is, " world&done" gets escaped
|
||||||
|
assert_eq!(result, "echo hello\\ world\\&done");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_escapes_special_in_filename() {
|
||||||
|
let tmp = std::env::temp_dir().join("shed_test_tab_esc");
|
||||||
|
let _ = std::fs::create_dir_all(&tmp);
|
||||||
|
std::fs::write(tmp.join("hello world.txt"), "").unwrap();
|
||||||
|
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
std::env::set_current_dir(&tmp).unwrap();
|
||||||
|
|
||||||
|
vi.feed_bytes(b"echo hello\t");
|
||||||
|
let _ = vi.process_input();
|
||||||
|
|
||||||
|
let line = vi.editor.as_str().to_string();
|
||||||
|
assert!(
|
||||||
|
line.contains("hello\\ world.txt"),
|
||||||
|
"expected escaped space in completion: {line:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&tmp).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_does_not_escape_user_text() {
|
||||||
|
let tmp = std::env::temp_dir().join("shed_test_tab_noesc");
|
||||||
|
let _ = std::fs::create_dir_all(&tmp);
|
||||||
|
std::fs::write(tmp.join("my file.txt"), "").unwrap();
|
||||||
|
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
std::env::set_current_dir(&tmp).unwrap();
|
||||||
|
|
||||||
|
// User types "echo my\ " with the space already escaped
|
||||||
|
vi.feed_bytes(b"echo my\\ \t");
|
||||||
|
let _ = vi.process_input();
|
||||||
|
|
||||||
|
let line = vi.editor.as_str().to_string();
|
||||||
|
// The user's "my\ " should be preserved, not double-escaped to "my\\\ "
|
||||||
|
assert!(
|
||||||
|
!line.contains("my\\\\ "),
|
||||||
|
"user text should not be double-escaped: {line:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
line.contains("my\\ file.txt"),
|
||||||
|
"expected completion with preserved user escape: {line:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&tmp).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Integration tests (pty) =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_completes_filename() {
|
||||||
|
let tmp = std::env::temp_dir().join("shed_test_tab_fn");
|
||||||
|
let _ = std::fs::create_dir_all(&tmp);
|
||||||
|
std::fs::write(tmp.join("unique_shed_test_file.txt"), "").unwrap();
|
||||||
|
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
std::env::set_current_dir(&tmp).unwrap();
|
||||||
|
|
||||||
|
// Type "echo unique_shed_test" then press Tab
|
||||||
|
vi.feed_bytes(b"echo unique_shed_test\t");
|
||||||
|
let _ = vi.process_input();
|
||||||
|
|
||||||
|
let line = vi.editor.as_str().to_string();
|
||||||
|
assert!(
|
||||||
|
line.contains("unique_shed_test_file.txt"),
|
||||||
|
"expected completion in line: {line:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&tmp).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_completes_directory_with_slash() {
|
||||||
|
let tmp = std::env::temp_dir().join("shed_test_tab_dir");
|
||||||
|
let _ = std::fs::create_dir_all(tmp.join("mysubdir"));
|
||||||
|
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
std::env::set_current_dir(&tmp).unwrap();
|
||||||
|
|
||||||
|
vi.feed_bytes(b"cd mysub\t");
|
||||||
|
let _ = vi.process_input();
|
||||||
|
|
||||||
|
let line = vi.editor.as_str().to_string();
|
||||||
|
assert!(
|
||||||
|
line.contains("mysubdir/"),
|
||||||
|
"expected dir completion with trailing slash: {line:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&tmp).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tab_after_equals() {
|
||||||
|
let tmp = std::env::temp_dir().join("shed_test_tab_eq");
|
||||||
|
let _ = std::fs::create_dir_all(&tmp);
|
||||||
|
std::fs::write(tmp.join("eqfile.txt"), "").unwrap();
|
||||||
|
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
std::env::set_current_dir(&tmp).unwrap();
|
||||||
|
|
||||||
|
vi.feed_bytes(b"cmd --opt=eqf\t");
|
||||||
|
let _ = vi.process_input();
|
||||||
|
|
||||||
|
let line = vi.editor.as_str().to_string();
|
||||||
|
assert!(
|
||||||
|
line.contains("--opt=eqfile.txt"),
|
||||||
|
"expected completion after '=': {line:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::remove_dir_all(&tmp).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,9 @@ impl Highlighter {
|
|||||||
while prefix_chars.peek().is_some() {
|
while prefix_chars.peek().is_some() {
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
Some(c) if c == markers::VISUAL_MODE_START || c == markers::VISUAL_MODE_END => continue,
|
Some(c) if c == markers::VISUAL_MODE_START || c == markers::VISUAL_MODE_END => continue,
|
||||||
Some(c) if Some(&c) == prefix_chars.peek() => { prefix_chars.next(); }
|
Some(c) if Some(&c) == prefix_chars.peek() => {
|
||||||
|
prefix_chars.next();
|
||||||
|
}
|
||||||
_ => return text.to_string(), // mismatch, return original
|
_ => return text.to_string(), // mismatch, return original
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,9 @@ impl Highlighter {
|
|||||||
let mut si = suffix_chars.len();
|
let mut si = suffix_chars.len();
|
||||||
|
|
||||||
while si > 0 {
|
while si > 0 {
|
||||||
if ti == 0 { return text.to_string(); }
|
if ti == 0 {
|
||||||
|
return text.to_string();
|
||||||
|
}
|
||||||
ti -= 1;
|
ti -= 1;
|
||||||
if chars[ti] == markers::VISUAL_MODE_START || chars[ti] == markers::VISUAL_MODE_END {
|
if chars[ti] == markers::VISUAL_MODE_START || chars[ti] == markers::VISUAL_MODE_END {
|
||||||
continue; // skip visual markers
|
continue; // skip visual markers
|
||||||
@@ -346,7 +350,9 @@ impl Highlighter {
|
|||||||
recursive_highlighter.highlight();
|
recursive_highlighter.highlight();
|
||||||
// Read back visual state — selection may have started/ended inside
|
// Read back visual state — selection may have started/ended inside
|
||||||
self.in_selection = recursive_highlighter.in_selection;
|
self.in_selection = recursive_highlighter.in_selection;
|
||||||
self.style_stack.append(&mut recursive_highlighter.style_stack);
|
self
|
||||||
|
.style_stack
|
||||||
|
.append(&mut recursive_highlighter.style_stack);
|
||||||
if selection_at_entry {
|
if selection_at_entry {
|
||||||
self.emit_style(Style::BgWhite | Style::Black);
|
self.emit_style(Style::BgWhite | Style::Black);
|
||||||
self.output.push_str(prefix);
|
self.output.push_str(prefix);
|
||||||
|
|||||||
@@ -217,18 +217,36 @@ pub struct History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl History {
|
impl History {
|
||||||
|
pub fn empty() -> Self {
|
||||||
|
Self {
|
||||||
|
path: PathBuf::new(),
|
||||||
|
pending: None,
|
||||||
|
entries: Vec::new(),
|
||||||
|
search_mask: Vec::new(),
|
||||||
|
fuzzy_finder: FuzzySelector::new("History").number_candidates(true),
|
||||||
|
no_matches: false,
|
||||||
|
cursor: 0,
|
||||||
|
//search_direction: Direction::Backward,
|
||||||
|
ignore_dups: false,
|
||||||
|
max_size: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn new() -> ShResult<Self> {
|
pub fn new() -> ShResult<Self> {
|
||||||
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
|
let ignore_dups = crate::state::read_shopts(|s| s.core.hist_ignore_dupes);
|
||||||
let max_hist = crate::state::read_shopts(|s| s.core.max_hist);
|
let max_hist = crate::state::read_shopts(|s| s.core.max_hist);
|
||||||
|
|
||||||
let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({
|
let path = PathBuf::from(env::var("SHEDHIST").unwrap_or({
|
||||||
let home = env::var("HOME").unwrap();
|
let home = env::var("HOME").unwrap();
|
||||||
format!("{home}/.shed_history")
|
format!("{home}/.shed_history")
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let mut entries = read_hist_file(&path)?;
|
let mut entries = read_hist_file(&path)?;
|
||||||
|
|
||||||
// Enforce max_hist limit on loaded entries (negative = unlimited)
|
// Enforce max_hist limit on loaded entries (negative = unlimited)
|
||||||
if max_hist >= 0 && entries.len() > max_hist as usize {
|
if max_hist >= 0 && entries.len() > max_hist as usize {
|
||||||
entries = entries.split_off(entries.len() - max_hist as usize);
|
entries = entries.split_off(entries.len() - max_hist as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
let search_mask = dedupe_entries(&entries);
|
let search_mask = dedupe_entries(&entries);
|
||||||
let cursor = search_mask.len();
|
let cursor = search_mask.len();
|
||||||
let max_size = if max_hist < 0 {
|
let max_size = if max_hist < 0 {
|
||||||
@@ -236,6 +254,7 @@ impl History {
|
|||||||
} else {
|
} else {
|
||||||
Some(max_hist as u32)
|
Some(max_hist as u32)
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
entries,
|
entries,
|
||||||
@@ -481,12 +500,8 @@ mod tests {
|
|||||||
env::set_var(key, val);
|
env::set_var(key, val);
|
||||||
}
|
}
|
||||||
guard(prev, move |p| match p {
|
guard(prev, move |p| match p {
|
||||||
Some(v) => unsafe {
|
Some(v) => unsafe { env::set_var(key, v) },
|
||||||
env::set_var(key, v)
|
None => unsafe { env::remove_var(key) },
|
||||||
},
|
|
||||||
None => unsafe {
|
|
||||||
env::remove_var(key)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,12 +518,7 @@ mod tests {
|
|||||||
fn write_history_file(path: &Path) {
|
fn write_history_file(path: &Path) {
|
||||||
fs::write(
|
fs::write(
|
||||||
path,
|
path,
|
||||||
[
|
[": 1;1;first\n", ": 2;1;second\n", ": 3;1;third\n"].concat(),
|
||||||
": 1;1;first\n",
|
|
||||||
": 2;1;second\n",
|
|
||||||
": 3;1;third\n",
|
|
||||||
]
|
|
||||||
.concat(),
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
@@ -567,12 +577,7 @@ mod tests {
|
|||||||
let hist_path = tmp.path().join("history");
|
let hist_path = tmp.path().join("history");
|
||||||
fs::write(
|
fs::write(
|
||||||
&hist_path,
|
&hist_path,
|
||||||
[
|
[": 1;1;repeat\n", ": 2;1;unique\n", ": 3;1;repeat\n"].concat(),
|
||||||
": 1;1;repeat\n",
|
|
||||||
": 2;1;unique\n",
|
|
||||||
": 3;1;repeat\n",
|
|
||||||
]
|
|
||||||
.concat(),
|
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,14 +9,14 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
|
|||||||
|
|
||||||
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
|
use crate::builtin::keymap::{KeyMapFlags, KeyMapMatch};
|
||||||
use crate::expand::expand_prompt;
|
use crate::expand::expand_prompt;
|
||||||
use crate::libsh::sys::TTY_FILENO;
|
|
||||||
use crate::libsh::utils::AutoCmdVecUtils;
|
use crate::libsh::utils::AutoCmdVecUtils;
|
||||||
use crate::parse::lex::{LexStream, QuoteState};
|
use crate::parse::lex::{LexStream, QuoteState};
|
||||||
use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
|
use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
|
||||||
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
use crate::readline::term::{Pos, TermReader, calc_str_width};
|
||||||
use crate::readline::vimode::{ViEx, ViVerbatim};
|
use crate::readline::vimode::{ViEx, ViVerbatim};
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars
|
AutoCmdKind, ShellParam, Var, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta,
|
||||||
|
write_vars,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
@@ -39,6 +39,9 @@ pub mod term;
|
|||||||
pub mod vicmd;
|
pub mod vicmd;
|
||||||
pub mod vimode;
|
pub mod vimode;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
pub mod markers {
|
pub mod markers {
|
||||||
use super::Marker;
|
use super::Marker;
|
||||||
|
|
||||||
@@ -238,6 +241,7 @@ impl Default for Prompt {
|
|||||||
pub struct ShedVi {
|
pub struct ShedVi {
|
||||||
pub reader: PollReader,
|
pub reader: PollReader,
|
||||||
pub writer: TermWriter,
|
pub writer: TermWriter,
|
||||||
|
pub tty: RawFd,
|
||||||
|
|
||||||
pub prompt: Prompt,
|
pub prompt: Prompt,
|
||||||
pub highlighter: Highlighter,
|
pub highlighter: Highlighter,
|
||||||
@@ -249,7 +253,6 @@ pub struct ShedVi {
|
|||||||
pub repeat_action: Option<CmdReplay>,
|
pub repeat_action: Option<CmdReplay>,
|
||||||
pub repeat_motion: Option<MotionCmd>,
|
pub repeat_motion: Option<MotionCmd>,
|
||||||
pub editor: LineBuf,
|
pub editor: LineBuf,
|
||||||
pub next_is_escaped: bool,
|
|
||||||
|
|
||||||
pub old_layout: Option<Layout>,
|
pub old_layout: Option<Layout>,
|
||||||
pub history: History,
|
pub history: History,
|
||||||
@@ -263,10 +266,10 @@ impl ShedVi {
|
|||||||
reader: PollReader::new(),
|
reader: PollReader::new(),
|
||||||
writer: TermWriter::new(tty),
|
writer: TermWriter::new(tty),
|
||||||
prompt,
|
prompt,
|
||||||
|
tty,
|
||||||
completer: Box::new(FuzzyCompleter::default()),
|
completer: Box::new(FuzzyCompleter::default()),
|
||||||
highlighter: Highlighter::new(),
|
highlighter: Highlighter::new(),
|
||||||
mode: Box::new(ViInsert::new()),
|
mode: Box::new(ViInsert::new()),
|
||||||
next_is_escaped: false,
|
|
||||||
saved_mode: None,
|
saved_mode: None,
|
||||||
pending_keymap: Vec::new(),
|
pending_keymap: Vec::new(),
|
||||||
old_layout: None,
|
old_layout: None,
|
||||||
@@ -289,6 +292,37 @@ impl ShedVi {
|
|||||||
Ok(new)
|
Ok(new)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_no_hist(prompt: Prompt, tty: RawFd) -> ShResult<Self> {
|
||||||
|
let mut new = Self {
|
||||||
|
reader: PollReader::new(),
|
||||||
|
writer: TermWriter::new(tty),
|
||||||
|
tty,
|
||||||
|
prompt,
|
||||||
|
completer: Box::new(FuzzyCompleter::default()),
|
||||||
|
highlighter: Highlighter::new(),
|
||||||
|
mode: Box::new(ViInsert::new()),
|
||||||
|
saved_mode: None,
|
||||||
|
pending_keymap: Vec::new(),
|
||||||
|
old_layout: None,
|
||||||
|
repeat_action: None,
|
||||||
|
repeat_motion: None,
|
||||||
|
editor: LineBuf::new(),
|
||||||
|
history: History::empty(),
|
||||||
|
needs_redraw: true,
|
||||||
|
};
|
||||||
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"SHED_VI_MODE",
|
||||||
|
VarKind::Str(new.mode.report_mode().to_string()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
new.prompt.refresh();
|
||||||
|
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
|
||||||
|
new.print_line(false)?;
|
||||||
|
Ok(new)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||||
self
|
self
|
||||||
@@ -310,7 +344,7 @@ impl ShedVi {
|
|||||||
pub fn fix_column(&mut self) -> ShResult<()> {
|
pub fn fix_column(&mut self) -> ShResult<()> {
|
||||||
self
|
self
|
||||||
.writer
|
.writer
|
||||||
.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
|
.fix_cursor_column(&mut TermReader::new(self.tty))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
|
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
|
||||||
@@ -380,7 +414,7 @@ impl ShedVi {
|
|||||||
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
|
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
|
||||||
let lex_result2 =
|
let lex_result2 =
|
||||||
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
|
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
|
||||||
let is_top_level = self.editor.auto_indent_level == 0;
|
let is_top_level = self.editor.indent_ctx.ctx().is_empty();
|
||||||
|
|
||||||
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
|
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
@@ -407,7 +441,6 @@ impl ShedVi {
|
|||||||
|
|
||||||
// Process all available keys
|
// Process all available keys
|
||||||
while let Some(key) = self.reader.read_key()? {
|
while let Some(key) = self.reader.read_key()? {
|
||||||
log::debug!("Read key: {key:?} in mode {:?}, self.reader.verbatim = {}", self.mode.report_mode(), self.reader.verbatim);
|
|
||||||
// If completer or history search are active, delegate input to it
|
// If completer or history search are active, delegate input to it
|
||||||
if self.history.fuzzy_finder.is_active() {
|
if self.history.fuzzy_finder.is_active() {
|
||||||
self.print_line(false)?;
|
self.print_line(false)?;
|
||||||
@@ -590,10 +623,6 @@ impl ShedVi {
|
|||||||
|
|
||||||
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
|
||||||
if self.should_accept_hint(&key) {
|
if self.should_accept_hint(&key) {
|
||||||
log::debug!(
|
|
||||||
"Accepting hint on key {key:?} in mode {:?}",
|
|
||||||
self.mode.report_mode()
|
|
||||||
);
|
|
||||||
self.editor.accept_hint();
|
self.editor.accept_hint();
|
||||||
if !self.history.at_pending() {
|
if !self.history.at_pending() {
|
||||||
self.history.reset_to_pending();
|
self.history.reset_to_pending();
|
||||||
@@ -658,7 +687,8 @@ impl ShedVi {
|
|||||||
VarKind::Str(self.mode.report_mode().to_string()),
|
VarKind::Str(self.mode.report_mode().to_string()),
|
||||||
VarFlags::NONE,
|
VarFlags::NONE,
|
||||||
)
|
)
|
||||||
}).ok();
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
// If we are here, we hit a case where pressing tab returned a single candidate
|
// If we are here, we hit a case where pressing tab returned a single candidate
|
||||||
// So we can just go ahead and reset the completer after this
|
// So we can just go ahead and reset the completer after this
|
||||||
@@ -668,13 +698,19 @@ impl ShedVi {
|
|||||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
|
||||||
let candidates = self.completer.all_candidates();
|
let candidates = self.completer.all_candidates();
|
||||||
let num_candidates = candidates.len();
|
let num_candidates = candidates.len();
|
||||||
with_vars([
|
with_vars(
|
||||||
|
[
|
||||||
("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)),
|
("_NUM_MATCHES".into(), Into::<Var>::into(num_candidates)),
|
||||||
("_MATCHES".into(), Into::<Var>::into(candidates)),
|
("_MATCHES".into(), Into::<Var>::into(candidates)),
|
||||||
("_SEARCH_STR".into(), Into::<Var>::into(self.completer.token())),
|
(
|
||||||
], || {
|
"_SEARCH_STR".into(),
|
||||||
|
Into::<Var>::into(self.completer.token()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
post_cmds.exec();
|
post_cmds.exec();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if self.completer.is_active() {
|
if self.completer.is_active() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
@@ -696,14 +732,14 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key {
|
} else if let KeyEvent(KeyCode::Char('R'), ModKeys::CTRL) = key
|
||||||
|
&& self.mode.report_mode() == ModeReport::Insert
|
||||||
|
{
|
||||||
let initial = self.editor.as_str();
|
let initial = self.editor.as_str();
|
||||||
match self.history.start_search(initial) {
|
match self.history.start_search(initial) {
|
||||||
Some(entry) => {
|
Some(entry) => {
|
||||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistorySelect));
|
||||||
with_vars([
|
with_vars([("_HIST_ENTRY".into(), entry.clone())], || {
|
||||||
("_HIST_ENTRY".into(), entry.clone()),
|
|
||||||
], || {
|
|
||||||
post_cmds.exec_with(&entry);
|
post_cmds.exec_with(&entry);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -717,7 +753,9 @@ impl ShedVi {
|
|||||||
None => {
|
None => {
|
||||||
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
|
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnHistoryOpen));
|
||||||
let entries = self.history.fuzzy_finder.candidates();
|
let entries = self.history.fuzzy_finder.candidates();
|
||||||
let matches = self.history.fuzzy_finder
|
let matches = self
|
||||||
|
.history
|
||||||
|
.fuzzy_finder
|
||||||
.filtered()
|
.filtered()
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -726,15 +764,18 @@ impl ShedVi {
|
|||||||
|
|
||||||
let num_entries = entries.len();
|
let num_entries = entries.len();
|
||||||
let num_matches = matches.len();
|
let num_matches = matches.len();
|
||||||
with_vars([
|
with_vars(
|
||||||
("_ENTRIES".into(),Into::<Var>::into(entries)),
|
[
|
||||||
("_NUM_ENTRIES".into(),Into::<Var>::into(num_entries)),
|
("_ENTRIES".into(), Into::<Var>::into(entries)),
|
||||||
("_MATCHES".into(),Into::<Var>::into(matches)),
|
("_NUM_ENTRIES".into(), Into::<Var>::into(num_entries)),
|
||||||
("_NUM_MATCHES".into(),Into::<Var>::into(num_matches)),
|
("_MATCHES".into(), Into::<Var>::into(matches)),
|
||||||
|
("_NUM_MATCHES".into(), Into::<Var>::into(num_matches)),
|
||||||
("_SEARCH_STR".into(), Into::<Var>::into(initial)),
|
("_SEARCH_STR".into(), Into::<Var>::into(initial)),
|
||||||
], || {
|
],
|
||||||
|
|| {
|
||||||
post_cmds.exec();
|
post_cmds.exec();
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if self.history.fuzzy_finder.is_active() {
|
if self.history.fuzzy_finder.is_active() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
@@ -755,14 +796,6 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
|
|
||||||
&& !self.next_is_escaped
|
|
||||||
{
|
|
||||||
self.next_is_escaped = true;
|
|
||||||
} else {
|
|
||||||
self.next_is_escaped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
|
let Ok(cmd) = self.mode.handle_key_fallible(key) else {
|
||||||
// it's an ex mode error
|
// it's an ex mode error
|
||||||
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
self.mode = Box::new(ViNormal::new()) as Box<dyn ViMode>;
|
||||||
@@ -781,8 +814,7 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.is_submit_action()
|
if cmd.is_submit_action()
|
||||||
&& !self.next_is_escaped
|
&& !self.editor.cursor_is_escaped()
|
||||||
&& !self.editor.buffer.ends_with('\\')
|
|
||||||
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
||||||
{
|
{
|
||||||
if self.editor.attempt_history_expansion(&self.history) {
|
if self.editor.attempt_history_expansion(&self.history) {
|
||||||
@@ -814,7 +846,7 @@ impl ShedVi {
|
|||||||
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
|
||||||
|
|
||||||
let before = self.editor.buffer.clone();
|
let before = self.editor.buffer.clone();
|
||||||
self.exec_cmd(cmd)?;
|
self.exec_cmd(cmd, false)?;
|
||||||
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
|
||||||
for key in keys {
|
for key in keys {
|
||||||
self.handle_key(key)?;
|
self.handle_key(key)?;
|
||||||
@@ -839,7 +871,7 @@ impl ShedVi {
|
|||||||
|
|
||||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
let (cols, _) = get_win_size(self.tty);
|
||||||
Layout::from_parts(cols, self.prompt.get_ps1(), to_cursor, line)
|
Layout::from_parts(cols, self.prompt.get_ps1(), to_cursor, line)
|
||||||
}
|
}
|
||||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||||
@@ -1018,6 +1050,7 @@ impl ShedVi {
|
|||||||
let pending_seq = self.mode.pending_seq().unwrap_or_default();
|
let pending_seq = self.mode.pending_seq().unwrap_or_default();
|
||||||
write!(buf, "\n: {pending_seq}").unwrap();
|
write!(buf, "\n: {pending_seq}").unwrap();
|
||||||
new_layout.end.row += 1;
|
new_layout.end.row += 1;
|
||||||
|
new_layout.cursor.row += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(buf, "{}", &self.mode.cursor_style()).unwrap();
|
write!(buf, "{}", &self.mode.cursor_style()).unwrap();
|
||||||
@@ -1072,10 +1105,9 @@ impl ShedVi {
|
|||||||
post_mode_change.exec();
|
post_mode_change.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
|
fn exec_mode_transition(&mut self, cmd: ViCmd, from_replay: bool) -> ShResult<()> {
|
||||||
let mut select_mode = None;
|
let mut select_mode = None;
|
||||||
let mut is_insert_mode = false;
|
let mut is_insert_mode = false;
|
||||||
if cmd.is_mode_transition() {
|
|
||||||
let count = cmd.verb_count();
|
let count = cmd.verb_count();
|
||||||
|
|
||||||
let mut mode: Box<dyn ViMode> = if matches!(
|
let mut mode: Box<dyn ViMode> = if matches!(
|
||||||
@@ -1092,12 +1124,19 @@ impl ShedVi {
|
|||||||
match cmd.verb().unwrap().1 {
|
match cmd.verb().unwrap().1 {
|
||||||
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
|
||||||
is_insert_mode = true;
|
is_insert_mode = true;
|
||||||
Box::new(ViInsert::new().with_count(count as u16))
|
Box::new(
|
||||||
|
ViInsert::new()
|
||||||
|
.with_count(count as u16)
|
||||||
|
.record_cmd(cmd.clone()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Verb::ExMode => Box::new(ViEx::new()),
|
Verb::ExMode => Box::new(ViEx::new()),
|
||||||
|
|
||||||
Verb::VerbatimMode => Box::new(ViVerbatim::read_one().with_count(count as u16)),
|
Verb::VerbatimMode => {
|
||||||
|
self.reader.verbatim_single = true;
|
||||||
|
Box::new(ViVerbatim::new().with_count(count as u16))
|
||||||
|
}
|
||||||
|
|
||||||
Verb::NormalMode => Box::new(ViNormal::new()),
|
Verb::NormalMode => Box::new(ViNormal::new()),
|
||||||
|
|
||||||
@@ -1145,7 +1184,7 @@ impl ShedVi {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode.is_repeatable() {
|
if mode.is_repeatable() && !from_replay {
|
||||||
self.repeat_action = mode.as_replay();
|
self.repeat_action = mode.as_replay();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1174,7 +1213,24 @@ impl ShedVi {
|
|||||||
})?;
|
})?;
|
||||||
self.prompt.refresh();
|
self.prompt.refresh();
|
||||||
|
|
||||||
return Ok(());
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone_mode(&self) -> Box<dyn ViMode> {
|
||||||
|
match self.mode.report_mode() {
|
||||||
|
ModeReport::Normal => Box::new(ViNormal::new()),
|
||||||
|
ModeReport::Insert => Box::new(ViInsert::new()),
|
||||||
|
ModeReport::Visual => Box::new(ViVisual::new()),
|
||||||
|
ModeReport::Ex => Box::new(ViEx::new()),
|
||||||
|
ModeReport::Replace => Box::new(ViReplace::new()),
|
||||||
|
ModeReport::Verbatim => Box::new(ViVerbatim::new()),
|
||||||
|
ModeReport::Unknown => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec_cmd(&mut self, mut cmd: ViCmd, from_replay: bool) -> ShResult<()> {
|
||||||
|
if cmd.is_mode_transition() {
|
||||||
|
return self.exec_mode_transition(cmd, from_replay);
|
||||||
} else if cmd.is_cmd_repeat() {
|
} else if cmd.is_cmd_repeat() {
|
||||||
let Some(replay) = self.repeat_action.clone() else {
|
let Some(replay) = self.repeat_action.clone() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -1186,12 +1242,37 @@ impl ShedVi {
|
|||||||
if count > 1 {
|
if count > 1 {
|
||||||
repeat = count as u16;
|
repeat = count as u16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let old_mode = self.mode.report_mode();
|
||||||
|
|
||||||
for _ in 0..repeat {
|
for _ in 0..repeat {
|
||||||
let cmds = cmds.clone();
|
let cmds = cmds.clone();
|
||||||
for cmd in cmds {
|
for (i, cmd) in cmds.iter().enumerate() {
|
||||||
self.editor.exec_cmd(cmd)?
|
self.exec_cmd(cmd.clone(), true)?;
|
||||||
|
// After the first command, start merging so all subsequent
|
||||||
|
// edits fold into one undo entry (e.g. cw + inserted chars)
|
||||||
|
if i == 0
|
||||||
|
&& let Some(edit) = self.editor.undo_stack.last_mut()
|
||||||
|
{
|
||||||
|
edit.start_merge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Stop merging at the end of the replay
|
||||||
|
if let Some(edit) = self.editor.undo_stack.last_mut() {
|
||||||
|
edit.stop_merge();
|
||||||
|
}
|
||||||
|
|
||||||
|
let old_mode_clone = match old_mode {
|
||||||
|
ModeReport::Normal => Box::new(ViNormal::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Insert => Box::new(ViInsert::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Visual => Box::new(ViVisual::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Ex => Box::new(ViEx::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Replace => Box::new(ViReplace::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Verbatim => Box::new(ViVerbatim::new()) as Box<dyn ViMode>,
|
||||||
|
ModeReport::Unknown => unreachable!(),
|
||||||
|
};
|
||||||
|
self.mode = old_mode_clone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
CmdReplay::Single(mut cmd) => {
|
CmdReplay::Single(mut cmd) => {
|
||||||
if count > 1 {
|
if count > 1 {
|
||||||
@@ -1253,7 +1334,7 @@ impl ShedVi {
|
|||||||
self.swap_mode(&mut mode);
|
self.swap_mode(&mut mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.is_repeatable() {
|
if cmd.is_repeatable() && !from_replay {
|
||||||
if self.mode.report_mode() == ModeReport::Visual {
|
if self.mode.report_mode() == ModeReport::Visual {
|
||||||
// The motion is assigned in the line buffer execution, so we also have to
|
// The motion is assigned in the line buffer execution, so we also have to
|
||||||
// assign it here in order to be able to repeat it
|
// assign it here in order to be able to repeat it
|
||||||
@@ -1272,7 +1353,11 @@ impl ShedVi {
|
|||||||
|
|
||||||
self.editor.exec_cmd(cmd.clone())?;
|
self.editor.exec_cmd(cmd.clone())?;
|
||||||
|
|
||||||
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
|
if self.mode.report_mode() == ModeReport::Visual
|
||||||
|
&& cmd
|
||||||
|
.verb()
|
||||||
|
.is_some_and(|v| v.1.is_edit() || v.1 == Verb::Yank)
|
||||||
|
{
|
||||||
self.editor.stop_selecting();
|
self.editor.stop_selecting();
|
||||||
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
|
||||||
self.swap_mode(&mut mode);
|
self.swap_mode(&mut mode);
|
||||||
@@ -1421,6 +1506,7 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
|||||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||||
match class {
|
match class {
|
||||||
TkRule::Pipe
|
TkRule::Pipe
|
||||||
|
| TkRule::Bang
|
||||||
| TkRule::ErrPipe
|
| TkRule::ErrPipe
|
||||||
| TkRule::And
|
| TkRule::And
|
||||||
| TkRule::Or
|
| TkRule::Or
|
||||||
@@ -1512,6 +1598,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
|
|
||||||
let mut insertions: Vec<(usize, Marker)> = vec![];
|
let mut insertions: Vec<(usize, Marker)> = vec![];
|
||||||
|
|
||||||
|
// Heredoc tokens have spans covering the body content far from the <<
|
||||||
|
// operator, which breaks position tracking after marker insertions
|
||||||
|
if token.flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
|
return insertions;
|
||||||
|
}
|
||||||
|
|
||||||
if token.class != TkRule::Str
|
if token.class != TkRule::Str
|
||||||
&& let Some(marker) = marker_for(&token.class)
|
&& let Some(marker) = marker_for(&token.class)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,24 @@ use std::{fmt::Display, sync::Mutex};
|
|||||||
|
|
||||||
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub static SAVED_REGISTERS: Mutex<Option<Registers>> = Mutex::new(None);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn save_registers() {
|
||||||
|
let mut saved = SAVED_REGISTERS.lock().unwrap();
|
||||||
|
*saved = Some(REGISTERS.lock().unwrap().clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub fn restore_registers() {
|
||||||
|
let mut saved = SAVED_REGISTERS.lock().unwrap();
|
||||||
|
if let Some(ref registers) = *saved {
|
||||||
|
*REGISTERS.lock().unwrap() = registers.clone();
|
||||||
|
}
|
||||||
|
*saved = None;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {
|
pub fn read_register(ch: Option<char>) -> Option<RegisterContent> {
|
||||||
let lock = REGISTERS.lock().unwrap();
|
let lock = REGISTERS.lock().unwrap();
|
||||||
lock.get_reg(ch).map(|r| r.content().clone())
|
lock.get_reg(ch).map(|r| r.content().clone())
|
||||||
@@ -79,7 +97,7 @@ impl RegisterContent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct Registers {
|
pub struct Registers {
|
||||||
default: Register,
|
default: Register,
|
||||||
a: Register,
|
a: Register,
|
||||||
|
|||||||
@@ -294,12 +294,14 @@ impl Read for TermBuffer {
|
|||||||
|
|
||||||
struct KeyCollector {
|
struct KeyCollector {
|
||||||
events: VecDeque<KeyEvent>,
|
events: VecDeque<KeyEvent>,
|
||||||
|
ss3_pending: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyCollector {
|
impl KeyCollector {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
events: VecDeque::new(),
|
events: VecDeque::new(),
|
||||||
|
ss3_pending: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +339,55 @@ impl Default for KeyCollector {
|
|||||||
|
|
||||||
impl Perform for KeyCollector {
|
impl Perform for KeyCollector {
|
||||||
fn print(&mut self, c: char) {
|
fn print(&mut self, c: char) {
|
||||||
|
log::trace!("print: {c:?}");
|
||||||
// vte routes 0x7f (DEL) to print instead of execute
|
// vte routes 0x7f (DEL) to print instead of execute
|
||||||
|
if self.ss3_pending {
|
||||||
|
self.ss3_pending = false;
|
||||||
|
match c {
|
||||||
|
'A' => {
|
||||||
|
self.push(KeyEvent(KeyCode::Up, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'B' => {
|
||||||
|
self.push(KeyEvent(KeyCode::Down, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'C' => {
|
||||||
|
self.push(KeyEvent(KeyCode::Right, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'D' => {
|
||||||
|
self.push(KeyEvent(KeyCode::Left, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'H' => {
|
||||||
|
self.push(KeyEvent(KeyCode::Home, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'F' => {
|
||||||
|
self.push(KeyEvent(KeyCode::End, ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'P' => {
|
||||||
|
self.push(KeyEvent(KeyCode::F(1), ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'Q' => {
|
||||||
|
self.push(KeyEvent(KeyCode::F(2), ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'R' => {
|
||||||
|
self.push(KeyEvent(KeyCode::F(3), ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
'S' => {
|
||||||
|
self.push(KeyEvent(KeyCode::F(4), ModKeys::empty()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c == '\x7f' {
|
if c == '\x7f' {
|
||||||
self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty()));
|
self.push(KeyEvent(KeyCode::Backspace, ModKeys::empty()));
|
||||||
} else {
|
} else {
|
||||||
@@ -346,6 +396,7 @@ impl Perform for KeyCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn execute(&mut self, byte: u8) {
|
fn execute(&mut self, byte: u8) {
|
||||||
|
log::trace!("execute: {byte:#04x}");
|
||||||
let event = match byte {
|
let event = match byte {
|
||||||
0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@
|
0x00 => KeyEvent(KeyCode::Char(' '), ModKeys::CTRL), // Ctrl+Space / Ctrl+@
|
||||||
0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I)
|
0x09 => KeyEvent(KeyCode::Tab, ModKeys::empty()), // Tab (Ctrl+I)
|
||||||
@@ -370,6 +421,9 @@ impl Perform for KeyCollector {
|
|||||||
_ignore: bool,
|
_ignore: bool,
|
||||||
action: char,
|
action: char,
|
||||||
) {
|
) {
|
||||||
|
log::trace!(
|
||||||
|
"CSI dispatch: params={params:?}, intermediates={intermediates:?}, action={action:?}"
|
||||||
|
);
|
||||||
let params: Vec<u16> = params
|
let params: Vec<u16> = params
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| p.first().copied().unwrap_or(0))
|
.map(|p| p.first().copied().unwrap_or(0))
|
||||||
@@ -481,16 +535,11 @@ impl Perform for KeyCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
|
fn esc_dispatch(&mut self, intermediates: &[u8], _ignore: bool, byte: u8) {
|
||||||
// SS3 sequences (ESC O P/Q/R/S for F1-F4)
|
log::trace!("ESC dispatch: intermediates={intermediates:?}, byte={byte:#04x}");
|
||||||
if intermediates == [b'O'] {
|
// SS3 sequences
|
||||||
let key = match byte {
|
if byte == b'O' {
|
||||||
b'P' => KeyCode::F(1),
|
self.ss3_pending = true;
|
||||||
b'Q' => KeyCode::F(2),
|
return;
|
||||||
b'R' => KeyCode::F(3),
|
|
||||||
b'S' => KeyCode::F(4),
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
self.push(KeyEvent(key, ModKeys::empty()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,6 +548,7 @@ pub struct PollReader {
|
|||||||
parser: Parser,
|
parser: Parser,
|
||||||
collector: KeyCollector,
|
collector: KeyCollector,
|
||||||
byte_buf: VecDeque<u8>,
|
byte_buf: VecDeque<u8>,
|
||||||
|
pub verbatim_single: bool,
|
||||||
pub verbatim: bool,
|
pub verbatim: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,6 +558,7 @@ impl PollReader {
|
|||||||
parser: Parser::new(),
|
parser: Parser::new(),
|
||||||
collector: KeyCollector::new(),
|
collector: KeyCollector::new(),
|
||||||
byte_buf: VecDeque::new(),
|
byte_buf: VecDeque::new(),
|
||||||
|
verbatim_single: false,
|
||||||
verbatim: false,
|
verbatim: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,6 +582,18 @@ impl PollReader {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_one_verbatim(&mut self) -> Option<KeyEvent> {
|
||||||
|
if self.byte_buf.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bytes: Vec<u8> = self.byte_buf.drain(..).collect();
|
||||||
|
let verbatim_str = String::from_utf8_lossy(&bytes).to_string();
|
||||||
|
Some(KeyEvent(
|
||||||
|
KeyCode::Verbatim(verbatim_str.into()),
|
||||||
|
ModKeys::empty(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||||
self.byte_buf.extend(bytes);
|
self.byte_buf.extend(bytes);
|
||||||
}
|
}
|
||||||
@@ -544,18 +607,27 @@ impl Default for PollReader {
|
|||||||
|
|
||||||
impl KeyReader for PollReader {
|
impl KeyReader for PollReader {
|
||||||
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
fn read_key(&mut self) -> Result<Option<KeyEvent>, ShErr> {
|
||||||
|
if self.verbatim_single {
|
||||||
|
if let Some(key) = self.read_one_verbatim() {
|
||||||
|
self.verbatim_single = false;
|
||||||
|
return Ok(Some(key));
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
if self.verbatim {
|
if self.verbatim {
|
||||||
if let Some(paste) = self.handle_bracket_paste() {
|
if let Some(paste) = self.handle_bracket_paste() {
|
||||||
return Ok(Some(paste));
|
return Ok(Some(paste));
|
||||||
}
|
}
|
||||||
// If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys
|
// If we're in verbatim mode but haven't seen the end marker yet, don't attempt to parse keys
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else if self.byte_buf.len() == 1
|
} else if self.byte_buf.front() == Some(&b'\x1b') {
|
||||||
&& self.byte_buf.front() == Some(&b'\x1b') {
|
// Escape: if it's the only byte, or the next byte isn't a valid
|
||||||
// User pressed escape
|
// escape sequence prefix ([ or O), emit a standalone Escape
|
||||||
self.byte_buf.pop_front(); // Consume the escape byte
|
if self.byte_buf.len() == 1 || !matches!(self.byte_buf.get(1), Some(b'[') | Some(b'O')) {
|
||||||
|
self.byte_buf.pop_front();
|
||||||
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
|
return Ok(Some(KeyEvent(KeyCode::Esc, ModKeys::empty())));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
while let Some(byte) = self.byte_buf.pop_front() {
|
while let Some(byte) = self.byte_buf.pop_front() {
|
||||||
self.parser.advance(&mut self.collector, &[byte]);
|
self.parser.advance(&mut self.collector, &[byte]);
|
||||||
if let Some(key) = self.collector.pop() {
|
if let Some(key) = self.collector.pop() {
|
||||||
@@ -567,7 +639,7 @@ impl KeyReader for PollReader {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Ok(Some(key))
|
_ => return Ok(Some(key)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
518
src/readline/tests.rs
Normal file
518
src/readline/tests.rs
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
readline::{Prompt, ShedVi, annotate_input},
|
||||||
|
testutil::TestGuard,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn assert_annotated(input: &str, expected: &str) {
|
||||||
|
let result = annotate_input(input);
|
||||||
|
assert_eq!(result, expected, "\nInput: {input:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position.
|
||||||
|
macro_rules! vi_test {
|
||||||
|
{ $($name:ident: $input:expr => $op:expr => $expected_text:expr,$expected_cursor:expr);* } => {
|
||||||
|
$(
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let (mut vi, _g) = test_vi($input);
|
||||||
|
vi.feed_bytes(b"\x1b"); // Start in normal mode
|
||||||
|
vi.process_input().unwrap();
|
||||||
|
|
||||||
|
vi.feed_bytes($op.as_bytes());
|
||||||
|
vi.process_input().unwrap();
|
||||||
|
assert_eq!(vi.editor.as_str(), $expected_text);
|
||||||
|
assert_eq!(vi.editor.cursor.get(), $expected_cursor);
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Annotation Tests =====================
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_simple_command() {
|
||||||
|
assert_annotated("echo hello", "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_pipeline() {
|
||||||
|
assert_annotated(
|
||||||
|
"ls | grep foo",
|
||||||
|
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_conjunction() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo foo && echo bar",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_redirect_output() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo hello > file.txt",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_redirect_append() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo hello >> file.txt",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_redirect_input() {
|
||||||
|
assert_annotated(
|
||||||
|
"cat < file.txt",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_fd_redirect() {
|
||||||
|
assert_annotated("cmd 2>&1", "\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_variable_sub() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo $HOME",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_variable_brace_sub() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo ${HOME}",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_command_sub() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo $(ls)",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_single_quoted_string() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo 'hello world'",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_double_quoted_string() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo \"hello world\"",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_assignment() {
|
||||||
|
assert_annotated("FOO=bar", "\u{e107}FOO=bar\u{e11a}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_assignment_with_command() {
|
||||||
|
assert_annotated(
|
||||||
|
"FOO=bar echo hello",
|
||||||
|
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_if_statement() {
|
||||||
|
assert_annotated(
|
||||||
|
"if true; then echo yes; fi",
|
||||||
|
"\u{e103}if\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}then\u{e11a} \u{e101}echo\u{e11a} \u{e102}yes\u{e11a}\u{e108}; \u{e11a}\u{e103}fi\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_for_loop() {
|
||||||
|
assert_annotated(
|
||||||
|
"for i in a b c; do echo $i; done",
|
||||||
|
"\u{e103}for\u{e11a} \u{e102}i\u{e11a} \u{e103}in\u{e11a} \u{e102}a\u{e11a} \u{e102}b\u{e11a} \u{e102}c\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}\u{e10c}$i\u{e10d}\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_while_loop() {
|
||||||
|
assert_annotated(
|
||||||
|
"while true; do echo hello; done",
|
||||||
|
"\u{e103}while\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_case_statement() {
|
||||||
|
assert_annotated(
|
||||||
|
"case foo in bar) echo bar;; esac",
|
||||||
|
"\u{e103}case\u{e11a} \u{e102}foo\u{e11a} \u{e103}in\u{e11a} \u{e104}bar\u{e109})\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}\u{e108};; \u{e11a}\u{e103}esac\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_brace_group() {
|
||||||
|
assert_annotated(
|
||||||
|
"{ echo hello; }",
|
||||||
|
"\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_comment() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo hello # this is a comment",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_semicolon_sep() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo foo; echo bar",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_escaped_char() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo hello\\ world",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_glob() {
|
||||||
|
assert_annotated(
|
||||||
|
"ls *.txt",
|
||||||
|
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_heredoc_operator() {
|
||||||
|
assert_annotated(
|
||||||
|
"cat <<EOF",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_herestring_operator() {
|
||||||
|
assert_annotated(
|
||||||
|
"cat <<< hello",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_nested_command_sub() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo $(echo $(ls))",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_var_in_double_quotes() {
|
||||||
|
assert_annotated(
|
||||||
|
"echo \"hello $USER\"",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_func_def() {
|
||||||
|
assert_annotated(
|
||||||
|
"foo() { echo hello; }",
|
||||||
|
"\u{e103}foo()\u{e11a} \u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_negate() {
|
||||||
|
assert_annotated(
|
||||||
|
"! echo hello",
|
||||||
|
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_or_conjunction() {
|
||||||
|
assert_annotated(
|
||||||
|
"false || echo fallback",
|
||||||
|
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_complex_pipeline() {
|
||||||
|
assert_annotated(
|
||||||
|
"cat file.txt | grep pattern | wc -l",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e102}file.txt\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}pattern\u{e11a} \u{e104}|\u{e11a} \u{e100}wc\u{e11a} \u{e102}-l\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotate_multiple_redirects() {
|
||||||
|
assert_annotated(
|
||||||
|
"cmd > out.txt 2> err.txt",
|
||||||
|
"\u{e100}cmd\u{e11a} \u{e105}>\u{e11a} \u{e102}out.txt\u{e11a} \u{e105}2>\u{e11a} \u{e102}err.txt\u{e11a}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== Vi Tests =====================
|
||||||
|
|
||||||
|
fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
|
||||||
|
let g = TestGuard::new();
|
||||||
|
let prompt = Prompt::default();
|
||||||
|
let vi = ShedVi::new_no_hist(prompt, g.pty_slave().as_raw_fd())
|
||||||
|
.unwrap()
|
||||||
|
.with_initial(initial);
|
||||||
|
|
||||||
|
(vi, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Why can't I marry a programming language
|
||||||
|
vi_test! {
|
||||||
|
vi_dw_basic : "hello world" => "dw" => "world", 0;
|
||||||
|
vi_dw_middle : "one two three" => "wdw" => "one three", 4;
|
||||||
|
vi_dd_whole_line : "hello world" => "dd" => "", 0;
|
||||||
|
vi_x_single : "hello" => "x" => "ello", 0;
|
||||||
|
vi_x_middle : "hello" => "llx" => "helo", 2;
|
||||||
|
vi_X_backdelete : "hello" => "llX" => "hllo", 1;
|
||||||
|
vi_h_motion : "hello" => "$h" => "hello", 3;
|
||||||
|
vi_l_motion : "hello" => "l" => "hello", 1;
|
||||||
|
vi_h_at_start : "hello" => "h" => "hello", 0;
|
||||||
|
vi_l_at_end : "hello" => "$l" => "hello", 4;
|
||||||
|
vi_w_forward : "one two three" => "w" => "one two three", 4;
|
||||||
|
vi_b_backward : "one two three" => "$b" => "one two three", 8;
|
||||||
|
vi_e_end : "one two three" => "e" => "one two three", 2;
|
||||||
|
vi_ge_back_end : "one two three" => "$ge" => "one two three", 6;
|
||||||
|
vi_w_punctuation : "foo.bar baz" => "w" => "foo.bar baz", 3;
|
||||||
|
vi_e_punctuation : "foo.bar baz" => "e" => "foo.bar baz", 2;
|
||||||
|
vi_b_punctuation : "foo.bar baz" => "$b" => "foo.bar baz", 8;
|
||||||
|
vi_w_at_eol : "hello" => "$w" => "hello", 4;
|
||||||
|
vi_b_at_bol : "hello" => "b" => "hello", 0;
|
||||||
|
vi_W_forward : "foo.bar baz" => "W" => "foo.bar baz", 8;
|
||||||
|
vi_B_backward : "foo.bar baz" => "$B" => "foo.bar baz", 8;
|
||||||
|
vi_E_end : "foo.bar baz" => "E" => "foo.bar baz", 6;
|
||||||
|
vi_gE_back_end : "one two three" => "$gE" => "one two three", 6;
|
||||||
|
vi_W_skip_punct : "one-two three" => "W" => "one-two three", 8;
|
||||||
|
vi_B_skip_punct : "one two-three" => "$B" => "one two-three", 4;
|
||||||
|
vi_E_skip_punct : "one-two three" => "E" => "one-two three", 6;
|
||||||
|
vi_dW_big : "foo.bar baz" => "dW" => "baz", 0;
|
||||||
|
vi_cW_big : "foo.bar baz" => "cWx\x1b" => "x baz", 0;
|
||||||
|
vi_zero_bol : " hello" => "$0" => " hello", 0;
|
||||||
|
vi_caret_first_char : " hello" => "$^" => " hello", 2;
|
||||||
|
vi_dollar_eol : "hello world" => "$" => "hello world", 10;
|
||||||
|
vi_g_last_nonws : "hello " => "g_" => "hello ", 4;
|
||||||
|
vi_g_no_trailing : "hello" => "g_" => "hello", 4;
|
||||||
|
vi_pipe_column : "hello world" => "6|" => "hello world", 5;
|
||||||
|
vi_pipe_col1 : "hello world" => "1|" => "hello world", 0;
|
||||||
|
vi_I_insert_front : " hello" => "Iworld \x1b" => " world hello", 7;
|
||||||
|
vi_A_append_end : "hello" => "A world\x1b" => "hello world", 10;
|
||||||
|
vi_f_find : "hello world" => "fo" => "hello world", 4;
|
||||||
|
vi_F_find_back : "hello world" => "$Fo" => "hello world", 7;
|
||||||
|
vi_t_till : "hello world" => "tw" => "hello world", 5;
|
||||||
|
vi_T_till_back : "hello world" => "$To" => "hello world", 8;
|
||||||
|
vi_f_no_match : "hello" => "fz" => "hello", 0;
|
||||||
|
vi_semicolon_repeat : "abcabc" => "fa;;" => "abcabc", 3;
|
||||||
|
vi_comma_reverse : "abcabc" => "fa;;," => "abcabc", 0;
|
||||||
|
vi_df_semicolon : "abcabc" => "fa;;dfa" => "abcabc", 3;
|
||||||
|
vi_t_at_target : "aab" => "lta" => "aab", 1;
|
||||||
|
vi_D_to_end : "hello world" => "wD" => "hello ", 5;
|
||||||
|
vi_d_dollar : "hello world" => "wd$" => "hello ", 5;
|
||||||
|
vi_d0_to_start : "hello world" => "$d0" => "d", 0;
|
||||||
|
vi_dw_multiple : "one two three" => "d2w" => "three", 0;
|
||||||
|
vi_dt_char : "hello world" => "dtw" => "world", 0;
|
||||||
|
vi_df_char : "hello world" => "dfw" => "orld", 0;
|
||||||
|
vi_dh_back : "hello" => "lldh" => "hllo", 1;
|
||||||
|
vi_dl_forward : "hello" => "dl" => "ello", 0;
|
||||||
|
vi_dge_back_end : "one two three" => "$dge" => "one tw", 5;
|
||||||
|
vi_dG_to_end : "hello world" => "dG" => "", 0;
|
||||||
|
vi_dgg_to_start : "hello world" => "$dgg" => "", 0;
|
||||||
|
vi_d_semicolon : "abcabc" => "fad;" => "abcabc", 3;
|
||||||
|
vi_cw_basic : "hello world" => "cwfoo\x1b" => "foo world", 2;
|
||||||
|
vi_C_to_end : "hello world" => "wCfoo\x1b" => "hello foo", 8;
|
||||||
|
vi_cc_whole : "hello world" => "ccfoo\x1b" => "foo", 2;
|
||||||
|
vi_ct_char : "hello world" => "ctwfoo\x1b" => "fooworld", 2;
|
||||||
|
vi_s_single : "hello" => "sfoo\x1b" => "fooello", 2;
|
||||||
|
vi_S_whole_line : "hello world" => "Sfoo\x1b" => "foo", 2;
|
||||||
|
vi_cl_forward : "hello" => "clX\x1b" => "Xello", 0;
|
||||||
|
vi_ch_backward : "hello" => "llchX\x1b" => "hXllo", 1;
|
||||||
|
vi_cb_word_back : "hello world" => "$cbfoo\x1b" => "hello food", 8;
|
||||||
|
vi_ce_word_end : "hello world" => "cefoo\x1b" => "foo world", 2;
|
||||||
|
vi_c0_to_start : "hello world" => "wc0foo\x1b" => "fooworld", 2;
|
||||||
|
vi_yw_p_basic : "hello world" => "ywwP" => "hello hello world", 11;
|
||||||
|
vi_dw_p_paste : "hello world" => "dwP" => "hello world", 5;
|
||||||
|
vi_dd_p_paste : "hello world" => "ddp" => "\nhello world", 1;
|
||||||
|
vi_y_dollar_p : "hello world" => "wy$P" => "hello worldworld", 10;
|
||||||
|
vi_ye_p : "hello world" => "yewP" => "hello helloworld", 10;
|
||||||
|
vi_yy_p : "hello world" => "yyp" => "hello world\nhello world", 12;
|
||||||
|
vi_Y_p : "hello world" => "Yp" => "hhello worldello world", 11;
|
||||||
|
vi_p_after_x : "hello" => "xp" => "ehllo", 1;
|
||||||
|
vi_P_before : "hello" => "llxP" => "hello", 2;
|
||||||
|
vi_paste_empty : "hello" => "p" => "hello", 0;
|
||||||
|
vi_r_replace : "hello" => "ra" => "aello", 0;
|
||||||
|
vi_r_middle : "hello" => "llra" => "healo", 2;
|
||||||
|
vi_r_at_end : "hello" => "$ra" => "hella", 4;
|
||||||
|
vi_r_space : "hello" => "r " => " ello", 0;
|
||||||
|
vi_r_with_count : "hello" => "3rx" => "xxxlo", 2;
|
||||||
|
vi_tilde_single : "hello" => "~" => "Hello", 1;
|
||||||
|
vi_tilde_count : "hello" => "3~" => "HELlo", 3;
|
||||||
|
vi_tilde_at_end : "HELLO" => "$~" => "HELLo", 4;
|
||||||
|
vi_tilde_mixed : "hElLo" => "5~" => "HeLlO", 4;
|
||||||
|
vi_gu_word : "HELLO world" => "guw" => "hello world", 0;
|
||||||
|
vi_gU_word : "hello WORLD" => "gUw" => "HELLO WORLD", 0;
|
||||||
|
vi_gu_dollar : "HELLO WORLD" => "gu$" => "hello world", 0;
|
||||||
|
vi_gU_dollar : "hello world" => "gU$" => "HELLO WORLD", 0;
|
||||||
|
vi_gu_0 : "HELLO WORLD" => "$gu0" => "hello worlD", 0;
|
||||||
|
vi_gU_0 : "hello world" => "$gU0" => "HELLO WORLd", 0;
|
||||||
|
vi_gtilde_word : "hello WORLD" => "g~w" => "HELLO WORLD", 0;
|
||||||
|
vi_gtilde_dollar : "hello WORLD" => "g~$" => "HELLO world", 0;
|
||||||
|
vi_diw_inner : "one two three" => "wdiw" => "one three", 4;
|
||||||
|
vi_ciw_replace : "hello world" => "ciwfoo\x1b" => "foo world", 2;
|
||||||
|
vi_daw_around : "one two three" => "wdaw" => "one three", 4;
|
||||||
|
vi_yiw_p : "hello world" => "yiwAp \x1bp" => "hello worldp hello", 17;
|
||||||
|
vi_diW_big_inner : "one-two three" => "diW" => " three", 0;
|
||||||
|
vi_daW_big_around : "one two-three end" => "wdaW" => "one end", 4;
|
||||||
|
vi_ciW_big : "one-two three" => "ciWx\x1b" => "x three", 0;
|
||||||
|
vi_di_dquote : "one \"two\" three" => "f\"di\"" => "one \"\" three", 5;
|
||||||
|
vi_da_dquote : "one \"two\" three" => "f\"da\"" => "one three", 4;
|
||||||
|
vi_ci_dquote : "one \"two\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
|
||||||
|
vi_di_squote : "one 'two' three" => "f'di'" => "one '' three", 5;
|
||||||
|
vi_da_squote : "one 'two' three" => "f'da'" => "one three", 4;
|
||||||
|
vi_di_backtick : "one `two` three" => "f`di`" => "one `` three", 5;
|
||||||
|
vi_da_backtick : "one `two` three" => "f`da`" => "one three", 4;
|
||||||
|
vi_ci_dquote_empty : "one \"\" three" => "f\"ci\"x\x1b" => "one \"x\" three", 5;
|
||||||
|
vi_di_paren : "one (two) three" => "f(di(" => "one () three", 5;
|
||||||
|
vi_da_paren : "one (two) three" => "f(da(" => "one three", 4;
|
||||||
|
vi_ci_paren : "one (two) three" => "f(ci(x\x1b" => "one (x) three", 5;
|
||||||
|
vi_di_brace : "one {two} three" => "f{di{" => "one {} three", 5;
|
||||||
|
vi_da_brace : "one {two} three" => "f{da{" => "one three", 4;
|
||||||
|
vi_di_bracket : "one [two] three" => "f[di[" => "one [] three", 5;
|
||||||
|
vi_da_bracket : "one [two] three" => "f[da[" => "one three", 4;
|
||||||
|
vi_di_angle : "one <two> three" => "f<di<" => "one <> three", 5;
|
||||||
|
vi_da_angle : "one <two> three" => "f<da<" => "one three", 4;
|
||||||
|
vi_di_paren_nested : "fn(a, (b, c))" => "f(di(" => "fn()", 3;
|
||||||
|
vi_di_paren_empty : "fn() end" => "f(di(" => "fn() end", 3;
|
||||||
|
vi_dib_alias : "one (two) three" => "f(dib" => "one () three", 5;
|
||||||
|
vi_diB_alias : "one {two} three" => "f{diB" => "one {} three", 5;
|
||||||
|
vi_percent_paren : "(hello) world" => "%" => "(hello) world", 6;
|
||||||
|
vi_percent_brace : "{hello} world" => "%" => "{hello} world", 6;
|
||||||
|
vi_percent_bracket : "[hello] world" => "%" => "[hello] world", 6;
|
||||||
|
vi_percent_from_close: "(hello) world" => "f)%" => "(hello) world", 0;
|
||||||
|
vi_d_percent_paren : "(hello) world" => "d%" => " world", 0;
|
||||||
|
vi_i_insert : "hello" => "iX\x1b" => "Xhello", 0;
|
||||||
|
vi_a_append : "hello" => "aX\x1b" => "hXello", 1;
|
||||||
|
vi_I_front : " hello" => "IX\x1b" => " Xhello", 2;
|
||||||
|
vi_A_end : "hello" => "AX\x1b" => "helloX", 5;
|
||||||
|
vi_o_open_below : "hello" => "oworld\x1b" => "hello\nworld", 10;
|
||||||
|
vi_O_open_above : "hello" => "Oworld\x1b" => "world\nhello", 4;
|
||||||
|
vi_empty_input : "" => "i hello\x1b" => " hello", 5;
|
||||||
|
vi_insert_escape : "hello" => "aX\x1b" => "hXello", 1;
|
||||||
|
vi_ctrl_w_del_word : "hello world" => "A\x17\x1b" => "hello ", 5;
|
||||||
|
vi_ctrl_h_backspace : "hello" => "A\x08\x1b" => "hell", 3;
|
||||||
|
vi_u_undo_delete : "hello world" => "dwu" => "hello world", 0;
|
||||||
|
vi_u_undo_change : "hello world" => "ciwfoo\x1bu" => "hello world", 0;
|
||||||
|
vi_u_undo_x : "hello" => "xu" => "hello", 0;
|
||||||
|
vi_ctrl_r_redo : "hello" => "xu\x12" => "ello", 0;
|
||||||
|
vi_u_multiple : "hello world" => "xdwu" => "ello world", 0;
|
||||||
|
vi_redo_after_undo : "hello world" => "dwu\x12" => "world", 0;
|
||||||
|
vi_dot_repeat_x : "hello" => "x." => "llo", 0;
|
||||||
|
vi_dot_repeat_dw : "one two three" => "dw." => "three", 0;
|
||||||
|
vi_dot_repeat_cw : "one two three" => "cwfoo\x1bw." => "foo foo three", 6;
|
||||||
|
vi_dot_repeat_r : "hello" => "ra.." => "aello", 0;
|
||||||
|
vi_dot_repeat_s : "hello" => "sX\x1bl." => "XXllo", 1;
|
||||||
|
vi_count_h : "hello world" => "$3h" => "hello world", 7;
|
||||||
|
vi_count_l : "hello world" => "3l" => "hello world", 3;
|
||||||
|
vi_count_w : "one two three four" => "2w" => "one two three four", 8;
|
||||||
|
vi_count_b : "one two three four" => "$2b" => "one two three four", 8;
|
||||||
|
vi_count_x : "hello" => "3x" => "lo", 0;
|
||||||
|
vi_count_dw : "one two three four" => "2dw" => "three four", 0;
|
||||||
|
vi_verb_count_motion : "one two three four" => "d2w" => "three four", 0;
|
||||||
|
vi_count_s : "hello" => "3sX\x1b" => "Xlo", 0;
|
||||||
|
vi_indent_line : "hello" => ">>" => "\thello", 1;
|
||||||
|
vi_dedent_line : "\thello" => "<<" => "hello", 0;
|
||||||
|
vi_indent_double : "hello" => ">>>>" => "\t\thello", 2;
|
||||||
|
vi_J_join_lines : "hello\nworld" => "J" => "hello world", 5;
|
||||||
|
vi_v_u_lower : "HELLO" => "vlllu" => "hellO", 0;
|
||||||
|
vi_v_U_upper : "hello" => "vlllU" => "HELLo", 0;
|
||||||
|
vi_v_d_delete : "hello world" => "vwwd" => "", 0;
|
||||||
|
vi_v_x_delete : "hello world" => "vwwx" => "", 0;
|
||||||
|
vi_v_c_change : "hello world" => "vwcfoo\x1b" => "fooorld", 2;
|
||||||
|
vi_v_y_p_yank : "hello world" => "vwyAp \x1bp" => "hello worldp hello w", 19;
|
||||||
|
vi_v_dollar_d : "hello world" => "wv$d" => "hello ", 5;
|
||||||
|
vi_v_0_d : "hello world" => "$v0d" => "", 0;
|
||||||
|
vi_ve_d : "hello world" => "ved" => " world", 0;
|
||||||
|
vi_v_o_swap : "hello world" => "vllod" => "lo world", 0;
|
||||||
|
vi_v_r_replace : "hello" => "vlllrx" => "xxxxo", 0;
|
||||||
|
vi_v_tilde_case : "hello" => "vlll~" => "HELLo", 0;
|
||||||
|
vi_V_d_delete : "hello world" => "Vd" => "", 0;
|
||||||
|
vi_V_y_p : "hello world" => "Vyp" => "hello world\nhello world", 12;
|
||||||
|
vi_V_S_change : "hello world" => "VSfoo\x1b" => "foo", 2;
|
||||||
|
vi_ctrl_a_inc : "num 5 end" => "w\x01" => "num 6 end", 4;
|
||||||
|
vi_ctrl_x_dec : "num 5 end" => "w\x18" => "num 4 end", 4;
|
||||||
|
vi_ctrl_a_negative : "num -3 end" => "w\x01" => "num -2 end", 4;
|
||||||
|
vi_ctrl_x_to_neg : "num 0 end" => "w\x18" => "num -1 end", 4;
|
||||||
|
vi_ctrl_a_count : "num 5 end" => "w3\x01" => "num 8 end", 4;
|
||||||
|
vi_ctrl_a_width : "num -00001 end" => "w\x01" => "num 00000 end", 4;
|
||||||
|
vi_delete_empty : "" => "x" => "", 0;
|
||||||
|
vi_undo_on_empty : "" => "u" => "", 0;
|
||||||
|
vi_w_single_char : "a b c" => "w" => "a b c", 2;
|
||||||
|
vi_dw_last_word : "hello" => "dw" => "", 0;
|
||||||
|
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_vw_doesnt_crash : "" => "vw" => "", 0;
|
||||||
|
vi_indent_cursor_pos : "echo foo" => ">>" => "\techo foo", 1;
|
||||||
|
vi_join_indent_lines : "echo foo\n\t\techo bar" => "J" => "echo foo echo bar", 8
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vi_auto_indent() {
|
||||||
|
let (mut vi, _g) = test_vi("");
|
||||||
|
|
||||||
|
// Type each line and press Enter separately so auto-indent triggers
|
||||||
|
let lines = [
|
||||||
|
"func() {",
|
||||||
|
"case foo in",
|
||||||
|
"bar)",
|
||||||
|
"while true; do",
|
||||||
|
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (i, line) in lines.iter().enumerate() {
|
||||||
|
vi.feed_bytes(line.as_bytes());
|
||||||
|
if i != lines.len() - 1 {
|
||||||
|
vi.feed_bytes(b"\r");
|
||||||
|
}
|
||||||
|
vi.process_input().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
vi.editor.as_str(),
|
||||||
|
"func() {\n\tcase foo in\n\t\tbar)\n\t\t\twhile true; do\n\t\t\t\techo foo \\\n\t\t\t\t\tbar \\\n\t\t\t\t\tbiz \\\n\t\t\t\t\tbazz\n\t\t\t\tbreak\n\t\t\tdone\n\t\t;;\n\tesac\n}"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -343,7 +343,7 @@ pub enum Motion {
|
|||||||
HalfOfScreen,
|
HalfOfScreen,
|
||||||
HalfOfScreenLineText,
|
HalfOfScreenLineText,
|
||||||
WholeBuffer,
|
WholeBuffer,
|
||||||
BeginningOfBuffer,
|
StartOfBuffer,
|
||||||
EndOfBuffer,
|
EndOfBuffer,
|
||||||
ToColumn,
|
ToColumn,
|
||||||
ToDelimMatch,
|
ToDelimMatch,
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ impl ViInsert {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
pub fn record_cmd(mut self, cmd: ViCmd) -> Self {
|
||||||
|
self.cmds.push(cmd);
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
pub fn with_count(mut self, repeat_count: u16) -> Self {
|
||||||
self.repeat_count = repeat_count;
|
self.repeat_count = repeat_count;
|
||||||
self
|
self
|
||||||
@@ -62,7 +66,9 @@ impl ViMode for ViInsert {
|
|||||||
flags: Default::default(),
|
flags: Default::default(),
|
||||||
}),
|
}),
|
||||||
E(K::Verbatim(seq), _) => {
|
E(K::Verbatim(seq), _) => {
|
||||||
self.pending_cmd.set_verb(VerbCmd(1, Verb::Insert(seq.to_string())));
|
self
|
||||||
|
.pending_cmd
|
||||||
|
.set_verb(VerbCmd(1, Verb::Insert(seq.to_string())));
|
||||||
self.register_and_return()
|
self.register_and_return()
|
||||||
}
|
}
|
||||||
E(K::Char('W'), M::CTRL) => {
|
E(K::Char('W'), M::CTRL) => {
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ impl ViNormal {
|
|||||||
'g' => {
|
'g' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||||
}
|
}
|
||||||
'e' => {
|
'e' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
|
|||||||
@@ -4,19 +4,11 @@ use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct ViVerbatim {
|
pub struct ViVerbatim {
|
||||||
pending_seq: String,
|
|
||||||
sent_cmd: Vec<ViCmd>,
|
sent_cmd: Vec<ViCmd>,
|
||||||
repeat_count: u16,
|
repeat_count: u16,
|
||||||
read_one: bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ViVerbatim {
|
impl ViVerbatim {
|
||||||
pub fn read_one() -> Self {
|
|
||||||
Self {
|
|
||||||
read_one: true,
|
|
||||||
..Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
@@ -31,7 +23,7 @@ impl ViVerbatim {
|
|||||||
impl ViMode for ViVerbatim {
|
impl ViMode for ViVerbatim {
|
||||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
match key {
|
match key {
|
||||||
E(K::Verbatim(seq), _mods) if self.read_one => {
|
E(K::Verbatim(seq), _mods) => {
|
||||||
log::debug!("Received verbatim key sequence: {:?}", seq);
|
log::debug!("Received verbatim key sequence: {:?}", seq);
|
||||||
let cmd = ViCmd {
|
let cmd = ViCmd {
|
||||||
register: RegisterName::default(),
|
register: RegisterName::default(),
|
||||||
@@ -43,22 +35,6 @@ impl ViMode for ViVerbatim {
|
|||||||
self.sent_cmd.push(cmd.clone());
|
self.sent_cmd.push(cmd.clone());
|
||||||
Some(cmd)
|
Some(cmd)
|
||||||
}
|
}
|
||||||
E(K::Verbatim(seq), _mods) => {
|
|
||||||
self.pending_seq.push_str(&seq);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
E(K::BracketedPasteEnd, _mods) => {
|
|
||||||
log::debug!("Received verbatim paste: {:?}", self.pending_seq);
|
|
||||||
let cmd = ViCmd {
|
|
||||||
register: RegisterName::default(),
|
|
||||||
verb: Some(VerbCmd(1, Verb::Insert(self.pending_seq.clone()))),
|
|
||||||
motion: None,
|
|
||||||
raw_seq: std::mem::take(&mut self.pending_seq),
|
|
||||||
flags: CmdFlags::EXIT_CUR_MODE,
|
|
||||||
};
|
|
||||||
self.sent_cmd.push(cmd.clone());
|
|
||||||
Some(cmd)
|
|
||||||
}
|
|
||||||
_ => common_cmds(key),
|
_ => common_cmds(key),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ impl ViVisual {
|
|||||||
let ch = chars_clone.next()?;
|
let ch = chars_clone.next()?;
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
register,
|
register,
|
||||||
verb: Some(VerbCmd(1, Verb::ReplaceChar(ch))),
|
verb: Some(VerbCmd(1, Verb::ReplaceCharInplace(ch, 1))),
|
||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
@@ -237,6 +237,24 @@ impl ViVisual {
|
|||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
's' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Delete)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
'S' => {
|
||||||
|
return Some(ViCmd {
|
||||||
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Change)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
|
}
|
||||||
'U' => {
|
'U' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
register,
|
register,
|
||||||
@@ -283,8 +301,13 @@ impl ViVisual {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
return Some(ViCmd {
|
||||||
break 'verb_parse Some(VerbCmd(count, Verb::Yank));
|
register,
|
||||||
|
verb: Some(VerbCmd(count, Verb::Yank)),
|
||||||
|
motion: None,
|
||||||
|
raw_seq: self.take_cmd(),
|
||||||
|
flags: CmdFlags::empty(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
'd' => {
|
'd' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -335,7 +358,7 @@ impl ViVisual {
|
|||||||
'g' => {
|
'g' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::BeginningOfBuffer));
|
break 'motion_parse Some(MotionCmd(count, Motion::StartOfBuffer));
|
||||||
}
|
}
|
||||||
'e' => {
|
'e' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
|
|||||||
62
src/shopt.rs
62
src/shopt.rs
@@ -146,6 +146,7 @@ pub struct ShOptCore {
|
|||||||
pub bell_enabled: bool,
|
pub bell_enabled: bool,
|
||||||
pub max_recurse_depth: usize,
|
pub max_recurse_depth: usize,
|
||||||
pub xpg_echo: bool,
|
pub xpg_echo: bool,
|
||||||
|
pub noclobber: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShOptCore {
|
impl ShOptCore {
|
||||||
@@ -238,6 +239,15 @@ impl ShOptCore {
|
|||||||
};
|
};
|
||||||
self.xpg_echo = val;
|
self.xpg_echo = val;
|
||||||
}
|
}
|
||||||
|
"noclobber" => {
|
||||||
|
let Ok(val) = val.parse::<bool>() else {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::SyntaxErr,
|
||||||
|
"shopt: expected 'true' or 'false' for noclobber value",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
self.noclobber = val;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
@@ -304,6 +314,12 @@ impl ShOptCore {
|
|||||||
output.push_str(&format!("{}", self.xpg_echo));
|
output.push_str(&format!("{}", self.xpg_echo));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
|
"noclobber" => {
|
||||||
|
let mut output =
|
||||||
|
String::from("Prevent > from overwriting existing files (use >| to override)\n");
|
||||||
|
output.push_str(&format!("{}", self.noclobber));
|
||||||
|
Ok(Some(output))
|
||||||
|
}
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'core' option '{query}'"),
|
format!("shopt: Unexpected 'core' option '{query}'"),
|
||||||
@@ -327,6 +343,7 @@ impl Display for ShOptCore {
|
|||||||
output.push(format!("bell_enabled = {}", self.bell_enabled));
|
output.push(format!("bell_enabled = {}", self.bell_enabled));
|
||||||
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
|
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
|
||||||
output.push(format!("xpg_echo = {}", self.xpg_echo));
|
output.push(format!("xpg_echo = {}", self.xpg_echo));
|
||||||
|
output.push(format!("noclobber = {}", self.noclobber));
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
@@ -346,6 +363,7 @@ impl Default for ShOptCore {
|
|||||||
bell_enabled: true,
|
bell_enabled: true,
|
||||||
max_recurse_depth: 1000,
|
max_recurse_depth: 1000,
|
||||||
xpg_echo: false,
|
xpg_echo: false,
|
||||||
|
noclobber: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -360,6 +378,8 @@ pub struct ShOptPrompt {
|
|||||||
pub linebreak_on_incomplete: bool,
|
pub linebreak_on_incomplete: bool,
|
||||||
pub leader: String,
|
pub leader: String,
|
||||||
pub line_numbers: bool,
|
pub line_numbers: bool,
|
||||||
|
pub screensaver_cmd: String,
|
||||||
|
pub screensaver_idle_time: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShOptPrompt {
|
impl ShOptPrompt {
|
||||||
@@ -431,6 +451,18 @@ impl ShOptPrompt {
|
|||||||
};
|
};
|
||||||
self.line_numbers = val;
|
self.line_numbers = val;
|
||||||
}
|
}
|
||||||
|
"screensaver_cmd" => {
|
||||||
|
self.screensaver_cmd = val.to_string();
|
||||||
|
}
|
||||||
|
"screensaver_idle_time" => {
|
||||||
|
let Ok(val) = val.parse::<usize>() else {
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::SyntaxErr,
|
||||||
|
"shopt: expected a positive integer for screensaver_idle_time value",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
self.screensaver_idle_time = val;
|
||||||
|
}
|
||||||
"custom" => {
|
"custom" => {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
@@ -496,6 +528,17 @@ impl ShOptPrompt {
|
|||||||
output.push_str(&format!("{}", self.line_numbers));
|
output.push_str(&format!("{}", self.line_numbers));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
|
"screensaver_cmd" => {
|
||||||
|
let mut output = String::from("Command to execute as a screensaver after idle timeout\n");
|
||||||
|
output.push_str(&self.screensaver_cmd);
|
||||||
|
Ok(Some(output))
|
||||||
|
}
|
||||||
|
"screensaver_idle_time" => {
|
||||||
|
let mut output =
|
||||||
|
String::from("Idle time in seconds before running screensaver_cmd (0 = disabled)\n");
|
||||||
|
output.push_str(&format!("{}", self.screensaver_idle_time));
|
||||||
|
Ok(Some(output))
|
||||||
|
}
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'prompt' option '{query}'"),
|
format!("shopt: Unexpected 'prompt' option '{query}'"),
|
||||||
@@ -519,6 +562,11 @@ impl Display for ShOptPrompt {
|
|||||||
));
|
));
|
||||||
output.push(format!("leader = {}", self.leader));
|
output.push(format!("leader = {}", self.leader));
|
||||||
output.push(format!("line_numbers = {}", self.line_numbers));
|
output.push(format!("line_numbers = {}", self.line_numbers));
|
||||||
|
output.push(format!("screensaver_cmd = {}", self.screensaver_cmd));
|
||||||
|
output.push(format!(
|
||||||
|
"screensaver_idle_time = {}",
|
||||||
|
self.screensaver_idle_time
|
||||||
|
));
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
@@ -537,6 +585,8 @@ impl Default for ShOptPrompt {
|
|||||||
linebreak_on_incomplete: true,
|
linebreak_on_incomplete: true,
|
||||||
leader: "\\".to_string(),
|
leader: "\\".to_string(),
|
||||||
line_numbers: true,
|
line_numbers: true,
|
||||||
|
screensaver_cmd: String::new(),
|
||||||
|
screensaver_idle_time: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -548,9 +598,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn all_core_fields_covered() {
|
fn all_core_fields_covered() {
|
||||||
let ShOptCore {
|
let ShOptCore {
|
||||||
dotglob, autocd, hist_ignore_dupes, max_hist,
|
dotglob,
|
||||||
interactive_comments, auto_hist, bell_enabled, max_recurse_depth,
|
autocd,
|
||||||
|
hist_ignore_dupes,
|
||||||
|
max_hist,
|
||||||
|
interactive_comments,
|
||||||
|
auto_hist,
|
||||||
|
bell_enabled,
|
||||||
|
max_recurse_depth,
|
||||||
xpg_echo,
|
xpg_echo,
|
||||||
|
noclobber,
|
||||||
} = ShOptCore::default();
|
} = ShOptCore::default();
|
||||||
// If a field is added to the struct, this destructure fails to compile.
|
// If a field is added to the struct, this destructure fails to compile.
|
||||||
let _ = (
|
let _ = (
|
||||||
@@ -563,6 +620,7 @@ mod tests {
|
|||||||
bell_enabled,
|
bell_enabled,
|
||||||
max_recurse_depth,
|
max_recurse_depth,
|
||||||
xpg_echo,
|
xpg_echo,
|
||||||
|
noclobber,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
src/state.rs
116
src/state.rs
@@ -1,5 +1,11 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::RefCell, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration
|
cell::RefCell,
|
||||||
|
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
|
||||||
|
fmt::Display,
|
||||||
|
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign},
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::unistd::{User, gethostname, getppid};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
@@ -36,7 +42,7 @@ thread_local! {
|
|||||||
pub static SHED: Shed = Shed::new();
|
pub static SHED: Shed = Shed::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone,Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Shed {
|
pub struct Shed {
|
||||||
pub jobs: RefCell<JobTab>,
|
pub jobs: RefCell<JobTab>,
|
||||||
pub var_scopes: RefCell<ScopeStack>,
|
pub var_scopes: RefCell<ScopeStack>,
|
||||||
@@ -315,6 +321,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 +474,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 +499,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);
|
||||||
}
|
}
|
||||||
@@ -495,7 +534,10 @@ impl ScopeStack {
|
|||||||
return val.clone();
|
return val.clone();
|
||||||
}
|
}
|
||||||
// Positional params are scope-local; only check the current scope
|
// Positional params are scope-local; only check the current scope
|
||||||
if matches!(param, ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount) {
|
if matches!(
|
||||||
|
param,
|
||||||
|
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount
|
||||||
|
) {
|
||||||
if let Some(scope) = self.scopes.last() {
|
if let Some(scope) = self.scopes.last() {
|
||||||
return scope.get_param(param);
|
return scope.get_param(param);
|
||||||
}
|
}
|
||||||
@@ -978,19 +1020,7 @@ macro_rules! impl_var_from {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl_var_from!(
|
impl_var_from!(
|
||||||
i8,
|
i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, String, &str, bool
|
||||||
i16,
|
|
||||||
i32,
|
|
||||||
i64,
|
|
||||||
isize,
|
|
||||||
u8,
|
|
||||||
u16,
|
|
||||||
u32,
|
|
||||||
u64,
|
|
||||||
usize,
|
|
||||||
String,
|
|
||||||
&str,
|
|
||||||
bool
|
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
@@ -1012,7 +1042,7 @@ impl VarTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let vars = HashMap::new();
|
let vars = Self::init_sh_vars();
|
||||||
let params = Self::init_params();
|
let params = Self::init_params();
|
||||||
Self::init_env();
|
Self::init_env();
|
||||||
let mut var_tab = Self {
|
let mut var_tab = Self {
|
||||||
@@ -1031,6 +1061,11 @@ impl VarTab {
|
|||||||
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
|
params.insert(ShellParam::LastJob, "".into()); // PID of the last background job (if any)
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
|
fn init_sh_vars() -> HashMap<String, Var> {
|
||||||
|
let mut vars = HashMap::new();
|
||||||
|
vars.insert("COMP_WORDBREAKS".into(), " \t\n\"'@><=;|&(".into());
|
||||||
|
vars
|
||||||
|
}
|
||||||
fn init_env() {
|
fn init_env() {
|
||||||
let pathbuf_to_string =
|
let pathbuf_to_string =
|
||||||
|pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
|
|pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
|
||||||
@@ -1295,6 +1330,15 @@ impl VarTab {
|
|||||||
.get(&ShellParam::Status)
|
.get(&ShellParam::Status)
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or("0".into()),
|
.unwrap_or("0".into()),
|
||||||
|
ShellParam::AllArgsStr => {
|
||||||
|
let ifs = get_separator();
|
||||||
|
self
|
||||||
|
.params
|
||||||
|
.get(&ShellParam::AllArgs)
|
||||||
|
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
_ => self
|
_ => self
|
||||||
.params
|
.params
|
||||||
.get(¶m)
|
.get(¶m)
|
||||||
@@ -1305,8 +1349,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 +1378,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 +1404,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;
|
||||||
@@ -1782,6 +1851,15 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_separator() -> String {
|
||||||
|
env::var("IFS")
|
||||||
|
.unwrap_or(String::from(" "))
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_status() -> i32 {
|
pub fn get_status() -> i32 {
|
||||||
read_vars(|v| v.get_param(ShellParam::Status))
|
read_vars(|v| v.get_param(ShellParam::Status))
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
|
|||||||
176
src/testutil.rs
176
src/testutil.rs
@@ -1,9 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::{HashMap, HashSet},
|
||||||
env,
|
env,
|
||||||
os::fd::{AsRawFd, OwnedFd},
|
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{self, MutexGuard},
|
sync::{self, Arc, MutexGuard},
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::{
|
use nix::{
|
||||||
@@ -14,10 +14,12 @@ use nix::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
expand::expand_aliases,
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{Redir, RedirType, execute::exec_input},
|
parse::{ParsedSrc, Redir, RedirType, execute::exec_input, lex::LexFlags},
|
||||||
procio::{IoFrame, IoMode, RedirGuard},
|
procio::{IoFrame, IoMode, RedirGuard},
|
||||||
state::{MetaTab, SHED},
|
readline::register::{restore_registers, save_registers},
|
||||||
|
state::{MetaTab, SHED, read_logic},
|
||||||
};
|
};
|
||||||
|
|
||||||
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
static TEST_MUTEX: sync::Mutex<()> = sync::Mutex::new(());
|
||||||
@@ -41,9 +43,9 @@ pub struct TestGuard {
|
|||||||
old_cwd: PathBuf,
|
old_cwd: PathBuf,
|
||||||
saved_env: HashMap<String, String>,
|
saved_env: HashMap<String, String>,
|
||||||
pty_master: OwnedFd,
|
pty_master: OwnedFd,
|
||||||
_pty_slave: OwnedFd,
|
pty_slave: OwnedFd,
|
||||||
|
|
||||||
cleanups: Vec<Box<dyn FnOnce()>>
|
cleanups: Vec<Box<dyn FnOnce()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestGuard {
|
impl TestGuard {
|
||||||
@@ -51,56 +53,55 @@ impl TestGuard {
|
|||||||
let _lock = TEST_MUTEX.lock().unwrap();
|
let _lock = TEST_MUTEX.lock().unwrap();
|
||||||
|
|
||||||
let pty = openpty(None, None).unwrap();
|
let pty = openpty(None, None).unwrap();
|
||||||
let (pty_master,pty_slave) = (pty.master, pty.slave);
|
let (pty_master, pty_slave) = (pty.master, pty.slave);
|
||||||
let mut attrs = tcgetattr(&pty_slave).unwrap();
|
let mut attrs = tcgetattr(&pty_slave).unwrap();
|
||||||
attrs.output_flags &= !OutputFlags::ONLCR;
|
attrs.output_flags &= !OutputFlags::ONLCR;
|
||||||
tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap();
|
tcsetattr(&pty_slave, SetArg::TCSANOW, &attrs).unwrap();
|
||||||
|
|
||||||
let mut frame = IoFrame::new();
|
let mut frame = IoFrame::new();
|
||||||
frame.push(
|
frame.push(Redir::new(
|
||||||
Redir::new(
|
|
||||||
IoMode::Fd {
|
IoMode::Fd {
|
||||||
tgt_fd: 0,
|
tgt_fd: 0,
|
||||||
src_fd: pty_slave.as_raw_fd(),
|
src_fd: pty_slave.as_raw_fd(),
|
||||||
},
|
},
|
||||||
RedirType::Input,
|
RedirType::Input,
|
||||||
),
|
));
|
||||||
);
|
frame.push(Redir::new(
|
||||||
frame.push(
|
|
||||||
Redir::new(
|
|
||||||
IoMode::Fd {
|
IoMode::Fd {
|
||||||
tgt_fd: 1,
|
tgt_fd: 1,
|
||||||
src_fd: pty_slave.as_raw_fd(),
|
src_fd: pty_slave.as_raw_fd(),
|
||||||
},
|
},
|
||||||
RedirType::Output,
|
RedirType::Output,
|
||||||
),
|
));
|
||||||
);
|
frame.push(Redir::new(
|
||||||
frame.push(
|
|
||||||
Redir::new(
|
|
||||||
IoMode::Fd {
|
IoMode::Fd {
|
||||||
tgt_fd: 2,
|
tgt_fd: 2,
|
||||||
src_fd: pty_slave.as_raw_fd(),
|
src_fd: pty_slave.as_raw_fd(),
|
||||||
},
|
},
|
||||||
RedirType::Output,
|
RedirType::Output,
|
||||||
),
|
));
|
||||||
);
|
|
||||||
|
|
||||||
let _redir_guard = frame.redirect().unwrap();
|
let _redir_guard = frame.redirect().unwrap();
|
||||||
|
|
||||||
let old_cwd = env::current_dir().unwrap();
|
let old_cwd = env::current_dir().unwrap();
|
||||||
let saved_env = env::vars().collect();
|
let saved_env = env::vars().collect();
|
||||||
SHED.with(|s| s.save());
|
SHED.with(|s| s.save());
|
||||||
|
save_registers();
|
||||||
Self {
|
Self {
|
||||||
_lock,
|
_lock,
|
||||||
_redir_guard,
|
_redir_guard,
|
||||||
old_cwd,
|
old_cwd,
|
||||||
saved_env,
|
saved_env,
|
||||||
pty_master,
|
pty_master,
|
||||||
_pty_slave: pty_slave,
|
pty_slave,
|
||||||
cleanups: vec![],
|
cleanups: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn pty_slave(&self) -> BorrowedFd<'_> {
|
||||||
|
unsafe { BorrowedFd::borrow_raw(self.pty_slave.as_raw_fd()) }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
|
pub fn add_cleanup(&mut self, f: impl FnOnce() + 'static) {
|
||||||
self.cleanups.push(Box::new(f));
|
self.cleanups.push(Box::new(f));
|
||||||
}
|
}
|
||||||
@@ -111,10 +112,11 @@ impl TestGuard {
|
|||||||
fcntl(
|
fcntl(
|
||||||
self.pty_master.as_raw_fd(),
|
self.pty_master.as_raw_fd(),
|
||||||
FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK),
|
FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
let mut buf = [0;4096];
|
let mut buf = [0; 4096];
|
||||||
loop {
|
loop {
|
||||||
match read(self.pty_master.as_raw_fd(), &mut buf) {
|
match read(self.pty_master.as_raw_fd(), &mut buf) {
|
||||||
Ok(0) => break,
|
Ok(0) => break,
|
||||||
@@ -123,10 +125,7 @@ impl TestGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fcntl(
|
fcntl(self.pty_master.as_raw_fd(), FcntlArg::F_SETFL(flags)).unwrap();
|
||||||
self.pty_master.as_raw_fd(),
|
|
||||||
FcntlArg::F_SETFL(flags),
|
|
||||||
).unwrap();
|
|
||||||
|
|
||||||
String::from_utf8_lossy(&out).to_string()
|
String::from_utf8_lossy(&out).to_string()
|
||||||
}
|
}
|
||||||
@@ -142,14 +141,133 @@ impl Drop for TestGuard {
|
|||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
env::set_current_dir(&self.old_cwd).ok();
|
env::set_current_dir(&self.old_cwd).ok();
|
||||||
for (k, _) in env::vars() {
|
for (k, _) in env::vars() {
|
||||||
unsafe { env::remove_var(&k); }
|
unsafe {
|
||||||
|
env::remove_var(&k);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (k, v) in &self.saved_env {
|
for (k, v) in &self.saved_env {
|
||||||
unsafe { env::set_var(k, v); }
|
unsafe {
|
||||||
|
env::set_var(k, v);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for cleanup in self.cleanups.drain(..).rev() {
|
for cleanup in self.cleanups.drain(..).rev() {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
SHED.with(|s| s.restore());
|
SHED.with(|s| s.restore());
|
||||||
|
restore_registers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ast(input: &str) -> ShResult<Vec<crate::parse::Node>> {
|
||||||
|
let log_tab = read_logic(|l| l.clone());
|
||||||
|
let input = expand_aliases(input.into(), HashSet::new(), &log_tab);
|
||||||
|
|
||||||
|
let source_name = "test_input".to_string();
|
||||||
|
let mut parser = ParsedSrc::new(Arc::new(input))
|
||||||
|
.with_lex_flags(LexFlags::empty())
|
||||||
|
.with_name(source_name.clone());
|
||||||
|
|
||||||
|
parser
|
||||||
|
.parse_src()
|
||||||
|
.map_err(|e| e.into_iter().next().unwrap())?;
|
||||||
|
|
||||||
|
Ok(parser.extract_nodes())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::parse::Node {
|
||||||
|
pub fn assert_structure(
|
||||||
|
&mut self,
|
||||||
|
expected: &mut impl Iterator<Item = NdKind>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut full_structure = vec![];
|
||||||
|
let mut before = vec![];
|
||||||
|
let mut after = vec![];
|
||||||
|
let mut offender = None;
|
||||||
|
|
||||||
|
self.walk_tree(&mut |s| {
|
||||||
|
let expected_rule = expected.next();
|
||||||
|
full_structure.push(s.class.as_nd_kind());
|
||||||
|
|
||||||
|
if offender.is_none()
|
||||||
|
&& expected_rule
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|e| *e != s.class.as_nd_kind())
|
||||||
|
{
|
||||||
|
offender = Some((s.class.as_nd_kind(), expected_rule));
|
||||||
|
} else if offender.is_none() {
|
||||||
|
before.push(s.class.as_nd_kind());
|
||||||
|
} else {
|
||||||
|
after.push(s.class.as_nd_kind());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
expected.next().is_none(),
|
||||||
|
"Expected structure has more nodes than actual structure"
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some((nd_kind, expected_rule)) = offender {
|
||||||
|
let expected_rule = expected_rule.map_or("(none — expected array too short)".into(), |e| {
|
||||||
|
format!("{e:?}")
|
||||||
|
});
|
||||||
|
let full_structure_hint = full_structure
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| format!("\tNdKind::{s:?},"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n");
|
||||||
|
let full_structure_hint =
|
||||||
|
format!("let expected = &mut [\n{full_structure_hint}\n].into_iter();");
|
||||||
|
|
||||||
|
let output = [
|
||||||
|
"Structure assertion failed!\n".into(),
|
||||||
|
format!(
|
||||||
|
"Expected node type '{:?}', found '{:?}'",
|
||||||
|
expected_rule, nd_kind
|
||||||
|
),
|
||||||
|
format!("Before offender: {:?}", before),
|
||||||
|
format!("After offender: {:?}\n", after),
|
||||||
|
format!("hint: here is the full structure as an array\n {full_structure_hint}"),
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
Err(output)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum NdKind {
|
||||||
|
IfNode,
|
||||||
|
LoopNode,
|
||||||
|
ForNode,
|
||||||
|
CaseNode,
|
||||||
|
Command,
|
||||||
|
Pipeline,
|
||||||
|
Conjunction,
|
||||||
|
Assignment,
|
||||||
|
BraceGrp,
|
||||||
|
Negate,
|
||||||
|
Test,
|
||||||
|
FuncDef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::parse::NdRule {
|
||||||
|
pub fn as_nd_kind(&self) -> NdKind {
|
||||||
|
match self {
|
||||||
|
Self::Negate { .. } => NdKind::Negate,
|
||||||
|
Self::IfNode { .. } => NdKind::IfNode,
|
||||||
|
Self::LoopNode { .. } => NdKind::LoopNode,
|
||||||
|
Self::ForNode { .. } => NdKind::ForNode,
|
||||||
|
Self::CaseNode { .. } => NdKind::CaseNode,
|
||||||
|
Self::Command { .. } => NdKind::Command,
|
||||||
|
Self::Pipeline { .. } => NdKind::Pipeline,
|
||||||
|
Self::Conjunction { .. } => NdKind::Conjunction,
|
||||||
|
Self::Assignment { .. } => NdKind::Assignment,
|
||||||
|
Self::BraceGrp { .. } => NdKind::BraceGrp,
|
||||||
|
Self::Test { .. } => NdKind::Test,
|
||||||
|
Self::FuncDef { .. } => NdKind::FuncDef,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
319
tests/gen_vi_tests.lua
Normal file
319
tests/gen_vi_tests.lua
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
-- Generate Rust vi_test! macro invocations using neovim as oracle
|
||||||
|
-- Usage: nvim --headless --clean -l tests/gen_vi_tests.lua
|
||||||
|
--
|
||||||
|
-- Define test cases as { name, input_text, key_sequence }
|
||||||
|
-- Key sequences use vim notation: <Esc>, <CR>, <C-w>, etc.
|
||||||
|
-- The script executes each in a fresh buffer and captures the result.
|
||||||
|
|
||||||
|
local tests = {
|
||||||
|
-- ===================== basic char motions =====================
|
||||||
|
{ "dw_basic", "hello world", "dw" },
|
||||||
|
{ "dw_middle", "one two three", "wdw" },
|
||||||
|
{ "dd_whole_line", "hello world", "dd" },
|
||||||
|
{ "x_single", "hello", "x" },
|
||||||
|
{ "x_middle", "hello", "llx" },
|
||||||
|
{ "X_backdelete", "hello", "llX" },
|
||||||
|
{ "h_motion", "hello", "$h" },
|
||||||
|
{ "l_motion", "hello", "l" },
|
||||||
|
{ "h_at_start", "hello", "h" },
|
||||||
|
{ "l_at_end", "hello", "$l" },
|
||||||
|
|
||||||
|
-- ===================== word motions (small) =====================
|
||||||
|
{ "w_forward", "one two three", "w" },
|
||||||
|
{ "b_backward", "one two three", "$b" },
|
||||||
|
{ "e_end", "one two three", "e" },
|
||||||
|
{ "ge_back_end", "one two three", "$ge" },
|
||||||
|
{ "w_punctuation", "foo.bar baz", "w" },
|
||||||
|
{ "e_punctuation", "foo.bar baz", "e" },
|
||||||
|
{ "b_punctuation", "foo.bar baz", "$b" },
|
||||||
|
{ "w_at_eol", "hello", "$w" },
|
||||||
|
{ "b_at_bol", "hello", "b" },
|
||||||
|
|
||||||
|
-- ===================== word motions (big) =====================
|
||||||
|
{ "W_forward", "foo.bar baz", "W" },
|
||||||
|
{ "B_backward", "foo.bar baz", "$B" },
|
||||||
|
{ "E_end", "foo.bar baz", "E" },
|
||||||
|
{ "gE_back_end", "one two three", "$gE" },
|
||||||
|
{ "W_skip_punct", "one-two three", "W" },
|
||||||
|
{ "B_skip_punct", "one two-three", "$B" },
|
||||||
|
{ "E_skip_punct", "one-two three", "E" },
|
||||||
|
{ "dW_big", "foo.bar baz", "dW" },
|
||||||
|
{ "cW_big", "foo.bar baz", "cWx<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== line motions =====================
|
||||||
|
{ "zero_bol", " hello", "$0" },
|
||||||
|
{ "caret_first_char", " hello", "$^" },
|
||||||
|
{ "dollar_eol", "hello world", "$" },
|
||||||
|
{ "g_last_nonws", "hello ", "g_" },
|
||||||
|
{ "g_no_trailing", "hello", "g_" },
|
||||||
|
{ "pipe_column", "hello world", "6|" },
|
||||||
|
{ "pipe_col1", "hello world", "1|" },
|
||||||
|
{ "I_insert_front", " hello", "Iworld <Esc>" },
|
||||||
|
{ "A_append_end", "hello", "A world<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== find motions =====================
|
||||||
|
{ "f_find", "hello world", "fo" },
|
||||||
|
{ "F_find_back", "hello world", "$Fo" },
|
||||||
|
{ "t_till", "hello world", "tw" },
|
||||||
|
{ "T_till_back", "hello world", "$To" },
|
||||||
|
{ "f_no_match", "hello", "fz" },
|
||||||
|
{ "semicolon_repeat", "abcabc", "fa;;" },
|
||||||
|
{ "comma_reverse", "abcabc", "fa;;," },
|
||||||
|
{ "df_semicolon", "abcabc", "fa;;dfa" },
|
||||||
|
{ "t_at_target", "aab", "lta" },
|
||||||
|
|
||||||
|
-- ===================== delete operations =====================
|
||||||
|
{ "D_to_end", "hello world", "wD" },
|
||||||
|
{ "d_dollar", "hello world", "wd$" },
|
||||||
|
{ "d0_to_start", "hello world", "$d0" },
|
||||||
|
{ "dw_multiple", "one two three", "d2w" },
|
||||||
|
{ "dt_char", "hello world", "dtw" },
|
||||||
|
{ "df_char", "hello world", "dfw" },
|
||||||
|
{ "dh_back", "hello", "lldh" },
|
||||||
|
{ "dl_forward", "hello", "dl" },
|
||||||
|
{ "dge_back_end", "one two three", "$dge" },
|
||||||
|
{ "dG_to_end", "hello world", "dG" },
|
||||||
|
{ "dgg_to_start", "hello world", "$dgg" },
|
||||||
|
{ "d_semicolon", "abcabc", "fad;" },
|
||||||
|
|
||||||
|
-- ===================== change operations =====================
|
||||||
|
{ "cw_basic", "hello world", "cwfoo<Esc>" },
|
||||||
|
{ "C_to_end", "hello world", "wCfoo<Esc>" },
|
||||||
|
{ "cc_whole", "hello world", "ccfoo<Esc>" },
|
||||||
|
{ "ct_char", "hello world", "ctwfoo<Esc>" },
|
||||||
|
{ "s_single", "hello", "sfoo<Esc>" },
|
||||||
|
{ "S_whole_line", "hello world", "Sfoo<Esc>" },
|
||||||
|
{ "cl_forward", "hello", "clX<Esc>" },
|
||||||
|
{ "ch_backward", "hello", "llchX<Esc>" },
|
||||||
|
{ "cb_word_back", "hello world", "$cbfoo<Esc>" },
|
||||||
|
{ "ce_word_end", "hello world", "cefoo<Esc>" },
|
||||||
|
{ "c0_to_start", "hello world", "wc0foo<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== yank and paste =====================
|
||||||
|
{ "yw_p_basic", "hello world", "ywwP" },
|
||||||
|
{ "dw_p_paste", "hello world", "dwP" },
|
||||||
|
{ "dd_p_paste", "hello world", "ddp" },
|
||||||
|
{ "y_dollar_p", "hello world", "wy$P" },
|
||||||
|
{ "ye_p", "hello world", "yewP" },
|
||||||
|
{ "yy_p", "hello world", "yyp" },
|
||||||
|
{ "Y_p", "hello world", "Yp" },
|
||||||
|
{ "p_after_x", "hello", "xp" },
|
||||||
|
{ "P_before", "hello", "llxP" },
|
||||||
|
{ "paste_empty", "hello", "p" },
|
||||||
|
|
||||||
|
-- ===================== replace =====================
|
||||||
|
{ "r_replace", "hello", "ra" },
|
||||||
|
{ "r_middle", "hello", "llra" },
|
||||||
|
{ "r_at_end", "hello", "$ra" },
|
||||||
|
{ "r_space", "hello", "r " },
|
||||||
|
{ "r_with_count", "hello", "3rx" },
|
||||||
|
|
||||||
|
-- ===================== case operations =====================
|
||||||
|
{ "tilde_single", "hello", "~" },
|
||||||
|
{ "tilde_count", "hello", "3~" },
|
||||||
|
{ "tilde_at_end", "HELLO", "$~" },
|
||||||
|
{ "tilde_mixed", "hElLo", "5~" },
|
||||||
|
{ "gu_word", "HELLO world", "guw" },
|
||||||
|
{ "gU_word", "hello WORLD", "gUw" },
|
||||||
|
{ "gu_dollar", "HELLO WORLD", "gu$" },
|
||||||
|
{ "gU_dollar", "hello world", "gU$" },
|
||||||
|
{ "gu_0", "HELLO WORLD", "$gu0" },
|
||||||
|
{ "gU_0", "hello world", "$gU0" },
|
||||||
|
{ "gtilde_word", "hello WORLD", "g~w" },
|
||||||
|
{ "gtilde_dollar", "hello WORLD", "g~$" },
|
||||||
|
|
||||||
|
-- ===================== text objects: word =====================
|
||||||
|
{ "diw_inner", "one two three", "wdiw" },
|
||||||
|
{ "ciw_replace", "hello world", "ciwfoo<Esc>" },
|
||||||
|
{ "daw_around", "one two three", "wdaw" },
|
||||||
|
{ "yiw_p", "hello world", "yiwAp <Esc>p" },
|
||||||
|
{ "diW_big_inner", "one-two three", "diW" },
|
||||||
|
{ "daW_big_around", "one two-three end", "wdaW" },
|
||||||
|
{ "ciW_big", "one-two three", "ciWx<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== text objects: quotes =====================
|
||||||
|
{ "di_dquote", 'one "two" three', 'f"di"' },
|
||||||
|
{ "da_dquote", 'one "two" three', 'f"da"' },
|
||||||
|
{ "ci_dquote", 'one "two" three', 'f"ci"x<Esc>' },
|
||||||
|
{ "di_squote", "one 'two' three", "f'di'" },
|
||||||
|
{ "da_squote", "one 'two' three", "f'da'" },
|
||||||
|
{ "di_backtick", "one `two` three", "f`di`" },
|
||||||
|
{ "da_backtick", "one `two` three", "f`da`" },
|
||||||
|
{ "ci_dquote_empty", 'one "" three', 'f"ci"x<Esc>' },
|
||||||
|
|
||||||
|
-- ===================== text objects: delimiters =====================
|
||||||
|
{ "di_paren", "one (two) three", "f(di(" },
|
||||||
|
{ "da_paren", "one (two) three", "f(da(" },
|
||||||
|
{ "ci_paren", "one (two) three", "f(ci(x<Esc>" },
|
||||||
|
{ "di_brace", "one {two} three", "f{di{" },
|
||||||
|
{ "da_brace", "one {two} three", "f{da{" },
|
||||||
|
{ "di_bracket", "one [two] three", "f[di[" },
|
||||||
|
{ "da_bracket", "one [two] three", "f[da[" },
|
||||||
|
{ "di_angle", "one <two> three", "f<di<" },
|
||||||
|
{ "da_angle", "one <two> three", "f<da<" },
|
||||||
|
{ "di_paren_nested", "fn(a, (b, c))", "f(di(" },
|
||||||
|
{ "di_paren_empty", "fn() end", "f(di(" },
|
||||||
|
{ "dib_alias", "one (two) three", "f(dib" },
|
||||||
|
{ "diB_alias", "one {two} three", "f{diB" },
|
||||||
|
|
||||||
|
-- ===================== delimiter matching =====================
|
||||||
|
{ "percent_paren", "(hello) world", "%" },
|
||||||
|
{ "percent_brace", "{hello} world", "%" },
|
||||||
|
{ "percent_bracket", "[hello] world", "%" },
|
||||||
|
{ "percent_from_close", "(hello) world", "f)%" },
|
||||||
|
{ "d_percent_paren", "(hello) world", "d%" },
|
||||||
|
|
||||||
|
-- ===================== insert mode entry =====================
|
||||||
|
{ "i_insert", "hello", "iX<Esc>" },
|
||||||
|
{ "a_append", "hello", "aX<Esc>" },
|
||||||
|
{ "I_front", " hello", "IX<Esc>" },
|
||||||
|
{ "A_end", "hello", "AX<Esc>" },
|
||||||
|
{ "o_open_below", "hello", "oworld<Esc>" },
|
||||||
|
{ "O_open_above", "hello", "Oworld<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== insert mode operations =====================
|
||||||
|
{ "empty_input", "", "i hello<Esc>" },
|
||||||
|
{ "insert_escape", "hello", "aX<Esc>" },
|
||||||
|
{ "ctrl_w_del_word", "hello world", "A<C-w><Esc>" },
|
||||||
|
{ "ctrl_h_backspace", "hello", "A<C-h><Esc>" },
|
||||||
|
|
||||||
|
-- ===================== undo / redo =====================
|
||||||
|
{ "u_undo_delete", "hello world", "dwu" },
|
||||||
|
{ "u_undo_change", "hello world", "ciwfoo<Esc>u" },
|
||||||
|
{ "u_undo_x", "hello", "xu" },
|
||||||
|
{ "ctrl_r_redo", "hello", "xu<C-r>" },
|
||||||
|
{ "u_multiple", "hello world", "xdwu" },
|
||||||
|
{ "redo_after_undo", "hello world", "dwu<C-r>" },
|
||||||
|
|
||||||
|
-- ===================== dot repeat =====================
|
||||||
|
{ "dot_repeat_x", "hello", "x." },
|
||||||
|
{ "dot_repeat_dw", "one two three", "dw." },
|
||||||
|
{ "dot_repeat_cw", "one two three", "cwfoo<Esc>w." },
|
||||||
|
{ "dot_repeat_r", "hello", "ra.." },
|
||||||
|
{ "dot_repeat_s", "hello", "sX<Esc>l." },
|
||||||
|
|
||||||
|
-- ===================== counts =====================
|
||||||
|
{ "count_h", "hello world", "$3h" },
|
||||||
|
{ "count_l", "hello world", "3l" },
|
||||||
|
{ "count_w", "one two three four", "2w" },
|
||||||
|
{ "count_b", "one two three four", "$2b" },
|
||||||
|
{ "count_x", "hello", "3x" },
|
||||||
|
{ "count_dw", "one two three four", "2dw" },
|
||||||
|
{ "verb_count_motion", "one two three four", "d2w" },
|
||||||
|
{ "count_s", "hello", "3sX<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== indent / dedent =====================
|
||||||
|
{ "indent_line", "hello", ">>" },
|
||||||
|
{ "dedent_line", "\thello", "<<" },
|
||||||
|
{ "indent_double", "hello", ">>>>" },
|
||||||
|
|
||||||
|
-- ===================== join =====================
|
||||||
|
{ "J_join_lines", "hello\nworld", "J" },
|
||||||
|
|
||||||
|
-- ===================== case in visual =====================
|
||||||
|
{ "v_u_lower", "HELLO", "vlllu" },
|
||||||
|
{ "v_U_upper", "hello", "vlllU" },
|
||||||
|
|
||||||
|
-- ===================== visual mode =====================
|
||||||
|
{ "v_d_delete", "hello world", "vwwd" },
|
||||||
|
{ "v_x_delete", "hello world", "vwwx" },
|
||||||
|
{ "v_c_change", "hello world", "vwcfoo<Esc>" },
|
||||||
|
{ "v_y_p_yank", "hello world", "vwyAp <Esc>p" },
|
||||||
|
{ "v_dollar_d", "hello world", "wv$d" },
|
||||||
|
{ "v_0_d", "hello world", "$v0d" },
|
||||||
|
{ "ve_d", "hello world", "ved" },
|
||||||
|
{ "v_o_swap", "hello world", "vllod" },
|
||||||
|
{ "v_r_replace", "hello", "vlllrx" },
|
||||||
|
{ "v_tilde_case", "hello", "vlll~" },
|
||||||
|
|
||||||
|
-- ===================== visual line mode =====================
|
||||||
|
{ "V_d_delete", "hello world", "Vd" },
|
||||||
|
{ "V_y_p", "hello world", "Vyp" },
|
||||||
|
{ "V_S_change", "hello world", "VSfoo<Esc>" },
|
||||||
|
|
||||||
|
-- ===================== increment / decrement =====================
|
||||||
|
{ "ctrl_a_inc", "num 5 end", "w<C-a>" },
|
||||||
|
{ "ctrl_x_dec", "num 5 end", "w<C-x>" },
|
||||||
|
{ "ctrl_a_negative", "num -3 end", "w<C-a>" },
|
||||||
|
{ "ctrl_x_to_neg", "num 0 end", "w<C-x>" },
|
||||||
|
{ "ctrl_a_count", "num 5 end", "w3<C-a>" },
|
||||||
|
|
||||||
|
-- ===================== misc / edge cases =====================
|
||||||
|
{ "delete_empty", "", "x" },
|
||||||
|
{ "undo_on_empty", "", "u" },
|
||||||
|
{ "w_single_char", "a b c", "w" },
|
||||||
|
{ "dw_last_word", "hello", "dw" },
|
||||||
|
{ "dollar_single", "h", "$" },
|
||||||
|
{ "caret_no_ws", "hello", "$^" },
|
||||||
|
{ "f_last_char", "hello", "fo" },
|
||||||
|
{ "r_on_space", "hello world", "5|r-" },
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Map vim special key names to Rust string escape sequences
|
||||||
|
local key_to_bytes = {
|
||||||
|
["<Esc>"] = "\\x1b",
|
||||||
|
["<CR>"] = "\\r",
|
||||||
|
["<BS>"] = "\\x7f",
|
||||||
|
["<Tab>"] = "\\t",
|
||||||
|
["<Del>"] = "\\x1b[3~",
|
||||||
|
["<Up>"] = "\\x1b[A",
|
||||||
|
["<Down>"] = "\\x1b[B",
|
||||||
|
["<Right>"] = "\\x1b[C",
|
||||||
|
["<Left>"] = "\\x1b[D",
|
||||||
|
["<Home>"] = "\\x1b[H",
|
||||||
|
["<End>"] = "\\x1b[F",
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Convert vim key notation to Rust string escape sequences
|
||||||
|
local function keys_to_rust(keys)
|
||||||
|
local result = keys
|
||||||
|
result = result:gsub("<C%-(.)>", function(ch)
|
||||||
|
local byte = string.byte(ch:lower()) - string.byte('a') + 1
|
||||||
|
return string.format("\\x%02x", byte)
|
||||||
|
end)
|
||||||
|
for name, bytes in pairs(key_to_bytes) do
|
||||||
|
result = result:gsub(vim.pesc(name), bytes)
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Escape a string for use in a Rust string literal
|
||||||
|
local function rust_escape(s)
|
||||||
|
return s:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n"):gsub("\t", "\\t")
|
||||||
|
end
|
||||||
|
|
||||||
|
io.write("vi_test! {\n")
|
||||||
|
|
||||||
|
for i, test in ipairs(tests) do
|
||||||
|
local name, input, keys = test[1], test[2], test[3]
|
||||||
|
|
||||||
|
-- Fresh buffer and register state
|
||||||
|
local input_lines = vim.split(input, "\n", { plain = true })
|
||||||
|
vim.api.nvim_buf_set_lines(0, 0, -1, false, input_lines)
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 1, 0 })
|
||||||
|
vim.fn.setreg('"', '')
|
||||||
|
|
||||||
|
-- Execute the key sequence synchronously
|
||||||
|
local translated = vim.api.nvim_replace_termcodes(keys, true, false, true)
|
||||||
|
vim.api.nvim_feedkeys(translated, "ntx", false)
|
||||||
|
vim.api.nvim_exec_autocmds("CursorMoved", {})
|
||||||
|
|
||||||
|
-- Capture result
|
||||||
|
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
||||||
|
local result = table.concat(lines, "\n")
|
||||||
|
local cursor_col = vim.api.nvim_win_get_cursor(0)[2]
|
||||||
|
|
||||||
|
local rust_keys = keys_to_rust(keys)
|
||||||
|
local rust_input = rust_escape(input)
|
||||||
|
local rust_result = rust_escape(result)
|
||||||
|
|
||||||
|
local sep = ";"
|
||||||
|
if i == #tests then sep = "" end
|
||||||
|
|
||||||
|
io.write(string.format('\tvi_%s: "%s" => "%s" => "%s", %d%s\n',
|
||||||
|
name, rust_input, rust_keys, rust_result, cursor_col, sep))
|
||||||
|
end
|
||||||
|
|
||||||
|
io.write("}\n")
|
||||||
|
|
||||||
|
vim.cmd("qa!")
|
||||||
Reference in New Issue
Block a user