Benningtons.net

Stuff what I did

Bash Completion —

Command-line completion is a wonderful thing. If you get bored of typing a long filename (for example) you can just hit tab and it will complete the name for you. If the shell can’t be certain of which filename you wanted then you can hit tab again and either (depending on the shell) get a list of matching names or toggle through a list of possibilities.
Some may say that’s just being lazy but I prefer to see it as being efficient and surely the less typing you do then the fewer mistakes you’ll make?
The only downsides of command-line completion are a) that it lulls you into thinking really long filenames are ok and b) that you’ll find yourself hitting tab out of habit whenever you find yourself having to type a long name, which can be annoying when you’re not even at a command-line.

Whilst working on a small project to create some shell commands of my own I realised that completion didn’t work for them. So I investigated further and was impressed by how easy completion was to configure and how you can use it to do more than just complete the current word.
Here is a summary of my findings whilst using the Bash shell on Debian 8 (Jessie).

Setup:

Wherever you’re using Bash you’ll probably find basic completion already configured. But for more advanced programmable completion you’ll need to install the bash-completion package.

When starting a terminal session, one of the Bash startup scripts (either /etc/bash.bashrc for all users or your own ~/.bashrc) will configure completion by running /usr/share/bash-completion/bash-completion.
Completion rules for individual commands can be found within the files in /usr/share/bash-completion/completions/ and in /etc/bash-completion.d/. The former appears to be for commands that came with your distro and the latter for subsequent additions.

So having looked at the examples already in these directories I decided to try and create one for my project. As I’m not changing any existing files then I don’t appear to be at risk of breaking anything. My new addition is called ce_complete.sh and I’m placing it in /etc/bash-completion.d/. To do so I need to use sudo so that system changes are made by the root user.

Note that after making any changes to these files you need to reload your Bash completion settings. This can be done by closing your terminal session and opening a new one (to cause the startup scripts to run) or you could simply re-run the completion config script with . /etc/bash-completion (which just runs the /usr/share/bash-completion/bash-completion script). Note the space in . / at the start of that command. That ensures the script is run by your current process and not in a spawned process that would reload its own completion rules instead of your own.

Now let’s look at the contents of my completion script as I add more features.

For commands:

I can’t say I’m comfortable with the use of completion on commands. Commands generally have very short names so completion shouldn’t be necessary and if you’ve forgotten the full command name then typing a few letters and then hoping for completion to fill-in the rest is dangerous.
However, Bash provides command completion by checking all the directories listed in the PATH variable to find a matching file. Use echo $PATH to see what those directories are and to test it try typing c followed by two tabs to get a list of all commands starting with ‘c’.
One of my own commands is called clice and I’ve placed the executable file in /usr/local/bin/ so it’s now listed amongst all those commands that start with ‘c’.
I can also type cli followed by tab to complete the command name but I don’t recommend it.

Long name command options however can be a pain to type and they should be descriptive enough to make completion safe. So cp --str followed by a tab will expand to cp --strip-trailing-slashes which is a useful time saver.
I realise that cp doesn’t need the full name and that cp --str is unambiguous enough for it to complete the task but it’s reassuring to complete the full name and confirm that I’ve picked the right option.

As every command appears to complete their long name options then I feel I should do the same for my clice command. Even though those options are currently just the standard --version and --help.
To achieve this my /etc/bash-completion.d/ce_complete.sh file now looks like this:

_ce_clice()
{
	local cur prev words cword
	_init_completion || return

	local opts="--help --version"
	COMPREPLY=( $( compgen -W "$opts" "$cur" ) )

	return 0
}
complete -F _ce_clice clice

[Remember that any such changes will only take effect once you have restarted your terminal session or reloaded completion settings (see above).]
Starting at the bottom of this code snippet is the complete command that links my command clice with the function _ce_clice. So whenever I now type clice and press tab, bash will call the function that makes up the rest of this code snippet.
The _ce_clice function starts in the same way as all other completion functions by declaring some local variables and then initialising them. Returning to the command line should that initialisation fail.
Those variables are:
cur=what’s been typed of the current word,
prev=the previous word in the command-line,
words=the command name and
cwords=a count of previous words on the command-line

The boilerplate for Bash completion functions then has a gap where I declare a local variable opts and load it with a space separated list of my command options before the boilerplate continues.
The function provides a response back to the command-line by loading the COMPREPLY variable with a list of valid completion results. If there is only one result then that will be inserted into the command-line.
To generate that list of results the compgen function is used to list all my command options in variable opts that match the partially typed word in variable cur.

For filenames:

In addition to the clice command my project includes a shell script for managing how I edit programs. The script is called ce_edit.sh which I place in /usr/local/bin/ but to make it easier to use I have aliased it to the command name edc by adding alias edc=’. ce_edit.sh’ to my bash startup script ~.bashrc. Note the space again in =’. ce to ensure this command runs in my current process and not a spawned process.

ce_edit.sh takes the filename that I wish to edit as an argument, so command-line completion would be very helpful. To set this up I added the following code to the same ce_completion.sh script that my previous clice command uses. I could use separate completion scripts but I prefer to keep everything related to my project together. As my completion commands call different completion functions then there is no confusion

_ce_edit()
{
	local cur prev words cword
	_init_completion || return

	COMPREPLY=( $( compgen -f ${cur} ) )

	return 0
}
complete -F _ce_edit edc

As you can see, this contains the same boilerplate code with a couple of changes. Firstly compgen is using a -f option to generate a list of files in the current directory that match the partially typed filename in the cur variable.
Secondly note that the complete command at the bottom is linking my aliased edc command to this new function and that it does not use the full script name of ce_edit.sh.

I can now type edc ce_c followed by a tab to get edc completion.sh which is really useful.

Beyond word completion:

I could have stopped there but, having noticed that these completion functions are just Bash scripts, I wondered if I could do more than just complete the current word on the command-line?

Completion for the edc command works well but I need to be in the right directory in the first place.

As a naming convention I give each project a two letter prefix (ce for my “command-line coding ecosystem” project), I keep all the code for each project in a seperate directory (~/Code/CE/ for this project) and I start each filename with the same project prefix (e.g. ce_completion.sh and ce_edit.sh for this project). So the source code for clice can be found in ~/Code/CE/ce_clice.c

So in theory I could be in any directory and type edc ce_c followed by a tab and my completion function should have enough detail to know where I need to go and which file I want to edit.
I’ve implemented this with the following additions to the _ce_edit completion function.

_ce_edit()
{
	local cur prev words cword
	_init_completion || return

# Do we have at least two letters to determine the project directory?
	if [[  ${#cur} > 1 ]]
	 then
		SOURCEDIR="/home/ben/Code/$(echo ${cur:0:2}|tr [a-z] [A-Z])"
		if [ -d "$SOURCEDIR" ]                          # does the project directory exist?
		 then
			cd ${SOURCEDIR}/
			COMPREPLY=( $( compgen -f ${cur} ) )
		fi
	fi

	return 0
}

Some may shudder at the thought of a completion function changing your working directory but this works a treat and saves a lot of time when editing code at the command-line.
You may have noticed that I haven’t managed to get the function to resolve ~ to my home directory so I’ve had to type that in full for now.

The echo and tr commands convert the first two letters at the start of a filename to uppercase for the directory name.
This can’t resolve the project directory unless two letters are typed. For completeness I should look to enable a single letter, say ‘c’, to resolve to CE or to a list of all projects starting with ‘c’.
That’s on my #TODO list but in the meantime I’m impressed by how easy Bash completion was to setup for my project.

All code for this and my other projects can now be found on my GitHub account: https://github.com/agben


Categorised as: Computer Stuff

Comments are disabled on this post


Comments are closed.