1222 lines
39 KiB
Rust
1222 lines
39 KiB
Rust
//! Completion integration tests for brush shell.
|
|
|
|
// For now, only compile this for Linux.
|
|
#![cfg(target_os = "linux")]
|
|
#![cfg(test)]
|
|
#![allow(clippy::panic_in_result_fn)]
|
|
|
|
use anyhow::Result;
|
|
use assert_fs::prelude::*;
|
|
use brush_builtins::ShellBuilderExt;
|
|
use std::path::PathBuf;
|
|
|
|
/// Returns each candidate in `completions`.
|
|
fn candidate_texts(completions: &brush_core::completion::Completions) -> Vec<String> {
|
|
completions.candidates.clone()
|
|
}
|
|
|
|
/// Returns the range of the line that the only candidate in `completions` replaces.
|
|
fn replaced_range(
|
|
completions: &brush_core::completion::Completions,
|
|
) -> Result<std::ops::Range<usize>> {
|
|
match completions.candidates.as_slice() {
|
|
[_] => {
|
|
let start = completions.insertion_index;
|
|
Ok(start..start + completions.delete_count)
|
|
}
|
|
candidates => Err(anyhow::anyhow!(
|
|
"expected one candidate, got {candidates:?}"
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// A shell to complete in, with a temporary working directory.
|
|
struct TestShell {
|
|
shell: brush_core::Shell,
|
|
temp_dir: assert_fs::TempDir,
|
|
}
|
|
|
|
const DEFAULT_BASH_COMPLETION_SCRIPT: &str = "/usr/share/bash-completion/bash_completion";
|
|
|
|
impl TestShell {
|
|
/// Returns a shell with native completion only.
|
|
async fn new() -> Result<Self> {
|
|
let mut shell = brush_core::Shell::builder()
|
|
.profile(brush_core::ProfileLoadBehavior::Skip)
|
|
.rc(brush_core::RcLoadBehavior::Skip)
|
|
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
|
// Don't leave external commands running when a test drops a completion.
|
|
.kill_external_commands_on_drop(true)
|
|
.build()
|
|
.await?;
|
|
|
|
let temp_dir = assert_fs::TempDir::new()?;
|
|
shell.set_working_dir(temp_dir.path())?;
|
|
|
|
Ok(Self { shell, temp_dir })
|
|
}
|
|
|
|
/// Returns a shell with bash-completion loaded.
|
|
async fn with_bash_completion() -> Result<Self> {
|
|
let mut test_shell = Self::new().await?;
|
|
|
|
let exec_params = test_shell.shell.default_exec_params();
|
|
let source_result = test_shell
|
|
.shell
|
|
.source_script(
|
|
Self::find_bash_completion_script()?.as_path(),
|
|
std::iter::empty::<String>(),
|
|
&exec_params,
|
|
)
|
|
.await?;
|
|
if !source_result.is_success() {
|
|
return Err(anyhow::anyhow!("failed to source bash completion script"));
|
|
}
|
|
|
|
Ok(test_shell)
|
|
}
|
|
|
|
fn find_bash_completion_script() -> Result<PathBuf> {
|
|
// See if an environmental override was provided.
|
|
let script_path = std::env::var("BASH_COMPLETION_PATH").map_or_else(
|
|
|_| PathBuf::from(DEFAULT_BASH_COMPLETION_SCRIPT),
|
|
PathBuf::from,
|
|
);
|
|
|
|
if script_path.exists() {
|
|
Ok(script_path)
|
|
} else {
|
|
Err(anyhow::anyhow!(
|
|
"bash completion script not found: {}",
|
|
script_path.display()
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Completes `line` at `pos`.
|
|
async fn complete(
|
|
&mut self,
|
|
line: &str,
|
|
pos: usize,
|
|
) -> Result<brush_core::completion::Completions> {
|
|
Ok(self.shell.complete(line, pos).await?)
|
|
}
|
|
|
|
/// Completes `input` at its `|`, which marks the cursor (or at its end, if it has none).
|
|
async fn complete_at_marker(
|
|
&mut self,
|
|
input: &str,
|
|
) -> Result<brush_core::completion::Completions> {
|
|
let cursor = input.find('|').unwrap_or(input.len());
|
|
let line = input.replacen('|', "", 1);
|
|
self.complete(&line, cursor).await
|
|
}
|
|
|
|
/// Completes `line` at its end.
|
|
async fn complete_end_of_line_full(
|
|
&mut self,
|
|
line: &str,
|
|
) -> Result<brush_core::completion::Completions> {
|
|
self.complete(line, line.len()).await
|
|
}
|
|
|
|
/// Completes `line` at its end, returning the text of each candidate.
|
|
async fn complete_end_of_line(&mut self, line: &str) -> Result<Vec<String>> {
|
|
let completions = self.complete_end_of_line_full(line).await?;
|
|
Ok(candidate_texts(&completions))
|
|
}
|
|
|
|
async fn run(&mut self, script: &str) -> Result<()> {
|
|
let exec_params = self.shell.default_exec_params();
|
|
// Setup scripts may end in a failure on purpose (e.g. to set `$?`); their effects
|
|
// are what the tests check.
|
|
let _ = self
|
|
.shell
|
|
.run_string(
|
|
script.to_owned(),
|
|
&brush_core::SourceInfo::default(),
|
|
&exec_params,
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
fn get_var(&self, name: &str) -> Option<String> {
|
|
self.shell
|
|
.env()
|
|
.get(name)
|
|
.map(|(_, v)| v.value().to_cow_str(&self.shell).into_owned())
|
|
}
|
|
|
|
fn set_var(&mut self, name: &str, value: &str) -> Result<()> {
|
|
self.shell
|
|
.env_mut()
|
|
.set_global(name, brush_core::ShellVariable::new(value))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_relative_file_path() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
test_shell.temp_dir.child(".dot_item1").touch()?;
|
|
test_shell.temp_dir.child("..dot_item2").touch()?;
|
|
|
|
// Complete; expect to see the two files.
|
|
let mut results = test_shell.complete_end_of_line("ls item").await?;
|
|
|
|
assert_eq!(results, ["item1", "item2"]);
|
|
|
|
results = test_shell.complete_end_of_line("ls .").await?;
|
|
// Some versions of bash-completion filter out "." and ".." via
|
|
// `-X '?(*/)@(.|..)'`; others don't. Accept either outcome.
|
|
assert!(
|
|
results == ["..dot_item2", ".dot_item1"]
|
|
|| results == [".", "..", "..dot_item2", ".dot_item1"],
|
|
"unexpected completions for 'ls .': {results:?}"
|
|
);
|
|
|
|
results = test_shell.complete_end_of_line("ls ..").await?;
|
|
assert_eq!(results, ["..", "..dot_item2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_relative_file_path_ignoring_case() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
test_shell
|
|
.shell
|
|
.options_mut()
|
|
.case_insensitive_pathname_expansion = true;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("ITEM1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete; expect to see the two files.
|
|
let results = test_shell.complete_end_of_line("ls item").await?;
|
|
|
|
assert_eq!(results, ["ITEM1", "item2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_relative_dir_path() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete; expect to see just the dir.
|
|
let results = test_shell.complete_end_of_line("cd item").await?;
|
|
|
|
assert_eq!(results, ["item2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_under_empty_dir() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("empty").create_dir_all()?;
|
|
|
|
// Complete; expect to see nothing.
|
|
let results = test_shell.complete_end_of_line("ls empty/").await?;
|
|
|
|
assert_eq!(results, Vec::<String>::new());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_nonexistent_relative_path() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Complete; expect to see nothing.
|
|
let results = test_shell.complete_end_of_line("ls item").await?;
|
|
|
|
assert_eq!(results, Vec::<String>::new());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_absolute_paths() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete; expect to see just the dir.
|
|
let input = std::format!("ls {}", test_shell.temp_dir.path().join("item").display());
|
|
let results = test_shell.complete_end_of_line(input.as_str()).await?;
|
|
|
|
assert_eq!(
|
|
results,
|
|
[
|
|
test_shell
|
|
.temp_dir
|
|
.child("item1")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
test_shell
|
|
.temp_dir
|
|
.child("item2")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_path_with_var() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete; expect to see the two files.
|
|
let results = test_shell.complete_end_of_line("ls $PWD/item").await?;
|
|
|
|
assert_eq!(
|
|
results,
|
|
[
|
|
test_shell
|
|
.temp_dir
|
|
.child("item1")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
test_shell
|
|
.temp_dir
|
|
.child("item2")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_path_with_tilde() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Set HOME to the temp dir so we can use ~ to reference it.
|
|
test_shell.set_var(
|
|
"HOME",
|
|
test_shell
|
|
.temp_dir
|
|
.path()
|
|
.to_string_lossy()
|
|
.to_string()
|
|
.as_str(),
|
|
)?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete; expect to see the two files.
|
|
let results = test_shell.complete_end_of_line("ls ~/item").await?;
|
|
|
|
assert_eq!(
|
|
results,
|
|
[
|
|
test_shell
|
|
.temp_dir
|
|
.child("item1")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
test_shell
|
|
.temp_dir
|
|
.child("item2")
|
|
.path()
|
|
.display()
|
|
.to_string(),
|
|
]
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_variable_names() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Set a few vars.
|
|
test_shell.set_var("TESTVAR1", "")?;
|
|
test_shell.set_var("TESTVAR2", "")?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("echo $TESTVAR").await?;
|
|
assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_variable_names_with_braces() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Set a few vars.
|
|
test_shell.set_var("TESTVAR1", "")?;
|
|
test_shell.set_var("TESTVAR2", "")?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("echo ${TESTVAR").await?;
|
|
assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_help_topic() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("help expor").await?;
|
|
assert_eq!(results, ["export"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_command_option() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("ls --hel").await?;
|
|
assert_eq!(results, ["--help"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests completion with some well-known programs that have been good manual test cases
|
|
/// for us in the past.
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_path_args_to_well_known_programs() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Create file and dir.
|
|
test_shell.temp_dir.child("item1").touch()?;
|
|
test_shell.temp_dir.child("item2").create_dir_all()?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("tar tvf ./item").await?;
|
|
|
|
assert_eq!(results, ["./item2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests some 'find' completion.
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_find_command() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
// Complete.
|
|
let results = test_shell.complete_end_of_line("find . -na").await?;
|
|
|
|
assert_eq!(results, ["-name"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_quoted_filenames() -> Result<()> {
|
|
let mut test_shell = TestShell::with_bash_completion().await?;
|
|
|
|
test_shell.temp_dir.child("item1 item2").touch()?;
|
|
test_shell.temp_dir.child("item1'item2").touch()?;
|
|
|
|
let mut results = test_shell.complete_end_of_line("ls item1\\ ").await?;
|
|
assert_eq!(results, ["item1 item2"]);
|
|
|
|
results = test_shell.complete_end_of_line("ls item1'").await?;
|
|
assert_eq!(results, ["item1 item2", "item1'item2"]);
|
|
|
|
results = test_shell.complete_end_of_line("ls item1").await?;
|
|
assert_eq!(results, ["item1 item2", "item1'item2"]);
|
|
|
|
results = test_shell.complete_end_of_line("ls 'item1 ").await?;
|
|
assert_eq!(results, ["item1 item2"]);
|
|
|
|
results = test_shell.complete_end_of_line("ls \"item1 ").await?;
|
|
assert_eq!(results, ["item1 item2"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that interactive completion sets `COMP_KEY` and `COMP_TYPE` to 9 (TAB).
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn interactive_completion_sets_comp_key_and_comp_type() -> Result<()> {
|
|
let mut shell = brush_core::Shell::builder()
|
|
.profile(brush_core::ProfileLoadBehavior::Skip)
|
|
.rc(brush_core::RcLoadBehavior::Skip)
|
|
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
|
// Don't leave external commands running when a test drops a completion.
|
|
.kill_external_commands_on_drop(true)
|
|
.build()
|
|
.await?;
|
|
|
|
// Register a completion function that captures COMP_KEY and COMP_TYPE.
|
|
let exec_params = shell.default_exec_params();
|
|
let source_info = brush_core::SourceInfo::default();
|
|
let result = shell
|
|
.run_string(
|
|
r"
|
|
_test_comp() {
|
|
CAPTURED_COMP_KEY=$COMP_KEY
|
|
CAPTURED_COMP_TYPE=$COMP_TYPE
|
|
COMPREPLY=(done)
|
|
}
|
|
complete -F _test_comp mycmd
|
|
"
|
|
.to_string(),
|
|
&source_info,
|
|
&exec_params,
|
|
)
|
|
.await?;
|
|
anyhow::ensure!(result.is_success());
|
|
|
|
// Trigger interactive completion.
|
|
let _completions = shell.complete("mycmd ", 6).await?;
|
|
|
|
// Check the captured values.
|
|
let comp_key = shell
|
|
.env()
|
|
.get("CAPTURED_COMP_KEY")
|
|
.map(|(_, v)| v.value().to_cow_str(&shell).to_string());
|
|
let comp_type = shell
|
|
.env()
|
|
.get("CAPTURED_COMP_TYPE")
|
|
.map(|(_, v)| v.value().to_cow_str(&shell).to_string());
|
|
|
|
assert_eq!(comp_key.as_deref(), Some("9"), "COMP_KEY should be 9 (TAB)");
|
|
assert_eq!(
|
|
comp_type.as_deref(),
|
|
Some("9"),
|
|
"COMP_TYPE should be 9 (TAB)"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests completing right after a word-break char (e.g. `--opt=`): like readline, the
|
|
/// word being completed is the empty one after the `=`, even though `COMP_WORDS`
|
|
/// still sees `=` as its own word.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn complete_after_word_break_char() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run(
|
|
r#"
|
|
_test_comp() {
|
|
CAPTURED="[$2][$3][${COMP_WORDS[COMP_CWORD]}]"
|
|
COMPREPLY=(root)
|
|
}
|
|
complete -F _test_comp mycmd
|
|
complete -W "root" mycmd2
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
let line = "mycmd --owner=";
|
|
let completions = test_shell.complete(line, line.len()).await?;
|
|
assert_eq!(replaced_range(&completions)?, line.len()..line.len());
|
|
assert_eq!(candidate_texts(&completions), ["root"]);
|
|
|
|
assert_eq!(
|
|
test_shell.get_var("CAPTURED").as_deref(),
|
|
Some("[][--owner][=]")
|
|
);
|
|
|
|
let line = "mycmd2 a:";
|
|
let completions = test_shell.complete(line, line.len()).await?;
|
|
assert_eq!(replaced_range(&completions)?, line.len()..line.len());
|
|
assert_eq!(candidate_texts(&completions), ["root"]);
|
|
|
|
// Cursor inside a run of word-break chars: still the empty word at the cursor.
|
|
let line = "mycmd2 a:=val";
|
|
let cursor = "mycmd2 a:".len();
|
|
let completions = test_shell.complete(line, cursor).await?;
|
|
assert_eq!(replaced_range(&completions)?, cursor..cursor);
|
|
assert_eq!(candidate_texts(&completions), ["root"]);
|
|
|
|
let line = "mycmd --owner:=val";
|
|
let cursor = "mycmd --owner:".len();
|
|
test_shell.complete(line, cursor).await?;
|
|
assert_eq!(
|
|
test_shell.get_var("CAPTURED").as_deref(),
|
|
Some("[][--owner][:=]")
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Checks what a completion function sees for the word being completed (`$2`), the one
|
|
/// before it (`$3`), and `COMP_WORDS`/`COMP_CWORD`, for each input with the cursor at
|
|
/// its `|`.
|
|
async fn check_completion_function_args(cases: &[(&str, &str)]) -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run(
|
|
r#"
|
|
_test_comp() {
|
|
local IFS=,
|
|
CAPTURED="\$2=<$2> \$3=<$3> CWORD=$COMP_CWORD WORDS=<${COMP_WORDS[*]}>"
|
|
}
|
|
complete -F _test_comp mycmd
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
for (input, expected) in cases {
|
|
test_shell.set_var("CAPTURED", "")?;
|
|
test_shell.complete_at_marker(input).await?;
|
|
assert_eq!(
|
|
test_shell.get_var("CAPTURED").as_deref(),
|
|
Some(*expected),
|
|
"{input}"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Expected values in the tests below were captured from bash 5.3 by pressing Tab at the
|
|
// cursor.
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[allow(clippy::too_many_lines)]
|
|
async fn completion_function_args_match_bash() -> Result<()> {
|
|
check_completion_function_args(&[
|
|
("mycmd aa|", "$2=<aa> $3=<mycmd> CWORD=1 WORDS=<mycmd,aa>"),
|
|
("mycmd aa |", "$2=<> $3=<aa> CWORD=2 WORDS=<mycmd,aa,>"),
|
|
("mycmd a|b", "$2=<a> $3=<mycmd> CWORD=1 WORDS=<mycmd,ab>"),
|
|
("mycmd |aa", "$2=<> $3=<mycmd> CWORD=1 WORDS=<mycmd,aa>"),
|
|
(
|
|
"mycmd aa| bb",
|
|
"$2=<aa> $3=<mycmd> CWORD=1 WORDS=<mycmd,aa,bb>",
|
|
),
|
|
(
|
|
"mycmd aa | bb",
|
|
"$2=<> $3=<aa> CWORD=2 WORDS=<mycmd,aa,,bb>",
|
|
),
|
|
("mycmd aa |bb", "$2=<> $3=<aa> CWORD=2 WORDS=<mycmd,aa,bb>"),
|
|
(
|
|
"mycmd --owner=|",
|
|
"$2=<> $3=<--owner> CWORD=2 WORDS=<mycmd,--owner,=>",
|
|
),
|
|
(
|
|
"mycmd --owner=|val",
|
|
"$2=<> $3=<=> CWORD=3 WORDS=<mycmd,--owner,=,val>",
|
|
),
|
|
(
|
|
"mycmd --owner|=val",
|
|
"$2=<--owner> $3=<--owner> CWORD=2 WORDS=<mycmd,--owner,=,val>",
|
|
),
|
|
(
|
|
"mycmd a:|=val",
|
|
"$2=<> $3=<a> CWORD=2 WORDS=<mycmd,a,:=,val>",
|
|
),
|
|
("mycmd a:|", "$2=<> $3=<a> CWORD=2 WORDS=<mycmd,a,:>"),
|
|
(
|
|
"mycmd \"a b|",
|
|
"$2=<a b> $3=<mycmd> CWORD=1 WORDS=<mycmd,\"a b>",
|
|
),
|
|
(
|
|
"mycmd 'a b|",
|
|
"$2=<a b> $3=<mycmd> CWORD=1 WORDS=<mycmd,'a b>",
|
|
),
|
|
(
|
|
"mycmd a'b c|",
|
|
"$2=<b c> $3=<mycmd> CWORD=1 WORDS=<mycmd,a'b c>",
|
|
),
|
|
(
|
|
"mycmd 'a b'|",
|
|
"$2=<'a b'> $3=<mycmd> CWORD=1 WORDS=<mycmd,'a b'>",
|
|
),
|
|
(
|
|
"mycmd 'a b'c|",
|
|
"$2=<'a b'c> $3=<mycmd> CWORD=1 WORDS=<mycmd,'a b'c>",
|
|
),
|
|
(
|
|
r"mycmd a\ b|",
|
|
r"$2=<a\ b> $3=<mycmd> CWORD=1 WORDS=<mycmd,a\ b>",
|
|
),
|
|
(
|
|
"mycmd \"a:b|",
|
|
"$2=<a:b> $3=<mycmd> CWORD=1 WORDS=<mycmd,\"a:b>",
|
|
),
|
|
(
|
|
"mycmd x=\"a b|",
|
|
"$2=<a b> $3=<=> CWORD=3 WORDS=<mycmd,x,=,\"a b>",
|
|
),
|
|
(
|
|
"mycmd --opt='a b|",
|
|
"$2=<a b> $3=<=> CWORD=3 WORDS=<mycmd,--opt,=,'a b>",
|
|
),
|
|
(
|
|
r"mycmd a\:b|",
|
|
r"$2=<a\:b> $3=<mycmd> CWORD=1 WORDS=<mycmd,a\:b>",
|
|
),
|
|
(
|
|
r#"mycmd "a\"b c|"#,
|
|
r#"$2=<a\"b c> $3=<mycmd> CWORD=1 WORDS=<mycmd,"a\"b c>"#,
|
|
),
|
|
(
|
|
"mycmd 'a:b' c:|",
|
|
"$2=<> $3=<c> CWORD=3 WORDS=<mycmd,'a:b',c,:>",
|
|
),
|
|
(
|
|
"mycmd a\"b\"c:d|",
|
|
"$2=<d> $3=<:> CWORD=3 WORDS=<mycmd,a\"b\"c,:,d>",
|
|
),
|
|
("mycmd a@b|", "$2=<@b> $3=<@> CWORD=3 WORDS=<mycmd,a,@,b>"),
|
|
(
|
|
"mycmd a:@b|",
|
|
"$2=<@b> $3=<:@> CWORD=3 WORDS=<mycmd,a,:@,b>",
|
|
),
|
|
("mycmd a@|", "$2=<@> $3=<a> CWORD=2 WORDS=<mycmd,a,@>"),
|
|
(
|
|
r"mycmd a\@b|",
|
|
r"$2=<a\@b> $3=<mycmd> CWORD=1 WORDS=<mycmd,a\@b>",
|
|
),
|
|
(
|
|
"mycmd \"a@b|",
|
|
"$2=<a@b> $3=<mycmd> CWORD=1 WORDS=<mycmd,\"a@b>",
|
|
),
|
|
(
|
|
"mycmd $'a b|",
|
|
"$2=<a b> $3=<mycmd> CWORD=1 WORDS=<mycmd,$'a b>",
|
|
),
|
|
("mycmd a >b|", "$2=<b> $3=<>> CWORD=3 WORDS=<mycmd,a,>,b>"),
|
|
("mycmd a>b|", "$2=<b> $3=<>> CWORD=3 WORDS=<mycmd,a,>,b>"),
|
|
])
|
|
.await
|
|
}
|
|
|
|
/// Like bash, `COMP_WORDS` should keep a command or parameter substitution together as
|
|
/// one word, even though readline's word to complete (`$2`) doesn't.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[ignore = "TODO(completions): group $(...), ${...}, and `...` in COMP_WORDS"]
|
|
#[allow(
|
|
clippy::literal_string_with_formatting_args,
|
|
reason = "the inputs are shell syntax"
|
|
)]
|
|
async fn completion_function_args_match_bash_in_substitutions() -> Result<()> {
|
|
check_completion_function_args(&[
|
|
(
|
|
"mycmd $(a b|",
|
|
"$2=<b> $3=<mycmd> CWORD=1 WORDS=<mycmd,$(a b>",
|
|
),
|
|
(
|
|
"mycmd $(a b)|",
|
|
"$2=<b)> $3=<mycmd> CWORD=1 WORDS=<mycmd,$(a b)>",
|
|
),
|
|
(
|
|
"mycmd $(a b) c|",
|
|
"$2=<c> $3=<$(a b)> CWORD=2 WORDS=<mycmd,$(a b),c>",
|
|
),
|
|
(
|
|
"mycmd x$(a b)y|",
|
|
"$2=<b)y> $3=<mycmd> CWORD=1 WORDS=<mycmd,x$(a b)y>",
|
|
),
|
|
(
|
|
"mycmd ${a:b|",
|
|
"$2=<b> $3=<mycmd> CWORD=1 WORDS=<mycmd,${a:b>",
|
|
),
|
|
(
|
|
"mycmd ${a:b}|",
|
|
"$2=<b}> $3=<mycmd> CWORD=1 WORDS=<mycmd,${a:b}>",
|
|
),
|
|
(
|
|
"mycmd ${a:b} c|",
|
|
"$2=<c> $3=<${a:b}> CWORD=2 WORDS=<mycmd,${a:b},c>",
|
|
),
|
|
(
|
|
"mycmd `a b|",
|
|
"$2=<b> $3=<mycmd> CWORD=1 WORDS=<mycmd,`a b>",
|
|
),
|
|
])
|
|
.await
|
|
}
|
|
|
|
/// Like bash, completion should only see the simple command containing the cursor: after
|
|
/// the last command separator, and past any leading variable assignments.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[ignore = "TODO(completions): scope completion to the current simple command "]
|
|
async fn completion_function_args_match_bash_in_command_sequences() -> Result<()> {
|
|
check_completion_function_args(&[
|
|
(
|
|
"echo x; mycmd a|",
|
|
"$2=<a> $3=<mycmd> CWORD=1 WORDS=<mycmd,a>",
|
|
),
|
|
(
|
|
"echo x && mycmd a|",
|
|
"$2=<a> $3=<mycmd> CWORD=1 WORDS=<mycmd,a>",
|
|
),
|
|
("(mycmd a|", "$2=<a> $3=<mycmd> CWORD=1 WORDS=<mycmd,a>"),
|
|
("X=1 mycmd a|", "$2=<a> $3=<mycmd> CWORD=1 WORDS=<mycmd,a>"),
|
|
])
|
|
.await
|
|
}
|
|
|
|
/// The cursor position is a byte offset, which must be a char boundary in the line.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_position_must_be_char_boundary() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
assert!(test_shell.complete_end_of_line_full("ls é").await.is_ok());
|
|
// Inside the multi-byte `é`, or past the end of the line.
|
|
assert!(test_shell.complete("ls é", 4).await.is_err());
|
|
assert!(test_shell.complete("ls", 3).await.is_err());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests the range completions replace when the word being completed is in an open quote:
|
|
/// the candidates replace that quote too.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_range_in_open_quote() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell.run("complete -W 'ab' mycmd").await?;
|
|
|
|
for (line, start) in [
|
|
("mycmd a", 6),
|
|
("mycmd 'a", 6),
|
|
("mycmd --x=\"a", 10),
|
|
("mycmd x'a", 7),
|
|
] {
|
|
let completions = test_shell.complete_end_of_line_full(line).await?;
|
|
assert_eq!(replaced_range(&completions)?, start..line.len(), "{line}");
|
|
assert_eq!(candidate_texts(&completions), ["ab"], "{line}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, a completion command (`complete -C`) gets the `COMP_*` variables exported to
|
|
/// it, and they aren't left set afterwards. Running it doesn't change `$?`.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_command_gets_comp_vars() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
// The command exits 0 (its arguments go to `true`), unlike the `false` before it.
|
|
.run("complete -C 'printenv COMP_LINE COMP_POINT; true' cmd; false")
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(candidate_texts(&completions), ["5", "cmd x"]);
|
|
|
|
test_shell
|
|
.run("status=$?; [[ -v COMP_LINE ]] && line=set")
|
|
.await?;
|
|
assert_eq!(test_shell.get_var("status").as_deref(), Some("1"));
|
|
assert_eq!(test_shell.get_var("line"), None);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, a completion function's `COMP_*` variables aren't left set afterwards, but
|
|
/// what it leaves in other variables is.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_function_vars_are_not_left_set() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
.run(r#"_f() { seen="$COMP_LINE|${COMP_WORDS[*]}"; COMPREPLY=(xy); }; complete -F _f cmd"#)
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(candidate_texts(&completions), ["xy"]);
|
|
assert_eq!(test_shell.get_var("seen").as_deref(), Some("cmd x|cmd x"));
|
|
|
|
for var in [
|
|
"COMP_LINE",
|
|
"COMP_POINT",
|
|
"COMP_WORDS",
|
|
"COMP_CWORD",
|
|
"COMPREPLY",
|
|
] {
|
|
assert_eq!(test_shell.get_var(var), None, "{var}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, the empty-line spec (`complete -E`) applies when there's nothing before the
|
|
/// cursor and it isn't at the start of a word; otherwise, at the start of a line, the
|
|
/// initial-word spec (`complete -I`) does. Expected values were captured from bash 5.3.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn empty_line_spec_completes_empty_line() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run("complete -E -W empty; complete -I -W initial")
|
|
.await?;
|
|
|
|
for (line, cursor, expected) in [
|
|
("", 0, "empty"),
|
|
(" cmd", 0, "empty"),
|
|
(" ", 2, "initial"),
|
|
("cmd", 0, "initial"),
|
|
] {
|
|
let completions = test_shell.complete(line, cursor).await?;
|
|
assert_eq!(
|
|
candidate_texts(&completions),
|
|
[expected],
|
|
"{line:?} at {cursor}"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Completion gives up, with no candidates, if a completion function keeps asking for it to
|
|
/// restart.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_that_keeps_restarting_gives_up() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run("_restart() { return 124; }; complete -F _restart -D")
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd xy").await?;
|
|
assert_eq!(candidate_texts(&completions), [] as [String; 0]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, a completion function for a special spec gets bash's name for it as `$1`;
|
|
/// with no words on the line, `COMP_WORDS` is empty and `COMP_CWORD` is -1; and when
|
|
/// completing the first word, `$3` is that word. Tests each input with the cursor at its
|
|
/// `|`; expected values were captured from bash 5.3.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn special_spec_function_args_match_bash() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run(
|
|
r#"
|
|
_test_comp() {
|
|
local IFS=,
|
|
CAPTURED="\$1=<$1> \$2=<$2> \$3=<$3> CWORD=$COMP_CWORD NWORDS=${#COMP_WORDS[@]} WORDS=<${COMP_WORDS[*]}>"
|
|
}
|
|
complete -E -F _test_comp
|
|
complete -I -F _test_comp
|
|
complete -D -F _test_comp
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
for (input, expected) in [
|
|
(
|
|
"|",
|
|
"$1=<_EmptycmD_> $2=<> $3=<> CWORD=-1 NWORDS=0 WORDS=<>",
|
|
),
|
|
(
|
|
"| cmd",
|
|
"$1=<_EmptycmD_> $2=<> $3=<> CWORD=0 NWORDS=2 WORDS=<,cmd>",
|
|
),
|
|
(
|
|
" |",
|
|
"$1=<_InitialWorD_> $2=<> $3=<> CWORD=-1 NWORDS=0 WORDS=<>",
|
|
),
|
|
(
|
|
"cm|",
|
|
"$1=<_InitialWorD_> $2=<cm> $3=<cm> CWORD=0 NWORDS=1 WORDS=<cm>",
|
|
),
|
|
(
|
|
"|cmd",
|
|
"$1=<_InitialWorD_> $2=<> $3=<cmd> CWORD=0 NWORDS=1 WORDS=<cmd>",
|
|
),
|
|
(
|
|
"foo x|",
|
|
"$1=<foo> $2=<x> $3=<foo> CWORD=1 NWORDS=2 WORDS=<foo,x>",
|
|
),
|
|
] {
|
|
test_shell.set_var("CAPTURED", "")?;
|
|
test_shell.complete_at_marker(input).await?;
|
|
assert_eq!(
|
|
test_shell.get_var("CAPTURED").as_deref(),
|
|
Some(expected),
|
|
"{input}"
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, the `file` action makes a completion's candidates file names, and so does the
|
|
/// `directory` action if it finds any. Expected values were captured from bash 5.3.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn file_and_directory_actions_complete_file_names() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell.temp_dir.child("a b").touch()?;
|
|
test_shell.temp_dir.child("dir x").create_dir_all()?;
|
|
test_shell
|
|
.run(
|
|
r"complete -f filecmd; complete -d dircmd
|
|
complete -W 'q\ r' -f wfilecmd; complete -W 'q\ r' -d wdircmd",
|
|
)
|
|
.await?;
|
|
|
|
for (line, expected, file_names) in [
|
|
("filecmd a", "a b", true),
|
|
("dircmd di", "dir x", true),
|
|
// Even with no file names found, `-f` makes the other candidates file names...
|
|
("wfilecmd q", "q r", true),
|
|
// ...but `-d` doesn't, if it finds no directories.
|
|
("wdircmd q", "q r", false),
|
|
] {
|
|
let completions = test_shell.complete_end_of_line_full(line).await?;
|
|
assert_eq!(candidate_texts(&completions), [expected], "{line}");
|
|
assert_eq!(completions.options.treat_as_filenames, file_names, "{line}");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, `compopt` changes the options of the completion in progress, which a
|
|
/// `compgen` call from the completion function doesn't disturb.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn compopt_survives_compgen_in_completion_function() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
.run(
|
|
"_f() { compopt -o nospace; compgen -W x >/dev/null; COMPREPLY=(xy); }; complete -F _f cmd",
|
|
)
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(completions.candidates, ["xy"]);
|
|
assert!(completions.options.no_trailing_space_at_end_of_line);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, a subshell of a completion function is in the completion too (so `compopt`
|
|
/// succeeds there), but `compopt` there changes only the subshell's options, not the
|
|
/// completion's.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn compopt_in_subshell_does_not_change_completion() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
.run("_f() { (compopt -o nospace) && COMPREPLY=(xy); }; complete -F _f cmd")
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(completions.candidates, ["xy"]);
|
|
assert!(!completions.options.no_trailing_space_at_end_of_line);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// A completion that's cancelled (e.g. by Ctrl-C) while its completion function runs leaves
|
|
/// nothing of it behind: the function's call, its variables (and the `COMP_*` ones set for
|
|
/// it), the block on traps, or the completion in progress (so `compopt` acts as it does
|
|
/// outside one).
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn cancelled_completion_leaves_nothing_behind() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
test_shell
|
|
.run("_slow() { local x=1; FOO=1 _block; }; _block() { sleep 10 >/dev/null 2>&1; }; complete -F _slow mycmd")
|
|
.await?;
|
|
|
|
let completion = test_shell.complete_end_of_line_full("mycmd x");
|
|
let result = tokio::time::timeout(std::time::Duration::from_millis(500), completion).await;
|
|
assert!(result.is_err(), "the completion should have been cancelled");
|
|
|
|
let shell = &test_shell.shell;
|
|
assert!(!shell.in_function());
|
|
assert!(shell.env_str("x").is_none());
|
|
assert!(shell.env_str("FOO").is_none());
|
|
|
|
test_shell.run("compopt -o nospace; rc=$?").await?;
|
|
assert_eq!(test_shell.get_var("rc").as_deref(), Some("1"));
|
|
|
|
test_shell
|
|
.run("leaked=0; [[ -v COMP_LINE ]] && leaked=1")
|
|
.await?;
|
|
assert_eq!(test_shell.get_var("leaked").as_deref(), Some("0"));
|
|
assert!(!test_shell.shell.call_stack().is_trap_delivery_suppressed());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests native variable completion without braces (e.g., $VAR)
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn native_complete_variable_names() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
// Set test variables.
|
|
test_shell.set_var("TESTVAR1", "value1")?;
|
|
test_shell.set_var("TESTVAR2", "value2")?;
|
|
|
|
// Complete.
|
|
let completions = test_shell
|
|
.complete_end_of_line_full("echo $TESTVAR")
|
|
.await?;
|
|
let results: Vec<String> = completions.candidates.into_iter().collect();
|
|
assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);
|
|
|
|
// Variable completions should not be treated as filenames (to avoid escaping $)
|
|
assert!(
|
|
!completions.options.treat_as_filenames,
|
|
"variable completions should not be treated as filenames"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests native variable completion with braces (e.g., ${VAR})
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn native_complete_variable_names_with_braces() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
// Set test variables.
|
|
test_shell.set_var("TESTVAR1", "value1")?;
|
|
test_shell.set_var("TESTVAR2", "value2")?;
|
|
|
|
// Complete.
|
|
let completions = test_shell
|
|
.complete_end_of_line_full("echo ${TESTVAR")
|
|
.await?;
|
|
let results: Vec<String> = completions.candidates.into_iter().collect();
|
|
assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);
|
|
|
|
// Variable completions should not be treated as filenames (to avoid escaping $)
|
|
assert!(
|
|
!completions.options.treat_as_filenames,
|
|
"variable completions should not be treated as filenames"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that file completion works after a variable (e.g., $VAR/path)
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn native_complete_path_after_variable() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
// Create files in temp dir.
|
|
test_shell.temp_dir.child("file1.txt").touch()?;
|
|
test_shell.temp_dir.child("file2.txt").touch()?;
|
|
|
|
// Set a variable pointing to the temp dir.
|
|
let temp_path = test_shell.temp_dir.path().to_str().unwrap().to_owned();
|
|
test_shell.set_var("MYDIR", &temp_path)?;
|
|
|
|
// Complete files after variable expansion.
|
|
// The completion system expands variables before completing, so results use expanded paths.
|
|
let completions = test_shell
|
|
.complete_end_of_line_full("ls $MYDIR/file")
|
|
.await?;
|
|
let results: Vec<String> = completions.candidates.into_iter().collect();
|
|
let expected = [
|
|
std::format!("{temp_path}/file1.txt"),
|
|
std::format!("{temp_path}/file2.txt"),
|
|
];
|
|
assert_eq!(results, expected);
|
|
|
|
// Path completions should be treated as filenames
|
|
assert!(
|
|
completions.options.treat_as_filenames,
|
|
"path completions should be treated as filenames"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, `compopt -o nosort` in a completion function keeps the candidates in the order
|
|
/// generated.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn compopt_nosort_applies_to_completion_in_progress() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
.run("_f() { compopt -o nosort; COMPREPLY=(xb xa xc); }; complete -F _f cmd")
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(completions.candidates, ["xb", "xa", "xc"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// A `compgen` call in a completion function doesn't leak its options (e.g. its own
|
|
/// `nosort`) into the completion: like bash, the candidates are still sorted.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn compgen_in_completion_function_keeps_sorting() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
test_shell
|
|
.run("_f() { compgen -W x >/dev/null; COMPREPLY=(xb xa); }; complete -F _f cmd")
|
|
.await?;
|
|
|
|
let completions = test_shell.complete_end_of_line_full("cmd x").await?;
|
|
assert_eq!(completions.candidates, ["xa", "xb"]);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Like bash, a `complete -X` pattern's leading `!` is interpreted when completing, not
|
|
/// when the spec is registered: with extglob on by then, `!(...)` is an extglob pattern.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn completion_filter_is_interpreted_when_completing() -> Result<()> {
|
|
let mut test_shell = TestShell::new().await?;
|
|
|
|
let exec_params = test_shell.shell.default_exec_params();
|
|
let result = test_shell
|
|
.shell
|
|
.run_string(
|
|
"shopt -u extglob; complete -W 'foo bar fab' -X '!(f*)' mycmd; shopt -s extglob"
|
|
.to_owned(),
|
|
&brush_core::SourceInfo::default(),
|
|
&exec_params,
|
|
)
|
|
.await?;
|
|
anyhow::ensure!(result.is_success());
|
|
|
|
// `!(f*)` matches what doesn't start with `f`, so those are removed.
|
|
let completions = test_shell.complete_end_of_line_full("mycmd ").await?;
|
|
assert_eq!(completions.candidates, ["fab", "foo"]);
|
|
|
|
Ok(())
|
|
}
|