2020-03-04 21:01:23 +00:00
|
|
|
use crate::display;
|
|
|
|
use crate::filesystem;
|
2020-03-18 01:39:00 +00:00
|
|
|
use crate::fnv::HashLine;
|
2020-03-04 21:01:23 +00:00
|
|
|
use crate::option::Config;
|
2020-03-15 16:46:58 +00:00
|
|
|
use crate::welcome;
|
2020-03-04 21:01:23 +00:00
|
|
|
use regex::Regex;
|
2020-03-18 01:39:00 +00:00
|
|
|
use std::collections::{HashMap, HashSet};
|
2020-03-04 21:01:23 +00:00
|
|
|
use std::fs;
|
|
|
|
use std::io::Write;
|
|
|
|
|
2020-03-14 08:09:09 +00:00
|
|
|
#[derive(Debug, PartialEq)]
|
2020-03-04 21:01:23 +00:00
|
|
|
pub struct SuggestionOpts {
|
|
|
|
pub header_lines: u8,
|
|
|
|
pub column: Option<u8>,
|
2020-03-11 14:30:25 +00:00
|
|
|
pub delimiter: Option<String>,
|
2020-03-14 12:33:07 +00:00
|
|
|
pub suggestion_type: SuggestionType,
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
|
2020-03-14 12:33:07 +00:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
2020-03-14 07:59:31 +00:00
|
|
|
pub enum SuggestionType {
|
2020-03-18 11:38:13 +00:00
|
|
|
/// fzf will not print any suggestions
|
2020-03-14 07:59:31 +00:00
|
|
|
Disabled,
|
2020-03-14 12:41:28 +00:00
|
|
|
/// fzf will only select one of the suggestions
|
2020-03-14 07:59:31 +00:00
|
|
|
SingleSelection,
|
2020-03-18 11:38:13 +00:00
|
|
|
/// fzf will select multiple suggestions
|
2020-03-14 07:59:31 +00:00
|
|
|
MultipleSelections,
|
2020-03-18 11:38:13 +00:00
|
|
|
/// fzf will select one of the suggestions or use the query
|
2020-03-14 07:59:31 +00:00
|
|
|
SingleRecommendation,
|
2020-03-14 12:41:28 +00:00
|
|
|
/// initial snippet selection
|
2020-03-14 07:59:31 +00:00
|
|
|
SnippetSelection,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub type Suggestion = (String, Option<SuggestionOpts>);
|
2020-03-04 21:01:23 +00:00
|
|
|
|
2020-03-18 11:38:13 +00:00
|
|
|
pub struct VariableMap(HashMap<u64, Suggestion>);
|
|
|
|
|
|
|
|
impl VariableMap {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
Self(HashMap::new())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn insert(&mut self, tags: &str, variable: &str, value: Suggestion) -> Option<Suggestion> {
|
|
|
|
self.0.insert(gen_key(tags, variable), value)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get(&self, tags: &str, variable: &str) -> Option<&Suggestion> {
|
|
|
|
self.0.get(&gen_key(tags, variable))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-15 16:46:58 +00:00
|
|
|
fn remove_quotes(txt: &str) -> String {
|
2020-03-12 22:04:47 +00:00
|
|
|
txt.replace('"', "").replace('\'', "")
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:01:23 +00:00
|
|
|
fn parse_opts(text: &str) -> SuggestionOpts {
|
|
|
|
let mut header_lines: u8 = 0;
|
|
|
|
let mut column: Option<u8> = None;
|
|
|
|
let mut multi = false;
|
2020-03-17 15:39:38 +00:00
|
|
|
let mut prevent_extra = false;
|
2020-03-11 14:30:25 +00:00
|
|
|
let mut delimiter: Option<String> = None;
|
2020-03-04 21:01:23 +00:00
|
|
|
|
|
|
|
let mut parts = text.split(' ');
|
|
|
|
|
|
|
|
while let Some(p) = parts.next() {
|
|
|
|
match p {
|
|
|
|
"--multi" => multi = true,
|
2020-03-17 15:39:38 +00:00
|
|
|
"--prevent-extra" => prevent_extra = true,
|
2020-03-11 14:30:25 +00:00
|
|
|
"--header" | "--headers" | "--header-lines" => {
|
2020-03-15 16:46:58 +00:00
|
|
|
header_lines = remove_quotes(parts.next().unwrap()).parse::<u8>().unwrap()
|
|
|
|
}
|
|
|
|
"--column" => {
|
|
|
|
column = Some(remove_quotes(parts.next().unwrap()).parse::<u8>().unwrap())
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
2020-03-15 16:46:58 +00:00
|
|
|
"--delimiter" => delimiter = Some(remove_quotes(parts.next().unwrap()).to_string()),
|
2020-03-04 21:01:23 +00:00
|
|
|
_ => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-14 12:41:28 +00:00
|
|
|
SuggestionOpts {
|
2020-03-04 21:01:23 +00:00
|
|
|
header_lines,
|
|
|
|
column,
|
2020-03-11 14:30:25 +00:00
|
|
|
delimiter,
|
2020-03-17 15:39:38 +00:00
|
|
|
suggestion_type: match (multi, prevent_extra) {
|
2020-03-14 12:33:07 +00:00
|
|
|
(true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra
|
2020-03-17 15:39:38 +00:00
|
|
|
(false, false) => SuggestionType::SingleRecommendation,
|
|
|
|
(false, true) => SuggestionType::SingleSelection,
|
2020-03-14 08:09:09 +00:00
|
|
|
},
|
2020-03-14 12:41:28 +00:00
|
|
|
}
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn parse_variable_line(line: &str) -> (&str, &str, Option<SuggestionOpts>) {
|
|
|
|
let re = Regex::new(r"^\$\s*([^:]+):(.*)").unwrap();
|
|
|
|
let caps = re.captures(line).unwrap();
|
|
|
|
let variable = caps.get(1).unwrap().as_str().trim();
|
|
|
|
let mut command_plus_opts = caps.get(2).unwrap().as_str().split("---");
|
2020-03-14 15:46:33 +00:00
|
|
|
let command = command_plus_opts.next().unwrap();
|
|
|
|
let command_options = command_plus_opts.next().map(parse_opts);
|
2020-03-14 08:09:09 +00:00
|
|
|
(variable, command, command_options)
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
|
2020-03-14 21:08:57 +00:00
|
|
|
fn write_cmd(
|
|
|
|
tags: &str,
|
|
|
|
comment: &str,
|
|
|
|
snippet: &str,
|
|
|
|
tag_width: usize,
|
|
|
|
comment_width: usize,
|
|
|
|
stdin: &mut std::process::ChildStdin,
|
|
|
|
) -> bool {
|
|
|
|
if snippet.is_empty() {
|
|
|
|
true
|
|
|
|
} else {
|
|
|
|
stdin
|
|
|
|
.write_all(
|
2020-03-18 11:38:13 +00:00
|
|
|
display::format_line(&tags, &comment, &snippet, tag_width, comment_width)
|
|
|
|
.as_bytes(),
|
2020-03-14 21:08:57 +00:00
|
|
|
)
|
|
|
|
.is_ok()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-18 11:38:13 +00:00
|
|
|
fn gen_key(tags: &str, variable: &str) -> u64 {
|
|
|
|
format!("{};{}", tags, variable).hash_line()
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:01:23 +00:00
|
|
|
fn read_file(
|
|
|
|
path: &str,
|
2020-03-18 11:38:13 +00:00
|
|
|
variables: &mut VariableMap,
|
2020-03-04 21:01:23 +00:00
|
|
|
stdin: &mut std::process::ChildStdin,
|
2020-03-18 01:39:00 +00:00
|
|
|
set: &mut HashSet<u64>,
|
2020-03-15 16:46:58 +00:00
|
|
|
) -> bool {
|
2020-03-04 21:01:23 +00:00
|
|
|
let mut tags = String::from("");
|
|
|
|
let mut comment = String::from("");
|
|
|
|
let mut snippet = String::from("");
|
2020-03-18 11:38:13 +00:00
|
|
|
let mut should_break = false;
|
2020-03-04 21:01:23 +00:00
|
|
|
|
2020-03-07 21:03:51 +00:00
|
|
|
let (tag_width, comment_width) = *display::WIDTHS;
|
2020-03-04 21:01:23 +00:00
|
|
|
|
|
|
|
if let Ok(lines) = filesystem::read_lines(path) {
|
|
|
|
for l in lines {
|
2020-03-18 11:38:13 +00:00
|
|
|
if should_break {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:01:23 +00:00
|
|
|
let line = l.unwrap();
|
2020-03-18 01:39:00 +00:00
|
|
|
let hash = line.hash_line();
|
|
|
|
if set.contains(&hash) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
set.insert(hash);
|
2020-03-04 21:01:23 +00:00
|
|
|
|
2020-03-14 21:14:30 +00:00
|
|
|
// blank
|
|
|
|
if line.is_empty() {
|
|
|
|
}
|
2020-03-04 21:01:23 +00:00
|
|
|
// tag
|
2020-03-14 21:14:30 +00:00
|
|
|
else if line.starts_with('%') {
|
2020-03-14 21:08:57 +00:00
|
|
|
if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
|
2020-03-18 11:38:13 +00:00
|
|
|
should_break = true
|
2020-03-14 21:08:57 +00:00
|
|
|
}
|
|
|
|
snippet = String::from("");
|
2020-03-04 21:01:23 +00:00
|
|
|
tags = String::from(&line[2..]);
|
|
|
|
}
|
2020-03-09 18:02:54 +00:00
|
|
|
// metacomment
|
|
|
|
else if line.starts_with(';') {
|
|
|
|
}
|
2020-03-04 21:01:23 +00:00
|
|
|
// comment
|
|
|
|
else if line.starts_with('#') {
|
2020-03-14 21:08:57 +00:00
|
|
|
if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
|
2020-03-18 11:38:13 +00:00
|
|
|
should_break = true
|
2020-03-14 21:08:57 +00:00
|
|
|
}
|
|
|
|
snippet = String::from("");
|
2020-03-04 21:01:23 +00:00
|
|
|
comment = String::from(&line[2..]);
|
|
|
|
}
|
|
|
|
// variable
|
|
|
|
else if line.starts_with('$') {
|
2020-03-14 21:08:57 +00:00
|
|
|
if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
|
2020-03-18 11:38:13 +00:00
|
|
|
should_break = true
|
2020-03-14 21:08:57 +00:00
|
|
|
}
|
|
|
|
snippet = String::from("");
|
2020-03-14 08:09:09 +00:00
|
|
|
let (variable, command, opts) = parse_variable_line(&line);
|
2020-03-18 11:38:13 +00:00
|
|
|
variables.insert(&tags, &variable, (String::from(command), opts));
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
2020-03-18 11:38:13 +00:00
|
|
|
// first snippet line
|
|
|
|
else if (&snippet).is_empty() {
|
|
|
|
snippet.push_str(&line);
|
|
|
|
}
|
|
|
|
// other snippet lines
|
2020-03-04 21:01:23 +00:00
|
|
|
else {
|
2020-03-14 21:08:57 +00:00
|
|
|
snippet.push_str(display::LINE_SEPARATOR);
|
|
|
|
snippet.push_str(&line);
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
}
|
2020-03-15 16:46:58 +00:00
|
|
|
|
2020-03-18 11:38:13 +00:00
|
|
|
if !should_break {
|
|
|
|
write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin);
|
|
|
|
}
|
|
|
|
|
2020-03-15 16:46:58 +00:00
|
|
|
return true;
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
2020-03-14 21:08:57 +00:00
|
|
|
|
2020-03-15 16:46:58 +00:00
|
|
|
false
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
|
2020-03-18 11:38:13 +00:00
|
|
|
pub fn read_all(config: &Config, stdin: &mut std::process::ChildStdin) -> VariableMap {
|
|
|
|
let mut variables = VariableMap::new();
|
2020-03-15 16:46:58 +00:00
|
|
|
let mut found_something = false;
|
|
|
|
let paths = filesystem::cheat_paths(config);
|
|
|
|
let folders = paths.split(':');
|
2020-03-18 01:39:00 +00:00
|
|
|
let mut set = HashSet::new();
|
2020-03-04 21:01:23 +00:00
|
|
|
|
|
|
|
for folder in folders {
|
|
|
|
if let Ok(paths) = fs::read_dir(folder) {
|
|
|
|
for path in paths {
|
2020-03-18 01:39:00 +00:00
|
|
|
let path = path.unwrap().path();
|
|
|
|
let path_str = path.to_str().unwrap();
|
2020-03-15 16:46:58 +00:00
|
|
|
if path_str.ends_with(".cheat")
|
2020-03-18 01:39:00 +00:00
|
|
|
&& read_file(path_str, &mut variables, stdin, &mut set)
|
2020-03-15 16:46:58 +00:00
|
|
|
&& !found_something
|
|
|
|
{
|
|
|
|
found_something = true;
|
2020-03-12 16:03:04 +00:00
|
|
|
}
|
2020-03-04 21:01:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-15 16:46:58 +00:00
|
|
|
if !found_something {
|
|
|
|
welcome::cheatsheet(stdin);
|
|
|
|
}
|
|
|
|
|
2020-03-04 21:01:23 +00:00
|
|
|
variables
|
|
|
|
}
|
2020-03-14 08:09:09 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_variable_line() {
|
2020-03-14 12:33:07 +00:00
|
|
|
let (variable, command, command_options) =
|
|
|
|
parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --allow-extra");
|
2020-03-14 08:09:09 +00:00
|
|
|
assert_eq!(command, " echo -e \"$(whoami)\\nroot\" ");
|
|
|
|
assert_eq!(variable, "user");
|
2020-03-14 12:33:07 +00:00
|
|
|
assert_eq!(
|
|
|
|
command_options,
|
|
|
|
Some(SuggestionOpts {
|
|
|
|
header_lines: 0,
|
|
|
|
column: None,
|
|
|
|
delimiter: None,
|
|
|
|
suggestion_type: SuggestionType::SingleRecommendation
|
|
|
|
})
|
|
|
|
);
|
2020-03-14 08:09:09 +00:00
|
|
|
}
|
|
|
|
use std::process::{Command, Stdio};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_read_file() {
|
|
|
|
let path = "tests/cheats/ssh.cheat";
|
2020-03-18 11:38:13 +00:00
|
|
|
let mut variables = VariableMap::new();
|
2020-03-14 12:33:07 +00:00
|
|
|
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
|
2020-03-14 08:09:09 +00:00
|
|
|
let child_stdin = child.stdin.as_mut().unwrap();
|
2020-03-18 11:38:13 +00:00
|
|
|
let mut set: HashSet<u64> = HashSet::new();
|
|
|
|
read_file(path, &mut variables, child_stdin, &mut set);
|
|
|
|
let mut result = VariableMap::new();
|
|
|
|
let suggestion = (
|
|
|
|
r#" echo -e "$(whoami)\nroot" "#.to_string(),
|
|
|
|
Some(SuggestionOpts {
|
|
|
|
header_lines: 0,
|
|
|
|
column: None,
|
|
|
|
delimiter: None,
|
|
|
|
suggestion_type: SuggestionType::SingleRecommendation,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
result.insert("ssh", "user", suggestion);
|
|
|
|
let actual_suggestion = result.get("ssh", "user");
|
|
|
|
assert_eq!(
|
|
|
|
Some(&(
|
2020-03-14 15:46:33 +00:00
|
|
|
r#" echo -e "$(whoami)\nroot" "#.to_string(),
|
2020-03-14 12:33:07 +00:00
|
|
|
Some(SuggestionOpts {
|
|
|
|
header_lines: 0,
|
|
|
|
column: None,
|
|
|
|
delimiter: None,
|
|
|
|
suggestion_type: SuggestionType::SingleRecommendation,
|
|
|
|
}),
|
2020-03-18 11:38:13 +00:00
|
|
|
)),
|
|
|
|
actual_suggestion
|
2020-03-14 08:09:09 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|