aprendiendo ( Erlang ).

jueves, 21 de junio de 2012

REPL en Erlang (MyniCommand)

| 3 comentarios |

Hace dos días un amigo, me enseño un código en Python. Un código para interpretar comandos y realizar ciertas operaciones… Y pensé … es un buen ejemplo para realizar con Erlangun REPL, si, eso.

Todo interprete de comandos que se precie empieza con un pequeño bucle, que permite recoger los comandos introducidos por el usuario. Y continua, con la interpretación de dichos comandos. Para ser más exactos, nos estamos refiriendo a un paradigma que los Lispers conocen como REPL (Read-eval-print loop). Erlang, como la mayoría de los lenguajes interpretando tiene un REPL en su sistema, ¿lo adivinas? … cierto, el EShell sigue este paradigma.

Lo primero, que hay que resolver es, la lectura de teclado (entrada estándar). Me dedique a revisar el post que realice sobre manejo de ficheros y puse mis ojos en la función io:get_line/2 (para más información en la página oficial). Y su comportamiento es ideal para nuestro propósito.

-module(myniCommand).
-export([start/0]).

-define (PROMPT, " Cmd > ").

start() ->
    io:format("Bienvenido a myniCommand (mi mini interprete de comandos).~n"),
    linea_comandos(),
    io:format("Adios~n" ).

linea_comandos() ->
    case io:get_line(standard_io, ?PROMPT) of
        {error, Motivo} ->
            io:format(" error: ~p~n", [Motivo]),
            linea_comandos();
        {eof} -> 
            linea_comandos();
        "q\n" ->
            ok;
        Comando -> % operamos con el comando
            procesar(Comando),
            linea_comandos()
    end.

procesar(Comando) ->
    io:format("     ~s", [Comando]).

El bucle como puedes ver lo realiza la función linea_comandos/0 con llamadas recursivas. Dicha función, linea_comandos/0, lee de la entrada estándar, mediante la función io:get_line/2, mostrando un prompt previo. Una vez que estamos en posesión del comando, lo que toca es procesarlo o interpretarlo. Del proceso de interpretación de los comandos se va a encargar la función procesar/1 que recibe el comando para su evaluación e imprime el resultado.

1> c(myniCommand).
{ok,myniCommand}
2> myniCommand:start().
Bienvenido a myniCommand (mi mini interprete de comandos).
 Cmd > hola
     hola
 Cmd > q
Adios
ok

Ya tenemos la base para cualquier interprete de comandos que deseemos realizar. Se me ocurre, por ejemplo, algún tipo de lenguaje interpretado, o aquellos fantásticos juegos de línea de comando como gnuchess.

Ahora, con la función os:cmd/1 (documentación oficial), vamos a realizar un pequeño interprete de comandos. La función os:cmd/1 se encarga de evaluar el comando que le pasemos y recoger la salida estándar del sistema operativo y devolvérnosla.

procesar2(Comando) ->
    io:format("~s", [os:cmd(Comando)]).

Veamos lo que hace.

1> c(myniCommand).
./myniCommand.erl:32: Warning: function procesar/1 is unused
{ok,myniCommand}
2> myniCommand:start().
Bienvenido a myniCommand (mi mini interprete de comandos).
 Cmd > pwd
/home/verdi/Documentos/aprendiendo-erlang/Myni-Command
 Cmd > ls -la
total 24
drwxrwxr-x  2 verdi verdi 4096 2012-06-16 16:27 .
drwxrwxr-x 69 verdi verdi 4096 2012-06-16 16:09 ..
-rw-rw-r--  1 verdi verdi  916 2012-06-16 16:27 myniCommand.beam
-rw-rw-r--  1 verdi verdi  934 2012-06-16 16:26 myniCommand.erl
-rwxrwxr-x  1 verdi verdi  618 2012-06-16 16:09 myniCommand.escript
-rw-rw-r--  1 verdi verdi 3323 2012-06-16 16:25 myni-command.org
 Cmd > comando no valido
/bin/sh: comando: not found
 Cmd > pwd
/home/verdi/Documentos/aprendiendo-erlang/Myni-Command
 Cmd > cd /home/verdi
 Cmd > pwd
/home/verdi/Documentos/aprendiendo-erlang/Myni-Command
 Cmd > q
Adios
ok

El comportamiento es bueno, pero no es el deseado. Esta función tiene limitaciones como podemos ver en los últimos comandos evaluados. Esto es debido a que la función os:cmd/1 abre un interprete, un shell de sistema, para evalúar el comando en cuestión y posteriormente lo cierra. Al cerra el shell de sistema perdemos, evidentemente, el estado en el que estabamos.

Pero, mi objetivo es crear un sistema que pueda reutilizar. Es decir, que cuando necesite un sistema REPL lo pueda realizar de forma rápida y sencilla, y para ello, mi siguiente paso debe ser generalizarlo.

-module(myniCommand2).
-export([linea_comandos/2]).

linea_comandos(Prompt, Interprete) ->
    case io:get_line(standard_io, Prompt) of
        {error, Motivo} ->
            io:format(" error: ~p~n", [Motivo]),
            linea_comandos(Prompt, Interprete);
        {eof} -> 
            linea_comandos(Prompt, Interprete);
        Comando -> % operamos con el comando
            case Interprete(Comando) of
                exit ->
                    ok;
                _ ->
                    linea_comandos(Prompt, Interprete)
            end
    end.

Que te parece … no es bello … tenemos el núcleo de un sistema REPL en algo más de … 15 líneas mal contadas. Fantástico.

El módulo myniCommand2 es la versión evolucionada. El principal cambio radica en la forma de interpretar los comandos leídos. Ahora ya no evaluamos e imprimimos el resultado del comando sino, que lo delegamos en una función que recibiremos por parámetro. A dicha función le vamos a exigir que reciba un comando y que devuelva exit si se trata del comando utilizado para salir del interprete.

Veamos como quedan nuestro pequeño editor de comandos.

-module(myCmd).
-export([start/0]).

start() ->
    io:format("Bienvenido a myCmd.~n"),
    myniCommand2:linea_comandos(" Cmd > ", fun interprete/1),
    io:format("Adios~n" ).

interprete ("q\n") ->
    exit;

interprete (Comando) ->
    io:format("~s", [os:cmd(Comando)]).

Como puedes ver … su simpleza asusta. Y en 0.2 … listo ya tenemos nuestro REPL completamente operativo.

Publicar un comentario en la entrada

3 comentarios:

  1. Manuel Rubio dijo...

    Hola Verdi, ¡muy buen post!, como siempre... no obstante, la parte general que expones, la completaría encapsulándola en un behaviour, algo como esto:

    -module(mycmd).

    -export([start/2, behaviour_info/1]).

    behaviour_info(callbacks) ->
    [{interprete/3}];
    behaviour_info(_) ->
    undefined.

    start(Prompt, Interprete) ->
    {ok, spawn(Interprete)}.

    linea_comandos(Prompt, fun interprete/1, State) ->
    case io:get_line(standard_io, Prompt) of
    {error, Motivo} ->
    io:format(" error: ~p~n", [Motivo]),
    linea_comandos(Prompt, Interprete, State);
    {eof} ->
    linea_comandos(Prompt, Interprete, State);
    Comando -> % operamos con el comando
    case Interprete(Comando) of
    exit ->
    ok;
    _ ->
    linea_comandos(Prompt, Interprete, State)
    end
    end.

    Más o menos como lo que escribí en este artículo:

    http://bosqueviejo.net/2012/06/12/behaviours-la-potencia-de-otp/

    Un saludo y sigue así!

  2. Verdi dijo...

    Tienes toda la razón, la implementación de un "behaviour" es lo suyo. Y como pienso seguir insistiendo en este tema, realizaré las modificaciones oportunas para que sea un comportamiento OTP.

    Como nota curiosa, decir que, esta mañana revisando el artículo y buscando aportar una mayor flexibilidad al código, pensé en incorporar un nuevo argumento a la función linea_comando/2. ¿Ya te imaginaras cuál? Pues si, se trata de un parámetro de estado (State) ;P. La idea surgió como un fogonazo "un historial". ¿Que pasa si quiero tener un historial de comandos ejecutados en un momento dado? Como he podido comprobar, por el código que has puesto, coincidimos en ello.


    Verdi!gracias.

  3. Verdi dijo...

    El Sr. Manuel Rubio a tenido a bien complementar este post con uno de cosecha propia. En él, explica claramente como dotar al "myniCommand" de comportamiento al estilo OTP.

    Os dejo la URL para que lo leáis:
    http://bosqueviejo.net/2012/07/04/repl-en-erlang/

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.