<filename>chmod_walk.py
"""Python 3 script to recursively change the mode of directories and files."""

import argparse
import os
import os.path
import stat
import unittest

_ERROR_MESSAGE = 'Invalid mode'
"""String error message for an invalid mode."""

_MODE_MASK = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
"""Integer bit mask for the permission bits."""

_MODE_TABLE = [
    ('r', stat.S_IRUSR),
    ('w', stat.S_IWUSR),
    ('x', stat.S_IXUSR),
    ('r', stat.S_IRGRP),
    ('w', stat.S_IWGRP),
    ('x', stat.S_IXGRP),
    ('r', stat.S_IROTH),
    ('w', stat.S_IWOTH),
    ('x', stat.S_IXOTH)
]
"""List of tuples containing the corresponding letter and its mode mask."""

def _change_permissions(path, new_mode):
    """Change the permissions of the directory or file at path to new_mode.

    Args:
        path: String path to a directory or file whose permissions to change.
        new_mode: Integer new permissions.
    """
    old_mode = os.stat(path).st_mode & _MODE_MASK
    if old_mode != new_mode:
        os.chmod(path, new_mode)
        print('Changed {old:o} to {new:o} for {path}'.format(
            old=old_mode, new=new_mode, path=path))

def _print_error(error):
    """Print the OSError generated by os.walk()."""
    print(error)

def _walk(path, dir_mode, file_mode):
    """Walk the directory at path and change the permissions.

    Args:
        path: String path to a directory to walk.
        dir_mode: Integer permissions for directories.
        file_mode: Integer permissions for files.
    """
    for parent, dirnames, filenames in os.walk(path, onerror=_print_error):
        for dirname in dirnames:
            path = os.path.join(parent, dirname)
            _change_permissions(path, dir_mode)
        for filename in filenames:
            path = os.path.join(parent, filename)
            _change_permissions(path, file_mode)

def parse_mode(mode_string):
    """Return integer permissions corresponding to mode_string.

    Args:
        mode_string: String permissions as a string of octal digits or 'rwx-'.
    Returns:
        Integer permissions corresponding to mode_string.
    """
    if not isinstance(mode_string, str):
        raise TypeError(_ERROR_MESSAGE)
    mode_string = mode_string.strip()
    if len(mode_string) <= 0:
        raise argparse.ArgumentTypeError(_ERROR_MESSAGE)

    if mode_string.isdigit():
        mode_string = (mode_string + '000')[:3]
        try:
            value = int(mode_string, 8)
        except ValueError:
            raise argparse.ArgumentTypeError(_ERROR_MESSAGE)
        else:
            return value
    else:
        mode_string = (mode_string.lower() + '---------')[:9]
        value = 0
        for char, (letter, bit) in zip(mode_string, _MODE_TABLE):
            if char == letter:
                value += bit
            elif char != '-':
                raise argparse.ArgumentTypeError(_ERROR_MESSAGE)
        return value


class _UnitTest(unittest.TestCase):
    def test_parse_mode(self):
        """Test parsing a string mode."""
        for value in [None, 42, []]:
            self.assertRaises(TypeError, parse_mode, value)
        for value in ['', '8', '9', '88', '99', '888', '911', '999',
                      'foo', 'bar', 'foo---', 'bar---', '---foo', '---bar',
                      'foobar', '---foobar', 'foo---bar', 'foobar---',
                      'foobarbaz',
                      'w', 'x', 'w--', 'x--', 'w-----', 'x-----',
                      'w--------', 'x--------',
                      '-r', '-x', '-r-', '-x-', '-r----', '-x----',
                      '-r-------', '-x-------',
                      '--r', '--w', '--r---', '--w---',
                      '--r------', '--w------']:
            self.assertRaises(argparse.ArgumentTypeError, parse_mode, value)
        for value, expected in [
            ('000', 0o000),
            ('001', 0o001),
            ('002', 0o002),
            ('003', 0o003),
            ('004', 0o004),
            ('005', 0o005),
            ('006', 0o006),
            ('007', 0o007),
            ('010', 0o010),
            ('020', 0o020),
            ('030', 0o030),
            ('040', 0o040),
            ('050', 0o050),
            ('060', 0o060),
            ('070', 0o070),
            ('100', 0o100),
            ('200', 0o200),
            ('300', 0o300),
            ('400', 0o400),
            ('500', 0o500),
            ('600', 0o600),
            ('700', 0o700),
            ('411', 0o411),
            ('711', 0o711),
            ('777', 0o777),
            ('-', 0o000),
            ('--', 0o000),
            ('---', 0o000),
            ('----', 0o000),
            ('-----', 0o000),
            ('------', 0o000),
            ('-------', 0o000),
            ('--------', 0o000),
            ('---------', 0o000),
            ('--------x', 0o001),
            ('-------w-', 0o002),
            ('-------wx', 0o003),
            ('------r--', 0o004),
            ('------r-x', 0o005),
            ('------rw-', 0o006),
            ('------rwx', 0o007),
            ('-----x---', 0o010),
            ('----w----', 0o020),
            ('----wx---', 0o030),
            ('---r-----', 0o040),
            ('---r-x---', 0o050),
            ('---rw----', 0o060),
            ('---rwx---', 0o070),
            ('--x------', 0o100),
            ('-w-------', 0o200),
            ('-wx------', 0o300),
            ('r--------', 0o400),
            ('r-x------', 0o500),
            ('rw-------', 0o600),
            ('rwx------', 0o700),
            ('r----x--x', 0o411),
            ('rwx--x--x', 0o711),
            ('rwxrwxrwx', 0o777),
            # Test padding
            ('-----x', 0o010),
            ('----w', 0o020),
            ('----w-', 0o020),
            ('----wx', 0o030),
            ('---r', 0o040),
            ('---r--', 0o040),
            ('---r-x', 0o050),
            ('---rw', 0o060),
            ('---rw-', 0o060),
            ('---rwx', 0o070),
            ('--x', 0o100),
            ('-w', 0o200),
            ('-w-', 0o200),
            ('-wx', 0o300),
            ('r', 0o400),
            ('r--', 0o400),
            ('r-x', 0o500),
            ('rw', 0o600),
            ('rw-', 0o600),
            ('rwx', 0o700)]:
            self.assertEqual(parse_mode(value), expected)
            self.assertEqual(parse_mode('    ' + value), expected)
            self.assertEqual(parse_mode(value + '\n'), expected)
            self.assertEqual(parse_mode('\t' + value + '    '), expected)
            self.assertEqual(parse_mode(value.title()), expected)
            self.assertEqual(parse_mode(value.upper()), expected)
        for user in range(8):
            self.assertEqual(parse_mode(str(user)), user * 8 * 8)
            for group in range(8):
                value = ''.join([str(user), str(group)])
                expected = (user * 8 * 8) + (group * 8)
                self.assertEqual(parse_mode(value), expected)
                for other in range(8):
                    value = ''.join([str(user), str(group), str(other)])
                    expected = (user * 8 * 8) + (group * 8) + other
                    self.assertEqual(parse_mode(value), expected)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-d', '--dirmode', type=parse_mode, default='755',
                        help='mode to use for directories')
    parser.add_argument('-f', '--filemode', type=parse_mode, default='644',
                        help='mode to use for files')
    parser.add_argument('path', nargs='?', default='',
                        help='path to the directory to walk')
    args = parser.parse_args()

    if len(args.path) <= 0:
        suite = unittest.defaultTestLoader.loadTestsFromTestCase(_UnitTest)
        unittest.TextTestRunner(verbosity=2).run(suite)
    elif os.path.isfile(args.path):
        _change_permissions(args.path, args.filemode)
    elif os.path.isdir(args.path):
        _walk(args.path, args.dirmode, args.filemode)

<filename>chmod_walk.py
"""Python 3 script to recursively change the mode of directories and files."""

import argparse
import os
import os.path
import stat
import unittest

_ERROR_MESSAGE = 'Invalid mode'
"""String error message for an invalid mode."""

_MODE_MASK = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO
"""Integer bit mask for the permission bits."""

_MODE_TABLE = [
    ('r', stat.S_IRUSR),
    ('w', stat.S_IWUSR),
    ('x', stat.S_IXUSR),
    ('r', stat.S_IRGRP),
    ('w', stat.S_IWGRP),
    ('x', stat.S_IXGRP),
    ('r', stat.S_IROTH),
    ('w', stat.S_IWOTH),
    ('x', stat.S_IXOTH)
]
"""List of tuples containing the corresponding letter and its mode mask."""

def _change_permissions(path, new_mode):
    """Change the permissions of the directory or file at path to new_mode.

    Args:
        path: String path to a directory or file whose permissions to change.
        new_mode: Integer new permissions.
    """
    old_mode = os.stat(path).st_mode & _MODE_MASK
    if old_mode != new_mode:
        os.chmod(path, new_mode)
        print('Changed {old:o} to {new:o} for {path}'.format(
            old=old_mode, new=new_mode, path=path))

def _print_error(error):
    """Print the OSError generated by os.walk()."""
    print(error)

def _walk(path, dir_mode, file_mode):
    """Walk the directory at path and change the permissions.

    Args:
        path: String path to a directory to walk.
        dir_mode: Integer permissions for directories.
        file_mode: Integer permissions for files.
    """
    for parent, dirnames, filenames in os.walk(path, onerror=_print_error):
        for dirname in dirnames:
            path = os.path.join(parent, dirname)
            _change_permissions(path, dir_mode)
        for filename in filenames:
            path = os.path.join(parent, filename)
            _change_permissions(path, file_mode)

def parse_mode(mode_string):
    """Return integer permissions corresponding to mode_string.

    Args:
        mode_string: String permissions as a string of octal digits or 'rwx-'.
    Returns:
        Integer permissions corresponding to mode_string.
    """
    if not isinstance(mode_string, str):
        raise TypeError(_ERROR_MESSAGE)
    mode_string = mode_string.strip()
    if len(mode_string) <= 0:
        raise argparse.ArgumentTypeError(_ERROR_MESSAGE)

    if mode_string.isdigit():
        mode_string = (mode_string + '000')[:3]
        try:
            value = int(mode_string, 8)
        except ValueError:
            raise argparse.ArgumentTypeError(_ERROR_MESSAGE)
        else:
            return value
    else:
        mode_string = (mode_string.lower() + '---------')[:9]
        value = 0
        for char, (letter, bit) in zip(mode_string, _MODE_TABLE):
            if char == letter:
                value += bit
            elif char != '-':
                raise argparse.ArgumentTypeError(_ERROR_MESSAGE)
        return value


class _UnitTest(unittest.TestCase):
    def test_parse_mode(self):
        """Test parsing a string mode."""
        for value in [None, 42, []]:
            self.assertRaises(TypeError, parse_mode, value)
        for value in ['', '8', '9', '88', '99', '888', '911', '999',
                      'foo', 'bar', 'foo---', 'bar---', '---foo', '---bar',
                      'foobar', '---foobar', 'foo---bar', 'foobar---',
                      'foobarbaz',
                      'w', 'x', 'w--', 'x--', 'w-----', 'x-----',
                      'w--------', 'x--------',
                      '-r', '-x', '-r-', '-x-', '-r----', '-x----',
                      '-r-------', '-x-------',
                      '--r', '--w', '--r---', '--w---',
                      '--r------', '--w------']:
            self.assertRaises(argparse.ArgumentTypeError, parse_mode, value)
        for value, expected in [
            ('000', 0o000),
            ('001', 0o001),
            ('002', 0o002),
            ('003', 0o003),
            ('004', 0o004),
            ('005', 0o005),
            ('006', 0o006),
            ('007', 0o007),
            ('010', 0o010),
            ('020', 0o020),
            ('030', 0o030),
            ('040', 0o040),
            ('050', 0o050),
            ('060', 0o060),
            ('070', 0o070),
            ('100', 0o100),
            ('200', 0o200),
            ('300', 0o300),
            ('400', 0o400),
            ('500', 0o500),
            ('600', 0o600),
            ('700', 0o700),
            ('411', 0o411),
            ('711', 0o711),
            ('777', 0o777),
            ('-', 0o000),
            ('--', 0o000),
            ('---', 0o000),
            ('----', 0o000),
            ('-----', 0o000),
            ('------', 0o000),
            ('-------', 0o000),
            ('--------', 0o000),
            ('---------', 0o000),
            ('--------x', 0o001),
            ('-------w-', 0o002),
            ('-------wx', 0o003),
            ('------r--', 0o004),
            ('------r-x', 0o005),
            ('------rw-', 0o006),
            ('------rwx', 0o007),
            ('-----x---', 0o010),
            ('----w----', 0o020),
            ('----wx---', 0o030),
            ('---r-----', 0o040),
            ('---r-x---', 0o050),
            ('---rw----', 0o060),
            ('---rwx---', 0o070),
            ('--x------', 0o100),
            ('-w-------', 0o200),
            ('-wx------', 0o300),
            ('r--------', 0o400),
            ('r-x------', 0o500),
            ('rw-------', 0o600),
            ('rwx------', 0o700),
            ('r----x--x', 0o411),
            ('rwx--x--x', 0o711),
            ('rwxrwxrwx', 0o777),
            # Test padding
            ('-----x', 0o010),
            ('----w', 0o020),
            ('----w-', 0o020),
            ('----wx', 0o030),
            ('---r', 0o040),
            ('---r--', 0o040),
            ('---r-x', 0o050),
            ('---rw', 0o060),
            ('---rw-', 0o060),
            ('---rwx', 0o070),
            ('--x', 0o100),
            ('-w', 0o200),
            ('-w-', 0o200),
            ('-wx', 0o300),
            ('r', 0o400),
            ('r--', 0o400),
            ('r-x', 0o500),
            ('rw', 0o600),
            ('rw-', 0o600),
            ('rwx', 0o700)]:
            self.assertEqual(parse_mode(value), expected)
            self.assertEqual(parse_mode('    ' + value), expected)
            self.assertEqual(parse_mode(value + '\n'), expected)
            self.assertEqual(parse_mode('\t' + value + '    '), expected)
            self.assertEqual(parse_mode(value.title()), expected)
            self.assertEqual(parse_mode(value.upper()), expected)
        for user in range(8):
            self.assertEqual(parse_mode(str(user)), user * 8 * 8)
            for group in range(8):
                value = ''.join([str(user), str(group)])
                expected = (user * 8 * 8) + (group * 8)
                self.assertEqual(parse_mode(value), expected)
                for other in range(8):
                    value = ''.join([str(user), str(group), str(other)])
                    expected = (user * 8 * 8) + (group * 8) + other
                    self.assertEqual(parse_mode(value), expected)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('-d', '--dirmode', type=parse_mode, default='755',
                        help='mode to use for directories')
    parser.add_argument('-f', '--filemode', type=parse_mode, default='644',
                        help='mode to use for files')
    parser.add_argument('path', nargs='?', default='',
                        help='path to the directory to walk')
    args = parser.parse_args()

    if len(args.path) <= 0:
        suite = unittest.defaultTestLoader.loadTestsFromTestCase(_UnitTest)
        unittest.TextTestRunner(verbosity=2).run(suite)
    elif os.path.isfile(args.path):
        _change_permissions(args.path, args.filemode)
    elif os.path.isdir(args.path):
        _walk(args.path, args.dirmode, args.filemode)

Python 3 script to recursively change the mode of directories and files. String error message for an invalid mode. Integer bit mask for the permission bits. List of tuples containing the corresponding letter and its mode mask. Change the permissions of the directory or file at path to new_mode.      Args:         path: String path to a directory or file whose permissions to change.         new_mode: Integer new permissions. Print the OSError generated by os.walk(). Walk the directory at path and change the permissions.      Args:         path: String path to a directory to walk.         dir_mode: Integer permissions for directories.         file_mode: Integer permissions for files. Return integer permissions corresponding to mode_string.      Args:         mode_string: String permissions as a string of octal digits or 'rwx-'.     Returns:         Integer permissions corresponding to mode_string. Test parsing a string mode. # Test padding