17.4.17

Bash Code Completion

Work expands so as to fill the time available for its completion.
    - C. Northcote Parkinson
I usually have several projects going on at the same time.  There are a dozen or so reasons why I might pick something up, be it for experimentation, learning, work, fun, ???, or profit.  Over the years I've developed an ad hoc directory structure that all my code ends up in.  Everything is nested under $HOME/projects/.  From there I have directories of groups, topical or other, that help me keep things categorized.  Because I'm lazy, I created a quick script that let me jump to the correct directory by running the command:
goto group/topic_directory
goto takes the form of a function in my .bashrc:
function goto {
  cd $HOME/projects/$1
  clear
}
The method is simple enough, but because it's scoped to $HOME/projects, bash completion doesn't work, and as a result using it can be a complete PITA because I don't always recall the names of my groups or topics. So, I decided to write a quick bash completion handler for my goto tool.

Bash Completion Handlers

Bash completion handlers are pretty simple. You can see what user specified completions you may have in your configuration by running:
complete
For me, this yields:
complete -o bashdefault -o default -o nospace -F __git_wrap__git_main git
complete -o bashdefault -o default -o nospace -F __git_wrap__gitk_main gitk
These were installed by a git completion package I have installed. To break down the command complete is an internal bash command (so, man complete won't show you much). The flag -o enables the specified options, which can be found here. The first two shown here, bashdefault and default, basically tell bash to return normal bash completions where the completion script doesn't return a result. nospace tells bash not to add a space.  There are a few others, but I've decided to use these for my goto completion script.

The real choice to be made is selecting between -F function and -C command options. The -F option runs a function that exists within the running bash environment and it runs it from within the current environment. The function "communicates" with bash using an array variable COMPREPLY. It passes the command being completed at $1, the current input being completed at $2, and the previous input parameter at $3. The 'previous input' can be the same as $1, if no other input has been given:
goto [tab]
would yield
['goto', '', 'goto']
So far, so simple. A working completion for my goto function looks like this:
function goto {
  cd $HOME/projects/$1
  clear
}

function __goto_completion {
  cd $HOME/projects
  COMPREPLY=($(ls -d $2*/ 2>/dev/null))
  cd - &>/dev/null
}

complete -o bashdefault -o default -o nospace -F __goto_completion goto
I'll leave it as an exercise for the reader to parse all the bashisms. The main point to drive home here is that I'm having to cd into the directory and cd back out when I've made my list. This ensures that the suggestions are relative to projects and that the current directory doesn't remain in $HOME/projects/ after the completion script completes. If you recall the -F functions are called from within the current environment, meaning they can affect your current state, so you have to be care not to make changes.

On the other side of the table, the -C option runs a command in a spawned child environment. It passes the command being called to $1, the bash command being completed at $2, the current input being completed at $3, and the previous input parameter at $4 (similar to -F, $4 may be the same as $2, here). To simplify the completion list for the command mechanism, standard output is treated as the completion value (separated by newlines). For this demoing the command mechanism, I wrote a quick python script to extract path information from the projects directory. First the bash:
function goto {
  cd $HOME/projects/$1
  clear
}

complete -o bashdefault -o default -o nospace -C /path/to/completions/goto-completion.py goto
Notice the changes to the complete call. Now, we're passing in a path to the script to be executed.
#!/usr/bin/env python

import sys, os;

script = sys.argv[0]
command = sys.argv[1]
current_op = sys.argv[2]
last_op = sys.argv[3]

os.system("cd $HOME/projects/; ls -d1 %s*" % current_op)
I went a little overkill on the variables, considering what was used, but I figure this conveys the totality of the command arguments more clearly. Notice I do not have to worry about changing back to the original directory here. There are actually 2 reasons in this case. (1) The script above is actually creating a third shell to execute the ls command and letting the stdout pass through to python's stdout. This would act to shield the user's environment when using the -F as well. (2) The commands are still run in a child environment. Even if this script was a bash script, it could not effect the user's bash session directly. And this gives you an added layer of safety if you're doing more complex bash completions.

Conclusion

In a lot of cases, bash-completion is a step too far. The --help or the man-page provides the necessary flags to use the software effectively.  There are some cases, though, that bash integration can considerably improve user-experience, even if you're the only user. For custom productivity tools, sometimes the difference between a marginal improvement and a huge improvement is a quick bash-completion.

1 comment:

  1. The Borgata Hotel Casino & Spa - Hotels - JamBase
    Make your hotel 경상북도 출장마사지 stay 경상북도 출장마사지 at the 화성 출장마사지 Borgata one of the best 양산 출장안마 hotels in Atlantic City, NJ. It's convenient and convenient for business 포천 출장안마 travel.

    ReplyDelete