How to Copy ACLs Between Files on macOS: A Robust Alternative to getfacl/setfacl


7 views

While Linux systems provide the convenient getfacl and setfacl commands for ACL management, macOS takes a different approach by integrating ACL handling directly into chmod. This creates a challenge when you need to copy ACLs from one file to another.

The ls -le command displays ACLs in a verbose format that needs processing before it can be fed to chmod -E. Here's what the raw output looks like:


$ ls -led testfile
-rw-r--r--+ 1 user  staff  0 Jan 1 12:34 testfile
 0: user:johndoe allow read
 1: group:admin allow write

Here's a more robust version of the shell pipeline that handles edge cases better:


ls -led source_file | awk 'NR > 1 {sub(/^[[:space:]]*[0-9]+:[[:space:]]*/, ""); print}' | chmod -E target_file

This version:

  • Uses awk instead of sed for more reliable pattern matching
  • Properly handles spaces in usernames/groupnames
  • Skips the first line (file metadata) cleanly

For those who prefer a Python solution using only built-in modules:


import subprocess

def copy_acls(source, target):
    # Get ACLs from source file
    ls_process = subprocess.Popen(['ls', '-led', source], stdout=subprocess.PIPE)
    awk_process = subprocess.Popen(['awk', 'NR > 1 {sub(/^[[:space:]]*[0-9]+:[[:space:]]*/, ""); print}'], 
                                  stdin=ls_process.stdout, stdout=subprocess.PIPE)
    ls_process.stdout.close()
    
    # Apply ACLs to target file
    chmod_process = subprocess.Popen(['chmod', '-E', target], stdin=awk_process.stdout)
    awk_process.stdout.close()
    chmod_process.wait()

# Usage example
copy_acls('source.txt', 'destination.txt')

When dealing with inherited ACLs or complex permission sets, consider these additional steps:


# For directories with default ACLs
ls -led source_dir | awk 'NR > 1 {gsub(/^[[:space:]]*[0-9]+:[[:space:]]*|default:[[:space:]]*/, ""); print}' | chmod -E target_dir

# To preserve ACL inheritance flags
ls -le source_dir | grep -E '^[[:space:]]*[0-9]+:' | sed -E 's/^[[:space:]]*[0-9]+:[[:space:]]*//; s/inherited//' | chmod -E target_dir

For a more direct (but less portable) solution, you can work with the raw extended attributes:


# Copy all ACL-related xattrs
xattr -px com.apple.acl.text source_file | xattr -wx com.apple.acl.text target_file

# For directories (including default ACLs)
xattr -px com.apple.acl.text source_dir | xattr -wx com.apple.acl.text target_dir

Unlike traditional Unix systems that use getfacl/setfacl, macOS implements Access Control Lists through extended chmod functionality. The chmod -E command accepts ACL specifications in a specific format.

Here's a robust method to copy ACLs between files using built-in macOS tools:

# Extract ACLs from source file
SOURCE_ACLS=$(ls -led source_file | tail -n +2 | sed -E 's/^[[:space:]]*[0-9]+:[[:space:]]*//')

# Apply to destination file
echo "$SOURCE_ACLS" | chmod -E destination_file

For Python implementations without third-party dependencies:

import subprocess

def copy_acls(source, dest):
    # Get ACLs from source
    proc = subprocess.Popen(['ls', '-led', source], 
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
    out, err = proc.communicate()
    if proc.returncode != 0:
        raise Exception(f"Error reading ACLs: {err.decode()}")
    
    # Process output
    acls = []
    for line in out.decode().split('\n')[1:]:
        if not line.strip():
            continue
        # Extract the ACL part (after the numeric index)
        acl = line.split(':', 1)[1].strip()
        acls.append(acl)
    
    # Apply ACLs to destination
    acl_input = '\n'.join(acls)
    proc = subprocess.Popen(['chmod', '-E', dest],
                          stdin=subprocess.PIPE)
    proc.communicate(input=acl_input.encode())
    if proc.returncode != 0:
        raise Exception("Failed to apply ACLs")

Some important considerations when working with ACLs on macOS:

# Preserve inheritance flags
ls -le source_file | grep 'inherited' | while read -r line; do
    echo "${line#*: }" | chmod -E destination_file
done

# Handle multiple ACEs (Access Control Entries)
for entry in $(ls -led source_file | tail -n +2 | awk -F': ' '{print $2}'); do
    echo "$entry" | chmod -E destination_file
done

For batch operations, consider these variations:

# Copy ACLs to multiple files
SOURCE_ACLS=$(ls -led template_file | tail -n +2 | sed -E 's/^[[:space:]]*[0-9]+:[[:space:]]*//')
find . -name "*.conf" -exec sh -c 'echo "$0" | chmod -E "$1"' "$SOURCE_ACLS" {} \;