#!/usr/bin/python # encoding: utf-8 # # Copyright 2011-2016 Greg Neagle. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ manifestutil Created by Greg Neagle on 2011-03-04. """ import ctypes import fnmatch import optparse import os import plistlib import readline import shlex import subprocess import sys import thread import time from ctypes.util import find_library from xml.parsers.expat import ExpatError try: from munkilib.munkicommon import get_version except ImportError: # munkilib is not available def get_version(): '''Placeholder if munkilib is not available''' return 'UNKNOWN' if 'libedit' in readline.__doc__: # readline module was compiled against libedit LIBEDIT = ctypes.cdll.LoadLibrary(find_library('libedit')) else: LIBEDIT = None def raw_input_with_default(prompt, default_text): '''A nasty, nasty hack to get around Python readline limitations under OS X. Gives us editable default text for munkiimport choices''' def insert_default_text(prompt, text): '''Helper function''' time.sleep(0.01) LIBEDIT.rl_set_prompt(prompt) readline.insert_text(text) LIBEDIT.rl_forced_update_display() readline.clear_history() if not default_text: return unicode(raw_input(prompt), encoding=sys.stdin.encoding) elif LIBEDIT: # readline module was compiled against libedit thread.start_new_thread(insert_default_text, (prompt, default_text)) return unicode(raw_input(), encoding=sys.stdin.encoding) else: readline.set_startup_hook(lambda: readline.insert_text(default_text)) try: return unicode(raw_input(prompt), encoding=sys.stdin.encoding) finally: readline.set_startup_hook() def get_installer_item_names(cataloglist): '''Returns a list of unique installer item (pkg) names from the given list of catalogs''' item_list = [] catalogs_path = os.path.join(pref('repo_path'), 'catalogs') for filename in os.listdir(catalogs_path): if filename in cataloglist: try: catalog = plistlib.readPlist( os.path.join(catalogs_path, filename)) except (IOError, OSError, ExpatError): # skip items that aren't valid plists # or that we can't read pass else: item_list.extend([item['name'] for item in catalog if not item.get('update_for')]) item_list = list(set(item_list)) item_list.sort() return item_list def get_manifest_names(): '''Returns a list of available manifests''' manifests_path = os.path.join(pref('repo_path'), 'manifests') manifests = [] for dirpath, dirnames, filenames in os.walk(manifests_path): for dirname in dirnames: # don't recurse into directories that start # with a period. if dirname.startswith('.'): dirnames.remove(dirname) subdir = dirpath[len(manifests_path):] for name in filenames: if name.startswith("."): # don't process these continue manifests.append(os.path.join(subdir, name).lstrip('/')) manifests.sort() return manifests def get_catalogs(): '''Returns a list of available catalogs''' catalogs_path = os.path.join(pref('repo_path'), 'catalogs') catalogs = [] for name in os.listdir(catalogs_path): if name.startswith(".") or name == 'all': # don't process these continue try: _ = plistlib.readPlist(os.path.join(catalogs_path, name)) except (IOError, OSError, ExpatError): # skip items that aren't valid plists pass else: catalogs.append(name) catalogs.sort() return catalogs def get_manifest_pkg_sections(): '''Returns a list of manifest sections that can contain pkg names''' return ['managed_installs', 'managed_uninstalls', 'managed_updates', 'optional_installs'] def printplistitem(label, value, indent=0): """Prints a plist item in an 'attractive' way""" indentspace = ' ' if type(value) == type(None): print indentspace*indent, '%s: !NONE!' % label elif type(value) == list or type(value).__name__ == 'NSCFArray': if label: print indentspace*indent, '%s:' % label for item in value: printplistitem('', item, indent+1) elif type(value) == dict or type(value).__name__ == 'NSCFDictionary': if label: print indentspace*indent, '%s:' % label for subkey in value.keys(): printplistitem(subkey, value[subkey], indent+1) else: if label: print indentspace*indent, '%s: %s' % (label, value) else: print indentspace*indent, '%s' % value def printplist(plistdict): """Prints plist dictionary in a pretty(?) way""" keys = list(plistdict.keys()) keys.sort() for key in keys: printplistitem(key, plistdict[key]) def get_manifest(manifest_name): '''Gets the contents of a manifest''' manifest_path = os.path.join( pref('repo_path'), 'manifests', manifest_name) if os.path.exists(manifest_path): try: return plistlib.readPlist(manifest_path) except (IOError, OSError, ExpatError): print >> sys.stderr, ( 'Could not read manifest %s' % manifest_name) return None else: print >> sys.stderr, 'Manifest %s doesn\'t exist!' % manifest_name return None def save_manifest(manifest_dict, manifest_name, overwrite_existing=False): '''Saves a manifest to disk''' manifest_path = os.path.join( pref('repo_path'), 'manifests', manifest_name) if not overwrite_existing: if os.path.exists(manifest_path): print >> sys.stderr, '%s already exists!' % manifest_name return False try: plistlib.writePlist(manifest_dict, manifest_path) return True except (IOError, OSError, ExpatError), err: print >> sys.stderr, 'Saving %s failed: %s' % (manifest_name, err) return False def repo_available(): """Checks the repo path for proper directory structure. If the directories look wrong we probably don't have a valid repo path. Returns True if things look OK.""" repo_path = pref('repo_path') if not repo_path: print >> sys.stderr, 'No repo path specified.' return False if not os.path.exists(repo_path): mount_repo_cli() if not os.path.exists(repo_path): return False for subdir in ['catalogs', 'manifests', 'pkgs', 'pkgsinfo']: if not os.path.exists(os.path.join(repo_path, subdir)): print >> sys.stderr, "%s is missing %s" % (repo_path, subdir) return False # if we get this far, the repo path looks OK return True def mount_repo_cli(): """Attempts to connect to the repo fileshare""" global WE_MOUNTED_THE_REPO repo_path = pref('repo_path') repo_url = pref('repo_url') if os.path.exists(repo_path): return os.mkdir(repo_path) print 'Attempting to mount fileshare %s:' % repo_url if repo_url.startswith('afp:'): cmd = ['/sbin/mount_afp', '-i', repo_url, repo_path] elif repo_url.startswith('smb:'): cmd = ['/sbin/mount_smbfs', repo_url[4:], repo_path] elif repo_url.startswith('nfs://'): cmd = ['/sbin/mount_nfs', repo_url[6:], repo_path] else: print >> sys.stderr, 'Unsupported filesystem URL!' return retcode = subprocess.call(cmd) if retcode: os.rmdir(repo_path) else: WE_MOUNTED_THE_REPO = True def unmount_repo_cli(): """Attempts to unmount the repo fileshare""" repo_path = pref('repo_path') if not os.path.exists(repo_path): return cmd = ['/sbin/umount', repo_path] return subprocess.call(cmd) def cleanup_and_exit(exitcode): """Give the user the chance to unmount the repo when we exit""" result = 0 if WE_MOUNTED_THE_REPO: answer = raw_input('Unmount the repo fileshare? [y/n] ') if answer.lower().startswith('y'): result = unmount_repo_cli() exit(exitcode or result) _PREFS = {} def pref(prefname): """Returns a preference for prefname""" global _PREFS if not _PREFS: try: _PREFS = plistlib.readPlist(PREFSPATH) except (IOError, OSError, ExpatError): pass if prefname in _PREFS: return _PREFS[prefname] else: return None def update_cached_manifest_list(): '''Updates our cached list of available manifests so our completer will return all the available manifests.''' CMD_ARG_DICT['manifests'] = get_manifest_names() ##### subcommand functions ##### class MyOptParseError(Exception): '''Exception for our custom option parser''' pass class MyOptParseExit(Exception): '''Raised when optparse calls self.exit() so we can handle it instead of exiting''' pass class MyOptionParser(optparse.OptionParser): '''Custom option parser that overrides the error handler and the exit handler so printing help doesn't exit the interactive psuedo-shell''' def error(self, msg): """error(msg : string) """ self.print_usage(sys.stderr) raise MyOptParseError('option error: %s' % msg) def exit(self, status=0, msg=None): if msg: sys.stderr.write(msg) raise MyOptParseExit def version(args): '''Prints version number''' if len(args) != 0: print >> sys.stderr, 'Usage: version' return 22 # Invalid argument print get_version() return 0 def list_catalogs(args): '''Prints the names of the available catalogs''' parser = MyOptionParser() parser.set_usage('''list-catalogs Prints the names of the available catalogs''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) != 0: parser.print_usage(sys.stderr) return 22 # Invalid argument for item in get_catalogs(): print item return 0 def list_catalog_items(args): '''Lists items in the given catalogs''' parser = MyOptionParser() parser.set_usage('''list-catalog-items CATALOG_NAME [CATALOG_NAME ...] Lists items in the given catalogs''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) == 0: parser.print_usage(sys.stderr) return 1 # Operation not permitted available_catalogs = get_catalogs() for catalog in arguments: if catalog not in available_catalogs: print >> sys.stderr, '%s: no such catalog!' % catalog return 2 # No such file or directory for pkg in get_installer_item_names(arguments): print pkg return 0 def list_manifests(args): '''Prints names of available manifests, filtering on arg[0] similar to Unix file globbing''' parser = MyOptionParser() parser.set_usage('''list-manifests [FILE_NAME_MATCH_STRING] Prints names of available manifests.''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) == 1: list_filter = arguments[0] elif len(arguments) == 0: list_filter = '*' else: parser.print_usage(sys.stderr) return 7 # Argument list too long for item in get_manifest_names(): if fnmatch.fnmatch(item, list_filter): print item return 0 def find(args): '''Find text in manifests, optionally searching just a specific manifest section specified by keyname''' parser = MyOptionParser() parser.set_usage('''find FIND_TEXT [--section SECTION_NAME] Find text in manifests, optionally searching a specific manifest section''') parser.add_option('--section', metavar='SECTION_NAME', help=('(Optional) Section of the manifest to search for ' 'FIND_TEXT')) try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.section and len(arguments) == 2: options.section = arguments[1] del arguments[1] if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long findtext = arguments[0] keyname = options.section manifests_path = os.path.join(pref('repo_path'), 'manifests') count = 0 for name in get_manifest_names(): pathname = os.path.join(manifests_path, name) try: manifest = plistlib.readPlist(pathname) except (IOError, OSError, ExpatError): print >> sys.stderr, 'Error reading %s' % pathname continue if keyname: if keyname in manifest: value = manifest[keyname] if type(value) == list or type(value).__name__ == 'NSCFArray': for item in value: try: if findtext.upper() in item.upper(): print '%s: %s' % (name, item) count += 1 except AttributeError: pass elif findtext.upper() in value.upper(): print '%s: %s' % (name, value) count += 1 else: for key in manifest.keys(): value = manifest[key] if type(value) == list or type(value).__name__ == 'NSCFArray': for item in value: try: if findtext.upper() in item.upper(): print '%s (%s): %s' % (name, key, item) count += 1 except AttributeError: pass elif findtext.upper() in value.upper(): print '%s (%s): %s' % (name, key, value) count += 1 print '%s matches found.' % count return 0 def display_manifest(args): '''Prints contents of a given manifest''' parser = MyOptionParser() parser.set_usage('''display-manifest MANIFESTNAME Prints the contents of the specified manifest''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long manifestname = arguments[0] manifest = get_manifest(manifestname) if manifest: printplist(manifest) return 0 else: return 2 # No such file or directory def new_manifest(args): '''Creates a new, empty manifest''' parser = MyOptionParser() parser.set_usage('''new-manifest MANIFESTNAME Creates a new empty manifest''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long manifest_name = arguments[0] manifest = {'catalogs': [], 'included_manifests': [], 'managed_installs': [], 'managed_uninstalls': []} if save_manifest(manifest, manifest_name): update_cached_manifest_list() return 0 else: return 1 # Operation not permitted def copy_manifest(args): '''Copies one manifest to another''' parser = MyOptionParser() parser.set_usage( '''copy-manifest SOURCE_MANIFEST DESTINATION_MANIFEST Copies the contents of one manifest to another''') try: _, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if len(arguments) != 2: parser.print_usage(sys.stderr) return 7 # Argument list too long source_manifest = arguments[0] dest_manifest = arguments[1] manifest = get_manifest(source_manifest) if manifest and save_manifest(manifest, dest_manifest): update_cached_manifest_list() return 0 else: return 1 # Operation not permitted def add_pkg(args): '''Adds a package to a manifest.''' parser = MyOptionParser() parser.set_usage( '''add-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME] Adds a package to a manifest. Package is added to managed_installs unless a different manifest section is specified with the --section option''') parser.add_option('--manifest', metavar='MANIFESTNAME', help='name of manifest on which to operate') parser.add_option('--section', default='managed_installs', metavar='SECTIONNAME', help=('manifest section to which to add the package. ' 'Defaults to managed_installs.')) try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long pkgname = arguments[0] manifest = get_manifest(options.manifest) if not manifest: return 2 # No such file or directory for section in get_manifest_pkg_sections(): if pkgname in manifest.get(section, []): print >> sys.stderr, ( 'Package %s is already in section %s of manifest %s.' % (pkgname, section, options.manifest)) return 1 # Operation not permitted manifest_catalogs = manifest.get('catalogs', []) available_pkgnames = get_installer_item_names(manifest_catalogs) if pkgname not in available_pkgnames: print >> sys.stderr, ( 'WARNING: Package %s is not available in catalogs %s ' 'of manifest %s.' % (pkgname, manifest_catalogs, options.manifest)) if not options.section in manifest: manifest[options.section] = [pkgname] else: manifest[options.section].append(pkgname) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Added %s to section %s of manifest %s.' % (pkgname, options.section, options.manifest)) return 0 else: return 1 # Operation not permitted def remove_pkg(args): '''Removes a package from a manifest.''' parser = MyOptionParser() parser.set_usage( '''remove-pkg PKGNAME --manifest MANIFESTNAME [--section SECTIONNAME] Removes a package from a manifest. Package is removed from managed_installs unless a different manifest section is specified with the --section option''') parser.add_option('--manifest', metavar='MANIFESTNAME', help='''name of manifest on which to operate''') parser.add_option('--section', default='managed_installs', metavar='SECTIONNAME', help=('manifest section from which to remove the ' 'package. Defaults to managed_installs.')) try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long pkgname = arguments[0] manifest = get_manifest(options.manifest) if not manifest: return 2 # No such file or directory if not options.section in manifest: print >> sys.stderr, ('Section %s is not in manifest %s.' % (options.section, manifest)) return 1 # Operation not permitted if pkgname not in manifest[options.section]: print >> sys.stderr, ('Package %s is not in section %s ' 'of manifest %s.' % (pkgname, options.section, options.manifest)) return 1 # Operation not permitted else: manifest[options.section].remove(pkgname) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Removed %s from section %s of manifest %s.' % (pkgname, options.section, options.manifest)) return 0 else: return 1 # Operation not permitted def add_catalog(args): '''Adds a catalog to a manifest.''' parser = MyOptionParser() parser.set_usage('''add-catalog CATALOGNAME --manifest MANIFESTNAME Adds a catalog to a manifest''') parser.add_option('--manifest', metavar='MANIFESTNAME', help='name of manifest on which to operate') try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long catalogname = arguments[0] available_catalogs = get_catalogs() if catalogname not in available_catalogs: print >> sys.stderr, 'Unknown catalog name: %s.' % catalogname return 2 # no such file or directory manifest = get_manifest(options.manifest) if not manifest: return 2 # no such file or directory if not 'catalogs' in manifest: manifest['catalogs'] = [] if catalogname in manifest['catalogs']: print >> sys.stderr, ( 'Catalog %s is already in manifest %s.' % (catalogname, options.manifest)) return 1 # Operation not permitted else: # put it at the front of the catalog list as that is usually # what is wanted... manifest['catalogs'].insert(0, catalogname) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Added %s to catalogs of manifest %s.' % (catalogname, options.manifest)) return 0 else: return 1 # Operation not permitted def remove_catalog(args): '''Removes a catalog from a manifest.''' parser = MyOptionParser() parser.set_usage('''remove-catalog CATALOGNAME --manifest MANIFESTNAME Removes a catalog from a manifest''') parser.add_option('--manifest', metavar='MANIFESTNAME', help='name of manifest on which to operate') try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long catalogname = arguments[0] manifest = get_manifest(options.manifest) if not manifest: return 2 # no such file or directory if catalogname not in manifest.get('catalogs', []): print >> sys.stderr, ( 'Catalog %s is not in manifest %s.' % (catalogname, options.manifest)) return 1 # Operation not permitted else: manifest['catalogs'].remove(catalogname) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Removed %s from catalogs of manifest %s.' % (catalogname, options.manifest)) return 0 else: return 1 # Operation not permitted def add_included_manifest(args): '''Adds an included manifest to a manifest.''' parser = MyOptionParser() parser.set_usage( '''add-included-manifest MANIFEST_TO_INCLUDE --manifest TARGET_MANIFEST Adds a manifest to the included_manifests of the TARGET_MANIFEST''') parser.add_option('--manifest', metavar='TARGET_MANIFEST', help='name of manifest on which to operate') try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long manifest_to_include = arguments[0] available_manifests = get_manifest_names() if manifest_to_include not in available_manifests: print >> sys.stderr, ('Unknown manifest name: %s.' % manifest_to_include) return 2 # no such file or directory if manifest_to_include == options.manifest: print >> sys.stderr, ('Can\'t include %s in itself!.' % manifest_to_include) return 1 # Operation not permitted manifest = get_manifest(options.manifest) if not manifest: return 2 # no such file or directory if not 'included_manifests' in manifest: manifest['included_manifests'] = [] if manifest_to_include in manifest['included_manifests']: print >> sys.stderr, ( 'Manifest %s is already included in manifest %s.' % (manifest_to_include, options.manifest)) return 1 # Operation not permitted else: manifest['included_manifests'].append(manifest_to_include) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Added %s to included_manifests of manifest %s.' % (manifest_to_include, options.manifest)) return 0 else: return 1 # Operation not permitted def remove_included_manifest(args): '''Removes an included manifest from a manifest.''' parser = MyOptionParser() parser.set_usage( '''remove-included_manifest INCLUDED_MANIFEST --manifest TARGET_MANIFEST Removes a manifest from the included_manifests of TARGET_MANIFEST''') parser.add_option('--manifest', metavar='TARGET_MANIFEST', help='name of manifest on which to operate') try: options, arguments = parser.parse_args(args) except MyOptParseError, errmsg: print >> sys.stderr, str(errmsg) return 22 # Invalid argument if not options.manifest: if len(arguments) == 2: options.manifest = arguments[1] del arguments[1] else: parser.print_usage(sys.stderr) return 7 # Argument list too long if len(arguments) != 1: parser.print_usage(sys.stderr) return 7 # Argument list too long included_manifest = arguments[0] manifest = get_manifest(options.manifest) if not manifest: return 2 # no such file or directory if included_manifest not in manifest.get('included_manifests', []): print >> sys.stderr, ( 'Manifest %s is not included in manifest %s.' % (included_manifest, options.manifest)) return 1 # Operation not permitted else: manifest['included_manifests'].remove(included_manifest) if save_manifest(manifest, options.manifest, overwrite_existing=True): print ('Removed %s from included_manifests of manifest %s.' % (included_manifest, options.manifest)) return 0 else: return 1 # Operation not permitted def show_help(): '''Prints available subcommands''' print "Available sub-commands:" subcommands = CMD_ARG_DICT['cmds'].keys() subcommands.sort() for item in subcommands: print '\t%s' % item return 0 def configure(args): """Configures manifestutil for use""" if len(args): print >> sys.stderr, 'Usage: configure' return 22 # Invalid argument for (key, prompt) in [ ('repo_path', 'Path to munki repo (example: /Volumes/repo)'), ('repo_url', 'Repo fileshare URL (example: afp://munki.example.com/repo)')]: newvalue = raw_input_with_default('%15s: ' % prompt, pref(key)) _PREFS[key] = newvalue or pref(key) or '' try: plistlib.writePlist(_PREFS, PREFSPATH) return 0 except (IOError, OSError, ExpatError): print >> sys.stderr, 'Could not save configuration to %s' % PREFSPATH return 1 # Operation not permitted def tab_completer(text, state): '''Called by the readline lib to calculate possible completions''' array_to_match = None if readline.get_begidx() == 0: # since we are at the start of the line # we are matching commands array_to_match = 'cmds' match_list = CMD_ARG_DICT.get('cmds', {}).keys() else: # we are matching args cmd_line = readline.get_line_buffer()[0:readline.get_begidx()] cmd = shlex.split(cmd_line)[-1] array_to_match = CMD_ARG_DICT.get('cmds', {}).get(cmd) if array_to_match: match_list = CMD_ARG_DICT[array_to_match] else: array_to_match = CMD_ARG_DICT.get('options', {}).get(cmd) if array_to_match: match_list = CMD_ARG_DICT[array_to_match] else: array_to_match = 'options' match_list = CMD_ARG_DICT.get('options', {}).keys() matches = [item for item in match_list if item.upper().startswith(text.upper())] try: return matches[state] except IndexError: return None def set_up_tab_completer(): '''Starts our tab-completer when running interactively''' readline.set_completer(tab_completer) if LIBEDIT: # readline module was compiled against libedit readline.parse_and_bind("bind ^I rl_complete") else: readline.parse_and_bind("tab: complete") def handle_subcommand(args): '''Does all our subcommands''' # check if any arguments are passed. # if not, list subcommands. if len(args) < 1: show_help() return 2 # strip leading hyphens and # replace embedded hyphens with underscores # so '--add-pkg' becomes 'add_pkg' # and 'new-manifest' becomes 'new_manifest' subcommand = args[0].lstrip('-').replace('-', '_') # special case the exit command if subcommand == 'exit': cleanup_and_exit(0) if (subcommand not in ['version', 'configure', 'help'] and '-h' not in args and '--help' not in args): if not repo_available(): exit(-1) try: # find function to call by looking in the global name table # for a function with a name matching the subcommand subcommand_function = globals()[subcommand] return subcommand_function(args[1:]) except MyOptParseExit: return 0 except (TypeError, KeyError): print >> sys.stderr, 'Unknown subcommand: %s' % subcommand show_help() return 2 PREFSNAME = 'com.googlecode.munki.munkiimport.plist' PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences', PREFSNAME)) WE_MOUNTED_THE_REPO = False INTERACTIVE_MODE = False CMD_ARG_DICT = {} def main(): '''Our main routine''' global INTERACTIVE_MODE cmds = {'add-pkg': 'pkgs', 'add-catalog': 'catalogs', 'add-included-manifest': 'manifests', 'remove-pkg': 'pkgs', 'remove-catalog': 'catalogs', 'remove-included-manifest': 'manifests', 'list-manifests': 'manifests', 'list-catalogs': 'default', 'list-catalog-items': 'catalogs', 'display-manifest': 'manifests', 'find': 'default', 'new-manifest': 'default', 'copy-manifest': 'manifests', 'exit': 'default', 'help': 'default', 'configure': 'default', 'version': 'default' } CMD_ARG_DICT['cmds'] = cmds if len(sys.argv) > 1: # some commands or options were passed at the command line cmd = sys.argv[1].lstrip('-') retcode = handle_subcommand(sys.argv[1:]) cleanup_and_exit(retcode) else: # if we get here, no options or commands, # so let's enter interactive mode INTERACTIVE_MODE = True # must have an available repo for interfactive mode if not repo_available(): exit(-1) # build the rest of our dict to enable tab completion CMD_ARG_DICT['options'] = {'--manifest': 'manifests', '--section': 'sections'} CMD_ARG_DICT['default'] = [] CMD_ARG_DICT['sections'] = get_manifest_pkg_sections() CMD_ARG_DICT['manifests'] = get_manifest_names() CMD_ARG_DICT['catalogs'] = get_catalogs() CMD_ARG_DICT['pkgs'] = get_installer_item_names(get_catalogs()) set_up_tab_completer() print 'Entering interactive mode... (type "help" for commands)' while 1: try: cmd = raw_input('> ') except (KeyboardInterrupt, EOFError): # React to Control-C and Control-D print # so we finish off the raw_input line cleanup_and_exit(0) args = shlex.split(cmd) handle_subcommand(args) if __name__ == '__main__': main()