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:
BOOL ←
FALSE]
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:
BOOL ←
TRUE]
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:
BOOL ←
TRUE]
RETURNS [fullPath:
ROPE ←
NIL] = {
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:
BOOL ←
TRUE] = {
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: ROPE ← NIL;
wDirRoot: ROPE ← NIL;
temp: ROPE ← NIL;
ambiguous: BOOL ← FALSE;
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:
BOOL ←
FALSE] = {
lst: LIST OF ROPE ← NIL;
p:
PROC [key:
ROPE, procData: Commander.CommandProcHandle]
RETURNS [stop:
BOOL ←
FALSE] = {
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.STREAM ← IO.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: ROPE ← NIL;
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: ROPE ← NIL,
commandFileName: ROPE ← NIL
];
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:
ROPE ←
NIL] = {
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:
ROPE ←
NIL, rest:
ROPE ←
NIL, terminator:
CHAR ← '\n, someExpansion:
BOOL ←
FALSE] = {
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: BOOL ← FALSE;
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