Command line and terminal programs are also tightly coupled with the history of programming (especially scripting) languages and system design since for a long time they where the major tools for human-computer interaction. Remember the Digital Command Language (DCL) or IBM’s Job Control Language (JCL) for mainframes? Hundreds of shell implementations (Bourne shell, Korn shell, bash, zsh, (t)csh, fish, cmd.exe, iSeries QShell, Windows PowerShell, Pash, Hotwire, etc.) for all kind of operating systems and the long tradition of command line utilities (e.g. AIX smitty) especially in UNIX-like environments clearly show the importance of the CLI. For embedded and networking devices (e.g. routers, switches. etc.) mastering the command line is still a necessary requirement for professional certifications (e.g. CCNP).
From a more modern perspective command line tools and the interrelated mini or little languages (e.g. check Eric Raymond’s book about The Art of Unix Programming) are special cases of Domain-specific languages (DSL) with the additional focus on interactivity and do not look that old fashioned any more! The syntactic resemblance of command line expressions with functional language constructs e.g. in Haskell or Scala shows further paths of investigation and potential innovations.
The past and future of command line tools
So, how can we build smarter command line tools for systems and especially configuration management? Before I try to answer this question and also delve into some implementation details, let’s look at potential features of a more sophisticated CLI. You will easily recognize that they are not new and have been implemented to some extent in a lot of environments but I am not aware of any shell (framework) which realizes all of them.
1. Everything can be done with a command
This looks very similar to the TCL (Tool Command Language) principle everything is a command but has a slightly different meaning here, i.e. you do not have to edit configuration files (e.g. located in /etc) anymore but just do everything interactively or in a shell script with commands. Most router operating systems follow this approach since they want to control the interaction with the user in a very precise way. As an example take the following DHCP configuration using the Cisco IOS shell:
Router(config)# interface ethernet0/0
Router(config-if)# ip address 192.168.1.1 255.255.255.0
Router(config-if)# no shutdown
Router(config)# ip dhcp pool 1
Router(dhcp-config)# network 192.168.1.0 /24
Router(dhcp-config)# domain-name foobar.com
Router(dhcp-config)# dns-server 192.168.1.254
Router(dhcp-config)# default-router 192.168.1.254
Router(dhcp-config)# lease 30
This looks very much like editing /etc/dhcpd.conf interactively similar to the omshell (i.e. OMAPI command shell) which allows real-time updates for the ISC DHCP server without restarting it. The Cobbler install server (a Red Hat emerging technologies project written in Python) makes quite extensive use of the omshell borrowing some ideas from the Kusu HPC framework.
Wrapping all system configuration actions in commands is theoretically not a big deal since you can write scripts that manipulate configurations files or database/registry/LDAP/Active Directory entries. But in practice this is a very hard problem because of the many different configuration file formats and all project which tried to solve it basically failed. A novel approach is the Augeas project which is a command line tool to manipulate configuration files from the shell. It also includes a C-library, language bindings and a domain-specific language to describe configuration file formats. Here’s an example with /etc/hosts. For details and explanations check the Augeas website.
augtool> set /files/etc/hosts/10000/ipaddr 192.168.0.1
augtool> set /files/etc/hosts/10000/canonical pigiron.example.com
augtool> set /files/etc/hosts/10000/alias pigiron
saugtool> et /files/etc/hosts/10000/alias piggy
Augeas is carefully designed to keep the structure of your configuration files intact but also shows the complexity of this problem. Standardizing on XML would not be a solution either and just adds another very interaction unfriendly format to the zoo of formats especially if you expose it directly to the user. Navigating and manipulating an XML tree with commands and completely hiding the XML behind it is a good approach for legacy free projects though. Very instructive examples of configuration file free commands are zpool and zfs from the OpenSolaris project. You can just start using them as shown in the following example:
# zpool create tank c1t0d0
# zfs create tank/home
# zfs set mountpoint=/export/home tank/home
# zfs set sharenfs=rw tank/home
It should be clear by now what is meant by everything can be done with a command and how powerful this approach is but we will push this principle much further in the following paragraphs. If Unix had takes this approach from the beginning we would probably not have this chaos of configuration files today.
2. Eliminate flat namespaces with object/command hierarchies
Thousands of commands available at your fingertips is nothing unusual on a Unix/Linux system but the global and flat command namespace is getting bigger and bigger every day with many projects introducing new commands. This can be quite challenging and intimidating for beginners.
Technically speaking this is not necessary if you introduce a hierarchy of configuration or system objects similar to the /proc or /sys filesystem and navigate through them with commands like cd, ls, set and create (or mk) instead of using the many ls* (lspci, lshal, lsusb,…), *adm (mdadm, iscsiadm, fsadm,…) or *config* (nss-config, sdl-config, system-config-*,…) commands.
Let’s start this virtual hierarchy e.g. at /system but feel free to choose a different name of your choice. Staying outside this directory you get your normal shell behavior which is important since another principle we will follow is to enable users with new possibilities and not enforce anything if they do not like it. As soon as you enter the /system directory you will get a completely different environment with dynamically created object/filesystem hierarchies, a restricted $PATH variable with commands depending on the context and much more. Let’s take a look at a simple example:
# cd /system
(system)# cd users
(users)# cd t<TAB>
tanita therese tom
(users)# cd tom
cd ls set ...
name = Tom Waits
home = /home/tom
shell = /bin/bash
groups = tom,users
(users/tom)# cd ..
(users)# create user tim
(users)# cd tim
(users/tim)# set <TAB>
name home shell groups ...
(users/tim)# set name Tim Brian
(users/tim)# set shell /bin/zsh
(users/tim)# set groups tim,users
(users/tim)# cd ..
tanita therese tim tom
So there is no need for specialized commands like adduser, useradd, etc. anymore. You can use standard commands for object creation (in the example above I used create user), set their properties with set (using command line completion) and navigate around with cd, ls, etc. It’s not my intention to standardize commands but this could be a side effect in the long run. Each configuration domain (storage, servers, networking, InfiniBand, FCoE, iSCSI, backup, archiving, security, authentication, file systems, clusters, virtualization, cloud computing, groupware, etc.) should be discussed by subject matter experts and different implementations can compete against each other until a favorite CLI syntax or environment shows up.
It’s also important to start with commands at a higher abstraction level than system commands to get an immediate benefit for such an approach. Very good candidates are Linux cluster (Rocks, xCAT, Oscar), virtual machine or cloud computing frameworks (like virsh, Enomalism, Ganeti, OpenQRM, Eucalyptus, OpenNebula, Nimbus) and Linux appliance construction toolkits.
Take for example virsh to manage virtual machines. Why implement a special shell for this purpose? Just define a new namespace for virtual machines and appropriate commands like in the following example executed on host1:
# cd /system/vms
fedora1 fedora2 debian1
(vms)# ls -l
...more detailed info about the vms including status
(vms)# cd fedora1
(vms/fedora1)# ls <TAB>
capabilites status vcpus mem ..
(vms/fedora1)# cd ..
(vms)# migrate fedora1 to host2
A cd command in this context could also mean that we log into the virtual machine and get a restricted shell (using the same framework) running inside the virtual machine. This leads us to our next requirement.
3. Network transparency
The virtual machine example above shows that it would be very convenient to handle local and remote virtual machines (VMs) in the same way with network transparent commands (cd /systems/host2/vms; ls). The implementation could use Expect-like mechanisms, a simple ssh, XMPP (or even Google Wave), Telepathy DTubes, the Plan9 resource sharing protocol 9P (take a look at the xcpu project using this idea), special tools and libraries (like libvirt) for certain commands, a FUSE based file system or some RPC-mechanism for non-UNIX operating systems.
We can also integrate routers, switches, storage devices, terminal servers, intelligent PDUs and other network enabled equipment into our object hierarchy and access it exactly the same way (e.g. cd /systems/switch1). The user would either get the real shell prompt of the device (if there is one available) or work in a restriced/simulated environment with features like command line completion or object hierarchies even if the real hardware does not support them. Accessing the
- management module of a Blade chassis (cd /systems/chassis1/mm)
- service processor of a rack server (cd /systems/host1/ipmi)
with ssh, SMASH or IPMI are some examples. There is no need to implement a special IPMI shell if you can integrate IPMI commands with ipmicmd, ipmitool, etc. into a more general and powerful framework! SMASH is interesting because it gives you exactly the proposed network transparent hierarchical access to your hardware in an administrator-friendly way hiding all the complexity of CIM-XML over HTTP, WS-MAN and other XML standards. Since SMASH is concentrating more on the hardware itself it is a nice and orthogonal supplement to our proposal.
4. SQL-like and functional command syntax
How are we going to manipulate groups of objects in our framework? Assume you want to migrate your oldest not accessed files bigger than 10 MB to a tape system when your expensive storage gold pool is more than 90% full. IBM’s General Parallel File System (GPGS) uses a very interesting SQL-like syntax (and a parallel implementation) for this kind of problems:
MIGRATE FROM POOL ‘gold' THRESHOLD(90,85) WEIGHT(CURRENT_TIMESTAMP - ACCESS_TIME)
TO POOL ‘hsm’ WHERE FileSize > 10MB
The general syntax is much more sophisticated but you should get the idea from this example. GPFS is using a lot of interesting commands starting with mm (likemmlsfs, mmlsnsd, mmlscluster, mmlsquota, mmlsdisk, mmlsmount, etc.) for its configuration and management. This flat namespace is of course yet another consolidation candidate (cd /system/gpfs/nsd; ls), but let’s come back to the idea of using a SQL-like syntax in our smarter CLI. Assume you could access all objects or commands in your file system hierarchy like in the following example:
# set myadmins select users from group admins where lastlogin greater 30days
# cd myadmins
(myadmins)# set shell bin/false
The strength of this approach is quite obvious. Depending on your programming language skills and background you will probably suggest different solutions but keep in mind that the little (almost natural) language design has to be command line completion friendly and basically use one-liners! Here is another example:
# cd /system/clusters
# migrate hosts from cluster1 where cpuload greater 200% to cluster2
A simplified TCL version with its everything is a command approach could be a very nice test bed for these kind of ideas and I hope the TCL community will accept the challenge and come up with better proposals! Let’s assume for a moment that you are a TCL fan trying to implement the idea above. Instead of using the generic name set we take e.g. select as our initial command. Additionally we put myadmins at the end of the line since it’s the only syntactic element which can not be auto-completed.
# select users from group admins where lastlogin greater 30days as myadmins
# cd myadmins
Going one step forward and executing the command in the groupscontext we can simplify the syntax even more by eliminating the word group since it is clear now that we are selecting a group of users from the admins group:
# cd /system/groups
# select users from admins where lastlogin greater 30days as myadmins
# cd myadmins
We can even construct a command without from admins if we limit the context even further.
# cd /system/groups/admins
# select users where lastlogin greater 30days as myadmins
# cd myadmins
We can also select a group of users in the users directory
# cd /system/users
# select users where lastlogin greater 30days as myadmins
# cd myadmins
but this is probably not a good idea and only shows the flexibility of our approach. One question remains though! Is a group of objects created directly after execution of the select command or the first (each) time you use the group? It depends!
One could extend the selectsyntax with more keywords like static, dynamic, first-use or set properties of the generated group (cd myadmins; set generation dynamic) assuming that the default behavior is static. Using different commands like dynselcet, staticselect, etc. for each case is another solution:
# dynselect hosts from hostgroup cluster where cpuload greater 200% as overloaded
# dynselect hosts from hostgroup cluster where os equals RHEL-5.3 as rhel5.3
# dynselect hosts from hostgroup cluster where package is-installed OpenIPMI as ipmihosts
# dsh ipmihosts chkconfig ipmi on
# cd ipmihosts
(ipmihosts)# service ipmi start
The last example shows the use of a global parallel shell command (dsh) applied to a group of hosts and also the approach we have been promoting so far (i.e. command execution in a hierarchical context).
We can also take a more functional and less verbose approach for object selection using map, filter and other well known methods from languages like Haskell and Scala. The only difference is that that we should try to avoid brackets and nesting since these constructs are not really command line completion friendly.
# cd /systems/hostgroups
(hostgroups)# dynamic overloaded = cluster filter cpuload greater 200%
You do not really need the equal sign but it clearly enhances readability. Instead of writing _.cpuload as in Scala we drop the underscore reducing the possibilities and expressiveness in our example above. We can also use the equal sign in combination with SQL-syntax:
# cd /systems/hostgroups
(hostgroups)# ls cl*
cluster cluster1 cluster2
(hostgroups)# dynamic overloaded = select hosts from cluster where cpuload greater 200%
(hostgroups)# dynamic rhel5.3 = select hosts from cluster where os equals RHEL-5.3
5. Logging, auditing and change/configuration management
Our feature wish list is far from complete, so let’s continue. If every system configuration activity is using a command, logging and auditing is more or less trivial. Just write e.g. user@host, time and command (or a message in standard IETF syslog format) to a file, database or write once device and you know exactly what happened on your system (at least from a configuration point of view). No need for revision control systems or special filesystems watching your files anymore and change management can be done with a list of pre-approved commands or scripts. Another interesting possibility is to use the ZeitGeist event engine to track the commands, analyze (or even block) them, generate reports, etc.
A configuration management database (CMDB) or more abstract descriptions of your systems (e.g. using one of the many Open Source configuration management systems) can still be used in the background but the same observation about simplification as above applies here. By using a sequence of (high level) commands describing how a system achieves its state you can replace the descriptive approach by a procedural one and are probably more in sync with the actual work of system administrators.
6.Transactions, rollbacks and state changes
Transactional commands which can be rolled back (e.g. deliberately or in case of failures) would be nice to have but are difficult or even impossible to implement. Why not using a special command (since everything can be done with a command according to our first rule) to define if a command is transactional (e.g. by setting a property of the command) and add a rollback command if this is the case. In so doing one can enter a transactional mode where only commands with rollback functionality are shown.
Implementing a new installation and boot process using transactional commands would result in a system which can be transformed very easily into another configuration state by running a list of commands. Red Hat’s kickstart files which can be viewed as recipes how to fully automate the installation process are using this approach and only need some minor changes to completely fit our model. All the commands of a kickstart-ng file could then be executed in a shell allowing easy debugging using a standard start-stop-step cycle. This is very close to what the Debian installer d-i is doing (d-i netcfg/get_gateway string 192.168.1.1) but in sharp contrast to a descriptive style using reply files written in XML (e.g. Novell/SUSE’S autoyast).
In a similar fashion many of the available Linux, FreeBSD, OpenSolaris, etc. HOWTOS could be transformed into a sequence of commands you could read almost like a book instead of following explanations how to type some commands, edit configuration files and do other system configuration changes with web applications or desktop GUIs.
B.t.w. what’s the reason why system administrators do not really trust many of the available web and GUI tools? Because they do not know what they are doing in the background. If these tools were a more convenient way of entering data on top of a CLI (showing the executed commands in an expert mode) you would get more confidence in them. AIX’s smitty is doing exactly that and this is one of the reasons why people really like it. Another benefit of putting more attention to the command line in this way are more structured certifications tests (LPI, Red Hat, Novell, Ubuntu, etc.) which could be done with the CLI or the GUI delivering comparable results.
7. Integrated help
Another important feature of our CLI is integrated help. Traditionally GNU and other utilities use man/info pages and have help options like -h and –help to display all other command line parameters (the Z shell can even use this output to automatically generate command line completion for them!). To have the help information at your fingertips while typing with examples you can immediately integrate into the command line would be a very useful feature! Defining help information and syntax examples for commands and their options will be done with commands of course, as you can see in the following very simple examples:
# cd /system/commands/vms/suspend
# set help "Suspend a running domain. It is kept in memory but won’t be scheduled anymore."
# set example 1 "cd /system/vms/fedora; suspend"
# set example 2 "cd /system/host2/vms/fedora; suspend"
Help information will be defined in many different formats (text, html, wiki, man, info,…) and displayed in different ways according to you preferences with
This is definitively not your grandfathers static text-based help help but could incorporate blog entries, videos, e-learning, RSS feeds, instant messages, e-mails, Google waves, wiki pages, HOWTO’s, personal notes, twitter feeds, etc. for a more advanced help experience.
We all know from Zed Shaw’s that the ACL is dead (I completely agree with that!) but enabling and using them with care should be possible for our CLI. We basically give users and groups access to certain commands or subsets of them. When we define new commands or extend old ones we define who can see and execute which part of it. To enhance security the default behavior of our commands should be no visibility and no rights at all if they are not explicitly defined.
10. CLI syntax
A quick look at some command line utilities shows that there is no universally accepted CLI standard. Many tools use the GNU conventions (with short and long options) but there are many exceptions as the following examples show:
# cd /root
# dd if=/dev/cdrom of=cd.iso
# tar xvfz test.tar.gz
# tar -tf test.tar.gz
# tail -f -n 30 /var/log/messages
# cobbler system add --name=h1 --mac=00:14:5E:EC:10:EE --ip=192.168.1.5 --profile=F11-i386
# qemu linux.img -net nic,macaddr=52:54:00:12:34:57
# qemu linux.img -drive if=ide,index=1,media=cdrom
# chkconfig --list ipmi
# chkconfig --level 2345 ipmi on
# ip route show
Since we are looking for a CLI with command line completion friendly commands fitting on one line (e.g. max. 80 characters including the prompt) our general approach will be much simpler. There should be no restrictions for more sophisticated users though! New commands of our CLI will be defined by special commands and not by using a descriptive approach with XML files and the like (take a look at the interesting clish shell for the second approach). We will also put the commands themselves into a command hierarchy applying our first two principles also to commands.
# cd /system/commands/
# create command select
# cd select
(select)# set generation static
(select)# set transactional yes
(select)# set ACL ...
(select)# set syntax "..."
(select)# set option -v "..."
Finding a good syntax for command definitions will be an interesting challenge and hopefully stimulate the imagination and creativity of many developers and language designers. This leads us to the question how to implement a shell with all the proposed features.
11. CLI implementation details
Looking at many different shell implementations I came to the conclusion that implementing a new shell had almost no chance of universal acceptance. Router, firewall and embedded systems often take this approach, sometimes with rather poor results because of time constraints (i.e. no command line completion!).
A new universal shell written in Java, C#, C++ or a scripting language like Python, Perl, Ruby, TCL, Lisp, etc. would not attract all the different communities so C is almost a must for the core framework. But implementing, debugging and stabilizing a new C-based shell is far from trivial and takes a long time.
Of course you could take a higher level framework like pycopia (written in Python) and make it extensible with other programming languages but language flame wars are quite predictable with this approach. The same is true for CLIs being closely related to one programming language (like IPython for Python which is very popular in the scientific computing community) so they are not an option either.
There are hundreds of Open Source projects facing exactly the problems described here and most of them just implement their own command line tools or shell frameworks. One interesting example is the Vyatta Open Source Router having its root in the Quagga-project. Vyatta used the very powerful xorp shell but for a typical Linux user it felt somewhat unnatural so they switched to a bash based CLI reusing the built-in command line completion of bash.
Although Vyatta is using a flat command hierarchy it shows how our CLI could be implemented by taking a standard shell like bash (e.g. the Vyatta bash version) or zsh and their command line completion frameworks (bash’s compgen and complete commands or zsh’s completion system and completion widget). The bash-completion and the many zsh completion scripts show how powerful these shells are regarding command line completion so there is really no need to reinvent the wheel.
In addition the cd command needs to dynamically manipulate the $PATH variable so the full namespace is only visible when navigating outside e.g. /system. The implementation of the whole feature set of our CLI needs some effort though to make it useable for non scripting experts so they can concentrate on little language designs and not bother with shell scripting issues. Maybe some kind of plug-in, templating or even AOP system could turn out to be useful so contributors can extend commands (with some specialized commands) without manipulating the whole syntax (e.g. adding an option, an example, etc.) or add aspects to enable fancy features.
12. Some last words
The more I was thinking about a smarter CLI the more it became evident that the topic is far from dead. Quite the contrary is the case and I hope some people will pick up the ideas presented here and start producing some code. I am convinced that in the process of implementing this smarter CLI a lot of new ideas, different approaches and hopefully much better command line tools than the ones available today will pop up. I am even convinced that you can use this CLI to describe use cases for new software with carefully chosen new commands instead of just plain text descriptions.