Refactor string escaping code (#114)

Fix #111
This commit is contained in:
Denis Isidoro 2019-10-05 14:08:00 -03:00 committed by GitHub
parent 13ddc7c226
commit 6129d9e954
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 151 additions and 125 deletions

2
navi
View file

@ -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 "$@"

View file

@ -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

View file

@ -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
}

41
src/cmd.sh Normal file
View file

@ -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
}

View file

@ -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

View file

@ -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'

View file

@ -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
result="$(cmd::loop "$cmd" "$cheat")"
arg="$(dict::get "$result" arg)"
value="$(dict::get "$result" value)"
cmd="$(dict::get "$result" cmd)"
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")"
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

View file

@ -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
}

View file

@ -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"
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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"
}