Added -j flag to 'complete' for completing job names/pids
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_logic, write_logic},
|
state::{self, read_logic, write_logic},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,88 +1,105 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use nix::{libc::STDOUT_FILENO, unistd::write};
|
use nix::{libc::STDOUT_FILENO, unistd::write};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, readline::complete::{BashCompSpec, CompContext, CompSpec}, state::{self, read_meta, write_meta}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
readline::complete::{BashCompSpec, CompContext, CompSpec},
|
||||||
|
state::{self, read_meta, write_meta},
|
||||||
|
};
|
||||||
|
|
||||||
pub const COMPGEN_OPTS: [OptSpec;8] = [
|
pub const COMPGEN_OPTS: [OptSpec; 9] = [
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('F'),
|
opt: Opt::Short('F'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('W'),
|
opt: Opt::Short('W'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('j'),
|
||||||
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('f'),
|
opt: Opt::Short('f'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('d'),
|
opt: Opt::Short('d'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('c'),
|
opt: Opt::Short('c'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('u'),
|
opt: Opt::Short('u'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('v'),
|
opt: Opt::Short('v'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('o'),
|
opt: Opt::Short('o'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const COMP_OPTS: [OptSpec;11] = [
|
pub const COMP_OPTS: [OptSpec; 12] = [
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('F'),
|
opt: Opt::Short('F'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('W'),
|
opt: Opt::Short('W'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('A'),
|
opt: Opt::Short('A'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('j'),
|
||||||
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('p'),
|
opt: Opt::Short('p'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('r'),
|
opt: Opt::Short('r'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('f'),
|
opt: Opt::Short('f'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('d'),
|
opt: Opt::Short('d'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('c'),
|
opt: Opt::Short('c'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('u'),
|
opt: Opt::Short('u'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('v'),
|
opt: Opt::Short('v'),
|
||||||
takes_arg: false
|
takes_arg: false,
|
||||||
},
|
},
|
||||||
OptSpec {
|
OptSpec {
|
||||||
opt: Opt::Short('o'),
|
opt: Opt::Short('o'),
|
||||||
takes_arg: true
|
takes_arg: true,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -93,8 +110,9 @@ bitflags! {
|
|||||||
const CMDS = 0b0000000100;
|
const CMDS = 0b0000000100;
|
||||||
const USERS = 0b0000001000;
|
const USERS = 0b0000001000;
|
||||||
const VARS = 0b0000010000;
|
const VARS = 0b0000010000;
|
||||||
const PRINT = 0b0000100000;
|
const JOBS = 0b0000100000;
|
||||||
const REMOVE = 0b0001000000;
|
const PRINT = 0b0001000000;
|
||||||
|
const REMOVE = 0b0010000000;
|
||||||
}
|
}
|
||||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct CompOptFlags: u32 {
|
pub struct CompOptFlags: u32 {
|
||||||
@@ -123,7 +141,8 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
assert!(!argv.is_empty());
|
assert!(!argv.is_empty());
|
||||||
let src = argv.clone()
|
let src = argv
|
||||||
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
|
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
|
||||||
.collect::<ShResult<Vec<String>>>()?
|
.collect::<ShResult<Vec<String>>>()?
|
||||||
@@ -143,7 +162,7 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
read_meta(|m| {
|
read_meta(|m| {
|
||||||
for (cmd,_) in &argv {
|
for (cmd, _) in &argv {
|
||||||
if let Some(spec) = m.comp_specs().get(cmd) {
|
if let Some(spec) = m.comp_specs().get(cmd) {
|
||||||
println!("{}", spec.source());
|
println!("{}", spec.source());
|
||||||
}
|
}
|
||||||
@@ -157,7 +176,7 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
|
|||||||
|
|
||||||
if comp_opts.flags.contains(CompFlags::REMOVE) {
|
if comp_opts.flags.contains(CompFlags::REMOVE) {
|
||||||
write_meta(|m| {
|
write_meta(|m| {
|
||||||
for (cmd,_) in &argv {
|
for (cmd, _) in &argv {
|
||||||
m.remove_comp_spec(cmd);
|
m.remove_comp_spec(cmd);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -168,13 +187,16 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
|
|||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
return Err(ShErr::full(ShErrKind::ExecFail, "complete: no command specified", blame));
|
return Err(ShErr::full(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
"complete: no command specified",
|
||||||
|
blame,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let comp_spec = BashCompSpec::from_comp_opts(comp_opts)
|
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
|
||||||
.with_source(src);
|
|
||||||
|
|
||||||
for (cmd,_) in argv {
|
for (cmd, _) in argv {
|
||||||
write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone())));
|
write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone())));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,30 +214,25 @@ pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) ->
|
|||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
assert!(!argv.is_empty());
|
assert!(!argv.is_empty());
|
||||||
let src = argv.clone()
|
let src = argv
|
||||||
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
|
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
|
||||||
.collect::<ShResult<Vec<String>>>()?
|
.collect::<ShResult<Vec<String>>>()?
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
|
let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
|
||||||
let prefix = argv
|
let prefix = argv.clone().into_iter().nth(1).unwrap_or_default();
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.nth(1)
|
|
||||||
.unwrap_or_default();
|
|
||||||
let comp_opts = get_comp_opts(opts)?;
|
let comp_opts = get_comp_opts(opts)?;
|
||||||
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
|
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
|
||||||
let comp_spec = BashCompSpec::from_comp_opts(comp_opts)
|
|
||||||
.with_source(src);
|
|
||||||
|
|
||||||
let dummy_ctx = CompContext {
|
let dummy_ctx = CompContext {
|
||||||
words: vec![prefix.clone()],
|
words: vec![prefix.clone()],
|
||||||
cword: 0,
|
cword: 0,
|
||||||
line: prefix.to_string(),
|
line: prefix.to_string(),
|
||||||
cursor_pos: prefix.as_str().len()
|
cursor_pos: prefix.as_str().len(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let results = comp_spec.complete(&dummy_ctx)?;
|
let results = comp_spec.complete(&dummy_ctx)?;
|
||||||
@@ -235,17 +252,16 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
|
|||||||
|
|
||||||
for opt in opts {
|
for opt in opts {
|
||||||
match opt {
|
match opt {
|
||||||
Opt::ShortWithArg('F',func) => {
|
Opt::ShortWithArg('F', func) => {
|
||||||
comp_opts.func = Some(func);
|
comp_opts.func = Some(func);
|
||||||
},
|
}
|
||||||
Opt::ShortWithArg('W',wordlist) => {
|
Opt::ShortWithArg('W', wordlist) => {
|
||||||
comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect());
|
comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect());
|
||||||
},
|
}
|
||||||
Opt::ShortWithArg('A',action) => {
|
Opt::ShortWithArg('A', action) => {
|
||||||
comp_opts.action = Some(action);
|
comp_opts.action = Some(action);
|
||||||
}
|
}
|
||||||
Opt::ShortWithArg('o', opt_flag) => {
|
Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() {
|
||||||
match opt_flag.as_str() {
|
|
||||||
"default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT,
|
"default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT,
|
||||||
"dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES,
|
"dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES,
|
||||||
"nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE,
|
"nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE,
|
||||||
@@ -253,20 +269,20 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
|
|||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::InvalidOpt,
|
ShErrKind::InvalidOpt,
|
||||||
format!("complete: invalid option: {}", opt_flag),
|
format!("complete: invalid option: {}", opt_flag),
|
||||||
Default::default()
|
Default::default(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
|
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
|
||||||
|
Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS,
|
||||||
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,
|
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,
|
||||||
Opt::Short('f') => comp_opts.flags |= CompFlags::FILES,
|
Opt::Short('f') => comp_opts.flags |= CompFlags::FILES,
|
||||||
Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS,
|
Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS,
|
||||||
Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS,
|
Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS,
|
||||||
Opt::Short('u') => comp_opts.flags |= CompFlags::USERS,
|
Opt::Short('u') => comp_opts.flags |= CompFlags::USERS,
|
||||||
Opt::Short('v') => comp_opts.flags |= CompFlags::VARS,
|
Opt::Short('v') => comp_opts.flags |= CompFlags::VARS,
|
||||||
_ => unreachable!()
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ use std::{env, path::PathBuf};
|
|||||||
|
|
||||||
use nix::{libc::STDOUT_FILENO, unistd::write};
|
use nix::{libc::STDOUT_FILENO, unistd::write};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::Span}, procio::{IoStack, borrow_fd}, state::{self, read_meta, write_meta}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node, lex::Span},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
state::{self, read_meta, write_meta},
|
||||||
|
};
|
||||||
|
|
||||||
enum StackIdx {
|
enum StackIdx {
|
||||||
FromTop(usize),
|
FromTop(usize),
|
||||||
@@ -11,12 +18,10 @@ enum StackIdx {
|
|||||||
|
|
||||||
fn print_dirs() -> ShResult<()> {
|
fn print_dirs() -> ShResult<()> {
|
||||||
let current_dir = env::current_dir()?;
|
let current_dir = env::current_dir()?;
|
||||||
let dirs_iter = read_meta(|m| {
|
let dirs_iter = read_meta(|m| m.dirs().clone().into_iter());
|
||||||
m.dirs()
|
let all_dirs = [current_dir]
|
||||||
.clone()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
});
|
.chain(dirs_iter)
|
||||||
let all_dirs = [current_dir].into_iter().chain(dirs_iter)
|
|
||||||
.map(|d| d.to_string_lossy().to_string())
|
.map(|d| d.to_string_lossy().to_string())
|
||||||
.map(|d| {
|
.map(|d| {
|
||||||
let Ok(home) = env::var("HOME") else {
|
let Ok(home) = env::var("HOME") else {
|
||||||
@@ -29,7 +34,8 @@ fn print_dirs() -> ShResult<()> {
|
|||||||
} else {
|
} else {
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
let stdout = borrow_fd(STDOUT_FILENO);
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
@@ -78,7 +84,10 @@ fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult<StackIdx> {
|
|||||||
if digits.is_empty() {
|
if digits.is_empty() {
|
||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("{cmd}: missing index after '{}'", if from_top { "+" } else { "-" }),
|
format!(
|
||||||
|
"{cmd}: missing index after '{}'",
|
||||||
|
if from_top { "+" } else { "-" }
|
||||||
|
),
|
||||||
blame,
|
blame,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -112,8 +121,11 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
let blame = node.get_span().clone();
|
let blame = node.get_span().clone();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv
|
argv,
|
||||||
} = node.class else { unreachable!() };
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
@@ -122,7 +134,9 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
let mut no_cd = false;
|
let mut no_cd = false;
|
||||||
|
|
||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) {
|
if arg.starts_with('+')
|
||||||
|
|| (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit())
|
||||||
|
{
|
||||||
rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?);
|
rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?);
|
||||||
} else if arg == "-n" {
|
} else if arg == "-n" {
|
||||||
no_cd = true;
|
no_cd = true;
|
||||||
@@ -165,7 +179,8 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(dir) = new_cwd
|
if let Some(dir) = new_cwd
|
||||||
&& !no_cd {
|
&& !no_cd
|
||||||
|
{
|
||||||
change_directory(&dir, blame)?;
|
change_directory(&dir, blame)?;
|
||||||
print_dirs()?;
|
print_dirs()?;
|
||||||
}
|
}
|
||||||
@@ -192,8 +207,11 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let blame = node.get_span().clone();
|
let blame = node.get_span().clone();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv
|
argv,
|
||||||
} = node.class else { unreachable!() };
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
@@ -201,7 +219,9 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let mut no_cd = false;
|
let mut no_cd = false;
|
||||||
|
|
||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) {
|
if arg.starts_with('+')
|
||||||
|
|| (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit())
|
||||||
|
{
|
||||||
remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?);
|
remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?);
|
||||||
} else if arg == "-n" {
|
} else if arg == "-n" {
|
||||||
no_cd = true;
|
no_cd = true;
|
||||||
@@ -290,8 +310,11 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let blame = node.get_span().clone();
|
let blame = node.get_span().clone();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv
|
argv,
|
||||||
} = node.class else { unreachable!() };
|
} = node.class
|
||||||
|
else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
@@ -301,13 +324,16 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let mut clear_stack = false;
|
let mut clear_stack = false;
|
||||||
let mut target_idx: Option<StackIdx> = None;
|
let mut target_idx: Option<StackIdx> = None;
|
||||||
|
|
||||||
for (arg,_) in argv {
|
for (arg, _) in argv {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"-p" => one_per_line = true,
|
"-p" => one_per_line = true,
|
||||||
"-v" => one_per_line_indexed = true,
|
"-v" => one_per_line_indexed = true,
|
||||||
"-c" => clear_stack = true,
|
"-c" => clear_stack = true,
|
||||||
"-l" => abbreviate_home = false,
|
"-l" => abbreviate_home = false,
|
||||||
_ if (arg.starts_with('+') || arg.starts_with('-')) && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit() => {
|
_ if (arg.starts_with('+') || arg.starts_with('-'))
|
||||||
|
&& arg.len() > 1
|
||||||
|
&& arg.as_bytes()[1].is_ascii_digit() =>
|
||||||
|
{
|
||||||
target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?);
|
target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?);
|
||||||
}
|
}
|
||||||
_ if arg.starts_with('-') => {
|
_ if arg.starts_with('-') => {
|
||||||
@@ -329,13 +355,13 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
|
|
||||||
if clear_stack {
|
if clear_stack {
|
||||||
write_meta(|m| m.dirs_mut().clear());
|
write_meta(|m| m.dirs_mut().clear());
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let mut dirs: Vec<String> = read_meta(|m| {
|
let mut dirs: Vec<String> = read_meta(|m| {
|
||||||
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
|
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
|
||||||
let stack = [current_dir].into_iter()
|
let stack = [current_dir]
|
||||||
|
.into_iter()
|
||||||
.chain(m.dirs().clone())
|
.chain(m.dirs().clone())
|
||||||
.map(|d| d.to_string_lossy().to_string());
|
.map(|d| d.to_string_lossy().to_string());
|
||||||
|
|
||||||
@@ -343,14 +369,16 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
let Ok(home) = env::var("HOME") else {
|
let Ok(home) = env::var("HOME") else {
|
||||||
return stack.collect();
|
return stack.collect();
|
||||||
};
|
};
|
||||||
stack.map(|d| {
|
stack
|
||||||
|
.map(|d| {
|
||||||
if d.starts_with(&home) {
|
if d.starts_with(&home) {
|
||||||
let new = d.strip_prefix(&home).unwrap();
|
let new = d.strip_prefix(&home).unwrap();
|
||||||
format!("~{new}")
|
format!("~{new}")
|
||||||
} else {
|
} else {
|
||||||
d
|
d
|
||||||
}
|
}
|
||||||
}).collect()
|
})
|
||||||
|
.collect()
|
||||||
} else {
|
} else {
|
||||||
stack.collect()
|
stack.collect()
|
||||||
}
|
}
|
||||||
@@ -367,10 +395,13 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
} else {
|
} else {
|
||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("dirs: directory index out of range: {}", match idx {
|
format!(
|
||||||
|
"dirs: directory index out of range: {}",
|
||||||
|
match idx {
|
||||||
StackIdx::FromTop(n) => format!("+{n}"),
|
StackIdx::FromTop(n) => format!("+{n}"),
|
||||||
StackIdx::FromBottom(n) => format!("-{n}"),
|
StackIdx::FromBottom(n) => format!("-{n}"),
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
blame.clone(),
|
blame.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
builtin::setup_builtin,
|
builtin::setup_builtin,
|
||||||
expand::expand_prompt,
|
expand::expand_prompt,
|
||||||
getopt::{get_opts_from_tokens, Opt, OptSpec},
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,7 +59,6 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
borrow_fd(STDOUT_FILENO)
|
borrow_fd(STDOUT_FILENO)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
let mut echo_output = prepare_echo_args(
|
let mut echo_output = prepare_echo_args(
|
||||||
argv
|
argv
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -197,7 +196,6 @@ pub fn prepare_echo_args(
|
|||||||
prepared_args.push(prepared_arg);
|
prepared_args.push(prepared_arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(prepared_args)
|
Ok(prepared_args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{
|
|||||||
builtin::setup_builtin,
|
builtin::setup_builtin,
|
||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{execute::exec_input, NdRule, Node},
|
parse::{NdRule, Node, execute::exec_input},
|
||||||
procio::IoStack,
|
procio::IoStack,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
builtin::setup_builtin,
|
builtin::setup_builtin,
|
||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{execute::ExecArgs, NdRule, Node},
|
parse::{NdRule, Node, execute::ExecArgs},
|
||||||
procio::IoStack,
|
procio::IoStack,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{execute::prepare_argv, NdRule, Node},
|
parse::{NdRule, Node, execute::prepare_argv},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
||||||
@@ -31,7 +31,7 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
|||||||
code = status;
|
code = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (kind,message) = match kind {
|
let (kind, message) = match kind {
|
||||||
LoopContinue(_) => (LoopContinue(code), "'continue' found outside of loop"),
|
LoopContinue(_) => (LoopContinue(code), "'continue' found outside of loop"),
|
||||||
LoopBreak(_) => (LoopBreak(code), "'break' found outside of loop"),
|
LoopBreak(_) => (LoopBreak(code), "'break' found outside of loop"),
|
||||||
FuncReturn(_) => (FuncReturn(code), "'return' found outside of function"),
|
FuncReturn(_) => (FuncReturn(code), "'return' found outside of function"),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
jobs::{JobBldr, JobCmdFlags, JobID},
|
jobs::{JobBldr, JobCmdFlags, JobID},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{lex::Span, NdRule, Node},
|
parse::{NdRule, Node, lex::Span},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_jobs, write_jobs},
|
state::{self, read_jobs, write_jobs},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ use crate::{
|
|||||||
execute::prepare_argv,
|
execute::prepare_argv,
|
||||||
lex::{Span, Tk},
|
lex::{Span, Tk},
|
||||||
},
|
},
|
||||||
procio::{IoStack, RedirGuard}, state,
|
procio::{IoStack, RedirGuard},
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod alias;
|
pub mod alias;
|
||||||
pub mod cd;
|
pub mod cd;
|
||||||
|
pub mod complete;
|
||||||
|
pub mod dirstack;
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
pub mod varcmds;
|
pub mod eval;
|
||||||
|
pub mod exec;
|
||||||
pub mod flowctl;
|
pub mod flowctl;
|
||||||
pub mod jobctl;
|
pub mod jobctl;
|
||||||
pub mod pwd;
|
pub mod pwd;
|
||||||
@@ -24,16 +28,14 @@ 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 zoltraak;
|
pub mod zoltraak;
|
||||||
pub mod dirstack;
|
|
||||||
pub mod exec;
|
|
||||||
pub mod eval;
|
|
||||||
pub mod complete;
|
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 35] = [
|
pub const BUILTINS: [&str; 35] = [
|
||||||
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias",
|
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
|
||||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
|
||||||
"pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen"
|
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
|
||||||
|
"unset", "complete", "compgen",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sets up a builtin command
|
/// Sets up a builtin command
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ use nix::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::setup_builtin,
|
builtin::setup_builtin,
|
||||||
getopt::{get_opts_from_tokens, Opt, OptSpec},
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
readline::term::RawModeGuard,
|
readline::term::RawModeGuard,
|
||||||
state::{self, read_vars, write_vars, VarFlags, VarKind},
|
state::{self, VarFlags, VarKind, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const READ_OPTS: [OptSpec; 7] = [
|
pub const READ_OPTS: [OptSpec; 7] = [
|
||||||
@@ -81,7 +81,10 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
|
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("read_builtin: starting read with delim={}", read_opts.delim as char);
|
log::info!(
|
||||||
|
"read_builtin: starting read with delim={}",
|
||||||
|
read_opts.delim as char
|
||||||
|
);
|
||||||
|
|
||||||
let input = if isatty(STDIN_FILENO)? {
|
let input = if isatty(STDIN_FILENO)? {
|
||||||
// Restore default terminal settings
|
// Restore default terminal settings
|
||||||
@@ -182,9 +185,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
|||||||
};
|
};
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
write_vars(|v| {
|
write_vars(|v| v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE))?;
|
||||||
v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE)
|
|
||||||
})?;
|
|
||||||
} else {
|
} else {
|
||||||
// get our field separator
|
// get our field separator
|
||||||
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
|
|
||||||
write(output_channel, output.as_bytes())?;
|
write(output_channel, output.as_bytes())?;
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (arg, span) in argv {
|
for (arg, span) in argv {
|
||||||
@@ -42,7 +42,6 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
write(output_channel, output.as_bytes())?;
|
write(output_channel, output.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
|
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_logic, write_logic},
|
state::{self, read_logic, write_logic},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -59,12 +59,10 @@ impl FromStr for TrapTarget {
|
|||||||
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
|
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
|
||||||
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
|
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
|
||||||
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
|
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
|
||||||
_ => {
|
_ => Err(ShErr::simple(
|
||||||
Err(ShErr::simple(
|
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("invalid trap target '{}'", s),
|
format!("invalid trap target '{}'", s),
|
||||||
))
|
)),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,16 +67,16 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
|
|||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"unset: Expected at least one argument",
|
"unset: Expected at least one argument",
|
||||||
blame
|
blame,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (arg,span) in argv {
|
for (arg, span) in argv {
|
||||||
if !read_vars(|v| v.var_exists(&arg)) {
|
if !read_vars(|v| v.var_exists(&arg)) {
|
||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("unset: No such variable '{arg}'"),
|
format!("unset: No such variable '{arg}'"),
|
||||||
span
|
span,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
write_vars(|v| v.unset_var(&arg))?;
|
write_vars(|v| v.unset_var(&arg))?;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
getopt::{get_opts_from_tokens, Opt, OptSpec},
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
jobs::JobBldr,
|
jobs::JobBldr,
|
||||||
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
|
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::setup_builtin;
|
use super::setup_builtin;
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ use crate::parse::{Redir, RedirType};
|
|||||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||||
use crate::readline::markers;
|
use crate::readline::markers;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars
|
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta,
|
||||||
|
write_vars,
|
||||||
};
|
};
|
||||||
use crate::{jobs, prelude::*};
|
use crate::{jobs, prelude::*};
|
||||||
|
|
||||||
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
|
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
|
||||||
|
|
||||||
|
|
||||||
impl Tk {
|
impl Tk {
|
||||||
/// Create a new expanded token
|
/// Create a new expanded token
|
||||||
pub fn expand(self) -> ShResult<Self> {
|
pub fn expand(self) -> ShResult<Self> {
|
||||||
@@ -565,10 +565,9 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs)
|
read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs)
|
||||||
},
|
|
||||||
_ => read_vars(|v| v.index_var(&var_name, idx))?
|
|
||||||
}
|
}
|
||||||
|
_ => read_vars(|v| v.index_var(&var_name, idx))?,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
perform_param_expansion(&var_name)?
|
perform_param_expansion(&var_name)?
|
||||||
};
|
};
|
||||||
@@ -583,19 +582,32 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
|||||||
chars.next(); // consume the bracket
|
chars.next(); // consume the bracket
|
||||||
if bracket_depth == 0 {
|
if bracket_depth == 0 {
|
||||||
let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?;
|
let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?;
|
||||||
idx = Some(expanded_idx.parse::<ArrIndex>().map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("Array index must be a number, got '{expanded_idx}'")))?);
|
idx = Some(expanded_idx.parse::<ArrIndex>().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
|
ShErrKind::ParseErr,
|
||||||
|
format!("Array index must be a number, got '{expanded_idx}'"),
|
||||||
|
)
|
||||||
|
})?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ch if bracket_depth > 0 => {
|
ch if bracket_depth > 0 => {
|
||||||
chars.next(); // safe to consume
|
chars.next(); // safe to consume
|
||||||
if ch == '{' { idx_brace_depth += 1; }
|
if ch == '{' {
|
||||||
if ch == '}' { idx_brace_depth -= 1; }
|
idx_brace_depth += 1;
|
||||||
|
}
|
||||||
|
if ch == '}' {
|
||||||
|
idx_brace_depth -= 1;
|
||||||
|
}
|
||||||
idx_raw.push(ch);
|
idx_raw.push(ch);
|
||||||
}
|
}
|
||||||
ch if brace_depth > 0 => {
|
ch if brace_depth > 0 => {
|
||||||
chars.next(); // safe to consume
|
chars.next(); // safe to consume
|
||||||
if ch == '{' { inner_brace_depth += 1; }
|
if ch == '{' {
|
||||||
if ch == '}' { inner_brace_depth -= 1; }
|
inner_brace_depth += 1;
|
||||||
|
}
|
||||||
|
if ch == '}' {
|
||||||
|
inner_brace_depth -= 1;
|
||||||
|
}
|
||||||
var_name.push(ch);
|
var_name.push(ch);
|
||||||
}
|
}
|
||||||
ch if var_name.is_empty() && PARAMETERS.contains(&ch) => {
|
ch if var_name.is_empty() && PARAMETERS.contains(&ch) => {
|
||||||
@@ -1411,12 +1423,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
None => expand_raw(&mut default.chars().peekable()),
|
None => expand_raw(&mut default.chars().peekable()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::DefaultUnset(default) => {
|
ParamExp::DefaultUnset(default) => match vars.try_get_var(&var_name) {
|
||||||
match vars.try_get_var(&var_name) {
|
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
None => expand_raw(&mut default.chars().peekable()),
|
None => expand_raw(&mut default.chars().peekable()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
ParamExp::SetDefaultUnsetOrNull(default) => {
|
ParamExp::SetDefaultUnsetOrNull(default) => {
|
||||||
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
@@ -1427,28 +1437,22 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::SetDefaultUnset(default) => {
|
ParamExp::SetDefaultUnset(default) => match vars.try_get_var(&var_name) {
|
||||||
match vars.try_get_var(&var_name) {
|
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
None => {
|
None => {
|
||||||
let expanded = expand_raw(&mut default.chars().peekable())?;
|
let expanded = expand_raw(&mut default.chars().peekable())?;
|
||||||
write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?;
|
write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?;
|
||||||
Ok(expanded)
|
Ok(expanded)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
ParamExp::AltSetNotNull(alt) => match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
||||||
ParamExp::AltSetNotNull(alt) => {
|
|
||||||
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
|
||||||
Some(_) => expand_raw(&mut alt.chars().peekable()),
|
Some(_) => expand_raw(&mut alt.chars().peekable()),
|
||||||
None => Ok("".into()),
|
None => Ok("".into()),
|
||||||
}
|
},
|
||||||
}
|
ParamExp::AltNotNull(alt) => match vars.try_get_var(&var_name) {
|
||||||
ParamExp::AltNotNull(alt) => {
|
|
||||||
match vars.try_get_var(&var_name) {
|
|
||||||
Some(_) => expand_raw(&mut alt.chars().peekable()),
|
Some(_) => expand_raw(&mut alt.chars().peekable()),
|
||||||
None => Ok("".into()),
|
None => Ok("".into()),
|
||||||
}
|
},
|
||||||
}
|
|
||||||
ParamExp::ErrUnsetOrNull(err) => {
|
ParamExp::ErrUnsetOrNull(err) => {
|
||||||
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
@@ -1462,8 +1466,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ParamExp::ErrUnset(err) => {
|
ParamExp::ErrUnset(err) => match vars.try_get_var(&var_name) {
|
||||||
match vars.try_get_var(&var_name) {
|
|
||||||
Some(val) => Ok(val),
|
Some(val) => Ok(val),
|
||||||
None => {
|
None => {
|
||||||
let expanded = expand_raw(&mut err.chars().peekable())?;
|
let expanded = expand_raw(&mut err.chars().peekable())?;
|
||||||
@@ -1473,8 +1476,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
|||||||
notes: vec![],
|
notes: vec![],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
ParamExp::Substr(pos) => {
|
ParamExp::Substr(pos) => {
|
||||||
let value = vars.get_var(&var_name);
|
let value = vars.get_var(&var_name);
|
||||||
if let Some(substr) = value.get(pos..) {
|
if let Some(substr) = value.get(pos..) {
|
||||||
@@ -2058,7 +2060,18 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
|
|||||||
PromptTk::SuccessSymbol => todo!(),
|
PromptTk::SuccessSymbol => todo!(),
|
||||||
PromptTk::FailureSymbol => todo!(),
|
PromptTk::FailureSymbol => todo!(),
|
||||||
PromptTk::JobCount => {
|
PromptTk::JobCount => {
|
||||||
let count = read_jobs(|j| j.jobs().iter().filter(|j| j.as_ref().is_some_and(|j| j.get_stats().iter().all(|st| matches!(st, WtStat::StillAlive)))).count());
|
let count = read_jobs(|j| {
|
||||||
|
j.jobs()
|
||||||
|
.iter()
|
||||||
|
.filter(|j| {
|
||||||
|
j.as_ref().is_some_and(|j| {
|
||||||
|
j.get_stats()
|
||||||
|
.iter()
|
||||||
|
.all(|st| matches!(st, WtStat::StillAlive))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
});
|
||||||
result.push_str(&count.to_string());
|
result.push_str(&count.to_string());
|
||||||
}
|
}
|
||||||
PromptTk::Function(f) => {
|
PromptTk::Function(f) => {
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
|
|||||||
(non_opts, opts)
|
(non_opts, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
pub fn get_opts_from_tokens(
|
||||||
|
tokens: Vec<Tk>,
|
||||||
|
opt_specs: &[OptSpec],
|
||||||
|
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
|
||||||
let mut tokens_iter = tokens
|
let mut tokens_iter = tokens
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|t| t.expand())
|
.map(|t| t.expand())
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::{
|
|||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
},
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoMode},
|
procio::{IoMode, borrow_fd},
|
||||||
signal::{disable_reaping, enable_reaping},
|
signal::{disable_reaping, enable_reaping},
|
||||||
state::{self, read_jobs, set_status, write_jobs},
|
state::{self, read_jobs, set_status, write_jobs},
|
||||||
};
|
};
|
||||||
@@ -632,6 +632,9 @@ impl Job {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
pub fn name(&self) -> Option<&str> {
|
||||||
|
self.children().first().and_then(|child| child.cmd())
|
||||||
|
}
|
||||||
pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String {
|
pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String {
|
||||||
let long = flags.contains(JobCmdFlags::LONG);
|
let long = flags.contains(JobCmdFlags::LONG);
|
||||||
let init = flags.contains(JobCmdFlags::INIT);
|
let init = flags.contains(JobCmdFlags::INIT);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
getopt::Opt, libsh::term::{Style, Styled}, parse::lex::Span, prelude::*
|
getopt::Opt,
|
||||||
|
libsh::term::{Style, Styled},
|
||||||
|
parse::lex::Span,
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type ShResult<T> = Result<T, ShErr>;
|
pub type ShResult<T> = Result<T, ShErr>;
|
||||||
|
|||||||
@@ -91,12 +91,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
|||||||
let mut cur_split = vec![];
|
let mut cur_split = vec![];
|
||||||
for tk in self {
|
for tk in self {
|
||||||
match tk.class {
|
match tk.class {
|
||||||
TkRule::Pipe |
|
TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg | TkRule::Sep => {
|
||||||
TkRule::ErrPipe |
|
|
||||||
TkRule::And |
|
|
||||||
TkRule::Or |
|
|
||||||
TkRule::Bg |
|
|
||||||
TkRule::Sep => {
|
|
||||||
splits.push(std::mem::take(&mut cur_split));
|
splits.push(std::mem::take(&mut cur_split));
|
||||||
}
|
}
|
||||||
_ => cur_split.push(tk.clone()),
|
_ => cur_split.push(tk.clone()),
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -54,7 +54,7 @@ struct ShedArgs {
|
|||||||
#[arg(short)]
|
#[arg(short)]
|
||||||
interactive: bool,
|
interactive: bool,
|
||||||
|
|
||||||
#[arg(long,short)]
|
#[arg(long, short)]
|
||||||
login_shell: bool,
|
login_shell: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,12 @@ fn main() -> ExitCode {
|
|||||||
args.login_shell = true;
|
args.login_shell = true;
|
||||||
}
|
}
|
||||||
if args.version {
|
if args.version {
|
||||||
println!("shed {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS);
|
println!(
|
||||||
|
"shed {} ({} {})",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
std::env::consts::ARCH,
|
||||||
|
std::env::consts::OS
|
||||||
|
);
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +118,8 @@ fn main() -> ExitCode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
||||||
&& let Err(e) = exec_input(trap, None, false) {
|
&& let Err(e) = exec_input(trap, None, false)
|
||||||
|
{
|
||||||
eprintln!("shed: error running EXIT trap: {e}");
|
eprintln!("shed: error running EXIT trap: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +234,10 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if stdin has data
|
// Check if stdin has data
|
||||||
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
|
if fds[0]
|
||||||
|
.revents()
|
||||||
|
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||||
|
{
|
||||||
let mut buffer = [0u8; 1024];
|
let mut buffer = [0u8; 1024];
|
||||||
match read(*TTY_FILENO, &mut buffer) {
|
match read(*TTY_FILENO, &mut buffer) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
@@ -287,7 +296,7 @@ fn shed_interactive() -> ShResult<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => eprintln!("{e}"),
|
_ => eprintln!("{e}"),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt};
|
use std::{
|
||||||
|
collections::{HashSet, VecDeque},
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{
|
builtin::{
|
||||||
alias::{alias, unalias}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, true_builtin, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak
|
alias::{alias, unalias},
|
||||||
|
cd::cd,
|
||||||
|
complete::{compgen_builtin, complete_builtin},
|
||||||
|
dirstack::{dirs, popd, pushd},
|
||||||
|
echo::echo,
|
||||||
|
eval, exec,
|
||||||
|
flowctl::flowctl,
|
||||||
|
jobctl::{JobBehavior, continue_job, disown, jobs},
|
||||||
|
pwd::pwd,
|
||||||
|
read::read_builtin,
|
||||||
|
shift::shift,
|
||||||
|
shopt::shopt,
|
||||||
|
source::source,
|
||||||
|
test::double_bracket_test,
|
||||||
|
trap::{TrapTarget, trap},
|
||||||
|
true_builtin,
|
||||||
|
varcmds::{export, local, readonly, unset},
|
||||||
|
zoltraak::zoltraak,
|
||||||
},
|
},
|
||||||
expand::{expand_aliases, glob_to_regex},
|
expand::{expand_aliases, glob_to_regex},
|
||||||
jobs::{ChildProc, JobStack, dispatch_job},
|
jobs::{ChildProc, JobStack, dispatch_job},
|
||||||
libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils},
|
libsh::{
|
||||||
|
error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
|
utils::RedirVecUtils,
|
||||||
|
},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{IoMode, IoStack},
|
procio::{IoMode, IoStack},
|
||||||
state::{self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars},
|
state::{
|
||||||
|
self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -36,7 +61,9 @@ pub fn is_in_path(name: &str) -> bool {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
let Ok(path) = env::var("PATH") else { return false };
|
let Ok(path) = env::var("PATH") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
let paths = path.split(':');
|
let paths = path.split(':');
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let full_path = Path::new(path).join(name);
|
let full_path = Path::new(path).join(name);
|
||||||
@@ -226,7 +253,8 @@ impl Dispatcher {
|
|||||||
self.exec_subsh(node)
|
self.exec_subsh(node)
|
||||||
} else if read_shopts(|s| s.core.autocd)
|
} else if read_shopts(|s| s.core.autocd)
|
||||||
&& Path::new(cmd.span.as_str()).is_dir()
|
&& Path::new(cmd.span.as_str()).is_dir()
|
||||||
&& !is_in_path(cmd.span.as_str()) {
|
&& !is_in_path(cmd.span.as_str())
|
||||||
|
{
|
||||||
let dir = cmd.span.as_str().to_string();
|
let dir = cmd.span.as_str().to_string();
|
||||||
let stack = IoStack {
|
let stack = IoStack {
|
||||||
stack: self.io_stack.clone(),
|
stack: self.io_stack.clone(),
|
||||||
@@ -395,7 +423,6 @@ impl Dispatcher {
|
|||||||
self.io_stack.append_to_frame(brc_grp.redirs);
|
self.io_stack.append_to_frame(brc_grp.redirs);
|
||||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||||
let brc_grp_logic = |s: &mut Self| -> ShResult<()> {
|
let brc_grp_logic = |s: &mut Self| -> ShResult<()> {
|
||||||
|
|
||||||
for node in body {
|
for node in body {
|
||||||
let blame = node.get_span();
|
let blame = node.get_span();
|
||||||
s.dispatch_node(node).try_blame(blame)?;
|
s.dispatch_node(node).try_blame(blame)?;
|
||||||
@@ -569,7 +596,13 @@ impl Dispatcher {
|
|||||||
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
||||||
|
|
||||||
for (var, val) in chunk_iter {
|
for (var, val) in chunk_iter {
|
||||||
write_vars(|v| v.set_var(&var.to_string(), VarKind::Str(val.to_string()), VarFlags::NONE))?;
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
&var.to_string(),
|
||||||
|
VarKind::Str(val.to_string()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
for_guard.vars.insert(var.to_string());
|
for_guard.vars.insert(var.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +733,10 @@ impl Dispatcher {
|
|||||||
}
|
}
|
||||||
fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> {
|
fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> {
|
||||||
let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS);
|
let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS);
|
||||||
let cmd_raw = cmd.get_command().unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class)).to_string();
|
let cmd_raw = cmd
|
||||||
|
.get_command()
|
||||||
|
.unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class))
|
||||||
|
.to_string();
|
||||||
|
|
||||||
if fork_builtins {
|
if fork_builtins {
|
||||||
log::trace!("Forking builtin: {}", cmd_raw);
|
log::trace!("Forking builtin: {}", cmd_raw);
|
||||||
@@ -785,15 +821,12 @@ impl Dispatcher {
|
|||||||
"true" | ":" => {
|
"true" | ":" => {
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
"false" => {
|
"false" => {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
}
|
||||||
_ => unimplemented!(
|
_ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw),
|
||||||
"Have not yet added support for builtin '{}'",
|
|
||||||
cmd_raw
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
|
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
|
||||||
@@ -917,9 +950,7 @@ impl Dispatcher {
|
|||||||
|
|
||||||
// Parse and expand array index BEFORE entering write_vars borrow
|
// Parse and expand array index BEFORE entering write_vars borrow
|
||||||
let indexed = state::parse_arr_bracket(var)
|
let indexed = state::parse_arr_bracket(var)
|
||||||
.map(|(name, idx_raw)| {
|
.map(|(name, idx_raw)| state::expand_arr_index(&idx_raw).map(|idx| (name, idx)))
|
||||||
state::expand_arr_index(&idx_raw).map(|idx| (name, idx))
|
|
||||||
})
|
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
|
|||||||
@@ -738,7 +738,8 @@ impl Iterator for LexStream {
|
|||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
"Unclosed brace group",
|
"Unclosed brace group",
|
||||||
Span::new(start..self.cursor, self.source.clone()),
|
Span::new(start..self.cursor, self.source.clone()),
|
||||||
)).into();
|
))
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
|
let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
|
||||||
self.flags |= LexFlags::STALE;
|
self.flags |= LexFlags::STALE;
|
||||||
@@ -776,7 +777,8 @@ impl Iterator for LexStream {
|
|||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
"Unclosed brace group",
|
"Unclosed brace group",
|
||||||
Span::new(start..self.cursor, self.source.clone()),
|
Span::new(start..self.cursor, self.source.clone()),
|
||||||
)).into();
|
))
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -899,11 +901,12 @@ pub fn is_field_sep(ch: char) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_keyword(slice: &str) -> bool {
|
pub fn is_keyword(slice: &str) -> bool {
|
||||||
KEYWORDS.contains(&slice) || (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()"))
|
KEYWORDS.contains(&slice)
|
||||||
|
|| (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_cmd_sub(slice: &str) -> bool {
|
pub fn is_cmd_sub(slice: &str) -> bool {
|
||||||
slice.starts_with("$(") && ends_with_unescaped(slice,")")
|
slice.starts_with("$(") && ends_with_unescaped(slice, ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
|
pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
|
||||||
|
|||||||
@@ -117,7 +117,8 @@ impl Node {
|
|||||||
if let NdRule::Command {
|
if let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv,
|
argv,
|
||||||
} = &self.class {
|
} = &self.class
|
||||||
|
{
|
||||||
argv.iter().next()
|
argv.iter().next()
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -1473,7 +1474,9 @@ impl ParseStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(assign_kind) = assign_kind && !var_name.is_empty() {
|
if let Some(assign_kind) = assign_kind
|
||||||
|
&& !var_name.is_empty()
|
||||||
|
{
|
||||||
let var = Tk::new(TkRule::Str, Span::new(name_range, token.source()));
|
let var = Tk::new(TkRule::Str, Span::new(name_range, token.source()));
|
||||||
let val = Tk::new(TkRule::Str, Span::new(val_range, token.source()));
|
let val = Tk::new(TkRule::Str, Span::new(val_range, token.source()));
|
||||||
let flags = if var_val.starts_with('(') && var_val.ends_with(')') {
|
let flags = if var_val.starts_with('(') && var_val.ends_with(')') {
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
|||||||
pub use bitflags::bitflags;
|
pub use bitflags::bitflags;
|
||||||
pub use nix::{
|
pub use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
fcntl::{open, OFlag},
|
fcntl::{OFlag, open},
|
||||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||||
sys::{
|
sys::{
|
||||||
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
|
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||||
stat::Mode,
|
stat::Mode,
|
||||||
termios::{self},
|
termios::{self},
|
||||||
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
|
wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
|
||||||
},
|
},
|
||||||
unistd::{
|
unistd::{
|
||||||
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
|
ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
|
||||||
tcsetpgrp, write, ForkResult, Pid,
|
setpgid, tcgetpgrp, tcsetpgrp, write,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::{
|
|||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::RedirVecUtils,
|
utils::RedirVecUtils,
|
||||||
},
|
},
|
||||||
parse::{get_redir_file, Redir, RedirType},
|
parse::{Redir, RedirType, get_redir_file},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,50 @@
|
|||||||
use std::{collections::HashSet, env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
use std::{
|
||||||
|
collections::HashSet, env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{BUILTINS, complete::{CompFlags, CompOptFlags, CompOpts}},
|
builtin::{
|
||||||
libsh::{error::{ShErr, ShErrKind, ShResult}, utils::TkVecUtils},
|
BUILTINS,
|
||||||
parse::{execute::{VarCtxGuard, exec_input}, lex::{self, LexFlags, Tk, TkFlags, TkRule, ends_with_unescaped}},
|
complete::{CompFlags, CompOptFlags, CompOpts},
|
||||||
|
},
|
||||||
|
libsh::{
|
||||||
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
|
utils::TkVecUtils,
|
||||||
|
},
|
||||||
|
parse::{
|
||||||
|
execute::{VarCtxGuard, exec_input},
|
||||||
|
lex::{self, LexFlags, Tk, TkFlags, TkRule, ends_with_unescaped},
|
||||||
|
},
|
||||||
readline::{
|
readline::{
|
||||||
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
||||||
markers::{self, is_marker},
|
markers::{self, is_marker},
|
||||||
},
|
},
|
||||||
state::{VarFlags, VarKind, read_logic, read_meta, read_vars, write_vars},
|
state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub fn complete_jobs(start: &str) -> Vec<String> {
|
||||||
|
if let Some(prefix) = start.strip_prefix('%') {
|
||||||
|
read_jobs(|j| {
|
||||||
|
j.jobs()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|j| j.as_ref())
|
||||||
|
.filter_map(|j| j.name())
|
||||||
|
.filter(|name| name.starts_with(prefix))
|
||||||
|
.map(|name| format!("%{name}"))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
read_jobs(|j| {
|
||||||
|
j.jobs()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|j| j.as_ref())
|
||||||
|
.map(|j| j.pgid().to_string())
|
||||||
|
.filter(|pgid| pgid.starts_with(start))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn complete_users(start: &str) -> Vec<String> {
|
pub fn complete_users(start: &str) -> Vec<String> {
|
||||||
let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else {
|
let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else {
|
||||||
return vec![];
|
return vec![];
|
||||||
@@ -25,10 +59,10 @@ pub fn complete_users(start: &str) -> Vec<String> {
|
|||||||
|
|
||||||
pub fn complete_vars(start: &str) -> Vec<String> {
|
pub fn complete_vars(start: &str) -> Vec<String> {
|
||||||
let Some((var_name, name_start, _end)) = extract_var_name(start) else {
|
let Some((var_name, name_start, _end)) = extract_var_name(start) else {
|
||||||
return vec![]
|
return vec![];
|
||||||
};
|
};
|
||||||
if !read_vars(|v| v.get_var(&var_name)).is_empty() {
|
if !read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||||
return vec![]
|
return vec![];
|
||||||
}
|
}
|
||||||
// if we are here, we have a variable substitution that isn't complete
|
// if we are here, we have a variable substitution that isn't complete
|
||||||
// so let's try to complete it
|
// so let's try to complete it
|
||||||
@@ -39,7 +73,6 @@ pub fn complete_vars(start: &str) -> Vec<String> {
|
|||||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||||
.map(|k| format!("{prefix}{k}"))
|
.map(|k| format!("{prefix}{k}"))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +139,10 @@ fn complete_commands(start: &str) -> Vec<String> {
|
|||||||
|
|
||||||
fn complete_dirs(start: &str) -> Vec<String> {
|
fn complete_dirs(start: &str) -> Vec<String> {
|
||||||
let filenames = complete_filename(start);
|
let filenames = complete_filename(start);
|
||||||
filenames.into_iter().filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false)).collect()
|
filenames
|
||||||
|
.into_iter()
|
||||||
|
.filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn complete_filename(start: &str) -> Vec<String> {
|
fn complete_filename(start: &str) -> Vec<String> {
|
||||||
@@ -169,11 +205,12 @@ fn complete_filename(start: &str) -> Vec<String> {
|
|||||||
|
|
||||||
pub enum CompSpecResult {
|
pub enum CompSpecResult {
|
||||||
NoSpec, // No compspec registered
|
NoSpec, // No compspec registered
|
||||||
NoMatch { flags: CompOptFlags }, // Compspec found but no candidates matched, returns behavior flags
|
NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns
|
||||||
Match(CompResult) // Compspec found and candidates returned
|
* behavior flags */
|
||||||
|
Match(CompResult), // Compspec found and candidates returned
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug,Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct BashCompSpec {
|
pub struct BashCompSpec {
|
||||||
/// -F: The name of a function to generate the possible completions.
|
/// -F: The name of a function to generate the possible completions.
|
||||||
pub function: Option<String>,
|
pub function: Option<String>,
|
||||||
@@ -187,14 +224,16 @@ pub struct BashCompSpec {
|
|||||||
pub commands: bool,
|
pub commands: bool,
|
||||||
/// -u: complete user names
|
/// -u: complete user names
|
||||||
pub users: bool,
|
pub users: bool,
|
||||||
/// -v complete variable names
|
/// -v: complete variable names
|
||||||
pub vars: bool,
|
pub vars: bool,
|
||||||
/// -A signal: complete signal names
|
/// -A signal: complete signal names
|
||||||
pub signals: bool,
|
pub signals: bool,
|
||||||
|
/// -j: complete job pids or names
|
||||||
|
pub jobs: bool,
|
||||||
|
|
||||||
pub flags: CompOptFlags,
|
pub flags: CompOptFlags,
|
||||||
/// The original command
|
/// The original command
|
||||||
pub source: String
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BashCompSpec {
|
impl BashCompSpec {
|
||||||
@@ -237,8 +276,18 @@ impl BashCompSpec {
|
|||||||
self.signals = enable;
|
self.signals = enable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn jobs(mut self, enable: bool) -> Self {
|
||||||
|
self.jobs = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
pub fn from_comp_opts(opts: CompOpts) -> Self {
|
pub fn from_comp_opts(opts: CompOpts) -> Self {
|
||||||
let CompOpts { func, wordlist, action: _, flags, opt_flags } = opts;
|
let CompOpts {
|
||||||
|
func,
|
||||||
|
wordlist,
|
||||||
|
action: _,
|
||||||
|
flags,
|
||||||
|
opt_flags,
|
||||||
|
} = opts;
|
||||||
Self {
|
Self {
|
||||||
function: func,
|
function: func,
|
||||||
wordlist,
|
wordlist,
|
||||||
@@ -247,42 +296,73 @@ impl BashCompSpec {
|
|||||||
commands: flags.contains(CompFlags::CMDS),
|
commands: flags.contains(CompFlags::CMDS),
|
||||||
users: flags.contains(CompFlags::USERS),
|
users: flags.contains(CompFlags::USERS),
|
||||||
vars: flags.contains(CompFlags::VARS),
|
vars: flags.contains(CompFlags::VARS),
|
||||||
|
jobs: flags.contains(CompFlags::JOBS),
|
||||||
flags: opt_flags,
|
flags: opt_flags,
|
||||||
signals: false, // TODO: implement signal completion
|
signals: false, // TODO: implement signal completion
|
||||||
source: String::new()
|
source: String::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult<Vec<String>> {
|
pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult<Vec<String>> {
|
||||||
let mut vars_to_unset = HashSet::new();
|
let mut vars_to_unset = HashSet::new();
|
||||||
for var in [ "COMP_WORDS", "COMP_CWORD", "COMP_LINE", "COMP_POINT", "COMPREPLY" ] {
|
for var in [
|
||||||
|
"COMP_WORDS",
|
||||||
|
"COMP_CWORD",
|
||||||
|
"COMP_LINE",
|
||||||
|
"COMP_POINT",
|
||||||
|
"COMPREPLY",
|
||||||
|
] {
|
||||||
vars_to_unset.insert(var.to_string());
|
vars_to_unset.insert(var.to_string());
|
||||||
}
|
}
|
||||||
let _guard = VarCtxGuard::new(vars_to_unset);
|
let _guard = VarCtxGuard::new(vars_to_unset);
|
||||||
|
|
||||||
let CompContext { words, cword, line, cursor_pos } = ctx;
|
let CompContext {
|
||||||
|
words,
|
||||||
|
cword,
|
||||||
|
line,
|
||||||
|
cursor_pos,
|
||||||
|
} = ctx;
|
||||||
|
|
||||||
let raw_words = words.to_vec().into_iter().map(|tk| tk.to_string()).collect();
|
let raw_words = words.iter().clone().map(|tk| tk.to_string()).collect();
|
||||||
write_vars(|v| v.set_var("COMP_WORDS", VarKind::arr_from_vec(raw_words), VarFlags::NONE))?;
|
write_vars(|v| {
|
||||||
write_vars(|v| v.set_var("COMP_CWORD", VarKind::Str(cword.to_string()), VarFlags::NONE))?;
|
v.set_var(
|
||||||
|
"COMP_WORDS",
|
||||||
|
VarKind::arr_from_vec(raw_words),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"COMP_CWORD",
|
||||||
|
VarKind::Str(cword.to_string()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
write_vars(|v| v.set_var("COMP_LINE", VarKind::Str(line.to_string()), VarFlags::NONE))?;
|
write_vars(|v| v.set_var("COMP_LINE", VarKind::Str(line.to_string()), VarFlags::NONE))?;
|
||||||
write_vars(|v| v.set_var("COMP_POINT", VarKind::Str(cursor_pos.to_string()), VarFlags::NONE))?;
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"COMP_POINT",
|
||||||
|
VarKind::Str(cursor_pos.to_string()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let cmd_name = words
|
let cmd_name = words.first().map(|s| s.to_string()).unwrap_or_default();
|
||||||
.first()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let cword_str = words.get(*cword)
|
let cword_str = words.get(*cword).map(|s| s.to_string()).unwrap_or_default();
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let pword_str = if *cword > 0 {
|
let pword_str = if *cword > 0 {
|
||||||
words.get(cword - 1).map(|s| s.to_string()).unwrap_or_default()
|
words
|
||||||
|
.get(cword - 1)
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
let input = format!("{} {cmd_name} {cword_str} {pword_str}", self.function.as_ref().unwrap());
|
let input = format!(
|
||||||
|
"{} {cmd_name} {cword_str} {pword_str}",
|
||||||
|
self.function.as_ref().unwrap()
|
||||||
|
);
|
||||||
exec_input(input, None, false)?;
|
exec_input(input, None, false)?;
|
||||||
|
|
||||||
Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default())
|
Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default())
|
||||||
@@ -310,13 +390,11 @@ impl CompSpec for BashCompSpec {
|
|||||||
if self.users {
|
if self.users {
|
||||||
candidates.extend(complete_users(&expanded));
|
candidates.extend(complete_users(&expanded));
|
||||||
}
|
}
|
||||||
|
if self.jobs {
|
||||||
|
candidates.extend(complete_jobs(&expanded));
|
||||||
|
}
|
||||||
if let Some(words) = &self.wordlist {
|
if let Some(words) = &self.wordlist {
|
||||||
candidates.extend(
|
candidates.extend(words.iter().filter(|w| w.starts_with(&expanded)).cloned());
|
||||||
words
|
|
||||||
.iter()
|
|
||||||
.filter(|w| w.starts_with(&expanded))
|
|
||||||
.cloned(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if self.function.is_some() {
|
if self.function.is_some() {
|
||||||
candidates.extend(self.exec_comp_func(ctx)?);
|
candidates.extend(self.exec_comp_func(ctx)?);
|
||||||
@@ -362,7 +440,7 @@ pub struct CompContext {
|
|||||||
pub words: Vec<Tk>,
|
pub words: Vec<Tk>,
|
||||||
pub cword: usize,
|
pub cword: usize,
|
||||||
pub line: String,
|
pub line: String,
|
||||||
pub cursor_pos: usize
|
pub cursor_pos: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompContext {
|
impl CompContext {
|
||||||
@@ -391,7 +469,7 @@ impl CompResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default,Debug,Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct Completer {
|
pub struct Completer {
|
||||||
pub candidates: Vec<String>,
|
pub candidates: Vec<String>,
|
||||||
pub selected_idx: usize,
|
pub selected_idx: usize,
|
||||||
@@ -399,7 +477,7 @@ pub struct Completer {
|
|||||||
pub token_span: (usize, usize),
|
pub token_span: (usize, usize),
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
pub dirs_only: bool,
|
pub dirs_only: bool,
|
||||||
pub no_space: bool
|
pub no_space: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer {
|
impl Completer {
|
||||||
@@ -506,7 +584,9 @@ impl Completer {
|
|||||||
.map(|c| {
|
.map(|c| {
|
||||||
if !ends_with_unescaped(&c, "/") // directory
|
if !ends_with_unescaped(&c, "/") // directory
|
||||||
&& !ends_with_unescaped(&c, "=") // '='-type arg
|
&& !ends_with_unescaped(&c, "=") // '='-type arg
|
||||||
&& !ends_with_unescaped(&c, " ") { // already has a space
|
&& !ends_with_unescaped(&c, " ")
|
||||||
|
{
|
||||||
|
// already has a space
|
||||||
format!("{} ", c)
|
format!("{} ", c)
|
||||||
} else {
|
} else {
|
||||||
c
|
c
|
||||||
@@ -541,7 +621,6 @@ impl Completer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_completed_line(&self) -> String {
|
pub fn get_completed_line(&self) -> String {
|
||||||
if self.candidates.is_empty() {
|
if self.candidates.is_empty() {
|
||||||
return self.original_input.clone();
|
return self.original_input.clone();
|
||||||
@@ -578,18 +657,25 @@ impl Completer {
|
|||||||
|
|
||||||
let relevant_pos = segments
|
let relevant_pos = segments
|
||||||
.iter()
|
.iter()
|
||||||
.position(|tks| tks.iter().next().is_some_and(|tk| tk.span.start > cursor_pos))
|
.position(|tks| {
|
||||||
|
tks
|
||||||
|
.iter()
|
||||||
|
.next()
|
||||||
|
.is_some_and(|tk| tk.span.start > cursor_pos)
|
||||||
|
})
|
||||||
.map(|i| i.saturating_sub(1))
|
.map(|i| i.saturating_sub(1))
|
||||||
.unwrap_or(segments.len().saturating_sub(1));
|
.unwrap_or(segments.len().saturating_sub(1));
|
||||||
|
|
||||||
let mut relevant = segments[relevant_pos].to_vec();
|
let mut relevant = segments[relevant_pos].to_vec();
|
||||||
|
|
||||||
let cword = if let Some(pos) = relevant.iter().position(|tk| {
|
let cword = if let Some(pos) = relevant
|
||||||
cursor_pos >= tk.span.start && cursor_pos <= tk.span.end
|
.iter()
|
||||||
}) {
|
.position(|tk| cursor_pos >= tk.span.start && cursor_pos <= tk.span.end)
|
||||||
|
{
|
||||||
pos
|
pos
|
||||||
} else {
|
} else {
|
||||||
let insert_pos = relevant.iter()
|
let insert_pos = relevant
|
||||||
|
.iter()
|
||||||
.position(|tk| tk.span.start > cursor_pos)
|
.position(|tk| tk.span.start > cursor_pos)
|
||||||
.unwrap_or(relevant.len());
|
.unwrap_or(relevant.len());
|
||||||
|
|
||||||
@@ -620,9 +706,13 @@ impl Completer {
|
|||||||
|
|
||||||
let candidates = spec.complete(ctx)?;
|
let candidates = spec.complete(ctx)?;
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
Ok(CompSpecResult::NoMatch { flags: spec.get_flags() })
|
Ok(CompSpecResult::NoMatch {
|
||||||
|
flags: spec.get_flags(),
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(CompSpecResult::Match(CompResult::from_candidates(candidates)))
|
Ok(CompSpecResult::Match(CompResult::from_candidates(
|
||||||
|
candidates,
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +762,8 @@ impl Completer {
|
|||||||
|
|
||||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||||
|
|
||||||
// Use marker-based context detection for sub-token awareness (e.g. VAR_SUB inside a token)
|
// Use marker-based context detection for sub-token awareness (e.g. VAR_SUB
|
||||||
|
// inside a token)
|
||||||
let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||||
self.token_span.0 = token_start;
|
self.token_span.0 = token_start;
|
||||||
cur_token
|
cur_token
|
||||||
@@ -709,9 +800,11 @@ impl Completer {
|
|||||||
_ => complete_filename(&expanded),
|
_ => complete_filename(&expanded),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Graft unexpanded prefix onto candidates to preserve things like $SOME_PATH/file.txt
|
// Graft unexpanded prefix onto candidates to preserve things like
|
||||||
// Skip for var completions — complete_vars already returns the full $VAR form
|
// $SOME_PATH/file.txt Skip for var completions — complete_vars already
|
||||||
let is_var_completion = last_marker == Some(markers::VAR_SUB) && !candidates.is_empty()
|
// returns the full $VAR form
|
||||||
|
let is_var_completion = last_marker == Some(markers::VAR_SUB)
|
||||||
|
&& !candidates.is_empty()
|
||||||
&& candidates.iter().any(|c| c.starts_with('$'));
|
&& candidates.iter().any(|c| c.starts_with('$'));
|
||||||
if !is_var_completion {
|
if !is_var_completion {
|
||||||
candidates = candidates
|
candidates = candidates
|
||||||
@@ -728,5 +821,4 @@ impl Completer {
|
|||||||
|
|
||||||
Ok(CompResult::from_candidates(candidates))
|
Ok(CompResult::from_candidates(candidates))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ use std::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::term::{Style, StyleSet, Styled},
|
libsh::term::{Style, StyleSet, Styled},
|
||||||
readline::{annotate_input, markers::{self, is_marker}},
|
readline::{
|
||||||
|
annotate_input,
|
||||||
|
markers::{self, is_marker},
|
||||||
|
},
|
||||||
state::{read_logic, read_meta, read_shopts},
|
state::{read_logic, read_meta, read_shopts},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ pub struct Highlighter {
|
|||||||
linebuf_cursor_pos: usize,
|
linebuf_cursor_pos: usize,
|
||||||
style_stack: Vec<StyleSet>,
|
style_stack: Vec<StyleSet>,
|
||||||
last_was_reset: bool,
|
last_was_reset: bool,
|
||||||
in_selection: bool
|
in_selection: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
@@ -35,7 +38,7 @@ impl Highlighter {
|
|||||||
linebuf_cursor_pos: 0,
|
linebuf_cursor_pos: 0,
|
||||||
style_stack: Vec::new(),
|
style_stack: Vec::new(),
|
||||||
last_was_reset: true, // start as true so we don't emit a leading reset
|
last_was_reset: true, // start as true so we don't emit a leading reset
|
||||||
in_selection: false
|
in_selection: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +117,6 @@ impl Highlighter {
|
|||||||
|
|
||||||
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||||
|
|
||||||
|
|
||||||
markers::ASSIGNMENT => {
|
markers::ASSIGNMENT => {
|
||||||
let mut var_name = String::new();
|
let mut var_name = String::new();
|
||||||
|
|
||||||
@@ -140,7 +142,9 @@ impl Highlighter {
|
|||||||
|
|
||||||
markers::ARG => {
|
markers::ARG => {
|
||||||
let mut arg = String::new();
|
let mut arg = String::new();
|
||||||
let is_last_arg = !input_chars.clone().any(|c| c == markers::ARG || c.is_whitespace());
|
let is_last_arg = !input_chars
|
||||||
|
.clone()
|
||||||
|
.any(|c| c == markers::ARG || c.is_whitespace());
|
||||||
|
|
||||||
if !is_last_arg {
|
if !is_last_arg {
|
||||||
self.push_style(Style::White);
|
self.push_style(Style::White);
|
||||||
@@ -173,7 +177,10 @@ impl Highlighter {
|
|||||||
}
|
}
|
||||||
cmd_name.push(ch);
|
cmd_name.push(ch);
|
||||||
}
|
}
|
||||||
let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") {
|
let style = if matches!(
|
||||||
|
Self::strip_markers(&cmd_name).as_str(),
|
||||||
|
"break" | "continue" | "return"
|
||||||
|
) {
|
||||||
Style::Magenta.into()
|
Style::Magenta.into()
|
||||||
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
|
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
|
||||||
Style::Green.into()
|
Style::Green.into()
|
||||||
@@ -318,7 +325,8 @@ impl Highlighter {
|
|||||||
|
|
||||||
if path.is_absolute()
|
if path.is_absolute()
|
||||||
&& let Some(parent_dir) = path.parent()
|
&& let Some(parent_dir) = path.parent()
|
||||||
&& let Ok(entries) = parent_dir.read_dir() {
|
&& let Ok(entries) = parent_dir.read_dir()
|
||||||
|
{
|
||||||
let files = entries
|
let files = entries
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ use crate::{
|
|||||||
libsh::{
|
libsh::{
|
||||||
error::ShResult,
|
error::ShResult,
|
||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts
|
},
|
||||||
|
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
|
||||||
|
prelude::*,
|
||||||
|
readline::{
|
||||||
|
markers,
|
||||||
|
register::{RegisterContent, write_register},
|
||||||
|
},
|
||||||
|
state::read_shopts,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
|
||||||
@@ -621,16 +628,17 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
pub fn select_range(&self) -> Option<(usize, usize)> {
|
pub fn select_range(&self) -> Option<(usize, usize)> {
|
||||||
match self.select_mode? {
|
match self.select_mode? {
|
||||||
SelectMode::Char(_) => {
|
SelectMode::Char(_) => self.select_range,
|
||||||
self.select_range
|
|
||||||
}
|
|
||||||
SelectMode::Line(_) => {
|
SelectMode::Line(_) => {
|
||||||
let (start, end) = self.select_range?;
|
let (start, end) = self.select_range?;
|
||||||
let start = self.pos_line_number(start);
|
let start = self.pos_line_number(start);
|
||||||
let end = self.pos_line_number(end);
|
let end = self.pos_line_number(end);
|
||||||
let (select_start,_) = self.line_bounds(start);
|
let (select_start, _) = self.line_bounds(start);
|
||||||
let (_,select_end) = self.line_bounds(end);
|
let (_, select_end) = self.line_bounds(end);
|
||||||
if self.read_grapheme_before(select_end).is_some_and(|gr| gr == "\n") {
|
if self
|
||||||
|
.read_grapheme_before(select_end)
|
||||||
|
.is_some_and(|gr| gr == "\n")
|
||||||
|
{
|
||||||
Some((select_start, select_end - 1))
|
Some((select_start, select_end - 1))
|
||||||
} else {
|
} else {
|
||||||
Some((select_start, select_end))
|
Some((select_start, select_end))
|
||||||
@@ -657,7 +665,8 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn pos_line_number(&self, pos: usize) -> usize {
|
pub fn pos_line_number(&self, pos: usize) -> usize {
|
||||||
self.read_slice_to(pos)
|
self
|
||||||
|
.read_slice_to(pos)
|
||||||
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
|
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
@@ -1935,7 +1944,8 @@ impl LineBuf {
|
|||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.unwrap_or(self.buffer.clone());
|
.unwrap_or(self.buffer.clone());
|
||||||
let input = Arc::new(to_cursor);
|
let input = Arc::new(to_cursor);
|
||||||
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else {
|
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
|
||||||
|
else {
|
||||||
log::error!("Failed to lex buffer for indent calculation");
|
log::error!("Failed to lex buffer for indent calculation");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1994,7 +2004,8 @@ impl LineBuf {
|
|||||||
if self.cursor.exclusive
|
if self.cursor.exclusive
|
||||||
&& line.ends_with("\n")
|
&& line.ends_with("\n")
|
||||||
&& self.grapheme_at(target_pos) == Some("\n")
|
&& self.grapheme_at(target_pos) == Some("\n")
|
||||||
&& line != "\n" // Allow landing on newline for empty lines
|
&& line != "\n"
|
||||||
|
// Allow landing on newline for empty lines
|
||||||
{
|
{
|
||||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||||
// newline
|
// newline
|
||||||
@@ -2235,7 +2246,8 @@ impl LineBuf {
|
|||||||
if self.cursor.exclusive
|
if self.cursor.exclusive
|
||||||
&& line.ends_with("\n")
|
&& line.ends_with("\n")
|
||||||
&& self.grapheme_at(target_pos) == Some("\n")
|
&& self.grapheme_at(target_pos) == Some("\n")
|
||||||
&& line != "\n" // Allow landing on newline for empty lines
|
&& line != "\n"
|
||||||
|
// Allow landing on newline for empty lines
|
||||||
{
|
{
|
||||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||||
// newline
|
// newline
|
||||||
@@ -2247,7 +2259,6 @@ impl LineBuf {
|
|||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
||||||
}
|
}
|
||||||
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
|
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
|
||||||
@@ -2484,10 +2495,13 @@ impl LineBuf {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
log::debug!("Initial range from motion: ({start}, {end})");
|
log::debug!("Initial range from motion: ({start}, {end})");
|
||||||
log::debug!("self.grapheme_indices().len(): {}", self.grapheme_indices().len());
|
log::debug!(
|
||||||
|
"self.grapheme_indices().len(): {}",
|
||||||
|
self.grapheme_indices().len()
|
||||||
|
);
|
||||||
|
|
||||||
let mut do_indent = false;
|
let mut do_indent = false;
|
||||||
if verb == Verb::Change && (start,end) == self.this_line_exclusive() {
|
if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
|
||||||
do_indent = read_shopts(|o| o.prompt.auto_indent);
|
do_indent = read_shopts(|o| o.prompt.auto_indent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2508,8 +2522,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
let is_linewise = matches!(
|
let is_linewise = matches!(
|
||||||
motion,
|
motion,
|
||||||
MotionKind::InclusiveWithTargetCol(..) |
|
MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
|
||||||
MotionKind::ExclusiveWithTargetCol(..)
|
|
||||||
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
|
) || matches!(self.select_mode, Some(SelectMode::Line(_)));
|
||||||
let register_content = if is_linewise {
|
let register_content = if is_linewise {
|
||||||
if !text.ends_with('\n') && !text.is_empty() {
|
if !text.ends_with('\n') && !text.is_empty() {
|
||||||
@@ -2529,7 +2542,8 @@ impl LineBuf {
|
|||||||
self.cursor.add(1);
|
self.cursor.add(1);
|
||||||
}
|
}
|
||||||
} else if verb != Verb::Change
|
} else if verb != Verb::Change
|
||||||
&& let MotionKind::InclusiveWithTargetCol((_,_), col) = motion {
|
&& let MotionKind::InclusiveWithTargetCol((_, _), col) = motion
|
||||||
|
{
|
||||||
self.cursor.add(col);
|
self.cursor.add(col);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2726,7 +2740,9 @@ impl LineBuf {
|
|||||||
Anchor::After => self.end_of_line(),
|
Anchor::After => self.end_of_line(),
|
||||||
Anchor::Before => self.start_of_line(),
|
Anchor::Before => self.start_of_line(),
|
||||||
};
|
};
|
||||||
let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n");
|
let needs_newline = self
|
||||||
|
.grapheme_before(insert_idx)
|
||||||
|
.is_some_and(|gr| gr != "\n");
|
||||||
if needs_newline {
|
if needs_newline {
|
||||||
let full = format!("\n{}", text);
|
let full = format!("\n{}", text);
|
||||||
self.insert_str_at(insert_idx, &full);
|
self.insert_str_at(insert_idx, &full);
|
||||||
@@ -2951,7 +2967,11 @@ impl LineBuf {
|
|||||||
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
|
||||||
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
|
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
|
||||||
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
|
||||||
let is_line_motion = cmd.is_line_motion() || cmd.verb.as_ref().is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
|
let is_line_motion = cmd.is_line_motion()
|
||||||
|
|| cmd
|
||||||
|
.verb
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
|
||||||
let is_undo_op = cmd.is_undo_op();
|
let is_undo_op = cmd.is_undo_op();
|
||||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||||
|
|
||||||
@@ -3026,7 +3046,8 @@ impl LineBuf {
|
|||||||
|
|
||||||
if self.cursor.exclusive
|
if self.cursor.exclusive
|
||||||
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
|
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
|
||||||
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") {
|
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n")
|
||||||
|
{
|
||||||
// we landed on a newline, and we aren't inbetween two newlines.
|
// we landed on a newline, and we aren't inbetween two newlines.
|
||||||
self.cursor.sub(1);
|
self.cursor.sub(1);
|
||||||
self.update_select_range();
|
self.update_select_range();
|
||||||
@@ -3085,20 +3106,29 @@ impl Display for LineBuf {
|
|||||||
let start_byte = self.read_idx_byte_pos(start);
|
let start_byte = self.read_idx_byte_pos(start);
|
||||||
let end_byte = self.read_idx_byte_pos(end).min(full_buf.len());
|
let end_byte = self.read_idx_byte_pos(end).min(full_buf.len());
|
||||||
|
|
||||||
|
|
||||||
match mode.anchor() {
|
match mode.anchor() {
|
||||||
SelectAnchor::Start => {
|
SelectAnchor::Start => {
|
||||||
let mut inclusive = start_byte..=end_byte;
|
let mut inclusive = start_byte..=end_byte;
|
||||||
if *inclusive.end() == full_buf.len() {
|
if *inclusive.end() == full_buf.len() {
|
||||||
inclusive = start_byte..=end_byte.saturating_sub(1);
|
inclusive = start_byte..=end_byte.saturating_sub(1);
|
||||||
}
|
}
|
||||||
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[inclusive.clone()], markers::VISUAL_MODE_END)
|
let selected = format!(
|
||||||
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str());
|
"{}{}{}",
|
||||||
|
markers::VISUAL_MODE_START,
|
||||||
|
&full_buf[inclusive.clone()],
|
||||||
|
markers::VISUAL_MODE_END
|
||||||
|
)
|
||||||
|
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
|
||||||
full_buf.replace_range(inclusive, &selected);
|
full_buf.replace_range(inclusive, &selected);
|
||||||
}
|
}
|
||||||
SelectAnchor::End => {
|
SelectAnchor::End => {
|
||||||
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[start_byte..end_byte], markers::VISUAL_MODE_END)
|
let selected = format!(
|
||||||
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str());
|
"{}{}{}",
|
||||||
|
markers::VISUAL_MODE_START,
|
||||||
|
&full_buf[start_byte..end_byte],
|
||||||
|
markers::VISUAL_MODE_END
|
||||||
|
)
|
||||||
|
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
|
||||||
full_buf.replace_range(start_byte..end_byte, &selected);
|
full_buf.replace_range(start_byte..end_byte, &selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ pub mod markers {
|
|||||||
/// which breaks some commands
|
/// which breaks some commands
|
||||||
pub const NULL_EXPAND: Marker = '\u{e007}';
|
pub const NULL_EXPAND: Marker = '\u{e007}';
|
||||||
/// Explicit marker for argument separation
|
/// Explicit marker for argument separation
|
||||||
/// This is used to join the arguments given by "$@", and preserves exact formatting
|
/// This is used to join the arguments given by "$@", and preserves exact
|
||||||
/// of the original arguments, including quoting
|
/// formatting of the original arguments, including quoting
|
||||||
pub const ARG_SEP: Marker = '\u{e008}';
|
pub const ARG_SEP: Marker = '\u{e008}';
|
||||||
|
|
||||||
pub const VI_SEQ_EXP: Marker = '\u{e009}';
|
pub const VI_SEQ_EXP: Marker = '\u{e009}';
|
||||||
@@ -142,7 +142,8 @@ pub struct Prompt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Prompt {
|
impl Prompt {
|
||||||
const DEFAULT_PS1: &str = "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
const DEFAULT_PS1: &str =
|
||||||
|
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let Ok(ps1_raw) = env::var("PS1") else {
|
let Ok(ps1_raw) = env::var("PS1") else {
|
||||||
return Self::default();
|
return Self::default();
|
||||||
@@ -151,7 +152,12 @@ impl Prompt {
|
|||||||
return Self::default();
|
return Self::default();
|
||||||
};
|
};
|
||||||
let psr_raw = env::var("PSR").ok();
|
let psr_raw = env::var("PSR").ok();
|
||||||
let psr_expanded = psr_raw.clone().map(|r| expand_prompt(&r)).transpose().ok().flatten();
|
let psr_expanded = psr_raw
|
||||||
|
.clone()
|
||||||
|
.map(|r| expand_prompt(&r))
|
||||||
|
.transpose()
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
Self {
|
Self {
|
||||||
ps1_expanded,
|
ps1_expanded,
|
||||||
ps1_raw,
|
ps1_raw,
|
||||||
@@ -189,7 +195,8 @@ impl Prompt {
|
|||||||
impl Default for Prompt {
|
impl Default for Prompt {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
|
ps1_expanded: expand_prompt(Self::DEFAULT_PS1)
|
||||||
|
.unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
|
||||||
ps1_raw: Self::DEFAULT_PS1.to_string(),
|
ps1_raw: Self::DEFAULT_PS1.to_string(),
|
||||||
psr_expanded: None,
|
psr_expanded: None,
|
||||||
psr_raw: None,
|
psr_raw: None,
|
||||||
@@ -255,7 +262,6 @@ impl ShedVi {
|
|||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Reset readline state for a new prompt
|
/// Reset readline state for a new prompt
|
||||||
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
|
||||||
// Clear old display before resetting state — old_layout must survive
|
// Clear old display before resetting state — old_layout must survive
|
||||||
@@ -286,8 +292,10 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
let input = Arc::new(self.editor.buffer.clone());
|
let input = Arc::new(self.editor.buffer.clone());
|
||||||
self.editor.calc_indent_level();
|
self.editor.calc_indent_level();
|
||||||
let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
|
let lex_result1 =
|
||||||
let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
|
LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
|
||||||
|
let lex_result2 =
|
||||||
|
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
|
||||||
let is_top_level = self.editor.auto_indent_level == 0;
|
let is_top_level = self.editor.auto_indent_level == 0;
|
||||||
|
|
||||||
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
|
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
|
||||||
@@ -297,12 +305,8 @@ impl ShedVi {
|
|||||||
(true, false) => {
|
(true, false) => {
|
||||||
return Err(lex_result1.unwrap_err());
|
return Err(lex_result1.unwrap_err());
|
||||||
}
|
}
|
||||||
(false, true) => {
|
(false, true) => false,
|
||||||
false
|
(false, false) => true,
|
||||||
}
|
|
||||||
(false, false) => {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(is_complete && is_top_level)
|
Ok(is_complete && is_top_level)
|
||||||
@@ -363,7 +367,7 @@ impl ShedVi {
|
|||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
self.writer.send_bell().ok();
|
self.writer.send_bell().ok();
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.needs_redraw = true;
|
self.needs_redraw = true;
|
||||||
@@ -385,7 +389,9 @@ impl ShedVi {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) {
|
if cmd.is_submit_action()
|
||||||
|
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
|
||||||
|
{
|
||||||
self.editor.set_hint(None);
|
self.editor.set_hint(None);
|
||||||
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
|
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
|
||||||
self.print_line(true)?; // Redraw
|
self.print_line(true)?; // Redraw
|
||||||
@@ -512,7 +518,9 @@ impl ShedVi {
|
|||||||
let line = self.editor.to_string();
|
let line = self.editor.to_string();
|
||||||
let hint = self.editor.get_hint_text();
|
let hint = self.editor.get_hint_text();
|
||||||
if crate::state::read_shopts(|s| s.prompt.highlight) {
|
if crate::state::read_shopts(|s| s.prompt.highlight) {
|
||||||
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
|
self
|
||||||
|
.highlighter
|
||||||
|
.load_input(&line, self.editor.cursor_byte_pos());
|
||||||
self.highlighter.highlight();
|
self.highlighter.highlight();
|
||||||
let highlighted = self.highlighter.take();
|
let highlighted = self.highlighter.take();
|
||||||
format!("{highlighted}{hint}")
|
format!("{highlighted}{hint}")
|
||||||
@@ -527,12 +535,17 @@ impl ShedVi {
|
|||||||
let pending_seq = self.mode.pending_seq();
|
let pending_seq = self.mode.pending_seq();
|
||||||
let mut prompt_string_right = self.prompt.psr_expanded.clone();
|
let mut prompt_string_right = self.prompt.psr_expanded.clone();
|
||||||
|
|
||||||
if prompt_string_right.as_ref().is_some_and(|psr| psr.lines().count() > 1) {
|
if prompt_string_right
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|psr| psr.lines().count() > 1)
|
||||||
|
{
|
||||||
log::warn!("PSR has multiple lines, truncating to one line");
|
log::warn!("PSR has multiple lines, truncating to one line");
|
||||||
prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
|
prompt_string_right =
|
||||||
|
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let row0_used = self.prompt
|
let row0_used = self
|
||||||
|
.prompt
|
||||||
.get_ps1()
|
.get_ps1()
|
||||||
.lines()
|
.lines()
|
||||||
.next()
|
.next()
|
||||||
@@ -541,30 +554,56 @@ impl ShedVi {
|
|||||||
.unwrap_or_default() as usize;
|
.unwrap_or_default() as usize;
|
||||||
let one_line = new_layout.end.row == 0;
|
let one_line = new_layout.end.row == 0;
|
||||||
|
|
||||||
|
|
||||||
if let Some(layout) = self.old_layout.as_ref() {
|
if let Some(layout) = self.old_layout.as_ref() {
|
||||||
self.writer.clear_rows(layout)?;
|
self.writer.clear_rows(layout)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
|
self
|
||||||
|
.writer
|
||||||
|
.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
|
||||||
|
|
||||||
let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
|
let seq_fits = pending_seq
|
||||||
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width()));
|
.as_ref()
|
||||||
|
.is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
|
||||||
|
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| {
|
||||||
|
new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())
|
||||||
|
});
|
||||||
|
|
||||||
if !final_draw && let Some(seq) = pending_seq && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits {
|
if !final_draw
|
||||||
|
&& let Some(seq) = pending_seq
|
||||||
|
&& !seq.is_empty()
|
||||||
|
&& !(prompt_string_right.is_some() && one_line)
|
||||||
|
&& seq_fits
|
||||||
|
{
|
||||||
let to_col = self.writer.t_cols - calc_str_width(&seq);
|
let to_col = self.writer.t_cols - calc_str_width(&seq);
|
||||||
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
|
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
|
||||||
|
|
||||||
let move_up = if up > 0 { format!("\x1b[{up}A") } else { String::new() };
|
let move_up = if up > 0 {
|
||||||
|
format!("\x1b[{up}A")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
// Save cursor, move up to top row, move right to column, write sequence, restore cursor
|
// Save cursor, move up to top row, move right to column, write sequence,
|
||||||
self.writer.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
|
// restore cursor
|
||||||
} else if !final_draw && let Some(psr) = prompt_string_right && psr_fits {
|
self
|
||||||
|
.writer
|
||||||
|
.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
|
||||||
|
} else if !final_draw
|
||||||
|
&& let Some(psr) = prompt_string_right
|
||||||
|
&& psr_fits
|
||||||
|
{
|
||||||
let to_col = self.writer.t_cols - calc_str_width(&psr);
|
let to_col = self.writer.t_cols - calc_str_width(&psr);
|
||||||
let down = new_layout.end.row - new_layout.cursor.row;
|
let down = new_layout.end.row - new_layout.cursor.row;
|
||||||
let move_down = if down > 0 { format!("\x1b[{down}B") } else { String::new() };
|
let move_down = if down > 0 {
|
||||||
|
format!("\x1b[{down}B")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
|
||||||
self.writer.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
|
self
|
||||||
|
.writer
|
||||||
|
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||||
@@ -853,19 +892,22 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
|||||||
/// - Unimplemented features (comments, brace groups)
|
/// - Unimplemented features (comments, brace groups)
|
||||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||||
match class {
|
match class {
|
||||||
TkRule::Pipe |
|
TkRule::Pipe
|
||||||
TkRule::ErrPipe |
|
| TkRule::ErrPipe
|
||||||
TkRule::And |
|
| TkRule::And
|
||||||
TkRule::Or |
|
| TkRule::Or
|
||||||
TkRule::Bg |
|
| TkRule::Bg
|
||||||
TkRule::BraceGrpStart |
|
| TkRule::BraceGrpStart
|
||||||
TkRule::BraceGrpEnd => {
|
| TkRule::BraceGrpEnd => Some(markers::OPERATOR),
|
||||||
Some(markers::OPERATOR)
|
|
||||||
}
|
|
||||||
TkRule::Sep => Some(markers::CMD_SEP),
|
TkRule::Sep => Some(markers::CMD_SEP),
|
||||||
TkRule::Redir => Some(markers::REDIRECT),
|
TkRule::Redir => Some(markers::REDIRECT),
|
||||||
TkRule::Comment => Some(markers::COMMENT),
|
TkRule::Comment => Some(markers::COMMENT),
|
||||||
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None,
|
TkRule::Expanded { exp: _ }
|
||||||
|
| TkRule::EOI
|
||||||
|
| TkRule::SOI
|
||||||
|
| TkRule::Null
|
||||||
|
| TkRule::Str
|
||||||
|
| TkRule::CasePattern => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -880,9 +922,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
std::cmp::Ordering::Equal => {
|
std::cmp::Ordering::Equal => {
|
||||||
let priority = |m: Marker| -> u8 {
|
let priority = |m: Marker| -> u8 {
|
||||||
match m {
|
match m {
|
||||||
markers::VISUAL_MODE_END
|
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
|
||||||
| markers::VISUAL_MODE_START
|
|
||||||
| markers::RESET => 0,
|
|
||||||
markers::VAR_SUB
|
markers::VAR_SUB
|
||||||
| markers::VAR_SUB_END
|
| markers::VAR_SUB_END
|
||||||
| markers::CMD_SUB
|
| markers::CMD_SUB
|
||||||
@@ -911,9 +951,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
std::cmp::Ordering::Equal => {
|
std::cmp::Ordering::Equal => {
|
||||||
let priority = |m: Marker| -> u8 {
|
let priority = |m: Marker| -> u8 {
|
||||||
match m {
|
match m {
|
||||||
markers::VISUAL_MODE_END
|
markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
|
||||||
| markers::VISUAL_MODE_START
|
|
||||||
| markers::RESET => 0,
|
|
||||||
markers::VAR_SUB
|
markers::VAR_SUB
|
||||||
| markers::VAR_SUB_END
|
| markers::VAR_SUB_END
|
||||||
| markers::CMD_SUB
|
| markers::CMD_SUB
|
||||||
@@ -926,7 +964,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
|||||||
| markers::STRING_SQ_END
|
| markers::STRING_SQ_END
|
||||||
| markers::SUBSH_END => 2,
|
| markers::SUBSH_END => 2,
|
||||||
|
|
||||||
| markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
|
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
|
||||||
_ => 1,
|
_ => 1,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -117,15 +117,21 @@ fn enumerate_lines(s: &str, left_pad: usize) -> String {
|
|||||||
let trail_pad = left_pad.saturating_sub(prefix_len);
|
let trail_pad = left_pad.saturating_sub(prefix_len);
|
||||||
if i == total_lines - 1 {
|
if i == total_lines - 1 {
|
||||||
// Don't add a newline to the last line
|
// Don't add a newline to the last line
|
||||||
write!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
write!(
|
||||||
|
acc,
|
||||||
|
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
||||||
" ".repeat(num_pad),
|
" ".repeat(num_pad),
|
||||||
" ".repeat(trail_pad),
|
" ".repeat(trail_pad),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
} else {
|
} else {
|
||||||
writeln!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
writeln!(
|
||||||
|
acc,
|
||||||
|
"\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
|
||||||
" ".repeat(num_pad),
|
" ".repeat(num_pad),
|
||||||
" ".repeat(trail_pad),
|
" ".repeat(trail_pad),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
acc
|
acc
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
use super::register::{append_register, read_register, write_register, RegisterContent};
|
use super::register::{RegisterContent, append_register, read_register, write_register};
|
||||||
|
|
||||||
//TODO: write tests that take edit results and cursor positions from actual
|
//TODO: write tests that take edit results and cursor positions from actual
|
||||||
// neovim edits and test them against the behavior of this editor
|
// neovim edits and test them against the behavior of this editor
|
||||||
@@ -383,7 +383,12 @@ impl Motion {
|
|||||||
pub fn is_linewise(&self) -> bool {
|
pub fn is_linewise(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
|
Self::WholeLineInclusive
|
||||||
|
| Self::WholeLineExclusive
|
||||||
|
| Self::LineUp
|
||||||
|
| Self::LineDown
|
||||||
|
| Self::ScreenLineDown
|
||||||
|
| Self::ScreenLineUp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1020,15 +1020,13 @@ impl ViNormal {
|
|||||||
impl ViMode for ViNormal {
|
impl ViMode for ViNormal {
|
||||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||||
let mut cmd = match key {
|
let mut cmd = match key {
|
||||||
E(K::Char('V'), M::NONE) => {
|
E(K::Char('V'), M::NONE) => Some(ViCmd {
|
||||||
Some(ViCmd {
|
|
||||||
register: Default::default(),
|
register: Default::default(),
|
||||||
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
|
verb: Some(VerbCmd(1, Verb::VisualModeLine)),
|
||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: "".into(),
|
raw_seq: "".into(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
}),
|
||||||
}
|
|
||||||
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
E(K::Char(ch), M::NONE) => self.try_parse(ch),
|
||||||
E(K::Backspace, M::NONE) => Some(ViCmd {
|
E(K::Backspace, M::NONE) => Some(ViCmd {
|
||||||
register: Default::default(),
|
register: Default::default(),
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ impl ShOpts {
|
|||||||
pub fn display_opts(&mut self) -> ShResult<String> {
|
pub fn display_opts(&mut self) -> ShResult<String> {
|
||||||
let output = [
|
let output = [
|
||||||
format!("core:\n{}", self.query("core")?.unwrap_or_default()),
|
format!("core:\n{}", self.query("core")?.unwrap_or_default()),
|
||||||
format!("prompt:\n{}",self.query("prompt")?.unwrap_or_default())
|
format!("prompt:\n{}", self.query("prompt")?.unwrap_or_default()),
|
||||||
];
|
];
|
||||||
|
|
||||||
Ok(output.join("\n"))
|
Ok(output.join("\n"))
|
||||||
@@ -542,7 +542,10 @@ impl Display for ShOptPrompt {
|
|||||||
output.push(format!("comp_limit = {}", self.comp_limit));
|
output.push(format!("comp_limit = {}", self.comp_limit));
|
||||||
output.push(format!("highlight = {}", self.highlight));
|
output.push(format!("highlight = {}", self.highlight));
|
||||||
output.push(format!("auto_indent = {}", self.auto_indent));
|
output.push(format!("auto_indent = {}", self.auto_indent));
|
||||||
output.push(format!("linebreak_on_incomplete = {}", self.linebreak_on_incomplete));
|
output.push(format!(
|
||||||
|
"linebreak_on_incomplete = {}",
|
||||||
|
self.linebreak_on_incomplete
|
||||||
|
));
|
||||||
|
|
||||||
let final_output = output.join("\n");
|
let final_output = output.join("\n");
|
||||||
|
|
||||||
|
|||||||
234
src/state.rs
234
src/state.rs
@@ -1,14 +1,34 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::RefCell, cmp::Ordering, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration
|
cell::RefCell,
|
||||||
|
cmp::Ordering,
|
||||||
|
collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
|
||||||
|
fmt::Display,
|
||||||
|
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::unistd::{User, gethostname, getppid};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{
|
builtin::{BUILTINS, trap::TrapTarget},
|
||||||
|
exec_input,
|
||||||
|
jobs::JobTab,
|
||||||
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::VecDequeExt,
|
utils::VecDequeExt,
|
||||||
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Tk}}, prelude::*, readline::{complete::{BashCompSpec, CompSpec}, markers}, shopt::ShOpts
|
},
|
||||||
|
parse::{
|
||||||
|
ConjunctNode, NdRule, Node, ParsedSrc,
|
||||||
|
lex::{LexFlags, LexStream, Tk},
|
||||||
|
},
|
||||||
|
prelude::*,
|
||||||
|
readline::{
|
||||||
|
complete::{BashCompSpec, CompSpec},
|
||||||
|
markers,
|
||||||
|
},
|
||||||
|
shopt::ShOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Shed {
|
pub struct Shed {
|
||||||
@@ -154,7 +174,7 @@ impl ScopeStack {
|
|||||||
}
|
}
|
||||||
Err(ShErr::simple(
|
Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' not found", var_name)
|
format!("Variable '{}' not found", var_name),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
pub fn export_var(&mut self, var_name: &str) {
|
pub fn export_var(&mut self, var_name: &str) {
|
||||||
@@ -199,48 +219,55 @@ impl ScopeStack {
|
|||||||
self.set_var_global(var_name, val, flags)
|
self.set_var_global(var_name, val, flags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn set_var_indexed(&mut self, var_name: &str, idx: ArrIndex, val: String, flags: VarFlags) -> ShResult<()> {
|
pub fn set_var_indexed(
|
||||||
|
&mut self,
|
||||||
|
var_name: &str,
|
||||||
|
idx: ArrIndex,
|
||||||
|
val: String,
|
||||||
|
flags: VarFlags,
|
||||||
|
) -> ShResult<()> {
|
||||||
let is_local = self.is_local_var(var_name);
|
let is_local = self.is_local_var(var_name);
|
||||||
if flags.contains(VarFlags::LOCAL) || is_local {
|
if flags.contains(VarFlags::LOCAL) || is_local {
|
||||||
let Some(scope) = self.scopes.last_mut() else { return Ok(()) };
|
let Some(scope) = self.scopes.last_mut() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
scope.set_index(var_name, idx, val)
|
scope.set_index(var_name, idx, val)
|
||||||
} else {
|
} else {
|
||||||
let Some(scope) = self.scopes.first_mut() else { return Ok(()) };
|
let Some(scope) = self.scopes.first_mut() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
scope.set_index(var_name, idx, val)
|
scope.set_index(var_name, idx, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> {
|
fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> {
|
||||||
let Some(scope) = self.scopes.first_mut() else {
|
let Some(scope) = self.scopes.first_mut() else {
|
||||||
return Ok(())
|
return Ok(());
|
||||||
};
|
};
|
||||||
scope.set_var(var_name, val, flags)
|
scope.set_var(var_name, val, flags)
|
||||||
}
|
}
|
||||||
fn set_var_local(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> {
|
fn set_var_local(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> {
|
||||||
let Some(scope) = self.scopes.last_mut() else {
|
let Some(scope) = self.scopes.last_mut() else {
|
||||||
return Ok(())
|
return Ok(());
|
||||||
};
|
};
|
||||||
scope.set_var(var_name, val, flags)
|
scope.set_var(var_name, val, flags)
|
||||||
}
|
}
|
||||||
pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> {
|
pub fn get_arr_elems(&self, var_name: &str) -> ShResult<Vec<String>> {
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
if scope.var_exists(var_name)
|
if scope.var_exists(var_name)
|
||||||
&& let Some(var) = scope.vars().get(var_name) {
|
&& let Some(var) = scope.vars().get(var_name)
|
||||||
|
{
|
||||||
match var.kind() {
|
match var.kind() {
|
||||||
VarKind::Arr(items) => {
|
VarKind::Arr(items) => {
|
||||||
let mut item_vec = items.clone()
|
let mut item_vec = items.clone().into_iter().collect::<Vec<(usize, String)>>();
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<(usize, String)>>();
|
|
||||||
|
|
||||||
item_vec.sort_by_key(|(idx, _)| *idx); // sort by index
|
item_vec.sort_by_key(|(idx, _)| *idx); // sort by index
|
||||||
|
|
||||||
return Ok(item_vec.into_iter()
|
return Ok(item_vec.into_iter().map(|(_, s)| s).collect());
|
||||||
.map(|(_,s)| s)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' is not an array", var_name)
|
format!("Variable '{}' is not an array", var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,33 +275,34 @@ impl ScopeStack {
|
|||||||
}
|
}
|
||||||
Err(ShErr::simple(
|
Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' not found", var_name)
|
format!("Variable '{}' not found", var_name),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
pub fn index_var(&self, var_name: &str, idx: ArrIndex) -> ShResult<String> {
|
pub fn index_var(&self, var_name: &str, idx: ArrIndex) -> ShResult<String> {
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
if scope.var_exists(var_name)
|
if scope.var_exists(var_name)
|
||||||
&& let Some(var) = scope.vars().get(var_name) {
|
&& let Some(var) = scope.vars().get(var_name)
|
||||||
|
{
|
||||||
match var.kind() {
|
match var.kind() {
|
||||||
VarKind::Arr(items) => {
|
VarKind::Arr(items) => {
|
||||||
let idx = match idx {
|
let idx = match idx {
|
||||||
ArrIndex::Literal(n) => {
|
ArrIndex::Literal(n) => n,
|
||||||
n
|
|
||||||
}
|
|
||||||
ArrIndex::FromBack(n) => {
|
ArrIndex::FromBack(n) => {
|
||||||
if items.len() >= n {
|
if items.len() >= n {
|
||||||
items.len() - n
|
items.len() - n
|
||||||
} else {
|
} else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Index {} out of bounds for array '{}'", n, var_name)
|
format!("Index {} out of bounds for array '{}'", n, var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(ShErr::simple(
|
_ => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Cannot index all elements of array '{}'", var_name)
|
format!("Cannot index all elements of array '{}'", var_name),
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(item) = items.get(&idx) {
|
if let Some(item) = items.get(&idx) {
|
||||||
@@ -282,14 +310,14 @@ impl ScopeStack {
|
|||||||
} else {
|
} else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Index {} out of bounds for array '{}'", idx, var_name)
|
format!("Index {} out of bounds for array '{}'", idx, var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' is not an array", var_name)
|
format!("Variable '{}' is not an array", var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,11 +358,10 @@ impl ScopeStack {
|
|||||||
std::env::var(var_name).unwrap_or_default()
|
std::env::var(var_name).unwrap_or_default()
|
||||||
}
|
}
|
||||||
pub fn is_local_var(&self, var_name: &str) -> bool {
|
pub fn is_local_var(&self, var_name: &str) -> bool {
|
||||||
self.scopes
|
self.scopes.last().is_some_and(|s| {
|
||||||
.last()
|
s.get_var_flags(var_name)
|
||||||
.is_some_and(|s|
|
.is_some_and(|flags| flags.contains(VarFlags::LOCAL))
|
||||||
s.get_var_flags(var_name).is_some_and(|flags| flags.contains(VarFlags::LOCAL))
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> {
|
pub fn get_var_flags(&self, var_name: &str) -> Option<VarFlags> {
|
||||||
for scope in self.scopes.iter().rev() {
|
for scope in self.scopes.iter().rev() {
|
||||||
@@ -383,9 +410,9 @@ thread_local! {
|
|||||||
|
|
||||||
/// A shell function
|
/// A shell function
|
||||||
///
|
///
|
||||||
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to.
|
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers
|
||||||
/// The Node must be stored with the ParsedSrc because the tokens of the node
|
/// to. The Node must be stored with the ParsedSrc because the tokens of the
|
||||||
/// contain an Arc<String> Which refers to the String held in ParsedSrc
|
/// node contain an Arc<String> Which refers to the String held in ParsedSrc
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ShFunc(Node);
|
pub struct ShFunc(Node);
|
||||||
|
|
||||||
@@ -537,7 +564,7 @@ pub enum ArrIndex {
|
|||||||
Literal(usize),
|
Literal(usize),
|
||||||
FromBack(usize),
|
FromBack(usize),
|
||||||
AllJoined,
|
AllJoined,
|
||||||
AllSplit
|
AllSplit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ArrIndex {
|
impl FromStr for ArrIndex {
|
||||||
@@ -556,27 +583,24 @@ impl FromStr for ArrIndex {
|
|||||||
}
|
}
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("Invalid array index: {}", s)
|
format!("Invalid array index: {}", s),
|
||||||
))
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hashmap_to_vec(map: HashMap<usize, String>) -> Vec<String> {
|
pub fn hashmap_to_vec(map: HashMap<usize, String>) -> Vec<String> {
|
||||||
let mut items = map.into_iter()
|
let mut items = map.into_iter().collect::<Vec<(usize, String)>>();
|
||||||
.collect::<Vec<(usize, String)>>();
|
|
||||||
items.sort_by_key(|(idx, _)| *idx);
|
items.sort_by_key(|(idx, _)| *idx);
|
||||||
|
|
||||||
items.into_iter()
|
items.into_iter().map(|(_, i)| i).collect()
|
||||||
.map(|(_,i)| i)
|
|
||||||
.collect()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum VarKind {
|
pub enum VarKind {
|
||||||
Str(String),
|
Str(String),
|
||||||
Int(i32),
|
Int(i32),
|
||||||
Arr(HashMap<usize,String>),
|
Arr(HashMap<usize, String>),
|
||||||
AssocArr(Vec<(String, String)>),
|
AssocArr(Vec<(String, String)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,7 +615,7 @@ impl VarKind {
|
|||||||
}
|
}
|
||||||
let raw = raw[1..raw.len() - 1].to_string();
|
let raw = raw[1..raw.len() - 1].to_string();
|
||||||
|
|
||||||
let tokens: HashMap<usize,String> = LexStream::new(Arc::new(raw), LexFlags::empty())
|
let tokens: HashMap<usize, String> = LexStream::new(Arc::new(raw), LexFlags::empty())
|
||||||
.map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words()))
|
.map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words()))
|
||||||
.try_fold(vec![], |mut acc, wrds| {
|
.try_fold(vec![], |mut acc, wrds| {
|
||||||
match wrds {
|
match wrds {
|
||||||
@@ -608,9 +632,7 @@ impl VarKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn arr_from_vec(vec: Vec<String>) -> Self {
|
pub fn arr_from_vec(vec: Vec<String>) -> Self {
|
||||||
let tokens: HashMap<usize,String> = vec.into_iter()
|
let tokens: HashMap<usize, String> = vec.into_iter().enumerate().collect();
|
||||||
.enumerate()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self::Arr(tokens)
|
Self::Arr(tokens)
|
||||||
}
|
}
|
||||||
@@ -847,10 +869,12 @@ impl VarTab {
|
|||||||
self.vars.get(var_name).map(|var| var.flags)
|
self.vars.get(var_name).map(|var| var.flags)
|
||||||
}
|
}
|
||||||
pub fn unset_var(&mut self, var_name: &str) -> ShResult<()> {
|
pub fn unset_var(&mut self, var_name: &str) -> ShResult<()> {
|
||||||
if let Some(var) = self.vars.get(var_name) && var.flags.contains(VarFlags::READONLY) {
|
if let Some(var) = self.vars.get(var_name)
|
||||||
|
&& var.flags.contains(VarFlags::READONLY)
|
||||||
|
{
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("cannot unset readonly variable '{}'", var_name)
|
format!("cannot unset readonly variable '{}'", var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.vars.remove(var_name);
|
self.vars.remove(var_name);
|
||||||
@@ -859,27 +883,28 @@ impl VarTab {
|
|||||||
}
|
}
|
||||||
pub fn set_index(&mut self, var_name: &str, idx: ArrIndex, val: String) -> ShResult<()> {
|
pub fn set_index(&mut self, var_name: &str, idx: ArrIndex, val: String) -> ShResult<()> {
|
||||||
if self.var_exists(var_name)
|
if self.var_exists(var_name)
|
||||||
&& let Some(var) = self.vars_mut().get_mut(var_name) {
|
&& let Some(var) = self.vars_mut().get_mut(var_name)
|
||||||
|
{
|
||||||
match var.kind_mut() {
|
match var.kind_mut() {
|
||||||
VarKind::Arr(items) => {
|
VarKind::Arr(items) => {
|
||||||
let idx = match idx {
|
let idx = match idx {
|
||||||
ArrIndex::Literal(n) => {
|
ArrIndex::Literal(n) => n,
|
||||||
n
|
|
||||||
}
|
|
||||||
ArrIndex::FromBack(n) => {
|
ArrIndex::FromBack(n) => {
|
||||||
if items.len() >= n {
|
if items.len() >= n {
|
||||||
items.len() - n
|
items.len() - n
|
||||||
} else {
|
} else {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Index {} out of bounds for array '{}'", n, var_name)
|
format!("Index {} out of bounds for array '{}'", n, var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => return Err(ShErr::simple(
|
_ => {
|
||||||
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Cannot index all elements of array '{}'", var_name)
|
format!("Cannot index all elements of array '{}'", var_name),
|
||||||
)),
|
));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
items.insert(idx, val);
|
items.insert(idx, val);
|
||||||
@@ -888,7 +913,7 @@ impl VarTab {
|
|||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' is not an array", var_name)
|
format!("Variable '{}' is not an array", var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -900,7 +925,7 @@ impl VarTab {
|
|||||||
if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) {
|
if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("Variable '{}' is readonly", var_name)
|
format!("Variable '{}' is readonly", var_name),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
var.kind = val;
|
var.kind = val;
|
||||||
@@ -975,7 +1000,44 @@ pub struct MetaTab {
|
|||||||
|
|
||||||
impl MetaTab {
|
impl MetaTab {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::default()
|
Self {
|
||||||
|
comp_specs: Self::get_builtin_comp_specs(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_builtin_comp_specs() -> HashMap<String, Box<dyn CompSpec>> {
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
map.insert(
|
||||||
|
"cd".into(),
|
||||||
|
Box::new(BashCompSpec::new().dirs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"pushd".into(),
|
||||||
|
Box::new(BashCompSpec::new().dirs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"popd".into(),
|
||||||
|
Box::new(BashCompSpec::new().dirs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"source".into(),
|
||||||
|
Box::new(BashCompSpec::new().files(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"bg".into(),
|
||||||
|
Box::new(BashCompSpec::new().jobs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"fg".into(),
|
||||||
|
Box::new(BashCompSpec::new().jobs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"disown".into(),
|
||||||
|
Box::new(BashCompSpec::new().jobs(true)) as Box<dyn CompSpec>,
|
||||||
|
);
|
||||||
|
|
||||||
|
map
|
||||||
}
|
}
|
||||||
pub fn cached_cmds(&self) -> &HashSet<String> {
|
pub fn cached_cmds(&self) -> &HashSet<String> {
|
||||||
&self.path_cache
|
&self.path_cache
|
||||||
@@ -1002,7 +1064,8 @@ impl MetaTab {
|
|||||||
let path = env::var("PATH").unwrap_or_default();
|
let path = env::var("PATH").unwrap_or_default();
|
||||||
let cwd = env::var("PWD").unwrap_or_default();
|
let cwd = env::var("PWD").unwrap_or_default();
|
||||||
if self.old_path.as_ref().is_some_and(|old| *old == path)
|
if self.old_path.as_ref().is_some_and(|old| *old == path)
|
||||||
&& self.old_pwd.as_ref().is_some_and(|old| *old == cwd) {
|
&& self.old_pwd.as_ref().is_some_and(|old| *old == cwd)
|
||||||
|
{
|
||||||
log::trace!("PATH and PWD unchanged, skipping rehash");
|
log::trace!("PATH and PWD unchanged, skipping rehash");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1012,17 +1075,20 @@ impl MetaTab {
|
|||||||
self.path_cache.clear();
|
self.path_cache.clear();
|
||||||
self.old_path = Some(path.clone());
|
self.old_path = Some(path.clone());
|
||||||
self.old_pwd = Some(cwd.clone());
|
self.old_pwd = Some(cwd.clone());
|
||||||
let paths = path.split(":")
|
let paths = path.split(":").map(PathBuf::from);
|
||||||
.map(PathBuf::from);
|
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Ok(entries) = path.read_dir() {
|
if let Ok(entries) = path.read_dir() {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let Ok(meta) = std::fs::metadata(entry.path()) else { continue };
|
let Ok(meta) = std::fs::metadata(entry.path()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||||
|
|
||||||
if meta.is_file() && is_exec
|
if meta.is_file()
|
||||||
&& let Some(name) = entry.file_name().to_str() {
|
&& is_exec
|
||||||
|
&& let Some(name) = entry.file_name().to_str()
|
||||||
|
{
|
||||||
self.path_cache.insert(name.to_string());
|
self.path_cache.insert(name.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1030,11 +1096,15 @@ impl MetaTab {
|
|||||||
}
|
}
|
||||||
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let Ok(meta) = std::fs::metadata(entry.path()) else { continue };
|
let Ok(meta) = std::fs::metadata(entry.path()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||||
|
|
||||||
if meta.is_file() && is_exec
|
if meta.is_file()
|
||||||
&& let Some(name) = entry.file_name().to_str() {
|
&& is_exec
|
||||||
|
&& let Some(name) = entry.file_name().to_str()
|
||||||
|
{
|
||||||
self.path_cache.insert(format!("./{}", name));
|
self.path_cache.insert(format!("./{}", name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1066,11 +1136,15 @@ impl MetaTab {
|
|||||||
|
|
||||||
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
if let Ok(entries) = Path::new(&cwd).read_dir() {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let Ok(meta) = std::fs::metadata(entry.path()) else { continue };
|
let Ok(meta) = std::fs::metadata(entry.path()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
let is_exec = meta.permissions().mode() & 0o111 != 0;
|
||||||
|
|
||||||
if meta.is_file() && is_exec
|
if meta.is_file()
|
||||||
&& let Some(name) = entry.file_name().to_str() {
|
&& is_exec
|
||||||
|
&& let Some(name) = entry.file_name().to_str()
|
||||||
|
{
|
||||||
self.cwd_cache.insert(name.to_string());
|
self.cwd_cache.insert(name.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1158,7 +1232,9 @@ pub fn parse_arr_bracket(var_name: &str) -> Option<(String, String)> {
|
|||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
'\\' => { chars.next(); }
|
'\\' => {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
'[' => {
|
'[' => {
|
||||||
bracket_depth += 1;
|
bracket_depth += 1;
|
||||||
if bracket_depth > 1 {
|
if bracket_depth > 1 {
|
||||||
@@ -1204,10 +1280,12 @@ pub fn expand_arr_index(idx_raw: &str) -> ShResult<ArrIndex> {
|
|||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| ShErr::simple(ShErrKind::ParseErr, "Empty array index"))?;
|
.ok_or_else(|| ShErr::simple(ShErrKind::ParseErr, "Empty array index"))?;
|
||||||
|
|
||||||
expanded.parse::<ArrIndex>().map_err(|_| ShErr::simple(
|
expanded.parse::<ArrIndex>().map_err(|_| {
|
||||||
|
ShErr::simple(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
format!("Invalid array index: {}", expanded)
|
format!("Invalid array index: {}", expanded),
|
||||||
))
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
|
pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use tempfile::TempDir;
|
|||||||
|
|
||||||
use crate::prompt::readline::complete::Completer;
|
use crate::prompt::readline::complete::Completer;
|
||||||
use crate::prompt::readline::markers;
|
use crate::prompt::readline::markers;
|
||||||
use crate::state::{write_logic, write_vars, VarFlags};
|
use crate::state::{VarFlags, write_logic, write_vars};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -192,10 +192,12 @@ fn complete_filename_with_slash() {
|
|||||||
|
|
||||||
// Should complete files in subdir/
|
// Should complete files in subdir/
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
assert!(completer
|
assert!(
|
||||||
|
completer
|
||||||
.candidates
|
.candidates
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.contains("nested.txt")));
|
.any(|c| c.contains("nested.txt"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,10 +704,12 @@ fn complete_special_characters_in_filename() {
|
|||||||
|
|
||||||
if result.is_some() {
|
if result.is_some() {
|
||||||
// Should handle special chars in filenames
|
// Should handle special chars in filenames
|
||||||
assert!(completer
|
assert!(
|
||||||
|
completer
|
||||||
.candidates
|
.candidates
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")));
|
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ fn unclosed_squote() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn unclosed_brc_grp() {
|
fn unclosed_brc_grp() {
|
||||||
let input = "{ foo bar";
|
let input = "{ foo bar";
|
||||||
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
|
let tokens =
|
||||||
.collect::<ShResult<Vec<_>>>();
|
LexStream::new(Arc::new(input.into()), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
|
||||||
|
|
||||||
let Err(err) = tokens else {
|
let Err(err) = tokens else {
|
||||||
panic!("Expected an error, got {:?}", tokens);
|
panic!("Expected an error, got {:?}", tokens);
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ use super::*;
|
|||||||
#[test]
|
#[test]
|
||||||
fn simple_expansion() {
|
fn simple_expansion() {
|
||||||
let varsub = "$foo";
|
let varsub = "$foo";
|
||||||
write_vars(|v| v.set_var("foo", VarKind::Str("this is the value of the variable".into()), VarFlags::NONE));
|
write_vars(|v| {
|
||||||
|
v.set_var(
|
||||||
|
"foo",
|
||||||
|
VarKind::Str("this is the value of the variable".into()),
|
||||||
|
VarFlags::NONE,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
|
||||||
.map(|tk| tk.unwrap())
|
.map(|tk| tk.unwrap())
|
||||||
@@ -308,7 +314,10 @@ fn dquote_escape_dollar() {
|
|||||||
fn dquote_escape_backslash() {
|
fn dquote_escape_backslash() {
|
||||||
// "\\" in double quotes should produce a single backslash
|
// "\\" in double quotes should produce a single backslash
|
||||||
let result = unescape_str(r#""\\""#);
|
let result = unescape_str(r#""\\""#);
|
||||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
let inner: String = result
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c != markers::DUB_QUOTE)
|
||||||
|
.collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inner, "\\",
|
inner, "\\",
|
||||||
"Double backslash should produce single backslash"
|
"Double backslash should produce single backslash"
|
||||||
@@ -319,7 +328,10 @@ fn dquote_escape_backslash() {
|
|||||||
fn dquote_escape_quote() {
|
fn dquote_escape_quote() {
|
||||||
// "\"" should produce a literal double quote
|
// "\"" should produce a literal double quote
|
||||||
let result = unescape_str(r#""\"""#);
|
let result = unescape_str(r#""\"""#);
|
||||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
let inner: String = result
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c != markers::DUB_QUOTE)
|
||||||
|
.collect();
|
||||||
assert!(
|
assert!(
|
||||||
inner.contains('"'),
|
inner.contains('"'),
|
||||||
"Escaped quote should produce literal quote"
|
"Escaped quote should produce literal quote"
|
||||||
@@ -330,7 +342,10 @@ fn dquote_escape_quote() {
|
|||||||
fn dquote_escape_backtick() {
|
fn dquote_escape_backtick() {
|
||||||
// "\`" should strip backslash, produce literal backtick
|
// "\`" should strip backslash, produce literal backtick
|
||||||
let result = unescape_str(r#""\`""#);
|
let result = unescape_str(r#""\`""#);
|
||||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
let inner: String = result
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c != markers::DUB_QUOTE)
|
||||||
|
.collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inner, "`",
|
inner, "`",
|
||||||
"Escaped backtick should produce literal backtick"
|
"Escaped backtick should produce literal backtick"
|
||||||
@@ -341,7 +356,10 @@ fn dquote_escape_backtick() {
|
|||||||
fn dquote_escape_nonspecial_preserves_backslash() {
|
fn dquote_escape_nonspecial_preserves_backslash() {
|
||||||
// "\a" inside double quotes should preserve the backslash (a is not special)
|
// "\a" inside double quotes should preserve the backslash (a is not special)
|
||||||
let result = unescape_str(r#""\a""#);
|
let result = unescape_str(r#""\a""#);
|
||||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
let inner: String = result
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c != markers::DUB_QUOTE)
|
||||||
|
.collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
inner, "\\a",
|
inner, "\\a",
|
||||||
"Backslash before non-special char should be preserved"
|
"Backslash before non-special char should be preserved"
|
||||||
@@ -362,10 +380,16 @@ fn dquote_unescaped_dollar_expands() {
|
|||||||
fn dquote_mixed_escapes() {
|
fn dquote_mixed_escapes() {
|
||||||
// "hello \$world \\end" should have literal $, single backslash
|
// "hello \$world \\end" should have literal $, single backslash
|
||||||
let result = unescape_str(r#""hello \$world \\end""#);
|
let result = unescape_str(r#""hello \$world \\end""#);
|
||||||
assert!(!result.contains(markers::VAR_SUB), "Escaped $ should not expand");
|
assert!(
|
||||||
|
!result.contains(markers::VAR_SUB),
|
||||||
|
"Escaped $ should not expand"
|
||||||
|
);
|
||||||
assert!(result.contains('$'), "Literal $ should be in output");
|
assert!(result.contains('$'), "Literal $ should be in output");
|
||||||
// Should have exactly one backslash (from \\)
|
// Should have exactly one backslash (from \\)
|
||||||
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect();
|
let inner: String = result
|
||||||
|
.chars()
|
||||||
|
.filter(|&c| c != markers::DUB_QUOTE)
|
||||||
|
.collect();
|
||||||
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
||||||
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use super::*;
|
|||||||
use crate::expand::{expand_aliases, unescape_str};
|
use crate::expand::{expand_aliases, unescape_str};
|
||||||
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
|
NdRule, Node, ParseStream,
|
||||||
lex::{LexFlags, LexStream, Tk, TkRule},
|
lex::{LexFlags, LexStream, Tk, TkRule},
|
||||||
node_operation, NdRule, Node, ParseStream,
|
node_operation,
|
||||||
};
|
};
|
||||||
use crate::state::{write_logic, write_vars};
|
use crate::state::{write_logic, write_vars};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
expand::expand_prompt, libsh::{
|
expand::expand_prompt,
|
||||||
|
libsh::{
|
||||||
error::ShErr,
|
error::ShErr,
|
||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
}, prompt::readline::{
|
},
|
||||||
Prompt, ShedVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal}
|
prompt::readline::{
|
||||||
}
|
Prompt, ShedVi,
|
||||||
|
history::History,
|
||||||
|
keys::{KeyCode, KeyEvent, ModKeys},
|
||||||
|
linebuf::LineBuf,
|
||||||
|
term::{KeyReader, LineWriter, raw_mode},
|
||||||
|
vimode::{ViInsert, ViMode, ViNormal},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@@ -251,7 +258,11 @@ fn linebuf_ascii_content() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn expand_default_prompt() {
|
fn expand_default_prompt() {
|
||||||
let prompt = expand_prompt("\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ".into()).unwrap();
|
let prompt = expand_prompt(
|
||||||
|
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(prompt)
|
insta::assert_debug_snapshot!(prompt)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
lex::{LexFlags, LexStream},
|
|
||||||
NdRule, Node, ParseStream, Redir, RedirType,
|
NdRule, Node, ParseStream, Redir, RedirType,
|
||||||
|
lex::{LexFlags, LexStream},
|
||||||
};
|
};
|
||||||
use crate::procio::{IoFrame, IoMode, IoStack};
|
use crate::procio::{IoFrame, IoMode, IoStack};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user