#!/usr/bin/env sh # tuxi is a cli assistant created by Bugswriter # to get answers for your questions instantly. # tuxi is currently developed by many collaborators # you can get more information on our repo :) # https://github.com/Bugswriter/tuxi © GPL-3.0 License ############################### ##### Constants ##### ############################### # setting this overrides getting the system language variable # the -l commandline flag overrides everything # this can also be set in your shell environment with # TUXI_LANG= [ -n "$TUXI_LANG" ] && LANGUAGE="$TUXI_LANG" || LANGUAGE="" # if you find more than one answer is being printed (and you're not using -a) # increase this number by a little (you want it to be as low as possible) # this can also be set in your shell environment with # TUXI_DELAY= [ -n "$TUXI_DELAY" ] && MICRO_DELAY="$TUXI_DELAY" || MICRO_DELAY=250 VERSION="dev 2.0" MAIN_PID="$$" [ ! "$XDG_CACHE_HOME" ] && XDG_CACHE_HOME="$HOME/.cache" ######################################### ##### macOS compatibility ##### ######################################### # credit to @Zhann in #149 # to use it, you will need to have GNU core utils installed case "$OSTYPE" in darwin*) sed() { gsed "$@" } paste() { gpaste "$@" } ;; esac ###################################### ##### Snippet priority ##### ###################################### # priority importance # this variable determines the order the tests are started, they are processed in parallel # even though these are started in order, by default, the first answer to resolve is the one printed # the order here might only make a very small difference # priority order # the first word should be the name of the a_function() followed by a space # you can disable tests by commenting out the line(s) priority=" tracklist # Album track lists ( eg: noisia outer edges tracklist ) richcast # Rich Rich Answers ( eg: social network cast ) define # Define ( eg: define Aggrandize ) lists # Simple lists ( eg Need for Speed Heat cars list ) kno_val # Chem facts ( eg: density of silver, density of hydrogen, what is the triple point of oxygen ) pronounce # Learn to pronounce ( eg: pronounce linux ) lyrics_int # Lyrics ( eg: gecgecgec lyrics ) weather # Weather ( eg: weather new york ) math # Math ( eg: log_2(3) * pi^e ) unit # Units Conversion ( eg: 1m into 1 cm ) currency # Currency Conversion ( eg: 1 USD in rupee ) kno_top # Knowledge Graph - top ( list ) ( eg: the office cast ) basic # Basic Answers ( eg: christmas day ) feat # Featured Snippets ( eg: who is garfield ) quotes # Quotes ( eg: mahatma gandhi quotes ) trans # Translate ( eg: Vais para cascais? em ingles ) sport_fixture # Shows last or next fixture of a sports team ( eg. Chelsea next game ) lyrics_us # Lyrics for US users, above does not work for US kno_right # Knowledge Graph - right ( eg: the office ) " ############################## ##### Defaults ##### ############################## # system language fallback LANG=$(echo $LANG | sed 's/\..*//') # options raw=false quiet=false all=false best_match=false pick_search=false debug=false save_html=false use_cache=false pick_lang=false no_pipe=false plus_urls=false # color codes N="\033[0m" # Reset B="\033[1m" # Bold R="\033[1;31m" # Red G="\033[1;32m" # Green Y="\033[1;33m" # Yellow M="\033[1;35m" # Magenta C="\033[1;36m" # Cyan ################################## ##### Help message ##### ################################## help_text() { printf "%bUsage:%b tuxi %b[options]%b %bquery%b\n" "$G" "$N" "$Y" "$N" "$M" "$N" printf "%bOR:%b %bquery source%b | tuxi %b[options]%b\n" "$G" "$N" "$M" "$N" "$Y" "$N" printf "\n" printf "%bOptions:%b\n" "$G" "$N" printf " -h Show this help message and exit.\n" printf " -v Print tuxi version info and exit.\n" printf "\n" printf " -r Raw search results.\n" printf " (no pretty output, no colors)\n" printf "\n" printf " -q Only output search results.\n" printf " (silences \"Did you mean?\", greeting, usage)\n" printf "\n" printf " -a Prints all valid answers.\n" printf "\n" printf " -u Prints out the top handful of URLs for your search query\n" printf " (this is automatically printed out if tuxi can't find you an answer)\n" printf "\n" printf " -b Tries to select the best answer based on keywords at the start and end of your query.\n" printf " (experimental - eg: define WORD, SONG lyrics, PERSON quotes, weather CITY, FILM cast)\n" printf "\n" printf " -t Pick answers to test.\n" printf " (you can specify multiple answers using tuxi_NAME in your query)\n" printf "\n" printf " -l use LANG_[lang] in your query to override the language used\n" printf " (eg: tuxi -l LANG_en_US my search query)\n" printf "\n" printf "%btuxi supports the following environment variables:%b\n" "$G" "$N" printf " TUXI_LANG=[lang] sets default search language (eg: TUXI_LANG='en_US')\n" printf "\n" printf " TUXI_DELAY=[int] if you find more than one answer is being printed (and you're not using -a)\n" printf " increase this number by a little (you want it to be as low as possible)\n" printf " default value is 250 (eg: TUXI_DELAY=270)\n" printf "\n" printf "%bdeveloper flags:%b\n" "$G" "$N" printf " -d prints debug info along with results\n" printf " -s saves HTML for this query to $XDG_CACHE_HOME/tuxi/[date]-[query].html\n" printf "\n" printf " -c use most recent cached result and query\n" printf " this can be combined with -t flag to more quickly test for different answers\n" printf "\n" printf " -p disable pipe support (it can break some scripts including our own test script)\n" printf "\n" printf "%bReport bugs at%b %bhttps://github.com/Bugswriter/tuxi/issues%b\n" "$G" "$N" "$C" "$N" } ############################# ##### Getopts ##### ############################# # -r : raw output # -v : version info # -h : help # -q : silences greeting and did you mean # -a : print all answers # -b : best match # -t : specify answer type # -l : specify language using LANG_[code] - eg LANG_en_US # -d : print debug info # -s : save google HTML response # -c : use most recent cached results # -p : disable pipe support (needed for test script) # -u : also print out the top links while getopts "rvhqabtldscpu" OPT; do case "$OPT" in r) raw=true ;; v) printf "tuxi %s\n" "$VERSION" exit 0 ;; h) help_text exit 0 ;; q) quiet=true ;; a) all=true ;; b) best_match=true ;; t) pick_search=true ;; d) debug=true ;; s) save_html=true ;; c) use_cache=true ;; l) pick_lang=true ;; p) no_pipe=true ;; u) plus_urls=true ;; *) help_text | head -n 1 exit 1 ;; esac done # shifts to query shift $((OPTIND - 1)) $pick_search && $best_match && echo "sorry but -b and -t mutually exclusive" && exit 1 # TODO this may need reworking later to use read instead and only capture the first line # question | tuxi [-flags] --> answer :) if ! $no_pipe; then [ -p /dev/stdin ] && query=$(cat) fi ####################################### ##### Output formatting ##### ####################################### # search result output format (changes if raw=true) output() { printf "%b---%b\n%s\n%b---%b\n" "$G" "$N" "$*" "$G" "$N" } # If raw=true: No colors, No pretty output if $raw; then N="" B="" R="" G="" Y="" M="" C="" output() { printf "%s\n" "$*" } fi info_msg() { printf "%b>%b %s\n" "$G" "$N" "$*" } error_msg() { printf "%b%s%b\n" "$R" "$*" "$N" } ###################################### ##### Dependency check ##### ###################################### # Checks if dependencies are installed. check_deps() { while [ -n "$1" ]; do if [ ! "$(command -v $1)" ]; then error_msg "\"$1\" not found!" exit 2 fi shift done } # Dependencies # pup : https://github.com/ericchiang/pup # recode : https://github.com/rrthomas/recode # jq : https://github.com/stedolan/jq check_deps "pup" "recode" "jq" ######################################## ##### Query manipulation ##### ######################################## # Conditions to Query # If query is empty and -c is passed: use query from cached result # If query is empty (no -c): exit # If quiet=false: Prints greeting and usage if [ -z "$1" ] && [ -z "$query" ]; then if ! $use_cache; then if ! $quiet; then printf "Hi, I'm Tuxi. Ask me anything!\n" help_text | head -n 1 fi exit 0 else query=$(ls -1t $XDG_CACHE_HOME/tuxi | head -n1 | sed -e 's/tuxi-*[0-9]*-//' -e 's/.html//' -e 's/_/ /g') fi fi # Else, all arguments are saved in $query [ -z "$query" ] && query="$*" # language select: the -l flag # language specified on the command line overwrites both # the variable set at the top of this script and the system language if $pick_lang; then query="$(printf '%b\n' "$query" | sed 's/ /\\n/g')" LANGUAGE="$(printf '%b\n' "$query" | grep 'LANG_' | sed 's/LANG_//g')" query="$(printf '%b\n' "$query" | grep -v "LANG_" | sed 's/\\n/ /g')" fi # Custom answers: the -t flag # clears the list of snippets to check (saving the original list to print out if a mistake is made) # then loops through the query looking for tuxi_ and updates the priority variable to use only those snippets if $pick_search; then list_priority="$priority" snippet_check=$(printf '%b\n' "$list_priority" | cut -d ' ' -f1 | sed -e '/^\s*#.*$/d' -e '/^\s*$/d') matched=false priority="" query="$(printf '%b\n' "$query" | sed 's/ /\\n/g')" for pick_words in $(printf '%b\n' "$query" | grep 'tuxi_' | sed 's/tuxi_//g'); do for check_pick_words in $(printf '%b\n' "$snippet_check"); do if [ "$check_pick_words" = "$pick_words" ]; then [ -z "$priority" ] && priority="$(printf '%s\n' "$pick_words")" \ || priority="$(printf '%b\n%s\n' "$priority" "$pick_words")" matched=true fi done if ! $matched; then printf "Sorry but %s is not a valid search type\nPlease retry your search using one of the following: tuxi_\n" "$pick_words" printf "%b\n" "$list_priority" printf "\n" printf "If %s is on that list could you please file a bug report, thanks! (and sorry)\n" "$pick_words" exit 1 fi done query="$(printf '%b\n' "$query" | grep -v "tuxi_" | sed 's/\\n/ /g')" fi # our patented (honest!) "smrt search" algorithm: the -b flag # jokes aside, this is going to need some iterating on, I'll turn it into a tidy loop later if $best_match; then j=8 use_quotes=false use_lyrics=false use_weather=false use_cast=false use_weather=false use_define=false use_list=false use_pronounce=false use_tracklist=false query_check="$(printf '%b\n' "$query" | sed 's/ /\\n/g' | tr '[:upper:]' '[:lower:]')" first_word=$(printf '%b\n' "$query_check" | head -n1) last_word=$(printf '%b\n' "$query_check" | tail -n1) for keywords in printf '%s\n%s\n' "$first_word" "$last_word"; do case "$keywords" in quote | quotes) use_quotes=true ;; lyrics) use_lyrics=true ;; weather) use_weather=true ;; cast) use_cast=true ;; define | definition) use_define=true ;; list) use_list=true ;; pronounce | pronunciation) use_pronounce=true ;; tracklist | songs) use_tracklist=true ;; esac done $use_quotes && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'quotes')" || j=$(($j - 1)) $use_lyrics && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'lyrics')" || j=$(($j - 1)) $use_weather && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'weather')" || j=$(($j - 1)) $use_cast && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep -e 'lists' -e 'kno_')" || j=$(($j - 1)) $use_define && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'define')" || j=$(($j - 1)) $use_pronounce && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'pronounce')" || j=$(($j - 1)) $use_tracklist && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep 'tracklist')" || j=$(($j - 1)) $use_list && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | grep -e 'lists' -e 'kno_')" || j=$(($j - 1)) [ $j -eq 0 ] && priority="$(printf '%b\n' "$priority" | cut -d ' ' -f1 | sed -e '/^\s*#.*$/d' -e '/^[[:space:]]*$/d' | grep -v 'quotes' | grep -v 'lyrics' | grep -v 'weather')" fi ###################################### ##### Answer functions ##### ###################################### # the following divs have been removed due to seemingly being redundant # Im leaving them here just in case they're needed again in future # div.ujudUb (seems to be another lyrics scrape) # FUNCTION TEMPLATE # NewAnswerName should be the word used in $priority # a_NewAnswerName() { # Answer description (and example) # echo "$google_html" | pup ... [ SCRAPE METHOD HERE ] ... # } # NOTE: the order of these functions doesn't matter, priority is determined by the variable # Define (eg: define Aggrandize) //original snippet credit @igaurab a_define() { case "$LANGUAGE" in en_*) dfn_use_new=true ;; *) dfn_use_new=false ;; esac if $dfn_use_new; then define="$(echo "$google_html" | pup 'div.VpH2eb.dZd3De.vmod text{}' | sed '/^[[:space:]]*$/d' | recode html..ISO-8859-1)" if [ -n "$define" ]; then printf 'pronounced: %b%s%b\n\n' "$C" "$(printf '%s\n' "$define" | grep -m1 -A1 -w '/' | tail -n1)" "$N" dfn_top=true dfn_end_top=false dfn_start=false dfn_append=false dfn_marker=false dfn_sim_op=false dfn_skip=false printf '%s\n' "$define" | while IFS= read -r dfn_foo; do if $dfn_top; then if $dfn_end_top; then case "$dfn_foo" in *"noun: " | *"verb: " | *"adjective: " | *"adverb: " | *"pronoun: " | *"preposition: " | *"conjunction: " | *"determiner: " | *"exclamation: ") printf '%s ' "$dfn_foo" dfn_append=true ;; *"noun:" | *"verb:" | *"adjective:" | *"adverb:" | *"pronoun:" | *"preposition:" | *"conjunction:" | *"determiner:" | *"exclamation:") printf '%s ' "$dfn_foo" dfn_append=true ;; *) printf '%b%s%b\n\t%b%s%b\n' "$Y" "$dfn_backup" "$N" "$B" "$dfn_foo" "$N" ;; esac dfn_top=false else case "$dfn_foo" in noun | verb | adjective | adverb | pronoun | preposition | conjunction | determiner | exclamation) dfn_backup="$dfn_foo" dfn_end_top=true ;; *) continue ;; esac fi elif $dfn_append; then printf '%b%s%b\n' "$C" "$dfn_foo" "$N" dfn_append=false elif [ $dfn_foo -eq $dfn_foo ] 2>/dev/null; then printf '\n' $dfn_sim_op && dfn_sim_op=false elif [ "$dfn_foo" = '/' ]; then $dfn_skip && dfn_skip=false || dfn_skip=true elif $dfn_skip; then continue elif [ "$dfn_foo" = '. ' ]; then $dfn_sim_op && dfn_sim_op=false elif [ "$dfn_foo" = '.' ]; then printf '\n' $dfn_sim_op && dfn_sim_op=false elif $dfn_marker; then [ "$dfn_foo" = 'Similar:' ] && dfn_hl="$G" || dfn_hl="$R" printf '%b%s%b\n' "$dfn_hl" "$dfn_foo" "$N" dfn_marker=false dfn_sim_op=true elif $dfn_start; then case "$dfn_foo" in informal) printf '(informal) ' ;; British) printf '(British) ' ;; rare) printf '(rare) ' ;; *) printf '%s ' "$dfn_foo" dfn_append=true dfn_start=false ;; esac else case "$dfn_foo" in "; "*) printf '%s ' "$dfn_foo" dfn_append=true ;; *"noun: " | *"verb: " | *"adjective: " | *"adverb: " | *"pronoun: " | *"preposition: " | *"conjunction: " | *"determiner: " | *"exclamation: ") printf '\n%s ' "$dfn_foo" $dfn_sim_op && dfn_sim_op=false dfn_append=true ;; " h ") dfn_marker=true ;; noun | verb | adjective | adverb | pronoun | preposition | conjunction | determiner | exclamation) printf '\n' $dfn_sim_op && dfn_sim_op=false dfn_start=true ;; *) $dfn_sim_op && printf '\t%s\n' "$dfn_foo" \ || printf '\t%b%s%b\n' "$B" "$dfn_foo" "$N" ;; esac fi done fi else echo "$google_html" | pup 'div.DgZBFd, div.vdBwhd, div[data-dobid="dfn"] text{}' | sed -e 's/^/* /' -e '1 s/^* //' | recode html..ISO-8859-1 fi } # this div is google's top line answer, works for simple dates, values etc # eg: density of silver, what is the triple point of oxygen, elevation of mount everest, christmas day # "what is the " seems to be required for some things //credit @sudocanttype a_kno_val() { echo "$google_html" | pup 'div.Z0LcW.XcVN5d text{}' | tr '\n' ' ' } # Math ( eg: log_2(3) * pi^e ) //credit @BeyondMagic a_math() { echo "$google_html" | pup 'span.qv3Wpe text{}' | tr -d '\n ' | recode html..ISO-8859-1 } # Knowledge Graph - top (list) ( eg: the office cast ) //credit @Bugswriter a_kno_top() { echo "$google_html" | pup 'div.dAassd json{}' | jq -r '.[] | .children | .[] | .text' | sed ':a;N;$!ba;s/\n/ /g;s/null/\n/g' | sed '1s/.*/* &/;2,$s/.*/*&/;$d' | recode html..ISO-8859-1 } # Quotes ( eg: mahatma gandhi quotes ) //credit @PoseidonCoder a_quotes() { echo "$google_html" | pup 'div.Qynugf text{}' | recode html..ISO-8859-1 } # Basic Answers ( eg: summer solstice || easter ) // @Bugswriter # this displays similar info to kno_val but uses a different div in the google results a_basic() { echo "$google_html" | pup 'div.zCubwf text{}' | tr -d '\n' | recode html..ISO-8859-1 } # Rich Rich Answers ( eg: social network cast ) //credit @BeyondMagic a_richcast() { echo "$google_html" | pup 'a.ct5Ked json{}' | jq -r '.[] | .title' | sed 's/^/* /' | recode html..ISO-8859-1 } # Simple lists (eg: how to exit vim || how to update windows) //original snippet credit @BeyondMagic a_lists() { lists="$(echo "$google_html" | pup 'div.co8aDb.XcVN5d, li.TrT0Xe' | sed 's/^[[:blank:]]*//g' | recode html..ISO-8859-1)" if [ -n "$lists" ]; then lists_num=0 printf '%s\n' "$lists" | while IFS= read -r lists_foo; do case "$lists_foo" in "