Skip to content

Commit

Permalink
Merge pull request #259 from mrVanDalo/feature/allow-extra
Browse files Browse the repository at this point in the history
add `--allow-extra` option
  • Loading branch information
denisidoro authored Mar 14, 2020
2 parents e99e151 + 0d22fe4 commit b809739
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 74 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
91 changes: 79 additions & 12 deletions src/cheat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -32,13 +47,15 @@ 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(' ');

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()
}
Expand All @@ -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,
},
}
}

Expand All @@ -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("");
Expand All @@ -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),
Expand Down Expand Up @@ -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(|| {
Expand All @@ -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);
}
}
65 changes: 35 additions & 30 deletions src/cmds/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()
};

Expand All @@ -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 {
Expand All @@ -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();

Expand All @@ -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() {
Expand All @@ -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()
};

Expand All @@ -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];

if values.get(varname).is_none() {
let k = format!("{};{}", tags, varname);

let value = match variables.get(&k[..]) {
Some(suggestion) => prompt_with_suggestions(varname, &config, suggestion, &values),
None => prompt_without_suggestions(varname),
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(variable_name).is_none() {
let key = format!("{};{}", tags, variable_name);

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(),
);
}
}

Expand All @@ -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);

Expand Down
Loading

0 comments on commit b809739

Please sign in to comment.