Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More compgen implementation #13

Merged
merged 1 commit into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cli/tests/cases/builtins/compgen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: "Builtins: compgen"
cases:
- name: "compgen -A alias"
stdin: |
alias myalias="ls"
alias myalias2="echo hi"

compgen -A alias myalias

- name: "compgen -A builtin"
stdin: |
compgen -A builtin cd

- name: "compgen -A directory"
known_failure: true
stdin: |
touch somefile
mkdir somedir
mkdir somedir2

compgen -A directory some

- name: "compgen -A file"
known_failure: true
stdin: |
touch somefile
mkdir somedir
mkdir somedir2

compgen -A file some

- name: "compgen -A function"
stdin: |
myfunc() {
echo hi
}

myfunc2() {
echo hello
}

compgen -A function myfunc

- name: "compgen -A keyword"
stdin: |
compgen -A keyword esa

- name: "compgen -A variable"
stdin: |
declare myvar=10
declare myvar2=11

compgen -A variable myvar
2 changes: 1 addition & 1 deletion shell/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ whoami = "1.5.1"

[target.'cfg(unix)'.dependencies]
exec = "0.3.1"
nix = { version = "0.28.0", features = ["fs", "process", "signal"] }
nix = { version = "0.28.0", features = ["fs", "process", "signal", "user"] }
tokio-command-fds = "0.2.1"
uzers = "0.12.0"

Expand Down
40 changes: 24 additions & 16 deletions shell/src/builtins/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,21 +477,23 @@ pub(crate) struct CompOptCommand {
impl BuiltinCommand for CompOptCommand {
async fn execute(
&self,
_context: crate::context::CommandExecutionContext<'_>,
context: crate::context::CommandExecutionContext<'_>,
) -> Result<crate::builtin::BuiltinExitCode, crate::error::Error> {
if self.update_default {
return error::unimp("compopt -D");
}
if self.update_empty {
return error::unimp("compopt -E");
}
if self.update_initial_word {
return error::unimp("compopt -I");
}
if !self.names.is_empty() {
tracing::debug!("UNIMPLEMENTED: compopt with names");
return error::unimp("compopt with names");
}

let target_spec = if self.update_default {
Some(&mut context.shell.completion_config.default)
} else if self.update_empty {
Some(&mut context.shell.completion_config.empty_line)
} else if self.update_initial_word {
Some(&mut context.shell.completion_config.initial_word)
} else {
None
};

let mut options = HashMap::new();
for option in &self.disabled_options {
options.insert(option.clone(), false);
Expand All @@ -500,12 +502,18 @@ impl BuiltinCommand for CompOptCommand {
options.insert(option.clone(), true);
}

// TODO: implement options
for (option, value) in options {
if value {
tracing::debug!("compopt: enabling {option:?}");
} else {
tracing::debug!("compopt: disabling {option:?}");
if let Some(Some(target_spec)) = target_spec {
for (option, value) in options {
match option {
CompleteOption::BashDefault => target_spec.bash_default = value,
CompleteOption::Default => target_spec.default = value,
CompleteOption::DirNames => target_spec.dir_names = value,
CompleteOption::FileNames => target_spec.file_names = value,
CompleteOption::NoQuote => target_spec.no_quote = value,
CompleteOption::NoSort => target_spec.no_sort = value,
CompleteOption::NoSpace => target_spec.no_space = value,
CompleteOption::PlusDirs => target_spec.plus_dirs = value,
}
}
}

Expand Down
30 changes: 2 additions & 28 deletions shell/src/builtins/typ.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{io::Write, sync::Arc};
use clap::Parser;
use parser::ast;

use crate::keywords;
use crate::{
builtin::{BuiltinCommand, BuiltinExitCode},
Shell,
Expand Down Expand Up @@ -134,7 +135,7 @@ impl TypeCommand {
}

// Check for keywords.
if is_keyword(shell, name) {
if keywords::is_keyword(shell, name) {
types.push(ResolvedType::Keyword);
}

Expand Down Expand Up @@ -165,30 +166,3 @@ impl TypeCommand {
types
}
}

fn is_keyword(shell: &Shell, name: &str) -> bool {
match name {
"!" => true,
"{" => true,
"}" => true,
"case" => true,
"do" => true,
"done" => true,
"elif" => true,
"else" => true,
"esac" => true,
"fi" => true,
"for" => true,
"if" => true,
"in" => true,
"then" => true,
"until" => true,
"while" => true,
// N.B. Some shells also treat the following as reserved.
"[[" if !shell.options.sh_mode => true,
"]]" if !shell.options.sh_mode => true,
"function" if !shell.options.sh_mode => true,
"select" if !shell.options.sh_mode => true,
_ => false,
}
}
124 changes: 90 additions & 34 deletions shell/src/completion.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use clap::ValueEnum;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};

use crate::{env, error, patterns, variables::ShellValueLiteral, Shell};
use crate::{env, error, namedoptions, patterns, users, variables::ShellValueLiteral, Shell};

#[derive(Clone, Debug, ValueEnum)]
pub enum CompleteAction {
Expand Down Expand Up @@ -183,19 +183,36 @@ impl CompletionSpec {
candidates.push(name.to_string());
}
}
CompleteAction::ArrayVar => tracing::debug!("UNIMPLEMENTED: complete -A arrayvar"),
CompleteAction::ArrayVar => {
for (name, var) in shell.env.iter() {
if var.value().is_array() {
candidates.push(name.to_owned());
}
}
}
CompleteAction::Binding => tracing::debug!("UNIMPLEMENTED: complete -A binding"),
CompleteAction::Builtin => {
let mut builtin_names = shell.get_builtin_names();
candidates.append(&mut builtin_names);
}
CompleteAction::Command => tracing::debug!("UNIMPLEMENTED: complete -A command"),
CompleteAction::Command => {
let mut command_completions = get_command_completions(shell, context);
candidates.append(&mut command_completions);
}
CompleteAction::Directory => {
let mut file_completions = get_file_completions(shell, context, true);
candidates.append(&mut file_completions);
}
CompleteAction::Disabled => tracing::debug!("UNIMPLEMENTED: complete -A disabled"),
CompleteAction::Enabled => tracing::debug!("UNIMPLEMENTED: complete -A enabled"),
CompleteAction::Disabled => {
// For now, no builtins are disabled.
tracing::debug!("UNIMPLEMENTED: complete -A disabled");
}
CompleteAction::Enabled => {
// For now, all builtins are enabled
tracing::debug!("UNIMPLEMENTED: complete -A enabled");
let mut builtin_names = shell.get_builtin_names();
candidates.append(&mut builtin_names);
}
CompleteAction::Export => {
for (key, value) in shell.env.iter() {
if value.is_exported() {
Expand All @@ -212,20 +229,43 @@ impl CompletionSpec {
candidates.push(name.to_owned());
}
}
CompleteAction::Group => tracing::debug!("UNIMPLEMENTED: complete -A group"),
CompleteAction::Group => {
let mut names = users::get_all_groups()?;
candidates.append(&mut names);
}
CompleteAction::HelpTopic => {
tracing::debug!("UNIMPLEMENTED: complete -A helptopic");
}
CompleteAction::HostName => tracing::debug!("UNIMPLEMENTED: complete -A hostname"),
CompleteAction::HostName => {
// N.B. We only retrieve one hostname.
if let Ok(name) = hostname::get() {
candidates.push(name.to_string_lossy().to_string());
}
}
CompleteAction::Job => tracing::debug!("UNIMPLEMENTED: complete -A job"),
CompleteAction::Keyword => tracing::debug!("UNIMPLEMENTED: complete -A keyword"),
CompleteAction::Keyword => {
for keyword in shell.get_keywords() {
candidates.push(keyword.clone());
}
}
CompleteAction::Running => tracing::debug!("UNIMPLEMENTED: complete -A running"),
CompleteAction::Service => tracing::debug!("UNIMPLEMENTED: complete -A service"),
CompleteAction::SetOpt => tracing::debug!("UNIMPLEMENTED: complete -A setopt"),
CompleteAction::ShOpt => tracing::debug!("UNIMPLEMENTED: complete -A shopt"),
CompleteAction::SetOpt => {
for (name, _) in namedoptions::SET_O_OPTIONS.iter() {
candidates.push((*name).to_owned());
}
}
CompleteAction::ShOpt => {
for (name, _) in namedoptions::SHOPT_OPTIONS.iter() {
candidates.push((*name).to_owned());
}
}
CompleteAction::Signal => tracing::debug!("UNIMPLEMENTED: complete -A signal"),
CompleteAction::Stopped => tracing::debug!("UNIMPLEMENTED: complete -A stopped"),
CompleteAction::User => tracing::debug!("UNIMPLEMENTED: complete -A user"),
CompleteAction::User => {
let mut names = users::get_all_users()?;
candidates.append(&mut names);
}
CompleteAction::Variable => {
for (key, _) in shell.env.iter() {
candidates.push(key.to_owned());
Expand All @@ -235,10 +275,18 @@ impl CompletionSpec {
}

if let Some(glob_pattern) = &self.glob_pattern {
tracing::debug!("UNIMPLEMENTED: complete -G({glob_pattern})");
let pattern = patterns::Pattern::from(glob_pattern.as_str());
let mut expansions = pattern.expand(
shell.working_dir.as_path(),
shell.parser_options().enable_extended_globbing,
Some(&patterns::Pattern::accept_all_expand_filter),
)?;

candidates.append(&mut expansions);
}
if let Some(word_list) = &self.word_list {
tracing::debug!("UNIMPLEMENTED: complete -W({word_list})");
let mut words = split_string_using_ifs(word_list, shell);
candidates.append(&mut words);
}
if let Some(function_name) = &self.function_name {
let call_result = self
Expand All @@ -262,7 +310,9 @@ impl CompletionSpec {

// Apply filter pattern, if present.
if let Some(filter_pattern) = &self.filter_pattern {
tracing::debug!("UNIMPLEMENTED: complete -X (filter pattern): {filter_pattern}");
if !filter_pattern.is_empty() {
tracing::debug!("UNIMPLEMENTED: complete -X (filter pattern): '{filter_pattern}'");
}
}

// Add prefix and/or suffix, if present.
Expand Down Expand Up @@ -535,15 +585,13 @@ fn get_file_completions(
) -> Vec<String> {
let glob = std::format!("{}*", context.token_to_complete);

let metadata_filter = |metadata: Option<&std::fs::Metadata>| {
!must_be_dir || metadata.is_some_and(|md| md.is_dir())
};
let path_filter = |path: &Path| !must_be_dir || path.is_dir();

// TODO: Pass through quoting.
if let Ok(mut candidates) = patterns::Pattern::from(glob).expand(
shell.working_dir.as_path(),
shell.options.extended_globbing,
Some(&metadata_filter),
Some(&path_filter),
) {
for candidate in &mut candidates {
if Path::new(candidate.as_str()).is_dir() {
Expand All @@ -556,6 +604,19 @@ fn get_file_completions(
}
}

fn get_command_completions(shell: &Shell, context: &CompletionContext) -> Vec<String> {
let mut candidates = HashSet::new();
let glob_pattern = std::format!("{}*", context.token_to_complete);

for path in shell.find_executables_in_path(&glob_pattern) {
if let Some(file_name) = path.file_name() {
candidates.insert(file_name.to_string_lossy().to_string());
}
}

candidates.into_iter().collect()
}

fn get_completions_using_basic_lookup(
shell: &Shell,
context: &CompletionContext,
Expand All @@ -571,21 +632,8 @@ fn get_completions_using_basic_lookup(
&& !context.token_to_complete.is_empty()
&& !context.token_to_complete.contains('/')
{
let glob_pattern = std::format!("{}*", context.token_to_complete);

for path in shell.find_executables_in_path(&glob_pattern) {
if let Some(file_name) = path.file_name() {
candidates.push(file_name.to_string_lossy().to_string());
}
}
}

if context.token_index + 1 >= context.tokens.len() {
for candidate in &mut candidates {
if !candidate.ends_with('/') {
candidate.push(' ');
}
}
let mut command_completions = get_command_completions(shell, context);
candidates.append(&mut command_completions);
}

#[cfg(windows)]
Expand All @@ -598,3 +646,11 @@ fn get_completions_using_basic_lookup(

CompletionResult::Candidates(candidates)
}

fn split_string_using_ifs<S: AsRef<str>>(s: S, shell: &Shell) -> Vec<String> {
let ifs_chars: Vec<char> = shell.get_ifs().chars().collect();
s.as_ref()
.split(ifs_chars.as_slice())
.map(|s| s.to_owned())
.collect()
}
Loading
Loading