Object-oriented shell for *nix

I can think of three desirable features in a shell:

  • Interactive usability: common commands should be quick to type; completion; ...
  • Programming: data structures; concurrency (jobs, pipe, ...); ...
  • System access: working with files, processes, windows, databases, system configuration, ...

Unix shells tend to concentrate on the interactive aspect and subcontract most of the system access and some of the programming to external tools, such as:

  • bc for simple math
  • openssl for cryptography
  • sed, awk and others for text processing
  • nc for basic TCP/IP networking
  • ftp for FTP
  • mail, Mail, mailx, etc. for basic e-mail
  • cron for scheduled tasks
  • wmctrl for basic X window manipulation
  • dcop for KDE ≤3.x libraries
  • dbus tools (dbus-* or qdbus) for various system information and configuration tasks (including modern desktop environments such as KDE ≥4)

Many, many things can be done by invoking a command with the right arguments or piped input. This is a very powerful approach — better have one tool per task that does it well, than a single program that does everything but badly — but it does have its limitations.

A major limitation of unix shells, and I suspect this is what you're after with your “object-oriented scripting” requirement, is that they are not good at retaining information from one command to the next, or combining commands in ways fancier than a pipeline. In particular, inter-program communication is text-based, so applications can only be combined if they serialize their data in a compatible way. This is both a blessing and a curse: the everything-is-text approach makes it easy to accomplish simple tasks quickly, but raises the barrier for more complex tasks.

Interactive usability also runs rather against program maintainability. Interactive programs should be short, require little quoting, not bother you with variable declarations or typing, etc. Maintainable programs should be readable (so not have many abbreviations), should be readable (so you don't have to wonder whether a bare word is a string, a function name, a variable name, etc.), should have consistency checks such as variable declarations and typing, etc.

In summary, a shell is a difficult compromise to reach. Ok, this ends the rant section, on to the examples.


  • The Perl Shell (psh) “combines the interactive nature of a Unix shell with the power of Perl”. Simple commands (even pipelines) can be entered in shell syntax; everything else is Perl. The project hasn't been in development for a long time. It's usable, but hasn't reached the point where I'd consider using it over pure Perl (for scripting) or pure shell (interactively or for scripting).

  • IPython is an improved interactive Python console, particularly targetted at numerical and parallel computing. This is a relatively young project.

  • irb (interactive ruby) is the Ruby equivalent of the Python console.

  • scsh is a scheme implementation (i.e. a decent programming language) with the kind of system bindings traditionally found in unix shells (strings, processes, files). It doesn't aim to be usable as an interactive shell however.

  • zsh is an improved interactive shell. Its strong point is interactivity (command line edition, completion, common tasks accomplished with terse but cryptic syntax). Its programming features aren't that great (on par with ksh), but it comes with a number of libraries for terminal control, regexps, networking, etc.

  • fish is a clean start at a unix-style shell. It doesn't have better programming or system access features. Because it breaks compatibility with sh, it has more room to evolve better features, but that hasn't happened.


Addendum: another part of the unix toolbox is treating many things as files:

  • Most hardware devices are accessible as files.
  • Under Linux, /sys provides more hardware and system control.
  • On many unix variants, process control can be done through the /proc filesystem.
  • FUSE makes it easy to write new filesystems. There are already existing filesystems for converting file formats on the fly, accessing files over various network protocols, looking inside archives, etc.

Maybe the future of unix shells is not better system access through commands (and better control structures to combine commands) but better system access through filesystems (which combine somewhat differently — I don't think we've worked out what the key idioms (like the shell pipe) are yet).


You don't need much bash code to implement classes or objects in bash.

Say, 100 lines.

Bash has associative arrays that can be used to implement a simple Object system with inheritance, methods and properties.

So, you would might define a class like this:

class Queue N=10 add=q_add remove=q_remove

Creating an instance of this Queue might be done like this:

class Q:Queue N=100

or

inst Q:Queue N=100

Since classes are implemented with an array, class and inst are really synonyms - sort of like in javascript.

Adding items into this queue could be done like this:

$Q add 1 2 aaa bbb "a string"

Removing items into a variable X might be done like this:

$Q remove X

And dumping structure of an object could be done like this:

$Q dump

Which would return something like this:

Q {
      parent=Queue {
                     parent=ROOT {
                                   this=ROOT
                                   0=dispatch ROOT
                                 }
                     class=Queue
                     N=10
                     add=q_add
                     remove=q_remove
                     0=dispatch Queue
                   }
      class=Q
      N=4
      add=q_add
      remove=q_remove
      0=dispatch Q
      1=
      2=ccc ddd
      3=
      4=
    }

Classes are created using a class function like this:

class(){
    local _name="$1:"                            # append a : to handle case of class with no parent
    printf "$FUNCNAME: %s\n" $_name
    local _this _parent _p _key _val _members
    _this=${_name%%:*}                           # get class name
    _parent=${_name#*:}                          # get parent class name
    _parent=${_parent/:/}                        # remove handy :
    declare -g -A $_this                         # make class storage
    [[ -n $_parent ]] && {                       # copy parent class members into this class
        eval _members=\"\${!$_parent[*]}\"       # get indices of members
        for _key in $_members; do                # inherit members from parent
            eval _val=\"\${$_parent[$_key]}\"    # get parent value
            eval $_this[$_key]=\"$_val\"         # set this member
        done
    }
    shift 1

    # overwrite with specific values for this object
    ROOT_set $_this "$@" "0=dispatch $_this" "parent=${_parent:-ROOT}" "class=$_this"
}

NOTE: When defining a new class or instance, you can override any member value or function.

Bash associative arrays have a quirk that makes this work neatly: $Q[0]} is identical to $Q. This means that we can use array name to call a method dispatch function:

dispatch(){
    local _this=$1 _method=$2 _fn
    shift 2
    _fn="$_this[$_method]"                       # reference to method name
    ${!_fn} $_this "$@"
}

A down side is that I can not use [0] for data so my queues (in this case) start from index=1. Alternatively I could have used associative indices like "q+0".

To get and set members you might do something like this:

# basic set and get for key-value members
ROOT_set(){                                       # $QOBJ set key=value
    local _this=$1 _exp _key _val
    shift
    for _exp in "$@"; do
        _key=${_exp%%=*}
        _val="${_exp#*=}"
        eval $_this[$_key]=\"$_val\"
    done
}

ROOT_get(){                                       # $QOBJ get var=key
    local _this=$1 _exp _var _key
    shift
    for _exp in "$@"; do
        _var=${_exp%%=*}
        _key=${_exp#*=}
        eval $_var=\"\${$_this[$_key]}\"
    done
}

And to dump an object structure, I made this:

NOTE: This is not required for OOP in bash, but it is nice to see how objects are made.

# dump any object
obj_dump(){                                      # obj_dump <object/class name>
    local _this=$1 _j _val _key; local -i _tab=${2:-(${#_this}+2)}  # add 2 for " {"
    _tab+=2                                      # hanging indent from {
    printf "%s {\n" $_this
    eval "_key=\"\${!$_this[*]}\""
    for _j in $_key; do                          # print all members
        eval "_val=\"\${$_this[\$_j]}\""
        case $_j in
            # special treatment for parent
            parent) printf "%*s%s=" $_tab "" $_j; ${!_val} dump $(( _tab+${#_j}+${#_val}+2 ));;
                 *) printf "%*s%s=%s\n" $_tab "" $_j "$_val";;
        esac
    done
    (( _tab-=2 ))
    printf "%*s}\n" $_tab ""
    return 0
}

My OOP design has not considered objects within objects - except for inherited class. You could create them separately, or make a special constructor like class(). *obj_dump* would need to be modified to detect internal classes to recursively print them.

Oh! and I manually define a ROOT class to simplify class function:

declare -gA ROOT=(    \
  [this]=ROOT         \
  [0]="dispatch ROOT" \
  [dump]=obj_dump     \
  [set]="ROOT_set"    \
  [get]="ROOT_get"    \
)

With a few queue functions I defined some classes like this:

class Queue          \
    in=0 out=0 N=10  \
    dump=obj_dump    \
    add=q_add        \
    empty=q_empty    \
    full=q_full      \
    peek=q_peek      \
    remove=q_remove

class RoughQueue:Queue     \
    N=100                  \
    shove=q_shove          \
    head_drop=q_head_drop

Created some Queue instances and made them work:

class Q:Queue N=1000
$Q add aaa bbb "ccc ddd"
$Q peek X
$Q remove X
printf "X=%s\n" "$X"
$Q remove X
printf "X=%s\n" "$X"
$Q remove X
printf "X=%s\n" "$X"


class R:RoughQueue N=3
$R shove aa bb cc dd ee ff gg hh ii jj
$R dump

ksh93t+ is introducing some OO concepts while retaining the bourne/posix shell syntax: http://blog.fpmurphy.com/2010/05/ksh93-using-types-to-create-object-orientated-scripts.html