Skip to content

build_shell

Zwetan Kjukov edited this page May 21, 2016 · 3 revisions

Building Shell Programs

Read-eval-print loop (wikipedia)

A read–eval–print loop (REPL), also known as an interactive toplevel or language shell,
is a simple, interactive computer programming environment that takes single user inputs
(i.e. single expressions), evaluates them, and returns the result to the user;
a program written in a REPL environment is executed piecewise. The term is most usually
used to refer to programming interfaces similar to the classic Lisp machine interactive
environment. Common examples include command line shells and similar environments for
programming languages, and is particularly characteristic of scripting languages.

How To build a REPL Program with Redtamarin

First, you need to define a loop, and more precisely an infinite loop.

public function loop():void
{
    while( true )
    {

        // infinitely looping here

    }
}

Note:
the redshell runtime can loop forever
at the contrary of the Flash Player or Adobe AIR runtimes.

Only if you use the flag -Dtimeout then
a maximum timeout of 15 seconds is enforced.

OK, so we are looping forever, now we need to be able to detect a user input.

For that, we will use kbhit() that can be found in the package C.conio, which simply returns a non-zero value if a character value is waiting in the buffer (from stdin).

And to read this input we will use getchar() from the package C.stdio to read the last byte from the input stream aka stdin.

import C.stdio.*;
import C.conio.*;

public function loop():void
{
    while( true )
    {
        // we know a key has been pressed
        if( kbhit() != 0 )
        {

            var charcode:int = getchar();
            var key:String   = String.fromCharCode( charcode );

        }

    }
}

When you have the power of C in AS3 that's pretty easy :).

Note:
conio.h is a Windows thing, but it has been implemented
cross-platform for Mac OS X and Linux, so this kbhit()
call and any other C.conio functions will work everywhere.

Other functions you may want to use are

  • canonical( true )
    Turns the console/terminal canonical mode on or off.
    By default, canonical mode is on.
    This is also called the "cooked mode", when you read from the console/terminal,
    it returns a line at a time instead of each character as it is received.

    Changing the canonical mode to off is called the "raw mode" (or non-canonical).
    With this mode the input will not be put into lines before it is returned,
    and it will also not process some special characters: ERASE, KILL, EOF,
    NL, EOL, EOL2, CR, REPRINT, STATUS and WERASE.

  • echo( true )
    Turns the console/terminal echo mode on or off.
    By default, echo mode is on.
    Each time you read stdin the console/terminal echo it to stdout.

    If you turn echo off, you will have to manage manually
    the user input "feedback" (eg. echo to stdout yourself)
    but it also allow you to modify what you output,
    for example: prepend a text, change the color, etc.

If you are not sure, don't use those functions, for example: managing yourself the BACKSPACE key can be quite complicated, or other example if you need to deal with UTF-8 input from your users.

Our infinite loop is for our program to keep running forever, but we need another loop to listen on user input as we want to read "words" and not just "keys".

import C.stdio.*;
import C.conio.*;

public function loop():void
{

    // a buffer for the user input 
    var buffer:String = "";

    while( true )
    {
        // we know a key has been pressed
        if( kbhit() != 0 )
        {

            // we want to listen to all the keys
            var listen:Boolean = true;
            while( listen )
            {
                var charcode:int = getchar();
                var key:String   = String.fromCharCode( charcode );

                /* If the ENTER key is pressed we stop listening for user input
                   otherwise we accumulate the keys in a buffer
                */
                switch( key )
                {
                    case "\n":
                    listen = false;
                    break;

                    default:
                    buffer += key;
                }
            }

            // more stuff
            trace( "user input is [" + buffer + "]" );

        }

    }
}

Voila, our loop is complete: we can listen for user input and retrieve "words".

Let's parse this buffer.

public function parse( buffer:String ):void
{
    var len:uint = buffer.length;

    // we want only to interpret non-empty input
    if( len > 0 )
    {
        var cmd:String = buffer;

        // we want to be sure no line return end the string
        if( cmd.charAt( cmd.length - 1 ) == "\n" )
        {
            cmd = cmd.substr( 0, cmd.length - 1 );
        }

        interpret( cmd );
    }
    else
    {
        //trace( "no input" );
    }
}

Usually in a "shell" or "repl" if a user only enter a new line or carriage return we simply go to the line without doing anything, but in some case you may want to display a little message.

We mainly want to be sure that we Read a clean string, but you could do many other things like limiting the size of the input (maybe you don't want to read more than n bytes), or logging the entry, etc.

After that, it's about interpreting that string, the Eval part.

import C.stdlib.*;

public function interpret( command:String ):void
{
    switch( command )
    {
        case "quit":
        exit( EXIT_SUCCESS ); 
        break;

        default:
        trace( command + ": command not found" );
    }
}

This interpreter is extremely basic, you may want to use some regexp, detect the start and/or end of the command, etc.

You can see also that we merged the Print part by simply using the trace() function which write to stdout with a new line at the end.

All that are the very basic of a Read-Eval-Print Loop (REPL).

Now, let's make it a bit more nicer.

You may want to display a custom prompt and so write to the output without a new line at the end.

import C.stdio.*;

public function prompt():void
{
    fputs( "as3> ", stdout );
    fflush( stdout );
}

Note:
the C functions are preferred instead of Program.write()
because we want to be able to flush the standard stream
where we are writing the data, otherwise in some cases
when you use canonical() and echo() the order of chars
written to the stdout could be messed up, fflush() fix that.

From there simply call prompt() just before you call loop()
and after each call to parse()

public function loop():void
{
    // we display the prompt by default
    prompt();

    // a buffer for the user input 
    var buffer:String = "";

    while( true )
    {
        // we know a key has been pressed
        if( kbhit() != 0 )
        {

            // we want to listen to all the keys
            var listen:Boolean = true;
            while( listen )
            {
                var charcode:int = getchar();
                var key:String   = String.fromCharCode( charcode );

                /* If the ENTER key is pressed we stop listening for user input
                   otherwise we accumulate the keys in a buffer
                */
                switch( key )
                {
                    case "\n":
                    listen = false;
                    break;

                    default:
                    buffer += key;
                }
            }

            // eval the data
            parse( buffer );
            
            // reset the buffer
            buffer = "";

            //display the prompt again
            prompt();
        }

    }
}

Here the full shell program

package corsaair.helloworld
{
    import shell.*;
    import C.stdlib.*;
    import C.stdio.*;
    import C.conio.*;
    import flash.utils.ByteArray;
    
    public class Shell
    {
        private var _version:String;

        public var run:Boolean;
        public var debug:Boolean;

        public function Shell( version:String = "0.0.0" )
        {
            super();
            
            _version = version;

            run   = false;
            debug = false;
        }

        public function get version():String { return _version; }

        private function _format( c:String ):String
        {
            switch( c )
            {
                case "\b":
                return "\\b";

                case "\t":
                return "\\t";

                case "\n":
                return "\\n";

                case "\v":
                return "\\v";

                case "\f":
                return "\\f";

                case "\r":
                return "\\r";

                default:
                var ccode:int = c.charCodeAt(0);
                if( ccode > 0xff )
                {
                    var hex:String = ccode.toString( 16 );
                    while( hex.length < 4 )
                    {
                        hex = "0" + hex;
                    }

                    return "\\u" + hex;
                }
                else
                {
                    return c;
                }
            }
        }

        public function parse( buffer:String ):void
        {
            var len:uint = buffer.length;

            if( len > 0 )
            {
                var cmd:String = buffer;

                if( cmd.charAt( cmd.length - 1 ) == "\n" )
                {
                    cmd = cmd.substr( 0, cmd.length - 1 );
                }

                interpret( cmd );
            }
            else
            {
                if( debug )
                {
                    trace( "no input" );
                }
            }

        }

        public function prompt():void
        {
            fputs( "as3> ", stdout );
            fflush( stdout );
        }

        public function loop():void
        {
            run = true;
            prompt();
            
            var buffer:String = "";

            while( run )
            {
                if( kbhit() != 0 )
                {

                    var listen:Boolean = true;
                    while( listen )
                    {

                        var charcode:int = getchar();
                        var key:String   = String.fromCharCode( charcode );

                        if( debug )
                        {
                            trace( "key [" + _format( key ) + "] (" + charcode + ")" );
                        }
                        
                        switch( key )
                        {
                            case "\n":
                            listen = false;
                            break;

                            default:
                            buffer += key;
                        }
                    }

                    parse( buffer );
                    buffer = "";

                    if( run )
                    {
                        prompt();
                    }
                }
            }

        }

        public function interpret( command:String ):void
        {
            switch( command )
            {
                case "quit":
                case "exit":
                run = false;
                break;

                case "debug":
                debug = !debug;
                trace( "[debug mode: " + (debug?"ON":"OFF") + "]" );
                break;

                case "clear":
                // will work only under a Bash shell
                system( "clear" );
                break;

                case "?":
                case "h":
                case "help":
                trace( "Sorry, no help." );
                break;

                case "info":
                trace( "name = " + Program.filename );
                trace( "args = " + Program.argv );
                trace( "type = " + Program.type );
                break;

                case "version":
                trace( version );
                break;

                default:
                trace( command + ": command not found" );
            }
        }

        public function main( argv:Array = null ):void
        {
            // shell banner
            trace( "Custom Shell v" + version );

            loop();

            exit( EXIT_SUCCESS );
        }
    }

}

See how it look like in REPL / Shell helloworld example

asciicast

TODO

Clone this wiki locally