#!/bin/bash APP_SVNID='$HeadURL$ $LastChangedRevision$' . $(ade-config ade_share_prefix)/include/ade.sh || { echo "${0##*/}: INTERNAL ERROR: failed to load ade.sh" >&2; exit 3; } PIN_DEFINED_ERRORS=( "KEY=PIN_ERR_MISC; FMT=\"%s\"" ) # INVOCATION_PREFIX is used *exclusively* in error messages and will be either something # like 'todo' or 'pin -i todo'. Use of '-i ...' will append '-i ...' to INVOCATION_PREFIX. INVOCATION_PREFIX=${0##*/} RC_DIR=${PIN_DIR:-$HOME/.pin} DATABASES_FILE=$RC_DIR/databases.conf PASSPROG_ENTRY_ID="the-only-entry" pin() { local ERRSTACK_REF="$1"; shift local RC I_CREATED_HOST_LOCK_FILE_FLAG PROGNAME local -a NEW_DOLLAR_AT # Set ade options ade_register_error_types PIN_DEFINED_ERRORS # Defaults for options OPT_MODE=unset ade_get_progname "$ERRSTACK_REF" PROGNAME OPT_DATABASE_NAME=$PROGNAME # Register pin options ade_register_options "$ERRSTACK_REF" -o ieslP: --longoptions=init,edit,search,list,progname: --callback-template="pin_opt_handler_%s" || return $? ade_set_callbacks "$ERRSTACK_REF" pin_usage_help pin_version pin_paths || return $? # Process options ade_process_options "$ERRSTACK_REF" NEW_DOLLAR_AT "$@" || return $? set -- "${NEW_DOLLAR_AT[@]}" # Argument processing (delegated) # Sanity checks and derivations if [ "X$OPT_MODE" = Xunset ]; then ade_debug "$ERRSTACK_REF" 10 "pin: mode is unset; deriving ..." if [ $# = 0 ]; then OPT_MODE=edit else OPT_MODE=search fi fi [[ $OPT_MODE != unset ]] || ade_show_bad_usage "$ERRSTACK_REF" mkdir -p $RC_DIR # Locking if [[ $OPT_MODE =~ (init|edit|search) ]]; then lock_host "$ERRSTACK_REF" $RC_DIR/$OPT_DATABASE_NAME.host I_CREATED_HOST_LOCK_FILE_FLAG $RC_DIR/$OPT_DATABASE_NAME.pid || return $? ade_lock "$ERRSTACK_REF" $RC_DIR/$OPT_DATABASE_NAME.pid || { RC=$? unlock_host "$ERRSTACK_REF" $RC_DIR/$OPT_DATABASE_NAME.host $I_CREATED_HOST_LOCK_FILE_FLAG return $RC } fi # At this point we no longer care about I_CREATED_HOST_LOCK_FILE_FLAG because # the host lock file was either created by this process (in which case we # will want to later delete it) or it is stale but inherited by this process # (in which case we should also inherit the responsibility to later delete it). # Guts mkdir -p $RC_DIR [ -f $DATABASES_FILE ] || touch $DATABASES_FILE ade_debug "$ERRSTACK_REF" 10 "pin: calling [$OPT_MODE $*] ..." $OPT_MODE "$ERRSTACK_REF" "$@" || return $? # Unlocking if [[ $OPT_MODE =~ (init|edit|search) ]]; then ade_unlock "$ERRSTACK_REF" $RC_DIR/$OPT_DATABASE_NAME.pid # Note the 'true' meaning we created the lock file, even if it was # actually stale-but-inherited (see comment above). unlock_host "$ERRSTACK_REF" $RC_DIR/$OPT_DATABASE_NAME.host true fi return $ADE_OK } init() { local ERRSTACK_REF="$1"; shift local DEFAULT ACCEPTABLE_ANSWER_PROVIDED ENCRYPTED_FLAG KEY_ID ENTRY_TYPE # Sanity checks and derivations if get_database_parameters "$ERRSTACK_REF" $OPT_DATABASE_NAME ENCRYPTED_FLAG KEY_ID ENTRY_TYPE; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "already initialised (hint: run '$INVOCATION_PREFIX' to add some entries to the database)" return $ADE_FAIL fi ade_set_messaging_parameters "$ERRSTACK_REF" "stack=$ERRSTACK_REF" # Collect database parameters from user while :; do DEFAULT=n # Don't use 'read -p' because it aborts on EOF even before prompting. echo -n "encrypted (y/n) [$DEFAULT]: " if ! read ENCRYPTED_FLAG; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "missing response" return $ADE_FAIL fi ENCRYPTED_FLAG=${ENCRYPTED_FLAG:-$DEFAULT} case $ENCRYPTED_FLAG in y|yes|t|true|1) ENCRYPTED_FLAG=true; break ;; n|no|f|false|0) ENCRYPTED_FLAG=false; break ;; esac ade_warning "$ERRSTACK_REF" PIN_ERR_MISC "$ENCRYPTED_FLAG: invalid response" done if ! $ENCRYPTED_FLAG; then KEY_ID=unused else while :; do DEFAULT= #gpg --list-secret-keys # Don't use 'read -p' because it aborts on EOF even before prompting. echo -n "GPG key to use [$DEFAULT]: " if ! read KEY_ID; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "missing response" return $ADE_FAIL fi KEY_ID=${KEY_ID:-$DEFAULT} if gpg --list-secret-keys "$KEY_ID" >/dev/null 2>&1; then break fi ade_warning "$ERRSTACK_REF" PIN_ERR_MISC "$KEY_ID: invalid response" done fi while :; do DEFAULT=p # Don't use 'read -p' because it aborts on EOF even before prompting. echo -n "each entry is (l)ine, (p)aragraph or (u)nsearchable [$DEFAULT]: " if ! read ENTRY_TYPE; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "missing response" return $ADE_FAIL fi ENTRY_TYPE=${ENTRY_TYPE:-$DEFAULT} case $ENTRY_TYPE in l) ENTRY_TYPE=line; break ;; p) ENTRY_TYPE=paragraph; break ;; u) ENTRY_TYPE=unsearchable; break ;; esac ade_warning "$ERRSTACK_REF" PIN_ERR_MISC "$ENTRY_TYPE: invalid response; please try again" done ade_debug "$ERRSTACK_REF" 10 "init: ENCRYPTED_FLAG=$ENCRYPTED_FLAG, KEY_ID=$KEY_ID, ENTRY_TYPE=$ENTRY_TYPE" # Save the database parameters. set_database_parameters "$ERRSTACK_REF" "$OPT_DATABASE_NAME" "$ENCRYPTED_FLAG" "$KEY_ID" "$ENTRY_TYPE" # Configure the database. if $ENCRYPTED_FLAG; then umask 077 ade_debug "$ERRSTACK_REF" 10 "init: calling pass to initialise database ..." PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass init "$KEY_ID" > /dev/null ade_debug "$ERRSTACK_REF" 10 "init: calling [PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass insert --multiline $PASSPROG_ENTRY_ID] to add a dummy entry database in order to get-over noisy first real edit ..." echo | PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass insert --multiline $PASSPROG_ENTRY_ID > /dev/null else ade_debug "$ERRSTACK_REF" 10 "init: touching empty file ..." > $RC_DIR/$OPT_DATABASE_NAME fi return $ADE_OK } edit() { local ERRSTACK_REF="$1"; shift local ENCRYPTED_FLAG KEY_ID ENTRY_TYPE # Process arguments [ $# = 0 ] || ade_show_bad_usage "$ERRSTACK_REF" ade_debug "$ERRSTACK_REF" 10 "edit: sof" get_database_parameters "$ERRSTACK_REF" $OPT_DATABASE_NAME ENCRYPTED_FLAG KEY_ID ENTRY_TYPE || { RC=$? ade_error "$ERRSTACK_REF" PIN_ERR_MISC "not initialised (hint: run '$INVOCATION_PREFIX -i' first)" return $RC } ade_debug "$ERRSTACK_REF" 10 "edit: ENCRYPTED_FLAG=$ENCRYPTED_FLAG, KEY_ID=$KEY_ID, ENTRY_TYPE=$ENTRY_TYPE" if $ENCRYPTED_FLAG; then umask 077 # Avoid VIMINIT line causing an error. [ -f ~/.vimrc ] || touch ~/.vimrc # If the file is not changed then pass's exit code is non-zero. Cast that away. ade_debug "$ERRSTACK_REF" 10 "edit: calling [PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass edit $PASSPROG_ENTRY_ID] ..." VIMINIT=":source ~/.vimrc|:set noswapfile|:set nobackup|:set noundofile|:set viminfo=\"\"" PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass edit $PASSPROG_ENTRY_ID || true else ${EDITOR:-vi} $RC_DIR/$OPT_DATABASE_NAME fi return $ADE_OK } list() { local ERRSTACK_REF="$1"; shift local ENCRYPTED_FLAG KEY_ID ENTRY_TYPE DATABASE_NAMES DATABASE_NAME # Process arguments [ $# = 0 ] || ade_show_bad_usage "$ERRSTACK_REF" ade_debug "$ERRSTACK_REF" 10 "list: sof" DATABASE_NAMES=$(cut -f1 -d: $DATABASES_FILE | paste -d' ' -s) for DATABASE_NAME in $DATABASE_NAMES; do get_database_parameters "$ERRSTACK_REF" $DATABASE_NAME ENCRYPTED_FLAG KEY_ID ENTRY_TYPE || ade_internal "$ERRSTACK_REF" "list: $DATABASE_NAME: failed to get parameters for defined database (how is this possible?)" ade_debug "$ERRSTACK_REF" 10 "list: DATABASE_NAME=$DATABASE_NAME, ENCRYPTED_FLAG=$ENCRYPTED_FLAG, KEY_ID=$KEY_ID, ENTRY_TYPE=$ENTRY_TYPE" echo "$DATABASE_NAME" done return $ADE_OK } search() { local ERRSTACK_REF="$1"; shift local PASSPROG_OPTS ENCRYPTED_FLAG KEY_ID ENTRY_TYPE ade_debug "$ERRSTACK_REF" 10 "search: sof" # Process arguments [ $# = 1 ] || ade_show_bad_usage "$ERRSTACK_REF" ade_debug "$ERRSTACK_REF" 10 "search: search term is [$*]" # Sanity checks and derivations get_database_parameters "$ERRSTACK_REF" $OPT_DATABASE_NAME ENCRYPTED_FLAG KEY_ID ENTRY_TYPE || { RC=$? ade_error "$ERRSTACK_REF" PIN_ERR_MISC "not initialised (hint: run '$INVOCATION_PREFIX' first)" return $RC } ade_debug "$ERRSTACK_REF" 10 "search: ENCRYPTED_FLAG=$ENCRYPTED_FLAG, KEY_ID=$KEY_ID, ENTRY_TYPE=$ENTRY_TYPE" if [ $ENTRY_TYPE = unsearchable ]; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "entries in this database are unsearchable" return $ADE_FAIL fi # Guts if $ENCRYPTED_FLAG; then umask 077 PASSWORD_STORE_DIR=$RC_DIR/$OPT_DATABASE_NAME pass $PASSPROG_OPTS show $PASSPROG_ENTRY_ID else cat $RC_DIR/$OPT_DATABASE_NAME fi | case $ENTRY_TYPE in line) grep "$1" ;; paragraph) grep_stanza "$ERRSTACK_REF" "$1" ;; esac return $ADE_OK } grep_stanza() { local ERRSTACK_REF="$1"; shift local PATTERN [ $# = 1 ] || ade_internal "$ERRSTACK_REF" "grep_stanza: $#: bad argument count" PATTERN="$1" perl -e '$/ = ""; $f=1; while () { if (/$ARGV[0]/i) { print; $f=0; } } exit $f' -- "$PATTERN" return $ADE_OK } get_database_parameters() { local ERRSTACK_REF="$1"; shift local LOCAL_ENCRYPTED_FLAG LOCAL_KEY_ID LOCAL_ENTRY_TYPE DATABASE_NAME local ENCRYPTED_FLAG_REF KEY_ID_REF ENTRY_TYPE_REF [ $# = 4 ] || ade_internal "$ERRSTACK_REF" "get_database_parameters: $#: bad argument count" DATABASE_NAME=$1 ENCRYPTED_FLAG_REF=$2 KEY_ID_REF=$3 ENTRY_TYPE_REF=$4 eval "$(sed -n "s/^$DATABASE_NAME:\\(true\\|false\\):\\([^:]*\\):\\(line\\|paragraph\\|unsearchable\\)\$/LOCAL_ENCRYPTED_FLAG=\"\\1\";LOCAL_KEY_ID=\"\\2\";LOCAL_ENTRY_TYPE=\"\\3\"/p" $DATABASES_FILE)" ade_debug "$ERRSTACK_REF" 10 "get_database_parameters: LOCAL_ENCRYPTED_FLAG=$LOCAL_ENCRYPTED_FLAG, LOCAL_KEY_ID=$LOCAL_KEY_ID, LOCAL_ENTRY_TYPE=$LOCAL_ENTRY_TYPE" if [ ! -f $RC_DIR/$DATABASE_NAME -a ! -d $RC_DIR/$DATABASE_NAME ]; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "database does not exist! (hint: do you need to run '$INVOCATION_PREFIX -i'?)" return $ADE_FAIL fi if ! [[ $LOCAL_ENCRYPTED_FLAG =~ ^(true|false)$ ]]; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "$LOCAL_ENCRYPTED_FLAG: not 'true' or 'false' (hint: edit $DATABASES_FILE and correct this)" return $ADE_FAIL fi if ! [[ $LOCAL_ENTRY_TYPE =~ ^(line|paragraph|unsearchable)$ ]]; then ade_error "$ERRSTACK_REF" PIN_ERR_MISC "$LOCAL_ENTRY_TYPE: not 'line', 'paragraph' or 'unsearchable' (hint: edit $DATABASES_FILE and correct this)" return $ADE_FAIL fi eval "$ENCRYPTED_FLAG_REF=\"\$LOCAL_ENCRYPTED_FLAG\"" eval "$KEY_ID_REF=\"\$LOCAL_KEY_ID\"" eval "$ENTRY_TYPE_REF=\"\$LOCAL_ENTRY_TYPE\"" return $ADE_OK } set_database_parameters() { local ERRSTACK_REF="$1"; shift local DATABASE_NAME ENCRYPTED_FLAG KEY_ID ENTRY_TYPE [ $# = 4 ] || ade_internal "$ERRSTACK_REF" "set_database_parameters: $#: bad argument count" DATABASE_NAME=$1 ENCRYPTED_FLAG=$2 KEY_ID=$3 ENTRY_TYPE=$4 echo "$DATABASE_NAME:$ENCRYPTED_FLAG:$KEY_ID:$ENTRY_TYPE" >> $DATABASES_FILE return $ADE_OK } lock_host() { local ERRSTACK_REF="$1"; shift local HOST_LOCK_FILE I_CREATED_HOST_LOCK_FILE_FLAG_REF PID_LOCK_FILE [ $# = 3 ] || ade_internal "$ERRSTACK_REF" "lock_host: $#: bad argument count" HOST_LOCK_FILE=$1 I_CREATED_HOST_LOCK_FILE_FLAG_REF=$2 PID_LOCK_FILE=$3 UNAMEN=${PIN_UNAMEN:-$(uname -n)} # accept PIN_UNAMEN from environment to allow automated testing of locking. # None of this is atomic; it should be. if [ -f $HOST_LOCK_FILE ] && [ "X$(cat $HOST_LOCK_FILE 2>/dev/null)" != "X$UNAMEN" ]; then eval "$I_CREATED_HOST_LOCK_FILE_FLAG_REF=irrelevant" ade_error "$ERRSTACK_REF" PIN_ERR_MISC "lock held by PID $(cat $HOST_LOCK_FILE):$(cat $PID_LOCK_FILE 2>/dev/null)" return $ADE_FAIL fi if [ ! -f $HOST_LOCK_FILE ]; then ade_debug "$ERRSTACK_REF" 10 "lock_host: creating host lock file ..." ade_register_temp_file "$ERRSTACK_REF" $HOST_LOCK_FILE echo $UNAMEN > $HOST_LOCK_FILE eval "$I_CREATED_HOST_LOCK_FILE_FLAG_REF=true" else # Either there is a pin process running on this host (which we'll only # determine when we create the pid lock) or there host lock file is stale # (which we'll only # determine when we create the pid lock). If # later we discover it is not stale then we won't want to remove the # host lock file. ade_debug "$ERRSTACK_REF" 10 "lock_host: not creating host lock file ..." eval "$I_CREATED_HOST_LOCK_FILE_FLAG_REF=false" fi return $ADE_OK } unlock_host() { local ERRSTACK_REF="$1"; shift local HOST_LOCK_FILE I_CREATED_HOST_LOCK_FILE_FLAG HOST_LOCK_FILE=$1 I_CREATED_HOST_LOCK_FILE_FLAG=$2 UNAMEN=${PIN_UNAMEN:-$(uname -n)} # accept PIN_UNAMEN from environment to allow automated testing of locking. if $I_CREATED_HOST_LOCK_FILE_FLAG; then ade_debug "$ERRSTACK_REF" 10 "unlock_host: deleting host lock file ..." rm -f $HOST_LOCK_FILE ade_deregister_temp_file "$ERRSTACK_REF" $HOST_LOCK_FILE fi return $ADE_OK } pin_opt_handler_i() { local ERRSTACK_REF="$1"; shift OPT_MODE=init return $ADE_OK } pin_opt_handler_e() { local ERRSTACK_REF="$1"; shift OPT_MODE=edit return $ADE_OK } pin_opt_handler_s() { local ERRSTACK_REF="$1"; shift OPT_MODE=search return $ADE_OK } pin_opt_handler_l() { local ERRSTACK_REF="$1"; shift OPT_MODE=list return $ADE_OK } pin_opt_handler_P() { local ERRSTACK_REF="$1"; shift OPT_DATABASE_NAME=$1 INVOCATION_PREFIX+=" -P $OPT_DATABASE_NAME" return $ADE_OK } pin_opt_handler_init() { pin_opt_handler_i "$@" } pin_opt_handler_edit() { pin_opt_handler_e "$@" } pin_opt_handler_search() { pin_opt_handler_s "$@" } pin_opt_handler_list() { pin_opt_handler_l "$@" } pin_opt_handler_progname() { local ERRSTACK_REF="$1"; shift OPT_DATABASE_NAME=$1 INVOCATION_PREFIX+=" --progname=$OPT_DATABASE_NAME" return $ADE_OK } pin_usage_help() { local ERRSTACK_REF="$1"; shift local USAGE_TEXT_SHORT_REF="$1"; shift local USAGE_TEXT_LONG_REF="$1"; shift eval "$USAGE_TEXT_SHORT_REF=\"\"" eval "$USAGE_TEXT_LONG_REF=\"\ -i | --init initialise database -e | --edit edit database -s | --search search database -l | --list list databases -P | --progname use 's database\"" return $ADE_OK } pin_paths() { local ERRSTACK_REF="$1"; shift local PATHLIST_REF=$1; shift eval "$PATHLIST_REF=\"\"" return $ADE_OK } pin_version() { local ERRSTACK_REF="$1"; shift local VERSION_REF=$1; shift ade_extract_version "$ERRSTACK_REF" "$APP_SVNID" "$VERSION_REF" return $ADE_OK } ade_main pin "$@"