3 # Copyright (C) 2012 - 2014 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
4 # This file is licensed under the GPLv2+. Please see COPYING for more information.
6 # modified by tobias girstmair for the hardpass project at github.com/girst/hardpass
7 # pass show displays a 'flat' list of all passwords, for easier scripting and uses a
8 # new variable $PASS_GPG_PHRASE which can handle the master password instead of gpg-
9 # agent, which is hard to script. Usage: PASS_GPG_PHRASE='Pa$$w0rd' pass show github/girst
11 umask "${PASSWORD_STORE_UMASK:-077}"
14 GPG_OPTS
=( "--quiet" "--yes" "--compress-algo=none" "--no-encrypt-to" "--passphrase" $PASS_GPG_PHRASE)
16 export GPG_TTY
="${GPG_TTY:-$(tty 2>/dev/null)}"
17 which gpg2
&>/dev
/null
&& GPG
="gpg2"
18 [[ -n $GPG_AGENT_INFO ||
$GPG == "gpg2" ]] && GPG_OPTS
+=( "--batch" "--use-agent" )
20 PREFIX
="${PASSWORD_STORE_DIR:-$HOME/.password-store}"
21 X_SELECTION
="${PASSWORD_STORE_X_SELECTION:-clipboard}"
22 CLIP_TIME
="${PASSWORD_STORE_CLIP_TIME:-45}"
24 export GIT_DIR
="${PASSWORD_STORE_GIT:-$PREFIX}/.git"
25 export GIT_WORK_TREE
="${PASSWORD_STORE_GIT:-$PREFIX}"
28 # BEGIN helper functions
32 [[ -d $GIT_DIR ]] ||
return
33 git add
"$1" ||
return
34 [[ -n $
(git status
--porcelain "$1") ]] ||
return
39 [[ -d $GIT_DIR ]] ||
return
40 [[ $
(git config
--bool --get pass.signcommits
) == "true" ]] && sign
="-S"
41 git commit
$sign -m "$1"
44 [[ -t 0 ]] ||
return 0
46 read -r -p "$1 [y/N] " response
47 [[ $response == [yY
] ]] ||
exit 1
53 set_gpg_recipients
() {
54 GPG_RECIPIENT_ARGS
=( )
57 if [[ -n $PASSWORD_STORE_KEY ]]; then
58 for gpg_id
in $PASSWORD_STORE_KEY; do
59 GPG_RECIPIENT_ARGS
+=( "-r" "$gpg_id" )
60 GPG_RECIPIENTS
+=( "$gpg_id" )
65 local current
="$PREFIX/$1"
66 while [[ $current != "$PREFIX" && ! -f $current/.gpg-id
]]; do
67 current
="${current%/*}"
69 current
="$current/.gpg-id"
71 if [[ ! -f $current ]]; then
74 $PROGRAM init your-gpg-id
75 before you may use the password store.
83 while read -r gpg_id
; do
84 GPG_RECIPIENT_ARGS
+=( "-r" "$gpg_id" )
85 GPG_RECIPIENTS
+=( "$gpg_id" )
90 local prev_gpg_recipients
="" gpg_keys
="" current_keys
="" index passfile
91 local groups
="$($GPG --list-config --with-colons | grep "^cfg
:group
:.
*")"
92 while read -r -d "" passfile
; do
93 local passfile_dir
="${passfile%/*}"
94 passfile_dir
="${passfile_dir#$PREFIX}"
95 passfile_dir
="${passfile_dir#/}"
96 local passfile_display
="${passfile#$PREFIX/}"
97 passfile_display
="${passfile_display%.gpg}"
98 local passfile_temp
="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
100 set_gpg_recipients
"$passfile_dir"
101 if [[ $prev_gpg_recipients != "${GPG_RECIPIENTS[*]}" ]]; then
102 for index
in "${!GPG_RECIPIENTS[@]}"; do
103 local group
="$(sed -n "s
/^cfg
:group
:$
(sed 's/[\/&]/\\&/g' <<<"${GPG_RECIPIENTS[$index]}"):\\(.
*\\)\$
/\\1/p
" <<<"$groups" | head -n 1)"
104 [[ -z $group ]] && continue
105 IFS
=";" eval 'GPG_RECIPIENTS+=( $group )' # http://unix.stackexchange.com/a/92190
106 unset GPG_RECIPIENTS
[$index]
108 gpg_keys
="$($GPG --list-keys --with-colons "${GPG_RECIPIENTS[@]}" | sed -n 's/sub:[^:]*:[^:]*:[^:]*:\([^:]*\):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[a-zA-Z]*e[a-zA-Z]*:.*/\1/p' | LC_ALL=C sort -u)"
110 current_keys
="$($GPG -v --no-secmem-warning --no-permission-warning --list-only --keyid-format long "$passfile" 2>&1 | cut -d ' ' -f 5 | LC_ALL=C sort -u)"
112 if [[ $gpg_keys != "$current_keys" ]]; then
113 echo "$passfile_display: reencrypting to ${gpg_keys//$'\n'/ }"
114 $GPG -d "${GPG_OPTS[@]}" "$passfile" | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}" &&
115 mv "$passfile_temp" "$passfile" ||
rm -f "$passfile_temp"
117 prev_gpg_recipients
="${GPG_RECIPIENTS[*]}"
118 done < <(find "$1" -iname '*.gpg' -print0)
120 check_sneaky_paths
() {
123 [[ $path =~
/\.\.$ ||
$path =~ ^\.\.
/ ||
$path =~
/\.\.
/ ||
$path =~ ^\.\.$
]] && die
"Error: You've attempted to pass a sneaky path to pass. Go home."
128 # END helper functions
132 # BEGIN platform definable
136 # This base64 business is because bash cannot store binary data in a shell
137 # variable. Specifically, it cannot store nulls nor (non-trivally) store
138 # trailing new lines.
139 local sleep_argv0
="password store sleep on display $DISPLAY"
140 pkill
-f "^$sleep_argv0" 2>/dev
/null
&& sleep 0.5
141 local before
="$(xclip -o -selection "$X_SELECTION" 2>/dev/null | base64)"
142 echo -n "$1" | xclip
-selection "$X_SELECTION" || die
"Error: Could not copy data to the clipboard"
144 ( exec -a "$sleep_argv0" sleep "$CLIP_TIME" )
145 local now
="$(xclip -o -selection "$X_SELECTION" | base64)"
146 [[ $now != $
(echo -n "$1" | base64
) ]] && before
="$now"
148 # It might be nice to programatically check to see if klipper exists,
149 # as well as checking for other common clipboard managers. But for now,
150 # this works fine -- if qdbus isn't there or if klipper isn't running,
151 # this essentially becomes a no-op.
153 # Clipboard managers frequently write their history out in plaintext,
155 qdbus org.kde.klipper
/klipper org.kde.klipper.klipper.clearClipboardHistory
&>/dev
/null
157 echo "$before" | base64
-d | xclip
-selection "$X_SELECTION"
158 ) 2>/dev
/null
& disown
159 echo "Copied $2 to clipboard. Will clear in $CLIP_TIME seconds."
162 [[ -n $SECURE_TMPDIR ]] && return
164 [[ $1 == "nowarn" ]] && warn
=0
165 local template
="$PROGRAM.XXXXXXXXXXXXX"
166 if [[ -d /dev
/shm
&& -w /dev
/shm
&& -x /dev
/shm
]]; then
167 SECURE_TMPDIR
="$(mktemp -d "/dev
/shm
/$template")"
169 rm -rf "$SECURE_TMPDIR"
171 trap remove_tmpfile INT TERM EXIT
173 [[ $warn -eq 1 ]] && yesno
"$(cat <<-_EOF
174 Your system does not have /dev/shm, which means that it may
175 be difficult to entirely erase the temporary non-encrypted
176 password file after editing.
178 Are you sure you would like to continue?
181 SECURE_TMPDIR
="$(mktemp -d "${TMPDIR:-/tmp}/$template")"
183 find "$SECURE_TMPDIR" -type f
-exec $SHRED {} +
184 rm -rf "$SECURE_TMPDIR"
186 trap shred_tmpfile INT TERM EXIT
195 # END platform definable
200 # BEGIN subcommand functions
205 ============================================
206 = pass: the standard unix password manager =
210 = Jason A. Donenfeld =
213 = http://www.passwordstore.org/ =
214 ============================================
223 $PROGRAM init [--path=subfolder,-p subfolder] gpg-id...
224 Initialize new password storage and use gpg-id for encryption.
225 Selectively reencrypt existing passwords using new gpg-id.
226 $PROGRAM [ls] [subfolder]
228 $PROGRAM find pass-names...
229 List passwords that match pass-names.
230 $PROGRAM [show] [--clip,-c] pass-name
231 Show existing password and optionally put it on the clipboard.
232 If put on the clipboard, it will be cleared in $CLIP_TIME seconds.
233 $PROGRAM grep search-string
234 Search for password files containing search-string when decrypted.
235 $PROGRAM insert [--echo,-e | --multiline,-m] [--force,-f] pass-name
236 Insert new password. Optionally, echo the password back to the console
237 during entry. Or, optionally, the entry may be multiline. Prompt before
238 overwriting existing password unless forced.
239 $PROGRAM edit pass-name
240 Insert a new password or edit an existing password using ${EDITOR:-vi}.
241 $PROGRAM generate [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length
242 Generate a new password of pass-length with optionally no symbols.
243 Optionally put it on the clipboard and clear board after $CLIP_TIME seconds.
244 Prompt before overwriting existing password unless forced.
245 Optionally replace only the first line of an existing file with a new password.
246 $PROGRAM rm [--recursive,-r] [--force,-f] pass-name
247 Remove existing password or directory, optionally forcefully.
248 $PROGRAM mv [--force,-f] old-path new-path
249 Renames or moves old-path to new-path, optionally forcefully, selectively reencrypting.
250 $PROGRAM cp [--force,-f] old-path new-path
251 Copies old-path to new-path, optionally forcefully, selectively reencrypting.
252 $PROGRAM git git-command-args...
253 If the password store is a git repository, execute a git command
254 specified by git-command-args.
258 Show version information.
260 More information may be found in the pass(1) man page.
265 local opts id_path
=""
266 opts
="$($GETOPT -o p: -l path: -n "$PROGRAM" -- "$@
")"
269 while true
; do case $1 in
270 -p|
--path) id_path
="$2"; shift 2 ;;
274 [[ $err -ne 0 ||
$# -lt 1 ]] && die
"Usage: $PROGRAM $COMMAND [--path=subfolder,-p subfolder] gpg-id..."
275 [[ -n $id_path ]] && check_sneaky_paths
"$id_path"
276 [[ -n $id_path && ! -d $PREFIX/$id_path && -e $PREFIX/$id_path ]] && die
"Error: $PREFIX/$id_path exists but is not a directory."
278 local gpg_id
="$PREFIX/$id_path/.gpg-id"
280 if [[ $# -eq 1 && -z $1 ]]; then
281 [[ ! -f "$gpg_id" ]] && die
"Error: $gpg_id does not exist and so cannot be removed."
282 rm -v -f "$gpg_id" ||
exit 1
283 if [[ -d $GIT_DIR ]]; then
285 git_commit
"Deinitialize ${gpg_id}."
287 rmdir -p "${gpg_id%/*}" 2>/dev
/null
289 mkdir
-v -p "$PREFIX/$id_path"
290 printf "%s\n" "$@" > "$gpg_id"
291 local id_print
="$(printf "%s
, " "$@
")"
292 echo "Password store initialized for ${id_print%, }"
293 git_add_file
"$gpg_id" "Set GPG id to ${id_print%, }."
296 reencrypt_path
"$PREFIX/$id_path"
297 git_add_file
"$PREFIX/$id_path" "Reencrypt password store using new GPG id ${id_print%, }."
302 opts
="$($GETOPT -o c -l clip -n "$PROGRAM" -- "$@
")"
305 while true
; do case $1 in
306 -c|
--clip) clip
=1; shift ;;
310 [[ $err -ne 0 ]] && die
"Usage: $PROGRAM $COMMAND [--clip,-c] [pass-name]"
313 local passfile
="$PREFIX/$path.gpg"
314 check_sneaky_paths
"$path"
315 if [[ -f $passfile ]]; then
316 if [[ $clip -eq 0 ]]; then
317 $GPG -d "${GPG_OPTS[@]}" "$passfile" ||
exit $?
319 local pass
="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | head -n 1)"
320 [[ -n $pass ]] ||
exit 1
323 elif [[ -d $PREFIX/$path ]]; then
324 if [[ -z $path ]]; then
325 echo "Password Store">/dev
/null
329 #tree -C -l --noreport "$PREFIX/$path" | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g' # remove .gpg at end of line, but keep colors
331 tree
-f -i -l --noreport "$PREFIX/$path" |
tail -n +2 |
grep "\.gpg"|
sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g' |
sed "s|$PREFIX/||g"
333 #find "$PREFIX/$path" -type f \( ! -iname ".*" \) not -iwholename '*.git*' | tail -n +2 | sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g' | sed 's|'$PREFIX'/||' # remove .gpg at end of line, but keep colors
334 elif [[ -z $path ]]; then
335 die
"Error: password store is empty. Try \"pass init\"."
337 die
"Error: $path is not in the password store."
342 [[ -z "$@" ]] && die
"Usage: $PROGRAM $COMMAND pass-names..."
343 IFS
="," eval 'echo "Search Terms: $*"'
344 local terms
="*$(printf '%s*|*' "$@
")"
345 tree
-n -f -i -l --noreport -P "${terms%|*}" --prune --matchdirs --ignore-case "$PREFIX" |
tail -n +2 |
sed 's/\.gpg\(\x1B\[[0-9]\+m\)\{0,1\}\( ->\|$\)/\1\2/g'
349 [[ $# -ne 1 ]] && die
"Usage: $PROGRAM $COMMAND search-string"
350 local search
="$1" passfile grepresults
351 while read -r -d "" passfile
; do
352 grepresults
="$($GPG -d "${GPG_OPTS[@]}" "$passfile" | grep --color=always "$search")"
353 [ $?
-ne 0 ] && continue
354 passfile
="${passfile%.gpg}"
355 passfile
="${passfile#$PREFIX/}"
356 local passfile_dir
="${passfile%/*}/"
357 [[ $passfile_dir == "${passfile}/" ]] && passfile_dir
=""
358 passfile
="${passfile##*/}"
359 printf "\e[94m%s\e[1m%s\e[0m:\n" "$passfile_dir" "$passfile"
361 done < <(find -L "$PREFIX" -iname '*.gpg' -print0)
365 local opts multiline
=0 noecho
=1 force
=0
366 opts
="$($GETOPT -o mef -l multiline,echo,force -n "$PROGRAM" -- "$@
")"
369 while true
; do case $1 in
370 -m|
--multiline) multiline
=1; shift ;;
371 -e|
--echo) noecho
=0; shift ;;
372 -f|
--force) force
=1; shift ;;
376 [[ $err -ne 0 ||
( $multiline -eq 1 && $noecho -eq 0 ) ||
$# -ne 1 ]] && die
"Usage: $PROGRAM $COMMAND [--echo,-e | --multiline,-m] [--force,-f] pass-name"
378 local passfile
="$PREFIX/$path.gpg"
379 check_sneaky_paths
"$path"
381 [[ $force -eq 0 && -e $passfile ]] && yesno
"An entry already exists for $path. Overwrite it?"
383 mkdir
-p -v "$PREFIX/$(dirname "$path")"
384 set_gpg_recipients
"$(dirname "$path")"
386 if [[ $multiline -eq 1 ]]; then
387 echo "Enter contents of $path and press Ctrl+D when finished:"
389 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}"
390 elif [[ $noecho -eq 1 ]]; then
391 local password password_again
393 read -r -p "Enter password for $path: " -s password ||
exit 1
395 read -r -p "Retype password for $path: " -s password_again ||
exit 1
397 if [[ $password == "$password_again" ]]; then
398 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
401 echo "Error: the entered passwords do not match."
406 read -r -p "Enter password for $path: " -e password
407 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$password"
409 git_add_file
"$passfile" "Add given password for $path to store."
413 [[ $# -ne 1 ]] && die
"Usage: $PROGRAM $COMMAND pass-name"
416 check_sneaky_paths
"$path"
417 mkdir
-p -v "$PREFIX/$(dirname "$path")"
418 set_gpg_recipients
"$(dirname "$path")"
419 local passfile
="$PREFIX/$path.gpg"
421 tmpdir
#Defines $SECURE_TMPDIR
422 local tmp_file
="$(mktemp -u "$SECURE_TMPDIR/XXXXX
")-${path//\//-}.txt"
426 if [[ -f $passfile ]]; then
427 $GPG -d -o "$tmp_file" "${GPG_OPTS[@]}" "$passfile" ||
exit 1
430 ${EDITOR:-vi} "$tmp_file"
431 [[ -f $tmp_file ]] || die
"New password not saved."
432 $GPG -d -o - "${GPG_OPTS[@]}" "$passfile" |
diff - "$tmp_file" &>/dev
/null
&& die
"Password unchanged."
433 while ! $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" "$tmp_file"; do
434 yesno
"GPG encryption failed. Would you like to try again?"
436 git_add_file
"$passfile" "$action password for $path using ${EDITOR:-vi}."
440 local opts clip
=0 force
=0 symbols
="-y" inplace
=0
441 opts
="$($GETOPT -o ncif -l no-symbols,clip,in-place,force -n "$PROGRAM" -- "$@
")"
444 while true
; do case $1 in
445 -n|
--no-symbols) symbols
=""; shift ;;
446 -c|
--clip) clip
=1; shift ;;
447 -f|
--force) force
=1; shift ;;
448 -i|
--in-place) inplace
=1; shift ;;
452 [[ $err -ne 0 ||
$# -ne 2 ||
( $force -eq 1 && $inplace -eq 1 ) ]] && die
"Usage: $PROGRAM $COMMAND [--no-symbols,-n] [--clip,-c] [--in-place,-i | --force,-f] pass-name pass-length"
455 check_sneaky_paths
"$path"
456 [[ ! $length =~ ^
[0-9]+$
]] && die
"Error: pass-length \"$length\" must be a number."
457 mkdir
-p -v "$PREFIX/$(dirname "$path")"
458 set_gpg_recipients
"$(dirname "$path")"
459 local passfile
="$PREFIX/$path.gpg"
461 [[ $inplace -eq 0 && $force -eq 0 && -e $passfile ]] && yesno
"An entry already exists for $path. Overwrite it?"
463 local pass
="$(pwgen -s $symbols $length 1)"
464 [[ -n $pass ]] ||
exit 1
465 if [[ $inplace -eq 0 ]]; then
466 $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile" "${GPG_OPTS[@]}" <<<"$pass"
468 local passfile_temp
="${passfile}.tmp.${RANDOM}.${RANDOM}.${RANDOM}.${RANDOM}.--"
469 if $GPG -d "${GPG_OPTS[@]}" "$passfile" | sed $'1c \\\n'"$(sed 's/[\/&]/\\&/g' <<<"$pass")"$'\n' | $GPG -e "${GPG_RECIPIENT_ARGS[@]}" -o "$passfile_temp" "${GPG_OPTS[@]}"; then
470 mv "$passfile_temp" "$passfile"
472 rm -f "$passfile_temp"
473 die
"Could not reencrypt new password."
477 [[ $inplace -eq 1 ]] && verb
="Replace"
478 git_add_file
"$passfile" "$verb generated password for ${path}."
480 if [[ $clip -eq 0 ]]; then
481 printf "\e[1m\e[37mThe generated password for \e[4m%s\e[24m is:\e[0m\n\e[1m\e[93m%s\e[0m\n" "$path" "$pass"
488 local opts recursive
="" force
=0
489 opts
="$($GETOPT -o rf -l recursive,force -n "$PROGRAM" -- "$@
")"
492 while true
; do case $1 in
493 -r|
--recursive) recursive
="-r"; shift ;;
494 -f|
--force) force
=1; shift ;;
497 [[ $# -ne 1 ]] && die
"Usage: $PROGRAM $COMMAND [--recursive,-r] [--force,-f] pass-name"
499 check_sneaky_paths
"$path"
501 local passfile
="$PREFIX/${path%/}"
502 if [[ ! -d $passfile ]]; then
503 passfile
="$PREFIX/$path.gpg"
504 [[ ! -f $passfile ]] && die
"Error: $path is not in the password store."
507 [[ $force -eq 1 ]] || yesno
"Are you sure you would like to delete $path?"
509 rm $recursive -f -v "$passfile"
510 if [[ -d $GIT_DIR && ! -e $passfile ]]; then
511 git
rm -qr "$passfile"
512 git_commit
"Remove $path from store."
514 rmdir -p "${passfile%/*}" 2>/dev
/null
518 local opts move
=1 force
=0
519 [[ $1 == "copy" ]] && move
=0
521 opts
="$($GETOPT -o f -l force -n "$PROGRAM" -- "$@
")"
524 while true
; do case $1 in
525 -f|
--force) force
=1; shift ;;
528 [[ $# -ne 2 ]] && die
"Usage: $PROGRAM $COMMAND [--force,-f] old-path new-path"
529 check_sneaky_paths
"$@"
530 local old_path
="$PREFIX/${1%/}"
531 local new_path
="$PREFIX/$2"
532 local old_dir
="$old_path"
534 if [[ ! -d $old_path ]]; then
535 old_dir
="${old_path%/*}"
536 old_path
="${old_path}.gpg"
537 [[ ! -f $old_path ]] && die
"Error: $1 is not in the password store."
540 mkdir
-p -v "${new_path%/*}"
541 [[ -d $old_path ||
-d $new_path ||
$new_path =~
/$
]] || new_path
="${new_path}.gpg"
543 local interactive
="-i"
544 [[ ! -t 0 ||
$force -eq 1 ]] && interactive
="-f"
546 if [[ $move -eq 1 ]]; then
547 mv $interactive -v "$old_path" "$new_path" ||
exit 1
548 [[ -e "$new_path" ]] && reencrypt_path
"$new_path"
550 if [[ -d $GIT_DIR && ! -e $old_path ]]; then
551 git
rm -qr "$old_path"
552 git_add_file
"$new_path" "Rename ${1} to ${2}."
554 rmdir -p "$old_dir" 2>/dev
/null
556 cp $interactive -r -v "$old_path" "$new_path" ||
exit 1
557 [[ -e "$new_path" ]] && reencrypt_path
"$new_path"
558 git_add_file
"$new_path" "Copy ${1} to ${2}."
563 if [[ $1 == "init" ]]; then
565 git_add_file
"$PREFIX" "Add current contents of password store."
567 echo '*.gpg diff=gpg' > "$PREFIX/.gitattributes"
568 git_add_file .gitattributes
"Configure git repository for gpg file diff."
569 git config
--local diff.gpg.binary true
570 git config
--local diff.gpg.textconv
"$GPG -d ${GPG_OPTS[*]}"
571 elif [[ -d $GIT_DIR ]]; then
572 tmpdir nowarn
#Defines $SECURE_TMPDIR. We don't warn, because at most, this only copies encrypted files.
573 export TMPDIR
="$SECURE_TMPDIR"
576 die
"Error: the password store is not a git repository. Try \"$PROGRAM git init\"."
581 # END subcommand functions
588 init
) shift; cmd_init
"$@" ;;
589 help|
--help) shift; cmd_usage
"$@" ;;
590 version|
--version) shift; cmd_version
"$@" ;;
591 show|
ls|list
) shift; cmd_show
"$@" ;;
592 find|search
) shift; cmd_find
"$@" ;;
593 grep) shift; cmd_grep
"$@" ;;
594 insert|add
) shift; cmd_insert
"$@" ;;
595 edit
) shift; cmd_edit
"$@" ;;
596 generate
) shift; cmd_generate
"$@" ;;
597 delete|
rm|remove
) shift; cmd_delete
"$@" ;;
598 rename|
mv) shift; cmd_copy_move
"move" "$@" ;;
599 copy|
cp) shift; cmd_copy_move
"copy" "$@" ;;
600 git
) shift; cmd_git
"$@" ;;
601 *) COMMAND
="show"; cmd_show
"$@" ;;