1#!/usr/local/bin/bash
2VERSION="0.0.2"
3
4# with tool
5# usage: with <program>
6# opens an interactive shell instance that automatically prefixes all subsequent commands with program name
7
8# bind TAB to completion function within the script
9bind -x '"\C-i": "with_completion"' &> /dev/null
10
11print_version()
12{
13  echo "with, version $VERSION"
14  exit 0
15}
16
17print_options()
18{
19  echo "  -h, --help   : Display command help"
20  echo "  -v, --version: Display the currently installed version of with"
21}
22
23print_usage()
24{
25  print """
26    USAGE:
27      with <prefix>
28
29    Prefix can be any string with a valid executable.
30  """
31  exit 0
32}
33
34#add options here, such as -h, -v
35declare -a prefix
36prefix=( "$@" )
37
38case ${prefix[*]} in
39  "" )
40    echo "Missing arguments."
41    echo "usage: with <program>"
42    exit 1;;
43  "-v"|"--version")
44    print_version;;
45  "-h"|"--help")
46    print_help;;
47  -*|--*)
48    echo "Unrecognised option:" ${prefix[*]}
49    print_options
50    exit 1;;
51esac
52
53pmpt=${prefix[*]}
54
55setup()
56{
57
58  # source bash completions
59  [ -f /etc/bash_completion ] && source /etc/bash_completion
60
61  BASH_COMPLETION_DEFAULT_DIR=/usr/share/bash-completion/completions
62  for completion_file in $BASH_COMPLETION_DEFAULT_DIR/* $BASH_COMPLETION_COMPAT_DIR/*
63  do
64    . "$completion_file" &> /dev/null
65  done
66
67  # initialise history file
68  touch /tmp/with_history
69
70  # set up colour codes
71  __blk="$(tput setaf 0)"
72  __red="$(tput setaf 1)"
73  __grn="$(tput setaf 2)"
74  __yel="$(tput setaf 3)"
75  __blu="$(tput setaf 4)"
76  __mag="$(tput setaf 5)"
77  __cyn="$(tput setaf 6)"
78  __wht="$(tput setaf 7)"
79
80  __bold_blk="$__bold$__blk"
81  __bold_red="$__bold$__red"
82  __bold_grn="$__bold$__grn"
83  __bold_yel="$__bold$__yel"
84  __bold_blu="$__bold$__blu"
85  __bold_mag="$__bold$__mag"
86  __bold_cyn="$__bold$__cyn"
87  __bold_wht="$__bold$__wht"
88
89  __on_blk="$(tput setab 0)"
90  __on_red="$(tput setab 1)"
91  __on_grn="$(tput setab 2)"
92  __on_yel="$(tput setab 3)"
93  __on_blu="$(tput setab 4)"
94  __on_mag="$(tput setab 5)"
95  __on_cyn="$(tput setab 6)"
96  __on_wht="$(tput setab 7)"
97
98  # color reset
99  __nc="$(tput sgr0)"
100
101  __blk() { echo -n "$__blk$*$__nc"; }
102  __red() { echo -n "$__red$*$__nc"; }
103  __grn() { echo -n "$__grn$*$__nc"; }
104  __yel() { echo -n "$__yel$*$__nc"; }
105  __blu() { echo -n "$__blu$*$__nc"; }
106  __mag() { echo -n "$__mag$*$__nc"; }
107  __cyn() { echo -n "$__cyn$*$__nc"; }
108  __wht() { echo -n "$__wht$*$__nc"; }
109
110  __bold_blk() { echo -n "$__bold_blk$*$__nc"; }
111  __bold_red() { echo -n "$__bold_red$*$__nc"; }
112  __bold_grn() { echo -n "$__bold_grn$*$__nc"; }
113  __bold_yel() { echo -n "$__bold_yel$*$__nc"; }
114  __bold_blu() { echo -n "$__bold_blu$*$__nc"; }
115  __bold_mag() { echo -n "$__bold_mag$*$__nc"; }
116  __bold_cyn() { echo -n "$__bold_cyn$*$__nc"; }
117  __bold_wht() { echo -n "$__bold_wht$*$__nc"; }
118
119  __on_blk() { echo -n "$__on_blk$*$__nc"; }
120  __on_red() { echo -n "$__on_red$*$__nc"; }
121  __on_grn() { echo -n "$__on_grn$*$__nc"; }
122  __on_yel() { echo -n "$__on_yel$*$__nc"; }
123  __on_blu() { echo -n "$__on_blu$*$__nc"; }
124  __on_mag() { echo -n "$__on_mag$*$__nc"; }
125  __on_cyn() { echo -n "$__on_cyn$*$__nc"; }
126  __on_wht() { echo -n "$__on_wht$*$__nc"; }
127}
128
129__print_prompt() {
130  __prefix="${prefix[*]}" print_prompt "$@"
131}
132
133print_prompt() {
134
135  # TODO: change name to correct
136  hashdollar() {
137    (( UID )) && echo '$' \
138              || echo '#'
139  }
140
141  colorise_prompt() {
142    local to_be_replaced=(blk red grn yel blu mag cyn wht)
143
144    local SED_COMMAND_LINE=('sed' '-E')
145
146    for color in "${to_be_replaced[@]}"; do
147      SED_COMMAND_LINE+=(
148        '-e' "s/%on_$color%/$(eval echo "\$__on_$color")/g"
149        '-e' "s/%bold_$color%/$(eval echo "\$__bold_$color")/g"
150        '-e' "s/%$color%/$(eval echo "\$__$color")/g"
151        )
152    done
153
154    "${SED_COMMAND_LINE[@]}" -e "s/%nc%/$__nc/g"
155  }
156  local __escaped_prefix=$(echo -n "$__prefix" | sed -e 's/\./\\./g' -e 's/\//\\\//g')
157  echo -n "$*" | colorise_prompt | sed -E -e "s/%prefix%/$__escaped_prefix/g" \
158                                          -e "s/\\$/$(hashdollar)/g"
159}
160
161with_completion()
162{
163  # print readline's prompt for visual separation
164  if [ "$#" -eq 0 ]; then
165      echo "$(__print_prompt "$PROMPT_FORMAT")$READLINE_LINE"
166  fi
167
168  # remove part after readline cursor from completion line
169  local completion_line completion_word
170  completion_line="${READLINE_LINE:0:READLINE_POINT}"
171  completion_word="${completion_line##* }"
172
173  # set completion cursor according to pmpt length
174  COMP_POINT=$((${#pmpt}+${#completion_line}+1))
175  COMP_WORDBREAKS="\n\"'><=;|&(:"
176  COMP_LINE="$pmpt $completion_line"
177  COMP_WORDS=($COMP_LINE)
178
179  # TODO: the purpose of these variables is still unclear
180  # COMP_TYPE=63
181  # COMP_KEY=9
182
183  # get index of word to be completed
184  local whitespaces_count escaped_whitespaces_count
185  whitespaces_count=$(echo "$COMP_LINE" | grep -o ' ' | wc -l)
186  escaped_whitespaces_count=$(echo "$COMP_LINE" | grep -o '\\ ' | wc -l)
187  COMP_CWORD=$((whitespaces_count-escaped_whitespaces_count))
188
189  # get sourced completion command
190  local program_name complete_command
191  program_name=${COMP_WORDS[0]}
192  program_name=$(basename "$program_name")
193  complete_command=$(complete -p | grep " ${program_name}$")
194
195  COMPREPLY=()
196
197  # execute appropriate complete actions
198  if [[ "$complete_command" =~  \ -F\  ]]
199  then
200    local complete_function
201    complete_function=$(awk '{for(i=1;i<=NF;i++) if ($i=="-F") print $(i+1)}' <(echo "$complete_command"))
202
203    # generate completions
204    $complete_function
205  else
206    # construct compgen command
207    local compgen_command
208    compgen_command=$(echo "$complete_command" | sed 's/^complete/compgen/g')
209    compgen_command="${compgen_command//$program_name/$completion_word}"
210
211    # generate completions
212    COMPREPLY=($($compgen_command))
213  fi
214
215  # get commmon prefix of available completions
216  local completions_prefix readline_prefix readline_suffix
217  completions_prefix=$(printf "%s\n" "${COMPREPLY[@]}" | \
218    sed -e '$!{N;s/^\(.*\).*\n\1.*$/\1\n\1/;D;}' | xargs)
219  readline_prefix="${READLINE_LINE:0:READLINE_POINT}"
220  readline_suffix="${READLINE_LINE:READLINE_POINT}"
221  # remove the word to be completed
222  readline_prefix=$(sed s/'\w*$'// <(echo "$readline_prefix") | xargs)
223
224  READLINE_LINE=""
225  if [[ "$readline_prefix" != "" ]]; then
226    READLINE_LINE="$readline_prefix "
227  fi
228
229  READLINE_LINE="$READLINE_LINE$completions_prefix"
230  # adjust readline cursor position
231  READLINE_POINT=$((${#READLINE_LINE}+1))
232
233  if [[ "$readline_suffix" != "" ]]; then
234    READLINE_LINE="$READLINE_LINE $readline_suffix"
235  fi
236
237  local completions_count display_all
238  completions_count=${#COMPREPLY[@]}
239  display_all="y"
240  if [[ $completions_count -eq 1 ]]; then
241    READLINE_LINE=$(echo "$READLINE_LINE" | xargs)
242    READLINE_LINE="$READLINE_LINE "
243    return
244  elif [[ $completions_count -gt 80 ]]; then
245    echo -en "Display all $completions_count possibilities? (y or n) "
246    read -N 1 display_all
247    echo "$display_all"
248  fi
249
250  if [[ "$display_all" = "y" ]]; then
251    for completion in "${COMPREPLY[@]}"; do echo "$completion"; done | column
252  fi
253}
254
255finish()
256{
257  # save history to bash history
258  if [ -f ~/.bash_history ]; then
259    cat /tmp/with_history >> ~/.bash_history
260  fi
261  rm /tmp/with_history
262}
263
264drop_with()
265{
266  if [ ${#prefix[@]} -gt 1 ]
267  then
268    prefix=( "${prefix[@]:0:${#prefix[@]}-1}" )
269  else
270    exit 0
271  fi
272}
273
274add_with()
275{
276  # separate into white space
277  # FIXME: foo "bar baz" should add two elements not one
278  IFS=' ' read -r -a parse_array <<< "$@"
279  prefix=( "${prefix[@]}" "${parse_array[@]}" )
280}
281
282run_with()
283{
284  while IFS="" read -r -e -d $'\n' -p "$(__print_prompt "$PROMPT_FORMAT")" options; do
285    history -s "$options" > /dev/null 2>&1
286
287    curr_command="$(echo "$options" | { read -r first rest ; echo "$first" ; })"
288    case $curr_command in
289      "" )
290        # null case: run prefix
291        ${prefix[*]} ;;
292      !* )
293        # replace current command
294        drop_with
295        parsed=${options#"!"}
296        add_with "$parsed" ;;
297      -* )
298        # remove with
299        parsed=${options#"-"}
300        if [ -z "$parsed" ]; then
301          drop_with
302        else
303          for ((x=0; x<$((parsed)); x++)) {
304            drop_with
305          }
306        fi
307        pmpt="${prefix[*]}" ;;
308      +* )
309        # nesting withs
310        parsed=${options#"+"}
311        add_with "$parsed"
312        pmpt="${prefix[*]}" ;;
313      :* )
314        # shell command
315        parsed=${options#":"}
316        if [ "$parsed" = "q" ]; then
317          exit 0
318        fi
319        IFS=' ' read -r -a parsed_array <<< "$parsed"
320        echo "${parsed_array[@]}" >> /tmp/with_history
321        eval "${parsed_array[@]}" ;;
322      * )
323        # prepend prefix to command
324        echo "${prefix[*]} ${options}" >> /tmp/with_history
325        eval "${prefix[*]} ${options}"
326    esac
327  done
328}
329
330main()
331{
332  trap finish exit
333  [ "$PROMPT_FORMAT" ] || PROMPT_FORMAT+='%yel%$%nc% ' \
334                        PROMPT_FORMAT+='%cyn%%prefix%%nc% ' \
335                        PROMPT_FORMAT+='%wht%>%nc% '
336
337  HISTCONTROL=ignoreboth
338
339  # run script setup
340  setup
341
342  if [ "$1" == "" ]; then
343    print_usage
344  elif ! type "$1" > /dev/null 2>&1; then
345    echo "error: \"$1\" is not a valid executable"
346    exit 1
347  fi
348
349  while true ; do
350    run_with
351  done
352}
353
354main "$@"
355