commit 24818a84793ebcd8c116486a319879544f53e948 Author: Konstantin Nazarov Date: Sat Sep 4 18:56:19 2021 +0100 Initial version of q.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..5975365 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# CLI to answer natural language questions + +`q.sh` is a command-line tool that allows you to interact with your +computer using natural language. + +Example usage: + +```sh +export Q_SCRIPT_DIR=`pwd` + +./q 2.0 kg in pounds +4.41 + +./q 1 pounds in kg +0.45 + +./q random number 1-100 +53 +``` + +## Extending q.sh + +You can extend `q.sh` using any programming language. In order to do so, you +need to place an executable file starting with `q-` in `Q_SCRIPT_DIR` +(by default, ~/.config/q.sh). + +When called without parameters, your script should return a newline-separated +list of regular expressions that match strings it's interested in. + +When called with an argument, this argument will always be what it expects to +get as input. `q.sh` will take care of dispatching the calls properly. + +To get an idea how it works, you can stury `q-weight-convert` and `q-random-number` +that are located in this repository. + +## License + +Distributed under the terms of the BSD License diff --git a/q b/q new file mode 100755 index 0000000..8ecbc07 --- /dev/null +++ b/q @@ -0,0 +1,126 @@ +#!/bin/bash +# +# Distributed under the terms of the BSD License +# +# Copyright (c) 2021, Konstantin Nazarov +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set -e + +if [[ -z "Q_SCRIPT_DIR" ]]; then + Q_SCRIPT_DIR=~/.config/q.sh +fi + +INDEX_FILE=~/.q.index + +new_scripts_exist() { + if [[ ! -d "$Q_SCRIPT_DIR" ]]; then + return 0 + fi + + if [[ ! -s "$INDEX_FILE" ]]; then + return 0 + fi + + NEWER_FILES="$(find "$Q_SCRIPT_DIR" -name "q-*" -newer "$INDEX_FILE")" + + if [[ -z "$NEWER_FILES" ]]; then + return 1 + fi + + return 0 +} + +rebuild_index() { + if [[ ! -d "$Q_SCRIPT_DIR" ]]; then + return + fi + + pushd "$Q_SCRIPT_DIR" >/dev/null + find . -type f -name 'q-*' | while read -r FN + do + FILENAME="$(echo "$FN" | sed 's/^\.\///g')" + + "$Q_SCRIPT_DIR/$FILENAME" | while read -r line + do + echo "/$line/ {print \"$FILENAME\"}" + done + done + popd > /dev/null +} + +get_scripts_for_cmd() { + if [[ ! -f "$INDEX_FILE" ]]; then + return + fi + + echo "$1" | awk -f "$INDEX_FILE" | while read -r line + do + echo "$line" + done +} + +if [[ ! -f "$INDEX_FILE" ]] || new_scripts_exist; then + rebuild_index > "$INDEX_FILE" +fi + +LIST=0 +if [[ "$1" == "--list" ]] || [[ "$1" == "-l" ]]; then + shift + LIST=1 +fi + +DRYRUN=0 +if [[ "$1" == "--dry-run" ]] || [[ "$1" == "-d" ]]; then + shift + DRYRUN=1 +fi + +COMMAND="$@" + +if [[ -z "$COMMAND" ]] && [[ ! -t 0 ]]; then + COMMAND="$(cat)" +fi + +if [[ ! -z "$COMMAND" ]]; then + SCRIPTS="$(get_scripts_for_cmd "$COMMAND")" + + if [[ "$LIST" == "1" ]]; then + echo "$SCRIPTS" + else + if [ "$(echo "$SCRIPTS" | wc -w)" -gt "1" ]; then + printf "Too many scripts matched:\n$SCRIPTS\n" 1>&2 + exit 1 + elif [[ ! -z "$SCRIPTS" ]]; then + if [[ "$DRYRUN" == "1" ]]; then + "$Q_SCRIPT_DIR/$SCRIPTS" --dry-run "$COMMAND" + else + "$Q_SCRIPT_DIR/$SCRIPTS" "$COMMAND" + fi + else + printf "No scripts matched:\n$SCRIPTS\n" 1>&2 + exit 1 + fi + fi +fi diff --git a/q-random-number b/q-random-number new file mode 100755 index 0000000..b5596a3 --- /dev/null +++ b/q-random-number @@ -0,0 +1,19 @@ +#!/bin/bash +# +# Generates random number in a specified range. Example: +# random number 1-10 + +if [[ -z "$@" ]]; then + echo "^random number [0-9]+-[0-9]+$" +elif [[ "$1" == "--dry-run" ]]; then + shift + echo "$@" +else + BOUNDS="$(echo "$@" | sed -E 's/^random number ([0-9]+)-([0-9]+)$/\1 \2/g')" + + MIN="$(echo $BOUNDS | cut -d' ' -f1)" + MAX="$(echo $BOUNDS | cut -d' ' -f2)" + + awk -v min=5 -v max=10 "BEGIN{srand(); print int($MIN+rand()*($MAX-$MIN+1))}" +fi + diff --git a/q-weight-convert b/q-weight-convert new file mode 100755 index 0000000..4c62c87 --- /dev/null +++ b/q-weight-convert @@ -0,0 +1,20 @@ +#!/bin/bash +# +# Converts weights between pounds and kilograms. Examples: +# 10 pounds in kg +# 2.5 kg in pounds + +if [[ -z "$@" ]]; then + echo "^[0-9]+(\.[0-9]+)? kg in pounds$" + echo "^[0-9]+(\.[0-9]+)? pounds in kg$" +elif [[ "$1" == "--dry-run" ]]; then + shift + echo "$@" +else + NUM="$(echo "$@" | sed -E 's/([0-9]+) .*/\1/g')" + if echo "$@" | grep -E "^[0-9]+(\.[0-9]+)? pounds in kg$" > /dev/null; then + awk "BEGIN {printf \"%.2f\n\", $NUM/2.20462}" + else + awk "BEGIN {printf \"%.2f\n\", $NUM*2.20462}" + fi +fi