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) March 6, 1985 3:36:28 pm PST
DIRECTORY
Commander USING [CommandProc, CommandProcHandle, CommandProcObject, Enumerate, Handle, Lookup],
CommandTool,
CommandToolExtras,
Convert USING [Error, RopeFromLiteral],
FileNames USING [ConvertToSlashFormat, CurrentWorkingDirectory, Directory, FileWithSearchRules, ResolveRelativePath],
FS USING [Error, Open, OpenFile, nullOpenFile, StreamFromOpenFile],
IO USING [Close, CreateStream, CreateStreamProcs, GetInfo, PutChar, PutRope, RopeFromROS, ROS, STREAM],
IOUtils USING [closedStreamProcs],
List USING [AList, Append, Assoc, CopyTopList, DotCons, Memb, PutAssoc, Remove],
ProcessExtras USING [CheckForAbort],
ProcessProps USING [AddPropList],
ReadEvalPrint USING [Handle],
Rope USING [Cat, Concat, Equal, Fetch, Find, IsEmpty, Length, Match, Replace, ROPE, Substr],
RopeFrom USING [Stream],
ViewerClasses USING [Viewer];
CommandToolUtilitiesImpl: CEDAR PROGRAM
IMPORTS Commander, CommandTool, Convert, FileNames, FS, IO, IOUtils, List, ProcessExtras, ProcessProps, Rope, RopeFrom
EXPORTS CommandTool, CommandToolExtras
SHARES IO
= BEGIN
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[List.Assoc[key: $ReadEvalPrintHandle, aList: cmd.propertyList]];
IF rep # NIL AND rep.viewer # NIL THEN RETURN[rep.viewer]
ELSE rep ← NIL;
cmd ← NARROW[List.Assoc[key: $ParentCommander, aList: cmd.propertyList]];
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[List.Assoc[key: $ReadEvalPrintHandle, aList: cmd.propertyList]];
IF rep # NIL AND (NOT topLevel OR rep.topLevel) THEN EXIT
ELSE rep ← NIL;
cmd ← NARROW[List.Assoc[key: $ParentCommander, aList: cmd.propertyList]];
IF cmd = NIL THEN EXIT;
ENDLOOP;
RETURN[rep];
};
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.
CopyAList: PUBLIC PROC [old: List.AList] RETURNS [new: List.AList] = {
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 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.
PutLocalProperty: PUBLIC PROC [key, val: REF ANY, aList: List.AList, origList: List.AList ← NIL] RETURNS [List.AList] = {
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 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.
Insulate: PUBLIC PROC [stream: IO.STREAM] RETURNS [safeStream: IO.STREAM] = {
Note: looks like I have to do it by hand.
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 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;
CallList: PUBLIC PROC [property: REF ANY, cmd: Commander.Handle, proc: PROC [result: REF, msg: Rope.ROPE] RETURNS [stop: BOOL]] = {
result: REF;
msg: Rope.ROPE;
WITH List.Assoc[key: property, aList: cmd.propertyList] 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 is used to construct or alter a LIST OF REF ANY whose elements are actually Commander.CommandProcHandles.
AddProcToList: PUBLIC PROC [aList: List.AList, listKey: REF ANY, proc: Commander.CommandProcHandle, append: BOOLTRUE] RETURNS [List.AList] = {
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];
};
RemoveProcToList is used to remove a particular Commander.CommandProcHandle from a LIST OF REF ANY whose elements are actually Commander.CommandProcHandles.
RemoveProcFromList: PUBLIC PROC [aList: List.AList, listKey: REF ANY, proc: Commander.CommandProcHandle] RETURNS [List.AList] = {
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 uses the following rules:
FileWithSearchRules: PUBLIC PROC [root: Rope.ROPE, defaultExtension: Rope.ROPE, cmd: Commander.Handle] RETURNS [fullPath: Rope.ROPENIL] = {
RETURN[FileNames.FileWithSearchRules[root: root, defaultExtension: defaultExtension, searchRules: List.Assoc[key: $SearchRules, aList: cmd.propertyList]].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.ROPE] RETURNS [Rope.ROPE] = {
RETURN[FileNames.ResolveRelativePath[path]];
};
ConvertToSlashFormat: PUBLIC PROC [path: Rope.ROPE] RETURNS [Rope.ROPE] = {
RETURN[FileNames.ConvertToSlashFormat[path]];
};
CopyListOfRefAny searches for the binding of the given key. If it is a LIST OF REF ANY, then it is List.CopyTopList-ed and put back.
CopyListOfRefAny: PUBLIC PROC [key: REF ANY, aList: List.AList] RETURNS [List.AList] = {
ref: REF ANY ← List.Assoc[key: key, aList: aList];
IF ref = NIL THEN RETURN[aList];
WITH ref SELECT FROM
lra: LIST OF REF ANY => RETURN[List.PutAssoc[key: key, val: List.CopyTopList[lra], aList: aList]];
ENDCASE;
RETURN[aList];
};
If rule = NIL THEN deletes all search rules. If append = FALSE, then dir becomes the first rule
AddSearchRule: PUBLIC PROC [cmd: Commander.Handle, dir: Rope.ROPE, append: BOOLTRUE] = {
rules: LIST OF REF ANY;
length: INT;
first, last: CHAR;
rules ← NARROW[List.Assoc[key: $SearchRules, aList: cmd.propertyList]];
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.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.ROPE] = {
RETURN[FileNames.CurrentWorkingDirectory[]];
};
LookupWithSearchRules:
get previous result via $Result property
try root
If root is not full path name
try Concat[$WorkingDirectory, root]
For each search rule
try Concat[search rule, root]
try to find a unique match for Concat[root, "*"];
If root is not full path name
try to find a unique match for Cat[$WorkingDirectory, root, "*"];
For each search rule
try to find a unique match for Cat[search rule, root, "*"];
Return result = $Ambiguous if either more than one possibility was found or if a unique result was found as a result of a pattern match.
LookupWithSearchRules: PUBLIC Commander.CommandProc = {
root: Rope.ROPE ← cmd.command;
wDir: Rope.ROPENIL;
wDirRoot: Rope.ROPENIL;
temp: Rope.ROPENIL;
ambiguous: BOOL ← List.Assoc[key: $Result, aList: cmd.propertyList] = $Ambiguous;
rules: LIST OF REF ANY;
fullPath: BOOL;
Try: PROC [name: Rope.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.ROPE] RETURNS [foundSomething: BOOLFALSE] = {
lst: LIST OF Rope.ROPENIL;
p: PROC [key: Rope.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];
};
result ← NIL;
IF root.Length[] = 0 THEN RETURN[$Failure, NIL];
root ← FileNames.ResolveRelativePath[root];
root ← FileNames.ConvertToSlashFormat[root];
try root
IF Try[root] THEN RETURN;
fullPath ← root.Fetch[0] = '/;
IF NOT fullPath THEN {
try Concat[$WorkingDirectory, root]
wDir ← FileNames.CurrentWorkingDirectory[];
wDirRoot ← Rope.Concat[wDir, root];
IF Try[wDirRoot] THEN RETURN;
rules ← NARROW[List.Assoc[key: $SearchRules, aList: cmd.propertyList]];
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 RETURN;
ENDLOOP;
};
try to find a unique match for Concat[root, "*"];
IF ambiguous OR UniqueMatch[root] THEN RETURN;
IF NOT fullPath THEN {
try to find a unique match for Cat[$WorkingDirectory, root, "*"];
IF ambiguous OR UniqueMatch[wDirRoot] THEN RETURN;
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 RETURN;
ENDLOOP;
};
};
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
CommandFileWithSearchRules: PUBLIC Commander.CommandProc = {
commandFileName: Rope.ROPE;
ambiguous: BOOL ← List.Assoc[key: $Result, aList: cmd.propertyList] = $Ambiguous;
[commandFileName, ambiguous] ← FileNames.FileWithSearchRules[root: cmd.command, defaultExtension: ".cm", requireExtension: FALSE, requireExact: ambiguous, searchRules: List.Assoc[key: $SearchRules, aList: cmd.propertyList]];
result ← NIL;
IF ambiguous THEN result ← $Ambiguous;
IF commandFileName.IsEmpty[] THEN RETURN;
cmd.commandLine ← Rope.Concat[commandFileName, " "];
cmd.command ← "Commander";
cmd.procData ← commandFileProcData;
};
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
LoadAndRunWithSearchRules: PUBLIC Commander.CommandProc = {
commandFileName: Rope.ROPE;
ambiguous: BOOL;
sc: SavedCommand ← NIL;
IF cmd.command.Find["."] # -1 THEN RETURN;
ambiguous ← List.Assoc[key: $Result, aList: cmd.propertyList] = $Ambiguous;
[commandFileName, ambiguous] ← FileNames.FileWithSearchRules[root: cmd.command, defaultExtension: ".load", requireExtension: TRUE, requireExact: ambiguous, searchRules: List.Assoc[key: $SearchRules, aList: cmd.propertyList]];
result ← NIL;
IF ambiguous THEN result ← $Ambiguous;
IF commandFileName.IsEmpty[] THEN RETURN;
We have located a command file, although perhaps ambiguous. If it is, we don't want to run it just yet.
sc ← NEW[SavedCommandObject ← [command: cmd.command, commandFileName: commandFileName]];
cmd.procData ← NEW[Commander.CommandProcObject ← [ReallyLoadAndRun, "Command not yet loaded", sc]];
};
These are used when LoadAndRun has to save the original command information
SavedCommand: TYPE = REF SavedCommandObject;
SavedCommandObject: TYPE = RECORD [
command: Rope.ROPENIL,
commandFileName: Rope.ROPENIL
];
ReallyLookupHandle: Commander.CommandProcHandle ← NEW[Commander.CommandProcObject ← [ReallyLoadAndRun, "ReallyLoadAndRun", "execute .load file and run command"]];
procData.clientData is a SavedCommand, load the .load file whose name is in commandLine and then Lookup and execute the saved command.
ReallyLoadAndRun: Commander.CommandProc = {
sc: SavedCommand ← NARROW[cmd.procData.clientData];
commandFileName: Rope.ROPE ← sc.commandFileName;
originalCommandLine: Rope.ROPE ← cmd.commandLine;
oldOut: IO.STREAM;
oldIn: IO.STREAM;
loadFileDirectory: Rope.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 ← List.Assoc[key: $Result, aList: cmd.propertyList];
}
ELSE RETURN[$Failure, ".load file failed to register command"];
};
AtSignLimit: NAT ← 20;
AtSignFile: PROC [name: Rope.ROPE] RETURNS [contents: Rope.ROPE] = {
file: FS.OpenFile ← FS.nullOpenFile;
fileStream: IO.STREAM;
{ ENABLE UNWIND => { IF fileStream # NIL THEN fileStream.Close[]; };
name ← FileNames.ResolveRelativePath[name];
file ← FS.Open[name: name ! FS.Error => IF error.group = user THEN CONTINUE];
IF file = FS.nullOpenFile THEN file ← FS.Open[name: Rope.Concat[name, ".cm"] ! FS.Error => IF error.group = user THEN CONTINUE];
IF file = FS.nullOpenFile THEN ERROR CommandTool.Failed[Rope.Cat["Can't find file |", name, "|"]];
I have used RopeFrom.Stream[FS.StreamFromOpenFile[]] rather than RopeFrom.File[FS.Open[]] because I believe that the stream will correctly handle Tioga source but that RopeFrom.File will not.
fileStream ← FS.StreamFromOpenFile[openFile: file];
contents ← RopeFrom.Stream[stream: fileStream];
fileStream.Close[];
};
};
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.
Pass1: PUBLIC PROC [initial: Rope.ROPE, nameOnly: BOOL] RETURNS [first: Rope.ROPENIL, rest: Rope.ROPENIL, terminator: CHAR ← '\n, someExpansion: BOOLFALSE] = {
{
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
ProcessExtras.CheckForAbort[];
{
c ← initial.Fetch[i];
IF c = '\\ THEN {
slashPosition: INT ← i;
slashSequenceLength: INT ← 2;
escapeRope: Rope.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;
};
' , '  => {
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.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