/*
 * nl: number lines.
 *
 * Written 30-April-1999 by orc@pell.chi.il.us (david parsons) and released
 * into the public domain.
 */

static const char rcsid[] = "Mastodon $Id: s.nl.c 1.3 00/05/21 16:33:42-00:00 orc $";
 
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <malloc.h>
#include <regex.h>
#include <basis/options.h>
#include <basis/rlstring.h>

/* style for header, body, or trailer
 */
typedef struct style {
    char *title;
    enum { ALL, NONEMPTY, NONE, RE } style;
    regex_t regex;
} Style;

static enum section { HEADER=0, BODY=1, TRAILER=2 } section = BODY;
static Style styles[3] = {
    { "header", NONE,     0 },
    { "body",   NONEMPTY, 0 },
    { "trailer",NONE,     0 },
};

static int dont_renumber = 0;		/* don't restart line numbers at each
					 * body section
					 */
static int first_line_number = 1;	/* first line number */
static int line_increment = 1;		/* gap between line numbers */
static char *delimiter = "\t";		/* padding between line numbers and
					 * the text
					 */
static int number_width = 6;		/* how many spaces to allow for */
static int merge_lines = 0;		/* collapse blank line numbers */
static enum { LN, RN, RZ } number_format = RN;
					/* number display format */
static char marker[3] = "\\:";		/* header/body/trailer marker */
static char number_format_string[80];	/* format to print numbers */
static long lineno;			/* current lineno */

char *__progname;			/* basename(program) */

struct x_option options[] = {
    { 'f', 'f', "footer-numbering", "STYLE",	"set the numbering style for\n"	
						"lines in the footer of each\n"
						"page" },
    { 'b', 'b', "body-numbering", "STYLE",	"set the body numbering style\n"
						"(cf: --footer-numbering" },
    { 'h', 'h', "header_numbering", "STYLE",	"set the header numbering style\n" },
    { 'p', 'p', "no-renumber",		0,	"don't reset the line number at\n"
						"the start of a page." },
    { 'v', 'v', "first-page",	"NUMBER",	"On each page, start numbering at\n"
						"NUMBER" },
    { 'i', 'i', "page-increment", "NUMBER",	"Increment line number by NUMBER" },
    { 'l', 'l', "join-blank-lines", "NUMBER",	"Treat NUMBER blank lines as one line", },
    { 's', 's', "number-separator", "DELIM",	"Separate numbers from the string\n"
						"with DELIM" },
    { 'w', 'w', "number-width", "WIDTH",	"Make the number field WIDTH characters\n"
						"wide (6 is the default)" },
    { 'n', 'n', "number-format", "FMT",		"Set the numbering format" },
    { 'd', 'd', "section-delimiter", "CC",	"Set the section delimiter" },
    { '?', '?', "help",			0,	"Get this menu" },
    { 'V',  0 , "version",		0,	"Print the version number, then\n"
						"exit." },
} ;
#define NR_OPTIONS	(sizeof options/sizeof options[0])


/*
 * usage() spits out an error message then exits with the appropriate status.
 */
static void
usage(int yes_we_have_an_error)
{
    fprintf(stderr, "\nusage: %s [options]\n\n", __progname);
    showopts(stderr, NR_OPTIONS, options);

    exit (yes_we_have_an_error ? EXIT_FAILURE : EXIT_SUCCESS);
} /* usage */


/*
 * set_style() sets up a header/body/footer printing style
 */
void
set_style(Style *sty, char *arg)
{
    switch (arg[0]) {
    case 'a':	/* number all lines */
	sty->style = ALL;
	break;
    case 't':	/* number nonblank lines */
	sty->style = NONEMPTY;
	break;
    case 'n':	/* don't number lines */
	sty->style = NONE;
	break;
    case 'p':	/* number lines that match this RE */
	sty->style = RE;
	if (regcomp(&(sty->regex), 1+arg, REG_NOSUB|REG_NEWLINE) != 0) {
	    fprintf(stderr, "%s: badly formed RE (%s) in %s style\n",
				__progname, 1+arg, sty->title);
	    exit(EXIT_FAILURE);
	}
	break;
    default:	/* complain bitterly, then die */
	fprintf(stderr, "%s: bad %s style (%s). Legal ones are\n"
			"\ta\tNumber all lines\n"
			"\tt\tNumber nonempty lines\n"
			"\tn\tDon't number lines\n"
			"\tpRE\tOnly number lines matching RE\n",
			__progname, sty->title, arg);
	usage(1);
    }
} /* set_style */


/*
 * istrailer() looks at a line to see if it's a trailer flag
 */
static int
istrailer(rlString *line)
{
    return (line->size == 3) && (memcmp(line->data, marker, 2) == 0);
} /* istrailer */


/*
 * isbody() looks at a line to see if it's a body flag
 */
static int
isbody(rlString *line)
{
    rlString substr = { line->data, line->size-2, EOF };

    if (line->size == 5 && istrailer(&substr))
	return memcmp((line->data)+2, marker, 2) == 0;
    return 0;
} /* isbody */


/* 
 * isheader() looks at a line to see if it's a header flag
 */
static int
isheader(rlString *line)
{
    rlString substr = { line->data, line->size-2, EOF };

    if (line->size == 7 && isbody(&substr))
	return memcmp((line->data)+4, marker, 2) == 0;
    return 0;
} /* isheader */


/*
 * emit_line() spits out a line with an optional prefix
 */
static void
emit_line(rlString *line, int blankforce)
{
    switch (styles[section].style) {
    case RE:
	    if (regexec(&(styles[section].regex), line->data, 0, 0, 0) != 0) {
		/* if the RE doesn't match, dummy up a blank number line
		 * to bring this column into conformance with all the
		 * other lines.  For empty lines, we don't need to do
		 * anything, of course.
		 */
		if (line->size > 1)
		    printf("%*s%s", number_width, "", delimiter);
		break;
	    }
	    goto print;
    case NONEMPTY:
	    if (line->size == 1 && !blankforce)
		break;
    case ALL:
    print:
	    printf(number_format_string, lineno);
	    lineno += line_increment;
	    break;
    case NONE:
	    break;
    }
    rlputs(line, stdout);
} /* emit_line */


/*
 * nl() spits out a single file, aux line numbers
 */
void
nl(FILE *file)
{
    static rlString line;
    static rlString eol = { "\n", 1, EOF };
    int blank=0;
    char *p;

    lineno = first_line_number;
    section = BODY;

    while (rlgets(&line, file) != 0) {
	/* if the line contains nothing, just bump up the
	 * blank line counter
	 */
	if (merge_lines > 0 && line.size == 1) {
	    if (blank++ == merge_lines)
		emit_line(&line, 1);
	    else
		emit_line(&eol, 0);
	    continue;
	}
	blank = 0;
	if (isheader(&line)) {
	    if (!dont_renumber)
		lineno = first_line_number;
	    section = HEADER;
	}
	else if (isbody(&line)) {
	    if (section != HEADER && section != TRAILER && !dont_renumber)
		lineno = first_line_number;
	    section = BODY;
	}
	else if (istrailer(&line))
	    section = TRAILER;
	else {
	    emit_line(&line, 0);
	    continue;
	}
	emit_line(&eol, 0);	/* if it's a header, body, or trailer flag,
				 * spit out a blank line after processing
				 * the flag
				 */
    }
} /* nl */


/*
 * nl, in mortal flesh
 */
void
main(int argc, char **argv)
{
    int opt;
    int x;
    FILE *file;

    __progname = basename(argv[0]);

    while ((opt = x_getopt(argc, argv, NR_OPTIONS, options)) != EOF)
	switch (opt) {
	case 'f':
		set_style(&styles[TRAILER], x_optarg);
		break;
	case 'b':
		set_style(&styles[BODY], x_optarg);
		break;
	case 'h':
		set_style(&styles[HEADER], x_optarg);
		break;
	case 'p':
		dont_renumber = 1;
		break;
	case 'v':
		first_line_number = atoi(x_optarg);
		break;
	case 'i':
		line_increment = atoi(x_optarg);
		break;
	case 'l':
		merge_lines = atoi(x_optarg);
		break;
	case 's':
		delimiter = x_optarg;
		break;
	case 'w':
		number_width = atoi(x_optarg);
		break;
	case 'n':
		if (strcmp(x_optarg, "ln") == 0)
		    number_format = LN;
		else if (strcmp(x_optarg, "rn") == 0)
		    number_format = RN;
		else if (strcmp(x_optarg, "rz") == 0)
		    number_format = RZ;
		else {
		    fprintf(stderr,"%s: bad number format!\n"
				    "\tln\tleft justified, no leading zeros\n"
				    "\trn\tright justified, no leading zeros\n"
				    "\trz\tright justified, leading zeros\n",
			    __progname);
		    exit(EXIT_FAILURE);
		}
		break;
	case 'd':
		if (x_optarg[0]) {
		    marker[0] = x_optarg[0];
		    if (x_optarg[1])
			marker[1] = x_optarg[1];
		}
		break;
	case 'V':	puts("$Revision: 1.3 $");
			exit(EXIT_SUCCESS);
	case '?':
	default:	usage(opt == '?');
	}

    argc -= x_optind;
    argv += x_optind;

    /* build up the number format string before charging into
     * battle
     */
    switch (number_format) {
    case LN:
	snprintf(number_format_string, sizeof number_format_string,
		    "%%-%dld%s", number_width, delimiter);
	break;
    case RZ:
	snprintf(number_format_string, sizeof number_format_string,
		    "%%0%dld%s", number_width, delimiter);
	break;
    case RN:
    default:
	snprintf(number_format_string, sizeof number_format_string,
		    "%%%dld%s", number_width, delimiter);
	break;
    }

    if (argc == 0)
	nl(stdin);
    else
	for (x = 0; x < argc; x++)
	    if (strcmp(argv[x], "-") == 0)
		nl(stdin);
	    else if ((file = fopen(argv[x], "r")) != 0) {
		nl(file);
		fclose(file);
	    }
	    else
		perror(argv[x]);

    exit(EXIT_SUCCESS);
} /* main */
