/*
*
*   Copyright (c) 1996-2001, Darren Hiebert
*
*   This source code is released for free distribution under the terms of the
*   GNU General Public License.
*
*   This module contains functions to process command line options.
*/

/*
*   INCLUDE FILES
*/
#include "general.h"	/* must always come first */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>	/* to declare isspace () */

#include "ctags.h"
#include "debug.h"
#include "main.h"
#define OPTION_WRITE
#include "options.h"
#include "parse.h"

/*
*   MACROS
*/
#define INVOCATION  "Usage: %s [options] [file(s)]\n"

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

#define CTAGS_FILE	"tags"
#define ETAGS_FILE	"TAGS"

#ifndef ETAGS
# define ETAGS	"etags"		/* name which causes default use of to -e */
#endif

/*  The following separators are permitted for list options.
 */
#define EXTENSION_SEPARATOR '.'
#define PATTERN_START '('
#define PATTERN_STOP  ')'
#define IGNORE_SEPARATORS   ", \t\n"

#ifndef DEFAULT_FILE_FORMAT
# define DEFAULT_FILE_FORMAT	2
#endif

#if defined (MSDOS) || defined (WIN32) || defined (OS2) || defined (AMIGA) || defined (HAVE_OPENDIR)
# define RECURSE_SUPPORTED
#endif

#define isCompoundOption(c)	(boolean) (strchr ("fohiILpDb", (c)) != NULL)

/*
*   Data declarations
*/

enum eOptionLimits {
    MaxHeaderExtensions	= 100,	/* maximum number of extensions in -h option */
    MaxSupportedTagFormat = 2
};

typedef struct sOptionDescription {
    int usedByEtags;
    const char *const description;
} optionDescription;

typedef void (*parametricOptionHandler) (const char *const option, const char *const parameter);

typedef const struct {
    const char* name;		/* name of option as specified by user */
    parametricOptionHandler handler;	/* routine to handle option */
    boolean initOnly;		/* option must be specified before any files */
} parametricOption;

typedef const struct {
    const char* name;		/* name of option as specified by user */
    boolean* pValue;		/* pointer to option value */
    boolean initOnly;		/* option must be specified before any files */
} booleanOption;

/*
*   DATA DEFINITIONS
*/

static boolean ParsedLeadingOptions = FALSE;

static const char *const HeaderExtensions [] = {
    "h", "H", "hh", "hpp", "hxx", "h++", "inc", "def", NULL
};

optionValues Option = {
    {
	FALSE,		/* --extra=f */
	FALSE,		/* --extra=q */
	TRUE,		/* --file-scope */
    },
    {
	TRUE,		/* -fields=a */
	TRUE,		/* -fields=f */
	FALSE,		/* -fields=m */
	TRUE,		/* -fields=i */
	FALSE,		/* -fields=k */
	TRUE,		/* -fields=z */
	TRUE,		/* -fields=K */
	FALSE,		/* -fields=l */
	TRUE,		/* -fields=n */
	TRUE,		/* -fields=s */
	TRUE,		/* -fields=P */
	TRUE		/* -fields=A */
    },
    NULL,		/* -I */
    FALSE,		/* -a */
    FALSE,		/* -B */
    FALSE,		/* -e */
#ifdef MACROS_USE_PATTERNS
    EX_PATTERN,		/* -n, --excmd */
#else
    EX_MIX,		/* -n, --excmd */
#endif
    FALSE,		/* -R */
    TRUE,		/* -u, --sort */
    FALSE,		/* -V */
    FALSE,		/* -x */
    NULL,		/* -L */
    NULL,		/* -o */
    NULL,		/* -h */
    NULL,		/* --etags-include */
    DEFAULT_FILE_FORMAT,/* --format */
    FALSE,		/* --if0 */
    FALSE,		/* --kind-long */
    LANG_AUTO,		/* --lang */
    TRUE,		/* --links */
    FALSE,		/* --filter */
    NULL,		/* --filter-terminator */
    FALSE,		/* --qualified-tags */
    FALSE,		/* --tag-relative */
    FALSE,		/* --totals */
    FALSE,		/* --line-directives */
	FALSE,
#ifdef TM_DEBUG
    0, 0		/* -D, -b */
#endif
};

/*
-   Locally used only
*/

static optionDescription LongOptionDescription [] = {
 {1,"  -a   Append the tags to an existing tag file."},
#ifdef TM_DEBUG
 {1,"  -b <line>"},
 {1,"       Set break line."},
#endif
 {0,"  -B   Use backward searching patterns (?...?)."},
#ifdef TM_DEBUG
 {1,"  -D <level>"},
 {1,"       Set debug level."},
#endif
 {0,"  -e   Output tag file for use with Emacs."},
 {1,"  -f <name>"},
 {1,"       Write tags to specified file. Value of \"-\" writes tags to stdout"},
 {1,"       [\"tags\"; or \"TAGS\" when -e supplied]."},
 {0,"  -F   Use forward searching patterns (/.../) (default)."},
 {1,"  -h <list>"},
 {1,"       Specify list of file extensions to be treated as include files."},
 {1,"       [\".h.H.hh.hpp.hxx.h++\"]."},
 {1,"  -I <list|@file>"},
 {1,"       A list of tokens to be specially handled is read from either the"},
 {1,"       command line or the specified file."},
 {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   Equivalent to --excmd=number."},
 {0,"  -N   Equivalent to --excmd=pattern."},
 {1,"  -o   Alternative for -f."},
#ifdef RECURSE_SUPPORTED
 {1,"  -R   Equivalent to --recurse."},
#else
 {1,"  -R   Not supported on this platform."},
#endif
 {0,"  -u   Equivalent to --sort=no."},
 {1,"  -V   Equivalent to --verbose."},
 {1,"  -x   Print a tabular cross reference file to standard output."},
 {1,"  --append=[yes|no]"},
 {1,"       Should tags should be appended to existing tag file [no]?"},
 {1,"  --etags-include=file"},
 {1,"      Include reference to 'file' in Emacs-style tag file (requires -e)."},
 {1,"  --exclude=pattern"},
 {1,"      Exclude files and directories matching 'pattern'."},
 {0,"  --excmd=number|pattern|mix"},
#ifdef MACROS_USE_PATTERNS
 {0,"       Uses the specified type of EX command to locate tags [pattern]."},
#else
 {0,"       Uses the specified type of EX command to locate tags [mix]."},
#endif
 {1,"  --extra=[+|-]flags"},
 {1,"      Include extra tag entries for selected information (flags: \"fq\")."},
 {1,"  --fields=[+|-]flags"},
 {1,"      Include selected extension fields (flags: \"afmikKlnszAP\") [fks]."},
 {1,"  --file-scope=[yes|no]"},
 {1,"       Should tags scoped only for a single file (e.g. \"static\" tags"},
 {1,"       be included in the output [yes]?"},
 {1,"  --filter=[yes|no]"},
 {1,"       Behave as a filter, reading file names from standard input and"},
 {1,"       writing tags to standard output [no]."},
 {1,"  --filter-terminator=string"},
 {1,"       Specify string to print to stdout following the tags for each file"},
 {1,"       parsed when --filter is enabled."},
 {0,"  --nest"},
 {0,"      Nest inside function bodies for tags."},
 {0,"  --format=level"},
#if DEFAULT_FILE_FORMAT == 1
 {0,"       Force output of specified tag file format [1]."},
#else
 {0,"       Force output of specified tag file format [2]."},
#endif
 {1,"  --help"},
 {1,"       Print this option summary."},
 {1,"  --if0=[yes|no]"},
 {1,"       Should C code within #if 0 conditional branches be parsed [no]?"},
 {1,"  --langdef=name"},
 {1,"       Define a new language to be parsed with regular expressions."},
 {1,"  --langmap=map(s)"},
 {1,"       Override default mapping of language to source file extension."},
 {1,"  --language-force=language"},
 {1,"       Force all files to be interpreated with specified language."},
 {1,"  --languages=[+|-]list"},
 {1,"       Restrict files scanned for tags to those mapped to langauges"},
 {1,"       specified in the comma-separated 'list'. The list can contain any"},
 {1,"       built-in or user-defined language [all]."},
 {1,"  --license"},
 {1,"       Print details of software license."},
 {0,"  --line-directives=[yes|no]"},
 {0,"       Should #line directives be processed [no]?"},
 {1,"  --links=[yes|no]"},
 {1,"       Indicate whether symbolic links should be followed [yes]."},
 {1,"  --options=file"},
 {1,"       Specify file from which command line options should be read."},
 {1,"  --recurse=[yes|no]"},
#ifdef RECURSE_SUPPORTED
 {1,"       Recurse into directories supplied on command line [no]."},
#else
 {1,"       Not supported on this platform."},
#endif
#ifdef HAVE_REGEX
 {1,"  --regex-<LANG>=/line_pattern/name_pattern/[flags]"},
 {1,"       Define regular expression for locating tags in specific language."},
#endif
 {0,"  --sort=[yes|no]"},
 {0,"       Should tags be sorted [yes]?"},
 {0,"  --tag-relative=[yes|no]"},
 {0,"       Should paths be relative to location of tag file [no; yes when -e]?"},
 {1,"  --totals=[yes|no]"},
 {1,"       Print statistics about source and tag files [no]."},
 {1,"  --verbose=[yes|no]"},
 {1,"       Enable verbose messages describing actions on each source file."},
 {1,"  --version"},
 {1,"       Print version identifier to standard output."},
 {1, NULL}
};

static const char* const License =
"This program is free software; you can redistribute it and/or\n\
modify it under the terms of the GNU General Public License\n\
as published by the Free Software Foundation; either version 2\n\
of the License, or (at your option) any later version.\n\
\n\
This program is distributed in the hope that it will be useful,\n\
but WITHOUT ANY WARRANTY; without even the implied warranty of\n\
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n\
GNU General Public License for more details.\n\
\n\
You should have received a copy of the GNU General Public License\n\
along with this program; if not, write to the Free Software\n\
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\n";

/*  Contains a set of strings describing the set of "features" compiled into
 *  the code.
 */
static const char *const Features [] = {
#ifdef WIN32
    "win32",
#endif
#ifdef DJGPP
    "msdos_32",
#else
# ifdef MSDOS
    "msdos_16",
# endif
#endif
#ifdef OS2
    "os2",
#endif
#ifdef AMIGA
    "amiga",
#endif
#ifdef VMS
    "vms",
#endif
#ifdef HAVE_FNMATCH
    "wildcards",
#endif
#ifdef HAVE_REGEX
    "regex",
#endif
#ifndef EXTERNAL_SORT
    "internal-sort",
#endif
#ifdef CUSTOM_CONFIGURATION_FILE
    "custom-conf",
#endif
#if (defined (MSDOS) || defined (WIN32) || defined (OS2)) && defined (UNIX_PATH_SEPARATOR)
    "unix-path-separator",
#endif
#ifdef TM_DEBUG
    "debug",
#endif
    NULL
};

/*
*   FUNCTION PROTOTYPES
*/
static boolean parseFileOptions (const char *const fileName);

/*
*   FUNCTION DEFINITIONS
*/

extern void verbose (const char *const format, ...)
{
    if (Option.verbose)
    {
	va_list ap;
	va_start (ap, format);
	vprintf (format, ap);
	va_end (ap);
    }
}

static char *stringCopy (const char *const string)
{
    char* result = NULL;
    if (string != NULL)
	result = eStrdup (string);
    return result;
}

static void freeString (char **const pString)
{
    if (*pString != NULL)
    {
	eFree (*pString);
	*pString = NULL;
    }
}

extern void freeList (stringList** const pList)
{
    if (*pList != NULL)
    {
	stringListDelete (*pList);
	*pList = NULL;
    }
}

extern void setDefaultTagFileName (void)
{
    if (Option.tagFileName != NULL)
	;		/* accept given name */
    else if (Option.etags)
	Option.tagFileName = stringCopy (ETAGS_FILE);
    else
	Option.tagFileName = stringCopy (CTAGS_FILE);
}

extern void checkOptions (void)
{
    const char* notice;
    if (Option.xref)
    {
	notice = "xref output";
	if (Option.include.fileNames)
	{
	    error (WARNING, "%s disables file name tags", notice);
	    Option.include.fileNames = FALSE;
	}
    }
    if (Option.append)
    {
	notice = "append mode is not compatible with";
	if (isDestinationStdout ())
	    error (FATAL, "%s tags to stdout", notice);
    }
    if (Option.filter)
    {
	notice = "filter mode";
	if (Option.printTotals)
	{
	    error (WARNING, "%s disables totals", notice);
	    Option.printTotals = FALSE;
	}
	if (Option.tagFileName != NULL)
	    error (WARNING, "%s ignores output tag file name", notice);
    }
#ifdef UPDATE_ENABLED
    if (Option.update)
    {
	notice = "update option is not compatible with";
	if (Option.etags)
	    error (FATAL, "%s emacs-style tags", notice);
	if (Option.filter)
	    error (FATAL, "%s filter option", notice);
	if (isDestinationStdout ())
	    error (FATAL, "%s tags to stdout", notice);
	Option.append = TRUE;
    }
#endif
}

static void setEtagsMode (void)
{
    Option.etags = TRUE;
    Option.sorted = FALSE;
    Option.lineDirectives = FALSE;
    Option.tagRelative = TRUE;
}

extern void testEtagsInvocation (void)
{
    char* const execName = eStrdup (getExecutableName ());
    char* const etags = eStrdup (ETAGS);
#ifdef CASE_INSENSITIVE_FILENAMES
    toLowerString (execName);
    toLowerString (etags);
#endif
    if (strstr (execName, etags) != NULL)
    {
	verbose ("Running in etags mode\n");
	setEtagsMode ();
    }
}

/*
 *  Cooked argument parsing
 */

static void parseShortOption (cookedArgs *const args)
{
    args->simple [0] = *args->shortOptions++;
    args->simple [1] = '\0';
    args->item = args->simple;
    if (! isCompoundOption (*args->simple))
	args->parameter = "";
    else if (*args->shortOptions == '\0')
    {
	argForth (args->args);
	if (argOff (args->args))
	    args->parameter = NULL;
	else
	    args->parameter = argItem (args->args);
	args->shortOptions = NULL;
    }
    else
    {
	args->parameter = args->shortOptions;
	args->shortOptions = NULL;
    }
}

static void parseLongOption (cookedArgs *const args, const char *item)
{
    const char* const equal = strchr (item, '=');
    if (equal == NULL)
    {
	args->item = eStrdup (item);
	args->parameter = "";
    }
    else
    {
	const size_t length = equal - item;
	args->item = xMalloc (length + 1, char);
	strncpy (args->item, item, length);
	args->item [length] = '\0';
	args->parameter = equal + 1;
    }
}

static void cArgRead (cookedArgs *const current)
{
    char* item;

    Assert (current != NULL);
    if (! argOff (current->args))
    {
	item = argItem (current->args);
	current->shortOptions = NULL;
	Assert (item != NULL);
	if (strncmp (item, "--", (size_t) 2) == 0)
	{
	    current->isOption = TRUE;
	    current->longOption = TRUE;
	    parseLongOption (current, item + 2);
	}
	else if (*item == '-')
	{
	    current->isOption = TRUE;
	    current->longOption = FALSE;
	    current->shortOptions = item + 1;
	    parseShortOption (current);
	}
	else
	{
	    current->isOption = FALSE;
	    current->longOption = FALSE;
	    current->item = item;
	    current->parameter = "";
	}
    }
}

extern cookedArgs* cArgNewFromString (const char* string)
{
    cookedArgs* const result = xMalloc (1, cookedArgs);
    memset (result, 0, sizeof (cookedArgs));
    result->args = argNewFromString (string);
    cArgRead (result);
    return result;
}

extern cookedArgs* cArgNewFromArgv (char* const* const argv)
{
    cookedArgs* const result = xMalloc (1, cookedArgs);
    memset (result, 0, sizeof (cookedArgs));
    result->args = argNewFromArgv (argv);
    cArgRead (result);
    return result;
}

extern cookedArgs* cArgNewFromFile (FILE* const fp)
{
    cookedArgs* const result = xMalloc (1, cookedArgs);
    memset (result, 0, sizeof (cookedArgs));
    result->args = argNewFromFile (fp);
    cArgRead (result);
    return result;
}

extern cookedArgs* cArgNewFromLineFile (FILE* const fp)
{
    cookedArgs* const result = xMalloc (1, cookedArgs);
    memset (result, 0, sizeof (cookedArgs));
    result->args = argNewFromLineFile (fp);
    cArgRead (result);
    return result;
}

extern void cArgDelete (cookedArgs* const current)
{
    Assert (current != NULL);
    argDelete (current->args);
    memset (current, 0, sizeof (cookedArgs));
    eFree (current);
}

static boolean cArgOptionPending (cookedArgs* const current)
{
    boolean result = FALSE;
    if (current->shortOptions != NULL)
	if (*current->shortOptions != '\0')
	    result = TRUE;
    return result;
}

extern boolean cArgOff (cookedArgs* const current)
{
    Assert (current != NULL);
    return (boolean) (argOff (current->args) && ! cArgOptionPending (current));
}

extern boolean cArgIsOption (cookedArgs* const current)
{
    Assert (current != NULL);
    return current->isOption;
}

extern const char* cArgItem (cookedArgs* const current)
{
    Assert (current != NULL);
    return current->item;
}

extern void cArgForth (cookedArgs* const current)
{
    Assert (current != NULL);
    Assert (! cArgOff (current));
    if (cArgOptionPending (current))
	parseShortOption (current);
    else
    {
	Assert (! argOff (current->args));
	argForth (current->args);
	if (! argOff (current->args))
	    cArgRead (current);
	else
	{
	    current->isOption = FALSE;
	    current->longOption = FALSE;
	    current->shortOptions = NULL;
	    current->item = NULL;
	    current->parameter = NULL;
	}
    }
}

/*
 *  File extension and language mapping
 */

static void addExtensionList (stringList *const slist,
			      const char *const elist, const boolean clear)
{
    char *const extensionList = eStrdup (elist);
    const char *extension = NULL;
    boolean first = TRUE;

    if (clear)
    {
	verbose ("      clearing\n");
	stringListClear (slist);
    }
    verbose ("      adding: ");
    if (elist != NULL  &&  *elist != '\0')
    {
	extension = extensionList;
	if (elist [0] == EXTENSION_SEPARATOR)
	    ++extension;
    }
    while (extension != NULL)
    {
	char *separator = strchr (extension, EXTENSION_SEPARATOR);
	if (separator != NULL)
	    *separator = '\0';
	verbose ("%s%s", first ? "" : ", ",
		*extension == '\0' ? "(NONE)" : extension);
	stringListAdd (slist, vStringNewInit (extension));
	first = FALSE;
	if (separator == NULL)
	    extension = NULL;
	else
	    extension = separator + 1;
    }
    if (Option.verbose)
    {
	printf ("\n      now: ");
	stringListPrint (slist);
	putchar ('\n');
    }
    eFree (extensionList);
}

extern const char *fileExtension (const char *const fileName)
{
    const char *extension;
    const char *pDelimiter = NULL;
#ifdef QDOS
    pDelimiter = strrchr (fileName, '_');
#endif
    if (pDelimiter == NULL)
        pDelimiter = strrchr (fileName, '.');

    if (pDelimiter == NULL)
	extension = "";
    else
	extension = pDelimiter + 1;	/* skip to first char of extension */

    return extension;
}

/*  Determines whether the specified file name is considered to be a header
 *  file for the purposes of determining whether enclosed tags are global or
 *  static.
 */
extern boolean isIncludeFile (const char *const fileName)
{
    boolean result = FALSE;
    const char *const extension = fileExtension (fileName);
    if (Option.headerExt != NULL)
	result = stringListExtensionMatched (Option.headerExt, extension);
    return result;
}

static void processLanguageForceOption (const char *const option,
					const char *const parameter)
{
    langType language;
    if (stricmp (parameter, "auto") == 0)
	language = LANG_AUTO;
    else
	language = getNamedLanguage (parameter);

    if (strcmp (option, "lang") == 0  ||  strcmp (option, "language") == 0)
	error (WARNING,
	       "\"--%s\" option is obsolete; use \"--language-force\" instead",
	       option);
    if (language == LANG_IGNORE)
	error (FATAL, "Uknown language specified in \"%s\" option", option);
    else
	Option.language = language;
}

static char* skipPastMap (char* p)
{
    while (*p != EXTENSION_SEPARATOR  &&
	    *p != PATTERN_START  &&  *p != ','  &&  *p != '\0')
	++p;
    return p;
}

/* Parses the mapping beginning at `map', adds it to the language map, and
 * returns first character past the map.
 */
static char* addLanguageMap (const langType language, char* map)
{
    char* p = NULL;
    const char first = *map;
    if (first == EXTENSION_SEPARATOR)	    /* extension map */
    {
	++map;
	p = skipPastMap (map);
	if (*p == '\0')
	{
	    verbose (" .%s", map);
	    addLanguageExtensionMap (language, map);
	    p = map + strlen (map);
	}
	else
	{
	    const char separator = *p;
	    *p = '\0';
	    verbose (" .%s", map);
	    addLanguageExtensionMap (language, map);
	    *p = separator;
	}
    }
    else if (first == PATTERN_START)	    /* pattern map */
    {
	++map;
	for (p = map  ;  *p != PATTERN_STOP  &&  *p != '\0'  ;  ++p)
	{
	    if (*p == '\\'  &&  *(p + 1) == PATTERN_STOP)
		++p;
	}
	if (*p == '\0')
	    error (FATAL, "Unterminated file name pattern for %s language",
	       getLanguageName (language));
	else
	{
	    *p++ = '\0';
	    verbose (" (%s)", map);
	    addLanguagePatternMap (language, map);
	}
    }
    else
	error (FATAL, "Badly formed language map for %s language",
		getLanguageName (language));
    return p;
}

static char* processLanguageMap (char* map)
{
    char* const separator = strchr (map, ':');
    char* result = NULL;
    if (separator != NULL)
    {
	langType language;
	char *list = separator + 1;
	boolean clear = FALSE;
	*separator = '\0';
	language = getNamedLanguage (map);
	if (language != LANG_IGNORE)
	{
	    char* p;
	    if (*list == '+')
		++list;
	    else
		clear = TRUE;
	    for (p = list  ;  *p != ','  &&  *p != '\0'  ;  ++p) /*no-op*/ ;
	    if (strnicmp (list, "default", p - list) == 0)
	    {
		verbose ("    Restoring default %s language map: ", getLanguageName (language));
		installLanguageMapDefault (language);
		list = p;
	    }
	    else
	    {
		if (clear)
		{
		    verbose ("    Setting %s language map:", getLanguageName (language));
		    clearLanguageMap (language);
		}
		else
		    verbose ("    Adding to %s language map:", getLanguageName (language));
		while (list != NULL  &&  *list != '\0'  &&  *list != ',')
		    list = addLanguageMap (language, list);
		verbose ("\n");
	    }
	    if (list != NULL  &&  *list == ',')
		++list;
	    result = list;
	}
    }
    return result;
}

static void processLanguageMapOption (const char *const option,
				      const char *const parameter)
{
    char *const maps = eStrdup (parameter);
    char *map = maps;

    if (strcmp (parameter, "default") == 0)
    {
	verbose ("    Restoring default language maps:\n");
	installLanguageMapDefaults ();
    }
    else while (map != NULL  &&  *map != '\0')
    {
	char* const next = processLanguageMap (map);
	if (next == NULL)
	    error (WARNING, "Unknown language specified in \"%s\" option", option);
	map = next;
    }
    eFree (maps);
}

static void processLanguageListOption (const char *const option,
				       const char *const parameter)
{
    char *const langs = eStrdup (parameter);
    enum { Add, Remove, Replace } mode = Replace;
    boolean first = TRUE;
    char *lang = langs;
    const char* prefix = "";
    verbose ("    Enabled languages: ");
    while (lang != NULL)
    {
	char *const end = strchr (lang, ',');
	if (lang [0] == '+')
	{
	    ++lang;
	    mode = Add;
	    prefix = "+ ";
	}
	else if (lang [0] == '-')
	{
	    ++lang;
	    mode = Remove;
	    prefix = "- ";
	}
	if (mode == Replace)
	    enableLanguages (FALSE);
	if (end != NULL)
	    *end = '\0';
	if (lang [0] != '\0')
	{
	    if (strcmp (lang, "all") == 0)
		enableLanguages ((boolean) (mode != Remove));
	    else
	    {
		const langType language = getNamedLanguage (lang);
		if (language == LANG_IGNORE)
		    error (WARNING, "Unknown language specified in \"%s\" option", option);
		else
		    enableLanguage (language, (boolean) (mode != Remove));
	    }
	    verbose ("%s%s%s", (first ? "" : ", "), prefix, lang);
	    prefix = "";
	    first = FALSE;
	    if (mode == Replace)
		mode = Add;
	}
	lang = (end != NULL ? end + 1 : NULL);
    }
    verbose ("\n");
    eFree (langs);
}

static void processOptionFile (const char *const option,
			       const char *const parameter)
{
    if (parameter == NULL  ||  parameter [0] == '\0')
	error (WARNING, "no option file supplied for \"%s\"", option);
    else if (! parseFileOptions (parameter))
	error (FATAL | PERROR, "cannot open option file \"%s\"", parameter);
}

static void installHeaderListDefaults (void)
{
    Option.headerExt = stringListNewFromArgv (HeaderExtensions);
    if (Option.verbose)
    {
	printf ("    Setting default header extensions: ");
	stringListPrint (Option.headerExt);
	putchar ('\n');
    }
}

static void processHeaderListOption (const int option, const char *parameter)
{
    /*  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 (doesFileExist (parameter))
	error (FATAL, "-%c: Invalid list", option);
    if (strcmp (parameter, "default") == 0)
	installHeaderListDefaults ();
    else
    {
	boolean clear = TRUE;

	if (parameter [0] == '+')
	{
	    ++parameter;
	    clear = FALSE;
	}
	if (Option.headerExt == NULL)
	    Option.headerExt = stringListNew ();
	verbose ("    Header Extensions:\n");
	addExtensionList (Option.headerExt, parameter, clear);
    }
}

/*
 *  Token ignore processing
 */

/*  Determines whether or not "name" should be ignored, per the ignore list.
 */
extern boolean isIgnoreToken (const char *const name,
			      boolean *const pIgnoreParens,
			      const char **const replacement)
{
    boolean result = FALSE;

    if (Option.ignore != NULL)
    {
	const size_t nameLen = strlen (name);
	unsigned int i;

	if (pIgnoreParens != NULL)
	    *pIgnoreParens = FALSE;

	for (i = 0  ;  i < stringListCount (Option.ignore)  ;  ++i)
	{
	    vString *token = stringListItem (Option.ignore, i);

	    if (strncmp (vStringValue (token), name, nameLen) == 0)
	    {
		const size_t tokenLen = vStringLength (token);

		if (nameLen == tokenLen)
		{
		    result = TRUE;
		    break;
		}
		else if (tokenLen == nameLen + 1  &&
			vStringChar (token, tokenLen - 1) == '+')
		{
		    result = TRUE;
		    if (pIgnoreParens != NULL)
			*pIgnoreParens = TRUE;
		    break;
		}
		else if (vStringChar (token, nameLen) == '=')
		{
		    if (replacement != NULL)
			*replacement = vStringValue (token) + nameLen + 1;
		    break;
		}
	    }
	}
    }
    return result;
}

static void saveIgnoreToken (vString *const ignoreToken)
{
    if (Option.ignore == NULL)
	Option.ignore = stringListNew ();
    stringListAdd (Option.ignore, ignoreToken);
    verbose ("    ignore token: %s\n", vStringValue (ignoreToken));
}

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

    while (token != NULL)
    {
	vString *const entry = vStringNewInit (token);

	saveIgnoreToken (entry);
	token = strtok (NULL, IGNORE_SEPARATORS);
    }
    eFree (newList);
}

void addIgnoreListFromFile (const char *const fileName)
{
    stringList* tokens = stringListNewFromFile (fileName);
    if (Option.ignore == NULL)
	Option.ignore = tokens;
    else
	stringListCombine (Option.ignore, tokens);
}

static void processIgnoreOption (const char *const list)
{
    if (strchr ("@./\\", list [0]) != NULL)
    {
	const char* fileName = (*list == '@') ? list + 1 : list;
	addIgnoreListFromFile (fileName);
    }
#if defined (MSDOS) || defined (WIN32) || defined (OS2)
    else if (isalpha (list [0])  &&  list [1] == ':')
	addIgnoreListFromFile (list);
#endif
    else if (strcmp (list, "-") == 0)
    {
	freeList (&Option.ignore);
	verbose ("    clearing list\n");
    }
    else
	readIgnoreList (list);
}

/*
 *  Specific option processing
 */

static void printfFeatureList (void)
{
    int i;

    for (i = 0 ; Features [i] != NULL ; ++i)
    {
	if (i == 0)
	    printf ("  Optional compiled features: ");
	printf ("%s+%s", (i>0 ? ", " : ""), Features [i]);
#ifdef CUSTOM_CONFIGURATION_FILE
	if (strcmp (Features [i], "custom-conf") == 0)
	    printf ("=%s", CUSTOM_CONFIGURATION_FILE);
#endif
    }
    if (i > 0)
	putchar ('\n');
}

static void printProgramIdentification (void)
{
    printf ("%s %s, Copyright (C) 1996-2001 %s\n",
	    PROGRAM_NAME, PROGRAM_VERSION, AUTHOR_NAME);
    printf ("  Addresses: <%s>, %s\n", AUTHOR_EMAIL, PROGRAM_URL);
    printfFeatureList ();
}

static void printInvocationDescription (void)
{
    printf (INVOCATION, getExecutableName ());
}

static void printOptionDescriptions (const optionDescription *const optDesc)
{
    int i;
    for (i = 0 ; optDesc [i].description != NULL ; ++i)
    {
	if (! Option.etags || optDesc [i].usedByEtags)
	    puts (optDesc [i].description);
    }
}

static void printHelp (const optionDescription *const optDesc)
{

    printProgramIdentification ();
    putchar ('\n');
    printInvocationDescription ();
    putchar ('\n');
    printOptionDescriptions (optDesc);
}

static void processExcmdOption (const char *const option,
				const char *const parameter)
{
    switch (*parameter)
    {
	case 'm': Option.locate = EX_MIX;	break;
	case 'n': Option.locate = EX_LINENUM;	break;
	case 'p': Option.locate = EX_PATTERN;	break;
	default:
	    error (FATAL, "Invalid value for \"%s\" option", option);
	    break;
    }
}

static void processFormatOption (const char *const option,
				 const char *const parameter)
{
    unsigned int format;

    if (sscanf (parameter, "%u", &format) < 1)
	error (FATAL, "Invalid value for \"%s\" option",option);
    else if (format <= (unsigned int) MaxSupportedTagFormat)
	Option.tagFileFormat = format;
    else
	error (FATAL, "Unsupported value for \"%s\" option", option);
}

static void processEtagsInclude (const char *const __unused__ option,
				 const char *const parameter)
{
    vString *const file = vStringNewInit (parameter);
    if (Option.etagsInclude == NULL)
	Option.etagsInclude = stringListNew ();
    stringListAdd (Option.etagsInclude, file);
}

static void processFilterTerminatorOption (const char *const __unused__ option,
					   const char *const parameter)
{
    freeString (&Option.filterTerminator);
    Option.filterTerminator = stringCopy (parameter);
}

static void processExtensionFieldsOption (const char *const option,
					  const char *const parameter)
{
    struct sExtFields *const field = &Option.extensionFields;
    const char *p = parameter;
    boolean mode = TRUE;
    int c;

    if (*p != '+'  &&  *p != '-')
    {
	field->access		= FALSE;
	field->fileScope	= FALSE;
	field->implementation	= FALSE;
	field->inheritance	= FALSE;
	field->kind		= FALSE;
	field->kindKey		= FALSE;
	field->kindLong		= FALSE;
	field->language		= FALSE;
	field->scope		= FALSE;
	field->argList		= FALSE;
    }
    while ((c = *p++) != '\0') switch (c)
    {
	case '+': mode = TRUE;			break;
	case '-': mode = FALSE;			break;

	case 'a': field->access		= mode;	break;
	case 'f': field->fileScope	= mode;	break;
	case 'm': field->implementation	= mode;	break;
	case 'i': field->inheritance	= mode;	break;
	case 'k': field->kind		= mode;	break;
	case 'K': field->kindLong	= mode;	break;
	case 'l': field->language	= mode;	break;
	case 'n': field->lineNumber	= mode;	break;
	case 's': field->scope		= mode;	break;
	case 'z': field->kindKey	= mode;	break;
	case 'P': field->filePosition	= mode;	break;
	case 'A': field->argList	= mode;	break;

	default: error(WARNING, "Unsupported parameter '%c' for \"%s\" option",
		       c, option);
	    break;
    }
}

static void processExtraTagsOption (const char *const option,
				    const char *const parameter)
{
    struct sInclude *const inc = &Option.include;
    const char *p = parameter;
    boolean mode = TRUE;
    int c;

    if (*p != '+'  &&  *p != '-')
    {
	inc->fileNames		= FALSE;
	inc->qualifiedTags	= FALSE;
#if 0
	inc->fileScope		= FALSE;
#endif
    }
    while ((c = *p++) != '\0') switch (c)
    {
	case '+': mode = TRUE;			break;
	case '-': mode = FALSE;			break;

	case 'f': inc->fileNames	= mode;	break;
	case 'q': inc->qualifiedTags	= mode;	break;
#if 0
	case 'F': inc->fileScope	= mode;	break;
#endif

	default: error(WARNING, "Unsupported parameter '%c' for \"%s\" option",
		       c, option);
	    break;
    }
}

/*
 *  Option tables
 */

static parametricOption ParametricOptions [] = {
    { "etags-include",		processEtagsInclude,		FALSE	},
    { "exclude",		processExcludeOption,		FALSE	},
    { "excmd",			processExcmdOption,		FALSE	},
    { "extra",			processExtraTagsOption,		FALSE	},
    { "fields",			processExtensionFieldsOption,	FALSE	},
    { "filter-terminator",	processFilterTerminatorOption,	TRUE	},
    { "format",			processFormatOption,		TRUE	},
    { "lang",			processLanguageForceOption,	FALSE	},
    { "language",		processLanguageForceOption,	FALSE	},
    { "language-force",		processLanguageForceOption,	FALSE	},
    { "languages",		processLanguageListOption,	FALSE	},
    { "langdef",		processLanguageDefineOption,	FALSE	},
    { "langmap",		processLanguageMapOption,	FALSE	},
    { "options",		processOptionFile,		FALSE	},
};

static booleanOption BooleanOptions [] = {
    { "append",		&Option.append,			TRUE	},
    { "file-scope",	&Option.include.fileScope,	FALSE	},
    { "file-tags",	&Option.include.fileNames,	FALSE	},
    { "filter",		&Option.filter,			TRUE	},
    { "if0",		&Option.if0,			FALSE	},
    { "kind-long",	&Option.kindLong,		TRUE	},
    { "line-directives",&Option.lineDirectives,		FALSE	},
    { "nest",&Option.nestFunction,		FALSE	},
    { "links",		&Option.followLinks,		FALSE	},
#ifdef RECURSE_SUPPORTED
    { "recurse",	&Option.recurse,		FALSE	},
#endif
    { "sort",		&Option.sorted,			TRUE	},
    { "tag-relative",	&Option.tagRelative,		TRUE	},
    { "totals",		&Option.printTotals,		TRUE	},
    { "verbose",	&Option.verbose,		FALSE	},
};

/*
 *  Generic option parsing
 */

static void checkOptionOrder (const char* const option)
{
    if (ParsedLeadingOptions)
	error (FATAL, "-%s option may not follow a file name", option);
}

static boolean processParametricOption (const char *const option,
					const char *const parameter)
{
    const int count = sizeof (ParametricOptions) / sizeof (parametricOption);
    boolean found = FALSE;
    int i;

    for (i = 0  ;  i < count  &&  ! found  ;  ++i)
    {
	parametricOption* const entry = &ParametricOptions [i];
	if (strcmp (option, entry->name) == 0)
	{
	    found = TRUE;
	    if (entry->initOnly)
		checkOptionOrder (option);
	    (entry->handler) (option, parameter);
	}
    }
    return found;
}

static boolean getBooleanOption (const char *const option,
				 const char *const parameter)
{
    boolean selection = TRUE;

    if (parameter [0] == '\0')
	selection = TRUE;
    else if (strcmp (parameter, "0"  ) == 0  ||
	     strcmp (parameter, "no" ) == 0  ||
	     strcmp (parameter, "off") == 0)
	selection = FALSE;
    else if (strcmp (parameter, "1"  ) == 0  ||
	     strcmp (parameter, "yes") == 0  ||
	     strcmp (parameter, "on" ) == 0)
	selection = TRUE;
    else
	error (FATAL, "Invalid value for \"%s\" option", option);

    return selection;
}

static boolean processBooleanOption (const char *const option,
				     const char *const parameter)
{
    const int count = sizeof (BooleanOptions) / sizeof (booleanOption);
    boolean found = FALSE;
    int i;

    for (i = 0  ;  i < count  &&  ! found  ;  ++i)
    {
	booleanOption* const entry = &BooleanOptions [i];
	if (strcmp (option, entry->name) == 0)
	{
	    found = TRUE;
	    if (entry->initOnly)
		checkOptionOrder (option);
	    *entry->pValue = getBooleanOption (option, parameter);
	}
    }
    return found;
}

static void processLongOption (const char *const option,
			       const char *const parameter)
{
    if (parameter == NULL  ||  *parameter == '\0')
	verbose ("  Option: --%s\n", option);
    else
	verbose ("  Option: --%s=%s\n", option, parameter);

    if (processBooleanOption (option, parameter))
	;
    else if (processParametricOption (option, parameter))
	;
    else if (processKindOption (option, parameter))
	;
#ifdef HAVE_REGEX
    else if (processRegexOption (option, parameter))
	;
#endif
#define isOption(item)	(boolean) (strcmp (item,option) == 0)
    else if (isOption ("help"))
    {
	printHelp (LongOptionDescription);
	printKindOptions ();
	exit (0);
    }
    else if (isOption ("license"))
    {
	printProgramIdentification ();
	puts ("");
	puts (License);
	exit (0);
    }
    else if (isOption ("version"))
    {
	printProgramIdentification ();
	exit (0);
    }
#ifndef RECURSE_SUPPORTED
    else if (isOption ("recurse"))
	error (WARNING, "%s option not supported on this host", option);
#endif
    else
	error (FATAL, "Unknown option: --%s", option);
#undef isOption
}

static void processShortOption (const char *const option,
				const char *const parameter)
{
    if (parameter == NULL  ||  *parameter == '\0')
	verbose ("  Option: -%s\n", option);
    else
	verbose ("  Option: -%s %s\n", option, parameter);

    if (isCompoundOption (*option) && *parameter == '\0')
	error (FATAL, "Missing parameter for \"%s\" option", option);
    else switch (*option)
    {
	case '?':
	    printHelp (LongOptionDescription);
	    exit (0);
	    break;
	case 'a':
	    checkOptionOrder (option);
	    Option.append = TRUE;
	    break;
#ifdef TM_DEBUG
	case 'b':
	    if (atol (parameter) < 0)
		error (FATAL, "-%s: Invalid line number", option);
	    Option.breakLine = atol (parameter);
	    break;
	case 'D':
	    Option.debugLevel = strtol (parameter, NULL, 0);
	    if (debug (DEBUG_STATUS))
		Option.verbose = TRUE;
	    break;
#endif
	case 'B':
	    Option.backward = TRUE;
	    break;
	case 'e':
	    checkOptionOrder (option);
	    setEtagsMode ();
	    break;
	case 'f':
	case 'o':
	    checkOptionOrder (option);
	    if (Option.tagFileName != NULL)
	    {
		error (WARNING,
		    "-%s option specified more than once, last value used",
		    option);
		freeString (&Option.tagFileName);
	    }
	    else if (parameter [0] == '-'  &&  parameter [1] != '\0')
		error (FATAL, "output file name may not begin with a '-'");
	    Option.tagFileName = stringCopy (parameter);
	    break;
	case 'F':
	    Option.backward = FALSE;
	    break;
	case 'h':
	    processHeaderListOption (*option, parameter);
	    break;
	case 'i':
	    processLegacyKindOption (parameter);
	    break;
	case 'I':
	    processIgnoreOption (parameter);
	    break;
	case 'L':
	    if (Option.fileList != NULL)
	    {
		error (WARNING,
		    "-%s option specified more than once, last value used",
		    option);
		freeString (&Option.fileList);
	    }
	    Option.fileList = stringCopy (parameter);
	    break;
	case 'n':
	    Option.locate = EX_LINENUM;
	    break;
	case 'N':
	    Option.locate = EX_PATTERN;
	    break;
	case 'R':
#ifdef RECURSE_SUPPORTED
	    Option.recurse = TRUE;
#else
	    error (WARNING, "-%s option not supported on this host", option);
#endif
	    break;
	case 'u':
	    checkOptionOrder (option);
	    Option.sorted = FALSE;
	    break;
	case 'V':
	    Option.verbose = TRUE;
	    break;
	case 'w':
	    /* silently ignored */
	    break;
	case 'x':
	    checkOptionOrder (option);
	    Option.xref = TRUE;
	    break;
	default:
	    error (FATAL, "Unknown option: -%s", option);
	    break;
    }
}

extern void parseOption (cookedArgs* const args)
{
    Assert (! cArgOff (args));
    if (args->isOption)
    {
	if (args->longOption)
	    processLongOption (args->item, args->parameter);
	else
	{
	    const char *parameter = args->parameter;
	    while (*parameter == ' ')
		++parameter;
	    processShortOption (args->item, parameter);
	}
	cArgForth (args);
    }
}

extern void parseOptions (cookedArgs* const args)
{
    while (! cArgOff (args)  &&  cArgIsOption (args))
	parseOption (args);
    if (! cArgOff (args)  &&  ! cArgIsOption (args))
	ParsedLeadingOptions = TRUE;
}

static boolean parseFileOptions (const char* const fileName)
{
    boolean fileFound = FALSE;
    FILE* const fp = fopen (fileName, "r");
    if (fp != NULL)
    {
	cookedArgs* const args = cArgNewFromLineFile (fp);
	verbose ("Reading options from %s\n", fileName);
	parseOptions (args);
	cArgDelete (args);
	fclose (fp);
	fileFound = TRUE;
    }
    return fileFound;
}

extern void previewFirstOption (cookedArgs* const args)
{
    if (cArgIsOption (args) &&
	(strcmp (args->item, "V") == 0 || strcmp (args->item, "verbose") == 0))
    {
	parseOption (args);
    }
}

static void parseConfigurationFileOptions (void)
{
    const char* const home = getenv ("HOME");
    parseFileOptions ("/etc/ctags.conf");
    parseFileOptions ("/usr/local/etc/ctags.conf");
#ifdef CUSTOM_CONFIGURATION_FILE
    parseFileOptions (CUSTOM_CONFIGURATION_FILE);
#endif
    if (home != NULL)
    {
	vString* const dotFile = combinePathAndFile (home, ".ctags");
	parseFileOptions (vStringValue (dotFile));
	vStringDelete (dotFile);
    }
    parseFileOptions (".ctags");
}

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

    if (Option.etags)
	envOptions = getenv (ETAGS_ENVIRONMENT);
    if (envOptions == NULL)
	envOptions = getenv (CTAGS_ENVIRONMENT);
    if (envOptions != NULL  &&  envOptions [0] != '\0')
    {
	cookedArgs* const args = cArgNewFromString (envOptions);
	verbose ("Reading options from $CTAGS\n");
	parseOptions (args);
	cArgDelete (args);
    }
}

extern void readOptionConfiguration (void)
{
    parseConfigurationFileOptions ();
    parseEnvironmentOptions ();
}

/*
*   Option initialization
*/

extern void initOptions (void)
{
    verbose ("Setting option defaults\n");
    installHeaderListDefaults ();
    verbose ("    Installing default language mappings:\n");
    installLanguageMapDefaults ();
}

extern void freeOptionResources (void)
{
    freeString (&Option.tagFileName);
    freeString (&Option.fileList);
    freeString (&Option.filterTerminator);

    freeList (&Option.ignore);
    freeList (&Option.headerExt);
    freeList (&Option.etagsInclude);
}

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