Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and versions are tracked in the repo-root `VERSION` file.
library surface and standalone checkout path.
- Added public associative-array assertions and moved `arg_parse` caller-owned
map validation onto the public assertion API.
- Added cleanup path unregister support and used it for eager temp cleanup.
- Made `gh_run` report failed GitHub CLI commands even when callers enable
`set -e`, and preserved argument boundaries in GitHub command failure logs.
- Documented and tested `str_split` trailing-separator behavior.
Expand Down
8 changes: 7 additions & 1 deletion lib/bash/file/lib_file.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ __file_remove_temp_paths__() {
local path

for path; do
[[ -n "$path" ]] && rm -f -- "$path"
if [[ -n "$path" ]]; then
rm -f -- "$path"
std_unregister_cleanup_path "$path"
fi
done
return 0
}
Expand Down Expand Up @@ -397,6 +400,7 @@ update_file_section() {
}
}
' "$target_file" > "$temp_file" && mv -f "$temp_file" "$target_file"; then
std_unregister_cleanup_path "$temp_file"
__file_remove_temp_paths__ "$new_content_file"
return 0
fi
Expand All @@ -423,6 +427,7 @@ update_file_section() {
print $0
}
' "$target_file" > "$temp_file" && mv -f "$temp_file" "$target_file"; then
std_unregister_cleanup_path "$temp_file"
__file_remove_temp_paths__ "$new_content_file"
return 0
fi
Expand Down Expand Up @@ -463,6 +468,7 @@ update_file_section() {
return 1
fi

std_unregister_cleanup_path "$temp_file"
__file_remove_temp_paths__ "$new_content_file"
return 0
fi
Expand Down
23 changes: 23 additions & 0 deletions lib/bash/file/tests/lib_file.bats
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,29 @@ EOF
[[ "$(cat "$registration_file")" == *"config.txt."* ]]
}

@test "update_file_section unregisters temp files after eager cleanup" {
local cleanup_path
local target="$TEST_TMPDIR/config.txt"
cat <<'EOF' > "$target"
before
# BEGIN
old
# END
after
EOF

update_file_section "$target" "# BEGIN" "# END" "new"

for cleanup_path in "${__std_cleanup_paths[@]}"; do
if [[ "$cleanup_path" == *"base-file-section-new."* ||
"$cleanup_path" == *"base-file-section-current."* ||
"$cleanup_path" == *"config.txt."* ]]; then
printf 'stale cleanup path: %s\n' "$cleanup_path" >&2
return 1
fi
done
}

@test "update_file_section skips unchanged existing section" {
local before_inode
local target="$TEST_TMPDIR/config.txt"
Expand Down
9 changes: 9 additions & 0 deletions lib/bash/std/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,15 @@ components are rejected before registration. When one call mixes safe and unsafe
paths, safe paths are registered, unsafe paths are rejected, and the helper
returns nonzero.

When a script removes or moves a registered path before exit, unregister it so
the shared cleanup registry only contains paths that may still need fallback
cleanup:

```bash
rm -rf -- "$workspace"
std_unregister_cleanup_path "$workspace"
```

For custom cleanup, register a function name:

```bash
Expand Down
53 changes: 53 additions & 0 deletions lib/bash/std/lib_std.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
# fatal_error msg... # Convenience wrapper: exit with last status or 1.
# std_register_cleanup_hook fn # Run a cleanup function from the shared EXIT trap.
# std_register_cleanup_path p # Remove files/directories from the shared EXIT trap.
# std_unregister_cleanup_path p
# # Drop files/directories from the shared EXIT cleanup list.
# std_make_temp_file var [pfx] # Create a temp file and store its path in var.
# std_make_temp_dir var [pfx] # Create a temp directory and store its path in var.
# std_command_path var cmd # Resolve an external command path without exiting.
Expand Down Expand Up @@ -1062,6 +1064,7 @@ __std_run_with_timeout_fallback__() {
command_status=124
fi
rm -f -- "$timeout_marker"
std_unregister_cleanup_path "$timeout_marker"

return "$command_status"
}
Expand Down Expand Up @@ -1349,6 +1352,56 @@ std_register_cleanup_path() {
return "$status"
}

#
# std_unregister_cleanup_path - Removes files or directories from the shared EXIT cleanup path list.
#
# This is useful after eager cleanup removes or moves a path that was previously
# registered for fallback cleanup. Paths use the same safety checks as
# registration. Safe paths in a mixed call are still unregistered, and the
# function returns nonzero if any path was rejected.
#
# Usage:
# rm -rf -- "$workspace"
# std_unregister_cleanup_path "$workspace"
#
std_unregister_cleanup_path() {
local path existing_path
local should_remove had_valid_path=0 status=0
local -a paths_to_remove=() remaining_paths=()

if (($# == 0)); then
log_warn "std_unregister_cleanup_path: No paths provided."
return 0
fi

for path; do
if ! __std_is_safe_cleanup_path__ "$path"; then
log_error "std_unregister_cleanup_path: refusing to unregister unsafe cleanup path '$path'."
status=1
continue
fi

had_valid_path=1
paths_to_remove+=("$path")
done

if ((had_valid_path)); then
for existing_path in "${__std_cleanup_paths[@]}"; do
should_remove=0
for path in "${paths_to_remove[@]}"; do
if [[ "$existing_path" == "$path" ]]; then
should_remove=1
break
fi
done
((should_remove)) || remaining_paths+=("$existing_path")
done
__std_cleanup_paths=("${remaining_paths[@]}")
fi

return "$status"
}

######################################################## TEMP FILES ####################################################

__std_make_temp_path__() {
Expand Down
30 changes: 30 additions & 0 deletions lib/bash/std/tests/lib_std.bats
Original file line number Diff line number Diff line change
Expand Up @@ -1476,6 +1476,36 @@ EOF
[[ " ${__std_cleanup_paths[*]} " != *" / "* ]]
}

@test "cleanup path registration supports unregistering eager cleanup paths" {
local keep_file="$TEST_TMPDIR/cleanup-keep-file.txt"
local drop_file="$TEST_TMPDIR/cleanup-drop-file.txt"

printf 'keep\n' > "$keep_file"
printf 'drop\n' > "$drop_file"

std_register_cleanup_path "$keep_file" "$drop_file"
rm -f -- "$drop_file"
std_unregister_cleanup_path "$drop_file"

[[ " ${__std_cleanup_paths[*]} " == *" $keep_file "* ]]
[[ " ${__std_cleanup_paths[*]} " != *" $drop_file "* ]]
}

@test "cleanup path unregister rejects dangerous paths without clearing valid registrations" {
local target_file="$TEST_TMPDIR/cleanup-unregister-valid-file.txt"
local stderr_file="$TEST_TMPDIR/cleanup-unregister-mixed.err"
local rc=0

printf 'sample\n' > "$target_file"
std_register_cleanup_path "$target_file"

std_unregister_cleanup_path "/" 2>"$stderr_file" || rc=$?

[ "$rc" -eq 1 ]
[[ "$(cat "$stderr_file")" == *"std_unregister_cleanup_path: refusing to unregister unsafe cleanup path '/'"* ]]
[[ " ${__std_cleanup_paths[*]} " == *" $target_file "* ]]
}

@test "std_make_temp_file creates a file under TMPDIR and cleans it up" {
local script="$TEST_TMPDIR/temp-file.sh"
local temp_root="$TEST_TMPDIR/temp-root"
Expand Down
Loading