| #!/usr/bin/python |
| # |
| # Copyright (c) 2014 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Pretty print (and check) a set of group/user accounts""" |
| |
| from __future__ import print_function |
| |
| import collections |
| import glob |
| import os |
| import sys |
| |
| |
| # Objects to hold group/user accounts. |
| Group = collections.namedtuple('Group', ['group', 'password', 'gid', 'users', |
| 'defunct']) |
| User = collections.namedtuple('User', ['user', 'password', 'uid', 'gid', |
| 'gecos', 'home', 'shell', 'defunct']) |
| |
| |
| def _ParseAccount(content, obj, defaults): |
| """Parse the raw data in |content| and return a new |obj|""" |
| d = defaults.copy() |
| |
| for line in content.splitlines(): |
| if not line or line.startswith('#'): |
| continue |
| |
| key, val = line.split(':') |
| if key not in obj._fields: |
| raise ValueError('unknown key: %s' % key) |
| d[key] = val |
| |
| missing_keys = set(obj._fields) - set(d.keys()) |
| if missing_keys: |
| raise ValueError('missing keys: %s' % ' '.join(missing_keys)) |
| |
| return obj(**d) |
| |
| |
| def ParseGroup(content): |
| """Parse |content| as a Group object""" |
| defaults = { |
| 'password': '!', |
| 'users': '', |
| 'defunct': '', |
| } |
| return _ParseAccount(content, Group, defaults) |
| |
| |
| def ParseUser(content): |
| """Parse |content| as a User object""" |
| defaults = { |
| 'gecos': '', |
| 'home': '/dev/null', |
| 'password': '!', |
| 'shell': '/bin/false', |
| 'defunct': '', |
| } |
| return _ParseAccount(content, User, defaults) |
| |
| |
| def AlignWidths(arr): |
| """Calculate a set of widths for alignment |
| |
| Args: |
| arr: An array of collections.namedtuple objects |
| |
| Returns: |
| A dict whose fields have the max length |
| """ |
| d = {} |
| for f in arr[0]._fields: |
| d[f] = 0 |
| |
| for a in arr: |
| for f in a._fields: |
| d[f] = max(d[f], len(getattr(a, f))) |
| |
| return d |
| |
| |
| def DisplayAccounts(accts, order): |
| """Display |accts| as a table using |order| for field ordering |
| |
| Args: |
| accts: An array of collections.namedtuple objects |
| order: The order in which to display the members |
| """ |
| obj = type(accts[0]) |
| header_obj = obj(**dict([(k, (v if v else k).upper()) for k, v in order])) |
| keys = [k for k, _ in order] |
| sorter = lambda x: int(getattr(x, keys[0])) |
| |
| widths = AlignWidths([header_obj] + accts) |
| def p(obj): |
| for k in keys: |
| print('%-*s ' % (widths[k], getattr(obj, k)), end='') |
| print() |
| |
| for a in [header_obj] + sorted(accts, key=sorter): |
| p(a) |
| |
| |
| def main(args): |
| if not args: |
| accounts_dir = os.path.dirname(os.path.realpath(__file__)) |
| args = (glob.glob(os.path.join(accounts_dir, 'group', '*')) + |
| glob.glob(os.path.join(accounts_dir, 'user', '*'))) |
| |
| groups = [] |
| users = [] |
| for f in args: |
| try: |
| content = open(f).read() |
| if 'group:' in content: |
| groups.append(ParseGroup(content)) |
| else: |
| users.append(ParseUser(content)) |
| except ValueError as e: |
| print('error: %s: %s' % (f, e)) |
| return os.EX_DATAERR |
| |
| if groups: |
| order = ( |
| ('gid', ''), |
| ('group', ''), |
| ('password', 'pass'), |
| ('users', ''), |
| ('defunct', ''), |
| ) |
| DisplayAccounts(groups, order) |
| |
| if users: |
| if groups: |
| print() |
| order = ( |
| ('uid', ''), |
| ('gid', ''), |
| ('user', ''), |
| ('shell', ''), |
| ('home', ''), |
| ('password', 'pass'), |
| ('gecos', ''), |
| ('defunct', ''), |
| ) |
| DisplayAccounts(users, order) |
| |
| |
| if __name__ == '__main__': |
| exit(main(sys.argv[1:])) |