Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Tmain/oneshot.d/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright: 2026 Masatake YAMATO
# License: GPL-2

CTAGS=$1

gen()
{
cat <<EOF
int v;
int f(void)
{
return 0;
}
EOF
}

gen | ${CTAGS} --quiet --options=NONE --oneshot=sort-no.c --sort=no -o -
gen | ${CTAGS} --quiet --options=NONE --oneshot=sort-yes.c --sort=yes -o -
gen | ${CTAGS} --quiet --options=NONE --oneshot=x.c -x
Empty file.
6 changes: 6 additions & 0 deletions Tmain/oneshot.d/stdout-expected.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
v sort-no.c /^int v;$/;" v typeref:typename:int
f sort-no.c /^int f(void)$/;" f typeref:typename:int
f sort-yes.c /^int f(void)$/;" f typeref:typename:int
v sort-yes.c /^int v;$/;" v typeref:typename:int
f function 2 x.c int f(void)
v variable 1 x.c int v;
42 changes: 42 additions & 0 deletions docs/man/ctags.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,48 @@ Input/Output File Options

This option is quite esoteric and is empty by default.

``--oneshot=<filename>``
Makes ctags behave as a filter, reading source
file contents from standard input and printing their tags to
standard output. When this option is enabled, the options ``-L``,
``-f``, ``-o``, and ``--totals`` are ignored.

Comment on lines +224 to +229
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The man page documents --oneshot, but the newly introduced --oneshot-limit=<bytes> option is not described here. Consider adding a short paragraph in this section explaining the limit behavior (including that 0 disables the limit and the default is 32MB).

Copilot uses AI. Check for mistakes.
ctags assumes *<filename>* is the input file
name of the contents. It affects language selection.
*<filename>* appears as the input file name in the tags output.

This option is useful for extracting interesting names in command
output.

For example, you can extract C preprocessor macro names in the source
code specified by a URL:

.. code-block:: console

$ curl -s -o - \
'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \
| ./ctags --oneshot=buf.c --kinds-C='{macro}'
BH_ENTRY buf.c /^#define BH_ENTRY(/;" d file:
...

Make the output in JSON format:

.. code-block:: console

$ curl -s -o - \
'https://raw.githubusercontent.com/torvalds/linux/refs/heads/master/fs/buffer.c' \
| ./ctags --oneshot=buf.c --kinds-C='{macro}' --output-format=json \
| jq .
{
"_type": "tag",
"name": "BH_ENTRY",
"path": "buf.c",
"pattern": "/^#define BH_ENTRY(/",
"file": true,
"kind": "macro"
}
...

``--links[=(yes|no)]``
Indicates whether symbolic links (if supported) should be followed.
When disabled, symbolic links are ignored. This option is on by default.
Expand Down
6 changes: 6 additions & 0 deletions docs/news/HEAD.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ Extend ``--map-<LANG>=`` and ``--langmap=`` options to choose a parser using reg

``--list-maps`` has also been extended to include regular-expression-based mappings.

New option: ``--oneshot=<filename>``

Makes @CTAGS_NAME_EXECUTABLE@ behave as a filter, reading source
file contents from standard input and printing their tags to
standard output.

Incompatible changes
---------------------------------------------------------------------
Messages for broken symlinks are now emitted at NOTICE level instead of
Expand Down
20 changes: 12 additions & 8 deletions main/entry.c
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ extern void openTagFile (void)
*/
if (TagsToStdout)
{
if (Option.interactive == INTERACTIVE_SANDBOX)
if (Option.interactive & INTERACTIVE_WITH_SANDBOX)
{
TagFile.mio = mio_new_memory (NULL, 0, eRealloc, eFreeNoNullCheck);
TagFile.name = NULL;
Expand Down Expand Up @@ -536,7 +536,6 @@ static int replacementTruncate (const char *const name, const long size)

#endif

#ifndef EXTERNAL_SORT
static void internalSortTagFile (void)
{
MIO *mio;
Expand All @@ -562,20 +561,25 @@ static void internalSortTagFile (void)
if (! TagsToStdout)
mio_unref (mio);
}
#endif

static void sortTagFile (void)
static void sortTagFile (bool forceUseInternalSort)
{
if (TagFile.numTags.added > 0L)
{
if (Option.sorted != SO_UNSORTED)
{
verbose ("sorting tag file\n");

if (forceUseInternalSort)
internalSortTagFile ();
else
{
#ifdef EXTERNAL_SORT
externalSortTags (TagsToStdout, TagFile.mio);
externalSortTags (TagsToStdout, TagFile.mio);
#else
internalSortTagFile ();
internalSortTagFile ();
#endif
}
}
else if (TagsToStdout)
catFile (TagFile.mio);
Expand Down Expand Up @@ -632,7 +636,7 @@ static void writeEtagsIncludes (MIO *const mio)
}
}

extern void closeTagFile (const bool resize)
extern void closeTagFile (const bool resize, bool forceUseInternalSort)
{
long desiredSize, size;

Expand All @@ -656,7 +660,7 @@ extern void closeTagFile (const bool resize)
TagFile.name? TagFile.name: "<mio>", size, desiredSize); )
resizeTagFile (desiredSize);
}
sortTagFile ();
sortTagFile (forceUseInternalSort);
if (TagsToStdout)
{
if (mio_unref (TagFile.mio) != 0)
Expand Down
2 changes: 1 addition & 1 deletion main/entry_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ extern const roleDefinition* getTagRole(const tagEntryInfo *const tag, int roleI
extern void freeTagFileResources (void);
extern const char *tagFileName (void);
extern void openTagFile (void);
extern void closeTagFile (const bool resize);
extern void closeTagFile (const bool resize, bool forceUseInternalSort);
extern void setupWriter (void *writerClientData);
extern bool teardownWriter (const char *inputFilename);

Expand Down
20 changes: 19 additions & 1 deletion main/interactive_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,30 @@
struct interactiveModeArgs
{
bool sandbox;
const char *fname; /* --_interactive=oneshot:FNAME */
size_t limit; /* Upper limit of input data
* received via stdin.
* Used in oneshot mode.
* 0 means no limit. */
};

#ifdef HAVE_JANSSON
void interactiveLoop (cookedArgs *args, void *user);
bool jsonErrorPrinter (const errorSelection selection, const char *const format, va_list ap,
void *data);
int installSyscallFilter (void);
#endif

void interactiveOneshot (cookedArgs *args, void *user);
void batchOneshot (cookedArgs *args, void *user);

enum syscallSet {
syscall_coreset = 1 << 0,
syscall_open = 1 << 1,
syscall_close = 1 << 2,
syscall_ctrlset = 1 << 3,
};

int installSyscallFilter (unsigned int set);

#endif /* CTAGS_MAIN_INTERACTIVE_H */

Expand Down
99 changes: 79 additions & 20 deletions main/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@
#include "writer_p.h"
#include "xtag_p.h"

#ifdef HAVE_JANSSON
#include "interactive_p.h"
#ifdef HAVE_JANSSON
#include <jansson.h>
#include <errno.h>
#endif
Expand Down Expand Up @@ -376,7 +376,7 @@ static void batchMakeTags (cookedArgs *args, void *user CTAGS_ATTR_UNUSED)
timeStamp (1);

if ((! Option.filter) && (!Option.printLanguage))
closeTagFile (resize);
closeTagFile (resize, false);

timeStamp (2);

Expand All @@ -391,27 +391,24 @@ static void batchMakeTags (cookedArgs *args, void *user CTAGS_ATTR_UNUSED)
#undef timeStamp
}

static void prepareSandbox (unsigned int set)
{
if (installSyscallFilter (set)) {
error (FATAL, "install_syscall_filter failed");
/* The explicit exit call is needed because
"error (FATAL,..." just prints a message in
interactive mode. */
exit (1);
}
}

#ifdef HAVE_JANSSON
void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
{
struct interactiveModeArgs *iargs = user;

if (iargs->sandbox) {
/* As of jansson 2.6, the object hashing is seeded off
of /dev/urandom, so trigger the hash seeding
before installing the syscall filter.
*/
json_t * tmp = json_object ();
json_decref (tmp);

if (installSyscallFilter ()) {
error (FATAL, "install_syscall_filter failed");
/* The explicit exit call is needed because
"error (FATAL,..." just prints a message in
interactive mode. */
exit (1);
}
}
if (iargs->sandbox)
prepareSandbox (syscall_coreset);

char buffer[1024];
json_t *request;
Expand Down Expand Up @@ -457,7 +454,7 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
if (iargs->sandbox) {
error (FATAL,
"invalid request in sandbox submode: reading file contents from a file is limited");
closeTagFile (false);
closeTagFile (false, false);
goto next;
}

Expand All @@ -472,7 +469,7 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
mio_unref (mio);
}

closeTagFile (false);
closeTagFile (false, false);
fputs ("{\"_type\": \"completed\", \"command\": \"generate-tags\"}\n", stdout);
fflush(stdout);
}
Expand All @@ -488,6 +485,68 @@ void interactiveLoop (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
}
#endif

static void oneshotCommon (const char *fname, size_t limit, bool sandbox, int strictSyscallset)
{
openTagFile ();

if (sandbox && strictSyscallset)
prepareSandbox (strictSyscallset);

MIO *mio = mio_new_memory (NULL, 0, eRealloc, eFreeNoNullCheck);
int c;
size_t r = 0;
while ((c = getchar()) != EOF)
{
r++;
if (limit != 0 && r > limit)
{
error (WARNING, "input read from stdin exceeds the limit: name: %s, size: %lu",
fname,
(unsigned long) limit);
break;
}
mio_putc (mio, c);
}
Comment on lines +495 to +509
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading stdin one byte at a time via getchar() + mio_putc() adds avoidable overhead, especially with the default 32MB limit. Consider buffered reads (e.g., fread() into a stack buffer and mio_write()), while still enforcing iargs->limit.

Copilot uses AI. Check for mistakes.

mio_seek (mio, 0, SEEK_SET);

parseFileWithMio (fname, mio, NULL);

mio_unref (mio);

closeTagFile (false, sandbox ? true : false);
}

extern void batchOneshot (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
{
struct interactiveModeArgs *iargs = user;

Assert (iargs->fname);

unsigned int extra_set;

extra_set = isDestinationStdout () ? syscall_close | syscall_ctrlset
: syscall_open | syscall_close | syscall_ctrlset;
if (iargs->sandbox)
prepareSandbox (syscall_coreset | extra_set);

extra_set = isDestinationStdout () ? 0
: syscall_open | syscall_close;
oneshotCommon (iargs->fname, iargs->limit, iargs->sandbox, syscall_coreset | extra_set);
}

extern void interactiveOneshot (cookedArgs *args CTAGS_ATTR_UNUSED, void *user)
{
struct interactiveModeArgs *iargs = user;

Assert (iargs->fname);

if (iargs->sandbox)
prepareSandbox (syscall_coreset);

oneshotCommon (iargs->fname, iargs->limit, iargs->sandbox, 0);
}

static bool isSafeVar (const char* var)
{
const char *safe_vars[] = {
Expand Down
Loading
Loading