CommandToolParseImpl.mesa
Copyright © 1984, 1985, 1986 by Xerox Corporation. All rights reserved.
Swinehart, June 25, 1984 2:48:53 pm PDT
Russ Atkinson (RRA) April 2, 1986 4:35:33 pm PST
DIRECTORY
Atom USING [MakeAtom],
Commander USING [Handle, GetProperty],
CommandTool,
Convert USING [Error, IntFromRope],
FileNames USING [Directory, ResolveRelativePath],
FS USING [AccessOptions, EnumerateForNames, Error, NameProc, StreamOpen],
IO USING [PutChar, RopeFromROS, ROS, STREAM],
List USING [PutAssoc],
ProcessProps USING [GetPropList],
Rope USING [Cat, Concat, Equal, Fetch, Find, Flatten, IsEmpty, Length, Replace, ROPE, Size, SkipTo, Substr];
CommandToolParseImpl:
CEDAR
PROGRAM
IMPORTS Atom, Commander, CommandTool, Convert, FileNames, FS, IO, List, ProcessProps, Rope
EXPORTS CommandTool = {
OPEN CommandTool;
Handle: TYPE = Commander.Handle;
ROPE: TYPE = Rope.ROPE;
STREAM: TYPE = IO.STREAM;
Failed: PUBLIC ERROR [errorMsg: ROPE] = CODE;
ParseRecord:
TYPE =
RECORD [
argumentVector: CommandTool.ArgumentVector ←
NIL,
The arguments to the command. argumentVector.argc is the number of arguments, argumentVector[0] is the command
argumentPointer:
INT ← -1,
position of last fetched argument (initially none fetched)
commandLine:
ROPE
used to check if the command line has been parsed already for the current command
];
GetParseRecord:
PROC [cmd: Handle]
RETURNS [pr:
REF ParseRecord] = {
pr ← NARROW[CommandTool.GetProp[cmd, $ArgumentVector]];
IF pr =
NIL
OR (pr.argumentVector.argc > 0
AND pr.commandLine # cmd.commandLine)
THEN {
pr ← NEW[ParseRecord];
pr.argumentVector ← Parse[cmd ! Failed => CONTINUE];
IF pr.argumentVector = NIL THEN pr.argumentVector ← NEW[CommandTool.ArgHandleObject[0]];
pr.argumentPointer ← 0;
pr.commandLine ← cmd.commandLine;
cmd.propertyList ← List.PutAssoc[key: $ArgumentVector, val: pr, aList: cmd.propertyList];
};
};
NumArgs:
PUBLIC
PROC [cmd: Handle]
RETURNS [
INT] = {
pp: REF ParseRecord ← GetParseRecord[cmd];
RETURN[pp.argumentVector.argc];
};
NextArgument:
PUBLIC
PROC [cmd: Handle]
RETURNS [
ROPE] = {
pp: REF ParseRecord ← GetParseRecord[cmd];
n: NAT ← pp.argumentPointer + 1;
IF pp = NIL OR n >= pp.argumentVector.argc THEN RETURN[NIL];
pp.argumentPointer ← n;
RETURN[pp.argumentVector[n]];
};
ArgN:
PUBLIC
PROC [cmd: Handle, n:
INT]
RETURNS [
ROPE] = {
pp: REF ParseRecord ← GetParseRecord[cmd];
IF pp = NIL OR n < 0 OR n >= pp.argumentVector.argc THEN RETURN[NIL];
pp.argumentPointer ← n;
RETURN[pp.argumentVector[n]];
};
RegistrationDirectory:
PUBLIC
PROC [cmd: Handle]
RETURNS [dir:
ROPE] = {
dir ← FileNames.Directory[cmd.command];
IF dir.IsEmpty[] THEN RETURN["///"];
};
Parse:
PUBLIC
PROC [cmd: Handle, starExpand:
BOOL ←
FALSE, switchChar:
CHAR ← '-]
RETURNS [argv: ArgumentVector] = {
IF starExpand THEN StarExpansion[cmd];
argv ← InnerParse[cmd, switchChar];
IF argv # NIL THEN argv[0] ← cmd.command;
};
AmpersandSubstitution:
PUBLIC
PROC [cmd: Handle] = {
AmpersandProc:
PROC [thingName:
ROPE]
RETURNS [
ROPE] = {
arg: ATOM ← Atom.MakeAtom[thingName];
value: REF ANY;
value ← Commander.GetProperty[key: arg, aList: ProcessProps.GetPropList[]];
IF value = NIL OR NOT ISTYPE[value, ROPE] THEN value ← Commander.GetProperty[key: arg, aList: cmd.propertyList];
IF NOT ISTYPE[value, ROPE] THEN RETURN[Rope.Concat["&", thingName]];
RETURN[NARROW[value, ROPE]];
};
Letters:
PROC [c:
CHAR]
RETURNS [keep:
BOOL] = {
RETURN [c IN ['a..'z] OR c IN ['A..'Z] OR c IN ['0..'9]];
};
cmd.commandLine ← LookForThings[line: cmd.commandLine, thingChar: '&, charPredicate: Letters, proc: AmpersandProc];
};
DollarSubstitution:
PUBLIC
PROC [cmd: Handle] = {
Letters:
PROC [c:
CHAR]
RETURNS [keep:
BOOL] = {
RETURN [c IN ['a..'z] OR c IN ['A..'Z] OR c IN ['0..'9]];
};
WITH CommandTool.GetProp[cmd, $CommandFileArguments]
SELECT
FROM
commandersArguments: ArgumentVector => {
highest: INT ← 0;
DollarProc:
PROC [thingName:
ROPE]
RETURNS [value:
ROPE ←
NIL] = {
IF Rope.Equal[thingName, "Rest",
FALSE]
THEN {
FOR i:
INT
IN (highest..commandersArguments.argc)
DO
value ← Rope.Cat[value, " ", commandersArguments[i]];
ENDLOOP;
RETURN[value];
}
ELSE {
argument:
INT ← Convert.IntFromRope[thingName
! Convert.Error => GO TO nasty
]+1;
IF argument < commandersArguments.argc
THEN {
IF argument > highest THEN highest ← argument;
RETURN [commandersArguments[argument]];
};
EXITS nasty => RETURN [Rope.Concat["$", thingName]];
};
};
cmd.commandLine ← LookForThings[line: cmd.commandLine, thingChar: '$, charPredicate: Letters, proc: DollarProc];
};
ENDCASE;
};
StarExpansion:
PUBLIC
PROC [cmd: Handle] = {
token: ROPE;
listOfTokens: LIST OF ROPE ← NIL;
uecpArgv: ArgumentVector;
IF cmd.commandLine.Find["*"] = -1 THEN RETURN;
uecpArgv ← InnerParse[cmd];
FOR i:
NAT
IN [1..uecpArgv.argc)
DO
err: ROPE ← NIL;
token ← uecpArgv[i];
IF Rope.Find[token, "*"] # -1
THEN {
ENABLE FS.Error => IF error.group # bug THEN {err ← error.explanation; GO TO oops};
ConsProc:
FS.NameProc = {
listOfTokens ← CONS[fullFName, listOfTokens]; -- on front of list
RETURN[TRUE];
};
FS.EnumerateForNames[pattern: FileNames.ResolveRelativePath[token], proc: ConsProc];
EXITS oops => ERROR CommandTool.Failed[err];
}
ELSE listOfTokens ← CONS[token, listOfTokens]; -- on front of list
ENDLOOP;
Now the tokens must be glued back together again, but the listOfTokens is in reverse order
cmd.commandLine ← "\n";
WHILE listOfTokens #
NIL
DO
cmd.commandLine ← Rope.Cat[" ", listOfTokens.first, cmd.commandLine];
listOfTokens ← listOfTokens.rest;
ENDLOOP;
cmd.commandLine ← Rope.Flatten[cmd.commandLine];
};
IORedirection:
PUBLIC
PROC [cmd: Handle]
RETURNS [inRedirected:
BOOL ←
FALSE, outRedirected:
BOOL ←
FALSE] = {
token: ROPE;
listOfTokens: LIST OF ROPE ← NIL;
uecpArgv: ArgumentVector;
i: NAT;
{
IF cmd.commandLine.SkipTo[pos: 0, skip: "<>"] = cmd.commandLine.Length[] THEN RETURN;
uecpArgv ← InnerParse[cmd];
IF uecpArgv = NIL THEN ERROR Failed["Command line parse failure"];
cmd.commandLine ← NIL;
i ← 1;
DO
IF i = uecpArgv.argc THEN EXIT;
token ← uecpArgv[i];
i ← i + 1;
SELECT
TRUE
FROM
Rope.Equal[token, "<"] => {
IF i = uecpArgv.argc THEN GO TO FailSyntax;
IF inRedirected THEN GO TO FailDouble;
cmd.in ← OpenRedirect[uecpArgv[i], $read];
i ← i + 1;
inRedirected ← TRUE;
LOOP;
};
Rope.Equal[token, ">"] => {
IF i = uecpArgv.argc THEN GO TO FailSyntax;
IF outRedirected THEN GO TO FailDouble;
cmd.out ← OpenRedirect[uecpArgv[i], $create];
i ← i + 1;
outRedirected ← TRUE;
LOOP;
};
Rope.Equal[token, ">>"] => {
IF i = uecpArgv.argc THEN GO TO FailSyntax;
IF outRedirected THEN GO TO FailDouble;
cmd.out ← OpenRedirect[uecpArgv[i], $append];
i ← i + 1;
outRedirected ← TRUE;
LOOP;
};
ENDCASE => cmd.commandLine ← Rope.Cat[cmd.commandLine, " ", token];
ENDLOOP;
cmd.commandLine ← Rope.Flatten[Rope.Concat[cmd.commandLine, "\n"]];
EXITS
FailDouble => ERROR Failed["I/O redirected twice"];
FailSyntax => ERROR Failed["redirection without filename"];
};
};
ParseToList:
PUBLIC
PROC [cmd: Handle, starExpand:
BOOL ←
FALSE, switchChar:
CHAR ← '-]
RETURNS [list:
LIST
OF
ROPE, length:
NAT ← 0] = {
IF starExpand THEN StarExpansion[cmd];
[list, length] ← InnerParseToList[cmd, switchChar];
length ← length - 1;
list ← list.rest;
};
InnerParseToList:
PROC [cmd: Handle, switchChar:
CHAR ← '-]
RETURNS [list:
LIST
OF
ROPE, length:
NAT ← 1] = {
cpd: REF CPData ← NEW[CPData];
tail: LIST OF ROPE ← list ← LIST[cmd.command];
cpd.swChar ← switchChar;
cpd.rope ← cmd.commandLine;
DO
r: ROPE ← IGet[cpd ! UnmatchedQuote => GOTO Lose];
IF r = NIL THEN EXIT;
length ← length + 1;
tail ← tail.rest ← LIST[r];
ENDLOOP;
EXITS Lose => Failed["Misatched quotes"];
};
LookForThings:
PROC [line:
ROPE, thingChar:
CHAR, charPredicate:
PROC [c:
CHAR]
RETURNS [keep:
BOOL], proc:
PROC [thingName:
ROPE]
RETURNS [
ROPE]]
RETURNS [
ROPE] = {
state: {outside, insideQuotes, insideThing} ← outside;
c: CHAR;
i: INT ← 0;
thingPosition: INT;
WHILE i < line.Length[]
DO
c ← line.Fetch[i];
SELECT state
FROM
outside => {
SELECT c
FROM
'" => {
state ← insideQuotes;
};
thingChar => {
state ← insideThing;
thingPosition ← i;
};
ENDCASE;
};
insideQuotes => {
IF c = '"
THEN {
IF line.Length[] > (i + 1) AND line.Fetch[i+1] = '" THEN i ← i + 1
ELSE state ← outside;
};
};
insideThing => {
IF
NOT charPredicate[c]
THEN {
thingName: ROPE ← Rope.Substr[base: line, start: thingPosition + 1, len: i - thingPosition - 1];
thingValue: ROPE ← proc[thingName: thingName];
line ← line.Replace[start: thingPosition, len: IF c = thingChar THEN thingName.Length[] + 2 ELSE thingName.Length[] + 1, with: thingValue];
i ← thingPosition + thingValue.Length[];
state ← outside;
};
};
ENDCASE => ERROR;
i ← i + 1;
ENDLOOP;
IF state = insideQuotes THEN ERROR Failed["Mismatched quotes"];
RETURN[line];
};
OpenRedirect:
PROC [fileName:
ROPE, accessOptions:
FS.AccessOptions]
RETURNS [stream:
STREAM ←
NIL] = {
errorMsg: ROPE;
fileName ← FileNames.ResolveRelativePath[fileName];
stream ←
FS.StreamOpen[fileName: fileName, accessOptions: accessOptions, keep: 2
! FS.Error => IF error.group = user THEN { errorMsg ← error.explanation; CONTINUE }];
IF stream =
NIL
THEN
ERROR Failed[Rope.Concat["Can't redirect I/O: ", errorMsg]];
};
UnmatchedQuote: ERROR = CODE;
CPData:
TYPE =
RECORD [
ch: CHAR ← ' , -- first char of next token; initially blank
swChar: CHAR, -- switch character
ropeIndex: LONG INTEGER ← 0, -- index into Rope command line
rope: ROPE ← NIL, -- command line passed from client
done: BOOL ← FALSE -- used up all of command line
];
InnerParse:
PROC [cmd: Handle, switchChar:
CHAR ← '-]
RETURNS [argv: ArgumentVector ←
NIL] = {
list: LIST OF ROPE ← NIL;
nArgs: NAT ← 0;
[list, nArgs] ← InnerParseToList[cmd: cmd, switchChar: switchChar];
argv ← NEW[ArgHandleObject[nArgs]];
FOR i:
NAT
IN [0..nArgs)
DO
argv[i] ← list.first;
list ← list.rest;
ENDLOOP;
};
IGet:
PROC [cpd:
REF CPData]
RETURNS [result:
ROPE] = {
Returns next token. This scanner
(1) ignores leading blanks,
(2) returns a quoted token intact (excluding the quotes),
(3) returns an unquoted token delimited by a blank (not included) or switchchar (included in next token), the default switchchar is '-
(4) returns NIL at CR or end of file.
(5) if the switchchar is '- then it must be preceded by a blank
workstr: STREAM ← NIL;
chlook:
CHAR;
delimiter for token: either blank, meaning blank
or switchchar (usually '-),
or '", or '", meaning '" not followed by another '".
WHILE (cpd.ch = ' OR cpd.ch = ' ) AND GetCh[cpd] DO ENDLOOP;
IF cpd.done OR cpd.ch = '\n THEN RETURN[NIL];
chlook ← ' ;
IF cpd.ch = '"
THEN {
[] ← GetCh[cpd];
chlook ← '";
};
DO
IF cpd.done OR (chlook # '" AND cpd.ch = '\n) THEN GOTO EOL;
SELECT chlook
FROM
' , '\t =>
SELECT cpd.ch
FROM
' , '" => IF workstr # NIL THEN EXIT;
cpd.swChar => IF cpd.ch#'- AND workstr#NIL THEN EXIT;
ENDCASE;
'" =>
SELECT cpd.ch
FROM
'" => IF (NOT GetCh[cpd]) OR (cpd.ch # '") THEN EXIT;
ENDCASE;
ENDCASE => ERROR;
ch now contains a character to be included in the token
IF workstr = NIL THEN workstr ← IO.ROS[];
IO.PutChar[workstr, cpd.ch];
IF NOT GetCh[cpd] THEN GOTO EOL;
REPEAT
EOL => IF chlook = '" THEN ERROR UnmatchedQuote;
ENDLOOP;
IF workstr = NIL THEN RETURN [NIL];
RETURN [IO.RopeFromROS[workstr]];
};
GetCh:
PROC [cpd:
REF CPData]
RETURNS [
BOOL] = {
IF cpd.ropeIndex >= Rope.Size[cpd.rope] THEN GOTO EOS;
cpd.ch ← Rope.Fetch[cpd.rope, cpd.ropeIndex];
cpd.ropeIndex ← SUCC[cpd.ropeIndex];
RETURN [TRUE];
EXITS
EOS => {
cpd.done ← TRUE;
cpd.ch ← ' ;
RETURN [FALSE];
};
};
}.