// clang-format off
/*
*   $Id: options.c 576 2007-06-30 04:16:23Z elliotth $
*
*   Copyright (c) 1996-2003, 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 "third_party/ctags/general.h"  /* must always come first */

#include "libc/calls/calls.h"
#include "libc/calls/dprintf.h"
#include "libc/calls/termios.h"
#include "libc/fmt/conv.h"
#include "libc/limits.h"
#include "libc/mem/alg.h"
#include "libc/mem/mem.h"
#include "libc/runtime/runtime.h"
#include "libc/stdio/rand.h"
#include "libc/stdio/temp.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/exit.h"
#include "third_party/gdtoa/gdtoa.h"
#include "third_party/getopt/getopt.h"
#include "third_party/musl/crypt.h"
#include "third_party/musl/rand48.h"
#include "libc/mem/alg.h"
#include "libc/str/str.h"
#include "libc/calls/calls.h"
#include "libc/calls/dprintf.h"
#include "libc/calls/weirdtypes.h"
#include "libc/fmt/fmt.h"
#include "libc/mem/fmt.h"
#include "libc/stdio/stdio.h"
#include "libc/stdio/temp.h"
#include "third_party/musl/tempnam.h"
#include "libc/str/str.h"  /* to declare isspace () */

#include "third_party/ctags/ctags.h"
#include "third_party/ctags/debug.h"
#include "third_party/ctags/main.h"
#define OPTION_WRITE
#include "third_party/ctags/options.h"
#include "third_party/ctags/parse.h"
#include "third_party/ctags/routines.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 (HAVE_OPENDIR) || defined (HAVE_FINDFIRST) || defined (HAVE__FINDFIRST) || defined (AMIGA)
# 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 *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 NonOptionEncountered;
static stringList *OptionFiles;
static stringList* Excluded;
static boolean FilesRequired = TRUE;
static boolean SkipConfiguration;

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 */
	},
	{
		FALSE,  /* -fields=a */
		TRUE,   /* -fields=f */
		FALSE,  /* -fields=m */
		FALSE,  /* -fields=i */
		TRUE,   /* -fields=k */
		FALSE,  /* -fields=z */
		FALSE,  /* -fields=K */
		FALSE,  /* -fields=l */
		FALSE,  /* -fields=n */
		TRUE,   /* -fields=s */
		FALSE,  /* -fields=S */
		TRUE    /* -fields=t */
	},
	NULL,       /* -I */
	FALSE,      /* -a */
	FALSE,      /* -B */
	FALSE,      /* -e */
#ifdef MACROS_USE_PATTERNS
	EX_PATTERN, /* -n, --excmd */
#else
	EX_MIX,     /* -n, --excmd */
#endif
	FALSE,      /* -R */
	SO_SORTED,  /* -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,      /* --tag-relative */
	FALSE,      /* --totals */
	FALSE,      /* --line-directives */
#ifdef DEBUG
	0, 0        /* -D, -b */
#endif
};

/*
-   Locally used only
*/

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,"       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: \"afmikKlnsStz\") [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,"  --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,"  --<LANG>-kinds=[+|-]kinds"},
 {1,"       Enable/disable tag kinds for language <LANG>."},
 {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 interpreted using 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,"  --list-kinds=[language|all]"},
 {1,"       Output a list of all tag kinds for specified language or all."},
 {1,"  --list-languages"},
 {1,"       Output list of supported languages."},
 {1,"  --list-maps=[language|all]"},
 {1,"       Output list of language mappings."},
 {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|foldcase]"},
 {0,"       Should tags be sorted (optionally ignoring case) [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 License1 =
"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";
static const char* const License2 =
"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 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 boolean filesRequired (void)
{
	boolean result = FilesRequired;
	if (Option.recurse)
		result = FALSE;
	return result;
}

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);
	}
}

static void setEtagsMode (void)
{
	Option.etags = TRUE;
	Option.sorted = SO_UNSORTED;
	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 ();
	}
	eFree (execName);
	eFree (etags);
}

/*
 *  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); /* FIXME: memory leak. */
		args->parameter = "";
	}
	else
	{
		const size_t length = equal - item;
		args->item = xMalloc (length + 1, char); /* FIXME: memory leak. */
		strncpy (args->item, item, length);
		args->item [length] = '\0';
		args->parameter = equal + 1;
	}
	Assert (args->item != NULL);
	Assert (args->parameter != NULL);
}

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);
			Assert (current->item != NULL);
			Assert (current->parameter != NULL);
		}
		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 = NULL;
		}
	}
}

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);
}

static boolean isFalse (const char *parameter)
{
	return (boolean) (
		strcasecmp (parameter, "0"  ) == 0  ||
		strcasecmp (parameter, "n"  ) == 0  ||
		strcasecmp (parameter, "no" ) == 0  ||
		strcasecmp (parameter, "off") == 0);
}

static boolean isTrue (const char *parameter)
{
	return (boolean) (
		strcasecmp (parameter, "1"  ) == 0  ||
		strcasecmp (parameter, "y"  ) == 0  ||
		strcasecmp (parameter, "yes") == 0  ||
		strcasecmp (parameter, "on" ) == 0);
}

/*  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;
}

/*
 *  Specific option processing
 */

static void processEtagsInclude (
		const char *const option, const char *const parameter)
{
	if (! Option.etags)
		error (FATAL, "Etags must be enabled to use \"%s\" option", option);
	else
	{
		vString *const file = vStringNewInit (parameter);
		if (Option.etagsInclude == NULL)
			Option.etagsInclude = stringListNew ();
		stringListAdd (Option.etagsInclude, file);
		FilesRequired = FALSE;
	}
}

static void processExcludeOption (
		const char *const option __unused, const char *const parameter)
{
	const char *const fileName = parameter + 1;
	if (parameter [0] == '\0')
		freeList (&Excluded);
	else if (parameter [0] == '@')
	{
		stringList* const sl = stringListNewFromFile (fileName);
		if (sl == NULL)
			error (FATAL | PERROR, "cannot open \"%s\"", fileName);
		if (Excluded == NULL)
			Excluded = sl;
		else
			stringListCombine (Excluded, sl);
		verbose ("    adding exclude patterns from %s\n", fileName);
	}
	else
	{
		vString *const item = vStringNewInit (parameter);
		if (Excluded == NULL)
			Excluded = stringListNew ();
		stringListAdd (Excluded, item);
		verbose ("    adding exclude pattern: %s\n", parameter);
	}
}

extern boolean isExcludedFile (const char* const name)
{
	const char* base = baseFilename (name);
	boolean result = FALSE;
	if (Excluded != NULL)
	{
		result = stringListFileMatched (Excluded, base);
		if (! result  &&  name != base)
			result = stringListFileMatched (Excluded, name);
	}
	return result;
}

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 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;
	}
}

static void processFieldsOption (
		const char *const option, const char *const parameter)
{
	struct sExtFields *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->typeRef          = 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 'S': field->signature      = mode; break;
		case 'z': field->kindKey        = mode; break;
		case 't': field->typeRef        = mode; break;

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

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

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 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 printFeatureList (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, %s %s\n",
	        PROGRAM_NAME, PROGRAM_VERSION,
	        PROGRAM_COPYRIGHT, AUTHOR_NAME);
	printf ("  Addresses: <%s>, %s\n", AUTHOR_EMAIL, PROGRAM_URL);
	printFeatureList ();
}

static void processHelpOption (
		const char *const option __unused,
		const char *const parameter __unused)
{
	printProgramIdentification ();
	putchar ('\n');
	printInvocationDescription ();
	putchar ('\n');
	printOptionDescriptions (LongOptionDescription);
	exit (0);
}

static void processLanguageForceOption (
		const char *const option, const char *const parameter)
{
	langType language;
	if (strcasecmp (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, "Unknown language \"%s\" in \"%s\" option", parameter, 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)
		{
			const char *const deflt = "default";
			char* p;
			if (*list == '+')
				++list;
			else
				clear = TRUE;
			for (p = list  ;  *p != ','  &&  *p != '\0'  ;  ++p)  /*no-op*/ ;
			if ((size_t) (p - list) == strlen (deflt) &&
				strncasecmp (list, deflt, 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 \"%s\" in \"%s\" option", parameter, option);
		map = next;
	}
	eFree (maps);
}

static void processLanguagesOption (
		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 \"%s\" in \"%s\" option", lang, 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 processLicenseOption (
		const char *const option __unused,
		const char *const parameter __unused)
{
	printProgramIdentification ();
	puts ("");
	puts (License1);
	puts (License2);
	exit (0);
}

static void processListKindsOption (
		const char *const option, const char *const parameter)
{
	if (parameter [0] == '\0' || strcasecmp (parameter, "all") == 0)
	    printLanguageKinds (LANG_AUTO);
	else
	{
		langType language = getNamedLanguage (parameter);
		if (language == LANG_IGNORE)
			error (FATAL, "Unknown language \"%s\" in \"%s\" option", parameter, option);
		else
			printLanguageKinds (language);
	}
	exit (0);
}

static void processListMapsOption (
		const char *const __unused option,
		const char *const __unused parameter)
{
	if (parameter [0] == '\0' || strcasecmp (parameter, "all") == 0)
	    printLanguageMaps (LANG_AUTO);
	else
	{
		langType language = getNamedLanguage (parameter);
		if (language == LANG_IGNORE)
			error (FATAL, "Unknown language \"%s\" in \"%s\" option", parameter, option);
		else
			printLanguageMaps (language);
	}
	exit (0);
}

static void processListLanguagesOption (
		const char *const option __unused,
		const char *const parameter __unused)
{
	printLanguageList ();
	exit (0);
}

static void processOptionFile (
		const char *const option, const char *const parameter)
{
	if (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 processSortOption (
		const char *const option, const char *const parameter)
{
	if (isFalse (parameter))
		Option.sorted = SO_UNSORTED;
	else if (isTrue (parameter))
		Option.sorted = SO_SORTED;
	else if (strcasecmp (parameter, "f") == 0 ||
			strcasecmp (parameter, "fold") == 0 ||
			strcasecmp (parameter, "foldcase") == 0)
		Option.sorted = SO_FOLDSORTED;
	else
		error (FATAL, "Invalid value for \"%s\" option", option);
}

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);
}

static void addIgnoreListFromFile (const char *const fileName)
{
	stringList* tokens = stringListNewFromFile (fileName);
	if (tokens == NULL)
		error (FATAL | PERROR, "cannot open \"%s\"", 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);
}

static void processVersionOption (
		const char *const option __unused,
		const char *const parameter __unused)
{
	printProgramIdentification ();
	exit (0);
}

/*
 *  Option tables
 */

static parametricOption ParametricOptions [] = {
	{ "etags-include",          processEtagsInclude,            FALSE   },
	{ "exclude",                processExcludeOption,           FALSE   },
	{ "excmd",                  processExcmdOption,             FALSE   },
	{ "extra",                  processExtraTagsOption,         FALSE   },
	{ "fields",                 processFieldsOption,            FALSE   },
	{ "filter-terminator",      processFilterTerminatorOption,  TRUE    },
	{ "format",                 processFormatOption,            TRUE    },
	{ "help",                   processHelpOption,              TRUE    },
	{ "lang",                   processLanguageForceOption,     FALSE   },
	{ "language",               processLanguageForceOption,     FALSE   },
	{ "language-force",         processLanguageForceOption,     FALSE   },
	{ "languages",              processLanguagesOption,         FALSE   },
	{ "langdef",                processLanguageDefineOption,    FALSE   },
	{ "langmap",                processLanguageMapOption,       FALSE   },
	{ "license",                processLicenseOption,           TRUE    },
	{ "list-kinds",             processListKindsOption,         TRUE    },
	{ "list-maps",              processListMapsOption,          TRUE    },
	{ "list-languages",         processListLanguagesOption,     TRUE    },
	{ "options",                processOptionFile,              FALSE   },
	{ "sort",                   processSortOption,              TRUE    },
	{ "version",                processVersionOption,           TRUE    },
};

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   },
	{ "links",          &Option.followLinks,            FALSE   },
#ifdef RECURSE_SUPPORTED
	{ "recurse",        &Option.recurse,                FALSE   },
#endif
	{ "tag-relative",   &Option.tagRelative,            TRUE    },
	{ "totals",         &Option.printTotals,            TRUE    },
	{ "verbose",        &Option.verbose,                FALSE   },
};

/*
 *  Generic option parsing
 */

static void checkOptionOrder (const char* const option)
{
	if (NonOptionEncountered)
		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 (isFalse (parameter))
		selection = FALSE;
	else if (isTrue (parameter))
		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)
{
	Assert (parameter != NULL);
	if (parameter == NULL  &&  parameter [0] == '\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))
		;
	else if (processRegexOption (option, parameter))
		;
#ifndef RECURSE_SUPPORTED
	else if (strcmp (option, "recurse") == 0)
		error (WARNING, "%s option not supported on this host", option);
#endif
	else
		error (FATAL, "Unknown option: --%s", option);
}

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

	if (isCompoundOption (*option) && (parameter == NULL  ||  parameter [0] == '\0'))
		error (FATAL, "Missing parameter for \"%s\" option", option);
	else switch (*option)
	{
		case '?':
			processHelpOption ("?", NULL);
			exit (0);
			break;
		case 'a':
			checkOptionOrder (option);
			Option.append = TRUE;
			break;
#ifdef 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':
			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 = SO_UNSORTED;
			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)
{
	NonOptionEncountered = FALSE;
	while (! cArgOff (args)  &&  cArgIsOption (args))
		parseOption (args);
	if (! cArgOff (args)  &&  ! cArgIsOption (args))
		NonOptionEncountered = TRUE;
}

static const char *CheckFile;
static boolean checkSameFile (const char *const fileName)
{
	return isSameFile (CheckFile, fileName);
}

static boolean parseFileOptions (const char* const fileName)
{
	boolean fileFound = FALSE;
	const char* const format = "Considering option file %s: %s\n";
	CheckFile = fileName;
	if (stringListHasTest (OptionFiles, checkSameFile))
		verbose (format, fileName, "already considered");
	else
	{
		FILE* const fp = fopen (fileName, "r");
		if (fp == NULL)
			verbose (format, fileName, "not found");
		else
		{
			cookedArgs* const args = cArgNewFromLineFile (fp);
			vString* file = vStringNewInit (fileName);
			stringListAdd (OptionFiles, file);
			verbose (format, fileName, "reading...");
			parseOptions (args);
			if (NonOptionEncountered)
				error (WARNING, "Ignoring non-option in %s\n", fileName);
			cArgDelete (args);
			fclose (fp);
			fileFound = TRUE;
		}
	}
	return fileFound;
}

/* Actions to be taken before reading any other options */
extern void previewFirstOption (cookedArgs* const args)
{
	while (cArgIsOption (args))
	{
		if (strcmp (args->item, "V") == 0 || strcmp (args->item, "verbose") == 0)
			parseOption (args);
		else if (strcmp (args->item, "options") == 0  &&
				strcmp (args->parameter, "NONE") == 0)
		{
			fprintf (stderr, "No options will be read from files or environment\n");
			SkipConfiguration = TRUE;
			cArgForth (args);
		}
		else
			break;
	}
}

static void parseConfigurationFileOptionsInDirectoryWithLeafname (const char* directory, const char* leafname)
{
	vString* const pathname = combinePathAndFile (directory, leafname);
	parseFileOptions (vStringValue (pathname));
	vStringDelete (pathname);
}

static void parseConfigurationFileOptionsInDirectory (const char* directory)
{
	parseConfigurationFileOptionsInDirectoryWithLeafname (directory, ".ctags");
#ifdef MSDOS_STYLE_PATH
	parseConfigurationFileOptionsInDirectoryWithLeafname (directory, "ctags.cnf");
#endif
}

static void parseConfigurationFileOptions (void)
{
	/* We parse .ctags on all systems, and additionally ctags.cnf on DOS. */
	const char* const home = getenv ("HOME");
#ifdef CUSTOM_CONFIGURATION_FILE
	parseFileOptions (CUSTOM_CONFIGURATION_FILE);
#endif
#ifdef MSDOS_STYLE_PATH
	parseFileOptions ("/ctags.cnf");
#endif
	parseFileOptions ("/etc/ctags.conf");
	parseFileOptions ("/usr/local/etc/ctags.conf");
	if (home != NULL)
	{
		parseConfigurationFileOptionsInDirectory (home);
	}
	else
	{
#ifdef MSDOS_STYLE_PATH
		/*
		 * Windows users don't usually set HOME.
		 * The OS sets HOMEDRIVE and HOMEPATH for them.
		 */
		const char* homeDrive = getenv ("HOMEDRIVE");
		const char* homePath = getenv ("HOMEPATH");
		if (homeDrive != NULL && homePath != NULL)
		{
			vString* const windowsHome = vStringNew ();
			vStringCatS (windowsHome, homeDrive);
			vStringCatS (windowsHome, homePath);
			parseConfigurationFileOptionsInDirectory (vStringValue (windowsHome));
			vStringDelete (windowsHome);
		}
#endif
	}
	parseConfigurationFileOptionsInDirectory (".");
}

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

	if (Option.etags)
	{
		var = ETAGS_ENVIRONMENT;
		envOptions = getenv (var);
	}
	if (envOptions == NULL)
	{
		var = CTAGS_ENVIRONMENT;
		envOptions = getenv (var);
	}
	if (envOptions != NULL  &&  envOptions [0] != '\0')
	{
		cookedArgs* const args = cArgNewFromString (envOptions);
		verbose ("Reading options from $CTAGS\n");
		parseOptions (args);
		cArgDelete (args);
		if (NonOptionEncountered)
			error (WARNING, "Ignoring non-option in %s variable", var);
	}
}

extern void readOptionConfiguration (void)
{
	if (! SkipConfiguration)
	{
		parseConfigurationFileOptions ();
		parseEnvironmentOptions ();
	}
}

/*
*   Option initialization
*/

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

	/* always excluded by default */
	verbose ("  Installing default exclude patterns:\n");
	processExcludeOption (NULL, "{arch}");
	processExcludeOption (NULL, ".arch-ids");
	processExcludeOption (NULL, ".arch-inventory");
	processExcludeOption (NULL, "autom4te.cache");
	processExcludeOption (NULL, "BitKeeper");
	processExcludeOption (NULL, ".bzr");
	processExcludeOption (NULL, ".bzrignore");
	processExcludeOption (NULL, "CVS");
	processExcludeOption (NULL, ".cvsignore");
	processExcludeOption (NULL, "_darcs");
	processExcludeOption (NULL, ".deps");
	processExcludeOption (NULL, "EIFGEN");
	processExcludeOption (NULL, ".git");
	processExcludeOption (NULL, ".hg");
	processExcludeOption (NULL, "PENDING");
	processExcludeOption (NULL, "RCS");
	processExcludeOption (NULL, "RESYNC");
	processExcludeOption (NULL, "SCCS");
	processExcludeOption (NULL, ".svn");
}

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

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

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