MacOS – How to change group owner on a large directory containing locked files

command linemacospermission

The command to change the group owner on a whole directory is:

chgrp -R newgroup dir_name

For example:

chgrp -R staff .

But when dir_name contains locked files or directories, this command will fail with:

chgrp -R staff .
chgrp: ./file.txt: Operation not permitted

If you run the command with super user privileges it will fail too:

/usr/bin/sudo chgrp -R staff .
Password:
chgrp: ./file.txt: Operation not permitted

If the number of files locked is small, there is a manual workaround
through the use of the command chflags.

Check that there is a specific flag (uchg) associated with a given locked file:

ls -@delO file.txt 
-rw-r--r--  1 bob  staff  uchg 32 Aug  8 11:38 file.txt

Remove temporarily the uchg flag:

chflags nouchg file.txt

Change recursively the group:

chgrp -R admin .

Reset the initial uchg flag:

chflags uchg file.txt

But this method can't adapt to a huge directory structure with hundreds
of locked files. This receipe doesn't scale at all.


What is the basic simple method to change the group owner of a whole directory structure containing locked files, while preserving this locked
status?

The same problem arises with many commands to manage directories containing locked files but I focused here on a practical
and simple case.

Best Answer

The following is an example of how I would workaround the issue by creating a wrapper script for the chgrp command to act upon a target directory having issues with locked files.

Using a bash script, named chgrpld, I'd pass it the same command line arguments (options, operand and directory) I would if executing chgrp on a directory. Note that my naming convention was simply to add the letters ld to it representing locked directory, in that it works on directories that are locked, or not, and or containing files/directories that are locked too. So where I'd normally use, e.g., chgrp -R staff foo, where foo is a directory, I'd use chgrpld -R staff foo instead. The script is also coded to work on a directory that is not locked and or does not contain locked filesystem objects as well, thus allow the use of chgrpld as a regular replacement for chgrp when targeting a directory, if I don't want to check for locked filesystem objects first or have chgrp fail first to then have to use chgrpld anyway.

Note: This script employes a limited amount of error checking, enough to work under limited testing conditions. It is not coded to handle an embedded newline in the name of a filesystem object, which IMO doesn't belong there in the first place! Feel free to modify as needed/wanted to suite your needs, adding additional error checking if/when wanted/needed.

The testing environment was OS X 10.11.5 in a temporary directory, as shown in the listing below, having two directories, each with one file in it and one of the files locked. One directory/file set is in the admin group and the other set is in the staff group and I'm wanting everything to be in the staff group. (See Testing Note at end of answer for additional testing information.)

The Terminal output shows:

  • A recursive directory listing.
  • An attempt to use chgrp, showing its error output.
  • Using chgrpld, executing without error.
  • Another recursive directory listing, to show the group has changed.

$ ls -lRO
total 0
drwxr-xr-x  3 user  staff  - 102 Aug  9 2:00 bar
drwxr-xr-x  3 user  admin  - 102 Aug  9 2:00 foo

./bar:
total 0
-rw-r--r--  1 user  staff  - 0 Aug  9 2:00 foo

./foo:
total 0
-rw-r--r--  1 user  admin  uchg 0 Aug  9 2:00 bar
$ chgrp -R staff foo
chgrp: foo/bar: Operation not permitted
$ chgrpld -R staff foo
$ ls -lRO
total 0
drwxr-xr-x  3 user  staff  - 102 Aug  9 2:00 bar
drwxr-xr-x  3 user  staff  - 102 Aug  9 2:00 foo

./bar:
total 0
-rw-r--r--  1 user  staff  - 0 Aug  9 2:00 foo

./foo:
total 0
-rw-r--r--  1 user  staff  uchg 0 Aug  9 2:00 bar
$ 

To create the chgrpld bash script and have it available at the command prompt:

In Terminal:

  • Type, touch chgrpld and press enter.
  • Type, open chgrpld and press enter.

From the Browser:

  • Copy and paste, the code below into the opened chgrpld file and save it, then close the file.

Back in Terminal:

  • Type, chmod +x chgrpld and press enter.
  • Type, sudo mkdir -p /usr/local/bin and press enter. Note that on a clean install of OS X 10.11.5, the bin directory at /usr/local did not exist even though it's already in the PATH.
    • Type in password and press enter.
  • Type sudo cp chgrpld /usr/local/bin and press enter.
    • Type in password and press enter. (If necessary.)
  • chgrpld should now be available to use, just like the chgrp command.

Source code for chgrpld:

#!/bin/bash

o="${@:1:$(($#-1))}"
d="${@: -1}"
if [ -d "$d" ]; then
    f=".locked_files_list_$(date "+%s")"
    find "$d" -flags uchg > "$f" 2>/dev/null
    if [ -s "$f" ]; then
        while read -r l; do
            chflags nouchg "$l"
        done<"$f"
            $(chgrp $o "$d")
        while read -r l; do
            chflags uchg "$l"
        done<"$f"
        rm "$f"
    else
        rm "$f"
        $(chgrp $o "$d")
    fi
else
    echo "  \"$d\" is not a directory!..."
    exit 1
fi
exit 0

Synopsis: chgrpld [−fhv][−R[−H | −L | −P]] group directory

  • See man chgrp in Terminal for description of options and group operand.

  • directory is the name/pathname of the target directory that is locked, or not, and or containing files/directories that are locked too. Note that unlike chgrp which allows multiple objects, e.g. group file ... to act upon, chgrpld is coded to act upon one target directory, and recursively with the -R option, at a time.

  • Note: The user invoking chgrpld must belong to the specified group and be the owner of the directory and locked filesystem objects, or be the super-user.

  • As expected in SIP versions of the OS, this will not work upon SIP protected filesystem objects if SIP is enabled.


The image below shows syntactical highlighting of the code with spaced and indented comments for easier reading to help explain a little about what the code is doing.

Image of Code


Testing Note: Note that even though the testing environment shown above is limited nonetheless I did test it under a more complexed hierarchal directory structure with many more locked nested directories and files, with names containing spaces, and or backslashes within the filename, without issue. Obviously for demonstration purposes, I'm only showing a bare minimum structure for proof of concept. Again, as noted above, "It is not coded to handle an embedded newline in the name of a filesystem object, which IMO doesn't belong there in the first place!".