diff --git a/src/filesystem.rs b/src/filesystem.rs
index 989df30..914a860 100644
--- a/src/filesystem.rs
+++ b/src/filesystem.rs
@@ -1,17 +1,23 @@
use crate::structures::option::Config;
use anyhow::Context;
use anyhow::Error;
+use core::fmt::Display;
use std::fs;
use std::fs::File;
-use std::io::{self, BufRead, BufReader, Lines};
+use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
-pub fn read_lines
(filename: P) -> io::Result>>
+pub fn read_lines(filename: P) -> Result, Error>
where
- P: AsRef,
+ P: AsRef + Display,
{
+ let error_string = format!("Failed to read lines from {}", filename);
let file = File::open(filename)?;
- Ok(io::BufReader::new(file).lines())
+ io::BufReader::new(file)
+ .lines()
+ .map(|line| line.map_err(Error::from))
+ .collect::, Error>>()
+ .with_context(|| error_string)
}
pub fn pathbuf_to_string(pathbuf: PathBuf) -> Result {
diff --git a/src/parser.rs b/src/parser.rs
index 3d90995..0683c18 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -5,7 +5,7 @@ use crate::structures::fnv::HashLine;
use crate::structures::fzf::{Opts as FzfOpts, SuggestionType};
use crate::structures::option::Config;
use crate::welcome;
-use anyhow::Error;
+use anyhow::{Context, Error};
use regex::Regex;
use std::collections::HashSet;
use std::fs;
@@ -16,31 +16,64 @@ lazy_static! {
Regex::new(r"^\$\s*([^:]+):(.*)").expect("Invalid regex");
}
-fn parse_opts(text: &str) -> FzfOpts {
+fn parse_opts(text: &str) -> Result {
let mut multi = false;
let mut prevent_extra = false;
let mut opts = FzfOpts::default();
- let parts_vec = shellwords::split(text).unwrap();
- let mut parts = parts_vec.into_iter();
+ let parts = shellwords::split(text)
+ .map_err(|_| anyhow!("Given options are missing a closing quote"))?;
- while let Some(p) = parts.next() {
- match p.as_str() {
- "--multi" => multi = true,
- "--prevent-extra" => prevent_extra = true,
- "--headers" | "--header-lines" => {
- opts.header_lines = parts.next().unwrap().parse::().unwrap()
+ parts
+ .into_iter()
+ .filter(|part| {
+ // We'll take parts in pairs of 2: (argument, value). Flags don't have a value tho, so we filter and handle them beforehand.
+ match part.as_str() {
+ "--multi" => {
+ multi = true;
+ false
+ }
+ "--prevent-extra" => {
+ prevent_extra = true;
+ false
+ }
+ _ => true,
}
- "--column" => opts.column = Some(parts.next().unwrap().parse::().unwrap()),
- "--delimiter" => opts.delimiter = Some(parts.next().unwrap().to_string()),
- "--query" => opts.query = Some(parts.next().unwrap().to_string()),
- "--filter" => opts.filter = Some(parts.next().unwrap().to_string()),
- "--preview" => opts.preview = Some(parts.next().unwrap().to_string()),
- "--preview-window" => opts.preview_window = Some(parts.next().unwrap().to_string()),
- "--header" => opts.header = Some(parts.next().unwrap().to_string()),
- "--overrides" => opts.overrides = Some(parts.next().unwrap().to_string()),
- _ => (),
- }
- }
+ })
+ .collect::>()
+ .chunks(2)
+ .map(|flag_and_value| {
+ if let [flag, value] = flag_and_value {
+ match flag.as_str() {
+ "--headers" | "--header-lines" => {
+ opts.header_lines = value
+ .parse::()
+ .context("Value for `--headers` is invalid u8")?
+ }
+ "--column" => {
+ opts.column = Some(
+ value
+ .parse::()
+ .context("Value for `--column` is invalid u8")?,
+ )
+ }
+ "--delimiter" => opts.delimiter = Some(value.to_string()),
+ "--query" => opts.query = Some(value.to_string()),
+ "--filter" => opts.filter = Some(value.to_string()),
+ "--preview" => opts.preview = Some(value.to_string()),
+ "--preview-window" => opts.preview_window = Some(value.to_string()),
+ "--header" => opts.header = Some(value.to_string()),
+ "--overrides" => opts.overrides = Some(value.to_string()),
+ _ => (),
+ }
+ Ok(())
+ } else if let [flag] = flag_and_value {
+ Err(anyhow!("No value provided for the flag {}", flag))
+ } else {
+ unreachable!() // Chunking by 2 allows only for tuples of 1 or 2 items...
+ }
+ })
+ .collect::>()
+ .context("Failed to parse fzf options")?;
let suggestion_type = match (multi, prevent_extra) {
(true, _) => SuggestionType::MultipleSelections, // multi wins over allow-extra
@@ -49,16 +82,31 @@ fn parse_opts(text: &str) -> FzfOpts {
};
opts.suggestion_type = suggestion_type;
- opts
+ Ok(opts)
}
-fn parse_variable_line(line: &str) -> (&str, &str, Option) {
- let caps = VAR_LINE_REGEX.captures(line).unwrap();
- 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 command_options = command_plus_opts.next().map(parse_opts);
- (variable, command, command_options)
+fn parse_variable_line(line: &str) -> Result<(&str, &str, Option), Error> {
+ let caps = VAR_LINE_REGEX.captures(line).ok_or_else(|| {
+ anyhow!(
+ "No variables, command, and options found in the line {}",
+ line
+ )
+ })?;
+ let variable = caps
+ .get(1)
+ .ok_or_else(|| anyhow!("No variable captured in the line {}", line))?
+ .as_str()
+ .trim();
+ let mut command_plus_opts = caps
+ .get(2)
+ .ok_or_else(|| anyhow!("No command and options captured in the line {}", line))?
+ .as_str()
+ .split("---");
+ let command = command_plus_opts
+ .next()
+ .ok_or_else(|| anyhow!("No command captured in the line {}", line))?;
+ let command_options = command_plus_opts.next().map(parse_opts).transpose()?;
+ Ok((variable, command, command_options))
}
fn write_cmd(
@@ -68,16 +116,16 @@ fn write_cmd(
tag_width: usize,
comment_width: usize,
stdin: &mut std::process::ChildStdin,
-) -> bool {
+) -> Result<(), Error> {
if snippet.is_empty() {
- true
+ Ok(())
} else {
stdin
.write_all(
display::format_line(&tags, &comment, &snippet, tag_width, comment_width)
.as_bytes(),
)
- .is_ok()
+ .context("Failed to write command to fzf's stdin")
}
}
@@ -86,7 +134,7 @@ fn read_file(
variables: &mut VariableMap,
visited_lines: &mut HashSet,
stdin: &mut std::process::ChildStdin,
-) -> bool {
+) -> Result<(), Error> {
let mut tags = String::from("");
let mut comment = String::from("");
let mut snippet = String::from("");
@@ -94,71 +142,71 @@ fn read_file(
let (tag_width, comment_width) = *display::WIDTHS;
- if let Ok(lines) = filesystem::read_lines(path) {
- for l in lines {
- if should_break {
- break;
- }
-
- let line = l.unwrap();
-
- // duplicate
- if !tags.is_empty() && !comment.is_empty() {}
-
- // blank
- if line.is_empty() {
- }
- // tag
- else if line.starts_with('%') {
- if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
- should_break = true
- }
- snippet = String::from("");
- tags = String::from(&line[2..]);
- }
- // metacomment
- else if line.starts_with(';') {
- }
- // comment
- else if line.starts_with('#') {
- if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
- should_break = true
- }
- snippet = String::from("");
- comment = String::from(&line[2..]);
- }
- // variable
- else if line.starts_with('$') {
- if !write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin) {
- should_break = true
- }
- snippet = String::from("");
- let (variable, command, opts) = parse_variable_line(&line);
- variables.insert(&tags, &variable, (String::from(command), opts));
- }
- // snippet
- else {
- let hash = format!("{}{}", &comment, &line).hash_line();
- if visited_lines.contains(&hash) {
- continue;
- }
- visited_lines.insert(hash);
-
- if !(&snippet).is_empty() {
- snippet.push_str(display::LINE_SEPARATOR);
- }
- snippet.push_str(&line);
- }
+ for (line_nr, line) in filesystem::read_lines(path)?.into_iter().enumerate() {
+ if should_break {
+ break;
}
- if !should_break {
- write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin);
- }
+ // duplicate
+ if !tags.is_empty() && !comment.is_empty() {}
- return true;
+ // blank
+ if line.is_empty() {
+ }
+ // tag
+ else if line.starts_with('%') {
+ if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
+ should_break = true
+ }
+ snippet = String::from("");
+ tags = String::from(&line[2..]);
+ }
+ // metacomment
+ else if line.starts_with(';') {
+ }
+ // comment
+ else if line.starts_with('#') {
+ if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
+ should_break = true
+ }
+ snippet = String::from("");
+ comment = String::from(&line[2..]);
+ }
+ // variable
+ else if line.starts_with('$') {
+ if write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin).is_err() {
+ should_break = true
+ }
+ snippet = String::from("");
+ let (variable, command, opts) = parse_variable_line(&line).with_context(|| {
+ format!(
+ "Failed to parse variable line. See line nr.{} in cheatsheet {}",
+ line_nr + 1,
+ path
+ )
+ })?;
+ variables.insert(&tags, &variable, (String::from(command), opts));
+ }
+ // snippet
+ else {
+ let hash = format!("{}{}", &comment, &line).hash_line();
+ if visited_lines.contains(&hash) {
+ continue;
+ }
+ visited_lines.insert(hash);
+
+ if !(&snippet).is_empty() {
+ snippet.push_str(display::LINE_SEPARATOR);
+ }
+ snippet.push_str(&line);
+ }
}
- false
+ if !should_break {
+ let _ = write_cmd(&tags, &comment, &snippet, tag_width, comment_width, stdin);
+ }
+
+ Ok(())
}
pub fn read_all(
@@ -172,16 +220,21 @@ pub fn read_all(
let folders = paths.split(':');
for folder in folders {
- if let Ok(paths) = fs::read_dir(folder) {
- for path in paths {
- let path = path.unwrap().path();
- let path_str = path.to_str().unwrap();
- if path_str.ends_with(".cheat")
- && read_file(path_str, &mut variables, &mut visited_lines, stdin)
- && !found_something
- {
- found_something = true;
- }
+ let dir_entries =
+ fs::read_dir(folder).with_context(|| format!("Unable to read directory {}", folder))?;
+
+ for entry in dir_entries {
+ let path = entry
+ .with_context(|| format!("Unable to read directory {}", folder))?
+ .path();
+ let path_str = path
+ .to_str()
+ .ok_or_else(|| anyhow!("Invalid path {}", path.display()))?;
+ if path_str.ends_with(".cheat")
+ && read_file(path_str, &mut variables, &mut visited_lines, stdin).is_ok()
+ && !found_something
+ {
+ found_something = true;
}
}
}
@@ -200,7 +253,7 @@ mod tests {
#[test]
fn test_parse_variable_line() {
let (variable, command, command_options) =
- parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --allow-extra");
+ parse_variable_line("$ user : echo -e \"$(whoami)\\nroot\" --- --allow-extra").unwrap();
assert_eq!(command, " echo -e \"$(whoami)\\nroot\" ");
assert_eq!(variable, "user");
assert_eq!(
@@ -223,7 +276,7 @@ mod tests {
let mut child = Command::new("cat").stdin(Stdio::piped()).spawn().unwrap();
let child_stdin = child.stdin.as_mut().unwrap();
let mut visited_lines: HashSet = HashSet::new();
- read_file(path, &mut variables, &mut visited_lines, child_stdin);
+ read_file(path, &mut variables, &mut visited_lines, child_stdin).unwrap();
let expected_suggestion = (
r#" echo -e "$(whoami)\nroot" "#.to_string(),
Some(FzfOpts {