Eric Bower
·
2026-09-16
1#!/usr/bin/env bash
2set -euo pipefail
3
4if ! command -v ctags >/dev/null 2>&1; then
5 echo "error: ctags not found in PATH" >&2
6 exit 1
7fi
8
9if ! command -v fd >/dev/null 2>&1; then
10 echo "error: fd not found in PATH" >&2
11 exit 1
12fi
13
14outfile=""
15paths=()
16while [[ $# -gt 0 ]]; do
17 case "$1" in
18 -f) outfile="$2"; shift 2 ;;
19 *) paths+=("$1"); shift ;;
20 esac
21done
22
23if [[ -z "$outfile" ]]; then
24 echo "error: -f <outfile> is required" >&2
25 exit 1
26fi
27
28[[ ${#paths[@]} -eq 0 ]] && paths=(".")
29
30# .tsx isn't mapped to the TypeScript parser by default in Universal Ctags,
31# so every .tsx component would otherwise go untagged.
32ctags_flags=(--map-TypeScript=+.tsx --fields=+S -f "$outfile")
33
34# A single-file target (e.g. per-file tag update on save) is tagged directly.
35if [[ ${#paths[@]} -eq 1 && -f "${paths[0]}" ]]; then
36 exec ctags "${ctags_flags[@]}" "${paths[0]}"
37fi
38
39# Directory targets: fd respects .gitignore, so build output, dependency
40# dirs, and caches (which can hold single lines many MB long, e.g. turbo
41# manifests) never get scanned and bloat the tags file.
42fd -t f -H --no-require-git -E .git . "${paths[@]}" | ctags "${ctags_flags[@]}" -L -