Skip to content

Instantly share code, notes, and snippets.

@bruno-de-queiroz
Last active January 30, 2018 13:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bruno-de-queiroz/a1c9e5b24b6118e45f4eb2402e69b2a4 to your computer and use it in GitHub Desktop.
Save bruno-de-queiroz/a1c9e5b24b6118e45f4eb2402e69b2a4 to your computer and use it in GitHub Desktop.
Autocomplete example and annotation parser for bash scripts to make easier to expose functions and flags, to create help info and short command list for autocompletion script
#!/bin/bash
#
# Autocomplete example and annotation parser for bash scripts to make easier to expose functions and flags,
# to create help info and short command list for autocompletion script
#
# Usage:
#
# source $SCRIPT_PATH/core.sh
# ...
# #@flag -e|--environment
# #@description sets the environment to use. default: "production"
# ENVIRONMENT=${ENVIRONMENT:-"production"}
# ...
# #@public
# #@description run the script
# function run(){
# ...
# }
#
# init $0 "$@"
#
GLOBAL_SCRIPT=""
GLOBAL_METHODS=""
GLOBAL_FLAGS=""
function _shortlist(){
if [ -z "$1" ]; then
echo -ne "$GLOBAL_METHODS" | cut -f 1 -d " "
else
if [ ! -z "$2" ]; then
if [[ "$2" =~ ^- ]]; then
return 0
fi
fi
echo -ne "$GLOBAL_FLAGS" | cut -f 1 -d " " | tr "|" " "
fi
}
function _help(){
local methods=$(echo -e "$GLOBAL_METHODS" | cut -f 1 -d " " | tr '\n' ' ')
local flags=$(echo -e "$GLOBAL_FLAGS" | cut -f 1 -d " " | sed -E 's/\|\-\-[^[:space:]]+//g' | tr '\n' ' ')
echo -n "Usage: $(basename ${GLOBAL_SCRIPT//.sh/}) [ ${methods}]"
if [ -n "$GLOBAL_FLAGS" ]; then
echo -n "[ $flags]"
echo ""
echo ""
echo "Flags:"
echo -e "$GLOBAL_FLAGS"
echo ""
else
echo ""
echo ""
fi
echo "Commands:"
echo -e "$GLOBAL_METHODS"
}
function init(){
local all=($@)
local script=${all[0]}
local content=$(cat $script | sed -n -E '/#@/,/^[^#]/ p' | tr '\n' '%' | sed -E 's/(%[^#]+)%#/\1\\n#/g')
all=(${all[@]:1})
local str="${all[@]}"
local flags=""
local methods=""
#TODO try to put this inside a method
#Iterating through lines on $content
while read -r f; do
local is_flag=$(echo $f | grep '#@flag')
local is_method=$(echo $f | egrep '#@public.*function')
if [[ "$is_flag" ]]; then
local flag=$(echo $f | sed -E 's/.*#@flag([^\%]+)%.*/\1/g')
local property=$(echo $f | sed -E 's/.*%([^=]+)\=.*/\1/g')
local description=$(echo $f | sed -E 's/.*#@description([^\%]+)%.*/\1/g')
#Adding content to variable that holds flags descriptions
flags+=$(printf "$([[ -n "$flags" ]] && echo '\\n')%-20s %s" $flag "$description")
#If flag has been passed set the variables to the value passed
if [[ "${str}" =~ ($flag)([[:space:]]|=)([\"|\'][^\"]+[\"|\']|[^[:space:]]+) ]]; then
local line=${BASH_REMATCH[0]}
local key=${BASH_REMATCH[1]}
local value=${BASH_REMATCH[3]}
#Setting the variable with the value
export $property="$value"
str="${str//$line/ }"
fi
content=${content//"#@flag $f\n"/}
elif [[ "$is_method" ]]; then
local method=$(echo $f | sed -E 's/.*function([^\(]+)\(\).*/\1/g')
local description=$(echo $f | sed -E 's/.*#@description([^\%]+)%.*/\1/g')
#Adding content to variable that holds methods descriptions
methods+=$(printf "$([[ -n "$methods" ]] && echo '\\n')%-20s %s" $method "$description")
fi
done <<< "$(echo -e $content)"
methods+=$(printf "$([[ -n "$methods" ]] && echo '\\n')%-20s %s" help " show options and flags available")
all=($str)
GLOBAL_SCRIPT=$script
GLOBAL_FLAGS=$flags
GLOBAL_METHODS=$methods
#Verify if the method called is public
local first=${all[0]}
all=(${all[@]:1})
if [ -n "$(echo -e $content | egrep "#@(public|protected).*function $first\(\)")" ]; then
#Call method with the arguments passed
$first "${all[@]}"
exit 0
fi
#Show help if the method called was not found
case $first in
shortlist)
echo $(_shortlist "${all[@]}")
;;
*|help)
_help "$script" "$flags" "$methods"
;;
esac
}
#!/bin/bash
_foo()
{
local cur index method option last opts src
COMPREPLY=()
src="/usr/local/bin/foo"
cur="${COMP_WORDS[COMP_CWORD]}"
method=""
option=""
last=""
if [[ $COMP_CWORD -gt 1 ]]; then
index=$(($COMP_CWORD - 1))
first=$(($COMP_CWORD - $index))
method="${COMP_WORDS[first]}"
if [[ $COMP_CWORD -gt 2 ]]; then
option="${COMP_WORDS[first+1]}"
if [[ $COMP_CWORD -gt 3 ]]; then
last="${COMP_WORDS[index]}"
fi
fi
fi
opts=$($src shortlist ${method} ${option} ${last})
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _foo foo
#!/bin/bash
# @description module name
#import core.sh
. core.sh
# All variables with the @flag annotation will be exposed as flag, and the @description is required.
#@flag -t|--test
#@description description of the flag
LOG_MODULE_TEST_FLAG=""
# Methods with the @public annotation will be exposed as command, and the @description is required.
#@public
#@description prints $LOG_MODULE_TEST_FLAG value
function test(){
echo $LOG_MODULE_TEST_FLAG
}
# Methods with the @protected annotation will not be exposed as command in helps and autocomplete
# but can be called from command-line
#@protected
function protected(){
echo "Protected method"
}
# Methods with no annotations will not be exposed as command in helps and autocomplete
# and can only be called by another scripts if they import the script. (i.e.: source $LOG_SCRIPTS/modules/test.sh)
function private(){
echo "private"
}
# Bootstraps the parser
init $0 "$@"
$ ./foo.sh help
Usage: foo [ hello ] [ -e|--environment ]
Flags
-e|--environment sets the environment to use. default: "production"
Options
hello prints hello world
$ ./foo.sh hello
hello world production
$ ./foo.sh hello -e test
hello world test
$ ./foo.sh hello --environment dev
hello world dev
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment