class Account(object):
    """Class that holds all the information specific to an account.

    Add desired account information as attributes of the class.
    """

    __NO_MASTER = True
        # prevents master seed from being added to this base class
    _accounts = {}

    # all_loaded_accounts() {{{2
    @classmethod
    def all_loaded_accounts(cls):
        for sub in cls.__subclasses__():
            # do not yield the base StealthAccount
            if sub != StealthAccount:
                yield sub
            for each in sub.all_loaded_accounts():
                yield each

    # get_account() {{{2
    @staticmethod
    def get_account(name):
        canonical = canonicalize(name)
        try:
            return Account._accounts[canonical]
        except KeyError:
            # account not found, assemble message giving suggested account names
            names = Account._accounts.keys()
            candidates = get_close_matches(canonical, names, 9, 0.6)

            # do not want to give multiple options all of which are aliases for
            # the same accounts, so look for up to three unique accounts
            close_matches = []
            for candidate in candidates:
                account = Account._accounts[candidate]
                if account not in close_matches:
                    close_matches.append(account)
                if len(close_matches) >= 3:
                    break

            # add the aliases to be base account names if available
            offer = []
            for account in close_matches:
                aliases = getattr(account, 'aliases', None)
                if aliases:
                    offer.append('{} ({})'.format(
                        account.get_name(), ', '.join(aliases)
                    ))
                else:
                    offer.append(account.get_name())

            # generate the message handling 0, 1, 2 or 3 candidates gracefully
            msg = 'account not found'
            if offer:
                msg = '{}, did you mean:\n    {}?'.format(
                    msg, conjoin(offer, sep=',\n    ', conj=', or\n    ')
                )
            raise PasswordError(full_stop(msg), culprit=name)

    # get_name() {{{2
    @classmethod
    def get_name(cls):
        """Get account name.

        Returns:
            Returns the primary account name. This is generally the class name
            converted to lower case unless it was overridden with the NAME
            attribute.
        """

        try:
            return cls.NAME
        except AttributeError:
            # consider converting lower to upper case transitions in __name__ to
            # dashes.
            return cls.__name__.lower()

    # get_seed() {{{2
    @classmethod
    def get_seed(cls):
        try:
            return cls.account_seed
        except AttributeError:
            return cls.get_name()

    # override_master() {{{2
    @classmethod
    def request_seed(cls):
        return getattr(cls, '_interactive_seed_', False)

    # preprocess() {{{2
    @classmethod
    def preprocess(cls, master, fileinfo, seen):

        # return if this account has already been processed
        if hasattr(cls, '_file_info_'):
            return  # account has already been processed

        # add fileinfo
        cls._file_info_ = fileinfo

        # dedent any string attributes
        for k, v in cls.__dict__.items():
            if is_str(v) and '\n' in v:
                setattr(cls, k, dedent(v))

        # add master seed
        if master and not hasattr(cls, '_%s__NO_MASTER' % cls.__name__):
            if not hasattr(cls, 'master_seed'):
                cls.master_seed = master
                cls._master_source_ = 'file'
            else:
                cls._master_source_ = 'account'

        # convert aliases to a list
        if hasattr(cls, 'aliases'):
            aliases = list(Collection(cls.aliases))
            cls.aliases = aliases
        else:
            aliases = []

        # canonicalize names and look for duplicates
        new = {}
        account_name = cls.get_name()
        path = cls._file_info_.path
        for name in [account_name] + aliases:
            canonical = canonicalize(name)
            Account._accounts[canonical] = cls
            if canonical in seen:
                if name == account_name:
                    warn('duplicate account name.', culprit=name)
                else:
                    warn('alias duplicates existing name.', culprit=name)
                codicil('Seen in %s in %s.' % seen[canonical])
                codicil('And in %s in %s.' % (account_name, path))
                break
            else:
                new[canonical] = (account_name, path)
        seen.update(new)
            # this two step approach to updating seen prevents us from
            # complaining about aliases that duplicate the account name,
            # or duplicate aliases, both of which are harmless

    # id_contains() {{{2
    @classmethod
    def id_contains(cls, target):
        target = target.lower()
        if target in cls.get_name().lower():
            return True
        try:
            for alias in cls.aliases:
                if target in alias.lower():
                    return True
        except AttributeError:
            pass
        return False

    # account_contains() {{{2
    @classmethod
    def account_contains(cls, target):
        if cls.id_contains(target):
            return True
        target = target.lower()
        for key, value in cls.items():
            try:
                if is_collection(value):
                    for k, v in Collection(value).items():
                        if target in v.lower():
                            return True
                elif target in value.lower():
                    return True
            except AttributeError:
                pass
        return False

    # recognize() {{{2
    @classmethod
    def recognize(cls, data, verbose):
        # try the specified recognizers
        discovery = getattr(cls, 'discovery', ())
        for recognizer in Collection(discovery):
            if isinstance(recognizer, Recognizer):
                script = recognizer.match(data, cls, verbose)
                name = getattr(recognizer, 'name', None)
                if script:
                    yield name, script
        if discovery:
            return

        # If no recognizers specified, just check the urls
        for url in Collection(cls.get_composite('urls', default=[])):
            url = str(url)  # this is needed because url may be Script
            url = url if '//' in url else ('//'+url)
            url_components = urlparse(url)
            if url_components.scheme:
                protocol = url_components.scheme
            else:
                protocol = get_setting('default_protocol')
            host = url_components.netloc
            if host == data.get('host'):
                if (protocol == data.get('protocol')):
                    if verbose:
                        log('    %s: matches.' % cls.get_name())
                    yield None, True
                    return
                else:
                    msg = 'URL matches, but uses wrong protocol.'
                    notify(msg)
                    raise PasswordError(msg, culprit=cls.get_name())

    # initialize() {{{2
    @classmethod
    def initialize(cls, interactive_seed=False, stealth_name=None):
        cls._interactive_seed_ = interactive_seed
        log('Initializing account:', cls.get_name())
        try:
            if cls.master_seed.is_secure():
                if not cls._file_info_.encrypted:
                    warn(
                        'high value master seed not contained in encrypted',
                        'account file.', culprit=cls.get_name()
                    )
        except AttributeError:
            pass
        for bad, good in get_setting('commonly_mistaken_attributes').items():
            if hasattr(cls, bad):
                warn(
                    '{} attribute found,'.format(bad),
                    'should probably be {}.'.format(good),
                    culprit=cls.get_name()
                )

        # do some more error checking
        if isinstance(getattr(cls, 'master_seed', None), GeneratedSecret):
            raise PasswordError(
                'master must not be a subclass of GeneratedSecret.',
                culprit=cls.get_name()
            )

    # fields() {{{2
    @classmethod
    def fields(cls, all=False, sort=False):
        if sort:
            fields = sorted(cls.__dict__)
        else:
            fields = cls.__dict__
        hidden_fields = get_setting('hidden_fields').split() + HIDDEN_TOOL_FIELDS
        for field in fields:
            if field.startswith('_') or is_forbidden_field(field):
                continue
            if all or field not in hidden_fields:
                yield field

    # items() {{{2
    @classmethod
    def items(cls, all=False, sort=False):
        for field in cls.fields(all, sort):
            yield field, getattr(cls, field)

    # get_fields() {{{2
    @classmethod
    def get_fields(cls, all=False, sort=False):
        """Iterate through fields.

        Iterates through the field names.

        Example::

            for name, keys in account.get_fields():
                for key, value in account.get_values(name):
                    display(indent(
                        value.render(('{f} ({d}): {v}', '{f}: {v}'))
                    ))

        Example::

            fields = [
                account.combine_field(name, key)
                for name, keys in account.get_fields()
                for key in keys
            ]
            for field in fields:
                value = account.get_value(field)
                display(f'{field}: {value!s}')

        Args:
            all (bool):
                If False, ignore the tool fields.

            sort (bool):
                If False, use natural sort order.

        Returns:
            A pair (2-tuple) that contains both field name and the key names.
            None is returned for the key names if the field holds a scalar value.
        """
        for field in cls.fields():
            value = getattr(cls, field)
            if is_collection(value):
                yield field, Collection(value).keys()
            else:
                yield field, [None]

    # get_scalar() {{{2
    @classmethod
    def get_scalar(cls, name, key=None, default=False):
        """Get field value given a field name and key.

        A lower level interface than *get_value()* that given a name and perhaps
        a key returns a scalar value.  Also takes an optional default value that is
        returned if the value is not found. Unlike *get_value()*, the actual value
        is returned, not a object that contains multiple facets of the value.

        The *name* is the field name, and the *key* would identity which value is
        desired if the field is a composite. If default is False, an error is raised
        if the value is not present, otherwise the default value itself is returned.

        Args:
            name (str):
                The name of the field.
            key (str or int)
                The key for the desired value (should be None for scalar values).
            default:
                The value to return if the requested value is not available.

        Returns:
            The requested value.
        """
        value = getattr(cls, name, None)
        if value is None:
            if name == 'NAME':
                return cls.get_name()
            if default is False:
                raise PasswordError(
                    'field not found.',
                    culprit=(cls.get_name(), cls.combine_field(name, key))
                )
            return default

        if key is None:
            if is_collection(value):
                choices = {}
                for k, v in Collection(value).items():
                    try:
                        desc = v.get_description()
                    except AttributeError:
                        desc = None
                    if desc:
                        choices['   %s: %s' % (k, desc)] = k
                    else:
                        choices['   %s:' % k] = k
                raise PasswordError(
                    'composite value found, need key. Choose from:',
                    *sorted(choices.keys()),
                    sep = '\n',
                    culprit=(cls.get_name(), cls.combine_field(name, key)),
                    is_collection = True,
                    collection = choices
                )
        else:
            try:
                if is_collection(value):
                    value = value[key]
                else:
                    warn('not a composite value, key ignored.', culprit=name)
                    key = None
            except (IndexError, KeyError, TypeError):
                raise PasswordError(
                    'key not found.',
                    culprit=(cls.get_name(), cls.combine_field(name, key))
                )

        # initialize the value if needed
        try:
            value.initialize(cls, name, key)
                # if Secret or Script, initialize otherwise raise exception
        except AttributeError:
            pass
        return value

    # is_secret() {{{2
    @classmethod
    def is_secret(cls, name, key=None):
        value = cls.__dict__.get(name)
        if isinstance(value, Script):
            str(value)  # side effect of convert to string is computing is_secret
            return value.is_secret
        if key is None:
            try:
                return value.is_secret
            except AttributeError:
                return isinstance(value, (GeneratedSecret, ObscuredSecret))
        else:
            try:
                try:
                    return value[key].is_secret
                except AttributeError:
                    return isinstance(value[key], (GeneratedSecret, ObscuredSecret))
            except (IndexError, KeyError, TypeError):
                raise PasswordError(
                    'not found.', culprit=cls.combine_field(name, key)
                )

    # split_field() {{{2
    @classmethod
    def split_field(cls, field):
        # Account fields can either be scalars or composites (vectors or
        # dictionaries). This function takes a string or tuple (field) that the
        # user provides to specify which account value they wish and splits it
        # into a name and a key.  If the field is a scalar, the key will be
        # None.  Users request a value using one of the following forms:
        #     True: use default name
        #     field: scalar value (key=None)
        #     index: questions (field->'questions', key=index)
        #     field[index] or field.index: for vector value
        #     field[key] or field.key: for dictionary value

        # handle empty field
        if field is True or field is False or field == '':
            field = None
        if field is None:
            field = cls.get_scalar('default', default=None)

        # convert field into integer if possible
        try:
            field = int(field)
        except (ValueError, TypeError):
            pass

        # separate field into name and key
        if type(field) is tuple:
            # split field if given as a tuple
            if len(field) == 1:
                name, key = field[0], None
            elif len(field) == 2:
                name, key = field[0], field[1]
            else:
                raise PasswordError('too many values.', culprit=field)
        elif is_str(field):
            # split field if given in the form: name[key]
            match = VECTOR_PATTERN.match(field)
            if match:
                name, key = match.groups()
            else:
                # split field if given in the form: name.key
                try:
                    name, key = field.split('.')
                except ValueError:
                    # must be simple name
                    name, key = field, None
        elif field is None:
            # handle defaulting
            defaults = get_setting('default_field').split()
            for default in defaults:
                if hasattr(cls, default):
                    name, key = default, None
                    break
            else:
                raise PasswordError(
                    'no default available, you must request a specific value.',
                    culprit=cls.get_name()
                )
        elif type(field) is int:
            name, key = get_setting('default_vector_field'), int(field)
        else:
            raise PasswordError('invalid field.', culprit=field)

        # look up name and key
        return cls.find_field(name, key)

    # find_field() {{{2
    @classmethod
    def find_field(cls, name, key=None):
        # look up field name while ignoring case and treating - and _ as same
        names = {
            canonicalize(n): n
            for n in cls.__dict__.keys()
            if not is_forbidden_field(n)
        }
        try:
            name = names[canonicalize(name)]
        except KeyError:
            raise PasswordError('field not found.', culprit=name)

        # name is now the true field name, now resolve key
        if key is None:
            return name, None
        try:
            return name, int(key)
        except ValueError:
            pass
        try:
            value = getattr(cls, name)
            keys = {
                canonicalize(n): n
                for n in value.keys()
            }
            try:
                key = keys[canonicalize(key)]
            except KeyError:
                raise PasswordError('key not found.', culprit=key)
        except AttributeError:
            key = None
        return name, key

    # combine_field() {{{2
    @classmethod
    def combine_field(cls, name, key=None):
        # Inverse of split_field().

        if key is None:
            return name
        else:
            return '%s.%s' % (name, key)

    # get_value() {{{2
    @classmethod
    def get_value(cls, field=None):
        """Get account value.

        Return value from the account given a user friendly identifier or
        script. User friendly identifiers include:

         |  *None*: value of default field
         |  *name*: scalar value
         |  *name.key* or *name[key]*:
         |      member of a dictionary or array
         |      key is string for dictionary, integer for array

        Scripts are simply strings with embedded attributes.
        Ex: *'username: {username}, password: {passcode}'*

        Args:
            field (str):
                Field identifier or script.

        Returns:
            :class:`avendesora.AccountValue`: the desired value.
        """
        # get default if field was not given
        if not field:
            field = cls.get_scalar('default', default=None)

        # determine whether field is actually a script
        is_script = is_str(field) and '{' in field and '}' in field

        if is_script:
            # run the script
            script = Script(field)
            script.initialize(cls)
            value = str(script)
            is_secret = script.is_secret
            name = key = desc = None
        else:
            name, key = cls.split_field(field)

            value = cls.get_scalar(name, key)
            is_secret = cls.is_secret(name, key)
            try:
                desc = value.get_description()
            except AttributeError:
                desc = None
        return AccountValue(value, is_secret, name, key, desc)

    # get_values() {{{2
    @classmethod
    def get_values(cls, name):
        """Iterate through the values for a field.

        Args:
            name (str):
                The name of the field.

        Returns:
            Returns a pair (2-tuple) that contains the key and the value given
            as an :class:`avendesora.AccountValue` for each of the values.  If
            the value is a scalar, the key is None.
        """
        name, key = cls.find_field(name)
        value = getattr(cls, name, None)
        if value is None:
            if name == 'NAME':
                value = cls.get_name()
            else:
                return

        values = Collection(value, splitter=False)
        for key, val in values.items():
            value = cls.get_scalar(name, key)
            is_secret = cls.is_secret(name, key)
            try:
                desc = value.get_description()
            except AttributeError:
                desc = None
            yield key, AccountValue(value, is_secret, name, key, desc)

    # get_composite() {{{2
    @classmethod
    def get_composite(cls, name, default=None):
        """Get field value given a field name.

        A lower level interface than *get_value()* that given a name returns the
        value of the associated field, which may be a scalar (string or integer)
        or a composite (array of dictionary).  Unlike *get_value()*, the actual
        value is returned, not a object that contains multiple facets of the
        value.

        Args:
            name (str):
                The name of the field.

        Returns:
            The requested value.
        """
        value = getattr(cls, name, None)
        if value is None:
            if name == 'NAME':
                return cls.get_name()
            return default

        if is_collection(value):
            if is_mapping(value):     # a dictionary or dictionary-like object
                result = {}
                for key in value.keys():
                    v = cls.get_scalar(name, key)
                    if isinstance(v, GeneratedSecret) or isinstance(v, ObscuredSecret):
                        v = str(v)
                    result[key] = v
            else:
                result = []
                for index in range(len(value)):
                    v = cls.get_scalar(name, index)
                    if isinstance(v, GeneratedSecret) or isinstance(v, ObscuredSecret):
                        v = str(v)
                    result.append(v)
        else:
            result = cls.get_scalar(name)
            if isinstance(result, GeneratedSecret) or isinstance(result, ObscuredSecret):
                result = str(result)
        return result

    # write_summary() {{{2
    @classmethod
    def write_summary(cls, all=False, sort=False):
        # present all account values that are not explicitly secret to the user
        label_color = get_setting('_label_color')
        highlight_color = get_setting('_highlight_color')

        def fmt_field(name, value='', key=None, level=0):
            hl = False

            # resolve values
            if isinstance(value, Script):
                hl = True
                value = value.script
            elif cls.is_secret(name, key):
                reveal = "reveal with: {}".format(highlight_color(join(
                    'avendesora',
                    'value',
                    cls.get_name(),
                    cls.combine_field(name, key)
                )))
                value = ', '.join(cull([value.get_description(), reveal]))
            elif isinstance(value, (GeneratedSecret, ObscuredSecret)):
                v = cls.get_scalar(name, key)
                value = ', '.join(cull([value.get_description(), str(v)]))
            else:
                value = str(value)

            # format values
            if '\n' in value:
                value = indent(dedent(value), get_setting('indent')).strip('\n')
                sep = '\n'
            elif value:
                sep = ' '
            else:
                sep = ''
            if hl:
                value = highlight_color(value)
            name = str(name).replace('_', ' ')
            leader = level * get_setting('indent')
            return indent(
                label_color((name if key is None else str(key)) + ':') + sep + value,
                leader
            )

        # preload list with the names associated with this account
        names = [cls.get_name()] + getattr(cls, 'aliases', [])
        lines = [fmt_field('names', ', '.join(names))]

        for key, value in cls.items(all=all, sort=sort):
            if is_collection(value):
                lines.append(fmt_field(key))
                for k, v in Collection(value).items():
                    lines.append(fmt_field(key, v, k, level=1))
            else:
                lines.append(fmt_field(key, value))

        output(*lines, sep='\n')

    @classmethod
    def extract(cls, value, name, key=None):
        "Generate all secrets in an account value"
        if not is_collection(value):
            if hasattr(value, 'initialize'):
                value.initialize(cls, name, key)
            return value
        try:
            return {k: cls.extract(v, name, k) for k, v in value.items()}
        except AttributeError:
            return [cls.extract(v, name, i) for i, v in enumerate(value)]

    # archive() {{{2
    @classmethod
    def archive(cls):
        """Return all account fields along with their values

        Used when creating the accounts archive.

        Returns:
            A dictionary containing all account fields with all secrets included.
        """
        return {
            k: cls.extract(v, k)
            for k, v in cls.items(True)
            if k not in ['master_seed', 'account_seed']
        }

    # export() {{{2
    @classmethod
    def export(cls, fold_level=1):
        # return all account fields along with their values
        """Return all account fields along with their values as a dictionary

        Used when exporting accounts.  If fold_level is truthy, it should be a
        positive integer that indicates the fold level for each account.

        Returns:
            A string that contains all account fields with all secrets included
            formatted as an Account class.
        """
        values = [
            '{} = {}'.format(k, render(cls.extract(v, k), level=1))
            for k, v in cls.items(True)
            if k not in ['master_seed', 'account_seed']
        ]
        return dedent('''
            class {name}(Account):{fold}
                {values}
        ''').format(
            fold = '  # {' + '{' + '{' + str(fold_level) if fold_level else '',
            name = cls.__name__,
            values = '\n    '.join(values),
        ).strip()

    # open_browser() {{{2
    @classmethod
    def open_browser(cls, key=None, browser_name=None, list_urls=False):
        if not browser_name:
            browser_name = cls.get_scalar('browser', default=None)
        browser = StandardBrowser(browser_name)
        label_color = get_setting('_label_color')
        highlight_color = get_setting('_highlight_color')

        # get the urls from the urls attribute
        # this must be second so it overrides those from recognizers.
        primary_urls = getattr(cls, 'urls', [])
        if type(primary_urls) != dict:
            if is_str(primary_urls):
                primary_urls = primary_urls.split()
            primary_urls = {None: primary_urls} if primary_urls else {}

        # get the urls from the url recognizers
        discovery = getattr(cls, 'discovery', ())
        urls = {}
        for each in Collection(discovery):
            urls.update(each.all_urls())

        # combine, primary_urls must be added to urls, so they dominate
        urls.update(primary_urls)

        if list_urls:
            default = getattr(cls, 'default_url', None)
            for name, url in urls.items():
                if is_collection(url):
                    url = list(Collection(url))[0]
                if name == default:
                    url += highlight_color(' [default]')
                    if not name:
                        name = ''
                elif not name:
                    continue
                output(label_color('{:>24s}:'.format(name)), url)
            return

        # select the urls
        keys = cull(list(urls.keys()))
        if not key:
            key = getattr(cls, 'default_url', None)
        if not key and keys and len(keys) == 1:
            key = keys[0]
        try:
            urls = urls[key]
        except KeyError:
            if keys:
                if key:
                    msg = 'unknown key, choose from {}.'
                else:
                    msg = 'key required, choose from {}.'
                raise PasswordError(
                    msg.format(conjoin(repr(k) for k in keys)), culprit=key
                )
            else:
                if key:
                    raise PasswordError(
                        'keys are not supported with urls on this account.',
                        culprit=key
                    )
                else:
                    raise PasswordError('no url available.')

        # open the url
        urls = Collection(urls)
        url = list(urls)[0]  # use the first url specified
        browser.run(url)

    # has_field() {{{2
    @classmethod
    def has_field(cls, name):
        return name in dir(cls)

    # get_username() {{{2
    @classmethod
    def get_username(cls):
        """Get the username.

        Like *get_value()*, but tries the *credential_ids* in order and returns
        the first found. *credential_ids* is an Avendesora configuration setting
        that by default is *username* and *email*.

        Returns:
            The username or email address.
        """
        identities = Collection(get_setting('credential_ids'))
        for identity in identities:
            try:
                return cls.get_value(identity)
            except PasswordError:
                pass

    # get_passcode() {{{2
    @classmethod
    def get_passcode(cls):
        """Get the passcode.

        Like *get_value()*, but tries the *credential_secrets* in order and returns
        the first found. *credential_secrets* is an Avendesora configuration setting
        that by default is *password*, *passphrase*, and *passcode*.

        Returns:
            The passcode.
        """
        secrets = Collection(get_setting('credential_secrets'))
        for passcode in secrets:
            try:
                return cls.get_value(passcode)
            except PasswordError:
                pass def read_config():
    if Config.get('READ'):
        return  # already read

    # First open the config file
    from .gpg import PythonFile
    path = get_setting('config_file')
    assert path.suffix.lower() not in ['.gpg', '.asc']
    config_file = PythonFile(path)
    if not config_file.exists():
        # have not yet initialized this account
        return
    try:
        contents = config_file.run()
        for k, v in contents.items():
            if k.startswith('_'):
                continue
            if k not in CONFIG_DEFAULTS:
                warn('%s: unknown.' % k, culprit=config_file)
                continue
            if k.endswith('_executable'):
                argv = v.split() if is_str(v) else list(v)
                path = Path(argv[0])
                if not path.is_absolute():
                    warn(
                        'should use absolute path for executables.',
                        culprit=(config_file, k)
                    )
            Config[k] = v
        Config['READ'] = True
    except PasswordError:
        comment('not found.', culprit=config_file)

    # Now open the hashes file
    hashes_file = PythonFile(get_setting('hashes_file'))
    try:
        contents = hashes_file.run()
        Config.update({k.lower(): v for k, v in contents.items()})
    except PasswordError:
        pass

    # Now open the account list file
    account_list_file = PythonFile(get_setting('account_list_file'))
    try:
        contents = account_list_file.run()
        Config.update({k.lower(): v for k, v in contents.items()})
    except PasswordError:
        pass

    # initilize GPG
    from .gpg import GnuPG
    GnuPG.initialize()

    # Now read the user key file
    user_key_file = get_setting('user_key_file')
    if user_key_file:
        user_key_file = PythonFile(get_setting('user_key_file'))
        try:
            contents = user_key_file.run()
            Config.update({
                k.lower(): v
                for k, v in contents.items()
                if not k.startswith('__')
            })
        except PasswordError:
            pass

    # Set the user-selected colors
    Config['_label_color'] = Color(
        color = get_setting('label_color'),
        scheme = get_setting('color_scheme'),
        enable = Color.isTTY()
    )
    Config['_highlight_color'] = Color(
        color = get_setting('highlight_color'),
        scheme = get_setting('color_scheme'),
        enable = Color.isTTY()
    ) def get_setting(name, default=None, expand=True):
    name = name.lower()
    try:
        value = Config[name]
    except KeyError:
        try:
            value = CONFIG_DEFAULTS[name]
        except KeyError:
            try:
                value = NONCONFIG_SETTINGS[name]
            except KeyError:
                return default
    if value is None:
        return default
    if name == 'gpg_ids':
        value = value.split() if is_str(value) else value
    elif name.endswith('_dir'):
        value = Path(value)
    elif expand and name.endswith('_file'):
        value = get_setting('settings_dir') / value
    return value def show_list_dialog(title, choices):

    # import gi and define ListDialog inside show_list_dialog to prevent the
    # import from slowing down startup. This way it is only imported if it
    # is used.

    import gi
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk as gtk
    from gi.repository import Gdk as gdk

    class ListDialog (gtk.Window):
        def __init__(self, title, choices):
            gtk.Window.__init__(self)
            self.set_type_hint(gdk.WindowTypeHint.DIALOG)
            self.connect('key_press_event', self.on_hotkey)
            self.connect('destroy', self.close)

            self.model = gtk.ListStore(str)
            self.view = gtk.TreeView(self.model)
            self.view.connect('button_press_event', self.on_mouse)

            cell = gtk.CellRendererText()
            column = gtk.TreeViewColumn(title, cell, text=0)
            self.view.append_column(column)

            self.choice = None
            self.choices = choices

            for choice in choices:
                row = self.model.append()
                self.model.set(row, 0, choice)

            self.add(self.view)

        def run(self):
            self.show_all()
            gtk.main()
            return self.choice

        def accept(self):
            path, column = self.view.get_cursor()
            iter = self.model.get_iter(path[0])
            self.choice = self.model.get_value(iter, 0)
            self.close()

        def close(self, *args):
            self.destroy()
            # Consume all pending events - include the window destroy
            while gtk.events_pending():
                gtk.main_iteration()
            gtk.main_quit()

        def on_hotkey(self, widget, event):
            key = gdk.keyval_name(event.keyval)
            selection = self.view.get_selection()
            model, iter = selection.get_selected()
            path = self.model.get_path(iter)

            def scroll(path, dx):
                return (path[0] + dx) % len(self.choices)

            if key in ['j', 'Down']:
                self.view.set_cursor(scroll(path, 1))
            elif key in ['k', 'Up']:
                self.view.set_cursor(scroll(path, -1))
            elif key in ['l', 'Right', 'Return']:
                self.accept()
            elif key in ['h', 'Left', 'Escape']:
                self.close()

            return True

        def on_mouse(self, widget, event):
            if event.type == gdk.EventType._2BUTTON_PRESS:
                self.accept()

    selector = get_setting('selection_utility')
    if selector == 'gtk':
        return ListDialog(title, choices).run()
    elif selector == 'dmenu':
        return dmenu_dialog(title, choices)
    raise Error('selection utility not found.', culprit=selector) class Dictionary(object):
    """Read Dictionary"""
    def __init__(self, dictionary=None):
        self.dictionary = dictionary
        self.words = known_dictionaries.get(dictionary)

    def get_words(self):
        if not self.words:
            if self.dictionary in known_dictionaries:
                self.words = known_dictionaries[self.dictionary]
            else:
                if not self.dictionary:
                    self.dictionary = get_setting('dictionary_file')
                if not self.dictionary or self.dictionary in ['default']:
                    self.words = WORDS
                elif self.dictionary in ['bip39', 'mnemonic']:
                    self.words = BIP39_WORDS
                else:
                    try:
                        path = to_path(self.dictionary)
                        contents = path.read_text()
                    except OSError as e:
                        raise PasswordError(os_error(e))
                    self.words = contents.split()
                known_dictionaries[self.dictionary] = self.words
        return self.words class PasswordError(Error):
    """Password error.

    This exception subclasses
    `Inform.Error <https://github.com/KenKundert/inform#exception>`_.
    """
    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs class AccountFiles:
    def __init__(self, warnings=True):  # {{{2
        # initialize object {{{3
        self.loaded = set()
        self.name_index = {}
        self.name_manifests = {}
        self.url_manifests = {}
        self.title_manifests = {}
        self.shared_secrets = {}
        self.existing_names = {}

        # check permissions, dates on account files {{{3
        most_recently_updated = 0
        mask = get_setting('account_file_mask')
        for filename in get_setting('accounts_files', []):
            path = get_setting('settings_dir') / filename
            resolved_path = path.resolve()

            # check file permissions
            permissions = getmod(resolved_path)
            violation = permissions & mask
            if violation:
                recommended = permissions & ~mask & 0o777
                warn(
                    "file permissions are too loose;",
                    f"tightening to {recommended:03o}.",
                    culprit = path
                )
                chmod(recommended, resolved_path)

            # determine time of most recently updated account file
            updated = resolved_path.stat().st_mtime
            if updated > most_recently_updated:
                most_recently_updated = updated

        # check for missing or stale archive file {{{3
        archive_file = get_setting('archive_file')
        if archive_file and warnings:
            if archive_file.exists():
                resolved_path = archive_file.resolve()

                # check file permissions
                permissions = getmod(resolved_path)
                violation = permissions & mask
                if violation:
                    recommended = permissions & ~mask & 0o777
                    warn(
                        "file permissions are too loose;",
                        f"tightening to {recommended:03o}.",
                        culprit = path
                    )
                    chmod(recommended, resolved_path)

                # warn user if archive file is out of date
                stale = float(get_setting('archive_stale'))
                archive_updated = resolved_path.stat().st_mtime
                if most_recently_updated > archive_updated:
                    log('Avendesora archive is {:.0f} hours old and out of date.'.format(
                        (most_recently_updated - archive_updated)/3600
                    ))
                    # archive_age = time() - archive_updated
                    account_age = time() - most_recently_updated
                    if account_age > 86400 * stale:
                        warn('stale archive.')
                        codicil(dedent("""\
                            Recommend running 'avendesora changed' to determine
                            which account entries have changed, and if all the
                            changes are expected, running 'avendesora archive'
                            to update the archive.
                        """), wrap=True)
                else:
                    log('Avendesora archive is up to date.')
            else:
                warn('archive missing.')
                codicil(
                    "Recommend running 'avendesora archive'",
                    "to create the archive."
                )

    def load_account_file(self, filename):  # {{{2
        if filename in self.loaded:
            return

        # read the file
        settings_dir = get_setting('settings_dir')
        path = settings_dir / filename
        account_file = PythonFile(path)
        contents = account_file.run()
        master_seed = contents.get('master_seed')
        if master_seed:
            self.shared_secrets[path.stem] = master_seed

        # traverse through all accounts and pass in fileinfo and master seed;
        # will be ignored if already set
        for account in Account.all_loaded_accounts():
            account.preprocess(master_seed, account_file, self.existing_names)

        self.loaded.add(filename)

    def load_account(self, name):  # {{{2
        canonical_name = canonicalize(name)
        self.read_manifests()
        if canonical_name in self.name_index:
            filename = self.name_index[canonical_name]
            self.load_account_file(filename)
            if canonical_name in Account._accounts:
                self.write_manifests()
                return
        # not in name_index, just read files until it is found
        for filename in get_setting('accounts_files', []):
            self.load_account_file(filename)
            if canonical_name in Account._accounts:
                break
        self.write_manifests()

    def load_account_files(self):  # {{{2
        for filename in get_setting('accounts_files', []):
            self.load_account_file(filename)
        self.write_manifests()

    def load_matching(self, title_components):  # {{{2
        self.read_manifests()
        found = False

        # load filenames that contain matching urls
        if 'host' in title_components:
            for filename, urls in self.url_manifests.items():
                for host, path, exact_path in urls:
                    if host != title_components['host']:
                        continue
                    if exact_path:
                        if path != title_components['path']:
                            continue
                    else:
                        if not title_components['path'].startswith(path):
                            continue
                    self.load_account_file(filename)
                    found = True

        # load filenames that contain matching titles
        title = title_components.get('rawtitle')
        if title:
            for filename, titles in self.title_manifests.items():
                for candidate in titles:
                    if fnmatch(title, candidate):
                        self.load_account_file(filename)
                        found = True

        # url/title not in cache (perhaps cache was missing), load everything
        if not found:
            self.load_account_files()

    def read_manifests(self):  # {{{2
        if self.name_index:
            return
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        try:
            encrypted = manifests_path.read_bytes()

            user_key = get_setting('user_key')
            if not user_key:
                raise Error('no user key.')
            key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest())
            fernet = Fernet(key)
            contents = fernet.decrypt(encrypted)

            try:
                cache = pickle.loads(contents, **PICKLE_ARGS)
                self.name_manifests = cache['names']
                self.url_manifests = cache['urls']
                self.title_manifests = cache['titles']

                # build the name_index by inverting the name_manifests
                self.name_index = {
                    h: n for n, l in self.name_manifests.items() for h in l
                }
            except (ValueError, pickle.UnpicklingError) as e:
                warn('garbled manifest.', culprit=manifests_path, codicil=str(e))
                manifests_path.unlink()
            assert isinstance(self.name_index, dict)
        except OSError as e:
            comment(os_error(e))
        except Exception as e:
            comment(e)

    def write_manifests(self):  # {{{2
        # do not modify existing name_index if no account files were loaded
        if not self.loaded:
            return
        log('Updating manifests.')
        name_manifests = self.name_manifests
        url_manifests = self.url_manifests
        title_manifests = self.title_manifests

        # remove entries for loaded files from name_index
        for filename in self.loaded:
            name_manifests[filename] = []
            url_manifests[filename] = []
            title_manifests[filename] = []

        # build name manifests
        for name, account in Account._accounts.items():
            filename = account._file_info_.path.name
            name_manifests[filename].append(name)

        # build url and title manifests
        for account in Account.all_loaded_accounts():
            filename = account._file_info_.path.name
            if filename not in self.loaded:
                # This should not occur, but if PasswordGenerator is
                # instantiated more than once, which should only occur in the
                # tests, then there could be accounts hanging around from
                # previous instantiations. Ignore them.
                continue
            discovery = getattr(account, 'discovery', ())

            # urls
            urls = []
            for each in Collection(discovery):
                for u in each.all_urls(components=True).values():
                    urls += u
            # urls is a list of triples, each triple consists of the host and
            # path components of the url, and a boolean indicating whether the
            # entire path must match.
            url_manifests[filename].extend(urls)

            # titles
            titles = []
            for each in Collection(discovery):
                for t in each.all_titles().values():
                    titles += t
            title_manifests[filename].extend(titles)

        # trip out the duplicates
        for filename in url_manifests:
            if filename in self.loaded:
                url_manifests[filename] = set(url_manifests[filename])
                title_manifests[filename] = set(title_manifests[filename])

        # write the manifests
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        cache = dict(
            names=name_manifests, urls=url_manifests, titles=title_manifests
        )
        contents = pickle.dumps(cache)

        user_key = get_setting('user_key')
        if not user_key:
            return
        key = base64.urlsafe_b64encode(sha256(user_key.encode('ascii')).digest())
        fernet = Fernet(key)
        encrypted = fernet.encrypt(contents)

        try:
            mkdir(cache_dir)
            manifests_path.write_bytes(encrypted)
        except OSError as e:
            warn(os_error(e))

    @staticmethod
    def delete_manifests():  # {{{2
        cache_dir = get_setting('cache_dir')
        manifests_path = cache_dir / MANIFESTS_FILENAME
        rm(manifests_path) class GnuPG(object):
    def __init__(self, path):
        self.path = to_path(path)

    @classmethod
    def initialize(cls, gpg_path=None, gpg_home=None, armor=None):

        cls.gpg_path = to_path(
            gpg_path if gpg_path else get_setting('gpg_executable')
        )
        override_setting('gpg_executable', cls.gpg_path)

        cls.gpg_home = to_path(
            gpg_home if gpg_home else get_setting('gpg_home')
        )
        override_setting('gpg_home', cls.gpg_home)

        armor = armor if armor is not None else get_setting('gpg_armor')
        if armor not in ARMOR_CHOICES:
            warn(
                "'%s' is not valid, choose from %s." % (
                    armor, conjoin(ARMOR_CHOICES)
                ), culprit=setting_path('gpg_armor')
            )
            armor = 'extension'
        cls.armor = armor
        override_setting('gpg_armor', armor)

        gpg_args = {}
        if cls.gpg_path:
            gpg_args.update({'gpgbinary': str(cls.gpg_path)})
        if cls.gpg_home:
            gpg_args.update({'gnupghome': str(cls.gpg_home)})
        try:
            cls.gpg = gnupg.GPG(**gpg_args)
        except ValueError as e:
            fatal(e)

    def save(self, contents, gpg_ids=None):
        path = self.path
        if not gpg_ids:
            gpg_ids = get_setting('gpg_ids', [])
        if is_str(gpg_ids):
            gpg_ids = gpg_ids.split()
        if not gpg_ids:
            # raise PasswordError('must specify GPG ID.')
            log('no gpg id available, using symmetric encryption.')

        use_gpg, use_armor = self._choices()
        if use_gpg:
            try:
                encoded = contents.encode(get_setting('encoding'))
                if gpg_ids:
                    encrypted = self.gpg.encrypt(encoded, gpg_ids, armor=use_armor)
                else:
                    encrypted = self.gpg.encrypt(encoded, None, symmetric='AES256', armor=use_armor)
                if not encrypted.ok:
                    msg = ' '.join(cull([
                        'unable to encrypt.',
                        getattr(encrypted, 'stderr', None)
                    ]))
                    raise PasswordError(msg, culprit=path, sep='\n')
                else:
                    path.write_bytes(encrypted.data)
            except ValueError as e:
                raise PasswordError(full_stop(e), culprit=path)
        else:
            path.write_text(contents, encoding=get_setting('encoding'))
        self.chmod()

    def read(self):
        path = self.path
        # file is only assumed to be encrypted if path has gpg extension
        if path.suffix.lower() in GPG_EXTENSIONS:
            try:
                with path.open('rb') as f:
                    decrypted = self.gpg.decrypt_file(f)
                    if not decrypted.ok:
                        msg = ' '.join(cull([
                            'unable to decrypt.',
                            getattr(decrypted, 'stderr', None)
                        ]))
                        raise PasswordError(msg, culprit=path, sep='\n')
            except ValueError as e:
                raise PasswordError(full_stop(e), culprit=path)
            except OSError as e:
                raise PasswordError(os_error(e))
            return decrypted.data.decode(get_setting('encoding'))
        else:
            return path.read_text(encoding=get_setting('encoding'))

    def _choices(self):
        extension = self.path.suffix.lower()
        if extension in GPG_EXTENSIONS:
            if self.armor == 'never':
                return True, False
            elif self.armor != 'always' and extension == '.gpg':
                return True, False
            return True, True
        else:
            return False, False

    def remove(self):
        self.path.unlink()

    def backup(self, extension):
        """Creates a backup copy of the file.

        The name of the new file has the specified extension prepended to the
        existing suffixes.
        """
        if extension:
            # prepend extension to list of suffixes
            suffixes = self.path.suffixes
            stem = self.path.stem.partition('.')[0]  # remove all suffixes
            new = to_path(self.path.parent, ''.join([stem, extension] + suffixes))
            self.backup_path = new

            cp(self.path, new)
            return new

    def restore(self):
        "Restores the backup copy of the file."
        cp(self.backup_path, self.path)

    def will_encrypt(self):
        return self.path.suffix in GPG_EXTENSIONS

    def exists(self):
        return self.path.exists()

    def chmod(self, mode=0o600):
        self.path.chmod(mode)

    def __str__(self):
        return str(self.path) class PythonFile(GnuPG):
    def run(self):
        global ActivePythonFile
        ActivePythonFile = self.path
        path = self.path
        self.encrypted = path.suffix in ['.gpg', '.asc']
        narrate('reading.', culprit=path)
        try:
            self.code = self.read()
                # need to save the code for the new command
        except OSError as e:
            raise PasswordError(os_error(e))

        try:
            compiled = compile(self.code, str(path), 'exec')
        except SyntaxError as e:
            culprit = (e.filename, e.lineno)
            if e.text is None or e.offset is None:
                raise PasswordError(full_stop(e.msg), culprit=culprit)
            else:
                raise PasswordError(
                    e.msg + ':', e.text.rstrip(), (e.offset-1)*' ' + '^',
                    culprit=culprit, sep='\n'
                )
                # File "/home/ken/.config/avendesora/config", line 18
                #   'c': 'google-chrome %s'
                #      ^

        contents = {}
        try:
            exec(compiled, contents)
        except Exception as e:
            from .utilities import error_source
            raise PasswordError(full_stop(e), culprit=error_source())
        ActivePythonFile = None
        return contents

    def create(self, contents, gpg_ids=None):
        path = self.path
        try:
            # check to see if file already exists
            if path.exists():
                # file creation (init) requested, but file already exists
                # don't overwrite the file, instead read it so the information
                # can be used to create any remaining files.
                display("%s: already exists." % path)
                return

            # create the file
            display('%s: creating.' % path)
            if path.suffix in ['.gpg', '.asc']:
                narrate('encrypting.', culprit=path)
                # encrypt it
                if not gpg_ids:
                    raise PasswordError('gpg_ids missing.')
                self.save(contents, gpg_ids)
            else:
                narrate('not encrypting.', culprit=path)
                # file is not encrypted
                with path.open('wb') as f:
                    f.write(contents.encode(get_setting('encoding')))
        except OSError as e:
            raise PasswordError(os_error(e)) GPG_EXTENSIONS = ['.gpg', '.asc'] class Hidden(ObscuredSecret):
    """Hidden text

    This encoding obscures but does not encrypt the text.

    Args:
        encoded_text (str):
            The value of interest encoded in base64.
        secure (bool):
            Indicates that this secret is of high value and so should not be
            found in an unencrypted accounts file.
        encoding (str):
            The encoding to use for the decoded text.
        is_secret (bool):
            Should value be hidden from user unless explicitly requested.

    Raises:
        :exc:`avendesora.PasswordError`: invalid value.
    """

    NAME = 'base64'
    DESC = '''
        This encoding obscures but does not encrypt the text. It can
        protect text from observers that get a quick glance of the
        encoded text, but if they are able to capture it they can easily
        decode it.
    '''

    def __init__(self, encoded_text, *, secure=True, encoding=None, is_secret=True):
        self.encoded_text = encoded_text
        encoding = encoding if encoding else get_setting('encoding')
        try:
            self.plaintext = a2b_base64(encoded_text).decode(encoding)
            self.secure = secure
        except BinasciiError as e:
            raise PasswordError(
                'invalid value specified to Hidden(): %s.' % str(e),
                culprit=error_source()
            )
        self.is_secret = is_secret

    def is_secure(self):
        return self.secure

    def render(self):
        return self.plaintext

    @staticmethod
    def conceal(plaintext, decorate=False, encoding=None, **kwargs):
        encoding = encoding if encoding else get_setting('encoding')
        plaintext = str(plaintext).encode(encoding)
        encoded = b2a_base64(plaintext).rstrip().decode('ascii')
        if decorate:
            return decorate_concealed('Hidden', encoded)
        else:
            return encoded

    @staticmethod
    def reveal(value, encoding=None):
        encoding = encoding if encoding else get_setting('encoding')
        try:
            value = a2b_base64(value.encode('ascii'))
            return value.decode(encoding)
        except BinasciiError as e:
            raise PasswordError(
                str(e), template='Unable to decode base64 string: {0}.'
            )
        except UnicodeDecodeError:
            raise PasswordError('Unable to decode base64 string.') CONFIG_DEFAULTS = {
    'accounts_files': [
        NONCONFIG_SETTINGS['default_accounts_file'],
        NONCONFIG_SETTINGS['default_stealth_accounts_file'],
    ],
    'account_list_file': 'accounts_files',
    'archive_file': 'archive.gpg',
    'archive_stale': 1,
    'previous_archive_file': 'archive.prev.gpg',
    'config_dir_mask': 0o077,
    'account_file_mask': 0o077,
    'command_aliases': None,
    'credential_ids': 'username email',
    'credential_secrets': 'passcode password passphrase',
    'label_color': 'blue',
    'highlight_color': 'magenta',
    'color_scheme': 'dark',
    'browsers': {
        'c':  'google-chrome {url}',
        'ci': 'google-chrome --incognito {url}',
        'f':  'firefox -new-tab {url}',
        'fp':  'firefox -private-window {url}',
        'q':  'qutebrowser {url}',
        't':  'torbrowser {url}',
        'x':  'xdg-open {url}',  # system default browser
    },
    'help_url': 'https://avendesora.readthedocs.io/en/latest',
    'default_field': 'passcode password passphrase',
    'default_vector_field': 'questions',
    'dynamic_fields': '',
    'hidden_fields': '',
    'default_browser': 'x',
    'display_time': 60,
    'ms_per_char': None,
    'dictionary_file': None,
    'encoding': 'utf-8',
    'edit_account': (
        'gvim',                       # use gvim -v so that user can access
        '-v',                         # the X clipboard buffers
        '+silent! /^class {account}(Account):/',
        '+silent! normal zozt',       # open the fold, position near top of screen
        '{filepath}'
    ),
    'edit_template': (
        'gvim',                       # use gvim -v so that user can access
        '-v',                         # the X clipboard buffers
        r'+silent! /_[A-Z0-9_]\+_/',  # matches user modifiable template fields
                                      # fields take the form '_AAA_'
        '+silent! normal zozt',       # open the fold, position near top of screen
        '{filepath}'
    ),
    'hashes_file': 'hashes',
    'indent': '    ',
    'log_file': 'log.gpg',
    'default_protocol': 'https',
    'user_key_file': 'key.gpg',
    'use_pager': True,
    'selection_utility': 'gtk',
    'verbose': False,
    'default_account_template': 'bank',
    'account_templates': {
        'website': dedent("""
            class _NAME_(Account): # %s1
                desc = '_DESCRIPTION_'
                aliases = '_ALIAS1_ _ALIAS2_'
                username = '_USERNAME_'
                email = '_EMAIL_'
                passcode = Password()
            # Avendesora: above provides a random selection of letters and digits
            # Avendesora: Alternative 1: passcode = PasswordRecipe('12 2u 2d 2s')
            # Avendesora:     length is 12, includes 2 upper, 2 digits and 2 symbols
            # Avendesora:     use '12 2u 2d 2c!@#$&' to specify valid symbol characters.
            # Avendesora: Alternative 2: passcode = Passphrase()
            # Avendesora:     provides a random selection of 4 words
                questions = [
                    Question("_QUESTION1_?"),
                    Question("_QUESTION2_?"),
                    Question("_QUESTION3_?"),
                ]
                urls = '_URL_'
            # Avendesora: specify URL or URLs to be used by the browse command.
            # Avendesora: generally this is the URL of the sign-in page.
                discovery = RecognizeURL(
                    'https://_URL_',
                    script='{email}{tab}{passcode}{return}'
                )
            # Avendesora: Specify multiple urls to recognizer if several pages need same script.
            # Avendesora: Specify list of recognizers if multiple pages need different scripts.

            # Avendesora: Tailor the account entry to suit you needs.
            # Avendesora: You can add or delete class attributes as you see fit.
            # Avendesora: The 'n' key should take you to the next field name.
            # Avendesora: Use 'cw' to specify a field value, or 'dd' to delete it if unneeded.
            # Avendesora: Fields surrounded by << and >> will be hidden.
            # Avendesora: All lines that begin with '# Avendesora:' are automatically removed.
        """ % (3*'{')),
        'shell': dedent("""
            class _NAME_(Account): # %s1
                desc = '_DESCRIPTION_'
                aliases = '_ALIAS1_ _ALIAS2_'
                passcode = Passphrase()
            # Avendesora: Alternatively use PasswordRecipe('12 2u 2d 2s')
            # Avendesora: or '12 2u 2d 2c!@#$&' to specify valid symbol characters.
                discovery = RecognizeTitle(
                    '_TITLE1_', '_TITLE2_',
                    script='{passcode}{return}'
                )

            # Avendesora: Tailor the account entry to suit you needs.
            # Avendesora: You can add or delete class attributes as you see fit.
            # Avendesora: The 'n' key should take you to the next field name.
            # Avendesora: Use 'cw' to specify a field name, or delete it if unneeded.
            # Avendesora: Fields surrounded by << and >> will be hidden.
            # Avendesora: All lines that begin with '# Avendesora:' are deleted.
        """ % (3*'{')),
        'bank': dedent("""
            class _NAME_(Account): # %s1
                desc = '_DESCRIPTION_'
                aliases = '_ALIAS1_ _ALIAS2_'
                username = '_NAME_'
                email = '_EMAIL_'
                accounts = {
                    'checking':   <<_ACCOUNT_NUMBER_>>,
                    'savings':    <<_ACCOUNT_NUMBER_>>,
                    'creditcard': <<_ACCOUNT_NUMBER_>>,
                }
                customer_service = '_PHONE_NUMBER_'
                passcode = PasswordRecipe('12 2u 2d 2s')
            # Avendesora: length is 12, includes 2 upper, 2 digits and 2 symbols
            # Avendesora: Alternatively use '12 2u 2d 2c!@#$&' to specify valid symbol characters.
            # Avendesora: Alternatively use Passphrase()
                verbal = Passphrase(length=2)
                pin = PIN()
                questions = [
                    Question("_QUESTION1_?"),
                    Question("_QUESTION2_?"),
                    Question("_QUESTION3_?"),
                ]
                urls = '_URL_'
            # Avendesora: specify urls to indicate the url to be used by the browse command.
                discovery = RecognizeURL(
                    'https://_URL_',
                    script='{email}{tab}{passcode}{return}'
                )
            # Avendesora: Specify multiple urls to recognizer if several pages need same script.
            # Avendesora: Specify list of recognizers if multiple pages need different scripts.

            # Avendesora: Tailor the account entry to suit you needs.
            # Avendesora: You can add or delete class attributes as you see fit.
            # Avendesora: The 'n' key should take you to the next field name.
            # Avendesora: Use 'cw' to specify a field name, or delete it if unneeded.
            # Avendesora: Fields surrounded by << and >> will be hidden.
            # Avendesora: All lines that begin with '# Avendesora:' are deleted.
        """ % (3*'{')),
    },

    # use absolute paths for executables so they cannot be maliciously replaced
    # by changing the path.
    'gpg_executable': '/usr/bin/gpg2',
    'gpg_home': '~/.gnupg',
    'gpg_armor': 'extension',
    'gpg_ids': None,
    'xdotool_executable': '/usr/bin/xdotool',
    'xsel_executable': '/usr/bin/xsel',
    'dmenu_executable': '/usr/bin/dmenu',
    'arp_executable': '/sbin/arp',
} NONCONFIG_SETTINGS = {
    'config_file': 'config',
    'config_doc_file': 'config.doc',
    'settings_dir': user_config_dir('avendesora'),
    'cache_dir': user_cache_dir('avendesora'),
    'default_accounts_file': 'accounts.gpg',
    'default_stealth_accounts_file': 'stealth_accounts',
    'charsets_hash': 'a055240c4c498e1470f5f3e80b3ec599',
    'dict_hash': '5dbabd4114eae520c1de8963a8b8d09d',
    'mnemonic_hash': 'cafd522d6627011d78e576f2d0b6ed58',
    'secrets_hash': '0826ec53a58a607e65bbfca7eafb4f19',
    'discard_logfile': False,
    'commonly_mistaken_attributes': {
        'url': 'urls',
        'alias': 'aliases',
        'description': 'desc',
    }
} ACCOUNTS_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Accounts
    # vim: filetype=python sw=4 sts=4 et ai ff=unix fileencoding={encoding} foldmethod=marker :

    # Description {section}
    # Subclass Account to create new accounts. Attributes of the subclass are
    # the account fields. Fields can be strings, lists, dictionaries or
    # Avendesora classes.
    #
    # Example:
    #     class Mortgage(Account):
    #         username = 'herbie962'
    #         account = '0036727963'
    #         passcode = PasswordRecipe('12 2u 2d 2s')
    #         phone = '800-529-2345'
    #         discovery = [
    #             RecognizeURL(
    #                 'https://www.loanservicing.com/login.jsp',
    #                 script='{{account}}{{tab}}{{passcode}}{{return}}',
    #             ),
    #         ]

    # Imports {section}
    from avendesora import (
        # Basics
        Account, StealthAccount, Hide, Hidden, GPG, Script, WriteFile,

        # Character sets
        exclude, LOWERCASE, UPPERCASE, LETTERS, DIGITS, ALPHANUMERIC,
        HEXDIGITS, PUNCTUATION, SYMBOLS, WHITESPACE, PRINTABLE, DISTINGUISHABLE,

        # Secrets
        Password, Passphrase, PIN, Question, MixedPassword, PasswordRecipe,
        BirthDate, OTP,

        # Account Discovery
        RecognizeAll, RecognizeAny, RecognizeCWD, RecognizeEnvVar,
        RecognizeFile, RecognizeHost, RecognizeNetwork, RecognizeTitle,
        RecognizeURL, RecognizeUser,
    )
    try:
        import Scrypt
    except ImportError:
        # You need to install scrypt using 'pip install scrypt' to use Scrypt
        pass

    # Shared Values {section}
    # These values are shared across all accounts defined in this file.
    #
    # Never disclose the master seed to anyone you are not collaborating with.
    # Never give it to your collaborator using an insecure channel, such as
    # email.  If you have encrypted this file with your colloborator's public
    # GPG key, is best to simply send the encrypted file to your
    # collaborator the first time. After than, you can send the account
    # informaiton in clear text as long as you are careful not to expose the
    # master_seed.
    master_seed = Hidden({master_seed}, secure=True)
    gpg_ids = {gpg_ids}


    # Accounts
''') CONFIG_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Configuration
    #
    # This file is read by Avendesora. Use it to change any configuration
    # settings from their default values.  See {config_doc_file} for the list of
    # available settings along with a description and the default value of the
    # setting.

    # Default identity to use when creating encrypted files.
    gpg_ids = {gpg_ids}

    default_browser = 'f'
    browsers = {{
        'c': 'google-chrome {{url}}',
        'ci': 'google-chrome --incognito {{url}}',
        'f': 'firefox -new-tab {{url}}',
        'fp': 'firefox -private-window {{url}}',
        'q': 'qutebrowser {{url}}',
        't': 'torbrowser {{url}}',
        'x': 'xdg-open {{url}}',
    }}
    command_aliases = dict(
        a = 'add',
        A = 'archive',
        b = 'browse',
        bc = 'browse --browser c',
        c = 'conceal',
        C = 'changed',
        e = 'edit',
        f = 'find',
        h = 'help',
        ident = 'identity',
        I = 'identity',
        init = 'initialize',
        i = 'interactive',
        login = 'credentials',
        l = 'credentials',
        N = 'new',
        alphabet = 'phonetic',
        p = 'phonetic',
        quest = 'questions',
        q = 'questions',
        qc = 'questions --clipboard',
        r = 'reveal',
        s = 'search',
        val = 'value',
        v = 'value',
        vc = 'value --clipboard',
        vals = 'values',
        vs = 'values',
        V = 'values',
    )
    # vim: filetype=python sw=4 sts=4 et ai ff=unix nofen fileencoding={encoding} :
''') CONFIG_DOC_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Configuration Documentation
    #
    # This file is generated and updated by Avendesora using the initialize
    # command. It documents the supported configuration settings and gives the
    # default values for those settings. However it is not read by Avendesora. To
    # change your configuration settings, you should modify your config file:
    # {config_file}. In general you want to include only those settings that
    # differ from their defaults in {config_file}. In this way, if the defaults
    # change as Avendesora is upgraded you will naturally pick up the new
    # values.
    #
    # This file is replaced every time you run 'avendesora initialize'. Any
    # changes you make to this file will be lost.

    # Avendesora settings
    log_file = {log_file}
        # The desired location of the log file (relative to config directory).
        # Adding a suffix of .gpg or .asc causes the file to be encrypted
        # (otherwise it can leak account names). Use None to disable logging.

    archive_file = {archive_file}
    previous_archive_file = {previous_archive_file}
        # The desired location of the archive files (relative to config director).
        # End the path in .gpg or .asc. Use None to disable archiving.

    archive_stale = {archive_stale}
        # The archive file is consider stale if it is this many days older than
        # the most recently updated account file.

    default_field = {default_field}
        # The name of the field to use for the value command when one is not
        # given. May be a space separated list of names, in which case the first
        # that is found is used.

    default_vector_field = {default_vector_field}
        # The name of the field to use when an integer is given as the argument
        # to the value command. In this case the field is expected to be a list
        # and the argument is taken to be the index of the desired value.
        # For example, if default_vector_field is 'question' and the argument
        # given with the value command is 1, then question[1] is produced.

    dynamic_fields = {dynamic_fields}
        # Fields whose values can change in real time. These fields will not be
        # mentioned by the *changed* command, even if their value differs from
        # when the last archive was created.

    hidden_fields = {hidden_fields}
        # Names of fields that should not appear in the summary produced by the
        # values command unless the --all option is specified.  A typical value
        # includes estimated_value, postmortem_recipients, and bitwarden.

    credential_ids = {credential_ids}
        # A string that contains the field names (space separated) that should be
        # considered by the credentials command for the account identity.

    credential_secrets = {credential_secrets}
        # A string that contains the field names (space separated) that should be
        # considered by the credentials command for the primary account secret.

    display_time = {display_time}
        # The number of seconds that the secret will be displayed before it is
        # erase when writing to the TTY or clibboard.

    ms_per_char = {ms_per_char}
        # The time between keystrokes when autotyping in milliseconds. The
        # default is 12ms.

    encoding = {encoding!r}
        # The unicode encoding to use when reading or writing files.

    edit_account = {edit_account}
        # The command used when editing an account. The command is given as
        # list of strings. The strings may contain {{filepath}} and {{account}},
        # which are replaced by the path to the file and the name of the
        # account.

    edit_template = {edit_template}
        # The command used when creating a new account that has been initialized
        # with a template. The command is given as list of strings. The strings
        # may contain {{filepath}}, which is replaced by the path to the file.

    browsers = {browsers}
        # A dictionary containing the supported browsers. For each entry the key
        # is the name to be used for the browser, and the value is string that
        # contains the command that invokes the browser. The value may contain
        # {{url}}, which is replaced by the URL to open.

    default_browser = {default_browser}
        # The name of the default browser. This name should be one of the keys
        # in the browsers dictionary.

    command_aliases = {command_aliases}
        # A dictionary of command short cuts. Each alias should map to two
        # values, the full command name and a list of command line options,
        # which may be None.  For example:
        #     command_aliases = dict(
        #         v = ('value', None),
        #         vc = ('value', ['--clipboard']),
        #     )

    default_protocol = {default_protocol}
        # The default protocol to use for a URL if the protocol is not specified
        # in the requested URL. Generally this should be 'https' or 'http',
        # though 'https' is recommended.

    config_dir_mask = {config_dir_mask}
        # An integer that determines if a warning should be printed about the
        # file permissions on the Avendesora configuration directory
        # (~/.config/avendesora) being too loose. A bitwise and operation is
        # performed between this value and the actual file permissions, and if
        # the result is nonzero, a warning is printed.  Set to 0o000 to disable
        # the warning. Set to 0o077 to generate a warning if the configuration
        # directory is readable or writable by the group or others. Set to 0o007
        # to generated a warning if the directory is readable or writable by
        # others.

    account_file_mask = {account_file_mask}
        # An integer that determines if a warning should be printed about the
        # file permissions of the accounts and archive files being too loose.  A
        # bitwise and operation is performed between this value and the actual
        # file permissions, and if the result is nonzero, a warning is printed.
        # Set to 0o000 to disable the warning. Set to 0o077 to generate a
        # warning if the configuration directory is readable or writable by the
        # group or others. Set to 0o007 to generated a warning if the directory
        # is readable or writable by others.

    label_color = {label_color}
        # The color of the label use by the value and values commands.
        # Choose from black, red, green, yellow, blue, magenta, cyan, white.

    highlight_color = {highlight_color}
        # The color of the highlight use by the value and values commands.
        # Choose from black, red, green, yellow, blue, magenta, cyan, white.

    color_scheme = {color_scheme}
        # The color scheme used for the label color.  Choose from dark, light.
        # If the shell background color is light, use dark.

    use_pager = {use_pager}
        # Use a external program to break long output into pages.
        # May be either a boolean or a string. If a string the string is taken
        # to be a command line use to invoke a paging program (like 'more'). If
        # True, the program name is taken from the PAGER environment variable if
        # set, or 'less' is used if not set. If False, a paging program is not
        # used.

    selection_utility = {use_pager}
        Which utility should be used when it becomes necessary for you to
        interactively make a choice. Two utilities are available: 'gtk', the
        default, and 'dmenu'.

        'gtk' is the built-in selection. When needed it pops a small dialog box in
        the middle of the screen. You can use the 'j' and 'k' to navigate to your
        selection and 'l' to make the selection or 'h' to cancel.  Alternately you
        can use the arrow keys and Enter and Esc to navigate, select, and cancel.

        'dmenu' is an external utility, and must be installed. With *dmenu* you
        type the first few letters of your selection to highlight it, then type
        'Enter' to select or 'Esc' to cancel.

    verbose = {verbose}
        # Set this to True to generate additional information in the log file
        # that can help debug account discovery issues.  Normally it should be
        # False to avoid leaking account information into log file.
        # This is most useful when debugging account discovery, and in that case
        # this setting has largely been superseded by the use of the --title and
        # --verbose command line options.

    help_url = {help_url}
        # The base URL for Avendesora's online documentation.

    # Account templates
    # Use these settings to specify the available account templates that are
    # used when creating new accounts.  The templates are given as a dictionary
    # where the key is the name of the template and the value is the template
    # itself. The template is passed through textwrap.dedent() to remove any
    # leading white space.  Any lines that begin with '# Avendesora: ' represent
    # comments that can contain instructions to the user. They will are removed
    # when the account is created.
    account_templates = {account_templates}
    default_account_template = {default_account_template}

    # GPG settings
    # Information used by GPG when encrypting and decrypting files.
    gpg_ids = {gpg_ids}
        # the GPG IDs to use by default when creating encrypted files (the
        # archive and account files).
    gpg_armor = {gpg_armor}
        # In the GPG world, armoring a file means converting it to simple ASCI.
        # Choose between 'always', 'never' and 'extension' (.asc: armor, .gpg:
        # no).
    gpg_home = {gpg_home}
        # This is your GPG home directory. By default it will be ~/.gnupg.

    # External Executables
    # Use of full absolute paths for executables is recommended.
    gpg_executable = {gpg_executable}
        # Path to the gpg2 executable.
    xdotool_executable = {xdotool_executable}
    xsel_executable = {xsel_executable}
        # recommend '/usr/bin/xsel -p' if you wish to use mouse middle click
        # recommend '/usr/bin/xsel -b' if you wish to use mouse right click then paste
    dmenu_executable = {dmenu_executable}

    # vim: filetype=python sw=4 sts=4 et ai ff=unix nofen fileencoding={encoding} :
''') USER_KEY_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Personal Key
    # vim: filetype=python sw=4 sts=4 et ai ff=unix fileencoding={encoding} :
    #
    # This is your personal encryption key.
    # DO NOT CHANGE THIS KEY.  DO NOT SHARE THIS KEY.
    user_key = ({user_key})
''') HASH_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Hashes
    # vim: filetype=python sw=4 sts=4 et ai ff=unix fileencoding={encoding} :
    #
    # Changing the contents of the dictionary, secrets, or charsets files can change
    # the secrets you generate, thus you should be very careful in changing
    # these files.  These files may change when upgrading the program. Care is
    # taken to assure than your generated passwords do not change as a result of
    # an upgrade, but mistakes can be made.  Hashes for the contents of these
    # files are stored here, and the hashes are checked each time Avendesora is
    # run.  If you get a message indicating that the hashes have changed, it
    # does not mean that there is a problem. Rather it indicates that a change
    # has been made that could conceivably have created a problem and you
    # should do some checking before continuing to use the program. Once you are
    # convinced everything is working as expected, you should update these
    # hashes as needed.
    #
    # When upgrading the program, here is the procedure you should use.
    # 1. Before upgrading you should run 'avendesora changed' and examine all
    #    the differences and make sure they are all expected.
    # 2. Run 'avendesora archive' to create a clean archive.
    # 3. Update Avendesora
    #    This version may emit hash warnings.
    # 4. Run 'avendesora changed'. If no changes are noted, it is okay to update
    #    the appropriate hashes below.
    # 5. If there are changes, do not upgrade and report the situation to
    #    Avendesora's authors.

    charsets_hash = {charsets_hash}
    dict_hash     = {dict_hash}
    mnemonic_hash = {mnemonic_hash}
    secrets_hash  = {secrets_hash}

    # CHANGE THESE VALUES ONLY WHEN REQUESTED BY AVENDESORA AND IF YOU HAVE
    # FOLLOWED THE ABOVE PROCEDURE TO ASSURE YOUR GENERATED SECRETS HAVE NOT
    # CHANGED.

    # Generally the best way to upgrade this file is to simply delete it and
    # then run 'avendesora init', which will regenerate it with the updated
    # values.
''') STEALTH_ACCOUNTS_FILE_INITIAL_CONTENTS = dedent('''\
    # Avendesora Stealth Accounts
    # vim: filetype=python sw=4 sts=4 et ai ff=unix fileencoding={encoding} foldmethod=marker :

    # Description {section}
    # Stealth accounts are accounts where the class name is not taken to be the
    # account name, instead the user is interactively asked for the account
    # name. In this way there is no record that the account actually exists.

    # Imports {section}
    from avendesora import (
        # Account
        StealthAccount,

        # Character sets
        exclude, LOWERCASE, UPPERCASE, LETTERS, DIGITS, ALPHANUMERIC,
        HEXDIGITS, PUNCTUATION, WHITESPACE, PRINTABLE, DISTINGUISHABLE,

        # Secrets
        Password, PasswordRecipe, Passphrase, PIN, Hidden
    )

    # Accounts
    # Alphanumeric passwords {section}
    class Anum4(StealthAccount):
        passcode = Password(length=4, alphabet=DISTINGUISHABLE)

    class Anum6(StealthAccount):
        passcode = Password(length=6, alphabet=DISTINGUISHABLE)

    class Anum8(StealthAccount):
        passcode = Password(length=8, alphabet=DISTINGUISHABLE)

    class Anum10(StealthAccount):
        passcode = Password(length=10, alphabet=DISTINGUISHABLE)

    class Anum12(StealthAccount):
        aliases = 'anum'
        passcode = Password(length=12, alphabet=DISTINGUISHABLE)

    class Anum14(StealthAccount):
        passcode = Password(length=14, alphabet=DISTINGUISHABLE)

    class Anum16(StealthAccount):
        passcode = Password(length=16, alphabet=DISTINGUISHABLE)

    class Anum18(StealthAccount):
        passcode = Password(length=18, alphabet=DISTINGUISHABLE)

    class Anum20(StealthAccount):
        passcode = Password(length=20, alphabet=DISTINGUISHABLE)

    class Anum24(StealthAccount):
        passcode = Password(length=24, alphabet=DISTINGUISHABLE)

    class Anum32(StealthAccount):
        passcode = Password(length=32, alphabet=DISTINGUISHABLE)

    class Anum48(StealthAccount):
        passcode = Password(length=48, alphabet=DISTINGUISHABLE)

    class Anum64(StealthAccount):
        passcode = Password(length=64, alphabet=DISTINGUISHABLE)


    # Ascii passwords {section}
    class Char4(StealthAccount):
        passcode = Password(length=4, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char6(StealthAccount):
        passcode = Password(length=6, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char8(StealthAccount):
        passcode = Password(length=8, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char10(StealthAccount):
        passcode = Password(length=10, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char12(StealthAccount):
        aliases = 'char'
        passcode = Password(length=12, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char14(StealthAccount):
        passcode = Password(length=14, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char16(StealthAccount):
        passcode = Password(length=16, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char18(StealthAccount):
        passcode = Password(length=18, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char20(StealthAccount):
        passcode = Password(length=20, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char24(StealthAccount):
        passcode = Password(length=24, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char32(StealthAccount):
        passcode = Password(length=32, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char48(StealthAccount):
        passcode = Password(length=48, alphabet=ALPHANUMERIC+PUNCTUATION)

    class Char64(StealthAccount):
        alias = 'extreme'
        passcode = Password(length=64, alphabet=ALPHANUMERIC+PUNCTUATION)


    # Hex passwords {section}
    class Hex4(StealthAccount):
        passcode = Password(length=4, alphabet=HEXDIGITS, prefix='0x')

    class Hex8(StealthAccount):
        passcode = Password(length=8, alphabet=HEXDIGITS, prefix='0x')

    class Hex16(StealthAccount):
        passcode = Password(length=16, alphabet=HEXDIGITS, prefix='0x')

    class Hex24(StealthAccount):
        passcode = Password(length=24, alphabet=HEXDIGITS, prefix='0x')

    class Hex32(StealthAccount):
        passcode = Password(length=32, alphabet=HEXDIGITS, prefix='0x')

    class Hex48(StealthAccount):
        passcode = Password(length=48, alphabet=HEXDIGITS, prefix='0x')

    class Hex64(StealthAccount):
        passcode = Password(length=64, alphabet=HEXDIGITS, prefix='0x')

    class Hex96(StealthAccount):
        passcode = Password(length=96, alphabet=HEXDIGITS, prefix='0x')


    # PINs {section}
    class Pin2(StealthAccount):
        passcode = PIN(length=2)

    class Pin3(StealthAccount):
        passcode = PIN(length=3)

    class Pin4(StealthAccount):
        aliases = 'pin'
        passcode = PIN(length=4)

    class Pin5(StealthAccount):
        passcode = PIN(length=5)

    class Pin6(StealthAccount):
        passcode = PIN(length=6)

    class Pin7(StealthAccount):
        passcode = PIN(length=7)

    class Pin8(StealthAccount):
        aliases = 'num'
        passcode = PIN(length=8)

    class Pin10(StealthAccount):
        passcode = PIN(length=10)

    class Pin12(StealthAccount):
        passcode = PIN(length=12)

    class Pin14(StealthAccount):
        passcode = PIN(length=14)

    class Pin16(StealthAccount):
        passcode = PIN(length=16)

    class Pin18(StealthAccount):
        passcode = PIN(length=18)

    class Pin20(StealthAccount):
        passcode = PIN(length=20)

    class Pin24(StealthAccount):
        passcode = PIN(length=24)

    class Pin32(StealthAccount):
        passcode = PIN(length=32)

    class Pin48(StealthAccount):
        passcode = PIN(length=48)

    class Pin64(StealthAccount):
        passcode = PIN(length=64)


    # Passphrases {section}
    class Word1(StealthAccount):
        aliases = 'word'
        passcode = Passphrase(length=1)

    class Word2(StealthAccount):
        aliases = 'pair'
        passcode = Passphrase(length=2)

    class Word3(StealthAccount):
        passcode = Passphrase(length=3)

    class Word4(StealthAccount):
        aliases = 'words xkcd'
        passcode = Passphrase(length=4)

    class Word5(StealthAccount):
        passcode = Passphrase(length=5)

    class Word6(StealthAccount):
        passcode = Passphrase(length=6)

    class Word7(StealthAccount):
        passcode = Passphrase(length=7)

    class Word8(StealthAccount):
        passcode = Passphrase(length=8)

    class Word10(StealthAccount):
        passcode = Passphrase(length=10)

    class Word12(StealthAccount):
        passcode = Passphrase(length=12)

    class Word16(StealthAccount):
        passcode = Passphrase(length=16)

    class Word20(StealthAccount):
        passcode = Passphrase(length=20)

    class Word24(StealthAccount):
        passcode = Passphrase(length=24)

    class Word32(StealthAccount):
        passcode = Passphrase(length=32)


    # Web passwords {section}
    class Web6(StealthAccount):
        passcode = PasswordRecipe('6 u d s')

    class Web8(StealthAccount):
        passcode = PasswordRecipe('8 u d s')

    class Web10(StealthAccount):
        passcode = PasswordRecipe('10 2u 2d 2s')

    class Web12(StealthAccount):
        aliases = 'web'
        passcode = PasswordRecipe('12 2u 2d 2s')

    class Web14(StealthAccount):
        passcode = PasswordRecipe('14 2u 2d 2s')

    class Web16(StealthAccount):
        passcode = PasswordRecipe('16 3u 3d 3s')

    class Web18(StealthAccount):
        passcode = PasswordRecipe('18 3u 3d 3s')

    class Web20(StealthAccount):
        passcode = PasswordRecipe('20 3u 3d 3s')

    class Web24(StealthAccount):
        passcode = PasswordRecipe('24 4u 4d 4s')

    class Web32(StealthAccount):
        passcode = PasswordRecipe('32 5u 5d 5s')

    class Web48(StealthAccount):
        passcode = PasswordRecipe('48 6u 6d 6s')

    class Web64(StealthAccount):
        passcode = PasswordRecipe('64 8u 8d 8s')
''') ACCOUNT_LIST_FILE_CONTENTS = dedent('''\
    # The list of files that contain accounts.
    #
    # The first file given becomes the default file for the add command, meaning
    # that the add command will add a new account to the first file specified
    # unless a different files is specified explicitly.
    #
    # When an account is not in the cache, these files are searched in the order
    # given.  You should list the most likely candidates first.

    accounts_files = {accounts_files}
''') class Script:
    """Script

    Takes a string that contains attributes. Those attributes are expanded
    before being output. For example::

        Script('username: {username}, password: {passcode}')

    In this case, *{username}* and *{passcode}* are replaced by with the value
    of the corresponding account attribute. In addition to the account
    attributes, *{tab}* and *{return}* are replaced by a tab or carriage return
    character.

    Args:
        script (str):  The script.
    """
    SPLITTER = re.compile(r'({[\w. -]+})')

    def __init__(self, script='username: {username}, password: {passcode}'):
        self.script = script
        self.account = None
        self.rendered = None
        self.is_secret = None

    def initialize(self, account, field_name=None, field_key=None):
        self.account = account

    def __str__(self):
        if self.rendered is not None:
            return self.rendered
        self.is_secret = False
        components = []
        for cmd, val in self.components():
            components.append(val)
            if cmd == 'secret':
                self.is_secret = True
        return ''.join(components)

    def components(self, ask=False):
        """Iterates through the script.

        Yields a tuple for each component of a script. The tuple consists of the
        type of the component and the value of the component.  The type may be
        'tab' (a tab character), 'return' (a return character), 'text' (raw
        text), 'value' (the value of a field that is not a secret), 'sleep N' (a
        request to sleep N seconds), 'rate N' (set the autotype to 1 keystroke
        every N milliseconds), and finally a field name (the value of a
        field that is secret)..

        Args:
            ask (bool):
                Determines what happens when a composite field is encountered.
                If ask is true a dialog window is opened that that allows the
                user to select the desired member of the collection. If false, a
                PasswordError is raised.

        Raises:
            :exc:`avendesora.PasswordError`: attribute not found.
        """
        account = self.account
        for term in self.SPLITTER.split(self.script):
            if term and term[0] == '{' and term[-1] == '}':
                # we have found a command
                cmd = term[1:-1].lower()

                if cmd == 'tab':
                    val = '\t'
                elif cmd == 'return':
                    val = '\n'
                elif cmd.startswith('sleep '):
                    val = ''
                elif cmd.startswith('rate '):
                    val = ''
                elif cmd.startswith('remind '):
                    notify(cmd[7:])
                    val = ''
                else:
                    if cmd.startswith('paste '):
                        _, field = cmd.split(maxsplit=1)
                    else:
                        field = cmd
                        cmd = None
                    name, key = account.split_field(field)
                    try:
                        value = account.get_scalar(name, key)
                    except PasswordError as e:
                        if ask and e.is_collection and len(e.collection):
                            # is composite value, ask user which one is desired
                            choices = e.collection
                            if len(choices) == 1:
                                choice = choices.keys()[0]
                            else:
                                choice = show_list_dialog(
                                    'Choose from %s' % name,
                                    sorted(choices.keys())
                                )
                                if choice is None:
                                    raise PasswordError(
                                        'user abort.',
                                        culprit = (
                                            account.get_name(),
                                            account.combine_field(name, key)
                                        )
                                    )
                            key = choices[choice]
                            value = account.get_scalar(name, key)
                        else:
                            raise
                    try:
                        val = dedent(str(value)).strip()
                    except RecursionError:
                        raise PasswordError(
                            'script must not reference itself.', culprit=field
                        )

                    if not cmd:
                        if account.is_secret(name, key):
                            cmd = 'secret'
                        else:
                            cmd = 'value'
            else:
                cmd = 'text'
                val = term
                if not term:
                    continue
            yield cmd, val

    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.script) class Passphrase(Password):
    """Generate passphrase.

    Similar to Password in that it generates an arbitrary passphrase by
    selecting symbols from the given alphabet at random, but in this case
    the default alphabet is a dictionary containing about 10,000 words.

    Args:
        length (int):
            The number of items to draw from the alphabet when creating the
            password.
        dictionary (str, [str], or callable):
            The reservoir of legal symbols to use when creating the
            password. If not given, or if 'default' is given, this is a
            predefined list of 10,000 words. If given as 'bip39' or
            'mnemonic', this is a predefined list of the 2048 bitcoin BIP-39
            seed words.  Any other string is treated as a path to a file
            that would contain the words. A list is taken as is. Finally,
            you can pass a function that returns the list of words, in which
            case the calling of the function is deferred until the words are
            needed, which is helpful if creating the list is slow.
        master (str):
            Overrides the master seed that is used when generating the password.
            Generally, there is one master seed shared by all accounts contained
            in an account file.  This argument overrides that behavior and
            instead explicitly specifies the master seed for this secret.
        version (str):
            An optional seed. Changing this value will change the generated
            password.
        sep (str):
            A string that is placed between each symbol in the generated
            password.
        prefix (str):
            A string added to the front of the generated password.
        suffix (str):
            A string added to the end of the generated password.
        is_secret (bool):
            Should value be hidden from user unless explicitly requested.

    Raises:
        :exc:`avendesora.SecretExhausted`:
            The available entropy has been exhausted.
            This occurs when the requested length is too long.

    Example::

        >>> secret = Passphrase()
        >>> secret.initialize(account, 'dux')
        >>> str(secret)
        'graveyard cockle intone provider'

    """

    def __init__(self,
        length = 4,
        *,
        dictionary = None,
        master = None,
        version = None,
        sep = ' ',
        prefix = '',
        suffix = '',
        is_secret = True,
    ):
        try:
            self.length = int(length)
        except ValueError:
            raise PasswordError(
                'expecting an integer for length.', culprit=error_source()
            )
        if not dictionary or is_str(dictionary):
            self.alphabet = Dictionary(dictionary).get_words
        else:
            self.alphabet = dictionary
        self.master = master
        self.version = version
        self.shift_sort = False
        self.sep = sep
        self.prefix = prefix
        self.suffix = suffix
        self.is_secret = is_secret def to_path(*args):
    try:
        return Path(*args).expanduser()
    except AttributeError:
        return Path(*args) def getmod(path):
    "Return the permission bits for a file or directory"
    return os.stat(str(path)).st_mode & 0o777 def mv(*paths):
    "Move file or directory (supports moves across filesystems)"
    dest = to_path(paths[-1])
    srcs = list(to_paths(paths[:-1]))
    assert len(srcs) >= 1
    if dest.is_dir():
        for src in srcs:
            fulldest = Path(dest, src.name)
            # required because dest cannot exist with shutil.move
            shutil.move(to_str(src), to_str(fulldest))
        return
    else:
        if len(srcs) > 1:
            raise_os_error(errno.ENOTDIR, dest)
    src = srcs[0]
    if dest.is_file():
        if src.is_dir():
            raise_os_error(errno.EISDIR, src)
        else:
            # overwrite destination
            shutil.move(to_str(src), to_str(dest))
    else:
        # destination does not exist
        if dest.exists():
            raise_os_error(errno.EEXIST, dest)
        shutil.move(to_str(src), to_str(dest)) def rm(*paths):
    "Remove files or directories (equivalent to rm -rf)"
    for path in to_paths(paths):
        try:
            if path.is_dir() and not path.is_symlink():
                shutil.rmtree(to_str(path))
            else:
                path.unlink()
        # suitable for python3 only
        # except FileNotFoundError:
        #     pass
        except (IOError, OSError) as err:
            # don't complain if the file never existed
            if err.errno != errno.ENOENT:
                raise class Title(object):
    def __init__(self, url=None, override=None):
        if url:
            self.data = self._process_url(url)
            return
        if override:
            title = override
        else:
            xdotool = get_setting('xdotool_executable')
            if not xdotool:
                raise PasswordError(
                    "must set xdotool_executable'.",
                    culprit=setting_path('xdotool_executable')
                )
            cmd = [xdotool, 'getactivewindow', 'getwindowname']
            try:
                output = Run(cmd, 'sOEW')
            except Error as e:
                e.reraise(culprit=xdotool)
            title = output.stdout.strip()
        log('Focused window title:\n    %s' % title)
        data = {'rawtitle': title}
        for sub in sorted(Title.__subclasses__(), key=lambda c: c.PRIORITY):
            matched = sub._process(title, data)
            log('%s: %s.' % (sub.__name__, 'matched' if matched else 'no match'))
            if matched:
                break

        # log the components of the title
        log('Recognized title components;')
        for k, v in data.items():
            log('    %s: %s' % (k, v))

        self.data = data

    def get_data(self):
        return self.data

    @classmethod
    def _process(cls, title, data):
        match = cls.PATTERN.match(title)
        if match:
            found = match.groupdict()
            if 'url' in found:
                components = cls._process_url(found['url'])
                if components:
                    data.update(found)
                    data.update(components)
                    return True

    @staticmethod
    def _process_url(url):
        components = urlparse(url)
        if components.netloc:
            return dict(
                protocol = components.scheme,
                host = components.netloc,
                path = components.path,
                fragment = components.fragment,
            ) def generate_random_string(length=64):
    # Generate a random long string to act as the default password

    from string import ascii_letters, digits, punctuation
    import random
    rand = random.SystemRandom()

    # Create alphabet from letters, digits, and punctuation, but
    # replace double quote with a space so password can be safely
    # represented as a double-quoted string.
    alphabet = (ascii_letters + digits + punctuation).replace('"', ' ')

    password = ''
    for i in range(length):
        password += rand.choice(alphabet)
    return password
from .account import Account
from .config import read_config, get_setting
from .dialog import show_list_dialog
from .dictionary import Dictionary
from .error import PasswordError
from .files import AccountFiles
from .gpg import GnuPG, PythonFile, GPG_EXTENSIONS
from .obscure import Hidden
from .preferences import (
    CONFIG_DEFAULTS, NONCONFIG_SETTINGS, ACCOUNTS_FILE_INITIAL_CONTENTS,
    CONFIG_FILE_INITIAL_CONTENTS, CONFIG_DOC_FILE_INITIAL_CONTENTS,
    USER_KEY_FILE_INITIAL_CONTENTS, HASH_FILE_INITIAL_CONTENTS,
    STEALTH_ACCOUNTS_FILE_INITIAL_CONTENTS, ACCOUNT_LIST_FILE_CONTENTS,
)
from .script import Script
from .secrets import Passphrase
from .shlib import to_path, getmod, mv, rm
from .title import Title
from .utilities import generate_random_string
from inform import codicil, conjoin, log, os_error, render, warn, is_str
from textwrap import dedent, wrap
from pathlib import Path
        from pkg_resources import resource_filename
                from arrow import utcnow
import hashlib
        init (bool): Create user's directory.

        gpg_ids (list of strings):
            List of GPG identities to use when creating user's directory.

        check_integrity (bool):
            If true will validate that certain critical components in Avendesora
            have not be tampered with.  Checking the integrity can take up to a
            second, so recommend you pass False on interactive commands that
            benefit from low startup overhead and True on the more expensive
            commands to assure integrity is occasionally checked.

        warnings (bool):
            Suppress warnings from accounts. Useful when processing many
            accounts.  Does not affect warnings from the integrity check.

    Raises:
        :exc:`avendesora.PasswordError`:
            Indicates an issue opening the user's accounts.
    """

    # Constructor {{{2
    def __init__(
            self, init=False, gpg_ids=None, check_integrity=False, warnings=True
    ):
        # initialize avendesora (these should already be done if called from
        # main, but it is safe to call them again)
        read_config()
        GnuPG.initialize()

        # create the avendesora data directory
        if init:
            self._initialize(gpg_ids, init)
            return

        # check the integrity of avendesora
        if check_integrity:
            self._validate_components()

        # prepare to read accounts files
        self.account_files = AccountFiles(warnings)

    # _initialize() (private){{{2
    def _initialize(self, gpg_ids, filename):
        # If filename is True, this is being called as part of the Avendesora
        # 'initialize' command, in which case all missing files should be created.
        # If filename is a string, this is being called as part of the
        # Avendesora 'new' command, in which case a single new account file
        # should be created.
        def split(s, l=72):
            # Break long string into a series of adjacent shorter strings
            if len(s) < l:
                return '"%s"' % s
            chunks = ['    "%s"' % s[i:i+l] for i in range(0, len(s), l)]
            return '\n' + '\n'.join(chunks) + '\n'

        # create settings directory if it does not exist
        settings_dir = to_path(get_setting('settings_dir'))
        if not settings_dir.exists():
            settings_dir.mkdir(mode=0o700, parents=True)
            settings_dir.chmod(0o700)

        # Create dictionary of available substitutions for CONTENTS strings
        fields = {}
        for key in CONFIG_DEFAULTS:
            value = get_setting(key, expand=False)
            value = render(str(value) if isinstance(value, Path) else value)
            fields.update({key: value})
        for key in NONCONFIG_SETTINGS:
            value = get_setting(key, expand=False)
            value = render(str(value) if isinstance(value, Path) else value)
            fields.update({key: value})
        fields['encoding'] = get_setting('encoding')
            # get this one again, this time without the quotes
        gpg_ids = gpg_ids if gpg_ids else get_setting('gpg_ids', [])
        fields.update({
            'section': '{''{''{''1',
            'master_seed': split(Hidden.conceal(generate_random_string(72))),
            'user_key': split(Hidden.conceal(generate_random_string(72))),
            'gpg_ids': repr(' '.join(gpg_ids)),
        })

        # delete the config_doc_file so we always get an updated version.
        if (get_setting('config_doc_file')):
            try:
                rm(get_setting('config_doc_file'))
            except OSError as e:
                warn(os_error(e))

        # create the initial versions of the files in the settings directory
        if filename is True:
            path = to_path(get_setting('account_list_file'))
            if path.suffix in GPG_EXTENSIONS:
                raise PasswordError('encryption is not supported.', culprit=path)

            # Assure that the default initial set of files is present
            for path, contents in [
                (get_setting('config_file'), CONFIG_FILE_INITIAL_CONTENTS),
                (get_setting('config_doc_file'), CONFIG_DOC_FILE_INITIAL_CONTENTS),
                (get_setting('hashes_file'), HASH_FILE_INITIAL_CONTENTS),
                (get_setting('user_key_file'), USER_KEY_FILE_INITIAL_CONTENTS),
                (get_setting('default_accounts_file'), ACCOUNTS_FILE_INITIAL_CONTENTS),
                (get_setting('default_stealth_accounts_file'), STEALTH_ACCOUNTS_FILE_INITIAL_CONTENTS),
                (get_setting('account_list_file'), ACCOUNT_LIST_FILE_CONTENTS),
            ]:
                if path:
                    log('Creating initial version.', culprit=path)
                    f = PythonFile(path)
                    f.create(contents.format(**fields), gpg_ids)
                        # create will not overwrite an existing file, instead it
                        # reads the file.
        else:
            # Create a new accounts file
            fields['accounts_files'] = render(
                get_setting('accounts_files', []) + [filename]
            )
                # do not sort, the first file is treated special
            path = to_path(get_setting('settings_dir'), filename)
            if path.exists():
                raise PasswordError('exists.', culprit=path)

            if path.suffix in GPG_EXTENSIONS and not gpg_ids: