Skip to content

Commit

Permalink
Finalize completion command
Browse files Browse the repository at this point in the history
  • Loading branch information
cnpryer committed Oct 31, 2022
1 parent e70f6f3 commit bed5b3c
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 149 deletions.
190 changes: 127 additions & 63 deletions src/bin/huak/commands/config/completion.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,91 @@
use std::fs::File;
use std::io::{Error, Write};
use std::path::Path;
use std::process::ExitCode;

use clap::Command;
use clap::{Command, CommandFactory};
use clap_complete::{generate, Shell};
use huak::errors::HuakError;

use crate::commands::Cli;
use crate::errors::{CliError, CliResult};

/// Prints the script to stdout and a way to add the script to the shell init file to stderr. This
/// way if the user runs completion <shell> > completion.sh only the stdout will be redirected into
/// completion.sh.
pub fn run(
shell: Option<Shell>,
install: bool,
uninstall: bool,
) -> CliResult<()> {
if (install || uninstall) && shell.is_none() {
return Err(CliError::new(
HuakError::ConfigurationError("No shell provided".to_string()),
ExitCode::FAILURE,
));
}
if install {
run_with_install(shell)?;
} else if uninstall {
run_with_uninstall(shell)?;
} else {
generate_shell_completion_script()
}
Ok(())
}

fn generate_shell_completion_script() {
let mut cmd = Cli::command();

generate(Shell::Bash, &mut cmd, "huak", &mut std::io::stdout())
}

fn run_with_install(shell: Option<Shell>) -> CliResult<()> {
let err = Err(CliError::new(
HuakError::ConfigurationError("Invalid shell".to_string()),
ExitCode::FAILURE,
));
let sh = match shell {
Some(it) => it,
None => return err,
};
let mut cmd: Command = Cli::command();
match sh {
Shell::Bash => add_completion_bash(),
Shell::Elvish => add_completion_elvish(),
Shell::Fish => add_completion_fish(&mut cmd),
Shell::PowerShell => add_completion_powershell(),
Shell::Zsh => add_completion_zsh(&mut cmd),
_ => {
return err;
}
}?;

Ok(())
}

fn run_with_uninstall(shell: Option<Shell>) -> CliResult<()> {
let err = Err(CliError::new(
HuakError::ConfigurationError("Invalid shell".to_string()),
ExitCode::FAILURE,
));
let sh = match shell {
Some(it) => it,
None => return err,
};
match sh {
Shell::Bash => remove_completion_bash(),
Shell::Elvish => remove_completion_elvish(),
Shell::Fish => remove_completion_fish(),
Shell::PowerShell => remove_completion_powershell(),
Shell::Zsh => remove_completion_zsh(),
_ => {
return err;
}
}?;

Ok(())
}

/// Bash has a couple of files that can contain the actual completion script.
/// Only the line `eval "$(huak config completion bash)"` needs to be added
Expand All @@ -12,7 +94,7 @@ use clap_complete::{generate, Shell};
/// ~/.bash_login
/// ~/.profile
/// ~/.bashrc
pub fn add_completion_bash() -> Result<(), Error> {
pub fn add_completion_bash() -> CliResult<()> {
let home = match std::env::var("HOME") {
Ok(dir) => dir,
Err(e) => {
Expand All @@ -22,13 +104,10 @@ pub fn add_completion_bash() -> Result<(), Error> {
}
};

let _file_path = format!("{}/.bashrc", home);

#[cfg(test)]
let _file_path = format!("test_files/test_bashrc");
let file_path = format!("{}/.bashrc", home);

// opening file in append mode
let mut file: File = File::options().append(true).open(_file_path)?;
let mut file = File::options().append(true).open(file_path)?;

// This needs to be a string since there will be a \n prepended if it is
file.write_all(
Expand All @@ -40,114 +119,87 @@ pub fn add_completion_bash() -> Result<(), Error> {
}

// TODO
pub fn add_completion_elvish() -> Result<(), Error> {
pub fn add_completion_elvish() -> CliResult<()> {
todo!()
}

/// huak config completion fish > ~/.config/fish/completions/huak.fish
/// Fish has a completions directory in which all files are loaded on init.
/// The naming convention is $HOME/.config/fish/completions/huak.fish
pub fn add_completion_fish(cli: &mut Command) -> Result<(), Error> {
pub fn add_completion_fish(cli: &mut Command) -> CliResult<()> {
let home = match std::env::var("HOME") {
Ok(dir) => dir,
Err(e) => {
// defaulting to root, this might not be the right call
eprintln!("{}", e);
String::from("root")
}
Err(e) => return Err(CliError::from(e)),
};

let _target_file = format!("{}/.config/fish/completions/huak.fish", home);

#[cfg(test)]
let _target_file = "test_files/test_fish".to_string();
let target_file = format!("{}/.config/fish/completions/huak.fish", home);

generate_target_file(_target_file, cli)?;
generate_target_file(target_file, cli)?;
Ok(())
}

// TODO
pub fn add_completion_powershell() -> Result<(), Error> {
pub fn add_completion_powershell() -> CliResult<()> {
todo!()
}

/// Zsh and fish are the same in the sense that the use an entire directory to collect shell init
/// scripts.
pub fn add_completion_zsh(cli: &mut Command) -> Result<(), Error> {
let _target_file = "/usr/local/share/zsh/site-functions/_huak".to_string();

#[cfg(test)]
let _target_file = "test_files/test_zsh".to_string();
pub fn add_completion_zsh(cli: &mut Command) -> CliResult<()> {
let target_file = "/usr/local/share/zsh/site-functions/_huak".to_string();

generate_target_file(_target_file, cli)?;
generate_target_file(target_file, cli)?;
Ok(())
}

/// Reads the entire file and removes lines that match exactly with:
/// \neval "$(huak config completion)
pub fn remove_completion_bash() -> Result<(), Error> {
pub fn remove_completion_bash() -> CliResult<()> {
let home = match std::env::var("HOME") {
Ok(dir) => dir,
Err(e) => {
// defaulting to root, this might not be the right call
eprintln!("{}", e);
String::from("root")
}
Err(e) => return Err(CliError::from(e)),
};

let _file_path = format!("{}/.bashrc", home);
let file_path = format!("{}/.bashrc", home);

#[cfg(test)]
let _file_path = format!("test_files/test_bashrc");

let file_content = std::fs::read_to_string(&_file_path)?;
let file_content = std::fs::read_to_string(&file_path)?;
let new_content = file_content.replace(
&format!(r##"{}eval "$(huak config completion)"{}"##, '\n', '\n'),
"",
);

std::fs::write(&_file_path, new_content)?;
std::fs::write(&file_path, new_content)?;

Ok(())
}

// TODO
pub fn remove_completion_elvish() -> Result<(), Error> {
todo!()
pub fn remove_completion_elvish() -> CliResult<()> {
unimplemented!()
}

pub fn remove_completion_fish() -> Result<(), Error> {
pub fn remove_completion_fish() -> CliResult<()> {
let home = match std::env::var("HOME") {
Ok(dir) => dir,
Err(e) => {
// defaulting to root, this might not be the right call
eprintln!("{}", e);
String::from("root")
}
Err(e) => return Err(CliError::from(e)),
};

let _target_file = format!("{}/.config/fish/completions/huak.fish", home);

#[cfg(test)]
let _target_file = "test_files/test_fish".to_string();
let target_file = format!("{}/.config/fish/completions/huak.fish", home);

std::fs::remove_file(_target_file)?;
std::fs::remove_file(target_file)?;

Ok(())
}

// TODO
pub fn remove_completion_powershell() -> Result<(), Error> {
todo!()
pub fn remove_completion_powershell() -> CliResult<()> {
unimplemented!()
}

pub fn remove_completion_zsh() -> Result<(), Error> {
let _target_file = "/usr/local/share/zsh/site-functions/_huak".to_string();

#[cfg(test)]
let _target_file = "test_files/test_zsh".to_string();
pub fn remove_completion_zsh() -> CliResult<()> {
let target_file = "/usr/local/share/zsh/site-functions/_huak".to_string();

std::fs::remove_file(_target_file)?;
std::fs::remove_file(target_file)?;

Ok(())
}
Expand All @@ -159,13 +211,16 @@ fn generate_target_file<P>(
where
P: AsRef<Path>,
{
let mut file: File = File::create(&target_file)?;
let mut file = File::create(&target_file)?;

generate(Shell::Fish, cmd, "huak", &mut file);

Ok(())
}

// TODO:
// - Use tempdir and mocking for testing these features.
// - Requires refactors of functions and their signatures.
#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -176,14 +231,16 @@ mod tests {
#[derive(Parser)]
struct Cli {}

#[cfg(target = "linux")]
#[cfg(target_family = "unix")]
#[ignore = "incomplete test"] // See TODO
#[test]
/// This test ensures the order of operations is always correct
fn test_bash_completion() {
test_adding_completion_bash();
test_remove_completion_bash();
}

#[cfg(target_family = "unix")]
fn test_adding_completion_bash() {
let _ = add_completion_bash();

Expand All @@ -203,6 +260,7 @@ eval "$(huak config completion)"
)
}

#[cfg(target_family = "unix")]
fn test_remove_completion_bash() {
let _ = remove_completion_bash();

Expand All @@ -215,7 +273,8 @@ eval "$(huak config completion)"
", file_content)
}

#[cfg(target = "linux")]
#[cfg(target_family = "unix")]
#[ignore = "incomplete test"] // See TODO
#[test]
/// This test ensures the order of operations is always correct
fn test_fish_completion() {
Expand All @@ -225,6 +284,7 @@ eval "$(huak config completion)"
test_remove_completion_fish();
}

#[cfg(target_family = "unix")]
fn test_adding_completion_fish(cmd: &mut Command) {
let _ = add_completion_fish(cmd);

Expand All @@ -233,14 +293,16 @@ eval "$(huak config completion)"
assert_eq!(true, result.is_ok());
}

#[cfg(target_family = "unix")]
fn test_remove_completion_fish() {
let _ = remove_completion_fish();

let result = std::fs::read("test_files/test_fish");
assert_eq!(true, result.is_err());
}

#[cfg(target = "linux")]
#[cfg(target_family = "unix")]
#[ignore = "incomplete test"] // See TODO
#[test]
/// This test ensures the order of operations is always correct
fn test_zsh_completion() {
Expand All @@ -250,6 +312,7 @@ eval "$(huak config completion)"
test_remove_completion_zsh();
}

#[cfg(target_family = "unix")]
fn test_adding_completion_zsh(cmd: &mut Command) {
let _ = add_completion_zsh(cmd);

Expand All @@ -258,6 +321,7 @@ eval "$(huak config completion)"
assert_eq!(true, result.is_ok());
}

#[cfg(target_family = "unix")]
fn test_remove_completion_zsh() {
let _ = remove_completion_zsh();

Expand Down
Loading

0 comments on commit bed5b3c

Please sign in to comment.