Subject: Make ctags a thin wrapper around etags

From: Paul Eggert <[email-masked]>
Date: Sat, 15 Feb 2025 21:21:00 -0800

ctags and etags are both built from the same source file, with a 
different compile-time flag. This takes quite a while on my old desktop 
when I configure with --enable-gcc-warnings as is my usual practice. 
Also, it means two very similar executables are floating around where 
one would do. This has long been a FIXME and I finally got annoyed by 
the long compiles enough to do something about it. Patch attached.

From 8c3234d254617eaaced21a3431baaa881f8bb482 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Sat, 15 Feb 2025 14:58:54 -0800
Subject: [PATCH] Make ctags a thin wrapper around etags

This fixes a FIXME in lib-src/Makefile.in and speeds up compilation
quite a bit on my older CPU when I compile with	--enable-gcc-warnings.
It also lessens installation and runtime footprint.
* .gitignore: Do not ignore lib-src/ctags.
* java/Makefile.in (install_temp): Treat ctags like rcs2log.
* lib-src/Makefile.in (INSTALLABLE_EXES, INSTALLABLE_SCRIPTS):
New macros, replacing INSTALLABLES.  All uses changed.
(UTILITY_EXES, UTILITY_SCRIPTS): Likewise, replacing UTILITIES.
(ctags${EXEEXT}): Remove rule, as it is now a distributed script.
* lib-src/ctags: New shell file.
* lib-src/ctags.c: Remove.
* lib-src/etags.c (CTAGS): Remove.  All uses replaced by ...
(ctags): ... this new static var.
(STDIN): Remove macro.  All uses replaced by new STDIN_OPTION constant.
(CTAGS_OPTION, STDIN_OPTION): New contants.
(longopts): New --ctags option.
(ctags_default_C_help): New constant,
to override default_C_help at runtime.
(default_C_help): Now always the etags version.
(C_LANG_NAMES_INDEX): New macro.
(print_language_names): Do not assume etags.
(PROGRAM_NAME): Remove.  All uses removed.
(print_help): Document --ctags if PRINT_UNDOCUMENTED_OPTIONS_HELP.
(main): Support new --ctags option, and support all [ce]tags options.
---
 .gitignore          |   1 -
 etc/NEWS            |   3 +
 etc/PROBLEMS        |   4 +-
 java/Makefile.in    |   7 ++-
 lib-src/Makefile.in |  41 ++++++++------
 lib-src/ctags       |   2 +
 lib-src/ctags.c     |   2 -
 lib-src/etags.c     | 131 ++++++++++++++++++++++----------------------
 8 files changed, 101 insertions(+), 90 deletions(-)
 create mode 100755 lib-src/ctags
 delete mode 100644 lib-src/ctags.c

diff --git a/.gitignore b/.gitignore
index e6309ed0b1f..a56def05e4a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -229,7 +229,6 @@ a.out
 lib-src/asset-directory-tool
 lib-src/be-resources
 lib-src/blessmail
-lib-src/ctags
 lib-src/ebrowse
 lib-src/emacsclient
 lib-src/etags
diff --git a/etc/NEWS b/etc/NEWS
index 6d934b2029c..22316a1f2a6 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -29,6 +29,9 @@ applies, and please also update docstrings as needed.
 The traditional unexec dumper, deprecated since Emacs 27, has been
 removed.
 
+** ctags is now merely a thin shell script wrapper around etags.
+This speeds up compilation and lessens installation footprint.
+
 ** Changed GCC default options on 32-bit x86 systems.
 When using GCC 4 or later to build Emacs on 32-bit x86 systems,
 'configure' now defaults to using the GCC options '-mfpmath=sse' (if the
diff --git a/etc/PROBLEMS b/etc/PROBLEMS
index 5fdb4bd95db..4c85dbb7da8 100644
--- a/etc/PROBLEMS
+++ b/etc/PROBLEMS
@@ -3947,7 +3947,7 @@ There are several known workarounds:
 
 The linker error messages look like this:
 
- oo-spd/i386/ctags.o:ctags.c:(.text+0x156e): undefined reference to `_imp__re_set_syntax'
+ oo-spd/i386/etags.o:etags.c:(.text+0x156e): undefined reference to `_imp__re_set_syntax'
  collect2: ld returned 1 exit status
 
 This happens because GCC finds an incompatible regex.h header
@@ -3978,7 +3978,7 @@ Some versions of mingw32 make on some versions of Windows do not seem
 to detect the shell correctly.  Try "make SHELL=cmd.exe", or if that
 fails, try running make from Cygwin bash instead.
 
-*** Building 'ctags' for MS-Windows with the MinGW port of GCC fails.
+*** Building 'etags' for MS-Windows with the MinGW port of GCC fails.
 
 This might happen due to a bug in the MinGW header assert.h, which
 defines the 'assert' macro with a trailing semi-colon.  The following
diff --git a/java/Makefile.in b/java/Makefile.in
index 39d6e85c7a7..21ac4e4250d 100644
--- a/java/Makefile.in
+++ b/java/Makefile.in
@@ -305,8 +305,13 @@ install_temp:
 	    cp -f $$file install_temp/lib/$(ANDROID_ABI);	\
 	  fi 							\
 	done
-# Next, produce a version of rcs2log befitting Android's naming
+# Next, produce versions of ctags and rcs2log befitting Android's naming
 # conventions and shell interpreter location.
+	$(AM_V_at) \
+	sed 's|/bin/sh|/system/bin/sh|'					\
+	 $(top_srcdir)/lib-src/ctags >					\
+	 install_temp/lib/$(ANDROID_ABI)/libctags.so
+	$(AM_V_at) chmod +x install_temp/lib/$(ANDROID_ABI)/libctags.so
 	$(AM_V_at) \
 	sed 's|/bin/sh|/system/bin/sh|'					\
 	 $(top_srcdir)/lib-src/rcs2log >				\
diff --git a/lib-src/Makefile.in b/lib-src/Makefile.in
index 439d9a1a52d..64307a5918c 100644
--- a/lib-src/Makefile.in
+++ b/lib-src/Makefile.in
@@ -155,12 +155,13 @@ ANDROID=
 CLIENTW = @CLIENTW@
 
 # Things that a user might actually run, which should be installed in bindir.
-INSTALLABLES = etags${EXEEXT} ctags${EXEEXT} emacsclient${EXEEXT} $(CLIENTW) \
+INSTALLABLE_EXES = etags${EXEEXT} emacsclient${EXEEXT} $(CLIENTW) \
                ebrowse${EXEEXT}
+INSTALLABLE_SCRIPTS = ctags
 
 # Things that Emacs runs internally, or during the build process,
 #  which should not be installed in bindir.
-UTILITIES = hexl${EXEEXT} 			 	 \
+UTILITY_EXES = hexl${EXEEXT} \
 	    $(if $(with_mailutils), , movemail${EXEEXT}) \
             $(and $(use_gamedir), update-game-score${EXEEXT})
 
@@ -175,12 +176,12 @@ DONT_INSTALL=
 endif
 endif
 
-# Like UTILITIES, but they're not system-dependent, and should not be
+# Like UTILITY_EXES, but they're not system-dependent, and should not be
 #  deleted by the distclean target.
-SCRIPTS= rcs2log
+UTILITY_SCRIPTS= rcs2log
 
 # All files that are created by the linker, i.e., whose names end in ${EXEEXT}.
-EXE_FILES = ${INSTALLABLES} ${UTILITIES} ${DONT_INSTALL}
+EXE_FILES = ${INSTALLABLE_EXES} ${UTILITY_EXES} ${DONT_INSTALL}
 
 # Specify additional -D flags for movemail. Options:
 # -DMAIL_USE_FLOCK or -DMAIL_USE_LOCKF (use flock or lockf for file locking).
@@ -283,7 +284,7 @@ ALL_CXXFLAGS =
 # Configuration files for .o files to depend on.
 config_h = ../src/config.h $(srcdir)/../src/conf_post.h
 
-all: ${EXE_FILES} ${SCRIPTS}
+all: ${EXE_FILES} ${INSTALLABLE_SCRIPTS} ${UTILITY_SCRIPTS}
 
 ifeq ($(SECCOMP_FILTER),1)
 all: seccomp-filter.bpf seccomp-filter-exec.bpf
@@ -324,7 +325,7 @@ $(DESTDIR)${archlibdir}:
 	umask 022 && ${MKDIR_P} "$(DESTDIR)${archlibdir}"
 	exp_archlibdir=`cd "$(DESTDIR)${archlibdir}" && pwd -P` && \
 	if [ "$$exp_archlibdir" != "`pwd -P`" ]; then \
-	  for file in ${UTILITIES}; do \
+	  for file in ${UTILITY_EXES}; do \
 	    $(INSTALL_PROGRAM) $(INSTALL_STRIP) $$file \
 	      "$(DESTDIR)${archlibdir}/$$file" || exit; \
 	  done ; \
@@ -351,7 +352,7 @@ $(DESTDIR)${archlibdir}:
   endif
 	exp_archlibdir=`cd "$(DESTDIR)${archlibdir}" && pwd -P` && \
 	if [ "$$exp_archlibdir" != "`cd ${srcdir} && pwd -P`" ]; then \
-	  for file in ${SCRIPTS}; do \
+	  for file in ${UTILITY_SCRIPTS}; do \
 	    $(INSTALL_SCRIPT) ${srcdir}/$$file \
 	      "$(DESTDIR)${archlibdir}/$$file" || exit; \
 	  done ; \
@@ -364,22 +365,33 @@ install:
 	$(info $.)
 	$(info Installing utilities for users to run.)
 	umask 022 && ${MKDIR_P} "$(DESTDIR)${bindir}"
-	for file in ${INSTALLABLES} ; do \
+	for file in ${INSTALLABLE_EXES} ; do \
 	  $(INSTALL_PROGRAM) $(INSTALL_STRIP) $${file} \
 	    "$(DESTDIR)${bindir}"/` \
 	      echo $${file} | sed -e 's/${EXEEXT}$$//' -e '$(TRANSFORM)' \
 	    `${EXEEXT} || exit; \
 	done
+	for file in ${INSTALLABLE_SCRIPTS} ; do \
+	  $(INSTALL_PROGRAM) $${file} \
+	    "$(DESTDIR)${bindir}"/` \
+	      echo $${file} | sed -e '$(TRANSFORM)' \
+	    ` || exit; \
+	done
 
 uninstall:
-	for file in ${INSTALLABLES}; do \
+	for file in ${INSTALLABLE_EXES}; do \
 	  rm -f "$(DESTDIR)${bindir}"/` \
 	    echo $${file} | sed -e 's/${EXEEXT}$$//' -e '$(TRANSFORM)' \
 	  `${EXEEXT}; \
 	done
+	for file in ${INSTALLABLE_SCRIPTS}; do \
+	  rm -f "$(DESTDIR)${bindir}"/` \
+	    echo $${file} | sed -e '$(TRANSFORM)' \
+	  `; \
+	done
 	if [ -d "$(DESTDIR)${archlibdir}" ]; then \
 	  cd "$(DESTDIR)${archlibdir}" && \
-	  rm -f ${UTILITIES} ${SCRIPTS}; \
+	  rm -f ${UTILITY_EXES} ${UTILITY_SCRIPTS}; \
 	fi
 
 mostlyclean:
@@ -415,13 +427,6 @@ etags_libs =
 etags${EXEEXT}: ${etags_deps}
 	$(AM_V_CCLD)$(CC) ${ALL_CFLAGS} -o $@ $< $(etags_libs)
 
-## ctags.c is distinct from etags.c so that parallel makes do not write two
-## etags.o files on top of each other.
-## FIXME?
-## Can't we use a wrapper that calls 'etags --ctags'?
-ctags${EXEEXT}: ${srcdir}/ctags.c ${etags_deps}
-	$(AM_V_CCLD)$(CC) ${ALL_CFLAGS} -o $@ $< $(etags_libs)
-
 asset-directory-tool${EXEEXT}: ${srcdir}/asset-directory-tool.c $(config_h)
 	$(AM_V_CCLD)$(CC) ${ALL_CFLAGS} $< $(LOADLIBES) -o $@
 
diff --git a/lib-src/ctags b/lib-src/ctags
new file mode 100755
index 00000000000..9cc95f79b01
--- /dev/null
+++ b/lib-src/ctags
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec etags --ctags="$0" "$@"
diff --git a/lib-src/ctags.c b/lib-src/ctags.c
deleted file mode 100644
index 0a6838a9dbb..00000000000
--- a/lib-src/ctags.c
+++ /dev/null
@@ -1,2 +0,0 @@
-#define CTAGS 1
-#include "etags.c"
diff --git a/lib-src/etags.c b/lib-src/etags.c
index b59b70c9ec7..daa51c125d4 100644
--- a/lib-src/etags.c
+++ b/lib-src/etags.c
@@ -126,16 +126,6 @@ Copyright (C) 1984, 1987-1989, 1993-1995, 1998-2025 Free Software
 #include <getopt.h>
 #include <regex.h>
 
-/* Define CTAGS to make the program "ctags" compatible with the usual one.
- Leave it undefined to make the program "etags", which makes emacs-style
- tag tables and tags typedefs, #defines and struct/union/enum by default. */
-#ifdef CTAGS
-# undef  CTAGS
-# define CTAGS true
-#else
-# define CTAGS false
-#endif
-
 /* Define MERCURY_HEURISTICS_RATIO as it was necessary to disambiguate
    Mercury from Objective C, which have same file extensions .m
    See comments before function test_objc_is_mercury for details.  */
@@ -479,18 +469,20 @@ #define xrnew(op, n, m) ((op) = xnrealloc (op, n, (m) * sizeof *(op)))
 static bool ignoreindent;	/* -I: ignore indentation in C */
 static int packages_only;	/* --packages-only: in Ada, only tag packages*/
 static int class_qualify;	/* -Q: produce class-qualified tags in C++/Java */
+static bool ctags;		/* --ctags */
 static int debug;		/* --debug */
 static int fallback_lang;	/* --(no-)fallback-lang: Fortran/C fallbacks */
 static int empty_files;		/* --(no-)empty-file-entries */
-
-/* STDIN is defined in LynxOS system headers */
-#ifdef STDIN
-# undef STDIN
-#endif
-
-#define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
 static bool parsing_stdin;	/* --parse-stdin used */
 
+/* For long options that have no equivalent short option, use a
+   non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
+enum
+  {
+    CTAGS_OPTION = CHAR_MAX + 1,
+    STDIN_OPTION
+  };
+
 static regexp *p_head;		/* list of all regexps */
 static bool need_filebuf;	/* some regexes are multi-line */
 
@@ -499,6 +491,7 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
   { "append",             no_argument,       NULL,               'a'   },
   { "packages-only",      no_argument,       &packages_only,     1     },
   { "c++",                no_argument,       NULL,               'C'   },
+  { "ctags",              required_argument, NULL,        CTAGS_OPTION },
   { "debug",              no_argument,       &debug,             1     },
   { "declarations",       no_argument,       &declarations,      1     },
   { "no-line-directive",  no_argument,       &no_line_directive, 1     },
@@ -514,10 +507,10 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
   { "regex",              required_argument, NULL,               'r'   },
   { "no-regex",           no_argument,       NULL,               'R'   },
   { "ignore-case-regex",  required_argument, NULL,               'c'   },
-  { "parse-stdin",        required_argument, NULL,               STDIN },
+  { "parse-stdin",        required_argument, NULL,        STDIN_OPTION },
   { "version",            no_argument,       NULL,               'V'   },
 
-#if CTAGS /* Ctags options */
+  /* ctags options */
   { "backward-search",    no_argument,       NULL,               'B'   },
   { "cxref",              no_argument,       NULL,               'x'   },
   { "defines",            no_argument,       NULL,               'd'   },
@@ -528,7 +521,7 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
   { "vgrind",             no_argument,       NULL,               'v'   },
   { "no-warn",            no_argument,       NULL,               'w'   },
 
-#else /* Etags options */
+  /* etags options */
   { "no-defines",         no_argument,       NULL,               'D'   },
   { "no-globals",         no_argument,       &globals,           0     },
   { "include",            required_argument, NULL,               'i'   },
@@ -536,7 +529,7 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
   { "fallback-lang",      no_argument,       &fallback_lang,     1     },
   { "no-empty-file-entries", no_argument,    &empty_files,       0     },
   { "empty-file-entries", no_argument,       &empty_files,       1     },
-#endif
+
   { NULL }
 };
 
@@ -598,15 +591,17 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
    That is why default_C_entries is called for these. */
 static const char *default_C_suffixes [] =
   { "c", "h", NULL };
-#if CTAGS				/* C help for Ctags */
-static const char default_C_help [] =
+
+/* C help for ctags */
+static const char ctags_default_C_help[] =
 "In C code, any C function is a tag.  Use -t to tag typedefs.\n\
 Use -T to tag definitions of 'struct', 'union' and 'enum'.\n\
 Use -d to tag '#define' macro definitions and 'enum' constants.\n\
 Use --globals to tag global variables.\n\
 You can tag function declarations and external variables by\n\
 using '--declarations', and struct members by using '--members'.";
-#else					/* C help for Etags */
+
+/* C help for etags */
 static const char default_C_help [] =
 "In C code, any C function or typedef is a tag, and so are\n\
 definitions of 'struct', 'union' and 'enum'.  '#define' macro\n\
@@ -617,7 +612,6 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
 '--no-members' can make the tags table file much smaller.\n\
 You can tag function declarations and external variables by\n\
 using '--declarations'.";
-#endif	/* C help for Ctags and Etags */
 
 static const char *Cplusplus_suffixes [] =
   { "C", "c++", "cc", "cpp", "cxx", "H", "h++", "hh", "hpp", "hxx",
@@ -862,6 +856,7 @@ #define STDIN 0x1001		/* returned by getopt_long on --parse-stdin */
 {
   { "ada",       Ada_help,       Ada_funcs,         Ada_suffixes       },
   { "asm",       Asm_help,       Asm_labels,        Asm_suffixes       },
+#define C_LANG_NAMES_INDEX 2
   { "c",         default_C_help, default_C_entries, default_C_suffixes },
   { "c++",       Cplusplus_help, Cplusplus_entries, Cplusplus_suffixes },
   { "c*",        no_lang_help,   Cstar_entries,     Cstar_suffixes     },
@@ -928,24 +923,20 @@ print_language_names (void)
 Fortran is tried first; if no tags are found, C is tried next.\n\
 When parsing any C file, a \"class\" or \"template\" keyword\n\
 switches to C++.");
-  puts ("Compressed files are supported using gzip, bzip2, xz, and zstd.\n\
+  printf ("Compressed files are supported using gzip, bzip2, xz, and zstd.\n\
 \n\
 For detailed help on a given language use, for example,\n\
-etags --help --lang=ada.");
+%s --help --lang=ada.\n",
+	  progname);
 }
 
-#if CTAGS
-# define PROGRAM_NAME "ctags"
-#else
-# define PROGRAM_NAME "etags"
-#endif
 static _Noreturn void
 print_version (void)
 {
-  fputs ((PROGRAM_NAME " (" PACKAGE_NAME " " PACKAGE_VERSION ")\n"
-	  COPYRIGHT "\n"
-	  "This program is distributed under the terms in ETAGS.README\n"),
-	 stdout);
+  printf (("%s (%s %s)\n"
+	   "%s\n"
+	   "This program is distributed under the terms in ETAGS.README\n"),
+	  ctags ? "ctags" : "etags", PACKAGE_NAME, PACKAGE_VERSION, COPYRIGHT);
   exit (EXIT_SUCCESS);
 }
 
@@ -984,7 +975,7 @@ print_help (argument *argbuffer)
   puts ("--packages-only\n\
         For Ada files, only generate tags for packages.");
 
-  if (CTAGS)
+  if (ctags)
     puts ("-B, --backward-search\n\
         Write the search commands for the tag entries using '?', the\n\
         backward-search command instead of '/', the forward-search command.");
@@ -997,9 +988,13 @@ print_help (argument *argbuffer)
         Treat files whose name suffix defaults to C language as C++ files.");
   */
 
+  if (PRINT_UNDOCUMENTED_OPTIONS_HELP && !ctags)
+    puts ("--ctags=PROGRAM_NAME\n\
+	Implement ctags behavior under the name PROGRAM_NAME.");
+
   puts ("--declarations\n\
 	In C and derived languages, create tags for function declarations,");
-  if (CTAGS)
+  if (ctags)
     puts ("\tand create tags for extern variables if --globals is used.");
   else
     puts
@@ -1008,7 +1003,7 @@ print_help (argument *argbuffer)
   puts ("\tIn Mercury, tag both declarations starting a line with ':-' and\n\
         first predicates or functions in clauses.");
 
-  if (CTAGS)
+  if (ctags)
     puts ("-d, --defines\n\
         Create tag entries for C #define constants and enum constants, too.");
   else
@@ -1016,7 +1011,7 @@ print_help (argument *argbuffer)
         Don't create tag entries for C #define constants and enum constants.\n\
 	This makes the tags file smaller.");
 
-  if (!CTAGS)
+  if (!ctags)
     puts ("-i FILE, --include=FILE\n\
         Include a note in tag file indicating that, when searching for\n\
         a tag, one should also consult the tags file FILE after\n\
@@ -1026,7 +1021,7 @@ print_help (argument *argbuffer)
         Force the following files to be considered as written in the\n\
 	named language up to the next --language=LANG option.");
 
-  if (CTAGS)
+  if (ctags)
     puts ("--globals\n\
 	Create tag entries for global variables in some languages.");
   else
@@ -1037,7 +1032,7 @@ print_help (argument *argbuffer)
   puts ("--no-line-directive\n\
         Ignore #line preprocessor directives in C and derived languages.");
 
-  if (CTAGS)
+  if (ctags)
     puts ("--members\n\
 	Create tag entries for members of structures in some languages.");
   else
@@ -1045,7 +1040,7 @@ print_help (argument *argbuffer)
 	Do not create tag entries for members of structures\n\
 	in some languages.");
 
-  if (!CTAGS)
+  if (!ctags)
     {
       puts ("--fallback-lang\n\
 	If a file's language could not be determined, try to parse\n\
@@ -1092,7 +1087,7 @@ print_help (argument *argbuffer)
   puts ("--parse-stdin=NAME\n\
         Read from standard input and record tags as belonging to file NAME.");
 
-  if (CTAGS)
+  if (ctags)
     {
       puts ("-t, --typedefs\n\
         Generate tag entries for C and Ada typedefs.");
@@ -1101,7 +1096,7 @@ print_help (argument *argbuffer)
         and C++ member functions.");
     }
 
-  if (CTAGS)
+  if (ctags)
     puts ("-u, --update\n\
         Update the tag entries for the given files, leaving tag\n\
         entries for other files in place.  Currently, this is\n\
@@ -1110,7 +1105,7 @@ print_help (argument *argbuffer)
         tags file.  It is often faster to simply rebuild the entire\n\
         tag file than to use this.");
 
-  if (CTAGS)
+  if (ctags)
     {
       puts ("-v, --vgrind\n\
         Print on the standard output an index of items intended for\n\
@@ -1160,7 +1155,6 @@ main (int argc, char **argv)
   linebuffer filename_lb;
   bool help_asked = false;
   ptrdiff_t len;
-  char *optstring;
   int opt;
 
   progname = argv[0];
@@ -1192,9 +1186,7 @@ main (int argc, char **argv)
 
   /* When the optstring begins with a '-' getopt_long does not rearrange the
      non-options arguments to be at the end, but leaves them alone. */
-  optstring = concat ("-ac:Cf:Il:o:Qr:RSVhH",
-		      (CTAGS) ? "BxdtTuvw" : "Di:",
-		      "");
+  static char const optstring[] = "-aBc:CdDf:hHi:Il:o:Qr:RStTuvVwx";
 
   while ((opt = getopt_long (argc, argv, optstring, longopts, NULL)) != EOF)
     switch (opt)
@@ -1215,7 +1207,14 @@ main (int argc, char **argv)
 	++file_count;
 	break;
 
-      case STDIN:
+      case CTAGS_OPTION:
+	/* Operate as ctags, not etags.  */
+	ctags = true;
+	progname = optarg;
+	lang_names[C_LANG_NAMES_INDEX].help = ctags_default_C_help;
+	break;
+
+      case STDIN_OPTION:
 	/* Parse standard input.  Idea by Vivek <[email-masked]>. */
 	argbuffer[current_arg].arg_type = at_stdin;
 	argbuffer[current_arg].what     = optarg;
@@ -1325,7 +1324,7 @@ main (int argc, char **argv)
     }
 
   if (tagfile == NULL)
-    tagfile = savestr (CTAGS ? "tags" : "TAGS");
+    tagfile = savestr (ctags ? "tags" : "TAGS");
   cwd = etags_getcwd ();	/* the current working directory */
   if (cwd[strlen (cwd) - 1] != '/')
     {
@@ -1349,7 +1348,7 @@ main (int argc, char **argv)
   linebuffer_init (&filebuf);
   linebuffer_init (&token_name);
 
-  if (!CTAGS)
+  if (!ctags)
     {
       if (streq (tagfile, "-"))
 	{
@@ -1407,13 +1406,13 @@ main (int argc, char **argv)
   free (filebuf.buffer);
   free (token_name.buffer);
 
-  if (!CTAGS || cxref_style)
+  if (!ctags || cxref_style)
     {
       /* Write the remaining tags to tagf (ETAGS) or stdout (CXREF). */
       put_entries (nodehead);
       free_tree (nodehead);
       nodehead = NULL;
-      if (!CTAGS)
+      if (!ctags)
 	{
 	  fdesc *fdp;
 
@@ -1462,7 +1461,7 @@ main (int argc, char **argv)
   if (fclose (tagf) == EOF)
     pfatal (tagfile);
 
-  if (CTAGS)
+  if (ctags)
     if (append_to_tagfile || update)
       {
 	/* Maybe these should be used:
@@ -1860,7 +1859,7 @@ process_file (FILE *fh, char *fn, language *lang)
   /* If not Ctags, and if this is not metasource and if it contained no #line
      directives, we can write the tags and free all nodes pointing to
      curfdp. */
-  if (!CTAGS
+  if (!ctags
       && curfdp->usecharno	/* no #line directives in this file */
       && !curfdp->lang->metasource)
     {
@@ -2092,7 +2091,7 @@ make_tag (const char *name, 	/* tag name, or NULL if unnamed */
     fprintf (stderr, "%s on %s:%"PRIdMAX": %s\n",
 	     named ? name : "(unnamed)", curfdp->taggedfname, lno, linestart);
 
-  if (!CTAGS && named)		/* maybe set named to false */
+  if (!ctags && named)		/* maybe set named to false */
     /* Let's try to make an implicit tag name, that is, create an unnamed tag
        such that etags.el can guess a name from it. */
     {
@@ -2133,17 +2132,17 @@ pfnote (char *name,		/* tag name, or NULL if unnamed */
 {
   register node *np;
 
-  if ((CTAGS && name == NULL)
+  if ((ctags && name == NULL)
       /* We used to have an assertion here for the case below, but if we hit
 	 that case, it just means our parser got confused, and there's nothing
 	 to do about such empty "tags".  */
-      || (!CTAGS && name && name[0] == '\0'))
+      || (!ctags && name && name[0] == '\0'))
     return;
 
   np = xmalloc (sizeof *np);
 
   /* If ctags mode, change name "main" to M<thisfilename>. */
-  if (CTAGS && !cxref_style && streq (name, "main"))
+  if (ctags && !cxref_style && streq (name, "main"))
     {
       char *fp = strrchr (curfdp->taggedfname, '/');
       np->name = concat ("M", fp == NULL ? curfdp->taggedfname : fp + 1, "");
@@ -2168,7 +2167,7 @@ pfnote (char *name,		/* tag name, or NULL if unnamed */
   else
     np->cno = invalidcharno;
   np->left = np->right = NULL;
-  if (CTAGS && !cxref_style)
+  if (ctags && !cxref_style)
     {
       if (strnlen (linestart, 50) < 50)
 	np->regex = concat (linestart, "$", "");
@@ -2295,7 +2294,7 @@ add_node (node *np, node **cur_node_p)
       return;
     }
 
-  if (!CTAGS)
+  if (!ctags)
     /* Etags Mode */
     {
       /* For each file name, tags are in a linked sublist on the right
@@ -2394,7 +2393,7 @@ invalidate_nodes (fdesc *badfdp, node **npp)
   node *np = *npp;
   stkentry *stack = NULL;
 
-  if (CTAGS)
+  if (ctags)
     {
       while (np)
 	{
@@ -2504,7 +2503,7 @@ put_entry (node *np)
   /* Output this entry */
   if (np->valid)
     {
-      if (!CTAGS)
+      if (!ctags)
 	{
 	  /* Etags mode */
 	  if (fdp != np->fdp)
@@ -2576,7 +2575,7 @@ put_entries (node *np)
   if (np == NULL)
     return;
 
-  if (CTAGS)
+  if (ctags)
     {
       while (np)
 	{
-- 
2.45.2