summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xnavi2
-rw-r--r--src/arg.sh35
-rwxr-xr-xsrc/cheat.sh37
-rw-r--r--src/cmd.sh41
-rw-r--r--src/coll.sh4
-rw-r--r--src/dict.sh27
-rw-r--r--src/main.sh77
-rw-r--r--src/misc.sh8
-rw-r--r--src/opts.sh8
-rw-r--r--src/str.sh8
-rw-r--r--src/ui.sh21
-rw-r--r--test/dict_test.sh8
12 files changed, 151 insertions, 125 deletions
diff --git a/navi b/navi
index ec77421..f5d8af9 100755
--- a/navi
+++ b/navi
@@ -37,7 +37,7 @@ source "${SCRIPT_DIR}/src/main.sh"
##? full docs
##? Please refer to the README at https://github.com/denisidoro/navi
-VERSION="0.11.1"
+VERSION="0.12.0"
NAVI_ENV="${NAVI_ENV:-prod}"
opts::eval "$@"
diff --git a/src/arg.sh b/src/arg.sh
index 7c7d513..cebc8f3 100644
--- a/src/arg.sh
+++ b/src/arg.sh
@@ -1,13 +1,9 @@
#!/usr/bin/env bash
-ARG_REGEX_WITHOUT_BRACKETS="[a-zA-Z_]+([- ]?\w+)*"
-ARG_REGEX="<${ARG_REGEX_WITHOUT_BRACKETS}>"
-ARG_DELIMITER="\f"
-ARG_DELIMITER_2="\v"
-ARG_DELIMITER_3="\r"
+ARG_REGEX="<[a-zA-Z_]+([- ]?\w+)*>"
arg::dict() {
- local -r input="$(cat | sed 's/\\n/\\f/g')"
+ local -r input="$(cat)"
local -r fn="$(echo "$input" | awk -F'---' '{print $1}')"
local -r opts="$(echo "$input" | awk -F'---' '{print $2}')"
@@ -15,6 +11,12 @@ arg::dict() {
dict::new fn "$fn" opts "$opts"
}
+arg::escape() {
+ echo "$*" \
+ | tr '-' '_' \
+ | tr ' ' '_'
+}
+
arg::interpolate() {
local -r arg="$1"
local -r value="$2"
@@ -40,12 +42,19 @@ arg::deserialize() {
arg="${arg:1:${#arg}-2}"
echo "$arg" \
- | tr "${ARG_DELIMITER}" " " \
- | tr "${ARG_DELIMITER_2}" "'" \
- | tr "${ARG_DELIMITER_3}" '"'
+ | tr "${ESCAPE_CHAR}" " " \
+ | tr "${ESCAPE_CHAR_2}" "'" \
+ | tr "${ESCAPE_CHAR_3}" '"'
+}
+
+arg::serialize_code() {
+ printf "tr ' ' '${ESCAPE_CHAR}'"
+ printf " | "
+ printf "tr \"'\" '${ESCAPE_CHAR_2}'"
+ printf " | "
+ printf "tr '\"' '${ESCAPE_CHAR_3}'"
}
-# TODO: separation of concerns
arg::pick() {
local -r arg="$1"
local -r cheat="$2"
@@ -54,7 +63,7 @@ arg::pick() {
local -r length="$(echo "$prefix" | str::length)"
local -r arg_dict="$(echo "$cheat" | grep "$prefix" | str::sub $((length + 1)) | arg::dict)"
- local -r fn="$(dict::get "$arg_dict" fn | sed 's/\\f/\\n/g')"
+ local -r fn="$(dict::get "$arg_dict" fn)"
local -r args_str="$(dict::get "$arg_dict" opts)"
local arg_name=""
@@ -70,10 +79,10 @@ arg::pick() {
if [ -n "$fn" ]; then
local suggestions="$(eval "$fn" 2>/dev/null)"
if [ -n "$suggestions" ]; then
- echo "$suggestions" | ui::pick --prompt "$arg: " --header-lines "${headers:-0}" | str::column "${column:-}"
+ echo "$suggestions" | ui::fzf --prompt "$arg: " --header-lines "${headers:-0}" | str::column "${column:-}"
fi
elif ${NAVI_USE_FZF_ALL_INPUTS:-false}; then
- echo "" | ui::pick --prompt "$arg: " --print-query --no-select-1 --height 1
+ echo "" | ui::fzf --prompt "$arg: " --print-query --no-select-1 --height 1
else
printf "\033[0;36m${arg}:\033[0;0m " > /dev/tty
read -r value
diff --git a/src/cheat.sh b/src/cheat.sh
index 53fb17c..f9f1f4d 100755
--- a/src/cheat.sh
+++ b/src/cheat.sh
@@ -6,25 +6,26 @@ cheat::find() {
done
}
-cheat::_join_multiline_using_sed() {
- tr '\n' '\f' \
- | sed -E 's/\\\f *//g' \
- | tr '\f' '\n'
+cheat::export_cache() {
+ if [ -z "${NAVI_CACHE:-}" ]; then
+ export NAVI_CACHE="$*"
+ fi
}
-cheat::_join_multiline() {
- if ${NAVI_USE_PERL:-false}; then
- perl -0pe 's/\\\n *//g' \
- || cheat::_join_multiline_using_sed
+cheat::join_lines() {
+ if command_exists perl; then
+ perl -0pe 's/\\\n *//g'
else
- cheat::_join_multiline_using_sed
+ tr '\n' "$ESCAPE_CHAR" \
+ | sed -E 's/\\'$(printf "$ESCAPE_CHAR")' *//g' \
+ | tr "$ESCAPE_CHAR" '\n'
fi
}
cheat::read_all() {
for cheat in $(cheat::find); do
echo
- cat "$cheat" | cheat::_join_multiline
+ cat "$cheat"
echo
done
}
@@ -34,16 +35,13 @@ cheat::memoized_read_all() {
echo "$NAVI_CACHE"
return
fi
- if command_exists perl; then
- export NAVI_USE_PERL=true
- else
- export NAVI_USE_PERL=false
- fi
+
local -r cheats="$(cheat::read_all)"
- echo "$cheats"
+ echo "$cheats" \
+ | cheat::join_lines
}
-cheat::pretty() {
+cheat::prettify() {
awk 'function color(c,s) {
printf("\033[%dm%s\033[0m",30+c,s)
}
@@ -54,7 +52,7 @@ cheat::pretty() {
NF { print color(7, $0) color(60, tags); next }'
}
-cheat::_until_percentage() {
+cheat::until_percentage() {
awk 'BEGIN { count=0; }
/^%/ { if (count >= 1) exit;
@@ -70,6 +68,5 @@ cheat::from_selection() {
echo "$cheats" \
| grep "% ${tags}" -A99999 \
- | cheat::_until_percentage \
- || (echoerr "No valid cheatsheet!"; exit 67)
+ | cheat::until_percentage
}
diff --git a/src/cmd.sh b/src/cmd.sh
new file mode 100644
index 0000000..8e74282
--- /dev/null
+++ b/src/cmd.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+
+cmd::loop() {
+ local -r cmd="$1"
+ local -r cheat="$2"
+
+ local arg escaped_arg value escaped_cmd
+
+ arg="$(echo "$cmd" | arg::next)"
+ if [ -z "$arg" ]; then
+ dict::new cmd "$cmd"
+ return
+ fi
+
+ escaped_arg="$(arg::escape "$arg")"
+
+ escaped_cmd="$(echo "$cmd" | sed "s|<${arg}>|<${escaped_arg}>|g")"
+ arg="$escaped_arg"
+
+ local -r values="$(dict::get "$OPTIONS" values)"
+ value="$(echo "$values" | coll::get $i)"
+ [ -z "$value" ] && value="$(arg::pick "$arg" "$cheat")"
+
+ dict::new \
+ cmd "${escaped_cmd:-}" \
+ value "$value" \
+ arg "$arg"
+}
+
+cmd::finish() {
+ local -r cmd="$1"
+
+ local -r unresolved_arg="$(echo "$cmd" | arg::next)"
+
+ local -r print="$(dict::get "$OPTIONS" print)"
+ if $print || [ -n "$unresolved_arg" ]; then
+ echo "$cmd"
+ else
+ eval "$cmd"
+ fi
+} \ No newline at end of file
diff --git a/src/coll.sh b/src/coll.sh
index 58240eb..af2071e 100644
--- a/src/coll.sh
+++ b/src/coll.sh
@@ -38,7 +38,7 @@ coll::remove() {
done
}
-coll::_without_empty_line() {
+coll::without_empty_line() {
local -r input="$(cat)"
local -r words="$(echo "$input" | wc -w | xargs)"
if [[ $words > 0 ]]; then
@@ -47,7 +47,7 @@ coll::_without_empty_line() {
}
coll::add() {
- cat | coll::_without_empty_line
+ cat | coll::without_empty_line
for x in "$@"; do
echo "$x"
done
diff --git a/src/dict.sh b/src/dict.sh
index 5492504..df5f6ab 100644
--- a/src/dict.sh
+++ b/src/dict.sh
@@ -7,13 +7,11 @@
# values with non-trivial whitespaces (newlines, subsequent spaces, etc)
# aren't handled very well
-DICT_DELIMITER='\f'
-
dict::new() {
if [ $# = 0 ]; then
echo ""
else
- echo "" | dict::assoc "$@" | sed '/^$/d'
+ echo "" | dict::assoc "$@" | str::remove_empty_lines
fi
}
@@ -23,17 +21,17 @@ dict::dissoc() {
grep -Ev "^[\s]*${key}[^:]*:"
}
-dict::_escape_value() {
- tr '\n' "$DICT_DELIMITER" | sed "s/\\n/${DICT_DELIMITER}/g"
+dict::escape_value() {
+ tr '\n' "$ESCAPE_CHAR" | sed 's/\\n/'$(printf "$ESCAPE_CHAR")'/g'
}
-str::_without_trailing_newline() {
+str::without_trailing_newline() {
printf "%s" "$(cat)"
echo
}
-dict::_unescape_value() {
- tr "$DICT_DELIMITER" '\n' | str::_without_trailing_newline
+dict::unescape_value() {
+ tr "$ESCAPE_CHAR" '\n' | str::without_trailing_newline
}
dict::assoc() {
@@ -41,11 +39,11 @@ dict::assoc() {
local -r input="$(cat)"
if [ -z $key ]; then
- printf "$(echo "$input" | tr '%' '\v')" | tr '\v' '%'
+ printf "$(echo "$input" | tr '%' "$ESCAPE_CHAR_2")" | tr "$ESCAPE_CHAR_2" '%'
return
fi
- local -r value="$(echo "${2:-}" | dict::_escape_value)"
+ local -r value="$(echo "${2:-}" | dict::escape_value)"
shift 2
echo "$(echo "$input" | dict::dissoc "$key")${key}: ${value}\n" | dict::assoc "$@"
@@ -65,9 +63,9 @@ dict::get() {
local -r matches="$(echo "$result" | wc -l || echo 0)"
if [ $matches -gt 1 ]; then
- echo "$result" | dict::_unescape_value
+ echo "$result" | dict::unescape_value
else
- echo "$result" | sed -E "s/${prefix}//" | dict::_unescape_value
+ echo "$result" | sed -E "s/${prefix}//" | dict::unescape_value
fi
}
@@ -81,11 +79,6 @@ dict::values() {
| cut -c3-
}
-dict::merge() {
- awk -F':' '{$1=""; print $0}' \
- | cut -c3-
-}
-
dict::zipmap() {
IFS='\n'
diff --git a/src/main.sh b/src/main.sh
index 620558a..dfc9576 100644
--- a/src/main.sh
+++ b/src/main.sh
@@ -6,6 +6,7 @@ fi
source "${SCRIPT_DIR}/src/arg.sh"
source "${SCRIPT_DIR}/src/cheat.sh"
+source "${SCRIPT_DIR}/src/cmd.sh"
source "${SCRIPT_DIR}/src/coll.sh"
source "${SCRIPT_DIR}/src/dict.sh"
source "${SCRIPT_DIR}/src/health.sh"
@@ -18,44 +19,25 @@ source "${SCRIPT_DIR}/src/ui.sh"
handler::main() {
local -r cheats="$(cheat::memoized_read_all)"
-
- if [ -z "${NAVI_CACHE:-}" ]; then
- export NAVI_CACHE="$cheats"
- fi
-
+ cheat::export_cache "$cheats"
local -r selection="$(ui::select "$cheats")"
local -r cheat="$(cheat::from_selection "$cheats" "$selection")"
-
- [ -z "$cheat" ] && exit 67
+ [ -z "$cheat" ] && die "No valid cheatsheet!"
local -r interpolation="$(dict::get "$OPTIONS" interpolation)"
- local cmd="$(selection::cmd "$selection" "$cheat")"
- local arg value
- local -r args="$(dict::get "$OPTIONS" args)"
+ local cmd="$(selection::cmd "$selection" "$cheat")"
+ local result arg value
local i=0
while $interpolation; do
- arg="$(echo "$cmd" | arg::next || echo "")"
- if [ -z "$arg" ]; then
- break
- fi
-
- escaped_arg="$(echo "$arg" | tr '-' '_' | tr ' ' '_')"
- if ! [[ $escaped_arg =~ $ARG_REGEX_WITHOUT_BRACKETS ]]; then
- exit 1
- fi
-
- cmd="$(echo "$cmd" | sed "s|<${arg}>|<${escaped_arg}>|g")"
- arg="$escaped_arg"
-
- value="$(echo "$args" | coll::get $i)"
- [ -z "$value" ] && value="$(arg::pick "$arg" "$cheat")"
+ result="$(cmd::loop "$cmd" "$cheat")"
+ arg="$(dict::get "$result" arg)"
+ value="$(dict::get "$result" value)"
+ cmd="$(dict::get "$result" cmd)"
- if [ -z "$value" ]; then
- echoerr "Unable to fetch suggestions for '$arg'!"
- exit 1
- fi
+ [ -z "$arg" ] && break
+ [ -z "$value" ] && die "Unable to fetch suggestions for '$arg'!"
eval "local $arg"='$value'
cmd="$(echo "$cmd" | arg::interpolate "$arg" "$value")"
@@ -63,14 +45,7 @@ handler::main() {
i=$((i+1))
done
- local -r unresolved_arg="$(echo "$cmd" | arg::next || echo "")"
-
- local -r print="$(dict::get "$OPTIONS" print)"
- if $print || [ -n "$unresolved_arg" ]; then
- echo "$cmd"
- else
- eval "$cmd"
- fi
+ cmd::finish "$cmd"
}
handler::preview() {
@@ -82,7 +57,7 @@ handler::preview() {
}
handler::help() {
- echo "$TEXT"
+ opts::extract_help "$0"
}
handler::version() {
@@ -92,7 +67,8 @@ handler::version() {
if $full; then
source "${SCRIPT_DIR}/src/version.sh"
- version::code 2>/dev/null || echo "unknown code"
+ version::code 2>/dev/null \
+ || die "unknown code"
fi
}
@@ -106,18 +82,23 @@ handler::home() {
handler::widget() {
local widget
+ local -r print="$(dict::get "$OPTIONS" print)"
case "$SH" in
zsh) widget="${SCRIPT_DIR}/navi.plugin.zsh" ;;
bash) widget="${SCRIPT_DIR}/navi.plugin.bash" ;;
- *) echoerr "Invalid shell: $SH"; exit 1 ;;
+ *) die "Invalid shell: $SH" ;;
esac
- if "$(dict::get "$OPTIONS" print)"; then
- cat "$widget"
- else
- echo "$widget"
- fi
+ $print \
+ && cat "$widget" \
+ || echo "$widget"
+}
+
+handler::search() {
+ local -r query="$(dict::get "$OPTIONS" query)"
+ search::save "$query" || true
+ handler::main
}
main() {
@@ -125,13 +106,11 @@ main() {
preview)
local -r query="$(dict::get "$OPTIONS" query)"
handler::preview "$query" \
- || echo "Unable to find command for '$query'"
+ || echoerr "Unable to find command for '$query'"
;;
search)
health::fzf
- local -r query="$(dict::get "$OPTIONS" query)"
- search::save "$query" || true
- handler::main
+ handler::search
;;
version)
handler::version false
diff --git a/src/misc.sh b/src/misc.sh
index c1b8524..4acdce8 100644
--- a/src/misc.sh
+++ b/src/misc.sh
@@ -10,6 +10,7 @@ command_exists() {
}
platform::existing_command() {
+ local cmd
for cmd in "$@"; do
if command_exists "$cmd"; then
echo "$cmd"
@@ -25,11 +26,16 @@ echoerr() {
url::open() {
local -r cmd="$(platform::existing_command "${BROWSER:-}" xdg-open open google-chrome firefox)"
- "$cmd" "$@"
+ "$cmd" "$@" & disown
}
tap() {
local -r input="$(cat)"
echoerr "$input"
echo "$input"
+}
+
+die() {
+ echoerr "$@"
+ exit 42
} \ No newline at end of file
diff --git a/src/opts.sh b/src/opts.sh
index e1cba28..ab9666c 100644
--- a/src/opts.sh
+++ b/src/opts.sh
@@ -16,12 +16,12 @@ opts::eval() {
local autoselect=true
local best=false
local query=""
- local args=""
+ local values=""
case "${1:-}" in
--version|version) entry_point="version"; shift ;;
--full-version|full-version) entry_point="full-version"; shift ;;
- --help|help) entry_point="help"; TEXT="$(opts::extract_help "$0")"; shift ;;
+ --help|help) entry_point="help"; shift ;;
search) entry_point="search"; wait_for="search"; shift ;;
preview) entry_point="preview"; wait_for="preview"; shift ;;
query|q) wait_for="query"; shift ;;
@@ -46,7 +46,7 @@ opts::eval() {
--no-preview) preview=false ;;
--path|--dir) wait_for="path" ;;
--no-autoselect) autoselect=false ;;
- *) args="$(echo "$args" | coll::add "$arg")" ;;
+ *) values="$(echo "$values" | coll::add "$arg")" ;;
esac
done
@@ -58,7 +58,7 @@ opts::eval() {
autoselect "$autoselect" \
query "$query" \
best "$best" \
- args "$args")"
+ values "$values")"
export NAVI_PATH="$path"
}
diff --git a/src/str.sh b/src/str.sh
index e7e9a4f..abd50d1 100644
--- a/src/str.sh
+++ b/src/str.sh
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
+ESCAPE_CHAR="\034"
+ESCAPE_CHAR_2="\035"
+ESCAPE_CHAR_3="\036"
+
str::length() {
awk '{print length}'
}
@@ -54,4 +58,8 @@ str::not_empty() {
else
return 1
fi
+}
+
+str::remove_empty_lines() {
+ sed '/^$/d'
} \ No newline at end of file
diff --git a/src/ui.sh b/src/ui.sh
index 3f7fc48..98f61d6 100644
--- a/src/ui.sh
+++ b/src/ui.sh
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
-ui::pick() {
+ui::fzf() {
local -r autoselect="$(dict::get "$OPTIONS" autoselect)"
- declare -a args
+ local args
args+=("--height")
args+=("100%")
if ${autoselect:-false}; then
@@ -14,12 +14,11 @@ ui::pick() {
"$fzf_cmd" "${args[@]:-}" --inline-info "$@"
}
-# TODO: separation of concerns
ui::select() {
local -r cheats="$1"
local -r script_path="${SCRIPT_DIR}/navi"
- local -r preview_cmd="echo \'{}\' | tr \"'\" '${ARG_DELIMITER_2}' | tr ' ' '${ARG_DELIMITER}' | tr '\"' '${ARG_DELIMITER_3}' | xargs -I% \"${script_path}\" preview %"
+ local -r preview_cmd="echo \'{}\' | $(arg::serialize_code) | xargs -I% \"${script_path}\" preview %"
local -r query="$(dict::get "$OPTIONS" query)"
local -r entry_point="$(dict::get "$OPTIONS" entry_point)"
@@ -42,18 +41,10 @@ ui::select() {
args+=("--header"); args+=("Displaying online results. Please refer to 'navi --help' for details")
fi
- ui::_select_post() {
- if $best; then
- head -n1
- else
- cat
- fi
- }
-
echo "$cheats" \
- | cheat::pretty \
- | ui::pick "${args[@]}" \
- | ui::_select_post \
+ | cheat::prettify \
+ | ui::fzf "${args[@]}" \
+ | ($best && head -n1 || cat) \
| selection::dict
}
diff --git a/test/dict_test.sh b/test/dict_test.sh
index c5a3311..f310bec 100644
--- a/test/dict_test.sh
+++ b/test/dict_test.sh
@@ -6,8 +6,8 @@ inc() {
}
test::map_equals() {
- local -r actual="$(cat | dict::_unescape_value | sort)"
- local -r expected="$(dict::new "$@" | dict::_unescape_value | sort)"
+ local -r actual="$(cat | dict::unescape_value | sort)"
+ local -r expected="$(dict::new "$@" | dict::unescape_value | sort)"
echo "$actual" | test::equals "$expected"
}
@@ -15,7 +15,9 @@ test::map_equals() {
dict_assoc() {
dict::new \
| dict::assoc "foo" "42" \
- | tr -d '\f' \
+ | tr -d "$ESCAPE_CHAR" \
+ | tr -d "$ESCAPE_CHAR_2" \
+ | tr -d "$ESCAPE_CHAR_3" \
| test::equals "foo: 42"
}