summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml67
-rw-r--r--.github/workflows/codecov.yml5
-rw-r--r--.github/workflows/codeql.yml2
-rw-r--r--Makefile9
-rw-r--r--bench/bench.sh41
-rw-r--r--bench/ioq.c238
-rw-r--r--build/auto.mk54
-rwxr-xr-xbuild/cc.sh21
-rw-r--r--build/config.mk27
-rw-r--r--build/exports.mk5
-rwxr-xr-xbuild/flags-if.sh85
-rw-r--r--build/flags.mk74
-rw-r--r--build/flags/std.c12
-rw-r--r--build/has/tcsetwinsize.c9
-rw-r--r--build/header.mk11
-rw-r--r--build/pkgs.mk7
-rw-r--r--build/prelude.mk8
-rwxr-xr-xbuild/version.sh2
-rwxr-xr-xconfigure20
-rw-r--r--docs/CHANGELOG.md62
-rw-r--r--docs/CONTRIBUTING.md3
-rw-r--r--docs/bfs.12
-rw-r--r--src/alloc.c2
-rw-r--r--src/alloc.h22
-rw-r--r--src/bar.c18
-rw-r--r--src/bfs.h120
-rw-r--r--src/bfstd.c132
-rw-r--r--src/bfstd.h63
-rw-r--r--src/bftw.c6
-rw-r--r--src/color.c275
-rw-r--r--src/color.h6
-rw-r--r--src/ctx.c1
-rw-r--r--src/ctx.h2
-rw-r--r--src/diag.c2
-rw-r--r--src/diag.h118
-rw-r--r--src/dstring.c1
-rw-r--r--src/dstring.h56
-rw-r--r--src/eval.c10
-rw-r--r--src/exec.c2
-rw-r--r--src/expr.h3
-rw-r--r--src/fsade.c8
-rw-r--r--src/ioq.c6
-rw-r--r--src/list.h263
-rw-r--r--src/mtab.c2
-rw-r--r--src/opt.c31
-rw-r--r--src/parse.c419
-rw-r--r--src/prelude.h28
-rw-r--r--src/printf.c8
-rw-r--r--src/sanity.h14
-rw-r--r--src/sighook.c2
-rw-r--r--src/thread.c9
-rw-r--r--src/trie.c17
-rw-r--r--src/trie.h1
-rw-r--r--src/xspawn.c82
-rw-r--r--tests/bfs/color_bsd.out27
-rw-r--r--tests/bfs/color_bsd.sh1
-rw-r--r--tests/bfs/execdir_path_relative_slash.out19
-rw-r--r--tests/bfs/execdir_path_relative_slash.sh1
-rw-r--r--tests/bfs/files0_from_root.sh2
-rw-r--r--tests/bfstd.c176
-rw-r--r--tests/getopts.sh6
-rw-r--r--tests/gnu/execdir_self.out1
-rw-r--r--tests/gnu/execdir_self.sh9
-rw-r--r--tests/gnu/files0_from_empty.sh2
-rw-r--r--tests/gnu/files0_from_file_file.out2
-rw-r--r--tests/gnu/files0_from_file_file.sh3
-rw-r--r--tests/gnu/files0_from_ok.sh1
-rw-r--r--tests/gnu/files0_from_stdin_ok.sh1
-rw-r--r--tests/gnu/files0_from_stdin_ok_file.out45
-rw-r--r--tests/gnu/files0_from_stdin_ok_file.sh4
-rw-r--r--tests/gnu/files0_from_stdin_stdin.out45
-rw-r--r--tests/gnu/files0_from_stdin_stdin.sh2
-rw-r--r--tests/gnu/follow_files0_from.out42
-rw-r--r--tests/gnu/follow_files0_from.sh1
-rw-r--r--tests/gnu/ok_files0_from_stdin.sh1
-rw-r--r--tests/gnu/ok_flush.sh2
-rw-r--r--tests/list.c3
-rw-r--r--tests/main.c8
-rw-r--r--tests/posix/exec_sigmask.sh4
-rw-r--r--tests/posix/group_o_group.out19
-rw-r--r--tests/posix/group_o_group.sh3
-rw-r--r--tests/posix/root_order.out4
-rw-r--r--tests/posix/root_order.sh6
-rw-r--r--tests/posix/user_o_user.out19
-rw-r--r--tests/posix/user_o_user.sh3
-rw-r--r--tests/ptyx.c248
-rw-r--r--tests/run.sh17
-rw-r--r--tests/tests.h33
-rw-r--r--tests/util.sh27
-rw-r--r--tests/xspawn.c31
-rw-r--r--tests/xtouch.c4
91 files changed, 2345 insertions, 970 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e34313..3d9899e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,18 +5,19 @@ on: [push, pull_request]
jobs:
linux-x86:
name: Linux (x86)
-
runs-on: ubuntu-24.04
+ # Don't run on both pushes and pull requests
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install dependencies
run: |
sudo dpkg --add-architecture i386
sudo apt-get update -y
sudo apt-get install -y \
- expect \
mandoc \
gcc-multilib \
libgcc-s1:i386 \
@@ -47,17 +48,17 @@ jobs:
linux-arm:
name: Linux (Arm64)
-
runs-on: ubuntu-24.04-arm
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
- expect \
mandoc \
acl \
libacl1-dev \
@@ -78,11 +79,12 @@ jobs:
macos:
name: macOS
-
runs-on: macos-15
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install dependencies
run: |
@@ -95,14 +97,15 @@ jobs:
freebsd:
name: FreeBSD
-
runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run tests
- uses: cross-platform-actions/action@v0.27.0
+ uses: cross-platform-actions/action@v0.29.0
with:
operating_system: freebsd
version: "14.2"
@@ -110,10 +113,8 @@ jobs:
run: |
sudo pkg install -y \
bash \
- expect \
oniguruma \
- pkgconf \
- tcl-wrapper
+ pkgconf
sudo mount -t fdescfs none /dev/fd
.github/diag.sh make -j$(nproc) distcheck
@@ -124,22 +125,22 @@ jobs:
openbsd:
name: OpenBSD
-
runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run tests
- uses: cross-platform-actions/action@v0.27.0
+ uses: cross-platform-actions/action@v0.29.0
with:
operating_system: openbsd
- version: "7.6"
+ version: "7.7"
run: |
sudo pkg_add \
bash \
- expect \
gmake \
oniguruma
jobs=$(sysctl -n hw.ncpu)
@@ -153,14 +154,15 @@ jobs:
netbsd:
name: NetBSD
-
runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run tests
- uses: cross-platform-actions/action@v0.27.0
+ uses: cross-platform-actions/action@v0.29.0
with:
operating_system: netbsd
version: "10.1"
@@ -170,8 +172,7 @@ jobs:
sudo pkgin -y install \
bash \
oniguruma \
- pkgconf \
- tcl-expect
+ pkgconf
jobs=$(sysctl -n hw.ncpu)
./configure
.github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
@@ -183,11 +184,12 @@ jobs:
dragonflybsd:
name: DragonFly BSD
-
runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run tests
uses: vmactions/dragonflybsd-vm@v1
@@ -198,18 +200,17 @@ jobs:
prepare: |
pkg install -y \
bash \
- expect \
+ llvm18 \
oniguruma \
pkgconf \
- sudo \
- tcl-wrapper
+ sudo
pw useradd -n action -m -G wheel -s /usr/local/bin/bash
echo "%wheel ALL=(ALL) NOPASSWD: ALL" >>/usr/local/etc/sudoers
run: |
chown -R action:action .
jobs=$(sysctl -n hw.ncpu)
- sudo -u action ./configure
+ sudo -u action ./configure CC=clang18
sudo -u action .github/diag.sh make -j$jobs check TEST_FLAGS="--sudo --verbose=skipped"
- uses: actions/upload-artifact@v4
@@ -219,11 +220,12 @@ jobs:
omnios:
name: OmniOS
-
runs-on: ubuntu-24.04
+ if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
+
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Run tests
uses: vmactions/omnios-vm@v1
@@ -235,7 +237,6 @@ jobs:
pkg install \
bash \
build-essential \
- expect \
gnu-make \
onig \
sudo
diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml
index 4cce8ed..70a6ff8 100644
--- a/.github/workflows/codecov.yml
+++ b/.github/workflows/codecov.yml
@@ -7,13 +7,12 @@ jobs:
runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- name: Install dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
- expect \
gcc \
acl \
libacl1-dev \
@@ -25,7 +24,7 @@ jobs:
- name: Generate coverage
run: |
- ./configure --enable-gcov
+ ./configure --enable-gcov EXTRA_CFLAGS="-std=gnu2x"
make -j$(nproc) check TEST_FLAGS="--sudo"
gcov -abcfpu obj/*/*.o
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 1f2041c..2b4c80b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install dependencies
run: |
diff --git a/Makefile b/Makefile
index 8a7b270..5e6d25c 100644
--- a/Makefile
+++ b/Makefile
@@ -43,6 +43,7 @@ bfs: bin/bfs
BINS := \
bin/bfs \
bin/tests/mksock \
+ bin/tests/ptyx \
bin/tests/units \
bin/tests/xspawnee \
bin/tests/xtouch \
@@ -91,7 +92,7 @@ OBJS += obj/src/main.o
${BINS}:
@${MKDIR} ${@D}
- +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} ${.ALLSRC} ${_LDLIBS} -o $@
+ +${MSG} "[ LD ] $@" ${CC} ${_CFLAGS} ${_LDFLAGS} $^ ${_LDLIBS} -o $@
${POSTLINK}
# Get the .c file for a .o file
@@ -104,7 +105,7 @@ gen/version.i.new::
.SILENT: gen/version.i.new
gen/version.i: gen/version.i.new
- test -e $@ && cmp -s $@ ${.ALLSRC} && ${RM} ${.ALLSRC} || mv ${.ALLSRC} $@
+ test -e $@ && cmp -s $@ $^ && ${RM} $^ || mv $^ $@
.SILENT: gen/version.i
obj/src/version.o: gen/version.i
@@ -119,6 +120,7 @@ UTEST_BINS := \
# Integration test binaries
ITEST_BINS := \
bin/tests/mksock \
+ bin/tests/ptyx \
bin/tests/xtouch
# Build (but don't run) test binaries
@@ -179,6 +181,9 @@ integration-tests: ${INTEGRATION_TESTS}
bin/tests/mksock: obj/tests/mksock.o ${LIBBFS}
OBJS += obj/tests/mksock.o
+bin/tests/ptyx: obj/tests/ptyx.o ${LIBBFS}
+OBJS += obj/tests/ptyx.o
+
bin/tests/xtouch: obj/tests/xtouch.o ${LIBBFS}
OBJS += obj/tests/xtouch.o
diff --git a/bench/bench.sh b/bench/bench.sh
index f249ffc..c9ed978 100644
--- a/bench/bench.sh
+++ b/bench/bench.sh
@@ -22,6 +22,7 @@ PRINT_DEFAULT=(linux)
STRATEGIES_DEFAULT=(rust)
JOBS_DEFAULT=(rust)
EXEC_DEFAULT=(linux)
+SORTED_DEFAULT=(chromium)
usage() {
printf 'Usage: tailfin run %s\n' "${BASH_SOURCE[0]}"
@@ -60,6 +61,10 @@ usage() {
printf ' Process spawning benchmark.\n'
printf ' Default corpus is --exec=%s\n\n' "${EXEC_DEFAULT[*]}"
+ printf ' --sorted[=CORPUS]\n'
+ printf ' Sorted traversal benchmark.\n'
+ printf ' Default corpus is --sorted=%s\n\n' "${SORTED_DEFAULT[*]}"
+
printf ' --build=COMMIT\n'
printf ' Build this bfs commit and benchmark it. Specify multiple times to\n'
printf ' compare, e.g. --build=3.0.1 --build=3.0.2\n\n'
@@ -121,6 +126,7 @@ setup() {
STRATEGIES=()
JOBS=()
EXEC=()
+ SORTED=()
for arg; do
case "$arg" in
@@ -195,6 +201,12 @@ setup() {
--exec=*)
read -ra EXEC <<<"${arg#*=}"
;;
+ --sorted)
+ SORTED=("${SORTED_DEFAULT[@]}")
+ ;;
+ --sorted=*)
+ read -ra SORTED <<<"${arg#*=}"
+ ;;
--default)
COMPLETE=("${COMPLETE_DEFAULT[@]}")
EARLY_QUIT=("${EARLY_QUIT_DEFAULT[@]}")
@@ -203,6 +215,7 @@ setup() {
STRATEGIES=("${STRATEGIES_DEFAULT[@]}")
JOBS=("${JOBS_DEFAULT[@]}")
EXEC=("${EXEC_DEFAULT[@]}")
+ SORTED=("${SORTED_DEFAULT[@]}")
;;
--help)
usage
@@ -227,7 +240,7 @@ setup() {
as-user mkdir -p bench/corpus
declare -A cloned=()
- for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}"; do
+ for corpus in "${COMPLETE[@]}" "${EARLY_QUIT[@]}" "${STAT[@]}" "${PRINT[@]}" "${STRATEGIES[@]}" "${JOBS[@]}" "${EXEC[@]}" "${SORTED[@]}"; do
if ((cloned["$corpus"])); then
continue
fi
@@ -283,6 +296,7 @@ setup() {
export_array STRATEGIES
export_array JOBS
export_array EXEC
+ export_array SORTED
if ((UID == 0)); then
turbo-off
@@ -650,6 +664,29 @@ bench-exec() {
fi
}
+# Benchmark sorted traversal
+bench-sorted-corpus() {
+ subgroup '%s' "$1"
+
+ cmds=()
+ for bfs in "${BFS[@]}"; do
+ cmds+=("$bfs -s $2 -false")
+ done
+
+ do-hyperfine "${cmds[@]}"
+}
+
+# All sorted traversal benchmarks
+bench-sorted() {
+ if (($#)); then
+ group "Sorted traversal"
+
+ for corpus; do
+ bench-sorted-corpus "$corpus ${TAGS[$corpus]}" "bench/corpus/$corpus"
+ done
+ fi
+}
+
# Print benchmarked versions
bench-versions() {
subgroup "Versions"
@@ -698,6 +735,7 @@ bench() {
import_array STRATEGIES
import_array JOBS
import_array EXEC
+ import_array SORTED
bench-complete "${COMPLETE[@]}"
bench-early-quit "${EARLY_QUIT[@]}"
@@ -706,5 +744,6 @@ bench() {
bench-strategies "${STRATEGIES[@]}"
bench-jobs "${JOBS[@]}"
bench-exec "${EXEC[@]}"
+ bench-sorted "${SORTED[@]}"
bench-details
}
diff --git a/bench/ioq.c b/bench/ioq.c
index 5db585a..fb9edbc 100644
--- a/bench/ioq.c
+++ b/bench/ioq.c
@@ -17,6 +17,149 @@
#include <time.h>
#include <unistd.h>
+/** A latency sample. */
+struct lat {
+ /** The sampled latency. */
+ struct timespec time;
+ /** A random integer, for reservoir sampling. */
+ long key;
+};
+
+/** Number of latency samples to keep. */
+#define SAMPLES 1000
+/** Latency sampling period. */
+#define PERIOD 128
+
+/** Latency measurements. */
+struct lats {
+ /** Lowest observed latency. */
+ struct timespec min;
+ /** Highest observed latency. */
+ struct timespec max;
+ /** Total latency. */
+ struct timespec sum;
+ /** Number of measured requests. */
+ size_t count;
+
+ /** Priority queue for reservoir sampling. */
+ struct lat heap[SAMPLES];
+ /** Current size of the heap. */
+ size_t heap_size;
+};
+
+/** Initialize a latency reservoir. */
+static void lats_init(struct lats *lats) {
+ lats->min = (struct timespec) { .tv_sec = 1000 };
+ lats->max = (struct timespec) { 0 };
+ lats->sum = (struct timespec) { 0 };
+ lats->count = 0;
+ lats->heap_size = 0;
+}
+
+/** Binary heap parent. */
+static size_t heap_parent(size_t i) {
+ return (i - 1) / 2;
+}
+
+/** Binary heap left child. */
+static size_t heap_child(size_t i) {
+ return 2 * i + 1;
+}
+
+/** Binary heap smallest child. */
+static size_t heap_min_child(const struct lats *lats, size_t i) {
+ size_t j = heap_child(i);
+ size_t k = j + 1;
+ if (k < lats->heap_size && lats->heap[k].key < lats->heap[j].key) {
+ return k;
+ } else {
+ return j;
+ }
+}
+
+/** Check if the heap property is met. */
+static bool heap_check(const struct lat *parent, const struct lat *child) {
+ return parent->key <= child->key;
+}
+
+/** Reservoir sampling. */
+static void heap_push(struct lats *lats, const struct lat *lat) {
+ size_t i;
+
+ if (lats->heap_size < SAMPLES) {
+ // Heapify up
+ i = lats->heap_size++;
+ while (i > 0) {
+ size_t j = heap_parent(i);
+ if (heap_check(&lats->heap[j], lat)) {
+ break;
+ }
+ lats->heap[i] = lats->heap[j];
+ i = j;
+ }
+ } else if (lat->key > lats->heap[0].key) {
+ // Heapify down
+ i = 0;
+ while (true) {
+ size_t j = heap_min_child(lats, i);
+ if (j >= SAMPLES || heap_check(lat, &lats->heap[j])) {
+ break;
+ }
+ lats->heap[i] = lats->heap[j];
+ i = j;
+ }
+ } else {
+ // Reject
+ return;
+ }
+
+ lats->heap[i] = *lat;
+}
+
+/** Add a latency sample. */
+static void lats_push(struct lats *lats, const struct timespec *ts) {
+ timespec_min(&lats->min, ts);
+ timespec_max(&lats->max, ts);
+ timespec_add(&lats->sum, ts);
+ ++lats->count;
+
+ struct lat lat = {
+ .time = *ts,
+ .key = lrand48(),
+ };
+ heap_push(lats, &lat);
+}
+
+/** Merge two latency reservoirs. */
+static void lats_merge(struct lats *into, const struct lats *from) {
+ timespec_min(&into->min, &from->min);
+ timespec_max(&into->max, &from->max);
+ timespec_add(&into->sum, &from->sum);
+ into->count += from->count;
+
+ for (size_t i = 0; i < from->heap_size; ++i) {
+ heap_push(into, &from->heap[i]);
+ }
+}
+
+/** Latency qsort() comparator. */
+static int lat_cmp(const void *a, const void *b) {
+ const struct lat *la = a;
+ const struct lat *lb = b;
+ return timespec_cmp(&la->time, &lb->time);
+}
+
+/** Sort the latency reservoir. */
+static void lats_sort(struct lats *lats) {
+ qsort(lats->heap, lats->heap_size, sizeof(lats->heap[0]), lat_cmp);
+}
+
+/** Get the nth percentile. */
+static const struct timespec *lats_percentile(const struct lats *lats, int percent) {
+ size_t i = lats->heap_size * percent / 100;
+ return &lats->heap[i].time;
+}
+
/** Which clock to use for benchmarking. */
static clockid_t clockid = CLOCK_REALTIME;
@@ -38,86 +181,71 @@ struct times {
/** Total requests finished. */
size_t popped;
- /** Number of timed requests (latency). */
- size_t timed_reqs;
/** The start time for the currently tracked request. */
struct timespec req_start;
/** Whether a timed request is currently in flight. */
bool timing;
/** Latency measurements. */
- struct {
- struct timespec min;
- struct timespec max;
- struct timespec sum;
- } latency;
+ struct lats lats;
};
/** Initialize a timer. */
static void times_init(struct times *times) {
- *times = (struct times) {
- .latency = {
- .min = { .tv_sec = 1000 },
- },
- };
gettime(&times->start);
-}
-
-/** Start timing a single request. */
-static void start_request(struct times *times) {
- gettime(&times->req_start);
- times->timing = true;
+ times->pushed = 0;
+ times->popped = 0;
+ bfs_assert(!times->timing);
+ lats_init(&times->lats);
}
/** Finish timing a request. */
-static void finish_request(struct times *times) {
+static void track_latency(struct times *times) {
struct timespec elapsed;
gettime(&elapsed);
timespec_sub(&elapsed, &times->req_start);
-
- timespec_min(&times->latency.min, &elapsed);
- timespec_max(&times->latency.max, &elapsed);
- timespec_add(&times->latency.sum, &elapsed);
+ lats_push(&times->lats, &elapsed);
bfs_assert(times->timing);
times->timing = false;
- ++times->timed_reqs;
}
/** Add times to the totals, and reset the lap times. */
static void times_lap(struct times *total, struct times *lap) {
total->pushed += lap->pushed;
total->popped += lap->popped;
- total->timed_reqs += lap->timed_reqs;
-
- timespec_min(&total->latency.min, &lap->latency.min);
- timespec_max(&total->latency.max, &lap->latency.max);
- timespec_add(&total->latency.sum, &lap->latency.sum);
+ lats_merge(&total->lats, &lap->lats);
times_init(lap);
}
/** Print some times. */
-static void times_print(const struct times *times, long seconds) {
+static void times_print(struct times *times, long seconds) {
struct timespec elapsed;
gettime(&elapsed);
timespec_sub(&elapsed, &times->start);
double fsec = timespec_ns(&elapsed) / 1.0e9;
- double iops = times->popped / fsec;
- double mean = timespec_ns(&times->latency.sum) / times->timed_reqs;
- double min = timespec_ns(&times->latency.min);
- double max = timespec_ns(&times->latency.max);
if (seconds > 0) {
- printf("%9ld", seconds);
+ printf("%5ld", seconds);
} else if (elapsed.tv_nsec >= 10 * 1000 * 1000) {
- printf("%9.2f", fsec);
+ printf("%5.2f", fsec);
} else {
- printf("%9.0f", fsec);
+ printf("%5.0f", fsec);
}
- printf(" │ %'17.0f │ %'15.0f ∈ [%'6.0f .. %'7.0f]\n", iops, mean, min, max);
+ double iops = times->popped / fsec;
+ double mean = timespec_ns(&times->lats.sum) / times->lats.count;
+ double min = timespec_ns(&times->lats.min);
+ double max = timespec_ns(&times->lats.max);
+
+ lats_sort(&times->lats);
+ double n50 = timespec_ns(lats_percentile(&times->lats, 50));
+ double n90 = timespec_ns(lats_percentile(&times->lats, 90));
+ double n99 = timespec_ns(lats_percentile(&times->lats, 99));
+
+ printf(" │ %'12.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f │ %'7.0f\n", iops, mean, min, n50, n90, n99, max);
fflush(stdout);
}
@@ -126,9 +254,9 @@ static bool push(struct ioq *ioq, enum ioq_nop_type type, struct times *lap) {
void *ptr = NULL;
// Track latency for a small fraction of requests
- if (!lap->timing && (lap->pushed + 1) % 128 == 0) {
- start_request(lap);
+ if (!lap->timing && (lap->pushed + 1) % PERIOD == 0) {
ptr = lap;
+ gettime(&lap->req_start);
}
int ret = ioq_nop(ioq, type, ptr);
@@ -138,6 +266,9 @@ static bool push(struct ioq *ioq, enum ioq_nop_type type, struct times *lap) {
}
++lap->pushed;
+ if (ptr) {
+ lap->timing = true;
+ }
return true;
}
@@ -149,7 +280,7 @@ static bool pop(struct ioq *ioq, struct times *lap, bool block) {
}
if (ent->ptr) {
- finish_request(lap);
+ track_latency(lap);
}
ioq_free(ioq, ent);
@@ -177,9 +308,9 @@ int main(int argc, char *argv[]) {
setlocale(LC_ALL, "");
// -d: queue depth
- long depth = 4096;
+ unsigned int depth = 4096;
// -j: threads
- long threads = 0;
+ unsigned int threads = 0;
// -t: timeout
double timeout = 5.0;
// -L|-H: ioq_nop() type
@@ -190,13 +321,13 @@ int main(int argc, char *argv[]) {
while (c = getopt(argc, argv, ":d:j:t:LH"), c != -1) {
switch (c) {
case 'd':
- if (xstrtol(optarg, NULL, 10, &depth) != 0) {
+ if (xstrtoui(optarg, NULL, 10, &depth) != 0) {
fprintf(stderr, "%s: Bad depth '%s': %s\n", cmd, optarg, errstr());
return EXIT_FAILURE;
}
break;
case 'j':
- if (xstrtol(optarg, NULL, 10, &threads) != 0) {
+ if (xstrtoui(optarg, NULL, 10, &threads) != 0) {
fprintf(stderr, "%s: Bad thread count '%s': %s\n", cmd, optarg, errstr());
return EXIT_FAILURE;
}
@@ -222,7 +353,7 @@ int main(int argc, char *argv[]) {
}
}
- if (threads <= 0) {
+ if (!threads) {
threads = nproc();
if (threads > 8) {
threads = 8;
@@ -238,8 +369,8 @@ int main(int argc, char *argv[]) {
printf("I/O queue benchmark (%s)\n\n", bfs_version);
- printf("[-d] depth: %ld\n", depth);
- printf("[-j] threads: %ld (including main)\n", threads + 1);
+ printf("[-d] depth: %u\n", depth);
+ printf("[-j] threads: %u (including main)\n", threads + 1);
if (type == IOQ_NOP_HEAVY) {
printf("[-H] type: heavy (with syscalls)\n");
} else {
@@ -247,12 +378,13 @@ int main(int argc, char *argv[]) {
}
printf("\n");
- printf(" Time (s) │ Throughput (IO/s) │ Latency (ns/IO)\n");
- printf("══════════╪═══════════════════╪═════════════════\n");
+ printf(" Time │ Throughput │ Latency │ min │ 50%% │ 90%% │ 99%% │ max\n");
+ printf(" (s) │ (IO/s) │ (ns/IO) │ │ │ │ │\n");
+ printf("══════╪══════════════╪═════════╪═════════╪═════════╪═════════╪═════════╪═════════\n");
fflush(stdout);
struct ioq *ioq = ioq_create(depth, threads);
- bfs_everify(ioq, "ioq_create(%ld, %ld)", depth, threads);
+ bfs_everify(ioq, "ioq_create(%u, %u)", depth, threads);
// Pre-allocate all the requests
while (ioq_capacity(ioq) > 0) {
@@ -311,9 +443,9 @@ int main(int argc, char *argv[]) {
times_lap(&total, &lap);
if (load(&quit, relaxed)) {
- printf("\r────^C────┼───────────────────┼─────────────────\n");
+ printf("\r──^C──┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n");
} else {
- printf("──────────┼───────────────────┼─────────────────\n");
+ printf("──────┼──────────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────\n");
}
times_print(&total, 0);
diff --git a/build/auto.mk b/build/auto.mk
new file mode 100644
index 0000000..9cdf9e3
--- /dev/null
+++ b/build/auto.mk
@@ -0,0 +1,54 @@
+# Copyright © Tavian Barnes <tavianator@tavianator.com>
+# SPDX-License-Identifier: 0BSD
+
+# Makefile that generates gen/auto.mk
+
+include build/prelude.mk
+include gen/vars.mk
+include gen/early.mk
+include gen/late.mk
+include build/exports.mk
+
+# Auto-detected flags
+AUTO_FLAGS := \
+ gen/flags/std.mk \
+ gen/flags/bind-now.mk \
+ gen/flags/deps.mk \
+ gen/flags/pthread.mk \
+ gen/flags/Wformat.mk \
+ gen/flags/Wimplicit-fallthrough.mk \
+ gen/flags/Wimplicit.mk \
+ gen/flags/Wmissing-decls.mk \
+ gen/flags/Wmissing-var-decls.mk \
+ gen/flags/Wshadow.mk \
+ gen/flags/Wsign-compare.mk \
+ gen/flags/Wstrict-prototypes.mk \
+ gen/flags/Wundef-prefix.mk
+
+gen/auto.mk: ${AUTO_FLAGS}
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @cat $^ >>$@
+ @cat ${^:%=%.log} >gen/flags.log
+ ${VCAT} $@
+.PHONY: gen/auto.mk
+
+# Check that the C compiler works at all
+cc::
+ @build/cc.sh -q build/empty.c -o gen/.cc.out; \
+ ret=$$?; \
+ build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \
+ exit $$ret
+
+# The short name of the config test
+SLUG = ${@:gen/%.mk=%}
+# The source file to build
+CSRC = build/${SLUG}.c
+# The hidden output file name
+OUT = ${SLUG:flags/%=gen/flags/.%.out}
+
+${AUTO_FLAGS}: cc
+ @${MKDIR} ${@D}
+ @build/flags-if.sh ${CSRC} -o ${OUT} >$@ 2>$@.log; \
+ build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
+.PHONY: ${AUTO_FLAGS}
diff --git a/build/cc.sh b/build/cc.sh
index e1d2b0b..b6f38b5 100755
--- a/build/cc.sh
+++ b/build/cc.sh
@@ -9,26 +9,11 @@
set -eu
-QUIET=
+# Without -q, print the executed command for config.log
if [ "$1" = "-q" ]; then
- QUIET=y
shift
-fi
-
-# Source files can specify their own flags with lines like
-#
-# /// _CFLAGS += -Wmissing-variable-declarations
-#
-# which will be added to the makefile on success, or lines like
-#
-# /// -Werror
-#
-# which are just used for the current file.
-EXTRA_FLAGS=$(sed -n '\|^///|{s|^/// ||; s|[^=]*= ||; p;}' "$1")
-
-# Without -q, print the executed command for config.log
-if [ -z "$QUIET" ]; then
+else
set -x
fi
-$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $EXTRA_FLAGS $XLDLIBS
+$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS "$@" $XLDLIBS
diff --git a/build/config.mk b/build/config.mk
index 6296168..be57678 100644
--- a/build/config.mk
+++ b/build/config.mk
@@ -7,21 +7,23 @@ include build/prelude.mk
include build/exports.mk
# All configuration steps
-config: gen/config.mk
+config: gen/config.mk gen/config.h
.PHONY: config
-# Makefile fragments generated by `./configure`
+# All the generated makefile fragments
MKS := \
gen/vars.mk \
- gen/flags.mk \
- gen/pkgs.mk
+ gen/early.mk \
+ gen/auto.mk \
+ gen/pkgs.mk \
+ gen/late.mk
# The main configuration file, which includes the others
-gen/config.mk: ${MKS} gen/config.h
+gen/config.mk: ${MKS}
${MSG} "[ GEN] $@"
@printf '# %s\n' "$@" >$@
- @printf 'include %s\n' ${MKS} >>$@
- ${VCAT} gen/config.mk
+ @printf 'include %s\n' $^ >>$@
+ ${VCAT} $@
.PHONY: gen/config.mk
# Saves the configurable variables
@@ -42,16 +44,17 @@ gen/vars.mk::
# Sets the build flags. This depends on vars.mk and uses a recursive make so
# that the default flags can depend on variables like ${OS}.
-gen/flags.mk: gen/vars.mk
+gen/early.mk gen/late.mk: gen/vars.mk
@+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/flags.mk $@
-.PHONY: gen/flags.mk
+
+# Auto-detected build flags
+gen/auto.mk: gen/early.mk gen/late.mk
+ @+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/auto.mk $@
# Auto-detect dependencies and their build flags
-gen/pkgs.mk: gen/flags.mk
+gen/pkgs.mk: gen/auto.mk
@+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/pkgs.mk $@
-.PHONY: gen/pkgs.mk
# Compile-time feature detection
gen/config.h: gen/pkgs.mk
@+XMAKEFLAGS="$$MAKEFLAGS" ${MAKE} -sf build/header.mk $@
-.PHONY: gen/config.h
diff --git a/build/exports.mk b/build/exports.mk
index 913a1aa..a616c59 100644
--- a/build/exports.mk
+++ b/build/exports.mk
@@ -17,4 +17,9 @@ export XCFLAGS=${_CFLAGS}
export XLDFLAGS=${_LDFLAGS}
export XLDLIBS=${_LDLIBS}
+export CONF_CPPFLAGS=${CPPFLAGS} ${EXTRA_CPPFLAGS}
+export CONF_CFLAGS=${CFLAGS} ${EXTRA_CFLAGS}
+export CONF_LDFLAGS=${LDFLAGS} ${EXTRA_LDFLAGS}
+export CONF_LDLIBS=${LDLIBS} ${EXTRA_LDLIBS}
+
export XNOLIBS=${NOLIBS}
diff --git a/build/flags-if.sh b/build/flags-if.sh
index 81eb345..f7b4f33 100755
--- a/build/flags-if.sh
+++ b/build/flags-if.sh
@@ -3,26 +3,87 @@
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
-# Add flags to a makefile if a build succeeds
+# Add flags to a makefile if a build succeeds. Source files can specify their
+# own flags with lines like
+#
+# /// _CFLAGS += -Wmissing-variable-declarations
+#
+# which will be added to the makefile on success, or lines like
+#
+# /// -Werror
+#
+# which are just used for the current file. Lines like
+#
+# /// ---
+#
+# separate groups of flags, to try multiple ways to achieve something, e.g.
+#
+# /// CFLAGS += -pthread
+# /// ---
+# /// LDLIBS += -lpthread
set -eu
-build/cc.sh "$@" || exit 1
+# Any new flags we're using
+FLAGS=""
+# Any new makefile lines we're printing
+OUTPUT=""
-# If the build succeeded, print any lines like
-#
-# /// _CFLAGS += -foo
-#
-# (unless they're already set)
-OLD_FLAGS="$XCC $XCPPFLAGS $XCFLAGS $XLDFLAGS $XLDLIBS"
+# Check the existing flags so we don't add duplicates
+OLD_FLAGS=" $XCPPFLAGS $XCFLAGS $XLDFLAGS $XLDLIBS "
+
+add_flag() {
+ case "$1" in
+ -std=*)
+ # Don't overwrite -std=* flags
+ case "$OLD_FLAGS" in
+ *\ -std=*)
+ return 1
+ ;;
+ esac
+ ;;
+ *)
+ if [ "${OLD_FLAGS#* $1 }" != "$OLD_FLAGS" ]; then
+ return 1
+ fi
+ ;;
+ esac
+
+ FLAGS="${FLAGS}${FLAGS:+ }$1"
+}
+
+try_cc() {
+ build/cc.sh "$@" $FLAGS || return $?
+ printf '%s' "$OUTPUT"
+ exit
+}
while IFS="" read -r line; do
case "$line" in
- ///*=*)
- flag="${line#*= }"
- if [ "${OLD_FLAGS#*"$flag"}" = "$OLD_FLAGS" ]; then
- printf '%s\n' "${line#/// }"
+ "/// "*)
+ line="${line#/// }"
+ ;;
+ *)
+ continue
+ ;;
+ esac
+
+ case "$line" in
+ ---)
+ try_cc "$@" || :
+ FLAGS=
+ OUTPUT=
+ ;;
+ *=*)
+ if add_flag "${line#*= }"; then
+ OUTPUT="${OUTPUT}${line}
+"
fi
;;
+ *)
+ add_flag "$line" || :
+ ;;
esac
done <"$1"
+
+try_cc "$@"
diff --git a/build/flags.mk b/build/flags.mk
index fbdf8cf..c64a80b 100644
--- a/build/flags.mk
+++ b/build/flags.mk
@@ -1,14 +1,14 @@
# Copyright © Tavian Barnes <tavianator@tavianator.com>
# SPDX-License-Identifier: 0BSD
-# Makefile that generates gen/flags.mk
+# Makefile that generates gen/{early,late}.mk
include build/prelude.mk
include gen/vars.mk
# Internal flags
_CPPFLAGS := -Isrc -Igen -include src/prelude.h
-_CFLAGS := -std=c17
+_CFLAGS :=
_LDFLAGS :=
_LDLIBS :=
@@ -26,6 +26,7 @@ _ASAN := ${TRUTHY,${ASAN}}
_LSAN := ${TRUTHY,${LSAN}}
_MSAN := ${TRUTHY,${MSAN}}
_TSAN := ${TRUTHY,${TSAN}}
+_TYSAN := ${TRUTHY,${TYSAN}}
_UBSAN := ${TRUTHY,${UBSAN}}
_GCOV := ${TRUTHY,${GCOV}}
_LINT := ${TRUTHY,${LINT}}
@@ -38,25 +39,26 @@ ASAN_CFLAGS,y := -fsanitize=address
LSAN_CFLAGS,y := -fsanitize=leak
MSAN_CFLAGS,y := -fsanitize=memory -fsanitize-memory-track-origins
TSAN_CFLAGS,y := -fsanitize=thread
+TYSAN_CFLAGS,y := -fsanitize=type
UBSAN_CFLAGS,y := -fsanitize=undefined
_CFLAGS += ${ASAN_CFLAGS,${_ASAN}}
_CFLAGS += ${LSAN_CFLAGS,${_LSAN}}
_CFLAGS += ${MSAN_CFLAGS,${_MSAN}}
_CFLAGS += ${TSAN_CFLAGS,${_TSAN}}
+_CFLAGS += ${TYSAN_CFLAGS,${_TYSAN}}
_CFLAGS += ${UBSAN_CFLAGS,${_UBSAN}}
SAN_CFLAGS,y := -fno-sanitize-recover=all
-INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_UBSAN}}
+INSANE := ${NOT,${_ASAN}${_LSAN}${_MSAN}${_TSAN}${_TYSAN}${_UBSAN}}
SAN := ${NOT,${INSANE}}
_CFLAGS += ${SAN_CFLAGS,${SAN}}
-# MSAN and TSAN both need all code to be instrumented
-YESLIBS := ${NOT,${_MSAN}${_TSAN}}
+# MSan, TSan, and TySan need all code to be instrumented
+YESLIBS := ${NOT,${_MSAN}${_TSAN}${_TYSAN}}
NOLIBS ?= ${NOT,${YESLIBS}}
-# gcov only intercepts fork()/exec() with -std=gnu*
-GCOV_CFLAGS,y := -std=gnu17 --coverage
+GCOV_CFLAGS,y := --coverage
_CFLAGS += ${GCOV_CFLAGS,${_GCOV}}
LINT_CPPFLAGS,y := -D_FORTIFY_SOURCE=3 -DBFS_LINT
@@ -77,31 +79,10 @@ _CFLAGS += ${LTO_CFLAGS,${_LTO}}
# Configurable flags
CFLAGS ?= -g -Wall
-# Add the configurable flags last so they can override ours
-_CPPFLAGS += ${CPPFLAGS} ${EXTRA_CPPFLAGS}
-_CFLAGS += ${CFLAGS} ${EXTRA_CFLAGS}
-_LDFLAGS += ${LDFLAGS} ${EXTRA_LDFLAGS}
-# (except LDLIBS, as earlier libs override later ones)
-_LDLIBS := ${LDLIBS} ${EXTRA_LDLIBS} ${_LDLIBS}
-
include build/exports.mk
-# Conditionally-supported flags
-AUTO_FLAGS := \
- gen/flags/Wformat.mk \
- gen/flags/Wimplicit-fallthrough.mk \
- gen/flags/Wimplicit.mk \
- gen/flags/Wmissing-decls.mk \
- gen/flags/Wmissing-var-decls.mk \
- gen/flags/Wshadow.mk \
- gen/flags/Wsign-compare.mk \
- gen/flags/Wstrict-prototypes.mk \
- gen/flags/Wundef-prefix.mk \
- gen/flags/bind-now.mk \
- gen/flags/deps.mk \
- gen/flags/pthread.mk
-
-gen/flags.mk: ${AUTO_FLAGS}
+# Saves the internal flags
+gen/early.mk::
${MSG} "[ GEN] $@"
@printf '# %s\n' "$@" >$@
@printf '_CPPFLAGS := %s\n' "$$XCPPFLAGS" >>$@
@@ -110,27 +91,14 @@ gen/flags.mk: ${AUTO_FLAGS}
@printf '_LDLIBS := %s\n' "$$XLDLIBS" >>$@
@printf 'NOLIBS := %s\n' "$$XNOLIBS" >>$@
@test "${OS}-${SAN}" != FreeBSD-y || printf 'POSTLINK = elfctl -e +noaslr $$@\n' >>$@
- @cat ${.ALLSRC} >>$@
- @cat ${.ALLSRC:%=%.log} >gen/flags.log
${VCAT} $@
-.PHONY: gen/flags.mk
-
-# Check that the C compiler works at all
-cc::
- @build/cc.sh -q build/empty.c -o gen/.cc.out; \
- ret=$$?; \
- build/msg-if.sh "[ CC ] build/empty.c" test $$ret -eq 0; \
- exit $$ret
-
-# The short name of the config test
-SLUG = ${@:gen/%.mk=%}
-# The source file to build
-CSRC = build/${SLUG}.c
-# The hidden output file name
-OUT = ${SLUG:flags/%=gen/flags/.%.out}
-
-${AUTO_FLAGS}: cc
- @${MKDIR} ${@D}
- @build/flags-if.sh ${CSRC} -o ${OUT} >$@ 2>$@.log; \
- build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
-.PHONY: ${AUTO_FLAGS}
+
+# Save explicit flags from ./configure separately so they can override the rest
+gen/late.mk::
+ ${MSG} "[ GEN] $@"
+ @printf '# %s\n' "$@" >$@
+ @printf '_CPPFLAGS += %s\n' "$$CONF_CPPFLAGS" >>$@
+ @printf '_CFLAGS += %s\n' "$$CONF_CFLAGS" >>$@
+ @printf '_LDFLAGS += %s\n' "$$CONF_LDFLAGS" >>$@
+ @printf '_LDLIBS := %s $${_LDLIBS}\n' "$$CONF_LDLIBS" >>$@
+ ${VCAT} $@
diff --git a/build/flags/std.c b/build/flags/std.c
new file mode 100644
index 0000000..6030d1f
--- /dev/null
+++ b/build/flags/std.c
@@ -0,0 +1,12 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/// _CFLAGS += -std=c23
+/// ---
+/// _CFLAGS += -std=c2x
+/// ---
+/// _CFLAGS += -std=c17
+
+int main(void) {
+ return 0;
+}
diff --git a/build/has/tcsetwinsize.c b/build/has/tcsetwinsize.c
new file mode 100644
index 0000000..6717415
--- /dev/null
+++ b/build/has/tcsetwinsize.c
@@ -0,0 +1,9 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+#include <termios.h>
+
+int main(void) {
+ const struct winsize ws = {0};
+ return tcsetwinsize(0, &ws);
+}
diff --git a/build/header.mk b/build/header.mk
index 0e3af14..ae52c12 100644
--- a/build/header.mk
+++ b/build/header.mk
@@ -5,8 +5,10 @@
include build/prelude.mk
include gen/vars.mk
-include gen/flags.mk
+include gen/early.mk
+include gen/auto.mk
include gen/pkgs.mk
+include gen/late.mk
include build/exports.mk
# All header fragments we generate
@@ -56,6 +58,7 @@ HEADERS := \
gen/has/string-to-flags.h \
gen/has/strtofflags.h \
gen/has/tcgetwinsize.h \
+ gen/has/tcsetwinsize.h \
gen/has/timegm.h \
gen/has/timer-create.h \
gen/has/tm-gmtoff.h \
@@ -69,9 +72,9 @@ gen/config.h: ${PKG_HEADERS} ${HEADERS}
@printf '// %s\n' "$@" >$@
@printf '#ifndef BFS_CONFIG_H\n' >>$@
@printf '#define BFS_CONFIG_H\n' >>$@
- @cat ${.ALLSRC} >>$@
+ @cat $^ >>$@
@printf '#endif // BFS_CONFIG_H\n' >>$@
- @cat gen/flags.log ${.ALLSRC:%=%.log} >gen/config.log
+ @cat gen/flags.log ${^:%=%.log} >gen/config.log
${VCAT} $@
@printf '%s' "$$CONFFLAGS" | build/embed.sh >gen/confflags.i
@printf '%s' "$$XCC" | build/embed.sh >gen/cc.i
@@ -88,5 +91,5 @@ OUT = ${SLUG:has/%=gen/has/.%.out}
${HEADERS}::
@${MKDIR} ${@D}
- @build/define-if.sh ${SLUG} build/cc.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \
+ @build/define-if.sh ${SLUG} build/flags-if.sh build/${SLUG}.c -o ${OUT} >$@ 2>$@.log; \
build/msg-if.sh "[ CC ] ${SLUG}.c" test $$? -eq 0
diff --git a/build/pkgs.mk b/build/pkgs.mk
index 5de9ac2..bed17be 100644
--- a/build/pkgs.mk
+++ b/build/pkgs.mk
@@ -5,7 +5,9 @@
include build/prelude.mk
include gen/vars.mk
-include gen/flags.mk
+include gen/early.mk
+include gen/auto.mk
+include gen/late.mk
include build/exports.mk
HEADERS := ${ALL_PKGS:%=gen/with/%.h}
@@ -14,12 +16,11 @@ gen/pkgs.mk: ${HEADERS}
${MSG} "[ GEN] $@"
@printf '# %s\n' "$@" >$@
@gen() { \
- printf 'PKGS := %s\n' "$$*"; \
printf '_CFLAGS += %s\n' "$$(build/pkgconf.sh --cflags "$$@")"; \
printf '_LDFLAGS += %s\n' "$$(build/pkgconf.sh --ldflags "$$@")"; \
printf '_LDLIBS := %s $${_LDLIBS}\n' "$$(build/pkgconf.sh --ldlibs "$$@")"; \
}; \
- gen $$(grep -l ' true$$' ${.ALLSRC} | sed 's|.*/\(.*\)\.h|\1|') >>$@
+ gen $$(grep -l ' true$$' $^ | sed 's|.*/\(.*\)\.h|\1|') >>$@
${VCAT} $@
.PHONY: gen/pkgs.mk
diff --git a/build/prelude.mk b/build/prelude.mk
index c25dea4..6250d73 100644
--- a/build/prelude.mk
+++ b/build/prelude.mk
@@ -9,11 +9,9 @@
# We don't use any suffix rules
.SUFFIXES:
-# GNU make has $^ for the full list of targets, while BSD make has $> and the
-# long-form ${.ALLSRC}. We could write $^ $> to get them both, but that would
-# break if one of them implemented support for the other. So instead, bring
-# BSD's ${.ALLSRC} to GNU.
-.ALLSRC ?= $^
+# GNU make has $^ for the full list of targets, while BSD make has $> (and the
+# long-form ${.ALLSRC}). We use the GNU version, bringing it to BSD like this:
+^ ?= $>
# Installation paths
DESTDIR ?=
diff --git a/build/version.sh b/build/version.sh
index 8f89921..734d897 100755
--- a/build/version.sh
+++ b/build/version.sh
@@ -14,5 +14,5 @@ if [ "${VERSION-}" ]; then
elif [ -e "$DIR/.git" ] && command -v git >/dev/null 2>&1; then
git -C "$DIR" describe --always --dirty
else
- echo "4.0.6"
+ echo "4.1"
fi
diff --git a/configure b/configure
index f6818c3..51b543e 100755
--- a/configure
+++ b/configure
@@ -16,7 +16,7 @@ help() {
Usage:
\$ $0 [--enable-*|--disable-*] [--with-*|--without-*] [CC=...] [...]
- \$ $MAKE -j$(nproc)
+ \$ $MAKE -j$(_nproc)
Variables set in the environment or on the command line will be picked up:
@@ -40,7 +40,7 @@ The default flags result in a plain debug build. Other build profiles include:
--enable-release
Enable optimizations, disable assertions
- --enable-{asan,lsan,msan,tsan,ubsan}
+ --enable-{asan,lsan,msan,tsan,tysan,ubsan}
Enable sanitizers
--enable-gcov
Enable code coverage instrumentation
@@ -66,7 +66,7 @@ Packaging:
This script is a thin wrapper around a makefile-based configuration system.
Any other arguments will be passed directly to the $MAKE invocation, e.g.
- \$ $0 -j$(nproc) V=1
+ \$ $0 -j$(_nproc) V=1
EOF
}
@@ -85,9 +85,9 @@ invalid() {
}
# Get the number of cores to use
-nproc() {
+_nproc() {
{
- command nproc \
+ nproc \
|| sysctl -n hw.ncpu \
|| getconf _NPROCESSORS_ONLN \
|| echo 1
@@ -164,7 +164,7 @@ for arg; do
--enable-*|--disable-*)
case "$name" in
- release|lto|asan|lsan|msan|tsan|ubsan|lint|gcov)
+ release|lto|asan|lsan|msan|tsan|tysan|ubsan|lint|gcov)
set -- "$@" "$NAME=$yn"
;;
*)
@@ -197,7 +197,7 @@ for arg; do
;;
# Warn about MAKE variables that have documented configure flags
- RELEASE=*|LTO=*|ASAN=*|LSAN=*|MSAN=*|TSAN=*|UBSAN=*|LINT=*|GCOV=*)
+ RELEASE=*|LTO=*|ASAN=*|LSAN=*|MSAN=*|TSAN=*|TYSAN=*|UBSAN=*|LINT=*|GCOV=*)
name=$(printf '%s' "$NAME" | tr 'A-Z_' 'a-z-')
warn '"%s" is deprecated; use --enable-%s' "$arg" "$name"
set -- "$@" "$arg"
@@ -227,11 +227,11 @@ for arg; do
done
# Set up symbolic links for out-of-tree builds
-for f in Makefile build completions docs src tests; do
+for f in Makefile bench build completions docs src tests; do
test -e "$f" || ln -s "$DIR/$f" "$f"
done
-# Set MAKEFLAGS to -j$(nproc) if it's unset
-export MAKEFLAGS="${MAKEFLAGS--j$(nproc)}"
+# Set MAKEFLAGS to -j$(_nproc) if it's unset
+export MAKEFLAGS="${MAKEFLAGS--j$(_nproc)}"
$MAKE -rf build/config.mk "$@"
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 3e72baf..80511f3 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,6 +1,68 @@
4.*
===
+4.1
+---
+
+**August 11, 2025**
+
+
+### New features
+
+- Added support for the `$LSCOLORS` variable used by FreeBSD and macOS
+ ([#54](https://github.com/tavianator/bfs/issues/54),
+ [#157](https://github.com/tavianator/bfs/pull/157))
+
+### Changes
+
+- Switched from C17 to [C23](https://en.cppreference.com/w/c/23) by default.
+ C17 is still supported as a fallback as long as your compiler supports some common extensions.
+
+
+4.0.8
+-----
+
+**June 20, 2025**
+
+### Bug fixes
+
+- Fixed an invalid optimization that transformed
+
+ $ bfs -user you -or -user me
+
+ into just
+
+ $ bfs -user you
+
+ The bug was originally introduced in bfs 2.0 (October 14, 2020).
+ ([#155](https://github.com/tavianator/bfs/issues/155))
+
+
+4.0.7
+-----
+
+**June 15, 2025**
+
+### Changes
+
+- `bfs` now takes CPU affinity into account when picking how many threads to use
+ ([`a36774b`](https://github.com/tavianator/bfs/commit/a36774be636c3429c6e73de33bf65a1bdbdcfb4b))
+
+- `-execdir /bin/...` is now allowed even with a relative path in `$PATH`
+ ([`cb40f51`](https://github.com/tavianator/bfs/commit/cb40f51e4e6375a10265484b6959c6b1b0591378))
+
+- *Expect* is no longer a test suite dependency
+ ([`7102fec`](https://github.com/tavianator/bfs/commit/7102fec257835302cb4978160bba4cbebd0b63e1))
+
+### Bug fixes
+
+- Only the last `-files0-from` argument now has any effect, to match GNU find
+ ([`a662fda`](https://github.com/tavianator/bfs/commit/a662fda2642e17478bc8e78adb4c6642a8505cdb))
+
+- Fixed `-execdir {}`, which was inadvertently broken in bfs 4.0
+ ([`def4a83`](https://github.com/tavianator/bfs/commit/def4a832425bfe94b96b8cb1146a83552b754fb4))
+
+
4.0.6
-----
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 099157d..e411fde 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -17,7 +17,8 @@ This enables machine processing of license information based on the SPDX License
Implementation
--------------
-`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)), specifically [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision)).
+`bfs` is written in [C](https://en.wikipedia.org/wiki/C_(programming_language)).
+The build defaults to [C23](https://en.wikipedia.org/wiki/C23_(C_standard_revision)), but it also supports [C17](https://en.wikipedia.org/wiki/C17_(C_standard_revision)) as long as some popular C23 features are available as extensions.
You can get a feel for the coding style by skimming the source code.
[`main.c`](/src/main.c) contains an overview of the rest of source files.
A quick summary:
diff --git a/docs/bfs.1 b/docs/bfs.1
index f652106..daf4085 100644
--- a/docs/bfs.1
+++ b/docs/bfs.1
@@ -1,6 +1,6 @@
.\" Copyright © Tavian Barnes <tavianator@tavianator.com>
.\" SPDX-License-Identifier: 0BSD
-.TH BFS 1 2025-02-26 "bfs 4.0.6"
+.TH BFS 1 2025-08-11 "bfs 4.1"
.SH NAME
bfs \- breadth-first search for your files
.SH SYNOPSIS
diff --git a/src/alloc.c b/src/alloc.c
index f505eda..3cf9026 100644
--- a/src/alloc.c
+++ b/src/alloc.c
@@ -173,7 +173,7 @@ void arena_init(struct arena *arena, size_t align, size_t size) {
}
/** Allocate a new slab. */
-_cold
+[[_cold]]
static int slab_alloc(struct arena *arena) {
// Make the initial allocation size ~4K
size_t size = 4096;
diff --git a/src/alloc.h b/src/alloc.h
index 1fafbab..4f21ed0 100644
--- a/src/alloc.h
+++ b/src/alloc.h
@@ -131,8 +131,8 @@ static inline size_t flex_size(size_t align, size_t offset, size_t size, size_t
* @return
* The allocated memory, or NULL on failure.
*/
-_malloc(free, 1)
-_aligned_alloc(1, 2)
+[[_malloc(free, 1)]]
+[[_aligned_alloc(1, 2)]]
void *alloc(size_t align, size_t size);
/**
@@ -145,8 +145,8 @@ void *alloc(size_t align, size_t size);
* @return
* The allocated memory, or NULL on failure.
*/
-_malloc(free, 1)
-_aligned_alloc(1, 2)
+[[_malloc(free, 1)]]
+[[_aligned_alloc(1, 2)]]
void *zalloc(size_t align, size_t size);
/** Allocate memory for the given type. */
@@ -187,8 +187,8 @@ void *zalloc(size_t align, size_t size);
* @return
* The reallocated memory, or NULL on failure.
*/
-_aligned_alloc(2, 4)
-_nodiscard
+[[_nodiscard]]
+[[_aligned_alloc(2, 4)]]
void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size);
/** Reallocate memory for an array. */
@@ -214,7 +214,7 @@ void *xrealloc(void *ptr, size_t align, size_t old_size, size_t new_size);
* for (count + 1) elements. On failure, errno will be non-zero, and
* ptr will returned unchanged.
*/
-_nodiscard
+[[_nodiscard]]
void *reserve(void *ptr, size_t align, size_t size, size_t count);
/**
@@ -272,7 +272,7 @@ void arena_free(struct arena *arena, void *ptr);
/**
* Allocate an object out of the arena.
*/
-_malloc(arena_free, 2)
+[[_malloc(arena_free, 2)]]
void *arena_alloc(struct arena *arena);
/**
@@ -353,7 +353,7 @@ void varena_free(struct varena *varena, void *ptr, size_t count);
* @return
* The allocated struct, or NULL on failure.
*/
-_malloc(varena_free, 2)
+[[_malloc(varena_free, 2)]]
void *varena_alloc(struct varena *varena, size_t count);
/**
@@ -370,7 +370,7 @@ void *varena_alloc(struct varena *varena, size_t count);
* @return
* The resized struct, or NULL on failure.
*/
-_nodiscard
+[[_nodiscard]]
void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t new_count);
/**
@@ -385,7 +385,7 @@ void *varena_realloc(struct varena *varena, void *ptr, size_t old_count, size_t
* @return
* The resized struct, or NULL on failure.
*/
-_nodiscard
+[[_nodiscard]]
void *varena_grow(struct varena *varena, void *ptr, size_t *count);
/**
diff --git a/src/bar.c b/src/bar.c
index 3258df0..1f66a37 100644
--- a/src/bar.c
+++ b/src/bar.c
@@ -18,7 +18,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
@@ -33,25 +32,14 @@ struct bfs_bar {
/** Get the terminal size, if possible. */
static int bfs_bar_getsize(struct bfs_bar *bar) {
-#if BFS_HAS_TCGETWINSIZE || defined(TIOCGWINSZ)
struct winsize ws;
-
-# if BFS_HAS_TCGETWINSIZE
- int ret = tcgetwinsize(bar->fd, &ws);
-# else
- int ret = ioctl(bar->fd, TIOCGWINSZ, &ws);
-# endif
- if (ret != 0) {
- return ret;
+ if (xtcgetwinsize(bar->fd, &ws) != 0) {
+ return -1;
}
store(&bar->width, ws.ws_col, relaxed);
store(&bar->height, ws.ws_row, relaxed);
return 0;
-#else
- errno = ENOTSUP;
- return -1;
-#endif
}
/** Write a string to the status bar (async-signal-safe). */
@@ -139,7 +127,7 @@ static void bfs_bar_sigexit(int sig, siginfo_t *info, void *arg) {
}
/** printf() to the status bar with a single write(). */
-_printf(2, 3)
+[[_printf(2, 3)]]
static int bfs_bar_printf(struct bfs_bar *bar, const char *format, ...) {
va_list args;
va_start(args, format);
diff --git a/src/bfs.h b/src/bfs.h
index 32dbbae..bb83df2 100644
--- a/src/bfs.h
+++ b/src/bfs.h
@@ -8,6 +8,9 @@
#ifndef BFS_H
#define BFS_H
+#include <assert.h> // For __GLIBC__
+#include <stddef.h> // For offsetof
+
// Standard versions
/** Possible __STDC_VERSION__ values. */
@@ -53,15 +56,36 @@ extern const char bfs_cflags[];
extern const char bfs_ldflags[];
extern const char bfs_ldlibs[];
-// Get __GLIBC__
-#include <assert.h>
-
// Fundamental utilities
/**
- * Get the length of an array.
+ * Given `ptr = &t->member`, return `t`.
+ */
+#define container_of(ptr, type, member) \
+ (container_of_typecheck(ptr, type, member), \
+ (type *)((char *)ptr - offsetof(type, member)))
+
+#define container_of_typecheck(ptr, type, field) \
+ (void)sizeof(ptr - &((type *)NULL)->field)
+
+/**
+ * A preprocessor conditional.
+ *
+ * BFS_VA_IF(A)(B)(C) => B
+ * BFS_VA_IF( )(B)(C) => C
*/
-#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__]))
+#define BFS_VA_IF(...) BFS_VA_IF_AB ## __VA_OPT__(C)
+// BFS_VA_IF(A)(B)(C) => BFS_VA_IF_ABC(B)(C)
+// BFS_VA_IF( )(B)(C) => BFS_VA_IF_AB(B)(C)
+
+#define BFS_VA_IF_ABC(...) __VA_ARGS__ BFS_VA_IGNORE
+// BFS_VA_IF_ABC(B)(C) => B BFS_VA_IGNORE(C)
+
+#define BFS_VA_IF_AB(...) BFS_VA_REPEAT
+// BFS_VA_IF_AB(B)(C) => BFS_VA_REPEAT(C)
+
+#define BFS_VA_IGNORE(...)
+#define BFS_VA_REPEAT(...) __VA_ARGS__
/**
* False sharing/destructive interference/largest cache line size.
@@ -89,19 +113,12 @@ extern const char bfs_ldlibs[];
// Wrappers for attributes
/**
- * Silence warnings about switch/case fall-throughs.
- */
-#if __has_attribute(fallthrough)
-# define _fallthrough __attribute__((fallthrough))
-#else
-# define _fallthrough ((void)0)
-#endif
-
-/**
* Silence warnings about unused declarations.
*/
-#if __has_attribute(unused)
-# define _maybe_unused __attribute__((unused))
+#if __has_c_attribute(maybe_unused)
+# define _maybe_unused maybe_unused
+#elif __has_c_attribute(gnu::unused)
+# define _maybe_unused gnu::unused
#else
# define _maybe_unused
#endif
@@ -109,8 +126,10 @@ extern const char bfs_ldlibs[];
/**
* Warn if a value is unused.
*/
-#if __has_attribute(warn_unused_result)
-# define _nodiscard __attribute__((warn_unused_result))
+#if __has_c_attribute(nodiscard)
+# define _nodiscard nodiscard
+#elif __has_c_attribute(gnu::warn_unused_result)
+# define _nodiscard gnu::warn_unused_result
#else
# define _nodiscard
#endif
@@ -118,35 +137,38 @@ extern const char bfs_ldlibs[];
/**
* Hint to avoid inlining a function.
*/
-#if __has_attribute(noinline)
-# define _noinline __attribute__((noinline))
+#if __has_c_attribute(gnu::noinline)
+# define _noinline gnu::noinline
#else
# define _noinline
#endif
/**
- * Marks a non-returning function.
+ * Hint that a function is unlikely to be called.
*/
-#if __STDC_VERSION__ >= C23
-# define _noreturn [[noreturn]]
+#if __has_c_attribute(gnu::cold)
+# define _cold _noinline, gnu::cold
#else
-# define _noreturn _Noreturn
+# define _cold _noinline
#endif
/**
- * Hint that a function is unlikely to be called.
+ * Marks a non-returning function.
*/
-#if __has_attribute(cold)
-# define _cold _noinline __attribute__((cold))
+#if __has_c_attribute(noreturn)
+# define _noreturn noreturn
+#elif __has_c_attribute(gnu::noreturn)
+# define _noreturn gnu::noreturn
#else
-# define _cold _noinline
+# define _noreturn
#endif
+
/**
* Adds compiler warnings for bad printf()-style function calls, if supported.
*/
-#if __has_attribute(format)
-# define _printf(fmt, args) __attribute__((format(printf, fmt, args)))
+#if __has_c_attribute(gnu::format)
+# define _printf(fmt, args) gnu::format(printf, fmt, args)
#else
# define _printf(fmt, args)
#endif
@@ -154,8 +176,8 @@ extern const char bfs_ldlibs[];
/**
* Annotates functions that potentially modify and return format strings.
*/
-#if __has_attribute(format_arg)
-# define _format_arg(arg) __attribute__((format_arg(arg)))
+#if __has_c_attribute(gnu::format_arg)
+# define _format_arg(arg) gnu::format_arg(arg)
#else
# define _format_arg(arg)
#endif
@@ -163,11 +185,11 @@ extern const char bfs_ldlibs[];
/**
* Annotates allocator-like functions.
*/
-#if __has_attribute(malloc)
+#if __has_c_attribute(gnu::malloc)
# if __GNUC__ >= 11 && !__OPTIMIZE__ // malloc(deallocator) disables inlining on GCC
-# define _malloc(...) _nodiscard __attribute__((malloc(__VA_ARGS__)))
+# define _malloc(...) _nodiscard, gnu::malloc(__VA_ARGS__)
# else
-# define _malloc(...) _nodiscard __attribute__((malloc))
+# define _malloc(...) _nodiscard, gnu::malloc
# endif
#else
# define _malloc(...) _nodiscard
@@ -176,8 +198,8 @@ extern const char bfs_ldlibs[];
/**
* Specifies that a function returns allocations with a given alignment.
*/
-#if __has_attribute(alloc_align)
-# define _alloc_align(param) __attribute__((alloc_align(param)))
+#if __has_c_attribute(gnu::alloc_align)
+# define _alloc_align(param) gnu::alloc_align(param)
#else
# define _alloc_align(param)
#endif
@@ -185,8 +207,8 @@ extern const char bfs_ldlibs[];
/**
* Specifies that a function returns allocations with a given size.
*/
-#if __has_attribute(alloc_size)
-# define _alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#if __has_c_attribute(gnu::alloc_size)
+# define _alloc_size(...) gnu::alloc_size(__VA_ARGS__)
#else
# define _alloc_size(...)
#endif
@@ -194,7 +216,7 @@ extern const char bfs_ldlibs[];
/**
* Shorthand for _alloc_align() and _alloc_size().
*/
-#define _aligned_alloc(align, ...) _alloc_align(align) _alloc_size(__VA_ARGS__)
+#define _aligned_alloc(align, ...) _alloc_align(align), _alloc_size(__VA_ARGS__)
/**
* Check if function multiversioning via GNU indirect functions (ifunc) is supported.
@@ -202,7 +224,10 @@ extern const char bfs_ldlibs[];
* Disabled on TSan due to https://github.com/google/sanitizers/issues/342.
*/
#ifndef BFS_USE_TARGET_CLONES
-# if __has_attribute(target_clones) && (__GLIBC__ || __FreeBSD__) && !__SANITIZE_THREAD__
+# if __has_c_attribute(gnu::target_clones) \
+ && (__GLIBC__ || __FreeBSD__) \
+ && !__SANITIZE_THREAD__ \
+ && !__SANITIZE_TYPE__
# define BFS_USE_TARGET_CLONES true
# else
# define BFS_USE_TARGET_CLONES false
@@ -213,12 +238,23 @@ extern const char bfs_ldlibs[];
* Apply the target_clones attribute, if available.
*/
#if BFS_USE_TARGET_CLONES
-# define _target_clones(...) __attribute__((target_clones(__VA_ARGS__)))
+# define _target_clones(...) gnu::target_clones(__VA_ARGS__)
#else
# define _target_clones(...)
#endif
/**
+ * Mark the size of a flexible array member.
+ */
+#if __has_c_attribute(clang::counted_by)
+# define _counted_by(...) clang::counted_by(__VA_ARGS__)
+#elif __has_c_attribute(gnu::counted_by)
+# define _counted_by(...) gnu::counted_by(__VA_ARGS__)
+#else
+# define _counted_by(...)
+#endif
+
+/**
* Optimization hint to not unroll a loop.
*/
#if BFS_HAS_PRAGMA_NOUNROLL
diff --git a/src/bfstd.c b/src/bfstd.c
index 82663eb..b78af7a 100644
--- a/src/bfstd.c
+++ b/src/bfstd.c
@@ -23,10 +23,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/ioctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
+#include <termios.h>
#include <unistd.h>
#include <wchar.h>
@@ -187,16 +189,6 @@ char *xgetdelim(FILE *file, char delim) {
}
}
-int open_cterm(int flags) {
- char path[L_ctermid];
- if (ctermid(path) == NULL || strlen(path) == 0) {
- errno = ENOTTY;
- return -1;
- }
-
- return open(path, flags);
-}
-
const char *xgetprogname(void) {
const char *cmd = NULL;
#if BFS_HAS_GETPROGNAME
@@ -243,6 +235,36 @@ static int xstrtox_epilogue(const char *str, char **end, char *endp) {
return 0;
}
+int xstrtos(const char *str, char **end, int base, short *value) {
+ long n;
+ if (xstrtol(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n < SHRT_MIN || n > SHRT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+int xstrtoi(const char *str, char **end, int base, int *value) {
+ long n;
+ if (xstrtol(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n < INT_MIN || n > INT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
int xstrtol(const char *str, char **end, int base, long *value) {
if (xstrtox_prologue(str) != 0) {
return -1;
@@ -283,6 +305,70 @@ int xstrtod(const char *str, char **end, double *value) {
return xstrtox_epilogue(str, end, endp);
}
+int xstrtous(const char *str, char **end, int base, unsigned short *value) {
+ unsigned long n;
+ if (xstrtoul(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n > USHRT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+int xstrtoui(const char *str, char **end, int base, unsigned int *value) {
+ unsigned long n;
+ if (xstrtoul(str, end, base, &n) != 0) {
+ return -1;
+ }
+
+ if (n > UINT_MAX) {
+ errno = ERANGE;
+ return -1;
+ }
+
+ *value = n;
+ return 0;
+}
+
+/** Common epilogue for xstrtou*() wrappers. */
+static int xstrtoux_epilogue(const char *str, char **end, char *endp) {
+ if (xstrtox_epilogue(str, end, endp) != 0) {
+ return -1;
+ }
+
+ if (str[0] == '-') {
+ errno = ERANGE;
+ return -1;
+ }
+
+ return 0;
+}
+
+int xstrtoul(const char *str, char **end, int base, unsigned long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtoul(str, &endp, base);
+ return xstrtoux_epilogue(str, end, endp);
+}
+
+int xstrtoull(const char *str, char **end, int base, unsigned long long *value) {
+ if (xstrtox_prologue(str) != 0) {
+ return -1;
+ }
+
+ char *endp;
+ *value = strtoull(str, &endp, base);
+ return xstrtoux_epilogue(str, end, endp);
+}
+
/** Compile and execute a regular expression for xrpmatch(). */
static int xrpregex(nl_item item, const char *response) {
const char *pattern = nl_langinfo(item);
@@ -558,6 +644,32 @@ pid_t xwaitpid(pid_t pid, int *status, int flags) {
return ret;
}
+int open_cterm(int flags) {
+ char path[L_ctermid];
+ if (ctermid(path) == NULL || strlen(path) == 0) {
+ errno = ENOTTY;
+ return -1;
+ }
+
+ return open(path, flags);
+}
+
+int xtcgetwinsize(int fd, struct winsize *ws) {
+#if BFS_HAS_TCGETWINSIZE
+ return tcgetwinsize(fd, ws);
+#else
+ return ioctl(fd, TIOCGWINSZ, ws);
+#endif
+}
+
+int xtcsetwinsize(int fd, const struct winsize *ws) {
+#if BFS_HAS_TCSETWINSIZE
+ return tcsetwinsize(fd, ws);
+#else
+ return ioctl(fd, TIOCSWINSZ, ws);
+#endif
+}
+
int dup_cloexec(int fd) {
#ifdef F_DUPFD_CLOEXEC
return fcntl(fd, F_DUPFD_CLOEXEC, 0);
diff --git a/src/bfstd.h b/src/bfstd.h
index 28f473e..15dd949 100644
--- a/src/bfstd.h
+++ b/src/bfstd.h
@@ -158,16 +158,6 @@ FILE *xfopen(const char *path, int flags);
*/
char *xgetdelim(FILE *file, char delim);
-/**
- * Open the controlling terminal.
- *
- * @flags
- * The open() flags.
- * @return
- * An open file descriptor, or -1 on failure.
- */
-int open_cterm(int flags);
-
// #include <stdlib.h>
/**
@@ -179,6 +169,16 @@ int open_cterm(int flags);
const char *xgetprogname(void);
/**
+ * Like xstrtol(), but for short.
+ */
+int xstrtos(const char *str, char **end, int base, short *value);
+
+/**
+ * Like xstrtol(), but for int.
+ */
+int xstrtoi(const char *str, char **end, int base, int *value);
+
+/**
* Wrapper for strtol() that forbids leading spaces.
*/
int xstrtol(const char *str, char **end, int base, long *value);
@@ -189,6 +189,26 @@ int xstrtol(const char *str, char **end, int base, long *value);
int xstrtoll(const char *str, char **end, int base, long long *value);
/**
+ * Like xstrtoul(), but for unsigned short.
+ */
+int xstrtous(const char *str, char **end, int base, unsigned short *value);
+
+/**
+ * Like xstrtoul(), but for unsigned int.
+ */
+int xstrtoui(const char *str, char **end, int base, unsigned int *value);
+
+/**
+ * Wrapper for strtoul() that forbids leading spaces, negatives.
+ */
+int xstrtoul(const char *str, char **end, int base, unsigned long *value);
+
+/**
+ * Wrapper for strtoull() that forbids leading spaces, negatives.
+ */
+int xstrtoull(const char *str, char **end, int base, unsigned long long *value);
+
+/**
* Wrapper for strtof() that forbids leading spaces.
*/
int xstrtof(const char *str, char **end, float *value);
@@ -342,6 +362,29 @@ int xminor(dev_t dev);
*/
pid_t xwaitpid(pid_t pid, int *status, int flags);
+#include <sys/ioctl.h> // May be necessary for struct winsize
+#include <termios.h>
+
+/**
+ * Open the controlling terminal.
+ *
+ * @flags
+ * The open() flags.
+ * @return
+ * An open file descriptor, or -1 on failure.
+ */
+int open_cterm(int flags);
+
+/**
+ * tcgetwinsize()/ioctl(TIOCGWINSZ) wrapper.
+ */
+int xtcgetwinsize(int fd, struct winsize *ws);
+
+/**
+ * tcsetwinsize()/ioctl(TIOCSWINSZ) wrapper.
+ */
+int xtcsetwinsize(int fd, const struct winsize *ws);
+
// #include <unistd.h>
/**
diff --git a/src/bftw.c b/src/bftw.c
index f822456..a884e92 100644
--- a/src/bftw.c
+++ b/src/bftw.c
@@ -253,6 +253,7 @@ struct bftw_file {
/** The length of the file's name. */
size_t namelen;
/** The file's name. */
+ // [[_counted_by(namelen + 1)]]
char name[];
};
@@ -1439,7 +1440,7 @@ static bool bftw_must_stat(const struct bftw_state *state, size_t depth, enum bf
if (!(bftw_stat_flags(state, depth) & BFS_STAT_NOFOLLOW)) {
return true;
}
- _fallthrough;
+ [[fallthrough]];
default:
#if __linux__
@@ -1485,7 +1486,8 @@ fail:
/** Check if we should stat() a file asynchronously. */
static bool bftw_should_ioq_stat(struct bftw_state *state, struct bftw_file *file) {
- // To avoid surprising users too much, process the roots in order
+ // POSIX wants the root paths to be processed in order
+ // See https://www.austingroupbugs.net/view.php?id=1859
if (file->depth == 0) {
return false;
}
diff --git a/src/color.c b/src/color.c
index 0cc950b..2d0fc9c 100644
--- a/src/color.c
+++ b/src/color.c
@@ -32,6 +32,7 @@ struct esc_seq {
/** The length of the escape sequence. */
size_t len;
/** The escape sequence itself, without a terminating NUL. */
+ [[_counted_by(len)]]
char seq[];
};
@@ -48,6 +49,7 @@ struct ext_color {
/** Whether the comparison should be case-sensitive. */
bool case_sensitive;
/** The extension to match (NUL-terminated). */
+ // [[_counted_by(len + 1)]]
char ext[];
};
@@ -103,6 +105,8 @@ struct colors {
struct esc_seq *pipe;
struct esc_seq *socket;
+ struct esc_seq *dataless;
+
/** A mapping from color names (fi, di, ln, etc.) to struct fields. */
struct trie names;
@@ -161,26 +165,32 @@ static int cat_esc(dchar **dstr, const struct esc_seq *seq) {
return dstrxcat(dstr, seq->seq, seq->len);
}
-/** Set a named escape sequence. */
-static int set_esc(struct colors *colors, const char *name, dchar *value) {
- struct esc_seq **field = get_esc(colors, name);
- if (!field) {
- return 0;
+/** Set an escape sequence field. */
+static int set_esc_field(struct colors *colors, struct esc_seq **field, const dchar *value) {
+ struct esc_seq *seq = NULL;
+ if (value) {
+ seq = new_esc(colors, value, dstrlen(value));
+ if (!seq) {
+ return -1;
+ }
}
if (*field) {
free_esc(colors, *field);
- *field = NULL;
}
+ *field = seq;
- if (value) {
- *field = new_esc(colors, value, dstrlen(value));
- if (!*field) {
- return -1;
- }
+ return 0;
+}
+
+/** Set a named escape sequence. */
+static int set_esc(struct colors *colors, const char *name, const dchar *value) {
+ struct esc_seq **field = get_esc(colors, name);
+ if (!field) {
+ return 0;
}
- return 0;
+ return set_esc_field(colors, field, value);
}
/** Reverse a string, to turn suffix matches into prefix matches. */
@@ -607,6 +617,109 @@ fail:
return ret;
}
+/** Parse the FreeBSD $LSCOLORS format. */
+static int parse_bsd_ls_colors(struct colors *colors, const char *lscolors) {
+ static const char *fg_codes[256] = {
+ // 0-7: deprecated aliases for a-h
+ ['0'] = "30", ['1'] = "31", ['2'] = "32", ['3'] = "33",
+ ['4'] = "34", ['5'] = "35", ['6'] = "36", ['7'] = "37",
+ // a-h: first 8 ANSI foreground colors
+ ['a'] = "30", ['b'] = "31", ['c'] = "32", ['d'] = "33",
+ ['e'] = "34", ['f'] = "35", ['g'] = "36", ['h'] = "37",
+ // x: default foreground
+ ['x'] = "39",
+ // A-H: bold foreground colors
+ ['A'] = "1;30", ['B'] = "1;31", ['C'] = "1;32", ['D'] = "1;33",
+ ['E'] = "1;34", ['F'] = "1;35", ['G'] = "1;36", ['H'] = "1;37",
+ // X: bold default foreground
+ ['X'] = "1;39",
+ };
+
+ static const char *bg_codes[256] = {
+ // 0-7: deprecated aliases for a-h
+ ['0'] = "40", ['1'] = "41", ['2'] = "42", ['3'] = "43",
+ ['4'] = "44", ['5'] = "45", ['6'] = "46", ['7'] = "47",
+ // a-h: first 8 ANSI background colors
+ ['a'] = "40", ['b'] = "41", ['c'] = "42", ['d'] = "43",
+ ['e'] = "44", ['f'] = "45", ['g'] = "46", ['h'] = "47",
+ // x: default background
+ ['x'] = "49",
+ // A-H: background colors + underline
+ ['A'] = "4;40", ['B'] = "4;41", ['C'] = "4;42", ['D'] = "4;43",
+ ['E'] = "4;44", ['F'] = "4;45", ['G'] = "4;46", ['H'] = "4;47",
+ // X: default background + underline
+ ['X'] = "4;49",
+ };
+
+ // Please refer to https://man.freebsd.org/cgi/man.cgi?ls(1)#ENVIRONMENT
+ char complete_colors[] = "exfxcxdxbxegedabagacadah";
+
+ // For short $LSCOLORS, use the default colors for the rest
+ size_t max = strlen(complete_colors);
+ size_t len = strnlen(lscolors, max);
+ memcpy(complete_colors, lscolors, len);
+
+ struct esc_seq **keys[] = {
+ &colors->directory,
+ &colors->link,
+ &colors->socket,
+ &colors->pipe,
+ &colors->executable,
+ &colors->blockdev,
+ &colors->chardev,
+ &colors->setuid,
+ &colors->setgid,
+ &colors->sticky_other_writable,
+ &colors->other_writable,
+ &colors->dataless,
+ };
+
+ dchar *buf = dstralloc(8);
+ if (!buf) {
+ return -1;
+ }
+
+ int ret = -1;
+ for (size_t i = 0; i < countof(keys); ++i) {
+ uint8_t fg = complete_colors[2 * i];
+ uint8_t bg = complete_colors[2 * i + 1];
+
+ const char *fg_code = fg_codes[fg];
+ const char *bg_code = bg_codes[bg];
+
+ dstrshrink(buf, 0);
+ if (fg_code) {
+ if (dstrcat(&buf, fg_code) != 0) {
+ goto fail;
+ }
+ }
+ if (fg_code && bg_code) {
+ if (dstrcat(&buf, ";") != 0) {
+ goto fail;
+ }
+ }
+ if (bg_code) {
+ if (dstrcat(&buf, bg_code) != 0) {
+ goto fail;
+ }
+ }
+
+ const dchar *value = dstrlen(buf) > 0 ? buf : NULL;
+ if (set_esc_field(colors, keys[i], value) != 0) {
+ goto fail;
+ }
+ }
+
+ ret = 0;
+fail:
+ dstrfree(buf);
+ return ret;
+}
+
+static bool str_isset(const char *str) {
+ return str && *str;
+}
+
struct colors *parse_colors(void) {
struct colors *colors = ALLOC(struct colors);
if (!colors) {
@@ -672,16 +785,28 @@ struct colors *parse_colors(void) {
fail = fail || init_esc(colors, "pi", "33", &colors->pipe);
fail = fail || init_esc(colors, "so", "01;35", &colors->socket);
+ colors->dataless = NULL;
+
if (fail) {
goto fail;
}
- if (parse_gnu_ls_colors(colors, getenv("LS_COLORS")) != 0) {
- goto fail;
- }
- if (parse_gnu_ls_colors(colors, getenv("BFS_COLORS")) != 0) {
- goto fail;
+ const char *gnu_colors = getenv("LS_COLORS");
+ const char *bfs_colors = getenv("BFS_COLORS");
+ const char *bsd_colors = getenv("LSCOLORS");
+ if (str_isset(gnu_colors) || str_isset(bfs_colors)) {
+ if (parse_gnu_ls_colors(colors, gnu_colors) != 0) {
+ goto fail;
+ }
+ if (parse_gnu_ls_colors(colors, bfs_colors) != 0) {
+ goto fail;
+ }
+ } else if (str_isset(bsd_colors)) {
+ if (parse_bsd_ls_colors(colors, bsd_colors) != 0) {
+ goto fail;
+ }
}
+
if (build_iext_trie(colors) != 0) {
goto fail;
}
@@ -949,6 +1074,34 @@ static bool cpath_is_broken(const struct cpath *cpath) {
}
}
+/** Check if we need a statbuf to colorize a file. */
+static bool must_stat(const struct colors *colors, enum bfs_type type) {
+ switch (type) {
+ case BFS_REG:
+ if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) {
+ return true;
+ }
+
+#ifdef ST_DATALESS
+ if (colors->dataless) {
+ return true;
+ }
+#endif
+
+ return false;
+
+ case BFS_DIR:
+ if (colors->sticky_other_writable || colors->other_writable || colors->sticky) {
+ return true;
+ }
+
+ return false;
+
+ default:
+ return false;
+ }
+}
+
/** Get the color for a file. */
static const struct esc_seq *file_color(const struct colors *colors, const struct cpath *cpath) {
enum bfs_type type;
@@ -963,17 +1116,17 @@ static const struct esc_seq *file_color(const struct colors *colors, const struc
}
const struct bfs_stat *statbuf = NULL;
+ if (must_stat(colors, type)) {
+ statbuf = cpath_stat(cpath);
+ if (!statbuf) {
+ goto error;
+ }
+ }
+
const struct esc_seq *color = NULL;
switch (type) {
case BFS_REG:
- if (colors->setuid || colors->setgid || colors->executable || colors->multi_hard) {
- statbuf = cpath_stat(cpath);
- if (!statbuf) {
- goto error;
- }
- }
-
if (colors->setuid && (statbuf->mode & 04000)) {
color = colors->setuid;
} else if (colors->setgid && (statbuf->mode & 02000)) {
@@ -986,6 +1139,12 @@ static const struct esc_seq *file_color(const struct colors *colors, const struc
color = colors->multi_hard;
}
+#ifdef SF_DATALESS
+ if (!color && colors->dataless && (statbuf->attrs & SF_DATALESS)) {
+ color = colors->dataless;
+ }
+#endif
+
if (!color) {
const char *name = cpath->path + cpath->nameoff;
size_t namelen = cpath->valid - cpath->nameoff;
@@ -999,13 +1158,6 @@ static const struct esc_seq *file_color(const struct colors *colors, const struc
break;
case BFS_DIR:
- if (colors->sticky_other_writable || colors->other_writable || colors->sticky) {
- statbuf = cpath_stat(cpath);
- if (!statbuf) {
- goto error;
- }
- }
-
if (colors->sticky_other_writable && (statbuf->mode & 01002) == 01002) {
color = colors->sticky_other_writable;
} else if (colors->other_writable && (statbuf->mode & 00002)) {
@@ -1237,9 +1389,36 @@ static int print_link_target(CFILE *cfile, const struct BFTW *ftwbuf) {
}
/** Format some colored output to the buffer. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static int cbuff(CFILE *cfile, const char *format, ...);
+/** Print an expression's name, for diagnostics. */
+static int print_expr_name(CFILE *cfile, const struct bfs_expr *expr) {
+ switch (expr->kind) {
+ case BFS_FLAG:
+ return cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]);
+ case BFS_OPERATOR:
+ return cbuff(cfile, "${red}%pq${rs}", expr->argv[0]);
+ default:
+ return cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]);
+ }
+}
+
+/** Print an expression's args, for diagnostics. */
+static int print_expr_args(CFILE *cfile, const struct bfs_expr *expr) {
+ if (print_expr_name(cfile, expr) != 0) {
+ return -1;
+ }
+
+ for (size_t i = 1; i < expr->argc; ++i) {
+ if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
/** Dump a parsed expression tree, for debugging. */
static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, int depth) {
if (depth >= 2) {
@@ -1254,28 +1433,10 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i
return -1;
}
- int ret;
- switch (expr->kind) {
- case BFS_FLAG:
- ret = cbuff(cfile, "${cyn}%pq${rs}", expr->argv[0]);
- break;
- case BFS_OPERATOR:
- ret = cbuff(cfile, "${red}%pq${rs}", expr->argv[0]);
- break;
- default:
- ret = cbuff(cfile, "${blu}%pq${rs}", expr->argv[0]);
- break;
- }
- if (ret < 0) {
+ if (print_expr_args(cfile, expr) != 0) {
return -1;
}
- for (size_t i = 1; i < expr->argc; ++i) {
- if (cbuff(cfile, " ${bld}%pq${rs}", expr->argv[i]) < 0) {
- return -1;
- }
- }
-
if (verbose) {
double rate = 0.0, time = 0.0;
if (expr->evaluations) {
@@ -1312,7 +1473,7 @@ static int print_expr(CFILE *cfile, const struct bfs_expr *expr, bool verbose, i
return 0;
}
-_printf(2, 0)
+[[_printf(2, 0)]]
static int cvbuff(CFILE *cfile, const char *format, va_list args) {
const struct colors *colors = cfile->colors;
@@ -1413,6 +1574,16 @@ static int cvbuff(CFILE *cfile, const char *format, va_list args) {
return -1;
}
break;
+ case 'x':
+ if (print_expr_args(cfile, va_arg(args, const struct bfs_expr *)) != 0) {
+ return -1;
+ }
+ break;
+ case 'X':
+ if (print_expr_name(cfile, va_arg(args, const struct bfs_expr *)) != 0) {
+ return -1;
+ }
+ break;
default:
goto invalid;
diff --git a/src/color.h b/src/color.h
index 2394af2..d5e437c 100644
--- a/src/color.h
+++ b/src/color.h
@@ -95,19 +95,21 @@ int cfclose(CFILE *cfile);
* %pL: A colored link target, from a const struct BFTW * argument
* %pe: Dump a const struct bfs_expr *, for debugging.
* %pE: Dump a const struct bfs_expr * in verbose form, for debugging.
+ * %px: Print a const struct bfs_expr * with syntax highlighting.
+ * %pX: Print the name of a const struct bfs_expr *, without arguments.
* %%: A literal '%'
* ${cc}: Change the color to 'cc'
* $$: A literal '$'
* @return
* 0 on success, -1 on failure.
*/
-_printf(2, 3)
+[[_printf(2, 3)]]
int cfprintf(CFILE *cfile, const char *format, ...);
/**
* cfprintf() variant that takes a va_list.
*/
-_printf(2, 0)
+[[_printf(2, 0)]]
int cvfprintf(CFILE *cfile, const char *format, va_list args);
/**
diff --git a/src/ctx.c b/src/ctx.c
index d92d8ba..05baa1d 100644
--- a/src/ctx.c
+++ b/src/ctx.c
@@ -286,6 +286,7 @@ int bfs_ctx_free(struct bfs_ctx *ctx) {
}
free(ctx->paths);
+ free(ctx->kinds);
free(ctx->argv);
free(ctx);
}
diff --git a/src/ctx.h b/src/ctx.h
index be6e2af..908338f 100644
--- a/src/ctx.h
+++ b/src/ctx.h
@@ -29,6 +29,8 @@ struct bfs_ctx {
size_t argc;
/** The unparsed command line arguments. */
char **argv;
+ /** The argument token kinds. */
+ enum bfs_kind *kinds;
/** The root paths. */
const char **paths;
diff --git a/src/diag.c b/src/diag.c
index a86b060..e0db4d2 100644
--- a/src/diag.c
+++ b/src/diag.c
@@ -33,7 +33,7 @@ void bfs_diagf(const char *format, ...) {
va_end(args);
}
-_noreturn
+[[_noreturn]]
void bfs_abortf(const char *format, ...) {
va_list args;
va_start(args, format);
diff --git a/src/diag.h b/src/diag.h
index 645dbb1..12c0dd8 100644
--- a/src/diag.h
+++ b/src/diag.h
@@ -18,6 +18,7 @@
*
* bfs: func@src/file.c:0: Message
*/
+// Use (format) ? "..." : "" so the format string is required
#define BFS_DIAG_FORMAT_(format) \
((format) ? "%s: %s@%s:%d: " format "%s" : "")
@@ -25,57 +26,49 @@
* Add arguments to match a BFS_DIAG_FORMAT string.
*/
#define BFS_DIAG_ARGS_(...) \
- xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ "\n"
+ xgetprogname(), __func__, __FILE__, __LINE__, __VA_ARGS__ __VA_OPT__(,) "\n"
/**
* Print a low-level diagnostic message to standard error.
*/
-_printf(1, 2)
+[[_printf(1, 2)]]
void bfs_diagf(const char *format, ...);
/**
* Unconditional diagnostic message.
*/
-#define bfs_diag(...) \
- bfs_diag_(__VA_ARGS__, )
-
-#define bfs_diag_(format, ...) \
+#define bfs_diag(format, ...) \
bfs_diagf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__))
/**
* Print a diagnostic message including the last error.
*/
-#define bfs_ediag(...) \
- bfs_ediag_(__VA_ARGS__, )
-
-#define bfs_ediag_(format, ...) \
- bfs_diag_(format "%s%s", __VA_ARGS__ (sizeof("" format) > 1 ? ": " : ""), errstr(), )
+#define bfs_ediag(format, ...) \
+ BFS_VA_IF(format) \
+ (bfs_diag(format ": %s", __VA_ARGS__ __VA_OPT__(,) errstr())) \
+ (bfs_diag("%s", errstr()))
/**
* Print a message to standard error and abort.
*/
-_cold
-_printf(1, 2)
-_noreturn
+[[_cold]]
+[[_printf(1, 2)]]
+[[_noreturn]]
void bfs_abortf(const char *format, ...);
/**
* Unconditional abort with a message.
*/
-#define bfs_abort(...) \
- bfs_abort_(__VA_ARGS__, )
-
-#define bfs_abort_(format, ...) \
+#define bfs_abort(format, ...) \
bfs_abortf(BFS_DIAG_FORMAT_(format), BFS_DIAG_ARGS_(__VA_ARGS__))
/**
* Abort with a message including the last error.
*/
-#define bfs_eabort(...) \
- bfs_eabort_(__VA_ARGS__, )
-
-#define bfs_eabort_(format, ...) \
- ((format) ? bfs_abort_(format ": %s", __VA_ARGS__ errstr(), ) : (void)0)
+#define bfs_eabort(format, ...) \
+ BFS_VA_IF(__VA_ARGS__) \
+ (bfs_abort(format ": %s", __VA_ARGS__ __VA_OPT__(,) errstr())) \
+ (bfs_abort("%s", errstr()))
/**
* Abort in debug builds; no-op in release builds.
@@ -89,43 +82,20 @@ void bfs_abortf(const char *format, ...);
#endif
/**
- * Get the default assertion message, if no format string was specified.
- */
-#define BFS_DIAG_MSG_(format, str) \
- (sizeof(format) > 1 ? "" : str)
-
-/**
* Unconditional assert.
*/
-#define bfs_verify(...) \
- bfs_verify_(#__VA_ARGS__, __VA_ARGS__, "", )
-
-#define bfs_verify_(str, cond, format, ...) \
- ((cond) ? (void)0 : bfs_verify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__))
-
-#define bfs_verify__(format, ...) \
- bfs_abortf( \
- sizeof(format) > 1 \
- ? BFS_DIAG_FORMAT_("%s" format "%s") \
- : BFS_DIAG_FORMAT_("Assertion failed: `%s`"), \
- BFS_DIAG_ARGS_(__VA_ARGS__))
+#define bfs_verify(cond, ...) \
+ ((cond) ? (void)0 : BFS_VA_IF(__VA_ARGS__) \
+ (bfs_abort(__VA_ARGS__)) \
+ (bfs_abort("Assertion failed: `%s`", #cond)))
/**
* Unconditional assert, including the last error.
*/
-#define bfs_everify(...) \
- bfs_everify_(#__VA_ARGS__, __VA_ARGS__, "", )
-
-
-#define bfs_everify_(str, cond, format, ...) \
- ((cond) ? (void)0 : bfs_everify__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__))
-
-#define bfs_everify__(format, ...) \
- bfs_abortf( \
- sizeof(format) > 1 \
- ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \
- : BFS_DIAG_FORMAT_("Assertion failed: `%s`: %s"), \
- BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), ))
+#define bfs_everify(cond, ...) \
+ ((cond) ? (void)0 : BFS_VA_IF(__VA_ARGS__) \
+ (bfs_eabort(__VA_ARGS__)) \
+ (bfs_eabort("Assertion failed: `%s`", #cond)))
/**
* Assert in debug builds; no-op in release builds.
@@ -171,14 +141,14 @@ const char *debug_flag_name(enum debug_flags flag);
/**
* Like perror(), but decorated like bfs_error().
*/
-_cold
+[[_cold]]
void bfs_perror(const struct bfs_ctx *ctx, const char *str);
/**
* Shorthand for printing error messages.
*/
-_cold
-_printf(2, 3)
+[[_cold]]
+[[_printf(2, 3)]]
void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
/**
@@ -186,8 +156,8 @@ void bfs_error(const struct bfs_ctx *ctx, const char *format, ...);
*
* @return Whether a warning was printed.
*/
-_cold
-_printf(2, 3)
+[[_cold]]
+[[_printf(2, 3)]]
bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
/**
@@ -195,71 +165,71 @@ bool bfs_warning(const struct bfs_ctx *ctx, const char *format, ...);
*
* @return Whether a debug message was printed.
*/
-_cold
-_printf(3, 4)
+[[_cold]]
+[[_printf(3, 4)]]
bool bfs_debug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, ...);
/**
* bfs_error() variant that takes a va_list.
*/
-_cold
-_printf(2, 0)
+[[_cold]]
+[[_printf(2, 0)]]
void bfs_verror(const struct bfs_ctx *ctx, const char *format, va_list args);
/**
* bfs_warning() variant that takes a va_list.
*/
-_cold
-_printf(2, 0)
+[[_cold]]
+[[_printf(2, 0)]]
bool bfs_vwarning(const struct bfs_ctx *ctx, const char *format, va_list args);
/**
* bfs_debug() variant that takes a va_list.
*/
-_cold
-_printf(3, 0)
+[[_cold]]
+[[_printf(3, 0)]]
bool bfs_vdebug(const struct bfs_ctx *ctx, enum debug_flags flag, const char *format, va_list args);
/**
* Print the error message prefix.
*/
-_cold
+[[_cold]]
void bfs_error_prefix(const struct bfs_ctx *ctx);
/**
* Print the warning message prefix.
*/
-_cold
+[[_cold]]
bool bfs_warning_prefix(const struct bfs_ctx *ctx);
/**
* Print the debug message prefix.
*/
-_cold
+[[_cold]]
bool bfs_debug_prefix(const struct bfs_ctx *ctx, enum debug_flags flag);
/**
* Highlight parts of the command line in an error message.
*/
-_cold
+[[_cold]]
void bfs_argv_error(const struct bfs_ctx *ctx, const bool args[]);
/**
* Highlight parts of an expression in an error message.
*/
-_cold
+[[_cold]]
void bfs_expr_error(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
/**
* Highlight parts of the command line in a warning message.
*/
-_cold
+[[_cold]]
bool bfs_argv_warning(const struct bfs_ctx *ctx, const bool args[]);
/**
* Highlight parts of an expression in a warning message.
*/
-_cold
+[[_cold]]
bool bfs_expr_warning(const struct bfs_ctx *ctx, const struct bfs_expr *expr);
#endif // BFS_DIAG_H
diff --git a/src/dstring.c b/src/dstring.c
index 0f08679..5addb77 100644
--- a/src/dstring.c
+++ b/src/dstring.c
@@ -23,6 +23,7 @@ struct dstring {
/** Length of the string, *excluding* the terminating NUL. */
size_t len;
/** The string itself. */
+ [[_counted_by(cap)]]
alignas(dchar) char str[];
};
diff --git a/src/dstring.h b/src/dstring.h
index ce7ef86..8765f6e 100644
--- a/src/dstring.h
+++ b/src/dstring.h
@@ -16,14 +16,14 @@
/** Marker type for dynamic strings. */
#if BFS_LINT && __clang__
-// Abuse __attribute__(aligned) to make a type that allows
+// Abuse [[gnu::aligned]] to make a type that allows
//
// dchar * -> char *
//
// conversions, but warns (with Clang's -Walign-mismatch) on
//
// char * -> dchar *
-typedef __attribute__((aligned(alignof(size_t)))) char dchar;
+typedef char dchar [[gnu::aligned(alignof(size_t))]];
#else
typedef char dchar;
#endif
@@ -42,7 +42,7 @@ void dstrfree(dchar *dstr);
* @cap
* The initial capacity of the string.
*/
-_malloc(dstrfree, 1)
+[[_malloc(dstrfree, 1)]]
dchar *dstralloc(size_t cap);
/**
@@ -51,7 +51,7 @@ dchar *dstralloc(size_t cap);
* @str
* The NUL-terminated string to copy.
*/
-_malloc(dstrfree, 1)
+[[_malloc(dstrfree, 1)]]
dchar *dstrdup(const char *str);
/**
@@ -62,7 +62,7 @@ dchar *dstrdup(const char *str);
* @n
* The maximum number of characters to copy from str.
*/
-_malloc(dstrfree, 1)
+[[_malloc(dstrfree, 1)]]
dchar *dstrndup(const char *str, size_t n);
/**
@@ -71,7 +71,7 @@ dchar *dstrndup(const char *str, size_t n);
* @dstr
* The dynamic string to copy.
*/
-_malloc(dstrfree, 1)
+[[_malloc(dstrfree, 1)]]
dchar *dstrddup(const dchar *dstr);
/**
@@ -82,7 +82,7 @@ dchar *dstrddup(const dchar *dstr);
* @len
* The length of the string, which may include internal NUL bytes.
*/
-_malloc(dstrfree, 1)
+[[_malloc(dstrfree, 1)]]
dchar *dstrxdup(const char *str, size_t len);
/**
@@ -117,7 +117,7 @@ int dstreserve(dchar **dstr, size_t cap);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstresize(dchar **dstr, size_t len);
/**
@@ -139,7 +139,7 @@ void dstrshrink(dchar *dstr, size_t len);
* The string to append.
* @return 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrcat(dchar **dest, const char *src);
/**
@@ -154,7 +154,7 @@ int dstrcat(dchar **dest, const char *src);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrncat(dchar **dest, const char *src, size_t n);
/**
@@ -167,7 +167,7 @@ int dstrncat(dchar **dest, const char *src, size_t n);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrdcat(dchar **dest, const dchar *src);
/**
@@ -182,7 +182,7 @@ int dstrdcat(dchar **dest, const dchar *src);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrxcat(dchar **dest, const char *src, size_t len);
/**
@@ -195,7 +195,7 @@ int dstrxcat(dchar **dest, const char *src, size_t len);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrapp(dchar **str, char c);
/**
@@ -208,7 +208,7 @@ int dstrapp(dchar **str, char c);
* @returns
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrcpy(dchar **dest, const char *str);
/**
@@ -221,7 +221,7 @@ int dstrcpy(dchar **dest, const char *str);
* @returns
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrdcpy(dchar **dest, const dchar *str);
/**
@@ -236,7 +236,7 @@ int dstrdcpy(dchar **dest, const dchar *str);
* @returns
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrncpy(dchar **dest, const char *str, size_t n);
/**
@@ -251,7 +251,7 @@ int dstrncpy(dchar **dest, const char *str, size_t n);
* @returns
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrxcpy(dchar **dest, const char *str, size_t len);
/**
@@ -264,8 +264,8 @@ int dstrxcpy(dchar **dest, const char *str, size_t len);
* @return
* The created string, or NULL on failure.
*/
-_nodiscard
-_printf(1, 2)
+[[_nodiscard]]
+[[_printf(1, 2)]]
dchar *dstrprintf(const char *format, ...);
/**
@@ -278,8 +278,8 @@ dchar *dstrprintf(const char *format, ...);
* @return
* The created string, or NULL on failure.
*/
-_nodiscard
-_printf(1, 0)
+[[_nodiscard]]
+[[_printf(1, 0)]]
dchar *dstrvprintf(const char *format, va_list args);
/**
@@ -294,8 +294,8 @@ dchar *dstrvprintf(const char *format, va_list args);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
-_printf(2, 3)
+[[_nodiscard]]
+[[_printf(2, 3)]]
int dstrcatf(dchar **str, const char *format, ...);
/**
@@ -310,8 +310,8 @@ int dstrcatf(dchar **str, const char *format, ...);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
-_printf(2, 0)
+[[_nodiscard]]
+[[_printf(2, 0)]]
int dstrvcatf(dchar **str, const char *format, va_list args);
/**
@@ -326,7 +326,7 @@ int dstrvcatf(dchar **str, const char *format, va_list args);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrescat(dchar **dest, const char *str, enum wesc_flags flags);
/**
@@ -343,13 +343,13 @@ int dstrescat(dchar **dest, const char *str, enum wesc_flags flags);
* @return
* 0 on success, -1 on failure.
*/
-_nodiscard
+[[_nodiscard]]
int dstrnescat(dchar **dest, const char *str, size_t n, enum wesc_flags flags);
/**
* Repeat a string n times.
*/
-_nodiscard
+[[_nodiscard]]
dchar *dstrepeat(const char *str, size_t n);
#endif // BFS_DSTRING_H
diff --git a/src/eval.c b/src/eval.c
index 7c9da97..8b62419 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -67,7 +67,7 @@ struct bfs_eval {
/**
* Print an error message.
*/
-_printf(2, 3)
+[[_printf(2, 3)]]
static void eval_error(struct bfs_eval *state, const char *format, ...) {
const struct bfs_ctx *ctx = state->ctx;
@@ -279,10 +279,10 @@ bool eval_time(const struct bfs_expr *expr, struct bfs_eval *state) {
switch (expr->time_unit) {
case BFS_DAYS:
diff /= 60 * 24;
- _fallthrough;
+ [[fallthrough]];
case BFS_MINUTES:
diff /= 60;
- _fallthrough;
+ [[fallthrough]];
case BFS_SECONDS:
break;
}
@@ -408,7 +408,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c
if (expr->eval_fn == eval_exec) {
if (bfs_exec_finish(expr->exec) != 0) {
if (errno != 0) {
- bfs_error(ctx, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr());
+ bfs_error(ctx, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr());
}
ret = -1;
}
@@ -429,7 +429,7 @@ static int eval_exec_finish(const struct bfs_expr *expr, const struct bfs_ctx *c
bool eval_exec(const struct bfs_expr *expr, struct bfs_eval *state) {
bool ret = bfs_exec(expr->exec, state->ftwbuf) == 0;
if (errno != 0) {
- eval_error(state, "%s %s: %s.\n", expr->argv[0], expr->argv[1], errstr());
+ eval_error(state, "${blu}%pq${rs} ${bld}%pq${rs}: %s.\n", expr->argv[0], expr->argv[1], errstr());
}
return ret;
}
diff --git a/src/exec.c b/src/exec.c
index 45c9f1d..f57b28f 100644
--- a/src/exec.c
+++ b/src/exec.c
@@ -24,7 +24,7 @@
#include <unistd.h>
/** Print some debugging info. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static void bfs_exec_debug(const struct bfs_exec *execbuf, const char *format, ...) {
const struct bfs_ctx *ctx = execbuf->ctx;
diff --git a/src/expr.h b/src/expr.h
index 871b120..c116778 100644
--- a/src/expr.h
+++ b/src/expr.h
@@ -19,6 +19,9 @@
* Argument/token/expression kinds.
*/
enum bfs_kind {
+ /** A regular argument. */
+ BFS_ARG,
+
/** A flag (-H, -L, etc.). */
BFS_FLAG,
diff --git a/src/fsade.c b/src/fsade.c
index dfdf125..3d4c8ba 100644
--- a/src/fsade.c
+++ b/src/fsade.c
@@ -47,7 +47,7 @@
* Many of the APIs used here don't have *at() variants, but we can try to
* emulate something similar if /proc/self/fd is available.
*/
-_maybe_unused
+[[_maybe_unused]]
static const char *fake_at(const struct BFTW *ftwbuf) {
static atomic int proc_works = -1;
@@ -81,7 +81,7 @@ fail:
return ftwbuf->path;
}
-_maybe_unused
+[[_maybe_unused]]
static void free_fake_at(const struct BFTW *ftwbuf, const char *path) {
if (path != ftwbuf->path) {
dstrfree((dchar *)path);
@@ -91,7 +91,7 @@ static void free_fake_at(const struct BFTW *ftwbuf, const char *path) {
/**
* Check if an error was caused by the absence of support or data for a feature.
*/
-_maybe_unused
+[[_maybe_unused]]
static bool is_absence_error(int error) {
// If the OS doesn't support the feature, it's obviously not enabled for
// any files
@@ -171,7 +171,7 @@ static int bfs_acl_entry(acl_t acl, int which, acl_entry_t *entry) {
}
/** Unified interface for acl_get_tag_type(). */
-_maybe_unused
+[[_maybe_unused]]
static int bfs_acl_tag_type(acl_entry_t entry, acl_tag_t *tag) {
#if BFS_HAS_ACL_GET_TAG_TYPE
return acl_get_tag_type(entry, tag);
diff --git a/src/ioq.c b/src/ioq.c
index 1efedd7..5621e21 100644
--- a/src/ioq.c
+++ b/src/ioq.c
@@ -203,6 +203,7 @@ struct ioqq {
cache_align atomic size_t tail;
/** The circular buffer itself. */
+ // [[_counted_by(slot_mask + 1)]]
cache_align ioq_slot slots[];
};
@@ -275,7 +276,7 @@ static struct ioq_monitor *ioq_slot_monitor(struct ioqq *ioqq, ioq_slot *slot) {
}
/** Atomically wait for a slot to change. */
-_noinline
+[[_noinline]]
static uintptr_t ioq_slot_wait(struct ioqq *ioqq, ioq_slot *slot, uintptr_t value) {
uintptr_t ret;
@@ -323,7 +324,7 @@ done:
}
/** Wake up any threads waiting on a slot. */
-_noinline
+[[_noinline]]
static void ioq_slot_wake(struct ioqq *ioqq, ioq_slot *slot) {
struct ioq_monitor *monitor = ioq_slot_monitor(ioqq, slot);
@@ -593,6 +594,7 @@ struct ioq {
/** The number of background threads. */
size_t nthreads;
/** The background threads themselves. */
+ [[_counted_by(nthreads)]]
struct ioq_thread threads[];
};
diff --git a/src/list.h b/src/list.h
index 15c37a8..92000d5 100644
--- a/src/list.h
+++ b/src/list.h
@@ -86,7 +86,6 @@
#include "diag.h"
#include <stddef.h>
-#include <stdint.h>
#include <string.h>
/**
@@ -123,76 +122,21 @@
* The item to initialize.
* @node (optional)
* If specified, use item->node.next rather than item->next.
- *
- * ---
- *
- * We play some tricks with variadic macros to handle the optional parameter:
- *
- * SLIST_ITEM_INIT(item) => item->next = NULL
- * SLIST_ITEM_INIT(item, node) => item->node.next = NULL
- *
- * The first trick is that
- *
- * #define SLIST_ITEM_INIT(item, ...)
- *
- * won't work because both commas are required (until C23; see N3033). As a
- * workaround, we dispatch to another macro and add a trailing comma.
- *
- * SLIST_ITEM_INIT(item) => SLIST_ITEM_INIT_(item, )
- * SLIST_ITEM_INIT(item, node) => SLIST_ITEM_INIT_(item, node, )
*/
-#define SLIST_ITEM_INIT(...) \
- SLIST_ITEM_INIT_(__VA_ARGS__, )
+#define SLIST_ITEM_INIT(item, ...) \
+ SLIST_ITEM_INIT_((item), LIST_NEXT_(__VA_ARGS__))
-/**
- * Now we need a way to generate either ->next or ->node.next depending on
- * whether the node parameter was passed. The approach is based on
- *
- * #define FOO(...) BAR(__VA_ARGS__, 1, 2, )
- * #define BAR(x, y, z, ...) z
- *
- * FOO(a) => 2
- * FOO(a, b) => 1
- *
- * The LIST_NEXT_() macro uses this technique:
- *
- * LIST_NEXT_() => LIST_NODE_(next, )
- * LIST_NEXT_(node, ) => LIST_NODE_(next, node, )
- */
-#define LIST_NEXT_(...) \
- LIST_NODE_(next, __VA_ARGS__)
+#define SLIST_ITEM_INIT_(item, next) \
+ LIST_VOID_(item->next = NULL)
/**
- * LIST_NODE_() dispatches to yet another macro:
+ * Get the projection for an item's next pointer.
*
- * LIST_NODE_(next, ) => LIST_NODE__(next, , . , , )
- * LIST_NODE_(next, node, ) => LIST_NODE__(next, node, , . , , )
+ * LIST_NEXT_() => next
+ * LIST_NEXT_(node) => node.next
*/
-#define LIST_NODE_(dir, ...) \
- LIST_NODE__(dir, __VA_ARGS__, . , , )
-
-/**
- * And finally, LIST_NODE__() adds the node and the dot if necessary.
- *
- * dir node ignored dot
- * v v v v
- * LIST_NODE__(next, , . , , ) => next
- * LIST_NODE__(next, node, , . , , ) => node . next
- * ^ ^ ^ ^
- * dir node ignored dot
- */
-#define LIST_NODE__(dir, node, ignored, dot, ...) \
- node dot dir
-
-/**
- * SLIST_ITEM_INIT_() uses LIST_NEXT_() to generate the right name for the list
- * node, and finally delegates to the actual implementation.
- */
-#define SLIST_ITEM_INIT_(item, ...) \
- SLIST_ITEM_INIT__((item), LIST_NEXT_(__VA_ARGS__))
-
-#define SLIST_ITEM_INIT__(item, next) \
- LIST_VOID_(item->next = NULL)
+#define LIST_NEXT_(node) \
+ BFS_VA_IF(node)(node.next)(next)
/**
* Type-checking macro for singly-linked lists.
@@ -221,13 +165,6 @@
(!SLIST_HEAD(list))
/**
- * Like container_of(), but using the head pointer instead of offsetof() since
- * we don't have the type around.
- */
-#define SLIST_CONTAINER_(tail, head, next) \
- (void *)((char *)tail - ((char *)&head->next - (char *)head))
-
-/**
* Get the tail of a singly-linked list.
*
* @list
@@ -237,14 +174,11 @@
* @return
* The last item in the list.
*/
-#define SLIST_TAIL(...) \
- SLIST_TAIL_(__VA_ARGS__, )
-
-#define SLIST_TAIL_(list, ...) \
- SLIST_TAIL__((list), LIST_NEXT_(__VA_ARGS__))
+#define SLIST_TAIL(list, ...) \
+ SLIST_TAIL_((list), LIST_NEXT_(__VA_ARGS__))
-#define SLIST_TAIL__(list, next) \
- (list->head ? SLIST_CONTAINER_(list->tail, list->head, next) : NULL)
+#define SLIST_TAIL_(list, next) \
+ (list->head ? container_of(list->tail, typeof(*list->head), next) : NULL)
/**
* Check if an item is attached to a singly-linked list.
@@ -258,13 +192,10 @@
* @return
* Whether the item is attached to the list.
*/
-#define SLIST_ATTACHED(list, ...) \
- SLIST_ATTACHED_(list, __VA_ARGS__, )
-
-#define SLIST_ATTACHED_(list, item, ...) \
- SLIST_ATTACHED__((list), (item), LIST_NEXT_(__VA_ARGS__))
+#define SLIST_ATTACHED(list, item, ...) \
+ SLIST_ATTACHED_((list), (item), LIST_NEXT_(__VA_ARGS__))
-#define SLIST_ATTACHED__(list, item, next) \
+#define SLIST_ATTACHED_(list, item, next) \
(item->next || list->tail == &item->next)
/**
@@ -281,14 +212,11 @@
* @return
* A cursor for the next item.
*/
-#define SLIST_INSERT(list, cursor, ...) \
- SLIST_INSERT_(list, cursor, __VA_ARGS__, )
+#define SLIST_INSERT(list, cursor, item, ...) \
+ SLIST_INSERT_((list), (cursor), (item), LIST_NEXT_(__VA_ARGS__))
-#define SLIST_INSERT_(list, cursor, item, ...) \
- SLIST_INSERT__((list), (cursor), (item), LIST_NEXT_(__VA_ARGS__))
-
-#define SLIST_INSERT__(list, cursor, item, next) \
- (bfs_assert(!SLIST_ATTACHED__(list, item, next)), \
+#define SLIST_INSERT_(list, cursor, item, next) \
+ (bfs_assert(!SLIST_ATTACHED_(list, item, next)), \
item->next = *cursor, \
*cursor = item, \
list->tail = item->next ? list->tail : &item->next, \
@@ -304,11 +232,8 @@
* @node (optional)
* If specified, use item->node.next rather than item->next.
*/
-#define SLIST_APPEND(list, ...) \
- SLIST_APPEND_(list, __VA_ARGS__, )
-
-#define SLIST_APPEND_(list, item, ...) \
- LIST_VOID_(SLIST_INSERT_(list, (list)->tail, item, __VA_ARGS__))
+#define SLIST_APPEND(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT(list, (list)->tail, item, __VA_ARGS__))
/**
* Add an item to the head of a singly-linked list.
@@ -320,11 +245,8 @@
* @node (optional)
* If specified, use item->node.next rather than item->next.
*/
-#define SLIST_PREPEND(list, ...) \
- SLIST_PREPEND_(list, __VA_ARGS__, )
-
-#define SLIST_PREPEND_(list, item, ...) \
- LIST_VOID_(SLIST_INSERT_(list, &(list)->head, item, __VA_ARGS__))
+#define SLIST_PREPEND(list, item, ...) \
+ LIST_VOID_(SLIST_INSERT(list, &(list)->head, item, __VA_ARGS__))
/**
* Splice a singly-linked list into another.
@@ -368,31 +290,23 @@
* @return
* The removed item.
*/
-#define SLIST_REMOVE(list, ...) \
- SLIST_REMOVE_(list, __VA_ARGS__, )
-
-#define SLIST_REMOVE_(list, cursor, ...) \
- SLIST_REMOVE__((list), (cursor), LIST_NEXT_(__VA_ARGS__))
-
-// Scratch variables for the type-safe SLIST_REMOVE() implementation.
-// Not a pointer type due to https://github.com/llvm/llvm-project/issues/109718.
-_maybe_unused
-static thread_local uintptr_t slist_prev_, slist_next_;
-
-/** Suppress -Wunused-value. */
-_maybe_unused
-static inline void *slist_cast_(uintptr_t ptr) {
- return (void *)ptr;
+#define SLIST_REMOVE(list, cursor, ...) \
+ SLIST_REMOVE_((list), (cursor), LIST_NEXT_(__VA_ARGS__))
+
+#define SLIST_REMOVE_(list, cursor, next) \
+ (list->tail = (*cursor)->next ? list->tail : cursor, \
+ (typeof(*cursor))slist_remove_(*cursor, cursor, &(*cursor)->next, sizeof(*cursor)))
+
+// Helper for SLIST_REMOVE()
+static inline void *slist_remove_(void *ret, void *cursor, void *next, size_t size) {
+ // ret = *cursor;
+ // *cursor = ret->next;
+ memcpy(cursor, next, size);
+ // ret->next = NULL;
+ memset(next, 0, size);
+ return ret;
}
-#define SLIST_REMOVE__(list, cursor, next) \
- (slist_prev_ = (uintptr_t)(void *)*cursor, \
- slist_next_ = (uintptr_t)(void *)(*cursor)->next, \
- (*cursor)->next = NULL, \
- *cursor = (void *)slist_next_, \
- list->tail = *cursor ? list->tail : cursor, \
- slist_cast_(slist_prev_))
-
/**
* Pop the head off a singly-linked list.
*
@@ -403,14 +317,8 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @return
* The popped item, or NULL if the list was empty.
*/
-#define SLIST_POP(...) \
- SLIST_POP_(__VA_ARGS__, )
-
-#define SLIST_POP_(list, ...) \
- SLIST_POP__((list), __VA_ARGS__)
-
-#define SLIST_POP__(list, ...) \
- (list->head ? SLIST_REMOVE_(list, &list->head, __VA_ARGS__) : NULL)
+#define SLIST_POP(list, ...) \
+ ((list)->head ? SLIST_REMOVE(list, &(list)->head, __VA_ARGS__) : NULL)
/**
* Loop over the items in a singly-linked list.
@@ -424,15 +332,12 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use head->node.next rather than head->next.
*/
-#define for_slist(type, item, ...) \
- for_slist_(type, item, __VA_ARGS__, )
-
-#define for_slist_(type, item, list, ...) \
- for_slist__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+#define for_slist(type, item, list, ...) \
+ for_slist_(type, item, (list), LIST_NEXT_(__VA_ARGS__))
-#define for_slist__(type, item, list, next) \
- for (type *item = list->head, *_next; \
- item && (SLIST_CHECK_(list), _next = item->next, true); \
+#define for_slist_(type, item, list, next) \
+ for (type *item = SLIST_HEAD(list), *_next; \
+ item && (_next = item->next, true); \
item = _next)
/**
@@ -447,8 +352,8 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use head->node.next rather than head->next.
*/
-#define drain_slist(type, item, ...) \
- for (type *item; (item = SLIST_POP(__VA_ARGS__));)
+#define drain_slist(type, item, list, ...) \
+ for (type *item; (item = SLIST_POP(list, __VA_ARGS__));)
/**
* Initialize a doubly-linked list.
@@ -463,11 +368,11 @@ static inline void *slist_cast_(uintptr_t ptr) {
LIST_VOID_(list->head = list->tail = NULL)
/**
- * LIST_PREV_() => prev
- * LIST_PREV_(node, ) => node.prev
+ * LIST_PREV_() => prev
+ * LIST_PREV_(node) => node.prev
*/
-#define LIST_PREV_(...) \
- LIST_NODE_(prev, __VA_ARGS__)
+#define LIST_PREV_(node) \
+ BFS_VA_IF(node)(node.prev)(prev)
/**
* Initialize a doubly-linked list item.
@@ -477,13 +382,10 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use item->node.next rather than item->next.
*/
-#define LIST_ITEM_INIT(...) \
- LIST_ITEM_INIT_(__VA_ARGS__, )
-
-#define LIST_ITEM_INIT_(item, ...) \
- LIST_ITEM_INIT__((item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+#define LIST_ITEM_INIT(item, ...) \
+ LIST_ITEM_INIT_((item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
-#define LIST_ITEM_INIT__(item, prev, next) \
+#define LIST_ITEM_INIT_(item, prev, next) \
LIST_VOID_(item->prev = item->next = NULL)
/**
@@ -496,10 +398,7 @@ static inline void *slist_cast_(uintptr_t ptr) {
* Check if a doubly-linked list is empty.
*/
#define LIST_EMPTY(list) \
- LIST_EMPTY_((list))
-
-#define LIST_EMPTY_(list) \
- (LIST_CHECK_(list), !list->head)
+ (LIST_CHECK_(list), !(list)->head)
/**
* Add an item to the tail of a doubly-linked list.
@@ -511,8 +410,8 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use item->node.{prev,next} rather than item->{prev,next}.
*/
-#define LIST_APPEND(list, ...) \
- LIST_INSERT(list, (list)->tail, __VA_ARGS__)
+#define LIST_APPEND(list, item, ...) \
+ LIST_INSERT(list, (list)->tail, item, __VA_ARGS__)
/**
* Add an item to the head of a doubly-linked list.
@@ -524,8 +423,8 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use item->node.{prev,next} rather than item->{prev,next}.
*/
-#define LIST_PREPEND(list, ...) \
- LIST_INSERT(list, NULL, __VA_ARGS__)
+#define LIST_PREPEND(list, item, ...) \
+ LIST_INSERT(list, NULL, item, __VA_ARGS__)
/**
* Check if an item is attached to a doubly-linked list.
@@ -539,13 +438,10 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @return
* Whether the item is attached to the list.
*/
-#define LIST_ATTACHED(list, ...) \
- LIST_ATTACHED_(list, __VA_ARGS__, )
-
-#define LIST_ATTACHED_(list, item, ...) \
- LIST_ATTACHED__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+#define LIST_ATTACHED(list, item, ...) \
+ LIST_ATTACHED_((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
-#define LIST_ATTACHED__(list, item, prev, next) \
+#define LIST_ATTACHED_(list, item, prev, next) \
(item->prev || item->next || list->head == item || list->tail == item)
/**
@@ -560,14 +456,11 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use item->node.{prev,next} rather than item->{prev,next}.
*/
-#define LIST_INSERT(list, cursor, ...) \
- LIST_INSERT_(list, cursor, __VA_ARGS__, )
-
-#define LIST_INSERT_(list, cursor, item, ...) \
- LIST_INSERT__((list), (cursor), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
+#define LIST_INSERT(list, cursor, item, ...) \
+ LIST_INSERT_((list), (cursor), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
-#define LIST_INSERT__(list, cursor, item, prev, next) LIST_VOID_( \
- bfs_assert(!LIST_ATTACHED__(list, item, prev, next)), \
+#define LIST_INSERT_(list, cursor, item, prev, next) LIST_VOID_( \
+ bfs_assert(!LIST_ATTACHED_(list, item, prev, next)), \
item->prev = cursor, \
item->next = cursor ? cursor->next : list->head, \
*(item->prev ? &item->prev->next : &list->head) = item, \
@@ -583,13 +476,10 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use item->node.{prev,next} rather than item->{prev,next}.
*/
-#define LIST_REMOVE(list, ...) \
- LIST_REMOVE_(list, __VA_ARGS__, )
+#define LIST_REMOVE(list, item, ...) \
+ LIST_REMOVE_((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
-#define LIST_REMOVE_(list, item, ...) \
- LIST_REMOVE__((list), (item), LIST_PREV_(__VA_ARGS__), LIST_NEXT_(__VA_ARGS__))
-
-#define LIST_REMOVE__(list, item, prev, next) LIST_VOID_( \
+#define LIST_REMOVE_(list, item, prev, next) LIST_VOID_( \
*(item->prev ? &item->prev->next : &list->head) = item->next, \
*(item->next ? &item->next->prev : &list->tail) = item->prev, \
item->prev = item->next = NULL)
@@ -606,15 +496,12 @@ static inline void *slist_cast_(uintptr_t ptr) {
* @node (optional)
* If specified, use head->node.next rather than head->next.
*/
-#define for_list(type, item, ...) \
- for_list_(type, item, __VA_ARGS__, )
-
-#define for_list_(type, item, list, ...) \
- for_list__(type, item, (list), LIST_NEXT_(__VA_ARGS__))
+#define for_list(type, item, list, ...) \
+ for_list_(type, item, (list), LIST_NEXT_(__VA_ARGS__))
-#define for_list__(type, item, list, next) \
- for (type *item = list->head, *_next; \
- item && (LIST_CHECK_(list), _next = item->next, true); \
+#define for_list_(type, item, list, next) \
+ for (type *item = (LIST_CHECK_(list), list->head), *_next; \
+ item && (_next = item->next, true); \
item = _next)
#endif // BFS_LIST_H
diff --git a/src/mtab.c b/src/mtab.c
index 40a9885..04edfbd 100644
--- a/src/mtab.c
+++ b/src/mtab.c
@@ -69,7 +69,7 @@ struct bfs_mtab {
/**
* Add an entry to the mount table.
*/
-_maybe_unused
+[[_maybe_unused]]
static int bfs_mtab_add(struct bfs_mtab *mtab, const char *path, const char *type) {
size_t path_size = strlen(path) + 1;
size_t type_size = strlen(type) + 1;
diff --git a/src/opt.c b/src/opt.c
index 49e8873..0b2837f 100644
--- a/src/opt.c
+++ b/src/opt.c
@@ -374,7 +374,7 @@ struct bfs_opt {
};
/** Log an optimization. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool opt_debug(struct bfs_opt *opt, const char *format, ...) {
if (bfs_debug_prefix(opt->ctx, DEBUG_OPT)) {
for (int i = 0; i < opt->depth; ++i) {
@@ -392,7 +392,7 @@ static bool opt_debug(struct bfs_opt *opt, const char *format, ...) {
}
/** Log a recursive call. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool opt_enter(struct bfs_opt *opt, const char *format, ...) {
int depth = opt->depth;
if (depth > 0) {
@@ -412,7 +412,7 @@ static bool opt_enter(struct bfs_opt *opt, const char *format, ...) {
}
/** Log a recursive return. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool opt_leave(struct bfs_opt *opt, const char *format, ...) {
bool debug = false;
int depth = opt->depth;
@@ -436,7 +436,7 @@ static bool opt_leave(struct bfs_opt *opt, const char *format, ...) {
}
/** Log a shallow visit. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool opt_visit(struct bfs_opt *opt, const char *format, ...) {
int depth = opt->depth;
if (depth > 0) {
@@ -456,7 +456,7 @@ static bool opt_visit(struct bfs_opt *opt, const char *format, ...) {
}
/** Log the deletion of an expression. */
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool opt_delete(struct bfs_opt *opt, const char *format, ...) {
int depth = opt->depth;
@@ -618,7 +618,7 @@ static bool is_const(const struct bfs_expr *expr) {
}
/** Warn about an expression. */
-_printf(3, 4)
+[[_printf(3, 4)]]
static bool opt_warning(const struct bfs_opt *opt, const struct bfs_expr *expr, const char *format, ...) {
if (!opt->warn) {
return false;
@@ -1623,14 +1623,19 @@ static void data_flow_icmp(struct bfs_opt *opt, const struct bfs_expr *expr, enu
/** Transfer function for -{execut,read,writ}able. */
static struct bfs_expr *data_flow_access(struct bfs_opt *opt, struct bfs_expr *expr, const struct visitor *visitor) {
- if (expr->num & R_OK) {
+ switch (expr->num) {
+ case R_OK:
data_flow_pred(opt, READABLE_PRED, true);
- }
- if (expr->num & W_OK) {
+ break;
+ case W_OK:
data_flow_pred(opt, WRITABLE_PRED, true);
- }
- if (expr->num & X_OK) {
+ break;
+ case X_OK:
data_flow_pred(opt, EXECUTABLE_PRED, true);
+ break;
+ default:
+ bfs_bug("Unknown access() mode %lld", expr->num);
+ break;
}
return expr;
@@ -1655,7 +1660,7 @@ static struct bfs_expr *data_flow_gid(struct bfs_opt *opt, struct bfs_expr *expr
gid_t gid = range->min;
bool nogroup = !bfs_getgrgid(opt->ctx->groups, gid);
if (errno == 0) {
- data_flow_pred(opt, NOGROUP_PRED, nogroup);
+ constrain_pred(&opt->after_true.preds[NOGROUP_PRED], nogroup);
}
}
@@ -1729,7 +1734,7 @@ static struct bfs_expr *data_flow_uid(struct bfs_opt *opt, struct bfs_expr *expr
uid_t uid = range->min;
bool nouser = !bfs_getpwuid(opt->ctx->users, uid);
if (errno == 0) {
- data_flow_pred(opt, NOUSER_PRED, nouser);
+ constrain_pred(&opt->after_true.preds[NOUSER_PRED], nouser);
}
}
diff --git a/src/parse.c b/src/parse.c
index 42f71cc..febab7f 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -84,8 +84,6 @@ struct bfs_parser {
enum use_color use_color;
/** Whether a -print action is implied. */
bool implicit_print;
- /** Whether the default root "." should be used. */
- bool implicit_root;
/** Whether the expression has started. */
bool expr_started;
/** Whether an information option like -help or -version was passed. */
@@ -95,20 +93,20 @@ struct bfs_parser {
/** The last non-path argument. */
char **last_arg;
- /** A "-depth"-type argument, if any. */
- char **depth_arg;
- /** A "-limit" argument, if any. */
- char **limit_arg;
- /** A "-prune" argument, if any. */
- char **prune_arg;
- /** A "-mount" argument, if any. */
- char **mount_arg;
- /** An "-xdev" argument, if any. */
- char **xdev_arg;
- /** A "-files0-from -" argument, if any. */
- char **files0_stdin_arg;
- /** An "-ok"-type expression, if any. */
- const struct bfs_expr *ok_expr;
+ /** A "-depth"-type expression, if any. */
+ const struct bfs_expr *depth_expr;
+ /** A "-limit" expression, if any. */
+ const struct bfs_expr *limit_expr;
+ /** A "-prune" expression, if any. */
+ const struct bfs_expr *prune_expr;
+ /** A "-mount" expression, if any. */
+ const struct bfs_expr *mount_expr;
+ /** An "-xdev" expression, if any. */
+ const struct bfs_expr *xdev_expr;
+ /** A "-files0-from" expression, if any. */
+ const struct bfs_expr *files0_expr;
+ /** An expression that consumes stdin, if any. */
+ const struct bfs_expr *stdin_expr;
/** The current time (maybe modified by -daystart). */
struct timespec now;
@@ -140,7 +138,7 @@ static void highlight_args(const struct bfs_ctx *ctx, char **argv, size_t argc,
/**
* Print an error message during parsing.
*/
-_printf(2, 3)
+[[_printf(2, 3)]]
static void parse_error(const struct bfs_parser *parser, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
@@ -158,7 +156,7 @@ static void parse_error(const struct bfs_parser *parser, const char *format, ...
/**
* Print an error about some command line arguments.
*/
-_printf(4, 5)
+[[_printf(4, 5)]]
static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_t argc, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
@@ -176,14 +174,14 @@ static void parse_argv_error(const struct bfs_parser *parser, char **argv, size_
/**
* Print an error about conflicting command line arguments.
*/
-_printf(6, 7)
-static void parse_conflict_error(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
+[[_printf(4, 5)]]
+static void parse_conflict_error(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
- highlight_args(ctx, argv1, argc1, highlight);
- highlight_args(ctx, argv2, argc2, highlight);
+ highlight_args(ctx, expr1->argv, expr1->argc, highlight);
+ highlight_args(ctx, expr2->argv, expr2->argc, highlight);
bfs_argv_error(ctx, highlight);
va_list args;
@@ -195,7 +193,7 @@ static void parse_conflict_error(const struct bfs_parser *parser, char **argv1,
/**
* Print an error about an expression.
*/
-_printf(3, 4)
+[[_printf(3, 4)]]
static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
@@ -210,7 +208,7 @@ static void parse_expr_error(const struct bfs_parser *parser, const struct bfs_e
/**
* Print a warning message during parsing.
*/
-_printf(2, 3)
+[[_printf(2, 3)]]
static bool parse_warning(const struct bfs_parser *parser, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
@@ -231,14 +229,14 @@ static bool parse_warning(const struct bfs_parser *parser, const char *format, .
/**
* Print a warning about conflicting command line arguments.
*/
-_printf(6, 7)
-static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1, size_t argc1, char **argv2, size_t argc2, const char *format, ...) {
+[[_printf(4, 5)]]
+static bool parse_conflict_warning(const struct bfs_parser *parser, const struct bfs_expr *expr1, const struct bfs_expr *expr2, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
bool highlight[ctx->argc];
init_highlight(ctx, highlight);
- highlight_args(ctx, argv1, argc1, highlight);
- highlight_args(ctx, argv2, argc2, highlight);
+ highlight_args(ctx, expr1->argv, expr1->argc, highlight);
+ highlight_args(ctx, expr2->argv, expr2->argc, highlight);
if (!bfs_argv_warning(ctx, highlight)) {
return false;
}
@@ -253,7 +251,7 @@ static bool parse_conflict_warning(const struct bfs_parser *parser, char **argv1
/**
* Print a warning about an expression.
*/
-_printf(3, 4)
+[[_printf(3, 4)]]
static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs_expr *expr, const char *format, ...) {
const struct bfs_ctx *ctx = parser->ctx;
@@ -269,6 +267,21 @@ static bool parse_expr_warning(const struct bfs_parser *parser, const struct bfs
}
/**
+ * Report an error if stdin is already consumed, then consume it.
+ */
+static bool consume_stdin(struct bfs_parser *parser, const struct bfs_expr *expr) {
+ if (parser->stdin_expr) {
+ parse_conflict_error(parser, parser->stdin_expr, expr,
+ "%pX and %pX can't both use standard input.\n",
+ parser->stdin_expr, expr);
+ return false;
+ }
+
+ parser->stdin_expr = expr;
+ return true;
+}
+
+/**
* Allocate a new expression.
*/
static struct bfs_expr *parse_new_expr(const struct bfs_parser *parser, bfs_eval_fn *eval_fn, size_t argc, char **argv, enum bfs_kind kind) {
@@ -383,6 +396,8 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser);
* Advance by a single token.
*/
static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size_t argc) {
+ struct bfs_ctx *ctx = parser->ctx;
+
if (kind != BFS_FLAG && kind != BFS_PATH) {
parser->expr_started = true;
}
@@ -391,6 +406,9 @@ static char **parser_advance(struct bfs_parser *parser, enum bfs_kind kind, size
parser->last_arg = parser->argv;
}
+ size_t i = parser->argv - ctx->argv;
+ ctx->kinds[i] = kind;
+
char **argv = parser->argv;
parser->argv += argc;
return argv;
@@ -414,7 +432,6 @@ static int parse_root(struct bfs_parser *parser, const char *path) {
return -1;
}
- parser->implicit_root = false;
return 0;
}
@@ -978,16 +995,16 @@ static struct bfs_expr *parse_time(struct bfs_parser *parser, int field, int arg
switch (*tail) {
case 'w':
time *= 7;
- _fallthrough;
+ [[fallthrough]];
case 'd':
time *= 24;
- _fallthrough;
+ [[fallthrough]];
case 'h':
time *= 60;
- _fallthrough;
+ [[fallthrough]];
case 'm':
time *= 60;
- _fallthrough;
+ [[fallthrough]];
case 's':
break;
default:
@@ -1158,22 +1175,33 @@ static struct bfs_expr *parse_daystart(struct bfs_parser *parser, int arg1, int
* Parse -delete.
*/
static struct bfs_expr *parse_delete(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_delete);
+ if (!expr) {
+ return NULL;
+ }
+
struct bfs_ctx *ctx = parser->ctx;
ctx->flags |= BFTW_POST_ORDER;
ctx->dangerous = true;
- parser->depth_arg = parser->argv;
-
- return parse_nullary_action(parser, eval_delete);
+ parser->depth_expr = expr;
+ return expr;
}
/**
* Parse -d.
*/
-static struct bfs_expr *parse_depth(struct bfs_parser *parser, int arg1, int arg2) {
+static struct bfs_expr *parse_depth(struct bfs_parser *parser, int flag, int arg2) {
+ struct bfs_expr *expr = flag
+ ? parse_nullary_flag(parser)
+ : parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
parser->ctx->flags |= BFTW_POST_ORDER;
- parser->depth_arg = parser->argv;
- return parse_nullary_flag(parser);
+ parser->depth_expr = expr;
+ return expr;
}
/**
@@ -1219,6 +1247,41 @@ static struct bfs_expr *parse_empty(struct bfs_parser *parser, int arg1, int arg
return expr;
}
+/** Check for unsafe relative paths in $PATH. */
+static const char *unsafe_path(const struct bfs_exec *execbuf) {
+ if (!(execbuf->flags & BFS_EXEC_CHDIR)) {
+ // Not -execdir or -okdir
+ return NULL;
+ }
+
+ const char *exe = execbuf->tmpl_argv[0];
+ if (strchr(exe, '/')) {
+ // No $PATH lookups for /foo or foo/bar
+ return NULL;
+ }
+
+ if (strstr(exe, "{}")) {
+ // Substituted paths always contain a /
+ return NULL;
+ }
+
+ const char *path = getenv("PATH");
+ while (path) {
+ if (path[0] != '/') {
+ // Relative $PATH component!
+ return path;
+ }
+
+ path = strchr(path, ':');
+ if (path) {
+ ++path;
+ }
+ }
+
+ // No relative components in $PATH
+ return NULL;
+}
+
/**
* Parse -exec(dir)?/-ok(dir)?.
*/
@@ -1241,29 +1304,21 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg
// For pipe() in bfs_spawn()
expr->ephemeral_fds = 2;
- if (execbuf->flags & BFS_EXEC_CHDIR) {
- // Check for relative paths in $PATH
- const char *path = getenv("PATH");
- while (path) {
- if (*path != '/') {
- size_t len = strcspn(path, ":");
- char *comp = strndup(path, len);
- if (comp) {
- parse_expr_error(parser, expr,
- "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp);
- free(comp);
- } else {
- parse_perror(parser, "strndup()");
- }
- return NULL;
- }
-
- path = strchr(path, ':');
- if (path) {
- ++path;
- }
+ const char *unsafe = unsafe_path(execbuf);
+ if (unsafe) {
+ size_t len = strcspn(unsafe, ":");
+ char *comp = strndup(unsafe, len);
+ if (comp) {
+ parse_expr_error(parser, expr,
+ "This action would be unsafe, since ${bld}$$PATH${rs} contains the relative path ${bld}%pq${rs}\n", comp);
+ free(comp);
+ } else {
+ parse_perror(parser, "strndup()");
}
+ return NULL;
+ }
+ if (execbuf->flags & BFS_EXEC_CHDIR) {
// To dup() the parent directory
if (execbuf->flags & BFS_EXEC_MULTI) {
++expr->persistent_fds;
@@ -1273,7 +1328,9 @@ static struct bfs_expr *parse_exec(struct bfs_parser *parser, int flags, int arg
}
if (execbuf->flags & BFS_EXEC_CONFIRM) {
- parser->ok_expr = expr;
+ if (!consume_stdin(parser, expr)) {
+ return NULL;
+ }
} else {
ctx->dangerous = true;
}
@@ -1304,11 +1361,17 @@ static struct bfs_expr *parse_exit(struct bfs_parser *parser, int arg1, int arg2
* Parse -f PATH.
*/
static struct bfs_expr *parse_f(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_ctx *ctx = parser->ctx;
+
struct bfs_expr *expr = parse_unary_flag(parser);
if (!expr) {
return NULL;
}
+ // Mark the path as a path, not a regular argument
+ size_t i = expr->argv - ctx->argv;
+ ctx->kinds[i + 1] = BFS_PATH;
+
if (parse_root(parser, expr->argv[1]) != 0) {
return NULL;
}
@@ -1325,50 +1388,14 @@ static struct bfs_expr *parse_files0_from(struct bfs_parser *parser, int arg1, i
return NULL;
}
- const char *from = expr->argv[1];
-
- FILE *file;
- if (strcmp(from, "-") == 0) {
- file = stdin;
- } else {
- file = xfopen(from, O_RDONLY | O_CLOEXEC);
- }
- if (!file) {
- parse_expr_error(parser, expr, "%s.\n", errstr());
- return NULL;
- }
-
- while (true) {
- char *path = xgetdelim(file, '\0');
- if (!path) {
- if (errno) {
- goto fail;
- } else {
- break;
- }
- }
-
- int ret = parse_root(parser, path);
- free(path);
- if (ret != 0) {
- goto fail;
- }
- }
-
- if (file == stdin) {
- parser->files0_stdin_arg = expr->argv;
- } else {
- fclose(file);
- }
-
- parser->implicit_root = false;
+ // For compatibility with GNU find,
+ //
+ // bfs -files0-from a -files0-from b
+ //
+ // should *only* use b, not a. So stash the expression here and only
+ // process the last one at the end of parsing.
+ parser->files0_expr = expr;
return expr;
-
-fail:
- if (file != stdin) {
- fclose(file);
- }
- return NULL;
}
/**
@@ -1638,11 +1665,11 @@ static struct bfs_expr *parse_limit(struct bfs_parser *parser, int arg1, int arg
}
if (expr->num <= 0) {
- parse_expr_error(parser, expr, "The ${blu}%s${rs} must be at least ${bld}1${rs}.\n", expr->argv[0]);
+ parse_expr_error(parser, expr, "The %pX must be at least ${bld}1${rs}.\n", expr);
return NULL;
}
- parser->limit_arg = expr->argv;
+ parser->limit_expr = expr;
return expr;
}
@@ -1676,7 +1703,7 @@ static struct bfs_expr *parse_mount(struct bfs_parser *parser, int arg1, int arg
}
parser->ctx->flags |= BFTW_SKIP_MOUNTS;
- parser->mount_arg = expr->argv;
+ parser->mount_expr = expr;
return expr;
}
@@ -1855,9 +1882,15 @@ static struct bfs_expr *parse_nohidden(struct bfs_parser *parser, int arg1, int
* Parse -noleaf.
*/
static struct bfs_expr *parse_noleaf(struct bfs_parser *parser, int arg1, int arg2) {
- parse_warning(parser, "${ex}%s${rs} does not apply the optimization that ${blu}%s${rs} inhibits.\n\n",
- BFS_COMMAND, parser->argv[0]);
- return parse_nullary_option(parser);
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
+ parse_expr_warning(parser, expr,
+ "${ex}%s${rs} does not apply the optimization that %px inhibits.\n\n",
+ BFS_COMMAND, expr);
+ return expr;
}
/**
@@ -1940,7 +1973,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct
who = 0;
mask = 0777;
state = MODE_WHO;
- _fallthrough;
+ [[fallthrough]];
case MODE_WHO:
switch (*i) {
@@ -1967,7 +2000,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct
case MODE_EQUALS:
expr->file_mode &= ~who;
expr->dir_mode &= ~who;
- _fallthrough;
+ [[fallthrough]];
case MODE_PLUS:
expr->file_mode |= file_change;
expr->dir_mode |= dir_change;
@@ -1977,7 +2010,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct
expr->dir_mode &= ~dir_change;
break;
}
- _fallthrough;
+ [[fallthrough]];
case MODE_ACTION:
if (who == 0) {
@@ -2062,7 +2095,7 @@ static int parse_mode(const struct bfs_parser *parser, const char *mode, struct
break;
case 'x':
file_change |= mask & 0111;
- _fallthrough;
+ [[fallthrough]];
case 'X':
dir_change |= mask & 0111;
break;
@@ -2125,7 +2158,7 @@ static struct bfs_expr *parse_perm(struct bfs_parser *parser, int field, int arg
++mode;
break;
}
- _fallthrough;
+ [[fallthrough]];
default:
expr->mode_cmp = BFS_MODE_EQUAL;
break;
@@ -2193,8 +2226,13 @@ static struct bfs_expr *parse_printx(struct bfs_parser *parser, int arg1, int ar
* Parse -prune.
*/
static struct bfs_expr *parse_prune(struct bfs_parser *parser, int arg1, int arg2) {
- parser->prune_arg = parser->argv;
- return parse_nullary_action(parser, eval_prune);
+ struct bfs_expr *expr = parse_nullary_action(parser, eval_prune);
+ if (!expr) {
+ return NULL;
+ }
+
+ parser->prune_expr = expr;
+ return expr;
}
/**
@@ -2572,9 +2610,14 @@ static struct bfs_expr *parse_xattrname(struct bfs_parser *parser, int arg1, int
* Parse -xdev.
*/
static struct bfs_expr *parse_xdev(struct bfs_parser *parser, int arg1, int arg2) {
+ struct bfs_expr *expr = parse_nullary_option(parser);
+ if (!expr) {
+ return NULL;
+ }
+
parser->ctx->flags |= BFTW_PRUNE_MOUNTS;
- parser->xdev_arg = parser->argv;
- return parse_nullary_option(parser);
+ parser->xdev_expr = expr;
+ return expr;
}
/**
@@ -3048,10 +3091,10 @@ static const struct table_entry parse_table[] = {
{"-context", BFS_TEST, parse_context, true},
{"-csince", BFS_TEST, parse_since, BFS_STAT_CTIME},
{"-ctime", BFS_TEST, parse_time, BFS_STAT_CTIME},
- {"-d", BFS_FLAG, parse_depth},
+ {"-d", BFS_FLAG, parse_depth, true},
{"-daystart", BFS_OPTION, parse_daystart},
{"-delete", BFS_ACTION, parse_delete},
- {"-depth", BFS_OPTION, parse_depth_n},
+ {"-depth", BFS_OPTION, parse_depth_n, false},
{"-empty", BFS_TEST, parse_empty},
{"-exclude", BFS_OPERATOR},
{"-exec", BFS_ACTION, parse_exec, 0},
@@ -3503,6 +3546,73 @@ static struct bfs_expr *parse_expr(struct bfs_parser *parser) {
return expr;
}
+/** Handle -files0-from after parsing. */
+static int parse_files0_roots(struct bfs_parser *parser) {
+ const struct bfs_ctx *ctx = parser->ctx;
+ const struct bfs_expr *expr = parser->files0_expr;
+
+ if (ctx->npaths > 0) {
+ bool highlight[ctx->argc];
+ init_highlight(ctx, highlight);
+ highlight_args(ctx, expr->argv, expr->argc, highlight);
+
+ for (size_t i = 0; i < ctx->argc; ++i) {
+ if (ctx->kinds[i] == BFS_PATH) {
+ highlight[i] = true;
+ }
+ }
+
+ bfs_argv_error(ctx, highlight);
+ bfs_error(ctx, "Cannot combine %pX with explicit root paths.\n", expr);
+ return -1;
+ }
+
+ const char *from = expr->argv[1];
+
+ FILE *file;
+ if (strcmp(from, "-") == 0) {
+ if (!consume_stdin(parser, expr)) {
+ return -1;
+ }
+ file = stdin;
+ } else {
+ file = xfopen(from, O_RDONLY | O_CLOEXEC);
+ }
+ if (!file) {
+ parse_expr_error(parser, expr, "%s.\n", errstr());
+ return -1;
+ }
+
+ while (true) {
+ char *path = xgetdelim(file, '\0');
+ if (!path) {
+ if (errno) {
+ goto fail;
+ } else {
+ break;
+ }
+ }
+
+ int ret = parse_root(parser, path);
+ free(path);
+ if (ret != 0) {
+ goto fail;
+ }
+ }
+
+ if (file != stdin) {
+ fclose(file);
+ }
+
+ return 0;
+
+fail:
+ if (file != stdin) {
+ fclose(file);
+ }
+ return -1;
+}
+
/**
* Parse the top-level expression.
*/
@@ -3528,12 +3638,22 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
return NULL;
}
+ if (parser->files0_expr) {
+ if (parse_files0_roots(parser) != 0) {
+ return NULL;
+ }
+ } else if (ctx->npaths == 0) {
+ if (parse_root(parser, ".") != 0) {
+ return NULL;
+ }
+ }
+
if (parser->implicit_print) {
- char **limit = parser->limit_arg;
+ const struct bfs_expr *limit = parser->limit_expr;
if (limit) {
- parse_argv_error(parser, parser->limit_arg, 2,
- "With ${blu}%s${rs}, you must specify an action explicitly; for example, ${blu}-print${rs} ${blu}%s${rs} ${bld}%s${rs}.\n",
- limit[0], limit[0], limit[1]);
+ parse_expr_error(parser, limit,
+ "With %pX, you must specify an action explicitly; for example, ${blu}-print${rs} %px.\n",
+ limit, limit);
return NULL;
}
@@ -3549,16 +3669,16 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
}
}
- if (parser->mount_arg && parser->xdev_arg) {
- parse_conflict_warning(parser, parser->mount_arg, 1, parser->xdev_arg, 1,
- "${blu}%s${rs} is redundant in the presence of ${blu}%s${rs}.\n\n",
- parser->xdev_arg[0], parser->mount_arg[0]);
+ if (parser->mount_expr && parser->xdev_expr) {
+ parse_conflict_warning(parser, parser->mount_expr, parser->xdev_expr,
+ "%px is redundant in the presence of %px.\n\n",
+ parser->xdev_expr, parser->mount_expr);
}
- if (ctx->warn && parser->depth_arg && parser->prune_arg) {
- parse_conflict_warning(parser, parser->depth_arg, 1, parser->prune_arg, 1,
- "${blu}%s${rs} does not work in the presence of ${blu}%s${rs}.\n",
- parser->prune_arg[0], parser->depth_arg[0]);
+ if (ctx->warn && parser->depth_expr && parser->prune_expr) {
+ parse_conflict_warning(parser, parser->depth_expr, parser->prune_expr,
+ "%px does not work in the presence of %px.\n",
+ parser->prune_expr, parser->depth_expr);
if (ctx->interactive) {
bfs_warning(ctx, "Do you want to continue? ");
@@ -3570,13 +3690,6 @@ static struct bfs_expr *parse_whole_expr(struct bfs_parser *parser) {
fprintf(stderr, "\n");
}
- if (parser->ok_expr && parser->files0_stdin_arg) {
- parse_conflict_error(parser, parser->ok_expr->argv, parser->ok_expr->argc, parser->files0_stdin_arg, 2,
- "${blu}%s${rs} conflicts with ${blu}%s${rs} ${bld}%s${rs}.\n",
- parser->ok_expr->argv[0], parser->files0_stdin_arg[0], parser->files0_stdin_arg[1]);
- return NULL;
- }
-
return expr;
}
@@ -3758,6 +3871,12 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
goto fail;
}
+ ctx->kinds = ZALLOC_ARRAY(enum bfs_kind, argc);
+ if (!ctx->kinds) {
+ perror("zalloc()");
+ goto fail;
+ }
+
enum use_color use_color = COLOR_AUTO;
const char *no_color = getenv("NO_COLOR");
if (no_color && *no_color) {
@@ -3806,16 +3925,14 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
.stdout_tty = stdout_tty,
.use_color = use_color,
.implicit_print = true,
- .implicit_root = true,
.just_info = false,
.excluding = false,
.last_arg = NULL,
- .depth_arg = NULL,
- .prune_arg = NULL,
- .mount_arg = NULL,
- .xdev_arg = NULL,
- .files0_stdin_arg = NULL,
- .ok_expr = NULL,
+ .depth_expr = NULL,
+ .prune_expr = NULL,
+ .mount_expr = NULL,
+ .xdev_expr = NULL,
+ .stdin_expr = NULL,
.now = ctx->now,
};
@@ -3844,12 +3961,6 @@ struct bfs_ctx *bfs_parse_cmdline(int argc, char *argv[]) {
goto fail;
}
- if (ctx->npaths == 0 && parser.implicit_root) {
- if (parse_root(&parser, ".") != 0) {
- goto fail;
- }
- }
-
if ((ctx->flags & BFTW_FOLLOW_ALL) && !ctx->unique) {
// We need bftw() to detect cycles unless -unique does it for us
ctx->flags |= BFTW_DETECT_CYCLES;
diff --git a/src/prelude.h b/src/prelude.h
index de89a6c..51f1505 100644
--- a/src/prelude.h
+++ b/src/prelude.h
@@ -77,23 +77,26 @@
/** _Bool => bool, true, false */
#include <stdbool.h>
-/**
- * C23 deprecates `noreturn void` in favour of `[[noreturn]] void`, so we expose
- * _noreturn instead with the other attributes in "bfs.h".
- */
-// #include <stdnoreturn.h>
-
/** Part of <threads.h>, but we don't use anything else from it. */
#define thread_local _Thread_local
+/** Get the type of an expression. */
+#define typeof __typeof__
+/** Get the unqualified type of an expression. */
+#define typeof_unqual __typeof_unqual__
+
#endif // !C23
-// Feature detection
+// Future C standard backports
-// https://clang.llvm.org/docs/LanguageExtensions.html#has-attribute
-#ifndef __has_attribute
-# define __has_attribute(attr) false
-#endif
+/**
+ * Get the length of an array.
+ *
+ * https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3469.htm
+ */
+#define countof(...) (sizeof(__VA_ARGS__) / sizeof(0[__VA_ARGS__]))
+
+// Feature detection
// https://clang.llvm.org/docs/LanguageExtensions.html#has-builtin
#ifndef __has_builtin
@@ -126,5 +129,8 @@
#if __has_feature(thread_sanitizer) && !defined(__SANITIZE_THREAD__)
# define __SANITIZE_THREAD__ true
#endif
+#if __has_feature(type_sanitizer) && !defined(__SANITIZE_TYPE__)
+# define __SANITIZE_TYPE__ true
+#endif
#endif // BFS_PRELUDE_H
diff --git a/src/printf.c b/src/printf.c
index 30ec201..f9fca64 100644
--- a/src/printf.c
+++ b/src/printf.c
@@ -91,7 +91,7 @@ static bool should_color(CFILE *cfile, const struct bfs_fmt *fmt) {
(void)ret
/** Return a dynamic format string. */
-_format_arg(2)
+[[_format_arg(2)]]
static const char *dyn_fmt(const char *str, const char *fake) {
bfs_assert(strcmp(str + strlen(str) - strlen(fake) + 1, fake + 1) == 0,
"Mismatched format specifiers: '%s' vs. '%s'", str, fake);
@@ -99,7 +99,7 @@ static const char *dyn_fmt(const char *str, const char *fake) {
}
/** Wrapper for fprintf(). */
-_printf(3, 4)
+[[_printf(3, 4)]]
static int bfs_fprintf(CFILE *cfile, const struct bfs_fmt *fmt, const char *fake, ...) {
va_list args;
va_start(args, fake);
@@ -562,7 +562,7 @@ static int bfs_printf_Y(CFILE *cfile, const struct bfs_fmt *fmt, const struct BF
}
/** %Z: SELinux context */
-_maybe_unused
+[[_maybe_unused]]
static int bfs_printf_Z(CFILE *cfile, const struct bfs_fmt *fmt, const struct BFTW *ftwbuf) {
char *con = bfs_getfilecon(ftwbuf);
if (!con) {
@@ -708,7 +708,7 @@ int bfs_printf_parse(const struct bfs_ctx *ctx, struct bfs_expr *expr, const cha
case '+':
case ' ':
must_be_numeric = true;
- _fallthrough;
+ [[fallthrough]];
case '-':
if (strchr(fmt.str, c)) {
bfs_expr_error(ctx, expr);
diff --git a/src/sanity.h b/src/sanity.h
index be77eef..cc8043f 100644
--- a/src/sanity.h
+++ b/src/sanity.h
@@ -8,16 +8,18 @@
#ifndef BFS_SANITY_H
#define BFS_SANITY_H
+#include "bfs.h"
#include <stddef.h>
-// Call macro(ptr, size) or macro(ptr, sizeof(*ptr))
-#define SANITIZE_CALL(...) \
- SANITIZE_CALL_(__VA_ARGS__, )
+/** Get the default size for a sanitize macro call. */
+#define SANITIZE_SIZE_(ptr, size) \
+ BFS_VA_IF(size)(size)(sizeof(*ptr))
-#define SANITIZE_CALL_(macro, ptr, ...) \
- SANITIZE_CALL__(macro, ptr, __VA_ARGS__ sizeof(*(ptr)), )
+// Call macro(ptr, size) or macro(ptr, sizeof(*ptr))
+#define SANITIZE_CALL(macro, ptr, ...) \
+ SANITIZE_CALL_(macro, ptr, SANITIZE_SIZE_(ptr, __VA_ARGS__))
-#define SANITIZE_CALL__(macro, ptr, size, ...) \
+#define SANITIZE_CALL_(macro, ptr, size) \
macro(ptr, size)
#if __SANITIZE_ADDRESS__
diff --git a/src/sighook.c b/src/sighook.c
index a87bed5..42bd811 100644
--- a/src/sighook.c
+++ b/src/sighook.c
@@ -423,7 +423,7 @@ static bool is_fatal(int sig) {
}
/** Reraise a fatal signal. */
-_noreturn
+[[_noreturn]]
static void reraise(siginfo_t *info) {
int sig = info->si_signo;
diff --git a/src/thread.c b/src/thread.c
index b3604f8..8607bca 100644
--- a/src/thread.c
+++ b/src/thread.c
@@ -24,13 +24,12 @@
} \
} while (0)
-#define THREAD_INFALLIBLE(...) \
- THREAD_INFALLIBLE_(__VA_ARGS__, 0, )
+#define THREAD_INFALLIBLE(expr, ...) \
+ THREAD_INFALLIBLE_(expr, BFS_VA_IF(__VA_ARGS__)(__VA_ARGS__)(0))
-#define THREAD_INFALLIBLE_(expr, allowed, ...) \
+#define THREAD_INFALLIBLE_(expr, allowed) \
int err = expr; \
- bfs_verify(err == 0 || err == allowed, "%s: %s", #expr, xstrerror(err)); \
- (void)0
+ bfs_verify(err == 0 || err == allowed, "%s: %s", #expr, xstrerror(err))
int thread_create(pthread_t *thread, const pthread_attr_t *attr, thread_fn *fn, void *arg) {
THREAD_FALLIBLE(pthread_create(thread, attr, fn, arg));
diff --git a/src/trie.c b/src/trie.c
index 4e0944a..0a86b55 100644
--- a/src/trie.c
+++ b/src/trie.c
@@ -129,6 +129,7 @@ struct trie_node {
* tag to distinguish internal nodes from leaves. This is safe as long
* as all dynamic allocations are aligned to more than a single byte.
*/
+ // [[_counted_by(count_ones(bitmap))]]
uintptr_t children[];
};
@@ -192,7 +193,7 @@ static unsigned char trie_leaf_nibble(const struct trie_leaf *leaf, size_t offse
}
/** Get the number of children of an internal node. */
-_trie_clones
+[[_trie_clones]]
static unsigned int trie_node_size(const struct trie_node *node) {
return count_ones((unsigned int)node->bitmap);
}
@@ -204,7 +205,7 @@ static unsigned int trie_node_size(const struct trie_node *node) {
* that case, the first mismatch between the key and the representative will be
* the depth at which to make a new branch to insert the key.
*/
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_representative(const struct trie *trie, const void *key, size_t length) {
uintptr_t ptr = trie->root;
@@ -233,7 +234,7 @@ struct trie_leaf *trie_find_str(const struct trie *trie, const char *key) {
return trie_find_mem(trie, key, strlen(key) + 1);
}
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_find_mem_impl(const struct trie *trie, const void *key, size_t length) {
struct trie_leaf *rep = trie_representative(trie, key, length);
if (rep && rep->length == length && memcmp(rep->key, key, length) == 0) {
@@ -257,7 +258,7 @@ void *trie_get_mem(const struct trie *trie, const void *key, size_t length) {
return leaf ? leaf->value : NULL;
}
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_find_postfix_impl(const struct trie *trie, const char *key) {
size_t length = strlen(key);
struct trie_leaf *rep = trie_representative(trie, key, length + 1);
@@ -302,7 +303,7 @@ static bool trie_check_prefix(struct trie_leaf *leaf, size_t skip, const char *k
}
}
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_find_prefix_impl(const struct trie *trie, const char *key) {
uintptr_t ptr = trie->root;
if (!ptr) {
@@ -456,7 +457,7 @@ static size_t trie_mismatch(const struct trie_leaf *rep, const void *key, size_t
* | Z
* +--->...
*/
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_node_insert(struct trie *trie, uintptr_t *ptr, struct trie_leaf *leaf, unsigned char nibble) {
struct trie_node *node = trie_decode_node(*ptr);
unsigned int size = trie_node_size(node);
@@ -579,7 +580,7 @@ struct trie_leaf *trie_insert_str(struct trie *trie, const char *key) {
return trie_insert_mem(trie, key, strlen(key) + 1);
}
-_trie_clones
+[[_trie_clones]]
static struct trie_leaf *trie_insert_mem_impl(struct trie *trie, const void *key, size_t length) {
struct trie_leaf *rep = trie_representative(trie, key, length);
size_t mismatch = trie_mismatch(rep, key, length);
@@ -707,7 +708,7 @@ static int trie_collapse_node(struct trie *trie, uintptr_t *parent, struct trie_
return 0;
}
-_trie_clones
+[[_trie_clones]]
static void trie_remove_impl(struct trie *trie, struct trie_leaf *leaf) {
uintptr_t *child = &trie->root;
uintptr_t *parent = NULL;
diff --git a/src/trie.h b/src/trie.h
index d8cecab..fa10670 100644
--- a/src/trie.h
+++ b/src/trie.h
@@ -21,6 +21,7 @@ struct trie_leaf {
/** The length of the key in bytes. */
size_t length;
/** The key itself, stored inline. */
+ [[_counted_by(length)]]
char key[];
};
diff --git a/src/xspawn.c b/src/xspawn.c
index 3fa4e60..efa2219 100644
--- a/src/xspawn.c
+++ b/src/xspawn.c
@@ -119,7 +119,7 @@ int bfs_spawn_destroy(struct bfs_spawn *ctx) {
#if BFS_POSIX_SPAWN >= 0
/** Set some posix_spawnattr flags. */
-_maybe_unused
+[[_maybe_unused]]
static int bfs_spawn_addflags(struct bfs_spawn *ctx, short flags) {
short prev;
errno = posix_spawnattr_getflags(&ctx->attr, &prev);
@@ -232,12 +232,28 @@ int bfs_spawn_adddup2(struct bfs_spawn *ctx, int oldfd, int newfd) {
*/
#define BFS_POSIX_SPAWNP_AFTER_FCHDIR !(__APPLE__ || __NetBSD__)
+/**
+ * NetBSD even resolves the executable before file actions with posix_spawn()!
+ */
+#define BFS_POSIX_SPAWN_AFTER_FCHDIR !__NetBSD__
+
int bfs_spawn_addfchdir(struct bfs_spawn *ctx, int fd) {
struct bfs_spawn_action *action = bfs_spawn_action(BFS_SPAWN_FCHDIR);
if (!action) {
return -1;
}
+#if __APPLE__
+ // macOS has a bug that causes EBADF when an fchdir() action refers to a
+ // file opened by the file actions
+ for_slist (struct bfs_spawn_action, prev, ctx) {
+ if (fd == prev->out_fd) {
+ bfs_spawn_clear_posix(ctx);
+ break;
+ }
+ }
+#endif
+
#if BFS_HAS_POSIX_SPAWN_ADDFCHDIR
# define BFS_POSIX_SPAWN_ADDFCHDIR posix_spawn_file_actions_addfchdir
#elif BFS_HAS_POSIX_SPAWN_ADDFCHDIR_NP
@@ -401,18 +417,40 @@ static bool bfs_resolve_relative(const struct bfs_resolver *res) {
return false;
}
+/** Check if the actions include fchdir(). */
+static bool bfs_spawn_will_chdir(const struct bfs_spawn *ctx) {
+ if (ctx) {
+ for_slist (const struct bfs_spawn_action, action, ctx) {
+ if (action->op == BFS_SPAWN_FCHDIR) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+/** Check if we can call xfaccessat() before file actions. */
+static bool bfs_can_access_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
+ if (res->exe[0] == '/') {
+ return true;
+ }
+
+ if (bfs_spawn_will_chdir(ctx)) {
+ return false;
+ }
+
+ return true;
+}
+
/** Check if we can resolve the executable before file actions. */
static bool bfs_can_resolve_early(const struct bfs_resolver *res, const struct bfs_spawn *ctx) {
if (!bfs_resolve_relative(res)) {
return true;
}
- if (ctx) {
- for_slist (const struct bfs_spawn_action, action, ctx) {
- if (action->op == BFS_SPAWN_FCHDIR) {
- return false;
- }
- }
+ if (bfs_spawn_will_chdir(ctx)) {
+ return false;
}
return true;
@@ -442,17 +480,19 @@ static int bfs_resolve_early(struct bfs_resolver *res, const char *exe, const st
};
if (bfs_can_skip_resolve(res, ctx)) {
- // Do this check eagerly, even though posix_spawn()/execv() also
- // would, because:
- //
- // - faccessat() is faster than fork()/clone() + execv()
- // - posix_spawn() is not guaranteed to report ENOENT
- if (xfaccessat(AT_FDCWD, exe, X_OK) == 0) {
- res->done = true;
- return 0;
- } else {
- return -1;
+ if (bfs_can_access_early(res, ctx)) {
+ // Do this check eagerly, even though posix_spawn()/execv() also
+ // would, because:
+ //
+ // - faccessat() is faster than fork()/clone() + execv()
+ // - posix_spawn() is not guaranteed to report ENOENT
+ if (xfaccessat(AT_FDCWD, exe, X_OK) != 0) {
+ return -1;
+ }
}
+
+ res->done = true;
+ return 0;
}
res->path = getenv("PATH");
@@ -529,13 +569,19 @@ static bool bfs_use_posix_spawn(const struct bfs_resolver *res, const struct bfs
}
#endif
+#if !BFS_POSIX_SPAWN_AFTER_FCHDIR
+ if (res->exe[0] != '/' && bfs_spawn_will_chdir(ctx)) {
+ return false;
+ }
+#endif
+
return true;
}
#endif // BFS_POSIX_SPAWN >= 0
/** Actually exec() the new process. */
-_noreturn
+[[_noreturn]]
static void bfs_spawn_exec(struct bfs_resolver *res, const struct bfs_spawn *ctx, char **argv, char **envp, const sigset_t *mask, int pipefd[2]) {
xclose(pipefd[0]);
diff --git a/tests/bfs/color_bsd.out b/tests/bfs/color_bsd.out
new file mode 100644
index 0000000..2ad656f
--- /dev/null
+++ b/tests/bfs/color_bsd.out
@@ -0,0 +1,27 @@
+$'rainbow/\e[1m'
+$'rainbow/\e[1m/'$'\e[0m'
+rainbow
+rainbow/pipe
+rainbow/broken
+rainbow/chardev_link
+rainbow/link.txt
+rainbow/sugid
+rainbow/suid
+rainbow/sticky_ow
+rainbow/ow
+rainbow/sgid
+rainbow/exec.sh
+rainbow/socket
+rainbow/sticky
+rainbow/file.dat
+rainbow/file.txt
+rainbow/lower.gz
+rainbow/lower.tar
+rainbow/lower.tar.gz
+rainbow/lu.tar.GZ
+rainbow/mh1
+rainbow/mh2
+rainbow/ul.TAR.gz
+rainbow/upper.GZ
+rainbow/upper.TAR
+rainbow/upper.TAR.GZ
diff --git a/tests/bfs/color_bsd.sh b/tests/bfs/color_bsd.sh
new file mode 100644
index 0000000..2e99f0b
--- /dev/null
+++ b/tests/bfs/color_bsd.sh
@@ -0,0 +1 @@
+LSCOLORS="exFxcXDXbxeGxdXb" bfs_diff rainbow -color
diff --git a/tests/bfs/execdir_path_relative_slash.out b/tests/bfs/execdir_path_relative_slash.out
new file mode 100644
index 0000000..62b31f6
--- /dev/null
+++ b/tests/bfs/execdir_path_relative_slash.out
@@ -0,0 +1,19 @@
+./a
+./b
+./bar
+./bar
+./basic
+./baz
+./c
+./d
+./e
+./f
+./foo
+./foo
+./foo
+./g
+./h
+./i
+./j
+./k
+./l
diff --git a/tests/bfs/execdir_path_relative_slash.sh b/tests/bfs/execdir_path_relative_slash.sh
new file mode 100644
index 0000000..fb5a924
--- /dev/null
+++ b/tests/bfs/execdir_path_relative_slash.sh
@@ -0,0 +1 @@
+PATH="foo:$PATH" bfs_diff basic -execdir /bin/sh -c 'printf "%s\\n" "$@"' sh {} +
diff --git a/tests/bfs/files0_from_root.sh b/tests/bfs/files0_from_root.sh
new file mode 100644
index 0000000..6ba5f00
--- /dev/null
+++ b/tests/bfs/files0_from_root.sh
@@ -0,0 +1,2 @@
+printf 'basic\0' >"$TEST/input"
+! invoke_bfs basic -files0-from "$TEST/input"
diff --git a/tests/bfstd.c b/tests/bfstd.c
index a43783a..6e15e2b 100644
--- a/tests/bfstd.c
+++ b/tests/bfstd.c
@@ -6,35 +6,15 @@
#include "bfstd.h"
#include "diag.h"
+#include <errno.h>
#include <langinfo.h>
+#include <limits.h>
+#include <stdint.h>
#include <stdlib.h>
#include <string.h>
-/** Check the result of xdirname()/xbasename(). */
-static void check_base_dir(const char *path, const char *dir, const char *base) {
- char *xdir = xdirname(path);
- bfs_everify(xdir, "xdirname()");
- bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
- free(xdir);
-
- char *xbase = xbasename(path);
- bfs_everify(xbase, "xbasename()");
- bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
- free(xbase);
-}
-
-/** Check the result of wordesc(). */
-static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) {
- char buf[256];
- char *end = buf + sizeof(buf);
- char *esc = wordesc(buf, end, str, flags);
-
- if (bfs_check(esc != end)) {
- bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp);
- }
-}
-
-void check_bfstd(void) {
+/** asciilen() test cases. */
+static void check_asciilen(void) {
bfs_check(asciilen("") == 0);
bfs_check(asciilen("@") == 1);
bfs_check(asciilen("@@") == 2);
@@ -49,7 +29,23 @@ void check_bfstd(void) {
bfs_check(asciilen("@@@@@@@\xFF@@@@@@a\xFF@@@@@@@") == 7);
bfs_check(asciilen("@@@@@@@@\xFF@@@@@a\xFF@@@@@@@") == 8);
bfs_check(asciilen("@@@@@@@@@\xFF@@@@a\xFF@@@@@@@") == 9);
+}
+
+/** Check the result of xdirname()/xbasename(). */
+static void check_base_dir(const char *path, const char *dir, const char *base) {
+ char *xdir = xdirname(path);
+ bfs_everify(xdir, "xdirname()");
+ bfs_check(strcmp(xdir, dir) == 0, "xdirname('%s') == '%s' (!= '%s')", path, xdir, dir);
+ free(xdir);
+ char *xbase = xbasename(path);
+ bfs_everify(xbase, "xbasename()");
+ bfs_check(strcmp(xbase, base) == 0, "xbasename('%s') == '%s' (!= '%s')", path, xbase, base);
+ free(xbase);
+}
+
+/** xdirname()/xbasename() test cases. */
+static void check_basedirs(void) {
// From man 3p basename
check_base_dir("usr", ".", "usr");
check_base_dir("usr/", ".", "usr");
@@ -61,7 +57,21 @@ void check_bfstd(void) {
check_base_dir("/usr/lib", "/usr", "lib");
check_base_dir("//usr//lib//", "//usr", "lib");
check_base_dir("/home//dwc//test", "/home//dwc", "test");
+}
+
+/** Check the result of wordesc(). */
+static void check_wordesc(const char *str, const char *exp, enum wesc_flags flags) {
+ char buf[256];
+ char *end = buf + sizeof(buf);
+ char *esc = wordesc(buf, end, str, flags);
+ if (bfs_check(esc != end)) {
+ bfs_check(strcmp(buf, exp) == 0, "wordesc('%s') == '%s' (!= '%s')", str, buf, exp);
+ }
+}
+
+/** wordesc() test cases. */
+static void check_wordescs(void) {
check_wordesc("", "\"\"", WESC_SHELL);
check_wordesc("word", "word", WESC_SHELL);
check_wordesc("two words", "\"two words\"", WESC_SHELL);
@@ -80,7 +90,123 @@ void check_bfstd(void) {
check_wordesc("\xF0\x9F\x98\x80", "\xF0\x9F\x98\x80", WESC_SHELL | WESC_TTY);
check_wordesc("\xCB\x9Cuser", "\xCB\x9Cuser", WESC_SHELL);
}
+}
+
+/** xstrto*() test cases. */
+static void check_strtox(void) {
+ short s;
+ unsigned short us;
+ int i;
+ unsigned int ui;
+ long l;
+ unsigned long ul;
+ long long ll;
+ unsigned long long ull;
+ char *end;
+
+#define check_strtouerr(err, str, end, base) \
+ do { \
+ bfs_echeck(xstrtous(str, end, base, &us) != 0 && errno == err); \
+ bfs_echeck(xstrtoui(str, end, base, &ui) != 0 && errno == err); \
+ bfs_echeck(xstrtoul(str, end, base, &ul) != 0 && errno == err); \
+ bfs_echeck(xstrtoull(str, end, base, &ull) != 0 && errno == err); \
+ } while (0)
+
+ check_strtouerr(ERANGE, "-1", NULL, 0);
+ check_strtouerr(ERANGE, "-0x1", NULL, 0);
+ check_strtouerr(EINVAL, "-", NULL, 0);
+ check_strtouerr(EINVAL, "-q", NULL, 0);
+ check_strtouerr(EINVAL, "-1q", NULL, 0);
+ check_strtouerr(EINVAL, "-0x", NULL, 0);
+
+#define check_strtoerr(err, str, end, base) \
+ do { \
+ bfs_echeck(xstrtos(str, end, base, &s) != 0 && errno == err); \
+ bfs_echeck(xstrtoi(str, end, base, &i) != 0 && errno == err); \
+ bfs_echeck(xstrtol(str, end, base, &l) != 0 && errno == err); \
+ bfs_echeck(xstrtoll(str, end, base, &ll) != 0 && errno == err); \
+ check_strtouerr(err, str, end, base); \
+ } while (0)
+
+ check_strtoerr(EINVAL, "", NULL, 0);
+ check_strtoerr(EINVAL, "", &end, 0);
+ check_strtoerr(EINVAL, " 1 ", &end, 0);
+ check_strtoerr(EINVAL, " -1", NULL, 0);
+ check_strtoerr(EINVAL, " 123", NULL, 0);
+ check_strtoerr(EINVAL, "123 ", NULL, 0);
+ check_strtoerr(EINVAL, "0789", NULL, 0);
+ check_strtoerr(EINVAL, "789A", NULL, 0);
+ check_strtoerr(EINVAL, "0x", NULL, 0);
+ check_strtoerr(EINVAL, "0x789A", NULL, 10);
+ check_strtoerr(EINVAL, "0x-1", NULL, 0);
+
+#define check_strtotype(type, min, max, fmt, fn, str, base, v, n) \
+ do { \
+ if ((n) >= min && (n) <= max) { \
+ bfs_echeck(fn(str, NULL, base, &v) == 0); \
+ bfs_check(v == (type)(n), "%s('%s') == " fmt " (!= " fmt ")", #fn, str, v, (type)(n)); \
+ } else { \
+ bfs_echeck(fn(str, NULL, base, &v) != 0 && errno == ERANGE); \
+ } \
+ } while (0)
+
+#define check_strtoint(str, base, n) \
+ do { \
+ check_strtotype( signed short, SHRT_MIN, SHRT_MAX, "%d", xstrtos, str, base, s, n); \
+ check_strtotype( signed int, INT_MIN, INT_MAX, "%d", xstrtoi, str, base, i, n); \
+ check_strtotype( signed long, LONG_MIN, LONG_MAX, "%ld", xstrtol, str, base, l, n); \
+ check_strtotype( signed long long, LLONG_MIN, LLONG_MAX, "%lld", xstrtoll, str, base, ll, n); \
+ check_strtotype(unsigned short, 0, USHRT_MAX, "%u", xstrtous, str, base, us, n); \
+ check_strtotype(unsigned int, 0, UINT_MAX, "%u", xstrtoui, str, base, ui, n); \
+ check_strtotype(unsigned long, 0, ULONG_MAX, "%lu", xstrtoul, str, base, ul, n); \
+ check_strtotype(unsigned long long, 0, ULLONG_MAX, "%llu", xstrtoull, str, base, ull, n); \
+ } while (0)
+
+ check_strtoint("123", 0, 123);
+ check_strtoint("+123", 0, 123);
+ check_strtoint("-123", 0, -123);
+
+ check_strtoint("0123", 0, 0123);
+ check_strtoint("0x789A", 0, 0x789A);
+
+ check_strtoint("0123", 10, 123);
+ check_strtoint("0789", 10, 789);
+
+ check_strtoint("123", 16, 0x123);
+
+ check_strtoint("0x7FFF", 0, 0x7FFF);
+ check_strtoint("-0x8000", 0, -0x8000);
+
+ check_strtoint("0x7FFFFFFF", 0, 0x7FFFFFFFL);
+ check_strtoint("-0x80000000", 0, -0x7FFFFFFFL - 1);
+
+ check_strtoint("0x7FFFFFFFFFFFFFFF", 0, 0x7FFFFFFFFFFFFFFFLL);
+ check_strtoint("-0x8000000000000000", 0, -0x7FFFFFFFFFFFFFFFLL - 1);
+
+#define check_strtoend(str, estr, base, n) \
+ do { \
+ bfs_echeck(xstrtoll(str, &end, base, &ll) == 0); \
+ bfs_check(ll == (n), "xstrtoll('%s') == %lld (!= %lld)", str, ll, (long long)(n)); \
+ bfs_check(strcmp(end, estr) == 0, "xstrtoll('%s'): end == '%s' (!= '%s')", str, end, estr); \
+ } while (0)
+
+ check_strtoend("123 ", " ", 0, 123);
+ check_strtoend("0789", "89", 0, 07);
+ check_strtoend("789A", "A", 0, 789);
+ check_strtoend("0xDEFG", "G", 0, 0xDEF);
+}
+
+/** xstrwidth() test cases. */
+static void check_strwidth(void) {
bfs_check(xstrwidth("Hello world") == 11);
bfs_check(xstrwidth("Hello\1world") == 10);
}
+
+void check_bfstd(void) {
+ check_asciilen();
+ check_basedirs();
+ check_wordescs();
+ check_strtox();
+ check_strwidth();
+}
diff --git a/tests/getopts.sh b/tests/getopts.sh
index 255f2fa..a16511f 100644
--- a/tests/getopts.sh
+++ b/tests/getopts.sh
@@ -5,11 +5,7 @@
## Argument parsing
-if command -v nproc &>/dev/null; then
- JOBS=$(nproc)
-else
- JOBS=1
-fi
+JOBS=$(_nproc)
MAKE=
PATTERNS=()
SUDO=()
diff --git a/tests/gnu/execdir_self.out b/tests/gnu/execdir_self.out
new file mode 100644
index 0000000..3ad0640
--- /dev/null
+++ b/tests/gnu/execdir_self.out
@@ -0,0 +1 @@
+./bar.sh
diff --git a/tests/gnu/execdir_self.sh b/tests/gnu/execdir_self.sh
new file mode 100644
index 0000000..1fc5d04
--- /dev/null
+++ b/tests/gnu/execdir_self.sh
@@ -0,0 +1,9 @@
+cd "$TEST"
+mkdir foo
+cat >foo/bar.sh <<EOF
+#!/bin/sh
+printf '%s\n' "\$@"
+EOF
+chmod +x foo/bar.sh
+
+bfs_diff . -name bar.sh -execdir {} {} \;
diff --git a/tests/gnu/files0_from_empty.sh b/tests/gnu/files0_from_empty.sh
index 85eee8f..7b42772 100644
--- a/tests/gnu/files0_from_empty.sh
+++ b/tests/gnu/files0_from_empty.sh
@@ -1 +1 @@
-! printf "\0" | invoke_bfs -files0-from -
+! printf '\0' | invoke_bfs -files0-from -
diff --git a/tests/gnu/files0_from_file_file.out b/tests/gnu/files0_from_file_file.out
new file mode 100644
index 0000000..fb683c7
--- /dev/null
+++ b/tests/gnu/files0_from_file_file.out
@@ -0,0 +1,2 @@
+basic/g
+basic/g/h
diff --git a/tests/gnu/files0_from_file_file.sh b/tests/gnu/files0_from_file_file.sh
new file mode 100644
index 0000000..1119952
--- /dev/null
+++ b/tests/gnu/files0_from_file_file.sh
@@ -0,0 +1,3 @@
+printf 'basic/c\0' >"$TEST/in1"
+printf 'basic/g\0' >"$TEST/in2"
+bfs_diff -files0-from "$TEST/in1" -files0-from "$TEST/in2"
diff --git a/tests/gnu/files0_from_ok.sh b/tests/gnu/files0_from_ok.sh
deleted file mode 100644
index 8e145ce..0000000
--- a/tests/gnu/files0_from_ok.sh
+++ /dev/null
@@ -1 +0,0 @@
-! printf "basic\0" | invoke_bfs -files0-from - -ok echo {} \;
diff --git a/tests/gnu/files0_from_stdin_ok.sh b/tests/gnu/files0_from_stdin_ok.sh
new file mode 100644
index 0000000..0283c8d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok.sh
@@ -0,0 +1 @@
+! printf 'basic\0' | invoke_bfs -files0-from - -ok echo {} \;
diff --git a/tests/gnu/files0_from_stdin_ok_file.out b/tests/gnu/files0_from_stdin_ok_file.out
new file mode 100644
index 0000000..0f6b00d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok_file.out
@@ -0,0 +1,45 @@
+
+
+
+
+
+ /j
+ /j
+!
+!-
+!-/e
+!-/e
+!/d
+!/d
+(
+(-
+(-/c
+(-/c
+(/b
+(/b
+)
+)/g
+)/g
+*
+*/m
+*/m
+,
+,/f
+,/f
+-
+-/a
+-/a
+...
+.../h
+.../h
+/n
+/n
+[
+[/k
+[/k
+\
+\/i
+\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin_ok_file.sh b/tests/gnu/files0_from_stdin_ok_file.sh
new file mode 100644
index 0000000..028df0c
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_ok_file.sh
@@ -0,0 +1,4 @@
+FILE="$TMP/$TEST.in"
+cd weirdnames
+invoke_bfs -mindepth 1 -fprintf "$FILE" "%P\0"
+yes | bfs_diff -files0-from - -ok printf '%s\n' {} \; -files0-from "$FILE"
diff --git a/tests/gnu/files0_from_stdin_stdin.out b/tests/gnu/files0_from_stdin_stdin.out
new file mode 100644
index 0000000..0f6b00d
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_stdin.out
@@ -0,0 +1,45 @@
+
+
+
+
+
+ /j
+ /j
+!
+!-
+!-/e
+!-/e
+!/d
+!/d
+(
+(-
+(-/c
+(-/c
+(/b
+(/b
+)
+)/g
+)/g
+*
+*/m
+*/m
+,
+,/f
+,/f
+-
+-/a
+-/a
+...
+.../h
+.../h
+/n
+/n
+[
+[/k
+[/k
+\
+\/i
+\/i
+{
+{/l
+{/l
diff --git a/tests/gnu/files0_from_stdin_stdin.sh b/tests/gnu/files0_from_stdin_stdin.sh
new file mode 100644
index 0000000..8f6368f
--- /dev/null
+++ b/tests/gnu/files0_from_stdin_stdin.sh
@@ -0,0 +1,2 @@
+cd weirdnames
+invoke_bfs -mindepth 1 -printf "%P\0" | bfs_diff -files0-from - -files0-from -
diff --git a/tests/gnu/follow_files0_from.out b/tests/gnu/follow_files0_from.out
new file mode 100644
index 0000000..c77d546
--- /dev/null
+++ b/tests/gnu/follow_files0_from.out
@@ -0,0 +1,42 @@
+links
+links/broken
+links/broken
+links/deeply
+links/deeply
+links/deeply/nested
+links/deeply/nested
+links/deeply/nested
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/broken
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/dir
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/file
+links/deeply/nested/link
+links/deeply/nested/link
+links/deeply/nested/link
+links/deeply/nested/link
+links/file
+links/file
+links/hardlink
+links/hardlink
+links/notdir
+links/notdir
+links/skip
+links/skip
+links/skip/broken
+links/skip/broken
+links/skip/dir
+links/skip/dir
+links/skip/file
+links/skip/file
+links/skip/link
+links/skip/link
+links/symlink
+links/symlink
diff --git a/tests/gnu/follow_files0_from.sh b/tests/gnu/follow_files0_from.sh
new file mode 100644
index 0000000..8c20f6d
--- /dev/null
+++ b/tests/gnu/follow_files0_from.sh
@@ -0,0 +1 @@
+invoke_bfs links -print0 | bfs_diff -follow -files0-from -
diff --git a/tests/gnu/ok_files0_from_stdin.sh b/tests/gnu/ok_files0_from_stdin.sh
new file mode 100644
index 0000000..2c4de7b
--- /dev/null
+++ b/tests/gnu/ok_files0_from_stdin.sh
@@ -0,0 +1 @@
+! printf 'basic\0' | invoke_bfs -ok echo {} \; -files0-from -
diff --git a/tests/gnu/ok_flush.sh b/tests/gnu/ok_flush.sh
index 87c7298..a5dc0d0 100644
--- a/tests/gnu/ok_flush.sh
+++ b/tests/gnu/ok_flush.sh
@@ -1,4 +1,4 @@
# I/O streams should be flushed before -ok prompts
-yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | tr '\0' ' ' | sed 's/?.*?/?/' >"$OUT"
+yes | invoke_bfs basic -printf '%p ? ' -ok echo found \; 2>&1 | sed 's/?.*?/?/' >"$OUT"
sort_output
diff_output
diff --git a/tests/list.c b/tests/list.c
index 5d0403f..1d70f33 100644
--- a/tests/list.c
+++ b/tests/list.c
@@ -96,4 +96,7 @@ void check_list(void) {
SLIST_APPEND(&l2, &i12);
SLIST_SPLICE(&l1, &l1.head->next, &l2);
bfs_verify(check_list_items(&l1, ARRAY(10, 11, 12, 15, 20)));
+
+ // Check the return type of SLIST_POP()
+ bfs_check(SLIST_POP(&l1)->n == 10);
}
diff --git a/tests/main.c b/tests/main.c
index 4c770bd..9240e1c 100644
--- a/tests/main.c
+++ b/tests/main.c
@@ -222,15 +222,15 @@ int main(int argc, char *argv[]) {
}
tzset();
- long jobs = 0;
+ unsigned int jobs = 0;
const char *cmd = argc > 0 ? argv[0] : "units";
int c;
while (c = getopt(argc, argv, ":j:"), c != -1) {
switch (c) {
case 'j':
- if (xstrtol(optarg, NULL, 10, &jobs) != 0 || jobs <= 0) {
- fprintf(stderr, "%s: Bad job count '%s'\n", cmd, optarg);
+ if (xstrtoui(optarg, NULL, 10, &jobs) != 0) {
+ fprintf(stderr, "%s: Bad job count '%s': %s\n", cmd, optarg, errstr());
return EXIT_FAILURE;
}
break;
@@ -243,7 +243,7 @@ int main(int argc, char *argv[]) {
}
}
- if (jobs == 0) {
+ if (!jobs) {
jobs = nproc();
}
diff --git a/tests/posix/exec_sigmask.sh b/tests/posix/exec_sigmask.sh
index d1192a4..2907458 100644
--- a/tests/posix/exec_sigmask.sh
+++ b/tests/posix/exec_sigmask.sh
@@ -11,6 +11,6 @@ mkfifo p1 p2
} &
# Write the `sh` PID to p1, then hang reading p2 until we're killed
-! invoke_bfs p1 -exec sh -c 'echo $$ >p1 && read -r _ <p2' {} + || fail
+! invoke_bfs p1 -exec bash -c 'echo $$ >p1 && read -r _ <p2' bash {} + || fail
-wait
+_wait
diff --git a/tests/posix/group_o_group.out b/tests/posix/group_o_group.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/group_o_group.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/group_o_group.sh b/tests/posix/group_o_group.sh
new file mode 100644
index 0000000..60aefc0
--- /dev/null
+++ b/tests/posix/group_o_group.sh
@@ -0,0 +1,3 @@
+# Regression test for
+# https://github.com/tavianator/bfs/issues/155
+bfs_diff basic -group 0 -o -group "$(id -g)"
diff --git a/tests/posix/root_order.out b/tests/posix/root_order.out
new file mode 100644
index 0000000..ea94276
--- /dev/null
+++ b/tests/posix/root_order.out
@@ -0,0 +1,4 @@
+basic/a
+basic/b
+basic/c/d
+basic/e/f
diff --git a/tests/posix/root_order.sh b/tests/posix/root_order.sh
new file mode 100644
index 0000000..86adf20
--- /dev/null
+++ b/tests/posix/root_order.sh
@@ -0,0 +1,6 @@
+# Root paths must be processed in order
+# https://www.austingroupbugs.net/view.php?id=1859
+
+# -size forces a stat(), which we don't want to be async
+invoke_bfs basic/{a,b,c/d,e/f} -size -1000 >"$OUT"
+diff_output
diff --git a/tests/posix/user_o_user.out b/tests/posix/user_o_user.out
new file mode 100644
index 0000000..a7ccfe4
--- /dev/null
+++ b/tests/posix/user_o_user.out
@@ -0,0 +1,19 @@
+basic
+basic/a
+basic/b
+basic/c
+basic/c/d
+basic/e
+basic/e/f
+basic/g
+basic/g/h
+basic/i
+basic/j
+basic/j/foo
+basic/k
+basic/k/foo
+basic/k/foo/bar
+basic/l
+basic/l/foo
+basic/l/foo/bar
+basic/l/foo/bar/baz
diff --git a/tests/posix/user_o_user.sh b/tests/posix/user_o_user.sh
new file mode 100644
index 0000000..7c143ae
--- /dev/null
+++ b/tests/posix/user_o_user.sh
@@ -0,0 +1,3 @@
+# Regression test for
+# https://github.com/tavianator/bfs/issues/155
+bfs_diff basic -user 0 -o -user "$(id -u)"
diff --git a/tests/ptyx.c b/tests/ptyx.c
new file mode 100644
index 0000000..bd47699
--- /dev/null
+++ b/tests/ptyx.c
@@ -0,0 +1,248 @@
+// Copyright © Tavian Barnes <tavianator@tavianator.com>
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Execute a command in a pseudo-terminal.
+ *
+ * $ ptyx [-w WIDTH] [-h HEIGHT] [--] COMMAND [ARGS...]
+ */
+
+#include "bfs.h"
+#include "bfstd.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+
+#if __has_include(<stropts.h>)
+# include <stropts.h>
+#endif
+
+#if __sun
+/**
+ * Push a STREAMS module, if it's not already there.
+ *
+ * See https://www.illumos.org/issues/9042.
+ */
+static int i_push(int fd, const char *name) {
+ int ret = ioctl(fd, I_FIND, name);
+ if (ret < 0) {
+ return ret;
+ } else if (ret == 0) {
+ return ioctl(fd, I_PUSH, name);
+ } else {
+ return 0;
+ }
+}
+#endif
+
+int main(int argc, char *argv[]) {
+ const char *cmd = argc > 0 ? argv[0] : "ptyx";
+
+/** Report an error message and exit. */
+#define die(format, ...) \
+ do { \
+ fprintf(stderr, "%s: " format "\n", cmd __VA_OPT__(,) __VA_ARGS__); \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+/** Report an error code and exit. */
+#define edie(format, ...) \
+ do { \
+ fprintf(stderr, "%s: " format ": %s\n", cmd __VA_OPT__(,) __VA_ARGS__, errstr()); \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+ unsigned short width = 0;
+ unsigned short height = 0;
+
+ // Parse the command line
+ int c;
+ while (c = getopt(argc, argv, "+:w:h:"), c != -1) {
+ switch (c) {
+ case 'w':
+ if (xstrtous(optarg, NULL, 10, &width) != 0) {
+ edie("Bad width '%s'", optarg);
+ }
+ break;
+ case 'h':
+ if (xstrtous(optarg, NULL, 10, &height) != 0) {
+ edie("Bad height '%s'", optarg);
+ }
+ break;
+ case ':':
+ die("Missing argument to -%c", optopt);
+ case '?':
+ die("Unrecognized option -%c", optopt);
+ }
+ }
+
+ if (optind >= argc) {
+ die("Missing command");
+ }
+ char **args = argv + optind;
+
+ // Create a new pty, and set it up
+ int ptm = posix_openpt(O_RDWR | O_NOCTTY);
+ if (ptm < 0) {
+ edie("posix_openpt()");
+ }
+ if (grantpt(ptm) != 0) {
+ edie("grantpt()");
+ }
+ if (unlockpt(ptm) != 0) {
+ edie("unlockpt()");
+ }
+
+ // Get the subsidiary device path
+ char *name = ptsname(ptm);
+ if (!name) {
+ edie("ptsname()");
+ }
+
+ // Open the subsidiary device
+ int pts = open(name, O_RDWR | O_NOCTTY);
+ if (pts < 0) {
+ edie("%s", name);
+ }
+
+#if __sun
+ // On Solaris/illumos, a pty doesn't behave like a terminal until we
+ // push some STREAMS modules (see ptm(4D), ptem(4M), ldterm(4M)).
+ if (i_push(pts, "ptem") != 0) {
+ die("ioctl(I_PUSH, ptem)");
+ }
+ if (i_push(pts, "ldterm") != 0) {
+ die("ioctl(I_PUSH, ldterm)");
+ }
+#endif
+
+ // A new pty starts at 0x0, which is not very useful. Instead, grab the
+ // default size from the current controlling terminal, if possible.
+ if (!width || !height) {
+ int tty = open_cterm(O_RDONLY | O_CLOEXEC);
+ if (tty >= 0) {
+ struct winsize ws;
+ if (xtcgetwinsize(tty, &ws) != 0) {
+ edie("tcgetwinsize()");
+ }
+ if (!width) {
+ width = ws.ws_col;
+ }
+ if (!height) {
+ height = ws.ws_row;
+ }
+ xclose(tty);
+ }
+ }
+ if (!width) {
+ width = 80;
+ }
+ if (!height) {
+ height = 24;
+ }
+
+ // Update the pty size
+ struct winsize ws;
+ if (xtcgetwinsize(pts, &ws) != 0) {
+ edie("tcgetwinsize()");
+ }
+ ws.ws_col = width;
+ ws.ws_row = height;
+ if (xtcsetwinsize(pts, &ws) != 0) {
+ edie("tcsetwinsize()");
+ }
+
+ // Set custom terminal attributes
+ struct termios attrs;
+ if (tcgetattr(pts, &attrs) != 0) {
+ edie("tcgetattr()");
+ }
+ attrs.c_oflag &= ~OPOST; // Don't convert \n to \r\n
+ if (tcsetattr(pts, TCSANOW, &attrs) != 0) {
+ edie("tcsetattr()");
+ }
+
+ pid_t pid = fork();
+ if (pid < 0) {
+ edie("fork()");
+ } else if (pid == 0) {
+ // Child
+ close(ptm);
+
+ // Make ourselves a session leader so we can have our own
+ // controlling terminal
+ if (setsid() < 0) {
+ edie("setsid()");
+ }
+
+#ifdef TIOCSCTTY
+ // Set the pty as the controlling terminal
+ if (ioctl(pts, TIOCSCTTY, 0) != 0) {
+ edie("ioctl(TIOCSCTTY)");
+ }
+#endif
+
+ // Redirect std{in,out,err} to the pty
+ if (dup2(pts, STDIN_FILENO) < 0
+ || dup2(pts, STDOUT_FILENO) < 0
+ || dup2(pts, STDERR_FILENO) < 0) {
+ edie("dup2()");
+ }
+ if (pts > STDERR_FILENO) {
+ xclose(pts);
+ }
+
+ // Run the requested command
+ execvp(args[0], args);
+ edie("execvp(): %s", args[0]);
+ }
+
+ // Parent
+ xclose(pts);
+
+ // Read output from the pty and copy it to stdout
+ char buf[1024];
+ while (true) {
+ ssize_t len = read(ptm, buf, sizeof(buf));
+ if (len > 0) {
+ if (xwrite(STDOUT_FILENO, buf, len) < 0) {
+ edie("write()");
+ }
+ } else if (len == 0) {
+ break;
+ } else if (errno == EINTR) {
+ continue;
+ } else if (errno == EIO) {
+ // Linux reports EIO rather than EOF when pts is closed
+ break;
+ } else {
+ die("read()");
+ }
+ }
+
+ xclose(ptm);
+
+ int wstatus;
+ if (xwaitpid(pid, &wstatus, 0) < 0) {
+ edie("waitpid()");
+ }
+
+ if (WIFEXITED(wstatus)) {
+ return WEXITSTATUS(wstatus);
+ } else if (WIFSIGNALED(wstatus)) {
+ int sig = WTERMSIG(wstatus);
+ fprintf(stderr, "%s: %s: %s\n", cmd, args[0], strsignal(sig));
+ return 128 + sig;
+ } else {
+ return 128;
+ }
+}
diff --git a/tests/run.sh b/tests/run.sh
index e3a4e3f..3ed2a9c 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -96,16 +96,13 @@ reap_test() {
wait_test() {
local pid line ret
- while true; do
+ while :; do
line=$((LINENO + 1))
- wait -n -ppid
+ _wait -n -ppid
ret=$?
if [ "${pid:-}" ]; then
break
- elif ((ret > 128)); then
- # Interrupted by signal
- continue
else
debug "${BASH_SOURCE[0]}" $line "${RED}error $ret${RST}" >&$DUPERR
exit 1
@@ -362,20 +359,12 @@ invoke_bfs() {
fi
}
-if command -v unbuffer &>/dev/null; then
- UNBUFFER=unbuffer
-elif command -v expect_unbuffer &>/dev/null; then
- UNBUFFER=expect_unbuffer
-fi
-
# Run bfs with a pseudo-terminal attached
bfs_pty() {
- test -n "${UNBUFFER:-}" || skip
-
bfs_verbose "$@"
local ret=0
- "$UNBUFFER" bash -c 'stty cols 80 rows 24 && "$@" </dev/null' bash "${BFS[@]}" "$@" || ret=$?
+ "$PTYX" -w80 -h24 -- "${BFS[@]}" "$@" || ret=$?
if ((ret > 125)); then
exit $ret
diff --git a/tests/tests.h b/tests/tests.h
index d395c7c..cd5715f 100644
--- a/tests/tests.h
+++ b/tests/tests.h
@@ -8,6 +8,7 @@
#ifndef BFS_TESTS_H
#define BFS_TESTS_H
+#include "bfs.h"
#include "bfstd.h"
#include "diag.h"
@@ -44,31 +45,23 @@ bool bfs_check_impl(bool result);
/**
* Check a condition, logging a message on failure but continuing.
*/
-#define bfs_check(...) \
- bfs_check_(#__VA_ARGS__, __VA_ARGS__, "", )
+#define bfs_check(cond, ...) \
+ bfs_check_impl((cond) || (bfs_check_(#cond, __VA_ARGS__), false))
-#define bfs_check_(str, cond, format, ...) \
- bfs_check_impl((cond) || (bfs_check__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false))
-
-#define bfs_check__(format, ...) \
- bfs_diagf(sizeof(format) > 1 \
- ? BFS_DIAG_FORMAT_("%s" format "%s") \
- : BFS_DIAG_FORMAT_("Check failed: `%s`"), \
- BFS_DIAG_ARGS_(__VA_ARGS__))
+#define bfs_check_(str, ...) \
+ BFS_VA_IF(__VA_ARGS__) \
+ (bfs_diag(__VA_ARGS__)) \
+ (bfs_diag("Check failed: `%s`", str))
/**
* Check a condition, logging the current error string on failure.
*/
-#define bfs_echeck(...) \
- bfs_echeck_(#__VA_ARGS__, __VA_ARGS__, "", )
-
-#define bfs_echeck_(str, cond, format, ...) \
- bfs_check_impl((cond) || (bfs_echeck__(format, BFS_DIAG_MSG_(format, str), __VA_ARGS__), false))
+#define bfs_echeck(cond, ...) \
+ bfs_check_impl((cond) || (bfs_echeck_(#cond, __VA_ARGS__), false))
-#define bfs_echeck__(format, ...) \
- bfs_diagf(sizeof(format) > 1 \
- ? BFS_DIAG_FORMAT_("%s" format "%s: %s") \
- : BFS_DIAG_FORMAT_("Check failed: `%s`: %s"), \
- BFS_DIAG_ARGS_(__VA_ARGS__ errstr(), ))
+#define bfs_echeck_(str, ...) \
+ BFS_VA_IF(__VA_ARGS__) \
+ (bfs_ediag(__VA_ARGS__)) \
+ (bfs_ediag("Check failed: `%s`", str))
#endif // BFS_TESTS_H
diff --git a/tests/util.sh b/tests/util.sh
index d8b7036..c998927 100644
--- a/tests/util.sh
+++ b/tests/util.sh
@@ -16,6 +16,7 @@ ROOT=$(_realpath "$(dirname -- "$TESTS")")
TESTS="$ROOT/tests"
BIN="$ROOT/bin"
MKSOCK="$BIN/tests/mksock"
+PTYX="$BIN/tests/ptyx"
XTOUCH="$BIN/tests/xtouch"
UNAME=$(uname)
@@ -33,6 +34,7 @@ stdenv() {
export LS_COLORS=""
unset BFS_COLORS
+ unset LSCOLORS
if [ "$UNAME" = Darwin ]; then
# ASan on macOS likes to report
@@ -189,3 +191,28 @@ pop_defers() {
return $ret
}
+
+## Parallelism
+
+# Get the number of processors
+_nproc() {
+ {
+ nproc \
+ || sysctl -n hw.ncpu \
+ || getconf _NPROCESSORS_ONLN \
+ || echo 1
+ } 2>/dev/null
+}
+
+# Run wait, looping if interrupted
+_wait() {
+ local ret=130
+
+ # "If wait is interrupted by a signal, the return status will be greater than 128"
+ while ((ret > 128)); do
+ ret=0
+ wait "$@" || ret=$?
+ done
+
+ return $ret
+}
diff --git a/tests/xspawn.c b/tests/xspawn.c
index 0244006..6864192 100644
--- a/tests/xspawn.c
+++ b/tests/xspawn.c
@@ -99,6 +99,22 @@ static int reset_path(char *old_path) {
return ret;
}
+/** Spawn the test binary and check for success. */
+static void check_spawnee(const char *exe, const struct bfs_spawn *ctx, char **argv, char **envp) {
+ pid_t pid = bfs_spawn(exe, ctx, argv, envp);
+ if (!bfs_echeck(pid >= 0, "bfs_spawn('%s')", exe)) {
+ return;
+ }
+
+ int wstatus;
+ bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)
+ && bfs_check(WIFEXITED(wstatus));
+ if (exited) {
+ int wexit = WEXITSTATUS(wstatus);
+ bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit);
+ }
+}
+
/** Check that we resolve executables in $PATH correctly. */
static void check_use_path(bool use_posix) {
struct bfs_spawn spawn;
@@ -133,20 +149,9 @@ static void check_use_path(bool use_posix) {
}
char *argv[] = {"xspawnee", old_path, NULL};
- pid_t pid = bfs_spawn("xspawnee", &spawn, argv, envp);
- if (!bfs_echeck(pid >= 0, "bfs_spawn()")) {
- goto path;
- }
-
- int wstatus;
- bool exited = bfs_echeck(xwaitpid(pid, &wstatus, 0) == pid)
- && bfs_check(WIFEXITED(wstatus));
- if (exited) {
- int wexit = WEXITSTATUS(wstatus);
- bfs_check(wexit == EXIT_SUCCESS, "xspawnee: exit(%d)", wexit);
- }
+ check_spawnee("xspawnee", &spawn, argv, envp);
+ check_spawnee("tests/xspawnee", &spawn, argv, envp);
-path:
bfs_echeck(reset_path(old_path) == 0);
env:
for (char **var = envp; *var; ++var) {
diff --git a/tests/xtouch.c b/tests/xtouch.c
index 5d65a4c..f33c573 100644
--- a/tests/xtouch.c
+++ b/tests/xtouch.c
@@ -217,8 +217,8 @@ int main(int argc, char *argv[]) {
}
if (marg) {
- long mode;
- if (xstrtol(marg, NULL, 8, &mode) == 0 && mode >= 0 && mode < 01000) {
+ unsigned int mode;
+ if (xstrtoui(marg, NULL, 8, &mode) == 0 && mode < 01000) {
args.fmode = args.dmode = mode;
} else {
fprintf(stderr, "%s: Invalid mode '%s'\n", cmd, marg);