Merge pull request #259 from mrVanDalo/feature/allow-extra

add `--allow-extra` option
This commit is contained in:
Denis Isidoro 2020-03-14 15:21:55 -03:00 committed by GitHub
commit b809739113
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 239 additions and 73 deletions

View file

@ -176,7 +176,13 @@ $ y: echo -e "$((x+10))\n$((x+20))"
### Variable options
For lines starting with `$` you can add extra options using `---`.
For lines starting with `$` you can add use`---` to parse parameters to `fzf`.
* `--allow-extra` *(experimental)*: handles `fzf` option `--print-query`. `enter` will prefer a selection,
`tab` will prefer the query typed.
* `--multi` : forwarded option to `fzf`.
* `--header-lines` : forwarded option to `fzf`
* `--column` : forwarded option to `fzf`.
* `--delimiter` : forwarded option to `fzf`.
#### Table formatting

View file

@ -7,14 +7,29 @@ use std::collections::HashMap;
use std::fs;
use std::io::Write;
#[derive(Debug, PartialEq)]
pub struct SuggestionOpts {
pub header_lines: u8,
pub column: Option<u8>,
pub multi: bool,
pub delimiter: Option<String>,
pub suggestion_type: SuggestionType,
}
pub type Value = (String, Option<SuggestionOpts>);
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum SuggestionType {
/// fzf will not print any suggestions.
Disabled,
/// fzf will only select one of the suggestions
SingleSelection,
/// fzf will select multiple ones of the suggestions
MultipleSelections,
/// fzf will select one of the suggestions or use the Query
SingleRecommendation,
/// initial snippet selection
SnippetSelection,
}
pub type Suggestion = (String, Option<SuggestionOpts>);
fn gen_snippet(snippet: &str, line: &str) -> String {
if snippet.is_empty() {
@ -32,6 +47,7 @@ fn parse_opts(text: &str) -> SuggestionOpts {
let mut header_lines: u8 = 0;
let mut column: Option<u8> = None;
let mut multi = false;
let mut allow_extra = false;
let mut delimiter: Option<String> = None;
let mut parts = text.split(' ');
@ -39,6 +55,7 @@ fn parse_opts(text: &str) -> SuggestionOpts {
while let Some(p) = parts.next() {
match p {
"--multi" => multi = true,
"--allow-extra" => allow_extra = true,
"--header" | "--headers" | "--header-lines" => {
header_lines = remove_quote(parts.next().unwrap()).parse::<u8>().unwrap()
}
@ -51,8 +68,12 @@ fn parse_opts(text: &str) -> SuggestionOpts {
SuggestionOpts {
header_lines,
column,
multi,
delimiter,
suggestion_type: match (multi, allow_extra) {
(true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra
(false, true) => SuggestionType::SingleRecommendation,
(false, false) => SuggestionType::SingleSelection,
},
}
}
@ -62,16 +83,13 @@ fn parse_variable_line(line: &str) -> (&str, &str, Option<SuggestionOpts>) {
let variable = caps.get(1).unwrap().as_str().trim();
let mut command_plus_opts = caps.get(2).unwrap().as_str().split("---");
let command = command_plus_opts.next().unwrap();
let opts = match command_plus_opts.next() {
Some(o) => Some(parse_opts(o)),
None => None,
};
(variable, command, opts)
let command_options = command_plus_opts.next().map(parse_opts);
(variable, command, command_options)
}
fn read_file(
path: &str,
variables: &mut HashMap<String, Value>,
variables: &mut HashMap<String, Suggestion>,
stdin: &mut std::process::ChildStdin,
) {
let mut tags = String::from("");
@ -97,7 +115,7 @@ fn read_file(
}
// variable
else if line.starts_with('$') {
let (variable, command, opts) = parse_variable_line(&line[..]);
let (variable, command, opts) = parse_variable_line(&line);
variables.insert(
format!("{};{}", tags, variable),
(String::from(command), opts),
@ -135,8 +153,11 @@ fn read_file(
}
}
pub fn read_all(config: &Config, stdin: &mut std::process::ChildStdin) -> HashMap<String, Value> {
let mut variables: HashMap<String, Value> = HashMap::new();
pub fn read_all(
config: &Config,
stdin: &mut std::process::ChildStdin,
) -> HashMap<String, Suggestion> {
let mut variables: HashMap<String, Suggestion> = HashMap::new();
let mut fallback: String = String::from("");
let folders_str = config.path.as_ref().unwrap_or_else(|| {
@ -161,3 +182,49 @@ pub fn read_all(config: &Config, stdin: &mut std::process::ChildStdin) -> HashMa
variables
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_variable_line() {
let (variable, command, command_options) =
parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --allow-extra");
assert_eq!(command, " echo -e \"$(whoami)\\nroot\" ");
assert_eq!(variable, "user");
assert_eq!(
command_options,
Some(SuggestionOpts {
header_lines: 0,
column: None,
delimiter: None,
suggestion_type: SuggestionType::SingleRecommendation
})
);
}
use std::process::{Command, Stdio};
#[test]
fn test_read_file() {
let path = "tests/cheats/ssh.cheat";
let mut variables: HashMap<String, Suggestion> = HashMap::new();
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let child_stdin = child.stdin.as_mut().unwrap();
read_file(path, &mut variables, child_stdin);
let mut result: HashMap<String, (String, std::option::Option<_>)> = HashMap::new();
result.insert(
"ssh;user".to_string(),
(
r#" echo -e "$(whoami)\nroot" "#.to_string(),
Some(SuggestionOpts {
header_lines: 0,
column: None,
delimiter: None,
suggestion_type: SuggestionType::SingleRecommendation,
}),
),
);
assert_eq!(variables, result);
}
}

View file

@ -4,6 +4,7 @@ use crate::display;
use crate::fzf;
use crate::option::Config;
use crate::cheat::SuggestionType;
use regex::Regex;
use std::collections::HashMap;
use std::error::Error;
@ -22,7 +23,7 @@ fn gen_core_fzf_opts(variant: Variant, config: &Config) -> fzf::Opts {
preview: !config.no_preview,
autoselect: !config.no_autoselect,
overrides: config.fzf_overrides.as_ref(),
copyable: true,
suggestion_type: SuggestionType::SnippetSelection,
..Default::default()
};
@ -35,8 +36,8 @@ fn gen_core_fzf_opts(variant: Variant, config: &Config) -> fzf::Opts {
opts
}
fn extract_from_selections(raw_output: &str, contains_key: bool) -> (&str, &str, &str) {
let mut lines = raw_output.split('\n');
fn extract_from_selections(raw_snippet: &str, contains_key: bool) -> (&str, &str, &str) {
let mut lines = raw_snippet.split('\n');
let key = if contains_key {
lines.next().unwrap()
} else {
@ -58,20 +59,20 @@ fn extract_from_selections(raw_output: &str, contains_key: bool) -> (&str, &str,
fn prompt_with_suggestions(
varname: &str,
config: &Config,
suggestion: &cheat::Value,
suggestion: &cheat::Suggestion,
values: &HashMap<String, String>,
) -> String {
let mut vars_cmd = String::from("");
for (k, v) in values.iter() {
vars_cmd.push_str(format!("{}=\"{}\"; ", k, v).as_str());
for (key, value) in values.iter() {
vars_cmd.push_str(format!("{}=\"{}\"; ", key, value).as_str());
}
let cmd = format!("{vars} {cmd}", vars = vars_cmd, cmd = &suggestion.0);
let (suggestion_command, suggestion_options) = &suggestion;
let command = format!("{} {}", vars_cmd, suggestion_command);
let child = Command::new("bash")
.stdout(Stdio::piped())
.arg("-c")
.arg(cmd)
.arg(command)
.spawn()
.unwrap();
@ -88,8 +89,8 @@ fn prompt_with_suggestions(
let mut column: Option<u8> = None;
let mut delimiter = r"\s\s+";
if let Some(o) = &suggestion.1 {
opts.multi = o.multi;
if let Some(o) = &suggestion_options {
opts.suggestion_type = o.suggestion_type;
opts.header_lines = o.header_lines;
column = o.column;
if let Some(d) = o.delimiter.as_ref() {
@ -114,12 +115,12 @@ fn prompt_with_suggestions(
}
}
fn prompt_without_suggestions(varname: &str) -> String {
fn prompt_without_suggestions(variable_name: &str) -> String {
let opts = fzf::Opts {
preview: false,
autoselect: false,
suggestions: false,
prompt: Some(display::variable_prompt(varname)),
prompt: Some(display::variable_prompt(variable_name)),
suggestion_type: SuggestionType::Disabled,
..Default::default()
};
@ -139,29 +140,33 @@ fn gen_replacement(value: &str) -> String {
fn replace_variables_from_snippet(
snippet: &str,
tags: &str,
variables: HashMap<String, cheat::Value>,
variables: HashMap<String, cheat::Suggestion>,
config: &Config,
) -> String {
let mut interpolated_snippet = String::from(snippet);
let mut values: HashMap<String, String> = HashMap::new();
let re = Regex::new(r"<(\w[\w\d\-_]*)>").unwrap();
for cap in re.captures_iter(snippet) {
let bracketed_varname = &cap[0];
let varname = &bracketed_varname[1..bracketed_varname.len() - 1];
for captures in re.captures_iter(snippet) {
let bracketed_variable_name = &captures[0];
let variable_name = &bracketed_variable_name[1..bracketed_variable_name.len() - 1];
if values.get(varname).is_none() {
let k = format!("{};{}", tags, varname);
if values.get(variable_name).is_none() {
let key = format!("{};{}", tags, variable_name);
let value = match variables.get(&k[..]) {
Some(suggestion) => prompt_with_suggestions(varname, &config, suggestion, &values),
None => prompt_without_suggestions(varname),
let value = match variables.get(&key[..]) {
Some(suggestion) => {
prompt_with_suggestions(variable_name, &config, suggestion, &values)
}
None => prompt_without_suggestions(variable_name),
};
values.insert(varname.to_string(), value.clone());
values.insert(variable_name.to_string(), value.clone());
interpolated_snippet = interpolated_snippet
.replace(bracketed_varname, gen_replacement(&value[..]).as_str());
interpolated_snippet = interpolated_snippet.replace(
bracketed_variable_name,
gen_replacement(&value[..]).as_str(),
);
}
}
@ -171,11 +176,11 @@ fn replace_variables_from_snippet(
pub fn main(variant: Variant, config: Config, contains_key: bool) -> Result<(), Box<dyn Error>> {
let _ = display::WIDTHS;
let (raw_output, variables) = fzf::call(gen_core_fzf_opts(variant, &config), |stdin| {
let (raw_selection, variables) = fzf::call(gen_core_fzf_opts(variant, &config), |stdin| {
Some(cheat::read_all(&config, stdin))
});
let (key, tags, snippet) = extract_from_selections(&raw_output[..], contains_key);
let (key, tags, snippet) = extract_from_selections(&raw_selection[..], contains_key);
let interpolated_snippet =
replace_variables_from_snippet(snippet, tags, variables.unwrap(), &config);

View file

@ -2,6 +2,8 @@ use crate::cheat;
use crate::display;
use crate::filesystem;
use crate::cheat::SuggestionType;
use crate::cheat::SuggestionType::SingleSelection;
use std::collections::HashMap;
use std::process;
use std::process::{Command, Stdio};
@ -14,9 +16,7 @@ pub struct Opts<'a> {
pub autoselect: bool,
pub overrides: Option<&'a String>, // TODO: remove &'a
pub header_lines: u8,
pub multi: bool,
pub copyable: bool,
pub suggestions: bool,
pub suggestion_type: SuggestionType,
}
impl Default for Opts<'_> {
@ -29,20 +29,18 @@ impl Default for Opts<'_> {
overrides: None,
header_lines: 0,
prompt: None,
multi: false,
copyable: false,
suggestions: true,
suggestion_type: SingleSelection,
}
}
}
pub fn call<F>(opts: Opts, stdin_fn: F) -> (String, Option<HashMap<String, cheat::Value>>)
pub fn call<F>(opts: Opts, stdin_fn: F) -> (String, Option<HashMap<String, cheat::Suggestion>>)
where
F: Fn(&mut process::ChildStdin) -> Option<HashMap<String, cheat::Value>>,
F: Fn(&mut process::ChildStdin) -> Option<HashMap<String, cheat::Suggestion>>,
{
let mut c = Command::new("fzf");
let mut fzf_command = Command::new("fzf");
c.args(&[
fzf_command.args(&[
"--preview-window",
"up:2",
"--with-nth",
@ -56,38 +54,46 @@ where
]);
if opts.autoselect {
c.arg("--select-1");
fzf_command.arg("--select-1");
}
if opts.multi {
c.arg("--multi");
}
if opts.copyable {
c.args(&["--expect", "ctrl-y,enter"]);
match opts.suggestion_type {
SuggestionType::MultipleSelections => {
fzf_command.arg("--multi");
}
SuggestionType::Disabled => {
fzf_command.args(&["--print-query", "--no-select-1", "--height", "1"]);
}
SuggestionType::SnippetSelection => {
fzf_command.args(&["--expect", "ctrl-y,enter"]);
}
SuggestionType::SingleRecommendation => {
fzf_command.args(&["--print-query", "--expect", "tab,enter"]);
}
_ => {}
}
if opts.preview {
c.args(&[
fzf_command.args(&[
"--preview",
format!("{} preview {{}}", filesystem::exe_string()).as_str(),
]);
}
if let Some(q) = opts.query {
c.args(&["--query", &q]);
fzf_command.args(&["--query", &q]);
}
if let Some(f) = opts.filter {
c.args(&["--filter", &f]);
fzf_command.args(&["--filter", &f]);
}
if let Some(p) = opts.prompt {
c.args(&["--prompt", &p]);
fzf_command.args(&["--prompt", &p]);
}
if opts.header_lines > 0 {
c.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
fzf_command.args(&["--header-lines", format!("{}", opts.header_lines).as_str()]);
}
if let Some(o) = opts.overrides {
@ -96,15 +102,11 @@ where
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.for_each(|s| {
c.arg(s);
fzf_command.arg(s);
});
}
if !opts.suggestions {
c.args(&["--print-query", "--no-select-1", "--height", "1"]);
}
let child = c
let child = fzf_command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
@ -123,7 +125,7 @@ where
let out = child.wait_with_output().unwrap();
let mut text = match out.status.code() {
let text = match out.status.code() {
Some(0) | Some(1) => String::from_utf8(out.stdout).unwrap(),
Some(130) => process::exit(130),
_ => {
@ -132,7 +134,87 @@ where
panic!("External command failed:\n {}", err)
}
};
text.truncate(text.len() - 1);
(text, result)
(parse_output_single(text, opts.suggestion_type), result)
}
fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> String {
match suggestion_type {
SuggestionType::SingleSelection => text.lines().next().unwrap().to_string(),
SuggestionType::MultipleSelections
| SuggestionType::Disabled
| SuggestionType::SnippetSelection => {
text.truncate(text.len() - 1);
text
}
SuggestionType::SingleRecommendation => {
let lines: Vec<&str> = text.lines().collect();
match (lines.get(0), lines.get(1), lines.get(2)) {
(Some(one), Some(termination), Some(two)) if *termination == "enter" => {
if two.is_empty() {
one.to_string()
} else {
two.to_string()
}
}
(Some(one), Some(termination), None) if *termination == "enter" => one.to_string(),
(Some(one), Some(termination), _) if *termination == "tab" => one.to_string(),
_ => "".to_string(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_output1() {
let text = "palo\n".to_string();
let output = parse_output_single(text, SuggestionType::SingleSelection);
assert_eq!(output, "palo");
}
#[test]
fn test_parse_output2() {
let text = "\nenter\npalo".to_string();
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
assert_eq!(output, "palo");
}
#[test]
fn test_parse_recommendation_output_1() {
let text = "\nenter\npalo".to_string();
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
assert_eq!(output, "palo");
}
#[test]
fn test_parse_recommendation_output_2() {
let text = "p\nenter\npalo".to_string();
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
assert_eq!(output, "palo");
}
#[test]
fn test_parse_recommendation_output_3() {
let text = "peter\nenter\n".to_string();
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
assert_eq!(output, "peter");
}
#[test]
fn test_parse_output3() {
let text = "p\ntab\npalo".to_string();
let output = parse_output_single(text, SuggestionType::SingleRecommendation);
assert_eq!(output, "p");
}
#[test]
fn test_parse_snippet_request() {
let text = "enter\nssh login to a server and forward to ssh key (d… ssh -A <user>@<server> ssh login to a server and forward to ssh key (dangerous but usefull for bastion hosts) ssh -A <user>@<server> \n".to_string();
let output = parse_output_single(text, SuggestionType::SnippetSelection);
assert_eq!(output, "enter\nssh login to a server and forward to ssh key (d… ssh -A <user>@<server> ssh login to a server and forward to ssh key (dangerous but usefull for bastion hosts) ssh -A <user>@<server> ");
}
}

6
tests/cheats/ssh.cheat Normal file
View file

@ -0,0 +1,6 @@
% ssh
# login to a server with a key and port
ssh -i <sshkey> -p <port> <user>@<server>
$ user : echo -e "$(whoami)\nroot" --- --allow-extra