CONST

  Version ='1.9.e';
          { x.x.y    Revisors: Please only renumber y, let McGee renumber x.x }


  { ----------  CONFIGURATION to user's system and preferences ------------- }

  { hardware and OS configuration }

  SystemDrive ='A:';        { SHELL and all .CHN files will be on this disk }
  ShellName   ='SHELL.COM'; { .CMD on CP/M-86, .COM on CP/M-80 and MS-DOS }
  PipePrefix  ='$PIPE';     { prefix with memory disk if available }
  TempEditFile='$EDTEMP';   { same }
                            { (need to move STEMP and ARTEMP here also) }
  TabSpaces   = 8;          { 4 in K&P, but 8 better for most terminals }
  { To configure, also check inclusion of proper OS file in CHAPTER1.PAS  }

  { example configurations:
         1.  AppleII with CP/M card and two floppy disks
         2.  DEC Rainbow running CP/M-86, autobooting to
               Winchester E:, with large memory drive M:
         3.  DEC Rainbow running MS-DOS on two floppies,
               system on B:, memory drive on E:

                     AppleII         Rainbow        Rainbow
                     CP/M-80         CP/M-86        MS-DOS
                     ----------      ----------     ----------
    SystemDrive      'A:'            'E:'           'B:'
    ShellName        'SHELL.COM'     'SHELL.CMD'    'SHELL.COM
    PipePrefix       '$PIPE'         'M:$PIPE'      'E:$PIPE'
    TempEditFile     '$EDTEMP'       'M:$EDTEMP'    'E:$EDTEMP'
  }


  { user preference configurations }

  ShellPrompt ='$ ';
  EditPrompt  =TRUE;      { not in K&P; very hard to use edit without it }
  Debug   = FALSE ;       { prints more info; can be handy while learning }
  ListProcess = TRUE;     { echo second and subsequent processes }
  Abbreviate = false;     { can shorten commands -- uses first match }
  AppendFNamePAS = FALSE; { converts, i.e. filename "TEXT" to "TEXT.PAS" }
  { K&P had AppendFNamePAS=TRUE, but it's confusing for non-program files }

  { --------------------- end of CONFIGURATION section --------------------- }


  MAXCMD=20; { max arguments to one process }
  ENDFILE=255;
  ENDSTR=0;
  MAXSTR=130;
  { ASCII character set in decimal }
  BLANK=32;
  BACKSPACE=8; { backs up cursor one space; may be different from DELETE! }
  DELETE1 = 127; { user types this to delete prior character entered }
  DELETE2 =   8; { user can also delete with this (=DELETE1 to remove) }
  TAB=9;
  NEWLINE=13;   { internal eol flag; also, terminates console input line }
  EXCLAM=33;
  DQUOTE=34;
  SHARP=35;
  DOLLAR=36;
  PERCENT=37;
  AMPER=38;
  SQUOTE=39;
  ACUTE=SQUOTE;
  LPAREN=40;
  RPAREN=41;
  STAR=42;
  PLUS=43;
  COMMA=44;
  MINUS=45;
  DASH=MINUS;
  PERIOD=46;
  SLASH=47;
  COLON=58;
  SEMICOL=59;
  LESS=60;
  EQUALS=61;
  GREATER=62;
  QUESTION=63;
  ATSIGN=64;
  ESCAPE=ATSIGN;
  LBRACK=91;
  BACKSLASH=92;
  RBRACK=93;
  CARET=94;
  GRAVE=96;
  UNDERLINE=95;
  TILDE=126;
  LBRACE=123;
  BAR=124;
  RBRACE=125;

TYPE
  CHARACTER=0..255;
  XSTRING=ARRAY[1..MAXSTR]OF CHARACTER;
  STRING80=string[80];
  FILEDESC=(IOERROR,STDIN,STDOUT,STDERR,F4,F5,F6,F7,F8,F9,F10,MAXOPEN);
       (* add as many Fn numbers as you need files; > F7 needed only by sort *)
  FileModes = (IOREAD,IOWRITE);
  FILTYP=(CLOSED,STDIO,OpenFile);

VAR
   { The process and pipe vars MUST be the first declared in every program }
   { chained to.  Thus, do not declare any variables before $I TOOLU.PAS.  }

   ActiveProcessQ, FromPipe, ToPipe : boolean;
   PipeCount : integer;
   ProcessQueue : XSTRING;

   KBDN,KBDNEXT:INTEGER;
   KBDLINE,CMDLIN:XSTRING;
   CMDARGS:0..MAXCMD;
   CMDIDX:ARRAY[1..MAXCMD] OF 1..MAXSTR;
   GlobalArg1: STRING80;
   CMDFIL:ARRAY[STDIN..MAXOPEN]OF FILTYP;
   CMDText: ARRAY[STDIN..MAXOPEN] OF TEXT;
   ReadingShellCmd : boolean;


PROCEDURE PUTDEC(N,W:INTEGER);FORWARD;
FUNCTION ITOC(N:INTEGER;VAR S:XSTRING;I:INTEGER):INTEGER;FORWARD;
FUNCTION GETARG(N:INTEGER;VAR S:XSTRING; MAXSIZE:INTEGER):BOOLEAN;FORWARD;
  PROCEDURE SCOPY(VAR SRC:XSTRING;I:INTEGER;VAR DEST:XSTRING;J:INTEGER);FORWARD;
PROCEDURE ENDCMD;FORWARD;
PROCEDURE XCLOSE(FD:FILEDESC);FORWARD;
FUNCTION XLENGTH(VAR S:XSTRING):INTEGER;FORWARD;
PROCEDURE ERROR(STR:STRING80);FORWARD;
FUNCTION MAX(X,Y:INTEGER):INTEGER;FORWARD;
PROCEDURE REMOVE(NAME:XSTRING);FORWARD;
FUNCTION NARGS:INTEGER;FORWARD;
FUNCTION ADDSTR(C:CHARACTER;VAR OUTSET:XSTRING;VAR J:INTEGER;MAXSET:INTEGER):
                 BOOLEAN;FORWARD;
PROCEDURE PUTSTR(STR:XSTRING;FD:FILEDESC);FORWARD;
FUNCTION MIN(X,Y:INTEGER):INTEGER;FORWARD;
FUNCTION ISUPPER(C:CHARACTER):BOOLEAN;FORWARD;
FUNCTION EQUAL(VAR STR1,STR2:XSTRING):BOOLEAN;FORWARD;
FUNCTION INDEX(VAR S:XSTRING;C:CHARACTER):INTEGER;FORWARD;
FUNCTION ISALPHANUM(C:CHARACTER):BOOLEAN;FORWARD;
FUNCTION ESC(VAR S:XSTRING;VAR I:INTEGER): CHARACTER;FORWARD;
PROCEDURE FCOPY(FIN,FOUT:FILEDESC);FORWARD;
FUNCTION CTOI(VAR S:XSTRING;VAR I:INTEGER):INTEGER;FORWARD;
FUNCTION ISDIGIT(C:CHARACTER):BOOLEAN;FORWARD;
FUNCTION ISLOWER(C:CHARACTER):BOOLEAN;FORWARD;
FUNCTION ISLETTER(C:CHARACTER):BOOLEAN;FORWARD;


{ system support }


 PROCEDURE GenPipeName(PipeNumber: integer; var name: XSTRING);
 { Generate a pipe file name }
 var str: STRING80;
     len, i: integer;
 begin
   str := PipePrefix;  len := LENGTH(STR);
   for i := 1 to len do  name[i] := ORD(str[i]);
   name[len+1] := ENDSTR;
   i := ITOC(PipeNumber,name,(len+1)); { append digits }
 end;

procedure AssignPipe0(var f: text);
var s: STRING80; name: XSTRING; i:integer;
begin
  GenPipeName(0,name);
  s := '';  i := 1;
  while name[i] <> ENDSTR do begin
    s := s + chr(name[i]);  i:= i+1;
  end;
  {close(f);} { causes crash on CP/M-86 }
  assign(f,s);
end;



function EntryFromHost: boolean;
{ The routines EntryFromHost and SetEntryFromHost implement a boolean
  variable which is always TRUE when SHELL is first invoked, and which
  remains FALSE across subsequent invocations via Chain/Execute }
{ Implemented via a file name, which is portable across all Turbo systems }
var pipe0: text;
begin
  AssignPipe0(pipe0);
  {$I- } reset(pipe0);;  {$I+ }
  EntryFromHost := (IOResult<>0);  { false if file exists }
  close(pipe0);
  { CP/M-80 allows minor speedup at cost of portability:  }
  { replace all code in this procedure by: EntryFromHost:= (mem[$80]<>255) }
  { and comment-out all code in SetEntryFromHost                           }
end;

procedure SetEntryFromHost(entry: boolean);
  var pipe0: text;
begin
  AssignPipe0(pipe0);
  rewrite(pipe0); close(pipe0);     { access or create (empty) file }
  if entry then erase(pipe0); { remove file }
end;



procedure ExitToHost;
{ Exit program by calling this.  DO NOT CALL HALT DIRECTLY! }
BEGIN
  SetEntryFromHost(TRUE);
  HALT;
END;

procedure ExitToShell;
VAR cmdptr: file;
BEGIN
  assign(cmdptr,SystemDrive+ShellName);
  execute(cmdptr)
END;

procedure RemovePipe(OldPipe: integer);
var name: XSTRING;
begin
    GenPipeName(OldPipe,name);
    REMOVE(name);
end;


FUNCTION ISDIGIT;
BEGIN
  ISDIGIT:=C IN [ORD('0')..ORD('9')]
END;

FUNCTION ISLOWER;
BEGIN
  ISLOWER:=C IN [ORD('a')..ORD('z')]
END;

FUNCTION ISLETTER;
BEGIN
  ISLETTER:=C IN [ORD('A')..ORD('Z'),ORD('a')..ORD('z')]
END;

FUNCTION CTOI;
VAR N,SIGN:INTEGER;
BEGIN
  WHILE (S[I]=BLANK) OR (S[I]=TAB)DO
    I:=I+1;
  IF(S[I]=MINUS) THEN
    SIGN:=-1
  ELSE
    SIGN:=1;
  IF(S[I]=PLUS)OR(S[I]=MINUS)THEN
    I:=I+1;
  N:=0;
  WHILE(ISDIGIT(S[I])) DO BEGIN
    N:=10*N+S[I]-ORD('0');
    I:=I+1
  END;
  CTOI:=SIGN*N
END;


FUNCTION GETKBD(VAR C:CHARACTER):CHARACTER;
VAR DONE:BOOLEAN;
    i:integer;
    ch:char;
BEGIN
  IF (KBDN<=0) THEN BEGIN
    KBDNEXT:=1;
    DONE:=FALSE;
    if (kbdn=-2) then begin kbdn:=0 end
    else if (kbdn<0)then done:=true;
    WHILE(NOT DONE) DO BEGIN
      kbdn:=kbdn+1;
      DONE:=TRUE;
      if (eof(TRM)) then kbdn:=-1
      else if eoln(TRM) then begin
        kbdn:=kbdn-1;kbdline[kbdn]:=NEWLINE
      end
      else if (MAXSTR-1<=kbdn) then begin
        if ReadingShellCmd then
          ERROR(' Line too long - ignored')
        else  begin
          writeln(' Line too long - truncated');
          kbdline[kbdn]:=newline
        end
      END
      ELSE begin
        read(TRM,ch);kbdline[kbdn]:=ord(ch);
        if (ord(ch)in ([0..31]-[DELETE1,DELETE2,NEWLINE])) then
           write('^',chr(ord(ch)+64)) else
        if (kbdline[kbdn]<>DELETE1) and (kbdline[kbdn]<>DELETE2) then
        ELSE begin
          write(chr(BACKSPACE),' ',chr(BACKSPACE));
          if (1<kbdn)then begin
            kbdn:=kbdn-2;
            if kbdline[kbdn+1]in[0..31] then
               write(chr(BACKSPACE),' ',chr(BACKSPACE))
          end
          ELSE kbdn:=kbdn-1
        end;
        done:=false
      end;
    END
  END;
  reset(TRM);
  IF(KBDN<=0)THEN
    C:=ENDFILE
  ELSE BEGIN
    C:=KBDLINE[KBDNEXT];
    KBDNEXT:=KBDNEXT+1;
    if (c=NEWLINE) then kbdn:=-2
    ELSE KBDN:=KBDN-1
  END;
  GETKBD:=C
END;



FUNCTION FGETCF(VAR FIL:TEXT):CHARACTER;
 VAR CH:CHAR;
 BEGIN
   {     -disabled -  $  I- do not hang on I/O error }
   IF(EOF(FIL))THEN
      FGETCF:=ENDFILE
   ELSE IF(EOLN(FIL)) THEN BEGIN
      READLN(FIL);
      FGETCF:=NEWLINE
   END
   ELSE BEGIN
     READ(FIL,CH);
     FGETCF:=ORD(CH);
   END;
   if (IOresult <> 0) then
      ERROR('FGETCF: I/O error');
   {$I+ }
 END;


FUNCTION GETCF(VAR C:CHARACTER;FD:FILEDESC):CHARACTER;
 BEGIN
   IF CMDFIL[FD] = STDIO
     THEN GETCF := GETKBD(C)
     ELSE BEGIN C := FGETCF(CMDText[FD]); GETCF := C; END;
 END;


FUNCTION GETC(VAR C:CHARACTER):CHARACTER;
BEGIN
  GETC:=GETCF(C,STDIN)
END;


PROCEDURE PUTCF(C:CHARACTER;FD:FILEDESC);
BEGIN
  (* assert CMDFIL[FD] <> STDIO *)
  if C=NEWLINE
    THEN WRITELN(CMDText[FD])
    ELSE WRITE(CMDText[FD],chr(C));
END;


PROCEDURE PUTC(C:CHARACTER);
BEGIN
  (* PUTCF(C,STDOUT); *)
  if C=NEWLINE
    then writeln(CMDText[STDOUT])
    else write(CMDText[STDOUT],chr(C));
END;


PROCEDURE FCOPY;
VAR
  C:CHARACTER;
BEGIN
  WHILE(GETCF(C,FIN)<>ENDFILE) DO
    PUTCF(C,FOUT)
END;


FUNCTION INDEX;
VAR I:INTEGER;
BEGIN
  I:=1;
  WHILE(S[I]<>C) AND (S[I]<>ENDSTR)DO
    I:=I+1;
  IF (S[I]=ENDSTR) THEN
    INDEX:=0
  ELSE
    INDEX:=I
END;

FUNCTION ESC;
BEGIN
  IF(S[I]<>ATSIGN) THEN
    ESC:=S[I]
  ELSE IF(S[I+1]=ENDSTR) THEN (*@ NOT SPECIAL AT END*)
    ESC:=ATSIGN
  ELSE BEGIN
    I:=I+1;
    IF(S[I]=ORD('n'))THEN ESC:=NEWLINE
    ELSE IF (S[I]=ORD('t')) THEN
      ESC:=TAB
    ELSE
      ESC:=S[I]
  END
END;

FUNCTION ISALPHANUM;
BEGIN
  ISALPHANUM:=C IN
    [ORD('A')..ORD('Z'),ORD('0')..ORD('9'),
    ORD('a')..ORD('z')]
END;

FUNCTION MAX;
BEGIN
  IF(X>Y)THEN
    MAX:=X
  ELSE
    MAX:=Y
END;


FUNCTION MIN;
BEGIN
  IF X<Y THEN
    MIN:=X
  ELSE
    MIN:=Y
END;


FUNCTION ISUPPER;
  BEGIN
    ISUPPER:=C IN [ORD('A')..ORD('Z')]
  END;


FUNCTION XLENGTH;
VAR
  N:INTEGER;
BEGIN
  N:=1;
  WHILE(S[N]<>ENDSTR)DO
    N:=N+1;
  XLENGTH:=N-1
END;

FUNCTION GETARG;
BEGIN
  IF((N<1)OR(CMDARGS<N))THEN
    GETARG:=FALSE
  ELSE BEGIN
    SCOPY(CMDLIN,CMDIDX[N],S,1);
    GETARG:=TRUE
  END
END;(*GETARG*)


  PROCEDURE SCOPY;
  BEGIN
    SRC[MAXSTR]:=ENDSTR;  { safety }
    WHILE(SRC[I]<>ENDSTR)DO BEGIN
      DEST[J]:=SRC[I];
      I:=I+1;
      J:=J+1
    END;
    DEST[J]:=ENDSTR
  END;


PROCEDURE STRNAME(VAR STR:STRING80;VAR XSTR:XSTRING);
VAR I:INTEGER;
BEGIN
  IF AppendFNamePAS
    THEN STR:='.PAS' else STR := '' ;
  I:=1;
  WHILE(XSTR[I]<>ENDSTR)DO BEGIN
    INSERT('X',STR,I);
    STR[I]:=CHR(XSTR[I]);
    I:=I+1
  END
END;

PROCEDURE NAMESTR(VAR XSTR:XSTRING; STR:STRING80);
VAR I: INTEGER;
BEGIN
  FOR I:= 1 TO length(STR) DO XSTR[I]:=ord(STR[I]);
  XSTR[1+length(STR)] := ENDSTR;
END;

FUNCTION FDALLOC:FILEDESC;
VAR DONE:BOOLEAN;
FD:FILEDESC;
BEGIN
  IF Debug THEN begin write('entry to FDALLOC: ');
            for FD := STDIN TO MAXOPEN DO case CMDFIL[FD] OF
            CLOSED: WRITE(' c'); STDIO:WRITE(' s'); OpenFile:write(' o'); end;
           writeln;
       end;
  FD:=STDIN;
  DONE:=FALSE;
  WHILE(NOT DONE) DO
    IF((CMDFIL[FD]=CLOSED) OR (FD=MAXOPEN))THEN
      DONE:=TRUE
    ELSE FD:=SUCC(FD);
  IF(CMDFIL[FD]<>CLOSED) THEN
    FDALLOC:=IOERROR
  ELSE BEGIN
    CMDFIL[FD]:= OpenFile;
      FDALLOC:=FD
  END
END;(*FDALLOC*)


FUNCTION CREATE(VAR NAME:XSTRING;MODE:FileModes):FILEDESC;
VAR
  FD:FILEDESC;
  SNM:STRING80;
BEGIN
(*$I-*)
  FD:=FDALLOC;
  IF(FD<>IOERROR)THEN BEGIN
  STRNAME(SNM,NAME);
  ASSIGN(CMDText[FD],SNM); REWRITE(CMDText[FD]);
  IF(IORESULT<>0)THEN BEGIN
    XCLOSE(FD);
    FD:=IOERROR
  END
END;
CREATE:=FD;
END;
(*$I+*)


PROCEDURE ERROR;
BEGIN
  WRITELN(STR);
  ActiveProcessQ := FALSE;
  if ToPipe then RemovePipe(PipeCount);
  ENDCMD;
END;


FUNCTION MUSTCREATE(VAR NAME:XSTRING;MODE:FileModes):FILEDESC;
VAR
  FD:FILEDESC;
BEGIN
  FD:=CREATE(NAME,MODE);
  IF(FD=IOERROR)THEN BEGIN
    PUTSTR(NAME,STDERR);
    ERROR(': can''t create file')
  END;
  MUSTCREATE:=FD
END;

FUNCTION NARGS;
BEGIN
  NARGS:=CMDARGS
END;

FUNCTION OPEN(VAR NAME:XSTRING;MODE:FileModes):FILEDESC;
VAR FD:FILEDESC;
SNM:STRING80;
BEGIN
  FD:=FDALLOC;
  IF(FD<>IOERROR) THEN BEGIN
    STRNAME(SNM,NAME);
    ASSIGN(CMDText[FD],SNM);
(*$I-*)
    IF TRUE (* MODE=IOREAD *)
      THEN RESET(CMDText[FD])
      ELSE REWRITE(CMDText[FD]);
    IF(IORESULT<>0) THEN BEGIN
      XCLOSE(FD);
      FD:=IOERROR
    END
(*$I+*)
  END;
  OPEN:=FD
END;


PROCEDURE REMOVE;
VAR
  FD:FILEDESC;
BEGIN
  FD:=OPEN(NAME,IOREAD);
  IF(FD=IOERROR)THEN BEGIN
     PUTSTR(NAME,STDERR);
     WRITELN(': can''t remove file');
  END
  ELSE BEGIN
    IF Debug THEN BEGIN PUTSTR(NAME,STDERR); WRITELN(' being removed'); END;
    (* assert CMDFILE[FD]=OpenFile *)
    CLOSE(CMDText[FD]); ERASE(CMDText[FD]);
  END;
  CMDFIL[FD]:=CLOSED
END;

FUNCTION GETLINE(VAR STR:XSTRING;FD:FILEDESC; SIZE:INTEGER):BOOLEAN;
VAR I:INTEGER;
    DONE:BOOLEAN;
    CH:CHARACTER;
BEGIN
 I:=0;
 REPEAT
   DONE:=TRUE;
   CH:=GETCF(CH,FD);
   IF(CH=ENDFILE) THEN
     I:=0
   ELSE IF (CH=NEWLINE) THEN BEGIN
     I:=I+1;
     STR[I]:=NEWLINE
   END
   ELSE IF (SIZE-2<=I) THEN BEGIN
     WRITELN('LINE TOO LONG');
     I:=I+1;
     STR[I]:=NEWLINE
   END
   ELSE BEGIN
     DONE:=FALSE;
     I:=I+1;
     STR[I]:=CH
   END
 UNTIL(DONE);
 STR[I+1]:=ENDSTR;
 GETLINE:=(0<I)
END;(*GETLINE*)



PROCEDURE ENDCMD;
VAR FD:FILEDESC;
BEGIN
  if FromPipe then RemovePipe(PipeCount-ORD(ToPipe));
  if not ToPipe then PipeCount := 0;
  FOR FD:=STDIN TO MAXOPEN DO  XCLOSE(FD);
  ExitToShell;
END;

PROCEDURE XCLOSE;
BEGIN
  IF CMDFIL[FD] = OpenFile THEN CLOSE(CMDText[FD]);
  CMDFIL[FD]:=CLOSED
END;

FUNCTION ADDSTR;
BEGIN
  IF(J>MAXSET)THEN
    ADDSTR:=FALSE
  ELSE BEGIN
    OUTSET[J]:=C;
    J:=J+1;
    ADDSTR:=TRUE
  END
END;

PROCEDURE PUTSTR;
VAR I:INTEGER;
BEGIN
  I:=1;
  WHILE(STR[I]<>ENDSTR) DO BEGIN
    PUTCF(STR[I],FD);
    I:=I+1
  END
END;

FUNCTION MUSTOPEN(VAR NAME:XSTRING;MODE:FileModes):FILEDESC;
VAR FD:FILEDESC;
BEGIN
  FD:=OPEN(NAME,MODE);
  IF(FD=IOERROR)THEN BEGIN
    PUTSTR(NAME,STDERR);
    ERROR(': can''t open file.')
  END;
  MUSTOPEN:=FD
END;


FUNCTION ITOC;
BEGIN
  IF(N<0)THEN BEGIN
    S[I]:=ORD('-');
    ITOC:=ITOC(-N,S,I+1);
  END
  ELSE BEGIN
    IF (N>=10)THEN
      I:=ITOC(N DIV 10,S, I);
    S[I]:=N MOD 10 + ORD('0');
    S[I+1]:=ENDSTR;
    ITOC:=I+1;
  END
END;

PROCEDURE PUTDEC;
VAR I,ND:INTEGER;
  S:XSTRING;
BEGIN
  ND:=ITOC(N,S,1);
  FOR I:=ND TO W DO
    PUTC(BLANK);
  FOR I:=1 TO ND-1 DO
    PUTC(S[I])
END;
  
FUNCTION EQUAL;
VAR
  I:INTEGER;
BEGIN
  I:=1;
  WHILE(STR1[I]=STR2[I])AND(STR1[I]<>ENDSTR) DO
    I:=I+1;
  EQUAL:=(STR1[I]=STR2[I])
END;

