Fix --multi with --column + refactors (#290)

This commit is contained in:
Denis Isidoro 2020-03-19 09:19:50 -03:00 committed by GitHub
parent 84e28e7885
commit 2ca1d8fc85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 386 additions and 82 deletions

View file

@ -6,7 +6,7 @@
on: [push] on: [push]
name: Tests name: CI
jobs: jobs:
check: check:
@ -30,7 +30,7 @@ jobs:
command: check command: check
test: test:
name: Test Suite name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout sources - name: Checkout sources
@ -49,6 +49,12 @@ jobs:
with: with:
command: test command: test
- name: Install fzf
run: git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf; yes | ~/.fzf/install;
- name: Run bash tests
run: ./tests/run 'trivial\|multiple'
lints: lints:
name: Lints name: Lints
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -76,4 +82,4 @@ jobs:
continue-on-error: false continue-on-error: false
with: with:
command: clippy command: clippy
args: -- -D warnings args: -- -D warnings

2
Cargo.lock generated
View file

@ -245,7 +245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "navi" name = "navi"
version = "2.1.4" version = "2.2.0"
dependencies = [ dependencies = [
"dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "dirs 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"git2 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "git2 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "navi" name = "navi"
version = "2.1.4" version = "2.2.0"
authors = ["Denis Isidoro <denis_isidoro@live.com>"] authors = ["Denis Isidoro <denis_isidoro@live.com>"]
edition = "2018" edition = "2018"
description = "An interactive cheatsheet tool for the command-line" description = "An interactive cheatsheet tool for the command-line"
@ -34,7 +34,11 @@ version = "0.10.0"
default-features = false default-features = false
features = ["vendored-openssl"] features = ["vendored-openssl"]
[[bin]] [lib]
bench = false
path = "src/main.rs"
name = "navi" name = "navi"
path = "src/lib.rs"
[[bin]]
name = "navi"
path = "src/bin/main.rs"
bench = false

View file

@ -39,6 +39,7 @@ Table of contents
* [Advanced variable options](#advanced-variable-options) * [Advanced variable options](#advanced-variable-options)
* [Variable dependency](#variable-dependency) * [Variable dependency](#variable-dependency)
* [Multiline snippets](#multiline-snippets) * [Multiline snippets](#multiline-snippets)
* [Variable as multiple arguments](#variable-as-multiple-arguments)
* [List customization](#list-customization) * [List customization](#list-customization)
* [Related projects](#related-projects) * [Related projects](#related-projects)
* [Etymology](#etymology) * [Etymology](#etymology)
@ -227,6 +228,15 @@ true \
|| echo no || echo no
``` ```
### Variable as multiple arguments
```sh
# This will result into: cat "file1.json" "file2.json"
jsons=($(echo "<jsons>"))
cat "${jsons[@]}"
$ jsons: find . -iname '*.rs' -type f -print --- --multi
```
List customization List customization
------------------ ------------------

View file

@ -17,6 +17,8 @@ cargo fmt || true
header "dot code beautify..." header "dot code beautify..."
find scripts -type f | xargs -I% dot code beautify % || true find scripts -type f | xargs -I% dot code beautify % || true
dot code beautify "${NAVI_HOME}/tests/core.bash" || true
dot code beautify "${NAVI_HOME}/tests/run" || true
header "clippy..." header "clippy..."
cargo clippy || true cargo clippy || true

View file

@ -1,6 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# =====================
# paths
# =====================
FIRST_SOURCE_DIR="/opt/navi" FIRST_SOURCE_DIR="/opt/navi"
SECOND_SOURCE_DIR="${HOME}/.navi" SECOND_SOURCE_DIR="${HOME}/.navi"
@ -27,14 +32,106 @@ get_bin_dir() {
get_dir "$FIRST_BIN_DIR" "$SECOND_BIN_DIR" get_dir "$FIRST_BIN_DIR" "$SECOND_BIN_DIR"
} }
# =====================
# logging
# =====================
echoerr() { echoerr() {
echo "$@" 1>&2 echo "$@" 1>&2
} }
command_exists() { tap() {
type "$1" &>/dev/null local -r x="$(cat)"
echoerr "$x"
echo "$x"
} }
log::ansi() {
local bg=false
case "$@" in
*reset*) echo "\e[0m"; return 0 ;;
*black*) color=30 ;;
*red*) color=31 ;;
*green*) color=32 ;;
*yellow*) color=33 ;;
*blue*) color=34 ;;
*purple*) color=35 ;;
*cyan*) color=36 ;;
*white*) color=37 ;;
esac
case "$@" in
*regular*) mod=0 ;;
*bold*) mod=1 ;;
*underline*) mod=4 ;;
esac
case "$@" in
*background*) bg=true ;;
*bg*) bg=true ;;
esac
if $bg; then
echo "\e[${color}m"
else
echo "\e[${mod:-0};${color}m"
fi
}
_log() {
local template="$1"
shift
echoerr -e "$(printf "$template" "$@")"
}
_header() {
local TOTAL_CHARS=60
local total=$TOTAL_CHARS-2
local size=${#1}
local left=$((($total - $size) / 2))
local right=$(($total - $size - $left))
printf "%${left}s" '' | tr ' ' =
printf " $1 "
printf "%${right}s" '' | tr ' ' =
}
log::header() { _log "\n$(log::ansi bold)$(log::ansi purple)$(_header "$1")$(log::ansi reset)\n"; }
log::success() { _log "$(log::ansi green)✔ %s$(log::ansi reset)\n" "$@"; }
log::error() { _log "$(log::ansi red)✖ %s$(log::ansi reset)\n" "$@"; }
log::warning() { _log "$(log::ansi yellow)➜ %s$(log::ansi reset)\n" "$@"; }
log::note() { _log "$(log::ansi blue)%s$(log::ansi reset)\n" "$@"; }
# TODO: remove
header() {
echoerr "$*"
echoerr
}
die() {
log::error "$@"
exit 42
}
no_binary_warning() {
echoerr "There's no precompiled binary for your platform: $(uname -a)"
}
installation_finish_instructions() {
local -r shell="$(get_shell)"
echoerr -e "Finished. To call navi, restart your shell or reload the config file:\n source ~/.${shell}rc"
local code
if [[ $shell = "zsh" ]]; then
code="navi widget ${shell} | source"
else
code='source <(navi widget '"$shell"')'
fi
echoerr -e "\nTo add the Ctrl-G keybinding, add the following to ~/.${shell}rc:\n ${code}"
}
# =====================
# security
# =====================
sha256() { sha256() {
if command_exists sha256sum; then if command_exists sha256sum; then
sha256sum sha256sum
@ -48,10 +145,10 @@ sha256() {
fi fi
} }
header() {
echoerr "$*" # =====================
echoerr # github
} # =====================
latest_version_released() { latest_version_released() {
curl -s 'https://api.github.com/repos/denisidoro/navi/releases/latest' \ curl -s 'https://api.github.com/repos/denisidoro/navi/releases/latest' \
@ -59,15 +156,6 @@ latest_version_released() {
| sed 's|releases/tag/v||' | sed 's|releases/tag/v||'
} }
version_from_toml() {
cat "${NAVI_HOME}/Cargo.toml" \
| grep version \
| head -n1 \
| awk '{print $NF}' \
| tr -d '"' \
| tr -d "'"
}
asset_url() { asset_url() {
local -r version="$1" local -r version="$1"
local -r variant="${2:-}" local -r variant="${2:-}"
@ -96,6 +184,29 @@ sha_for_asset_on_github() {
curl -sL "$url" | sha256 | awk '{print $1}' curl -sL "$url" | sha256 | awk '{print $1}'
} }
# =====================
# code
# =====================
version_from_toml() {
cat "${NAVI_HOME}/Cargo.toml" \
| grep version \
| head -n1 \
| awk '{print $NF}' \
| tr -d '"' \
| tr -d "'"
}
# =====================
# platform
# =====================
command_exists() {
type "$1" &>/dev/null
}
get_target() { get_target() {
local -r unamea="$(uname -a)" local -r unamea="$(uname -a)"
local -r archi="$(uname -sm)" local -r archi="$(uname -sm)"
@ -114,25 +225,14 @@ get_target() {
echo "$target" echo "$target"
} }
no_binary_warning() {
echoerr "There's no precompiled binary for your platform: $(uname -a)"
}
get_shell() { get_shell() {
echo $SHELL | xargs basename echo $SHELL | xargs basename
} }
installation_finish_instructions() {
local -r shell="$(get_shell)" # =====================
echoerr -e "Finished. To call navi, restart your shell or reload the config file:\n source ~/.${shell}rc" # main
local code # =====================
if [[ $shell = "zsh" ]]; then
code="navi widget ${shell} | source"
else
code='source <(navi widget '"$shell"')'
fi
echoerr -e "\nTo add the Ctrl-G keybinding, add the following to ~/.${shell}rc:\n ${code}"
}
install_navi() { install_navi() {
export SOURCE_DIR="${SOURCE_DIR:-"$(get_source_dir)"}" export SOURCE_DIR="${SOURCE_DIR:-"$(get_source_dir)"}"

7
src/bin/main.rs Normal file
View file

@ -0,0 +1,7 @@
extern crate navi;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
navi::handle_config(navi::config_from_env())
}

View file

@ -6,12 +6,22 @@ use std::process::{Command, Stdio};
fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> String { fn get_column(text: String, column: Option<u8>, delimiter: Option<&str>) -> String {
if let Some(c) = column { if let Some(c) = column {
let mut result = String::from("");
let re = regex::Regex::new(delimiter.unwrap_or(r"\s\s+")).unwrap(); let re = regex::Regex::new(delimiter.unwrap_or(r"\s\s+")).unwrap();
let mut parts = re.split(text.as_str()); for line in text.split('\n') {
for _ in 0..(c - 1) { if (&line).is_empty() {
parts.next().unwrap(); continue;
}
let mut parts = re.split(line);
for _ in 0..(c - 1) {
parts.next().unwrap();
}
if !result.is_empty() {
result.push('\n');
}
result.push_str(parts.next().unwrap_or(""));
} }
parts.next().unwrap().to_string() result
} else { } else {
text text
} }
@ -134,21 +144,28 @@ fn parse_output_single(mut text: String, suggestion_type: SuggestionType) -> Str
SuggestionType::MultipleSelections SuggestionType::MultipleSelections
| SuggestionType::Disabled | SuggestionType::Disabled
| SuggestionType::SnippetSelection => { | SuggestionType::SnippetSelection => {
text.truncate(text.len() - 1); let len = text.len();
if len > 1 {
text.truncate(len - 1);
}
text text
} }
SuggestionType::SingleRecommendation => { SuggestionType::SingleRecommendation => {
let lines: Vec<&str> = text.lines().collect(); let lines: Vec<&str> = text.lines().collect();
match (lines.get(0), lines.get(1), lines.get(2)) { match (lines.get(0), lines.get(1), lines.get(2)) {
(Some(one), Some(termination), Some(two)) if *termination == "enter" => { (Some(one), Some(termination), Some(two))
if *termination == "enter" || termination.is_empty() =>
{
if two.is_empty() { if two.is_empty() {
(*one).to_string() (*one).to_string()
} else { } else {
(*two).to_string() (*two).to_string()
} }
} }
(Some(one), Some(termination), None) if *termination == "enter" => { (Some(one), Some(termination), None)
if *termination == "enter" || termination.is_empty() =>
{
(*one).to_string() (*one).to_string()
} }
(Some(one), Some(termination), _) if *termination == "tab" => (*one).to_string(), (Some(one), Some(termination), _) if *termination == "tab" => (*one).to_string(),

View file

@ -12,8 +12,5 @@ mod structures;
mod terminal; mod terminal;
mod welcome; mod welcome;
use std::error::Error; pub use handler::handle_config;
pub use structures::option::{config_from_env, config_from_iter};
fn main() -> Result<(), Box<dyn Error>> {
handler::handle_config(structures::option::config_from_env())
}

View file

@ -99,11 +99,9 @@ fn read_file(
} }
let line = l.unwrap(); let line = l.unwrap();
let hash = line.hash_line();
if visited_lines.contains(&hash) { // duplicate
continue; if !tags.is_empty() && !comment.is_empty() {}
}
visited_lines.insert(hash);
// blank // blank
if line.is_empty() { if line.is_empty() {
@ -136,13 +134,17 @@ fn read_file(
let (variable, command, opts) = parse_variable_line(&line); let (variable, command, opts) = parse_variable_line(&line);
variables.insert(&tags, &variable, (String::from(command), opts)); variables.insert(&tags, &variable, (String::from(command), opts));
} }
// first snippet line // snippet
else if (&snippet).is_empty() {
snippet.push_str(&line);
}
// other snippet lines
else { else {
snippet.push_str(display::LINE_SEPARATOR); 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); snippet.push_str(&line);
} }
} }

View file

@ -5,25 +5,17 @@
# escape code + subshell # escape code + subshell
echo -ne "\033]0;$(hostname)\007" echo -ne "\033]0;$(hostname)\007"
# sed with replacement
echo "8.8.8.8 via 172.17.0.1 dev eth0 src 172.17.0.2" | sed -E 's/.*src ([0-9.]+).*/\1/p' | head -n1
# trivial case
echo "foo"
# multiline command: without backslash
echo "foo"
echo "bar"
# env var # env var
echo "$HOME" echo "$HOME"
# multiline command: with backslash # multi + column
echo 'lorem ipsum' myfn() {
echo "foo" \ for i in $@; do
| grep -q "bar" \ echo -e "arg: $i\n"
&& echo "match" \ done
|| echo "no match" }
folders=($(echo "<multi_col>"))
myfn "${folders[@]}"
# second column: default delimiter # second column: default delimiter
echo "<table_elem> is cool" echo "<table_elem> is cool"
@ -37,17 +29,15 @@ echo "I like these languages: "$(printf '%s' "<langs>" | tr '\n' ',' | sed 's/,/
# return multiple results: multiple words # return multiple results: multiple words
echo "I like these examples: "$(printf '%s' "<examples>" | sed 's/^..*$/"&"/' | awk 1 ORS=', ' | sed 's/, $//')"" echo "I like these examples: "$(printf '%s' "<examples>" | sed 's/^..*$/"&"/' | awk 1 ORS=', ' | sed 's/, $//')""
# multiple replacements # multiple replacements -> "foo"
echo "<x> <y> <x> <z>" echo "<x> <y> <x> <z>"
# multi-word
echo "<multiword>"
$ x: echo '1 2 3' | tr ' ' '\n' $ x: echo '1 2 3' | tr ' ' '\n'
$ y: echo 'a b c' | tr ' ' '\n' $ y: echo 'a b c' | tr ' ' '\n'
$ z: echo 'foo bar' | tr ' ' '\n' $ z: echo 'foo bar' | tr ' ' '\n'
$ table_elem: echo -e '0 rust rust-lang.org\n1 clojure clojure.org' --- --column 2 $ table_elem: echo -e '0 rust rust-lang.org\n1 clojure clojure.org' --- --column 2
$ table_elem2: echo -e '0;rust;rust-lang.org\n1;clojure;clojure.org' --- --column 2 --delimiter ';' $ table_elem2: echo -e '0;rust;rust-lang.org\n1;clojure;clojure.org' --- --column 2 --delimiter ';'
$ multi_col: ls -la | awk '{print $1, $9}' --- --column 2 --delimiter '\s' --multi
$ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi $ langs: echo 'clojure rust javascript' | tr ' ' '\n' --- --multi
$ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi $ examples: echo -e 'foo bar\nlorem ipsum\ndolor sit' --- --multi
$ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz' $ multiword: echo -e 'foo bar\nlorem ipsum\ndolor sit\nbaz'

66
tests/core.bash Normal file
View file

@ -0,0 +1,66 @@
#!/usr/bin/env bash
# vim: filetype=sh
source "${NAVI_HOME}/scripts/install"
PASSED=0
FAILED=0
SKIPPED=0
SUITE=""
test::set_suite() {
SUITE="$*"
}
test::success() {
PASSED=$((PASSED+1))
log::success "Test passed!"
}
test::fail() {
FAILED=$((FAILED+1))
log::error "Test failed..."
return
}
test::skip() {
echo
log::note "${SUITE:-unknown} - ${1:-unknown}"
SKIPPED=$((SKIPPED+1))
log::warning "Test skipped..."
return
}
test::run() {
echo
log::note "${SUITE:-unknown} - ${1:-unknown}"
shift
"$@" && test::success || test::fail
}
test::equals() {
local -r actual="$(cat)"
local -r expected="$(echo "${1:-}")"
local -r actual2="$(echo "$actual" | xargs | sed -E 's/\s/ /g')"
local -r expected2="$(echo "$expected" | xargs | sed -E 's/\s/ /g')"
if [[ "$actual2" != "$expected2" ]]; then
log::error "Expected '${expected}' but got '${actual}'"
return 2
fi
}
test::finish() {
echo
if [ $SKIPPED -gt 0 ]; then
log::warning "${SKIPPED} tests skipped!"
fi
if [ $FAILED -gt 0 ]; then
log::error "${PASSED} tests passed but ${FAILED} failed... :("
exit "${FAILED}"
else
log::success "All ${PASSED} tests passed! :)"
exit 0
fi
}

View file

@ -0,0 +1,42 @@
; author: CI/CD
% test, ci/cd
# trivial case -> "foo"
echo "foo"
# sed with replacement -> "172.17.0.2"
echo "8.8.8.8 via 172.17.0.1 dev eth0 src 172.17.0.2" | sed -E 's/.*src ([0-9.]+).*/\1/p' | head -n1
# multiline command: no backslash -> "foo\nbar"
echo "foo"
echo "bar"
# multiline command: with backslash -> "lorem ipsum\nno match"
echo 'lorem ipsum'
echo "foo" \
| grep -q "bar" \
&& echo "match" \
|| echo "no match"
# 2nd column with default delimiter -> "rust is cool"
echo "<language> is cool"
# 2nd column with custom delimiter -> "clojure is cool"
echo "<language2> is cool"
# multiple words -> "lorem foo bar ipsum"
echo "lorem <multiword> ipsum"
# variable dependency -> "2 12 a 2"
echo "<x> <x2> <y> <x>"
$ x: echo '2'
$ x2: echo "$((x+10))"
$ y: echo 'a'
$ language: echo '0 rust rust-lang.org' --- --column 2
$ language2: echo '1;clojure;clojure.org' --- --column 2 --delimiter ';'
$ multiword: echo 'foo bar'
# this should be displayed -> "hi"
echo hi

58
tests/run Executable file
View file

@ -0,0 +1,58 @@
#!/usr/bin/env bash
export NAVI_HOME="$(cd "$(dirname "$0")/.." && pwd)"
source "${NAVI_HOME}/tests/core.bash"
TEST_CHEAT_PATH="${NAVI_HOME}/tests/no_prompt_cheats"
_navi() {
stty sane || true
local -r navi="./target/debug/navi"
local filter="$*"
local filter="${filter::-2}"
RUST_BACKTRACE=1 NAVI_PATH="$TEST_CHEAT_PATH" "$navi" best "$filter"
}
_navi_test() {
_navi "$1" \
| test::equals "$2"
}
_get_all_tests() {
cat "${TEST_CHEAT_PATH}/cases.cheat" \
| grep '#' \
| grep ' ->' \
| sed 's/\\n/ /g' \
| sed -E 's/# (.*) -> "(.*)"/\1|\2/g'
}
_get_tests() {
local -r filter="$1"
if [ -n "$filter" ]; then
_get_all_tests \
| grep "$filter"
else
_get_all_tests
fi
}
if ! command_exists fzf; then
export PATH="$PATH:$HOME/.fzf/bin"
fi
cd "$NAVI_HOME"
filter="${1:-}"
test::set_suite "cases"
ifs="$IFS"
IFS=$'\n'
for i in $(_get_tests "$filter"); do
IFS="$ifs"
query="$(echo "$i" | cut -d'|' -f1)"
expected="$(echo "$i" | cut -d'|' -f2)"
test::run "$query" _navi_test "$query" "$expected"
done
test::finish

View file

@ -2,6 +2,9 @@
mod tests { mod tests {
#[test] #[test]
fn it_works() { fn it_works() {
assert_eq!(2 + 2, 4); //let _x = navi::handle_config(navi::config_from_iter(
//"navi best trivial".split(' ').collect(),
//));
// assert_eq!(x, 3);
} }
} }