1

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.

2 Answers 2

1

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!".

6
  • Simple & clean code! Nice for the limitation warning about newlines ;).
    – dan
    Commented Aug 9, 2016 at 8:24
  • What if a "file" has multiple flags such as hidden,uchgor schg,uchg? Nice commenting in your script.
    – fd0
    Commented Aug 9, 2016 at 10:19
  • @fd0, Let me start by saying I've already said "The following is an example of how I would workaround the issue" which covers the scenario of the filesystem object being locked and "Feel free to modify as needed/wanted to suite your needs" which covers being locked and hidden at the same time. What I've presented is an example of a workaround wrapper script to be used as a template. Yes, adding a check for and handling uchg,hidden flags is easy to do, if you want to. Thanks for the complement on code commenting, I'd like to look back in 6 mo. and know what and why I wrote it that way. :) Commented Aug 9, 2016 at 11:19
  • @fd0, Out of idle curiosity I wrote a program that ran as root and analyzed the Flags on a recent and pretty clean, few user programs added, install of OS X 10.11.5 and there was only 1 file with just the uchg flag set and 0 with uchg,hidden set. However, here's the file totals for each individual and combined Flag(s) that were set: 35270 compressed; 80 hidden; 242946 restricted; 268419 restricted,compressed; 20 restricted,hidden; 3 restricted,sunlnk; 1 restricted,uchg; 467 sunlnk; 6 sunlnk,hidden; 1 uchg. I know hidden can stop chgrp from executing properly, haven't tested the others. Commented Aug 9, 2016 at 18:57
  • Well, it seems odd that hidden or for that matter compressed, opaque, nodump, archive would effect modifying a file.
    – fd0
    Commented Aug 10, 2016 at 12:59
0

2 shells solution

Finally after many attempts I built a 2 short shell scripts solution.

One pretty basic one which will do the:

chflags, chgrp, chflags

A more serious one which will do the directory structure traversal:

find ... do the chgrp correctly

Small shell: chflags, chgrp, chflags

Here is the content of chgrpld1.sh which typically will work called from find with exactly 2 arguments.

#!/bin/sh
# shell script to chgrp a file or directory
# when there exists a lock flags (uchg)

_cn=`basename $0`

USAGE="Usage: ${_cn} group file"

case $# in
2)
        _group=$1
        _file="$2"
        ;;
*)
        echo "$USAGE" >&2
        exit 2
        ;;
esac

chflags nouchg "${_file}"
chgrp ${_group} "${_file}"
chflags uchg "${_file}"

Big shell: find ... do the chgrp correctly

Here is the content of chgrpld.sh which will run a find on the directory structures, and avoid as much as possible error conditions in advance.

#!/bin/sh
# shell script to recursively chgrp a file or directory
# when there exists files which are locked

_cn=`basename $0`

USAGE="Usage: ${_cn} group file..."

case $# in
0|1)
        echo "$USAGE" >&2
        exit 2
        ;;
*)
        _group=$1
        shift
        _file_list="$@"
        ;;
esac

TMPDIR=/tmp
_file_flags_list=`mktemp -t ${_cn}`

clean() { rm -f ${_file_flags_list} ; }

trap clean 0 1 2 3

# first check if we don't have any sort of flags thus blocking this
# script

find "${_file_list}" -flags +sappnd,schg,uappend -exec ls -@delO "{}" \; >${_file_flags_list}

if [ -s ${_file_flags_list} ] ; then
        echo "${_cn}: can't run because of other flags set:"
        cat ${_file_flags_list}
        exit 1
fi

# if flag present then call chgrpld1, else plain chgrp

find "${_file_list}" \( -flags uchg -exec chgrpld1 ${_group} "{}" \; \) -or \( -exec chgrp ${_group} "{}" \; \)

Install

To make these 2 shells work together, it is necessary to install both of them in a local directory which is in the PATH. In my case I use /local/bin to stay outside of the system PATH components, and outside of the ones used by package managers (port, brew…).

Copy the 2 precedent files within the choosen directory:

chgrpld1.sh
chgrpld.sh

make chgrpld1 chgrpld

cp chgrpld1 chgrpld /local/bin

Test

To validate this shell runs correctly in every case, I made a test directory populated with all the combination of flags one could encounter with all the impossible filenames it should cope with without an error.

dir-ok is the subdirectory containing the horrors on which my script should succeed, dir-fail is the one containing all the case on which it should fail:

% ls -blOR
total 0
drwxr-xr-x  2 bob  admin          - 204 Aug 16 16:50 dir-fail
drwxr-xr-x  3 bob  localaccounts  - 238 Aug 16 16:55 dir-ok

./dir-fail:
total 0
-rw-r--r--  1 bob  admin  schg,uchg 0 Aug  9 18:59 file-multi-flags
-rw-r--r--  1 bob  admin  sappnd    0 Aug 16 16:13 file-sappnd
-rw-r--r--  1 bob  admin  schg      0 Aug 16 16:11 file-schg
-rw-r--r--  1 bob  admin  uappnd    0 Aug 16 16:12 file-uappend

./dir-ok:
total 0
-rw-r--r--  1 bob  localaccounts  -      0 Aug 16 16:07 a\nb
-rw-r--r--  1 bob  localaccounts  -      0 Aug  9 19:34 a b
-rw-r--r--  1 bob  localaccounts  -      0 Aug  9 19:34 a'b
-rw-r--r--  1 bob  localaccounts  -      0 Aug 16 16:46 a\\b
drwxr-xr-x  2 bob  localaccounts  uchg 272 Aug 16 16:39 subdir

./dir-ok/subdir:
total 0
-rw-r--r--  1 bob  localaccounts  -      0 Aug 16 16:08 file
-rw-r--r--  1 bob  localaccounts  arch   0 Aug 16 16:12 file-arch
-rw-r--r--@ 1 bob  localaccounts  hidden 0 Aug 16 16:10 file-hidden
-rw-r--r--  1 bob  localaccounts  nodump 0 Aug 16 16:11 file-nodump
-rw-r--r--  1 bob  localaccounts  opaque 0 Aug 16 16:10 file-opaque
-rw-r--r--  1 bob  localaccounts  uchg   0 Aug 16 16:08 file-uchg
  • Run on dir-ok

    $ chgrpld staff dir-ok
    $ ls -blOR dir-ok
    total 0
    -rw-r--r--  1 bob  staff  -      0 Aug 16 16:07 a\nb
    -rw-r--r--  1 bob  staff  -      0 Aug  9 19:34 a b
    -rw-r--r--  1 bob  staff  -      0 Aug  9 19:34 a'b
    -rw-r--r--  1 bob  staff  -      0 Aug 16 16:46 a\\b
    drwxr-xr-x  2 bob  staff  uchg 272 Aug 16 16:39 subdir
    
    dir-ok/subdir:
    total 0
    -rw-r--r--  1 bob  staff  -      0 Aug 16 16:08 file
    -rw-r--r--  1 bob  staff  arch   0 Aug 16 16:12 file-arch
    -rw-r--r--@ 1 bob  staff  hidden 0 Aug 16 16:10 file-hidden
    -rw-r--r--  1 bob  staff  nodump 0 Aug 16 16:11 file-nodump
    -rw-r--r--  1 bob  staff  opaque 0 Aug 16 16:10 file-opaque
    -rw-r--r--  1 bob  staff  uchg   0 Aug 16 16:08 file-uchg
    $
    
  • Run on dir-fail

    $ chgrpld staff dir-fail/file-multi-flags 
    chgrpld: can't run because of other flags set:
    -rw-r--r--  1 bob  admin  schg,uchg 0 Aug  9 18:59 dir-fail/file-multi-flags
    
    $ chgrpld staff dir-fail/file-sappnd      
    chgrpld: can't run because of other flags set:
    -rw-r--r--  1 bob  admin  sappnd 0 Aug 16 16:13 dir-fail/file-sappnd
    
    $ chgrpld staff dir-fail/file-schg  
    chgrpld: can't run because of other flags set:
    -rw-r--r--  1 bob  admin  schg 0 Aug 16 16:11 dir-fail/file-schg
    
    $ chgrpld staff dir-fail/file-uappend
    chgrpld: can't run because of other flags set:
    -rw-r--r--  1 bob  admin  uappnd 0 Aug 16 16:12 dir-fail/file-uappend
    $
    

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .