Skip to content

task-remove hangs at 100% CPU in _la_strip_line under bash 3.2 with a UTF-8 locale (#266) - #269

Merged
martin-conur merged 2 commits into
mainfrom
task/task-remove-hang
Oct 19, 2026
Merged

martin-conur merged 2 commits into
mainfrom
task/task-remove-hang

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Closes #266

What

Two "is anything left?" checks in lib/loadout-artifacts.sh used ${s//[[:space:]]/}. Bash 3.2's matcher is superlinear on a bracket class under a multibyte locale. On macOS /bin/bash, an 8KB UTF-8 file took 25s and a ~30KB CLAUDE.md never finished. The two checks:

  • _la_strip_line: did CLAUDE.md / .gitignore hold only task-init's line?
  • _la_workflow_doc_tail: does the workflow doc need a <doc>.bak? The issue didn't name this one; the suggested grep for other ${var//[[:class:]]/} uses turned it up. There are no others in the repo.

Both now go through a new _la_has_content, which runs grep -q '[^[:space:]]' on a here-string. A pipe (the issue's suggestion) is unsafe here: task-remove / task-config run under pipefail, and a grep -q that exits on its first match can SIGPIPE the printf. The non-blank file would then read as blank, and the user's CLAUDE.md would be deleted. _la_workflow_doc_tail now returns the tail verbatim instead of whitespace-stripped; its only caller tests -n.

Tests

  • New tests drive both functions on a ~40KB UTF-8 file under a UTF-8 locale, in their own bash with a 20s perl alarm. They redirect output rather than pipe it, so a hang kills the spinning process instead of orphaning it. On the old code both hit the alarm (status 142).
  • Also new: a whitespace-only remainder still deletes CLAUDE.md, and a doc tail of stub plus whitespace counts as empty.
  • Mutation check: forcing _la_has_content to return 1 or to return 0 turns tests red (old and new).
  • The large-file tests only discriminate on bash 3.2 (the macOS CI leg). On bash 5 they pin behaviour only.
  • ./run_tests.sh: 1370 ok, 0 failing. tools/check-drift.sh clean. shellcheck -x lib/loadout-artifacts.sh clean.

The CHANGELOG has an entry under Fixed. No task-init re-run needed.

🤖 Generated with Claude Code

martin-conur and others added 2 commits October 19, 2026 12:19
… UTF-8 locale: test for content with grep, not a whitespace substitution (#266)

`_la_strip_line` and `_la_workflow_doc_tail` decided "is anything left?"
with `${s//[[:space:]]/}`. Bash 3.2's matcher is superlinear on a bracket
class under a multibyte locale: on macOS /bin/bash an 8KB UTF-8 file took
25s and a ~30KB CLAUDE.md never finished.

Both now call `_la_has_content`, a `grep -q '[^[:space:]]'` on a
here-string. It is not a pipe: under the callers' pipefail an early-exiting
`grep -q` would SIGPIPE the writer and a non-blank CLAUDE.md would read as
blank, then get deleted. `_la_workflow_doc_tail` returns the tail verbatim;
its one caller only tests it for emptiness.

Tests drive both functions on a ~40KB UTF-8 file under a UTF-8 locale with
a 20s alarm (the old code hits it: status 142), and pin the
whitespace-only cases. Forcing the helper to either answer turns tests red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… UTF-8 locale: build the test fixture without reopening /dev/stdout (#266)

The doc-tail test wrote its big UTF-8 body with `> /dev/stdout` inside a
block already redirected to the doc. On Linux that reopens the file
O_TRUNC, wiping the marker lines the block had just written, so the doc
read as pre-markers and the test failed on ubuntu only. The helper now
writes to stdout and the callers redirect. Against the old lib both
large-file tests still hit the alarm (status 142).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur

Copy link
Copy Markdown
Owner Author

Spec compliance

Delivers #266. Both ${s//[[:space:]]/} sites in lib/loadout-artifacts.sh are replaced (_la_strip_line, the one named in the issue, and _la_workflow_doc_tail). The issue's "grep for other uses" request was done: I grepped lib/ and bin/ and found none left. The helper is grep -q '[^[:space:]]' on a here-string, as the issue suggested. The PR uses a here-string rather than a pipe, and that choice is correct: under pipefail a pipe into grep -q could SIGPIPE the writer and make a non-blank file read as blank, which would delete the user's CLAUDE.md.

Code-review findings

No correctness issues found.

  • Empty input is handled. A here-string of "" is a single newline, which has no non-space character, so the result is false.
  • _la_workflow_doc_tail now returns the tail verbatim instead of whitespace-stripped. Its only caller tests it for emptiness, so this is safe.
  • Changelog entry is present and says no task-init re-run is needed, which is right because only lib/ changes.

Verification, run on macOS /bin/bash 3.2.57:

  • CI exists for the head SHA (gh run list returned 1 run).
  • ./run_tests.sh task_remove passes 44/44 in about 20s.
  • tools/check-drift.sh is clean and shellcheck -x lib/loadout-artifacts.sh is clean.
  • Mutation check: I reverted _la_has_content to the old substitution. Tests 10 and 12 (the large-file tests) went red, and the run took about 63s instead of 20s. Tests 11 and 13 stayed green, as expected, since they pin whitespace behaviour. Restored afterwards.

Verdict

clean. Nothing to fix in this PR. PM holds the merge decision.

@martin-conur
martin-conur merged commit 1415b86 into main Oct 19, 2026
4 checks passed
@martin-conur
martin-conur deleted the task/task-remove-hang branch October 19, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

task-remove hangs at 100% CPU in _la_strip_line under bash 3.2 with a UTF-8 locale

1 participant