Early implementation of bash-like completions with 'complete' and 'compgen' builtins

This commit is contained in:
2026-02-27 01:10:52 -05:00
parent 4fbc25090d
commit 5f3610c298
17 changed files with 879 additions and 227 deletions

248
src/builtin/complete.rs Normal file
View File

@@ -0,0 +1,248 @@
use bitflags::bitflags;
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}};
pub const COMPGEN_OPTS: [OptSpec;7] = [
OptSpec {
opt: Opt::Short('F'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('W'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('f'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('d'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('u'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('v'),
takes_arg: false
}
];
pub const COMP_OPTS: [OptSpec;10] = [
OptSpec {
opt: Opt::Short('F'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('W'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('A'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('p'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('r'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('f'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('d'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('u'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('v'),
takes_arg: false
}
];
bitflags! {
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompFlags: u32 {
const FILES = 0b0000000001;
const DIRS = 0b0000000010;
const CMDS = 0b0000000100;
const USERS = 0b0000001000;
const VARS = 0b0000010000;
const PRINT = 0b0000100000;
const REMOVE = 0b0001000000;
}
}
#[derive(Default, Debug, Clone)]
pub struct CompOpts {
pub func: Option<String>,
pub wordlist: Option<Vec<String>>,
pub action: Option<String>,
pub flags: CompFlags
}
pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
assert!(!argv.is_empty());
let src = argv.clone()
.into_iter()
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
.collect::<ShResult<Vec<String>>>()?
.join(" ");
let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?;
let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() {
read_meta(|m| {
let specs = m.comp_specs().values();
for spec in specs {
println!("{}", spec.source());
}
})
} else {
read_meta(|m| {
for (cmd,_) in &argv {
if let Some(spec) = m.comp_specs().get(cmd) {
println!("{}", spec.source());
}
}
})
}
state::set_status(0);
return Ok(());
}
if comp_opts.flags.contains(CompFlags::REMOVE) {
write_meta(|m| {
for (cmd,_) in &argv {
m.remove_comp_spec(cmd);
}
});
state::set_status(0);
return Ok(());
}
if argv.is_empty() {
state::set_status(1);
return Err(ShErr::full(ShErrKind::ExecFail, "complete: no command specified", blame));
}
let comp_spec = BashCompSpec::from_comp_opts(comp_opts)
.with_source(src);
for (cmd,_) in argv {
write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone())));
}
state::set_status(0);
Ok(())
}
pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
assert!(!argv.is_empty());
let src = argv.clone()
.into_iter()
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
.collect::<ShResult<Vec<String>>>()?
.join(" ");
let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
let prefix = argv
.clone()
.into_iter()
.nth(1)
.unwrap_or_default();
let comp_opts = get_comp_opts(opts)?;
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let comp_spec = BashCompSpec::from_comp_opts(comp_opts)
.with_source(src);
log::debug!("compgen: prefix='{}', spec={:?}", prefix.as_str(), comp_spec);
let dummy_ctx = CompContext {
words: vec![prefix.clone()],
cword: 0,
line: prefix.to_string(),
cursor_pos: prefix.as_str().len()
};
let results = comp_spec.complete(&dummy_ctx)?;
log::debug!("compgen: {} results: {:?}", results.len(), results);
let stdout = borrow_fd(STDOUT_FILENO);
for result in &results {
write(stdout, result.as_bytes())?;
write(stdout, b"\n")?;
}
state::set_status(0);
Ok(())
}
pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
let mut comp_opts = CompOpts::default();
for opt in opts {
match opt {
Opt::ShortWithArg('F',func) => {
comp_opts.func = Some(func);
},
Opt::ShortWithArg('W',wordlist) => {
comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect());
},
Opt::ShortWithArg('A',action) => {
comp_opts.action = Some(action);
}
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,
Opt::Short('f') => comp_opts.flags |= CompFlags::FILES,
Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS,
Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS,
Opt::Short('u') => comp_opts.flags |= CompFlags::USERS,
Opt::Short('v') => comp_opts.flags |= CompFlags::VARS,
_ => unreachable!()
}
}
Ok(comp_opts)
}

View File

@@ -1,5 +1,3 @@
use std::sync::LazyLock;
use crate::{
builtin::setup_builtin,
expand::expand_prompt,

View File

@@ -1,5 +1,3 @@
use nix::{errno::Errno, unistd::execvpe};
use crate::{
builtin::setup_builtin,
jobs::JobBldr,

View File

@@ -1,7 +1,6 @@
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{execute::prepare_argv, NdRule, Node},
prelude::*,
};
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {

View File

@@ -8,7 +8,7 @@ use crate::{
execute::prepare_argv,
lex::{Span, Tk},
},
procio::{IoFrame, IoStack, RedirGuard}, state,
procio::{IoStack, RedirGuard}, state,
};
pub mod alias;
@@ -28,11 +28,12 @@ pub mod zoltraak;
pub mod dirstack;
pub mod exec;
pub mod eval;
pub mod complete;
pub const BUILTINS: [&str; 33] = [
pub const BUILTINS: [&str; 35] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias",
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
"pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset"
"pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen"
];
/// Sets up a builtin command

View File

@@ -150,7 +150,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
state::set_status(1);
break; // EOF
}
Ok(n) => {
Ok(_) => {
if buf[0] == read_opts.delim {
state::set_status(0);
break; // Delimiter reached, stop reading

View File

@@ -9,7 +9,6 @@ use regex::Regex;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
prelude::*,
};
#[derive(Debug, Clone)]

View File

@@ -60,10 +60,10 @@ impl FromStr for TrapTarget {
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
_ => {
return Err(ShErr::simple(
Err(ShErr::simple(
ShErrKind::ExecFail,
format!("invalid trap target '{}'", s),
));
))
}
}
}
@@ -117,7 +117,6 @@ impl Display for TrapTarget {
}
pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span();
let NdRule::Command {
assignments: _,
argv,

View File

@@ -1,7 +1,7 @@
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock};
use std::os::unix::fs::OpenOptionsExt;
use crate::{
getopt::{get_opts_from_tokens, Opt, OptSet, OptSpec},
getopt::{get_opts_from_tokens, Opt, OptSpec},
jobs::JobBldr,
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
@@ -121,9 +121,7 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
),
);
}
if let Err(e) = annihilate(&arg, flags).blame(span) {
return Err(e);
}
annihilate(&arg, flags).blame(span)?
}
Ok(())