View of xos/usr/more/more.c


XOS | Parent Directory | View | Download

/* Copyright (C) 2008  Emmanuel Varoquaux
 
   This file is part of XOS.
 
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */
 
/* Note : cette version de more presente quelques differences majeures avec son
 * homologue Linux (Berkeley) :
 * - tous les caracteres imprimables definis par la norme ISO 8859 sont comptes
 *   (contre les seuls caracteres imprimables ASCII dans Linux) ;
 * - les lignes comptees sont les lignes affichees a l'ecran (contre les lignes
 *   logiques dans Linux) ;
 * - certaines touches directionnelles sont reconnues pour la navigation dans
 *   le fichier. */
 
#include <getopt.h>
#include <error.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <stdarg.h>
 
#define CS_REVERSE       "\e[7m"
#define CS_NORMAL        "\e[0m"
#define CS_ERASE_BEGLINE "\e[1K"
#define CS_ERASE_SCREEN  "\e[2J"
#define CS_HOME          "\e[H"
 
#define LINE_BUF_SIZE 256
 
#define ctrl(letter) ((letter) & 077)
 
#define min(a, b) ({ \
  typeof (a) _a = (a); \
  typeof (b) _b = (b); \
  _a < _b ? _a : _b; \
})
 
enum {
  CM_NONE,
  CM_NEXT_LINE,
  CM_NEXT_SCREENFUL,
  CM_SCROLL,
  CM_SKIP_BW_LINE,
  CM_SKIP_BW_SCREENFUL,
  CM_SKIP_FW_LINE,
  CM_SKIP_FW_SCREENFUL,
  CM_BEGIN,
  CM_END,
  CM_REDRAW,
  CM_DISPLAY_LINE_NR,
  CM_DISPLAY_FILENAME,
  CM_PREVIOUS_FILE,
  CM_NEXT_FILE,
  CM_EXIT,
  CM_HELP,
  CM_REPEAT
};
 
enum {BT_NONE, BT_BANNER, BT_PAUSE};
 
/* caracteres textuels non imprimables */
static const char textcntrl[] = {'\b', '\t', '\n', '\f', '\r'};
 
/* extension de isprint() prenant en compte les caracteres imprimables definis
   par la norme ISO 8859 */
#define isprint_ext(c) (isprint(c) || (c) >= 160)
 
/* more est concu pour visualiser des fichiers dont tous les caracteres sont
   textuels (caracteres imprimables ou caracteres de controle). Le comportement
   de more sur des fichiers binaires est indefini. */
#define istext(c) (isprint_ext(c) || memchr(textcntrl, (c), sizeof textcntrl))
 
static int has_screen;
static struct winsize winsize;
static int screen_lines;
static int scroll_lines;
 
static struct termios saved_termios;
 
static int last_cmd;
 
static inline void ring_bell()
{
  fputc('\a', stderr);
}
 
/* terminal */
 
static void setup_term()
{
  struct termios new_termios;
 
  tcgetattr(fileno(stderr), &saved_termios);
  new_termios = saved_termios;
  new_termios.c_lflag &= ~(ICANON | ECHO | ISIG);
  new_termios.c_cc[VMIN] = 1;
  new_termios.c_cc[VTIME] = 0;
  tcsetattr(fileno(stderr), TCSANOW, &new_termios);
}
 
static void restore_term()
{
  tcsetattr(fileno(stderr), TCSANOW, &saved_termios);
}
 
/* pause */
 
static int read_char()
{
  unsigned char c;
 
  if (read(fileno(stderr), &c, 1) == -1) {
    restore_term();
    error(EXIT_FAILURE, errno, "read error");
  }
  return c;
}
 
static int read_cmd()
{
  int state;
  int c;
 
  state = 0;
  while (1) {
    c = read_char();
    switch (state) {
    case 0:
      switch (c) {
      case 'h':
      case '?':
        return CM_HELP;
      case ' ':
        return CM_NEXT_SCREENFUL;
      case 'z':
        return CM_NEXT_SCREENFUL;
      case '\n':
        return CM_NEXT_LINE;
      case 'd':
      case ctrl('D'):
        return CM_SCROLL;
      case 'q':
      case 'Q':
      case ctrl('C'):
        return CM_EXIT;
      case 's':
        return CM_SKIP_FW_LINE;
      case 'f':
        return CM_SKIP_FW_SCREENFUL;
      case 'b':
      case ctrl('B'):
        return CM_SKIP_BW_SCREENFUL;
      case '=':
        return CM_DISPLAY_LINE_NR;
      case ctrl('L'):
        return CM_REDRAW;
      case '.':
        return CM_REPEAT;
      case ':':
        state = 1;
        continue;
      case '\e': /* extension */
        /* touches directionnelles
           UP     \e[A   CM_SKIP_BW_LINE
           DOWN   \e[B   CM_NEXT_LINE
           RIGHT  \e[C   CM_NEXT_FILE
           LEFT   \e[D   CM_PREVIOUS_FILE
           PGUP   \e[5~  CM_SKIP_BW_SCREENFUL
           PGDN   \e[6~  CM_NEXT_SCREENFUL
           HOME   \e[1~  CM_BEGIN
           END    \e[4~  CM_END */
        state = 2;
        continue;
      }
      break;
    case 1:
      switch (c) {
      case 'n':
        return CM_NEXT_FILE;
      case 'p':
        return CM_PREVIOUS_FILE;
      case 'f':
        return CM_DISPLAY_FILENAME;
      }
      break;
    case 2:
      if (c == '[') {
        state = 3;
        continue;
      }
      break;
    case 3:
      switch (c) {
      case 'A':
        return CM_SKIP_BW_LINE;
      case 'B':
        return CM_NEXT_LINE;
      case 'C':
        return CM_NEXT_FILE;
      case 'D':
        return CM_PREVIOUS_FILE;
      case '1':
        state = 4;
        continue;
      case '4':
        state = 5;
        continue;
      case '5':
        state = 6;
        continue;
      case '6':
        state = 7;
        continue;
      }
      break;
    case 4:
      if (c == '~')
        return CM_BEGIN;
      break;
    case 5:
      if (c == '~')
        return CM_END;
      break;
    case 6:
      if (c == '~')
        return CM_SKIP_BW_SCREENFUL;
      break;
    case 7:
      if (c == '~')
        return CM_NEXT_SCREENFUL;
      break;
    }
    ring_bell();
    state = 0;
  }
}
 
static void display_prompt(const char *format, va_list ap)
{
  fputs(CS_REVERSE, stdout);
  fputs("--More--", stdout);
  if (format) {
    putchar('(');
    vprintf(format, ap);
    putchar(')');
  }
  fputs(CS_NORMAL, stdout);
  fflush(stdout);
}
 
static void display_line(int line)
{
  printf("%d", line);
  fflush(stdout);
}
 
static void display_filename(const char *filename, int line)
{
  if (filename)
    printf("\"%s\"", filename);
  else
    printf("[Not a file]");
  printf(" line %d", line);
  fflush(stdout);
}
 
static void erase_prompt()
{
  fputs(CS_ERASE_BEGLINE, stdout);
  putchar('\r');
}
 
static int pause_format(const char *format, ...)
{
  va_list ap;
  int cmd;
 
  va_start(ap, format);
  display_prompt(format, ap);
  va_end(ap);
  cmd = read_cmd();
  erase_prompt();
  return cmd;
}
 
static int pause_void()
{
  return pause_format(NULL);
}
 
static int pause_percent(int percent)
{
  return pause_format("%d%%", percent);
}
 
static int pause_nextfile(const char *filename)
{
  return pause_format("Next file: %s", filename);
}
 
static int pause_line(int line)
{
  int cmd;
 
  display_line(line);
  cmd = read_cmd();
  erase_prompt();
  return cmd;
}
 
static int pause_filename(const char *filename, int line)
{
  int cmd;
 
  display_filename(filename, line);
  cmd = read_cmd();
  erase_prompt();
  return cmd;
}
 
/* lecture du fichier */
 
static int must_clear_screen(FILE *fp)
{
  int c;
 
  if ((c = getc(fp)) != EOF)
    ungetc(c, fp);
  return c == '\f';
}
 
/* Retourne 1 si une pause est requise. */
static int get_line(FILE *fp, char buf[], size_t *len)
{
  int col;
  int formfeed;
  int need_wrap;
  char *p;
  int c;
 
  col = 0;
  formfeed = 0;
  need_wrap = 0;
  p = buf;
  while (p != &buf[LINE_BUF_SIZE - 1]) {
    if ((c = getc(fp)) == EOF) {
      if (p > buf)
        goto end_line;
      else
        goto end;
    }
    *p++ = c;
    switch (c) {
    case '\b': /* BS */
      if (col > 0) {
        col--;
        need_wrap = 0;
      }
      continue;
    case '\t': /* HT */
      col = min((col | 7) + 1, winsize.ws_col - 1);
      continue;
    case '\n': /* LF */
      goto end;
    case '\f': /* FF */
      if (need_wrap)
        goto wrap;
      p[-1] = '^';
      *p++ = 'L';
      formfeed = 1;
      if (col + 2 < winsize.ws_col)
        col += 2;
      else
        goto end;
      continue;
    case '\r': /* CR */
      col = 0;
      need_wrap = 0;
      continue;
    }
    if (isprint_ext(c)) { /* caracteres imprimables */
      if (need_wrap)
        goto wrap;
      if (col + 1 < winsize.ws_col)
        col++;
      else
        need_wrap = 1;
    }
    continue;
  wrap:
    ungetc(c, fp);
    p[-1] = '\n';
    goto end;
  }
 end_line:
  *p++ = '\n';
 end:
  *len = p - buf;
  return formfeed;
}
 
static int more_chars(FILE *fp)
{
  int c;
 
  if ((c = getc(fp)) == EOF)
    return 0;
  ungetc(c, fp);
  return 1;
}
 
/* impression */
 
static void clear_screen()
{
  fputs(CS_ERASE_SCREEN, stdout);
  fputs(CS_HOME, stdout);
}
 
static void print_separator()
{
  puts("-------------------------------------------------------------------------------");
}
 
static void print_skipping_lines(int lines)
{
  putchar('\n');
  if (lines == 1)
    puts("...skipping one line");
  else
    printf("...skipping %d lines\n", lines);
}
 
static void print_skipping_back_lines(int lines)
{
  putchar('\n');
  if (lines == 1)
    puts("...skipping back one line");
  else
    printf("...skipping back %d lines\n", lines);
}
 
static void print_skipping_back_pages(int pages)
{
  putchar('\n');
  if (pages == 1)
    puts("...back 1 page");
  else
    printf("...back %d pages\n", pages);
}
 
static void print_skipping_back_to_beginning()
{
  putchar('\n');
  puts("...skipping back to beginning");
}
 
static void print_skipping_to_file(const char *filename)
{
  putchar('\n');
  puts("...Skipping");
  printf("...Skipping to file %s\n", filename);
  putchar('\n');
}
 
static void print_skipping_back_to_file(const char *filename)
{
  putchar('\n');
  puts("...Skipping");
  printf("...Skipping back to file %s\n", filename);
  putchar('\n');
}
 
static void print_banner(const char *filename)
{
  puts("::::::::::::::");
  puts(filename);
  puts("::::::::::::::");
}
 
static void print_directory(const char *filename)
{
  putchar('\n');
  printf("*** %s: directory ***\n", filename);
  putchar('\n');
}
 
static void print_nottextfile(const char *filename)
{
  putchar('\n');
  printf("******** %s: Not a text file ********\n", filename);
  putchar('\n');
}
 
static void print_file(FILE *fp)
{
  int c;
 
  while ((c = getc(fp)) != EOF)
    putchar(c);
}
 
static void skip_lines(FILE *fp, int lines)
{
  char buf[LINE_BUF_SIZE + 1];
  size_t len;
 
  while (lines > 0) {
    get_line(fp, buf, &len);
    if (!len)
      break;
    lines--;
  }
}
 
static void print_line(const char *s, size_t len)
{
  while (len--)
    putchar(*s++);
}
 
/* Si lines >= 0, affiche lines lignes de texte.
 * Si lines < 0, affiche toutes les lignes de texte jusqu'a la fin du fichier.
 * Retourne le nombre de lignes affichees, ou -1 si le fichier est termine. */
static int print_lines(FILE *fp, int lines)
{
  int n;
  char buf[LINE_BUF_SIZE + 1];
  size_t len;
  int need_pause;
 
  if (lines >= 0) {
    n = 0;
    while (n < lines) {
      need_pause = get_line(fp, buf, &len);
      if (!len)
        return -1;
      print_line(buf, len);
      if (!more_chars(fp))
        return -1;
      n++;
      if (need_pause)
        break;
    }
    return n;
  }
  else
    while (1) {
      get_line(fp, buf, &len);
      if (!len)
        return -1;
      print_line(buf, len);
    }
}
 
static void print_commands()
{
  puts("  h or ?       Help: display a summary of these commands");
  puts("  <space>      Display next screenful of text");
  puts("  z            Display next screenful of text");
  puts("  <return>     Display next line of text");
  puts("  d or ctrl-D  Scroll 1 half-screenful of text");
  puts("  q or Q       Exit from more");
  puts("  s            Skip forward 1 line of text");
  puts("  f            Skip forward 1 screenful of text");
  puts("  b or ctrl-B  Skip backwards 1 screenful of text");
  puts("  =            Display current line number");
  puts("  ctrl-L       Redraw screen");
  puts("  :n           Go to next file");
  puts("  :p           Go to previous file");
  puts("  :f           Display current file name and line number");
  puts("  .            Repeat previous command");
  puts("  <up>         Skip backward 1 line of text");
  puts("  <down>       Display next line of text");
  puts("  <right>      Go to next file");
  puts("  <left>       Go to previous file");
  puts("  <pgup>       Skip backwards 1 screenful of text");
  puts("  <pgdn>       Display next screenful of text");
  puts("  <home>       Go to beginning of current file");
  puts("  <end>        Go to end of current file");
}
 
/* more */
 
static inline int get_percent(FILE *fp, const struct stat *statbuf)
{
  long offset;
 
  if (statbuf->st_size == 0 || (offset = ftell(fp)) == -1)
    return 0;
  return offset * 100 / statbuf->st_size;
}
 
/* filename == NULL pour visualiser l'entree standard.
 * Si filename == NULL, banner_type doit etre BT_NONE.
 * more() retourne dans next_file l'indice du prochain fichier a traiter a
 * partir du fichier courant. Si aucun fichier n'a ete demande par
 * l'utilisateur, la valeur par defaut 0 est retournee. */
static int more(const char *filename, int banner_type, int *next_file)
{
  int err;
  struct stat statbuf;
  int seekable;
  FILE *fp;
  unsigned char buf[1];
 
  int line;
  int started;
  int cmd;
  int lines;
 
  err = 0;
  *next_file = 0; /* valeur par defaut */
 
  if (filename) {
    if (stat(filename, &statbuf) == -1) {
      error(0, errno, filename);
      err = 1;
      goto end;
    }
    if (S_ISDIR(statbuf.st_mode)) {
      print_directory(filename);
      goto end;
    }
    seekable = S_ISREG(statbuf.st_mode);
    if (!(fp = fopen(filename, "r"))) {
      error(0, errno, filename);
      err = 1;
      goto end;
    }
    if (fread(buf, 1, sizeof buf, fp) == sizeof buf) {
      if (!istext(buf[0])) {
        print_nottextfile(filename);
        goto fclose;
      }
      rewind(fp);
    }
  }
  else {
    if (fstat(fileno(stdin), &statbuf) == -1) {
      error(0, errno, "standard input");
      err = 1;
      goto end;
    }
    seekable = 0;
    fp = stdin;
  }
 
  if (has_screen) {
    line = 0;
    started = 0;
    if (banner_type == BT_PAUSE)
      goto pause;
    lines = screen_lines;
 
    while (1) {
      if (!started) {
        if (must_clear_screen(fp))
          clear_screen();
        if (banner_type != BT_NONE) {
          print_banner(filename);
          if (screen_lines < 3)
            lines = 0;
          else if (lines > screen_lines - 3)
            lines = screen_lines - 3; /* banniere de 3 lignes */
        }
        started = 1;
      }
      if ((lines = print_lines(fp, lines)) == -1)
        break;
      line += lines;
 
    pause:
      if (!started)
        cmd = pause_nextfile(filename);
      else if (seekable)
        cmd = pause_percent(get_percent(fp, &statbuf));
      else
        cmd = pause_void();
 
    process_cmd:
      if (cmd == CM_REPEAT) {
        if (last_cmd == CM_NONE)
          goto invalid_cmd;
        cmd = last_cmd;
      }
      else
        last_cmd = cmd;
      switch (cmd) {
      case CM_NEXT_LINE:
        lines = 1;
        continue;
      case CM_NEXT_SCREENFUL:
        lines = screen_lines;
        continue;
      case CM_SCROLL:
        lines = scroll_lines;
        continue;
      case CM_SKIP_BW_LINE:
        if (!seekable)
          goto invalid_cmd;
        print_skipping_back_lines(1);
        fseek(fp, 0, SEEK_SET);
        if ((line -= screen_lines + 1) < 0)
          line = 0;
        skip_lines(fp, line);
        lines = screen_lines;
        continue;
      case CM_SKIP_BW_SCREENFUL:
        if (!seekable)
          goto invalid_cmd;
        print_skipping_back_pages(1);
        fseek(fp, 0, SEEK_SET);
        if ((line -= 2 * screen_lines) < 0)
          line = 0;
        skip_lines(fp, line);
        lines = screen_lines;
        continue;
      case CM_SKIP_FW_LINE:
        print_skipping_lines(1);
        skip_lines(fp, 1);
        line++;
        lines = screen_lines;
        continue;
      case CM_SKIP_FW_SCREENFUL:
        print_skipping_lines(screen_lines);
        skip_lines(fp, screen_lines);
        line += screen_lines;
        lines = screen_lines;
        continue;
      case CM_BEGIN:
        if (!seekable)
          goto invalid_cmd;
        print_skipping_back_to_beginning();
        fseek(fp, 0, SEEK_SET);
        line = 0;
        lines = screen_lines;
        continue;
      case CM_END:
        lines = -1;
        continue;
      case CM_REDRAW:
        if (!seekable)
          goto invalid_cmd;
        clear_screen();
        fseek(fp, 0, SEEK_SET);
        if ((line -= screen_lines) < 0)
          line = 0;
        skip_lines(fp, line);
        lines = screen_lines;
        continue;
      case CM_DISPLAY_LINE_NR:
        cmd = pause_line(line);
        goto process_cmd;
      case CM_DISPLAY_FILENAME:
        cmd = pause_filename(filename, line);
        goto process_cmd;
      case CM_NEXT_FILE:
        *next_file = 1;
        goto end_file;
      case CM_PREVIOUS_FILE:
        if (!filename)
          goto invalid_cmd;
        *next_file = -1;
        goto end_file;
      case CM_EXIT:
        restore_term();
        exit(EXIT_SUCCESS);
      case CM_HELP:
        print_separator();
        print_commands();
        print_separator();
        goto pause;
      }
    invalid_cmd:
      ring_bell();
      goto pause;
    }
  }
  else {
    if (banner_type != BT_NONE)
      print_banner(filename);
    print_file(fp);
  }
 
 end_file:
  if (filename) {
  fclose:
    fflush(stdout);
    if (fclose(fp) == EOF) {
      error(0, errno, filename);
      err = 1;
      goto end;
    }
  }
 end:
  return err ? -1 : 0;
}
 
static void __attribute__ ((noreturn)) die_bad_usage()
{
  fprintf(stderr, "Try `%s --help' for more information.\n", program_invocation_name);
  exit(EXIT_FAILURE);
}
 
static void print_help()
{
  printf("Usage: %s [OPTION]... [FILE]...\n", program_invocation_name);
  puts("Print file(s) to standard output, screen by screen.");
  putchar('\n');
  puts("Options are:");
  puts("  -h, --help  display this help and exit");
  putchar('\n');
  puts("Commands are:");
  print_commands();
}
 
int main(int argc, char **argv)
{
  static const struct option longopts[] = {
    {"help", no_argument, NULL, 'h'},
    {NULL, 0, NULL, 0}
  };
 
  int err;
  char **filename_list;
  int filename_count;
  int filename_ind;
  int next_file;
  int banner_type;
  int c;
 
  while ((c = getopt_long(argc, argv, "h", longopts, NULL)) != -1)
    switch (c) {
    case 'h':
      print_help();
      return EXIT_SUCCESS;
    case '?':
      die_bad_usage();
    }
 
  has_screen = ioctl(fileno(stdout), TIOCGWINSZ, &winsize) == 0 && winsize.ws_row > 1 && winsize.ws_col > 0;
  if (has_screen) {
    screen_lines = winsize.ws_row - 1;
    scroll_lines = winsize.ws_row / 2 - 1 ? : 1;
    setup_term();
  }
 
  err = 0;
  filename_list = argv + optind;
  filename_count = argc - optind;
  last_cmd = CM_NONE;
  if (!isatty(fileno(stdin))) {
    if (filename_count > 0 && !strcmp(filename_list[0], "-")) {
      filename_list++;
      filename_count--;
    }
    if (more(NULL, BT_NONE, &next_file) == -1) {
      err = 1;
      next_file = 0;
    }
    filename_ind = (next_file ? : 1) - 1;
    banner_type = BT_PAUSE;
  }
  else if (filename_count > 0) {
    filename_ind = 0;
    next_file = 0;
    banner_type = filename_count > 1 ? BT_BANNER : BT_NONE;
  }
  else {
    if (has_screen)
      restore_term();
    error(0, 0, "missing operand");
    die_bad_usage();
  }
  while (1) {
    if (filename_ind < 0)
      filename_ind = 0;
    if (filename_ind >= filename_count)
      break;
    if (next_file < 0)
      print_skipping_back_to_file(filename_list[filename_ind]);
    else if (next_file > 0)
      print_skipping_to_file(filename_list[filename_ind]);
    if (more(filename_list[filename_ind], banner_type, &next_file) == -1)
      err = 1;
    banner_type = BT_PAUSE;
    filename_ind += next_file ? : 1;
  }
  if (has_screen)
    restore_term();
  return err ? EXIT_FAILURE : EXIT_SUCCESS;
}