diff --git a/cheats/git.cheat b/cheats/git.cheat index 957239b..61933f8 100644 --- a/cheats/git.cheat +++ b/cheats/git.cheat @@ -58,4 +58,7 @@ git clean -dxf # Sign all commits in a branch based on master git rebase master -S -f +# See all open pull requests of a user on Github +url::open 'https://github.com/pulls?&q=author:+is:open+is:pr' + $ branch: git branch | awk '{print $NF}' diff --git a/navi b/navi index b442a54..8216c5d 100755 --- a/navi +++ b/navi @@ -36,6 +36,7 @@ source "${SCRIPT_DIR}/src/main.sh" ##? Please refer to the README at https://github.com/denisidoro/navi VERSION="0.8.1" -opts::eval "$@" +OPTIONS="$(opts::eval "$@")" +export NAVI_PATH="$(dict::get "$OPTIONS" path)" main "$@" diff --git a/scripts/install b/scripts/install index 43d2b31..5219540 100755 --- a/scripts/install +++ b/scripts/install @@ -4,8 +4,8 @@ set -euo pipefail export SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" script() { - echo "#!/usr/bin/env bash" - echo "${SCRIPT_DIR}/navi" '"$@"' + echo "#!/usr/bin/env bash" + echo "${SCRIPT_DIR}/navi" '"$@"' } BIN="/usr/local/bin/navi" diff --git a/scripts/lint b/scripts/lint index 2f4b94a..eb029f0 100755 --- a/scripts/lint +++ b/scripts/lint @@ -3,6 +3,10 @@ set -euo pipefail export SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +# please refer to https://github.com/denisidoro/dotfiles/blob/master/scripts/code/beautify + cd "${SCRIPT_DIR}" find . -iname '*.sh' | xargs -I% dot code beautify % +find scripts/* | xargs -I% dot code beautify % +dot code beautify "${SCRIPT_DIR}/test/run" dot code beautify "${SCRIPT_DIR}/navi" diff --git a/scripts/release b/scripts/release index 8a77e0c..e4ba93f 100755 --- a/scripts/release +++ b/scripts/release @@ -5,21 +5,21 @@ export SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" source "${SCRIPT_DIR}/src/main.sh" sha256() { - if command_exists sha256sum; then - sha256sum - elif command_exists shasum; then - shasum -a 256 - elif command_exists openssl; then - openssl dgst -sha256 - else - echoerr "Unable to calculate sha256!" - exit 43 - fi + if command_exists sha256sum; then + sha256sum + elif command_exists shasum; then + shasum -a 256 + elif command_exists openssl; then + openssl dgst -sha256 + else + echoerr "Unable to calculate sha256!" + exit 43 + fi } header() { - echo "$*" - echo + echo "$*" + echo } cd "$SCRIPT_DIR" diff --git a/src/arg.sh b/src/arg.sh index c3c2165..257979f 100644 --- a/src/arg.sh +++ b/src/arg.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash ARG_REGEX="<[0-9a-zA-Z_]+>" +ARG_DELIMITER="£" -arg::fn() { - awk -F'---' '{print $1}' -} +arg::dict() { + local -r fn="$(awk -F'---' '{print $1}')" + local -r opts="$(awk -F'---' '{print $2}')" -arg::opts() { - awk -F'---' '{print $2}' + dict::new fn "$fn" opts "$opts" } arg::interpolate() { @@ -19,21 +19,35 @@ arg::interpolate() { arg::next() { grep -Eo "$ARG_REGEX" \ + | xargs \ | head -n1 \ | tr -d '<' \ | tr -d '>' } +arg::deserialize() { + local -r arg="$1" + + if [ ${arg:0:1} = "'" ]; then + local -r out="$(echo "${arg:1:${#arg}-2}")" + else + local -r out="$arg" + fi + + echo "$out" | sed "s/${ARG_DELIMITER}/ /g" +} + +# TODO: separation of concerns arg::pick() { local -r arg="$1" local -r cheat="$2" local -r prefix="$ ${arg}:" local -r length="$(echo "$prefix" | str::length)" - local -r arg_description="$(grep "$prefix" "$cheat" | str::sub $((length + 1)))" + local -r arg_dict="$(grep "$prefix" "$cheat" | str::sub $((length + 1)) | arg::dict)" - local -r fn="$(echo "$arg_description" | arg::fn)" - local -r args_str="$(echo "$arg_description" | arg::opts | tr ' ' '\n' || echo "")" + local -r fn="$(dict::get "$arg_dict" fn)" + local -r args_str="$(dict::get "$arg_dict" opts | tr ' ' '\n' || echo "")" local arg_name="" for arg_str in $args_str; do diff --git a/src/cheat.sh b/src/cheat.sh index 1817147..d79490e 100755 --- a/src/cheat.sh +++ b/src/cheat.sh @@ -24,7 +24,7 @@ cheat::from_selection() { local -r cheats="$1" local -r selection="$2" - local -r tags="$(echo "$selection" | selection::tags)" + local -r tags="$(dict::get "$selection" tags)" for cheat in $cheats; do if grep -q "% $tags" "$cheat"; then diff --git a/src/dict.sh b/src/dict.sh new file mode 100644 index 0000000..4794425 --- /dev/null +++ b/src/dict.sh @@ -0,0 +1,110 @@ +#!/usr/bin/env bash + +DICT_DELIMITER='\f' + +dict::_post() { + sed -E 's/; /\\n/g' | awk 'NF > 0' | dict::_unescape_value | sort +} + +dict::new() { + if [ $# = 0 ]; then + echo "" + else + echo "" | dict::assoc "$@" + fi +} + +dict::dissoc() { + local -r key="$1" + + grep -Ev "^${key}[^:]*:" | dict::_post +} + +dict::_escape_value() { + tr '\n' "$DICT_DELIMITER" +} + +dict::_unescape_value() { + tr "$DICT_DELIMITER" '\n' +} + +dict::assoc() { + local -r key="${1:-}" + local -r value="$(echo "${2:-}" | dict::_escape_value)" + local -r input="$(cat)" + + if [ -z $key ]; then + printf "$input" | dict::_post + return + fi + + if [ -n "$input" ]; then + local -r base="$(printf "$input" | dict::dissoc "$key"); " + else + local -r base="" + fi + + shift 2 + printf "${base}${key}: ${value}" | dict::_post | dict::assoc "$@" | dict::_post +} + +dict::get() { + if [ $# = 1 ]; then + local -r input="$(cat)" + local -r key="$1" + else + local -r input="$1" + local -r key="$2" + fi + + local -r prefix="${key}[^:]*: " + local -r result="$(echo "$input" | grep -E "^${prefix}")" + local -r matches="$(echo "$result" | wc -l || echo 0)" + + if [ $matches -gt 1 ]; then + echo "$result" | dict::_unescape_value + else + echo "$result" | sed -E "s/${prefix}//" | dict::_unescape_value + fi +} + +dict::keys() { + grep -Eo '^[^:]+: ' | sed 's/: //g' +} + +dict::values() { + awk -F':' '{$1=""; print $0}' | cut -c3- +} + +dict::zipmap() { + IFS='\n' + + local -r keys_str="$1" + local -r values_str="$2" + + keys=() + values=() + for key in $keys_str; do + keys+=("$key") + done + for value in $values_str; do + values+=("$value") + done + + for ((i=0; i<${#keys[@]}; ++i)); do + if [ -n "${keys[i]}" ]; then + echo "${keys[i]}: ${values[i]}" + fi + done +} + +dict::update() { + local -r key="$1" + local -r fn="$2" + local -r input="$(cat)" + + local -r value="$(echo "$input" | dict::get "$key")" + local -r updated_value="$(eval "$fn" "$value")" + + echo "$input" | dict::assoc "$key" "$updated_value" +} \ No newline at end of file diff --git a/src/main.sh b/src/main.sh index 45977cd..a228811 100644 --- a/src/main.sh +++ b/src/main.sh @@ -2,6 +2,7 @@ source "${SCRIPT_DIR}/src/arg.sh" source "${SCRIPT_DIR}/src/cheat.sh" +source "${SCRIPT_DIR}/src/dict.sh" source "${SCRIPT_DIR}/src/health.sh" source "${SCRIPT_DIR}/src/misc.sh" source "${SCRIPT_DIR}/src/opts.sh" @@ -14,8 +15,11 @@ handler::main() { local -r cheats="$(cheat::find)" local -r selection="$(ui::select "$cheats")" local -r cheat="$(cheat::from_selection "$cheats" "$selection")" + [ -z "$cheat" ] && exit 67 - local cmd="$(selection::command "$selection" "$cheat")" + + local -r interpolation="$(dict::get "$OPTIONS" interpolation)" + local cmd="$(selection::cmd "$selection" "$cheat")" local arg value while $interpolation; do @@ -36,6 +40,7 @@ handler::main() { 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 @@ -44,26 +49,37 @@ handler::main() { } handler::preview() { - local -r selection="$(echo "$query" | selection::standardize)" + local -r query="$1" + local -r selection="$(echo "$query" | selection::dict)" local -r cheats="$(cheat::find)" local -r cheat="$(cheat::from_selection "$cheats" "$selection")" - [ -n "$cheat" ] && selection::command "$selection" "$cheat" + [ -n "$cheat" ] && selection::cmd "$selection" "$cheat" +} + +handler::text() { + dict::get "$OPTIONS" text + ui::clear_previous_line } main() { - case ${entry_point:-} in + case "$(dict::get "$OPTIONS" entry_point)" in preview) - handler::preview "$@" \ - || echo "Unable to find command for '${query:-}'" + local -r query="$(dict::get "$OPTIONS" query)" + handler::preview "$query" \ + || echo "Unable to find command for '$query'" ;; search) health::fzf + local -r query="$(dict::get "$OPTIONS" query)" search::save "$query" || true - handler::main "$@" + handler::main + ;; + text) + handler::text ;; *) health::fzf - handler::main "$@" + handler::main ;; esac } \ No newline at end of file diff --git a/src/misc.sh b/src/misc.sh index 980f13b..27b8a70 100644 --- a/src/misc.sh +++ b/src/misc.sh @@ -12,3 +12,15 @@ command_exists() { echoerr() { echo "$@" 1>&2 } + +url::open() { + if command_exists xdg-open; then + xdg-open "$@" + elif command_exists open; then + open "$@" + elif command_exists google-chrome; then + google-chrome "$@" + elif command_exists firefox; then + firefox "$@" + fi +} \ No newline at end of file diff --git a/src/opts.sh b/src/opts.sh index 1c943c9..5702a8b 100644 --- a/src/opts.sh +++ b/src/opts.sh @@ -6,37 +6,27 @@ opts::extract_help() { grep "^##?" "$file" | cut -c 5- } -opts::preview_hack() { - local -r arg="$1" - - if [ ${arg:0:1} = "'" ]; then - echo "${arg:1:${#arg}-2}" - else - echo "$arg" - fi -} - opts::eval() { local wait_for="" - - entry_point="main" - print=false - interpolation=true - preview=true + local entry_point="main" + local print=false + local interpolation=true + local preview=true + local path="${NAVI_PATH:-${NAVI_DIR:-${SCRIPT_DIR}/cheats}}" for arg in "$@"; do case $wait_for in - path) NAVI_PATH="$arg"; wait_for="" ;; - preview) query="$(opts::preview_hack "$arg" | tr "^" " ")"; wait_for=""; break ;; - search) query="$arg"; wait_for=""; export NAVI_PATH="${NAVI_PATH}:$(search::full_path "$query")"; ;; + path) path="$arg"; wait_for="" ;; + preview) query="$(arg::deserialize "$arg")"; wait_for="" ;; + search) query="$arg"; wait_for=""; path="${path}:$(search::full_path "$query")"; ;; query) query="$arg"; wait_for="" ;; esac case $arg in --print) print=true ;; --no-interpolation) interpolation=false ;; - --version) echo "${VERSION:-unknown}" && exit 0 ;; - help|--help) opts::extract_help "$0" && exit 0 ;; + --version) dict::new entry_point "text" text "${VERSION:-unknown}" && exit 0 ;; + help|--help) dict::new entry_point "text" text "$(opts::extract_help "$0")" && exit 0 ;; --command-for) wait_for="command-for" ;; --no-preview) preview=false ;; --path) wait_for="path" ;; @@ -45,10 +35,6 @@ opts::eval() { q|query) wait_for="query" ;; esac done -} -opts::fallback_path() { - echo "${NAVI_DIR:-${SCRIPT_DIR}/cheats}" + dict::new entry_point "$entry_point" print "$print" interpolation "$interpolation" preview "$preview" query "${query:-}" path "$path" } - -export NAVI_PATH="${NAVI_PATH:-$(opts::fallback_path)}" \ No newline at end of file diff --git a/src/selection.sh b/src/selection.sh index 9f10ca8..6745a47 100644 --- a/src/selection.sh +++ b/src/selection.sh @@ -1,31 +1,23 @@ #!/usr/bin/env bash -selection::standardize() { +selection::dict() { local -r str="$(cat)" local -r tags="$(echo "$str" | awk -F'[' '{print $NF}' | tr -d ']')" local -r core="$(echo "$str" | sed -e "s/ \[${tags}\]$//")" - echo "${core}^${tags}" -} - -selection::core() { - cut -d'^' -f1 -} - -selection::tags() { - cut -d'^' -f2 + dict::new core "$core" tags "$tags" } selection::core_is_comment() { grep -qE '^#' } -selection::command() { +selection::cmd() { local -r selection="$1" local -r cheat="$2" - local -r core="$(echo $selection | selection::core)" + local -r core="$(echo "$selection" | dict::get core)" if echo "$core" | selection::core_is_comment; then grep "$core" "$cheat" -A999 \ diff --git a/src/ui.sh b/src/ui.sh index 0cf8e7f..bb7c67a 100644 --- a/src/ui.sh +++ b/src/ui.sh @@ -4,10 +4,16 @@ ui::pick() { fzf --height '100%' --inline-info "$@" } +# TODO: separation of concerns ui::select() { local -r cheats="$1" - local -r script_path="$(which navi | head -n1 || echo "${SCRIPT_DIR}/navi")" - local -r preview_cmd="echo \"{}\" | tr ' ' '^' | xargs -I% \"${script_path}\" preview %" + + local -r script_path="${SCRIPT_DIR}/navi" + local -r preview_cmd="echo \"{}\" | tr ' ' '${ARG_DELIMITER}' | xargs -I% \"${script_path}\" preview %" + + local -r query="$(dict::get "$OPTIONS" query)" + local -r entry_point="$(dict::get "$OPTIONS" entry_point)" + local -r preview="$(dict::get "$OPTIONS" preview)" local args=() args+=("-i") @@ -16,10 +22,10 @@ ui::select() { args+=("--preview"); args+=("$preview_cmd") args+=("--preview-window"); args+=("up:1") fi - if [ -n "${query:-}" ]; then + if [ -n "$query" ]; then args+=("--query=${query} ") fi - if [ "${entry_point:-}" = "search" ]; then + if [ "$entry_point" = "search" ]; then args+=("--header") args+=("Displaying online results. Please refer to 'navi --help' for details") fi @@ -27,7 +33,7 @@ ui::select() { echo "$cheats" \ | cheat::read_many \ | ui::pick "${args[@]}" \ - | selection::standardize + | selection::dict } ui::clear_previous_line() { diff --git a/test/core.sh b/test/core.sh index dd747c9..e6718a3 100644 --- a/test/core.sh +++ b/test/core.sh @@ -2,6 +2,9 @@ source "${SCRIPT_DIR}/src/main.sh" +OPTIONS="$(opts::eval "$@")" +export NAVI_PATH="$(dict::get "$OPTIONS" path)" + PASSED=0 FAILED=0 @@ -23,6 +26,16 @@ test::run() { eval "$*" && test::success || test::fail } +test::equals() { + local -r actual="$(cat | tr -d '\n')" + local -r expected="$(echo "${1:-}" | tr -d '\n' | sed 's/\\n//g')" + + if [[ "$actual" != "$expected" ]]; then + echo "Expected '${expected}' but got '${actual}'" + return 2 + fi +} + test::finish() { echo if [ $FAILED -gt 0 ]; then diff --git a/test/dict_test.sh b/test/dict_test.sh new file mode 100644 index 0000000..b0f7a73 --- /dev/null +++ b/test/dict_test.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash + +inc() { + local -r x="$1" + echo $((x+1)) +} + +dict_assoc() { + dict::new \ + | dict::assoc "foo" "42" \ + | test::equals "foo: 42" +} + +dict_assoc_multiple() { + dict::new \ + | dict::assoc "foo" "42" "bar" "5" \ + | test::equals "bar: 5\nfoo: 42" +} + +dict_dissoc() { + dict::new \ + | dict::assoc "foo" "42" "bar" "5" \ + | dict::dissoc "bar" \ + | test::equals "foo: 42" +} + +dict_assoc_again() { + dict::new \ + | dict::assoc "foo" "42" \ + | dict::assoc "foo" "42" \ + | test::equals "foo: 42" +} + +dict_dissoc_nested() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.b" 6 "baz" 63 \ + | dict::dissoc "bar" \ + | test::equals "baz: 63\nfoo: 42" +} + +dict_assoc_nested() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.c" 7 "baz" 63 \ + | dict::assoc "bar.b" 6 \ + | test::equals "bar.a: 5\nbar.b: 6\nbar.c: 7\nbaz: 63\nfoo: 42" +} + +dict_get() { + dict::new \ + | dict::assoc "foo" "42" \ + | dict::get "foo" \ + | test::equals "42" +} + +dict_get_nested() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.b" 6 "baz" 63 \ + | dict::get "bar.a" \ + | test::equals "5" +} + +dict_get_dict() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.b" 6 "baz" 63 \ + | dict::get "bar" \ + | test::equals "bar.a: 5\nbar.b: 6" +} + +dict_get_keys() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.b" 6 "baz" 63 \ + | dict::keys \ + | test::equals "bar.a\nbar.b\nbaz\nfoo" +} + +dict_get_values() { + dict::new \ + | dict::assoc "foo" "42" "bar.a" 5 "bar.b" 6 "baz" 63 \ + | dict::values \ + | test::equals "5\n6\n63\n42" +} + +dict_zipmap() { + dict::zipmap "key1\nkey2\nkey3" "value1\nvalue2\nvalue3" \ + | test::equals "$(dict::new "key1" "value1" "key2" "value2" "key3" "value3")" +} + +dict_update() { + dict::new "foo" 42 "bar" 5 \ + | dict::update "bar" inc \ + | test::equals "$(dict::new "foo" 42 "bar" 6)" +} + +test::run "We can assoc a value" dict_assoc +test::run "We can assoc multiple values" dict_assoc_multiple +test::run "We can assoc a nested value" dict_assoc_nested +test::run "We can dissoc a value" dict_dissoc +test::run "Associng the same value is a no-op" dict_assoc_again +test::run "Dissocing a key will replace all its subvalues" dict_dissoc_nested +test::run "We can get a value" dict_get +test::run "We can get a nested value" dict_get_nested +test::run "We can get a dictionary" dict_get_dict +test::run "We can get all keys" dict_get_keys +test::run "We can get all values" dict_get_values +test::run "We can get create a dict from a zipmap" dict_zipmap +test::run "We can update a value" dict_update diff --git a/test/run b/test/run index d4117a3..758e36b 100755 --- a/test/run +++ b/test/run @@ -7,7 +7,7 @@ source "${SCRIPT_DIR}/test/core.sh" tests="$(find "$SCRIPT_DIR/test" -iname '*_test.sh')" for test in $tests; do - source "$test" + source "$test" done test::finish