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:
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
}