Subject: Re: bug#76322: Make ctags a thin wrapper around etags

From: Paul Eggert <[email-masked]>
Date: Tue, 11 Mar 2025 10:01:38 -0700

On 2025-03-11 05:11, Eli Zaretskii wrote:
> If we think our ctags is not being used (I suggested to query a few
> distros about that), I'd rather drop it entirely, first by adding the
> --ctags option to etags, and later maybe removing the ctags-only code
> from etags.c.

That would be simpler, yes. Proposed patch attached.

My impression is that distros generally prefer Universal Ctags these 
days, though you can use Exuberant Ctags if you're old-fashioned or 
Emacs ctags if your even older-fashioned. On Fedora, 'ctags' is 
Universal Ctags; on Ubuntu all three variants are available as options 
and /usr/bin/ctags is a symlink to /etc/alternatives/ctags which in turn 
can link to /usr/bin/ctags-universal or /usr/bin/ctags-exuberant or 
/usr/bin/ctags.emacs.

To some extent it's a chicken-and-egg problem: if we keep shipping ctags 
some distros will keep making it available as an option. But there's no 
real need for us to keep maintaining the third (and oldest) variant.

From c6e4caf9faf2050f006b85441a9a3b7e6d1b010a Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Tue, 11 Mar 2025 08:47:30 -0700
Subject: [PATCH] Remove ctags program

Remove our old ctags and suggest Universal Ctags instead.
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.
* admin/authors.el (authors-renamed-files-alist): Remove ctags.1.
* admin/check-man-pages: ctags.1 is no longer a special case.
* admin/quick-install-emacs (PUBLIC_LIBSRC_BINARIES): Remove ctags.
* cross/Makefile.in (LIBSRC_BINARIES): Remove lib-src/ctags.
* doc/man/ctags.1, lib-src/ctags.c: Remove.
* java/Makefile.in (CROSS_LIBSRC_BINS): Remove ctags.
* lib-src/Makefile.in (INSTALLABLES): Remove ctags${EXEEXT}.
(ctags${EXEEXT}): 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                 |   2 +-
 INSTALL                    |   2 +-
 Makefile.in                |   4 +-
 admin/authors.el           |   1 -
 admin/check-man-pages      |   7 ---
 admin/quick-install-emacs  |   2 +-
 cross/Makefile.in          |   2 +-
 doc/emacs/android.texi     |   6 +-
 doc/lispref/processes.texi |   7 +--
 doc/man/ctags.1            |   1 -
 doc/man/etags.1            |  32 +++++-----
 etc/NEWS                   |   6 ++
 etc/PROBLEMS               |   4 +-
 java/Makefile.in           |   4 +-
 lib-src/Makefile.in        |   9 +--
 lib-src/ctags.c            |   2 -
 lib-src/etags.c            | 123 ++++++++++++++++++-------------------
 nt/README                  |   2 +-
 nt/README.W32              |   2 +-
 src/callproc.c             |   6 +-
 20 files changed, 101 insertions(+), 123 deletions(-)
 delete mode 100644 doc/man/ctags.1
 delete mode 100644 lib-src/ctags.c

diff --git a/.gitignore b/.gitignore
index ea942b86f80..8642ff47c29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -209,7 +209,7 @@ test/infra/android/**/*.zip
 test/infra/android/**/*.jar
 test/infra/android/bin/build.sh
 
-# ctags, etags.
+# etags.
 TAGS
 !admin/notes/tags
 
diff --git a/INSTALL b/INSTALL
index d38fff7b2e6..40cb674d48f 100644
--- a/INSTALL
+++ b/INSTALL
@@ -612,7 +612,7 @@ installed locations, with 'make install'.  By default, Emacs's files
 are installed in the following directories:
 
 '/usr/local/bin' holds the executable programs users normally run -
-		'emacs', 'etags', 'ctags', 'emacsclient'.
+		'emacs', 'etags', 'emacsclient'.
 
 '/usr/local/share/emacs/VERSION/lisp' holds the Emacs Lisp library;
 		'VERSION' stands for the number of the Emacs version
diff --git a/Makefile.in b/Makefile.in
index ca6d991bce6..0f06ba00ee8 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -818,9 +818,7 @@ install-info:
 
 ## "gzip || true" is because some gzips exit with non-zero status
 ## if compression would not reduce the file size.  Eg, the gzip in
-## OpenBSD 4.9 seems to do this (2013/03).  In Emacs, this can
-## only happen with the tiny ctags.1 manpage.  We don't really care if
-## ctags.1 is compressed or not.  "gzip -f" is another option here,
+## OpenBSD 4.9 seems to do this (2013/03).  "gzip -f" is another option here,
 ## but not sure if portable.
 install-man:
 	umask 022; ${MKDIR_P} "$(DESTDIR)${man1dir}"
diff --git a/admin/authors.el b/admin/authors.el
index 598deb681e5..b9f5f170fc0 100644
--- a/admin/authors.el
+++ b/admin/authors.el
@@ -1471,7 +1471,6 @@ authors-renamed-files-alist
     ("org/COPYRIGHT-AND-LICENSE" . "org/README")
     ("lisp/net/idna.el" . "puny.el")
     ;; Moved to different directories.
-    ("ctags.1" . "ctags.1")
     ("etags.1" . "etags.1")
     ("emacs.1" . "emacs.1")
     ("emacsclient.1" . "emacsclient.1")
diff --git a/admin/check-man-pages b/admin/check-man-pages
index 409003e0462..84ca56bf084 100755
--- a/admin/check-man-pages
+++ b/admin/check-man-pages
@@ -32,13 +32,6 @@ exit_status=0
 
 cd "$PD"/../doc/man
 for page in *.1; do
-    # ctags.1 just includes the man page etags.1, which AFAICT will
-    # default to the one installed on the system (!), instead of the
-    # one in the repository.  So checking it is pointless, and we will
-    # in any case already check etags.1 separately.
-    if [ "$page" == "ctags.1" ]; then
-        continue
-    fi
     log=$(emacs_mktemp)
     LC_ALL=C.UTF-8 MANROFFSEQ='' MANWIDTH=80 \
         man --warnings=all,mac -E UTF-8 -l -Tutf8 -Z "$page" >/dev/null 2> "$log"
diff --git a/admin/quick-install-emacs b/admin/quick-install-emacs
index ceabe80024e..5453456d09c 100755
--- a/admin/quick-install-emacs
+++ b/admin/quick-install-emacs
@@ -27,7 +27,7 @@
 ## install emacs very often.  See the --help output for more details.

-PUBLIC_LIBSRC_BINARIES='emacsclient etags ctags ebrowse'
+PUBLIC_LIBSRC_BINARIES='emacsclient etags ebrowse'
 
 AVOID="CVS -DIC README COPYING ChangeLog ~ [.]orig$ [.]rej$ Makefile$ Makefile.in$ makefile$ makefile.w32-in$ stamp-subdir [.]cvsignore [.]arch-ids [{]arch[}] [.][cho]$ make-docfile"
 
diff --git a/cross/Makefile.in b/cross/Makefile.in
index 94a28a755bc..b1322ea4c5b 100644
--- a/cross/Makefile.in
+++ b/cross/Makefile.in
@@ -50,7 +50,7 @@ LIB_SRC_TOP_SRCDIR =
 
 # This is a list of binaries to build and install in lib-src.
 
-LIBSRC_BINARIES = lib-src/etags lib-src/ctags lib-src/emacsclient \
+LIBSRC_BINARIES = lib-src/etags lib-src/emacsclient \
 		  lib-src/ebrowse lib-src/hexl lib-src/movemail
 
 CLEAN_SUBDIRS = $(wildcard src lib-src lib etc)
diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi
index c318373536a..8b00ea94cb3 100644
--- a/doc/emacs/android.texi
+++ b/doc/emacs/android.texi
@@ -353,11 +353,11 @@ Android Environment
   The Emacs distribution also incorporates several binaries.  While
 being executable files, they are packaged as libraries in the library
 directory, because otherwise the system will not unpack them while
-Emacs is being installed.  This means that instead of @code{ctags} or
-@code{emacsclient}, Lisp code must specify @code{libctags.so} or
+Emacs is being installed.  This means that instead of
+@code{emacsclient}, Lisp code must specify
 @code{libemacsclient.so} on the command line when starting either of
 those programs in a subprocess; to determine which names to use,
-consult the values of the variables @code{ctags-program-name},
+consult the values of the variables
 @code{etags-program-name}, @code{hexl-program-name},
 @code{emacsclient-program-name}, @code{movemail-program-name},
 @code{ebrowse-program-name}, and @code{rcs2log-program-name}.
diff --git a/doc/lispref/processes.texi b/doc/lispref/processes.texi
index 804efb50162..56e78eac900 100644
--- a/doc/lispref/processes.texi
+++ b/doc/lispref/processes.texi
@@ -186,7 +186,6 @@ Subprocess Creation
 @end defun
 
 @cindex programs distributed with Emacs, starting
-@vindex ctags-program-name
 @vindex etags-program-name
 @vindex hexl-program-name
 @vindex emacsclient-program-name
@@ -197,11 +196,11 @@ Subprocess Creation
 must take into account that the program may have been renamed in order
 to comply with executable naming restrictions present on the system.
 
-  Instead of starting @command{ctags}, for example, you should specify
-the value of @code{ctags-program-name} instead.  Likewise, instead of
+  Instead of starting @command{emacsclient}, for example, you should specify
+the value of @code{emacsclient-program-name} instead.  Likewise, instead of
 starting @command{movemail}, you must start
 @code{movemail-program-name}, and the same goes for @command{etags},
-@command{hexl}, @command{emacsclient}, @code{rcs2log}, and
+@command{hexl}, @code{rcs2log}, and
 @command{ebrowse}.
 
 @node Shell Arguments
diff --git a/doc/man/ctags.1 b/doc/man/ctags.1
deleted file mode 100644
index 1eab02f2145..00000000000
--- a/doc/man/ctags.1
+++ /dev/null
@@ -1 +0,0 @@
-.so man1/etags.1
diff --git a/doc/man/etags.1 b/doc/man/etags.1
index a87ad22d48e..709d638b980 100644
--- a/doc/man/etags.1
+++ b/doc/man/etags.1
@@ -1,5 +1,5 @@
 .\" See section COPYING for copyright and redistribution information.
-.TH ETAGS 1 "2024-12-21" "GNU Tools" "GNU"
+.TH ETAGS 1 "2025-03-11" "GNU Tools" "GNU"
 .de BP
 .sp
 .ti -.2i
@@ -7,7 +7,7 @@
 ..
 
 .SH NAME
-etags, ctags \- generate tag file for Emacs, vi
+etags \- generate tag file for Emacs, vi
 .SH SYNOPSIS
 .hy 0
 .na
@@ -27,7 +27,7 @@ etags, ctags \- generate tag file for Emacs, vi
 [\|\-\-help\|] [\|\-\-version\|]
 \fIfile\fP .\|.\|.
 
-\fBctags\fP [\|\-aCdgIQRVh\|] [\|\-BtTuvwx\|] [\|\-l \fIlanguage\fP\|]
+\fBetags \-\-ctags\fP [\|\-aCdgIQRVh\|] [\|\-BtTuvwx\|] [\|\-l \fIlanguage\fP\|]
 .if n .br
 [\|\-o \fItagfile\fP\|] [\|\-r \fIregexp\fP\|]
 [\|\-\-parse\-stdin=\fIfile\fP\|]
@@ -47,17 +47,18 @@ etags, ctags \- generate tag file for Emacs, vi
 The \|\fBetags\fP\| program is used to create a tag table file, in a format
 understood by
 .BR emacs ( 1 )\c
-\&; the \|\fBctags\fP\| program is used to create a similar table in a
+\&; if the first option is \|\fB\-\-ctags\fP\|
+the program is used to create a similar table in a
 format understood by
 .BR vi ( 1 )\c
-\&.  Both forms of the program understand
+\&.  The program understands
 the syntax of C, Objective C, C++, Java, Fortran, Ada, Cobol, Erlang,
 Forth, Go, HTML, LaTeX, Emacs Lisp/Common Lisp, Lua, Makefile, Mercury, Pascal,
 Perl, Ruby, Rust, PHP, PostScript, Python, Prolog, Scheme and most
 assembler\-like syntaxes.
-Both forms read the files specified on the command line, and write a tag
-table (defaults: \fBTAGS\fP for \fBetags\fP, \fBtags\fP for
-\fBctags\fP) in the current working directory.
+It reads the files specified on the command line, and write a tag
+table (default: \fBTAGS\fP, or \fBtags\fP if
+\fB\-\-ctags\fP is used) in the current working directory.
 Files specified with relative file names will be recorded in the tag
 table with file names relative to the directory where the tag table
 resides.  If the tag table is in /dev or is the standard output,
@@ -73,8 +74,7 @@ parsing of the file names following the switch according to the given
 language, overriding guesses based on filename extensions.
 .SH OPTIONS
 Some options make sense only for the \fBvi\fP style tag files produced
-by ctags;
-\fBetags\fP does not recognize them.
+with the \fB\-\-ctags\fP option; they are ignored otherwise.
 The programs accept unambiguous abbreviations for long option names.
 .TP
 .B \-a, \-\-append
@@ -87,7 +87,7 @@ expression search instructions; the \fB\-B\fP option writes them using
 the delimiter "\|\fB?\fP\|", to search \fIbackwards\fP through files.
 The default is to use the delimiter "\|\fB/\fP\|", to search \fIforwards\fP
 through files.
-Only \fBctags\fP accepts this option.
+This option makes sense only if \fB\-\-ctags\fP is used.
 .TP
 .B \-\-declarations
 In C and derived languages, create tags for function declarations,
@@ -185,7 +185,7 @@ the previous ones.  The regexps are of one of the forms:
 where \fItagregexp\fP is used to match the tag.  It should not match
 useless characters.  If the match is such that more characters than
 needed are unavoidably matched by \fItagregexp\fP, it may be useful to
-add a \fInameregexp\fP, to narrow down the tag scope.  \fBctags\fP
+add a \fInameregexp\fP, to narrow down the tag scope.  \fB\-\-ctags\fP
 ignores regexps without a \fInameregexp\fP.  The syntax of regexps is
 the same as in Emacs, except that backslash escapes are the same
 as GNU grep (which means, for example, that shy groups are not supported),
@@ -281,15 +281,17 @@ tag entries for other files in place.  Currently, this is implemented
 by deleting the existing entries for the given files and then
 rewriting the new entries at the end of the tags file.  It is often
 faster to simply rebuild the entire tag file than to use this.
-Only \fBctags\fP accepts this option.
+This option makes sense only if \fB\-\-ctags\fP is used.
 .TP
 .B \-v, \-\-vgrind
 Instead of generating a tag file, write index (in \fBvgrind\fP format)
-to standard output.  Only \fBctags\fP accepts this option.
+to standard output.
+This option makes sense only if \fB\-\-ctags\fP is used.
 .TP
 .B \-x, \-\-cxref
 Instead of generating a tag file, write a cross reference (in
-\fBcxref\fP format) to standard output.  Only \fBctags\fP accepts this option.
+\fBcxref\fP format) to standard output.
+This option makes sense only if \fB\-\-ctags\fP is used.
 .TP
 .B \-h, \-H, \-\-help
 Print usage information.  Followed by one or more \-\-language=LANG
diff --git a/etc/NEWS b/etc/NEWS
index 926d426eaae..521d54528a5 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -29,6 +29,12 @@ applies, and please also update docstrings as needed.
 The traditional unexec dumper, deprecated since Emacs 27, has been
 removed.
 
+---
+** Emacs's old ctags program is no longer built or installed.
+You are encouraged to use Universal Ctags <https://ctags.io/> instead.
+For now, to get the old ctags behavior you can can run 'etags --ctags'
+or use a shell script named 'ctags' that runs 'etags --ctags "$@"'.
+
 ---
 ** Changed GCC default options on 32-bit x86 systems.
 When using GCC 4 or later to build Emacs on 32-bit x86 systems,
diff --git a/etc/PROBLEMS b/etc/PROBLEMS
index 0d61a3bc76e..952813f0c5b 100644
--- a/etc/PROBLEMS
+++ b/etc/PROBLEMS
@@ -3973,7 +3973,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
@@ -4004,7 +4004,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..4988290a207 100644
--- a/java/Makefile.in
+++ b/java/Makefile.in
@@ -128,7 +128,6 @@ IS_D8_R8 :=
 # containing the following files:
 #  lib/$(ANDROID_ABI)/libemacs.so
 #  lib/$(ANDROID_ABI)/libandroid-emacs.so
-#  lib/$(ANDROID_ABI)/libctags.so
 #  lib/$(ANDROID_ABI)/libetags.so
 #  lib/$(ANDROID_ABI)/libhexl.so
 #  lib/$(ANDROID_ABI)/libmovemail.so
@@ -143,8 +142,7 @@ all:
 
 # Binaries to cross-compile.
 CROSS_SRC_BINS := $(top_builddir)/cross/src/android-emacs
-CROSS_LIBSRC_BINS := $(top_builddir)/cross/lib-src/ctags	\
-		     $(top_builddir)/cross/lib-src/hexl		\
+CROSS_LIBSRC_BINS := $(top_builddir)/cross/lib-src/hexl		\
 		     $(top_builddir)/cross/lib-src/ebrowse	\
 		     $(top_builddir)/cross/lib-src/emacsclient	\
 		     $(top_builddir)/cross/lib-src/etags
diff --git a/lib-src/Makefile.in b/lib-src/Makefile.in
index 439d9a1a52d..49a243e4795 100644
--- a/lib-src/Makefile.in
+++ b/lib-src/Makefile.in
@@ -155,7 +155,7 @@ ANDROID=
 CLIENTW = @CLIENTW@
 
 # Things that a user might actually run, which should be installed in bindir.
-INSTALLABLES = etags${EXEEXT} ctags${EXEEXT} emacsclient${EXEEXT} $(CLIENTW) \
+INSTALLABLES = etags${EXEEXT} emacsclient${EXEEXT} $(CLIENTW) \
                ebrowse${EXEEXT}
 
 # Things that Emacs runs internally, or during the build process,
@@ -415,13 +415,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.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..d511bc39588 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.  */
@@ -465,12 +455,12 @@ #define xrnew(op, n, m) ((op) = xnrealloc (op, n, (m) * sizeof *(op)))
 				/* member functions. */
 static bool constantypedefs;	/* -d: create tags for C #define, enum */
 				/* constants and variables. */
-				/* -D: opposite of -d.  Default under ctags. */
+				/* -D: opposite of -d.  Default if ctags. */
 static int globals;		/* create tags for global variables */
 static int members;		/* create tags for C member variables */
 static int declarations;	/* --declarations: tag them and extern in C&Co*/
 static int no_line_directive;	/* ignore #line directives (undocumented) */
-static int no_duplicates;	/* no duplicate tags for ctags (undocumented) */
+static int no_duplicates;	/* no duplicate tags if ctags (undocumented) */
 static bool update;		/* -u: update tags */
 static bool vgrind_style;	/* -v: create vgrind style index output */
 static bool no_warnings;	/* -w: suppress warnings (undocumented) */
@@ -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",              no_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     },
@@ -934,15 +929,10 @@ print_language_names (void)
 etags --help --lang=ada.");
 }
 
-#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"
+  fputs (("etags (" PACKAGE_NAME " " PACKAGE_VERSION ")\n"
 	  COPYRIGHT "\n"
 	  "This program is distributed under the terms in ETAGS.README\n"),
 	 stdout);
@@ -984,7 +974,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 +987,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\n\
+	Implement ctags behavior.");
+
   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 +1002,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 +1010,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 +1020,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 +1031,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 +1039,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 +1086,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 +1095,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 +1104,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 +1154,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 +1185,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 +1206,13 @@ main (int argc, char **argv)
 	++file_count;
 	break;
 
-      case STDIN:
+      case CTAGS_OPTION:
+	/* Operate as ctags, not etags.  */
+	ctags = true;
+	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 +1322,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 +1346,7 @@ main (int argc, char **argv)
   linebuffer_init (&filebuf);
   linebuffer_init (&token_name);
 
-  if (!CTAGS)
+  if (!ctags)
     {
       if (streq (tagfile, "-"))
 	{
@@ -1407,13 +1404,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 +1459,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 +1857,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 +2089,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 +2130,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 +2165,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 +2292,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 +2391,7 @@ invalidate_nodes (fdesc *badfdp, node **npp)
   node *np = *npp;
   stkentry *stack = NULL;
 
-  if (CTAGS)
+  if (ctags)
     {
       while (np)
 	{
@@ -2504,7 +2501,7 @@ put_entry (node *np)
   /* Output this entry */
   if (np->valid)
     {
-      if (!CTAGS)
+      if (!ctags)
 	{
 	  /* Etags mode */
 	  if (fdp != np->fdp)
@@ -2576,7 +2573,7 @@ put_entries (node *np)
   if (np == NULL)
     return;
 
-  if (CTAGS)
+  if (ctags)
     {
       while (np)
 	{
diff --git a/nt/README b/nt/README
index a909ada1c87..14ff59de87c 100644
--- a/nt/README
+++ b/nt/README
@@ -44,7 +44,7 @@
   + addpm.exe - A basic installer that creates Start Menu icons for Emacs.
     Running this is optional.
 
-  + ctags.exe, etags.exe - Tools for generating tag files.  See the
+  + etags.exe - Tool for generating tag files.  See the
     `Tags' node of the Emacs manual.
 
   + ebrowse.exe - A tool for generating C++ browse information.  See the
diff --git a/nt/README.W32 b/nt/README.W32
index 25ca20bc7bf..fc415d67769 100644
--- a/nt/README.W32
+++ b/nt/README.W32
@@ -104,7 +104,7 @@ See the end of the file for license conditions.
   + addpm.exe - A basic installer that adds Emacs to "Start" menus and
     adds Emacs-related entries to the Windows Registry.
 
-  + ctags.exe, etags.exe - Tools for generating tag files.  See the
+  + etags.exe - Tool for generating tag files.  See the
     `Tags' node of the Emacs manual.
 
   + ebrowse.exe - A tool for generating C++ browse information.  See the
diff --git a/src/callproc.c b/src/callproc.c
index 33b0feaf266..7059a2bac6f 100644
--- a/src/callproc.c
+++ b/src/callproc.c
@@ -2167,14 +2167,10 @@ syms_of_callproc (void)
   Vprocess_environment = Qnil;
 
   DEFVAR_LISP ("ctags-program-name", Vctags_program_name,
-    doc: /* Name of the `ctags' program distributed with Emacs.
+    doc: /* Name of the `ctags' program.
 Use this instead of calling `ctags' directly, as `ctags' may have been
 renamed to comply with executable naming restrictions on the system.  */);
-#if !defined HAVE_ANDROID || defined ANDROID_STUBIFY
   Vctags_program_name = build_string ("ctags");
-#else
-  Vctags_program_name = build_string ("libctags.so");
-#endif
 
   DEFVAR_LISP ("etags-program-name", Vetags_program_name,
     doc: /* Name of the `etags' program distributed with Emacs.
-- 
2.45.2