#!/usr/bin/perl
# gpgpwd
# Copyright (C) Eskild Hustvedt 2012, 2013, 2014, 2015, 2016
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;
use 5.010;
use Getopt::Long;
use JSON qw(encode_json decode_json);
use Try::Tiny;
use IPC::Open2 qw(open2);
use IPC::Open3 qw(open3);
use MIME::Base64 qw(encode_base64 decode_base64);
use IO::Select;
use File::Path qw(mkpath);
use File::Copy qw(move copy);
use File::Basename qw(basename dirname);
use File::Temp;
use Term::ReadLine;
use Cwd qw(getcwd realpath);
use constant {
    true => 1,
    false => undef,

    V_INFO => 1,
    V_LOG => 2,
    V_DEBUG => 3,
    V_TRACE => 4,
};

my $VERSION          = '0.7.2';
my @gpg              = qw(gpg2 --gnupg --default-recipient-self --no-verbose --quiet --personal-compress-preferences uncompressed);
my $gpgpwdDir;
my $storagePath;
my $dataVersion      = 4;
my $enableGit        = false;
my $verbosity        = 0;
my $allMatches       = false;
my $autoGPGAgent;
my @clipboardTargets;
my $pDieUsepExit     = 1;
my $config           = {};
my $runtimeCache     = {};

# = = = = = = = = =
# Message helpers
# = = = = = = = = =

# Purpose: print() wrapper that obeys $verbosity
# Usage: printv(LEVEL, ...);
# LEVEL is one of the V_* constants, ... are the normal print() parameters
sub printv
{
    my $level = shift;
    if ($verbosity >= $level)
    {
        my @prefixes;
        $prefixes[V_INFO]  = 'info';
        $prefixes[V_LOG]   = 'log';
        $prefixes[V_DEBUG] = 'debug';
        $prefixes[V_TRACE] = 'trace';
        printf('[gpgpwd %-5s] ',$prefixes[$level]);
        if(ref($_[0]))
        {
            print encode_json(\@_)."\n";
        }
        else
        {
            print @_;
        }
    }
    return 1;
}

# Purpose: Outputs a status message during initialization
# Usage: statusOut(message)
#
# This is silent if verbosity is 0
sub statusOut
{
    # Ignore status messages if verbosity != 0
    if ($verbosity == 0)
    {
        my $message = shift;
        state $prevLength = 0;
        # Clear the previous message
        print "\r";
        for(my $i = $prevLength; $i > 0;$i--)
        {
            print ' ';
        }
        # Output the new message
        print "\r$message";
        $prevLength = length($message);
    }
    return 1;
}

# Purpose: system() wrapper that outputs the command if $verbosity >= V_LOG
# Usage: Same as system()
sub psystem
{
    if ($verbosity >= V_LOG)
    {
        printv(V_LOG,'Running: '.join(' ',@_)."\n");
    }
    return system(@_);
}

# Purpose: system() wrapper that discards all output from the child
# Usage: Same as system()
# This will also output information of the command if $verbosity >= V_LOG like
# psystem does.
sub silentSystem
{
    if ($verbosity >= V_DEBUG)
    {
        # Disable silencing in debugging mode
        return psystem(@_);
    }

    if ($verbosity >= V_LOG)
    {
        printv(V_LOG,'Running (silenced): '.join(' ',@_)."\n");
    }

    # Store references to our current STDERR/STDOUT
    open(my $storedErr,'>&',\*STDERR);
    open(my $storedOut,'>&',\*STDOUT);
    # Redirect both STDERR and STDOUT to /dev/null
    open(STDERR,'>','/dev/null');
    open(STDOUT,'>','/dev/null');

    # Run the command
    my $return = system(@_);

    # Restore STDERR/STDOUT
    open(STDERR,'>&',$storedErr);
    open(STDOUT,'>&',$storedOut);

    return $return;
}

# Purpose: Wrapper around die() that uses pExit
# Usage: Same as die()
sub pDie
{
    print "\r";
    if (!$pDieUsepExit)
    {
        die(@_);
    }
    warn(@_);
    pExit(254);
}

# Purpose: Output a message to stderr or die, restoring STDERR first
# Usage: reOut($stderr,die?,message);
sub reOut
{
    my $stderr = shift;
    my $die = shift;
    my $message = shift;

    open(my $storedErr,'>&',\*STDERR);
    open(STDERR,'>&',$stderr);
    if ($die)
    {
        pDie($message);
    }
    else
    {
        warn($message);
    }
    open(STDERR,'>&',$storedErr);
    return 1;
}

# = = = = = = =
# Core helpers
# = = = = = = =

# Purpose: Wrapper around exit that restores backups if needed
# Usage: Same as exit()
sub pExit
{
    my $ret = shift;
    # Gently kill our started gpg agent if needed
    if (defined($autoGPGAgent) && $autoGPGAgent > -1 && defined($ENV{GPG_AGENT_INFO}))
    {
        my @GPGInfo = split(/:/,$ENV{GPG_AGENT_INFO});
        if(defined $GPGInfo[1] && $GPGInfo[1] != -1)
        {
            kill('SIGINT',$GPGInfo[1]);
            printv(V_DEBUG,'Killed our gpg agent ('.$GPGInfo[1].')'."\n");
        }
    }
    # Restore backups if present
    if (defined $storagePath && -e $storagePath.'~' && ! -e $storagePath)
    {
        print 'Note: while exiting '.$storagePath.'~ existed, but '.$storagePath.' did not.'."\n";
        print 'Restored backup copy to '.$storagePath."\n";
        move($storagePath.'~',$storagePath);
    }
    exit($ret);
}

# Purpose: Check for a file in path
# Usage: InPath(FILE)
sub InPath
{
    foreach (split /:/, $ENV{PATH}) { if (-x "$_/@_" and ! -d "$_/@_" ) {    return "$_/@_"; } } return false;
}

# Purpose: Retrieve a simplistic "file ID" string for a file
# Usage: id = getFileID(FILE);
# The ID string is a very simple string combining the file size and
# file change time. If an older ID for the same file does not match
# the current ID then the file has been changed.
sub getFileID
{
    my $file = shift;
    if (! -e $file || ! -r $file)
    {
        pDie('Unable to read '.$file."\n");
    }
    my $ID = -s $file;
    $ID .= '|';
    $ID .= (stat($file))[9];
    return $ID;
}

# = = = = = = = = = = = = = = = = = = =
# Encryption and general GPG functions
# = = = = = = = = = = = = = = = = = = =

# Purpose: Check for gpg-agent and start one if needed
# Usage: autoInitGPGAgent()
sub autoInitGPGAgent
{
    if (defined($autoGPGAgent))
    {
        return 1;
    }
    elsif(!gpgAgentRunning())
    {
        if(InPath('gpg-agent'))
        {
            printv(V_DEBUG,'Autostarting gpg-agent'."\n");
            # Start the agent
            $autoGPGAgent = open3(my $in, my $out, my $err, qw(gpg-agent --daemon));
            # Read the first line containing the GPG_AGENT_INFO variable
            my $info = <$out>;
            # Parse the read data if possible
            if (defined $info)
            {
                # Parse out the variable contents
                if ($info =~ s/GPG_AGENT_INFO=(\S+); export GPG_AGENT_INFO/$1/)
                {
                    chomp($info);
                    # Set the GPG_AGENT_INFO environment variable for gpg to use
                    $ENV{GPG_AGENT_INFO} = $info;
                    printv(V_DEBUG,'gpg-agent['.$autoGPGAgent.'] started and listening at '.$info."\n");
                }
                else
                {
                    $info = undef;
                    printv(V_DEBUG,'gpg-agent didn\'t output status info'."\n");
                }
            }
            # Disable tty
            printv(V_DEBUG,'Enabling agent requirement (--no-tty)'."\n");
            push(@gpg,'--no-tty');
            # Explicitly enable the agent for gpg1
            if ($gpg[0] ne 'gpg2')
            {
                push(@gpg,'--use-agent');
            }
            my $home = $ENV{GNUPGHOME} // $ENV{HOME}.'/.gnupg';
            # Enable agent emulation for gpgv1 with agent-v2
            if(!defined($ENV{GPG_AGENT_INFO}) && $gpg[0] ne 'gpg2' && defined $home && -e $home.'/S.gpg-agent')
            {
                printv(V_DEBUG,'Emulating gpg-agent v1 support with gpg-agent v2'."\n");
                $ENV{GPG_AGENT_INFO} = $home.'/S.gpg-agent:-1:1';
            }
            if (!gpgAgentRunning())
            {
                if(defined $info)
                {
                    waitpid($autoGPGAgent,0);
                    my $ret = $? >> 8;
                    pDie('gpgpwd failed to start a gpg-agent, it exited with the exit status '.$ret."\n");
                }
                else
                {
                    pDie('gpgpwd failed to start a gpg-agent, did not return any status information'."\n");
                }
            }
        }
        else
        {
            # If we have no gpg-agent then the user will get tired of typing
            # their password fast, so output a message on how to avoid that.
            print "NOTICE: No gpg-agent available. Install gpg-agent to avoid unneccesary\npassword prompts\n";
            $autoGPGAgent = -1;
        }
    }
    elsif (!defined($autoGPGAgent))
    {
        printv(V_DEBUG,'Not autostarting gpg-agent: already appears to be running'."\n");
        $autoGPGAgent = -1;
    }
    return 1;
}

# Purpose: Check if a gpg agent is running
# Usage: bool = gpgAgentRunning()
sub gpgAgentRunning
{
    if (defined($ENV{GPG_AGENT_INFO}) && length($ENV{GPG_AGENT_INFO}))
    {
        # GPG_AGENT_INFO is a :-separated list of values. The first value is a path
        # to the gpg-agent socket. The second is the PID of the agent.
        my @GPGInfo   = split(/:/,$ENV{GPG_AGENT_INFO});
        # If the second parameter is -1, we're emulating gpg-agent v1 in v2, assume
        # that it is alive.
        if (-e $GPGInfo[0] && $GPGInfo[1] == -1)
        {
            return 1;
        }
        # Make sure the gpg-agent socket exists, and that its process is alive
        if (-e $GPGInfo[0] && kill(0,$GPGInfo[1]))
        {
            return 1;
        }
    }
    # If we're using gpg2 and the gnupg version is 2.1 or later then gpg will
    # handle starting the agent automatically on its own, so any test gpgpwd
    # makes for "is the agent running" should result in "yes"
    if ($gpg[0] eq 'gpg2')
    {
        # Cache the state so we only have to fetch the version once
        state $gpg2autoAgent;
        if (defined $gpg2autoAgent)
        {
            return $gpg2autoAgent;
        }

        # Retrieve the gpg2 version number
        my $gpg2VerNoRaw = getVersionFrom(qw(gpg2 --version));
        my @gpgVers = (0,0);
        if (defined $gpg2VerNoRaw)
        {
            @gpgVers = split(/\./,$gpg2VerNoRaw);
        }
        else
        {
            printv(V_DEBUG,'Failed to retrieve version number from gpg2, using 0.0 as version number'."\n");
        }
        # If it is version 2.1 or later, assume the agent is running
        if ($gpgVers[0] >= 2 && $gpgVers[1] >= 1)
        {
            printv(V_DEBUG,'Acting as if the gpg-agent is running due to gpg2 >= 2.1'."\n");
            $gpg2autoAgent = 1;
            return 1;
        }
        # If we reach this far then the gpg version is older than 2.1, and thus
        # we should cache it as "false" to avoid having to retrieve the version
        # number again on the next call
        $gpg2autoAgent = 0;
    }
    return 0;
}

# Purpose: Check if there are any gpg keys available
# Usage: verifyGPGKeys()
sub verifyGPGKeys
{
    # Used to make sure this is not run more than once
    state $verified = 0;
    if ($verified)
    {
        return;
    }
    $verified = 1;

    my $keyList = gpgIO(undef,[ qw(--list-keys) ], { captureSTDERR => true, requiresUnlocked => false });
    $keyList //= '';
    $keyList =~ s/^gpg:.*//g;

    if ($keyList !~ /\n/)
    {
        pDie("Error: You appear to have no GnuPG-keys, which gpgpwd uses for its\nencryption. Before you can use gpgpwd you need to generate a GnuPG-key.\n\nTo quickly generate a key use: ".$gpg[0]." --gen-key\nIf you do not know what kind of key to generate, a good key to start with is\nan RSA key of the largest size (4096).\nFor more information see: http://www.gnupg.org/\n");
    }
}

# Purpose: Write some data to gpg and return the output from gpg
# Usage: $output = gpgIO(data,[ gpgCommands ], { OPTS? })
#   or : ($output,$gpgExitStatus,$statusInfo) = gpgIO(data,[ gpgCommands ], { OPTS? })
# data is the data to be written to gpg
# gpgCommands is an arrayref of parameters for gpg
# { OPTS? } is a hash (or undef) with zero or more of the following:
#   requiresUnlocked => boolean. Set it to true if the provided gpgCommands require
#       the key to be unlocked. Default=false.
#   captureSTDERR is a boolean. Set it to true to capture both stdout and stderr.
#       Default=false.
#   useStatus is a boolean. Set it to true to enable GPG's --status-file mode
#       and return data from that as the third returned variable. This REQUIRES
#       that you accept multiple return values. Note that if status-file is
#       unavailable (gpg1) the returned data will be undef. Default=false.
sub gpgIO
{
    my $data             = shift;
    my $commandList      = shift;
    my $opts             = shift;
    if (!defined $opts)
    {
        $opts = {};
    }
    my @commands         = @{$commandList};

    my $output;
    my $stderr;
    my $gpgTMP;
    # Used to store the return value when using open() directly
    my $openRet;

    if (!$opts->{captureSTDERR})
    {
        open($stderr, '>&',\*STDERR);
        # This is used to silence useless warnings from gnupg when using the
        # gnome keyring daemon. It's not safe to do unless we are under gnome
        # and have a DISPLAY. If we don't then we risk killing password
        # prompts.
        if(defined($ENV{DISPLAY}) && defined($ENV{XDG_CURRENT_DESKTOP}) && $ENV{XDG_CURRENT_DESKTOP} eq 'GNOME' && $verbosity <= 0)
        {
            open(STDERR,'>','/dev/null');
        }
    }

    # Autostart a gpg-agent for us if needed
    if ($opts->{requiresUnlocked})
    {
        autoInitGPGAgent();
    }

    # Declare I/O variables
    my($in,$out,$err,$pid,$outputReader);

    # Generate the complete gpg commandline
    my @gpgCommand = @gpg;
    if ($runtimeCache->{key})
    {
        # Since we already have a key that was used for this database, we explicitly
        # tell gpg to reuse it to make sure we don't re-encrypt with a different key
        push(@gpgCommand,'--default-key',$runtimeCache->{key},'--local-user',$runtimeCache->{key});
    }
    push(@gpgCommand,@commands);
    # Enable status
    if ($opts->{useStatus})
    {
        if (!wantarray())
        {
            die('gpgIO() called with useStatus in scalar context');
        }
        $gpgTMP = File::Temp->new(
            TEMPLATE => 'gpg-gpgpwd-communication-XXXXXXXXXX',
            SUFFIX => '.tmp',
            TMPDIR => 1,
            UNLINK => true
        );
        my $gpg = shift(@gpgCommand);
        unshift(@gpgCommand,$gpg,'--status-file',$gpgTMP);
    }

    # If we're not requiring an agent then gpg will output some messages. To
    # differenciate it from "our" output, encase it in a text block.
    if (!gpgAgentRunning() && $opts->{requiresUnlocked})
    {
        print '-- gpg --'."\n";
    }
    # Use open3 to capture stdout+stderr
    if ($opts->{captureSTDERR})
    {
        printv(V_LOG,'Talking to gpg with open3: '.join(' ',@gpgCommand)."\n");
        $pid = open3($in,$out,$err,@gpgCommand) or reOut($stderr,true,'Failed to open3() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT and STDERR filehandles
        $outputReader = IO::Select->new($out,$err);
    }
    # Use open2 to capture stdout
    elsif(defined($data))
    {
        printv(V_LOG,'Talking to gpg with open2: '.join(' ',@gpgCommand)."\n");
        $pid = open2($out,$in,@gpgCommand) or reOut($stderr,true,'Failed to open2() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT filehandle
        $outputReader = IO::Select->new($out);
    }
    else
    {
        printv(V_LOG,'Talking to gpg with open: '.join(' ',@gpgCommand)."\n");
        $pid = open($out,'-|',@gpgCommand) or pDie('Failed to open() up communication with gpg: '.$!."\n");
        # Create an IO::Select handle that we use to query for pending data
        # on the STDOUT filehandle
        $outputReader = IO::Select->new($out);
    }
    # If we have data and it is an arrayref, print it line-by-line
    if(defined($data) && ref($data) eq 'ARRAY')
    {
        foreach my $l (@{$data})
        {
            print {$in} $l."\n" or reOut($stderr,true,'Failed to write data to gpg for output: '.$!."\n");
            # Check if there is pending output data that we need to read
            while(my @handles = $outputReader->can_read(0))
            {
                foreach my $handle (@handles)
                {
                    $output .= <$handle>;
                }
            }
        }
    }
    # Dump the entire data string if we have one
    elsif(defined($data))
    {
        print {$in} $data or reOut($stderr,true,'Failed to write data to gpg for output: '.$!."\n");
    }
    # Close the input filehandle
    if(defined($in))
    {
        close($in) or reOut($stderr,false,'Failed to close communication with gpg: '.$!."\n");
        # Store the return value if needed
        if (!defined $openRet)
        {
            $openRet = $?;
        }
    }
    # Read all pending output
    while(my @handles = $outputReader->can_read(120))
    {
        foreach my $handle (@handles)
        {
            my $read = false;
            while(my $input = <$handle>)
            {
                $read = true;
                $output .= $input;
            }
            if (!$read)
            {
                $outputReader->remove($handle);
                last;
            }
        }
    }

    if (!$opts->{captureSTDERR})
    {
        open(STDERR,'>&',$stderr);
    }

    # Close the output handle
    close($out) or warn('Failed to close communication with gpg: '.$!."\n");
    # Store the return value if needed
    if (!defined $openRet)
    {
        $openRet = $?;
    }
    # Close the stderr handle if needed
    if(defined($err))
    {
        close($err) or warn('Failed to close communication with gpg: '.$!."\n");
    }
    # If we're not requiring an agent then gpg will output some messages. To
    # differenciate it from "our" output, encase it in a text block.
    if (!gpgAgentRunning() && $opts->{requiresUnlocked})
    {
        print '-- --- --'."\n";
    }
    # Wait for children (ie. gpg) to exit, to avoid defunct processes
    waitpid($pid,0);

    # Check the return value from gpg
    my $ret = $?;
    # If the wait returned -1 then the return value may have been given to us when we
    # closed a filehandle, so use the return value stored in $openRet instead.
    if ($ret == -1)
    {
        $ret = $openRet;
    }
    if ($ret != 0)
    {
        $ret = $ret >> 8;
        # Check if the user has no gpg keys, which may be the cause of the error.
        # If the user has no keys gpgpwd will die with a message instructing how
        # to get one.
        verifyGPGKeys();
        # If our caller has not explicitly requested that we return multiple
        # values (ie. that we also return the gpg return value) then we error
        # out here to ensure that no corruption occurs. If the caller has
        # requested the return value we return to our caller and allow it to
        # handle the error directly.
        if(!wantarray())
        {
            pDie($gpgCommand[0]." exited with non-zero return value: $ret\n".'(gpgpwd was executing: '.join(' ',@gpgCommand).')'."\n");
        }
        else
        {
            printv(V_LOG,$gpgCommand[0]." exited with non-zero return value: $ret\n");
        }
    }

    # Read in the status file
    my $gpgStatus;
    if ($gpgTMP)
    {
        my @statusLines;
        my %seen;
        while(<$gpgTMP>)
        {
            next if !s/^\[GNUPG:\]\s+//;
            chomp;
            my $type = $_;
            my $value = $_;
            $type =~ s/^(\S+)\s+.*/$1/;
            $value =~ s/^\S+\s*//;
            push(@statusLines,{ $type => $value });
            if (!defined $seen{$type})
            {
                $seen{$type} = [];
            }
            push(@{$seen{$type}},$value);
        }
        $gpgStatus = {
            seen => \%seen,
            lines => \@statusLines,
        };
        printv(V_TRACE,$gpgStatus);
    }

    # Return whatever output we got from gpg. Optionally with the
    # return value if we are called in list context.
    if(wantarray())
    {
        return ($output,$ret,$gpgStatus);
    }
    else
    {
        return $output;
    }
}

# Purpose: Decrypt a Base64-encoded gpg-encrypted string and return it
# Usage: gpgDecryptString(STRING,Key);
# STRING is the string to encrypt
# Key is the name of the password being decrypted, used in error messages
sub gpgDecryptString
{
    my $string = shift;
    my $decryptingKey = shift;

    if (!defined $string)
    {
        pDie('Fatal error: gpgDecryptString() attempted to decrypt an empty (undef)'."\n".
            'value. This indicates either a bug in gpgpwd (most likely) or a partial or'."\n".
            'total corruption of the password database.'."\n".
            'Contact the gpgpwd developers at http://random.zerodogg.org/gpgpwd/bugs/'."\n"
        );
    }
    if(ref($string))
    {
        printv(V_DEBUG,'gpgDecryptString() receieved a reference for '.ref($string)."\n");
        if(ref($string) eq 'HASH')
        {
            printv(V_DEBUG,'It contains the following keys: '.join(' ',keys %{$string})."\n");
        }
        pDie('Fatal error: gpgDecryptString() attempted to decrypt a reference'."\n".
            'instead of a string when retrieving the password for '.$decryptingKey.".\n".
            'This indicates a partial or total corruption of the password database.'."\n".
            'Manual recovery is likely possible, either by reverting to an earlier'."\n".
            'revision in git (if git is in use), or by manually decrypting the data.'."\n".
            'Contact the gpgpwd developers at http://random.zerodogg.org/gpgpwd/support/'."\n".
            'if you require assistance.'."\n"
        );
    }

    # Decode the base64 string first
    try
    {
        $string = decode_base64($string);
    }
    catch
    {
        pDie('Failed to decode BASE64: '.$_."\n");
    };

    # Decrypt the string
    my ($output,$gpgRetVal) = gpgIO($string,[ qw(--decrypt) ],{ requiresUnlocked => true});
    if ($gpgRetVal != 0)
    {
        statusOut('');
        pDie('Fatal error: Failed to decrypt password for "'.$decryptingKey.'".'."\n".
            'gpg exited with non-zero return value ('.$gpgRetVal.')'."\n".
            'This indicates either a partial corruption of the password database, or'."\n".
            'that your database is using multiple encryption keys, and one of them is'."\n".
            'missing from this system.'."\n");
    }
    if (!defined $output || !length $output)
    {
        statusOut('');
        pDie('Fatal error: Decryption of password for "'.$decryptingKey.'" yielded an empty string.'."\n".'This is likely a bug in gpgpwd.'."\n");
    }

    # Return the decrypted data
    return $output;
}

# Purpose: Encrypt a string with gpg and return it
# Usage: gpgEncryptString(STRING);
# STRING is the string to encrypt
sub gpgEncryptString
{
    my $string = shift;
    my $base64 = shift;

    # Encrypt the data
    my ($output,$retV,$status) = gpgIO($string,[ qw(--encrypt) ], { useStatus => true });
    if ($retV != 0)
    {
        pDie('GPG returned nonzero while encrypting'."\n");
    }
    # Return base64 encoded data
    return encode_base64($output,'');
}

# = = = = = = = = = = =
# Data format handling
# = = = = = = = = = = =

# Purpose: Verify metadata signature
# Usage: verifyMetadataSignature(PATH,Data)
sub verifyMetadataSignature
{
    my $path = shift;
    my $data = shift;
    my $fileStatus = shift;

    printv(V_LOG,'Verifying metadata signature'."\n");

    # Reset LC_ALL to C
    my $LC_ALL = $ENV{LC_ALL};
    $ENV{LC_ALL} = 'C';
    my $pubkey;

    if(defined $fileStatus && $fileStatus->{seen}->{ENC_TO})
    {
        $pubkey = $fileStatus->{seen}->{ENC_TO}->[0];
        $pubkey =~ s/^(\S+)\s+.*/$1/;
        printv(V_DEBUG,'Used pubkey from gpg status file: '.$pubkey."\n");
    }
    else
    {
        my $pkRet;

        # Retrieve which pubkey the database is encrypted with
        ($pubkey,$pkRet) = gpgIO(undef,[ qw(--verbose --decrypt --list-only), $path ], { captureSTDERR => true, requiresUnlocked => true });
        # If we didn't get a list then something went wrong
        if (!defined($pubkey))
        {
            pDie('Signature validation failed: unable to retrieve encryption pubkey from database file'."\n");
        }
        if ($pkRet != 0)
        {
            pDie('Signature validation failed: gpg returned '.$pkRet.' when listing keys'."\n");
        }
        chomp($pubkey);
        # Parse out the public key
        $pubkey =~ s/gpg: public key is\s+//;
        # If there are spaces in it then the regex above appears to have failed and we're
        # left with an unusable string
        if ($pubkey =~ /\s/)
        {
            printv(V_DEBUG,'Public key parsed to be "'.$pubkey.'"'."\n");
            pDie('Signature validation failed: unable to parse out the pubkey from gpg output'."\n");
        }
    }

    # Verify the signature
    my ($signature,$signRet,$signStatus) = gpgIO($data,[ qw(--verify) ], { captureSTDERR => true, useStatus => true });
    my $sigKey;

    if(defined($signStatus))
    {
        if (! $signStatus->{seen}->{GOODSIG})
        {
            pDie('Signature validation failed: invalid signature'."\n");
        }
        $sigKey = $signStatus->{seen}->{GOODSIG}->[0];
        $sigKey =~ s/^(\S+)\s+.*/$1/;
        printv(V_DEBUG,'Used signature key from status file: '.$sigKey."\n");
    }
    else
    {
        # If no data was returned then something went wrong with gpg
        if (!defined($signature))
        {
            pDie('Signature validation failed: gpg did not return any response about signature validity'."\n");
        }

        # If it does not contain this text, then the validation failed
        if ($signature !~ /^gpg: Good signature from/m)
        {
            pDie('Signature validation failed: invalid signature'."\n");
        }

        # Parse out the signature key
        $sigKey = $signature;
        $sigKey =~ s/.*(gpg: Signature made .*? using \S+ key ID |gpg:\s+using\s+\S+\s+key\s+)(\S+).*/$2/s;
        # If there are still spaces left then we failed to parse the key
        if ($sigKey =~ /\s/)
        {
            printv(V_DEBUG,'Signature key parsed to be "'.$sigKey.'"'."\n");
            pDie('Signature validation failed: unable to parse out the sigkey from gpg output'."\n");
        }
    }

    # If no data was returned then something went wrong with gpg
    if ($signRet > 0)
    {
        pDie('Signature validation failed: gpg returned non-zero when checking the signature ('.$signRet.')'."\n");
    }

    printv(V_DEBUG,'gpg signature validation step succeeded'."\n");

    printv(V_DEBUG,'signing key is '.$sigKey.', pubkey for encryption is '.$pubkey."\n");

    $runtimeCache->{sigKey} = $sigKey;
    $runtimeCache->{key} = $pubkey;

    # Retrieve a list of all signatures matching the pubkey
    my ($keyList,$keyRet) = gpgIO(undef, [ qw(--with-colons --list-sigs), $pubkey ]);
    # If no data was returned then something went wrong with gpg
    if(!defined($keyList))
    {
        pDie('Signature validation failed: Unable to retrieve signature list from gpg'."\n");
    }
    # If no data was returned then something went wrong with gpg
    if ($keyRet > 0)
    {
        pDie('Signature validation failed: gpg returned non-zero when validating the signature key ('.$keyRet.')'."\n");
    }
    my $valid = false;
    my $sigKeyLen = length($sigKey);
    # Iterate through all lines returned from gpg
    foreach my $line(split(/\n/,$keyList))
    {
        # The content is :-separated, split it into an array
        my @content = split(/:/,$line);
        # We only care about 'sig' lines
        if ($content[0] eq 'sig')
        {
            # If the line contains the key, and the key is at the end of the line
            # (which is determined through the length of the line minus the length of the
            # key, index() will return at which point $sigKeyLen starts) then it is the
            # same key, and the validation has succeeded.
            if (index($content[4],$sigKey) == ( length($content[4]) - $sigKeyLen ) )
            {
                $valid = true;
                last;
            }
        }
    }

    # Error out if we have no valid signature
    if (!$valid)
    {
        pDie('Signature validation failed: Signature does not match encryption key'."\n");
    }

    # Switch LC_ALL back to the user value
    $ENV{LC_ALL} = $LC_ALL;
    return 1;
}

# Purpose: Retrieve the JSON structure stored in the password database
# Usage: ($preV2,$structure) = loadJSONStructure(PATH);
# NOTE: In practically all circumstances you want to call loadData() and NOT
#   loadJSONStructure. loadData performs validation, upgrades, safety checks
#   and so on which this function does not.
sub loadJSONStructure
{
    my $path = shift;

    # If there's no file at $path we just return the default (empty) structure.
    if (!-e $path)
    {
        printv(V_DEBUG,$path.': does not exist. Returning empty data structure from loadJSONStructure()'."\n");
        return (0, {
            gpgpwdDataVersion => $dataVersion,
            pwds => {},
        });
    }

    my $preV2  = true;
    my ($string,$gpgReturnVal,$fileStatus) = gpgIO(undef,[ '--decrypt',$path ], { useStatus => true, requiresUnlocked => true } );
    if (!defined($string))
    {
        pDie('Decryption failed: GPG did not return any data'."\n");
    }
    if ($gpgReturnVal > 0)
    {
        pDie('Decryption failed: GPG exited with a non-zero exit status ('.$gpgReturnVal.')'."\n");
    }

    # If we are using v2+ data format, then we need to perform additional parsing
    if (index($string,'-----BEGIN PGP SIGNED MESSAGE-----') == 0)
    {
        # We're not using a pre V2 data format
        $preV2 = false;
        # The JSON string will be stored here
        my $jsonString = '';
        my $seenStart  = false;
        # Iterate through all lines to parse out the JSON string
        foreach my $l (split(/\n/,$string))
        {
            # A line starting with { indicates the beginning of the JSON string
            if (!$seenStart && (index($l,'{') == 0))
            {
                $seenStart = true;
            }
            # The PGP SIGNATURE line indicates the end of the JSON string
            elsif (index($l,'-----BEGIN PGP SIGNATURE-----') == 0)
            {
                last;
            }
            # If we have seen the start (ie. the initial {), treat this as a JSON string
            if ($seenStart)
            {
                $jsonString .= $l;
            }
        }
        # Verify the GPG signature of the metadata
        verifyMetadataSignature($path,$string,$fileStatus);
        # Replace the raw string with the parsed JSON string
        $string = $jsonString;
    }

    my $data;

    # Decode the JSON
    try
    {
        $data = decode_json($string);
    }
    catch
    {
        pDie('Failed to decode encrypted JSON data. The file is either not a gpgpwd'."\n".
            'file, or the file is corrupt.'."\n".
            'JSON error: '.$_."\n\n".
            'If the file is a corrupt gpgpwd file, you may be able to recover it by'."\n".
            'manually decrypting the file and then editing it.'."\n");
    };
    return ($preV2,$data);
}

# Purpose: Upgrade a v1 database file to v3
# Usage: upgradeDataV1toCurrent();
sub upgradeDataV1toCurrent
{
    # Load the data
    my($preV2,$data) = loadJSONStructure($storagePath);
    # Check if the data has already been upgraded
    if($data->{gpgpwdDataVersion} > 1)
    {
        # If it is a preV2 file then someone has tampered with it
        if ($preV2)
        {
            pDie('Your database file is a v1 file claiming to be 2+'."\n".
                'Someone has tampered with your database file. Upgrade aborted.'."\n");
        }
        else
        {
            print 'Your database has already been upgraded.'."\n";
            pExit(0);
        }
    }
    # If we have seen V2+ data, but the file claims to be version 1 then
    # something is corrupt, so we abort to avoid performing multiple
    # conversions.
    if ($preV2 == 0)
    {
        pDie('Your password database is a v2 file claiming to be v1'."\n".
            'The password database may be corrupt. Upgrade aborted.'."\n");
    }
    # First perform a git pull
    if ($enableGit)
    {
        print "First performing a git pull...\n";
        git('pull',$storagePath);
        print "Reloading data from git...\n";
        ($preV2,$data) = loadJSONStructure($storagePath);
        if (!$preV2)
        {
            print "Data in git has been upgraded. Upgrade aborted\n";
            pExit(0);
        }
    }
    print 'Converting ...';
    # Perform data conversion. Encrypts every password explicitly.
    foreach my $pwd (keys %{$data->{pwds}})
    {
        $data->{pwds}->{$pwd} = { pwd => gpgEncryptString($data->{pwds}->{$pwd}) };
    }
    print "done\n";
    # Write out the data file with the converted data
    print 'Writing updated data...';
    # First, make a backup
    if (-e $storagePath.'.gpgpwdupgrade')
    {
        pDie($storagePath.'.gpgpwdupgrade already exists. Move this file out of the way.'."\n");
    }
    copy($storagePath,$storagePath.'.gpgpwdupgrade') or pDie('Error: Unable to create backup file - '.$!."\n".'Upgrade aborted.'."\n");
    # Write the data
    my $oldEnableGit = $enableGit;
    $enableGit = 0;
    writeData($storagePath,$data);
    $enableGit = $oldEnableGit;
    print "done\n";
    # Verify the integrity of the upgraded data file
    verifyUpgradeIntegrity();
    # Remove the backup
    unlink($storagePath.'.gpgpwdupgrade');
    print "done - upgrade successfully completed\n";
    # Push the upgraded file if needed
    git('push',$storagePath);
    pExit(0);
}

# Purpose: Upgrade a v2 database to v3
# Usage: upgradeDataV2toCurrent();
sub upgradeDataV2toCurrent
{
    # Load the data
    my($preV2,$data) = loadJSONStructure($storagePath);
    # Check if the data has already been upgraded
    if($data->{gpgpwdDataVersion} eq '3')
    {
        return 1;
    }
    if($data->{gpgpwdDataVersion} eq '1')
    {
        pDie('V2 to V3 subroutine triggered for V1 to V3 upgrades'."\n");
    }
    statusOut('');
    print "Database upgrade triggered...\n";
    # First perform a git pull
    if ($enableGit)
    {
        print "First performing a git pull...\n";
        git('pull',$storagePath);
        print "Reloading data from git...";
        ($preV2,$data) = loadJSONStructure($storagePath);
        if($data->{gpgpwdDataVersion} >= 3)
        {
            print "data in git has already been upgraded.\n\n";
            return $data;
        }
    }
    print 'Converting ...';
    # Perform data conversion. Encrypts every password explicitly.
    foreach my $pwd (keys %{$data->{pwds}})
    {
        $data->{pwds}->{$pwd} = { pwd => $data->{pwds}->{$pwd} };
    }
    print "done\n";
    # Write out the data file with the converted data
    print 'Writing updated data...';
    # First, make a backup
    if (-e $storagePath.'.gpgpwdupgrade')
    {
        pDie($storagePath.'.gpgpwdupgrade already exists. Move this file out of the way.'."\n");
    }
    copy($storagePath,$storagePath.'.gpgpwdupgrade') or pDie('Error: Unable to create backup file - '.$!."\n".'Upgrade aborted.'."\n");
    # Write the data
    my $oldEnableGit = $enableGit;
    $enableGit = 0;
    writeData($storagePath,$data);
    $enableGit = $oldEnableGit;
    print "done\n";
    # Verify the integrity of the upgraded data file
    verifyUpgradeIntegrity();
    # Remove the backup
    unlink($storagePath.'.gpgpwdupgrade');
    print "done - upgrade successfully completed\n";
    # Push the upgraded file if needed
    git('push',$storagePath);

    # Return the new data (we just return it directly, we know that it is valid
    # since verifyUpgradeIntegrity has done a check that includes performing a
    # loadData())
    return $data;
}

# Purpose: Verify that a data upgrade was successful
sub verifyUpgradeIntegrity
{
    print "Verifying integrity...";
    try
    {
        # Check that the new data is larger than the old
        if (-s $storagePath <= -s $storagePath.'.gpgpwdupgrade')
        {
            die('The old data file is larger than the new. This should not be possible.');
        }
        # Make pDie call die() directly instead of warn()+pExit()
        $pDieUsepExit = 0;
        # Temporarily reduce verbosity to avoid loadData() outputting
        # status information
        my $origVerbosity = $verbosity;
        $verbosity = -1;
        # Load the data, storing errors in $error
        my $error;
        try
        {
            my $data = loadData($storagePath);
            my $decryptedOne = 0;
            foreach my $k (keys %{ $data->{pwds} })
            {
                if (!ref($data->{pwds}->{$k}))
                {
                    die('it does not contain a v3 data structure'."\n");
                }
                if (!$decryptedOne)
                {
                    $decryptedOne = 1;
                    try
                    {
                        gpgDecryptString($data->{pwds}->{$k}->{pwd},$k);
                    }
                    catch
                    {
                        die('Failed to perform inner data decryption.'."\n");
                    }
                }
            }
        }
        catch
        {
            $error = $_;
        };
        # Reset pDie to its default mode
        $pDieUsepExit = 1;
        # Reset verbosity to its default or user-provided value
        $verbosity = $origVerbosity;
        # If the loadData failed, we error out
        if (defined $error)
        {
            die('Failed to load the upgraded file: '.$error);
        }
    }
    catch
    {
        # Restore the backup file
        print "failed\n";
        unlink($storagePath);
        move($storagePath.'.gpgpwdupgrade',$storagePath);
        # Make sure pDie is set to its default mode
        $pDieUsepExit = 1;
        # Error out
        pDie('Error: '.$_.
            'Restoring the old file and aborting the upgrade to avoid corruption.'."\n".
            'This is likely a bug in gpgpwd.'."\n"
        );
    };
}

# Purpose: Load the password database
# Usage: $data = loadData(PATH);
# PATH is the path to the location of the database
#
# If PATH does not exist then it will return an empty (but usable)
# $data ref.
sub loadData
{
    my $path         = shift;
    statusOut('(loading password database)');
    my($preV2,$data) = loadJSONStructure($path);

    if (!defined $data->{pwds} || ref($data->{pwds}) ne 'HASH')
    {
        pDie('Detected possible corruption in '.$path.' - refusing to continue'."\n");
    }
    elsif(! defined($data->{gpgpwdDataVersion}))
    {
        pDie($path.': does not specify data format version - refusing to continue'."\n");
    }
    elsif($data->{gpgpwdDataVersion} eq '1')
    {
        statusOut('');
        if ($enableGit)
        {
            print "Old data format detected, performing a git pull...\n";
            git('pull',$storagePath);
            print "Reloading data from git...\n";
            ($preV2,$data) = loadJSONStructure($path);
            if (!$preV2)
            {
                print "Data in git has been upgraded, continuing...\n";
                return loadData($path);
            }
        }
        pDie('Your database file is using the old v1 format. It needs to be upgraded.'."\n".
            'If you have already upgraded your database, then this means someone has '."\n".
            'tampered with your file and the database may be compromised.'."\n\n".
            'To upgrade the database run: gpgpwd upgrade'."\n"
        );
    }
    elsif($data->{gpgpwdDataVersion} ne '1' && $preV2)
    {
        # A data format v1 file is claiming to be a version 2 file. Abort.
        pDie('pre-2 dataformat claiming to be 2+. Someone has modified your password file. Aborting.'."\n");
    }
    elsif($data->{gpgpwdDataVersion} eq '2')
    {
        $data = upgradeDataV2toCurrent();
    }
    elsif ($data->{gpgpwdDataVersion} ne $dataVersion && $data->{gpgpwdDataVersion} != 3)
    {
        pDie($path.' is version '.$data->{gpgpwdDataVersion}.' of the gpgpwd file format'."\n".
            'This version only supports version '.$dataVersion.'.'."\n".
            'You need to upgrade gpgpwd, see http://random.zerodogg.org/gpgpwd/'."\n"
        );
    }

    $data->{pwds} //= {};

    return $data;
}

# Purpose: Write the password database
# Usage: writeData(PATH,$data);
# PATH is the path to the location to write to
# $data is the data hashref
sub writeData
{
    my $path    = shift;
    my $content = shift;

    my $encoded;

    $content->{generator}         = 'gpgpwd '.$VERSION.' - http://random.zerodogg.org/gpgpwd';
    $content->{lastVersion}       = $VERSION;
    $content->{gpgpwdDataVersion} = $dataVersion;

    try
    {
        $encoded = encode_json($content);
    }
    catch
    {
        pDie('Failed to encode data for JSON output. This is a bug!'."\n".
            'JSON error: '.$_."\n");
    };
    $encoded =~ s/,/,\n/g;
    my @encodedSplit = split(/\n/,$encoded);

    # Force the signature key to get unlocked. This is fairly ugly, but we need to
    # be able to write to and read from the gpg process, which can interfere with
    # password prompts, thus executing this first here avoids having to do that.
    gpgIO(undef, [ qw(--clearsign --output /dev/null /dev/null) ], { requiresUnlocked => true });

    # Sign the JSON string. The comment contains quick instructions on decrypting
    # the embedded passwords.
    $encoded = gpgIO(\@encodedSplit,[ qw(--clearsign --comment),'gpgpwd password file. Each password is gpg encrypted, so you will need to decrypt each password you want to look up manually: echo PASSWORD-STRING|gpg -d' ], { requiresUnlocked => true });

    # Make a backup of the current file
    if (-e $path)
    {
        move($path,$path.'~') or pDie('Failed to create backup file: '.$!."\n".'Refusing to write data to avoid loss of previous data'."\n");
    }

    # Write the JSON string encrypted to the supplied file
    gpgIO($encoded,[ '--encrypt','--output',$path ],{ requiresUnlocked => true });
    chmod(0600,$path);

    # Perform some paranoid sanity checks. Verify that the file we just wrote
    # is actually there and that it is above the minimum size of 1468 bytes.
    if (!-e $path)
    {
        pDie('Fatal error: "'.$path.'" did not exist after writing the file'."\n");
    }
    elsif(-s $path < 100)
    {
        pDie('Fatal error: the size of "'.$path.'" is too small'."\n");
    }

    unlink($path.'~');

    git('push',$path);
    return 1;
}

# Purpose: Load a list of passwords from a simple file
# Usage: loadFromFile(FILE,$data);
# $data is the data hashref
# FILE is the path to the file to read
sub loadFromFile
{
    my $file = shift;
    my $data = shift;

    my $line = 0;
    my $read = 0;
    open(my $in,'<',$file) or pDie('Failed to open '.$file.' for reading: '.$!."\n");
    while(<$in>)
    {
        $line++;

        chomp;

        next if !/\S/;
        next if !length($line);
        next if /^#/;

        my $name = $_;
        my $pwd = $_;

        $name =~ s/^(\S+)\s+.*/$1/;
        $pwd =~ s/^\S+\s+//;

        if (!length($name) || !length($pwd) || $name eq $_ || $pwd eq $_)
        {
            pDie('Failed to parse line '.$line.' in '.$file."\n");
        }
        if(defined $data->{pwds}->{$name})
        {
            my $decryptedPW = gpgDecryptString($data->{pwds}->{$name}->{pwd},$name);
            if ($decryptedPW ne $pwd)
            {
                print 'Changed '.$name.' from '.$decryptedPW.' to '.$pwd."\n";
            }
        }
        $read++;
        $data->{pwds}->{$name} = { pwd => gpgEncryptString($pwd) };
    }
    # Failing to close is not much of a problem, so just log it.
    close($in) or printv(V_INFO,'Failed to close filehandle: '.$!."\n");;
    print 'Read '.$read.' entries from '.$file."\n";
    return 1;
}

# Purpose: Handle the batchadd command
sub batchaddCommand
{
    my @params = @_;

    if (!@params)
    {
        warn('Missing parameter to batchadd: path to the file to read'."\n");
        usage(104);
    }
    elsif(@params != 1)
    {
        pDie('Too many parameters for "batchadd"'."\n");
    }
    git('pull',$storagePath);
    my $data = loadData($storagePath);
    statusOut('');
    loadFromFile(shift(@params),$data);
    writeData($storagePath,$data);
    return 1;
}

# = = = = = = = = = = = = = = = =
# Password retrieval and setting
# = = = = = = = = = = = = = = = =

# Purpose: Copy a string to the clipboard if possible
# Usage: $info = toClipboard(VALUE);
# VALUE is the value to copy to the clipboard
# toClipboard returns a string, which is empty if nothing was done and '
# (copied)' if something was copied to the clipboard.
sub toClipboard
{
    my $value        = shift;
    if (!@clipboardTargets)
    {
        return '';
    }
    if (!InPath('xclip') || !(defined($ENV{DISPLAY}) && length($ENV{DISPLAY})))
    {
        if(InPath('xclip'))
        {
            printv(V_LOG,'Use of clipboard disabled: no DISPLAY set'."\n");
        }
        else
        {
            printv(V_LOG,'Use of clipboard disabled: xclip not installed'."\n");
        }
        return '';
    }
    foreach my $target (@clipboardTargets)
    {
        open(my $write,'|-','xclip','-in','-selection',$target,'-silent') or return '';
        print {$write} $value or return '';
        close($write) or printv(V_INFO,'Failed to close filehandle to xclip: '.$!."\n");
        printv(V_DEBUG,'xclip instance started for '.$target."\n");
    }
    return ' (copied)';
}

# Purpose: Get a random password
# Usage: $randomPwd = randomPwd(length = (auto), alphaNumOnly = false);
sub randomPwd
{
    my $length = shift;
    my $alphaNumOnly = shift;
    if (!defined $length)
    {
        $length = $config->{defaultPasswordLength} || 15;
    }
    while(1)
    {
        my $pwd = '';
        # These characters are chosen specifically because they are usually selectable in
        # a terminal by simply double-clicking on the string.
        my @chars = ('a'..'z','A'..'Z',0..9);
        if (!$alphaNumOnly)
        {
            push(@chars,',','.','/','?','%','&','#',':','_','=','+','@','~');
        }
        while(length($pwd) < $length)
        {
            $pwd .= $chars[ rand scalar @chars ];
        }
        # Require a password to have at least one number, one lower- and one
        # upper-case character, and one non-word (symbol) character
        if ($pwd =~ /\d+/ && $pwd =~ /[A-Z]/ && $pwd =~ /[a-z]/ && ($alphaNumOnly || $pwd =~ /\W/))
        {
            return $pwd;
        }
        # Password not accepted, try again
    }
}

# Purpose: Get a fuzzy regexp for simple typos or missing characters
sub getTypoOrMissingRegex
{
    my $pattern = shift;
    my $fuzzyness = shift;

    # This is done by first removing any 'non-word' character.
    # Then each part is split into a regex that accepts the
    # current, previous and next character in the word, as well as $fuzzyness
    # characters after this one, which can be anything.
    #
    # Ie. the $name 'test', and $fuzzy=1 will become:
    # [te][tes][est][st]
    #
    # Whereas $fuzzy=4 will become:
    # [te].?.?.?[tes].?.?.?[est].?.?.?[st].?.?.?
    my @parts = split('',$pattern);
    $pattern = '';
    my $prev = '';
    my $fuzzyNo = $fuzzyness;
    my $fuzzyString = '';
    while($fuzzyNo--)
    {
        $fuzzyString .= '.?';
    }
    for(my $i = 0; $i < @parts; $i++)
    {
        my $part ='';
        if ($i != 0)
        {
            $part .= $parts[$i-1];
        }
        $part .= $parts[$i];
        if ( defined $parts[$i+1])
        {
            $part .= $parts[$i+1];
        }
        $pattern .= '['.$part.']'.$fuzzyString;
    }
    return (qr/$pattern/i,$pattern);
}

# Purpose: Get passwords from the database and output them to the user
# Usage: getPasswords($data,NAME);
# $data is the data hashref
# NAME is the regex to search for
sub getPasswords
{
    my $data        = shift;
    my $name        = shift;
    my $fuzzySearch = 0;
    my $matches     = {};
    my $hasUsernames;

    # Loop that tries fuzzier and fuzzier matching for the requested NAME until
    # it finds one that matches (or, if $allMatches is true, just keeps going
    # through all of them).
    foreach my $fuzzy (0..10)
    {
        my $grantsStartEndBonus = true;
        my $pattern = $name;

        # If we're doing a fuzzy match, remove all non-word characters to avoid
        # problems in the regex
        if ($fuzzy > 1)
        {
            $pattern =~ s/\W//g;
            if (!length($pattern))
            {
                last;
            }
        }

        my $regex;
        # Compile the regex and verify its syntax
        if ($fuzzy == 0)
        {
            $regex = qr/$name/i or pDie('Failed to parse "'.$name.'" as a perl regular expression'."\n");
        }
        # Fuzzy method: Multiple words in wrong order
        # Fuzzy method: Multiple words, where one of them is wrong
        elsif($fuzzy == 1 || $fuzzy == 8)
        {
            my @parts = split(/\W/,$name);
            if( scalar(@parts) == 1)
            {
                printv(V_LOG,'Skipping fuzzy test '.$fuzzy.' as there were no /\W/-separated parts in the expression'."\n");
                next;
            }
            my $expr = '('.join('|',@parts).')';
            $pattern = '';
            if ($fuzzy == 1)
            {
                while(defined shift(@parts))
                {
                    if ($pattern)
                    {
                        $pattern .= '.+';
                    }
                    $pattern .= $expr;
                }
            }
            elsif($fuzzy == 8)
            {
                $pattern = $expr;
            }
            $regex = qr/$pattern/i;
        }
        # Fuzzy method 2: Simple typos or missing characters
        elsif ($fuzzy == 2)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,true);
        }
        # Extra characters, or other mistakes, inside the word, but approx.
        # the correct length
        elsif($fuzzy == 3)
        {
            # This is based upon the assumption that the first and last
            # characters are correct, and that all characters that we need are
            # present. Additionally it assumes that the length is approximately
            # correct
            #
            # All non-word characters are removed.
            my @parts = split('',$pattern);
            my $minLength = scalar(@parts)-3;
            my $maxLength = scalar(@parts)+1;
            $pattern = $parts[0].'['.$pattern.']{'.$minLength.','.$maxLength.'}'.$parts[-1];
            $regex = qr/$pattern/;
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 4)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,2);
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 5)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,3);
        }
        # Simple typos or missing characters
        elsif ($fuzzy == 6)
        {
            ($regex,$pattern) = getTypoOrMissingRegex($pattern,4);
        }
        # Extra characters, or other mistakes, inside the word
        elsif ($fuzzy == 7 || $fuzzy == 11)
        {
            # This is based upon the assumption that the first and last characters
            # are correct, and that all characters that we need are present.
            #
            # All non-word characters are removed.
            my @parts = split('',$pattern);
            $pattern = $parts[0].'['.$pattern.']+'.$parts[-1];
            if ($fuzzy == 6)
            {
                $pattern = '^'.$pattern.'$';
                $grantsStartEndBonus = false;
            }
            $regex = qr/$pattern/;
        }
        # Correct length, incorrect order
        elsif ($fuzzy == 9)
        {
            # This is very general, all non-word characters are removed, and
            # we construct a character class consisting of all of the remaining
            # characters. This character class has to be present at least length($word)
            # times. This only works for short words, and isn't used if length > 8
            next if length($name) > 8;
            $pattern = '['.$pattern.']{'.length($pattern).'}';
            $regex = qr/$pattern/;
        }
        # Fuzzy method: Acronym detection
        elsif($fuzzy == 10)
        {
            # This is about as general as it can get. We assume all characters are
            # present, but that it was added as an acronym. Ie. "example site" could
            # be "es". It's extremely general and thus any match gets labelled
            # "very fuzzy", instead of just "fuzzy". It also isn't used if the expression
            # is longer than 12 characters.
            next if length($name) > 12;
            $fuzzySearch++;

            $pattern = '^['.$pattern.']+$';
            $regex = qr/$pattern/i;
            $grantsStartEndBonus = false;
        }
        else
        {
            pDie('Attempted to use unknown fuzzy method no. '.$fuzzy);
        }

        if ($fuzzy)
        {
            printv(V_LOG,'Trying fuzzy regex ('.$fuzzy.'): '.$pattern."\n");
        }
        else
        {
            printv(V_LOG,'Trying exact match ('.$fuzzy.'): '.$pattern."\n");
        }

        $hasUsernames = getMatches($matches,$data,$regex,$name,$fuzzy,$grantsStartEndBonus);

        if ($allMatches)
        {
            next;
        }

        if(keys %{$matches})
        {
            if ($fuzzy)
            {
                $fuzzySearch++;
            }
            last;
        }
    }
    statusOut('');

    if (! (keys %{$matches}))
    {
        print '(no passwords found for "'.$name.'")'."\n";
        return;
    }

    # Caluculates the password length for the header
    my $passwordWidth = $config->{defaultPasswordLength} || 15;
    # Add the size of " (copied)" (9) plus padding (10)
    $passwordWidth += 19;
    if ($config->{storeUsernames} eq 'true' && $hasUsernames)
    {
        printf('%-20s  %-'.$passwordWidth.'s %s'."\n",'Name','Password','Username');
        printf('%-20s  %-'.$passwordWidth.'s %s'."\n",'----','--------','--------');
    }
    else
    {
        printf('%-20s  %-'.$passwordWidth.'s'."\n",'Name','Password');
        printf('%-20s  %-'.$passwordWidth.'s'."\n",'----','--------');
    }
    if ($allMatches)
    {
        print ' (showing all matches, including very fuzzy ones)';
        print "\n";
    }
    elsif ($fuzzySearch)
    {
        print ' (found using ';
        if ($fuzzySearch > 1)
        {
            print 'very ';
        }
        print 'fuzzy search)';
        print "\n";
    }

    my $entries = scalar(keys(%{ $matches }));
    my $entryNo = 0;
    foreach my $entry (sort { $matches->{$a}->{score} <=> $matches->{$b}->{score} } keys %{$matches})
    {
        $entryNo++;
        outputEntry($entry,$matches->{$entry}, $entryNo == 1,$passwordWidth);
    }

    return 1;
}

# Purpose: Resolve an alias
# Usage: resolvedName = resolveAlias(data,aliasName,originalName);
sub resolveAlias
{
    my $data = shift;
    my $alias = shift;
    my $orig = shift;
    my $result;
    my $loopN = 100;
    while(--$loopN)
    {
        if ($data->{pwds}->{$alias} && $data->{pwds}->{$alias}->{alias})
        {
            $alias = $data->{pwds}->{$alias}->{alias};
        }
        else
        {
            $result = $alias;
        }
    }
    if (!defined $result)
    {
        die('Fatal error: failed to resolve alias for "'.$orig.'"'."\n".
            'The depth could be too deep or there are circular references'."\n");
    }
    if (!defined $data->{pwds}->{$result})
    {
        die('Fatal error: failed to resolve alias for "'.$orig.'"'."\n".
            'The depth could be too deep or there are dangling references'."\n");
    }
    printv(V_LOG,'Resolved alias entry '.$orig.' to '.$result."\n");
    return $result;
}

# Purpose: Get data entries that match a given regex
# Usage: $hasUsernames = getMatches($data,$regex,$name);
# hasUsernames is a bool, true if any of the matched entries had a username
sub getMatches
{
    my $matches             = shift;
    my $data                = shift;
    my $regex               = shift;
    my $name                = shift;
    my $score               = shift;
    my $grantsStartEndBonus = shift;
    my $searchLength        = length($name);
    my $fuzzyNo             = $score;
    my $hasUsernames;

    # Iterate through all of the possible passwords, checking each if it
    # matches the $regex. If it does we assign a numeric score to it, where a
    # lower score means a more exact match.
    foreach my $key (sort keys %{$data->{pwds}})
    {
        # Skip this check if it has already been matched
        next if defined $matches->{$key};
        if ($key =~ $regex)
        {
            # Set the default score
            my $entryScore = $score;
            if ($key eq $name)
            {
                # Grant a -5 bonus if it is an exact match
                $entryScore -= 5;
                printv(V_DEBUG,'Granting a -5 bonus to '.$key.' because it is an exact match'."\n");
            }
            else
            {
                # Add a 0.5 penalty for each character above the search string length
                # IF the $name is only alphanumeric
                if ($name !~ /\W/)
                {
                    my $keyLen = length($key);
                    if ($keyLen > $searchLength)
                    {
                        my $penalty = ($keyLen-$searchLength)*0.25;
                        printv(V_DEBUG,'Penalizing '.$key.' '.$penalty.' due to its length'."\n");
                        $entryScore += ($keyLen-$searchLength)*0.25;
                    }
                }
                # Grant a bonuses if the regex matched at the beginning or end, if
                # requested
                if ($grantsStartEndBonus)
                {
                    my ($bonus,$reason);
                    # Grant a -3 bonus for matching the regex at the start of
                    # the string followed by a non-word character and the fuzzy
                    # method in use is 0
                    if ($fuzzyNo == 0 && $key =~ /^$regex\S/)
                    {
                        $bonus = -3;
                        $reason = 'start-nonword';
                    }
                    # Grant a -2 bonus for matching the regex at the start of the string
                    elsif ($key =~ /^$regex/)
                    {
                        $bonus = -2;
                        $reason = 'start';
                    }
                    # Grant a -1 bonus for matching the regex at the end of the string
                    elsif($key =~ /$regex$/)
                    {
                        $bonus = -1;
                        $reason = 'end';
                    }
                    if ($reason)
                    {
                        printv(V_DEBUG,'Granting a '.$bonus.' bonus to '.$key.' due to '.$reason.' match'."\n");
                        $entryScore += $bonus;
                    }
                }
            }
            if(defined $data->{pwds}->{$key}->{alias})
            {
                printv(V_DEBUG,$key.' is aliased to '.$data->{pwds}->{$key}->{alias}.' - switching to it'."\n");
                $key = resolveAlias($data,$key,$key);
            }
            if(defined $data->{pwds}->{$key}->{user})
            {
                printv(V_DEBUG,'Granting a -0.5 bonus to '.$key.' due to having a user entry'."\n");
                $entryScore += -0.5;
                $hasUsernames = 1;
            }
            if (defined $matches->{$key})
            {
                if($matches->{$key}->{score} >= $entryScore)
                {
                    next;
                }
            }
            $matches->{$key} = {
                password => $data->{pwds}->{$key}->{pwd},
                user => $data->{pwds}->{$key}->{user},
                score => $entryScore,
            };
        }
    }
    return $hasUsernames;
}

# Purpose: Set a password value in the database
# Usage: setPassword($data,NAME);
# $data is the data hashref
# NAME is the name of the entry to add
sub setPassword
{
    my $data = shift;
    my $name = shift;

    # Ensure the gpg agent has been initialized at this point
    autoInitGPGAgent();

    my ($password, $copied);
    my $alphaNumeric  = 0;
    my $length;
    my $readLine      = Term::ReadLine->new('gpgpwd');
    my $existing;
    my $existingUser;
    if(defined $data->{pwds}->{$name})
    {
        if(defined $data->{pwds}->{$name}->{alias})
        {
            statusOut('');
            print '"'.$name.'" already exists and is an alias. You must first remove the alias before you'."\n";
            print 'can add a password with that name (or you can use the "alias" command to change what'."\n";
            print ' "'.$name.'" is an alias for).'."\n";
            exit(1);
        }
        $existing = gpgDecryptString($data->{pwds}->{$name}->{pwd},$name);
        $existingUser = $data->{pwds}->{$name}->{user};
    }
    statusOut('');

    # Loop to retreive the password
    while(1)
    {
        # If $password is defined then this is a second-or-later iteration
        # through the loop
        if(defined $password)
        {
            print "\n";
        }
        my $prompt = 'Password> ';
        # Generate a random password
        my $random = randomPwd($length,$alphaNumeric);
        # Copy it to the clipboard if needed
        $copied    = toClipboard($random);
        if (defined $existing)
        {
            print 'Changing the entry for '.$name."\n";
        }
        else
        {
            print 'Adding an entry for '.$name."\n";
        }
        print "\n";
        print 'Random password: '.$random.$copied."\n";
        print '  Enter /help for help.'."\n";
        if(defined $existing)
        {
            if ($config->{storeUsernames} eq 'true')
            {
                print '  Enter - to keep the previously stored password ('.$existing.').'."\n";
            }
            print '  Enter your new password to add a new custom password.'."\n";
        }
        else
        {
            print '  Enter a password to use a custom password.'."\n";
        }
        print '  Just press enter to use the random password.'."\n";
        $password = $readLine->readline($prompt);
        # If a user sends EOF, just go through the loop again
        if (!defined $password)
        {
            print "\n";
            next;
        }
        chomp $password;
        if($password eq '-' && defined $existing)
        {
            $password = $existing;
        }
        if (!length $password)
        {
            $password = $random;
            if (!defined $existing)
            {
                print "Using password: $random\n";
            }
            last;
        }
        # Output help text
        elsif(index($password,'/help') == 0)
        {
            print "\n";
            print "The following commands are available:\n";
            printHelp('','/help','Display this help screen');
            printHelp('','/alphanumeric','Generate an alphanumeric password (a password with only letters and numbers, without any symbols');
            printHelp('','/regenerate','Regenerate a new password (with symbols, letters and numbers)');
            print "Both /alphanumeric and /regenerate can take a single parameter,\n";
            print "the length of the password to be generated. Ie. /alphanumeric 15\n";
            print "will generate a 15-character long alphanumeric password\n";
        }
        # Generate a alphanumeric-only password
        elsif(index($password,'/alphanumeric') == 0)
        {
            $alphaNumeric = 1;
        }
        # Generate a new password
        elsif(index($password,'/regenerate') == 0)
        {
            $alphaNumeric = 0;
        }
        else
        {
            last;
        }
        # Handle a length-parameter supplied to /regenerate or /alphanumeric
        if ($password =~ s{^/(regenerate|alphanumeric)\s+(\d+)\s*}{$2})
        {
            if ($password < 0 && $password > 1000)
            {
                print 'The password length must be higher than zero'."\n";
            }
            else
            {
                $length = $password;
            }
        }
        else
        {
            $length = undef;
        }
    }
    my $user;
    if ($config->{storeUsernames} eq 'true')
    {
        print "\n";
        if(defined $existingUser)
        {
            print '  Enter a new username for this entry.'."\n";
            print '  Just press enter to keep the current username ('.$existingUser.').'."\n";
            print '  Enter a single dot (".") to remove the stored username.'."\n";
        }
        else
        {
            print '  Enter a username for this entry.'."\n";
            if(defined $config->{defaultUsername} && length($config->{defaultUsername}))
            {
                print '  Just press enter to store the default username ('.$config->{defaultUsername}.').'."\n";
                print '  Enter a single dot (".") to not store any username.'."\n";
            }
            else
            {
                print '  Just press enter to not store any username.'."\n";
            }
        }
        $user = $readLine->readline('Username> ');
        chomp $user;
    }

    if(defined $existing && $existing ne $password)
    {
        print 'Changed '.$name.' from '.$existing.' to '.$password."\n";
    }
    if(defined $existingUser && defined $user)
    {
        if($user eq '.')
        {
            print 'Removed the username for '.$name."\n";
            $user = undef;
        }
        elsif(!length($user))
        {
            $user = $existingUser;
        }
        else
        {
            print 'Changed the username for '.$name.' from '.$existingUser.' to '.$user."\n";
        }
    }
    elsif(defined $config->{defaultUsername} && length($config->{defaultUsername}))
    {
        if($user eq '.')
        {
            $user = undef;
        }
        elsif(length($user) == 0)
        {
            $user = $config->{defaultUsername};
        }
    }
    my $entry = {};
    if (defined $existing && $existing eq $password)
    {
        $entry->{pwd} = $data->{pwds}->{$name}->{pwd};
    }
    else
    {
        $entry->{pwd} = gpgEncryptString($password);
    }
    if(defined $user && length($user))
    {
        $entry->{user} = $user;
    }
    else
    {
        delete($entry->{user});
    }
    $data->{pwds}->{$name} = $entry;

    return 1;
}

# Purpose: Output a password
# Usage: outputEntry(KEY,VALUE,COPY);
# KEY is the title
# VALUE is the content (ie. password)
# COPY is a bool, if true it will toClipboard() the VALUE
sub outputEntry
{
    my $key           = shift;
    my $content       = shift;
    my $copy          = shift;
    my $passwordWidth = shift;
    my $copied  = '';
    if ($config->{storeUsernames} eq 'true' && defined($content->{user}))
    {
        printf('%-20s: %-'.$passwordWidth.'s %s',
            $key,
            '...',
            $content->{user});
    }
    else
    {
        printf('%-20s: %s',$key,'...');
    }
    my $value = gpgDecryptString($content->{password},$key);
    if ($copy)
    {
        $copied = toClipboard($value);
    }

    printv(V_LOG,$key.' had a score of '.$content->{score}."\n");
    if ($config->{storeUsernames} eq 'true' && defined($content->{user}))
    {
        printf("\r".'%-20s: %-'.$passwordWidth.'s %s'."\n",
            $key,
            $value.$copied,
            $content->{user});
    }
    else
    {
        printf("\r".'%-20s: %s'."\n",$key,$value.$copied);
    }

    return 1;
}

# Purpose: Handle requests for passwords
sub getPWCommand
{
    my @params = @ARGV;

    if(! @params)
    {
        warn('Missing parameter to get: what to retrieve'."\n");
        usage(103);
    }
    elsif(@params != 1)
    {
        warn('Warning: Too many parameters for "get", joining all parameters together'."\n".'to a single string'."\n\n");
        @params = (join(' ',@ARGV));
    }

    my $data = loadData($storagePath);
    getPasswords($data,@params);

    if ($enableGit)
    {
        my $fileID = getFileID($storagePath);
        git('safepull',$storagePath);
        # If the file has changed due to the pull, then we re-fetch
        if ($fileID ne getFileID($storagePath))
        {
            print "\n";
            print "File updated by git, re-reading passwords:\n";
            $data = loadData($storagePath);
            getPasswords($data,@params);
        }
    }

    return 1;
}

# Purpose: Handle requests to remove passwords
sub removePWCommand
{
    my @params = @_;

    if (!@params)
    {
        warn('Missing parameter to remove: what to remove'."\n");
        usage(104);
    }
    elsif(@params != 1)
    {
        pDie('Too many parameters for "remove"'."\n");
    }
    my $name = shift(@params);

    git('pull',$storagePath);
    my $data = loadData($storagePath);
    statusOut('');
    if ($data->{pwds}->{$name})
    {
        if ($data->{pwds}->{$name}->{alias})
        {
            print 'Removed '.$name.' (which was an alias pointing to '.$data->{pwds}->{$name}->{alias}.')'."\n";
        }
        else
        {
            my $hadPassword = gpgDecryptString($data->{pwds}->{$name}->{pwd},$name);
            print 'Removed '.$name.' (with the password '.$hadPassword.')'."\n";
        }
        delete($data->{pwds}->{$name});
        my @deepCheck = ($name);
        while(my $value = shift(@deepCheck))
        {
            foreach my $entry (keys %{$data->{pwds}})
            {
                if (defined $data->{pwds}->{$entry}->{alias} && $data->{pwds}->{$entry}->{alias} eq $value)
                {
                    print 'Also removed the alias "'.$entry.'" which was pointing to "'.$value.'"'."\n";
                    delete($data->{pwds}->{$entry});
                    push(@deepCheck,$entry);
                }
            }
        }
    }
    else
    {
        print 'No entry named '.$name.' found. Doing nothing.'."\n";
        pExit(0);
    }
    writeData($storagePath,$data);

    return 1;
}

# Purpose: Handle requests to add a new password
sub addPWCommand
{
    my $command = shift;
    my @params  = @_;

    if (!@params)
    {
        warn('Missing parameter to '.$command.': what to set'."\n");
        usage(104);
    }
    elsif(@params != 1)
    {
        pDie('Too many parameters for "'.$command.'" (note that you will be prompted for a password)'."\n");
    }
    git('pull',$storagePath);
    my $data = loadData($storagePath);
    setPassword($data,@params);
    writeData($storagePath,$data);

    return 1;
}

# Purpose: Handle requests to rename a password entry
sub renamePWCommand
{
    my @params = @_;

    my $old = shift(@params);
    my $new = shift(@params);
    if (!defined $old)
    {
        warn('Missing parameters to rename: old name, new name'."\n");
        usage(104);
    }
    elsif (!defined $new)
    {
        warn('Missing parameters to rename: new name'."\n");
        usage(104);
    }
    elsif(@params != 0)
    {
        pDie('Too many parameters for "rename"'."\n");
    }
    git('pull',$storagePath);
    my $data = loadData($storagePath);
    statusOut('');

    my $entry = $data->{pwds}->{$old};
    if (!defined $entry)
    {
        pDie('Failed to find "'.$old.'". Note that you must specify the exact name when'."\n".'using "rename", as it does no fuzzy searching'."\n");
    }
    $data->{pwds}->{$new} = $entry;
    delete($data->{pwds}->{$old});

    foreach my $entry (keys %{$data->{pwds}})
    {
        if (defined $data->{pwds}->{$entry}->{alias} && $data->{pwds}->{$entry}->{alias} eq $old)
        {
            $data->{pwds}->{$entry}->{alias} = $new;
        }
    }

    print 'Renamed the entry for '.$old.' to '.$new."\n";

    writeData($storagePath,$data);

    return 1;
}

# Purpose: Add or change an alias
sub aliasCommand
{
    my $to = shift;
    my $name = shift;
    my $existed;
    if (!defined $to || !defined $name)
    {
        print "Usage: alias <to> <name>\n";
        print "   <to> is a password entry that already exists\n";
        print "   <name> is the name of the alias itself\n";
        exit(1);
    }
    git('pull',$storagePath);
    my $data = loadData($storagePath);
    statusOut('');
    if (!defined $data->{pwds}->{$to})
    {
        die('There is no existing password entry for "'.$to.'"'."\n");
    }
    if (defined $data->{pwds}->{$name})
    {
        if (!defined $data->{pwds}->{$name}->{alias})
        {
            die('A password entry for "'.$name.'" already exists and is not an alias'."\n");
        }
        $existed = $data->{pwds}->{$name}->{alias};
    }
    $data->{pwds}->{$name} = {
        alias => $to
    };
    if ($existed)
    {
        if ($existed eq $to)
        {
            print 'The alias "'.$name.'" already exists and points to "'.$to.'"'."\n";
            exit(0);
        }
        print 'Changed the alias for "'.$name.'" from "'.$existed.'" to "'.$to.'"'."\n";
    }
    else
    {
        print 'Added an alias for "'.$name.'" that references "'.$to.'"'."\n";
    }
    writeData($storagePath,$data);
}

# = = = = = = =
# GIT handling
# = = = = = = =

# Purpose: Perform git actions
# Usage: git(ACTION,PATH);
# PATH is the path to the data file we are operating on
# ACTION is one of:
#   pull       Pull changes
#   safepull   Pull changes IF we have an ssh agent
#   push       Push changes
sub git
{
    my $command = shift;
    my $path    = shift;

    if (!$enableGit)
    {
        return;
    }

    my $cwd = getcwd;
    chdir(dirname($path));

    if ($command eq 'safepull')
    {
        if (
            (defined $ENV{SSH_AGENT_PID}) ||
            (
                (defined $ENV{GNOME_KEYRING_PID} || (defined $ENV{XDG_CURRENT_DESKTOP} && $ENV{XDG_CURRENT_DESKTOP} eq 'GNOME') )
                && defined $ENV{SSH_AUTH_SOCK})
            )
        {
            $command = 'pull';
        }
        else
        {
            $command = 'noop';
            printv(V_INFO,'Not pulling since SSH_AGENT_PID is not set, and neither is GNOME_KEYRING_PID or XDG_CURRENT_DESKTOP=GNOME and SSH_AUTH_SOCK'."\n");
        }
    }

    my $hasUpstream = 0;
    if ( (silentSystem(qw(git config --local --get branch.master.remote)) == 0) || (silentSystem(qw(git config --local --get remote.origin.fetch .*master.*)) == 0))
    {
        $hasUpstream = 1;
    }

    if (!$hasUpstream)
    {
        print "Warning: git pull/push disabled: no remote set for the \"master\" branch\n";
        print "If you are pushing to a new remote, try: ".basename($0)." git initremote [remote]\n";
        return;
    }

    if($command eq 'pull')
    {
        print "\n(git pulling): ";
        if (psystem('git','pull','--rebase','--quiet') != 0)
        {
            if(psystem('git','pull','--quiet') != 0)
            {
                pDie('Failed to git pull, you must manually resolve the conflict'."\n");
            }
        }
        elsif ($verbosity == 0)
        {
            print "\r";
        }
    }
    elsif($command eq 'push')
    {
        print 'Pushing git repository...'."\n";
        if(psystem('git','add',basename($path)) != 0)
        {
            print "  Warning: Error pushing the git repo: 'git add' command failed\n";
        }
        elsif(psystem('git','commit','--quiet','-m','Update by gpgpwd',basename($path)) != 0)
        {
            print "  Warning: Error pushing the git repo: 'git commit' command failed\n";
        }
        elsif(psystem('git','push','--quiet') != 0)
        {
            print "  Warning: Error pushing the git repo: 'git push' command failed\n";
        }
    }
    elsif($command ne 'noop')
    {
        printv(V_INFO,'WARNING: Unknown command "'.$command.'" in git()'."\n");
    }

    chdir($cwd);

    return 1;
}

# Purpose: Handle 'gpgpwd git'
sub gitCommand
{
    my $force      = shift;
    my $subcommand = shift;
    my @params     = @_;

    if (!defined $subcommand)
    {
        die('Missing parameter: which git command to run'."\n".
            'One of: pull, push, init, clone, remote, fetch, branch'."\n"
        );
    }
    elsif ($subcommand eq 'pull' || $subcommand eq 'push')
    {
        git($subcommand,$storagePath);
    }
    elsif($subcommand eq 'init')
    {
        initGitRepo(1,$force);
    }
    elsif($subcommand eq 'initremote')
    {
        my $remote = shift(@params) // 'origin';
        chdir(dirname($storagePath));
        exit(psystem(qw(git push --set-upstream),$remote,'master'));
    }
    elsif($subcommand eq 'clone')
    {
        my $url = shift;
        if (psystem('git','clone',$url,$gpgpwdDir.'/gitrepo') != 0)
        {
            die('gpgpwd: Cloning failed, aborting.'."\n");
        }
        initGitRepo(0);
    }
    elsif($subcommand eq 'remote' || $subcommand eq 'fetch' || $subcommand eq 'branch')
    {
        chdir(dirname($storagePath));
        psystem('git',$subcommand,@params);
    }

    return 1;
}

# Purpose: Initialize git
sub initGitRepo
{
    my $fresh = shift;
    my $force = shift;
    $| = 1;
    print "Initializing git:\n";
    # Fresh means that we're creating the repo from scratch
    if ($fresh)
    {
        # Abort if there's already a git repo in $storagePath
        if (-d dirname($storagePath).'/.git' && !$force)
        {
            pDie('ERROR: '.$storagePath." appears to already live in git.\nRefusing to move to a new repository. Use --force to override."."\n");
        }
        # Abort if there's already a gitrepo directory in $gpgpwdDir
        if (-d $gpgpwdDir.'/gitrepo')
        {
            pDie('ERROR: '.$gpgpwdDir.'/gitrepo appears to already exist. Refusing to overwrite.'."\nRemove this directory first if you wish to reinitialize.\n");
        }
        # Create the directory that will contain the git repo
        print '   creating path...';
        mkpath($gpgpwdDir.'/gitrepo') or pDie("failed to create directory: $!\n");
        print "done\n";
        print '   moving data into repository...';
        if (-e $storagePath)
        {
            # Move existing content into the repo directory
            move($storagePath,$gpgpwdDir.'/gitrepo') or pDie("failed to move data: $!\n");
            # Symlink the old path to the new path for compatibility
            symlink($gpgpwdDir.'/gitrepo/'.basename($storagePath),$storagePath) or warn('WARNING: failed to create symlink from old data path: '."$!\n");
            print "done\n";
        }
        else
        {
            print "(no data to move)\n";
            printv(V_DEBUG,$storagePath.': did not exist'."\n");
        }
        print '   creating repository...';
        chdir($gpgpwdDir.'/gitrepo');
        # This creates an initial git repository
        psystem('git','init','--quiet');
        psystem('git','add',basename($storagePath));
        psystem('git','commit','--quiet','-m','Initial commit');
        print "done\n";
    }
    else
    {
        print '   validating checkout...';
        # Check that the password file exists in the repo. If it doesn't then
        # that might indicate an invalid --password-file or that the user has
        # checked out the wrong repository
        if ($storagePath ne $gpgpwdDir.'/'.basename($storagePath))
        {
            print "WARNING:\n";
            print "             Your supplied password file does NOT live in the git repository\n";
            print "             that you just checked out. The repository may not be used.\n";
            print "             Check your --password-file parameter or dataPath configuration setting\n";
        }
        else
        {
            print "all good\n";
            print '   symlinking database...';
            # Symlink the non-git database path to the git one
            symlink($gpgpwdDir.'/gitrepo/'.basename($storagePath),$gpgpwdDir.'/gpgpwd.db');
            print "done\n";
        }
    }
    print '   enabling git support in the config...';
    # Make sure git=auto in the config
    $config->{git} = 'auto';
    writeOutConfig();
    print "done\n";
    print "\nGit repository initialized in $gpgpwdDir/gitrepo\n";

    return 1;
}

# = = = = = = = = = = = =
# Configuration handling
# = = = = = = = = = = = =

# Purpose: Handle configuration changes (gpgpwd config)
sub configCommand
{
    my $sub = shift;
    my @commands = @_;

    # We only support a single parameter
    if (@commands && $sub ne 'remove')
    {
        pDie("Too many parameters to \"gpgpwd config\"\n");
    }

    if (!defined $sub)
    {
        # Dump the config to STDOUT
        writeOutConfig(\*STDOUT);
    }
    else
    {
        if ($sub eq 'remove')
        {
            $sub = shift;
            if (!defined $config->{$sub})
            {
                print "\"$sub\" is not set\n";
                pExit(1);
            }
            delete($config->{$sub});
            print "Reset \"$sub\" to the default value\n";
            writeOutConfig();
        }
        # If a = character is in the input, treat it as a "assign this value to
        # that setting" command
        elsif ($sub =~ /=/)
        {
            my ($key,$value) = split(/=/,$sub);
            if (!defined $config->{$key})
            {
                print "$key is not a valid setting\n";
                pExit(1);
            }
            $config->{$key} = $value;
            print "Set \"$key\" to \"$value\"\n";
            writeOutConfig();
        }
        elsif(defined($config->{$sub}))
        {
            print $sub.'='.$config->{$sub}."\n";
        }
        else
        {
            print "No config setting for \"$sub\"\n";
        }
    }

    return 1;
}

# Purpose: Write a configuration file
# Usage: ConfigWriter(/path/to/file, \%Config, \%Descriptions);
sub ConfigWriter
{
	my ($File, $Config, $Description) = @_;

    my $CONFIG;

    # If $File is already a filehandle then write directly to it
    if(ref($File) ne 'GLOB')
    {
        # Open the config for writing
        open($CONFIG, '>', $File) or do {
            # If we can't then we error out, no need for failsafe stuff - it's just the config file
            warn("Unable to save the configuration file $File: $!");
            return(0);
        };
    }
    else
    {
        $CONFIG = $File;
    }
	if(defined($Description->{HEADER}))
    {
		print $CONFIG "# $Description->{HEADER}\n";
	}
	foreach(sort(keys(%{$Config})))
    {
        if ($_ =~ /^_/)
        {
            next;
        }
		if(defined($Description->{$_}))
        {
			print $CONFIG "\n# $Description->{$_}";
		}
		print $CONFIG "\n$_=$Config->{$_}\n";
	}
    if(ref($File) ne 'GLOB')
    {
        close($CONFIG);
    }

    return 1;
}

# Purpose: Load a configuration file
# Usage: ConfigLoader(/path/to/file, \%Config, \%OptionRegex, OnlyValidOptions?);
#  OptionRegeXhash can be available for only a select few of the config options
#  or skipped completely (by replacing it by undef).
#  If OnlyValidOptions is true it will cause ConfigLoader to skip options not in
#  the OptionRegexHash.
sub ConfigLoader
{
	my ($File, $ConfigHash, $OptionRegex, $OnlyValidOptions) = @_;

	open(my $CONFIG, '<', "$File") or do {
		warn(sprintf('Unable to read the configuration settings from %s: %s', $File, $!));
		return(0);
	};
	while(<$CONFIG>)
    {
		next if m/^\s*(#.*)?$/;
		next if ! m/=/;
		chomp;
		my $Option = $_;
		my $Value = $_;
		$Option =~ s/^\s*(\S+)\s*=.*/$1/;
		$Value =~ s/^\s*\S+\s*=\s*(.*)\s*/$1/;
		if($OnlyValidOptions)
        {
			if(!defined($OptionRegex->{$Option}))
            {
				warn("Unknown configuration option \"$Option\" (=$Value) in $File: Ignored.");
				next;
			}
		}
		if(!defined($Value))
        {
			warn("Empty value for option $Option in $File");
		}
		if(defined($OptionRegex) and defined($OptionRegex->{$Option}))
        {
			my $MustMatch = $OptionRegex->{$Option};
			if ($Value !~ /$MustMatch/)
            {
				warn("Invalid setting of $Option (=$Value) in the config file: Must match $OptionRegex->{Option}.");
				next;
			}
		}
		$ConfigHash->{$Option} = $Value;
	}
	close($CONFIG);

    return 1;
}

# Purpose: Write our config file
sub writeOutConfig
{
    my $out    = shift;
    my $header = "gpgpwd configuration file\n# You can also use 'gpgpwd config' to alter this file";

    if ($out)
    {
        $header = 'gpgpwd configuration';
        if ($config->{_override})
        {
            $header .= "\n# WARNING: Temporary overrides (via --set) are active";
        }
    }
    else
    {
        if ($config->{_override})
        {
            die('Fatal: Refusing to write configuration file when --set has been used'."\n");
        }
    }
    $out  //= $gpgpwdDir.'/gpgpwd.conf';


    return ConfigWriter($out, $config,
        {
            'HEADER'   => $header,
            'dataPath' => "The path to the password database file.\n# Use the special value DEFAULT for the default autodetected path",
            git        => "Configures gpgpwd's git mode. Default=auto\n# Valid settings:\n#\ttrue = always enable\n#\tauto = Enable if a .git directory exists in the same directory as the pwddb\n#\tfalse = never enable\n# The --git and --no-git parameters override this setting.",
            'clipboardMode' => "Configures which clipboard mode to use\n# Valid settings:\n# \t clipboard = (default) the normal clipboard\n#\t selection = the selection clipboard\n#\t both = use both\n#\tdisabled = disable automatic copying to the clipboard",
            'defaultPasswordLength' => "Sets the length of the default password generated by gpgpwd\n# An integer between 10-250. The special value 0 uses the gpg default\n# (which is currently 15, but may be increased later)\n#\n# You can override this temporarily with the /regenerate command\n# on the password prompt.",
            'defaultUsername' => "Sets the default username when adding new entries\n# Leave empty to not use any default value\n# Use \"gpgpwd config remove defaultUsername\" to remove the default\n# Requires storeUsernames=true",
            'storeUsernames' => "Enables or disables storage of usernames with gpgpwd\n# Valid settings:\n#\ttrue = enable username storage\n#\tfalse = disable username storage\n#\n# Any previously stored usernames will still be saved with this set to false\n# but they will not be retrieved or displayed by gpgpwd, and gpgpwd\n# will not prompt you to enter a username when adding new entries to the\n# database",
        });
}

# = = = = = = = = = = = = = = = = = = = = = = =
# Base initialization and command-line parsing
# = = = = = = = = = = = = = = = = = = = = = = =

# Purpose: Output our usage information and (optionally) exit
# Usage: usage(N);
#  If N is supplied, will pExit(N) after outputting.
sub usage
{
    my $exitValue = shift;

    print "\n";
    print 'Usage: '.basename($0).' [<get/set/remove> <name>]'."\n";
    print '  or : '.basename($0).' [<--options>] [<command> <parameters>]'."\n";
    print "\n";
    print "Options:\n";
    printHelp(''   , '--help'            , 'View this help screen');
    printHelp(''   , '--version'         , 'Display version information and exit');
    printHelp('-v' , '--verbose'         , 'Increase verbosity (can be supplied multiple times)');
    printHelp('-p' , '--password-file'   , 'Override the configured password file');
    printHelp('-s' , '--set [<key>=<value>]' , 'Temporarily override the configuration setting <key> to <value>');
    printHelp('-C' , '--no-clipboard'    , 'Disable copying of passwords to the clipboard when running under X');
    printHelp(''   , '--all'             , 'Return all possible results for "get", even very fuzzy results (default: return only the best results)');
    printHelp(''   , '--debuginfo'       , 'Display some information that can be useful for debugging');
    print "\n";
    print "Commands:\n";
    printHelp('' , 'get <name>'         , 'Get password for <name> (where <name> can be a perl-compatible regular expression)');
    printHelp('' , 'set <name>'         , 'Add or change password for <name>');
    printHelp('' , 'remove <name>'      , 'Remove the entry for <name>');
    printHelp('' , 'rename <old> <new>' , 'Rename the entry for <old> to <new>');
    printHelp('' , 'alias <to> <name>'  , 'Create an alias entry <name> that points to the entry for <to>');
    printHelp('' , 'batchadd <file>'    , 'Batch add passwords from a file, see the manpage for the file syntax');
    printHelp('' , 'config'             , 'Retrieve or set settings in the gpgpwd config file');
    printHelp('' , 'git'                , 'Perform git commands');
    printHelp('' , 'upgrade'            , 'Upgrade an old database file to the new format if needed');

    if (defined $exitValue)
    {
        pExit($exitValue);
    }

    return 1;
}

# Purpose: Print formatted --help output
# Usage: printHelp('-shortoption', '--longoption', 'description');
#  Description will be reformatted to fit within a normal terminal
sub printHelp
{
    # The short option
    my $short = shift,
    # The long option
    my $long = shift;
    # The description
    my $desc = shift;
    # The generated description that will be printed in the end
    my $GeneratedDesc;
    # The current line of the description
    my $currdesc = '';
    # The maximum length any line can be
    my $maxlen = 80;
    # The length the options take up
    my $optionlen = 23;
    # Check if the short/long are LONGER than optionlen, if so, we need
    # to do some additional magic to take up only $maxlen.
    # The +1 here is because we always add a space between them, no matter what
    if ((length($short) + length($long) + 1) > $optionlen)
    {
        $optionlen = length($short) + length($long) + 1;
    }
    # Split the description into lines
    foreach my $part (split(' ',$desc))
    {
        if(defined $GeneratedDesc)
        {
            if ((length($currdesc) + length($part) + 1 + 24) > $maxlen)
            {
                $GeneratedDesc .= "\n";
                $currdesc = '';
            }
            else
            {
                $currdesc .= ' ';
                $GeneratedDesc .= ' ';
            }
        }
        $currdesc .= $part;
        $GeneratedDesc .= $part;
    }
    # Something went wrong
    pDie('Option mismatch') if not $GeneratedDesc;
    # Print it all
    foreach my $description (split(/\n/,$GeneratedDesc))
    {
        printf "%-4s %-22s %s\n", $short,$long,$description;
        # Set short and long to '' to ensure we don't print the options twice
        $short = '';$long = '';
    }
    # Succeed
    return true;
}

# Purpose: Initialize environment if needed and load the config
sub initialize
{
    my $configOverride = shift;
    my $upgrade = 0;

    # Use XDG_CONFIG_HOME as per the XDG base directory specification
    my $XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME};
    $XDG_CONFIG_HOME  //= $ENV{HOME}.'/.config';
    # Our config dir
    $gpgpwdDir   = $XDG_CONFIG_HOME.'/gpgpwd';

    # The default location for the password database
    my $defaultStoragePath = $gpgpwdDir.'/gpgpwd.db';

    # Create our config dir if needed
    if (! -d $gpgpwdDir)
    {
        $upgrade = 1;
        # If a .gpgpwddb file exists then we are upgrading from gpgpwd <=0.5
        if (-e $ENV{HOME}.'/.gpgpwddb')
        {
            $upgrade = 2;
        }
        $| = 1;
        # Output an upgrade message if upgrading from gpgpwd <=0.5
        if ($upgrade == 2)
        {
            print 'gpgpwd is upgrading...';
        }
        mkpath($gpgpwdDir) or die("Failed to create config directory \"$gpgpwdDir\": $!\n");
        printv(V_LOG,'Created '.$gpgpwdDir."\n");
    }
    # The default config
    $config = { dataPath => 'DEFAULT', git => 'auto', 'clipboardMode' => 'clipboard', 'defaultPasswordLength' => 0, 'defaultUsername' => '', storeUsernames => 'true' };
    # If the config does not exist, write one
    if (!-e $gpgpwdDir.'/gpgpwd.conf')
    {
        print '.';
        # For compatibility reasons when upgrading from old gpgpwd versions,
        # set git to false
        if ($storagePath || -e $ENV{HOME}.'/.gpgpwddb')
        {
            $config->{git} = 'false';
        }
        writeOutConfig();
    }
    else
    {
        # Load the config file
        printv(V_INFO,'Loading config file at: '.$gpgpwdDir.'/gpgpwd.conf'."\n");
        ConfigLoader($gpgpwdDir.'/gpgpwd.conf',$config);
    }

    # Enable config overrides if needed
    foreach my $k (keys(%{$configOverride}))
    {
        $config->{$k} = $configOverride->{$k};
        $config->{_override} = 1;
    }

    if ($config->{defaultPasswordLength} !~ /^\d+$/ || ($config->{defaultPasswordLength} != 0 && $config->{defaultPasswordLength} < 10) || $config->{defaultPasswordLength} > 250)
    {
        warn('Warning: Invalid value for defaultPasswordLength: '.$config->{defaultPasswordLength}."\n");
        $config->{defaultPasswordLength} = 0;
    }

    # If the user hasn't supplied a --password-file, use the one set in the
    # config file
    if (!$storagePath)
    {
        if ($config->{dataPath} eq 'DEFAULT')
        {
            $storagePath = $defaultStoragePath;
        }
        else
        {
            $storagePath = $config->{dataPath};
        }
    }
    # Resolve all levels of symlinks
    if (-e $storagePath)
    {
        $storagePath = realpath($storagePath);
    }
    # If we still don't have any storagePath, something has gone very wrong
    if (!defined $storagePath)
    {
        die('Failed to resolve storage path');
    }
    # If we're upgrading, create a compatibility symlink to the old .gpgpwddb
    # to stay backwards compatible
    if ($upgrade && !-e $defaultStoragePath && -e $ENV{HOME}.'/.gpgpwddb')
    {
        print '.';
        move( $ENV{HOME}.'/.gpgpwddb', $defaultStoragePath ) or die("Failed to move old database ($ENV{HOME}/.gpgpwddb) to the new location at $defaultStoragePath: $!\n");
        # Compatibility symlink
        symlink($defaultStoragePath,  $ENV{HOME}.'/.gpgpwddb');
        printv(V_LOG,'Migrated from legacy database location'."\n");
    }
    # Parse and configure the clipboardMode set in the config file
    if (defined $config->{clipboardMode} && scalar(@clipboardTargets) == 0)
    {
        if ($config->{clipboardMode} eq 'both')
        {
            @clipboardTargets = qw(primary clipboard);
        }
        elsif($config->{clipboardMode} eq 'clipboard')
        {
            @clipboardTargets = qw(clipboard);
        }
        elsif($config->{clipboardMode} eq 'selection')
        {
            @clipboardTargets = qw(primary);
        }
        elsif($config->{clipboardMode} eq 'disabled')
        {
            @clipboardTargets = ();
        }
        else
        {
            warn('Warning: Unknown value for clipboardMode: '.$config->{clipboardMode}."\n");
        }
    }
    # Configure git according to the config
    if(defined $config->{git})
    {
        if ($config->{git} eq 'auto')
        {
            if (-d dirname($storagePath).'/.git')
            {
                $enableGit = true;
            }
        }
        elsif ($config->{git} eq 'true')
        {
            $enableGit = true;
        }
    }
    # Output a done message if we're upgrading from <=0.5
    if ($upgrade == 2)
    {
        print "done\n\n";
    }

    # Make sure we've got gpg or gpg2
    if (!InPath('gpg') && !InPath('gpg2'))
    {
        pDie('Failed to locate gpg which is required for gpgpwd to work, unable to continue'."\n");
    }
    elsif (
            # If we don't have gpg2 then we need to use gpg1
            !InPath('gpg2')
        )
    {
        # Note: If gpg isn't installed then we *know* gpg2 is installed,
        # because if neither is installed we refuse to run at all (test at the
        # top of main() )
        printv(V_INFO,'Using gpg instead of gpg2'."\n");
        $gpg[0] = 'gpg';
    }

    # Verify gpg keys on any upgrade
    verifyGPGKeys();

    # Verify that we're able to read and write to $storagePath, as well as
    # the directory $storagePath is located in (for backup files).
    if (-e $storagePath && ! -r $storagePath)
    {
        pDie($storagePath.': is not readable');
    }
    elsif (-e $storagePath && ! -w $storagePath)
    {
        pDie($storagePath.': is not writeable'."\n");
    }
    if ( ! -w dirname($storagePath) )
    {
        if (-e $storagePath)
        {
            warn('WARNING: '.dirname($storagePath).' is not writable, gpgpwd may be unable'."\n".'to create backup files!'."\n");
        }
        else
        {
            pDie(dirname($storagePath).': is not writeable'."\n");
        }
    }

    if(gpgAgentRunning)
    {
        # Add --no-tty to gpg's parameter list to reduce its output and
        # require a gpg-agent.
        push(@gpg,'--no-tty');
        # Explicitly enable the agent for gpg1
        if ($gpg[0] ne 'gpg2')
        {
            push(@gpg,'--use-agent');
        }
    }

    return 1;
}

# Purpose: Get the version of a shell utility
# Usage: version = getVersionFrom('command');
sub getVersionFrom
{
    if (!InPath($_[0]))
    {
        return 'not installed';
    }
    open3(my $in, my $out, my $err,@_);
    my $data;
    if ($out)
    {
        while(<$out>)
        {
            $data .= $_;
        }
    }
    if ($err)
    {
        while(<$err>)
        {
            $data .= $_;
        }
        close($err);
    }
    close($in);close($out);
    if(defined $data)
    {
        $data =~ s/^\D+(\S+).+/$1/s;
        return $data;
    }
    return;
}

# Purpose: Output some information useful for debugging and then exit
# Usage: debugInfo();
sub debugInfo
{
    my $configOverride = shift;
    # Don't require initialize() to succeed in order to get the debug output.
    $pDieUsepExit = 0;
    try
    {
        initialize($configOverride);
    }
    catch
    {
        warn('warning: initialize() failed: '.$_."\n");
    };
    # Don't require the agent to be working in order to get the debug output.
    try
    {
        autoInitGPGAgent();
    };

    print "gpgpwd version $VERSION\n";
    print "\n";
    my $pattern = "%-28s: %s\n";
    printf($pattern , 'Data file'         , $storagePath);
    printf($pattern , 'Config directory'  , $gpgpwdDir);
    printf($pattern , 'Data version'      , $dataVersion);
    printf($pattern , 'Perl version'      , sprintf('%vd', $^V));
    my $gpgV = getVersionFrom('gpg'       , '--version');
    if (InPath('gpg2') && -l InPath('gpg') && basename(readlink(InPath('gpg'))) eq 'gpg2')
    {
        $gpgV = '(symlinked to gpg2)';
    }
    printf($pattern , 'gpg version'       , $gpgV);
    printf($pattern , 'gpg2 version'      , getVersionFrom('gpg2'      , '--version'));
    printf($pattern , 'gpg-agent version' , getVersionFrom('gpg-agent' , '--version'));
    printf($pattern , 'xclip version'     , getVersionFrom('xclip'     , '-version'));
    printf($pattern , 'git version'       , getVersionFrom('git'       , '--version'));

    my $flags = '';
    foreach my $flag (qw(enableGit allMatches))
    {
        if (! eval('$'.$flag))
        {
            next;
        }
        $flags .= $flag.' ';
    }
    if ($gpg[0] eq 'gpg2')
    {
        $flags .= 'useGPGv2';
    }
    else
    {
        $flags .= 'useGPGv1';
    }
    if ($autoGPGAgent > -1)
    {
        $flags .= ' autoGPGAgent';
    }
    printf($pattern,'flags',$flags);

    my $conf = '';
    foreach my $c (sort keys %{$config})
    {
        $conf .= $c.'='.$config->{$c}.' ';
    }
    printf($pattern,'config',$conf);

    eval('use Digest::MD5;1;') or die('Failed to load Digest::MD5: '.$@."\n");
    my $md5 = Digest::MD5->new();
    my $self = $0;
    if(not -f $self)
    {
        $self = InPath($self);
    }
    open(my $f,'<',$self) or die('Failed to open self for reading: '.$!."\n");
    $md5->addfile($f);
    my $digest = $md5->hexdigest;
    close($f);
    printf($pattern,'MD5',$digest);

    pExit(0);
}

# Purpose: Main entry point
# Usage: main()
sub main
{
    $| = true;

    my $debugInfo = false;
    my $force     = false;
    my $configOverride = {};

    if (@ARGV == 0)
    {
        usage(0);
    }

    Getopt::Long::Configure('no_ignore_case','bundling');

    GetOptions(
        'help' => sub {
            usage(0);
        },
        'version' => sub
        {
            print 'gpgpwd version '.$VERSION."\n";
            pExit(0);
        },
        'v|verbose+' => \$verbosity,
        'p|password-file=s' => \$storagePath,
        'debuginfo' => \$debugInfo,
        'C|no-xclip|no-clipboard' => sub { $configOverride->{clipboardMode} = 'disabled'; },
        'force' => \$force,
        's|set=s' => sub {
            shift;
            my $kv = shift;
            my($key,$value) = split(/=/,$kv);
            $configOverride->{$key} = $value;
        },
        'G|no-git' => sub
        {
            die('Fatal: --no-git has been removed as of version 0.6. Use --set git=false instead.'."\n".
                'NOTE : Unlike --no-git, when using --set only the last option will be used.'."\n".
                '      (--no-git would previously override --git no matter where it appeared on'."\n".
                '       the command-line)'."\n");
        },
        'g|git|i|fast-git' => sub
        {
            print "Note: --git is deprecated as of version 0.6\n";
            print "      use the configuration setting git instead\n";
            print "      The parameter will be removed in gpgpwd 0.7\n";
            print "      Alternative: --set git=true\n\n";

            $configOverride->{git} = 'true';
        },
        'c|xclip-clipboard=s' => sub
        {
            shift;
            my $value = shift;

            print "Note: --xclip-clipboard is deprecated as of version 0.6\n";
            print "      use the configuration setting clipboardMode instead\n";
            print "      The parameter will be removed in gpgpwd 0.7\n";
            print "      Alternative: --set clipboardMode=$value\n";

            $configOverride->{'clipboardMode'} = $value;
        },
        'all' => \$allMatches,
        'r|R|t|T|require-agent|no-require-agent|try-require-agent|disable-agent' => sub
        {
            print 'Note: --*-agent parameters are no-ops as of version 0.4 The agent is required.'."\n";
        },
    ) or pDie('See --help for more information'."\n");

    if ($debugInfo)
    {
        debugInfo($configOverride);
    }

    initialize($configOverride);

    my $command = shift(@ARGV);

    if (!$command && !$debugInfo)
    {
        usage(0);
    }

    if ($command eq 'get')
    {
        getPWCommand(@ARGV);
    }
    elsif($command eq 'batchadd')
    {
        batchaddCommand(@ARGV);
    }
    elsif($command eq 'remove')
    {
        removePWCommand(@ARGV);
    }
    elsif($command eq 'set' || $command eq 'add' || $command eq 'change')
    {
        addPWCommand($command,@ARGV);
    }
    elsif($command eq 'rename')
    {
        renamePWCommand(@ARGV);
    }
    elsif($command eq 'upgrade')
    {
        upgradeDataV1toCurrent();
    }
    elsif($command eq 'git')
    {
        gitCommand($force,@ARGV);
    }
    elsif($command eq 'config')
    {
        configCommand(@ARGV);
    }
    elsif($command eq 'alias')
    {
        aliasCommand(@ARGV);
    }
    else
    {
        if (@ARGV == 0)
        {
            print "$command is an unknown command. Maybe you meant:\n";
            print basename($0).' get '.$command."\n";
            print '  or'."\n";
            print basename($0).' set '.$command."\n";
            print "\nSee ".basename($0)." --help for more information.\n";
            exit(103);
        }
        else
        {
            warn "Unknown command: $command\n";
            usage(102);
        }
    }
    pExit(0);
}

main();
__END__

=encoding utf8

=head1 NAME

gpgpwd - a command-line password manager based around GnuPG

=head1 SYNOPSIS

B<gpgpwd> [I<OPTIONS>] [I<COMMAND>] [I<PARAMETERS>]

=head1 DESCRIPTION

B<gpgpwd> is a terminal-based password manager. It stores a list of passwords
(and optionally the associated usernames) in a GnuPG encrypted file, and allows
you to easily retrieve, change and add to that file as needed. It also
generates random passwords that you can use, easily allowing you to have one
"master password" (for your gpg key), with one unique and random password for
each website or service you use, ensuring that your other accounts stay safe
even if one password gets leaked.

B<gpgpwd> can also utilize git(1) to allow you to easily synchronize your
passwords between different machines.

=head1 OPTIONS

=over

=item B<--help>

Display the help screen

=item B<--version>

Output the gpgpwd version and exit

=item B<-v, --verbose>

Increase gpgpwd verbosity. May be supplied multiple times to further increase
verbosity.

=item B<-p, --password-file> I<FILE>

Set the password file to I<FILE> instead of the default. This changes where
gpgpwd reads and writes the password database.

You may supply several --password-file arguments, but only the last one
will be used.

=item B<-s, --set> I<<key>>=I<<value>>

Temporarily sets I<<key>> to I<<value>> for the duration of a single gpgpwd
command. Using I<--set> temporarily disables the I<config> subcommand.

=item B<-C, --no-clipboard>

Disables copying of passwords to the clipboard. Depending on the clipboardMode
setting in the configuration file (which defaults to being enabled) gpgpwd will
copy passwords to the clipboard for easy pasting into password fields. When
this option is supplied it supresses this behaviour.

This is equivalent to providing I<--set clipboardMode=disabled>.

=item B<--all>

Return all posible results for a "get" request. This includes very fuzzy
results.  The default, which is to return only the best results, is usually
preferable to I<--all>.

=item B<--debuginfo>

Display some information useful for debugging.

=back

=head1 COMMANDS

=over

=item B<get> I<NAME>

Get the password for NAME. NAME can be a perl-compatible regular expression. If
no matches are found gpgpwd will attempt to perform a fuzzy search, to see if
something similar can be found (ie. to correct for typos).

If you want to retrieve your entire database you may simply supply . as NAME,
since it accepts a regular expression and . will match everything.

=item B<set> I<NAME>

Set (add or change) the password for NAME. You will be prompted interactively for the
password, and will be given a random password that you may use if you wish.

=item B<remove> I<NAME>

Remove the password for NAME from the database.

=item B<rename> I<OLDNAME> I<NEWNAME>

Rename the entry for OLDNAME to NEWNAME.

=item B<alias> I<TO> I<NAME>

This creates an alias (think symlink) to I<TO> named I<NAME>. That means that you
can enter 'get I<NAME>' and gpgpwd will respond with the entry for I<TO> instead.

=item B<batchadd> I<FILE>

Read and add a list of passwords from FILE. The format is simple:

    NAME PASSWORD

Everything up until the first bit of whitespace is taken to be the name,
and everything from the first non-whitespace character after that and
until the end of the line is taken to be the password. It will ignore
empty lines and lines starting with #.

=item B<git>

Provides certain git commands

=over

=item B<git init>

Initialize a gpgpwd git repository.

This can be used to keep a password database in sync between several different
computers.  This causes gpgpwd to I<git pull> before it writes any change to
the file, and I<git commit> and I<git push> after a change has been made. In
read mode it will I<git pull> after getting a password, if it detects that the
password file has changed after pulling, gpgpwd will process your get request
again, in case the password you wanted has changed.

=item B<git clone> I<git://compatible/url>

Clone an existing gpgpwd git repository from the git URL supplied (supports all
git URLs that your native git supports).

=item B<git initremote> I<remote?>

Initializes a fresh remote. Will push our git repository to the remote and
set our master branch to track it.

=item B<git pull>, B<git fetch>, B<git remote>, B<git branch>

Provides access to the git commands of the same name for the gpgpwd git
repository. See the manpages for the commands themselves for more
information. To pass command-line parameters to git you must first supply --, ie.
`gpgpwd fetch -- -v`

=back

=item B<upgrade>

Upgrades a database file using the old format (v1, used by gpgpwd 0.3 and
older) to the new format (v2, used by gpgpwd 0.4 and later).

=item B<config>

Manages the gpgpwd configuration file. Without any parameters it will output
the current configuration. You may also supply a single configuration key
to get the value for that key, or a key=value pair to set key to value.

=item B<config remove> I<entry>

Unsets a configuration value (which resets it to the default value)

=back

=head1 CONFIGURATION SETTINGS

=over

=item B<dataPath>

The path to the gpgpwd password database file. You can also override this with
I<--password-file> (ie. to temporarily operate on a different file).

=item B<git>

Configures the git mode for gpgpwd.

Set to I<true> to always enable.

Set to I<auto> to enable if the password file is in a directory contianing a
.git-directory. This is the default.

Set to I<false> to always disable.

=item B<clipboardMode>

By default gpgpwd will copy passwords to the clipboard (the one that pastes through
the usual I<ctrl+v> or "right click -> paste" means). With this you can change it.
It accepts the following parameters:

=over

=item I<clipboard>

The default, copies to the 'normal' clipboard. Paste with ie. I<ctrl+v>.

=item I<selection>

Copy to the 'selection' clipboard. Paste with ie. middle-click.

=item I<both>

Copy to both the 'normal' and 'selection' clipboards.

=item I<disabled>

Don't automatically copy passwords to the clipboard.

=back

=item B<defaultPasswordLength>

Sets the length of the default password generated by gpgpwd An integer between
10-250. The special value 0 uses the gpg default (which is currently 15, but
may be increased later)

You can override this temporarily with the /regenerate command on the password
prompt.

=item B<storeUsernames>

Enables or disables storing usernames. If this is set to "false" then gpgpwd
will not prompt for usernames, nor display usernames when an entry is retrieved.
If it is set to "true" then gpgpwd will both store and display usernames
for entries in the password database.

The default is true.

=back

=head1 EXAMPLES

=over

=item gpgpwd set test

Add a password for 'test' to the database, gpgpwd will prompt you for the password.

=item gpgpwd get test

Retrieve the password we just added.

=item gpgpwd remove test

Remove test from the adatabase

=item gpgpwd -g set testpwd

Add the password for testpwd to the database and commit+push the file using
git afterwards.

=item gpgpwd rename testpwd test-password

Rename 'testpwd' to 'test-password'.

=back

=head1 HELP/SUPPORT

If you need additional help, please visit the website at
L<http://random.zerodogg.org/gpgpwd>

=head1 BUGS AND LIMITATIONS

If you find a bug, please report it at L<http://random.zerodogg.org/gpgpwd/bugs>

Include the output of 'gpgpwd --debuginfo' in any bug report.

=head1 AUTHOR

B<gpgpwd> is written by Eskild Hustvedt I<<code aatt zerodogg d0t org>>

=head1 FILES

=over

=item I<XDG_CONFIG_HOME/gpgpwd>

The gpgpwd configuration and database directory. XDG_CONFIG_HOME is an
environment variable specified by the XDG Base Directory Specification. The
default value for XDG_CONFIG_HOME (and the value on most systems) is ~/.config,
so on most systems this will be I<~/.config/gpgpwd>.

=item I<XDG_CONFIG_HOME/gpgpwd/gpgpwd.db>

The default save location for the password database, overrideable by using
I<--password-file> and the dataPath configuration option.

=item I<XDG_CONFIG_HOME/gpgpwd/gpgpwd.conf>

The gpgpwd configraution file. Can also be managed using the 'gpgpwd config'
command.

=back

=head1 LICENSE AND COPYRIGHT

Copyright (C) Eskild Hustvedt 2012, 2013, 2014, 2015

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.
