REPL en Erlang

Leyendo el blog de Verdi, me encuentro con un artículo muy interesante sobre los REPL (Read-Eval-Print… and Loop), vamos la forma de crear consolas de interacción básicas. El hecho de escribir acerca de ello no es solo copiar, sino ampliar un poco más donde Verdi se quedó, agregando el soporte para que esto, dentro de Erlang/OTP, sea un behaviour.

A través de un comentario lo he escrito en el blog de Verdi, pero considero que me quedó un poco mal, ya que no tiene formateo de sintaxis, por lo que, con permiso de Verdi, copio aquí el código que él mismo hizo finalmente:

-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 puede emplearse de esta forma:

-module(myCmd).
-export([start/]).
 
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)]).

Hasta aquí, nada nuevo, todo lo que se explica en el post original.

Entrando en el mundo OTP

Convertir este código en un comportamiento (behaviour) es bastante simple, porque ya Verdi lo ha dejado muy mascadito. Solo hay que agregar un lanzador (el típico start) y el código se presentaría así:

-module(gen_cmd).
 
-export([start/2, behaviour_info/1]).
 
behaviour_info(callbacks) ->
    [{handle_cmd,2}, {init, 1}];
behaviour_info(_) ->
    undefined.
 
start(Prompt, Module) ->
    {ok, State} = Module:init([]),
    linea_comandos(Module, Prompt, State).
 
linea_comandos(Module, Prompt, State) ->
    case io:get_line(standard_io, Prompt) of
        {error, Motivo} ->
            io:format("error: ~p~n", [Motivo]),
            linea_comandos(Module, Prompt, State);
        {eof} -> 
            linea_comandos(Module, Prompt, State);
        Comando -> % operamos con el comando
            case Module:handle_cmd(Comando, State) of
                {stop, _Reason} ->
                    ok;
                {ok, NewState} ->
                    linea_comandos(Module, Prompt, NewState)
            end
    end.

Usarlo sería tan simple como:

-module(myCmd).
 
-export([init/1, handle_cmd/2, start/]).
 
-behaviour(gen_cmd).
 
start() ->
    io:format("Bienvenido a myCmd.~n"),
    gen_cmd:start("Cmd> ", ?MODULE),
    io:format("Adios.~n").
 
init([]) ->
    {ok, []}.
 
handle_cmd("q\n", _State) ->
    {stop, normal};
handle_cmd(Comando, State) ->
    io:format("~s", [os:cmd(Comando)]),
    {ok, State}.

Solo restaría agregar el cambio de código en caliente, que dado el código no es muy complejo.

Conclusiones

Con este gen_cmd es relativamente fácil implementar sistemas de shell para nuestros códigos, pero tiene el principal problema de que es una entrada estándar y no un sistema como readline, con lo que puede resultar difícil implementar cosas como la autocompleción o el histórico de comandos, pero ya es un mal menor, realmente.