; CONCAT.MAC ; Vers equ 06 ; version number SubVers equ ' ' ; modification level ; ; Concatenates two or more source files into a destination file, ; similar to PIP. For ZCPR3 only. ; ; USAGE: ; ; CONCAT {dir:}outfile = {dir:}infile {{dir:}infile {...}} {/options} ; ; Any file without a DU or DIR specification is assumed to be on ; the current drive/user. ; ; OPTIONS: ; ; O object file mode; ignore ^Z. The default is text mode, ; checking for a ^Z (CP/M end-of-file character). ; ; Q toggle quiet mode. A configuration byte at 111h decides ; whether CONCAT will print the names of the output and ; source files (a non-zero value defaults to quiet mode). ; This option puts CONCAT in the opposite mode. Setting ; the ZCPR3 quiet flag also puts CONCAT in quiet mode, but ; this option will toggle the meaning of the quiet flag. ; ; CONCAT requires an output file and at least one input file. Actually, ; it can be used as a simple file-copy utility, but that's not its ; purpose. The same filename may be given repeatedly in the input file ; list. The equal sign separates the output file from the input files. ; Commas and/or spaces separate multiple input files. While the equal ; sign is required, it may be separated from the filenames by one or ; more spaces. A filename cannot begin with a slash unless it is ; preceded by a DU or DIR specification. ; ; If an error occurs, such as an input file not found, the incomplete ; output file is erased (and the ZCPR3 error flag is set). If another ; file has the same name as the output file, it will be renamed to ; filetype BAK. ; ; An invalid option will be ignored. If the quiet flag is set, only ; error messages will be displayed (unless the Q option is used). ; ; On error the ZCPR3 error flag is set as follows: ; 8 ambiguous output or source filename ; 10 source file not found or not given ; 11 disk or directory full (write error) ; 4 all other errors ; ; Version 0.6 -- December 3, 1989 -- Gene Pizzetta ; Minor changes in setting error flag. Corrected serious error ; in detecting top of memory, found by Howard Goldstein, who also ; suggested several changes to make the code more efficient. ; Thanks, Howard. ; ; Version 0.5 -- November 25, 1989 -- Gene Pizzetta ; Added Q (quiet) option and error flag support. Fixed parser ; bug that allowed a null output filename. Now closes output ; file on error before erasing it. Tightened code. ; ; Version 0.4 -- November 12, 1989 -- Gene Pizzetta ; Added large output buffer. Corrected error in parsing filespec ; in current user. Relaxed command line syntax: commas no longer ; required. Now obeys quiet flag. ; ; Version 0.3 -- September 19, 1989 -- Gene Pizzetta ; First preliminary release. ; ; To report bugs or make suggestions: ; Gene Pizzetta ; 481 Revere Street ; Revere, MA 02151 ; ; Newton Centre Z-Node: (617) 965-7259 ; Lilliput Z-Node: (312) 649-1730 ; Voice: (617) 284-0891 ; ; Re-assembly requires MAC or SLRMAC and SYSLIB, Version 4. With MAC, ; Z80.LIB will also be needed. ; Bdos equ 05h CpmDma equ 80h TPA equ 100h ; ; Bdos functions . . . ; FRead equ 20 FWrite equ 21 CurDsk equ 25 SetDma equ 26 CurUsr equ 32 ; LF equ 0Ah CR equ 0Dh CpmEOF equ 1Ah ; ; The following output buffer size is in sectors (records) of 128 bytes. ; It may may be set to any number of sectors up to a maximum of 256 (32K). ; BufSiz equ 256 ; output buffer in sectors ; MACLIB Z80 ; ; Following routines are from VLIB, Z3LIB, and SYSLIB, Version 4 ; ext f$exist,f$open,f$mopen,f$rename,f$delete,f$close ext logud,epstr,pfn2,crlf,cout,pafdc,initfcb,codend ext z3vinit,zfname,stndout,stndend,getquiet,gzmtop ext puter2 ; jmp Start ; db 'Z3ENV' db 1 Z3EAdr: dw 0FE00h ; address of environment descriptor ; db 'QUIET>' QtFlag: db 0 ; 0 defaults to noisy mode ; ..non-zero defaults to quiet mode ; ; Messages . . . ; MsgUse: db 'CONCAT Version ' db Vers/10+'0','.',Vers mod 10+'0',SubVers,CR,LF db 'Usage:',CR,LF db ' CONCAT {dir:}outfile = {dir:}infile {{dir:}infile {...}}' db ' {/options}',CR,LF db 'Concatenates infiles to outfile.',CR,LF db 'Options:',CR,LF db ' O object files, ignore ^Z',CR,LF db ' Q toggle quiet mode ',0 MsgUon: db 'on',0 MsgUof: db 'off',0 MsgWrt: db 'Writing ',0 MsgRd: db ' Reading ',0 MsgNIG: db 'No source file given.',0 MsgNIF: db 'File not found.',0 MsgNOG: db 'No output file given.',0 MsgREr: db 'Read error.',0 MsgWEr: db 'Disk full!',0 MsgCEr: db 'Error closing file.',0 MsgAmb: db 'No ambiguous filenames.',0 MsgDlm: db 'Illegal command line.',0 MsgDne: db 'Done!',0 ; Start: lhld Z3EAdr ; set up environment call z3vinit sspd OldStk ; save old stack pointer call gzmtop ; get top of memory sphl ; ..and setup new stack ; call codend ; set buffer addresses shld IWork lxi d,128 dad d shld OWork call getquiet ; is ZCPR quiet flag set? jrnz SetZQt ; (yes) lda QtFlag ; no, get quiet config byte SetZQt: sta OpQFlg ; ..and store in Q option flag sub a sta OpOFlg ; initialize O option flag call GetDfD ; get and store default disk and user lda CpmDma ; see if there's a tail ora a jz OptH ; (no) ; GtTail: mov c,a ; move command tail to storage inr c mvi b,0 lxi h,CpmDma+1 lxi d,CTail ldir ; call GetOpt ; get options, if any lxi h,CTail ; get output filename from tail call EatSpc ; eat any spaces ora a ; is it NUL? jrz NoOFl ; (yes, no filename) cpi '/' ; is it a slash? jrz NoOFl ; (yes, no filename) cpi '=' ; is it an equal sign? jrnz GtOFl ; (no) NoOFl: mvi a,4 ; set error flag lxi h,MsgNOG jmp ErExit ; GtOFl: lxi d,OutFcb ; ..and put it in FCB call zfname jrz CkDelm ; (okay, so far) mvi a,8 ; set error flag lxi h,MsgAmb ; it's ambiguous jmp ErExit ; CkDelm: lda FcbOFn ; make sure there's a filename cpi '!' jrc NoOFl ; (there's not) call EatSpc mov a,m cpi '=' ; equal sign after filename? jrz GtOUsr ; (yes) mvi a,4 ; set error flag lxi h,MsgDlm jmp ErExit ; GtOUsr: inx h ; get past delimiter shld TailPt ; save command tail pointer lxi h,FcbOFn lxi d,OutFil lxi b,11 ldir lxi h,TmpTyp lxi d,FcbOFt lxi b,3 ldir lda OutFcb ; get drive (A=1) ora a ; is there one? jrnz GtOUs1 ; (yes) lda DftDsk ; no, get default GtOUs1: dcr a ; make A=0 sta OutDrv ; ..and store it lda OutFcb+13 ; get user sta OutUsr ; ..and store it ; lhld TailPt call EatSpc ora a jrz NoIFl cpi '/' jrnz GtIFl NoIFl: mvi a,10 ; set error flag lxi h,MsgNIG jmp ErExit ; GtIFl: call SetIFl ; set up input FCB call OpnInp ; open input file call OpnOut ; open output file ; lda OpQFlg ; quiet option? ora a jrnz NoPrt1 ; (yes, don't print anything) lxi h,MsgWrt ; print the filename we're writing call epstr lda OutDrv mov b,a lda OutUsr mov c,a lxi d,OutFil call PrtFn lxi h,MsgRd ; ..and the file we're reading call epstr lda InDrv mov b,a lda InUsr mov c,a lxi d,FcbIFn call PrtFn ; NoPrt1: call RdLoop ; read and write files ; CkMore: call ClsIFl ; close input file lda DftUsr ; set default user for parser mov e,a mvi c,CurUsr call Bdos lhld TailPt ; any more input files? call EatSpc cpi ',' jrnz CkMor2 inx h call EatSpc CkMor2: ora a jrz NoMore cpi '/' jrz NoMore call SetIFl ; set up input FCB call OpnInp ; open input file lda OpQFlg ; quiet option? ora a jrnz NoPrt2 ; (yes, don't print anything) lxi h,MsgRd ; print filename we're reading call epstr lda InDrv mov b,a lda InUsr mov c,a lxi d,FcbIFn call PrtFn NoPrt2: call RdLoop ; read and write files jr CkMore ; Abort: push psw ; save error code push h ; save message call OutDU lxi d,OutFcb call f$close ; close output file call f$delete ; yes, erase it pop h ; get back message pop psw jmp ErExit ; NoMore: call ClsOFl ; close output file lxi h,OutTyp ; move real filetype into FCB lxi d,FcbOFt lxi b,3 ldir lxi d,OutFcb call initfcb call f$exist ; see if XXX already exists jrz SkpBak ; (it doesn't) lxi h,BakTyp lxi d,FcbOFt lxi b,3 ldir lxi d,OutFcb ; point to existing BAK name call f$delete ; erase any existing file lxi h,FcbOFn lxi d,FcbOFn+12 lxi b,11 ldir lxi h,OutFil lxi d,FcbOFn lxi b,11 ldir lxi d,OutFcb ; point to old XXX name lxi h,OutFcb+12 ; point to new BAK name call f$rename ; ..and rename file SkpBak: lxi h,TmpTyp ; rename temporary $$$ file lxi d,FcbOFt ; ..to output filename lxi b,3 ldir lxi h,OutFil lxi d,FcbOFn+12 lxi b,11 ldir lxi h,OutFcb+12 ; point to new XXX name lxi d,OutFcb ; point to old $$$ name call f$rename ; Finish: lda OpQFlg ; quiet option? ora a mvi a,0 ; reset error flag jrnz Exit ; (yes) lxi h,MsgDne ErExit: call epstr ; print message Exit: call puter2 ; set error code lspd OldStk ; restore old stack pointer ret ; ..and return to CCP ; ; Subroutines . . . ; ; SetIFl -- set up input file control block ; SetIFl: lxi d,InpFcb ; point to input FCB call zfname ; ..and parse filespec jrz GtIUsr ; (still okay) mvi a,8 ; set error flag lxi h,MsgAmb ; it's ambiguous jmp Abort ; GtIUsr: shld TailPt ; save command tail pointer lda InpFcb ; get driv (A=1) ora a ; is there one? jrnz GtIUs1 ; (yes) lda DftDsk ; no, get default GtIUs1: dcr a ; make A=0 sta InDrv ; ..and store it lda InpFcb+13 ; get user sta InUsr ; ..and store it ret ; ; OpnInp -- open input file ; OpnInp: mvi a,128 ; initialize pointer sta GetPtr sub a ; initialize end-of-file flag sta GetFlg lxi d,InpFcb call initfcb call InDU ; set drive/user for input call f$open rz ; (all okay) mvi a,10 ; set error flag lxi h,MsgNIF ; open error jmp Abort ; ; OpnOut -- open output file ; OpnOut: sub a ; initialize counters sta PutCtr sta PutSec lhld OWork ; initialize buffer pointer shld PutPtr lxi d,OutFcb call initfcb ; initialize FCB call OutDU call f$delete ; erase any existing file call f$mopen rz ; (okay) mvi a,11 ; set error flag lxi h,MsgWEr ; open error jmp ErExit ; ; RdLoop -- reads and writes until end of file ; RdLoop: call FGetC ; get a character jc RdErr ; (read error) rz ; (end of file) cpi CpmEof ; end of file? cz ChkEof ; (yes, check mode) rz ; (yes) call FPutC ; write character jc WrtErr ; (write error) jr RdLoop ; RdErr: mvi a,4 ; set error flag lxi h,MsgREr ; we have an input read error jmp Abort ; WrtErr: mvi a,11 ; set error flag lxi h,MsgWEr ; we have an output write error jmp Abort ; ; ChkEof -- checks for Option O and, if so, ignores end-of-file character ; ChkEof: mov b,a ; save character in B lda OpOFlg ; get object flag ora a mov a,b ; get character back in A ret ; ; ClsIFl -- close input file ; ClsIFl: call InDU ; close input file lxi d,InpFcb call f$close rz ; (okay) mvi a,4 ; set error flag lxi h,MsgCEr jmp Abort ; ; ClsOFl -- closes output file ; ClsOFl: call OutDU lda OpOFlg ; check option O flag ora a jrnz WrLst ; (object file transfer, skip EOF) mvi a,CpmEof ; put end-of-file character call FPutC lda PutCtr ; check pointer ora a jz WrLst ; (sector finished) mov b,a mvi a,128 ; fill rest of sector with ^Z sub b mov b,a FillZ: mvi a,CpmEof push b call FPutC pop b djnz FillZ jr WrLst ; WrLst: lded OWork ; get beginning buffer address to DE lxi h,PutPtr ; HL -> buffer pointer mov a,e ; is pointer at zero? cmp m jrnz WrLst2 ; (no) mov a,d inx h cmp m jrnz WrLst2 ; (no) lda PutCtr ; is counter at zero? ora a jrnz WrLst2 ; (no) jr ClsOut ; nothing to write, so close it ; WrLst2: call FWrtF ; write what's left ora a ; check for error jnz WrtErr ; (yes, abort) ; ClsOut: call OutDU ; close output file lxi d,OutFcb call f$close rz ; (okay) mvi a,4 ; set error flag lxi h,MsgCEr jmp ErExit ; ; GetDfD -- gets default user and stores it; gets default disk (A=0) ; and stores it (A=1) ; GetDfD: mvi e,0FFh ; get current user mvi c,CurUsr call Bdos sta DftUsr mvi c,CurDsk ; get default disk call Bdos inr a ; make it fcb compatible sta DftDsk ret ; ; PrtFn -- Prints drive/user and filename on console ; PrtFn: call stndout mov a,b ; get drive adi 'A' ; make it printable call cout ; ..and print it mov a,c ; get user call pafdc ; ..and print it mvi a,':' call cout call pfn2 ; print filename call stndend call crlf ret ; ; EatSpc -- gobbles up spaces ; EatSpc: mov a,m cpi ' ' ; is it a space? inx h jrz EatSpc ; (yes) dcx h ret ; ; OutDU -- sets default drive and user for output file ; OutDU: lda OutUsr mov c,a lda OutDrv mov b,a call logud ret ; ; InDU -- sets default drive and user for input file ; InDU: lda InUsr mov c,a lda InDrv mov b,a call logud ret ; ; FGetC -- returns character from file. Assumes file has been ; successfully opened. Returns character or ^Z (end-of-file) in ; A. Zero set (Z) on end of file. Carry set (C) if error. ; FGetC: lda GetFlg ; check end-of-file flag ora a jrnz GetEof ; (yes) lda GetPtr ; get pointer cpi 128 ; done with buffer? jrc GetChr ; (no, get a character) lhld IWork xchg mvi c,SetDMA ; set DMA address call Bdos call InDU ; set DU lxi d,InpFcb mvi c,FRead ; read more file call Bdos cpi 1 ; return code? jrz GetEof ; (end of file) jrnc GetErr ; (a problem) sta GetPtr ; put 0 in pointer ; GetChr: lhld IWork ; point to DMA buffer mov e,a ; put pointer in DE mvi d,0 dad d ; add it to HL mov a,m ; get next character lxi h,GetPtr ; increment pointer inr m stc cmc ; clear carry ret ; GetEof: mvi a,CpmEof sta GetFlg ; set end-of-file flag stc cmc ; clear carry ret ; GetErr: mvi a,CpmEof sta GetFlg stc ; set carry ret ; ; FPutC -- Writes character to file. Assumes file has been successfully ; opened. Expects character in A. Returns carry set (C) on error. ; FPutC: mov c,a ; save character in C lda PutCtr ; get counter cpi 128 ; buffer full? jrc PutChr ; (no, so do it) push b ; the character is threatened from all sides lda PutSec ; get sector count cpi BufSiz-1 ; end of buffer? jrz FPutC2 ; (yes, we need to write it) inr a ; increment sector count sta PutSec ; ..and store it lhld PutPtr ; get current sector pointer lxi d,128 ; ..add 128 to it dad d shld PutPtr ; ..and store it xra a ; make A = 0 jr FPutC3 ; FPutC2: call FWrtF ; write buffer to disk push psw lhld OWork ; reset buffer pointer shld PutPtr xra a ; reset sector counter sta PutSec pop psw ; FPutC3: pop b ; get back output character ora a ; return code? jrnz PutErr ; (problem) sta PutCtr ; reset counter to 0 ; PutChr: lhld PutPtr ; point to current DMA buffer mov e,a ; move counter to DE mvi d,0 dad d ; ..and add it to HL mov m,c ; write character lda PutCtr inr a ; increment counter sta PutCtr sub a ; clear carry ret ; PutErr: stc ; set carry ret ; ; FWrtF -- write output buffer to disk ; FWrtF: lda PutSec ; get buffer sector count mov b,a ; put it in B lded OWork ; point to beginning of buffer FWrtF2: push b ; save sector count push d ; save DMA address call WrtSec ; write the sector pop h ; DMA address recovered in HL pop b ; get back sector count ora a ; write error? rnz ; (yes) cmp b ; end of buffer? rz ; (yes) dcr b ; decrement sector count lxi d,128 ; increment DMA address dad d xchg jr FWrtF2 ; WrtSec: mvi c,SetDma ; set DMA address call Bdos call OutDu ; set drive and user lxi d,OutFcb ; ..and write sector mvi c,FWrite call Bdos ret ; ; GetOpt -- checks command tail for user supplied options and sets ; appropriate option flags. Invalid options are ignored. ; GetOpt: lxi h,CTail ; point to command tail lda CpmDma ; anything there? ora a rz ; (no) mov b,a ; yes, put number of chars in B ScnDLp: mov a,m ; get character cpi '/' ; delimiter? jz ScnOpt ; (yes) mov d,a ; save character ScnDL2: inx h ; no, keep looking djnz ScnDLp ret ; (none found, return) ; ScnOpt: push psw ; save current character mov a,d ; get back previous character pop d ; put current character in D cpi ' ' ; was previous char a space? jnz ScnDL2 ; (no) jmp ScnOp2 ; ScnOLp: call ScnTbl xchg ; point back to options ScnOp2: inx h djnz ScnOLp ; loop through options ret ; ScnTbl: mov c,m ; put option in C lxi d,OptTbl ; point DE to option table ScnTLp: ldax d ; get table option ora a ; end of table? jz NoMat ; (yes, no match) inx d ; no, keep looking cmp c ; match? jz TMatch ; (yes) inx d ; move pointer to next entry inx d jmp ScnTLp ; ..and keep looking ; NoMat: xchg ret ; TMatch: push h ; save option pointer ldax d ; put address from table into HL mov l,a inx d ldax d mov h,a pop d ; recover option pointer in DE mvi a,1 ; set option flag by jumping to pchl ; ..table routine and returning ; ; OptTbl -- Option Jump Table ; OptTbl: db '/' ; / = usage message dw OptH db 'O' ; O = object file transfer dw OptO db 'Q' ; Q = toggle configured quiet flag dw OptQ db 0 ; end of option jump table ; ; Option setting routines ; OptH: lxi h,MsgUse ; print usage message call epstr lda OpQFlg ; check quiet mode ora a mvi a,0 ; reset error flag jrz OptH2 ; (off) lxi h,MsgUof jmp ErExit OptH2: lxi h,MsgUon jmp ErExit ; OptO: sta OpOFlg ret ; OptQ: lda OpQFlg ; get Q flag ora a jrz OptQ2 ; (not set) sub a sta OpQFlg ; zero it ret OptQ2: mvi a,1 sta OpQFlg ; set it ret ; ; Data storage . . . ; OutFil: db ' ' ; save original output filename here OutTyp: db ' ' BakTyp: db 'BAK' ; for BAK file TmpTyp: db '$$$' ; for temporary filename ; InpFcb: db 0 ; input file fcb FcbIFn: db ' ' FcbIFt: db ' ' ds 24 ; OutFcb: db 0 ; output file fcb FcbOFn: db ' ' FcbOFt: db ' ' ds 24 ; DSEG ; ; Uninitialized storage . . . ; OpOFlg: ds 1 OpQFlg: ds 1 ; GetFlg: ds 1 ; FGetC end-of-file flag GetPtr: ds 1 ; FGetC pointer PutCtr: ds 1 ; FPutC counter PutPtr: ds 2 ; FPutC pointer PutSec: ds 1 ; FPutC sector counter IWork: ds 2 ; input buffer address OWork: ds 2 ; output buffer address DftUsr: ds 1 ; default user area DftDsk: ds 1 ; default drive InDrv: ds 1 ; input file drive InUsr: ds 1 ; input file user OutDrv: ds 1 ; output file drive OutUsr: ds 1 ; output file user OldStk: ds 2 ; old stack pointer TailPt: ds 2 ; command tail index pointer CTail: ds 128 ; command tail storage ; end