Skip to content

Commit

Permalink
More compgen implementation (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno authored May 14, 2024
1 parent be1cfc7 commit 42f4e67
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 107 deletions.
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

0 comments on commit 42f4e67

Please sign in to comment.