m3msh/src/M3MiniShell.m3


---------------------------------------------------------------------------
MODULE M3MiniShell EXPORTS Main;

IMPORT ParseParams, Stdio, Rd, Wr, Process, Text, Thread, TextSeq, Rsrc,
       TextExtras AS TextEx, OSError, ASCII, Scan, Env, Pathname;
IMPORT SMsg AS Msg, System, RsrcUtils, TextReadingUtils, Copyright, PathRepr,
       ProcessEnv, Glob, FSUtils, TextUtils, RegEx, DirStack;
IMPORT MiniShellBundle, Creation;
IMPORT (* FSFixed AS *) FS;

<* FATAL Thread.Alerted *>
--------------------------------------------------------------------------
VAR
  rsrcPath       :  Rsrc.Path;
  targets	 :  TextSeq.T;
  nTargets       :  CARDINAL;
  keepGoing      :  BOOLEAN;
  noAction       :  BOOLEAN;
  pathnameConversion := TRUE;
  useInternalCmds    := TRUE;
--------------------------------------------------------------------------
VAR
  QuoteChar      := '`';
---------------------------------------------------------------------------
PROCEDURE M(msg : TEXT) =
  BEGIN
    TRY
      Wr.PutText(Stdio.stdout, msg & "\n");
      Wr.Flush(Stdio.stdout);
    EXCEPT ELSE
      Msg.Fatal("cannot write to stdout", 1000);
    END;
  END M;
--------------------------------------------------------------------------
PROCEDURE ProcessParameters() =
  VAR
    pageit := TRUE;
    myArgs := TRUE;
    act    :  TEXT;

  PROCEDURE ActIs(t : TEXT) : BOOLEAN =
    BEGIN
      RETURN Text.Equal(act, t);
    END ActIs;

  BEGIN
    WITH pp = NEW(ParseParams.T).init(Stdio.stderr) DO
      TRY
        WHILE myArgs DO
          act := pp.getNext();
          myArgs := FALSE;
          IF ActIs("-nopager") OR ActIs("-pipe") THEN
            myArgs := TRUE;
            pageit := FALSE;
          END;
          (* help option *)
          IF ActIs("-h") OR ActIs("-help") THEN
            myArgs := TRUE;
            TRY
              RsrcUtils.PageResource(rsrcPath, "m3msh.help", pageit);
            EXCEPT
              RsrcUtils.Error(e) => M("error listing description: " & e);
            END;
            Process.Exit(0);
          END;
          (* man option *)
          IF ActIs("-man") OR ActIs("-desc") THEN
            myArgs := TRUE;
            TRY
              RsrcUtils.PageResource(rsrcPath, "m3msh.desc", pageit);
            EXCEPT
              RsrcUtils.Error(e) => M("error listing description: " & e);
            END;
            Process.Exit(0);
          END;
          (* copyright option *)
          IF ActIs("-cr") OR ActIs("-copyright") THEN
            myArgs := TRUE;
            Copyright.Show(Copyright.T.All);
            Process.Exit(0);
          END;
          (* creation date option *)
          IF ActIs("-created") THEN
            M(Creation.Date & " on " & Creation.System);
            Process.Exit(0);
          END;
          (* some message and trace options *)
          IF ActIs("-v") THEN
            myArgs := TRUE;
            Msg.vFlag := TRUE;
          END;
          IF ActIs("-d") THEN
            myArgs := TRUE;
            Msg.dFlag := TRUE;
          END;
          IF ActIs("-q") THEN
            myArgs := TRUE;
            Msg.tFlag := FALSE;
          END;
          IF ActIs("-k") THEN
            myArgs := TRUE;
            keepGoing := TRUE;
          END;
          IF ActIs("-n") THEN
            myArgs := TRUE;
            noAction := TRUE;
          END;
          IF ActIs("-nointernalcmds") OR ActIs("-noic") THEN
            useInternalCmds := FALSE;
          END;
          IF ActIs("-qc") THEN
            myArgs := TRUE;
            WITH arg = pp.getNext() DO
              IF arg # NIL AND NOT Text.Empty(arg) THEN
                QuoteChar := Text.GetChar(arg, 0);
              END;
            END;
          END;
          IF ActIs("-npc") OR ActIs("-nopathnameconversion") THEN
            myArgs := TRUE;
            pathnameConversion := FALSE;
          END;

          (* add more options before this line *)
        END;

        IF NOT ActIs("--") THEN
          DEC(pp.next);
          pp.parsed[pp.next] := FALSE;
        END;
	nTargets := NUMBER(pp.arg^) - pp.next;
        (* build parameters *)
	targets := NEW(TextSeq.T).init(nTargets);
	FOR i := 1 TO nTargets DO
	  VAR t := pp.getNext(); BEGIN
            IF pathnameConversion THEN
              targets.addhi(PathRepr.Native(t));
            ELSE
              targets.addhi(t);
            END;
	  END;
	END;
        pp.finish();
      EXCEPT
        ParseParams.Error => Msg.Fatal("parameter error");
      END;
    END;
    (* all command line parameters handled *)
  END ProcessParameters;
---------------------------------------------------------------------------
CONST ExecFailure = 16_7FFFFFFF;
---------------------------------------------------------------------------
VAR
  longList   := FALSE;
  recursive  := FALSE;
  postorder  := FALSE;
  inorder    := FALSE;
---------------------------------------------------------------------------
PROCEDURE EvalSomeOptions(VAR args : TextSeq.T) =
  VAR cont := TRUE;
  BEGIN
    longList   := FALSE;
    recursive  := FALSE;
    postorder  := FALSE;
    inorder    := FALSE;
    WHILE cont AND args.size() > 0 DO
      WITH arg = args.get(0) DO
        IF Text.Equal(arg, "-l") THEN
          longList := TRUE;
          EVAL args.remlo();
        ELSIF Text.Equal(arg, "-r") THEN
          recursive := TRUE;
          EVAL args.remlo();
        ELSIF Text.Equal(arg, "-post") THEN
          postorder := TRUE;
          EVAL args.remlo();
        ELSIF Text.Equal(arg, "-in") THEN
          inorder := TRUE;
          EVAL args.remlo();
        ELSE
          cont := FALSE;
        END;
      END;
    END;
  END EvalSomeOptions;
---------------------------------------------------------------------------
PROCEDURE List(fn : Pathname.T) RAISES {} =
  VAR t : TEXT;
  BEGIN
    IF longList THEN
      IF FSUtils.IsFile(fn) THEN
        t := " [file]";
      ELSIF FSUtils.IsDir (fn) THEN
        t := " [dir]";
      ELSE
        t := " [unknown type]";
      END;
      M(fn & t);
    ELSE
      M(fn);
    END;
  END List;
---------------------------------------------------------------------------
PROCEDURE ListRec(fn : Pathname.T) RAISES {FSUtils.E} =
  BEGIN
    IF FSUtils.IsDir(fn) THEN
      VAR
        fns   := FileList(fn, levels := 1, prefix := fn);
        dirs  := NEW(TextSeq.T).init();
        files := NEW(TextSeq.T).init();
      BEGIN
        FOR i := 0 TO fns.size() - 1 DO
          WITH act = fns.get(i) DO
            IF FSUtils.IsDir(act) THEN
              IF inorder OR postorder THEN
                IF longList THEN List(act); END;
                ListRec(act);
              ELSE
                dirs.addhi(act);
              END;
            ELSE
              IF postorder THEN
                files.addhi(act);
              ELSE
                List(act);
              END;
            END;
          END;
        END;
        IF postorder THEN
          FOR j := 0 TO files.size() - 1 DO
            WITH file = files.get(j) DO
              List(file);
            END;
          END;
        ELSE
          FOR j := 0 TO dirs.size() - 1 DO
            WITH dir = dirs.get(j) DO
              IF longList THEN List(dir); END;
              ListRec(dir);
            END;
          END;
        END;
      END;
    ELSE
      List(fn);
    END;
  END ListRec;
---------------------------------------------------------------------------
PROCEDURE Rm(fn : Pathname.T) RAISES {FSUtils.E} =
  BEGIN
    IF FSUtils.Exists(fn) THEN
      FSUtils.Rm(fn);
    END;
  END Rm;
---------------------------------------------------------------------------
PROCEDURE RmRec(fn : Pathname.T) RAISES {FSUtils.E} =
  BEGIN
    IF FSUtils.Exists(fn) THEN
      FSUtils.RmRec(fn);
    END;
  END RmRec;
---------------------------------------------------------------------------
PROCEDURE Rmdir(fn : Pathname.T) RAISES {FSUtils.E} =
  BEGIN
    IF FSUtils.Exists(fn) THEN
      FSUtils.Rmdir(fn);
    END;
  END Rmdir;
---------------------------------------------------------------------------
PROCEDURE Exec(pgm : TEXT; args : TextSeq.T; env : ProcessEnv.T) : INTEGER =

  TYPE P = PROCEDURE(fn : Pathname.T) RAISES {FSUtils.E};

  PROCEDURE Apply(p : P; args : TextSeq.T) : INTEGER RAISES {FSUtils.E} =
    BEGIN
      FOR i := 0 TO args.size() -1 DO
        WITH fn = args.get(i) DO
          p(fn);
        END;
      END;
      RETURN 0;
    END Apply;

  BEGIN
    TRY
      IF Text.Equal(pgm, "cd") THEN
        IF args.size() > 0 THEN
          DirStack.SetWorkingDir(args.get(0));
        ELSE
          DirStack.SetWorkingDir(homeDir);
        END;
        RETURN 0;
      ELSIF Text.Equal(pgm, "pushd") THEN
        IF args.size() > 0 THEN
          DirStack.PushDir(args.get(0));
        ELSE
          DirStack.PushDir(homeDir);
        END;
        RETURN 0;
      ELSIF Text.Equal(pgm, "popd") THEN
        DirStack.PopDir();
        RETURN 0;
      ELSIF Text.Equal(pgm, "exit") THEN
        VAR ret : INTEGER := 0; BEGIN
          IF args.size() > 0 THEN
            TRY
              ret := Scan.Int(args.get(0));
            EXCEPT ELSE
              ret := -1;
            END;
          END;
          Process.Exit(ret);
          RETURN ret; (* make the compiler happy *)
        END;
      ELSE
        IF useInternalCmds THEN
          IF Text.Equal(pgm, "touch") THEN
            RETURN Apply(FSUtils.Touch, args);
          ELSIF Text.Equal(pgm, "mkdir") THEN
            RETURN Apply(FSUtils.Mkdir, args);
          ELSIF Text.Equal(pgm, "rm") THEN
            EvalSomeOptions(args);
            IF recursive THEN
              RETURN Apply(RmRec, args);
            ELSE
              RETURN Apply(Rm, args);
            END;
          ELSIF Text.Equal(pgm, "rmdir") THEN
            RETURN Apply(Rmdir, args);
          ELSIF Text.Equal(pgm, "list") THEN
            EvalSomeOptions(args);
            IF args.size() = 0 THEN
              args := ExpandWildcards("*");
            END;
            IF recursive THEN
              RETURN Apply(ListRec, args);
            ELSE
              RETURN Apply(List, args);
            END;
          END;
        END;
        RETURN System.Exec(pgm, args, env);
      END;
    EXCEPT
      System.ExecuteError(e) => Msg.Error(e); RETURN ExecFailure;
    | DirStack.Error(e)      => Msg.Error(e); RETURN ExecFailure;
    | FSUtils.E(e)           => Msg.Error(e); RETURN ExecFailure;
    END;
  END Exec;
---------------------------------------------------------------------------
PROCEDURE FileList(dir : TEXT; levels := 1; prefix := NIL) : TextSeq.T =
  VAR
    res  := NEW(TextSeq.T).init();

  PROCEDURE FL(dir : TEXT; levels : INTEGER; prefix : TEXT;
               VAR res : TextSeq.T) =
    VAR
      iter :  FS.Iterator;
      fn   :  TEXT;
    BEGIN
      IF NOT FSUtils.IsDir(dir) THEN
        Msg.Error(dir & " seems to be no directory");
        RETURN;
      END;
      IF levels > 0 THEN
        TRY
          iter := FS.Iterate(dir);
          WHILE iter.next(fn) DO
            IF prefix # NIL THEN
              fn := prefix & "/" & fn;
            END;
            IF FSUtils.IsDir(fn) THEN
              FL(fn, levels - 1, fn, res);
            END;
            res.addhi(fn);
          END;
        EXCEPT
          OSError.E(e) => Msg.Error("cannot list dir " &
            dir & ": " & System.AtomListToText(e));
        END;
      END;
    END FL;

  BEGIN (* FileList *)
    FL(dir, levels, prefix, res);
    RETURN res;
  END FileList;
---------------------------------------------------------------------------
PROCEDURE ExpandWildcards(token : TEXT) : TextSeq.T =
  VAR
    res     := NEW(TextSeq.T).init();
    levels  := TextUtils.Split(token, "/").size();
    files   :  TextSeq.T;
    first   :  CHAR;
    options := Glob.MatchOptions{Glob.MatchOption.Pathname,
                                 Glob.MatchOption.Period};
    rest    :  TEXT;
    updirs  :  TEXT := NIL;
  BEGIN
    IF token = NIL OR Text.Empty(token) THEN
      res.addhi("");
      RETURN res;
    END;
    first := Text.GetChar(token, 0);
    IF first = QuoteChar THEN
      res.addhi(Text.Sub(token, 1, LAST(INTEGER)));
      RETURN res;
    END;
    IF first = '/' THEN
      files := FileList("/", levels - 1, "");
    ELSE
      rest := token;
      (* cut off leading `./' sequences *)
      WHILE TextUtils.Pos(rest, "./") = 0 DO
        rest := Text.Sub(rest, 2, LAST(INTEGER));
        DEC(levels);
        IF updirs = NIL THEN
          updirs := ".";
        ELSE
          updirs := updirs & "/.";
        END;
      END;
      IF TextUtils.Pos(rest, "../") = 0 THEN
        (* rest starts with ../ *)
        REPEAT
          rest := Text.Sub(rest, 3, LAST(INTEGER));
          IF updirs = NIL THEN
            updirs := "..";
          ELSE
            updirs := updirs & "/..";
          END;
          DEC(levels);
        UNTIL TextUtils.Pos(rest, "../") # 0;
        files := FileList(updirs, levels, updirs);
      ELSE
        files := FileList(".", levels, updirs);
      END;
    END;
    FOR i := 0 TO files.size() - 1 DO
      WITH fn = files.get(i) DO
        TRY
          IF Glob.Match(token, fn, options) THEN
            res.addhi(fn);
          END;
        EXCEPT
          RegEx.Error(e) => Msg.Error("error in globbing expression: " & e);
        END;
      END;
    END;
    RETURN res;
  END ExpandWildcards;
---------------------------------------------------------------------------
PROCEDURE ContainsWildcards(token : TEXT) : BOOLEAN =
  VAR index : CARDINAL := 0;
  BEGIN
    RETURN TextEx.FindCharSet(token, ASCII.Set{'*', '?'}, index);
  END ContainsWildcards;
---------------------------------------------------------------------------
PROCEDURE DoWildcardExpansion(token : TEXT; VAR args : TextSeq.T) =
  VAR
    first   :  CHAR;
  BEGIN
    IF ContainsWildcards(token) THEN
      Msg.D("expand wildcards");
      WITH res = ExpandWildcards(token) DO
        FOR i := 0 TO res.size() - 1 DO
          WITH arg = res.get(i) DO
            Msg.D(" expanded to `" & arg & "'");
            args.addhi(arg);
          END;
        END;
        IF res.size() = 0 THEN
          args.addhi(token);
        END;
      END;
    ELSE
      Msg.D("no wildcards");
      IF token = NIL OR Text.Empty(token) THEN
        args.addhi("");
      ELSE
        first := Text.GetChar(token, 0);
        IF first = QuoteChar THEN
          WITH arg = Text.Sub(token, 1, LAST(INTEGER)) DO
            args.addhi(arg);
            Msg.D(" reduced to `" & arg & "'");
          END;
        ELSE
          Msg.D(" no change `" & token & "'");
          args.addhi(token);
        END;
      END;
    END;
  END DoWildcardExpansion;
---------------------------------------------------------------------------
VAR
  token : TEXT;
  pgm   : TEXT := NIL;
  args  : TextSeq.T := NIL;
  done  : BOOLEAN;
  ret   : INTEGER := 0;
  i     : INTEGER;
  env   : ProcessEnv.T := ProcessEnv.Current();
  homeDir := "/";
BEGIN
  Msg.tFlag := TRUE;
  token := Env.Get("HOME");
  IF token # NIL THEN
    homeDir := token;
  END;
  rsrcPath :=
      Rsrc.BuildPath("/usr/contrib/lib/compact",
                     "/usr/local/lib/compact",
                     "/opt/compact",
                     MiniShellBundle.Get());
  (* rsrcPath defined *)

  ProcessParameters();

  IF nTargets > 0 THEN
    (* execute only the given command line *)
    i := 0;
    WHILE i < nTargets DO
      pgm := targets.get(i);
      INC(i);
      args := NEW(TextSeq.T).init(10);
      done := FALSE;
      WHILE NOT done AND i < nTargets DO
        token := targets.get(i);
        INC(i);
        Msg.D(" token = `" & token & "'");
        IF Text.Equal(token, ";") THEN
          ret := Exec(pgm, args, env);
          done := TRUE;
        ELSIF Text.Equal(token, "&&") THEN
          ret := Exec(pgm, args, env);
          IF ret # 0 THEN
            Process.Exit(ret);
          END;
          done := TRUE;
        ELSIF Text.Equal(token, "||") THEN
          ret := Exec(pgm, args, env);
          IF ret = 0 THEN
            Process.Exit(ret);
          END;
          done := TRUE;
        ELSE
          DoWildcardExpansion(token, args);
        END;
      END;
    END;
  ELSE
    (* read from stdin until EOF *)
    TRY
      WHILE NOT Rd.EOF(Stdio.stdin) DO
        pgm := TextReadingUtils.GetToken(Stdio.stdin);
        args := NEW(TextSeq.T).init(10);
        done := FALSE;
        WHILE NOT done AND NOT Rd.EOF(Stdio.stdin) DO
          token := TextReadingUtils.GetTokenOrString(Stdio.stdin);
          Msg.D(" token = " & token);
          IF Text.Equal(token, ";") THEN
            ret := Exec(pgm, args, env);
            done := TRUE;
          ELSIF Text.Equal(token, "&&") THEN
            ret := Exec(pgm, args, env);
            IF ret # 0 THEN
              Process.Exit(ret);
            END;
            done := TRUE;
          ELSIF Text.Equal(token, "||") THEN
            ret := Exec(pgm, args, env);
            IF ret = 0 THEN
              Process.Exit(ret);
            END;
            done := TRUE;
          ELSE
            DoWildcardExpansion(token, args);
          END;
        END;
      END;
    EXCEPT
      Rd.Failure,
      Rd.EndOfFile => (* skip *)
    END;
  END;
  IF NOT done AND pgm # NIL AND args # NIL THEN
    ret := Exec(pgm, args, env);
  END;
  Process.Exit(ret);
END M3MiniShell.

interface ASCII is in:


interface Glob is in:


interface TextUtils is in:


interface RegEx is in:


interface Creation is in: