2020-03-04 21:01:23 +00:00
use crate ::display ;
use crate ::filesystem ;
2020-03-20 01:01:50 +00:00
use crate ::structures ::cheat ::VariableMap ;
2020-03-18 15:29:29 +00:00
use crate ::structures ::fnv ::HashLine ;
2020-03-20 01:01:50 +00:00
use crate ::structures ::fzf ::{ Opts as FzfOpts , SuggestionType } ;
2020-03-24 14:42:18 +00:00
use crate ::structures ::{ error ::filesystem ::InvalidPath , option ::Config } ;
2020-03-15 16:46:58 +00:00
use crate ::welcome ;
2020-03-22 00:51:38 +00:00
use anyhow ::{ Context , Error } ;
2020-03-04 21:01:23 +00:00
use regex ::Regex ;
2020-03-18 15:29:29 +00:00
use std ::collections ::HashSet ;
2020-03-04 21:01:23 +00:00
use std ::fs ;
use std ::io ::Write ;
2020-03-21 02:22:11 +00:00
lazy_static! {
pub static ref VAR_LINE_REGEX : Regex =
Regex ::new ( r "^\$\s*([^:]+):(.*)" ) . expect ( " Invalid regex " ) ;
}
2020-03-22 00:51:38 +00:00
fn parse_opts ( text : & str ) -> Result < FzfOpts , Error > {
2020-03-04 21:01:23 +00:00
let mut multi = false ;
2020-03-17 15:39:38 +00:00
let mut prevent_extra = false ;
2020-03-20 01:01:50 +00:00
let mut opts = FzfOpts ::default ( ) ;
2020-03-22 00:51:38 +00:00
let parts = shellwords ::split ( text )
. map_err ( | _ | anyhow! ( " Given options are missing a closing quote " ) ) ? ;
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 ,
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
} )
. collect ::< Vec < _ > > ( )
. 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 ::< u8 > ( )
. context ( " Value for `--headers` is invalid u8 " ) ?
}
" --column " = > {
opts . column = Some (
value
. parse ::< u8 > ( )
. 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 {
2020-03-22 15:31:49 +00:00
Err ( anyhow! ( " No value provided for the flag `{}` " , flag ) )
2020-03-22 00:51:38 +00:00
} else {
unreachable! ( ) // Chunking by 2 allows only for tuples of 1 or 2 items...
}
} )
. collect ::< Result < _ , _ > > ( )
. context ( " Failed to parse fzf options " ) ? ;
2020-03-04 21:01:23 +00:00
2020-03-20 01:01:50 +00:00
let suggestion_type = match ( multi , prevent_extra ) {
2020-03-24 12:42:54 +00:00
( true , _ ) = > SuggestionType ::MultipleSelections , // multi wins over prevent-extra
2020-03-20 01:01:50 +00:00
( false , false ) = > SuggestionType ::SingleRecommendation ,
( false , true ) = > SuggestionType ::SingleSelection ,
} ;
opts . suggestion_type = suggestion_type ;
2020-03-22 00:51:38 +00:00
Ok ( opts )
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
fn parse_variable_line ( line : & str ) -> Result < ( & str , & str , Option < FzfOpts > ) , Error > {
let caps = VAR_LINE_REGEX . captures ( line ) . ok_or_else ( | | {
anyhow! (
2020-03-22 15:31:49 +00:00
" No variables, command, and options found in the line `{}` " ,
2020-03-22 00:51:38 +00:00
line
)
} ) ? ;
let variable = caps
. get ( 1 )
2020-03-22 15:31:49 +00:00
. ok_or_else ( | | anyhow! ( " No variable captured in the line `{}` " , line ) ) ?
2020-03-22 00:51:38 +00:00
. as_str ( )
. trim ( ) ;
let mut command_plus_opts = caps
. get ( 2 )
2020-03-22 15:31:49 +00:00
. ok_or_else ( | | anyhow! ( " No command and options captured in the line `{}` " , line ) ) ?
2020-03-22 00:51:38 +00:00
. as_str ( )
. split ( " --- " ) ;
let command = command_plus_opts
. next ( )
2020-03-22 15:31:49 +00:00
. ok_or_else ( | | anyhow! ( " No command captured in the line `{}` " , line ) ) ? ;
2020-03-22 00:51:38 +00:00
let command_options = command_plus_opts . next ( ) . map ( parse_opts ) . transpose ( ) ? ;
Ok ( ( 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 ,
2020-03-22 00:51:38 +00:00
) -> Result < ( ) , Error > {
2020-03-14 21:08:57 +00:00
if snippet . is_empty ( ) {
2020-03-22 00:51:38 +00:00
Ok ( ( ) )
2020-03-14 21:08:57 +00:00
} 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
)
2020-03-22 00:51:38 +00:00
. context ( " Failed to write command to fzf's stdin " )
2020-03-14 21:08:57 +00:00
}
}
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-18 15:29:29 +00:00
visited_lines : & mut HashSet < u64 > ,
2020-03-04 21:01:23 +00:00
stdin : & mut std ::process ::ChildStdin ,
2020-03-22 00:51:38 +00:00
) -> Result < ( ) , Error > {
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
2020-03-23 16:16:13 +00:00
for ( line_nr , line_result ) in filesystem ::read_lines ( path ) ? . enumerate ( ) {
let line = line_result
. with_context ( | | format! ( " Failed to read line nr. {} from ` {} ` " , line_nr , path ) ) ? ;
2020-03-22 00:51:38 +00:00
if should_break {
break ;
}
2020-03-19 12:19:50 +00:00
2020-03-22 00:51:38 +00:00
// duplicate
if ! tags . is_empty ( ) & & ! comment . is_empty ( ) { }
2020-03-04 21:01:23 +00:00
2020-03-22 00:51:38 +00:00
// 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
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
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
2020-03-09 18:02:54 +00:00
}
2020-03-22 00:51:38 +00:00
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
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
snippet = String ::from ( " " ) ;
let ( variable , command , opts ) = parse_variable_line ( & line ) . with_context ( | | {
format! (
2020-03-22 15:31:49 +00:00
" Failed to parse variable line. See line nr.{} in cheatsheet `{}` " ,
2020-03-22 00:51:38 +00:00
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 ;
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
visited_lines . insert ( hash ) ;
2020-03-19 12:19:50 +00:00
2020-03-22 00:51:38 +00:00
if ! ( & snippet ) . is_empty ( ) {
snippet . push_str ( display ::LINE_SEPARATOR ) ;
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
snippet . push_str ( & line ) ;
2020-03-04 21:01:23 +00:00
}
2020-03-22 00:51:38 +00:00
}
2020-03-15 16:46:58 +00:00
2020-03-22 00:51:38 +00:00
if ! should_break {
let _ = write_cmd ( & tags , & comment , & snippet , tag_width , comment_width , stdin ) ;
2020-03-04 21:01:23 +00:00
}
2020-03-14 21:08:57 +00:00
2020-03-22 00:51:38 +00:00
Ok ( ( ) )
2020-03-04 21:01:23 +00:00
}
2020-03-22 01:53:40 +00:00
fn paths_from_path_param < ' a > ( env_var : & ' a str ) -> impl Iterator < Item = & ' a str > + ' a {
env_var . split ( ':' ) . filter ( | folder | folder ! = & " " )
}
2020-03-21 16:43:13 +00:00
pub fn read_all (
config : & Config ,
stdin : & mut std ::process ::ChildStdin ,
) -> Result < VariableMap , Error > {
2020-03-18 11:38:13 +00:00
let mut variables = VariableMap ::new ( ) ;
2020-03-15 16:46:58 +00:00
let mut found_something = false ;
2020-03-18 15:29:29 +00:00
let mut visited_lines = HashSet ::new ( ) ;
2020-03-24 14:42:18 +00:00
let paths = filesystem ::cheat_paths ( config ) ;
if paths . is_err ( ) {
welcome ::cheatsheet ( stdin ) ;
return Ok ( variables ) ;
}
let paths = paths . expect ( " Unable to get paths " ) ;
2020-03-22 01:53:40 +00:00
let folders = paths_from_path_param ( & paths ) ;
2020-03-04 21:01:23 +00:00
for folder in folders {
2020-03-22 21:54:02 +00:00
if let Ok ( dir_entries ) = fs ::read_dir ( folder ) {
for entry in dir_entries {
2020-03-24 14:42:18 +00:00
if entry . is_ok ( ) {
let path = entry . expect ( " Impossible to read an invalid entry " ) . path ( ) ;
let path_str = path
. to_str ( )
. ok_or_else ( | | InvalidPath ( path . to_path_buf ( ) ) ) ? ;
if path_str . ends_with ( " .cheat " )
& & read_file ( path_str , & mut variables , & mut visited_lines , stdin ) . is_ok ( )
& & ! found_something
{
found_something = true ;
}
2020-03-22 21:54:02 +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-21 16:43:13 +00:00
Ok ( variables )
2020-03-04 21:01:23 +00:00
}
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 ) =
2020-03-24 12:42:54 +00:00
parse_variable_line ( " $ user : echo -e \" $(whoami) \\ nroot \" --- --prevent-extra " )
. unwrap ( ) ;
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 ,
2020-03-20 01:01:50 +00:00
Some ( FzfOpts {
2020-03-14 12:33:07 +00:00
header_lines : 0 ,
column : None ,
delimiter : None ,
2020-03-24 12:42:54 +00:00
suggestion_type : SuggestionType ::SingleSelection ,
2020-03-20 01:01:50 +00:00
.. Default ::default ( )
2020-03-14 12:33:07 +00:00
} )
) ;
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 15:29:29 +00:00
let mut visited_lines : HashSet < u64 > = HashSet ::new ( ) ;
2020-03-22 00:51:38 +00:00
read_file ( path , & mut variables , & mut visited_lines , child_stdin ) . unwrap ( ) ;
2020-03-18 15:29:29 +00:00
let expected_suggestion = (
2020-03-18 11:38:13 +00:00
r # " echo -e "$(whoami)\nroot" "# . to_string ( ) ,
2020-03-20 01:01:50 +00:00
Some ( FzfOpts {
2020-03-18 11:38:13 +00:00
header_lines : 0 ,
column : None ,
delimiter : None ,
2020-03-24 12:42:54 +00:00
suggestion_type : SuggestionType ::SingleSelection ,
2020-03-20 01:01:50 +00:00
.. Default ::default ( )
2020-03-18 11:38:13 +00:00
} ) ,
) ;
2020-03-18 15:29:29 +00:00
let actual_suggestion = variables . get ( " ssh " , " user " ) ;
assert_eq! ( Some ( & expected_suggestion ) , actual_suggestion ) ;
2020-03-14 08:09:09 +00:00
}
2020-03-22 01:53:40 +00:00
#[ test ]
fn splitting_of_dirs_param_may_not_contain_empty_items ( ) {
// Trailing colon indicates potential extra path. Split returns an empty item for it. This empty item should be filtered away, which is what this test checks.
let given_path_config = " SOME_PATH:ANOTHER_PATH: " ;
let found_paths = paths_from_path_param ( given_path_config ) ;
let mut expected_paths = vec! [ " SOME_PATH " , " ANOTHER_PATH " ] . into_iter ( ) ;
for found in found_paths {
let expected = expected_paths . next ( ) . unwrap ( ) ;
assert_eq! ( found , expected )
}
}
2020-03-14 08:09:09 +00:00
}