esh, the easy shell.


esh, the easy shell.

1. Rationale

esh was primarily written out of a need for a simple and lightweight shell for Unix. As such, it deviates completely from all of the traditional shells, opting instead for a Lisp-like syntax. This allows exceptionally small size, both in terms of lines of code and memory consumption, while retaining remarkable flexibility and programmability.

2. Overview

2.1 Starting esh

To start the shell, simply type esh. There are no command-line parameters. However, since all the command-line arguments are available to the shell programmer, it is a simple matter to write your own argument processing routine and place it in your .eshrc file.

Both /etc/eshrc and the .eshrc in your home directory will be run by the shell on startup, if they exist.

2.2 Interaction

If the shell is running interactively, you will get a prompt, $ by default. Since the shell uses the readline library, you can use line-editing keystrokes, the history buffer, filename completion, and the rest of the goodies provided by the readline library.

To run a single executable, simply type it in as you would in any other shell. Example: /usr/games/fortune -m Unix.

To run a pipeline, type several commands separated by commas or vertical lines. Example: cat /var/log/messages, grep pppd, grep "IP address", xmessage -file -.

To run a pipeline with file redirection, place the redirection symbols and the filenames after the pipeline. Example: sort, uniq < names.raw > names.sorted. In this case, the contents of names.raw are used as the standard input of sort, and the output of the whole pipeline is saved to names.sorted. The order in which the redirection symbols are given does not matter, but a valid filename should always follow a redirection symbol.

Note one major pitfall: cd, bg, fg, etc., are all builtin shell commands! For info on running shell commands, see the next section. (see section 2.3 Simple Programming)

2.3 Simple Programming

All shell commands are specified using a parenthetical syntax very similar to that of Lisp or Scheme. Example: Use (cd /var) to change the current directory. In this case, cd is the name of the command, and /var is the argument. Multiple arguments to the same command are separated by white space. Example: (set HOME /home/ivan). Builtin commands never accept options in the style of compiled programs. Also note that quotes around string values are not required, though they are certainly allowed at any time if you wish to escape special characters. (In fact, using quotes is the only way to do this. Backslash escape sequences are not recognized.) Example: (cd "funny,directory name()"). In this case, the whole string inside the quotes is used as the argument verbatim. Single quotes and double quotes are equivalent.

If a command returns anything, the returned data will be printed by the shell. Example: (get HOME) => /home/ivan.

Note that you can certainly use the return values of one command as input to another. Example: (cd (get HOME))

If you want to pass a list instead of a string as an argument to a command, quote the list with a tilde. Example: (run-simple ~(ls --color=yes)). Here, the run command is given a list as an argument.

Note that a sublist of a quoted list is also quoted; that is, ~(1.1 (2.1 2.2) 1.2) is a list of three elements, the first and last are strings and the middle element is a list.

See section 2.6 Basic Usage, for an explanation of what cd, get, set, and run do.

2.4 Non-interactive

If the shell is started non-interactively -- for example, when the standard input to the shell is a file -- the shell will only accept commands. To run executables and construct pipes, simply use the run command. (see section 2.6 Basic Usage)

2.5 Job Control

If the shell is running interactively, you will have access to job control primitives.

To stop a job in the foreground, simply type C-z, as in other shells.

To bring the last known job into foreground or background, issue the fg or bg command, respectively, without arguments. (see section 2.3 Simple Programming)

To get a listing of all the currently known jobs, issue the jobs command, also without arguments.

If the fg or bg command is given a single numeric argument, it will act on the job number specified by that argument. To find out the job number for a command, simply issue the jobs command.

Note: the job number is not the PID!

2.6 Basic Usage

3. Details

A programmer's manual and a tutorial.

3.1 Syntax

Here is a description of the syntax of esh. Note, however, that this grammar is only a descriptive tool, since the actual parser in the shell is written by hand.

Also note that using the interactive-command syntax is not allowed when the shell is not started interactively.

openparen-symbol = '('
closeparen-symbol = ')'
delay-symbol = '~' | '$'
pipe-symbol = ',' | '|'
redirection-symbol = '<' | '>'

script = /* Nothing */ |
         list script

list = openparen-symbol list-elements closeparen-symbol

list-elements = /* Nothing */ |
                string list-elements |
                delay-symbol list list-elements |
                list list-elements

interactive-command = list |
                      interactive-pipeline

interactive-pipeline = /* Nothing */ | 
                      interactive-pipe redirection redirection

interactive-pipe = /* Nothing */ |
                   single-command pipe-symbol interactive-pipe

single-command = /* Nothing */ |
                 string single-command

redirection = /* Nothing */ |
              redirection-symbol string

3.2 Semantics

One major deviation of esh from other programming languages is that commands are not functions. Any command can return any number of values, without explicit unpacking syntax. (e.g. Python)

For example, (rest ~(foo bar baz)) => bar baz. These returned elements are not a list! Therefore, this code will produce an error: (rest (rest ~(foo bar baz))). Instead, you should write: (rest (list (rest ~(foo bar baz)))).

The first, most important concept is the define command. This command allows you to define new commands, which are no different from builtin commands as far as the programmer is concerned.

The syntax of define is simple: (define <string> ...). The first argument is simply the name of the command you are defining, and the rest of the arguments will simply be passed to eval whenever your new command is called.

It's important to realize that this code snippet (define foo ~(print bar) bar) (foo) is identical to this one: (eval ~(print bar) bar). Therefore, anything that applies to eval also applies to commands defined by define. (see section 3.3 Quoting Trickery for more on that.)

If you are familiar with Scheme or Lisp, you'll notice the lack of argument passing information in the syntax of define. The reason for that is that the argument passing convention is radically different in esh from other Lisp-like languages.

Every command in esh has a stack all to itself for scratch space and local variables. When a user-defined command is run, all the arguments are simply placed on the local-variable stack, the first argument at the top.

To access the local-variable stack, several commands can be used. (push <any>) will push a value onto the stack, (pop) will remove the top element of the stack and return it, (top) will return a copy of the top element, (stack) will return a copy of the whole stack, and finally (rot) will switch the top and the next-to-top elements of the stack and return a copy of the element that just became the top one.

The top-level of the interpreter also has a stack; that is, typing the stack manipulation commands directly into the interpreter will produce the expected results.

Note that commands called recursively do not inherit the stack of the calling command.

Also note that (stack) does not return a list, it returns an arbitrary number of elements. If you find typing (list (stack)) frequently, you can instead use the (l-stack) command, since they are identical.

The shell does not support looping constructs in the traditional sense -- like in Scheme, looping is accomplished by using recursion. Here is a concrete example:

(define print-squish
        ~(if ~(not-null? (top))
             ~(begin 
                   (print (squish (pop)))
                   (print-squish (stack)))
              ()))

The command begin simply returns the given arguments; its purpose is to allow the use of several commands where one is asked for. (see section 3.4 Command List for the syntax of if)

Since esh 0.2, there is an explicit true and false value; these values are different from all other possible values. Commands that operate on predicates (if, and, or, =, etc.) use these values extensively. You can use them in your own scripts with the true and false commands. Note that if, and, and or do not explicitly check for true, so to these commands any value that isn't false is "true", so to say.

3.3 Quoting Trickery

(Note: Since esh 0.6, the backslash quote has been removed. Use the tilde quote instead.)

Note that the meaning of the quote syntax is much different in esh than in Lisp -- in Lisp, the single quote is a syntactic trick to allow inputting lists and symbols as literals. In esh, the tilde quote has a very clear semantic meaning -- whenever a list is marked with a tilde, it is marked as such throughout it's life. Commands such as eval can then use this information to see which lists were supposed to be evaluated, and which ones were meant to be left alone.

In other words, any list marked with a tilde has a numeric flag set on it, which is preserved on the list until the list is deleted.

Also, nested tildes increase the "strength" of the tilde, so to say. eval will not evaluate lists which have a "strength" greater than itself, and calling eval recursively will increase it's strength.

This may sound confusing, but it has a simple intuitive meaning:

(eval ~(foo ~(bar baz)))

Here, as you would expect, eval will only evaluate foo, and pass (bar baz) as a list to foo.

Note that unlike in Lisp, esh has no "special forms"! A command that works on lists is indistinguishable from a command that works on code. That's why there is no lambda command, and also why arguments to if and firends need to be quoted with a tilde.

3.4 Command List

3.5 Tutorial

For starters, let us write a simple command to iterate through a list:

(define for-each
        ~(if ~(not-null? (rot))
             ~(begin ((rot) (rot))
                     (for-each (cdr (list (stack)))))
             ()))

Using this command is simple:

(define starrify 
        ~(squish '*' (top) '*'))

(for-each starrify foo bar baz)

results in *foo* *bar* *baz*.

Several notes to keep in mind: notice the tricky use of rot to shuffle stack elements. If you don't remember, rot switches the top and the next-to-top elements of the stack, and returns a copy of the element that just became the top one.

Think of it as such: the first call to rot brings the second argument to the front and checks that the stack is not empty, at the same time.

The second call to rot returns the first argument, and the third call to rot returns the second element and undoes the previous call of rot at the same time. At this point, the second argument is still on top of the stack.

Finally, when for-each is recursively called, it is given only below the top stack element as arguments. Effectively, the second element was excised from the stack.

Also note that ((rot) (rot)) is calling a command, even though the name of the command is not explicit.

Now for another example. Suppose that you want to remind yourself which directory serves which purpose, and you'd like a descriptive string to be listed along with a directory name in the prompt. (i.e. [/home/ivan/src/xcf => Backup of GIMP artwork]$ )

(prompt ~(push (get PWD))
        ~(push (hash-get (dir-names) (top)))
        "["
        ~(rot)
        ~(if ~(not-null? (rot))
             ~(squish " => " (top))
             "")
        "]$ "
        ~(null (pop))
        ~(null (pop)))

The meaning of prompt is simple -- whatever arguments are given to it are passed through eval and squish to become the string specifying the prompt. Again, as far as quoting is concerned, prompt is identical to eval.

Also, the command null simply returns an empty list.

To finish the job, you should insert the following code into your .eshrc:

(define dir-names (hash-make))

(hash-put (dir-names) "/home/ivan/src/xcf" "Backup of GIMP artwork")

This is a simple script that illustrates simple file I/O:

(define prepend
  ~(push (file-read (file-open file (pop))))
  ~(push (squish (pop)
                 (file-read (file-open file (top)))))
  ~(file-write (file-open truncate (rot)) (rot)))

A script such as this was used to append a header to every source code file in the esh distribution. Note the typecheck command; it is very useful for insuring the consistency of arguments to commands.

The only tricky part about using typecheck is the syntax of the first argument. Here is an explanation:

For example, "H(ss)s" will match any list of arguments that begin with an arbitrary number of hash tables, followed by a list of two strings, and ends with a single string.

4. Differences from Scheme

Note that in esh there is no numeric or "symbol" type -- all scalars are either strings, booleans, files, or process ID's. Most importantly, there is no "procedure" type -- hence, no lambda command. From the point of view of the interpreter, a command and a list are indistinguishable.

Don't be mislead by the naming of define -- it is actually equivalent to a Lisp-like macro definition.

Also, car and friends are more complicated because commands in esh can return any number of arguments. This is why (car (split "foo bar")) doesn't work -- car expects a list, while split returns an arbitrary number of elements. In a case like this, it is more convenient to write (car-l (split "foo bar")) instead, which is equivalent to (car (list (split "foo bar"))).

5. Differences from sh

The most obvious difference is the syntax -- other shells normally don't make much of a distinction between commands from a disk executable and the shell interpreter's builtin commands. Not so in esh. While other shell normally operate by complicated string substitution and matching rules, esh mainly uses command definitions and recursion. In that sense, it is more similar to a "real" programming language. esh also supports more complicated data structures -- lists and hash tables, for example. However, string operations are lacking in comparison to other shells.

esh is also more verbose and formal, though this could be beneficial when you're trying to compose large libraries of useful routines. (It's possible to write shell "modes", much as in emacs, when using esh.)

esh has a more generalized support for files. Instead of limiting file I/O to redirection only, the same commands can be used either on pipes or on files directly. (e.g. in the future, it may be possible to use a network socket as the output of a pipeline.)

Command Index

Jump to: * - + - - - / - = - a - b - c - d - e - f - g - h - i - j - l - m - n - o - p - r - s - t - u - v - w

*

  • *
  • +

  • +
  • -

  • -
  • /

  • /
  • =

  • =
  • a

  • alias
  • alias-hash
  • alive?
  • and
  • b

  • begin
  • begin-last
  • bg
  • c

  • car
  • car-l
  • cd
  • cdr
  • chars
  • chop!
  • chop-nl!
  • clone
  • copy
  • d

  • define
  • defined?
  • e

  • env
  • eval
  • exec
  • exit
  • f

  • false
  • fg
  • file-open
  • file-read, file-read
  • file-type
  • file-write, file-write
  • filter
  • first
  • first-l
  • for-each
  • g

  • get
  • gobble
  • h

  • hash-get
  • hash-keys
  • hash-make
  • hash-put
  • help
  • i

  • if
  • interactive?
  • j

  • jobs
  • l

  • l-stack
  • list
  • m

  • match
  • n

  • newline
  • nl
  • not
  • not-null?
  • null
  • null?
  • o

  • or
  • p

  • parse
  • pop
  • print
  • prompt, prompt
  • push
  • r

  • read
  • repeat
  • rest
  • reverse
  • rot
  • run
  • run-simple
  • s

  • script
  • set
  • split
  • squish
  • stack
  • standard
  • stderr
  • stderr-handler
  • substring?
  • t

  • top
  • true
  • typecheck, typecheck
  • u

  • unlist
  • v

  • version
  • void
  • w

  • while
  • Concept Index

    Jump to: . - a - b - c - d - e - f - g - i - j - l - n - o - p - q - r - s - t - v

    .

  • .eshrc
  • a

  • Arguments, to commands
  • b

  • Background
  • bash
  • Basic commands
  • Basics of shell programming
  • Builtin command list, basic
  • Builtin command list, detailed
  • c

  • Code-as-data
  • Command line parameters
  • Command list, Command list
  • Commands
  • Control structures
  • csh
  • d

  • Details
  • Differences, Differences
  • Differences from Scheme
  • Differences from sh
  • e

  • eshrc
  • eval, quoting for
  • Examples
  • Executing files
  • f

  • Files, as shell scripts
  • Foreground
  • g

  • Getting started
  • i

  • Interaction
  • Introduction
  • j

  • Job control
  • l

  • Launching esh
  • Lisp
  • Loops
  • n

  • Non-interactive
  • o

  • Overview
  • p

  • Pipes and redirection
  • Programmer's Manual
  • q

  • Quoting
  • r

  • Rationale
  • Recursion
  • Running commands
  • s

  • Sample code
  • Scheme
  • sh
  • Simple programming
  • Stack, local variable
  • Starting esh
  • Stopping jobs
  • Syntax
  • Syntax, basic
  • t

  • tcsh
  • Tilde, as a quote character
  • Tutorial, Tutorial
  • Typechecking
  • v

  • Variables

  • This document was generated on 8 March 1999 using texi2html 1.55k.