From b19958b8d6ab68bfda56e8c1ec2fa75ffe5762ec Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:57:26 -0400 Subject: [PATCH 1/2] Keep the end of long output, and pass the command line to _helpless The typescript was capped with head -n 1024, keeping the start of the output. Errors are usually at the end (tracebacks, make: *** Error, segfaults), so a program that printed a lot and then failed lost its error before any helper or _helpless saw it. Now keep the first 64 lines (where the command line is echoed and found) plus the last 1024, with a marker for what was omitted. _helpless now also receives the command line as a second argument, so whatever explains the output (in cs50/codespace, the CS50 Duck via cs50.ai) can see what was run. The default _helpless ignores it; output stays the first argument, so the codespace's empty-output check is unaffected. --- etc/profile.d/help50.sh | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 4805c89..7e9eded 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -53,8 +53,15 @@ function _help50() { # Remove script's own output (if this is user's first command) typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}') - # Cap typescript at MIN(1K lines, 1M bytes), else `read` is slow - typescript=$(echo "$typescript" | head -n 1024 | cut -b 1-1048576) + # Cap typescript, else `read` below is slow. Keep the first few lines, where the + # command line itself is echoed (found below), plus the last 1K lines, where + # errors tend to be (tracebacks, `make: *** Error`, segfaults); a long-running + # program that prints a lot and then crashes would otherwise lose its error. + typescript=$(echo "$typescript" | cut -b 1-1048576) + local total=$(echo "$typescript" | wc -l) + if [[ $total -gt 1088 ]]; then + typescript=$(echo "$typescript" | head -n 64; echo "[... $((total - 1088)) lines omitted ...]"; echo "$typescript" | tail -n 1024) + fi # Remove ANSI characters typescript=$(echo "$typescript" | ansi2txt) @@ -108,8 +115,10 @@ function _help50() { _helpful "$help" elif [[ $status -ne 0 ]]; then # If helpless - # Cap what's relayed downstream (e.g., ddb50 rejects > 10,000 characters), keeping the end, where errors tend to be - _helpless "$(echo "$typescript" | tail -c 8192)" + # Pass the output (capped, e.g., since ddb50 rejects > 10,000 characters, keeping the end, + # where errors tend to be) and the command line itself, so that whatever explains the + # output can see what was run + _helpless "$(echo "$typescript" | tail -c 8192)" "$argv" fi else _helped @@ -123,7 +132,11 @@ function _rhetorical() { _alert "That was a rhetorical question. <3" } -# Default helpers +# Default helpers, overridable (e.g., by cs50/codespace) by defining them before this file is sourced: +# _helped last command succeeded +# _helpful ADVICE a helper had advice for the failed command +# _helpless OUTPUT CMD no helper had advice; OUTPUT is the failed command's (cleaned, capped) +# output, possibly empty, and CMD its command line if ! type _helped >/dev/null 2>&1; then function _helped() { :; } # Silent fi From 9a6eb2e4b8a1afcf073da09803454f308c56518c Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 22 Sep 2026 01:35:08 -0400 Subject: [PATCH 2/2] Smoke-test what _helpless receives for a long failed command Nothing exercised _help50 itself: the head/tail cap and the new command-line argument were only checked by hand under a pty. Drive _help50 directly in the image instead, with a fabricated typescript and a fake _helpless, and assert that a command printing 3000 lines then an error yields the command line as the second argument and, as the first, output that starts at the program's first line, carries the omission marker, drops the middle, and ends with the error. Also check that a failed command with no output yields an empty first argument, which is what the codespace's empty-output check depends on. The check fails against the previous help50.sh. --- tests/smoke.sh | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/smoke.sh b/tests/smoke.sh index 41ac041..10d4a36 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -25,4 +25,27 @@ run "$IMAGE" bash --login -c 'valgrind python x.py < /dev/null; test $? -eq 1' 2 echo "- _fold wraps without a terminal" run "$IMAGE" bash --login -c '. /opt/cs50/lib/cli; TERM= _fold "$(printf "a %.0s" {1..100})"' | head -n 1 | grep -qE '^.{1,80}$' +echo "- _helpless gets the end of a failed command's long output, plus its command line" +run "$IMAGE" bash --login -c ' + export HELP50=$(mktemp) + _helpless() { printf "%s" "$1" > /tmp/output; printf "%s" "$2" > /tmp/cmd; } + . /etc/profile.d/help50.sh + + # A command that prints 3000 lines and then fails, as script(1) records it + { printf "$ ./slow\r\n"; seq 3000 | sed "s/$/\r/"; printf "Error: boom\r\n"; } > "$HELP50" + set -o history; history -s ./slow; set +o history + false; _help50 + test "$(cat /tmp/cmd)" = ./slow && + test "$(head -n 1 /tmp/output)" = 1 && + grep -qx "\[... 1914 lines omitted ...\]" /tmp/output && + ! grep -qx 1000 /tmp/output && + test "$(tail -n 1 /tmp/output)" = "Error: boom" || exit 1 + + # A command that fails without output + : > "$HELP50" + set -o history; history -s ./slow; set +o history + false; _help50 + test "$(cat /tmp/cmd)" = ./slow && test ! -s /tmp/output || exit 1 +' + echo "OK"