/*****************************************************************************
*   $Id: options.c,v 3.1 1997/05/13 05:34:26 darren Exp $
*
*   Copyright (c) 1996-1997, Darren Hiebert
*
*   Contains functions to process command line options.
*****************************************************************************/

/*============================================================================
=   Include files
============================================================================*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

/*  To declare "struct stat" and stat().
 */
#if __MWERKS__
# include <stat.h>	    /* there is no sys directory on the Mac */
#else
# include <sys/types.h>
# include <sys/stat.h>
#endif

#ifdef DEBUG
# include <assert.h>
#endif

#include "ctags.h"

/*============================================================================
=   Defines
============================================================================*/

/*----------------------------------------------------------------------------
 *  Miscellaneous defines
 *--------------------------------------------------------------------------*/

#define VERSION	    "Exuberant Ctags, Version 1.61, by Darren Hiebert"

#define CTAGS_INVOCATION  "\
  Usage: ctags [-aBdeFnNsStTuwWx] [-{f|o} name] [-h list] [-i [+-=]types]\n\
               [-I list] [-L file] [-p path] [--help] file(s)\n"

#define ETAGS_INVOCATION  "\
  Usage: etags [-adsStTx] [-{f|o} name] [-h list] [-i [+-=]types]\n\
               [-I list] [-L file] [-p path] [--help] file(s)\n"

#define CTAGS_ENVIRONMENT	"CTAGS"
#define ETAGS_ENVIRONMENT	"ETAGS"

/*  The following separators are permitted for list options.
 */
#define HEADER_SEPARATORS   ".,;"
#define IGNORE_SEPARATORS   ",; \t\n"

/*============================================================================
=   Data declarations
============================================================================*/
typedef struct {
    boolean usedByEtags;
    const char *const description;
} optionDescription;

/*============================================================================
=   Data definitions
============================================================================*/

/*----------------------------------------------------------------------------
-   Globally shared
----------------------------------------------------------------------------*/

optionValues Option = {
    {
	TRUE,		/* -id */
	TRUE,		/* -ie */
	TRUE,		/* -if */
	TRUE,		/* -ig */
	FALSE,		/* -ip */
	TRUE,		/* -it */
	TRUE,		/* -iv */
	FALSE,		/* -iP */
	TRUE,		/* -iS */
	TRUE		/* -iX */
    },
    { NULL, 0, 0 },	/* -I */
    FALSE,		/* -a */
    FALSE,		/* -B */
    FALSE,		/* -e */
    EX_MIX,		/* -n */
    NULL,		/* -p */
    FALSE,		/* -u */
    FALSE,		/* -w */
    FALSE,		/* -x */
    NULL,		/* -L */
    NULL,		/* -o */
    { "h", "H", "hpp", "hxx", "h++", NULL },	    /* -h */
#ifdef DEBUG
    0, 0,		/* -D, -b */
#endif
    FALSE,		/* started as etags */
    FALSE		/* brace formatting */
};

/*----------------------------------------------------------------------------
-   Locally used only
----------------------------------------------------------------------------*/

static optionDescription OptionDescription[] = {
 {1,"  --help     Prints a more detailed help message."},
 {1,"  -a         Append tags to existing tag file."},
#ifdef DEBUG
 {1,"  -b <line>  Set break line."},
#endif
 {0,"  -B         Use backward searching patterns (?...?)."},
#ifdef DEBUG
 {1,"  -D <level> Set debug level."},
#endif
 {0,"  -e         Output tag file for use with Emacs."},
 {1,"  -f <name>  Name for output tag file (default \"tags\"; or \"TAGS\" if -e)."},
 {0,"  -F         Use forward searching patterns (/.../) (default)."},
 {1,"  -h <list>  List of header file extensions (default \".h.H.hpp.hxx.h++\")."},
 {1,"  -i <types> List of tag types to include [defgptvPSX] (default \"=defgtvSX\")."},
 {1,"  -I <list>  List of tokens to ignore is read from command line or file."},
 {1,"  -L <file>  List of source file names are read from specified file."},
 {0,"  -n         Use line numbers in tag file instead of search patterns."},
 {0,"  -N         Use search patterns in tag file instead of line numbers."},
 {1,"  -o <name>  Alternative for -f."},
 {1,"  -p <path>  Default path to use for all (relative path) filenames."},
 {0,"  -u         Unsorted; do not sort tags. Disables duplicate tag warnings."},
 {0,"  -w         Exclude warnings about duplicate tags (default)."},
 {0,"  -W         Generate warnings about duplicate tags."},
 {1,"  -x         Print tabular cross reference file to standard output."},
 {1,"  -[dsStT]   Backward compatibility options. See --help for more info."},
 {1, NULL}
};

static optionDescription LongOptionDescription[] = {
 {1,"  -a   Append the tags to an existing tag file."},
#ifdef DEBUG
 {1,"  -b <line>"},
 {1,"       Set break line."},
#endif
 {0,"  -B   Use backward searching patterns (?...?)."},
#ifdef DEBUG
 {1,"  -D <level>"},
 {1,"       Set debug level."},
#endif
 {0,"  -e   Output tag file for use with Emacs."},
 {1,"  -f <name>"},
 {1,"       Output tags to the specified file (default is \"tags\"; or \"TAGS\""},
 {1,"       if -e is specified). If specified as \"-\", tags are written to"},
 {1,"       standard output."},
 {0,"  -F   Use forward searching patterns (/.../) (default)."},
 {1,"  -h <list>"},
 {1,"       Specifies a list of file extensions used for headers."},
 {1,"       The default list is \".h.H.hpp.hxx.h++\"."},
 {1,"  -i <types>"},
 {1,"       Specifies the list of tag types to include in the output file."},
 {1,"       \"Types\" is a group of letters designating the types of tags"},
 {1,"       affected. Each letter or group of letters may be preceded by"},
 {1,"       either a '+' sign (default, if omitted) to add it to those already"},
 {1,"       included, a '-' sign to exclude it from the list (e.g. to exclude"},
 {1,"       a default tag type), or an '=' sign to include its corresponding"},
 {1,"       tag type at the exclusion of those not listed. A space separating"},
 {1,"       the option letter from the list is optional. The following tag"},
 {1,"       types are supported:"},
 {1,"          d   macro definitions"},
 {1,"          e   enumerated values (values inside enum{...})"},
 {1,"          f   function and method definitions"},
 {1,"          g   enum/struct/union tags (or new C++ types)"},
 {1,"          p   external function prototypes"},
 {1,"          t   typedefs"},
 {1,"          v   variable declarations"},
 {1,"       In addition, the following two modifiers are accepted:"},
 {1,"          P   prefix static tags with \"filename:\" (Elvis 1.x style)."},
 {1,"          S   include static tags (special case of above types)"},
 {1,"          X   include extension flags at end of tag file lines"},
 {1,"       The default value of list is \"=defgtvSX\" (i.e all tag types"},
 {1,"       except for function prototypes; include static tags but do not"},
 {1,"       prefix them)."},
 {1,"  -I <list | file>"},
 {1,"       A list of tokens to ignore is read from either the command line,"},
 {1,"       or the specified file (if leading character is '.', '/', or '\\')."},
 {1,"       Particularly useful when a function definition or declaration"},
 {1,"       contains some special macro before the parameter list."},
 {1,"  -L <file>"},
 {1,"       A list of source file names are read from the specified file."},
 {1,"       If specified as \"-\", then standard input is read."},
 {0,"  -n   Use line numbers in tag file instead of search patterns."},
 {0,"  -N   Use search patterns in tag file instead of line numbers."},
 {1,"  -o   Alternative for -f."},
 {1,"  -p <path>"},
 {1,"       Default path to use for all (relative path) filenames."},
 {0,"  -u   Unsorted; do not sort tags. Disables warnings for duplicate tags"},
 {0,"  -w   Exclude warnings about duplicate tags (default)."},
 {0,"  -W   Generate warnings about duplicate tags."},
 {1,"  -x   Print a tabular cross reference file to standard output."},
 {1,""},
 {1,"  The following tag control options are accepted only for backwards"},
 {1,"  compatibility with other versions of ctags and (except for -s) are"},
 {1,"  selected by default in this version. Use of the -i option is preferred."},
 {1,"  -d   Include macro definitions (equiv. to -i+d)."},
 {1,"  -s   Include static tags prefixed by \"filename:\" (equiv. to -i+SP)."},
 {1,"  -S   Include static tags without filename prefix (equiv. to -i+S-P)."},
 {1,"  -t   Include typedefs (equiv. to -i+t)."},
 {1,"  -T   Include typedefs and enum/struct/union tags (equiv. to -i+gt)."},
 {1, NULL}
};

/*  Contains a set of strings describing the set of "features" compiled into
 *  the code.
 */
static const char *const Features[] = {
#ifdef DEBUG
    "debug",
#endif
#ifdef WIN32
    "win32",
#endif
#ifdef DJGPP
    "msdos_32",
#else
# ifdef MSDOS
    "msdos_16",
# endif
#endif
#ifdef OS2
    "os2",
#endif
#ifdef AMIGA
    "amiga",
#endif
#ifndef EXTERNAL_SORT
    "internal_sort",
#endif
    NULL
};

/*============================================================================
=   Function prototypes
============================================================================*/
static void printfFeatureList __ARGS((FILE *const where));
static void printInvocationDescription __ARGS((FILE *const where));
static void printOptionDescriptions __ARGS((const optionDescription *const optDesc, FILE *const where));
static void printHelp __ARGS((const optionDescription *const optDesc, FILE *const where));
static void readExtensionList __ARGS((char *const list));
static void clearTagList __ARGS((void));
static void applyTagInclusionList __ARGS((const char *const list));
static void readIgnoreList __ARGS((char *const list));
static void readIgnoreListFromFile __ARGS((const char *const fileName));
static char *readOptionArg __ARGS((const int option, char **const arg, char *const *const argList, int *const argNum));
static void processHeaderListOption __ARGS((const int option, char **const argP, char *const *const argList, int *const argNumP));
static void processIgnoreOption __ARGS((const int option, char **const argP, char *const *const argList, int *const argNumP));
static void processExtendedOption __ARGS((const char *const optionString));
static void processLongOption __ARGS((const int option, char **const argP, char *const *const argList, int *const argNumP));
static boolean processShortOption __ARGS((const int option));
static void parseStringToArgs __ARGS((const char *const string, char *parsedBuffer, char **const argList, const int maxArgs));
static char **creatArgListForString __ARGS((const char *const string));

/*============================================================================
=   Function definitions
============================================================================*/

static void printfFeatureList( where )
    FILE *const where;
{
    int i;

    for (i = 0 ; Features[i] != NULL ; ++i)
    {
	if (i == 0)
	    fputs(" (", where);
	fprintf(where, "%s+%s", (i>0 ? ", " : ""), Features[i]);
    }
    fputs(i>0 ? ")" : "", where);
}

static void printInvocationDescription( where )
    FILE *const where;
{
    if (Option.startedAsEtags)
	fprintf(where, ETAGS_INVOCATION);
    else
	fprintf(where, CTAGS_INVOCATION);
}

static void printOptionDescriptions( optDesc, where )
    const optionDescription *const optDesc;
    FILE *const where;
{
    int i;

    for (i = 0 ; optDesc[i].description != NULL ; ++i)
    {
	if (! Option.startedAsEtags || optDesc[i].usedByEtags)
	{
	    fputs(optDesc[i].description, where);
	    fputc('\n', where);
	}
    }
}

static void printHelp( optDesc, where )
    const optionDescription *const optDesc;
    FILE *const where;
{

    fprintf(where, "%s", VERSION);
    printfFeatureList(where);
    fputc('\n', where);

    printInvocationDescription(where);
    printOptionDescriptions(optDesc, where);
}

extern void printUsage( error )
    const char *const error;
{
    printHelp(OptionDescription, errout);
    if (error != NULL)
    {
	fprintf(errout, "\nError: %s", error);
#if !defined(MSDOS) && !defined(WIN32) && !defined(OS2)
	fputs("\n", errout);
#endif
    }
    exit(1);
}

/*  Reads a list of header file extensions.
 */
static void readExtensionList( list )
    char *const list;
{
    const char *extension = strtok(list, HEADER_SEPARATORS);
    int extIndex = 0;

    while (extension != NULL  &&  extIndex < MaxHeaderExtensions)
    {
#ifdef DEBUG
	if (debug(DEBUG_STATUS))
	    printf("header extension: %s\n", extension);
#endif
	Option.headerExt[extIndex++] = extension;
	extension = strtok(NULL, HEADER_SEPARATORS);
    }
    Option.headerExt[extIndex] = NULL;
}

static void clearTagList()
{
    Option.include.defines	= FALSE;
    Option.include.enumValues	= FALSE;
    Option.include.functions	= FALSE;
    Option.include.blockTags	= FALSE;
    Option.include.prototypes	= FALSE;
    Option.include.typedefs	= FALSE;
    Option.include.variables	= FALSE;
    Option.include.prefix	= FALSE;
    Option.include.statics	= FALSE;
    Option.include.extensions	= FALSE;
}

static void applyTagInclusionList( list )
    const char *const list;
{
    boolean mode = TRUE;	/* default mode is to add following types */
    const char *p;

    for (p = list  ;  *p != '\0'  ;  ++p)
    {
	switch (*p)
	{
	case '=':	/* exclusive mode; ONLY types following are included */
	    clearTagList();
	    mode = TRUE;
	    break;
	case '+': mode = TRUE;	break;	    /* types following are included */
	case '-': mode = FALSE;	break;	    /* types following are excluded */

	case 'd': Option.include.defines    = mode;	break;
	case 'e': Option.include.enumValues = mode;	break;
	case 'f': Option.include.functions  = mode;	break;
	case 'g': Option.include.blockTags  = mode;	break;
	case 'p': Option.include.prototypes = mode;	break;
	case 't': Option.include.typedefs   = mode;	break;
	case 'v': Option.include.variables  = mode;	break;
	case 'P': Option.include.prefix	    = mode;	break;
	case 'S': Option.include.statics    = mode;	break;
	case 'X': Option.include.extensions = mode;	break;

	default:
	    {
		char msg[80];

		sprintf(msg, "-i: Invalid tag option '%c'", *p);
		printUsage(msg);
		break;
	    }
	}
    }
}

static void readIgnoreList( list )
    char *const list;
{
    const char *token = strtok(list, IGNORE_SEPARATORS);

    while (token != NULL)
    {
	unsigned int i = Option.ignore.count++;

	if (Option.ignore.count > Option.ignore.max)
	{
	    Option.ignore.max = Option.ignore.count + 10;
	    Option.ignore.list = (char **)realloc(Option.ignore.list,
					    Option.ignore.max * sizeof(char *));
	}
	Option.ignore.list[i] = (char *)malloc(strlen(token) + 1);
	strcpy(Option.ignore.list[i], token);
#ifdef DEBUG
	if (debug(DEBUG_STATUS))
	    printf("ignore token: %s\n", token);
#endif
	token = strtok(NULL, IGNORE_SEPARATORS);
    }
}

static void readIgnoreListFromFile( fileName )
    const char *const fileName;
{
    FILE *const fp = fopen(fileName, "r");

    if (fp == NULL)
    {
	perror(fileName);
	exit(1);
    }
    else
    {
	char ignoreToken[MaxNameLength];
	unsigned int i = Option.ignore.count;

	while (fscanf(fp, "%255s", ignoreToken) == 1)
	    ++Option.ignore.count;
	rewind(fp);
	if (Option.ignore.count > Option.ignore.max)
	{
	    Option.ignore.max = Option.ignore.count;
	    Option.ignore.list = (char **)realloc(Option.ignore.list,
					    Option.ignore.max * sizeof(char *));
	}
	if (Option.ignore.list == NULL)
	    perror("Cannot create ignore list");
	else while (fscanf(fp, "%255s", ignoreToken) == 1)
	{
#ifdef DEBUG
	    assert(i < Option.ignore.count);
#endif
	    Option.ignore.list[i] = (char *)malloc(strlen(ignoreToken) + 1);
	    strcpy(Option.ignore.list[i], ignoreToken);
	    ++i;
#ifdef DEBUG
	    if (debug(DEBUG_STATUS))
		printf("ignore token: %s\n", ignoreToken);
#endif
	}
    }
}

static char *readOptionArg( option, arg, argList, argNum )
    const int option;
    char **const arg;
    char *const *const argList;
    int *const argNum;
{
    char *list = NULL;

    if ((*arg)[0] != '\0')	    /* does list immediately follow option? */
    {
	list = *arg;
	*arg += strlen(*arg);
    }
    else if ((list = argList[++(*argNum)]) == NULL) /* at least 1 more arg? */
    {
	char msg[80];

	sprintf(msg, "-%c: Parameter missing", option);
	printUsage(msg);
    }
#ifdef DEBUG
    if (debug(DEBUG_OPTION))
	fputs(list, stderr);
#endif
    return list;
}

extern void freeIgnoreList()
{
    while (Option.ignore.count > 0)
	free(Option.ignore.list[--Option.ignore.count]);

    if (Option.ignore.list != NULL)
	free(Option.ignore.list);

    Option.ignore.list = NULL;
    Option.ignore.max = 0;
}

static void processHeaderListOption( option, argP, argList, argNumP )
    const int option;
    char **const argP;
    char *const *const argList;
    int *const argNumP;
{
    struct stat file_status;
    char *const param = readOptionArg(option, argP, argList, argNumP);

    /*  Check to make sure that the user did not enter "ctags -h *.c"
     *  by testing to see if the list is a filename that exists.
     */
    if (stat(param, &file_status) == 0)
	printUsage("-h: Invalid list");
    else
	readExtensionList(param);
}

static void processIgnoreOption( option, argP, argList, argNumP )
    const int option;
    char **const argP;
    char *const *const argList;
    int *const argNumP;
{
    char *const param = readOptionArg(option, argP, argList, argNumP);

    if (strchr("./\\", param[0]) != NULL)
	readIgnoreListFromFile(param);
    else
	readIgnoreList(param);
}

static void processExtendedOption( optionString )
    const char *const optionString;
{
    if (strcmp(optionString,"help") == 0)
    {
	printHelp(LongOptionDescription, stdout);
	exit(0);
    }
}

static void processLongOption( option, argP, argList, argNumP )
    const int option;
    char **const argP;
    char *const *const argList;
    int *const argNumP;
{
    char *param = NULL;

#ifdef DEBUG
    if (debug(DEBUG_OPTION))
	fprintf(stderr, "Option: -%c ", option);
#endif
    switch (option)
    {
    /*	Options requiring parameters.
    */
    case '-':	processExtendedOption(*argP);
		break;
    case 'f':
    case 'o':	Option.tagFile = readOptionArg(option, argP, argList, argNumP);
		break;
    case 'h':	processHeaderListOption(option, argP, argList, argNumP);
		break;
    case 'i':	param = readOptionArg(option, argP, argList, argNumP);
		applyTagInclusionList(param);
		break;
    case 'I':	processIgnoreOption(option, argP, argList, argNumP);
		break;
    case 'L':	Option.fileList = readOptionArg(option, argP, argList, argNumP);
		break;
    case 'p':	Option.path = readOptionArg(option, argP, argList, argNumP);
		break;
#ifdef DEBUG
    case 'D':	param = readOptionArg(option, argP, argList, argNumP);
		Option.debugLevel = atoi(param);
		break;
    case 'b':	param = readOptionArg(option, argP, argList, argNumP);
		Option.breakLine = atol(param);
		break;
#endif
    default:
	{
	char msg[40];

	sprintf(msg, "Invalid option character: '%c'", option);
	printUsage(msg);
	break;
	}
    }
#ifdef DEBUG
    if (debug(DEBUG_OPTION))
	fputs("\n", stderr);
#endif
}

static boolean processShortOption( option )
    const int option;
{
    boolean handled = TRUE;

    switch (option)
    {
    case 'a':	Option.append		= TRUE;		break;
    case 'B':	Option.backward		= TRUE;		break;
    case 'd':	Option.include.defines	= TRUE;		break;
    case 'e':	Option.etags		= TRUE;
		Option.unsorted		= TRUE;		break;
    case 'F':	Option.backward		= FALSE;	break;
    case 'n':	Option.locate		= EX_LINENUM;	break;
    case 'N':	Option.locate		= EX_PATTERN;	break;
    case 's':	Option.include.statics	= TRUE;
		Option.include.prefix	= TRUE;		break;
    case 'S':	Option.include.statics	= TRUE;
		Option.include.prefix	= FALSE;	break;
    case 't':	Option.include.typedefs	= TRUE;		break;
    case 'T':	Option.include.blockTags= TRUE;
		Option.include.typedefs	= TRUE;		break;
    case 'u':	Option.unsorted		= TRUE;		break;
    case 'w':	Option.warnings		= FALSE;	break;
    case 'W':	Option.warnings		= TRUE;		break;
    case 'x':	Option.xref		= TRUE;		break;
    case '?':	printHelp(LongOptionDescription, stdout);
		exit(0);

    default:	handled = FALSE;			break;
    }
#ifdef DEBUG
    if (handled && debug(DEBUG_OPTION))
	fprintf(stderr, "Option: -%c\n", option);
#endif

    return handled;
}

extern char *const *parseOptions( argList )
    char *const *const argList;
{
    int	argNum;

    for (argNum = 0  ;  argList[argNum] != NULL  ;  ++argNum)
    {
	char *arg = argList[argNum];
	int c;

	if (*arg++ != '-')		/* stop at first non-option switch */
	    break;
	while ((c = *arg++) != '\0')
	    if (! processShortOption(c))
		processLongOption(c, &arg, argList, &argNum);
    }
    return &argList[argNum];
}

static void parseStringToArgs( string, parsedBuffer, argList, maxArgs )
    const char *const string;
    char *parsedBuffer;
    char **const argList;
    const int maxArgs;
{
    boolean argInProgress = FALSE;
    int count = 0;
    const char *src;

    for (src = string  ;  *src != '\0'  ;  ++src)
    {
	if (*src == ' ')			/* designates end of argument */
	{
	    if (argInProgress)
	    {
		*parsedBuffer++ = '\0';		/* terminate arg in progress */
		argInProgress = FALSE;
		if (count >= maxArgs)
		    break;
	    }
	}
	else
	{
	    if (! argInProgress)
	    {
		argInProgress = TRUE;
		argList[count++] = parsedBuffer;	/* point to new arg */
	    }
	    if (*src == '\\')			/* next character is literal */
		++src;				/* skip over '\\' */
	    *parsedBuffer++ = *src;
	}
    }
    *parsedBuffer = '\0';		/* null terminate last argument */
    argList[count] = NULL;		/* terminate list */
}

static char **creatArgListForString( string )
    const char *const string;
{
    char **argList = NULL;

    if (string != NULL  &&  string[0] != '\0')
    {
	/*  We place the parsed string at the end of the memory block, past
	 *  the bottom of the argument table.
	 */
	const size_t argListSize= (MaxEnvOptions + 1) * sizeof(char *);
	const size_t blockSize	= argListSize + strlen(string) + 1;

	argList	= (char **)malloc(blockSize);
	if (argList != NULL)
	    parseStringToArgs(string, (char *)argList + argListSize,
			      argList, MaxEnvOptions);
    }
    return argList;
}

extern void *parseEnvironmentOptions()
{
    const char *envOptions = NULL;
    char **argList = NULL;

    if (Option.startedAsEtags)
	envOptions = getenv(ETAGS_ENVIRONMENT);
    if (envOptions == NULL)
	envOptions = getenv(CTAGS_ENVIRONMENT);
    if (envOptions != NULL)
    {
	argList = creatArgListForString(envOptions);
	parseOptions(argList);
    }
    return argList;
}

/* vi:set tabstop=8 shiftwidth=4: */
