; DETABZ.MAC ; Vers equ 14 ; version number SubVers equ ' ' ; modification level ; ; Replaces ASCII tabs in text files with the correct number of spaces. ; For ZCPR3 only. ; ; USAGE: ; ; DETABZ {dir:}infile {dir:}{outfile} {{/}options} ; ; If no outfile name is given, it will have the same name as the infile. ; If a file exists with the same name as the outfile, it will be renamed ; to filetype BAK. ; ; OPTIONS: ; ; n Increment for tab expansion (1-255). If no increment is ; given, the CP/M and MS-DOS default of 8 is used. ; ; Q Toggles quiet mode. Quiet mode is set by the ZCPR3 quiet ; flag or by a configuration byte (non-zero defaults to ; quiet mode). Option Q toggles the current mode. ; ; For more information, see accompanying documentation file. ; ; Version 1.4 -- November 28, 1990 -- Gene Pizzetta ; Increased size of input buffer and made several other changes to ; speed up processing. Now properly handles files that do not end ; with a ^Z. Corrected bug that caused two sectors of ^Z's to be ; added to a file if it ended exactly on a sector boundary. ; Implemented several suggestions from Howard Goldstein to improve ; the code. ; ; Version 1.3 -- October 5, 1990 -- Gene Pizzetta ; Corrected bugs that caused unpredictable destination for outfile if ; no filespec was given and a jump to the wrong error routine if the ; input file was not found. Now checks for sufficient TPA space. ; Several other code optimizations, mostly at the suggestion of ; Howard Goldstein. (Thanks, Howard.) ; ; Version 1.2 -- September 22, 1990 -- Gene Pizzetta ; Corrected bug that caused incorrect error messages to be displayed ; under some circumstances. ; ; Version 1.1 -- September 1, 1990 -- Gene Pizzetta ; If source file has a modify date, but no create date, the original ; modify date will become the create date of the new file. ; ; Version 1.0 -- August 25, 1990 -- Gene Pizzetta ; Initial release. ; ; Gene Pizzetta ; 481 Revere Street ; Revere, MA 02151 ; ; Newton Centre Z-Node: (617) 965-7259 ; Ladera Z-Node Central: (213) 670-9465 ; GEnie: E.PIZZETTA ; Voice: (617) 284-0891 ; ; System addresses . . . ; CpmFcb equ 5Ch AltFcb equ 6Ch CpmDma equ 80h ; ; ASCII characters . . . ; CtrlC equ 03h ; ^C BS equ 08h ; backspace TAB equ 09h ; tab LF equ 0Ah ; linefeed CR equ 0Dh ; carriage return CpmEof equ 1Ah ; CP/M end-of-file character (^Z) ; ; The following buffer sizes are in sectors (records) of 128 bytes. ; They may may be set to any number of sectors up to a maximum of 255. ; Of course, you can't have two buffers of 32K if you plan to have ; DETABZ and your operating system in memory at the same time. ; InSiz equ 128 ; input buffer in sectors (128=16K) OutSiz equ 128 ; output buffer in sectors (128=16K) ; ; Following routines are from ZSLIB, VLIB, Z3LIB, and SYSLIB ; .request zslib,vlib,z3lib,syslib ; ext getstp,setstp ext z3vinit,stndout,stndend ext zsyschk,getquiet,gzmtop,prtname,puter2,inverror ext f$open,f$make,f$rename,f$delete,f$close,f$read,f$write ext initfcb,setdma,codend,retud,logud,condin,cout,epstr ext pfn2,pafdc,phlfdc,isdigit,eval10 ; public pstr ; jp Start ; db 'Z3ENV' db 1 Z3EAdr: dw 0FE00h ; address of environment descriptor ; ; Configuration . . . ; dw 0 ; filler db 'DETABZ' ; filename for ZCNFG CFG file db Vers/10+'0',Vers mod 10+'0' QtFlag: db 0 ; ..non-zero defaults to quiet mode DefInc: db 8 ; default tab increment (8 recommended) ; ; Messages . . . ; MsgUse: db 'DETAB-Z Version ' db Vers/10+'0','.',Vers mod 10+'0',SubVers,CR,LF db 'Replaces ASCII tabs with spaces.',CR,LF db 'Usage:',CR,LF,' ',0 MsgUs1: db ' {dir:}infile {dir:}{outfile} {{/}options}',CR,LF db 'An input filename is required.',CR,LF db 'Options:',CR,LF db ' n tab increment (1-255, default ',0 MsgUs2: db ')',CR,LF db ' Q toggle quiet mode o',0 MsgUon: db 'n',0 MsgUof: db 'ff',0 MsgRd: db ' Detabbing ',0 MsgWrt: db ' to ',0 MsgDot: db ' .. ',0 MsgNSr: db ' No source file.',0 MsgFNF: db ' File not found.',0 MsgWEr: db ' Disk full!',0 MsgNSp: db ' No directory space.',0 MsgCEr: db ' Close error.',0 MsgAmb: db ' Ambiguous filename.',0 MsgDir: db ' Invalid directory.',0 MsgBdO: db ' Invalid option.',0 MsgBdN: db ' Invalid increment.',0 MsgMem: db ' Insufficient memory.',0 MsgAbt: db ' Aborted.',0 MsgDne: db ' tabs replaced.',0 ; Start: ld hl,(Z3EAdr) ; set up environment call zsyschk ; is this a Z-system? ret nz ; (nope) call z3vinit ld (OldStk),sp ; save old stack pointer call gzmtop ; get top of memory ld sp,hl ; ..and set up new stack push hl ; save top of memory ; call codend ; set buffer addresses ld (InBuf),hl ld de,128*InSiz add hl,de ld (OutBuf),hl ld de,128 ; add stack size and safety buffer add hl,de ex de,hl ; put in DE pop hl ; recover top of memory in HL or a sbc hl,de ; determine memory size ld a,h ; put number of pages in A cp OutSiz/2 ; compare to buffer size jr nc,Start1 ; (TPA okay) ld a,12 ; set error code ld hl,MsgMem ; insufficient memory jp ErExit ; Start1: call getquiet ; is ZCPR quiet flag set? rra ; make it 0 or FF sbc a,a jr nz,Start2 ; (yes) ld a,(QtFlag) ; no, get quiet config byte Start2: ld (OpQFlg),a ; ..and store in Q option flag ld a,(DefInc) ; get default tab increment ld (TabInc),a ; ..and store it ld hl,0 ; clear tab counter ld (TabCnt),hl ld hl,CpmDma ; see if there's a tail ld a,(hl) or a jp z,Usage ; (no) call GetOpt ; get options ; Check for input file specification ld a,(FSFlag) ; any filespec? or a jp z,Usage ; (nope) ld a,(CpmFcb+1) ; check input filename cp ' ' ; is there one? jp nz,Start3 ; (yep) ld hl,MsgNSr ; source file required ld a,8 ; set error code jp ErExit ; Start3: ld hl,CpmFcb+1 ; check for ambiguous name call ChkAmb MovInp: ld hl,CpmFcb+1 ; move filename to input fcb ld de,InFcb+1 ld bc,11 ldir ld a,(CpmFcb+15) ; check for valid directory or a jp nz,InvDir ; (nope) ld a,(CpmFcb) ; get drive (A=1) or a ; is there one? call z,GetDft ; (no, get default) dec a ; make A=0 ld b,a ; put drive in B ld a,(CpmFcb+13) ; get user ld c,a ; put user in C ld (InUsr),bc ; store user and drive ; Check for output file specification ld a,(FSFlag) dec a jr z,MovOut ; (no output filespec) ld a,(AltFcb+1) cp ' ' ; do we have an output filename? jr z,MovOut ; (no) ld hl,AltFcb+1 ; check for ambiguous name call ChkAmb ld hl,AltFcb+1 ; okay, move filename jr MovFn ; MovOut: ld hl,CpmFcb+1 ; move input filename to output fcb MovFn: ld de,OutFcb+1 ld bc,11 ldir ld a,(AltFcb+15) ; check for valid directory or a jp nz,InvDir ; (nope) ld a,(AltFcb) ; get drive (A=1) or a ; is there one? call z,GetDft ; (no, get default) dec a ; make A=0 ld b,a ; put drive in B ld a,(AltFcb+13) ; get user ld c,a ; put user in C ld (OutUsr),bc ; store user and drive ld hl,OutFcb+1 ; save real output filename ld de,OutFn+1 ld bc,11 ldir ld hl,TmpTyp ; make filetype $$$ ld de,OutFcb+9 ld bc,3 ldir ; ld a,(OpQFlg) ; quiet option? or a ld hl,MsgRd ; print the file we're reading call z,epstr ld bc,(InUsr) ; B=drive, C=user ld de,InFcb+1 call z,PrtFn call OpnInp ; open input file call GetIni ; initialize GetC ; ld a,(OpQFlg) ; quiet option? or a ld hl,MsgWrt ; ..and the file we're writing call z,epstr ld bc,(OutUsr) ; B=drive, C=user ld de,OutFn+1 call z,PrtFn call OpnOut ; open output file call PutIni ; initialize PutC ; ld a,(OpQFlg) ; quiet? or a ld hl,MsgDot call z,epstr ; ld de,0 ; zero column count call RdLoop ; read and write files call ClsInp ; close input file call WrLast ; close output file call DatStp ; get date stamp, if any ; call OutDU ld hl,BakTyp ; move BAK filetype to FCB ld de,OutFcb+9 ld bc,3 ldir ld de,OutFcb ; point to existing BAK name call initfcb call f$delete ; blind erase any existing BAK file ld de,OutFn ; point to old xxx name ld hl,OutFcb ; point to new BAK name call f$rename ; ..and blind rename any existing file ld hl,TmpTyp ; rename temporary $$$ file ld de,OutFcb+9 ; ..to output filename ld bc,3 ldir ld hl,OutFn ; point to new xxx name ld de,OutFcb ; point to old $$$ name call f$rename ; Finish: ld a,(OpQFlg) ; quiet option? or a ld a,0 ; reset error flag jr nz,Exit ; (yes) ld hl,(TabCnt) ; get tab count call phlfdc ld hl,MsgDne ErExit: call epstr ; print message Exit: call puter2 ; set error code ld b,a ; put error code in B or a ; do we have one? call nz,inverror ; (yes, call error handler) ld sp,(OldStk) ; restore old stack pointer ret ; ..and return to CCP ; UAbort: cp CtrlC ; control-C? ret nz ; (nope, continue) ld a,4 ld hl,MsgAbt Abort: push af ; save error code call OutDU ld de,OutFcb call f$close ; close output file call f$delete ; erase it pop af ; restore error code jr ErExit ; InvDir: ld a,2 ; invalid directory ld hl,MsgDir jr ErExit ; ; Subroutines . . . ; ; OpnInp -- open input file ; OpnInp: ld de,InFcb call initfcb call InDU ; set drive/user for input call f$open ret z ; (all okay) ld a,10 ; set error flag ld hl,MsgFNF ; file not found jr ErExit ; ; OpnOut -- open output file ; OpnOut: ld de,OutFcb call initfcb ; initialize FCB call OutDU call f$make ; create and open file cp 0FFh ret nz ; (okay) ld a,11 ; set error flag ld hl,MsgNSp ; no directory space jp ErExit ; ; RdLoop -- reads and writes until end of file ; RdLoop: call GetC ; get a character ret z ; (end of file) cp CpmEof ; end of file? ret z ; (yes) ; cp TAB ; is it a tab? jp z,IsTab ; (yes) call PutC ; output character jr c,WrtErr ; (write error) cp ' ' ; is it a control character? jp nc,RdLp3 ; (no) cp CR ; is it a carriage return? jp nz,RdLp2 ; (no) ld de,0 ; zero column counter jr RdLp4 ; RdLp2: cp BS ; backspace? jp nz,RdLp4 ; (no) ld h,a ; save character ld a,d ; test column count for zero or e ld a,h ; get back character jp z,RdLp4 ; column count is 0 (don't back up) dec de ; decrement column count jr RdLp4 ; RdLp3: cp 07Fh jp nc,RdLp4 inc de ; increment column count ; RdLp4: call condin ; user aborting? jr z,RdLoop ; (nope) cp CtrlC jr nz,RdLoop ld a,4 ; we're aborting ld hl,MsgAbt jp Abort ; ; IsTab -- We have a tab, so expand it ; IsTab: ld hl,(TabCnt) ; increment tab counter inc hl ld (TabCnt),hl ld a,(TabInc) ; get increment ld h,d ; duplicate column counter in HL ld l,e ld b,16 ; loop count in B ld c,a ; divisor in C xor a ; zero remainder IsTab1: add hl,hl ; shift left, dividend bit in carry rla ; accumulate dividend bit in A cp c ; size of divisor yet? jr c,IsTab2 ; (yes) sub c ; subtract divisor from remainder inc l ; make quotient low bit 1 IsTab2: djnz IsTab1 ; (repeat 16 times) neg ; negative of column mod TabInc ld hl,TabInc add a,(hl) ; A is number of spaces needed ld b,a ; B is loop counter ld a,' ' IsTab3: call PutC ; output space jr c,WrtErr ; (write error) inc de ; increment column counter djnz IsTab3 jr RdLp4 ; WrtErr: ld a,11 ; set error flag ld hl,MsgWEr ; no disk space jp Abort ; ; ClsInp -- close input file ; ClsInp: call InDU ; close input file ld de,InFcb call f$close ret z ; (okay) ld a,4 ; set error flag ld hl,MsgCEr ; close error jp Abort ; ; GetDft -- gets default drive and user ; GetDft: call retud ; C=user, B=drive (A=0) ld a,b ; A=drive (A=0) inc a ; A=drive (A=1) ret ; ; PrtFn -- Prints drive/user and filename on console ; PrtFn: call stndout ld a,b ; get drive add a,'A' ; make it printable call cout ; ..and print it ld a,c ; get user call pafdc ; ..and print it ld a,':' call cout call pfn2 ; print filename call stndend ret ; ; EatSpc -- gobbles up spaces and tabs ; EatSpc: ld a,(hl) or a ret z inc hl cp ' ' ; is it a space? jr z,EatSpc ; (yes) cp TAB ; is it a tab? jr z,EatSpc ; (yes) dec hl ret ; ; OutDU -- sets default drive and user for output file ; OutDU: push bc ld bc,(OutUsr) ; B=drive, C=user call logud pop bc ret ; ; InDU -- sets default drive and user for input file ; InDU: push bc ld bc,(InUsr) ; B=drive, C=user call logud pop bc ret ; ; GetC -- returns character from file. Assumes file has been ; successfully opened and GetIni has been called. Returns character ; or ^Z (end-of-file) in A. Zero set (Z) on end of file. ; Affected: AF ; GetC: push bc push de push hl ld a,(GetCnt) ; get counter cp 128 ; done with buffer? jr nc,GetC1 ; (yes, get next sector) GetChr: ld hl,(GetPtr) ; point to current DMA buffer ld e,a ; move counter to DE ld d,0 add hl,de ; ..and add it to HL inc a ; increment counter ld (GetCnt),a ld a,(hl) ; get next character cp CpmEof GetEof: pop hl pop de pop bc ret ; GetC1: call condin call nz,UAbort ; (check for abort) ld a,(GetMax) ld d,a ld a,(GetSec) ; get sector count cp d ; end of buffer? jr z,GetC2 ; (yes, we need to read more) inc a ; increment sector count ld (GetSec),a ; ..and store it ld hl,(GetPtr) ; get current sector pointer ld de,128 ; ..add 128 to it add hl,de ld (GetPtr),hl ; ..and store it xor a jr GetChr ; GetC2: ld a,(GetFlg) ; check end-of-file flag cp CpmEof jr z,GetEof ; (end of file) call InDU call ReadF ld hl,(InBuf) ld (GetPtr),hl xor a ld (GetSec),a jr GetChr ; ; GetIni -- must be called before first call to GetC. ; GetIni: ld hl,0 ; initialize flags ld (GetMax),hl ld (GetFlg),hl ; and GetSec ld a,128 ld (GetCnt),a ld hl,(InBuf) ; initialize pointer ld (GetPtr),hl ret ; ; ReadF -- read disk into input buffer ; ReadF: ld a,InSiz ld b,a dec a ld (GetMax),a ld hl,(InBuf) ReadF2: call RdSec jr nz,ReadF3 ld de,128 add hl,de djnz ReadF2 ret ; ReadF3: inc b ; we didn't read that last sector ld a,CpmEof ld (GetFlg),a ld a,InSiz ; store number of sectors read sub b ld (GetMax),a ret ; RdSec: call setdma ; set DMA address ld de,InFcb ; ..and read sector call f$read ret ; ; PutC -- Writes character to file. Assumes file has been successfully ; opened and PutIni has been called. Expects character in A. Returns ; carry set (C) on error (disk or directory full). ; Affected: Flags. ; PutC: push bc push de push hl ld c,a ; save character in C ld a,(PutCnt) ; get counter cp 128 ; buffer full? jr nc,PutC1 PutChr: ld hl,(PutPtr) ; point to current DMA buffer ld e,a ; move counter to DE ld d,0 add hl,de ; ..and add it to HL ld (hl),c ; write character inc a ; increment counter ld (PutCnt),a sub a ; clear carry PutEnd: ld a,c ; recover character pop hl pop de pop bc ret ; PutC1: ld a,(PutSec) ; get sector count inc a ; increment it cp OutSiz ; end of buffer? jr z,PutC2 ; (yes, we need to write it) ld (PutSec),a ; store new sector count ld hl,(PutPtr) ; get current sector pointer ld de,128 ; ..add 128 to it add hl,de ld (PutPtr),hl ; ..and store it xor a ; make A = 0 jr PutChr ; PutC2: call OutDU call WrtF ; write buffer to disk jr nz,PutErr ; (disk full) ld hl,(OutBuf) ; reset buffer pointer ld (PutPtr),hl xor a ; reset sector counter ld (PutSec),a jr PutChr ; PutErr: scf ; set carry jr PutEnd ; ; PutIni -- must be called before first call to PutC. ; PutIni: ld hl,0 ; initialize counters ld (PutCnt),hl ; and PutSec ld hl,(OutBuf) ; initialize buffer pointer ld (PutPtr),hl ret ; ; WrtF -- write output buffer to disk ; WrtF: ld a,(PutSec) ; get buffer sector count ld b,a ; put it in B inc b ld hl,(OutBuf) ; point to beginning of buffer WrtF2: call WrtSec ; write the sector ret nz ; (write error) ld de,128 ; increment DMA address add hl,de djnz WrtF2 ret ; WrtSec: call setdma ; set DMA address ld de,OutFcb ; ..and write sector call f$write ret ; ; WrLast -- writes remaining buffer contents and closes output file. ; WrLast: ld a,CpmEof ; put end-of-file character call PutC jp c,WrtErr ld hl,PutCnt ; point to current sector count ld a,128 sub (hl) jr z,WrLst1 ; (at end of sector) ld b,a ld a,CpmEof ; fill rest of sector with ^Z FillZ: call PutC jp c,WrtErr djnz FillZ WrLst1: ld de,(OutBuf) ; get beginning buffer address to DE ld hl,PutPtr ; HL -> buffer pointer ld a,e ; is pointer at zero? cp (hl) jr nz,WrLst2 ; (no) ld a,d inc hl cp (hl) jr nz,WrLst2 ; (no) ld a,(PutCnt) ; is counter at zero? or a jr nz,WrLst2 ; (no) jr ClsOut ; nothing to write, so close it ; WrLst2: call OutDU call WrtF ; write what's left jp nz,WrtErr ; (disk full, abort) ; ClsOut: call OutDU ; close output file ld de,OutFcb call f$close ret z ; (okay) ld a,4 ; set error flag ld hl,MsgCEr ; close error jp Abort ; ; GetOpt -- checks command tail for user supplied options and sets ; appropriate option flags. Also sets FSFlag to number of filespecs ; found. ; GetOpt: xor a ; initialize FSFlag ld (FSFlag),a ld hl,CpmDma ; point to command tail ld a,(hl) ; anything there? or a ret z ; (no) inc hl call EatSpc or a ret z ; (end of tail) cp '/' jr z,IsSlsh ld a,(FSFlag) ; increment number of filespecs inc a ld (FSFlag),a FnLp1: inc hl ; move past first filename ld a,(hl) or a ret z ; (end of tail) cp ' ' jr z,GetOp1 cp TAB jr nz,FnLp1 ; GetOp1: call EatSpc or a ret z ; (end of tail) cp '/' ; is it a slash? jr z,IsSlsh ; (yes) ld a,(FSFlag) ; increment number of filespecs inc a ld (FSFlag),a FnLp2: inc hl ; move past second filename ld a,(hl) or a ret z ; (end of tail) cp ' ' jr z,GetOp2 cp TAB jr nz,FnLp2 ; GetOp2: call EatSpc or a ret z ; (end of tail) cp '/' ; a slash? jr nz,GotOpt ; (no, get options) IsSlsh: inc hl ; move past slash GotOpt: ld a,(hl) ; get option inc hl or a ret z ; (end of tail) cp ' ' ; space? jr z,GotOpt ; (skip it) cp '/' ; usage request? jr z,Usage ; (yes) cp 'Q' ; quiet flag? jr z,OptQ ; (yes) call isdigit ; check for digit? jr z,OptI ; (yes) ld a,19 ; set invalid option error ld hl,MsgBdO jp ErExit ; ; Option setting routines ; OptQ: ld a,(OpQFlg) ; get Q flag cpl ; flip it ld (OpQFlg),a ; store it jr GotOpt ; OptI: dec hl ; point HL to number string call eval10 ; convert to binary ld a,d ; is it less than 256? or a jr nz,OptI2 ; (yes) ld a,e ; it it greater than 0? or a jr z,OptI2 ; (no) ld (TabInc),a ; store it jr GotOpt ; OptI2: ld a,9 ; it's a bad number ld hl,MsgBdN jp ErExit ; Usage: ld hl,MsgUse call epstr call prtname ld hl,MsgUs1 call epstr ld a,(DefInc) call pafdc ld hl,MsgUs2 call epstr ld a,(OpQFlg) ; check quiet mode or a ld a,0 ; reset error flag ld hl,MsgUof jp nz,ErExit Usage2: ld hl,MsgUon jp ErExit ; ; DatStp -- Get create stamp from original file, if available, and ; transfer it to new file. ; DatStp: call InDU ; setup for input file ld de,InFcb ld hl,CpmDma ; point to DMA buffer call getstp ; get ZSDOS file stamp ret nz ; (error) ld a,(CpmDma+1) ; check for create date or a jr nz,DatSt1 ; (we've got a create date) ld a,(CpmDma+11) ; none, so check for modify date or a ret z ; (no date stamp) ld hl,CpmDma+10 ; point to modify date jr DatSt2 DatSt1: ld hl,CpmDma ; point to create date DatSt2: ld de,StpTmp ; move date to storage ld bc,5 ldir call OutDU ; setup for output file ld de,OutFcb ld hl,CpmDma ; point to DMA buffer call getstp ; get file stamp ret nz ; (error) ld hl,StpTmp ld de,CpmDma ; move old create stamp to date string ld bc,5 ldir ld de,OutFcb ; setup for file stamping ld hl,CpmDma call setstp ; set file stamp ret ; ; ChkAmb -- checks for ambiguous filename. Expects address of filename ; in HL. ; ChkAmb: ld bc,11 ld a,'?' cpir ret nz ; (no question marks) ld a,8 ; ambiguous filename ld hl,MsgAmb jp ErExit ; ; PSTR -- forces ZSYSCHK to use EPSTR ; pstr: jp epstr ; ; Data storage . . . ; BakTyp: db 'BAK' ; for BAK file TmpTyp: db '$$$' ; for temporary filename ; DSEG ; ; Uninitialized storage . . . ; OutFn: ds 12 ; save original output filename here InFcb: ds 36 ; input file fcb OutFcb: ds 36 ; output file fcb StpTmp: ds 5 ; temporary create date storage TabCnt: ds 2 ; number of tabs replaced TabInc: ds 1 ; current tab increment OpQFlg: ds 1 ; FFh=quiet mode FSFlag: ds 1 ; number of filespecs (set by GetOpt) GetMax: ds 1 ; number of sectors read into input buffer GetFlg: ds 1 ; GetC end-of-file flag GetSec: ds 1 ; GetC sector counter GetCnt: ds 1 ; GetC counter GetPtr: ds 2 ; GetC pointer PutCnt: ds 1 ; PutC counter PutSec: ds 1 ; PutC sector counter PutPtr: ds 2 ; PutC pointer InUsr: ds 1 ; input file user InDrv: ds 1 ; input file drive OutUsr: ds 1 ; output file user OutDrv: ds 1 ; output file drive OldStk: ds 2 ; old stack pointer InBuf: ds 2 ; input buffer address OutBuf: ds 2 ; output buffer address ; end