Hatena::Grouperlang

檜山正幸のErlang未確認情報 RSSフィード

 | 

2009-03-31 (火)

ミニシェル

| 10:46

テケトウに作ったばっかりのものだけど、けっこう便利なんじゃねーの、コレ。改善の余地はある(つうか、作り直すだろう)けど、とりあえず使える。

このエントリーマニュアル風に書く。最後にソースコードが貼ってあるので、これを見るのが一番速いかも。「apply、アブネーー」の問題は放置してある、要注意。

それと、Edocで生成したHTMLドキュメント:

動機とウリ

ErlangにはEシェルがあるので、対話的な実験やテストができて便利。だが、同じような、しかし少し違ったコマンドを繰り返し繰り返しタイプするのにはウンザリ。コマンド入力をもっと簡略化できないの? というのが動機。

ミニシェル(minshモジュール*1)は、汎用のコマンドライン・フロントエンドで、コマンド関数を実装したコールバックモジュールにより、さまざまな用途に使える。

例えば、よく使うコマンド(関数呼び出し)を1文字、あるいは[ENTER]のみにすることができる。簡易なgen_serverのような機能を持っているので、関数ライブラリのgen_server化の前に、手入力でのテストができる。また、既にあるgen_serverへのフロントエンドとしても使える。

使い方

コマンド関数を実装したコールバックモジュール名を指定して起動する。開始のバナー(↓)はどうせ変わるるだろう。

15> minsh:start(foo).

*** Starting shell (command module: foo) ***

This is foo.

(foo)$  

ミニシェルからコールバックモジュールにどんな呼び出しがかかるかは、次のモジュールをコールバックに指定していじれば分かる。

%% -*- coding: utf-8 -*-

-module(command_echo).
-export([command_init/1, command_fin/1, command/3]).

command_init(Arg) ->
  io:format("~nThis is " ++ atom_to_list(?MODULE) ++ ".~n"),
  io:format("cammand_init/1 is calld: Arg = ~p~n~n", [Arg]),
  0.
  
command_fin(Arg) ->
  io:format("~nThis is " ++ atom_to_list(?MODULE) ++ ".~n"),
  io:format("cammand_fin/1 is calld: Arg = ~p~n", [Arg]),
  ok.
command(Cmd, Arg, State) ->
  io:format("cammand/3 is calld: Cmd = '~s', Arg = ~p, State=~p~n", 
            [Cmd, Arg, State]),
  {ok, State + 1}.

コールバックモジュールの書き方

コールバック関数は:

  1. CmdMod:command_init(InitArg) -> InitState (省略可能)
  2. CmdMod:command_fin(LastState) -> ok (省略可能)
  3. CmdMod:command(Cmd::atom(), Arg, State) -> {ok, NewState}|{quit, LastState} (必須)

CmdMod:command/3の第1引数以外の引数型は任意。

ミニシェルインタプリタ関数が {quit, LastState} を受け取ると、CmdMod:command_fin(LastState) を呼び出してからシェルを終了する。

なお、1文字のコマンド q はミニシェル側の組み込みとなっている。それ以外の組み込み関数はない。組み込みはたぶん増える、、そのうち。

コールバック関数を書くときの注意とコツ

ランタイムエラーで終了しないように、command/3 の最後の関数節に次を入れておくこと。

command(_, _, State) ->
  io:format("illegal command~n"),
  {ok, State}.

改行だけ、「空白+ターム」のコマンドを定義しておくと激しく便利

% ...
command('', Arg, State) ->
  case Arg of
    none -> % 改行だけ
      % ...
      {ok, NewState_1};
    SomePattern ->
      % ...
      {ok, NewState_2};
    % ...
  end;
command(_, _, State) ->
  io:format("illegal command~n"),
  {ok, State}.

今後

これをメンテナンスはしないような気がする。もっと別なモノを考えている。問題だと思っているのは、コマンドコールバック関数を書くのが面倒なこと。

orddictモジュールに対するフロントエンド。これで、orddictの使い方が練習できる。

%% -*- coding: utf-8 -*-

%% @doc orddictのテスト
-module(od_test).

% -export([]).
-compile(export_all).

%% プリティプリント
pp(OD) ->
  orddict:fold(
    fun(K, V, _A) ->
        io:format(" ~p => ~p~n", [K, V])
    end,
    ok,
    OD).

%% ヘルプ表示
help_str() ->
  (" ?          -- show this help\n"
   " .          -- dump\n"
   " a {K, V}   -- append K, V\n"
   " al {K, VL} -- append_list K, VL\n"
   " e K        -- erase K\n"
   " fe K       -- fetch K\n"
   " fk         -- fetch_keys\n"
   " f K        -- find K\n"
   " k K        -- is_key  K\n"
   " sz         -- size\n"
   " s {K, V}   -- store K, V\n"
   " x          -- exit\n"
   " ENTER      -- pretty-print\n"
   " SPACE {K, V} -- store K, V and pretty-print\n"
  ).

help() ->
  io:format(help_str()).

command_init(_Arg) ->
  io:format("~nThis is od_test. "
            "type '?' for help.~n~n"
           ),
  orddict:new().

command_fin(_LastState) ->
  ok. % do nothing

command('.', Arg, State) ->
  io:format("Dump: Arg = ~p, State = ~p~n", [Arg, State]),
  {ok, State};
command('?', _Arg, State) ->
  help(),
  {ok, State};
command(a, {K, V}, State) ->
  NewState = orddict:append(K, V, State),
  {ok, NewState};
command(al, {K, VL}, State) ->
  NewState = orddict:append_list(K, VL, State),
  {ok, NewState};
command(e, K, State) ->
  NewState = orddict:erase(K, State),
  {ok, NewState};
command(fe, K, State) ->
  V = orddict:fetch(K, State),
  io:format("value = ~p~n", [V]),
  {ok, State};
command(fk, _Arg, State) ->
  V = orddict:fetch_keys(State),
  io:format("value = ~p~n", [V]),
  {ok, State};
command(f, K, State) ->
  V = orddict:find(K, State),
  io:format("value = ~p~n", [V]),
  {ok, State};
command(k, K, State) ->
  V = orddict:is_key(K, State),
  io:format("value = ~p~n", [V]),
  {ok, State};
command(sz, _Arg, State) ->
  V = orddict:size(State),
  io:format("value = ~p~n", [V]),
  {ok, State};
command(s, {K, V}, State) ->
  NewState = orddict:store(K, V, State),
  {ok, NewState};
command(x, _Arg, State) ->
  {quit, State};
command('', Arg, State) ->
  case Arg of
    none -> % 改行だけ
      pp(State),
      {ok, State};
    {K, V} ->
      NewState = orddict:store(K, V, State),
      pp(NewState),
      {ok, NewState};
    Key ->
      V = orddict:find(Key, State),
      io:format("value = ~p~n", [V]),
      {ok, State}
  end;
command(_, _, State) ->
  io:format("illegal command~n"),
  {ok, State}.

minshのソース

%% -*- coding: utf-8 -*-

%% @doc ミニミニシェル.
%%
%% start(CmdMod) または start(CmdMod, InitArg) として起動する。
%% CmdMod はコマンドを実装しているコールバックモジュール名(アトム)、
%% InitArg はコールバックモジュール初期化に使う引数である。
%% InitArg が省略されるとアトム none が初期化に使われる。
%%
%% コールバック関数は:<br/>
%% <ol>
%% <li>CmdMod:command_init(InitArg) -> InitState (省略可能)</li>
%% <li>CmdMod:command_fin(LastState) -> ok (省略可能)</li>
%% <li>CmdMod:command(Cmd::atom(), Arg, State) -> {ok, NewState}|{quit, LastState} (必須)</li>
%% </ol>
%% CmdMod:command/3の第1引数以外の引数型は任意である。
%% 
%% インタプリタ関数が {quit, LastState} を受け取ると、
%% CmdMod:command_fin(LastState) を呼び出してからシェルを終了する。

-module(minsh).

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

-define(SPACE, 32).
-define(NL, 10).
-define(PROMPT, "$ ").

%% @equiv start(CmdMod, none)
%% @spec (atom()) -> ok
start(CmdMod) ->
  start(CmdMod, none).

%% @doc シェルの開始
%% @spec (atom(), term()) -> ok
start(CmdMod, InitArg) ->
  io:format("~n*** Starting shell (command module: ~s) ***~n~n", [CmdMod]),
  InitState = init(CmdMod, InitArg),
  Prompt = "(" ++ atom_to_list(CmdMod) ++ ")" ++ ?PROMPT,
  shell_loop(CmdMod, InitState, Prompt).

%% @doc コマンドモジュール初期化
%% @spec (atom(), term()) -> term()
init(CmdMod, InitArg) ->
  try
    apply(CmdMod, command_init, [InitArg])
  catch
    error:undef ->
      none
  end.

%% @doc コマンドモジュールの後始末
%% @spec (atom(), term()) -> ok
finalize(CmdMod, LastState) ->
  try
    apply(CmdMod, command_fin, [LastState])
  catch
    error:undef ->
      ok
  end.

%% @doc シェルのループ
%% @spec (atom(), term(), string()) -> ok
shell_loop(CmdMod, State, Prompt) ->
  Line = io:get_line(Prompt),
  case interpret(Line, CmdMod, State) of
    {ok , NewState} ->
      shell_loop(CmdMod, NewState, Prompt);
    {quit, LastState} ->
      finalize(CmdMod, LastState),
      io:format("~n*** bye. ***~n"),
      ok % exit from the loop
  end.

%% @doc 入力行の解釈実行
%% @spec (string(), atom(), term()) -> {ok, Cmd, Arg} | {error, Msg}
%% where
%%   Cmd = atom(), Arg = term(), Msg = IOList
interpret(Line, CmdMod, State) ->  
  CmdTuple = make_cmd_tuple(Line),
  case CmdTuple of
    {ok, Cmd, Arg} ->
      exec_command(Cmd, Arg, CmdMod, State);
    {error, Msg} ->
      io:format("Error: ~s~n", [Msg]),
      {ok, State}
  end.

%% @doc 入力行から、コマンドアトムと引数タームを作る
%% @spec (string()) -> {ok, Cmd::atom(), Arg::term()} | {error, Msg}
%% where
%%  Msg = IOList
make_cmd_tuple(Line) ->
  {CmdStr, ArgStr} = split_line(Line),
  case parse_term(ArgStr) of
    {ok, Term} ->
      {ok, list_to_atom(CmdStr), Term};
    {error, Msg} ->
      {error, Msg}
  end.

%% @doc 入力行をコマンドと引数に分割する
%% @spec (string()) -> {string(), string()}
split_line(Line) ->
  % Line_1 = string:strip(Line),
  split_line(Line, []).

%% @doc split_line/1 の作業関数
%% @spec (Looking::string(), Looked::string()) -> {string(), string()}
split_line(_Looking = [Ch|Rest], Looked) ->
  if Ch == ?SPACE orelse Ch == ?NL ->
      {lists:reverse(Looked), Rest};
     true ->
      split_line(Rest, [Ch|Looked])
  end;
split_line([], Looked) ->
  {lists:reverse(Looked), ""}.

%% @doc 文字列を解析してタームを作る
%% @spec (string()) -> {ok, term()} | {error, Msg}
%% where
%%   Msg = IOList
parse_term(Str) ->
  case erl_scan:string(Str) of
    {ok, Tokens, _} ->
      FixedTokens = fix_tokens(Tokens),
      case erl_parse:parse_term(FixedTokens) of
        {ok, Term} ->
          {ok, Term};
        {error, {_Line, erl_parse, Msg}} ->
          {error, Msg}
      end;
    {error, ErrorInfo, _} ->
      Msg = erl_scan:format_error(ErrorInfo),
      {error, Msg}
  end.

%% @doc Tokensの末尾にドットがないときは付け足す.
%% Tokensに関しては erl_scan を参照。
%% @spec (Tokens) -> Tokens
fix_tokens([]) ->
  [{atom, 1, none}, {dot, 1}];
fix_tokens(Tokens) ->
  case lists:last(Tokens) of
    {dot, _} ->
      Tokens;
    _ ->
      lists:append(Tokens, [{dot, 1}])
  end.

%% コマンドは必ず ok, quit のいずれかを返す。
%% インタプリタ関数が quit を受け取るとシェルを終了する。

%% @doc コマンドの実行
%% @spec (Cmd::atom(), Arg::term(), CmdMod::atom(), State::term()) -> 
%%   {ok, State::term()} | {quit, LastState::term()}
exec_command(q, _Arg, _CmdMod, State) -> % q は組み込み
  {quit, State};
%exec_command('', _, State, _) -> % 改行だけはNOP
%  {ok, State};
exec_command(Cmd, Arg, CmdMod, State) ->
  Result = 
    try 
      apply(CmdMod, command, [Cmd, Arg, State])
    catch
      error:undef ->
        finalize(CmdMod, State),
        throw(cannot_find_command_mod_fun)
    end,
  case Result of
    {ok, _NewState} ->
      Result;
    {quit, _NewState} ->
      Result;
    _Other ->
      finalize(CmdMod, State),
      throw(illegal_command_result)
  end.

*1:別に民主党のファンじゃねーぞ。

ValentinaValentina2012/08/24 18:51This piece was cogent, well-witrten, and pithy.

hzkiyhzhzkiyhz2012/08/25 15:48ulmEt6 <a href="http://gcoiyuwzcreu.com/">gcoiyuwzcreu</a>

kpyqduozykpyqduozy2012/08/27 00:13KrkVK3 <a href="http://dmuynnkjqulo.com/">dmuynnkjqulo</a>

dajugzudajugzu2012/08/27 22:29aIT0Zi , [url=http://jjbybzuabnzb.com/]jjbybzuabnzb[/url], [link=http://dffjjwepljnx.com/]dffjjwepljnx[/link], http://nbseayfagmif.com/

 |