m3tk-misc/src/M3Args.m3


*************************************************************************
                      Copyright (C) Olivetti 1989                        
                          All Rights reserved                            
                                                                         
 Use and copy of this software and preparation of derivative works based 
 upon this software are permitted to any person, provided this same      
 copyright notice and the following Olivetti warranty disclaimer are      
 included in any copy of the software or any modification thereof or     
 derivative work therefrom made by any person.                           
                                                                         
 This software is made available AS IS and Olivetti disclaims all        
 warranties with respect to this software, whether expressed or implied  
 under any law, including all implied warranties of merchantibility and  
 fitness for any purpose. In no event shall Olivetti be liable for any   
 damages whatsoever resulting from loss of use, data or profits or       
 otherwise arising out of or in connection with the use or performance   
 of this software.                                                       
*************************************************************************

MODULE M3Args EXPORTS M3Args, M3ArgsCL;
This implementation uses Args and also assumes that it is working from a command line decoding. It amalgamates all the keywords into a single template, and does a single decode. M3ArgsCL.Reset can be called to redo the decoding.

IMPORT Text, TextExtras, Args, Err, ASCII, RefList, RefListSort;

TYPE
  ArgState = OBJECT
    name: TEXT;
    nameAndKind: TEXT;
    usage: TEXT;
    shared: BOOLEAN;
  END;

  FlagArgState = ArgState BRANDED OBJECT END;
  StringArgState = ArgState BRANDED OBJECT END;
  StringListArgState = ArgState BRANDED OBJECT END;
  PrefixArgState = ArgState BRANDED OBJECT END;

REVEAL
  T = BRANDED REF RECORD
    toolName, toolDescription, version: TEXT;
    master: BOOLEAN;
    argList: RefList.T;
  END;

VAR
  toolList_g: RefList.T;   (* list of all registered tools *)
  master_g: T := NIL;      (* current master *)
  args_g: RECORD
    init: BOOLEAN;    (* have we done Args.NewTemplate/Args.Decode? *)
    cl: REF Args.T;   (* Command line *)
    template: Args.Template;
    handle: Args.Handle;
    keyString: TEXT;
    help, identify: BOOLEAN;
  END;

CONST
  IndentLength = 24;
  Indent = "                         ";

EXCEPTION
  DuplicateArg; (* no non-shared duplicates allowed *)
  ClashingShortform;

PROCEDURE New(toolName, toolDescription, version: TEXT;
    master := FALSE): T RAISES {} =
  VAR
    t: T;
  BEGIN
    t := NEW(T);
    t.toolName := toolName;
    t.toolDescription := toolDescription;
    t.version := version;
    t.master := master;
    t.argList := NIL;
    IF master THEN
      toolList_g := RefList.Cons(t, toolList_g); master_g := t;
    ELSE toolList_g := RefList.AppendD(toolList_g, RefList.List1(t));
    END;
    RETURN t;
  END New;

PROCEDURE SetMaster(t: T): T RAISES {}=
  PROCEDURE Compare(e1: REFANY; <*UNUSED*> e2: REFANY): [-1..1]=
    BEGIN
      IF e1 = t THEN RETURN -1 ELSE RETURN 1 END;
    END Compare;

  VAR r := master_g;
  BEGIN
    toolList_g := RefListSort.SortD(toolList_g, Compare);
    master_g := t;
    RETURN r;
  END SetMaster;

PROCEDURE Usage(t: T) RAISES {} =
  VAR
    al: RefList.T;
    a: ArgState;
    l: INTEGER;
  BEGIN
    Err.Print(t.toolDescription, Err.Severity.Comment);
    al := t.argList;
    WHILE al # NIL DO
      a := al.head;
      Err.Print("-", Err.Severity.Continue, FALSE);
      Err.Print(a.nameAndKind, Err.Severity.Continue, FALSE);
      l := Text.Length(a.nameAndKind);
      REPEAT Err.Print(" ", Err.Severity.Continue, FALSE); INC(l)
      UNTIL l >= IndentLength;
      Err.Print(a.usage, Err.Severity.Continue, FALSE);
      Err.Print("", Err.Severity.Continue);
      al := al.tail;
    END; (* while *)
    Err.Print("", Err.Severity.Continue)
  END Usage;

PROCEDURE RegisterFlag(
    t: T;
    argName: TEXT;
    usage: TEXT;
    shared := FALSE) RAISES {} =
  BEGIN
    RegisterArg(NEW(FlagArgState), t, argName, usage, Opt.Optional, shared);
  END RegisterFlag;

PROCEDURE RegisterString(
    t: T;
    argName: TEXT;
    usage: TEXT;
    opt: Opt := Opt.Optional;
    shared := FALSE)
    RAISES {} =
  BEGIN
    RegisterArg(NEW(StringArgState), t, argName, usage, opt, shared);
  END RegisterString;

PROCEDURE RegisterStringList(
    t: T;
    argName: TEXT;
    usage: TEXT;
    opt: Opt := Opt.Optional;
    shared := FALSE)
    RAISES {} =
  BEGIN
    RegisterArg(NEW(StringListArgState), t, argName, usage, opt, shared);
  END RegisterStringList;

PROCEDURE RegisterPrefix(
    t:T;
    argName: TEXT;
    usage: TEXT;
    opt: Opt := Opt.Optional;
    shared := FALSE) RAISES {}=
  BEGIN
    RegisterArg(NEW(PrefixArgState), t, argName, usage, opt, shared);
  END RegisterPrefix;

PROCEDURE RegisterArg(a: ArgState; t: T; argName: TEXT;
    usage: TEXT;
    opt: Opt;
    shared := FALSE) RAISES {} =
  VAR
    shortForm, nameAndKind: TEXT;
  BEGIN
    a.name := argName;
    a.usage := ExpandNL(usage);
    nameAndKind := ArgsArgName(argName, a, opt, shortForm);
    a.nameAndKind:= nameAndKind;
    a.shared := shared;
    IF IsDuplicated(argName, shortForm, ISTYPE(a, PrefixArgState), shared) THEN
      IF NOT shared THEN
        <*FATAL DuplicateArg*> BEGIN RAISE DuplicateArg END;
      END;
    ELSE
      args_g.keyString := args_g.keyString & a.nameAndKind;
      args_g.keyString := args_g.keyString & " ";
    END;
    t.argList:= RefList.AppendD(t.argList, RefList.List1(a));
  END RegisterArg;

<*INLINE*> PROCEDURE ExpandNL(t: TEXT): TEXT RAISES {}=
  VAR
    index: CARDINAL := 0;
  BEGIN
    LOOP
      IF TextExtras.FindChar(t, '\n', index) THEN
      	t := TextExtras.Extract(t, 0, index+1) & Indent &
	     TextExtras.Extract(t, index+1, Text.Length(t));
        INC(index);
      ELSE
      	EXIT
      END; (* if *)
    END; (* loop *)
    RETURN t;
  END ExpandNL;

PROCEDURE Help(t: T; preamble := TRUE) RAISES {} =
  BEGIN
    Setup(t);
    IF preamble THEN HelpPreamble(t); END;
    Usage(t);
  END Help;

PROCEDURE HelpPreamble(t: T; ) RAISES {} =
  BEGIN
    Setup(t);
    Err.Print(
  "Keywords - \'/f\' boolean flag. \'/l\' space separated list of values.\n" &
  "           \'/1\' single value. \'/r\' means mandatory.\n" &
  "           \'/p\' means positional argument (keyword can be omitted).\n" &
  "Capitalisation (and \'=short\') indicates alternative shortened form.\n",
      Err.Severity.Continue);
  END HelpPreamble;

PROCEDURE CheckHelp(display := TRUE): BOOLEAN RAISES {} =
  VAR
    tl: RefList.T; t: T;
  BEGIN
    Setup(NIL);
    IF args_g.help OR args_g.identify THEN
      IF display THEN
        tl := toolList_g;
        IF tl # NIL AND args_g.help THEN HelpPreamble(t) END;
        WHILE tl # NIL DO
          t := tl.head;
          SetName(t);
          IF args_g.identify THEN
            Err.Print("Version " & t.version, Err.Severity.Comment);
          END;
          IF args_g.help THEN Usage(t); END;
          tl := tl.tail;
        END; (* while *)
      END;
      RETURN TRUE
    ELSE
      RETURN FALSE
    END;
  END CheckHelp;

PROCEDURE Setup(t: T) RAISES {} =
  BEGIN
    SetName(t);
    ArgsInit();
  END Setup;

PROCEDURE ArgsInit() RAISES {} =
  BEGIN
    IF NOT args_g.init THEN
      args_g.cl := Args.CommandLine();
      <*FATAL Args.BadTemplate*>
      BEGIN
        args_g.template := Args.NewTemplate(args_g.keyString);
      END;
      ArgsDecode();
    END;
  END ArgsInit;

PROCEDURE ArgsDecode() RAISES {}=
  BEGIN
    Args.Standard(args_g.cl^, args_g.help, args_g.identify);
    args_g.handle := Args.Decode(args_g.template, args_g.cl^, TRUE);
    args_g.init := TRUE;
  END ArgsDecode;

PROCEDURE Reset(cl: REF Args.T) RAISES {}=
  BEGIN
    args_g.cl := cl;
    ArgsDecode();
  END Reset;

PROCEDURE SetName(t: T) RAISES {} =
  VAR name: TEXT;
  BEGIN
    IF t = NIL THEN name := "m3args" ELSE name := t.toolName; END;
    EVAL Err.SetProgramName(name);
  END SetName;

PROCEDURE Find(t: T): BOOLEAN RAISES {} =
  BEGIN
    Setup(t);
    IF Args.Good(args_g.handle) THEN
      RETURN TRUE;
    ELSE
      Err.Print("Bad args - use \'-help\' if in need of help",
          Err.Severity.Warning);
      RETURN FALSE;
    END;
  END Find;

PROCEDURE ArgsArgName(s: TEXT;
    a: ArgState; opt: Opt;
    VAR (*out*) shortForm: TEXT): TEXT RAISES {} =
  VAR
    ns: TEXT;
    l, index, lindex: CARDINAL;
    shortFormArray: REF ARRAY OF CHAR;
    ch: CHAR;
  BEGIN
    l := Text.Length(s);
    shortFormArray := NEW(REF ARRAY OF CHAR, l);
    index := 0; lindex := 0;
    WHILE index < l DO
      ch := Text.GetChar(s, index);
      IF ch IN ASCII.Uppers THEN
        shortFormArray[lindex] := ASCII.Lower[ch];
        INC(lindex);
      END;
      INC(index);
    END; (* while *)
    shortForm := Text.FromChars(SUBARRAY(shortFormArray^, 0, lindex));
    (* check and ignore if short form = long form *)
    IF TextExtras.CIEqual(s, shortForm) THEN
      lindex := 0;
    END; (* if *)
    IF lindex > 0 THEN
      ns := s & "=" & shortForm;
    ELSE
      ns := s;
    END; (* if *)
    TYPECASE a OF <*NOWARN*>
    | FlagArgState =>
        ns := Text.Cat(ns, "/f");
    | StringListArgState =>
        ns := Text.Cat(ns, "/l");
    | StringArgState =>
        ns := ns & "/1";
        IF opt = Opt.Required THEN
          ns := Text.Cat(ns, "/r");
        END;
    | PrefixArgState =>
        ns := ns & "/l/x"
    END;
    IF opt = Opt.Positional THEN ns := Text.Cat(ns, "/p") END;
    RETURN ns;
  END ArgsArgName;

PROCEDURE GetFlag(<*UNUSED*> t: T; s: TEXT): BOOLEAN RAISES {} =
  BEGIN
    TRY
      RETURN Args.Flag(args_g.handle, s)
    EXCEPT
    | Args.BadEnquiry => <*ASSERT FALSE*>
    END;
  END GetFlag;

PROCEDURE GetString(<*UNUSED*> t: T; s: TEXT): TEXT
    RAISES {} =
  BEGIN
    TRY
      RETURN Args.Single(args_g.handle, s);
    EXCEPT
    | Args.BadEnquiry => <*ASSERT FALSE*>
    END;
  END GetString;

PROCEDURE GetStringList(<*UNUSED*> t: T; s: TEXT): REF ARRAY OF TEXT
    RAISES {} =
  BEGIN
    TRY
      RETURN Args.Value(args_g.handle, s);
    EXCEPT
    | Args.BadEnquiry => <*ASSERT FALSE*>
    END;
  END GetStringList;

PROCEDURE GetPrefix(<*UNUSED*> t: T; s: TEXT): REF ARRAY OF TEXT
    RAISES {} =
  BEGIN
    TRY
      RETURN Args.Value(args_g.handle, s);
    EXCEPT
    | Args.BadEnquiry => <*ASSERT FALSE*>
    END;
  END GetPrefix;

PROCEDURE SetFlag(<*UNUSED*> t: T; s: TEXT; f: BOOLEAN) RAISES {} =
  VAR v: REF ARRAY OF TEXT;
  BEGIN
    IF f THEN v := NEW(REF ARRAY OF TEXT, 0) ELSE v := NIL END;
    TRY
      Args.Bind(args_g.handle, s, v, TRUE);
    EXCEPT
    | Args.BadBinding => <*ASSERT FALSE*>
    END;
  END SetFlag;

PROCEDURE SetString(<*UNUSED*> t: T; s: TEXT; val: TEXT)
    RAISES {} =
  VAR v: REF ARRAY OF TEXT;
  BEGIN
    v := NEW(REF ARRAY OF TEXT, 1); v[0] := val;
    TRY
      Args.Bind(args_g.handle, s, v, TRUE);
    EXCEPT
    | Args.BadBinding => <*ASSERT FALSE*>
    END;
  END SetString;

PROCEDURE SetStringList(<*UNUSED*> t: T; s: TEXT; sl: REF ARRAY OF TEXT)
    RAISES {} =
  BEGIN
    TRY
      Args.Bind(args_g.handle, s, sl, TRUE);
    EXCEPT
    | Args.BadBinding => <*ASSERT FALSE*>
    END;
  END SetStringList;

PROCEDURE SetPrefix(<*UNUSED*> t: T; s: TEXT; sl: REF ARRAY OF TEXT)
    RAISES {} =
  BEGIN
    TRY
      Args.Bind(args_g.handle, s, sl, TRUE);
    EXCEPT
    | Args.BadBinding => <*ASSERT FALSE*>
    END;
  END SetPrefix;

PROCEDURE SetStringAsList(<*UNUSED*> t: T; s: TEXT; sl: TEXT) RAISES {} =
  VAR
    start, end, l: CARDINAL;
    count := 0;
    v: REF ARRAY OF TEXT;
  BEGIN
    start := 0; end := 0;
    l := Text.Length(sl);
    LOOP
      IF TextExtras.FindCharSet(sl, ASCII.Set{' ', ','}, end) THEN END;
      IF end >= l THEN EXIT END;
      start := end+1; end := start;
      INC(count);
    END;
    v := NEW(REF ARRAY OF TEXT, count);
    start := 0; end := 0; count := 0;
    LOOP
      IF TextExtras.FindCharSet(sl, ASCII.Set{' ', ','}, end) THEN END;
      v[count] := TextExtras.Extract(sl, start, end);
      INC(count);
      IF end >= l THEN EXIT END;
      start := end+1; end := start;
    END;
    TRY
      Args.Bind(args_g.handle, s, v, TRUE);
    EXCEPT
    | Args.BadBinding => <*ASSERT FALSE*>
    END;
  END SetStringAsList;

PROCEDURE IsDuplicated(argName, shortForm: TEXT;
                       isPrefix: BOOLEAN; shared: BOOLEAN): BOOLEAN=
  PROCEDURE IsPrefixOf(t, pre: TEXT): BOOLEAN=
    VAR
      index: CARDINAL := 0;
    BEGIN
      RETURN TextExtras.FindSub(t, pre, index) AND index = 0
    END IsPrefixOf;

  VAR
    tl, al: RefList.T; t: T;
    a: ArgState;
    hasShort: BOOLEAN;
    result: BOOLEAN := FALSE;
  BEGIN
    hasShort := NOT Text.Equal(shortForm, "");
    tl := toolList_g;
    WHILE tl # NIL DO
      t := tl.head;
      al := t.argList;
      WHILE al # NIL DO
        a := al.head;
        (* both the full name and the short form must be unique *)
        IF isPrefix THEN
          IF IsPrefixOf(a.name, argName) OR
            (hasShort AND IsPrefixOf(ShortFormOf(a.nameAndKind), argName)) THEN
            <*FATAL ClashingShortform*> BEGIN RAISE ClashingShortform; END;
          END; (* if *)
        ELSIF TextExtras.CIEqual(argName, a.name) THEN
          IF NOT(shared AND a.shared) THEN result := TRUE; END;
        ELSIF (hasShort AND (TextExtras.CIEqual(shortForm,
                            ShortFormOf(a.nameAndKind)))) THEN
            <*FATAL ClashingShortform*> BEGIN RAISE ClashingShortform; END;
        END; (* if *)
        al := al.tail;
      END; (* while *)
      tl := tl.tail;
    END;
    RETURN result;
  END IsDuplicated;

PROCEDURE ShortFormOf(nameAndKind: TEXT): TEXT RAISES {} =
  VAR
    index, sindex: CARDINAL;
    nameAndShort: TEXT;
  BEGIN
    index := 0;
    IF TextExtras.FindChar(nameAndKind, '=', index) THEN
      sindex := index+1;
      IF TextExtras.FindChar(nameAndKind, '/', index) THEN
        nameAndShort := TextExtras.Extract(nameAndKind, sindex, index);
        RETURN nameAndShort;
      END;
    END;
    RETURN "";
  END ShortFormOf;

BEGIN
  args_g.init := FALSE; args_g.keyString := "";
END M3Args.