diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:28:58 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:28:58 +0300 |
| commit | 115756d3f1b14d01db40e07035cf96069b07a320 (patch) | |
| tree | 372c2ce901fdbe2011c3ae4b494fadb2300d3dbe /corebinutils | |
| parent | 3e9a42db4c6c61e0df8e808db51a834525c4fb0c (diff) | |
| parent | 3d6a84a5af67021ba95ebe6fd5b28e1e2d979253 (diff) | |
| download | Project-Tick-115756d3f1b14d01db40e07035cf96069b07a320.tar.gz Project-Tick-115756d3f1b14d01db40e07035cf96069b07a320.zip | |
Add 'corebinutils/rmdir/' from commit '3d6a84a5af67021ba95ebe6fd5b28e1e2d979253'
git-subtree-dir: corebinutils/rmdir
git-subtree-mainline: 3e9a42db4c6c61e0df8e808db51a834525c4fb0c
git-subtree-split: 3d6a84a5af67021ba95ebe6fd5b28e1e2d979253
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/rmdir/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/rmdir/GNUmakefile | 36 | ||||
| -rw-r--r-- | corebinutils/rmdir/LICENSE | 32 | ||||
| -rw-r--r-- | corebinutils/rmdir/LICENSES/BSD-3-Clause.txt | 11 | ||||
| -rw-r--r-- | corebinutils/rmdir/README.md | 31 | ||||
| -rw-r--r-- | corebinutils/rmdir/rmdir.1 | 111 | ||||
| -rw-r--r-- | corebinutils/rmdir/rmdir.c | 258 | ||||
| -rw-r--r-- | corebinutils/rmdir/tests/test.sh | 191 |
8 files changed, 695 insertions, 0 deletions
diff --git a/corebinutils/rmdir/.gitignore b/corebinutils/rmdir/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/rmdir/.gitignore @@ -0,0 +1,25 @@ +*.a +*.core +*.lo +*.nossppico +*.o +*.orig +*.pico +*.pieo +*.po +*.rej +*.so +*.so.[0-9]* +*.sw[nop] +*~ +.*DS_Store +.cache +.clangd +.ccls-cache +.depend* +compile_commands.json +compile_commands.events.json +tags +build/ +out/ +.linux-obj/ diff --git a/corebinutils/rmdir/GNUmakefile b/corebinutils/rmdir/GNUmakefile new file mode 100644 index 0000000000..64c5ea8ea8 --- /dev/null +++ b/corebinutils/rmdir/GNUmakefile @@ -0,0 +1,36 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/rmdir +OBJS := $(OBJDIR)/rmdir.o + +.PHONY: all clean dirs status test + +all: $(TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(OBJDIR)/rmdir.o: $(CURDIR)/rmdir.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/rmdir.c" -o "$@" + +test: $(TARGET) + RMDIR_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/rmdir/LICENSE b/corebinutils/rmdir/LICENSE new file mode 100644 index 0000000000..f34c01a0e0 --- /dev/null +++ b/corebinutils/rmdir/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1990, 1993 + The Regents of the University of California. All rights reserved. + +Copyright (c) 2026 + Project Tick. All rights reserved. + +This code is derived from software contributed to Berkeley by +the Institute of Electrical and Electronics Engineers, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. diff --git a/corebinutils/rmdir/LICENSES/BSD-3-Clause.txt b/corebinutils/rmdir/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/corebinutils/rmdir/LICENSES/BSD-3-Clause.txt @@ -0,0 +1,11 @@ +Copyright (c) <year> <owner>. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/corebinutils/rmdir/README.md b/corebinutils/rmdir/README.md new file mode 100644 index 0000000000..0e6ac0cb48 --- /dev/null +++ b/corebinutils/rmdir/README.md @@ -0,0 +1,31 @@ +# rmdir + +Standalone Linux-native port of FreeBSD `rmdir` for Project Tick BSD/Linux Distribution. + +## Build + +```sh +gmake -f GNUmakefile +gmake -f GNUmakefile CC=musl-gcc +``` + +## Test + +```sh +gmake -f GNUmakefile test +gmake -f GNUmakefile test CC=musl-gcc +``` + +## Port Strategy + +- The FreeBSD utility was rewritten into a standalone Linux-native build instead of preserving BSD build glue, `err(3)`, ATF integration, or in-place mutation of operand strings. +- Directory removal maps directly to Linux `unlinkat(2)` with `AT_REMOVEDIR`, using `AT_FDCWD` for path-based operation. +- `-p` is implemented as a lexical parent walk over a duplicated path buffer. Trailing slashes are trimmed dynamically, parent components are derived without `PATH_MAX`, and removal stops at the first parent that is not empty or otherwise cannot be removed. +- Verbose output stays manpage-aligned: the explicit operand is printed after successful removal, and with `-p` each successfully removed parent is printed in removal order. + +## Supported / Unsupported Semantics + +- Supported: FreeBSD/POSIX option surface `-p` and `-v`, strict option parsing, `--` end-of-options handling, and partial-success behavior across multiple operands. +- Supported: lexical parent pruning for repeated `/` separators and trailing `/` components, without relying on GNU extensions or glibc-specific behavior. +- Unsupported by design: GNU long options and GNU-specific parsing extensions. This port keeps the FreeBSD/POSIX command-line surface strict. +- Linux-native limitation: kernel error text for unsupported removals such as `/`, non-empty directories, or non-directory operands comes from Linux `unlinkat(2)`/VFS semantics, but failures are surfaced explicitly and never silently ignored. diff --git a/corebinutils/rmdir/rmdir.1 b/corebinutils/rmdir/rmdir.1 new file mode 100644 index 0000000000..b433281109 --- /dev/null +++ b/corebinutils/rmdir/rmdir.1 @@ -0,0 +1,111 @@ +.\"- +.\" Copyright (c) 1990, 1993 +.\" The Regents of the University of California. All rights reserved. +.\" +.\" This code is derived from software contributed to Berkeley by +.\" the Institute of Electrical and Electronics Engineers, Inc. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" 3. Neither the name of the University nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.Dd March 15, 2013 +.Dt RMDIR 1 +.Os +.Sh NAME +.Nm rmdir +.Nd remove directories +.Sh SYNOPSIS +.Nm +.Op Fl pv +.Ar directory ... +.Sh DESCRIPTION +The +.Nm +utility removes the directory entry specified by +each +.Ar directory +argument, provided it is empty. +.Pp +Arguments are processed in the order given. +In order to remove both a parent directory and a subdirectory +of that parent, the subdirectory +must be specified first so the parent directory +is empty when +.Nm +tries to remove it. +.Pp +The following option is available: +.Bl -tag -width indent +.It Fl p +Each +.Ar directory +argument is treated as a pathname of which all +components will be removed, if they are empty, +starting with the last most component. +(See +.Xr rm 1 +for fully non-discriminant recursive removal.) +.It Fl v +Be verbose, listing each directory as it is removed. +.El +.Sh EXIT STATUS +The +.Nm +utility exits with one of the following values: +.Bl -tag -width indent +.It Er 0 +Each +.Ar directory +referred to an empty directory and was removed successfully. +.It Er 1 +An error occurred while attempting to remove one or more directories. +.It Er 2 +Invalid arguments. +.El +.Sh EXAMPLES +Remove the directory +.Pa foobar , +if it is empty: +.Pp +.Dl $ rmdir foobar +.Pp +Remove all directories up to and including +.Pa cow , +stopping at the first non-empty directory (if any): +.Pp +.Dl $ rmdir -p cow/horse/monkey +.Sh SEE ALSO +.Xr rm 1 , +.Xr rmdir 2 +.Sh STANDARDS +The +.Nm +utility is expected to be +.St -p1003.2 +compatible. +.Sh HISTORY +A +.Nm +command appeared in +.At v1 . diff --git a/corebinutils/rmdir/rmdir.c b/corebinutils/rmdir/rmdir.c new file mode 100644 index 0000000000..a7bcf7389a --- /dev/null +++ b/corebinutils/rmdir/rmdir.c @@ -0,0 +1,258 @@ +#define _POSIX_C_SOURCE 200809L + +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1992, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * Copyright (c) 2026 + * Project Tick. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +typedef struct { + const char *progname; + bool remove_parents; + bool verbose; + int exit_status; +} options_t; + +static void error_errno(options_t *options, const char *fmt, ...); +static int print_removed(options_t *options, const char *path); +static const char *program_basename(const char *path); +static size_t parent_path_length(const char *path); +static int remove_directory(options_t *options, const char *path, + const char *display_path); +static int remove_operand(options_t *options, const char *path); +static int remove_parent_directories(options_t *options, const char *path); +static void trim_trailing_slashes(char *path); +static void usage(void) __attribute__((noreturn)); +static void *xmalloc(size_t size); + +static const char * +program_basename(const char *path) +{ + const char *slash; + + if (path == NULL || path[0] == '\0') { + return "rmdir"; + } + + slash = strrchr(path, '/'); + return slash == NULL ? path : slash + 1; +} + +static void +verror_message(options_t *options, bool with_errno, const char *fmt, va_list ap) +{ + int saved_errno; + + saved_errno = errno; + (void)fprintf(stderr, "%s: ", options->progname); + (void)vfprintf(stderr, fmt, ap); + if (with_errno) { + (void)fprintf(stderr, ": %s", strerror(saved_errno)); + } + (void)fputc('\n', stderr); +} + +static void +error_errno(options_t *options, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + verror_message(options, true, fmt, ap); + va_end(ap); + options->exit_status = 1; +} + +static void * +xmalloc(size_t size) +{ + void *ptr; + + ptr = malloc(size); + if (ptr == NULL) { + (void)fprintf(stderr, "rmdir: out of memory\n"); + exit(1); + } + return ptr; +} + +static int +print_removed(options_t *options, const char *path) +{ + if (!options->verbose) { + return 0; + } + if (printf("%s\n", path) < 0) { + error_errno(options, "stdout"); + return 1; + } + return 0; +} + +static int +remove_directory(options_t *options, const char *path, const char *display_path) +{ + if (unlinkat(AT_FDCWD, path, AT_REMOVEDIR) == -1) { + error_errno(options, "%s", display_path); + return 1; + } + return print_removed(options, display_path); +} + +static void +trim_trailing_slashes(char *path) +{ + size_t len; + + len = strlen(path); + while (len > 1 && path[len - 1] == '/') { + path[len - 1] = '\0'; + --len; + } +} + +static size_t +parent_path_length(const char *path) +{ + size_t len; + + len = strlen(path); + while (len > 0 && path[len - 1] == '/') { + --len; + } + while (len > 0 && path[len - 1] != '/') { + --len; + } + while (len > 0 && path[len - 1] == '/') { + --len; + } + + if (len == 0 && path[0] == '/') { + return 1; + } + return len; +} + +static int +remove_parent_directories(options_t *options, const char *path) +{ + char *copy; + size_t parent_len; + + copy = xmalloc(strlen(path) + 1); + memcpy(copy, path, strlen(path) + 1); + trim_trailing_slashes(copy); + + for (;;) { + parent_len = parent_path_length(copy); + if (parent_len == 0 || (parent_len == 1 && copy[0] == '/')) { + break; + } + + copy[parent_len] = '\0'; + if (remove_directory(options, copy, copy) != 0) { + free(copy); + return 1; + } + } + + free(copy); + return 0; +} + +static int +remove_operand(options_t *options, const char *path) +{ + int status; + + status = remove_directory(options, path, path); + if (status != 0 || !options->remove_parents) { + return status; + } + return remove_parent_directories(options, path); +} + +static void +usage(void) +{ + (void)fprintf(stderr, "%s\n", "usage: rmdir [-pv] directory ..."); + exit(2); +} + +int +main(int argc, char *argv[]) +{ + options_t options; + int ch; + int i; + + options.progname = program_basename(argv[0]); + options.remove_parents = false; + options.verbose = false; + options.exit_status = 0; + + opterr = 0; + while ((ch = getopt(argc, argv, "pv")) != -1) { + switch (ch) { + case 'p': + options.remove_parents = true; + break; + case 'v': + options.verbose = true; + break; + case '?': + default: + usage(); + } + } + + if (optind >= argc) { + usage(); + } + + for (i = optind; i < argc; ++i) { + (void)remove_operand(&options, argv[i]); + } + + if (fflush(stdout) == EOF) { + error_errno(&options, "stdout"); + } + + return options.exit_status; +} diff --git a/corebinutils/rmdir/tests/test.sh b/corebinutils/rmdir/tests/test.sh new file mode 100644 index 0000000000..bef29e525f --- /dev/null +++ b/corebinutils/rmdir/tests/test.sh @@ -0,0 +1,191 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +RMDIR_BIN=${RMDIR_BIN:-"$ROOT/out/rmdir"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/rmdir-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +LAST_STATUS=0 +LAST_STDOUT= +LAST_STDERR= +trap 'chmod -R u+rwx "$WORKDIR" 2>/dev/null || true; rm -rf "$WORKDIR"' EXIT INT TERM + +export LC_ALL=C + +USAGE_TEXT='usage: rmdir [-pv] directory ...' + +fail() { + printf '%s\n' "FAIL: $1" >&2 + exit 1 +} + +assert_eq() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" != "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected ---" >&2 + printf '%s' "$expected" >&2 + printf '\n%s\n' "--- actual ---" >&2 + printf '%s' "$actual" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_contains() { + name=$1 + text=$2 + pattern=$3 + case $text in + *"$pattern"*) ;; + *) fail "$name" ;; + esac +} + +assert_empty() { + name=$1 + text=$2 + if [ -n "$text" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected empty ---" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s' "$text" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected status: $expected" >&2 + printf '%s\n' "actual status: $actual" >&2 + exit 1 + fi +} + +run_capture() { + if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + LAST_STATUS=0 + else + LAST_STATUS=$? + fi + LAST_STDOUT=$(cat "$STDOUT_FILE") + LAST_STDERR=$(cat "$STDERR_FILE") +} + +[ -x "$RMDIR_BIN" ] || fail "missing binary: $RMDIR_BIN" + +run_capture "$RMDIR_BIN" +assert_status "usage status" 2 "$LAST_STATUS" +assert_empty "usage stdout" "$LAST_STDOUT" +assert_eq "usage stderr" "$USAGE_TEXT" "$LAST_STDERR" + +run_capture "$RMDIR_BIN" -z +assert_status "invalid option status" 2 "$LAST_STATUS" +assert_empty "invalid option stdout" "$LAST_STDOUT" +assert_eq "invalid option stderr" "$USAGE_TEXT" "$LAST_STDERR" + +run_capture "$RMDIR_BIN" -- +assert_status "dash-dash without operands status" 2 "$LAST_STATUS" +assert_empty "dash-dash without operands stdout" "$LAST_STDOUT" +assert_eq "dash-dash without operands stderr" "$USAGE_TEXT" "$LAST_STDERR" + +mkdir "$WORKDIR/basic" +run_capture "$RMDIR_BIN" "$WORKDIR/basic" +assert_status "basic remove status" 0 "$LAST_STATUS" +assert_empty "basic remove stdout" "$LAST_STDOUT" +assert_empty "basic remove stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/basic" ] || fail "basic directory still exists" + +mkdir "$WORKDIR/verbose" +run_capture "$RMDIR_BIN" -v "$WORKDIR/verbose" +assert_status "verbose remove status" 0 "$LAST_STATUS" +assert_eq "verbose remove stdout" "$WORKDIR/verbose" "$LAST_STDOUT" +assert_empty "verbose remove stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/verbose" ] || fail "verbose directory still exists" + +mkdir "$WORKDIR/non-empty" +printf 'payload\n' >"$WORKDIR/non-empty/file" +run_capture "$RMDIR_BIN" "$WORKDIR/non-empty" +assert_status "non-empty status" 1 "$LAST_STATUS" +assert_empty "non-empty stdout" "$LAST_STDOUT" +assert_contains "non-empty stderr path" "$LAST_STDERR" "$WORKDIR/non-empty" +[ -d "$WORKDIR/non-empty" ] || fail "non-empty directory removed unexpectedly" + +run_capture "$RMDIR_BIN" "$WORKDIR/missing" +assert_status "missing status" 1 "$LAST_STATUS" +assert_empty "missing stdout" "$LAST_STDOUT" +assert_contains "missing stderr path" "$LAST_STDERR" "$WORKDIR/missing" + +printf 'plain\n' >"$WORKDIR/plain-file" +run_capture "$RMDIR_BIN" "$WORKDIR/plain-file" +assert_status "file operand status" 1 "$LAST_STATUS" +assert_empty "file operand stdout" "$LAST_STDOUT" +assert_contains "file operand stderr path" "$LAST_STDERR" "$WORKDIR/plain-file" +[ -f "$WORKDIR/plain-file" ] || fail "file operand changed unexpectedly" + +mkdir -p "$WORKDIR/prune/a/b" +run_capture sh -c 'cd "$1" && "$2" -p prune/a/b' sh "$WORKDIR" "$RMDIR_BIN" +assert_status "parent prune status" 0 "$LAST_STATUS" +assert_empty "parent prune stdout" "$LAST_STDOUT" +assert_empty "parent prune stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/prune/a/b" ] || fail "leaf directory still exists" +[ ! -e "$WORKDIR/prune/a" ] || fail "parent directory still exists" +[ ! -e "$WORKDIR/prune" ] || fail "root prune directory still exists" + +mkdir -p "$WORKDIR/verbose-prune/one/two" +run_capture sh -c 'cd "$1" && "$2" -pv verbose-prune/one/two' sh "$WORKDIR" "$RMDIR_BIN" +assert_status "verbose prune status" 0 "$LAST_STATUS" +assert_eq "verbose prune stdout" "verbose-prune/one/two +verbose-prune/one +verbose-prune" "$LAST_STDOUT" +assert_empty "verbose prune stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/verbose-prune" ] || fail "verbose prune root still exists" + +mkdir -p "$WORKDIR/stop/a/b" +mkdir "$WORKDIR/stop/a/keep" +run_capture sh -c 'cd "$1" && "$2" -p stop/a/b' sh "$WORKDIR" "$RMDIR_BIN" +assert_status "parent stop status" 1 "$LAST_STATUS" +assert_empty "parent stop stdout" "$LAST_STDOUT" +assert_contains "parent stop stderr path" "$LAST_STDERR" "stop/a" +[ ! -e "$WORKDIR/stop/a/b" ] || fail "parent stop leaf still exists" +[ -d "$WORKDIR/stop/a" ] || fail "parent stop parent removed unexpectedly" +[ -d "$WORKDIR/stop" ] || fail "parent stop root removed unexpectedly" + +mkdir -p "$WORKDIR/slashy/a/b" +run_capture sh -c 'cd "$1" && "$2" -pv slashy//a///b///' sh "$WORKDIR" "$RMDIR_BIN" +assert_status "slashy prune status" 0 "$LAST_STATUS" +assert_eq "slashy prune stdout" "slashy//a///b/// +slashy//a +slashy" "$LAST_STDOUT" +assert_empty "slashy prune stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/slashy" ] || fail "slashy root still exists" + +mkdir "$WORKDIR/-dash" +run_capture "$RMDIR_BIN" -- "$WORKDIR/-dash" +assert_status "dash operand status" 0 "$LAST_STATUS" +assert_empty "dash operand stdout" "$LAST_STDOUT" +assert_empty "dash operand stderr" "$LAST_STDERR" +[ ! -e "$WORKDIR/-dash" ] || fail "dash operand still exists" + +mkdir "$WORKDIR/multi-ok" +run_capture "$RMDIR_BIN" "$WORKDIR/multi-ok" "$WORKDIR/multi-missing" +assert_status "multi operand partial failure status" 1 "$LAST_STATUS" +assert_empty "multi operand partial failure stdout" "$LAST_STDOUT" +assert_contains "multi operand partial failure stderr" "$LAST_STDERR" "$WORKDIR/multi-missing" +[ ! -e "$WORKDIR/multi-ok" ] || fail "multi operand successful removal failed" + +run_capture "$RMDIR_BIN" / +assert_status "root operand status" 1 "$LAST_STATUS" +assert_empty "root operand stdout" "$LAST_STDOUT" +assert_contains "root operand stderr path" "$LAST_STDERR" "/" + +printf '%s\n' "PASS" |
