#!/usr/bin/env bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2021 Ayush Agarwal # # vim: set expandtab ts=2 sw=2 sts=2: # # tessen - a data selection interface for pass and gopass on Wayland # ------------------------------------------------------------------------------ # don't leak password data if debug mode is enabled set +x # GLOBAL VARIABLES readonly tsn_version="2.0.0" declare pass_backend dmenu_backend tsn_action tsn_config declare -a dmenu_backend_opts tmp_opts declare tsn_userkey tsn_urlkey tsn_autokey tsn_delay tsn_web_browser # show both actions, 'autotype' and 'copy', to choose from by default tsn_action="default" tsn_otp=false # initialize default values for keys tsn_userkey="user" tsn_urlkey="url" tsn_autokey="autotype" tsn_delay=100 # initialize the default location of the config file tsn_config="${XDG_CONFIG_HOME:-$HOME/.config}"/tessen/config # variables with sensitive data which will be manually unset using _clear declare tsn_passfile tsn_username tsn_password tsn_url tsn_autotype chosen_key declare -A tsn_passdata # FIRST MENU: generate a list of pass files, let the user select one get_pass_files() { local tmp_prefix="${PASSWORD_STORE_DIR:-$HOME/.password-store}" if ! [[ -d "$tmp_prefix" ]]; then _die "password store directory not found" fi local -a tmp_pass_files # temporarily enable globbing, get the list of all gpg files recursively, # remove PASSWORD_STORE_DIR from the file names, and remove the '.gpg' suffix shopt -s nullglob globstar tmp_pass_files=("$tmp_prefix"/**/*.gpg) tmp_pass_files=("${tmp_pass_files[@]#"$tmp_prefix"/}") tmp_pass_files=("${tmp_pass_files[@]%.gpg}") shopt -u nullglob globstar tsn_passfile="$(printf "%s\n" "${tmp_pass_files[@]}" \ | "$dmenu_backend" "${dmenu_backend_opts[@]}")" if ! [[ -s "$tmp_prefix/$tsn_passfile".gpg ]]; then _die fi unset -v tmp_pass_files tmp_prefix } # FIRST MENU: generate a list of gopass files, let the user select one # this function feels like a hack to me. ideally, the issues that led to this # hack should be fixed in gopass but if anyone has any suggestions about making # this function better, please raise a PR get_gopass_files() { local line path_files file mount_name tmp_tsn_passfile local -A tmp_gopass_files local -a mount_name_arr # this feels like a hack and it's dependent on the output of `gopass config` # # still, this block of code saves us from using coreutils # # to be clear, this is needed to confirm whether the filename entered in the # dmenu actually exists or not because dmenu backends will happily print the # input received from a user even if that input doesn't exist in the menu # presented to the user # # if you're wondering why I didn't just use `gopass ls -f`, it's because in # an apparent effort to be user-friendly, `gopass show -n invalid-input` # doesn't seem to exit with an error # https://github.com/gopasspw/gopass/issues/551 # like drew devault wrote on his blog, I hate the stale bot # https://drewdevault.com/2021/10/26/stalebot.html shopt -s nullglob globstar while read -r line || [[ -n "$line" ]]; do # we could've used `gopass config path` but since we have parse the output # of `gopass config` because of possible mounts, better to just use `gopass # config` # we assume that we'll encounter `path: ...` only once and as soon as we # do, we parse the list of all the files inside the dir and store them in # an associative array with the name of the files as the index and the path # as the value if [[ "$line" == path* ]] && [[ -d "${line#* }" ]]; then path_files=("${line#* }"/**/*.gpg) path_files=("${path_files[@]#"${line#* }"/}") path_files=("${path_files[@]%.gpg}") for file in "${path_files[@]}"; do tmp_gopass_files["$file"]="${line#* }" done fi # similarly, we go through the mount points, generate the list of files # inside those mount points, add those files to the associative array with # the file names as the index and the location of the mount point as the # value # # there's no easy way to parse and associate file names with mount points # so we'll have to resort to some ugly hacks again if [[ "$line" == mount* ]]; then # remove the quotes from the parsed line line="${line//\"/}" # the mount name needs to be extracted to distinguish files with # potentially identical names mount_name="${line#mount *}" mount_name="${mount_name% =>*}" mount_name_arr+=("$mount_name") if [[ -d "${line#*=> }" ]]; then path_files=("${line#*=> }"/**/*.gpg) path_files=("${path_files[@]#"${line#*=> }"/}") path_files=("$mount_name"/"${path_files[@]%.gpg}") for file in "${path_files[@]}"; do tmp_gopass_files["$file"]="${line#*=> }" done fi fi done < <(gopass config) shopt -u nullglob globstar # the actual menu tsn_passfile="$(printf "%s\n" "${!tmp_gopass_files[@]}" \ | "$dmenu_backend" "${dmenu_backend_opts[*]}")" if [[ -z "$tsn_passfile" ]]; then _die fi # remove the mount name for the path check to be successful # initialize the temp variable with the value of tsn_passfile in case an # entry from the gopass path is chosen tmp_tsn_passfile="$tsn_passfile" for idx in "${mount_name_arr[@]}"; do if [[ "${tsn_passfile%%/*}" == "$idx" ]]; then tmp_tsn_passfile="${tsn_passfile#*/}" fi done # we had to use an associative array to keep track of the absolute path of # the selected file because it is possible to give invalid input to dmenu # while making a selection and tessen should exit in that case if [[ -n "${tmp_gopass_files["$tsn_passfile"]}" ]]; then if ! [[ -f "${tmp_gopass_files["$tsn_passfile"]}"/"$tmp_tsn_passfile".gpg ]]; then _die "the selected file was not found" fi fi unset -v tmp_gopass_files line path_files file mount_name mount_name_arr tmp_tsn_passfile } # parse the password store file for username, password, otp, custom autotype, # and other key value pairs get_pass_data() { local -a passdata local keyval_regex otp_regex idx key val if [[ "$pass_backend" == "pass" ]]; then mapfile -t passdata < <(pass show "$tsn_passfile" 2> /dev/null) if [[ "${#passdata[@]}" -eq 0 ]]; then _die "$tsn_passfile is empty" fi elif [[ "$pass_backend" == "gopass" ]]; then # gopass show -n -f is weird because it emits a first line 'Secret: # truncated-file-name' and that doesn't get assigned to a variable. but if # I redirect stdout to /dev/null, that first line gets redirected as well. # there doesn't seem to be any way to disable printing this first line. mapfile -t passdata < <(gopass show -n -f "$tsn_passfile" 2> /dev/null) if [[ "${#passdata[@]}" -eq 0 ]]; then _die "$tsn_passfile is empty" fi fi # the key can contain alphanumerics, spaces, hyphen, underscore, plus, at, # and hash # # the value can contain anything but `key:` and `val` should be separated # with a whitespace keyval_regex='^[[:alnum:][:blank:]+#@_-]+:[[:blank:]].+$' # parse the 'otpauth://' URI # this regex is borrowed from pass-otp at commit 3ba564c # # note that OTP support in gopass has been deprecated and for good reasons # I, for one, don't see how storing OTPs in the same place as storing your # passwords is a sane idea # https://github.com/gopasspw/gopass/blob/master/docs/features.md#adding-otp-secrets otp_regex='^otpauth:\/\/(totp|hotp)(\/(([^:?]+)?(:([^:?]*))?)(:([0-9]+))?)?\?(.+)$' # the first line should contain the only the password # this assumes the caveat highlighted earlier about gopass' behavior tsn_password="${passdata[0]}" # each key should be unique # if non-unique keys are present, the value of the first non-unique key will # be considered # in addition, the 'username', 'autotype', 'url', and 'password' keys are # considered as case insensitive for idx in "${passdata[@]:1}"; do key="${idx%%:*}" val="${idx#*: }" # keys with the case insensitive name 'password' are ignored if [[ "${key,,}" == "password" ]]; then continue elif [[ "${key,,}" =~ ^$tsn_userkey$ ]] && [[ -z "${tsn_username}" ]]; then tsn_userkey="${key,,}" tsn_username="$val" elif [[ "${key,,}" =~ ^$tsn_autokey$ ]] && [[ -z "${tsn_autotype}" ]]; then tsn_autokey="${key,,}" tsn_autotype="$val" elif [[ "${key,,}" =~ ^$tsn_urlkey$ ]] && [[ -z "${tsn_url}" ]]; then tsn_urlkey="${key,,}" tsn_url="$val" elif [[ "$idx" =~ $otp_regex ]] && [[ "$tsn_otp" == "false" ]]; then tsn_otp=true elif [[ "$idx" =~ $keyval_regex ]] && [[ -z "${tsn_passdata["$key"]}" ]]; then tsn_passdata["$key"]="$val" fi done # if $tsn_userkey isn't found, use the basename of file as username # also set the value of the tsn_userkey to the default value # this prevents the userkey from showing up as a regex in case a user has set # it in the config file # the same goes for other custom key variables if [[ -z "$tsn_username" ]]; then tsn_username="${tsn_passfile##*/}" tsn_userkey="user" fi if [[ -z "$tsn_autotype" ]]; then tsn_autokey="autotype" fi if [[ -z "$tsn_url" ]]; then tsn_urlkey="url" fi unset -v passdata keyval_regex otp_regex idx key val } # SECOND MENU: show a list of possible keys to choose from for autotyping or # copying, depending on the value of tsn_action # THIRD MENU: optional, this will show up if tsn_action is blank get_key() { local -a key_arr local ch flag=false # the 2nd menu for autotype, both, and the default actions will be the same # and the autotype key will be present in these cases # when tsn_action is set to copy, the autotype key shouldn't be shown in the 2nd menu case "$tsn_action" in autotype | both | default) if [[ "$1" == "key_list" ]]; then if [[ "$tsn_otp" == "false" ]] && [[ -z "$tsn_url" ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "false" ]] && [[ -n "$tsn_url" ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "$tsn_urlkey" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "true" ]] && [[ -z "$tsn_url" ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "true" ]] && [[ -n "$tsn_url" ]]; then key_arr=("$tsn_autokey" "$tsn_userkey" "password" "otp" "$tsn_urlkey" "${!tsn_passdata[@]}") fi fi # the (optional) third menu, its appearance depends on tsn_action being default if [[ "$tsn_action" == "default" ]] && [[ "$1" == "option" ]]; then key_arr=("$tsn_autokey" "copy") # the (optional) third menu if tsn_urlkey is chosen, it depends on # tsn_action being default elif [[ "$tsn_action" == "default" ]] && [[ "$1" == "$tsn_urlkey" ]]; then key_arr=("open" "copy") fi ;; copy) if [[ "$1" == "key_list" ]]; then if [[ "$tsn_otp" == "false" ]] && [[ -z "$tsn_url" ]]; then key_arr=("$tsn_userkey" "password" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "false" ]] && [[ -n "$tsn_url" ]]; then key_arr=("$tsn_userkey" "password" "$tsn_urlkey" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "true" ]] && [[ -z "$tsn_url" ]]; then key_arr=("$tsn_userkey" "password" "otp" "${!tsn_passdata[@]}") elif [[ "$tsn_otp" == "true" ]] && [[ -n "$tsn_url" ]]; then key_arr=("$tsn_userkey" "password" "otp" "$tsn_urlkey" "${!tsn_passdata[@]}") fi fi ;; esac # a global variable to hold the selected key for key_menu chosen_key="$(printf "%s\n" "${key_arr[@]}" | "$dmenu_backend" "${dmenu_backend_opts[@]}")" # validate the chosen key, if it doesn't exist, exit for ch in "${key_arr[@]}"; do if [[ "$chosen_key" == "$ch" ]]; then flag=true break fi done if [[ "$flag" == "false" ]]; then _die fi unset -v key_arr ch flag } # SECOND MENU: use 'get_key()' to show a list of possible keys to choose from key_menu() { get_key key_list case "$chosen_key" in "$tsn_autokey") auto_type_def ;; "$tsn_userkey") key_action "$tsn_username" ;; password) key_action "$tsn_password" ;; otp) key_otp ;; "$tsn_urlkey") key_action "$tsn_urlkey" ;; *) key_action "${tsn_passdata["$chosen_key"]}" ;; esac } # this function checks the value of tsn_action and decides if the third menu # should be presented or not # in case it receives a parameter called "url", autotype becomes equivalent to # opening the url in the web browser key_action() { local arg="$1" case "$tsn_action" in autotype) if [[ "$arg" == "$tsn_urlkey" ]]; then key_open_url || _die return 0 fi auto_type "$arg" ;; copy) if [[ "$arg" == "$tsn_urlkey" ]]; then wld_copy "$tsn_url" || _die return 0 fi wld_copy "$arg" ;; both) if [[ "$arg" == "$tsn_urlkey" ]]; then key_open_url wld_copy "$tsn_url" else printf "%s" "$arg" | wtype -s "$tsn_delay" - wld_copy "$arg" fi ;; default) if [[ "$arg" == "$tsn_urlkey" ]]; then get_key "$tsn_urlkey" if [[ "$chosen_key" == "open" ]]; then key_open_url || _die return 0 else wld_copy "$tsn_url" fi else get_key option if [[ "$chosen_key" == "$tsn_autokey" ]]; then auto_type "$arg" else wld_copy "$arg" fi fi ;; esac unset -v arg } # THIRD MENU: optional, this function is used if an 'otpauth://' URI is found # note that OTP support in gopass is deprecated and if they end up removing # support for it, we'll have to make changes here as well key_otp() { local tmp_otp if [[ "$pass_backend" == "pass" ]] && ! pass otp -h > /dev/null 2>&1; then _die "pass-otp is not installed" fi if [[ "$pass_backend" == "pass" ]]; then tmp_otp="$(pass otp "$tsn_passfile")" elif [[ "$pass_backend" == "gopass" ]]; then tmp_otp="$(gopass otp -o "$tsn_passfile")" fi if ! [[ "$tmp_otp" =~ ^[[:digit:]]+$ ]]; then _die "invalid OTP detected" fi key_action "$tmp_otp" unset -v tmp_otp } # open the url using either xdg-open or tsn_web_browser # if tsn_web_browser is defined, xdg-open won't be used key_open_url() { if [[ -n "$tsn_web_browser" ]]; then "$tsn_web_browser" "$tsn_url" > /dev/null 2>&1 || { printf "%s\n" "$tsn_web_browser was unable to open '$tsn_url'" >&2 return 1 } elif is_installed xdg-open; then xdg-open "$tsn_url" 2> /dev/null || { printf "%s\n" "xdg-open was unable to open '$tsn_url'" >&2 return 1 } else _die "failed to open '$tsn_urlkey'" fi } # SECOND MENU: the default autotype function, either autotype the username and # password or the custom autotype defined by the user auto_type_def() { local word tmp_otp if [[ -z "$tsn_autotype" ]]; then printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - wtype -s "$tsn_delay" -k Tab -- printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - else for word in $tsn_autotype; do case "$word" in ":delay") sleep 1 ;; ":tab") wtype -s "$tsn_delay" -k Tab -- ;; ":space") wtype -s "$tsn_delay" -k space -- ;; ":enter") wtype -s "$tsn_delay" -k Return -- ;; ":otp") key_otp ;; path | basename | filename) printf "%s" "${tsn_passfile##*/}" | wtype -s "$tsn_delay" - ;; "$tsn_userkey") printf "%s" "$tsn_username" | wtype -s "$tsn_delay" - ;; pass | password) printf "%s" "$tsn_password" | wtype -s "$tsn_delay" - ;; *) if [[ -n "${tsn_passdata["$word"]}" ]]; then printf "%s" "${tsn_passdata["$word"]}" | wtype -s "$tsn_delay" - else wtype -s "$tsn_delay" -k space -- fi ;; esac done fi _clear exit 0 } auto_type() { printf "%s" "$1" | wtype -s "$tsn_delay" - _clear exit 0 } # POTENTIAL IMPROVEMENT: We could restore the clipboard as it was before pass # was used. This is done by default by pass. wld_copy() { local tsn_cliptime if [[ "$pass_backend" == "pass" ]]; then tsn_cliptime="${PASSWORD_STORE_CLIP_TIME:-15}" if ! are_digits "$tsn_cliptime"; then printf "%s\n" "invalid clipboard timeout value in PASSWORD_STORE_CLIP_TIME" >&2 return 1 fi elif [[ "$pass_backend" == "gopass" ]]; then tsn_cliptime="$(gopass config cliptimeout)" tsn_cliptime="${tsn_cliptime##*: }" if ! are_digits "$tsn_cliptime"; then printf "%s\n" "invalid clipboard timeout value in cliptimeout" >&2 return 1 fi fi # it would've been better to use, or at least provide an option, to paste # only once using `wl-copy -o` but web browsers don't work well with this # feature # https://github.com/bugaevc/wl-clipboard/issues/107 printf "%s" "$1" | wl-copy if is_installed notify-send; then notify-send -t $((tsn_cliptime * 1000)) \ "Copied username to clipboard. Will clear in $tsn_cliptime seconds." fi { sleep "$tsn_cliptime" || exit 1 wl-copy --clear } > /dev/null 2>&1 & unset -v tsn_cliptime unset -v tsn_passfile tsn_passdata tsn_username tsn_password chosen_key } is_installed() { if command -v "${1%% *}" > /dev/null 2>&1; then return 0 else return 1 fi } setup_backend() { local backend="$1" local backend_opts="${backend#* }" backend="${backend%% *}" if ! is_installed "$backend"; then _die "'$backend' is not installed" fi if [[ "$backend_opts" == "$backend" ]]; then backend_opts="$(get_default_opts "$backend")" if [[ -z "$backend_opts" ]]; then printf "%s\n" "unable to determine any dmenu options for '$backend'" >&2 fi fi readonly tsn_backend="$backend" if [[ -n "$backend_opts" ]]; then mapfile -t -d ' ' backend_opts < <(printf "%s" "$backend_opts") readonly -a tsn_backend_opts=("${backend_opts[@]}") else readonly -a tsn_backend_opts=() fi } get_default_opts() { case "$1" in bemenu) printf "%s" "-i -l 10 -w --scrollbar=autohide -n" return 0 ;; rofi) printf "%s" "-dmenu" return 0 ;; wofi | fuzzel) printf "%s" "-d" return 0 ;; *) printf "" return 1 ;; esac } find_backend() { local dmbd for dmbd in "${tsn_known_backends[@]}"; do if is_installed "$dmbd"; then printf "%s" "$dmbd" return 0 fi done _die "%s\n" "unable to find a 'dmenu' compatible application" } validate_cliptime() { local clip_regex clip_regex="^[[:digit:]]+$" if [[ "$tsn_cliptime" =~ $clip_regex ]]; then return 0 else _die "Invalid clipboard time provided" fi } validate_action() { case "$1" in autotype) if ! is_installed "wtype"; then _die "wtype is not installed, unable to autotype pass data" fi tsn_action="autotype" ;; copy) if ! is_installed "wl-copy"; then _die "wl-clipboard is not installed, unable to copy-paste pass data" fi tsn_action="copy" ;; both) if ! is_installed "wtype"; then _die "wtype is not installed, unable to autotype pass data" elif ! is_installed "wl-copy"; then _die "wl-clipboard is not installed, unable to copy-paste pass data" fi tsn_action="both" ;; default) if is_installed "wtype" && is_installed "wl-copy"; then tsn_action="default" elif is_installed "wtype" && ! is_installed "wl-copy"; then printf "%s\n" "wl-clipboard is not installed, unable to copy-paste pass data" >&2 tsn_action="autotype" elif ! is_installed "wtype" && is_installed "wl-copy"; then printf "%s\n" "wtype is not installed, unable to autotype pass data" >&2 tsn_action="copy" elif ! is_installed "wtype" && ! is_installed "wl-copy"; then _die "please install at least one the following backends to use tessen: wtype | wl-clipboard " fi ;; *) _die "please specify a valid action: autotype | copy | both" ;; esac } _clear() { if [[ "$tsn_action" =~ ^(copy|both|default)$ ]]; then wl-copy --clear fi unset -v tsn_passfile tsn_passdata tsn_username tsn_password tsn_url unset -v tsn_autotype chosen_key } _die() { if [[ -n "$1" ]]; then printf "%s\n" "$1" >&2 fi _clear exit 1 } print_help() { local prog="${0##*/}" printf "%s" "\ $prog - autotype and copy data from password-store and gopass on wayland usage: $prog [options] $prog find a dmenu and pass backend, look for a config file in \$XDG_CONFIG_HOME/tessen/config, and either autotype OR copy data $prog -p gopass use gopass as the pass backend $prog -d rofi use rofi as the dmenu backend $prog -d rofi -a autotype use rofi and always autotype data $prog -d rofi -a copy use rofi and always copy data $prog -d rofi -a both use rofi and always autotype AND copy data $prog -c \$HOME/tsncfg specify a custom location for the $prog config file -p, --pass, --pass= choose either 'pass' or 'gopass' -d, --dmenu, --dmenu= specify a dmenu backend - 'rofi', 'fuzzel', 'bemenu', and 'wofi' are supported -a, --action, --action= choose either 'autotype', 'copy', or 'both' omit this option to use the default behavior -c, --config, --config= use a config file on a custom path -h, --help print this help menu -v, --version print the version of $prog for more details and additional features, please read the man page of $prog(1) for reporting bugs or feedback, visit https://github.com/ayushnix/tessen/issues " unset -v prog } # this block of code is needed because we can't source the file and execute # arbitrary input parse_config() { local _opt while [[ "$#" -gt 0 ]]; do _opt="$1" case "$_opt" in -c | --config) if [[ "$#" -lt 2 ]] || ! [[ -s "$2" ]]; then _die "please specify a valid path for the configuration file of tessen" fi tsn_config="$2" shift ;; --config=*) if ! [[ -s "${_opt##--config=}" ]]; then _die "please specify a valid path for the configuration file of tessen" fi tsn_config="${_opt##--config=}" ;; esac shift done unset -v _opt local line idx key val local -a config_arr local config_regex='^[[:alpha:]_]+="[[:alnum:]~_.\/^$|()*?-]+"$' # in case the user hasn't provided an explicit location, we'll have to check # if the default file exists before we parse it if [[ -s "$tsn_config" ]]; then while read -r line || [[ -n "$line" ]]; do if [[ "$line" == \#* ]]; then continue elif [[ "$line" =~ $config_regex ]]; then config_arr+=("$line") fi done < "$tsn_config" for idx in "${config_arr[@]}"; do key="${idx%=*}" val="${idx#*\"}" val="${val%*\"}" # here comes the ladder if [[ "$key" == "pass_backend" ]]; then validate_pass_backend "$val" elif [[ "$key" == "dmenu_backend" ]]; then validate_dmenu_backend "$val" elif [[ "$key" == "action" ]]; then validate_action "$val" elif [[ "$key" == "rofi_config_file" ]] && [[ "$dmenu_backend" == "rofi" ]]; then validate_dmenu_backend rofi dmenu_backend_opts+=("-config $val") tmp_opts+=("-config $val") elif [[ "$key" == "wofi_config_file" ]] && [[ "$dmenu_backend" == "wofi" ]]; then dmenu_backend_opts+=("-c $val") tmp_opts+=("-c $val") elif [[ "$key" == "wofi_style_file" ]] && [[ "$dmenu_backend" == "wofi" ]]; then dmenu_backend_opts+=("-s $val") tmp_opts+=("-s $val") elif [[ "$key" == "wofi_color_file" ]] && [[ "$dmenu_backend" == "wofi" ]]; then dmenu_backend_opts+=("-C $val") tmp_opts+=("-C $val") elif [[ "$key" == "userkey" ]]; then tsn_userkey="$val" elif [[ "$key" == "urlkey" ]]; then tsn_urlkey="$val" elif [[ "$key" == "autotype_key" ]]; then tsn_autokey="$val" elif [[ "$key" == "delay" ]]; then tsn_delay="$val" elif [[ "$key" == "web_browser" ]] && is_installed "$val"; then tsn_web_browser="$val" fi done fi unset -v line key val idx config_arr config_regex } main() { # initialize basic options for users who expect sane defaults and don't use # either the config file or args if [[ -z "$pass_backend" ]]; then find_pass_backend fi if [[ -z "$dmenu_backend" ]]; then find_dmenu_backend fi validate_dmenu_backend "$dmenu_backend" validate_action default # parse the config file parse_config "$@" # parse command line arguments after the config file because args hold more # precedence and priority than the config file local _opt while [[ "$#" -gt 0 ]]; do _opt="$1" case "$_opt" in -p | --pass) if [[ "$#" -lt 2 ]]; then _die "please specify a valid password store backend: pass | gopass" fi validate_pass_backend "$2" readonly pass_backend shift ;; --pass=*) if [[ -z "${_opt##--pass=}" ]]; then _die "please specify a valid password store backend: pass | gopass" fi validate_pass_backend "${_opt##--pass=}" readonly pass_backend ;; -d | --dmenu) if [[ "$#" -lt 2 ]]; then _die "please specify a valid dmenu backend: rofi | fuzzel | bemenu | wofi" fi validate_dmenu_backend "$2" # if rofi or wofi are selected, the additional values of # dmenu_backend_opts which were mentioned in the config file should be # appended # we need tmp_opts because even if dmenu_backend_opts is set to the # correct value, validating it would reset it back to the default # values readonly dmenu_backend if [[ "$dmenu_backend" == "rofi" ]] || [[ "$dmenu_backend" == "wofi" ]]; then if [[ "${#tmp_opts[@]}" -gt 0 ]]; then dmenu_backend_opts+=("${tmp_opts[@]}") readonly dmenu_backend_opts fi fi shift ;; --dmenu=*) if [[ -z "${_opt##--dmenu=}" ]]; then _die "please specify a valid dmenu backend: rofi | fuzzel | bemenu | wofi" fi validate_dmenu_backend "${_opt##--dmenu=}" readonly dmenu_backend if [[ "$dmenu_backend" == "rofi" ]] || [[ "$dmenu_backend" == "wofi" ]]; then if [[ "${#tmp_opts[@]}" -gt 0 ]]; then dmenu_backend_opts+=("${tmp_opts[@]}") readonly dmenu_backend_opts fi fi ;; -a | --action) if [[ "$#" -lt 2 ]]; then _die "please specify a valid action: autotype | copy | both" fi validate_action "$2" readonly tsn_action shift ;; --action=*) if [[ -z ${_opt##--action=} ]]; then _die "please specify a valid action: autotype | copy | both" fi validate_action "${_opt##--action=}" readonly tsn_action ;; # we've already parsed `-c` before so we'll need to skip it here -c | --config) shift ;; --config=*) : ;; -h | --help) print_help exit 0 ;; -v | --version) printf "%s\n" "${0##*/} version $tsn_version" exit 0 ;; --) shift break ;; *) _die "invalid argument detected" ;; esac shift done unset -v _opt tmp_opts trap '_clear' EXIT TERM INT if [[ "$pass_backend" == "pass" ]]; then get_pass_files elif [[ "$pass_backend" == "gopass" ]]; then get_gopass_files fi get_pass_data key_menu trap - EXIT TERM INT } main "$@"