#!/usr/bin/env python # -*- coding: utf-8 -*- # Evan Widloski - 2017-03-07 # Passhole - Keepass CLI + dmenu interface from __future__ import absolute_import from __future__ import print_function from builtins import input from .version import __version__ import subprocess from getpass import getpass from colorama import Fore, Back, Style from base64 import b64encode from io import BytesIO import readline # import gpgme import random import os from os.path import realpath, expanduser, dirname, join import sys import shutil import logging import argparse from configparser import ConfigParser from collections import OrderedDict logging.basicConfig(level=logging.ERROR, format='%(message)s') # hide INFO messages from pykeepass logging.getLogger("pykeepass").setLevel(logging.WARNING) log = logging.getLogger(__name__) default_config = expanduser('~/.config/passhole.ini') default_database = expanduser('~/.local/passhole/{}.kdbx') default_keyfile = expanduser('~/.local/passhole/{}.key') base_dir = dirname(realpath(__file__)) # taken from https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases wordlist_file = join(base_dir, 'wordlist.txt') template_database_file = join(base_dir, 'blank.kdbx') template_config_file = join(base_dir, 'passhole.ini') alphabetic = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' numeric = '0123456789' symbolic = '!@#$%^&*()_+-=[]{};:'"<>,./?\|`~" # gpg = gpgme.Context() reserved_fields = { 'username':'UserName', 'url':'URL', 'password':'Password', 'notes':'Notes', 'title': 'Title' } # convenience functions for colored prompts def red(text): return Fore.RED + text + Fore.RESET def green(text): return Fore.GREEN + text + Fore.RESET def blue(text): return Fore.BLUE + text + Fore.RESET def bold(text): return Style.BRIGHT + text + Style.NORMAL def editable_input(prompt, prefill=None): def hook(): readline.insert_text(prefill) readline.redisplay() readline.set_pre_input_hook(hook) result = input(green(prompt + ': ')) readline.set_pre_input_hook() return result def boolean_input(prompt, default=True): result = editable_input( prompt + ' (Y/n)' if default else prompt + ' (y/N)' ) if result.lower() == 'y': return True elif result.lower() == 'n': return False elif result == '': return default else: # ask again return boolean_input(prompt, default) # assertions for entry/group existence/non-existence def get_group(kp, path): _, path = split_db_prefix(path) group = kp.find_groups(path=path, first=True) if group is None: log.error(red("No such group ") + bold(path)) sys.exit() return group def get_entry(kp, path): _, path = split_db_prefix(path) entry = kp.find_entries(path=path, first=True) if entry is None: log.error(red("No such entry ") + bold(path)) sys.exit() return entry def get_field(entry, field_input): field = reserved_fields.get(field_input, field_input) if field not in entry._get_string_field_keys(): log.error(red("No such field ") + bold(field_input)) sys.exit() return field def no_entry(kp, path): if kp.find_entries(path=path, first=True): log.error(red("There is already an entry at ") + bold(path)) sys.exit() def no_group(kp, path): if kp.find_groups(path=path, first=True): log.error(red("There is already group at ") + bold(path)) sys.exit() def split_db_prefix(path): if path.startswith('@'): if '/' in path: return path.lstrip('@').split('/', 1) else: # return path.lstrip('@'), '' return path.lstrip('@'), None else: return None, path # def join_db_prefix(prefix, path): # if prefix is None: # return path # else: # return '@{}/{}'.format(prefix, path) def init_database(args): """Create database""" # from pykeepass.pykeepass import PyKeePass # ----- setup config ----- c = ConfigParser() if os.path.exists(args.config): c.read(args.config) if args.name is None: database_name = editable_input("Database name (no spaces)", "passhole") else: database_name = args.name if database_name in c.sections(): log.error( red("There is already a database named ") + bold(database_name) + red(" in ") + bold(args.config) ) sys.exit() else: c.add_section(database_name) if not os.path.exists(args.config): c.set(database_name, 'default', 'True') # ----- database prompt ----- if args.database is None: database_path = editable_input( "Desired database path", default_database.format(database_name) ) else: database_path = args.database # quit if database already exists if os.path.exists(database_path): log.error(red("Found database at ") + bold(database_path)) sys.exit() else: c.set(database_name, 'database', database_path) # ----- password prompt ----- if args.no_password: password = None c.set(database_name, 'no-password', 'True') else: use_password = boolean_input("Password protect database?") if use_password: password = getpass(green('Password: ')) password_confirm = getpass(green('Confirm: ')) if not password == password_confirm: log.error(red("Passwords do not match")) sys.exit() else: password = None c.set(database_name, 'no-password', 'True') # ----- keyfile prompt ----- if args.keyfile is None: use_keyfile = boolean_input("Use a keyfile?") if use_keyfile: keyfile = editable_input("Desired keyfile path", default_keyfile.format(database_name) ) c.set(database_name, 'keyfile', keyfile) else: keyfile = None else: keyfile = args.keyfile c.set(database_name, 'keyfile', keyfile) # ----- create keyfile/database/config ----- # create keyfile if keyfile is not None: log.debug("Looking for keyfile at {}".format(keyfile)) if os.path.exists(keyfile): print("Found existing keyfile at {} Exiting".format(bold(keyfile))) sys.exit() print("Creating keyfile at " + bold(keyfile)) os.makedirs(dirname(keyfile), exist_ok=True) with open(keyfile, 'w') as f: contents = ''' <?xml version="1.0" encoding="UTF-8"?> <KeyFile> <Meta><Version>1.00</Version></Meta> <Key><Data>{}</Data></Key> </KeyFile> ''' log.debug("keyfile contents {}".format(contents)) f.write(contents.format(b64encode(os.urandom(32)).decode())) # create database print("Creating database at {}".format(bold(database_path))) os.makedirs(dirname(database_path), exist_ok=True) shutil.copy(template_database_file, database_path) from pykeepass import PyKeePass kp = PyKeePass(database_path, password='password') kp.password = password kp.keyfile = keyfile kp.save() # create config print("Creating config at {}".format(bold(args.config))) os.makedirs(dirname(args.config), exist_ok=True) with open(args.config, 'w') as f: c.write(f) def open_database( keyfile=None, no_cache=False, cache_timeout=600, no_password=False, config=default_config, database=None, all=False, name=None, path=None, **kwargs ): """Load one or more databases Parameters ---------- keyfile : str, optional path to keyfile. if not given, assume database has no keyfile (default: None) no_cache : bool, optional don't read/cache database background thread (default: False) cache_timeout : int, optional seconds to keep read/cache database background thread, has no effect if no_cache=True (default: 300) no_password : bool, optional assume database has no password (default: False) config : str path to database config. no effect if `database` is not None. (default: ~/.config/passhole.ini) database : str, optional open database at this path and ignore config file if given (default: None). overrides all below options all : bool, optional return a list of 2-tuples containing all databases in the config (default: False). overrides all below options name : str, optional section name in config of database to open (default: None) overrides all below options path : str, optional entry or group path. the '@' prefix will be used to determine which database in the config to open (default: None) Returns ------- PyKeePass object or list of (name, PyKeePass) tuples """ from pykeepass_cache.pykeepass_cache import PyKeePass, cached_databases def prompt_open(name, database, keyfile, no_password, no_cache, cache_timeout): """Open a database and return KeePass object""" cache_timeout = int(cache_timeout) if database is not None: database = realpath(expanduser(database)) if keyfile is not None: keyfile = realpath(expanduser(keyfile)) # check if database exists if not os.path.exists(database): log.error( red("No database found at ") + bold(database) ) sys.exit() if not no_cache: opened_databases = cached_databases(timeout=cache_timeout) log.debug("opened databases:" + str(opened_databases)) # if database is already open on server if database in opened_databases: log.debug("opening {} from cache".format(database)) return opened_databases[database] log.debug("{} not found in cache".format(database)) # if path of given keyfile doesn't exist if keyfile is not None and not os.path.exists(keyfile): log.error(red("No keyfile found at ") + bold(keyfile)) sys.exit() if no_password: password = None else: if name is not None: prompt = 'Enter database password ({}):'.format(name) else: prompt = 'Enter database password:' # check if running in interactive shell if os.isatty(sys.stdout.fileno()): password = getpass('{} '.format(prompt)) # otherwise use zenity else: log.debug('Detected non-interactive shell') try: p = subprocess.Popen( ["zenity", "--entry", "--hide-text", "--text='{}'".format(prompt)], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=open(os.devnull, 'w'), close_fds=True ) except FileNotFoundError: log.error(bold("zenity ") + red("not found.")) sys.exit() password = p.communicate()[0].decode('utf-8').rstrip('\n') log.debug("opening {} with password:{} and keyfile:{}".format( database, None if password is None else 'redacted', str(keyfile) )) if no_cache: from pykeepass import PyKeePass as PyKeePass_nocache return PyKeePass_nocache(database, password=password, keyfile=keyfile) else: return PyKeePass(database, password=password, keyfile=keyfile, timeout=cache_timeout) # if 'database' argument given, ignore config completely if database is not None: kp = prompt_open(database, database, keyfile, no_password, no_cache, cache_timeout) if all: return [(None, kp)] else: return kp else: # read config if not os.path.exists(config): log.error(red("No config found at ") + bold(config)) sys.exit() c = ConfigParser() log.debug("reading config from {}".format(config)) c.read(config) # find default section for section in c.sections(): if c.has_option(section, 'default') and c[section].getboolean('default'): default_section = section log.debug('default_section {}'.format(default_section)) break else: default_section = None # validate that every section has 'database' for s in c.sections(): if not c.has_option(s, 'database'): log.error(bold('database') + red(' option is required')) sys.exit() # open all databases in config if all: kps = [] for section in c.sections(): kp = prompt_open( section, c[section].get('database'), c[section].get('keyfile'), c[section].get('no-password'), c[section].get('no-cache', no_cache), c[section].get('cache-timeout', cache_timeout) ) # set default database to be first if section == default_section: kps.insert(0, (section, kp)) else: kps.append((section, kp)) return kps # open a specific database in config by name elif name is not None: if name not in c.sections(): log.error(red("No config section found for " + bold(section))) sys.exit() return prompt_open( name, c[name]['database'], c[name].get('keyfile'), c[name].get('no-password'), c[name].get('no-cache', no_cache), c[name].get('cache-timeout', cache_timeout) ) # open a specific database in config using full Element path elif path is not None: section, _ = split_db_prefix(path) if section is None: if default_section is None: log.error(red("No default database specified in config")) sys.exit() return prompt_open( section, c[default_section].get('database'), c[default_section].get('keyfile'), c[default_section].get('no-password'), c[default_section].get('no-cache', no_cache), c[default_section].get('cache-timeout', cache_timeout) ) if section not in c.sections(): log.error(red("No config section found for " + bold(section))) sys.exit() return prompt_open( section, c[section].get('database'), c[section].get('keyfile'), c[section].get('no-password'), c[section].get('no-cache', no_cache), c[section].get('cache-timeout', cache_timeout) ) # open default database in config if default_section is None: log.error(red("No default database specified in config")) sys.exit() return prompt_open( section, c[default_section].get('database'), c[default_section].get('keyfile'), c[default_section].get('no-password'), c[default_section].get('no-cache', no_cache), c[default_section].get('cache-timeout', cache_timeout) ) def type_entries(args): """Type out password using keyboard Selects an entry using `prog`, then sends the password to the keyboard. If `tabbed` is true, both the username and password are typed, separated by a tab""" from Xlib.error import DisplayNameError try: from pynput.keyboard import Controller, Key except DisplayNameError: log.error(red("No X11 session found")) entry_texts = {} # type from all databases if args.name is None: databases = open_database(all=True, **vars(args)) # generate multi-line string to send to dmenu for name, kp in databases: for entry in kp.entries: if entry.title: if len(databases) > 1: entry_text = "@{}/{}".format(name, entry.path) else: entry_text = entry.path if args.username: entry_text += " ({})".format(entry.username) entry_texts[entry_text] = kp dmenu_text = '\n'.join(sorted(entry_texts.keys())) # type from specific database else: kp = open_database(**vars(args)) for entry in kp.entries: if entry.title: entry_text = entry.path if args.username: entry_text += " ({})".format(entry.username) entry_texts[entry_text] = kp dmenu_text = '\n'.join(sorted(entry_texts.keys())) # get the entry from dmenu try: p = subprocess.Popen( args.prog, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True ) except FileNotFoundError: log.error(bold(args.prog[0]) + red(" not found.")) sys.exit() stdout = p.communicate(input=dmenu_text.encode('utf-8'))[0].decode('utf-8').rstrip('\n') log.debug("text from dmenu: {}".format(stdout)) # if nothing was selected, return None if not stdout: log.warning("No path returned by {}".format(args.prog)) return kp = entry_texts[stdout] _, selection_path = split_db_prefix(stdout) selected_entry = get_entry(kp, selection_path) log.debug("selected_entry:{}".format(selected_entry)) def call_xdotool(args): try: subprocess.call(["xdotool"] + args) except FileNotFoundError: log.error(bold("xdotool ") + red("not found")) sys.exit() # type out password k = Controller() if args.tabbed: if selected_entry.username: if args.xdotool: call_xdotool(['type', selected_entry.username]) call_xdotool(['key', 'Tab']) else: k.type(selected_entry.username) k.press(Key.tab) k.release(Key.tab) else: log.warning("Selected entry does not have a username") if selected_entry.password: if args.xdotool: call_xdotool(['type', selected_entry.password]) else: k.type(selected_entry.password) else: log.warning("Selected entry does not have a password") def show(args): """Print out the contents of an entry to console""" kp = open_database(**vars(args)) entry = get_entry(kp, args.path) # show specified field if args.field: # handle lowercase field input gracefully field = get_field(entry, args.field) print(entry._get_string_field(field), end='') # otherwise, show all fields else: print(green("Title: ") + (entry.title or '')) print(green("UserName: ") + (entry.username or '')) print( green("Password: ") + Fore.RED + Back.RED + (entry.password or '') + Fore.RESET + Back.RESET ) print(green("URL: ") + (entry.url or '')) for field_name, field_value in entry.custom_properties.items(): print(green("{}: ".format(field_name)) + str(field_value or '')) print(green("Created: ") + entry.ctime.isoformat()) print(green("Modified: ") + entry.mtime.isoformat()) def list_entries(args): """List Entries/Groups in the database as a tree""" # recursive function to list items in a group def list_items(group, prefix, show_branches=True): branch_corner = "└── " if show_branches else "" branch_tee = "├── " if show_branches else "" branch_pipe = "│ " if show_branches else "" branch_blank = " " if show_branches else "" entries = sorted(group.entries, key=lambda x: str(x.title)) for entry in entries: if args.username: entry_string = "{} ({})".format(str(entry.title), str(entry.username)) else: entry_string = "{}".format(str(entry.title)) if entry == entries[-1] and len(group.subgroups) == 0: print(prefix + branch_corner + entry_string) else: print(prefix + branch_tee + entry_string) groups = sorted(group.subgroups, key=lambda x: x.__str__()) for group in groups: if group == groups[-1]: print(prefix + branch_corner + blue(bold(str(group.name)))) list_items(group, prefix + branch_blank) else: print(prefix + branch_tee + blue(bold(str(group.name)))) list_items(group, prefix + branch_pipe) # print all databases if args.path is None: databases = open_database(all=True, **vars(args)) for position, (name, kp) in enumerate(databases): # print names for config-provided databases if len(databases) > 1: print('{}{}{}'.format( '' if position == 0 else '\n', bold(green('@' + name)), ' (default)' if position == 0 else '' )) list_items(kp.root_group, "", show_branches=False) # print specific database else: kp = open_database(**vars(args)) # FIXME: write a function: parse_path -> type (db, group, entry) # if group, list items if args.path.endswith('/'): list_items(get_group(kp, args.path), "", show_branches=False) # if db, list items in root group elif args.path.startswith('@') and '/' not in args.path: list_items(kp.root_group, "", show_branches=False) # if entry, print entry contents else: args.field = None show(args) def grep(args): """Search all string fields for a string""" databases = open_database(all=True, **vars(args)) for position, (name, kp) in enumerate(databases): flags = 'i' if args.i else None log.debug("Searching database for pattern: {}".format(args.pattern)) if args.field: # handle lowercase field input gracefully args.field = reserved_fields.get(args.field, args.field) else: args.field = 'Title' entries = kp.find_entries(string={args.field: args.pattern}, regex=True, flags=flags) # print names for config-provided databases if len(databases) > 1 and len(entries) > 0: print('{}[{}]{}'.format( '' if position == 0 else '\n', bold(green(name)), ' (default)' if position == 0 else '' )) for entry in entries: print(entry.path) def decompose_path(path): """Process path into parent group and child item""" if '/' in path.strip('/'): [group_path, child_name] = path.strip('/').rsplit('/', 1) else: group_path = '' child_name = path.strip('/') log.debug("Decomposed path into: '{}' and '{}'".format(group_path + '/', child_name)) return [group_path + '/', child_name] def add(args): """Create new entry/group""" kp = open_database(**vars(args)) [group_path, child_name] = decompose_path(args.path) if not child_name: log.error(red("Path is invalid")) sys.exit() log.debug("args.path:{}".format(args.path)) log.debug("group_path:{} , child_name:{}".format(group_path, child_name)) parent_group = get_group(kp, group_path) # create a new group if args.path.endswith('/'): no_group(kp, args.path) kp.add_group(parent_group, child_name) # create a new entry else: no_entry(kp, args.path) username = editable_input('Username') # use urandom for number generation rng = random.SystemRandom() # generate correct-horse-battery-staple password if args.words: with open(wordlist_file, 'r') as f: wordlist = f.read().splitlines() selected = rng.sample(wordlist, args.words) password = ' '.join(selected) # generate alphanumeric password elif args.alphanumeric: selected = [rng.choice(alphabetic + numeric) for _ in range(0, args.alphanumeric)] password = ''.join(selected) # generate alphanumeric + symbolic password elif args.symbolic: selected = [rng.choice(alphabetic + numeric + symbolic) for _ in range(0, args.symbolic)] password = ''.join(selected) # prompt for password instead of generating it else: password = getpass(green('Password: ')) password_confirm = getpass(green('Confirm: ')) if not password == password_confirm: log.error(red("Passwords do not match")) sys.exit() # append fixed string to password if args.append: password += args.append url = editable_input('URL') # log.debug( # 'Adding entry: group:{}, title:{}, user:{}, pass:{}, url:{}'.format( # parent_group, child_name, username, password, url # ) # ) entry = kp.add_entry(parent_group, child_name, username, password, url=url) # set custom fields if args.fields is not None: for field in args.fields.split(','): # capitalize reserved fields field = reserved_fields.get(field, field).strip() value = editable_input(field) entry._set_string_field(field, value) kp.save() def remove(args): """Remove an Entry/Group""" kp = open_database(**vars(args)) # remove a group if args.path.endswith('/'): group = get_group(kp, args.path) if len(group.entries) > 0: log.error(red("Non-empty group ") + bold(args.path)) sys.exit() group.delete() # remove an entry else: entry = get_entry(kp, args.path) entry.delete() kp.save() def edit(args): """Edit fields of an Entry""" kp = open_database(**vars(args)) # edit group name if args.path.endswith('/'): group = get_group(kp, args.path) if args.set: field = args.set[0] if field.lower() != 'name': log.error(red("Only 'name' is supported for Group FIELD")) sys.exit() group.name = args.set[1] # otherwise, edit interactively else: value = editable_input('Name', group.name) group.name = value else: entry = get_entry(kp, args.path) # edit specific field if args.field: field = get_field(entry, args.field) value = editable_input(field, entry._get_string_field(field)) entry._set_string_field(field, value) # add/set a field elif args.set: field = reserved_fields.get(args.set[0], args.set[0]) entry._set_string_field(field, args.set[1]) # remove a field elif args.remove: field = get_field(entry, args.remove) results = entry._element.xpath('String/Key[text()="{}"]/..'.format(field)) entry._element.remove(results[0]) # otherwise, edit all fields interactively else: for field in entry._get_string_field_keys(): value = editable_input(field, entry._get_string_field(field)) entry._set_string_field(field, value) kp.save() def move(args): """Move an Entry/Group""" src_kp = open_database(path=args.src_path, **vars(args)) dest_kp = open_database(path=args.dest_path, **vars(args)) # FIXME: pykeepass_cache doesn't support moving elements between databases if src_kp.filename != dest_kp.filename: log.error(red("Moving elements between databases not supported")) sys.exit() [group_path, child_name] = decompose_path(args.dest_path) # parent_group = get_group(dest_kp, group_path) parent_group = get_group(src_kp, group_path) # if source path is group if args.src_path.endswith('/'): src = get_group(src_kp, args.src_path) # if dest path is group if args.dest_path.endswith('/'): # dest = dest_kp.find_groups(path=args.dest_path, first=True) dest = src_kp.find_groups(path=args.dest_path, first=True) if dest: # dest_kp.move_group(src, dest) src_kp.move_group(src, dest) else: src.name = child_name # dest_kp.move_group(src, parent_group) src_kp.move_group(src, parent_group) # if dest path is entry else: log.error(red("Destination must end in '/'")) sys.exit() # if source path is entry else: src = get_entry(src_kp, args.src_path) # if dest path is group if args.dest_path.endswith('/'): # dest = get_group(dest_kp, args.dest_path) dest = get_group(src_kp, args.dest_path) # dest_kp.move_entry(src, dest) src_kp.move_entry(src, dest) log.debug("Moving entry: {} -> {}".format(src, dest)) # if dest path is entry else: # no_entry(dest_kp, args.dest_path) no_entry(src_kp, args.dest_path) log.debug("Renaming entry: {} -> {}".format(src.title, child_name)) src.title = child_name log.debug("Moving entry: {} -> {}".format(src, parent_group)) # dest_kp.move_entry(src, parent_group) src_kp.move_entry(src, parent_group) src_kp.save() # dest_kp.save() def dump(args): """Pretty print database XML to console""" from lxml import etree kp = open_database(**vars(args)) print(kp.xml()) def info(args): """Print database information to console""" kp = open_database(**vars(args)) print(green("Key Derivation Algorithm: ") + kp.kdf_algorithm) print(green("Encryption Algorithm: ") + kp.encryption_algorithm) print(green("Database Version: ") + '.'.join(map(str, kp.version))) def create_parser(): """Create argparse object""" parser = argparse.ArgumentParser(description="Append -h to any command to view its syntax.") parser._positionals.title = "commands" subparsers = parser.add_subparsers() subparsers.dest = 'command' subparsers.required = True path_help = "entry path (e.g. 'foo') or group path (e.g. 'foo/')" # process args for `list` command list_parser = subparsers.add_parser('list', aliases=['ls'], help="list entries in the database") list_parser.add_argument('path', nargs='?', metavar='PATH', default=None, type=str, help=path_help) list_parser.add_argument('--username', action='store_true', default=False, help="show username in parenthesis") list_parser.set_defaults(func=list_entries) # process args for `add` command add_parser = subparsers.add_parser('add', help="add new entry or group") add_parser.add_argument('path', metavar='PATH', type=str, help=path_help) add_parser.add_argument('-w', '--words', metavar='length', type=int, nargs='?', const=6, default=None, help="generate 'correct horse battery staple' style password when creating entry ") add_parser.add_argument('-a', '--alphanumeric', metavar='length', type=int, nargs='?', const=16, default=None, help="generate alphanumeric password") add_parser.add_argument('-s', '--symbolic', metavar='length', type=int, nargs='?', const=16, default=None, help="generate alphanumeric + symbolic password") add_parser.add_argument('--append', metavar='STR', type=str, help="append string to generated password") add_parser.add_argument('--fields', metavar='FIELD1,...', type=str, help="comma separated list of custom fields") add_parser.set_defaults(func=add) # process args for `remove` command remove_parser = subparsers.add_parser('remove', aliases=['rm'], help="remove an entry") remove_parser.add_argument('path', metavar='PATH', type=str, help=path_help) remove_parser.set_defaults(func=remove) # process args for `move` command move_parser = subparsers.add_parser('move', aliases=['mv'], help="move an entry or group") move_parser.add_argument('src_path', metavar='SRC_PATH', type=str, help=path_help) move_parser.add_argument('dest_path', metavar='DEST_PATH', type=str, help=path_help) move_parser.set_defaults(func=move) # process args for `show` command show_parser = subparsers.add_parser('show', help="show the contents of an entry") show_parser.add_argument('path', metavar='PATH', type=str, help="path to entry") show_parser.add_argument('--field', metavar='FIELD', type=str, default=None, help="show the contents of a specific field") show_parser.set_defaults(func=show) # process args for `edit` command edit_parser = subparsers.add_parser('edit', help="edit the contents of an entry or group") edit_parser.add_argument('path', metavar='PATH', type=str, help=path_help) edit_parser.add_argument('--field', metavar='FIELD', type=str, default=None, help="edit the contents of a specific field") edit_parser.add_argument('--set', metavar=('FIELD', 'VALUE'), type=str, nargs=2, default=None, help="add/edit the contents of a specific field, noninteractively") edit_parser.add_argument('--remove', metavar='FIELD', type=str, default=None, help="remove a field from the entry") edit_parser.set_defaults(func=edit) # process args for `type` command type_parser = subparsers.add_parser('type', help="select entries using dmenu (or similar) and send to keyboard") type_parser.add_argument('name', type=str, nargs='?', default=None, help="name of database to type from") type_parser.add_argument('--prog', metavar='PROG', default='dmenu', help="dmenu-like program to call") type_parser.add_argument('--tabbed', action='store_true', default=False, help="type both username and password (tab separated)") type_parser.add_argument('--xdotool', action='store_true', default=False, help="use xdotool for typing passwords") type_parser.add_argument('--username', action='store_true', default=False, help="show username in parenthesis during selection") type_parser.set_defaults(func=type_entries) # process args for `init` command init_parser = subparsers.add_parser('init', help="initialize a new database") init_parser.add_argument('--name', type=str, help="name of database to initialize") init_parser.set_defaults(func=init_database) # process args for `grep` command grep_parser = subparsers.add_parser('grep', help="list entries with title matching regex pattern") grep_parser.add_argument('pattern', metavar='PATTERN', type=str, help="XSLT style regular expression") #FIXME - default='.*' doesn't work anymore for some reason grep_parser.add_argument('--field', metavar='FIELD', type=str, help="search entries for a match in a specific field") grep_parser.add_argument('-i', action='store_true', default=False, help="case insensitive searching") grep_parser.set_defaults(func=grep) # process args for `dump` command dump_parser = subparsers.add_parser('dump', help="pretty print database XML to console") dump_parser.add_argument('name', type=str, nargs='?', default=None, help="name of database") dump_parser.set_defaults(func=dump) # process args for `dump` command info_parser = subparsers.add_parser('info', help="print database information") info_parser.add_argument('name', type=str, nargs='?', default=None, help="name of database") info_parser.set_defaults(func=info) # optional arguments parser.add_argument('--debug', action='store_true', default=False, help="enable debug messages") parser.add_argument('--database', metavar='PATH', type=str, help="specify database path") parser.add_argument('--keyfile', metavar='PATH', type=str, default=None, help="specify keyfile path") parser.add_argument('--no-password', action='store_true', default=False, help="database has no password") parser.add_argument('--no-cache', action='store_true', default=False, help="don't cache this database in a background process") parser.add_argument('--cache-timeout', metavar='SEC', type=int, default=600, help="seconds to hold database open in a background process") parser.add_argument('--config', metavar='PATH', type=str, default=default_config, help="specify config path") parser.add_argument('-v', '--version', action='version', version=__version__, help="show version information") return parser def main(): parser = create_parser() args = parser.parse_args() if args.debug: print('Debugging enabled...') log.setLevel(logging.DEBUG) logging.getLogger('pykeepass_cache').setLevel(logging.DEBUG) try: args.func(args) except KeyboardInterrupt: print()