Generalized chmod function differentiating between directories and files (i.e.: with find)

bashchmodfindxargs

Is there a generalized bash function available that mimics chmod in every aspect, except that it also let's me differentiate between files and directories?

I know there's already numerous examples available, like the following, from this answer:

find /path/to/base/dir -type d -exec chmod 755 {} +
chmod 755 $(find /path/to/base/dir -type d)
find /path/to/base/dir -type d -print0 | xargs -0 chmod 755 

…but those are all with hard-coded arguments, and tedious to remember and type out.1)

I'd like to generalize it and make it dynamic, so that I will be able to do something like the following, for instance:

# for directories
chmod -d <any valid argument list for chmod> # or
chmodd <any valid argument list for chmod>


# for files
chmod -f <any valid argument list for chmod> # or
chmodf <any valid argument list for chmod>

I've tried to create a somewhat viable solution myself, but as my bash skills are sub-par and I'm not sure how to parse the correct arguments and insert them into the correct places, it's very crude and limited:

function chmodf {
  find . -mindepth 1 -type f -print0 | xargs -0 chmod "$@"
}

function chmodd {
  find . -mindepth 1 -type d -print0 | xargs -0 chmod "$@"
}

What I'd preferably like, of course, is something like the following (pseudo code):

function chmodd {
  paths = extract paths from arguments list
  recursive = extract recursive option from arguments list
  otherargs = remaining arguments
  if( recursive ) {
    find <paths> -mindepth 1 -type d -print0 | xargs -0 chmod <otherargs>
  }
  else {
    find <paths> -mindepth 1 -maxdepth 1 -type d -print0 | xargs -0 chmod <otherargs>
  }
}

Are you aware of the existence of such a function/binary already, or can you help me on my way to achieving this goal?


The main reason I want this, is that I find myself regularly needing to recursively set the setgid bit on directories, but not on files. But, as far as I am aware there's no capital letter g+S option for chmod.


1) I'm aware of the unix adagio "Do one thing and do it well", or something to that extent, but to be honest, I can't fathom how, at least to my knowledge, after nearly half a century of the existence of chmod and seeing numerous requests for this behavior, chmod has not already been amended with this functionality. It seems like such a obvious and appropriate functionality for chmod to have.

Best Answer

My attempt:

chmodx() (
   type="$1"
   shift
   cleared=
   marked=
   recursive=
   for a do
      if [ -z "$cleared" ]; then
         set -- -type "$type" -exec chmod
         cleared=y
      fi
      if [ -z "$marked" ]; then
         case "$a" in
         -- )
            set -- "$@" -- {} +
            if [ -z "$recursive" ]; then
               set -- -prune "$@"
            fi
            marked=y
            ;;
         -R|--recursive )
            recursive=y
            ;;
         * )
            set -- "$@" "$a"
            ;;
         esac
      else
         set -- "$a" "$@"
      fi
   done
   if [ -z "$marked" ]; then
      printf '`--'"'"' required\n'
      return 1
   fi
   exec find "$@"
)

alias chmodf='chmodx f'
alias chmodd='chmodx d'

The function and aliases turn

chmodf  args for chmod  --  starting points

into

find  points starting  -type f -exec chmod  args for chmod  {} +

Similarly chmodd … turns into find … -type d ….

It's mainly about rearranging arguments. There is almost no parsing. Everything before -- is treated as args for chmod. Everything after -- is treated as starting points for find. We don't want to actually run chmod recursively, but we may or may not run find recursively. Non-recursive operation is performed thanks to -prune in the right place. If there is -R or --recursive in args for chmod then it will disappear but -prune will not appear.

Notes:

  • I think the code is portable. I deliberately avoided using arrays.

  • chmodf and chmodd could be functions. Even then they should call chmodx to keep the code DRY. Implementing them as two independent functions is inelegant.

  • -- is mandatory because I decided to KISS. It's certainly possible to tell apart pathnames from other arguments without separating them with --. Actually chmod does this. It knows its own syntax, all its possible options and such; so everything else must be files to operate on. Embedding this knowledge and functionality in a shell script is WET. If you really want things to work without -- then you should rather solve your problem by improving chmod, modifying its source.

  • There are ways to abuse:

    • My code does not validate starting points, so you can inject e.g. -maxdepth 3 this way (as 3 -maxdepth because starting points become points starting).
    • Similarly there is no validation of args for chmod. If args for chmod include ; or {} + then the -exec primary will be terminated early in the line. This allows you to inject more primaries; or to break the whole command by generating invalid syntax.

    While technically possible, such tricks are ugly. If I needed to complicate things, I would definitely prefer to write a find command anew.

Related Question