Search code examples
ioerlangread-eval-print-looperlang-shell

Shell within Erlang process prints input many times


I built a REPL for a language in Erlang. I run it from the shell, like so:

repl:main(Args).

It reads using io_getchars until it encounters a ;, then parses and lexes it:

getchar(eof) -> throw(eof);
getchar([C]) -> C.
getchar() -> getchar(io:get_chars("", 1)).

read_term() ->
    case getchar() of
        $; -> ";";
        C -> [C|read_term()]
    end.

That's all well and good, but the interaction with the repl looks like this:

willow% erl -name MyName 
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V7.3  (abort with ^G)
(MyName)1> repl:main(Args).
Master is <0.39.0> in init_workers
1> map(fn(x)=x+1, '[1 2 3 4 5]);      
ap(fn(x)=x+1, '[1 2 3 4 5]);
p(fn(x)=x+1, '[1 2 3 4 5]);
(fn(x)=x+1, '[1 2 3 4 5]);
fn(x)=x+1, '[1 2 3 4 5]);
n(x)=x+1, '[1 2 3 4 5]);
(x)=x+1, '[1 2 3 4 5]);
x)=x+1, '[1 2 3 4 5]);
)=x+1, '[1 2 3 4 5]);
=x+1, '[1 2 3 4 5]);
x+1, '[1 2 3 4 5]);
+1, '[1 2 3 4 5]);
1, '[1 2 3 4 5]);
, '[1 2 3 4 5]);
 '[1 2 3 4 5]);
'[1 2 3 4 5]);
[1 2 3 4 5]);
1 2 3 4 5]);
 2 3 4 5]);
2 3 4 5]);
 3 4 5]);
3 4 5]);
 4 5]);
4 5]);
 5]);
5]);
]);
);
;
Delegated packet 0 
Delegated packet 1 
Delegated packet 2 
Delegated packet 3 
Delegated packet 4 
(2 3 4 5 6)
2> 

I also tried:

read_term(Acc) ->
    case io:request(standard_io, {get_until, '', scanner, token, [1]}) of
        {ok, EndToken={';;', _}, _} -> Acc ++ [EndToken];
        {ok, Token, _} -> read_term(Acc ++ [Token]);
        {error, token} -> {error, scanning_error};
        {eof, _} -> Acc
    end.

And it unfortunately has the same effect.

I'd rather not see every possible right-moving slice of the string I entered. What is causing this and how can I stop it?

EDIT: If I run it as erl -noshell -eval 'repl:main()' from the shell instead, this printing does not happen. Why?


Solution

  • You will have to use io:get_line instead, and iterate over the received string. Then you have to decide what to do if the received string does not contain any ";"

    for example:

    1> Eval = fun(X) -> io:format("evaluation of ~p~n",[X]) end.                                            
    #Fun<erl_eval.6.50752066>
    2> F = fun() -> 
           R = io:get_line("Enter a string : "),
           L = string:tokens(R,";"),
           [Eval(X) || X <- L]
       end.
    #Fun<erl_eval.20.50752066>
    3> F().                                                                                                 
    Enter a string :  map(fn(x)=x+1, '[1 2 3 4 5]);map(fn(x)=x+5, '[1 2 3 4 5]);uncomplete                                
    evaluation of " map(fn(x)=x+1, '[1 2 3 4 5])"
    evaluation of "map(fn(x)=x+5, '[1 2 3 4 5])"
    evaluation of "uncomplete\n"
    [ok,ok,ok]
    4> F().                                                                 
    Enter a string :  map(fn(x)=x+1, '[1 2 3 4 5]);                                       
    evaluation of " map(fn(x)=x+1, '[1 2 3 4 5])"
    evaluation of "\n"
    [ok,ok]       
    5>
    

    [edit]

    io:get_chars/2 is taking the number of chars given as parameter from the shell input, 1 in your case, and let the rest. The shell can't know that you will invoke again the io:get_chars/2 function, so it prints again the rest of the input string, and it does the same again and again at each call of the read_term/0 function, it is why you see this weird behavior.

    io:get_line/2 takes the whole input from the shell and return it in a string (in erlang it is a list of integer), it is why you have a clean behavior in the shell window, then it is up to you to choose your favorite algorithm to analyze this string. I have chosen string:tokens/2 because I try to use library functions when they exists, I think it is easier to read, and does not need documentation :o)

    If you don't want to take care of possible incorrect input, you can replace the F/0 function by

    2> F = fun() ->                               
    2>     Rep = io:get_line("Enter a string : "),
    2>     [L,"\n"] = string:tokens(Rep,";"),     
    2>     L
    2> end.
    

    and it will do the same thing that your read_term/0 function (except it removes the last $;, but it checks its presence):

    3> F().                                  
    Enter a string : correct input;
    "correct input"
    4> F().          
    Enter a string : missing semi colon
    ** exception error: no match of right hand side value ["missing semi colon\n"]
    5> F().              
    Enter a string : correct input; but extracharacters after semi colon
    ** exception error: no match of right hand side value ["correct input"," but extracharacters after semi colon\n"]