/*
# getline.c
# Written by D'Arcy J.M. Cain
# darcy@druid.com
# Copyright 1991, 1992

# This code may be used on any computer system for any purpose by anyone

NAME
	getline, xgetline, wgetline

SYNOPSIS
	char *getline(FILE *fp, int exclusive);
	char *xgetline(FILE *fp, int exclusive);
	char *wgetline(WINDOW *win, int exclusive);

	xcat [file ...] # shell function

DESCRIPTION
	Reads a line from the stream given by fp (or from the window given by
	win in wgetline) and returns a pointer to the string.  There is no
	length restriction on the returned string.  Space is dynamically
	allocated for the string as needed.  If the exclusive flag is set then
	the space won't be reused on the next call.

	In the xgetline version anything from '#' till the end of line is
	ignored and A  trailing '\' character is treated as a continuation
	character.  After this processing the resulting line is ignored if
	it is empty.  The '#' and '\' characters can be included by preceding
	them with a '\'.  Note that the leading backslash is left in the input
	string and must be dealt with by the caller.  This can be somewhat of a
	problem at the end of a line but in general should be workable.

	When the exclusive flag is set the space is made available to the
	caller on the same basis as malloc(3).  When finished with the
	string it should be free'ed.  If the flag is reset then the space
	is reused during the next call.

	If the first argument is NULL then any existing buffer is dealt with
	based on the exclusive flag.  If zero then the memory is freed otherwise
	it is protected as if the last call had the flag set.

	Defining XCAT causes a main function to be included allowing the
	xgetline function to be available in shell programs.

	I prototype getline and xgetline in stdio.h since their use always
	involves stdio routines.  Likewise I added a wgetline prototype
	to curses.h.  See further discussion below.

RETURNS
	A pointer to the string without the terminating newline is returned
	if successful or NULL if there was an error or end of file.  Use
	feof(3) and ferror(3) to find out if it was a file error, memory
	allocation problem or EOF condition.

AUTHOR
	D'Arcy J.M. Cain (darcy@druid.com)
	D'Arcy Cain Consulting

*/

/*
Note:  I prototype getline and xgetline and declare xline in stdio.h and
wgetline in curses.h.  I use "#ifdef __CAIN_H" wrappers so that it doesn't
interfere with other programs that don't know about my routines.  Feel
free to remove this line and prototype manually in your code or set up
your own system.
*/

#include	<stdio.h>
#include	<ctype.h>
#include	<errno.h>
#include	<stdlib.h>

#ifndef		CTRL
#define		CTRL(x)		((x) & 0x1f)
#endif

/* I originally was going to use 80 here as the most common case but */
/* decided that a few extra bytes to save a malloc from time to time */
/* would be a better choice.  Comments welcome.  */
#define		CHUNK	128

/* XCAT implies XGETLINE */
#ifdef	XCAT
#ifndef	XGETLINE_VERSION
#define	XGETLINE_VERSION
#endif
#endif

#ifdef	CURSES_VERSION
#define		STREAM		WINDOW
#define		getline		wgetline
#include	<curses.h>
#else
#define		STREAM		FILE
#ifdef	XGETLINE_VERSION
#define		getline		xgetline
int			xline = 0;
#endif	/* XGETLINE_VERSION */
#endif	/* CURSES_VERSION */

#ifdef	__MSDOS__
#ifdef	CURSES_VERSION
#error	There is no DOS Curses version
#else
#define	mygetc(fp)	getc(fp)
#endif	/* CURSES_VERSION */
#else
static int
mygetc(STREAM *fp)
{
	int			c;

#ifdef	CURSES_VERSION
	if ((c = wgetch(fp)) == '\r')
		return('\n');
#else
	static int	last = -1;

	if (last != -1)
	{
		c = last;
		last = -1;
	}

	if ((c = getc(fp)) == '\r')
	{
		if ((c = getc(fp)) != '\n')
			last = c;

		return('\n');
	}
#endif	/* CURSES_VERSION */
	return(c);
}
#endif	/* __MSDOS__ */


char *
getline(STREAM *fp, int exclusive)
{
	static char	*buf = NULL;
	size_t	sz = CHUNK;		/* this keeps track of the current size of buffer */
	size_t	i = 0;			/* index into string tracking current position */
	char	*ptr;			/* since we may set buf to NULL before returning */
	int		c;				/* to store getc return */
#ifdef	XGETLINE_VERSION
	int		in_comment = 0;	/* if we are in a comment */
	int		in_quote = 0;	/* if we are in quote and holds quote character */
#endif

	/* release or protect memory if first arg is NULL */
	if (!fp)
	{
		if (buf && !exclusive)
			free(buf);

		buf = NULL;
		return(NULL);
	}

	/* start out with buf set to CHUNK + 2 bytes */
	/* note that if this routine was previously called with exclusive */
	/* set that malloc rather than realloc will be called due to buf */
	/* being set to NULL in the "if (exclusive) code at end of routine */
	if (buf == NULL)
		buf = (char *)(malloc(CHUNK + 2));
	else
		buf = (char *)(realloc(buf, CHUNK + 2));

	/* check for memory problem */
	if (buf == NULL)
		return(NULL);

#ifdef	CURSES_VERSION
	/* get characters from window until newline or carriage return */
	while ((c = mygetc(fp)) != '\n')
#else
	/* get characters from stream until EOF */
#ifdef	XGETLINE_VERSION
	while ((c = mygetc(fp)) != EOF)
#else
	while ((c = mygetc(fp)) != EOF && c != '\n')
#endif
#endif
	{
#ifdef	XGETLINE_VERSION
		buf[i] = c;

		if (c == '\n')
		{
			in_comment = in_quote = 0;		/* nl ends comment and quote */
			xline++;						/* keep track of current line */

			/* lose trailing spaces */
			for (i++; i && isspace(buf[i - 1]); i--)
				buf[i - 1] = 0;

			if (i)
				break;
		}
		else if (in_comment)
		{
			/* continuation still ends comment - alternative would be silly */
			if (c == '\\' && (c = getc(fp)) == '\n')
			{
				in_comment = 0;
				xline++;

				while (isspace(c = getc(fp)))
					;

				ungetc(c, fp);
			}
		}
		else if (in_quote)
		{
			i++;

			if (c == in_quote)
				in_quote = 0;
		}
		else if (c == '\'' || c == '"')
		{
			i++;
			in_quote = c;
		}
		else if (c == '#')
			in_comment = 1;
		else if (c == '\\')
		{
			if ((c = getc(fp)) != '\n' && c != EOF)
			{
				buf[i++] = '\\';
				buf[i++] = c;
			}
			else
			{
				while (isspace(c = getc(fp)))
					;

				ungetc(c, fp);
				buf[i++] = ' ';
				xline++;
			}
		}
		else
			i++;

#else
#ifdef	CURSES_VERSION
		if (c == CTRL('L'))
			clearok(fp, 1);
		else
#endif

		/* the following needed in case we are in cbreak or raw mode */
		if (c != '\b')
			buf[i++] = c;
		else if (i)
			i--;
#endif

		/* check for buffer overflow */
		if (i >= sz)
			if ((buf = (char *)(realloc(buf, (sz += CHUNK) + 2))) == NULL)
				return(NULL);
	}

	/* is there anything to return? */
#ifndef	CURSES_VERSION
	if (c == EOF && !i)
	{
		free(buf);
		buf = NULL;
		return(NULL);
	}
#endif

	buf[i++] = 0;	/* yes I want the ++ */

	/* the realloc may be overkill here in most cases - perhaps it */
	/* should be moved to the 'if (exclusive)' block */
	ptr = buf = (char *)(realloc(buf, i));

	/* prevent reuse if necessary */
	if (exclusive)
		buf = NULL;

	return(ptr);
}

/* don't bother with TEST_MODULE if building curses version */
/* most of the testing for the curses version can be done as getline */
#ifdef	TEST_MODULE
int		main(void)
{
	char	*p;

	while ((p = getline(stdin, 0)) != NULL)
		printf("%s\n", p);

	return(0);
}
#endif

#ifdef	XCAT
#include	<string.h>

static void
xcat(FILE *fp)
{
	char	*p;

	while ((p = xgetline(fp, 0)) != NULL)
		printf("%s\n", p);
}

int
main(int argc, char **argv)
{
	FILE	*fp;
	int		k;

	if (argc < 2)
		xcat(stdin);
	else for (k = 1; k < argc; k++)
	{
		if ((fp = fopen(argv[k], "r")) == NULL)
			fprintf(stderr, "xcat: Can't open file %s - %s\n",
								argv[k], strerror(errno));
		else
		{
			xcat(fp);
			fclose(fp);
		}
	}

	return(0);
}
#endif	/* XCAT */
