Categories
Programming

Building a Command-Line Toolset Part I – Root Command

Introduction

Like many programmers, I have a deep affinity for the command-line. In my head, all good backend systems start with a solid core driven by terminal commands. This core system runs "silently" outside of a GUI context and relies on configuration and signals received during runtime to dictate its behavior. GUI's have a lot of great characteristics, such as contextual linking, but they come later IMHO.

I admire command-line interfaces that have a root command with subcommands. I think any good system should provide a family of commands for you to work with said system. There are tons of examples of this in the Gnu/Linux world, such as git or the Heroku Toolbelt. I'm not just talking about a single program with a ton of options, like ssh or rsync. Rather, a family of commands each with their own complete set of options and all bound by a common implicit configuration. An implicit configuration relieves the user of laboriously specifying this configuration every time they invoke one of the commands. Again, git is a good example. When your pwd is your git repo, your .git/config file is used. This allows you to give simple commands such as push or pull and benefit from the implicit specification of your origin's url.

In the last few months I've developed two command-line systems based on this methodology and I wanted to go over how I did it.

Root Command

In my case the root command is a shell script written in bash. In the examples and blog posts that follow, I'll be writing a fictitious drone fleet management system. Say we have a root command called drone. The bash script might look like this:

[code lang="bash"]

#!/bin/bash

droneprelude

CMD=$1

if [ -z $CMD ]; then
echo "drone is installed to $DRONE_HOME"
else
shift
if [ -f $DRONE_BIN/commands/$CMD ]; then
$DRONE_BIN/commands/$CMD $@
else
if [ ! -d "$DRONE_CORE/target" ]; then
echo "Scala scripts not compiled. Run drone package"
exit 1
fi
MEMSIZE=256M
CLASSPATH=$(gt4 classpath)
java -Xms$MEMSIZE -Xmx$MEMSIZE -cp $CLASSPATH com.nickcoding.dronetools.scripts.$CMD $@
exit #?
fi
fi
[/code]

You may notice a few things going on here:

  • The drone command is executable and invokes /bin/bash via the standard #!/bin/bash preamble
  • A shell function called droneprelude is called. This sources all environment variables needed by the scripts and sub-processes. This is important because a primary goal was to NOT pollute the standard interactive shell with a lot of extra environment variables for out system. Using this mechanism, the environment variables are available only within the drone command and sub-commands.
  • If no params are specified we simply exit with a simple message
  • We then go to launch some program. We first check to see if an executable command exists in the command/sub-directory. If it does, then it's launched. If not, we assume the command is the name of a class in the JVM and we launch java specifying that class and the proper CLASSPATH for all of our dependencies.

The droneprelude shell function looks like this:

[code lang="bash"]
droneprelude() {
source $DRONE_HOME/bin/.drone-prelude export
}
[/code]

And the .drone-prelude script looks like this:

[code lang="bash"]
DRONE_BIN=$DRONE_HOME/bin
DRONE_CORE=$DRONE_HOME/core
DRONE_SCODE=$DRONE_HOME/core/src/main/scala/com/nickcoding/dronetools/

DRONE_DB_HOME=/liquidnet/mongodb-linux-x86_64-2.4.9
DRONE_DB_DBPATH=$DRONE_HOME/database
DRONE_DB_LOGPATH=$DRONE_HOME/logs/mongo.log
DRONE_DB_SERVER=localhost
DRONE_DB_PORT=27017

if [ "$1" == "export" ]; then
drone=$(grep "^DRONE_" "$BASH_SOURCE" | cut -d= -f1)
for e in $drone; do
export $e
done
fi
[/code]

The last part is the most interesting. This is a bit of a hack, but what's happening is the prelude does something special when "export" is specified. The script greps itself and issues an export on each environment variable. This is essential so sub-processes can inherit the variable. Therefore, Scala scripts within the JVM will have access to all of the environment-based configuration.

Sure, a Scala program might not want to heavily rely on environment variables and rather use a good configuration library like Typesafe Config. However, some configuration is  useful in a bash context so a balance may need to be struck.

That's all I'll write about today. In a future post I'll describe more aspects of the system. Here is a list of topics I plan to cover:

  • Building simple commands which return aspects of the configuration or the status of the running system
  • Building JVM-based code and how it is integrated into the toolchain. We also cover JVM startup time and how to manage that.
  • How to manage stdout/stderr, prompts, suppression of prompts for automation
  • E-mail of output, managing control and escape sequences
  • Pretty formatting of JSON, XML, and ini file export formats
  • Using stdout, process substitution, and chaining to provide a flexible means of multiple command communicating with each other
  • and lots more!