diff options
-rw-r--r-- | cli/bash-completion.sh | 74 | ||||
-rwxr-xr-x | cli/icevault | 89 |
2 files changed, 77 insertions, 86 deletions
diff --git a/cli/bash-completion.sh b/cli/bash-completion.sh index bbef124..e1c6424 100644 --- a/cli/bash-completion.sh +++ b/cli/bash-completion.sh @@ -20,15 +20,53 @@ _icevault() { COMPREPLY=() declare -a files - local opts="--debug -h --help -p --show-passwords -s --socket= --version" - local args="fill insert dump clip edit ls" - local OPTIND IFS - # find out if our $cur is an optional argument or a command - for (( OPTIND = 1; OPTIND < cword; OPTIND++ )); do + local cmd firstopt=2 opts nargs + if [ $cword -eq 1 -a -z "$cur" ] || [[ "${words[1]}" =~ :// ]] || [[ "${words[1]}" =~ ^- ]]; then + firstopt=1 + cmd=fill # default command + elif [ $cword -gt 1 ]; then + cmd="${words[1]}" + fi + + local commands='fill clip cp dump edit git insert ls mv reencrypt rm' + case "$cmd" in + fill) opts='-f --force -p --show-passwords -s --socket='; nargs=1;; + clip) opts=; nargs=1 ;; + cp) opts='-f --force'; nargs=2;; + dump) opts='-p --show-passwords'; nargs=1;; + edit) opts=; nargs=1 ;; + git) opts= ;; + insert) opts='-f --force -s --socket='; nargs=0;; + ls) opts='-0 --zero -r --recursive'; nargs=-1;; + mv) opts='-f --force'; nargs=2;; + reencrypt) opts=; nargs=-1 ;; + rm) opts='-f --force -r --recursive'; nargs=-1;; + esac + opts="${opts:+$opts }--debug -? --help --version" + + if [ "$cmd" = git ]; then + # use bash completion for git by ignoring the first word ('icevault') + [ -f /usr/share/bash-completion/completions/git ] && . /usr/share/bash-completion/completions/git || return + COMP_CWORD=$(( $COMP_CWORD - 1 )) + COMP_WORDS=( "${COMP_WORDS[@]:1}" ) + local GIT_DIR GIT_WORK_TREE # fetch store and git-dir from the config-file + GIT_WORK_TREE=$(sed -n '/^store\s*=\s*/{s///p;q}' "${XDG_CONFIG_HOME:-$HOME/.config}/icevault") + GIT_WORK_TREE=$( echo "${GIT_WORK_TREE:-icevault}" | sed -re "s@^~/@$HOME@; t; s@^/@/@; t; s@^@${XDG_DATA_HOME:-$HOME/.local/share}/@" ) + GIT_DIR=$(sed -n '/^git-dir\s*=\s*/{s///p;q}' "${XDG_CONFIG_HOME:-$HOME/.config}/icevault") + GIT_DIR=$( echo "${GIT_DIR:-.git}" | sed -re "s@^~/@$HOME@; t; s@^/@/@; t; s@^@$GIT_WORK_TREE/@" ) + export GIT_DIR GIT_WORK_TREE + _git + return + fi + + local OPTIND socket + # find out if our $cur is an optional argument or not + for (( OPTIND = firstopt; OPTIND < cword; OPTIND++ )); do case "${words[OPTIND]}" in - -s) (( OPTIND++ ));; + -s) (( OPTIND++ )); [ $OPTIND -lt $cword ] && socket="${words[OPTIND]}";; --) break;; + --socket=*) [ $OPTIND -lt $cword ] && socket="${words[OPTIND]#--socket=}";; -*) ;; *) break;; esac @@ -45,20 +83,19 @@ _icevault() { [ -d "$p" -o -S "$p" ] && COMPREPLY+=( "$p" ) done return + elif [[ $OPTIND -eq $cword && "$cur" =~ ^- ]]; then + COMPREPLY+=( $(compgen -W "$opts" -- "$cur") ) + [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]}" = '--socket=' ] && compopt -o nospace + return fi - # complete options and commands - if [ $OPTIND -eq $cword -a "$cur" ]; then - COMPREPLY+=( $(compgen -W "$opts $args" -- "$cur") ) - [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]}" = --socket= ] && compopt -o nospace - fi - [ $(($OPTIND + 1)) -lt $cword ] && return - local trim= - if [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = insert ]; then + if [ "$cmd" = fill -a \( $OPTIND -eq $cword -o $(( $OPTIND + 1)) -eq $cword \) -a -z "$cur" ]; then + cur="$(icevault _geturi ${socket:+--socket="$socket"})"/ # get URI from webpage + elif [ -z "$cmd" -a "$cword" -eq 1 -a "$cur" ]; then + # autocomplete command + COMPREPLY+=( $(compgen -W "$commands" -- "$cur") ) return - elif [ -z "$cur" -a $OPTIND -eq $cword ] || [ -z "$cur" -a $(($OPTIND + 1)) -eq $cword -a "${words[OPTIND]}" = fill ]; then - cur="$(icevault _geturi)"/ # get URI from webpage else cur=$(dequote "$cur") # trim words with : or = in $COMP_WORDBREAKS; see __ltrim_colon_completions @@ -72,13 +109,14 @@ _icevault() { fi local uri - if [[ $OPTIND -eq $cword || ! "${words[OPTIND]}" =~ :// ]]; then + # autocomplete with identities as long as we don't exeed $nargs + if [ $nargs -lt 0 -o $(($cword - $OPTIND + 1)) -le $nargs ]; then compopt -o filenames -o noquote while read -r -d $'\0' uri; do # quote manually (so we don't quote the : and =) uri=$( echo "${uri#$trim}" | sed "s/[][\\{}*?~<>;'\"|&()\!$\` \t]/\\\\&/g" ) COMPREPLY+=( "$uri" ) - done < <(icevault -0 _complete "$cur") + done < <(icevault _complete -0 "$cur" 2>/dev/null) [ "${#COMPREPLY[@]}" -eq 1 -a "${COMPREPLY[0]: -1:1}" = / ] && compopt -o nospace return 0 fi diff --git a/cli/icevault b/cli/icevault index be7902f..1f662bf 100755 --- a/cli/icevault +++ b/cli/icevault @@ -308,73 +308,6 @@ sub matches($) { return @matches; } -# Get all identities with the given $prefix. If there are multiple -# matches and $all is false, limit the output to one depth more than the -# longuest common prefix. -sub complete($;$) { - my $prefix = shift // ''; - my $all = shift; - - my ($s, $h, $i); # extract URI components from the prefix - if ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\/([^\P{Print}\/]*)\z/) { - ($s, $h, $i) = ($1, $2, $3); - } elsif ($prefix =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}\/]*)\z/) { - ($s, $h, $i) = ($1, $2, undef); - } elsif ($prefix =~ /\A([A-Za-z0-9-]*)(:\/?)?\z/) { - ($s, $h, $i) = ($1, (defined $2 ? '' : undef), undef); - } else { - exit; - } - - # construct a glob pattern with these URI components - my ($gs, $gh, $gi) = ($s, $h, $i); - s/([\\\[\]\{\}\*\?\~])/\\$1/g foreach grep defined, ($gs, $gh, $gi); # escape meta chars - - # add trailing wildcards - $gs .= '*' if defined $gs and !defined $gh; - $gh .= '*' if defined $gh and !defined $gi; - $gi .= '*' if defined $gi; - - # construct regexp to extract the URI compontents of the matching URIs - my ($ps, $ph, $pi) = ($s, $h, $i); - $ps = defined $h ? qr/(?<s>\Q$s\E)/ : (defined $s and $s ne '') ? qr/(?<s>\Q$s\E[A-Za-z0-9-]*)/ : qr/(?<s>[A-Za-z0-9-]+)/; - $ph = defined $i ? qr/(?<h>\Q$h\E)/ : (defined $h and $h ne '') ? qr/(?<h>\Q$h\E[^\P{Graph}\/]*)/ : qr/(?<h>[^\P{Graph}\/]+)/; - $pi = (defined $i and $i ne '') ? qr/(?<i>\Q$i\E[^\P{Print}\/]*)/ : qr/(?<i>[^\P{Print}\/]+)/; - - my $store = $LOCALE->encode($CONFIG{store}); - my $template = $LOCALE->encode($CONFIG{template}); - $template =~ s/(\%.)([^\%]*)\z/$1.quotemeta($2)/e; - $template =~ s{(.*?)\%(.)}{$2 eq '%' ? '%' : - $2 eq 's' ? quotemeta($1).$ps : - $2 eq 'h' ? quotemeta($1).$ph : - $2 eq 'i' ? quotemeta($1).$pi : - die "Invalid placeholder %$1"}ge; - my $pattern = qr/\A\Q$store\/\E$template\z/; - myprintf \*STDERR, "Using regexp C<%s>\n", "$pattern" if $CONFIG{debug}; - - my @matches; - foreach my $filename (myglob($gs, $gh, $gi)) { - next unless -f $filename; - $LOCALE->decode($filename) =~ $pattern or die "$filename doesn't match $pattern"; - push @matches, "$+{s}://$+{h}/$+{i}"; - } - return @matches if $all or $#matches < 1; - - # find the longest common prefix to determine the depth level of completion - $matches[0] =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\// or die; - ($s, $h) = ($1, $2); - - if (all { /\A\Q$s\E:\/\/\Q$h\E\// } @matches) { # common host: list all ids - } elsif (all { /\A\Q$s\E:\/\// } @matches) { # common scheme: list only hosts - s#/[^\P{Print}\/]+\z#/# foreach @matches; - } else { # no common scheme: list only schemes - s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z#://# foreach @matches; - } - - my %matches = map {( $_ => 1 )} @matches; - return sort keys %matches; -} - # Redact passwords, unless $CONFIG{'show-passwords'} is set. sub safeValue($;$) { my $field = shift; @@ -776,15 +709,35 @@ sub getopts(%) { if ($COMMAND eq '_complete') { # used internaly for auto-completion GetOptions(\%CONFIG, qw/zero|0/) or die; + loadConfig(); die unless $#ARGV == 0; + + $CONFIG{recursive} = 1; + my @matches = grep defined, + map { $_->{id} =~ /\A\Q$ARGV[0]\E/ ? $_->{id} : undef} + list( $ARGV[0] =~ /\A(.*\/)/ ? $1 : $ARGV[0] =~ /\A([A-Za-z0-9-]+):\z/ ? "$1://" : () ); + + # find the longest common prefix to determine the depth level of completion + $matches[0] =~ /\A([A-Za-z0-9-]+):\/\/([^\P{Graph}:\/]+(?::\d+)?)\// or die; + my ($s, $h) = ($1, $2); + + if (all { /\A\Q$s\E:\/\/\Q$h\E\// } @matches) { # common host: list all ids + } elsif (all { /\A\Q$s\E:\/\// } @matches) { # common scheme: list only hosts + s#/[^\P{Print}\/]+\z#/# foreach @matches; + } else { # no common scheme: list only schemes + s#://[^\P{Graph}\/]+/[^\P{Print}\/]+\z#://# foreach @matches; + } + my %matches = map {($_ => 1)} grep defined, @matches; + my $delim = $CONFIG{zero} ? "\0" : "\n"; - print $LOCALE->encode($_), $delim foreach complete(shift @ARGV); + print $LOCALE->encode($_), $delim foreach sort keys %matches; exit; } elsif ($COMMAND eq '_geturi') { # used internaly for auto-completion GetOptions(\%CONFIG, qw/socket|s=s/) or die; + loadConfig(); die if @ARGV; print $LOCALE->encode( &connect($CONFIG{socket}) ), "\n"; sendCommand 'QUIT'; |