dlnash@ut-ngp.UUCP (Donald L. Nash) (12/07/85)
Well, folks, here it is. If you read net.micro.pc, then you may have read my posting "advertising" a more program for the IBM PC which is much better that the one supplied with PC-DOS. Well, I got enough responces to that posting to prompt me to post the program to net.sources. Here is the source code. Don Nash UUUU UUUU UU UU UUCP: ...!{ihnp4,allegra,seismo!ut-sally}!ut-ngp!dlnash UU TTTTTTTTTUUTTT APRA: dlnash@ngp.UTEXAS.EDU UU TT TT UU TT UU TT UU UU TTUU UUUUUUUU The University of Texas at Austin TT Hook 'em Horns! TTTT ------------------------cut here------------------------- /* PC-More, an improved pager for the IBM PC running PC-DOS 2.0 or later. */ #include <stdio.h> #define TRUE 1 #define FALSE 0 #define NCPL 79 /* Number of characters per line. */ #define NLPS 24 /* Number of lines per screen. */ #define NLPHS 12 /* Number of lines per half screen. */ #define CMASK 0377 /* Masks high bits of an int to zero. */ int x_mnl, x_srf, x_argc; long x_rwp; char x_file[128], *x_argv[128]; char *x_spc = " "; FILE *x_fp = 0; /* x_mnl is the number of line to print before calling wait(). x_srf if a flag which is set to 1 if stdin is redirected, 0 if not. This is determined by the number of parameters on the command line. If there are no parameters on the command line, then it is assumed that redirection is taking place. If there are parameters on the command line, then it is assumed that there is no redirection. This is not neccessarily the case, but it is usually safe to assume that no one will type something like this: dir|more file.ext If something like this is typed, then More will assume that stdin is not redirected, even though it is. This can produce bad results if a push of some sort it attempted (via the '!' or 'e' commands). Unfortunately, there is no other way to detect if stdin is being redirected. This construction will work if something like the above example is not typed. By the way, that example will read from file.ext, not from the pipe. x_rwp is the offset relative to the beginning of file which is given to fseek() in the bang() function. The fseek() will return the file to where it was left before the push took place. x_fp is the file pointer from which data is read. x_file is an array which holds the name of the file currently being read from. it is large enough to hold a very long pathname. x_argc and x_argv are the external copies of argc and argv. x_spc points to the string used by several functions to clear the screen. */ main(argc, argv) int argc; char *argv[]; { int ci, c, nc, nl, nlp, ts, i; char *strcpy(); /* c is ci with the upper 8 bits masked to zero. ci is the character read from the file. i is a working variable used in for loops, etc. nc is the number of characters on the current line. It is used to detect lines that are too long and to determine where the next tab stop is. nl is used to determine when to exit the for loop and call wait. nlp is used to determine when to clear the screen. The screen is only cleared when it is full. ts is a working variable for the tab expander. It is the number of spaces to the next tab stop. */ x_argc = argc; /* \ These 3 lines set up the external */ for (i = 0; i < argc; i++) /* > copies of argc and argv. */ x_argv[i] = argv[i]; /* / */ if (argc >= 2) { nextfile(1); /* This function will set x_fp and x_file. */ x_srf = FALSE; /* This flag set to 0 if not reading from stdin. */ } else { x_fp = stdin; strcpy(x_file, "stdin"); x_srf = 1; /* This flag set to 1 if reading from stdin. */ } x_mnl = NLPS; /* Start off by outputing one screenful. */ nc = 0; /* Haven't put out any characters yet. */ nlp = 24; /* This will make sure that the screen is cleared. */ while(TRUE) { /* Loop forever, guts of the loop will exit the loop. */ if ((nlp >= NLPS) && (x_mnl != 1)) { /* The (x_mnl != 1) condition keeps the screen from being cleared if only one line is to be displayed. */ crt_cls(); /* This clears the screen. */ crt_mode(7); /* crt_cls() is broken, this makes chars print. */ nlp = 0; /* Reset number of lines printed. */ } for (nl = 0; nl < x_mnl; ) { /* Put out x_mnl number of lines. */ if ((ci = getc(x_fp)) == EOF) { wait(1); /* Prompt for next file. */ nlp = 0; /* Wait(1) will clear screen, so reset nlp */ nl = 0; /* and nl to 0 to reflect this. */ ci = '\0'; /* Ci will still be printed, so make it a null. */ } c = ci & CMASK; /* Mask off upper 8 bits for safety. */ if (c == '\t') { /* Tab expander */ if ( nc > NCPL - 6) { /* This is part 1 (see notes below). */ c = '\n'; } else { /* This is part 2. */ ts = 8 - (nc % 8); /* Find spaces to next tab. */ if (ts == 8) /* If it comes out to be 8, */ ts = 0; /* then it should be 0. */ for (i = 1; i < ts; i++) { nc++; /* Increment # of chars. */ } /* for */ } /* else */ } /* End of tab expander */ /* If 73 or more chars have been printed out, then part 1 of the tab expander will turn the tab into a newline. In this program, tab stop 81 is the same as column 1 on the next line. Since 81 is the next tab stop after 73, changing the tab into a newline is the correct solution. The rest of the program does not even know that this has happened and simply thinks that a newline was read from the file. If 72 or less chars have been printed, then part 2 finds out how many spaces to the next stop and increments nc the proper number of times minus 1. The tab is not actually expanded, the char count is just incremented. When the tab prints, it is automagically expanded. It is important that this tab expander should come before newline detection and the truncate-too-long-line algorithm. */ if ((nc >= NCPL) && (c != '\n')) { /* If beyond right margin, */ nl++; /* increment # of lines, */ nlp++; /* increment # of lines on current screen, */ nc = 0; /* reset # of chars on current line to 0, */ putchar('\n'); /* start a new line, */ ungetc(c,x_fp); /* put the char back where it came from, */ c = '\0'; /* and make c a NULL character. */ /* c is made a NULL so that a test can be performed before "putchar(c)" and "nc++" are executed. If c is a NULL, these operations are not performed. The "ungetc(c,x_fp)" is executed so that the char is the first one available for the next line. This stuff is needed so that printing out one line to the screen will not result in the char being printed in front of the prompt. This does not happen when printing several lines. A newline is not ungetced because it will cause a blank line to be printed when it is put on the next line. */ } /* if */ if (c != '\0') { /* If c is not a NULL, */ putchar(c); /* print the character, */ nc++; /* and increment # of chars on line. */ } /* if */ if (c == '\n') { /* If c is a newline, */ nl++; /* increment # of lines, */ nlp++; /* increment # lines on current screen, */ nc = 0; /* and reset # of chars on current line to 0. */ } } /* for */ wait(0); /* After max # of lines have been output, prompt user. */ } /* while */ } /* main */ wait(e_o_f) /* Print prompt, then wait for and iterpret command. */ /* If wait is called with an argument of 0, then it has been called in the middle of a file. If it is called with an argument of 1, then it has been called at EOF and it will call nextfile() as necessary. The prompt is also made different at EOF. */ int e_o_f; { int w_c, i, arglen, pgf; char cmd[116], prompt[128], arg[6], *editor, *envfind(), *strcat(); char *strcpy(); FILE *fopen(), *temp; long ftell(); strcpy(prompt,x_file); /* Prompt with the filename. */ if (e_o_f) strcat(prompt," [EOF]"); /* If at EOF, make "EOF" part of prompt. */ printf("--%s--",prompt); /* Print prompt. */ arg[0] = '\0'; /* Initialize the argument string and */ arglen = 0; /* its length. */ pgf = FALSE; /* Set the "prompt gone flag". */ getcmd: /* Get char from keyboard and mask off upper 8 bits, which contain the scan code of the key pressed. Then act on it as a command. */ w_c = (key_getc() & CMASK); /* Get char and mask off scan code. */ /* All the cases in this switch statment (except the for the default) which end in a "goto getcmd;" statement contain the statments: arg[0] = '\0'; arglen = 0; pgf = FALSE; This resets the argument back to 0. Even commands which do not use the argument must reset it to 0 to avoid problems. This also resets the "prompt gone flag" to false. This flag is used to determine if the prompt has been erased for printing the argument. If the prompt has not been erased, pgf = FALSE. When a digit is typed, the prompt is erased, the digit printed, and pgf set to TRUE. When a command is executed, it will always cause the prompt or some message to be printed, so pgf is set to FALSE again. This is not needed when a case ends in "break," since wait() returns when the switch exits. An argument of 0 is interpreted by the function getval() to mean 1, so when no argument is entered, the default is 1. */ switch (w_c) { case ' ': /* Space means "next screen". */ if (e_o_f) /* If at end of current file, */ nextfile(1); /* go to next file. */ x_mnl = NLPS * getval(arg); /* Get # of pages to move. */ x_rwp = ftell(x_fp); /* Save current position in file for !. */ break; /* Break out of switch. */ case '\n': case '\r': /* Enter key means "next line". */ if (e_o_f) /* Everything else is similar to above. */ nextfile(1); x_mnl = getval(arg); x_rwp = ftell(x_fp); break; case 'h': case 'H': /* 'H' means "next half screen". */ if (e_o_f) nextfile(1); x_mnl = NLPHS * getval(arg); x_rwp = ftell(x_fp); break; case 'r': case 'R': /* 'R' Means "rewind current file". */ rewind(x_fp); x_mnl = NLPS; /* Print out full screen next time. */ x_rwp = 0L; /* Current position in file is beginning. */ break; case 'n': case 'N': /* 'N' means "go to next file". */ nextfile(1); /* Nextfile(1) goes to next file. */ x_mnl = NLPS; /* Put out a full screen next time. */ break; case 'p': case 'P': /* 'P' means "go to previous file". */ x_mnl = NLPS; /* Put out a full screen next time. */ if (nextfile(0)) { /* Nextfile(0) goes to previous file. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* If you tried to go before 1st file, */ } /* if */ /* a message is printed and you try a diff. */ else break; /* command, otherwise, exit "switch". */ case 'f': case 'F': printf("\r%s\rNew file: ",x_spc); i = key_gets(cmd, 13, "New file: Aborted."); /* Get filename. */ if (i == -1) { /* If key_gets() aborted, */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* then get next command. */ } /* if */ temp = fopen(cmd,"r"); /* Attempt to open file just named. */ if (temp == 0) { /* If couldn't open file, */ printf("\r%s\rCould not open %s.", x_spc, cmd); /* say so. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* Get next command. */ } /* if */ else { /* Otherwise, */ if (x_fp != stdin) /* if not reading from stdin, */ fclose(x_fp); /* close old file, */ x_fp = temp; /* set x_fp to new file, */ strcpy(x_file, cmd); /* make x_file correct, */ x_mnl = NLPS; /* set x_mnl to its default, */ x_rwp = 0L; /* and set x_rwp to top of file. */ } /* else */ break; /* Note that x_srf is not reset to 0. Even if you quit reading from stdin, it is still redirected because of the < or | on the command line. This means that all of the restrictions still apply; i.e. you still cannot push to an inferior shell after you change files, etc. You will also not be able to edit if you change files after reading from a pipe. This should not be too much of an annoyance, since reading from a pipe and reading from a file are usually not done in the same invokation of More. */ case 'q': case 'Q': /* 'Q' means "quit". */ printf("\r%s\r",x_spc); /* Erase prompt or message on last line. */ exit(0); case 'e': case 'E': /* 'E' means "edit current file". */ if (x_srf) { /* If stdio is redirected, can't edit. */ printf("\r%s\rCannot edit with stdin redirected.", x_spc); arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* Go get another command. */ } /* if */ editor = envfind("EDITOR"); /* Get editor name from environ var. */ if (editor == 0) /* If there is no such environ var., */ editor = "edlin"; /* then use EDLIN. */ strcpy(cmd, editor); /* Start command string with editor. */ strcat(cmd, " "); /* Now put a space. */ strcat(cmd, x_file); /* Now end command with filename. */ if (strcmp(editor,"edlin")) /* If editor != "edlin", string was */ free(editor); /* obtained with malloc, free it. */ bang(strlen(cmd),cmd,prompt); /* Execute the command. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* Get next command. */ case '!': /* '!' means "execute DOS command". */ printf("\r%s\r!",x_spc); /* Erase last line, print '!'. */ i = key_gets(cmd, 115, "Push aborted."); /* Get command string. */ if (i == -1) { arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; } /* if */ /* A command string passed to DOS must not be more than 127 characters long. DOS will ignore the rest. Since the system() command must always pass the string "command /c" to DOS (see DOS 2.0 manual), that leaves 117 characters. I rounded to 115 to give myself a bit of headroom. See definition of key_gets() to see what it expects for arguments. It returns the number of characters read from the keyboard or -1 if it was aborted. */ if (x_srf) { /* If stdin is redirected, issue warnings. */ if (!i) { /* If i == 0, then this is a push to shell. */ printf("\r%s\rCan't push when reading from stdin.", x_spc); arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; } /* if */ printf("\r%s\rThis is risky when reading from stdin. ",x_spc); if (!confirm()) { printf("\r%s\rAborted.",x_spc); arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; } } bang(i,cmd,prompt); /* Bang pushes to an inferior command.com. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; /* If stdin is redirected, then pushing to DOS is bad news. If a program (including command.com) reads from stdin (not through the BIOS or directly from the keybd), then instead of reading from the keybd, it gets its input from the pipe which More is reading from. If you simply pushed to an inferior shell, result is similar to a batch file. This is a fault of DOS, not mine. */ case '?': /* '?' means "help". */ printf("\r%s",x_spc); /* Erase prompt or message. */ help(prompt); /* Help prints out the prompt, so it needs it, */ fseek(x_fp, x_rwp, 0); /* and reposition read/write pointer. */ /* The read/write pointer is backed up so that the screen which was erased by help is reprinted. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; case 'v': case 'V': /* 'V' means "print version number." */ printf("\r%s\rPC-More v1.2",x_spc); arg[0] = '\0'; arglen = 0; pgf = FALSE; goto getcmd; case '\007': /* ^G means abort current argument. */ arg[0] = '\0'; arglen = 0; pgf = FALSE; printf("\r%s\r--%s--", x_spc, prompt); /* Erase argument. */ goto getcmd; default: if (w_c >= '0' && w_c <= '9') { /* If w_c is a digit, */ if (!pgf) { /* If prompt is not gone, */ printf("\r%s\r",x_spc); /* erase it, */ pgf = 1; /* and set flag. */ } arg[arglen++] = w_c; /* add w_c to string, */ arg[arglen] = '\0'; /* terminate string properly, */ putchar(w_c); /* and put w_c on screen. */ if (arglen > 3) { /* If more than 3 digits long, */ printf("\r%s\rArgument too long.",x_spc); /* say so, */ arg[0] = '\0'; /* and start over. */ arglen = 0; pgf = FALSE; } /* if */ goto getcmd; } /* if */ /* Arg[] is limited to 3 digits because the largest integer which can be held in a int is 32767. Arg[] is multiplied by a number which can be as large as 24. 24 * 999 (the largest 3 digit number) = 23976, which is within the range of an int. 24 * 9999 (the largest 4 digit number) = 239976, which is too big to fit in an int. Therefore, it is a safety precaution not to allow any number larger than 3 digits to be entered as an argument. */ if (w_c == '\b' && arglen >= 1) { /* If w_c is a backspace, */ arg[--arglen] = '\0'; /* then delete the last char in */ fputs("\b \b",stdout); /* arg[] if there is one there */ goto getcmd; /* and erase it from the screen. */ } /* if */ putchar('\007'); /* If the above tests fail, then beep. */ goto getcmd; } /* switch */ printf("\r%s\r",x_spc); /* Erase prompt or message. */ } /* wait */ /* Now return to main(). */ help(prompt) /* Prints out help text and the prompt. */ char *prompt; { char *ht; ht = "\nPC-More Help:\n\n <sp> next screen\n <cr> next line\n\ h next half page\n r rewind current file\n\ e edit current file\n n go to next file\n\ p go to previous file \n f choose a new file\n\ ! push to shell\n ? print this text\n\ v print version\n ^G abort a command while entering text\n\ q quit\n\n"; crt_cls(); crt_mode(7); printf("%s--%s--",ht,prompt); } bang(i,cmd,prompt) /* Bang() will push to an inferior shell if i == 0 or it will execute the command pointed to by cmd if i != 0. The command can be an internal DOS command or it can be a program to run. */ int i; /* Length of command to execute. */ char *cmd; /* Pointer to command to execute. */ char *prompt; /* Pointer to prompt to print when done. */ { int retrn; FILE *fopen(); if (!x_srf) /* If reading from a file, then */ fclose(x_fp); /* close file before push. */ if (!i) /* If no command was entered, */ retrn = system("command"); /* push to DOS. */ else /* If a command was entered, */ retrn = system(cmd); /* give DOS the command in cmd[]. */ if (retrn) printf("Push not successful, DOS error code = %d", retrn); if (!x_srf) { /* If reading from a file, */ x_fp = fopen(x_file,"r"); /* reopen file, */ if (x_fp == 0) /* check to see if it was reopened, */ abort("Unable to reopen file after push to DOS."); fseek(x_fp, x_rwp, 0); /* and reposition read/write pointer. */ } else /* If reading from stdin, */ x_fp = stdin; /* then "reopen" it. */ fputs("\n\nReturning to more...\n\n",stdout); printf("--%s--",prompt); } key_gets(string, len, msg) /* Key_gets() gets a string by reading one character at a time from the keyboard, not from stdin. It reads up to len-1 characters and puts them in string[]. If the user tries to backspace too far, then this is interpreted to mean "I really didn't want to do this after all," and key_gets() aborts, returning -1 to signal error. Key_gets() will read up until it receives a carriage return, a linefeed, or an end of file indication. It returns the number of characters actually read, or -1 if it was aborted. */ int len; /* How much to get. */ char string[]; /* Where to put it. */ char *msg; /* What to say when aborting. */ { int i, k_c; for (i = 0; i < len; i++) { /* Get len-1 characters for the string. */ k_c = (key_getc() & CMASK); /* Get char, mask scan code. */ if (k_c == '\r' || k_c == '\n' || k_c == EOF) { break; /* If 'Enter' or EOF found, quit getting characters. */ } /* if */ if (k_c == '\b') { /* If 'backspace', */ if (i > 0) { /* don't erase before start of command. */ string[--i] = '\0'; /* remove last char in string[], */ fputs("\b \b",stdout); /* erase it from screen. */ } /* if */ else { /* If user tries to erase too far, then abort. */ printf("\r%s",msg); /* Print aborting message. */ return (-1); /* Return with error. */ } /* else */ i--; /* Dec. i again since 'for' will increment it, */ continue; /* and go to end of for loop. */ } /* if */ if (k_c == '\007') { /* If 'bell', then abort: */ printf("\r%s\r%s", x_spc, msg); /* Erase anything, print msg, */ return (-1); /* and return with error. */ } /* if */ putchar(k_c); /* If not 'enter' or 'bs', then echo it, */ string[i] = k_c; /* and add it to the command string. */ } /* for */ string[i] = '\0'; /* Terminate the command string. */ return (i); } /* key_gets() */ /* I can't use gets() to collect the string since it reads from stdin. If stdin is redirected, strange things will happen. That's why I wrote key_gets() to read from the keyboard. */ confirm() { int c_c; printf("Confirm [N]\b\b"); c_c = key_getc() & CMASK; putchar(c_c); if (c_c == 'y' || c_c == 'Y') return(1); else return(0); } nextfile(next) int next; /* Nextfile steps through the argument list on the command line, trying to open each argument as a file. If it succeeds, then x_fp points to the file and x_file points to a string which is the name of the file. If it fails, abort is called. Since where is a static variable, this function "remembers" where it was the last time it was called and moves on to the next argument on the command line each time it is called. Nextfile does not return a value, since x_fp and x_file are external variables. Note: Nextfile also sets x_rwp to point to the top of the file. */ { static int where; FILE *fopen(); if (next) { /* If going to next file do this: */ if (++where == x_argc) /* Go to next file. Gone too far? */ exit(0); /* If so, exit. */ } else { /* If going to previous file, do this: */ if (--where < 1) { /* Go to prev file. Gone too far? */ printf("\r%s\rAt first file.",x_spc); /* If so, say so, */ where++; /* put where back where it belongs, */ return(1); /* and return with an error code. */ } } if (x_fp) /* If x_fp != 0 then it has been opened, so */ fclose(x_fp); /* close it so won't run out of file handles. */ x_fp = fopen(x_argv[where], "r"); /* Open next (or previous) file. */ strcpy(x_file, x_argv[where]); /* Set x_file to the new filename. */ x_rwp = 0L; /* Tell ! that at start of file. */ if (x_fp == 0) abort("Could not open file %s.\n",x_argv[where]); crt_cls(); crt_mode(7); /* Clear screen before going to the next file. */ return(0); /* For consistancy, return with an OK code. */ } /* Nextfile will return a value of 1 only if you try to go to the file which is before the first file on the command line. If no error occurs, then it returns 0. If any other error occurs, then it is fatal and execution is aborted. The only place where the return value is checked is "case 'p':" segment of the wait() subroutine. */ getval(string) char *string; { int i; /* This function gets the number entered */ i = atoi(string); /* as an argument in wait(). If atoi() */ if (i == 0) /* returns 0, then it is assumed that no */ i = 1; /* argument was entered and that the */ /* default of 1 should be used. */ return(i); }