pashage

Yet Another Opinionated Re-engineering of the Unix Password Store
git clone https://git.instinctive.eu/pashage.git
Log | Files | Refs | README | LICENSE

commit 98c8a6b7e0385e49c706bcdb9c24f15789576968
parent ad78faa2d4563c92b0543865d69231e3d4ebb632
Author: Natasha Kerensikova <natgh@instinctive.eu>
Date:   Mon, 10 Nov 2025 18:05:03 +0000

Tree output is rewritten without subshell

This leads to a 12x speed improvement (from 510 to 43 ms for my store in my machine).
Diffstat:
Mspec/action_spec.sh | 47+++++++++++++++++++++++++----------------------
Mspec/pashage_extra_spec.sh | 16+++++++++++++++-
Mspec/usage_spec.sh | 2+-
Msrc/pashage.sh | 172++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
4 files changed, 138 insertions(+), 99 deletions(-)

diff --git a/spec/action_spec.sh b/spec/action_spec.sh @@ -1916,8 +1916,8 @@ Describe 'Action Functions' LIST_VIEW=no When call do_list_or_show '' The status should be success - The output should be blank - The error should equal "$ do_tree ${PREFIX} Password Store" + The output should equal 'Password Store' + The error should equal '$ do_tree ' End It 'shows a decrypted age file' @@ -1954,8 +1954,8 @@ Describe 'Action Functions' LIST_VIEW=no When call do_list_or_show 'subdir' The status should be success - The output should be blank - The error should equal "$ do_tree ${PREFIX}/subdir subdir" + The output should equal 'subdir' + The error should equal '$ do_tree subdir' End It 'does not show a non-encrypted file' @@ -2207,6 +2207,7 @@ Describe 'Action Functions' %putsn data >"${PREFIX}/root.age" %putsn data >"${PREFIX}/subdir/hidden" %putsn data >"${PREFIX}/subdir/subsub/old.gpg" + %putsn data >"${PREFIX}/other/upper.age" %putsn data >"${PREFIX}/other/lower.age" } @@ -2220,16 +2221,16 @@ Describe 'Action Functions' It 'displays everything without a pattern' result() { %text - #|Title #|T_(B)empty(N) #|T_(B)other(N) - #|I_L_lower + #|I_T_lower + #|I_L_upper #|T_root #|L_(B)subdir(N) #|__L_(B)subsub(N) #|____L_(R)old(N) } - When call do_tree "${PREFIX}" 'Title' + When call do_tree '' The status should be success The output should equal "$(result)" End @@ -2237,45 +2238,47 @@ Describe 'Action Functions' It 'displays matching files and their non-matching parents' result() { %text - #|Title #|T_(B)other(N) #|I_L_lower #|L_(B)subdir(N) #|__L_(B)subsub(N) #|____L_(R)old(N) } - When call do_tree "${PREFIX}" 'Title' -i L + When call do_tree '' -i L The status should be success The output should equal "$(result)" End It 'does not display matching directories' - result() { - %text - #|Title - #|L_root - } - When call do_tree "${PREFIX}" 'Title' t + When call do_tree '' t The status should be success - The output should equal "$(result)" + The output should equal 'L_root' End It 'does not consider file extension when matching' - When call do_tree "${PREFIX}" 'Title' g + When call do_tree '' g The status should be success The output should equal '' End It 'might not display anything' - When call do_tree "${PREFIX}" 'Title' z + When call do_tree '' z The status should be success The output should equal '' End - It 'does not display an empty title' - When call do_tree "${PREFIX}" '' t - The status should be success - The output should equal 'L_root' + It 'defensively aborts on invalid prefix start' + When run do_tree_prefix '_XI_' + The output should be blank + The error should equal 'Invalid tree prefix: "XI_"' + The status should equal 1 + End + + It 'defensively aborts on invalid prefix end' + When run do_tree_prefix '_IX' + The output should be blank + The error should equal 'Invalid tree prefix: "X"' + The status should equal 1 End End End diff --git a/spec/pashage_extra_spec.sh b/spec/pashage_extra_spec.sh @@ -609,7 +609,7 @@ Describe 'Integrated Command Functions' #||- (B)extra(N) #|| |- (B)subdir(N) #|| | `- file - #|| `- (R)subdir(N) + #|| `- (R)subdir.gpg(N) #|`- (B)subdir(N) #| `- file } @@ -1795,6 +1795,20 @@ Describe 'Integrated Command Functions' The error should equal 'Unexpected SHOW value "invalid"' End + It 'includes invalid argument middle in do_tree_prefix' + When run do_tree_prefix '_X_I' + The status should equal 1 + The output should be blank + The error should equal 'Invalid tree prefix: "X_I"' + End + + It 'includes invalid argument ending in do_tree_prefix' + When run do_tree_prefix 'IX' + The status should equal 1 + The output should be blank + The error should equal 'Invalid tree prefix: "X"' + End + It 'includes interactive yesno' # Technically not unreachable, but not worse than faking a terminal # for each call of `yesno` when the whole test suite is outside diff --git a/spec/usage_spec.sh b/spec/usage_spec.sh @@ -606,7 +606,7 @@ Describe 'Command-Line Parsing' When call cmd_find -i pattern The status should be success The output should equal 'Search pattern: -i pattern' - The error should equal '$ do_tree /prefix -i pattern' + The error should equal '$ do_tree -i pattern' End It 'interprets the raw list flag' diff --git a/src/pashage.sh b/src/pashage.sh @@ -913,7 +913,8 @@ do_list_or_show() { if [ "${LIST_VIEW-no}" = "yes" ]; then do_list '' else - do_tree "${PREFIX}" "Password Store" + printf 'Password Store\n' + do_tree '' fi elif [ -f "${PREFIX}/$1.age" ]; then SECRET="$(do_decrypt "${PREFIX}/$1.age")" @@ -925,7 +926,8 @@ do_list_or_show() { if [ "${LIST_VIEW-no}" = "yes" ]; then do_list "${1%/}" else - do_tree "${PREFIX}/$1" "$1" + printf '%s\n' "${1%/}" + do_tree "${1%/}" fi elif [ -f "${PREFIX}/$1.gpg" ]; then SECRET="$(do_decrypt_gpg "${PREFIX}/$1.gpg")" @@ -1035,92 +1037,112 @@ do_show() { esac } -# Display the tree rooted at the given directory -# $1: root directory -# $2: title +# Display a list of secret as a tree +# $1: path relative to prefix # ...: (optional) grep arguments to filter do_tree() { - ( cd "$1" && shift && do_tree_cwd "$@" ) -} - -# Display the subtree rooted at the current directory -# $1: title -# ...: (optional) grep arguments to filter -do_tree_cwd() { - ACC="" - PREV="" - TITLE="$1" - shift + REVERSE="" + BEGIN_GPG_NAME="${RED_TEXT}" + END_GPG_NAME="${NORMAL_TEXT}" + LIST_EMPTY="${2+no}" + LIST_EMPTY="${LIST_EMPTY:-yes}" + ENTRY_LIST="$(do_list "$@")" + while read -r LINE; do + REVERSE="${LINE#"${1-}${1:+/}"}${REVERSE:+"${NL}"}${REVERSE}" + done <<-EOF + ${ENTRY_LIST} + EOF + unset BEGIN_GPG_NAME + unset END_GPG_NAME + unset LIST_EMPTY + unset ENTRY_LIST + + GRAPH="_" + TREE="" + PREV_LINE="" + while read -r LINE; do + LINE_COPY="${LINE}" + # Skip common directory prefix + while ! [ "${LINE_COPY%%/*}" = "${LINE_COPY}" ] \ + && ! [ "${PREV_LINE%%/*}" = "${PREV_LINE}" ] \ + && [ "${LINE_COPY%%/*}" = "${PREV_LINE%%/*}" ] + do + LINE_COPY="${LINE_COPY#*/}" + PREV_LINE="${PREV_LINE#*/}" + done - for ENTRY in *; do - [ -e "${ENTRY}" ] || continue - ITEM="$(do_tree_item "${ENTRY}" "$@")" - [ -z "${ITEM}" ] && continue + # Output obsolete directories + while ! [ "${PREV_LINE%/*}" = "${PREV_LINE}" ]; do + PREV_LINE="${PREV_LINE%/*}" + do_tree_prefix "${GRAPH%?}" + GRAPH="${GRAPH%??}I" + TREE="${PREV_LINE##*/}${NORMAL_TEXT}${NL}${TREE}" + TREE="${CGRAPH}${BLUE_TEXT}${TREE}" + unset CGRAPH + done - if [ -n "${PREV}" ]; then - ACC="$(printf '%s\n' "${PREV}" | do_tree_prefix "${ACC}" "${TREE_T}" "${TREE_I}")" - fi + [ -z "${LINE}" ] && continue - PREV="${ITEM}" - done - unset ENTRY + # Prepare new directories + while ! [ "${LINE_COPY%/*}" = "${LINE_COPY}" ]; do + LINE_COPY="${LINE_COPY%/*}" + GRAPH="${GRAPH}_" + done + unset LINE_COPY - if [ -n "${PREV}" ]; then - ACC="$(printf '%s\n' "${PREV}" | do_tree_prefix "${ACC}" "${TREE_L}" "${TREE__}")" - fi + if [ -n "${LINE##*/}" ]; then + do_tree_prefix "${GRAPH}" + GRAPH="${GRAPH%?}I" + TREE="${CGRAPH}${LINE##*/}${NL}${TREE}" + unset CGRAPH + fi - if [ $# -eq 0 ] || [ -n "${ACC}" ]; then - [ -n "${TITLE}" ] && printf '%s\n' "${TITLE}" - fi + PREV_LINE="${LINE}" + done <<-EOF + ${REVERSE} - [ -n "${ACC}" ] && printf '%s\n' "${ACC}" + EOF + # Note the extra blank line above to flush the first directories + unset GRAPH + unset LINE + unset PREV_LINE - unset ACC - unset PREV - unset TITLE + printf '%s' "${TREE}" + unset TREE } -# Display a node in a tree -# $1: item name -# ...: (optional) grep arguments to filter -do_tree_item() { - ITEM_NAME="$1" - shift - if [ -d "${ITEM_NAME}" ]; then - do_tree "${ITEM_NAME}" \ - "${BLUE_TEXT}${ITEM_NAME}${NORMAL_TEXT}" \ - "$@" - elif [ "${ITEM_NAME%.age}.age" = "${ITEM_NAME}" ]; then - if [ $# -eq 0 ] \ - || printf '%s\n' "${ITEM_NAME%.age}" | grep -q "$@" - then - printf '%s\n' "${ITEM_NAME%.age}" - fi - elif [ "${ITEM_NAME%.gpg}.gpg" = "${ITEM_NAME}" ]; then - if [ $# -eq 0 ] \ - || printf '%s\n' "${ITEM_NAME%.gpg}" | grep -q "$@" - then - printf '%s\n' \ - "${RED_TEXT}${ITEM_NAME%.gpg}${NORMAL_TEXT}" - fi - fi - - unset ITEM_NAME -} - -# Add a tree prefix -# $1: optional title before the first line -# $2: prefix of the first line -# $3: prefix of the following lines +# Convert a tree prefix into user-facing representation +# $1: encoded tree prefix +# CGRAPH: output user-facing representation do_tree_prefix() { - [ -n "$1" ] && printf '%s\n' "$1" - IFS= read -r LINE - printf '%s%s\n' "$2" "${LINE}" - while IFS= read -r LINE; do - printf '%s%s\n' "$3" "${LINE}" + CGRAPH="" + while [ -n "${1#?}" ]; do + case "${1%"${1#?}"}" in + (_) + CGRAPH="${CGRAPH}${TREE__}" + ;; + (I) + CGRAPH="${CGRAPH}${TREE_I}" + ;; + (*) + die "Invalid tree prefix: \"$1\"" + ;; + esac + set -- "${1#?}" done - unset LINE + + case "$1" in + (_) + CGRAPH="${CGRAPH}${TREE_L}" + ;; + (I) + CGRAPH="${CGRAPH}${TREE_T}" + ;; + (*) + die "Invalid tree prefix: \"$1\"" + ;; + esac } @@ -1286,7 +1308,7 @@ cmd_find() { do_list '' "$@" else printf 'Search pattern: %s\n' "$*" - do_tree "${PREFIX}" '' "$@" + do_tree '' "$@" fi }