next_inactive up previous


libeshell - a C++ library for writing shell-like console applications

stephan@s11n.net

Abstract:

This document describes libeshell, a C++ library for aiding in the creation of command-line and shell-style applications. It does not provide 100% coverage, but explains to the reader how to use the variour major features.


Contents

1 Introduction

CVS info: $Id: eshell.lyx,v 1.21 2005/02/20 23:29:48 sgbeal Exp $

ACHTUNG: This document is far from complete!

ACHTUNG #2: Versions 2004.09.25 and later have significant interface differences from earlier versions.

ACHTUNG #3: Versions released during or after February 2005 no longer support wildcard expansion.

This document gives an overview of libeshell, a C++ library for aiding in the creation of shell-like console applications. What does that mean? It means that it is used to write applications which have an interface similar to that of a typical Unix shell.

eshell is available from the s11n web site:

http://s11n.net/eshell/

1.1 License

The libeshell code is released into the Public Domain. It may, of course, fall under other licenses if it is linked with code from, e.g., the GNU GPL. For example, libreadline_cpp optionally links against GNU's libreadline, and if that support is included with eshell the resulting binary falls under the GNU GPL.

1.2 Requirements

eshell requires:

It is safe to say that if your box can build libs11n then it can build eshell as well.

1.3 Main Features

1.4 Main caveats/bugs

1.5 Short History

libeshell evolved from an old (and long-obsolete) mini-project i once worked on, called ''elib'', which was short for ''libessentials'' - a toolbox with all of my ''essential'' classes in it. i wanted an easy way to write test applications for several other projects and wrote eshell to fill that gap.

Over time it's interface has changed significantly - always in the interest of ease-of-use. The current model has been in place approximately 6 months, so it appears to be pretty stable. Thus... it's now time to document it a bit and dump it on the web site. :)

2 Getting Started

Using eshell is easy. Rather than start off with a list of it's more important functions and classes, we'll show a simple application which allows one to get to work immediately with eshell. The lines marked with a preceeding number are described in more detail below the source code.

2.1 Our first eshell application

#include <s11n.net/eshell/eshell.hpp> // eshell framework

// client-written command handler:

[1] int test_handler( const eshell::arguments & args ) {

// Iterate over the args either via index or using iterators...

[2]for( size_t i = 0; i < args.argc(); i++ ) {

eshell::ostream() << "args["<<i<<"] = ["<<args[i]<<"]"

<< std::endl;

}

return 0;

}

int main( int argc, char ** argv ) {

[3]acme::argv_parser & args = eshell::init( argc, argv );

[4]std::string sessfile = args.get( "session", "session.s11n" );

eshell::load_session( sessfile );

[5]eshell::map_commander( "test", test_handler );

[6]eshell::input_loop( "myshell >" );

eshell::save_session( sessfile );

return 0;

}

That should be all pretty straightforward. The marked lines are described here:

  1. This is an eshell-compliant ''command handler''. These functions are mapped to commands using eshell::map_commander(), as shown in [5]. Note that this interface changed in version 2004.09.25, to remove a bothersome second ostream argument.
  2. There are a number of ways to get the inividual elements from an eshell::arguments object. Here we use the index operator. See also arguments::shift() and the eshell::args_list type, or go STL-style via arguments::[const_]iterator. In the loop, the call to eshell::ostream() ensures that the output goes to the default ostream used by eshell. This is almost always std::cout, but it may be reset by arbitrary commands or client code. Client code is not required to use eshell::ostream() as their default output stream - it is just a suggestion.
  3. Here we initialize eshell, passing on the command line arguments. This this initializes the default command handlers and provides easy access to command line args via the return value (and, later, from eshell::args()). If you do not plan on using command line arguments, or want to implement your own parser for those, feel free to ignore the return value of init(). You ''should'' call init() even if you will ignore the returned arguments handler, in case the client passes on args which eshell inherently supports, like -classpath.
  4. This line sets the name for our session file. Our default is ''session.s11n'', but if the user passes -session=filename on the command line then that filename will be used.
  5. Here we map our command handler to the command name ''test''. Thus when a user enters test [args...] this handler will be called.
  6. This accepts input from the user, dispatching each line to it's mapped command handler, until Ctrl-D is pressed or one of the following commands are entered: quit, logout, exit.

2.2 Compiling and linking the application

Compiling and linking against libeshell

eshell installs a script named PREFIX/libeshell-config. You may pass it -includes to get a list of flags needed by clients compiling code against libeshell. Use the -libs option to get linker arguments. The output of those commands is suitable for direct assignment in a Makefile variable:

EC = $(shell which libeshell-config)

ifeq (,$(EC))

    $(error libeshell-config not found in PATH!)

endif

CXXFLAGS += $(shell $(EC) -includes)

LDFLAGS += $(shell $(EC) -libs)
Alternately, you may use pkg-config libeshell if your site has pkg-config and libeshell's pkg-config data file is in its search path.

When including the headers in client code, use this format:

#include <s11n.net/eshell/...>

3 Conventions

eshell is modelled to work similarly to conventional shell code. Thus it's conventions should be pretty easy for Unix users to get the hang of. The library is designed to be used as a framework for writing applications, and to this end it provides a number of functions and classes to handle tasks which are commonly necessary in shell-like applications.


3.1 Command Handlers

Command Handlers are the functions which eshell maps to command tokens. It uses several built-in handlers and allows user to plug in their own via eshell::map_commander(). The basics of the Command Handler interface are:

3.2 Data Entry

eshell collects input from an input prompt on the command line by default, though it can also be fed input from arbitrary streams. In the worst case eshell uses std::cin to read input. This works, but is not very fun to work with and also prohibits the use of command-line history. When linked with a fully-functional libreadline_cpp then either libeditline or GNU readline is used, providing a powerful set of command-line editing functionality. Note that libreadline_cpp is linked against automatically if the configure script finds it, but that your libreadline_cpp may or may not have editline/readline support linked in. (If not, then when you start eshell you will be warned that interactive editing will not be possible.)

One line of input is considered to be one command. When reading in from console mode, multiple commands may be entered on a single line if separated with semicolons:

mycmd arg1 arg2 foo.out; ! cat foo.out
(That works only with interactive console mode, not when reading streams via dispatch(istream) or the ''source'' command!)

When a user enters a command it is sent to eshell::dispatch(), then looks up the handler associated with the first token. If it finds one it passes the arguments on to that handler and returns the handler's result, otherwise it returns an arbitrary non-zero error code.

3.2.1 Built-in commands

A number of built-in commands are automatically set up when eshell is initializes (by calling eshell::init()). For the whole list see the API documentation for eshell::setup_default_handlers(). It includes all of the most basic commands, but does not have any complex constructs like loops.

3.2.2 More on readline_cpp, GNU Readline and libeditline

libreadline_cpp is a C++ library which wraps line-input functionality, so eshell doesn't have to do that by itself. It was originally part of eshell, and was intended as an API-workalike for GNU Readline (but also worked without Readline if it wasn't available). This allowed eshell to be distributed under different licenses, depending on which readline support which was linked in.

As of 28 November 2004 readline_cpp supports both GNU Readline and a BSD-licensed readline clone called liblineedit. liblineedit isn't quite as useful, out of the box, as readline, but the license is not as draconian, and therefore readline_cpp's configure script prefers it over GNU Readline (but, again, works with both). (Also, editline's sources are pretty easy to hack on. :)

If editline support is compiled in then it provides several built-in functions which operate at a level lower than eshell, and when entered by the user they will not be propagated back to eshell. For a list of these functions, enter the command el-help. If that gives an error or shows nothing, then you are not using liblineedit, otherwise it will give a list of editline's builtin functions. You can use, e.g., el-bind to change your key mappings.

If you have eshell installed, you probably have readline_cpp as well, but if you don't, you can get it and liblineedit from the s11n web site:

http://s11n.net/download/#readline_cpp

http://s11n.net/download/#editline

4 Using eshell's main features

4.1 Environment variables

eshell supports conventional environment, via the object returned by eshell::env(). That object, of type acme::environment, provides an easy interface for ''lexically casting'' variables to different types. To set environment variables do one of the following:

From the console interface:

set VAR=value

set VAR=''quote the value if it has spaces''
From C++:

eshell::env().set( ''VAR'', ''value'' );

eshell::env().set<int>( ''VAR'', 7 );

eshell::env().set( ''VAR'', 7.7 );
To get use vars from the console interface:

echo ${VAR}

some_command arg1 ${VAR} ...
And from C++:

double d = eshell::env().get( ''VAR'', -1.0 );

std::string s = eshell::env().get( ''VAR'', '''' );
See the API docs for acme::environment for the meaning of the second argument to get().

To expand variables in a string, simply do one of the following:

std::string std = eshell::env().expand_vars( mystring );
or:

eshell:env().expand_vars_inline( mystring );
Note that you can dynamically expand vars:

set foo=HOME

echo ${${foo}}
Will expand to the value of $HOME.

Missing feature: eshell currently has no way to ''protecting'' $VARS from expansion with quotes, as conventional shells do via, e.g., '$VAR'. This can potentially be fixed by extending the string tokenizer a bit, but so far hasn't become an issue. You can backslash-escape var names to keep them from being expanded, e.g.:

eshell >echo \$HOME

$HOME

eshell >


4.1.1 POSIX wordexp() and eshell

Achtung: as of February 2005, wordexp() support was removed from eshell. This means that eshell no longer does any wildcard/glob handling. i am debating replacing the wordexp() support with glob() support and making it togglable.

The main reason for removing wordexp() support is that wordexp() expects to be fed single lines of unparsed input, whereas eshell does pre-parsing of it's lines and fed individual tokens to wordexp(). This caused call kinds of grief via-a-via quoting, amongst other things. As wordexp() removes all quotes, it's not suitable for calling on user input before eshell can get ahold of it, as eshell does the quoted-string tokenizing itself.

4.2 Command line arguments

eshell comes with a simple command line arguments parser. It accepts arguments in any of the following formats:

The parser does not care if you use single or double dashes: -foo and -foo are treated identically.

Note that it does not support entering the same argument more than once: only the last one set takes effect. You may work around that by processing the argv array before or after passing the list to eshell::init().

The parser is initialized via a call to eshell::init(), and they can be collected in one of two ways:

acme::argv_parser and acme::environment both share a common base class, and thus their interfaces are almost identical. The minor difference comes in when using argv_parser's get(), is_set() and unset() members: there is no need to add dashes when querying them. For example:

acme::argv_parser & a = eshell::init( argc, argv );

std::string sessionfile = a.get( ''session'', ''default_file.s11n'' );
This will return whatever value the user entered via -session=filename, or ''default_file.s11n'' if no such argument was given.

Also, argv_parser adds a couple entries to the argument automatically: $0 .. $n, where the number is the argument at that position in the original argv passed to init(). Thus eshell::args().get_string(''$0'') will return the application's name, and getting $3 will return the third argument (or an empty string, if no such argument was given). Note that these forms are completely unparsed, and thus in this example:

myapp -session=foo
$1 will be the string ''-session=foo''.

Note that these $N vars are not available from the console user interface, only programmatically.

There are a couple minor caveats to be aware of in argv_parser: please see it's API docs for full details.

4.3 Command aliases

eshell's aliasing works very similarly to that of the Bash shell. To alias from the command line, do:

alias foo=''what foo should expand to''
Only the first token in a command is alias-expanded, as per long-standing Unix shell conventions. Before dispatching a command eshell does alias expansion on it's first token. Thus we can write a shortcut to run a system binary with this:

alias ls=''! ls''
That will cause the external command 'ls' to be run via the C system() call.

To write that alias in C++ code do this:

eshell::aliases().alias( ''ls'', ''! ls'' );
To expand the first token in a string via alias expansion, do:

std::string exp = eshell::aliases().expand( mystring );
That works recursively to expand aliases which themselves resolve to other aliases.

4.4 Running external applications

eshell offers three different approaches for running external commands:

See the class eshell::shell_process for the programmatic interface for the first 3 features.


4.5 ''emenu'' files

Since 30 June 2004 eshell has a built-in command called emenu. It supports simple menu-style command interfaces loaded from external files with lines in this format:

Menu Entry Label|command to pass to eshell::dispatch()
or:

command to pass to eshell::dispatch()
Each command line must be an eshell command which has a mapped command handler, plus any arguments. Menu files do not support multiple commands per line, separated by a semicolon.

Example:

File Listing|! ls

Show Current Directory|pwd

! who

# comment line
Passing emenu a name will cause it to try to load the menu from a file named one of:

If it finds one it will show the user a simple menu interface, where each number maps to a line in the menu file. e.g. using the above menu file we would see:

eshell >emenu demomenu

emenu: ./demomenu.emenu

0: Cancel

1: File Listing

2: Show Current Directory

3: ! who

Enter number:
The menu loops, returning after executing a command, until Cancel is selected. The Cancel option is always number zero, and on most terminals Ctrl-D acts as Cancel.

You may pass additional arguments to a menu item by typing them after the number. e.g., entering ''1 /tmp'' above would call ''! ls /tmp''.

Alias and environment var expansion are performed on each menu command before dispatching it.

5 Command Handlers in detail

The heart of eshell functionality revolves around the so-called Command Handlers, as briefly described in section 3.1. These functions are provided by clients and have the following signature:

int function_name( const eshell::arguments & )
They are mapped into the framework with a simple API call:

eshell::map_commander( ''mycommand'', function_name [, ''optional help text''] );
Once this is done, any input line starting with the mapped command is routed to the assigned handler. map_commander() accepts an optional third argument: a help text string which is shown when a user inputs 'help'.

Per long-standing C and shell conventions, handlers return zero on success and non-zero on error. The library does not apply any special meaning to any error codes, but the enumeration eshell::CommandErrors defines some commonly-used ones which might be useful. To reiterate: clients can use whatever error codes they like, as the library applies no special meaning to them.

The library does no catching of exceptions, so a thrown exception will almost certainly cause eshell to abort it's input loop (if indeed it is running).

5.1 Getting arguments

An eshell::arguments object encapsulates all arguments passed to a handler. They provide the following ways of getting their tokens:

To convert arguments to types other than strings simply use the from_string() function, like so:

int foo = tostring::from_string<int>( args[1], -1 );
Will return args[1]'s integer value, or -1 if the conversion to int fails or if args[1] is not set.

You are of course welcome to use any other string-to-T conversion you like, such as the classic C function atol().

5.2 Dispatching commands

Though client code ''should'' never need to do this directly, it may be useful to know how eshell dispatches input: it simply passes it to eshell::dispatch(). That will treat the given input as one command line and will dispatch it. dispatch() does no alias or variable expansion, and does not recognize multiple commands separated by semicolons. It may be passed either a std::string or an eshell::arguments object, plus a std::ostream.

It does the following additional tasks:


6 eshell, the generic libeshell client front-end

Client code may be implemented as shared libraries of command handler, loaded at will, as opposed to via a main() function front-end. This is accomplished with the eshell client application. It is used like so:

/path/to/eshell -dl libname[:libname2[:libnameN]] [-p ''command prompt text:'']
''libname'' is normally base name of a library to load, but an absolute path is also accepted. Multiple libs may be given by separating them with commas or colons.

Example usage:

stephan@owl:~> ~/bin/eshell -dl libeshell_demo_dll -p 'prompt :'

Opened DLL: /home/stephan/lib/libeshell_builtins.so

Opened DLL: /home/stephan/lib/libeshell_ui.so

Opened DLL: /home/stephan/lib/libeshell_demo_dll.so

prompt :
(The first two DLLs are automatically loaded by the eshell framework.)

6.1 DLL search path

The default search path used for DLLs is defined in the macro eshell_SHARED_LIB_PATH in eshell_config.hpp. If you pass -classpath=/a/colon:/delimited/PATH to eshell then that will be used instead of the default. The -classpath option is supported by all libeshell client apps which call eshell::init().

6.2 Creating DLLs

To create a compliant DLL, simply link one or more command handlers into a DLL file. The name of the DLL is unimportant, but it must have the standard DLL file name extension for your platform (only tested on Unix platforms with ''.so'' extension, so far).

The client DLL must be able to register its handler(s) with libeshell, and this is done using a simple trick. Simply add code similar to the following to your DLL:

namespace { // see notes below
void my_init_function() {
// DLL init code goes here.

// The name of this func is irrelevant, but it must be unique.

// i.e. loading a DLL should not cause duplicate symbols.

eshell::map_commander( ''foo'', my_foo_handler );

...
}

int my_init_placeholder = (my_init_function(),0);
} // end anonymous namespace
That will run my_init_function() when the DLL is opened for the first time. One simple way to enforce uniqueness of the init function signature and placeholder variable is to wrap the code in an anonymous namespace and don't declare it in any headers.

That's all there is to it. There are no changes to how normal libeshell client code is written or used.

Note that the command-line args given to eshell are available to the DLL code via eshell::args(), so a DLL can effectively add command line options to the eshell client. For example, the DLL init code could contain something like the following:

std::string fname = eshell::args().get( ''infile'', '''' );

if( fname.empty() ) { ... error ... }
If you want your DLL to force a change to the eshell input prompt, add this to your DLL init code:

eshell::env().set( "ESHELL_PROMPT", "my prompt:" );
This is fine for a DLL which is intended to be used as a ''main'' application, but may interfere with other DLLs or libeshell client code which links against your DLL, so it is not recommended.


6.3 Loading DLLs

To load a library from the libeshell prompt (from any libeshell client), use the builtin command dlload:

dlload libname [libname2...]
To load DLLs programatically, use whatever approach you like. How a DLL is opened is not important for our purposes. One simple, yet somewhat crude, way to do it:

eshell::dispatch( ''dlload MyCommandHandlers'' );
Would find and open MyCommandHandlers.so.

Or use:

std::string dll = cllite::open_dll( ''MyCommandHandlers'' );

if( dll.empty() ) { ... no DLL found or error opening it... }

// else dll holds the path to the DLL
If cllite::use_exceptions() is toggled on, a failure in the above open_dll() call would throw a cllite::cl_exception explaining the error (e.g., couldn't find DLL or the DLL open failed for some reason (often due to missing symbols on out-of-date DLLs)).

7 Tips and Tricks

Here are some useful tips and tricks for getting more out of eshell...

7.1 Input prompt

The input prompt, as served by eshell::input_loop(), accepts environment variables. e.g.

eshell::input_loop( ''${MY_PROMPT} >'' );
If the value of MY_PROMPT changes then the prompt will be updated. Use the set command to change it:

set MY_PROMPT=''foo :''
The equivalent, from the C++ API:

eshell::env().set( ''MY_PROMPT'', ''foo :'' );
Note that trailing spaces are trimmed from the variable's value , and will not show up in the prompt (sorry about that).

7.2 Going around eshell::input_loop()

eshell::input_loop() is a convenience function. Client code may instead choose to use eshell::read_line() to collect input, and then pass the result to dispatch(). eshell::read_line() uses readline_cpp's support, if available, otherwise it defaults to using std::cin. Client code may also provide their own input handler, and simply pass the input to eshell::dispatch().

Note that clients which do not use input_loop() will need to do their own multiple-commands-per-line parsing. (Tip: see stringutil::stdstring_tokenizer for a class which can do this.)

7.3 prompt_for_input()

Added in February 2005, this feature allows console and non-console UIs to collect input in a unified way. This function calls:

eshell::dispatch( ''eshell-prompt-for-input ...'' );
where ''...'' is the prompt string passed to prompt_for_input(). The default handler for the eshell-prompt-for-input command is eshell::handle_prompt_for_input(). See the docs for these two functions for the internal conventions which must be followed by clients who wish to override the eshell-prompt-for-input command, such that their implementation will work when the command is called from arbitrary eshell code. The eshell-prompt-for-input command has no usable effect from the console (unfortunately a side-effect of internal details).

Thus, client command handlers which prompt for input should call prompt_for_input(), and rely on the underlying handler to do the actual collection. For example, one curses-based client reads the input via a curses panel.

The returned string is not automatically processed for variable/alias expansion.

8 Random Notes

The eshell library has evolved fairly steadily since early 2000, when i first started learning the STL. i get a great deal of use out of it, and find it to be a very convenient way to create test front-ends for other libraries. For example, to test my classloader i could make use of an interface for which a session might look like:

cltest> new MyObject; new MyObject

cltest> ls

Object pool list:

1 @ 0x47893920

2 @ 0x47893928

cltest> delete 1

cltest> ls

Object pool list:

2 @ 0x47893928
Using such an interface it is often fairly simple to interactively test specific parts of a library. eshell's command history/editing, aliasing, scriptability and sessions support help a lot here.

8.1 Potential uses for eshell

Some other uses for which eshell might be useful are:

8.2 Potential TODOs

Some potential to-dos for eshell:

9 The End

If you have any feedback regarding eshell, please feel free to contact it's maintainer at the address at the top of this document. Any and all feedback is welcome.

For a working example of an eshell application see the eshell source tree: src/test.cpp. During the build it gets compiled to a binary named noshell. Alternately, see src/client.cpp, for the source for the client app described in section 6.

About this document ...

libeshell - a C++ library for writing shell-like console applications

This document was generated using the LaTeX2HTML translator Version 2002-2-1 (1.70)

Copyright © 1993, 1994, 1995, 1996, Nikos Drakos, Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999, Ross Moore, Mathematics Department, Macquarie University, Sydney.

The command line arguments were:
latex2html -no_subdir -split 0 -show_section_numbers /tmp/lyx_tmpdir27510GugvYS/lyx_tmpbuf0/eshell.tex

The translation was initiated by stephan on 2005-02-26


next_inactive up previous
stephan 2005-02-26