diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..63acf2ab --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/composure"] + path = lib/composure + url = git@github.com:erichs/composure.git diff --git a/lib/composure b/lib/composure new file mode 160000 index 00000000..f784e3a5 --- /dev/null +++ b/lib/composure @@ -0,0 +1 @@ +Subproject commit f784e3a59c6f5d57fb6a8a93adb2c88b0799c77b diff --git a/lib/composure.bash b/lib/composure.bash old mode 100644 new mode 100755 index 70d9027c..7ab23481 --- a/lib/composure.bash +++ b/lib/composure.bash @@ -1,365 +1,546 @@ +#!/bin/bash + # composure - by erichs # light-hearted functions for intuitive shell programming -# install: source this script in your ~/.profile or ~/.${SHELL}rc script - +# version: 1.3.1 # latest source available at http://git.io/composure + +# install: source this script in your ~/.profile or ~/.${SHELL}rc script # known to work on bash, zsh, and ksh93 # 'plumbing' functions -composure_keywords () -{ - echo "about author example group param version" +_bootstrap_composure() { + _generate_metadata_functions + _load_composed_functions + _determine_printf_cmd } -letterpress () +_get_composure_dir () { - typeset rightcol="$1" leftcol="${2:- }" + if [ -n "$XDG_DATA_HOME" ]; then + echo "$XDG_DATA_HOME/composure" + else + echo "$HOME/.local/composure" + fi +} - if [ -z "$rightcol" ]; then +_get_author_name () +{ + typeset name localname + localname="$(git --git-dir "$(_get_composure_dir)/.git" config --get user.name)" + for name in "$GIT_AUTHOR_NAME" "$localname"; do + if [ -n "$name" ]; then + echo "$name" + break + fi + done +} + +_composure_keywords () +{ + echo "about author example group param version" +} + +_letterpress () +{ + typeset rightcol="$1" leftcol="${2:- }" leftwidth="${3:-20}" + + if [ -z "$rightcol" ]; then + return + fi + + $_printf_cmd "%-*s%s\n" "$leftwidth" "$leftcol" "$rightcol" +} + +_determine_printf_cmd() { + if [ -z "$_printf_cmd" ]; then + _printf_cmd=printf + # prefer GNU gprintf if available + [ -x "$(which gprintf 2>/dev/null)" ] && _printf_cmd=gprintf + export _printf_cmd + fi +} + +_longest_function_name_length () +{ + echo "$1" | awk 'BEGIN{ maxlength=0 } + { + for(i=1;i<=NF;i++) + if (length($i)>maxlength) + { + maxlength=length($i) + } + } + END{ print maxlength}' +} + +_temp_filename_for () +{ + typeset file=$(mktemp "/tmp/$1.XXXX") + command rm "$file" 2>/dev/null # ensure file is unlinked prior to use + echo "$file" +} + +_prompt () +{ + typeset prompt="$1" + typeset result + case "$(_shell)" in + bash) + read -r -e -p "$prompt" result;; + *) + echo -n "$prompt" >&2; read -r result;; + esac + echo "$result" +} + +_add_composure_file () +{ + typeset func="$1" + typeset file="$2" + typeset operation="$3" + typeset comment="${4:-}" + typeset composure_dir=$(_get_composure_dir) + + ( + if ! cd "$composure_dir"; then + printf "%s\n" "Oops! Can't find $composure_dir!" + return + fi + if git rev-parse 2>/dev/null; then + if [ ! -f "$file" ]; then + printf "%s\n" "Oops! Couldn't find $file to version it for you..." return + fi + cp "$file" "$composure_dir/$func.inc" + git add --all . + if [ -z "$comment" ]; then + comment="$(_prompt 'Git Comment: ')" + fi + git commit -m "$operation $func: $comment" fi - - printf "%-20s%s\n" "$leftcol" "$rightcol" + ) } -transcribe () +_transcribe () { - typeset func=$1 - typeset file=$2 - typeset operation="$3" + typeset func="$1" + typeset file="$2" + typeset operation="$3" + typeset comment="${4:-}" + typeset composure_dir=$(_get_composure_dir) - if git --version >/dev/null 2>&1; then - if [ -d ~/.composure ]; then - ( - cd ~/.composure - if git rev-parse 2>/dev/null; then - if [ ! -f $file ]; then - printf "%s\n" "Oops! Couldn't find $file to version it for you..." - return - fi - cp $file ~/.composure/$func.inc - git add --all . - git commit -m "$operation $func" - fi - ) - else - if [ "$USE_COMPOSURE_REPO" = "0" ]; then - return # if you say so... - fi - printf "%s\n" "I see you don't have a ~/.composure repo..." - typeset input - typeset valid=0 - while [ $valid != 1 ]; do - printf "\n%s" 'would you like to create one? y/n: ' - read input - case $input in - y|yes|Y|Yes|YES) - ( - echo 'creating git repository for your functions...' - mkdir ~/.composure - cd ~/.composure - git init - echo "composure stores your function definitions here" > README.txt - git add README.txt - git commit -m 'initial commit' - ) - # if at first you don't succeed... - transcribe "$func" "$file" "$operation" - valid=1 - ;; - n|no|N|No|NO) - printf "%s\n" "ok. add 'export USE_COMPOSURE_REPO=0' to your startup script to disable this message." - valid=1 - ;; - *) - printf "%s\n" "sorry, didn't get that..." - ;; - esac - done - fi - fi -} - -typeset_functions () -{ - # unfortunately, there does not seem to be a easy, portable way to list just the - # names of the defined shell functions... - - # first, determine our shell: - typeset shell - if [ -n "$SHELL" ]; then - shell=$(basename $SHELL) # we assume this is set correctly! + if git --version >/dev/null 2>&1; then + if [ -d "$composure_dir" ]; then + _add_composure_file "$func" "$file" "$operation" "$comment" else - # we'll have to try harder - # here's a hack I modified from a StackOverflow post: - # we loop over the ps listing for the current process ($$), and print the last column (CMD) - # stripping any leading hyphens bash sometimes throws in there - typeset x ans - typeset this=$(for x in $(ps -p $$); do ans=$x; done; printf "%s\n" $ans | sed 's/^-*//') - typeset shell=$(basename $this) # e.g. /bin/bash => bash - fi - case "$shell" in - bash) - typeset -F | awk '{print $3}' + if [ "$USE_COMPOSURE_REPO" = "0" ]; then + return # if you say so... + fi + printf "%s\n" "I see you don't have a $composure_dir repo..." + typeset input='' + typeset valid=0 + while [ $valid != 1 ]; do + printf "\n%s" 'would you like to create one? y/n: ' + read -r input + case $input in + y|yes|Y|Yes|YES) + ( + echo 'creating git repository for your functions...' + mkdir -p "$composure_dir" || return 1 + cd "$composure_dir" || return 1 + git init + echo "composure stores your function definitions here" > README.txt + git add README.txt + git commit -m 'initial commit' + ) + # if at first you don't succeed... + _transcribe "$func" "$file" "$operation" "$comment" + valid=1 ;; - *) - # trim everything following '()' in ksh - typeset +f | sed 's/().*$//' - ;; - esac + n|no|N|No|NO) + printf "%s\n" "ok. add 'export USE_COMPOSURE_REPO=0' to your startup script to disable this message." + valid=1 + ;; + *) + printf "%s\n" "sorry, didn't get that..." + ;; + esac + done + fi + fi } +_typeset_functions () +{ + # unfortunately, there does not seem to be a easy, portable way to list just the + # names of the defined shell functions... -# bootstrap metadata keywords for porcelain functions -for f in $(composure_keywords) -do + case "$(_shell)" in + sh|bash) + typeset -F | awk '{print $3}' + ;; + *) + # trim everything following '()' in ksh/zsh + typeset +f | sed 's/().*$//' + ;; + esac +} + +_typeset_functions_about () +{ + typeset f + for f in $(_typeset_functions); do + typeset -f -- "$f" | grep -qE "^about[[:space:]]|[[:space:]]about[[:space:]]" && echo -- "$f" + done +} + +_shell () { + # here's a hack I modified from a StackOverflow post: + # get the ps listing for the current process ($$), and print the last column (CMD) + # stripping any leading hyphens shells sometimes throw in there + typeset this=$(ps -p $$ | tail -1 | awk '{print $NF}' | sed 's/^-*//') + echo "${this##*/}" # e.g. /bin/bash => bash +} + +_generate_metadata_functions() { + typeset f + for f in $(_composure_keywords) + do eval "$f() { :; }" -done -unset f + done +} + +_list_composure_files () { + typeset composure_dir="$(_get_composure_dir)" + [ -d "$composure_dir" ] && find "$composure_dir" -maxdepth 1 -name '*.inc' +} + +_load_composed_functions () { + # load previously composed functions into shell + # you may disable this by adding the following line to your shell startup: + # export LOAD_COMPOSED_FUNCTIONS=0 + + if [ "$LOAD_COMPOSED_FUNCTIONS" = "0" ]; then + return # if you say so... + fi + + typeset inc + for inc in $(_list_composure_files); do + # shellcheck source=/dev/null + . "$inc" + done +} + +_strip_trailing_whitespace () { + sed -e 's/ \+$//' +} + +_strip_semicolons () { + sed -e 's/;$//' +} # 'porcelain' functions cite () { - about creates one or more meta keywords for use in your functions - param one or more keywords - example '$ cite url username' - example '$ url http://somewhere.com' - example '$ username alice' - group composure + about 'creates one or more meta keywords for use in your functions' + param 'one or more keywords' + example '$ cite url username' + example '$ url http://somewhere.com' + example '$ username alice' + group 'composure' - # this is the storage half of the 'metadata' system: - # we create dynamic metadata keywords with function wrappers around - # the NOP command, ':' + # this is the storage half of the 'metadata' system: + # we create dynamic metadata keywords with function wrappers around + # the NOP command, ':' - # anything following a keyword will get parsed as a positional - # parameter, but stay resident in the ENV. As opposed to shell - # comments, '#', which do not get parsed and are not available - # at runtime. + # anything following a keyword will get parsed as a positional + # parameter, but stay resident in the ENV. As opposed to shell + # comments, '#', which do not get parsed and are not available + # at runtime. - # a BIG caveat--your metadata must be roughly parsable: do not use - # contractions, and consider single or double quoting if it contains - # non-alphanumeric characters + # a BIG caveat--your metadata must be roughly parsable: do not use + # contractions, and consider single or double quoting if it contains + # non-alphanumeric characters - if [ -z "$1" ]; then - printf '%s\n' 'missing parameter(s)' - reference cite - return - fi + if [ -z "$1" ]; then + printf '%s\n' 'missing parameter(s)' + reference cite + return + fi - typeset keyword - for keyword in $*; do - eval "$keyword() { :; }" - done + typeset keyword + for keyword in "$@"; do + eval "$keyword() { :; }" + done } draft () { - about wraps command from history into a new function, default is last command - param 1: name to give function - param 2: optional history line number - example '$ ls' - example '$ draft list' - example '$ draft newfunc 1120 # wraps command at history line 1120 in newfunc()' - group composure + about 'wraps command from history into a new function, default is last command' + param '1: name to give function' + param '2: optional history line number' + example '$ ls' + example '$ draft list' + example '$ draft newfunc 1120 # wraps command at history line 1120 in newfunc()' + group 'composure' - typeset func=$1 - typeset num=$2 - typeset cmd + typeset func=$1 + typeset num=$2 - if [ -z "$func" ]; then - printf '%s\n' 'missing parameter(s)' - reference draft - return - fi + if [ -z "$func" ]; then + printf '%s\n' 'missing parameter(s)' + reference draft + return + fi - # aliases bind tighter than function names, disallow them - if [ -n "$(LANG=C type -t $func 2>/dev/null | grep 'alias')" ]; then - printf '%s\n' "sorry, $(type -a $func). please choose another name." - return - fi + # aliases bind tighter than function names, disallow them + if type -a "$func" 2>/dev/null | grep -q 'is.*alias'; then + printf '%s\n' "sorry, $(type -a "$func"). please choose another name." + return + fi - if [ -z "$num" ]; then - # parse last command from fc output - # some versions of 'fix command, fc' need corrective lenses... - typeset myopic=$(fc -ln -1 | grep draft) - typeset lines=1 - if [ -n "$myopic" ]; then - lines=2 - fi - cmd=$(fc -ln -$lines | head -1 | sed 's/^[[:blank:]]*//') - else - # parse command from history line number - cmd=$(eval "history | grep '^[[:blank:]]*$num' | head -1" | sed 's/^[[:blank:][:digit:]]*//') - fi - eval "$func() { $cmd; }" - typeset file=$(mktemp /tmp/draft.XXXX) - typeset -f $func > $file - transcribe $func $file draft - rm $file 2>/dev/null + typeset cmd + if [ -z "$num" ]; then + # some versions of 'fix command, fc' need corrective lenses... + typeset lines=$(fc -ln -1 | grep -q draft && echo 2 || echo 1) + # parse last command from fc output + # shellcheck disable=SC2086 + cmd=$(fc -ln -$lines | head -1 | sed 's/^[[:blank:]]*//') + else + # parse command from history line number + cmd=$(eval "history | grep '^[[:blank:]]*$num' | head -1" | sed 's/^[[:blank:][:digit:]]*//') + fi + eval "function $func { + author '$(_get_author_name)' + about '' + param '' + example '' + group '' + + $cmd; +}" + typeset file=$(_temp_filename_for draft) + typeset -f "$func" | _strip_trailing_whitespace | _strip_semicolons > "$file" + _transcribe "$func" "$file" Draft "Initial draft" + command rm "$file" 2>/dev/null + revise "$func" } glossary () { - about displays help summary for all functions, or summary for a group of functions - param 1: optional, group name - example '$ glossary' - example '$ glossary misc' - group composure + about 'displays help summary for all functions, or summary for a group of functions' + param '1: optional, group name' + example '$ glossary' + example '$ glossary misc' + group 'composure' - typeset targetgroup=${1:-} + typeset targetgroup=${1:-} + typeset functionlist="$(_typeset_functions_about)" + typeset maxwidth=$(_longest_function_name_length "$functionlist" | awk '{print $1 + 5}') - for func in $(typeset_functions); do - if [ -n "$targetgroup" ]; then - typeset group="$(typeset -f $func | metafor group)" - if [ "$group" != "$targetgroup" ]; then - continue # skip non-matching groups, if specified - fi - fi - typeset about="$(typeset -f $func | metafor about)" - letterpress "$about" $func + for func in $(echo $functionlist); do + + if [ "X${targetgroup}X" != "XX" ]; then + typeset group="$(typeset -f -- $func | metafor group)" + if [ "$group" != "$targetgroup" ]; then + continue # skip non-matching groups, if specified + fi + fi + typeset about="$(typeset -f -- $func | metafor about)" + typeset aboutline= + echo "$about" | fmt | while read -r aboutline; do + _letterpress "$aboutline" "$func" "$maxwidth" + func=" " # only display function name once done + done } metafor () { - about prints function metadata associated with keyword - param 1: meta keyword - example '$ typeset -f glossary | metafor example' - group composure + about 'prints function metadata associated with keyword' + param '1: meta keyword' + example '$ typeset -f glossary | metafor example' + group 'composure' - typeset keyword=$1 + typeset keyword=$1 - if [ -z "$keyword" ]; then - printf '%s\n' 'missing parameter(s)' - reference metafor - return - fi + if [ -z "$keyword" ]; then + printf '%s\n' 'missing parameter(s)' + reference metafor + return + fi - # this sed-fu is the retrieval half of the 'metadata' system: - # 'grep' for the metadata keyword, and then parse/filter the matching line + # this sed-fu is the retrieval half of the 'metadata' system: + # 'grep' for the metadata keyword, and then parse/filter the matching line - # grep keyword # strip trailing '|"|; # ignore thru keyword and leading '|" - sed -n "/$keyword / s/['\";]*$//;s/^[ ]*$keyword ['\"]*\([^([].*\)*$/\1/p" + # grep keyword # strip trailing '|"|; # ignore thru keyword and leading '|" + sed -n "/$keyword / s/['\";]*\$//;s/^[ ]*$keyword ['\"]*\([^([].*\)*\$/\1/p" } reference () { - about displays apidoc help for a specific function - param 1: function name - example '$ reference revise' - group composure + about 'displays apidoc help for a specific function' + param '1: function name' + example '$ reference revise' + group 'composure' - typeset func=$1 - if [ -z "$func" ]; then - printf '%s\n' 'missing parameter(s)' - reference reference - return - fi + typeset func=$1 + if [ -z "$func" ]; then + printf '%s\n' 'missing parameter(s)' + reference reference + return + fi - typeset line + typeset line - typeset about="$(typeset -f $func | metafor about)" - letterpress "$about" $func + typeset about="$(typeset -f "$func" | metafor about)" + _letterpress "$about" "$func" - typeset author="$(typeset -f $func | metafor author)" - if [ -n "$author" ]; then - letterpress "$author" 'author:' - fi + typeset author="$(typeset -f $func | metafor author)" + if [ -n "$author" ]; then + _letterpress "$author" 'author:' + fi - typeset version="$(typeset -f $func | metafor version)" - if [ -n "$version" ]; then - letterpress "$version" 'version:' - fi + typeset version="$(typeset -f $func | metafor version)" + if [ -n "$version" ]; then + _letterpress "$version" 'version:' + fi - if [ -n "$(typeset -f $func | metafor param)" ]; then - printf "parameters:\n" - typeset -f $func | metafor param | while read line - do - letterpress "$line" - done - fi + if [ -n "$(typeset -f $func | metafor param)" ]; then + printf "parameters:\n" + typeset -f $func | metafor param | while read -r line + do + _letterpress "$line" + done + fi - if [ -n "$(typeset -f $func | metafor example)" ]; then - printf "examples:\n" - typeset -f $func | metafor example | while read line - do - letterpress "$line" - done - fi + if [ -n "$(typeset -f $func | metafor example)" ]; then + printf "examples:\n" + typeset -f $func | metafor example | while read -r line + do + _letterpress "$line" + done + fi } revise () { - about loads function into editor for revision - param 1: name of function - example '$ revise myfunction' - group composure + about 'loads function into editor for revision' + param ' -e: revise version stored in ENV' + param '1: name of function' + example '$ revise myfunction' + example '$ revise -e myfunction' + example 'save a zero-length file to abort revision' + group 'composure' - typeset func=$1 - typeset temp=$(mktemp /tmp/revise.XXXX) + typeset source='git' + if [ "$1" = '-e' ]; then + source='env' + shift + fi - if [ -z "$func" ]; then - printf '%s\n' 'missing parameter(s)' - reference revise - return - fi + typeset func=$1 + if [ -z "$func" ]; then + printf '%s\n' 'missing parameter(s)' + reference revise + return + fi - # populate tempfile... - if [ -f ~/.composure/$func.inc ]; then - # ...with contents of latest git revision... - cat ~/.composure/$func.inc >> $temp - else - # ...or from ENV if not previously versioned - typeset -f $func >> $temp - fi + typeset composure_dir=$(_get_composure_dir) + typeset temp=$(_temp_filename_for revise) + # populate tempfile... + if [ "$source" = 'env' ] || [ ! -f "$composure_dir/$func.inc" ]; then + # ...with ENV if specified or not previously versioned + typeset -f $func > $temp + else + # ...or with contents of latest git revision + cat "$composure_dir/$func.inc" > "$temp" + fi - if [ -z "$EDITOR" ] - then - typeset EDITOR=vi - fi + if [ -z "$EDITOR" ] + then + typeset EDITOR=vi + fi - $EDITOR $temp - . $temp # source edited file + $EDITOR "$temp" + if [ -s "$temp" ]; then + typeset edit='N' - transcribe $func $temp revise - rm $temp + # source edited file + # shellcheck source=/dev/null + . "$temp" || edit='Y' + + while [ $edit = 'Y' ]; do + echo -n "Re-edit? Y/N: " + read -r edit + case $edit in + y|yes|Y|Yes|YES) + edit='Y' + $EDITOR "$temp" + # shellcheck source=/dev/null + . "$temp" && edit='N';; + *) + edit='N';; + esac + done + _transcribe "$func" "$temp" Revise + else + # zero-length files abort revision + printf '%s\n' 'zero-length file, revision aborted!' + fi + command rm "$temp" } write () { - about writes one or more composed function definitions to stdout - param one or more function names - example '$ write finddown foo' - example '$ write finddown' - group composure +about 'writes one or more composed function definitions to stdout' +param 'one or more function names' +example '$ write finddown foo' +example '$ write finddown' +group 'composure' - if [ -z "$1" ]; then - printf '%s\n' 'missing parameter(s)' - reference write - return - fi +if [ -z "$1" ]; then + printf '%s\n' 'missing parameter(s)' + reference write + return +fi + +echo "#!/usr/bin/env ${SHELL##*/}" # bootstrap metadata cat <