CommandToolUtilitiesImpl.mesa
Copyright © 1984, 1985 by Xerox Corporation. All rights reserved.
Larry Stewart, January 16, 1984 7:57 pm
Swinehart, May 22, 1984 4:05:37 pm PDT
Russ Atkinson (RRA) May 23, 1985 3:27:43 pm PDT
DIRECTORY
Commander USING [CommandProc, CommandProcHandle, CommandProcObject, Enumerate, Handle, Lookup],
CommandTool,
CommandToolLookup USING [FindMatchingFiles],
Convert USING [Error, RopeFromLiteral],
FileNames USING [ConvertToSlashFormat, CurrentWorkingDirectory, Directory, FileWithSearchRules, ResolveRelativePath],
FS USING [Error],
IO USING [CreateStream, CreateStreamProcs, GetInfo, PutChar, PutF1, PutRope, RopeFromROS, ROS, STREAM],
IOUtils USING [closedStreamProcs],
List USING [AList, Append, Assoc, DotCons, Memb, PutAssoc, Remove],
Process USING [CheckForAbort],
ProcessProps USING [AddPropList, GetProp],
ReadEvalPrint USING [Handle],
Rope USING [Cat, Concat, Equal, Fetch, Length, Match, Replace, ROPE, Substr],
RopeFile USING [Create],
ViewerClasses USING [Viewer];
CommandToolUtilitiesImpl: CEDAR PROGRAM
IMPORTS Commander, CommandTool, CommandToolLookup, Convert, FileNames, FS, IO, IOUtils, List, Process, ProcessProps, Rope, RopeFile
EXPORTS CommandTool
SHARES IO
= BEGIN
ROPE: TYPE = Rope.ROPE;
commandFileProcData: Commander.CommandProcHandle ← NEW[Commander.CommandProcObject ← [CommandTool.CommandFile]];
GetViewer: PUBLIC PROC [cmd: Commander.Handle] RETURNS [ViewerClasses.Viewer] = {
rep: ReadEvalPrint.Handle ← NIL;
WHILE rep = NIL DO
rep ← NARROW[GetProp[cmd, $ReadEvalPrintHandle]];
IF rep # NIL AND rep.viewer # NIL THEN RETURN[rep.viewer]
ELSE rep ← NIL;
cmd ← NARROW[GetProp[cmd, $ParentCommander]];
IF cmd = NIL THEN EXIT;
ENDLOOP;
RETURN[NIL];
};
GetReadEvalPrint: PUBLIC PROC [cmd: Commander.Handle, topLevel: BOOLFALSE] RETURNS [ReadEvalPrint.Handle] = {
rep: ReadEvalPrint.Handle ← NIL;
Search up chain of parents to find a ReadEvalPrint.Handle and the associated commander
WHILE rep = NIL DO
rep ← NARROW[GetProp[cmd, $ReadEvalPrintHandle]];
IF rep # NIL AND (NOT topLevel OR rep.topLevel) THEN EXIT ELSE rep ← NIL;
cmd ← NARROW[GetProp[cmd, $ParentCommander]];
IF cmd = NIL THEN EXIT;
ENDLOOP;
RETURN[rep];
};
GetProp: PUBLIC PROC [cmd: Commander.Handle, key: REF] RETURNS [REF] = {
RETURN [List.Assoc[key, cmd.propertyList]];
};
CopyAList: PUBLIC PROC [old: List.AList] RETURNS [new: List.AList] = {
CopyAList copies the CONS cells of the list itself and also copies the DotCons cells which are the elements of the list. Because the DotCons cells are copied, one can change the key-value mappings in the new list without affecting the mappings in the old list. Because the CONS cells are copied, one can alter the list without affecting the old list.
tail: List.AList ← NIL;
new ← NIL;
UNTIL old = NIL DO
newItem: List.AList ← LIST[List.DotCons[key: old.first.key, val: old.first.val]];
IF tail = NIL THEN new ← newItem ELSE tail.rest ← newItem;
old ← old.rest;
tail ← newItem;
ENDLOOP;
};
PutLocalProperty: PUBLIC PROC [key, val: REF ANY, aList: List.AList, origList: List.AList ← NIL] RETURNS [List.AList] = {
PutLocalProperty is used to set a "local" property on a list. OrigList is intended to point into the middle of aList. If key is found on the part of aList in front of origList, then the old binding is changed and aList is returned. If key is not found on aList before origList is encountered, then a new property is added to the head of aList and the new list is returned.
newList: List.AList;
FOR l: List.AList ← aList, l.rest WHILE l # NIL AND l # origList DO
IF l.first.key = key THEN {
l.first.val ← val;
RETURN[aList];
};
ENDLOOP;
newList ← LIST[List.DotCons[key: key, val: val]];
newList.rest ← aList;
RETURN[newList];
};
Insulate: PUBLIC PROC [stream: IO.STREAM] RETURNS [safeStream: IO.STREAM] = {
Insulate creates streams for which close appears to work, but which do not close the backing stream. It also attaches the in stream to the commander stop button.
safeStream ← IO.CreateStream[streamProcs: IO.CreateStreamProcs[variety: stream.GetInfo[].variety, class: stream.GetInfo[].class, close: IOUtils.closedStreamProcs.close], streamData: NIL, backingStream: stream];
};
Several functions in the command tool use properties which are bound to lists of command procs. The following functions help to manage this arrangement.
CallList: PUBLIC PROC [property: REF ANY, cmd: Commander.Handle, proc: PROC [result: REF, msg: ROPE] RETURNS [stop: BOOL]] = {
CallList uses the given property name (typically an ATOM) to search cmd.propertyList. The value of the property should be LIST OF REF ANY. Each of the elements of the list should resolve to a Commander.CommandProcHandle. Each of the command procs found will be called with cmd as its argument. If proc is not NIL, it is called with the return values from each of the CommandProcs called. proc can stop CallList from proceeding by returning stop = TRUE;
result: REF;
msg: ROPE;
WITH GetProp[cmd, property] SELECT FROM
list: LIST OF REF ANY =>
FOR l: LIST OF REF ANY ← list, l.rest WHILE l # NIL DO
WITH l.first SELECT FROM
cpc: Commander.CommandProcHandle => {
[result: result, msg: msg] ← cpc.proc[cmd];
IF result # $Preserve THEN
cmd.propertyList ←
List.PutAssoc[key: $Result, val: result, aList: cmd.propertyList];
IF proc # NIL AND proc[result: result, msg: msg] THEN EXIT;
};
ENDCASE;
ENDLOOP;
ENDCASE;
};
AddProcToList: PUBLIC PROC [aList: List.AList, listKey: REF ANY, proc: Commander.CommandProcHandle, append: BOOLTRUE] RETURNS [List.AList] = {
AddProcToList is used to construct or alter a LIST OF REF ANY whose elements are actually Commander.CommandProcHandles.
maybeList: REF ANY ← List.Assoc[key: listKey, aList: aList];
list: LIST OF REF ANY;
alreadyThere: BOOL;
IF maybeList # NIL AND ISTYPE[maybeList, LIST OF REF ANY] THEN list ← NARROW[maybeList];
alreadyThere ← list # NIL AND List.Memb[ref: proc, list: list];
IF NOT alreadyThere THEN {
IF append THEN {
ra: REF ANY ← proc;
list ← List.Append[list, LIST[ra]];
}
ELSE list ← CONS[proc, list];
aList ← List.PutAssoc[key: listKey, val: list, aList: aList];
};
RETURN[aList];
};
RemoveProcFromList: PUBLIC PROC [aList: List.AList, listKey: REF ANY, proc: Commander.CommandProcHandle] RETURNS [List.AList] = {
RemoveProcToList is used to remove a particular Commander.CommandProcHandle from a LIST OF REF ANY whose elements are actually Commander.CommandProcHandles.
maybeList: REF ANY ← List.Assoc[key: listKey, aList: aList];
list: LIST OF REF ANY;
alreadyThere: BOOL;
IF maybeList # NIL AND ISTYPE[maybeList, LIST OF REF ANY] THEN list ← NARROW[maybeList];
alreadyThere ← list # NIL AND List.Memb[ref: proc, list: list];
IF alreadyThere THEN aList ← List.PutAssoc[key: listKey, val: List.Remove[ref: proc, list: list], aList: aList];
RETURN[aList];
};
FileWithSearchRules: PUBLIC PROC [root: ROPE, defaultExtension: ROPE, cmd: Commander.Handle, tryStar: BOOLTRUE] RETURNS [fullPath: ROPENIL] = {
RETURN[FileNames.FileWithSearchRules[root: root, defaultExtension: defaultExtension, requireExact: NOT tryStar, searchRules: GetProp[cmd, $SearchRules]].fullPath];
};
If path starts with ./ or ../, ResolveRelativePath converts it into the equivalent full path name using the $WorkingDirectory property on the process properties list.
If path is exactly . or .., ResolveRelativePath converts it to the current or parent directory.
ResolveRelativePath: PUBLIC PROC [path: ROPE] RETURNS [ROPE] = {
RETURN[FileNames.ResolveRelativePath[path]];
};
ConvertToSlashFormat: PUBLIC PROC [path: ROPE] RETURNS [ROPE] = {
RETURN[FileNames.ConvertToSlashFormat[path]];
};
CopyListOfRefAny: PUBLIC PROC [key: REF ANY, aList: List.AList] RETURNS [List.AList] = {
CopyListOfRefAny searches for the binding of the given key. If it is a LIST OF REF ANY, then it is List.Append-ed and put back.
ref: REF ANY ← List.Assoc[key: key, aList: aList];
IF ref = NIL THEN RETURN[aList];
WITH ref SELECT FROM
lra: LIST OF REF ANY =>
aList ← List.PutAssoc[key: key, val: List.Append[lra], aList: aList];
ENDCASE;
RETURN[aList];
};
AddSearchRule: PUBLIC PROC [cmd: Commander.Handle, dir: ROPE, append: BOOLTRUE] = {
If rule = NIL THEN deletes all search rules. If append = FALSE, then dir becomes the first rule
rules: LIST OF REF ANY;
length: INT;
first, last: CHAR;
rules ← NARROW[GetProp[cmd, $SearchRules]];
IF dir = NIL
THEN rules ← NIL
ELSE {
dir ← FileNames.ResolveRelativePath[dir];
length ← dir.Length[];
IF length < 3 THEN RETURN;
dir ← FileNames.ConvertToSlashFormat[dir];
first ← dir.Fetch[0];
last ← dir.Fetch[length - 1];
IF first # '/ THEN RETURN;
IF last # '/ THEN dir ← Rope.Concat[dir, "/"];
suppress duplicates
FOR r: LIST OF REF ANY ← rules, r.rest WHILE r # NIL DO
IF Rope.Equal[NARROW[r.first, ROPE], dir, FALSE] THEN RETURN;
ENDLOOP;
IF append
THEN {
ra: REF ANY ← dir;
rules ← List.Append[rules, LIST[ra]];
}
ELSE rules ← CONS[dir, rules];
};
[] ← List.PutAssoc[key: $SearchRules, val: rules, aList: cmd.propertyList];
};
CurrentWorkingDirectory: PUBLIC PROC RETURNS [ROPE] = {
RETURN[FileNames.CurrentWorkingDirectory[]];
};
LookupWithSearchRules: PUBLIC Commander.CommandProc = {
root: ROPE ← cmd.command;
wDir: ROPENIL;
wDirRoot: ROPENIL;
temp: ROPENIL;
ambiguous: BOOLFALSE;
rules: LIST OF REF ANY;
fullPath: BOOL;
Try: PROC [name: ROPE] RETURNS [foundSomething: BOOL] = {
cmd.procData ← Commander.Lookup[name];
IF cmd.procData # NIL AND cmd.procData.proc # NIL THEN {
cmd.command ← name;
RETURN[TRUE];
};
cmd.procData ← NIL;
RETURN[FALSE];
};
UniqueMatch: PROC [name: ROPE] RETURNS [foundSomething: BOOLFALSE] = {
lst: LIST OF ROPENIL;
p: PROC [key: ROPE, procData: Commander.CommandProcHandle] RETURNS [stop: BOOLFALSE] = {
IF procData.proc # NIL AND Rope.Match[pattern: name, object: key, case: FALSE] THEN lst ← CONS[key, lst];
};
name ← Rope.Concat[name, "*"];
[] ← Commander.Enumerate[p];
IF lst = NIL THEN RETURN[FALSE];
IF lst.rest # NIL THEN {
ambiguous prefix
ros: IO.STREAMIO.ROS[];
ambiguous ← TRUE;
result ← $Ambiguous;
ros.PutRope[" . . . command ambiguous ( "];
WHILE lst # NIL DO
ros.PutRope[lst.first];
ros.PutChar[' ];
lst ← lst.rest;
ENDLOOP;
ros.PutRope[")\n"];
msg ← IO.RopeFromROS[ros];
RETURN[TRUE];
};
Exactly one match
cmd.command ← lst.first;
cmd.procData ← Commander.Lookup[cmd.command];
RETURN[TRUE];
};
IF root.Length[] = 0 THEN GO TO notFound;
root ← FileNames.ResolveRelativePath[root];
root ← FileNames.ConvertToSlashFormat[root];
try root
IF Try[root] THEN GO TO found;
fullPath ← root.Fetch[0] = '/;
IF NOT fullPath THEN {
try Concat[$WorkingDirectory, root]
wDir ← FileNames.CurrentWorkingDirectory[];
wDirRoot ← Rope.Concat[wDir, root];
IF Try[wDirRoot] THEN GO TO found;
rules ← NARROW[GetProp[cmd, $SearchRules]];
For each search rule
try Concat[search rule, root]
FOR list: LIST OF REF ANY ← rules, list.rest WHILE list # NIL DO
try Concat[search rule, root]
IF Try[Rope.Concat[NARROW[list.first], root]] THEN GO TO found;
ENDLOOP;
};
try to find a unique match for Concat[root, "*"];
IF ambiguous OR UniqueMatch[root] THEN GO TO found;
IF NOT fullPath THEN {
try to find a unique match for Cat[$WorkingDirectory, root, "*"];
IF ambiguous OR UniqueMatch[wDirRoot] THEN GO TO found;
For each search rule
try to find a unique match for Cat[search rule, root, "*"];
FOR list: LIST OF REF ANY ← rules, list.rest WHILE list # NIL DO
IF ambiguous OR UniqueMatch[Rope.Concat[NARROW[list.first], root]] THEN GO TO found;
ENDLOOP;
};
GO TO notFound;
EXITS
found => IF result # $Ambiguous THEN result ← $Found;
notFound => result ← $Failed;
};
CommandFileWithSearchRules: PUBLIC Commander.CommandProc = {
CommandFileWithSearchRules: look in the FS directory for a command file
If $Result is $Ambiguous on entry, then only try exact matches
Return $Ambiguous if FileWithSearchRules does
commandFileName: ROPENIL;
ambiguous: BOOL ← GetProp[cmd, $Result] = $Ambiguous;
list: LIST OF ROPE ← CommandToolLookup.FindMatchingFiles[root: cmd.command, defaultExtension: ".cm", requireExact: ambiguous, searchRules: GetProp[cmd, $SearchRules]];
IF list = NIL THEN GO TO notFound;
commandFileName ← list.first;
IF list.rest # NIL THEN {
err: IO.STREAM ← cmd.err;
IO.PutRope[err, "[[Ambiguous command files:\n"];
FOR each: LIST OF ROPE ← list, each.rest WHILE each # NIL DO
IO.PutF1[err, " %g\n", [rope[each.first]] ];
ENDLOOP;
IO.PutRope[err, " ]]\n"];
GO TO ambig;
};
cmd.commandLine ← Rope.Concat[commandFileName, " "];
cmd.command ← "Commander";
cmd.procData ← commandFileProcData;
result ← $Found;
EXITS
ambig => result ← $Ambiguous;
notFound => result ← $Failed;
};
LoadAndRunWithSearchRules: PUBLIC Commander.CommandProc = {
LoadAndRunWithSearchRules: look in the FS directory for a file with extension ".load" If found, run it then attempt a normal commander lookup (using LookupWithSearchRules).
If $Result is $Ambiguous on entry, then only try exact matches
Return $Ambiguous if FileWithSearchRules does
commandFileName: ROPE;
sc: SavedCommand ← NIL;
list: LIST OF ROPE ← CommandToolLookup.FindMatchingFiles[root: cmd.command, defaultExtension: ".load", requireExact: FALSE, searchRules: GetProp[cmd, $SearchRules]];
IF list = NIL THEN GO TO notFound;
commandFileName ← list.first;
IF list.rest # NIL THEN {
err: IO.STREAM ← cmd.err;
result ← $Ambiguous;
IO.PutRope[err, "[[Ambiguous load files:\n"];
FOR each: LIST OF ROPE ← list, each.rest WHILE each # NIL DO
IO.PutF1[err, " %g\n", [rope[each.first]] ];
ENDLOOP;
IO.PutRope[err, " ]]\n"];
GO TO ambig;
};
We have located a command file.
cmd.procData ← NEW[Commander.CommandProcObject ← [
ReallyLoadAndRun,
"Command not yet loaded",
NEW[SavedCommandObject
← [command: cmd.command, commandFileName: commandFileName]]]];
result ← $Found;
EXITS
ambig => result ← $Ambiguous;
notFound => result ← $Failed;
};
These are used when LoadAndRun has to save the original command information
SavedCommand: TYPE = REF SavedCommandObject;
SavedCommandObject: TYPE = RECORD [
command: ROPENIL,
commandFileName: ROPENIL
];
ReallyLookupHandle: Commander.CommandProcHandle ← NEW[Commander.CommandProcObject ← [ReallyLoadAndRun, "ReallyLoadAndRun", "execute .load file and run command"]];
ReallyLoadAndRun: Commander.CommandProc = {
procData.clientData is a SavedCommand, load the .load file whose name is in commandLine and then Lookup and execute the saved command.
sc: SavedCommand ← NARROW[cmd.procData.clientData];
commandFileName: ROPE ← sc.commandFileName;
originalCommandLine: ROPE ← cmd.commandLine;
oldOut: IO.STREAM;
oldIn: IO.STREAM;
loadFileDirectory: ROPE;
inner: PROC = {
ENABLE UNWIND => {cmd.out ← oldOut; cmd.in ← oldIn};
[result, msg] ← CommandTool.CommandFile[cmd];
cmd.out ← oldOut;
cmd.in ← oldIn;
};
commandFileName ← FileNames.ConvertToSlashFormat[commandFileName];
loadFileDirectory ← FileNames.Directory[path: commandFileName];
cmd.command ← "CommandTool";
cmd.commandLine ← commandFileName;
cmd.procData ← commandFileProcData;
oldOut ← cmd.out;
cmd.out ← Insulate[cmd.err];
oldIn ← cmd.in;
cmd.in ← Insulate[oldIn];
set the working directory for this call (and this call only)
ProcessProps.AddPropList[
List.PutAssoc[key: $WorkingDirectory, val: loadFileDirectory, aList: NIL],
inner];
IF result = $Failure THEN RETURN[result, msg];
cmd.command ← sc.command;
cmd.commandLine ← originalCommandLine;
[result, msg] ← LookupWithSearchRules[cmd];
IF cmd.procData # NIL AND cmd.procData.proc # NIL
THEN {
Success. So pass along the result of executing the command.
CommandTool.ExecuteCommand[cmd, FALSE];
result ← CommandTool.GetProp[cmd, $Result];
}
ELSE RETURN[$Failure, ".load file failed to register command"];
};
AtSignLimit: NAT ← 20;
AtSignFile: PROC [name: ROPE] RETURNS [msg: ROPENIL] = {
WITH ProcessProps.GetProp[$CommanderHandle] SELECT FROM
cmd: Commander.Handle => {
rules: REF ← CommandTool.GetProp[cmd, $SearchRules];
paths: LIST OF ROPE ← CommandToolLookup.FindMatchingFiles[name, ".cm", TRUE, rules];
IF paths # NIL THEN name ← paths.first;
};
ENDCASE => {
RETURN [RopeFile.Create[name: Rope.Concat[name, ".cm"], raw: FALSE
! FS.Error => CONTINUE]];
};
RETURN [RopeFile.Create[name: name, raw: FALSE
! FS.Error => {
msg ← error.explanation;
GO TO fail}]];
EXITS
fail =>
ERROR CommandTool.Failed[msg];
};
Pass1: PUBLIC PROC [initial: ROPE, nameOnly: BOOL] RETURNS [first: ROPENIL, rest: ROPENIL, terminator: CHAR ← '\n, someExpansion: BOOLFALSE] = {
Pass1 handles the initial rope passed in from ReadEvalPrint. It searches for the first ';, if there is one, and restricts its attention to the part of the rope before it.
{
state: {outside, insideQuotes, insideAtName} ← outside;
c: CHAR;
i: INT ← 0;
atPosition: INT;
atSignDepth: NAT ← 0;
startPosition: INT ← 0;
pastLeadingWhiteSpace: BOOLFALSE;
IF NOT nameOnly THEN pastLeadingWhiteSpace ← TRUE;
WHILE i < initial.Length[] DO
Process.CheckForAbort[];
{
c ← initial.Fetch[i];
IF c = '\\ THEN {
slashPosition: INT ← i;
slashSequenceLength: INT ← 2;
escapeRope: ROPE;
i ← i + 1; -- pointing at char after the slash
IF i >= initial.Length[] THEN ERROR CommandTool.Failed["Backslash at end of command"];
c ← initial.Fetch[i]; -- get the char after the slash
IF c IN ['0..'9] THEN {
IF (i + 2) >= initial.Length[] THEN ERROR CommandTool.Failed["Not enough characters for backslash convention"];
slashSequenceLength ← 4;
};
escapeRope ← Convert.RopeFromLiteral[Rope.Cat["""", initial.Substr[start: slashPosition, len: slashSequenceLength], """"]
! Convert.Error => ERROR CommandTool.Failed["Backslash sequence error"];
];
initial ← Rope.Replace[base: initial, start: slashPosition, len: slashSequenceLength, with: escapeRope];
i ← slashPosition;
c ← initial.Fetch[i];
};
SELECT state FROM
outside => {
SELECT c FROM
'" => {
state ← insideQuotes;
pastLeadingWhiteSpace ← TRUE;
};
';, '\n, '| => {
terminator ← c;
GOTO FoundTerminator;
};
'@ => {
state ← insideAtName;
atPosition ← i;
};
' , '\t => {
IF nameOnly THEN {
IF pastLeadingWhiteSpace THEN {
terminator ← ' ;
GOTO FoundTerminator;
}
ELSE startPosition ← startPosition + 1;
};
};
'& => {
IF nameOnly THEN { -- end of name
terminator ← c;
GOTO FoundTerminator;
};
IF initial.Length[] > (i + 1) THEN {
SELECT initial.Fetch[i+1] FROM
' , ' , '\n, '; => { -- followed by whitespace
terminator ← c;
GOTO FoundTerminator;
};
ENDCASE => NULL;
}
ELSE { -- end of line
terminator ← c;
GOTO FoundTerminator;
}
};
ENDCASE => pastLeadingWhiteSpace ← TRUE;
};
insideQuotes => {
IF c = '" THEN {
IF initial.Length[] > (i + 1) AND initial.Fetch[i+1] = '" THEN i ← i + 1
ELSE state ← outside;
};
};
insideAtName => {
IF c = '@ OR c = ' OR c = '\n THEN {
nameLength: INT ← i - atPosition - 1;
atSignFileName: ROPE ← initial.Substr[start: atPosition + 1, len: nameLength];
initial ← Rope.Replace[base: initial, start: atPosition, len: IF c = '@ THEN nameLength + 2 ELSE nameLength + 1, with: AtSignFile[name: atSignFileName]];
i ← atPosition - 1;
state ← outside;
atSignDepth ← atSignDepth + 1;
IF atSignDepth > AtSignLimit THEN
ERROR CommandTool.Failed["Exceeded limit on expansion of @ command files"];
someExpansion ← TRUE;
};
};
ENDCASE => ERROR;
i ← i + 1;
};
REPEAT
FoundTerminator => {
rest ← Rope.Substr[base: initial, start: i + 1];
initial ← Rope.Substr[base: initial, start: startPosition, len: i - startPosition];
IF NOT nameOnly THEN initial ← Rope.Concat[initial, "\n"];
};
ENDLOOP;
IF state = insideQuotes THEN ERROR CommandTool.Failed["Mismatched quotes"];
IF state = insideAtName THEN ERROR CommandTool.Failed["Improper @-file specification"];
};
first ← initial;
};
END.
October 7, 1983 9:38 am, Stewart, Created
December 13, 1983 4:14 pm, Stewart, fixed bug in CallList
January 14, 1984 7:45 pm, Stewart, fixed bugs in ReallyLoadAndRun