From 1bd003a3695a1aa1d2bdb4807d3852c795da2c80 Mon Sep 17 00:00:00 2001 From: Luna Komorebi Date: Wed, 5 Jul 2023 23:11:28 +0200 Subject: [PATCH] add cht.sh client --- bin/cht.sh | 796 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 796 insertions(+) create mode 100755 bin/cht.sh diff --git a/bin/cht.sh b/bin/cht.sh new file mode 100755 index 0000000..2f662ac --- /dev/null +++ b/bin/cht.sh @@ -0,0 +1,796 @@ +#!/bin/bash +# shellcheck disable=SC1117,SC2001 +# +# [X] open section +# [X] one shot mode +# [X] usage info +# [X] dependencies check +# [X] help +# [X] yank/y/copy/c +# [X] Y/C +# [X] eof problem +# [X] more +# [X] stealth mode +# +# here are several examples for the stealth mode: +# +# zip lists +# list permutation +# random list element +# reverse a list +# read json from file +# append string to a file +# run process in background +# count words in text counter +# group elements list + +__CHTSH_VERSION=0.0.4 +__CHTSH_DATETIME="2021-04-25 12:30:30 +0200" + +# cht.sh configuration loading +# +# configuration is stored in ~/.cht.sh/ (can be overridden with CHTSH env var.) +# +CHTSH_HOME=${CHTSH:-"$HOME"/.cht.sh} +[ -z "$CHTSH_CONF" ] && CHTSH_CONF=$CHTSH_HOME/cht.sh.conf +# shellcheck disable=SC1090,SC2002 +[ -e "$CHTSH_CONF" ] && source "$CHTSH_CONF" +[ -z "$CHTSH_URL" ] && CHTSH_URL=https://cht.sh + +# currently we support only two modes: +# * lite = access the server using curl +# * auto = try standalone usage first +CHTSH_MODE="$(cat "$CHTSH_HOME"/mode 2> /dev/null)" +[ "$CHTSH_MODE" != lite ] && CHTSH_MODE=auto +CHEATSH_INSTALLATION="$(cat "$CHTSH_HOME/standalone" 2> /dev/null)" + + +export LESSSECURE=1 +STEALTH_MAX_SELECTION_LENGTH=5 + +case "$(uname -s)" in + Darwin) is_macos=yes ;; + *) is_macos=no ;; +esac + +# for KSH93 +# shellcheck disable=SC2034,SC2039,SC2168 +if echo "$KSH_VERSION" | grep -q ' 93' && ! local foo 2>/dev/null; then + alias local=typeset +fi + +fatal() +{ + echo "ERROR: $*" >&2 + exit 1 +} + +_say_what_i_do() +{ + [ -n "$LOG" ] && echo "$(date '+[%Y-%m-%d %H:%M%S]') $*" >> "$LOG" + + local this_prompt="\033[0;1;4;32m>>\033[0m" + printf "\n${this_prompt}%s\033[0m\n" " $* " +} + +cheatsh_standalone_install() +{ + # the function installs cheat.sh with the upstream repositories + # in the standalone mode + local installdir; installdir="$1" + local default_installdir="$HOME/.cheat.sh" + + [ -z "$installdir" ] && installdir=${default_installdir} + + if [ "$installdir" = help ]; then + cat </dev/null || \ + { echo "DEPENDENCY: \"$dep\" is needed to install cheat.sh in the standalone mode" >&2; _exit_code=1; } + done + [ "$_exit_code" -ne 0 ] && return "$_exit_code" + + while true; do + local _installdir + echo -n "Where should cheat.sh be installed [$installdir]? "; read -r _installdir + [ -n "$_installdir" ] && installdir=$_installdir + + if [ "$installdir" = y ] \ + || [ "$installdir" = Y ] \ + || [ "$(echo "$installdir" | tr "[:upper:]" "[:lower:]")" = yes ] + then + echo Please enter the directory name + echo If it was the directory name already, please prepend it with \"./\": "./$installdir" + else + break + fi + done + + if [ -e "$installdir" ]; then + echo "ERROR: Installation directory [$installdir] exists already" + echo "Please remove it first before continuing" + return 1 + fi + + if ! mkdir -p "$installdir"; then + echo "ERROR: Could not create the installation directory \"$installdir\"" + echo "ERROR: Please check the permissions and start the script again" + return 1 + fi + + local space_needed=700 + local space_available; space_available=$(($(df -k "$installdir" | awk '{print $4}' | tail -1)/1024)) + + if [ "$space_available" -lt "$space_needed" ]; then + echo "ERROR: Installation directory has no enough space (needed: ${space_needed}M, available: ${space_available}M" + echo "ERROR: Please clean up and start the script again" + rmdir "$installdir" + return 1 + fi + + _say_what_i_do Cloning cheat.sh locally + local url=https://github.com/chubin/cheat.sh + rmdir "$installdir" + git clone "$url" "$installdir" || fatal Could not clone "$url" with git into "$installdir" + cd "$installdir" || fatal "Cannot cd into $installdir" + + # after the repository cloned, we may have the log directory + # and we can write our installation log into it + mkdir -p "log/" + LOG="$PWD/log/install.log" + + # we use tee everywhere so we should set -o pipefail + set -o pipefail + + # currently the script uses python 2, + # but cheat.sh supports python 3 too + # if you want to switch it to python 3 + # set PYTHON2 to NO: + # PYTHON2=NO + # + PYTHON2=NO + if [[ $PYTHON2 = YES ]]; then + python="python2" + pip="pip" + virtualenv_python3_option=() + else + python="python3" + pip="pip3" + virtualenv_python3_option=(-p python3) + fi + + _say_what_i_do Creating virtual environment + virtualenv "${virtualenv_python3_option[@]}" ve \ + || fatal "Could not create virtual environment with 'virtualenv ve'" + + export CHEATSH_PATH_WORKDIR=$PWD + + # rapidfuzz does not support Python 2, + # so if we are using Python 2, install fuzzywuzzy instead + if [[ $PYTHON2 = YES ]]; then + sed -i s/rapidfuzz/fuzzywuzzy/ requirements.txt + echo "python-Levenshtein" >> requirements.txt + fi + + _say_what_i_do Installing python requirements into the virtual environment + ve/bin/"$pip" install -r requirements.txt > "$LOG" \ + || { + + echo "ERROR:" + echo "---" + tail -n 10 "$LOG" + echo "---" + echo "See $LOG for more" + fatal Could not install python dependencies into the virtual environment + } + echo "$(ve/bin/"$pip" freeze | wc -l) dependencies were successfully installed" + + _say_what_i_do Fetching the upstream cheat sheets repositories + ve/bin/python lib/fetch.py fetch-all | tee -a "$LOG" + + _say_what_i_do Running self-tests + ( + cd tests || exit + + if CHEATSH_TEST_STANDALONE=YES \ + CHEATSH_TEST_SKIP_ONLINE=NO \ + CHEATSH_TEST_SHOW_DETAILS=NO \ + PYTHON=../ve/bin/python bash run-tests.sh | tee -a "$LOG" + then + printf "\033[0;32m%s\033[0m\n" "SUCCESS" + else + printf "\033[0;31m%s\033[0m\n" "FAILED" + echo "Some tests were failed. Run the tests manually for further investigation:" + echo " cd $PWD; bash run-tests.sh)" + fi + ) + + mkdir -p "$CHTSH_HOME" + echo "$installdir" > "$CHTSH_HOME/standalone" + echo auto > "$CHTSH_HOME/mode" + + _say_what_i_do Done + + local v1; v1=$(printf "\033[0;1;32m") + local v2; v2=$(printf "\033[0m") + + cat < "$CHTSH_HOME/mode" + echo "Configured mode: $mode" + fi + else + echo "Unknown mode: $mode" + echo Supported modes: + echo " auto use the standalone installation first" + echo " lite use the cheat sheets server directly" + fi +} + +get_query_options() +{ + local query="$*" + if [ -n "$CHTSH_QUERY_OPTIONS" ]; then + case $query in + *\?*) query="$query&${CHTSH_QUERY_OPTIONS}";; + *) query="$query?${CHTSH_QUERY_OPTIONS}";; + esac + fi + printf "%s" "$query" +} + +do_query() +{ + local query="$*" + local b_opts= + local uri="${CHTSH_URL}/\"\$(get_query_options $query)\"" + + if [ -e "$CHTSH_HOME/id" ]; then + b_opts="-b \"\$CHTSH_HOME/id\"" + fi + + eval curl "$b_opts" -s "$uri" > "$TMP1" + + if [ -z "$lines" ] || [ "$(wc -l "$TMP1" | awk '{print $1}')" -lt "$lines" ]; then + cat "$TMP1" + else + ${PAGER:-$defpager} "$TMP1" + fi +} + +prepare_query() +{ + local section="$1"; shift + local input="$1"; shift + local arguments="$1" + + local query + if [ -z "$section" ] || [ x"${input}" != x"${input#/}" ]; then + query=$(printf %s "$input" | sed 's@ @/@; s@ @+@g') + else + query=$(printf %s "$section/$input" | sed 's@ @+@g') + fi + + [ -n "$arguments" ] && arguments="?$arguments" + printf %s "$query$arguments" +} + +get_list_of_sections() +{ + curl -s "${CHTSH_URL}"/:list | grep -v '/.*/' | grep '/$' | xargs +} + +gen_random_str() +( + len=$1 + if command -v openssl >/dev/null; then + openssl rand -base64 $((len*3/4)) | awk -v ORS='' // + else + rdev=/dev/urandom + for d in /dev/{srandom,random,arandom}; do + test -r "$d" && rdev=$d + done + if command -v hexdump >/dev/null; then + hexdump -vn $((len/2)) -e '1/1 "%02X" 1 ""' "$rdev" + elif command -v xxd >/dev/null; then + xxd -l $((len/2)) -ps "$rdev" | awk -v ORS='' // + else + cd /tmp || { echo Cannot cd into /tmp >&2; exit 1; } + s= + # shellcheck disable=SC2000 + while [ "$(echo "$s" | wc -c)" -lt "$len" ]; do + s="$s$(mktemp -u XXXXXXXXXX)" + done + printf "%.${len}s" "$s" + fi + fi +) + +if [ "$CHTSH_MODE" = auto ] && [ -d "$CHEATSH_INSTALLATION" ]; then + curl() { + # ignoring all options + # currently the standalone.py does not support them anyway + local opt + while getopts "b:s" opt; do + : + done + shift $((OPTIND - 1)) + + local url; url="$1"; shift + PYTHONIOENCODING=UTF-8 "$CHEATSH_INSTALLATION/ve/bin/python" "$CHEATSH_INSTALLATION/lib/standalone.py" "${url#"$CHTSH_URL"}" "$@" + } +elif [ "$(uname -s)" = OpenBSD ] && [ -x /usr/bin/ftp ]; then + # any better test not involving either OS matching or actual query? + curl() { + local opt args="-o -" + while getopts "b:s" opt; do + case $opt in + b) args="$args -c $OPTARG";; + s) args="$args -M -V";; + *) echo "internal error: unsupported cURL option '$opt'" >&2; exit 1;; + esac + done + shift $((OPTIND - 1)) + /usr/bin/ftp "$args" "$@" + } +else + command -v curl >/dev/null || { echo 'DEPENDENCY: install "curl" to use cht.sh' >&2; exit 1; } + _CURL=$(command -v curl) + if [ x"$CHTSH_CURL_OPTIONS" != x ]; then + curl() { + $_CURL "${CHTSH_CURL_OPTIONS}" "$@" + } + fi +fi + +if [ "$1" = --read ]; then + read -r a || a="exit" + printf "%s\n" "$a" + exit 0 +elif [ x"$1" = x--help ] || [ -z "$1" ]; then + + n=${0##*/} + s=$(echo "$n" | sed "s/./ /"g) + + cat </dev/null || echo 'DEPENDENCY: please install "wl-copy" for "copy"' >&2 + else + command -v xsel >/dev/null || echo 'DEPENDENCY: please install "xsel" for "copy"' >&2 + fi +fi +command -v rlwrap >/dev/null || { echo 'DEPENDENCY: install "rlwrap" to use cht.sh in the shell mode' >&2; exit 1; } + +mkdir -p "$CHTSH_HOME/" +lines=$(tput lines) + +if command -v less >/dev/null; then + defpager="less -R" +elif command -v more >/dev/null; then + defpager="more" +else + defpager="cat" +fi + +cmd_cd() { + if [ $# -eq 0 ]; then + section="" + else + new_section=$(echo "$input" | sed 's/cd *//; s@/*$@@; s@^/*@@') + if [ -z "$new_section" ] || [ ".." = "$new_section" ]; then + section="" + else + valid_sections=$(get_list_of_sections) + valid=no; for q in $valid_sections; do [ "$q" = "$new_section/" ] && { valid=yes; break; }; done + if [ "$valid" = no ]; then + echo "Invalid section: $new_section" + echo "Valid sections:" + echo "$valid_sections" \ + | xargs printf "%-10s\n" \ + | tr ' ' . \ + | xargs -n 10 \ + | sed 's/\./ /g; s/^/ /' + else + section="$new_section" + fi + fi + fi +} + +cmd_copy() { + if [ -z "$DISPLAY" ]; then + echo copy: supported only in the Desktop version + elif [ -z "$input" ]; then + echo copy: Make at least one query first. + else + curl -s "${CHTSH_URL}"/"$(get_query_options "$query"?T)" > "$TMP1" + if [ "$is_macos" != yes ]; then + if [ "$XDG_SESSION_TYPE" = wayland ]; then + wl-copy < "$TMP1" + else + xsel -bi < "$TMP1" + fi + else + pbcopy < "$TMP1" + fi + echo "copy: $(wc -l "$TMP1" | awk '{print $1}') lines copied to the selection" + fi +} + +cmd_ccopy() { + if [ -z "$DISPLAY" ]; then + echo copy: supported only in the Desktop version + elif [ -z "$input" ]; then + echo copy: Make at least one query first. + else + curl -s "${CHTSH_URL}"/"$(get_query_options "$query"?TQ)" > "$TMP1" + if [ "$is_macos" != yes ]; then + if [ "$XDG_SESSION_TYPE" = wayland ]; then + wl-copy < "$TMP1" + else + xsel -bi < "$TMP1" + fi + else + pbcopy < "$TMP1" + fi + echo "copy: $(wc -l "$TMP1" | awk '{print $1}') lines copied to the selection" + fi +} + +cmd_exit() { + exit 0 +} + +cmd_help() { + cat < python zip list + cht.sh/python> zip list + cht.sh/go> /python zip list +EOF +} + +cmd_hush() { + mkdir -p "$CHTSH_HOME/" && touch "$CHTSH_HOME/.hushlogin" && echo "Initial 'use help' message was disabled" +} + +cmd_id() { + id_file="$CHTSH_HOME/id" + + if [ id = "$input" ]; then + new_id="" + else + new_id=$(echo "$input" | sed 's/id *//; s/ *$//; s/ /+/g') + fi + if [ "$new_id" = remove ]; then + if [ -e "$id_file" ]; then + rm -f -- "$id_file" && echo "id is removed" + else + echo "id was not set, so you can't remove it" + fi + return + fi + if [ -n "$new_id" ] && [ reset != "$new_id" ] && [ "$(/bin/echo -n "$new_id" | wc -c)" -lt 16 ]; then + echo "ERROR: $new_id: Too short id. Minimal id length is 16. Use 'id reset' for a random id" + return + fi + if [ -z "$new_id" ]; then + # if new_id is not specified check if we have some id already + # if yes, just show it + # if not, generate a new id + if [ -e "$id_file" ]; then + awk '$6 == "id" {print $NF}' <"$id_file" | tail -n 1 + return + else + new_id=reset + fi + fi + if [ "$new_id" = reset ]; then + new_id=$(gen_random_str 12) + else + echo WARNING: if someone gueses your id, he can read your cht.sh search history + fi + if [ -e "$id_file" ] && grep -q '\tid\t[^\t][^\t]*$' "$id_file" 2> /dev/null; then + sed -i 's/\tid\t[^\t][^\t]*$/ id '"$new_id"'/' "$id_file" + else + if ! [ -e "$id_file" ]; then + printf '#\n\n' > "$id_file" + fi + printf ".cht.sh\tTRUE\t/\tTRUE\t0\tid\t$new_id\n" >> "$id_file" + fi + echo "$new_id" +} + +cmd_query() { + query=$(prepare_query "$section" "$input") + do_query "$query" +} + +cmd_stealth() { + if [ "$input" != stealth ]; then + arguments=$(echo "$input" | sed 's/stealth //; s/ /\&/') + fi + trap break INT + if [ "$is_macos" = yes ]; then + past=$(pbpaste) + else + if [ "$XDG_SESSION_TYPE" = wayland ]; then + past=$(wl-paste -p) + else + past=$(xsel -o) + fi + fi + printf "\033[0;31mstealth:\033[0m you are in the stealth mode; select any text in any window for a query\n" + printf "\033[0;31mstealth:\033[0m selections longer than $STEALTH_MAX_SELECTION_LENGTH words are ignored\n" + if [ -n "$arguments" ]; then + printf "\033[0;31mstealth:\033[0m query arguments: ?$arguments\n" + fi + printf "\033[0;31mstealth:\033[0m use ^C to leave this mode\n" + while true; do + if [ "$is_macos" = yes ]; then + current=$(pbpaste) + else + if [ "$XDG_SESSION_TYPE" = wayland ]; then + current=$(wl-paste -p) + else + current=$(xsel -o) + fi + fi + if [ "$past" != "$current" ]; then + past=$current + current_text="$(echo $current | tr -c '[a-zA-Z0-9]' ' ')" + if [ "$(echo "$current_text" | wc -w)" -gt "$STEALTH_MAX_SELECTION_LENGTH" ]; then + printf "\033[0;31mstealth:\033[0m selection length is longer than $STEALTH_MAX_SELECTION_LENGTH words; ignoring\n" + continue + else + printf "\n\033[0;31mstealth: \033[7m $current_text\033[0m\n" + query=$(prepare_query "$section" "$current_text" "$arguments") + do_query "$query" + fi + fi + sleep 1; + done + trap - INT +} + +cmd_update() { + [ -w "$0" ] || { echo "The script is readonly; please update manually: curl -s ${CHTSH_URL}/:cht.sh | sudo tee $0"; return; } + TMP2=$(mktemp /tmp/cht.sh.XXXXXXXXXXXXX) + curl -s "${CHTSH_URL}"/:cht.sh > "$TMP2" + if ! cmp "$0" "$TMP2" > /dev/null 2>&1; then + if grep -q ^__CHTSH_VERSION= "$TMP2"; then + # section was vaildated by us already + args=(--shell "$section") + cp "$TMP2" "$0" && echo "Updated. Restarting..." && rm "$TMP2" && CHEATSH_RESTART=1 exec "$0" "${args[@]}" + else + echo "Something went wrong. Please update manually" + fi + else + echo "cht.sh is up to date. No update needed" + fi + rm -f "$TMP2" > /dev/null 2>&1 +} + +cmd_version() { + insttime=$(ls -l -- "$0" | sed 's/ */ /g' | cut -d ' ' -f 6-8) + echo "cht.sh version $__CHTSH_VERSION of $__CHTSH_DATETIME; installed at: $insttime" + TMP2=$(mktemp /tmp/cht.sh.XXXXXXXXXXXXX) + if curl -s "${CHTSH_URL}"/:cht.sh > "$TMP2"; then + if ! cmp "$0" "$TMP2" > /dev/null 2>&1; then + echo "Update needed (type 'update' for that)". + else + echo "Up to date. No update needed" + fi + fi + rm -f "$TMP2" > /dev/null 2>&1 +} + +TMP1=$(mktemp /tmp/cht.sh.XXXXXXXXXXXXX) +trap 'rm -f $TMP1 $TMP2' EXIT +trap 'true' INT + +if ! [ -e "$CHTSH_HOME/.hushlogin" ] && [ -z "$this_query" ]; then + echo "type 'help' for the cht.sh shell help" +fi + +while true; do + if [ "$section" != "" ]; then + full_prompt="$prompt/$section> " + else + full_prompt="$prompt> " + fi + + input=$( + rlwrap -H "$CHTSH_HOME/history" -pgreen -C cht.sh -S "$full_prompt" bash "$0" --read | sed 's/ *#.*//' + ) + + cmd_name=${input%% *} + cmd_args=${input#* } + case $cmd_name in + "") continue;; # skip empty input lines + '?'|h|help) cmd_name=help;; + hush) cmd_name=hush;; + cd) cmd_name="cd";; + exit|quit) cmd_name="exit";; + copy|yank|c|y) cmd_name=copy;; + ccopy|cc|C|Y) cmd_name=ccopy;; + id) cmd_name=id;; + stealth) cmd_name=stealth;; + update) cmd_name=update;; + version) cmd_name=version;; + *) cmd_name="query"; cmd_args="$input";; + esac + "cmd_$cmd_name" $cmd_args +done