commit: 76fc87fe150c9d2c9aa2a4a3dea29891ceb32bd3
parent 5342b24f9fb82f1c77c50e5bd1a079d65ae4eec5
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Thu, 23 Jan 2025 11:24:02 +0100
cmd/chmod: add -F/--reference option
Diffstat:
3 files changed, 204 insertions(+), 6 deletions(-)
diff --git a/cmd/chmod.1 b/cmd/chmod.1
@@ -12,12 +12,18 @@
.Op Fl cRv
.Ar mode
.Ar file...
+.Nm
+.Op Fl cRv
+.Fl F Ar ref_file
+.Ar file...
.Sh DESCRIPTION
.Nm
sets the permissions bits given by
.Ar mode
+or copying permissions from
+.Ar ref_file
on each given
-.Ar file .
+.Ar file ,
.Pp
.Ar mode
can be either an octal natural number between 0 and 7777, or a symbolic operation like
@@ -40,6 +46,11 @@ like so:
.Bl -tag -width Ds
.It Fl c
Print mode changes
+.It Fl F Ar ref_file
+Copy permission bits from
+.Ar ref_file
+into each
+.Ar file .
.It Fl R
Recurse into directories passed to
.It Fl v
@@ -153,5 +164,12 @@ The
and
.Fl v
options are present for compatibility with other modern systems such as BusyBox and GNU coreutils.
+Similarly the
+.Fl F
+option was created for compatibility with
+.Fl -reference
+from GNU, BusyBox and
+.Nx
+while keeping the possibility of using short options.
.Sh AUTHORS
.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/cmd/chmod.c b/cmd/chmod.c
@@ -173,16 +173,156 @@ do_fchmodat(int fd, char *mode_arg, char *name, char *acc_path, bool recursive)
return err;
}
+static int
+copy_mode(int fd, mode_t mode, char *name, char *acc_path, bool recursive)
+{
+ struct stat stats;
+ int err = 0;
+
+ if(fstatat(fd, name, &stats, AT_SYMLINK_NOFOLLOW) != 0)
+ {
+ fprintf(stderr,
+ "%s: error: Failed getting status for '%s': %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ char mode_from[11] = "";
+ symbolize_mode(stats.st_mode, mode_from);
+
+ if(mode != stats.st_mode)
+ {
+ if(fchmodat(fd, name, mode, 0) != 0)
+ {
+ fprintf(stderr,
+ "%s: error: Failed setting permissions to 0%04o for '%s': %s\n",
+ argv0,
+ mode,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ if(opt_c || opt_v)
+ {
+ char mode_to[11] = "";
+ symbolize_mode(mode, mode_to);
+
+ printf("%s: Permissions changed from 0%04o/%s to 0%04o/%s for '%s'\n",
+ argv0,
+ stats.st_mode & 07777,
+ mode_from,
+ mode & 07777,
+ mode_to,
+ acc_path);
+ }
+ }
+ else if(opt_v)
+ printf("%s: Permissions already set to 0%04o/%s for '%s'\n",
+ argv0,
+ stats.st_mode & 07777,
+ mode_from,
+ acc_path);
+
+ if(recursive && S_ISDIR(stats.st_mode))
+ {
+ int dir = openat(fd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if(dir == -1)
+ {
+ fprintf(stderr,
+ "%s: error: Couldn't open '%s' as directory: %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ DIR *dirp = fdopendir(dir);
+ if(dirp == NULL)
+ {
+ fprintf(stderr,
+ "%s: error: Couldn't get DIR entry for opened '%s': %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ while(true)
+ {
+ struct dirent *dp = readdir(dirp);
+ if(dp == NULL)
+ {
+ if(errno == 0) break;
+
+ fprintf(stderr,
+ "%s: error: Failed reading directory '%s': %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ closedir(dirp); // FIXME: unhandled error
+ errno = 0;
+ return 1;
+ }
+
+ if(strcmp(dp->d_name, ".") == 0) continue;
+ if(strcmp(dp->d_name, "..") == 0) continue;
+
+ char new_path[PATH_MAX] = "";
+ if(snprintf(new_path, PATH_MAX, "%s/%s", acc_path, dp->d_name) < 0)
+ {
+ fprintf(
+ stderr,
+ "%s: error: Couldn't concatenate '%s' into parent '%s', skipping to next entry: %s\n",
+ argv0,
+ name,
+ acc_path,
+ strerror(errno));
+ err++;
+ errno = 0;
+ continue;
+ }
+
+ // No depth counter for now, unlikely to be a problem
+ int ret = copy_mode(dir, mode, dp->d_name, new_path, true);
+ if(ret != 0) return ret;
+ }
+
+ // fdopendir allocates memory for DIR, needs closedir
+ if(closedir(dirp) != 0)
+ {
+ fprintf(stderr,
+ "%s: error: Deallocating directory entry for '%s' failed: %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+ }
+
+ return err;
+}
+
static void
usage(void)
{
- fprintf(stderr, "Usage: chmod [-cRv] <mode> <file ...>\n");
+ fprintf(stderr, "\
+Usage: chmod [-cRv] <mode> <file...>\n\
+ chmod [-cRv] -F <ref_file> <file...>\n");
}
int
main(int argc, char *argv[])
{
bool opt_R = false;
+ char *ref_file = NULL;
#ifdef HAS_GETOPT_LONG
// Strictly for GNUisms compatibility so no long-only options
@@ -190,6 +330,7 @@ main(int argc, char *argv[])
static struct option opts[] = {
{"changes", no_argument, 0, 'c'},
{"recursive", no_argument, 0, 'R'},
+ {"reference", required_argument, 0, 'F'},
{"verbose", no_argument, 0, 'v'},
{0, 0, 0, 0},
};
@@ -211,9 +352,9 @@ main(int argc, char *argv[])
#ifdef HAS_GETOPT_LONG
// Need + as first character to get POSIX-style option parsing
- c = getopt_long(argc, argv, "+:cRv", opts, NULL);
+ c = getopt_long(argc, argv, "+:cF:Rv", opts, NULL);
#else
- c = getopt(argc, argv, ":cRv");
+ c = getopt(argc, argv, ":cF:Rv");
#endif
if(c == -1) break;
@@ -222,6 +363,9 @@ main(int argc, char *argv[])
case 'c': // GNU
opt_c = true;
break;
+ case 'F': // GNU & NetBSD for --reference, utils-std for -F
+ ref_file = optarg;
+ break;
case 'R': // POSIX
opt_R = true;
break;
@@ -242,9 +386,38 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
+ if(ref_file != NULL)
+ {
+ if(argc < 1)
+ {
+ fprintf(stderr, "%s: error: Expected >=1 arguments, %d given\n", argv0, argc);
+ usage();
+ return 1;
+ }
+
+ struct stat ref_stat;
+ if(stat(ref_file, &ref_stat) != 0)
+ {
+ fprintf(stderr,
+ "%s: error: Failed to get status from reference file '%s': %s\n",
+ argv0,
+ ref_file,
+ strerror(errno));
+ return 1;
+ }
+
+ for(int i = 0; i < argc; i++)
+ {
+ int ret = copy_mode(AT_FDCWD, ref_stat.st_mode, argv[i], argv[i], opt_R);
+ if(ret != 0) return ret;
+ }
+
+ return 0;
+ }
+
if(argc < 2)
{
- fprintf(stderr, "%s: error: Expects >=2 arguments, %d given\n", argv0, argc);
+ fprintf(stderr, "%s: error: Expected >=2 arguments, %d given\n", argv0, argc);
usage();
return 1;
}
diff --git a/test-cmd/chmod.sh b/test-cmd/chmod.sh
@@ -3,12 +3,14 @@
# SPDX-License-Identifier: MPL-2.0
target="$(dirname "$0")/../cmd/chmod"
-plans=11
+plans=13
. "$(dirname "$0")/tap.sh"
tmpfile="${TMPDIR-/tmp}/test_chmod_$(date +%s)"
+ref_file="${TMPDIR-/tmp}/test_chmod_$(date +%s).ref"
touch "$tmpfile" || exit 1
+touch "$ref_file" || exit 1
t '0' "-v 0 $tmpfile" "chmod: Permissions changed from 00644/-rw-r--r-- to 00000/---------- for '${tmpfile}'
"
@@ -42,4 +44,9 @@ t '__mode:-x,+w' "-v -- -x,+w $tmpfile" "chmod: Permissions changed from 00133/-
t '__mode:-w' "-v -- -w $tmpfile" "chmod: Permissions changed from 00222/--w--w--w- to 00022/-----w--w- for '${tmpfile}'
"
+t 'ref_file' "-v -F $ref_file $tmpfile" "chmod: Permissions changed from 00022/-----w--w- to 00644/-rw-r--r-- for '${tmpfile}'
+"
+t 'ref_file:repeat' "-v -F $ref_file $tmpfile" "chmod: Permissions already set to 00644/-rw-r--r-- for '${tmpfile}'
+"
+
rm -f "$tmpfile" || exit 1