fixed ss3 escape code parsing, added a cursor mode reset that triggers on child exit
This commit is contained in:
@@ -18,20 +18,20 @@ pub mod map;
|
|||||||
pub mod pwd;
|
pub mod pwd;
|
||||||
pub mod read;
|
pub mod read;
|
||||||
pub mod resource;
|
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 seek;
|
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 50] = [
|
pub const BUILTINS: [&str; 50] = [
|
||||||
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
|
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
|
||||||
"disown", "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", "seek"
|
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn true_builtin() -> ShResult<()> {
|
pub fn true_builtin() -> ShResult<()> {
|
||||||
|
|||||||
@@ -1,91 +1,101 @@
|
|||||||
use nix::{libc::STDOUT_FILENO, unistd::{Whence, lseek, write}};
|
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};
|
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] = [
|
pub const LSEEK_OPTS: [OptSpec; 2] = [
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('c'),
|
opt: Opt::Short('c'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('e'),
|
opt: Opt::Short('e'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub struct LseekOpts {
|
pub struct LseekOpts {
|
||||||
cursor_rel: bool,
|
cursor_rel: bool,
|
||||||
end_rel: bool
|
end_rel: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek(node: Node) -> ShResult<()> {
|
pub fn seek(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(argv, &LSEEK_OPTS)?;
|
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
|
||||||
let lseek_opts = get_lseek_opts(opts)?;
|
let lseek_opts = get_lseek_opts(opts)?;
|
||||||
let mut argv = prepare_argv(argv)?.into_iter();
|
let mut argv = prepare_argv(argv)?.into_iter();
|
||||||
argv.next(); // drop 'seek'
|
argv.next(); // drop 'seek'
|
||||||
|
|
||||||
let Some(fd) = argv.next() else {
|
let Some(fd) = argv.next() else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
"lseek: Missing required argument 'fd'",
|
"lseek: Missing required argument 'fd'",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let Ok(fd) = fd.0.parse::<u32>() else {
|
let Ok(fd) = fd.0.parse::<u32>() else {
|
||||||
return Err(ShErr::at(
|
return Err(
|
||||||
ShErrKind::ExecFail,
|
ShErr::at(ShErrKind::ExecFail, fd.1, "Invalid file descriptor")
|
||||||
fd.1,
|
.with_note("file descriptors are integers"),
|
||||||
"Invalid file descriptor",
|
);
|
||||||
).with_note("file descriptors are integers"));
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let Some(offset) = argv.next() else {
|
let Some(offset) = argv.next() else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
"lseek: Missing required argument 'offset'",
|
"lseek: Missing required argument 'offset'",
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let Ok(offset) = offset.0.parse::<i64>() else {
|
let Ok(offset) = offset.0.parse::<i64>() else {
|
||||||
return Err(ShErr::at(
|
return Err(
|
||||||
ShErrKind::ExecFail,
|
ShErr::at(ShErrKind::ExecFail, offset.1, "Invalid offset")
|
||||||
offset.1,
|
.with_note("offset can be a positive or negative integer"),
|
||||||
"Invalid offset",
|
);
|
||||||
).with_note("offset can be a positive or negative integer"));
|
};
|
||||||
};
|
|
||||||
|
|
||||||
let whence = if lseek_opts.cursor_rel {
|
let whence = if lseek_opts.cursor_rel {
|
||||||
Whence::SeekCur
|
Whence::SeekCur
|
||||||
} else if lseek_opts.end_rel {
|
} else if lseek_opts.end_rel {
|
||||||
Whence::SeekEnd
|
Whence::SeekEnd
|
||||||
} else {
|
} else {
|
||||||
Whence::SeekSet
|
Whence::SeekSet
|
||||||
};
|
};
|
||||||
|
|
||||||
match lseek(fd as i32, offset, whence) {
|
match lseek(fd as i32, offset, whence) {
|
||||||
Ok(new_offset) => {
|
Ok(new_offset) => {
|
||||||
let stdout = borrow_fd(STDOUT_FILENO);
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
let buf = new_offset.to_string() + "\n";
|
let buf = new_offset.to_string() + "\n";
|
||||||
write(stdout, buf.as_bytes())?;
|
write(stdout, buf.as_bytes())?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
return Err(e.into())
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
|
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
|
||||||
let mut lseek_opts = LseekOpts {
|
let mut lseek_opts = LseekOpts {
|
||||||
cursor_rel: false,
|
cursor_rel: false,
|
||||||
end_rel: false,
|
end_rel: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
for opt in opts {
|
for opt in opts {
|
||||||
match opt {
|
match opt {
|
||||||
|
|||||||
219
src/expand.rs
219
src/expand.rs
@@ -40,7 +40,7 @@ impl Tk {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Expander {
|
pub struct Expander {
|
||||||
flags: TkFlags,
|
flags: TkFlags,
|
||||||
raw: String,
|
raw: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,12 +51,15 @@ impl Expander {
|
|||||||
}
|
}
|
||||||
pub fn from_raw(raw: &str, flags: TkFlags) -> 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 = if flags.contains(TkFlags::IS_HEREDOC) {
|
let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
unescape_heredoc(&raw)
|
unescape_heredoc(&raw)
|
||||||
} else {
|
} else {
|
||||||
unescape_str(&raw)
|
unescape_str(&raw)
|
||||||
};
|
};
|
||||||
Ok(Self { raw: unescaped, flags })
|
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();
|
||||||
@@ -80,11 +83,11 @@ impl Expander {
|
|||||||
self.raw.insert_str(0, "./");
|
self.raw.insert_str(0, "./");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.flags.contains(TkFlags::IS_HEREDOC) {
|
if self.flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
Ok(vec![self.raw.clone()])
|
Ok(vec![self.raw.clone()])
|
||||||
} else {
|
} 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![];
|
||||||
@@ -1378,89 +1381,89 @@ pub fn unescape_str(raw: &str) -> String {
|
|||||||
/// - Backslash escapes (only before $, `, \, and newline)
|
/// - Backslash escapes (only before $, `, \, and newline)
|
||||||
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
|
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
|
||||||
pub fn unescape_heredoc(raw: &str) -> String {
|
pub fn unescape_heredoc(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();
|
||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('$') | Some('`') | Some('\\') | Some('\n') => {
|
Some('$') | Some('`') | Some('\\') | Some('\n') => {
|
||||||
let next_ch = chars.next().unwrap();
|
let next_ch = chars.next().unwrap();
|
||||||
if next_ch == '\n' {
|
if next_ch == '\n' {
|
||||||
// line continuation — discard both backslash and newline
|
// line continuation — discard both backslash and newline
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
result.push(markers::ESCAPE);
|
result.push(markers::ESCAPE);
|
||||||
result.push(next_ch);
|
result.push(next_ch);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// backslash is literal
|
// backslash is literal
|
||||||
result.push('\\');
|
result.push('\\');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'$' if chars.peek() == Some(&'(') => {
|
'$' if chars.peek() == Some(&'(') => {
|
||||||
result.push(markers::VAR_SUB);
|
result.push(markers::VAR_SUB);
|
||||||
chars.next(); // consume '('
|
chars.next(); // consume '('
|
||||||
result.push(markers::SUBSH);
|
result.push(markers::SUBSH);
|
||||||
let mut paren_count = 1;
|
let mut paren_count = 1;
|
||||||
while let Some(subsh_ch) = chars.next() {
|
while let Some(subsh_ch) = chars.next() {
|
||||||
match subsh_ch {
|
match subsh_ch {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
result.push(subsh_ch);
|
result.push(subsh_ch);
|
||||||
if let Some(next_ch) = chars.next() {
|
if let Some(next_ch) = chars.next() {
|
||||||
result.push(next_ch);
|
result.push(next_ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'(' => {
|
'(' => {
|
||||||
paren_count += 1;
|
paren_count += 1;
|
||||||
result.push(subsh_ch);
|
result.push(subsh_ch);
|
||||||
}
|
}
|
||||||
')' => {
|
')' => {
|
||||||
paren_count -= 1;
|
paren_count -= 1;
|
||||||
if paren_count == 0 {
|
if paren_count == 0 {
|
||||||
result.push(markers::SUBSH);
|
result.push(markers::SUBSH);
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
result.push(subsh_ch);
|
result.push(subsh_ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => result.push(subsh_ch),
|
_ => result.push(subsh_ch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'$' => {
|
'$' => {
|
||||||
result.push(markers::VAR_SUB);
|
result.push(markers::VAR_SUB);
|
||||||
if chars.peek() == Some(&'$') {
|
if chars.peek() == Some(&'$') {
|
||||||
chars.next();
|
chars.next();
|
||||||
result.push('$');
|
result.push('$');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'`' => {
|
'`' => {
|
||||||
result.push(markers::VAR_SUB);
|
result.push(markers::VAR_SUB);
|
||||||
result.push(markers::SUBSH);
|
result.push(markers::SUBSH);
|
||||||
while let Some(bt_ch) = chars.next() {
|
while let Some(bt_ch) = chars.next() {
|
||||||
match bt_ch {
|
match bt_ch {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
result.push(bt_ch);
|
result.push(bt_ch);
|
||||||
if let Some(next_ch) = chars.next() {
|
if let Some(next_ch) = chars.next() {
|
||||||
result.push(next_ch);
|
result.push(next_ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'`' => {
|
'`' => {
|
||||||
result.push(markers::SUBSH);
|
result.push(markers::SUBSH);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
_ => result.push(bt_ch),
|
_ => result.push(bt_ch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => result.push(ch),
|
_ => result.push(ch),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opposite of unescape_str - escapes a string to be executed as literal text
|
/// Opposite of unescape_str - escapes a string to be executed as literal text
|
||||||
@@ -3669,7 +3672,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw: "hello world\tfoo".to_string(),
|
raw: "hello world\tfoo".to_string(),
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
@@ -3684,7 +3687,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw: "a:b:c".to_string(),
|
raw: "a:b:c".to_string(),
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
@@ -3699,7 +3702,7 @@ mod tests {
|
|||||||
|
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw: "hello world".to_string(),
|
raw: "hello world".to_string(),
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
@@ -3711,9 +3714,9 @@ mod tests {
|
|||||||
|
|
||||||
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 {
|
let mut exp = Expander {
|
||||||
raw,
|
raw,
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
}
|
}
|
||||||
@@ -3726,9 +3729,9 @@ mod tests {
|
|||||||
|
|
||||||
let raw = format!("hello{}world", unescape_str("\\ "));
|
let raw = format!("hello{}world", unescape_str("\\ "));
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw,
|
raw,
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
}
|
}
|
||||||
@@ -3739,9 +3742,9 @@ mod tests {
|
|||||||
|
|
||||||
let raw = format!("hello{}world", unescape_str("\\\t"));
|
let raw = format!("hello{}world", unescape_str("\\\t"));
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw,
|
raw,
|
||||||
flags: TkFlags::empty()
|
flags: TkFlags::empty(),
|
||||||
};
|
};
|
||||||
let words = exp.split_words();
|
let words = exp.split_words();
|
||||||
assert_eq!(words, vec!["hello\tworld"]);
|
assert_eq!(words, vec!["hello\tworld"]);
|
||||||
}
|
}
|
||||||
@@ -3755,9 +3758,9 @@ mod tests {
|
|||||||
|
|
||||||
let raw = format!("a{}b:c", unescape_str("\\:"));
|
let raw = format!("a{}b:c", unescape_str("\\:"));
|
||||||
let mut exp = Expander {
|
let mut exp = Expander {
|
||||||
raw,
|
raw,
|
||||||
flags: TkFlags::empty()
|
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"]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,15 +96,15 @@ pub fn sort_tks(
|
|||||||
.map(|t| t.expand())
|
.map(|t| t.expand())
|
||||||
.collect::<ShResult<Vec<_>>>()?
|
.collect::<ShResult<Vec<_>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.peekable();
|
.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.push(token);
|
||||||
non_opts.extend(tokens_iter);
|
non_opts.extend(tokens_iter);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let parsed_opts = Opt::parse(&token.to_string());
|
let parsed_opts = Opt::parse(&token.to_string());
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +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
|
/// 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;
|
||||||
@@ -210,8 +210,8 @@ impl ShErr {
|
|||||||
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 {
|
} else {
|
||||||
self.notes = vec![];
|
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! {
|
||||||
@@ -150,6 +150,7 @@ impl RawModeGuard {
|
|||||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
|
||||||
let res = f();
|
let res = f();
|
||||||
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t).ok();
|
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t).ok();
|
||||||
|
unsafe { write(BorrowedFd::borrow_raw(*TTY_FILENO), b"\x1b[?1l\x1b>").ok() };
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
|
|||||||
let fd = 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
|
// Move the tty fd above the user-accessible range so that
|
||||||
// `exec 3>&-` and friends don't collide with shell internals.
|
// `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");
|
let high =
|
||||||
|
fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).expect("Failed to dup /dev/tty high");
|
||||||
close(fd).ok();
|
close(fd).ok();
|
||||||
high
|
high
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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, seek::seek, 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},
|
||||||
@@ -319,12 +341,12 @@ impl Dispatcher {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut elem_iter = elements.into_iter();
|
let mut elem_iter = elements.into_iter();
|
||||||
let mut skip = false;
|
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 {
|
if !skip {
|
||||||
self.dispatch_node(*cmd)?;
|
self.dispatch_node(*cmd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = state::get_status();
|
let status = state::get_status();
|
||||||
skip = match operator {
|
skip = match operator {
|
||||||
@@ -351,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_or(name.span.as_str());
|
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(
|
||||||
@@ -863,9 +889,9 @@ 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" {
|
if cmd_raw.as_str() == "exec" {
|
||||||
guard.persist();
|
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();
|
||||||
@@ -990,7 +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),
|
"seek" => seek(cmd),
|
||||||
"true" | ":" => {
|
"true" | ":" => {
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
534
src/parse/lex.rs
534
src/parse/lex.rs
@@ -218,31 +218,30 @@ impl Tk {
|
|||||||
self.span.as_str().trim() == ";;"
|
self.span.as_str().trim() == ";;"
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_opener(&self) -> bool {
|
pub fn is_opener(&self) -> bool {
|
||||||
OPENERS.contains(&self.as_str()) ||
|
OPENERS.contains(&self.as_str())
|
||||||
matches!(self.class, TkRule::BraceGrpStart) ||
|
|| matches!(self.class, TkRule::BraceGrpStart)
|
||||||
matches!(self.class, TkRule::CasePattern)
|
|| matches!(self.class, TkRule::CasePattern)
|
||||||
}
|
}
|
||||||
pub fn is_closer(&self) -> bool {
|
pub fn is_closer(&self) -> bool {
|
||||||
matches!(self.as_str(), "fi" | "done" | "esac") ||
|
matches!(self.as_str(), "fi" | "done" | "esac")
|
||||||
self.has_double_semi() ||
|
|| self.has_double_semi()
|
||||||
matches!(self.class, TkRule::BraceGrpEnd)
|
|| matches!(self.class, TkRule::BraceGrpEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_closer_for(&self, other: &Tk) -> bool {
|
pub fn is_closer_for(&self, other: &Tk) -> bool {
|
||||||
if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd))
|
if (matches!(other.class, TkRule::BraceGrpStart) && matches!(self.class, TkRule::BraceGrpEnd))
|
||||||
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi()) {
|
|| (matches!(other.class, TkRule::CasePattern) && self.has_double_semi())
|
||||||
return true;
|
{
|
||||||
}
|
return true;
|
||||||
match other.as_str() {
|
}
|
||||||
"for" |
|
match other.as_str() {
|
||||||
"while" |
|
"for" | "while" | "until" => matches!(self.as_str(), "done"),
|
||||||
"until" => matches!(self.as_str(), "done"),
|
"if" => matches!(self.as_str(), "fi"),
|
||||||
"if" => matches!(self.as_str(), "fi"),
|
"case" => matches!(self.as_str(), "esac"),
|
||||||
"case" => matches!(self.as_str(), "esac"),
|
_ => false,
|
||||||
_ => false
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Tk {
|
impl Display for Tk {
|
||||||
@@ -267,9 +266,9 @@ 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 IS_HEREDOC = 0b0000001000000000;
|
||||||
const LIT_HEREDOC = 0b0000010000000000;
|
const LIT_HEREDOC = 0b0000010000000000;
|
||||||
const TAB_HEREDOC = 0b0000100000000000;
|
const TAB_HEREDOC = 0b0000100000000000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,11 +321,10 @@ pub struct LexStream {
|
|||||||
brc_grp_depth: usize,
|
brc_grp_depth: usize,
|
||||||
brc_grp_start: Option<usize>,
|
brc_grp_start: Option<usize>,
|
||||||
case_depth: usize,
|
case_depth: usize,
|
||||||
heredoc_skip: Option<usize>,
|
heredoc_skip: Option<usize>,
|
||||||
flags: LexFlags,
|
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;
|
||||||
@@ -338,7 +336,7 @@ 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,
|
heredoc_skip: None,
|
||||||
case_depth: 0,
|
case_depth: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -411,13 +409,13 @@ 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() {
|
if let Some('|') = chars.peek() {
|
||||||
// noclobber force '>|'
|
// noclobber force '>|'
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some('>') = chars.peek() {
|
if let Some('>') = chars.peek() {
|
||||||
chars.next();
|
chars.next();
|
||||||
@@ -428,34 +426,34 @@ impl LexStream {
|
|||||||
break;
|
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 == '-') {
|
if chars.peek().is_some_and(|ch| *ch == '-') {
|
||||||
chars.next();
|
chars.next();
|
||||||
found_fd = true;
|
found_fd = true;
|
||||||
pos += 1;
|
pos += 1;
|
||||||
} else {
|
} 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;
|
||||||
self.cursor = pos;
|
self.cursor = pos;
|
||||||
return Some(Err(ShErr::at(
|
return Some(Err(ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
Span::new(span_start..pos, self.source.clone()),
|
Span::new(span_start..pos, self.source.clone()),
|
||||||
"Invalid redirection",
|
"Invalid redirection",
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'<' => {
|
'<' => {
|
||||||
if chars.peek() == Some(&'(') {
|
if chars.peek() == Some(&'(') {
|
||||||
@@ -463,93 +461,93 @@ impl LexStream {
|
|||||||
}
|
}
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('<') => {
|
Some('<') => {
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
|
|
||||||
match chars.peek() {
|
match chars.peek() {
|
||||||
Some('<') => {
|
Some('<') => {
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(ch) => {
|
Some(ch) => {
|
||||||
let mut ch = *ch;
|
let mut ch = *ch;
|
||||||
while is_field_sep(ch) {
|
while is_field_sep(ch) {
|
||||||
let Some(next_ch) = chars.next() else {
|
let Some(next_ch) = chars.next() else {
|
||||||
// Incomplete input — fall through to emit << as Redir
|
// Incomplete input — fall through to emit << as Redir
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
pos += next_ch.len_utf8();
|
pos += next_ch.len_utf8();
|
||||||
ch = next_ch;
|
ch = next_ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_field_sep(ch) {
|
if is_field_sep(ch) {
|
||||||
// Ran out of input while skipping whitespace — fall through
|
// Ran out of input while skipping whitespace — fall through
|
||||||
} else {
|
} else {
|
||||||
let saved_cursor = self.cursor;
|
let saved_cursor = self.cursor;
|
||||||
match self.read_heredoc(pos) {
|
match self.read_heredoc(pos) {
|
||||||
Ok(Some(heredoc_tk)) => {
|
Ok(Some(heredoc_tk)) => {
|
||||||
// cursor is set to after the delimiter word;
|
// cursor is set to after the delimiter word;
|
||||||
// heredoc_skip is set to after the body
|
// heredoc_skip is set to after the body
|
||||||
pos = self.cursor;
|
pos = self.cursor;
|
||||||
self.cursor = saved_cursor;
|
self.cursor = saved_cursor;
|
||||||
tk = heredoc_tk;
|
tk = heredoc_tk;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
// Incomplete heredoc — restore cursor and fall through
|
// Incomplete heredoc — restore cursor and fall through
|
||||||
self.cursor = saved_cursor;
|
self.cursor = saved_cursor;
|
||||||
}
|
}
|
||||||
Err(e) => return Some(Err(e)),
|
Err(e) => return Some(Err(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// No delimiter yet — input is incomplete
|
// No delimiter yet — input is incomplete
|
||||||
// Fall through to emit the << as a Redir token
|
// Fall through to emit the << as a Redir token
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some('>') => {
|
Some('>') => {
|
||||||
chars.next();
|
chars.next();
|
||||||
pos += 1;
|
pos += 1;
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Some('&') => {
|
Some('&') => {
|
||||||
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 == '-') {
|
if chars.peek().is_some_and(|ch| *ch == '-') {
|
||||||
chars.next();
|
chars.next();
|
||||||
found_fd = true;
|
found_fd = true;
|
||||||
pos += 1;
|
pos += 1;
|
||||||
} else {
|
} 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;
|
||||||
self.cursor = pos;
|
self.cursor = pos;
|
||||||
return Some(Err(ShErr::at(
|
return Some(Err(ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
Span::new(span_start..pos, self.source.clone()),
|
Span::new(span_start..pos, self.source.clone()),
|
||||||
"Invalid redirection",
|
"Invalid redirection",
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
tk = self.get_token(self.cursor..pos, TkRule::Redir);
|
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;
|
||||||
@@ -574,130 +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>> {
|
pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult<Option<Tk>> {
|
||||||
let slice = self.slice(pos..).unwrap_or_default().to_string();
|
let slice = self.slice(pos..).unwrap_or_default().to_string();
|
||||||
let mut chars = slice.chars();
|
let mut chars = slice.chars();
|
||||||
let mut delim = String::new();
|
let mut delim = String::new();
|
||||||
let mut flags = TkFlags::empty();
|
let mut flags = TkFlags::empty();
|
||||||
let mut first_char = true;
|
let mut first_char = true;
|
||||||
// Parse the delimiter word, stripping quotes
|
// Parse the delimiter word, stripping quotes
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
'-' if first_char => {
|
'-' if first_char => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
flags |= TkFlags::TAB_HEREDOC;
|
flags |= TkFlags::TAB_HEREDOC;
|
||||||
}
|
}
|
||||||
'\"' => {
|
'\"' => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
self.quote_state.toggle_double();
|
self.quote_state.toggle_double();
|
||||||
flags |= TkFlags::LIT_HEREDOC;
|
flags |= TkFlags::LIT_HEREDOC;
|
||||||
}
|
}
|
||||||
'\'' => {
|
'\'' => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
self.quote_state.toggle_single();
|
self.quote_state.toggle_single();
|
||||||
flags |= TkFlags::LIT_HEREDOC;
|
flags |= TkFlags::LIT_HEREDOC;
|
||||||
}
|
}
|
||||||
_ if self.quote_state.in_quote() => {
|
_ if self.quote_state.in_quote() => {
|
||||||
pos += ch.len_utf8();
|
pos += ch.len_utf8();
|
||||||
delim.push(ch);
|
delim.push(ch);
|
||||||
}
|
}
|
||||||
ch if is_hard_sep(ch) => {
|
ch if is_hard_sep(ch) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ch => {
|
ch => {
|
||||||
pos += ch.len_utf8();
|
pos += ch.len_utf8();
|
||||||
delim.push(ch);
|
delim.push(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
first_char = false;
|
first_char = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pos is now right after the delimiter word — this is where
|
// pos is now right after the delimiter word — this is where
|
||||||
// the cursor should return so the rest of the line gets lexed
|
// the cursor should return so the rest of the line gets lexed
|
||||||
let cursor_after_delim = pos;
|
let cursor_after_delim = pos;
|
||||||
|
|
||||||
// Re-slice from cursor_after_delim so iterator and pos are in sync
|
// Re-slice from cursor_after_delim so iterator and pos are in sync
|
||||||
// (the old chars iterator consumed the hard_sep without advancing pos)
|
// (the old chars iterator consumed the hard_sep without advancing pos)
|
||||||
let rest = self.slice(cursor_after_delim..).unwrap_or_default().to_string();
|
let rest = self
|
||||||
let mut chars = rest.chars();
|
.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)
|
// Scan forward to the newline (or use heredoc_skip from a previous heredoc)
|
||||||
let body_start = if let Some(skip) = self.heredoc_skip {
|
let body_start = if let Some(skip) = self.heredoc_skip {
|
||||||
// A previous heredoc on this line already read its body;
|
// A previous heredoc on this line already read its body;
|
||||||
// our body starts where that one ended
|
// our body starts where that one ended
|
||||||
let skip_offset = skip - cursor_after_delim;
|
let skip_offset = skip - cursor_after_delim;
|
||||||
for _ in 0..skip_offset {
|
for _ in 0..skip_offset {
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
skip
|
skip
|
||||||
} else {
|
} else {
|
||||||
// Skip the rest of the current line to find where the body begins
|
// Skip the rest of the current line to find where the body begins
|
||||||
let mut scan = pos;
|
let mut scan = pos;
|
||||||
let mut found_newline = false;
|
let mut found_newline = false;
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
scan += ch.len_utf8();
|
scan += ch.len_utf8();
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
found_newline = true;
|
found_newline = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found_newline {
|
if !found_newline {
|
||||||
if self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else {
|
} else {
|
||||||
return Err(ShErr::at(
|
return Err(ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
Span::new(pos..pos, self.source.clone()),
|
Span::new(pos..pos, self.source.clone()),
|
||||||
"Heredoc delimiter not found",
|
"Heredoc delimiter not found",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scan
|
scan
|
||||||
};
|
};
|
||||||
|
|
||||||
pos = body_start;
|
pos = body_start;
|
||||||
let start = pos;
|
let start = pos;
|
||||||
|
|
||||||
// Read lines until we find one that matches the delimiter exactly
|
// Read lines until we find one that matches the delimiter exactly
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
let mut line_start = pos;
|
let mut line_start = pos;
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
pos += ch.len_utf8();
|
pos += ch.len_utf8();
|
||||||
if ch == '\n' {
|
if ch == '\n' {
|
||||||
let trimmed = line.trim_end_matches('\r');
|
let trimmed = line.trim_end_matches('\r');
|
||||||
if trimmed == delim {
|
if trimmed == delim {
|
||||||
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
||||||
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
||||||
self.heredoc_skip = Some(pos);
|
self.heredoc_skip = Some(pos);
|
||||||
self.cursor = cursor_after_delim;
|
self.cursor = cursor_after_delim;
|
||||||
return Ok(Some(tk));
|
return Ok(Some(tk));
|
||||||
}
|
}
|
||||||
line.clear();
|
line.clear();
|
||||||
line_start = pos;
|
line_start = pos;
|
||||||
} else {
|
} else {
|
||||||
line.push(ch);
|
line.push(ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check the last line (no trailing newline)
|
// Check the last line (no trailing newline)
|
||||||
let trimmed = line.trim_end_matches('\r');
|
let trimmed = line.trim_end_matches('\r');
|
||||||
if trimmed == delim {
|
if trimmed == delim {
|
||||||
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
let mut tk = self.get_token(start..line_start, TkRule::Redir);
|
||||||
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
tk.flags |= TkFlags::IS_HEREDOC | flags;
|
||||||
self.heredoc_skip = Some(pos);
|
self.heredoc_skip = Some(pos);
|
||||||
self.cursor = cursor_after_delim;
|
self.cursor = cursor_after_delim;
|
||||||
return Ok(Some(tk));
|
return Ok(Some(tk));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
Err(ShErr::at(
|
Err(ShErr::at(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
Span::new(start..pos, self.source.clone()),
|
Span::new(start..pos, self.source.clone()),
|
||||||
format!("Heredoc delimiter '{}' not found", delim),
|
format!("Heredoc delimiter '{}' not found", delim),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
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();
|
||||||
@@ -1113,9 +1114,10 @@ impl Iterator for LexStream {
|
|||||||
// If a heredoc was parsed on this line, skip past the body
|
// If a heredoc was parsed on this line, skip past the body
|
||||||
// Only on newline — ';' is a command separator within the same line
|
// Only on newline — ';' is a command separator within the same line
|
||||||
if (ch == '\n' || ch == '\r')
|
if (ch == '\n' || ch == '\r')
|
||||||
&& let Some(skip) = self.heredoc_skip.take() {
|
&& let Some(skip) = self.heredoc_skip.take()
|
||||||
self.cursor = skip;
|
{
|
||||||
}
|
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 {
|
||||||
|
|||||||
325
src/parse/mod.rs
325
src/parse/mod.rs
@@ -12,7 +12,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
parse::lex::clean_input,
|
parse::lex::clean_input,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::IoMode, state::read_shopts,
|
procio::IoMode,
|
||||||
|
state::read_shopts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod execute;
|
pub mod execute;
|
||||||
@@ -280,17 +281,21 @@ bitflags! {
|
|||||||
pub struct Redir {
|
pub struct Redir {
|
||||||
pub io_mode: IoMode,
|
pub io_mode: IoMode,
|
||||||
pub class: RedirType,
|
pub class: RedirType,
|
||||||
pub span: Option<Span>
|
pub span: Option<Span>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Redir {
|
impl Redir {
|
||||||
pub fn new(io_mode: IoMode, class: RedirType) -> Self {
|
pub fn new(io_mode: IoMode, class: RedirType) -> Self {
|
||||||
Self { io_mode, class, span: None }
|
Self {
|
||||||
|
io_mode,
|
||||||
|
class,
|
||||||
|
span: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn with_span(mut self, span: Span) -> Self {
|
||||||
|
self.span = Some(span);
|
||||||
|
self
|
||||||
}
|
}
|
||||||
pub fn with_span(mut self, span: Span) -> Self {
|
|
||||||
self.span = Some(span);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
@@ -298,7 +303,7 @@ pub struct RedirBldr {
|
|||||||
pub io_mode: Option<IoMode>,
|
pub io_mode: Option<IoMode>,
|
||||||
pub class: Option<RedirType>,
|
pub class: Option<RedirType>,
|
||||||
pub tgt_fd: Option<RawFd>,
|
pub tgt_fd: Option<RawFd>,
|
||||||
pub span: Option<Span>,
|
pub span: Option<Span>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RedirBldr {
|
impl RedirBldr {
|
||||||
@@ -306,36 +311,36 @@ impl RedirBldr {
|
|||||||
Default::default()
|
Default::default()
|
||||||
}
|
}
|
||||||
pub fn with_io_mode(self, io_mode: IoMode) -> Self {
|
pub fn with_io_mode(self, io_mode: IoMode) -> Self {
|
||||||
Self {
|
Self {
|
||||||
io_mode: Some(io_mode),
|
io_mode: Some(io_mode),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_class(self, class: RedirType) -> Self {
|
pub fn with_class(self, class: RedirType) -> Self {
|
||||||
Self {
|
Self {
|
||||||
class: Some(class),
|
class: Some(class),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
|
pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tgt_fd: Some(tgt_fd),
|
tgt_fd: Some(tgt_fd),
|
||||||
..self
|
..self
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
pub fn with_span(self, span: Span) -> Self {
|
||||||
|
Self {
|
||||||
|
span: Some(span),
|
||||||
|
..self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn with_span(self, span: Span) -> Self {
|
|
||||||
Self {
|
|
||||||
span: Some(span),
|
|
||||||
..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn build(self) -> Redir {
|
pub fn build(self) -> Redir {
|
||||||
let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap());
|
let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap());
|
||||||
if let Some(span) = self.span {
|
if let Some(span) = self.span {
|
||||||
new.with_span(span)
|
new.with_span(span)
|
||||||
} else {
|
} else {
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,23 +360,23 @@ impl FromStr for RedirBldr {
|
|||||||
chars.next();
|
chars.next();
|
||||||
redir = redir.with_class(RedirType::Append);
|
redir = redir.with_class(RedirType::Append);
|
||||||
} else if let Some('|') = chars.peek() {
|
} else if let Some('|') = chars.peek() {
|
||||||
chars.next();
|
chars.next();
|
||||||
redir = redir.with_class(RedirType::OutputForce);
|
redir = redir.with_class(RedirType::OutputForce);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'<' => {
|
'<' => {
|
||||||
redir = redir.with_class(RedirType::Input);
|
redir = redir.with_class(RedirType::Input);
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
if chars.peek() == Some(&'>') {
|
if chars.peek() == Some(&'>') {
|
||||||
chars.next(); // consume the '>'
|
chars.next(); // consume the '>'
|
||||||
redir = redir.with_class(RedirType::ReadWrite);
|
redir = redir.with_class(RedirType::ReadWrite);
|
||||||
} else {
|
} else {
|
||||||
while count < 2 && matches!(chars.peek(), Some('<')) {
|
while count < 2 && matches!(chars.peek(), Some('<')) {
|
||||||
chars.next();
|
chars.next();
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redir = match count {
|
redir = match count {
|
||||||
1 => redir.with_class(RedirType::HereDoc),
|
1 => redir.with_class(RedirType::HereDoc),
|
||||||
@@ -380,23 +385,23 @@ impl FromStr for RedirBldr {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
'&' => {
|
'&' => {
|
||||||
if chars.peek() == Some(&'-') {
|
if chars.peek() == Some(&'-') {
|
||||||
chars.next();
|
chars.next();
|
||||||
src_fd.push('-');
|
src_fd.push('-');
|
||||||
} else {
|
} else {
|
||||||
while let Some(next_ch) = chars.next() {
|
while let Some(next_ch) = chars.next() {
|
||||||
if next_ch.is_ascii_digit() {
|
if next_ch.is_ascii_digit() {
|
||||||
src_fd.push(next_ch)
|
src_fd.push(next_ch)
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if src_fd.is_empty() {
|
if src_fd.is_empty() {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("Invalid character '{}' in redirection operator", ch),
|
format!("Invalid character '{}' in redirection operator", ch),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ if ch.is_ascii_digit() && tgt_fd.is_empty() => {
|
_ if ch.is_ascii_digit() && tgt_fd.is_empty() => {
|
||||||
@@ -410,27 +415,26 @@ impl FromStr for RedirBldr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(ShErr::simple(
|
_ => {
|
||||||
ShErrKind::ParseErr,
|
return Err(ShErr::simple(
|
||||||
format!("Invalid character '{}' in redirection operator", ch),
|
ShErrKind::ParseErr,
|
||||||
)),
|
format!("Invalid character '{}' in redirection operator", ch),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let tgt_fd = tgt_fd
|
let tgt_fd = tgt_fd
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.unwrap_or_else(|_| match redir.class.unwrap() {
|
.unwrap_or_else(|_| match redir.class.unwrap() {
|
||||||
RedirType::Input |
|
RedirType::Input | RedirType::ReadWrite | RedirType::HereDoc | RedirType::HereString => 0,
|
||||||
RedirType::ReadWrite |
|
|
||||||
RedirType::HereDoc |
|
|
||||||
RedirType::HereString => 0,
|
|
||||||
_ => 1,
|
_ => 1,
|
||||||
});
|
});
|
||||||
redir = redir.with_tgt(tgt_fd);
|
redir = redir.with_tgt(tgt_fd);
|
||||||
if src_fd.as_str() == "-" {
|
if src_fd.as_str() == "-" {
|
||||||
let io_mode = IoMode::Close { tgt_fd };
|
let io_mode = IoMode::Close { tgt_fd };
|
||||||
redir = redir.with_io_mode(io_mode);
|
redir = redir.with_io_mode(io_mode);
|
||||||
} else if let Ok(src_fd) = src_fd.parse::<i32>() {
|
} else if let Ok(src_fd) = src_fd.parse::<i32>() {
|
||||||
let io_mode = IoMode::fd(tgt_fd, src_fd);
|
let io_mode = IoMode::fd(tgt_fd, src_fd);
|
||||||
redir = redir.with_io_mode(io_mode);
|
redir = redir.with_io_mode(io_mode);
|
||||||
}
|
}
|
||||||
@@ -439,40 +443,40 @@ impl FromStr for RedirBldr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<Tk> for RedirBldr {
|
impl TryFrom<Tk> for RedirBldr {
|
||||||
type Error = ShErr;
|
type Error = ShErr;
|
||||||
fn try_from(tk: Tk) -> Result<Self, Self::Error> {
|
fn try_from(tk: Tk) -> Result<Self, Self::Error> {
|
||||||
let span = tk.span.clone();
|
let span = tk.span.clone();
|
||||||
if tk.flags.contains(TkFlags::IS_HEREDOC) {
|
if tk.flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
let flags = tk.flags;
|
let flags = tk.flags;
|
||||||
|
|
||||||
Ok(RedirBldr {
|
Ok(RedirBldr {
|
||||||
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
|
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
|
||||||
class: Some(RedirType::HereDoc),
|
class: Some(RedirType::HereDoc),
|
||||||
tgt_fd: Some(0),
|
tgt_fd: Some(0),
|
||||||
span: Some(span)
|
span: Some(span),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
match Self::from_str(tk.as_str()) {
|
match Self::from_str(tk.as_str()) {
|
||||||
Ok(bldr) => Ok(bldr.with_span(span)),
|
Ok(bldr) => Ok(bldr.with_span(span)),
|
||||||
Err(e) => Err(e.promote(span)),
|
Err(e) => Err(e.promote(span)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Copy, Debug)]
|
#[derive(PartialEq, Clone, Copy, Debug)]
|
||||||
pub enum RedirType {
|
pub enum RedirType {
|
||||||
Null, // Default
|
Null, // Default
|
||||||
Pipe, // |
|
Pipe, // |
|
||||||
PipeAnd, // |&, redirs stderr and stdout
|
PipeAnd, // |&, redirs stderr and stdout
|
||||||
Input, // <
|
Input, // <
|
||||||
Output, // >
|
Output, // >
|
||||||
OutputForce,// >|
|
OutputForce, // >|
|
||||||
Append, // >>
|
Append, // >>
|
||||||
HereDoc, // <<
|
HereDoc, // <<
|
||||||
IndentHereDoc, // <<-, strips leading tabs
|
IndentHereDoc, // <<-, strips leading tabs
|
||||||
HereString, // <<<
|
HereString, // <<<
|
||||||
ReadWrite, // <>, fd is opened for reading and writing
|
ReadWrite, // <>, fd is opened for reading and writing
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -887,7 +891,9 @@ impl ParseStream {
|
|||||||
|
|
||||||
// Two forms: "name()" as one token, or "name" followed by "()" as separate tokens
|
// Two forms: "name()" as one token, or "name" followed by "()" as separate tokens
|
||||||
let spaced_form = !is_func_name(self.peek_tk())
|
let spaced_form = !is_func_name(self.peek_tk())
|
||||||
&& self.peek_tk().is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD))
|
&& self
|
||||||
|
.peek_tk()
|
||||||
|
.is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD))
|
||||||
&& is_func_parens(self.tokens.get(1));
|
&& is_func_parens(self.tokens.get(1));
|
||||||
|
|
||||||
if !is_func_name(self.peek_tk()) && !spaced_form {
|
if !is_func_name(self.peek_tk()) && !spaced_form {
|
||||||
@@ -1032,7 +1038,7 @@ impl ParseStream {
|
|||||||
Ok(Some(node))
|
Ok(Some(node))
|
||||||
}
|
}
|
||||||
fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> {
|
fn parse_brc_grp(&mut self, from_func_def: bool) -> ShResult<Option<Node>> {
|
||||||
log::debug!("Trying to parse a brace group");
|
log::debug!("Trying to parse a brace group");
|
||||||
let mut node_tks: Vec<Tk> = vec![];
|
let mut node_tks: Vec<Tk> = vec![];
|
||||||
let mut body: Vec<Node> = vec![];
|
let mut body: Vec<Node> = vec![];
|
||||||
let mut redirs: Vec<Redir> = vec![];
|
let mut redirs: Vec<Redir> = vec![];
|
||||||
@@ -1045,7 +1051,7 @@ impl ParseStream {
|
|||||||
self.catch_separator(&mut node_tks);
|
self.catch_separator(&mut node_tks);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
log::debug!("Parsing a brace group body");
|
log::debug!("Parsing a brace group body");
|
||||||
if *self.next_tk_class() == TkRule::BraceGrpEnd {
|
if *self.next_tk_class() == TkRule::BraceGrpEnd {
|
||||||
node_tks.push(self.next_tk().unwrap());
|
node_tks.push(self.next_tk().unwrap());
|
||||||
break;
|
break;
|
||||||
@@ -1054,25 +1060,25 @@ impl ParseStream {
|
|||||||
node_tks.extend(node.tokens.clone());
|
node_tks.extend(node.tokens.clone());
|
||||||
body.push(node);
|
body.push(node);
|
||||||
} else if *self.next_tk_class() != TkRule::BraceGrpEnd {
|
} else if *self.next_tk_class() != TkRule::BraceGrpEnd {
|
||||||
let next = self.peek_tk().cloned();
|
let next = self.peek_tk().cloned();
|
||||||
let err = match next {
|
let err = match next {
|
||||||
Some(tk) => Err(parse_err_full(
|
Some(tk) => Err(parse_err_full(
|
||||||
&format!("Unexpected token '{}' in brace group body", tk.as_str()),
|
&format!("Unexpected token '{}' in brace group body", tk.as_str()),
|
||||||
&tk.span,
|
&tk.span,
|
||||||
self.context.clone(),
|
self.context.clone(),
|
||||||
)),
|
)),
|
||||||
None => Err(parse_err_full(
|
None => Err(parse_err_full(
|
||||||
"Unexpected end of input while parsing brace group body",
|
"Unexpected end of input while parsing brace group body",
|
||||||
&node_tks.get_span().unwrap(),
|
&node_tks.get_span().unwrap(),
|
||||||
self.context.clone(),
|
self.context.clone(),
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
self.panic_mode(&mut node_tks);
|
self.panic_mode(&mut node_tks);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
self.catch_separator(&mut node_tks);
|
self.catch_separator(&mut node_tks);
|
||||||
if !self.next_tk_is_some() {
|
if !self.next_tk_is_some() {
|
||||||
log::debug!("Hit end of input while parsing a brace group body, entering panic mode");
|
log::debug!("Hit end of input while parsing a brace group body, entering panic mode");
|
||||||
self.panic_mode(&mut node_tks);
|
self.panic_mode(&mut node_tks);
|
||||||
return Err(parse_err_full(
|
return Err(parse_err_full(
|
||||||
"Expected a closing brace for this brace group",
|
"Expected a closing brace for this brace group",
|
||||||
@@ -1082,13 +1088,15 @@ impl ParseStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Finished parsing brace group body, now looking for redirections if it's not a function definition");
|
log::debug!(
|
||||||
|
"Finished parsing brace group body, now looking for redirections if it's not a function definition"
|
||||||
|
);
|
||||||
|
|
||||||
if !from_func_def {
|
if !from_func_def {
|
||||||
self.parse_redir(&mut redirs, &mut node_tks)?;
|
self.parse_redir(&mut redirs, &mut node_tks)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::debug!("Finished parsing brace group redirections, constructing node");
|
log::debug!("Finished parsing brace group redirections, constructing node");
|
||||||
|
|
||||||
let node = Node {
|
let node = Node {
|
||||||
class: NdRule::BraceGrp { body },
|
class: NdRule::BraceGrp { body },
|
||||||
@@ -1106,7 +1114,11 @@ impl ParseStream {
|
|||||||
context: LabelCtx,
|
context: LabelCtx,
|
||||||
) -> ShResult<Redir> {
|
) -> ShResult<Redir> {
|
||||||
let redir_bldr = RedirBldr::try_from(redir_tk.clone()).unwrap();
|
let redir_bldr = RedirBldr::try_from(redir_tk.clone()).unwrap();
|
||||||
let next_tk = if redir_bldr.io_mode.is_none() { next() } else { None };
|
let next_tk = if redir_bldr.io_mode.is_none() {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if redir_bldr.io_mode.is_some() {
|
if redir_bldr.io_mode.is_some() {
|
||||||
return Ok(redir_bldr.build());
|
return Ok(redir_bldr.build());
|
||||||
}
|
}
|
||||||
@@ -1126,11 +1138,7 @@ impl ParseStream {
|
|||||||
"Expected a string after this redirection",
|
"Expected a string after this redirection",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let mut string = next_tk
|
let mut string = next_tk.unwrap().expand()?.get_words().join(" ");
|
||||||
.unwrap()
|
|
||||||
.expand()?
|
|
||||||
.get_words()
|
|
||||||
.join(" ");
|
|
||||||
string.push('\n');
|
string.push('\n');
|
||||||
let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?;
|
let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?;
|
||||||
Ok(redir_bldr.with_io_mode(io_mode).build())
|
Ok(redir_bldr.with_io_mode(io_mode).build())
|
||||||
@@ -1155,7 +1163,7 @@ impl ParseStream {
|
|||||||
while self.check_redir() {
|
while self.check_redir() {
|
||||||
let tk = self.next_tk().unwrap();
|
let tk = self.next_tk().unwrap();
|
||||||
node_tks.push(tk.clone());
|
node_tks.push(tk.clone());
|
||||||
let ctx = self.context.clone();
|
let ctx = self.context.clone();
|
||||||
let redir = Self::build_redir(&tk, || self.next_tk(), node_tks, ctx)?;
|
let redir = Self::build_redir(&tk, || self.next_tk(), node_tks, ctx)?;
|
||||||
redirs.push(redir);
|
redirs.push(redir);
|
||||||
}
|
}
|
||||||
@@ -1663,7 +1671,7 @@ impl ParseStream {
|
|||||||
node_tks.push(prefix_tk.clone());
|
node_tks.push(prefix_tk.clone());
|
||||||
assignments.push(assign)
|
assignments.push(assign)
|
||||||
} else if is_keyword {
|
} else if is_keyword {
|
||||||
return Ok(None)
|
return Ok(None);
|
||||||
} else if prefix_tk.class == TkRule::Sep {
|
} else if prefix_tk.class == TkRule::Sep {
|
||||||
// Separator ends the prefix section - add it so commit() consumes it
|
// Separator ends the prefix section - add it so commit() consumes it
|
||||||
node_tks.push(prefix_tk.clone());
|
node_tks.push(prefix_tk.clone());
|
||||||
@@ -1721,7 +1729,7 @@ impl ParseStream {
|
|||||||
}
|
}
|
||||||
TkRule::Redir => {
|
TkRule::Redir => {
|
||||||
node_tks.push(tk.clone());
|
node_tks.push(tk.clone());
|
||||||
let ctx = self.context.clone();
|
let ctx = self.context.clone();
|
||||||
let redir = Self::build_redir(tk, || tk_iter.next().cloned(), &mut node_tks, ctx)?;
|
let redir = Self::build_redir(tk, || tk_iter.next().cloned(), &mut node_tks, ctx)?;
|
||||||
redirs.push(redir);
|
redirs.push(redir);
|
||||||
}
|
}
|
||||||
@@ -1882,34 +1890,33 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
|
|||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
let result = match class {
|
let result = match class {
|
||||||
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
|
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
|
||||||
RedirType::Output => {
|
RedirType::Output => {
|
||||||
if read_shopts(|o| o.core.noclobber) && path.is_file() {
|
if read_shopts(|o| o.core.noclobber) && path.is_file() {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("shopt core.noclobber is set, refusing to overwrite existing file `{}`", path.display()),
|
format!(
|
||||||
));
|
"shopt core.noclobber is set, refusing to overwrite existing file `{}`",
|
||||||
}
|
path.display()
|
||||||
OpenOptions::new()
|
),
|
||||||
.write(true)
|
));
|
||||||
.create(true)
|
}
|
||||||
.truncate(true)
|
OpenOptions::new()
|
||||||
.open(path)
|
.write(true)
|
||||||
},
|
.create(true)
|
||||||
RedirType::ReadWrite => {
|
.truncate(true)
|
||||||
OpenOptions::new()
|
.open(path)
|
||||||
.write(true)
|
}
|
||||||
.read(true)
|
RedirType::ReadWrite => OpenOptions::new()
|
||||||
.create(true)
|
.write(true)
|
||||||
.truncate(false)
|
.read(true)
|
||||||
.open(path)
|
.create(true)
|
||||||
}
|
.truncate(false)
|
||||||
RedirType::OutputForce => {
|
.open(path),
|
||||||
OpenOptions::new()
|
RedirType::OutputForce => OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.truncate(true)
|
.truncate(true)
|
||||||
.open(path)
|
.open(path),
|
||||||
}
|
|
||||||
RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
|
RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
|
||||||
_ => unimplemented!("Unimplemented redir type: {:?}", class),
|
_ => unimplemented!("Unimplemented redir type: {:?}", class),
|
||||||
};
|
};
|
||||||
@@ -1936,9 +1943,7 @@ fn is_func_name(tk: Option<&Tk>) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_func_parens(tk: Option<&Tk>) -> bool {
|
fn is_func_parens(tk: Option<&Tk>) -> bool {
|
||||||
tk.is_some_and(|tk| {
|
tk.is_some_and(|tk| tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()")
|
||||||
tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()"
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform an operation on the child nodes of a given node
|
/// Perform an operation on the child nodes of a given node
|
||||||
@@ -2814,8 +2819,8 @@ pub mod tests {
|
|||||||
|
|
||||||
// ===================== Heredoc Execution =====================
|
// ===================== Heredoc Execution =====================
|
||||||
|
|
||||||
use crate::testutil::{TestGuard, test_input};
|
|
||||||
use crate::state::{VarFlags, VarKind, write_vars};
|
use crate::state::{VarFlags, VarKind, write_vars};
|
||||||
|
use crate::testutil::{TestGuard, test_input};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn heredoc_basic_output() {
|
fn heredoc_basic_output() {
|
||||||
|
|||||||
158
src/procio.rs
158
src/procio.rs
@@ -12,7 +12,8 @@ use crate::{
|
|||||||
utils::RedirVecUtils,
|
utils::RedirVecUtils,
|
||||||
},
|
},
|
||||||
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
|
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
|
||||||
prelude::*, state,
|
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
|
||||||
@@ -48,9 +49,9 @@ pub enum IoMode {
|
|||||||
pipe: Arc<OwnedFd>,
|
pipe: Arc<OwnedFd>,
|
||||||
},
|
},
|
||||||
Buffer {
|
Buffer {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
buf: String,
|
buf: String,
|
||||||
flags: TkFlags, // so we can see if its a heredoc or not
|
flags: TkFlags, // so we can see if its a heredoc or not
|
||||||
},
|
},
|
||||||
Close {
|
Close {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
@@ -91,7 +92,9 @@ 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, TkFlags::empty())?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of multiple paths
|
let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())?
|
||||||
|
.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);
|
||||||
|
|
||||||
@@ -100,8 +103,7 @@ impl IoMode {
|
|||||||
// collides with the target fd (e.g. `3>/tmp/foo` where open() returns 3,
|
// 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).
|
// causing dup2(3,3) to be a no-op and then OwnedFd drop closes it).
|
||||||
let raw = file.as_raw_fd();
|
let raw = file.as_raw_fd();
|
||||||
let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD))
|
let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).map_err(ShErr::from)?;
|
||||||
.map_err(ShErr::from)?;
|
|
||||||
drop(file); // closes the original low fd
|
drop(file); // closes the original low fd
|
||||||
self = IoMode::OpenedFile {
|
self = IoMode::OpenedFile {
|
||||||
tgt_fd,
|
tgt_fd,
|
||||||
@@ -110,9 +112,9 @@ impl IoMode {
|
|||||||
}
|
}
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
|
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
|
||||||
Ok(Self::Buffer { tgt_fd, buf, flags })
|
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();
|
||||||
(
|
(
|
||||||
@@ -244,74 +246,78 @@ impl<'e> IoFrame {
|
|||||||
fn apply_redirs(&mut self) -> ShResult<()> {
|
fn apply_redirs(&mut self) -> ShResult<()> {
|
||||||
for redir in &mut self.redirs {
|
for redir in &mut self.redirs {
|
||||||
let io_mode = &mut redir.io_mode;
|
let io_mode = &mut redir.io_mode;
|
||||||
match io_mode {
|
match io_mode {
|
||||||
IoMode::Close { tgt_fd } => {
|
IoMode::Close { tgt_fd } => {
|
||||||
if *tgt_fd == *TTY_FILENO {
|
if *tgt_fd == *TTY_FILENO {
|
||||||
// Don't let user close the shell's tty fd.
|
// Don't let user close the shell's tty fd.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
close(*tgt_fd).ok();
|
close(*tgt_fd).ok();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
IoMode::File { .. } => {
|
IoMode::File { .. } => match io_mode.clone().open_file() {
|
||||||
match io_mode.clone().open_file() {
|
Ok(file) => *io_mode = file,
|
||||||
Ok(file) => *io_mode = file,
|
Err(e) => {
|
||||||
Err(e) => {
|
if let Some(span) = redir.span.as_ref() {
|
||||||
if let Some(span) = redir.span.as_ref() {
|
return Err(e.promote(span.clone()));
|
||||||
return Err(e.promote(span.clone()));
|
}
|
||||||
}
|
return Err(e);
|
||||||
return Err(e)
|
}
|
||||||
}
|
},
|
||||||
}
|
IoMode::Buffer { tgt_fd, buf, flags } => {
|
||||||
}
|
let (rpipe, wpipe) = nix::unistd::pipe()?;
|
||||||
IoMode::Buffer { tgt_fd, buf, flags } => {
|
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
|
||||||
let (rpipe, wpipe) = nix::unistd::pipe()?;
|
buf.clone()
|
||||||
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
|
} else {
|
||||||
buf.clone()
|
let words = Expander::from_raw(buf, *flags)?.expand()?;
|
||||||
} else {
|
if flags.contains(TkFlags::IS_HEREDOC) {
|
||||||
let words = Expander::from_raw(buf, *flags)?.expand()?;
|
words.into_iter().next().unwrap_or_default()
|
||||||
if flags.contains(TkFlags::IS_HEREDOC) {
|
} else {
|
||||||
words.into_iter().next().unwrap_or_default()
|
let ifs = state::get_separator();
|
||||||
} else {
|
words.join(&ifs).trim().to_string() + "\n"
|
||||||
let ifs = state::get_separator();
|
}
|
||||||
words.join(&ifs).trim().to_string() + "\n"
|
};
|
||||||
}
|
if flags.contains(TkFlags::TAB_HEREDOC) {
|
||||||
};
|
let lines = text.lines();
|
||||||
if flags.contains(TkFlags::TAB_HEREDOC) {
|
let mut min_tabs = usize::MAX;
|
||||||
let lines = text.lines();
|
for line in lines {
|
||||||
let mut min_tabs = usize::MAX;
|
if line.is_empty() {
|
||||||
for line in lines {
|
continue;
|
||||||
if line.is_empty() { continue; }
|
}
|
||||||
let line_len = line.len();
|
let line_len = line.len();
|
||||||
let after_strip = line.trim_start_matches('\t').len();
|
let after_strip = line.trim_start_matches('\t').len();
|
||||||
let delta = line_len - after_strip;
|
let delta = line_len - after_strip;
|
||||||
min_tabs = min_tabs.min(delta);
|
min_tabs = min_tabs.min(delta);
|
||||||
}
|
}
|
||||||
if min_tabs == usize::MAX {
|
if min_tabs == usize::MAX {
|
||||||
// let's avoid possibly allocating a string with 18 quintillion tabs
|
// let's avoid possibly allocating a string with 18 quintillion tabs
|
||||||
min_tabs = 0;
|
min_tabs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if min_tabs > 0 {
|
if min_tabs > 0 {
|
||||||
let stripped = text.lines()
|
let stripped = text
|
||||||
.fold(vec![], |mut acc, ln| {
|
.lines()
|
||||||
if ln.is_empty() {
|
.fold(vec![], |mut acc, ln| {
|
||||||
acc.push("");
|
if ln.is_empty() {
|
||||||
return acc;
|
acc.push("");
|
||||||
}
|
return acc;
|
||||||
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
|
}
|
||||||
acc.push(stripped_ln);
|
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
|
||||||
acc
|
acc.push(stripped_ln);
|
||||||
})
|
acc
|
||||||
.join("\n");
|
})
|
||||||
text = stripped + "\n";
|
.join("\n");
|
||||||
}
|
text = stripped + "\n";
|
||||||
}
|
}
|
||||||
write(wpipe, text.as_bytes())?;
|
}
|
||||||
*io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() };
|
write(wpipe, text.as_bytes())?;
|
||||||
}
|
*io_mode = IoMode::Pipe {
|
||||||
_ => {}
|
tgt_fd: *tgt_fd,
|
||||||
}
|
pipe: rpipe.into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
let tgt_fd = io_mode.tgt_fd();
|
let tgt_fd = io_mode.tgt_fd();
|
||||||
let src_fd = io_mode.src_fd();
|
let src_fd = io_mode.src_fd();
|
||||||
if let Err(e) = dup2(src_fd, tgt_fd) {
|
if let Err(e) = dup2(src_fd, tgt_fd) {
|
||||||
|
|||||||
@@ -352,69 +352,73 @@ impl ClampedUsize {
|
|||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub struct IndentCtx {
|
pub struct IndentCtx {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
ctx: Vec<Tk>,
|
ctx: Vec<Tk>,
|
||||||
in_escaped_line: bool
|
in_escaped_line: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IndentCtx {
|
impl IndentCtx {
|
||||||
pub fn new() -> Self { Self::default() }
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn depth(&self) -> usize {
|
pub fn depth(&self) -> usize {
|
||||||
self.depth
|
self.depth
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ctx(&self) -> &[Tk] {
|
pub fn ctx(&self) -> &[Tk] {
|
||||||
&self.ctx
|
&self.ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descend(&mut self, tk: Tk) {
|
pub fn descend(&mut self, tk: Tk) {
|
||||||
self.ctx.push(tk);
|
self.ctx.push(tk);
|
||||||
self.depth += 1;
|
self.depth += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ascend(&mut self) {
|
pub fn ascend(&mut self) {
|
||||||
self.depth = self.depth.saturating_sub(1);
|
self.depth = self.depth.saturating_sub(1);
|
||||||
self.ctx.pop();
|
self.ctx.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
std::mem::take(self);
|
std::mem::take(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_tk(&mut self, tk: Tk) {
|
pub fn check_tk(&mut self, tk: Tk) {
|
||||||
if tk.is_opener() {
|
if tk.is_opener() {
|
||||||
self.descend(tk);
|
self.descend(tk);
|
||||||
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
|
} else if self.ctx.last().is_some_and(|t| tk.is_closer_for(t)) {
|
||||||
self.ascend();
|
self.ascend();
|
||||||
} else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line {
|
} else if matches!(tk.class, TkRule::Sep) && self.in_escaped_line {
|
||||||
self.in_escaped_line = false;
|
self.in_escaped_line = false;
|
||||||
self.depth = self.depth.saturating_sub(1);
|
self.depth = self.depth.saturating_sub(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn calculate(&mut self, input: &str) -> usize {
|
pub fn calculate(&mut self, input: &str) -> usize {
|
||||||
self.depth = 0;
|
self.depth = 0;
|
||||||
self.ctx.clear();
|
self.ctx.clear();
|
||||||
self.in_escaped_line = false;
|
self.in_escaped_line = false;
|
||||||
|
|
||||||
let input_arc = Arc::new(input.to_string());
|
let input_arc = Arc::new(input.to_string());
|
||||||
let Ok(tokens) = LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
|
let Ok(tokens) =
|
||||||
log::error!("Lexing failed during depth calculation: {:?}", input);
|
LexStream::new(input_arc, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
|
||||||
return 0;
|
else {
|
||||||
};
|
log::error!("Lexing failed during depth calculation: {:?}", input);
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
for tk in tokens {
|
for tk in tokens {
|
||||||
self.check_tk(tk);
|
self.check_tk(tk);
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.ends_with("\\\n") {
|
if input.ends_with("\\\n") {
|
||||||
self.in_escaped_line = true;
|
self.in_escaped_line = true;
|
||||||
self.depth += 1;
|
self.depth += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.depth
|
self.depth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
@@ -684,17 +688,17 @@ impl LineBuf {
|
|||||||
pub fn read_slice_to_cursor(&self) -> Option<&str> {
|
pub fn read_slice_to_cursor(&self) -> Option<&str> {
|
||||||
self.read_slice_to(self.cursor.get())
|
self.read_slice_to(self.cursor.get())
|
||||||
}
|
}
|
||||||
pub fn cursor_is_escaped(&mut self) -> bool {
|
pub fn cursor_is_escaped(&mut self) -> bool {
|
||||||
let Some(to_cursor) = self.slice_to_cursor() else {
|
let Some(to_cursor) = self.slice_to_cursor() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// count the number of backslashes
|
// count the number of backslashes
|
||||||
let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len();
|
let delta = to_cursor.len() - to_cursor.trim_end_matches('\\').len();
|
||||||
|
|
||||||
// an even number of backslashes means each one is escaped
|
// an even number of backslashes means each one is escaped
|
||||||
delta % 2 != 0
|
delta % 2 != 0
|
||||||
}
|
}
|
||||||
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
|
pub fn slice_to_cursor_inclusive(&mut self) -> Option<&str> {
|
||||||
self.slice_to(self.cursor.ret_add(1))
|
self.slice_to(self.cursor.ret_add(1))
|
||||||
}
|
}
|
||||||
@@ -2928,29 +2932,29 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
end = end.saturating_sub(1);
|
end = end.saturating_sub(1);
|
||||||
let mut last_was_whitespace = false;
|
let mut last_was_whitespace = false;
|
||||||
let mut last_was_escape = false;
|
let mut last_was_escape = false;
|
||||||
let mut i = start;
|
let mut i = start;
|
||||||
while i < end {
|
while i < end {
|
||||||
let Some(gr) = self.grapheme_at(i) else {
|
let Some(gr) = self.grapheme_at(i) else {
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if gr == "\n" {
|
if gr == "\n" {
|
||||||
if last_was_whitespace {
|
if last_was_whitespace {
|
||||||
self.remove(i);
|
self.remove(i);
|
||||||
end -= 1;
|
end -= 1;
|
||||||
} else {
|
} else {
|
||||||
self.force_replace_at(i, " ");
|
self.force_replace_at(i, " ");
|
||||||
}
|
}
|
||||||
if last_was_escape {
|
if last_was_escape {
|
||||||
// if we are here, then we just joined an escaped newline
|
// if we are here, then we just joined an escaped newline
|
||||||
// semantically, echo foo\\nbar == echo foo bar
|
// semantically, echo foo\\nbar == echo foo bar
|
||||||
// so a joined line should remove the escape.
|
// so a joined line should remove the escape.
|
||||||
self.remove(i - 1);
|
self.remove(i - 1);
|
||||||
end -= 1;
|
end -= 1;
|
||||||
}
|
}
|
||||||
last_was_whitespace = false;
|
last_was_whitespace = false;
|
||||||
last_was_escape = false;
|
last_was_escape = false;
|
||||||
let strip_pos = if self.grapheme_at(i) == Some(" ") {
|
let strip_pos = if self.grapheme_at(i) == Some(" ") {
|
||||||
i + 1
|
i + 1
|
||||||
} else {
|
} else {
|
||||||
@@ -2958,24 +2962,24 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
while self.grapheme_at(strip_pos) == Some("\t") {
|
while self.grapheme_at(strip_pos) == Some("\t") {
|
||||||
self.remove(strip_pos);
|
self.remove(strip_pos);
|
||||||
end -= 1;
|
end -= 1;
|
||||||
}
|
}
|
||||||
self.cursor.set(i);
|
self.cursor.set(i);
|
||||||
i += 1;
|
i += 1;
|
||||||
continue;
|
continue;
|
||||||
} else if gr == "\\" {
|
} else if gr == "\\" {
|
||||||
if last_was_whitespace && last_was_escape {
|
if last_was_whitespace && last_was_escape {
|
||||||
// if we are here, then the pattern of the last three chars was this:
|
// if we are here, then the pattern of the last three chars was this:
|
||||||
// ' \\', a space and two backslashes.
|
// ' \\', a space and two backslashes.
|
||||||
// This means the "last" was an escaped backslash, not whitespace.
|
// This means the "last" was an escaped backslash, not whitespace.
|
||||||
last_was_whitespace = false;
|
last_was_whitespace = false;
|
||||||
}
|
}
|
||||||
last_was_escape = !last_was_escape;
|
last_was_escape = !last_was_escape;
|
||||||
} else {
|
} else {
|
||||||
last_was_whitespace = is_whitespace(gr);
|
last_was_whitespace = is_whitespace(gr);
|
||||||
last_was_escape = false;
|
last_was_escape = false;
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2984,24 +2988,23 @@ impl LineBuf {
|
|||||||
self.cursor.add(1);
|
self.cursor.add(1);
|
||||||
let before_escaped = self.indent_ctx.in_escaped_line;
|
let before_escaped = self.indent_ctx.in_escaped_line;
|
||||||
let before = self.indent_ctx.depth();
|
let before = self.indent_ctx.depth();
|
||||||
if read_shopts(|o| o.prompt.auto_indent) {
|
if read_shopts(|o| o.prompt.auto_indent) {
|
||||||
let after = self.calc_indent_level();
|
let after = self.calc_indent_level();
|
||||||
// Only dedent if the depth decrease came from a closer, not from
|
// Only dedent if the depth decrease came from a closer, not from
|
||||||
// a line continuation bonus going away
|
// a line continuation bonus going away
|
||||||
if after < before
|
if after < before && !(before_escaped && !self.indent_ctx.in_escaped_line) {
|
||||||
&& !(before_escaped && !self.indent_ctx.in_escaped_line) {
|
let delta = before - after;
|
||||||
let delta = before - after;
|
let line_start = self.start_of_line();
|
||||||
let line_start = self.start_of_line();
|
for _ in 0..delta {
|
||||||
for _ in 0..delta {
|
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
|
||||||
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
|
self.remove(line_start);
|
||||||
self.remove(line_start);
|
if !self.cursor_at_max() {
|
||||||
if !self.cursor_at_max() {
|
self.cursor.sub(1);
|
||||||
self.cursor.sub(1);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fn verb_insert(&mut self, string: String) {
|
fn verb_insert(&mut self, string: String) {
|
||||||
self.insert_str_at_cursor(&string);
|
self.insert_str_at_cursor(&string);
|
||||||
@@ -3038,7 +3041,7 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
#[allow(clippy::unnecessary_to_owned)]
|
#[allow(clippy::unnecessary_to_owned)]
|
||||||
fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> {
|
fn verb_dedent(&mut self, motion: MotionKind) -> ShResult<()> {
|
||||||
let Some((start, mut end)) = self.range_from_motion(&motion) else {
|
let Some((start, mut end)) = self.range_from_motion(&motion) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -3270,27 +3273,27 @@ impl LineBuf {
|
|||||||
) -> ShResult<()> {
|
) -> ShResult<()> {
|
||||||
match verb {
|
match verb {
|
||||||
Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?,
|
Verb::Delete | Verb::Yank | Verb::Change => self.verb_ydc(motion, register, verb)?,
|
||||||
Verb::Rot13 => self.verb_rot13(motion)?,
|
Verb::Rot13 => self.verb_rot13(motion)?,
|
||||||
Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?,
|
Verb::ReplaceChar(ch) => self.verb_replace_char(motion, ch)?,
|
||||||
Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?,
|
Verb::ReplaceCharInplace(ch, count) => self.verb_replace_char_inplace(ch, count)?,
|
||||||
Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count),
|
Verb::ToggleCaseInplace(count) => self.verb_toggle_case_inplace(count),
|
||||||
Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?,
|
Verb::ToggleCaseRange => self.verb_case_transform(motion, CaseTransform::Toggle)?,
|
||||||
Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?,
|
Verb::ToLower => self.verb_case_transform(motion, CaseTransform::Lower)?,
|
||||||
Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?,
|
Verb::ToUpper => self.verb_case_transform(motion, CaseTransform::Upper)?,
|
||||||
Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?,
|
Verb::Redo | Verb::Undo => self.verb_undo_redo(verb)?,
|
||||||
Verb::RepeatLast => todo!(),
|
Verb::RepeatLast => todo!(),
|
||||||
Verb::Put(anchor) => self.verb_put(anchor, register)?,
|
Verb::Put(anchor) => self.verb_put(anchor, register)?,
|
||||||
Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(),
|
Verb::SwapVisualAnchor => self.verb_swap_visual_anchor(),
|
||||||
Verb::JoinLines => self.verb_join_lines()?,
|
Verb::JoinLines => self.verb_join_lines()?,
|
||||||
Verb::InsertChar(ch) => self.verb_insert_char(ch),
|
Verb::InsertChar(ch) => self.verb_insert_char(ch),
|
||||||
Verb::Insert(string) => self.verb_insert(string),
|
Verb::Insert(string) => self.verb_insert(string),
|
||||||
Verb::Indent => self.verb_indent(motion)?,
|
Verb::Indent => self.verb_indent(motion)?,
|
||||||
Verb::Dedent => self.verb_dedent(motion)?,
|
Verb::Dedent => self.verb_dedent(motion)?,
|
||||||
Verb::Equalize => todo!(),
|
Verb::Equalize => todo!(),
|
||||||
Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?,
|
Verb::InsertModeLineBreak(anchor) => self.verb_insert_mode_line_break(anchor)?,
|
||||||
Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?,
|
Verb::AcceptLineOrNewline => self.verb_accept_line_or_newline()?,
|
||||||
Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?,
|
Verb::IncrementNumber(n) => self.verb_adjust_number(n as i64)?,
|
||||||
Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?,
|
Verb::DecrementNumber(n) => self.verb_adjust_number(-(n as i64))?,
|
||||||
Verb::Complete
|
Verb::Complete
|
||||||
| Verb::ExMode
|
| Verb::ExMode
|
||||||
| Verb::EndOfFile
|
| Verb::EndOfFile
|
||||||
@@ -3349,9 +3352,9 @@ impl LineBuf {
|
|||||||
/*
|
/*
|
||||||
* Let's evaluate the motion now
|
* Let's evaluate the motion now
|
||||||
* If we got some weird command like 'dvw' we will
|
* If we got some weird command like 'dvw' we will
|
||||||
* have to simulate a visual selection to get the range
|
* have to simulate a visual selection to get the range
|
||||||
* If motion is None, we will try to use self.select_range
|
* If motion is None, we will try to use self.select_range
|
||||||
* If self.select_range is None, we will use MotionKind::Null
|
* If self.select_range is None, we will use MotionKind::Null
|
||||||
*/
|
*/
|
||||||
let motion_eval =
|
let motion_eval =
|
||||||
if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
|
if flags.intersects(CmdFlags::VISUAL | CmdFlags::VISUAL_LINE | CmdFlags::VISUAL_BLOCK) {
|
||||||
|
|||||||
@@ -441,11 +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)?;
|
||||||
@@ -628,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();
|
||||||
@@ -1257,10 +1248,6 @@ impl ShedVi {
|
|||||||
for _ in 0..repeat {
|
for _ in 0..repeat {
|
||||||
let cmds = cmds.clone();
|
let cmds = cmds.clone();
|
||||||
for (i, cmd) in cmds.iter().enumerate() {
|
for (i, cmd) in cmds.iter().enumerate() {
|
||||||
log::debug!(
|
|
||||||
"Replaying command {cmd:?} in mode {:?}, replay {i}/{repeat}",
|
|
||||||
self.mode.report_mode()
|
|
||||||
);
|
|
||||||
self.exec_cmd(cmd.clone(), true)?;
|
self.exec_cmd(cmd.clone(), true)?;
|
||||||
// After the first command, start merging so all subsequent
|
// After the first command, start merging so all subsequent
|
||||||
// edits fold into one undo entry (e.g. cw + inserted chars)
|
// edits fold into one undo entry (e.g. cw + inserted chars)
|
||||||
@@ -1430,8 +1417,6 @@ pub fn annotate_input(input: &str) -> String {
|
|||||||
for tk in tokens.into_iter().rev() {
|
for tk in tokens.into_iter().rev() {
|
||||||
let insertions = annotate_token(tk);
|
let insertions = annotate_token(tk);
|
||||||
for (pos, marker) in insertions {
|
for (pos, marker) in insertions {
|
||||||
log::info!("pos: {pos}, marker: {marker:?}");
|
|
||||||
log::info!("before: {annotated:?}");
|
|
||||||
let pos = pos.max(0).min(annotated.len());
|
let pos = pos.max(0).min(annotated.len());
|
||||||
annotated.insert(pos, marker);
|
annotated.insert(pos, marker);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,22 +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) {
|
||||||
|
log::trace!("ESC dispatch: intermediates={intermediates:?}, byte={byte:#04x}");
|
||||||
// SS3 sequences
|
// SS3 sequences
|
||||||
if intermediates == [b'O'] {
|
if byte == b'O' {
|
||||||
let key = match byte {
|
self.ss3_pending = true;
|
||||||
b'P' => KeyCode::F(1),
|
return;
|
||||||
b'Q' => KeyCode::F(2),
|
|
||||||
b'R' => KeyCode::F(3),
|
|
||||||
b'S' => KeyCode::F(4),
|
|
||||||
b'A' => KeyCode::Up,
|
|
||||||
b'B' => KeyCode::Down,
|
|
||||||
b'C' => KeyCode::Right,
|
|
||||||
b'D' => KeyCode::Left,
|
|
||||||
b'H' => KeyCode::Home,
|
|
||||||
b'F' => KeyCode::End,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
self.push(KeyEvent(key, ModKeys::empty()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,194 +35,249 @@ macro_rules! vi_test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_simple_command() {
|
fn annotate_simple_command() {
|
||||||
assert_annotated("echo hello",
|
assert_annotated("echo hello", "\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
||||||
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_pipeline() {
|
fn annotate_pipeline() {
|
||||||
assert_annotated("ls | grep foo",
|
assert_annotated(
|
||||||
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}");
|
"ls | grep foo",
|
||||||
|
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_conjunction() {
|
fn annotate_conjunction() {
|
||||||
assert_annotated("echo foo && echo bar",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}");
|
"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]
|
#[test]
|
||||||
fn annotate_redirect_output() {
|
fn annotate_redirect_output() {
|
||||||
assert_annotated("echo hello > file.txt",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}");
|
"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]
|
#[test]
|
||||||
fn annotate_redirect_append() {
|
fn annotate_redirect_append() {
|
||||||
assert_annotated("echo hello >> file.txt",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}");
|
"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]
|
#[test]
|
||||||
fn annotate_redirect_input() {
|
fn annotate_redirect_input() {
|
||||||
assert_annotated("cat < file.txt",
|
assert_annotated(
|
||||||
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}");
|
"cat < file.txt",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_fd_redirect() {
|
fn annotate_fd_redirect() {
|
||||||
assert_annotated("cmd 2>&1",
|
assert_annotated("cmd 2>&1", "\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
|
||||||
"\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_sub() {
|
fn annotate_variable_sub() {
|
||||||
assert_annotated("echo $HOME",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}");
|
"echo $HOME",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_brace_sub() {
|
fn annotate_variable_brace_sub() {
|
||||||
assert_annotated("echo ${HOME}",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}");
|
"echo ${HOME}",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_command_sub() {
|
fn annotate_command_sub() {
|
||||||
assert_annotated("echo $(ls)",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}");
|
"echo $(ls)",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_single_quoted_string() {
|
fn annotate_single_quoted_string() {
|
||||||
assert_annotated("echo 'hello world'",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}");
|
"echo 'hello world'",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_double_quoted_string() {
|
fn annotate_double_quoted_string() {
|
||||||
assert_annotated("echo \"hello world\"",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}");
|
"echo \"hello world\"",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_assignment() {
|
fn annotate_assignment() {
|
||||||
assert_annotated("FOO=bar",
|
assert_annotated("FOO=bar", "\u{e107}FOO=bar\u{e11a}");
|
||||||
"\u{e107}FOO=bar\u{e11a}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_assignment_with_command() {
|
fn annotate_assignment_with_command() {
|
||||||
assert_annotated("FOO=bar echo hello",
|
assert_annotated(
|
||||||
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
"FOO=bar echo hello",
|
||||||
|
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_if_statement() {
|
fn annotate_if_statement() {
|
||||||
assert_annotated("if true; then echo yes; fi",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_for_loop() {
|
fn annotate_for_loop() {
|
||||||
assert_annotated("for i in a b c; do echo $i; done",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_while_loop() {
|
fn annotate_while_loop() {
|
||||||
assert_annotated("while true; do echo hello; done",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_case_statement() {
|
fn annotate_case_statement() {
|
||||||
assert_annotated("case foo in bar) echo bar;; esac",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_brace_group() {
|
fn annotate_brace_group() {
|
||||||
assert_annotated("{ echo hello; }",
|
assert_annotated(
|
||||||
"\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}");
|
"{ 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]
|
#[test]
|
||||||
fn annotate_comment() {
|
fn annotate_comment() {
|
||||||
assert_annotated("echo hello # this is a comment",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}");
|
"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]
|
#[test]
|
||||||
fn annotate_semicolon_sep() {
|
fn annotate_semicolon_sep() {
|
||||||
assert_annotated("echo foo; echo bar",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}");
|
"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]
|
#[test]
|
||||||
fn annotate_escaped_char() {
|
fn annotate_escaped_char() {
|
||||||
assert_annotated("echo hello\\ world",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}");
|
"echo hello\\ world",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_glob() {
|
fn annotate_glob() {
|
||||||
assert_annotated("ls *.txt",
|
assert_annotated(
|
||||||
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}");
|
"ls *.txt",
|
||||||
|
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_heredoc_operator() {
|
fn annotate_heredoc_operator() {
|
||||||
assert_annotated("cat <<EOF",
|
assert_annotated(
|
||||||
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}");
|
"cat <<EOF",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_herestring_operator() {
|
fn annotate_herestring_operator() {
|
||||||
assert_annotated("cat <<< hello",
|
assert_annotated(
|
||||||
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}");
|
"cat <<< hello",
|
||||||
|
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_nested_command_sub() {
|
fn annotate_nested_command_sub() {
|
||||||
assert_annotated("echo $(echo $(ls))",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}");
|
"echo $(echo $(ls))",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_var_in_double_quotes() {
|
fn annotate_var_in_double_quotes() {
|
||||||
assert_annotated("echo \"hello $USER\"",
|
assert_annotated(
|
||||||
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}");
|
"echo \"hello $USER\"",
|
||||||
|
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_func_def() {
|
fn annotate_func_def() {
|
||||||
assert_annotated("foo() { echo hello; }",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_negate() {
|
fn annotate_negate() {
|
||||||
assert_annotated("! echo hello",
|
assert_annotated(
|
||||||
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
|
"! echo hello",
|
||||||
|
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_or_conjunction() {
|
fn annotate_or_conjunction() {
|
||||||
assert_annotated("false || echo fallback",
|
assert_annotated(
|
||||||
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}");
|
"false || echo fallback",
|
||||||
|
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_complex_pipeline() {
|
fn annotate_complex_pipeline() {
|
||||||
assert_annotated("cat file.txt | grep pattern | wc -l",
|
assert_annotated(
|
||||||
"\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}");
|
"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]
|
#[test]
|
||||||
fn annotate_multiple_redirects() {
|
fn annotate_multiple_redirects() {
|
||||||
assert_annotated("cmd > out.txt 2> err.txt",
|
assert_annotated(
|
||||||
"\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}");
|
"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 =====================
|
// ===================== Vi Tests =====================
|
||||||
@@ -437,27 +492,27 @@ vi_test! {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vi_auto_indent() {
|
fn vi_auto_indent() {
|
||||||
let (mut vi, _g) = test_vi("");
|
let (mut vi, _g) = test_vi("");
|
||||||
|
|
||||||
// Type each line and press Enter separately so auto-indent triggers
|
// Type each line and press Enter separately so auto-indent triggers
|
||||||
let lines = [
|
let lines = [
|
||||||
"func() {",
|
"func() {",
|
||||||
"case foo in",
|
"case foo in",
|
||||||
"bar)",
|
"bar)",
|
||||||
"while true; do",
|
"while true; do",
|
||||||
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}"
|
"echo foo \\\rbar \\\rbiz \\\rbazz\rbreak\rdone\r;;\resac\r}",
|
||||||
];
|
];
|
||||||
|
|
||||||
for (i,line) in lines.iter().enumerate() {
|
for (i, line) in lines.iter().enumerate() {
|
||||||
vi.feed_bytes(line.as_bytes());
|
vi.feed_bytes(line.as_bytes());
|
||||||
if i != lines.len() - 1 {
|
if i != lines.len() - 1 {
|
||||||
vi.feed_bytes(b"\r");
|
vi.feed_bytes(b"\r");
|
||||||
}
|
}
|
||||||
vi.process_input().unwrap();
|
vi.process_input().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vi.editor.as_str(),
|
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}"
|
"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}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,7 +315,8 @@ impl ShOptCore {
|
|||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
"noclobber" => {
|
"noclobber" => {
|
||||||
let mut output = String::from("Prevent > from overwriting existing files (use >| to override)\n");
|
let mut output =
|
||||||
|
String::from("Prevent > from overwriting existing files (use >| to override)\n");
|
||||||
output.push_str(&format!("{}", self.noclobber));
|
output.push_str(&format!("{}", self.noclobber));
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/state.rs
28
src/state.rs
@@ -1330,14 +1330,14 @@ 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 => {
|
ShellParam::AllArgsStr => {
|
||||||
let ifs = get_separator();
|
let ifs = get_separator();
|
||||||
self
|
self
|
||||||
.params
|
.params
|
||||||
.get(&ShellParam::AllArgs)
|
.get(&ShellParam::AllArgs)
|
||||||
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
|
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => self
|
_ => self
|
||||||
.params
|
.params
|
||||||
@@ -1852,12 +1852,12 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_separator() -> String {
|
pub fn get_separator() -> String {
|
||||||
env::var("IFS")
|
env::var("IFS")
|
||||||
.unwrap_or(String::from(" "))
|
.unwrap_or(String::from(" "))
|
||||||
.chars()
|
.chars()
|
||||||
.next()
|
.next()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status() -> i32 {
|
pub fn get_status() -> i32 {
|
||||||
|
|||||||
Reference in New Issue
Block a user