I've modified the patch form the link mentioned in the original post and added few tests. Any comments are welcomed.
JM On 23.11.2016 08:41, Jakub Martisko wrote: > Hello, according to the man page: "In contrast, chmod > ignores symbolic links encountered during recursive > directory traversals". Is there any reason for this? I've > done a bit of mailing list archaeology and found this [1] > thread with patch. I might try to rewrite the patch for > current version and add some tests if there is no reason > that would discourage from having this functionality. > > JM > > > [1] > http://lists.gnu.org/archive/html/bug-coreutils/2011-12/msg00089.html >
>From dca59f52c45d54601ecb8862a3755ddb1bea900c Mon Sep 17 00:00:00 2001 From: Jakub Martisko <[email protected]> Date: Mon, 28 Nov 2016 09:13:39 +0100 Subject: [PATCH] chmod: add support for -L -P -H -h --dereference options * src/chmod.c support for options mentioned above. Achieved by usage of FTS_ bitflags. Patch based on: lists.gnu.org/archive/html/bug-coreutils/2011-12/msg00089.html * tests/chmod/symlinks.sh add test cases for the new options. * tests/local.mk add the new tests. * man/chmod.x new options mentioned. --- man/chmod.x | 4 +- src/chmod.c | 108 +++++++++++++++++++++++++++++++++++++++++++----- tests/chmod/symlinks.sh | 93 +++++++++++++++++++++++++++++++++++++++++ tests/local.mk | 1 + 4 files changed, 195 insertions(+), 11 deletions(-) create mode 100644 tests/chmod/symlinks.sh diff --git a/man/chmod.x b/man/chmod.x index 5761d36..de0f773 100644 --- a/man/chmod.x +++ b/man/chmod.x @@ -70,7 +70,9 @@ changes the permissions of the pointed-to file. In contrast, .B chmod ignores symbolic links encountered during recursive directory -traversals. +traversals. Options that modify this behavior are described in the +.B OPTIONS +section. .SH "SETUID AND SETGID BITS" .B chmod clears the set-group-ID bit of a diff --git a/src/chmod.c b/src/chmod.c index 89e2232..5cd0970 100644 --- a/src/chmod.c +++ b/src/chmod.c @@ -68,6 +68,10 @@ static mode_t umask_value; /* If true, change the modes of directories recursively. */ static bool recurse; +/* 1 if --dereference, 0 if --no-dereference, -1 if neither has been + specified. */ +static int dereference = -1; + /* If true, force silence (suppress most of error messages). */ static bool force_silent; @@ -87,7 +91,8 @@ static struct dev_ino *root_dev_ino; non-character as a pseudo short option, starting with CHAR_MAX + 1. */ enum { - NO_PRESERVE_ROOT = CHAR_MAX + 1, + DEREFERENCE_OPTION = CHAR_MAX + 1, + NO_PRESERVE_ROOT, PRESERVE_ROOT, REFERENCE_FILE_OPTION }; @@ -95,7 +100,9 @@ enum static struct option const long_options[] = { {"changes", no_argument, NULL, 'c'}, + {"dereference", no_argument, NULL, DEREFERENCE_OPTION}, {"recursive", no_argument, NULL, 'R'}, + {"no-dereference", no_argument, NULL, 'h'}, {"no-preserve-root", no_argument, NULL, NO_PRESERVE_ROOT}, {"preserve-root", no_argument, NULL, PRESERVE_ROOT}, {"quiet", no_argument, NULL, 'f'}, @@ -190,11 +197,13 @@ process_file (FTS *fts, FTSENT *ent) char const *file_full_name = ent->fts_path; char const *file = ent->fts_accpath; const struct stat *file_stats = ent->fts_statp; + struct stat stat_buf; mode_t old_mode IF_LINT ( = 0); mode_t new_mode IF_LINT ( = 0); bool ok = true; bool chmod_succeeded = false; + switch (ent->fts_info) { case FTS_DP: @@ -234,10 +243,29 @@ process_file (FTS *fts, FTSENT *ent) break; case FTS_SLNONE: - if (! force_silent) - error (0, 0, _("cannot operate on dangling symlink %s"), - quoteaf (file_full_name)); - ok = false; + if (dereference) + { + if (! force_silent) + error (0, 0, _("cannot operate on dangling symlink %s"), + quoteaf (file_full_name)); + ok = false; + + } + break; + + case FTS_SL: + if (dereference == 1) + { + if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0) + { + if (! force_silent) + error (0, errno, _("cannot dereference %s"), + quote (file_full_name)); + ok = false; + } + + file_stats = &stat_buf; + } break; case FTS_DC: /* directory that causes cycles */ @@ -270,7 +298,8 @@ process_file (FTS *fts, FTSENT *ent) if (! S_ISLNK (old_mode)) { - if (chmodat (fts->fts_cwd_fd, file, new_mode) == 0) + if (fchmodat (fts->fts_cwd_fd, file, new_mode, + dereference ? 0 : AT_SYMLINK_NOFOLLOW) == 0) chmod_succeeded = true; else { @@ -388,7 +417,15 @@ With --reference, change the mode of each FILE to that of RFILE.\n\ -v, --verbose output a diagnostic for every file processed\n\ "), stdout); fputs (_("\ - --no-preserve-root do not treat '/' specially (the default)\n\ + --dereference affect the referent of each symbolic link (this is\n\ + the default), rather than the symbolic link itself\n\ + -h, --no-dereference affect each symbolic link instead of any referenced\n\ + file (useful only on systems that can change the\n\ + ownership of a symlink)\n\ + "), stdout); + + fputs (_("\ + --no-preserve-root do not treat '/' specially (the default)\n\ --preserve-root fail to operate recursively on '/'\n\ "), stdout); fputs (_("\ @@ -396,6 +433,20 @@ With --reference, change the mode of each FILE to that of RFILE.\n\ "), stdout); fputs (_("\ -R, --recursive change files and directories recursively\n\ +\n\ +"), stdout); + fputs (_("\ +The following options modify how a hierarchy is traversed when the -R\n\ +option is also specified. If more than one is specified, only the final\n\ +one takes effect.\n\ +\n\ + -H if a command line argument is a symbolic link\n\ + to a directory, traverse it (this is the\n\ + default)\n\ + -L traverse every symbolic link to a directory\n\ + encountered\n\ + -P do not traverse any symbolic links\n\ +\n\ "), stdout); fputs (HELP_OPTION_DESCRIPTION, stdout); fputs (VERSION_OPTION_DESCRIPTION, stdout); @@ -421,6 +472,8 @@ main (int argc, char **argv) bool preserve_root = false; char const *reference_file = NULL; int c; + int bit_flags = FTS_COMFOLLOW | FTS_PHYSICAL; + initialize_main (&argc, &argv); set_program_name (argv[0]); @@ -433,13 +486,35 @@ main (int argc, char **argv) recurse = force_silent = diagnose_surprises = false; while ((c = getopt_long (argc, argv, - ("Rcfvr::w::x::X::s::t::u::g::o::a::,::+::=::" + ("HLPRcfhvr::w::x::X::s::t::u::g::o::a::,::+::=::" "0::1::2::3::4::5::6::7::"), long_options, NULL)) != -1) { switch (c) { + + case 'H': /* Traverse command-line symlinks-to-directories. */ + bit_flags = FTS_COMFOLLOW | FTS_PHYSICAL; + break; + + case 'L': /* Traverse all symlinks-to-directories. */ + bit_flags = FTS_LOGICAL; + break; + + case 'P': /* Traverse no symlinks-to-directories. */ + bit_flags = FTS_PHYSICAL; + break; + + case 'h': /* --no-dereference: affect symlinks */ + dereference = 0; + break; + + case DEREFERENCE_OPTION: /* --dereference: affect the referent + of each symlink */ + dereference = 1; + break; + case 'r': case 'w': case 'x': @@ -509,6 +584,18 @@ main (int argc, char **argv) } } + if (recurse) + { + if (bit_flags == FTS_PHYSICAL) + { + if (dereference == 1) + error (EXIT_FAILURE, 0, + _("-R --dereference requires either -H or -L")); + dereference = 0; + } + } + + if (reference_file) { if (mode) @@ -563,8 +650,9 @@ main (int argc, char **argv) root_dev_ino = NULL; } - ok = process_files (argv + optind, - FTS_COMFOLLOW | FTS_PHYSICAL | FTS_DEFER_STAT); + bit_flags |= FTS_DEFER_STAT; + + ok = process_files (argv + optind, bit_flags); return ok ? EXIT_SUCCESS : EXIT_FAILURE; } diff --git a/tests/chmod/symlinks.sh b/tests/chmod/symlinks.sh new file mode 100644 index 0000000..354f4ae --- /dev/null +++ b/tests/chmod/symlinks.sh @@ -0,0 +1,93 @@ +#!/bin/sh +# Verify that chmod works correctly with odd option combinations. + +# Copyright (C) 2004-2016 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +print_ver_ chmod + +mkdir -p ./a/b +mkdir -p ./a/c + +touch ./a/b/file +touch ./a/c/file + +#dangling link +ln -s ./a/foo ./a/link + +#link to file +ln -s ../b/file ./a/c/link +#link to folder +ln -s ./a/b/ ./a/folderlink + +#set initial modes +chmod 777 ./a/b ./a/c ./a/b/file ./a/c/file +#try using -R (with default -H) +chmod 755 -R ./a/c +ls -l ./a/b | grep -c "rwxr-xr-x">b_out +ls -lR ./a/c | grep -c "rwxr-xr-x">c_out + +echo "0">b_exp +echo "1">c_exp + +compare b_exp b_out || fail=1 +compare c_exp c_out || fail=1 + +#reset +chmod 777 ./a/b ./a/c ./a/b/file ./a/c/file +# set ./a/c ./a/c/file and ./a/b/file (through symlink) to 755 +chmod 755 -LR ./a/c + +echo "$( ls -l ./a/b ) $( ls -lR ./a/c |grep file) $( ls -l ./a )"|grep -o "rwxr-xr-x"|wc -l > out +echo "3">exp + +compare exp out || fail=1 + + +#reset +chmod 777 ./a/b ./a/c ./a/b/file ./a/c/file +# set /a/file through symlink (should try to chmod the link itself) +chmod 755 -RP ./a/c/ +ls -l ./a/b | grep -c "rwxr-xr-x">out +echo "0">exp + +compare exp out || fail=1 + +#reset +chmod 777 ./a/b ./a/c ./a/b/file ./a/c/file +# set /a/b/file through symlink +chmod 755 --dereference ./a/c/link +ls -l ./a/b | grep -c "rwxr-xr-x">out +echo "1">exp + +compare exp out || fail=1 + +#reset +chmod 777 ./a/b ./a/c ./a/b/file ./a/c/file +# set /a/b/file through symlink (should try to chmod the link itself) +chmod 755 --no-dereference ./a/c/link 2>./err +ls -l ./a/b | grep -c "rwxr-xr-x">out +echo "0">exp + +compare exp out || fail=1 + +if [ $(uname) == "Linux" ] +then +echo "chmod: changing permissions of './a/c/link': Operation not supported" >exp +compare exp err || fail=1 +fi + +Exit $fail diff --git a/tests/local.mk b/tests/local.mk index 3335002..e1b607e 100644 --- a/tests/local.mk +++ b/tests/local.mk @@ -433,6 +433,7 @@ all_tests = \ tests/chmod/thru-dangling.sh \ tests/chmod/umask-x.sh \ tests/chmod/usage.sh \ + tests/chmod/symlinks.sh \ tests/chown/deref.sh \ tests/chown/preserve-root.sh \ tests/chown/separator.sh \ -- 2.5.5
