From e7781bf70f656c6b371e12bc7e599cc0e93b922e Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 4 Jan 2024 23:59:44 -0500 Subject: [PATCH 01/85] added helpers for make --- Dockerfile | 3 +- etc/profile.d/help50.sh | 66 +++++++++++++++++++++++++++++++++++++ opt/cs50/bin/make | 35 +++++++++----------- opt/cs50/lib/help50/cd.sh | 9 +++++ opt/cs50/lib/help50/ls.sh | 6 ++++ opt/cs50/lib/help50/make.sh | 42 +++++++++++++++++++++++ tests/bar.c | 1 + tests/foo/baz.c | 0 8 files changed, 142 insertions(+), 20 deletions(-) create mode 100644 etc/profile.d/help50.sh mode change 100755 => 100644 opt/cs50/bin/make create mode 100644 opt/cs50/lib/help50/cd.sh create mode 100644 opt/cs50/lib/help50/ls.sh create mode 100644 opt/cs50/lib/help50/make.sh create mode 100644 tests/bar.c create mode 100644 tests/foo/baz.c diff --git a/Dockerfile b/Dockerfile index ae2d949..374c0da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -153,10 +153,12 @@ RUN curl https://packagecloud.io/install/repositories/cs50/repo/script.deb.sh | bash-completion \ build-essential `# dpkg-dev, libc, gcc, g++, make, etc.`\ clang \ + colorized-logs `# For help50` \ coreutils `# For fold` \ cowsay \ dos2unix \ dnsutils `# For nslookup` \ + expect `# For help50` \ fonts-noto-color-emoji `# For render50` \ gdb \ git \ @@ -188,7 +190,6 @@ RUN curl https://packagecloud.io/install/repositories/cs50/repo/script.deb.sh | cs50 \ Flask \ Flask-Session \ - help50 \ pytest \ render50 \ setuptools \ diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh new file mode 100644 index 0000000..836a037 --- /dev/null +++ b/etc/profile.d/help50.sh @@ -0,0 +1,66 @@ +# Directory with helpers +HELPERS="/opt/cs50/lib/help50" + +# TEMP +alias make="help50 make" + +# Formatting +bold=$(tput bold) +normal=$(tput sgr0) + + +help50() { + + # Check for helper + if [[ $# -gt 0 && ! -f "${HELPERS}/${1}.sh" ]]; then + echo "Sorry, ${bold}help50${normal} does not yet know how to help with this!" + return 1 + fi + + # Duplicate file descriptors + exec 3>&1 4>&2 + + # Redirect output to a file too + local file="/tmp/help50.$$" # Use PID to support multiple terminals + exec > >(tee -a "$file") 2>&1 + + # Execute command + if [[ "$(type -P -t "$1")" == "file" ]]; then + unbuffer "$@" # Else, e.g., ls isn't colorized + else + "$@" # Can't unbuffer builtins (e.g., cd) + fi + + # Remember status + local status=$? + + # Remember command + local command="$1" + + # Remove command from $@ + shift + + # Get redirected output + local output=$(cat "$file") + + # Remove any ANSI codes + output=$(echo "$output" | ansi2txt | col -b) + + # Restore file descriptors + exec 1>&3 2>&4 + + # Close file descriptors + exec 3>&- 4>&- + + # Remove file + rm --force "$file" + + # Preserve command's status for helpers + (exit $status) + + # Try to get help + local help=$( . "${HELPERS}/${command}.sh" <<< "$output" ) + if [[ -n "$help" ]]; then + echo "🦆 $help" + fi +} diff --git a/opt/cs50/bin/make b/opt/cs50/bin/make old mode 100755 new mode 100644 index 4658e3e..d4275e8 --- a/opt/cs50/bin/make +++ b/opt/cs50/bin/make @@ -1,23 +1,20 @@ #!/bin/bash -# Ensure no targets end with .c -args="" -invalid_args=0 -for arg; do - case "$arg" in - (*.c) arg=${arg%.c}; invalid_args=1;; - esac - args="$args $arg" -done -if [ $invalid_args -eq 1 ]; then - echo "Did you mean 'make$args'?" - exit 1 -fi +# If a single target and not an option +if [[ $# -eq 1 ]] && [[ "$1" != -* ]]; then + + # If no Makefile + if [[ ! -f Makefile && ! -f makefile ]]; then -# Run make -if [[ -d "$1" ]]; then - echo "$1 is a directory" - exit 1 -else - /usr/bin/make -B -s $* + # If target ends with .c or is a directory + if [[ "$1" == *?.c || -d "$1" ]]; then + + # Don't suppress "Nothing to be done" with --silent + /usr/bin/make "$1" + exit $? + fi + fi fi + +# Don't echo recipes +/usr/bin/make --always-make --silent "$@" diff --git a/opt/cs50/lib/help50/cd.sh b/opt/cs50/lib/help50/cd.sh new file mode 100644 index 0000000..01e9087 --- /dev/null +++ b/opt/cs50/lib/help50/cd.sh @@ -0,0 +1,9 @@ +echo "EXIT: $?" +local stdin=$(cat) +#echo "stdout[$stdout]" +#echo "1[$1]" +#echo "2[$2]" +#echo "pwd[$PWD]" +#echo "STDOUT:$stdout" +echo "STDIN:$stdin" +return 0 diff --git a/opt/cs50/lib/help50/ls.sh b/opt/cs50/lib/help50/ls.sh new file mode 100644 index 0000000..ce456b2 --- /dev/null +++ b/opt/cs50/lib/help50/ls.sh @@ -0,0 +1,6 @@ +echo "argv:[$argv]" +echo "stdout[$stdout]" +echo "1[$1]" +echo "2[$2]" +echo "pwd[$PWD]" +return 0 diff --git a/opt/cs50/lib/help50/make.sh b/opt/cs50/lib/help50/make.sh new file mode 100644 index 0000000..5b64739 --- /dev/null +++ b/opt/cs50/lib/help50/make.sh @@ -0,0 +1,42 @@ +local output=$(cat) + +local regex="make: Nothing to be done for '(.*)'" +if [[ "$output" =~ $regex ]]; then + + # If target is a directory + if [[ -d "$1" ]]; then + echo "Cannot run ${bold}make${normal} on a directory. Did you mean to ${bold}cd ${1}${normal} first?" + return + fi + + # If target ends with .c + if [[ "$1" == *?.c ]]; then + base="${1%.c}" + if [[ -n "$base" && ! -d "$base" ]]; then + echo "Did you mean to ${bold}make ${base}${normal}?" + return + fi + fi + +fi + +local regex="No rule to make target '(.*)'" +if [[ "$output" =~ $regex ]]; then + + # If no .c file for target + local c="$1.c" + if [[ ! -f "$c" ]]; then + + # Search recursively for .c file + paths=$(find * -name "$c" 2> /dev/null) + lines=$(echo "$paths" | grep -c .) + echo -n "There isn't a file called ${bold}${c}${normal} in your current directory." + if [[ "$lines" -eq 1 ]]; then # If unambiguous + d=$(dirname "$paths") + echo " Did you mean to ${bold}cd ${d}${normal} first?" + else + echo + fi + return + fi +fi diff --git a/tests/bar.c b/tests/bar.c new file mode 100644 index 0000000..b552c8e --- /dev/null +++ b/tests/bar.c @@ -0,0 +1 @@ +int main(void) {} diff --git a/tests/foo/baz.c b/tests/foo/baz.c new file mode 100644 index 0000000..e69de29 From 1f25bf5be623035557cb276ec599f8359123f96e Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 18:35:02 -0500 Subject: [PATCH 02/85] tidying helper framework --- etc/profile.d/help50.sh | 52 +++++++++++++++++++++++++++---------- opt/cs50/bin/cp | 3 +++ opt/cs50/bin/curl | 3 +++ opt/cs50/bin/make | 0 opt/cs50/lib/help50/cd.sh | 9 ------- opt/cs50/lib/help50/ls.sh | 6 ----- opt/cs50/lib/help50/make.sh | 6 ++--- 7 files changed, 48 insertions(+), 31 deletions(-) create mode 100755 opt/cs50/bin/cp create mode 100755 opt/cs50/bin/curl mode change 100644 => 100755 opt/cs50/bin/make diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 836a037..e468df7 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,14 +1,21 @@ # Directory with helpers HELPERS="/opt/cs50/lib/help50" -# TEMP -alias make="help50 make" +# Temporary files +FILE="/tmp/help50.$$" # Use PID to support multiple terminals +HELP="${FILE}.help" +OUTPUT="${FILE}.output" + +# Supported helpers +#for helper in "$HELPERS"/*.sh; do +# command=$(basename "$helper" .sh) +# echo "$command"="help50 $command" +#done # Formatting bold=$(tput bold) normal=$(tput sgr0) - help50() { # Check for helper @@ -21,8 +28,7 @@ help50() { exec 3>&1 4>&2 # Redirect output to a file too - local file="/tmp/help50.$$" # Use PID to support multiple terminals - exec > >(tee -a "$file") 2>&1 + exec > >(tee -a "$FILE") 2>&1 # Execute command if [[ "$(type -P -t "$1")" == "file" ]]; then @@ -31,17 +37,15 @@ help50() { "$@" # Can't unbuffer builtins (e.g., cd) fi - # Remember status + # Remember these local status=$? - - # Remember command local command="$1" # Remove command from $@ shift - # Get redirected output - local output=$(cat "$file") + # Get tee'd output + local output=$(cat "$FILE") # Remove any ANSI codes output=$(echo "$output" | ansi2txt | col -b) @@ -52,15 +56,37 @@ help50() { # Close file descriptors exec 3>&- 4>&- - # Remove file + # Remove tee'd output rm --force "$file" - # Preserve command's status for helpers + # Restore $? to that of original command (exit $status) # Try to get help local help=$( . "${HELPERS}/${command}.sh" <<< "$output" ) if [[ -n "$help" ]]; then - echo "🦆 $help" + echo "$help" > "$HELP" + elif [[ $status -ne 0 ]]; then + echo "$output" > "$OUTPUT" + fi +} + +_help50() { + #history -a + if [[ -f "$HELP" ]]; then + echo -n "🦆 " + cat "$HELP" + rm --force "$HELP" + elif [[ -f "$OUTPUT" ]]; then + echo non-zero but no helper + rm --force "$OUTPUT" + else + echo zero fi } + +_help50() { + echo 222 +} + +export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/bin/cp b/opt/cs50/bin/cp new file mode 100755 index 0000000..9a2ab78 --- /dev/null +++ b/opt/cs50/bin/cp @@ -0,0 +1,3 @@ +#!/bin/bash + +cp --interactive "$@" diff --git a/opt/cs50/bin/curl b/opt/cs50/bin/curl new file mode 100755 index 0000000..0f6171c --- /dev/null +++ b/opt/cs50/bin/curl @@ -0,0 +1,3 @@ +#!/bin/bash + +curl --http2 "$@" diff --git a/opt/cs50/bin/make b/opt/cs50/bin/make old mode 100644 new mode 100755 diff --git a/opt/cs50/lib/help50/cd.sh b/opt/cs50/lib/help50/cd.sh index 01e9087..e69de29 100644 --- a/opt/cs50/lib/help50/cd.sh +++ b/opt/cs50/lib/help50/cd.sh @@ -1,9 +0,0 @@ -echo "EXIT: $?" -local stdin=$(cat) -#echo "stdout[$stdout]" -#echo "1[$1]" -#echo "2[$2]" -#echo "pwd[$PWD]" -#echo "STDOUT:$stdout" -echo "STDIN:$stdin" -return 0 diff --git a/opt/cs50/lib/help50/ls.sh b/opt/cs50/lib/help50/ls.sh index ce456b2..e69de29 100644 --- a/opt/cs50/lib/help50/ls.sh +++ b/opt/cs50/lib/help50/ls.sh @@ -1,6 +0,0 @@ -echo "argv:[$argv]" -echo "stdout[$stdout]" -echo "1[$1]" -echo "2[$2]" -echo "pwd[$PWD]" -return 0 diff --git a/opt/cs50/lib/help50/make.sh b/opt/cs50/lib/help50/make.sh index 5b64739..136c8f0 100644 --- a/opt/cs50/lib/help50/make.sh +++ b/opt/cs50/lib/help50/make.sh @@ -1,6 +1,6 @@ -local output=$(cat) +output=$(cat) -local regex="make: Nothing to be done for '(.*)'" +regex="make: Nothing to be done for '(.*)'" if [[ "$output" =~ $regex ]]; then # If target is a directory @@ -20,7 +20,7 @@ if [[ "$output" =~ $regex ]]; then fi -local regex="No rule to make target '(.*)'" +regex="No rule to make target '(.*)'" if [[ "$output" =~ $regex ]]; then # If no .c file for target From f92afe5216d9c2f939102c1e1776e19cdde1e7b3 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 20:05:18 -0500 Subject: [PATCH 03/85] using functions instead of aliases --- etc/profile.d/help50.sh | 20 +++++++++----------- opt/cs50/lib/help50/cd | 1 + opt/cs50/lib/help50/cd.sh | 0 opt/cs50/lib/help50/ls | 1 + opt/cs50/lib/help50/ls.sh | 0 opt/cs50/lib/help50/{make.sh => make} | 2 ++ 6 files changed, 13 insertions(+), 11 deletions(-) create mode 100755 opt/cs50/lib/help50/cd delete mode 100644 opt/cs50/lib/help50/cd.sh create mode 100755 opt/cs50/lib/help50/ls delete mode 100644 opt/cs50/lib/help50/ls.sh rename opt/cs50/lib/help50/{make.sh => make} (98%) mode change 100644 => 100755 diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index e468df7..3a023f5 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -6,11 +6,13 @@ FILE="/tmp/help50.$$" # Use PID to support multiple terminals HELP="${FILE}.help" OUTPUT="${FILE}.output" +alias cd="HOME=/tmp cd" + # Supported helpers -#for helper in "$HELPERS"/*.sh; do -# command=$(basename "$helper" .sh) -# echo "$command"="help50 $command" -#done +for helper in "$HELPERS"/*; do + name=$(basename "$helper") + eval "function ${name}() { help50 "$name" \"\$@\"; }" +done # Formatting bold=$(tput bold) @@ -19,7 +21,7 @@ normal=$(tput sgr0) help50() { # Check for helper - if [[ $# -gt 0 && ! -f "${HELPERS}/${1}.sh" ]]; then + if [[ $# -gt 0 && ! -f "${HELPERS}/${1}" ]]; then echo "Sorry, ${bold}help50${normal} does not yet know how to help with this!" return 1 fi @@ -34,7 +36,7 @@ help50() { if [[ "$(type -P -t "$1")" == "file" ]]; then unbuffer "$@" # Else, e.g., ls isn't colorized else - "$@" # Can't unbuffer builtins (e.g., cd) + command "$@" # Can't unbuffer builtins (e.g., cd) fi # Remember these @@ -63,7 +65,7 @@ help50() { (exit $status) # Try to get help - local help=$( . "${HELPERS}/${command}.sh" <<< "$output" ) + local help=$( . "${HELPERS}/${command}" <<< "$output" ) if [[ -n "$help" ]]; then echo "$help" > "$HELP" elif [[ $status -ne 0 ]]; then @@ -85,8 +87,4 @@ _help50() { fi } -_help50() { - echo 222 -} - export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/lib/help50/cd b/opt/cs50/lib/help50/cd new file mode 100755 index 0000000..a9bf588 --- /dev/null +++ b/opt/cs50/lib/help50/cd @@ -0,0 +1 @@ +#!/bin/bash diff --git a/opt/cs50/lib/help50/cd.sh b/opt/cs50/lib/help50/cd.sh deleted file mode 100644 index e69de29..0000000 diff --git a/opt/cs50/lib/help50/ls b/opt/cs50/lib/help50/ls new file mode 100755 index 0000000..a9bf588 --- /dev/null +++ b/opt/cs50/lib/help50/ls @@ -0,0 +1 @@ +#!/bin/bash diff --git a/opt/cs50/lib/help50/ls.sh b/opt/cs50/lib/help50/ls.sh deleted file mode 100644 index e69de29..0000000 diff --git a/opt/cs50/lib/help50/make.sh b/opt/cs50/lib/help50/make old mode 100644 new mode 100755 similarity index 98% rename from opt/cs50/lib/help50/make.sh rename to opt/cs50/lib/help50/make index 136c8f0..c6464be --- a/opt/cs50/lib/help50/make.sh +++ b/opt/cs50/lib/help50/make @@ -1,3 +1,5 @@ +#!/bin/bash + output=$(cat) regex="make: Nothing to be done for '(.*)'" From 5470fcf80b6c424c2ef6b9fd7dc9a14764a37701 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 20:09:13 -0500 Subject: [PATCH 04/85] removed support for exit statuses, supporting any executable helper --- etc/profile.d/help50.sh | 7 +------ opt/cs50/lib/help50/make | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 3a023f5..7647bd8 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -6,8 +6,6 @@ FILE="/tmp/help50.$$" # Use PID to support multiple terminals HELP="${FILE}.help" OUTPUT="${FILE}.output" -alias cd="HOME=/tmp cd" - # Supported helpers for helper in "$HELPERS"/*; do name=$(basename "$helper") @@ -61,11 +59,8 @@ help50() { # Remove tee'd output rm --force "$file" - # Restore $? to that of original command - (exit $status) - # Try to get help - local help=$( . "${HELPERS}/${command}" <<< "$output" ) + local help=$("${HELPERS}/${command}" "$@" <<< "$output") if [[ -n "$help" ]]; then echo "$help" > "$HELP" elif [[ $status -ne 0 ]]; then diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index c6464be..9cbb813 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -8,7 +8,7 @@ if [[ "$output" =~ $regex ]]; then # If target is a directory if [[ -d "$1" ]]; then echo "Cannot run ${bold}make${normal} on a directory. Did you mean to ${bold}cd ${1}${normal} first?" - return + exit fi # If target ends with .c @@ -16,7 +16,7 @@ if [[ "$output" =~ $regex ]]; then base="${1%.c}" if [[ -n "$base" && ! -d "$base" ]]; then echo "Did you mean to ${bold}make ${base}${normal}?" - return + exit fi fi @@ -39,6 +39,6 @@ if [[ "$output" =~ $regex ]]; then else echo fi - return + exit fi fi From c4b6fbefba45daebfaa87e768899c3420ff5dcf5 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 20:43:01 -0500 Subject: [PATCH 05/85] added PIPESTATUS for helpers --- etc/profile.d/help50.sh | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 7647bd8..2d5e8dd 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -18,12 +18,6 @@ normal=$(tput sgr0) help50() { - # Check for helper - if [[ $# -gt 0 && ! -f "${HELPERS}/${1}" ]]; then - echo "Sorry, ${bold}help50${normal} does not yet know how to help with this!" - return 1 - fi - # Duplicate file descriptors exec 3>&1 4>&2 @@ -60,7 +54,10 @@ help50() { rm --force "$file" # Try to get help - local help=$("${HELPERS}/${command}" "$@" <<< "$output") + local helper="${HELPERS}/${command}" + if [[ -f "$helper" && -x "$helper" ]]; then + local help=$(PIPESTATUS=($status) "$helper" "$@" <<< "$output") + fi if [[ -n "$help" ]]; then echo "$help" > "$HELP" elif [[ $status -ne 0 ]]; then From 8ee1e52500efcf630802e053c8c01cb97e48b123 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 21:05:28 -0500 Subject: [PATCH 06/85] removed PIPESTATUS --- etc/profile.d/help50.sh | 2 +- foo.sh | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100755 foo.sh diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 2d5e8dd..1ee222d 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -56,7 +56,7 @@ help50() { # Try to get help local helper="${HELPERS}/${command}" if [[ -f "$helper" && -x "$helper" ]]; then - local help=$(PIPESTATUS=($status) "$helper" "$@" <<< "$output") + local help=$("$helper" "$@" <<< "$output") fi if [[ -n "$help" ]]; then echo "$help" > "$HELP" diff --git a/foo.sh b/foo.sh new file mode 100755 index 0000000..f96cda6 --- /dev/null +++ b/foo.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +x=(a b c) +y=foo +export x +export y +env From b003947a79048988fedfb60c670903ead63ae408 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 21:05:39 -0500 Subject: [PATCH 07/85] removed test file --- foo.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 foo.sh diff --git a/foo.sh b/foo.sh deleted file mode 100755 index f96cda6..0000000 --- a/foo.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -x=(a b c) -y=foo -export x -export y -env From 9279b1dba4564a8da0c28cd81d600cb2212e7331 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 22:00:55 -0500 Subject: [PATCH 08/85] added _helpful, _helpless --- etc/profile.d/help50.sh | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 1ee222d..88bf7c0 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -3,8 +3,8 @@ HELPERS="/opt/cs50/lib/help50" # Temporary files FILE="/tmp/help50.$$" # Use PID to support multiple terminals -HELP="${FILE}.help" -OUTPUT="${FILE}.output" +HELPFUL="${FILE}.help" +HELPLESS="${FILE}.output" # Supported helpers for helper in "$HELPERS"/*; do @@ -12,10 +12,6 @@ for helper in "$HELPERS"/*; do eval "function ${name}() { help50 "$name" \"\$@\"; }" done -# Formatting -bold=$(tput bold) -normal=$(tput sgr0) - help50() { # Duplicate file descriptors @@ -58,25 +54,28 @@ help50() { if [[ -f "$helper" && -x "$helper" ]]; then local help=$("$helper" "$@" <<< "$output") fi - if [[ -n "$help" ]]; then - echo "$help" > "$HELP" - elif [[ $status -ne 0 ]]; then - echo "$output" > "$OUTPUT" + if [[ -n "$help" ]]; then # If helpful + echo "$help" > "$HELPFUL" + elif [[ $status -ne 0 ]]; then # If helpless + echo "$output" > "$HELPLESS" fi } _help50() { - #history -a - if [[ -f "$HELP" ]]; then - echo -n "🦆 " - cat "$HELP" - rm --force "$HELP" - elif [[ -f "$OUTPUT" ]]; then - echo non-zero but no helper - rm --force "$OUTPUT" - else - echo zero + if [[ -f "$HELPFUL" ]]; then + _helpful "$HELPFUL" + elif [[ -f "$HELPLESS" ]]; then + _helpless "$HELPLESS" fi + rm --force "$HELPFUL" "$HELPLESS" } -export PROMPT_COMMAND=_help50 +_helpful() { + echo -n "🦆 " + # https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 + cat "$1" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" +} + +_helpless() { :; } + +export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_help50" From 26e431d940bda3dd4098661fbc058392cfee1071 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 22:01:01 -0500 Subject: [PATCH 09/85] removed, updated wrappers --- opt/cs50/bin/cp | 3 --- opt/cs50/bin/curl | 3 --- opt/cs50/lib/help50/make | 8 ++++---- 3 files changed, 4 insertions(+), 10 deletions(-) delete mode 100755 opt/cs50/bin/cp delete mode 100755 opt/cs50/bin/curl diff --git a/opt/cs50/bin/cp b/opt/cs50/bin/cp deleted file mode 100755 index 9a2ab78..0000000 --- a/opt/cs50/bin/cp +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cp --interactive "$@" diff --git a/opt/cs50/bin/curl b/opt/cs50/bin/curl deleted file mode 100755 index 0f6171c..0000000 --- a/opt/cs50/bin/curl +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -curl --http2 "$@" diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 9cbb813..2b89551 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -7,7 +7,7 @@ if [[ "$output" =~ $regex ]]; then # If target is a directory if [[ -d "$1" ]]; then - echo "Cannot run ${bold}make${normal} on a directory. Did you mean to ${bold}cd ${1}${normal} first?" + echo "Cannot run \`make\` on a directory. Did you mean to \`cd ${1}\` first?" exit fi @@ -15,7 +15,7 @@ if [[ "$output" =~ $regex ]]; then if [[ "$1" == *?.c ]]; then base="${1%.c}" if [[ -n "$base" && ! -d "$base" ]]; then - echo "Did you mean to ${bold}make ${base}${normal}?" + echo "Did you mean to \`make ${base}\`?" exit fi fi @@ -32,10 +32,10 @@ if [[ "$output" =~ $regex ]]; then # Search recursively for .c file paths=$(find * -name "$c" 2> /dev/null) lines=$(echo "$paths" | grep -c .) - echo -n "There isn't a file called ${bold}${c}${normal} in your current directory." + echo -n "There isn't a file called \`${c}\` in your current directory." if [[ "$lines" -eq 1 ]]; then # If unambiguous d=$(dirname "$paths") - echo " Did you mean to ${bold}cd ${d}${normal} first?" + echo " Did you mean to \`cd ${d}\` first?" else echo fi From a0783ce10bcd0a8c7e0faf276ce7d7aaebcf9b7b Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 22:15:14 -0500 Subject: [PATCH 10/85] updated formatting --- opt/cs50/bin/http-server | 8 ++++---- opt/cs50/bin/sqlite3 | 10 +++++----- opt/cs50/bin/valgrind | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/opt/cs50/bin/http-server b/opt/cs50/bin/http-server index a0f1c26..120cd75 100755 --- a/opt/cs50/bin/http-server +++ b/opt/cs50/bin/http-server @@ -10,12 +10,12 @@ options="--no-dotfiles" t="-t0" # Formatting -bold=$(tput bold) -normal=$(tput sgr0) +smso=$(tput smso) +rmso=$(tput sgr0) # Check for app.py or wsgi.py if [[ -f app.py ]] || [[ -f wsgi.py ]]; then - read -p "Are you sure you want to run ${bold}http-server${normal} and not ${bold}flask${normal}? [y/N] " -r + read -p "Are you sure you want to run ${smso}http-server${rmso} and not ${smso}flask${rmso}? [y/N] " -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi @@ -23,7 +23,7 @@ fi # Check for path if [[ $# -eq 1 ]] && [[ $1 != -* ]] && [[ ! $1 =~ ^\./?$ ]]; then - read -p "Are you sure you want to serve ${bold}${1}${normal} and not your current directory? [y/N] " -r + read -p "Are you sure you want to serve ${smso}${1}${rmso} and not your current directory? [y/N] " -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi diff --git a/opt/cs50/bin/sqlite3 b/opt/cs50/bin/sqlite3 index 3c1c0e7..1aa8f93 100755 --- a/opt/cs50/bin/sqlite3 +++ b/opt/cs50/bin/sqlite3 @@ -1,8 +1,8 @@ #!/bin/bash # Formatting -bold=$(tput bold) -normal=$(tput sgr0) +smso=$(tput smso) +rmso=$(tput sgr0) # If data is coming from stdin (pipe or redirection) if [[ -p /dev/stdin || ! -t 0 ]]; then @@ -12,7 +12,7 @@ fi # If no command-line argument if [[ $# -eq 0 ]]; then - read -p "Are you sure you want to run ${bold}sqlite3${normal} without a command-line argument (e.g., the filename of a database)? [y/N] " -r + read -p "Are you sure you want to run ${smso}sqlite3${rmso} without a command-line argument (e.g., the filename of a database)? [y/N] " -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi @@ -21,12 +21,12 @@ if [[ $# -eq 0 ]]; then elif [[ $# -eq 1 ]] && [[ ! "$1" =~ ^- ]]; then if [[ ! -f "$1" ]]; then if [[ ! "$1" =~ \.db$ ]]; then - read -p "Are you sure you want to create ${bold}$1${normal}? SQLite filenames usually end in ${bold}.db${normal}. [y/N] " -r + read -p "Are you sure you want to create ${smso}$1${rmso}? SQLite filenames usually end in ${smso}.db${rmso}. [y/N] " -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi else - read -p "Are you sure you want to create ${bold}$1${normal}? [y/N] " -r + read -p "Are you sure you want to create ${smso}$1${rmso}? [y/N] " -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi diff --git a/opt/cs50/bin/valgrind b/opt/cs50/bin/valgrind index 60a6429..2ed1e53 100755 --- a/opt/cs50/bin/valgrind +++ b/opt/cs50/bin/valgrind @@ -1,12 +1,12 @@ #!/bin/bash # Formatting -bold=$(tput bold) -normal=$(tput sgr0) +smso=$(tput smso) +rmso=$(tput sgr0) # If run on Python program if [[ "$1" == "python" || "$1" == *.py ]]; then - echo "Afraid ${bold}valgrind${normal} does not support Python programs!" + echo "Afraid ${smso}valgrind${rmso} does not support Python programs!" exit 1 fi From cadd89002b0b1c659cb2a8d92b91d0af0fd53053 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 22:38:26 -0500 Subject: [PATCH 11/85] added _help function to standardize formatting --- etc/profile.d/help50.sh | 3 +-- opt/cs50/bin/_help | 13 +++++++++++++ opt/cs50/bin/http-server | 8 ++------ opt/cs50/bin/sqlite3 | 10 +++------- opt/cs50/bin/valgrind | 6 +----- 5 files changed, 20 insertions(+), 20 deletions(-) create mode 100755 opt/cs50/bin/_help diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 88bf7c0..7a4b37e 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -72,8 +72,7 @@ _help50() { _helpful() { echo -n "🦆 " - # https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 - cat "$1" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" + cat "$1" | _help } _helpless() { :; } diff --git a/opt/cs50/bin/_help b/opt/cs50/bin/_help new file mode 100755 index 0000000..fc7734b --- /dev/null +++ b/opt/cs50/bin/_help @@ -0,0 +1,13 @@ +#!/bin/bash + +# If command-line arguments +if [[ -t 0 ]]; then + input="$*" + +# If standard input +else + input=$(cat) +fi + +# https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 +echo "$input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" diff --git a/opt/cs50/bin/http-server b/opt/cs50/bin/http-server index 120cd75..dcb9369 100755 --- a/opt/cs50/bin/http-server +++ b/opt/cs50/bin/http-server @@ -9,13 +9,9 @@ port="-p 8080" options="--no-dotfiles" t="-t0" -# Formatting -smso=$(tput smso) -rmso=$(tput sgr0) - # Check for app.py or wsgi.py if [[ -f app.py ]] || [[ -f wsgi.py ]]; then - read -p "Are you sure you want to run ${smso}http-server${rmso} and not ${smso}flask${rmso}? [y/N] " -r + read -p "$(_help "Are you sure you want to run \`http-server\` and not \`flask\`? [y/N] ")" -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi @@ -23,7 +19,7 @@ fi # Check for path if [[ $# -eq 1 ]] && [[ $1 != -* ]] && [[ ! $1 =~ ^\./?$ ]]; then - read -p "Are you sure you want to serve ${smso}${1}${rmso} and not your current directory? [y/N] " -r + read -p "$(_help "Are you sure you want to serve \`${1}\` and not your current directory? [y/N] ")" -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi diff --git a/opt/cs50/bin/sqlite3 b/opt/cs50/bin/sqlite3 index 1aa8f93..d59bcd5 100755 --- a/opt/cs50/bin/sqlite3 +++ b/opt/cs50/bin/sqlite3 @@ -1,9 +1,5 @@ #!/bin/bash -# Formatting -smso=$(tput smso) -rmso=$(tput sgr0) - # If data is coming from stdin (pipe or redirection) if [[ -p /dev/stdin || ! -t 0 ]]; then /usr/local/bin/sqlite3 -nullvalue NULL -table "$@" < /dev/stdin @@ -12,7 +8,7 @@ fi # If no command-line argument if [[ $# -eq 0 ]]; then - read -p "Are you sure you want to run ${smso}sqlite3${rmso} without a command-line argument (e.g., the filename of a database)? [y/N] " -r + read -p "$(_help "Are you sure you want to run \`sqlite3\` without a command-line argument (e.g., the filename of a database)? [y/N] ")" -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi @@ -21,12 +17,12 @@ if [[ $# -eq 0 ]]; then elif [[ $# -eq 1 ]] && [[ ! "$1" =~ ^- ]]; then if [[ ! -f "$1" ]]; then if [[ ! "$1" =~ \.db$ ]]; then - read -p "Are you sure you want to create ${smso}$1${rmso}? SQLite filenames usually end in ${smso}.db${rmso}. [y/N] " -r + read -p "$(_help "Are you sure you want to create \`$1\`? SQLite filenames usually end in \`.db\`. [y/N] ")" -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi else - read -p "Are you sure you want to create ${smso}$1${rmso}? [y/N] " -r + read -p "$(_help "Are you sure you want to create \`$1\`? [y/N] ")" -r if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then exit 1 fi diff --git a/opt/cs50/bin/valgrind b/opt/cs50/bin/valgrind index 2ed1e53..dd19aa8 100755 --- a/opt/cs50/bin/valgrind +++ b/opt/cs50/bin/valgrind @@ -1,12 +1,8 @@ #!/bin/bash -# Formatting -smso=$(tput smso) -rmso=$(tput sgr0) - # If run on Python program if [[ "$1" == "python" || "$1" == *.py ]]; then - echo "Afraid ${smso}valgrind${rmso} does not support Python programs!" + echo "$(_help "Afraid \`valgrind\` does not support Python programs!")" exit 1 fi From 3041bf96b7ed0c566444ab68b2661444a3d3eeb7 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 5 Jan 2024 22:48:22 -0500 Subject: [PATCH 12/85] added on/off --- etc/profile.d/help50.sh | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 7a4b37e..3605fad 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -62,10 +62,12 @@ help50() { } _help50() { - if [[ -f "$HELPFUL" ]]; then - _helpful "$HELPFUL" - elif [[ -f "$HELPLESS" ]]; then - _helpless "$HELPLESS" + if [[ "$RUBBERDUCKING" != "0" ]]; then + if [[ -f "$HELPFUL" ]]; then + _helpful "$HELPFUL" + elif [[ -f "$HELPLESS" ]]; then + _helpless "$HELPLESS" + fi fi rm --force "$HELPFUL" "$HELPLESS" } @@ -77,4 +79,19 @@ _helpful() { _helpless() { :; } +duck() { + if [[ "$1" == "off" ]]; then + export RUBBERDUCKING=0 + elif [[ "$1" == "on" ]]; then + unset RUBBERDUCKING + elif [[ "$1" == "status" ]]; then + if [[ "$RUBBERDUCKING" == "0" ]]; then + echo "off" + else + echo "on" + fi + fi +} + export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_help50" +duck on From a3c55b3c6425300740004974e700e9e7df826e74 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 9 Jan 2024 08:26:42 -0500 Subject: [PATCH 13/85] added icon to _help --- opt/cs50/bin/_help | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opt/cs50/bin/_help b/opt/cs50/bin/_help index fc7734b..7d6ef4f 100755 --- a/opt/cs50/bin/_help +++ b/opt/cs50/bin/_help @@ -10,4 +10,4 @@ else fi # https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 -echo "$input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" +echo "⚠️ $input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" From 7f315ae7500dbd7c6096531d401555432d6b4403 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 17 Jan 2024 13:39:15 -0500 Subject: [PATCH 14/85] WIP --- etc/profile.d/help50.sh | 13 +++++++++---- opt/cs50/bin/{_help => _ansi} | 2 +- opt/cs50/bin/_sure | 14 ++++++++++++++ opt/cs50/bin/http-server | 6 ++---- opt/cs50/bin/sqlite3 | 9 +++------ opt/cs50/lib/help50/bash | 13 +++++++++++++ typescript | 13 +++++++++++++ 7 files changed, 55 insertions(+), 15 deletions(-) rename opt/cs50/bin/{_help => _ansi} (73%) create mode 100755 opt/cs50/bin/_sure create mode 100755 opt/cs50/lib/help50/bash create mode 100644 typescript diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 3605fad..92885c8 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,6 +1,11 @@ # Directory with helpers HELPERS="/opt/cs50/lib/help50" +# Disable yes, lest students type it at prompt +if command -v yes &> /dev/null; then + alias yes=":" +fi + # Temporary files FILE="/tmp/help50.$$" # Use PID to support multiple terminals HELPFUL="${FILE}.help" @@ -51,6 +56,7 @@ help50() { # Try to get help local helper="${HELPERS}/${command}" + echo "HELPER: $helper" if [[ -f "$helper" && -x "$helper" ]]; then local help=$("$helper" "$@" <<< "$output") fi @@ -64,17 +70,16 @@ help50() { _help50() { if [[ "$RUBBERDUCKING" != "0" ]]; then if [[ -f "$HELPFUL" ]]; then - _helpful "$HELPFUL" + _helpful "$(cat "$HELPFUL")" elif [[ -f "$HELPLESS" ]]; then - _helpless "$HELPLESS" + _helpless "$(cat "$HELPLESS")" fi fi rm --force "$HELPFUL" "$HELPLESS" } _helpful() { - echo -n "🦆 " - cat "$1" | _help + echo "$1" } _helpless() { :; } diff --git a/opt/cs50/bin/_help b/opt/cs50/bin/_ansi similarity index 73% rename from opt/cs50/bin/_help rename to opt/cs50/bin/_ansi index 7d6ef4f..fc7734b 100755 --- a/opt/cs50/bin/_help +++ b/opt/cs50/bin/_ansi @@ -10,4 +10,4 @@ else fi # https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 -echo "⚠️ $input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" +echo "$input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" diff --git a/opt/cs50/bin/_sure b/opt/cs50/bin/_sure new file mode 100755 index 0000000..74f688a --- /dev/null +++ b/opt/cs50/bin/_sure @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ $# -ne 1 ]]; then + exit 1 +fi +prompt=$(echo "$1" | _ansi) +while true; do + read -p "$prompt [y/N] " -r + if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then + exit 0 + elif [[ "${REPLY,,}" =~ ^(n|no)$ ]]; then + exit 1 + fi +done diff --git a/opt/cs50/bin/http-server b/opt/cs50/bin/http-server index dcb9369..7bbcd9e 100755 --- a/opt/cs50/bin/http-server +++ b/opt/cs50/bin/http-server @@ -11,16 +11,14 @@ t="-t0" # Check for app.py or wsgi.py if [[ -f app.py ]] || [[ -f wsgi.py ]]; then - read -p "$(_help "Are you sure you want to run \`http-server\` and not \`flask\`? [y/N] ")" -r - if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then + if ! _sure "Are you sure you want to run \`http-server\` and not \`flask\`?"; then exit 1 fi fi # Check for path if [[ $# -eq 1 ]] && [[ $1 != -* ]] && [[ ! $1 =~ ^\./?$ ]]; then - read -p "$(_help "Are you sure you want to serve \`${1}\` and not your current directory? [y/N] ")" -r - if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then + if ! _sure "Are you sure you want to serve \`${1}\` and not your current directory?"; then exit 1 fi fi diff --git a/opt/cs50/bin/sqlite3 b/opt/cs50/bin/sqlite3 index d59bcd5..80557d8 100755 --- a/opt/cs50/bin/sqlite3 +++ b/opt/cs50/bin/sqlite3 @@ -8,8 +8,7 @@ fi # If no command-line argument if [[ $# -eq 0 ]]; then - read -p "$(_help "Are you sure you want to run \`sqlite3\` without a command-line argument (e.g., the filename of a database)? [y/N] ")" -r - if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then + if ! _sure "Are you sure you want to run \`sqlite3\` without a command-line argument (e.g., the filename of a database)?"; then exit 1 fi @@ -17,13 +16,11 @@ if [[ $# -eq 0 ]]; then elif [[ $# -eq 1 ]] && [[ ! "$1" =~ ^- ]]; then if [[ ! -f "$1" ]]; then if [[ ! "$1" =~ \.db$ ]]; then - read -p "$(_help "Are you sure you want to create \`$1\`? SQLite filenames usually end in \`.db\`. [y/N] ")" -r - if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then + if ! _sure "Are you sure you want to create \`$1\`? SQLite filenames usually end in \`.db\`."; then exit 1 fi else - read -p "$(_help "Are you sure you want to create \`$1\`? [y/N] ")" -r - if [[ ! "${REPLY,,}" =~ ^y|yes$ ]]; then + if ! _sure "Are you sure you want to create \`$1\`?"; then exit 1 fi fi diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash new file mode 100755 index 0000000..f34adec --- /dev/null +++ b/opt/cs50/lib/help50/bash @@ -0,0 +1,13 @@ +#!/bin/bash + +output=$(cat) + +regex="bash: (.*).py: command not found" +if [[ "$output" =~ $regex ]]; then + + # If target is a directory + if [[ -f "$1" ]]; then + echo "Did you mean to run \`python ${1}`?" + exit + fi +fi diff --git a/typescript b/typescript new file mode 100644 index 0000000..e196aef --- /dev/null +++ b/typescript @@ -0,0 +1,13 @@ +Script started on 2024-01-09 14:43:54+00:00 [TERM="xterm" TTY="/dev/pts/0" COLUMNS="130" LINES="33"] +$ vi +[?1049h[>4;2m[?1h=[?2004h[?1004h[?12h[?12l▽ Pzz\[0%m [>c]10;?]11;? [No Name]  [?25l~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~  [No Name] CWD: /home/ubuntu Line: 0 VIM - Vi IMprovedversion 8.2.2121by Bram Moolenaar et al.Modified by team+vim@tracker.debian.orgVim is open source and freely distributableSponsor Vim development!type :help sponsor for information type :q to exit type :help or  for on-line helptype :help version8 for version info[?25h[?25l::[?25hq [?25l +[?2004l[>4;m[?1004l[?2004l[?1l>[?25h[>4;m[?1049l$ ls +Dockerfile etc foo.py index.js.patch LICENSE Makefile opt shell.c.patch tests tmp typescript +$ scrip^C +$ vi +[?1049h[>4;2m[?1h=[?2004h[?1004h[?12h[?12l▽ Pzz\[0%m [>c]10;?]11;? [No Name]  [?25l~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~  [No Name] CWD: /home/ubuntu Line: 0 VIM - Vi IMprovedversion 8.2.2121by Bram Moolenaar et al.Modified by team+vim@tracker.debian.orgVim is open source and freely distributableHelp poor children in Uganda!type :help iccf for information type :q to exit type :help or  for on-line helptype :help version8 for version info[?25h[?25l^M [?25h[?25l^M [?25h[?25la -- INSERT --[?25h+N o[No Name] [?25l          [+] CWD: /home/ubuntu Line: 2[?25h[?25l3 [?25h[?25l4 [?25h[?25l5 [?25h[?25l^[ [?25h[?25l::[?25hwq [?25lE32: No file name[?25h[?25ll [?25h[?25ls cl -- INSERT --[?25h[?25l^[ [?25h[?25l::[?25hwq! [?25lE32: No file name[?25h[?25l::[?25hw foo [?25l"foo" [New] 5L, 5B writtenfooN o ~/foo CWD: /home/ubuntu Line: 5 [?25h[?25l::[?25hq [?25l +[?2004l[>4;m[?1004l[?2004l[?1l>[?25h[>4;m[?1049l$ ls +Dockerfile etc foo foo.py index.js.patch LICENSE Makefile opt shell.c.patch tests tmp typescript +$ l  + +Script done on 2024-01-09 14:44:16+00:00 [COMMAND_EXIT_CODE="0"] From 5c70d230fa26a022e60a7a3b63cb4140aa0005c2 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 17 Jan 2024 13:40:41 -0500 Subject: [PATCH 15/85] WIP --- typescript | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 typescript diff --git a/typescript b/typescript deleted file mode 100644 index e196aef..0000000 --- a/typescript +++ /dev/null @@ -1,13 +0,0 @@ -Script started on 2024-01-09 14:43:54+00:00 [TERM="xterm" TTY="/dev/pts/0" COLUMNS="130" LINES="33"] -$ vi -[?1049h[>4;2m[?1h=[?2004h[?1004h[?12h[?12l▽ Pzz\[0%m [>c]10;?]11;? [No Name]  [?25l~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~  [No Name] CWD: /home/ubuntu Line: 0 VIM - Vi IMprovedversion 8.2.2121by Bram Moolenaar et al.Modified by team+vim@tracker.debian.orgVim is open source and freely distributableSponsor Vim development!type :help sponsor for information type :q to exit type :help or  for on-line helptype :help version8 for version info[?25h[?25l::[?25hq [?25l -[?2004l[>4;m[?1004l[?2004l[?1l>[?25h[>4;m[?1049l$ ls -Dockerfile etc foo.py index.js.patch LICENSE Makefile opt shell.c.patch tests tmp typescript -$ scrip^C -$ vi -[?1049h[>4;2m[?1h=[?2004h[?1004h[?12h[?12l▽ Pzz\[0%m [>c]10;?]11;? [No Name]  [?25l~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~  [No Name] CWD: /home/ubuntu Line: 0 VIM - Vi IMprovedversion 8.2.2121by Bram Moolenaar et al.Modified by team+vim@tracker.debian.orgVim is open source and freely distributableHelp poor children in Uganda!type :help iccf for information type :q to exit type :help or  for on-line helptype :help version8 for version info[?25h[?25l^M [?25h[?25l^M [?25h[?25la -- INSERT --[?25h+N o[No Name] [?25l          [+] CWD: /home/ubuntu Line: 2[?25h[?25l3 [?25h[?25l4 [?25h[?25l5 [?25h[?25l^[ [?25h[?25l::[?25hwq [?25lE32: No file name[?25h[?25ll [?25h[?25ls cl -- INSERT --[?25h[?25l^[ [?25h[?25l::[?25hwq! [?25lE32: No file name[?25h[?25l::[?25hw foo [?25l"foo" [New] 5L, 5B writtenfooN o ~/foo CWD: /home/ubuntu Line: 5 [?25h[?25l::[?25hq [?25l -[?2004l[>4;m[?1004l[?2004l[?1l>[?25h[>4;m[?1049l$ ls -Dockerfile etc foo foo.py index.js.patch LICENSE Makefile opt shell.c.patch tests tmp typescript -$ l  - -Script done on 2024-01-09 14:44:16+00:00 [COMMAND_EXIT_CODE="0"] From 36c860c6d338c065696747a148ef8721891783fc Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 13:42:53 -0500 Subject: [PATCH 16/85] WIP --- etc/profile.d/help50.sh | 157 ++++++++++++++++++------------------ etc/profile.d/help50.sh.old | 102 +++++++++++++++++++++++ opt/cs50/bin/make | 4 +- opt/cs50/lib/help50/bash | 4 +- opt/cs50/lib/help50/make | 8 +- 5 files changed, 189 insertions(+), 86 deletions(-) create mode 100644 etc/profile.d/help50.sh.old diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 92885c8..8eda6ed 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,102 +1,101 @@ # Directory with helpers HELPERS="/opt/cs50/lib/help50" -# Disable yes, lest students type it at prompt +# Disable yes, lest users type it at prompt if command -v yes &> /dev/null; then alias yes=":" fi -# Temporary files -FILE="/tmp/help50.$$" # Use PID to support multiple terminals -HELPFUL="${FILE}.help" -HELPLESS="${FILE}.output" +function _help50 () { -# Supported helpers -for helper in "$HELPERS"/*; do - name=$(basename "$helper") - eval "function ${name}() { help50 "$name" \"\$@\"; }" -done - -help50() { - - # Duplicate file descriptors - exec 3>&1 4>&2 - - # Redirect output to a file too - exec > >(tee -a "$FILE") 2>&1 - - # Execute command - if [[ "$(type -P -t "$1")" == "file" ]]; then - unbuffer "$@" # Else, e.g., ls isn't colorized - else - command "$@" # Can't unbuffer builtins (e.g., cd) - fi - - # Remember these + # Exit status of last command local status=$? - local command="$1" - - # Remove command from $@ - shift - # Get tee'd output - local output=$(cat "$FILE") + # Last command + local argv=$(fc -ln -1 | sed 's/^[[:space:]]*//') + # TODO: extract argv0 to determine if ./ command - # Remove any ANSI codes - output=$(echo "$output" | ansi2txt | col -b) + # If no typescript yet + if [[ -z "$SCRIPT" ]]; then - # Restore file descriptors - exec 1>&3 2>&4 + # Use this shell's PID as typescript's name, exporting so that subshells know script is already running + export SCRIPT="/tmp/help50.$$" - # Close file descriptors - exec 3>&- 4>&- + # Make a typescript of everything displayed in terminal (without using exec, which breaks sudo); + # --append avoids `bash: warning: command substitution: ignored null byte in input`; + # --quiet suppresses `Script started...` + script --append --command "bash --login" --flush --quiet "$SCRIPT" - # Remove tee'd output - rm --force "$file" + # Remove typescript before exiting this shell + rm --force "$SCRIPT" - # Try to get help - local helper="${HELPERS}/${command}" - echo "HELPER: $helper" - if [[ -f "$helper" && -x "$helper" ]]; then - local help=$("$helper" "$@" <<< "$output") + # Now exit this shell too + exit fi - if [[ -n "$help" ]]; then # If helpful - echo "$help" > "$HELPFUL" - elif [[ $status -ne 0 ]]; then # If helpless - echo "$output" > "$HELPLESS" - fi -} -_help50() { - if [[ "$RUBBERDUCKING" != "0" ]]; then - if [[ -f "$HELPFUL" ]]; then - _helpful "$(cat "$HELPFUL")" - elif [[ -f "$HELPLESS" ]]; then - _helpless "$(cat "$HELPLESS")" + # If last command erred + if [[ $status -ne 0 ]]; then + + # Read typescript from disk + local typescript=$(cat "$SCRIPT") + + # Remove script's own output (if this is user's first command) + typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}') + + # Remove any line continuations from command line + local lines="" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -z $done && $line =~ \\$ ]]; then + lines+="${line%\\}" + else + lines+="$line"$'\n' + local done=1 + fi + done <<< "$typescript" + typescript="$lines" + + # Remove command line from typescript + typescript=$(echo "$typescript" | sed '1d') + + # Remove ANSI characters + typescript=$(echo "$typescript" | ansi2txt) + + # Remove control characters + # https://superuser.com/a/237154 + typescript=$(echo "$typescript" | col -bp) + + # Try to get help + for helper in "$HELPERS"/*; do + if [[ -f "$helper" && -x "$helper" ]]; then + local help=$("$helper" <<< "$typescript") + if [[ -n "$help" ]]; then + break + fi + fi + done + if [[ -n "$help" ]]; then # If helpful + _helpful "$help" + elif [[ $status -ne 0 ]]; then # If helpless + _helpless "$text" fi + + # TEMP + echo "TEXT: $typescript" >> "$SCRIPT.log" + echo "---" >> "$SCRIPT.log" fi - rm --force "$HELPFUL" "$HELPLESS" -} -_helpful() { - echo "$1" + # Truncate typescript + truncate -s 0 "$SCRIPT" } -_helpless() { :; } - -duck() { - if [[ "$1" == "off" ]]; then - export RUBBERDUCKING=0 - elif [[ "$1" == "on" ]]; then - unset RUBBERDUCKING - elif [[ "$1" == "status" ]]; then - if [[ "$RUBBERDUCKING" == "0" ]]; then - echo "off" - else - echo "on" - fi - fi -} +if ! type _helpful >/dev/null 2>&1; then + function _helpful() { + echo "$1" + } +fi + +if ! type _helpless >/dev/null 2>&1; then + function _helpless() { :; } +fi -export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_help50" -duck on +export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" diff --git a/etc/profile.d/help50.sh.old b/etc/profile.d/help50.sh.old new file mode 100644 index 0000000..92885c8 --- /dev/null +++ b/etc/profile.d/help50.sh.old @@ -0,0 +1,102 @@ +# Directory with helpers +HELPERS="/opt/cs50/lib/help50" + +# Disable yes, lest students type it at prompt +if command -v yes &> /dev/null; then + alias yes=":" +fi + +# Temporary files +FILE="/tmp/help50.$$" # Use PID to support multiple terminals +HELPFUL="${FILE}.help" +HELPLESS="${FILE}.output" + +# Supported helpers +for helper in "$HELPERS"/*; do + name=$(basename "$helper") + eval "function ${name}() { help50 "$name" \"\$@\"; }" +done + +help50() { + + # Duplicate file descriptors + exec 3>&1 4>&2 + + # Redirect output to a file too + exec > >(tee -a "$FILE") 2>&1 + + # Execute command + if [[ "$(type -P -t "$1")" == "file" ]]; then + unbuffer "$@" # Else, e.g., ls isn't colorized + else + command "$@" # Can't unbuffer builtins (e.g., cd) + fi + + # Remember these + local status=$? + local command="$1" + + # Remove command from $@ + shift + + # Get tee'd output + local output=$(cat "$FILE") + + # Remove any ANSI codes + output=$(echo "$output" | ansi2txt | col -b) + + # Restore file descriptors + exec 1>&3 2>&4 + + # Close file descriptors + exec 3>&- 4>&- + + # Remove tee'd output + rm --force "$file" + + # Try to get help + local helper="${HELPERS}/${command}" + echo "HELPER: $helper" + if [[ -f "$helper" && -x "$helper" ]]; then + local help=$("$helper" "$@" <<< "$output") + fi + if [[ -n "$help" ]]; then # If helpful + echo "$help" > "$HELPFUL" + elif [[ $status -ne 0 ]]; then # If helpless + echo "$output" > "$HELPLESS" + fi +} + +_help50() { + if [[ "$RUBBERDUCKING" != "0" ]]; then + if [[ -f "$HELPFUL" ]]; then + _helpful "$(cat "$HELPFUL")" + elif [[ -f "$HELPLESS" ]]; then + _helpless "$(cat "$HELPLESS")" + fi + fi + rm --force "$HELPFUL" "$HELPLESS" +} + +_helpful() { + echo "$1" +} + +_helpless() { :; } + +duck() { + if [[ "$1" == "off" ]]; then + export RUBBERDUCKING=0 + elif [[ "$1" == "on" ]]; then + unset RUBBERDUCKING + elif [[ "$1" == "status" ]]; then + if [[ "$RUBBERDUCKING" == "0" ]]; then + echo "off" + else + echo "on" + fi + fi +} + +export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_help50" +duck on diff --git a/opt/cs50/bin/make b/opt/cs50/bin/make index d4275e8..ca6cbd7 100755 --- a/opt/cs50/bin/make +++ b/opt/cs50/bin/make @@ -11,7 +11,9 @@ if [[ $# -eq 1 ]] && [[ "$1" != -* ]]; then # Don't suppress "Nothing to be done" with --silent /usr/bin/make "$1" - exit $? + + # Else make exits with 0 + exit 1 fi fi fi diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index f34adec..37f1a8a 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -6,8 +6,8 @@ regex="bash: (.*).py: command not found" if [[ "$output" =~ $regex ]]; then # If target is a directory - if [[ -f "$1" ]]; then - echo "Did you mean to run \`python ${1}`?" + if [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`python ${BASH_REMATCH[1]}\`?" exit fi fi diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 2b89551..ee19a5f 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -6,14 +6,14 @@ regex="make: Nothing to be done for '(.*)'" if [[ "$output" =~ $regex ]]; then # If target is a directory - if [[ -d "$1" ]]; then + if [[ -d "${BASH_REMATCH[1]}" ]]; then echo "Cannot run \`make\` on a directory. Did you mean to \`cd ${1}\` first?" exit fi # If target ends with .c - if [[ "$1" == *?.c ]]; then - base="${1%.c}" + if [[ "${BASH_REMATCH[1]}" == *?.c ]]; then + base="${BASH_REMATCH[1]%.c}" if [[ -n "$base" && ! -d "$base" ]]; then echo "Did you mean to \`make ${base}\`?" exit @@ -26,7 +26,7 @@ regex="No rule to make target '(.*)'" if [[ "$output" =~ $regex ]]; then # If no .c file for target - local c="$1.c" + c="${BASH_REMATCH[1]}.c" if [[ ! -f "$c" ]]; then # Search recursively for .c file From 22a873bd9d827235e5ba1fedb26d1e9d56030084 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 14:44:28 -0500 Subject: [PATCH 17/85] WIP --- etc/profile.d/help50.sh | 11 ++-- etc/profile.d/help50.sh.old | 102 ------------------------------------ opt/cs50/lib/help50/bash | 14 ++++- 3 files changed, 20 insertions(+), 107 deletions(-) delete mode 100644 etc/profile.d/help50.sh.old diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 8eda6ed..d9fd2d0 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -6,14 +6,19 @@ if command -v yes &> /dev/null; then alias yes=":" fi +# Ignore duplicates (but not commands that begin with spaces) +export HISTCONTROL="ignoredups" + function _help50 () { # Exit status of last command local status=$? - # Last command - local argv=$(fc -ln -1 | sed 's/^[[:space:]]*//') - # TODO: extract argv0 to determine if ./ command + # Append to history right away + history -a + + # Parse command + read -a argv <<< $(history 1 | cut -c 8-) # If no typescript yet if [[ -z "$SCRIPT" ]]; then diff --git a/etc/profile.d/help50.sh.old b/etc/profile.d/help50.sh.old deleted file mode 100644 index 92885c8..0000000 --- a/etc/profile.d/help50.sh.old +++ /dev/null @@ -1,102 +0,0 @@ -# Directory with helpers -HELPERS="/opt/cs50/lib/help50" - -# Disable yes, lest students type it at prompt -if command -v yes &> /dev/null; then - alias yes=":" -fi - -# Temporary files -FILE="/tmp/help50.$$" # Use PID to support multiple terminals -HELPFUL="${FILE}.help" -HELPLESS="${FILE}.output" - -# Supported helpers -for helper in "$HELPERS"/*; do - name=$(basename "$helper") - eval "function ${name}() { help50 "$name" \"\$@\"; }" -done - -help50() { - - # Duplicate file descriptors - exec 3>&1 4>&2 - - # Redirect output to a file too - exec > >(tee -a "$FILE") 2>&1 - - # Execute command - if [[ "$(type -P -t "$1")" == "file" ]]; then - unbuffer "$@" # Else, e.g., ls isn't colorized - else - command "$@" # Can't unbuffer builtins (e.g., cd) - fi - - # Remember these - local status=$? - local command="$1" - - # Remove command from $@ - shift - - # Get tee'd output - local output=$(cat "$FILE") - - # Remove any ANSI codes - output=$(echo "$output" | ansi2txt | col -b) - - # Restore file descriptors - exec 1>&3 2>&4 - - # Close file descriptors - exec 3>&- 4>&- - - # Remove tee'd output - rm --force "$file" - - # Try to get help - local helper="${HELPERS}/${command}" - echo "HELPER: $helper" - if [[ -f "$helper" && -x "$helper" ]]; then - local help=$("$helper" "$@" <<< "$output") - fi - if [[ -n "$help" ]]; then # If helpful - echo "$help" > "$HELPFUL" - elif [[ $status -ne 0 ]]; then # If helpless - echo "$output" > "$HELPLESS" - fi -} - -_help50() { - if [[ "$RUBBERDUCKING" != "0" ]]; then - if [[ -f "$HELPFUL" ]]; then - _helpful "$(cat "$HELPFUL")" - elif [[ -f "$HELPLESS" ]]; then - _helpless "$(cat "$HELPLESS")" - fi - fi - rm --force "$HELPFUL" "$HELPLESS" -} - -_helpful() { - echo "$1" -} - -_helpless() { :; } - -duck() { - if [[ "$1" == "off" ]]; then - export RUBBERDUCKING=0 - elif [[ "$1" == "on" ]]; then - unset RUBBERDUCKING - elif [[ "$1" == "status" ]]; then - if [[ "$RUBBERDUCKING" == "0" ]]; then - echo "off" - else - echo "on" - fi - fi -} - -export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }_help50" -duck on diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 37f1a8a..de1198c 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -2,10 +2,20 @@ output=$(cat) -regex="bash: (.*).py: command not found" +regex="bash: (.*\.py): command not found" if [[ "$output" =~ $regex ]]; then - # If target is a directory + # If file exists + if [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`python ${BASH_REMATCH[1]}\`?" + exit + fi +fi + +regex="bash: (./.*\.py): Permission denied" +if [[ "$output" =~ $regex ]]; then + + # If file exists if [[ -f "${BASH_REMATCH[1]}" ]]; then echo "Did you mean to run \`python ${BASH_REMATCH[1]}\`?" exit From bbaa372e0ffba496cd795d0c31e11b8f2c4dad6d Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 14:53:59 -0500 Subject: [PATCH 18/85] WIP --- etc/profile.d/help50.sh | 6 +----- opt/cs50/lib/help50/make | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index d9fd2d0..cbb310b 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -17,7 +17,7 @@ function _help50 () { # Append to history right away history -a - # Parse command + # Parse command line in case we want ${argv[0]} read -a argv <<< $(history 1 | cut -c 8-) # If no typescript yet @@ -83,10 +83,6 @@ function _help50 () { elif [[ $status -ne 0 ]]; then # If helpless _helpless "$text" fi - - # TEMP - echo "TEXT: $typescript" >> "$SCRIPT.log" - echo "---" >> "$SCRIPT.log" fi # Truncate typescript diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index ee19a5f..1e281ce 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -7,7 +7,7 @@ if [[ "$output" =~ $regex ]]; then # If target is a directory if [[ -d "${BASH_REMATCH[1]}" ]]; then - echo "Cannot run \`make\` on a directory. Did you mean to \`cd ${1}\` first?" + echo "Cannot run \`make\` on a directory. Did you mean to \`cd ${BASH_REMATCH[1]}\` first?" exit fi From b1b23603142477092863f6853d5a2e94dffa23b3 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 15:18:27 -0500 Subject: [PATCH 19/85] checking for ctl-z --- etc/profile.d/help50.sh | 5 +++-- tests/foo/foo2/foo3/x.c | 0 tests/foo/qux | 0 tests/foo/y.c | 0 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 tests/foo/foo2/foo3/x.c create mode 100755 tests/foo/qux create mode 100644 tests/foo/y.c diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index cbb310b..377dc68 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -38,8 +38,9 @@ function _help50 () { exit fi - # If last command erred - if [[ $status -ne 0 ]]; then + # If last command erred (and not ctl-z) + # https://tldp.org/LDP/abs/html/exitcodes.html + if [[ $status -ne 0 && $status -ne 148 ]]; then # Read typescript from disk local typescript=$(cat "$SCRIPT") diff --git a/tests/foo/foo2/foo3/x.c b/tests/foo/foo2/foo3/x.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/foo/qux b/tests/foo/qux new file mode 100755 index 0000000..e69de29 diff --git a/tests/foo/y.c b/tests/foo/y.c new file mode 100644 index 0000000..e69de29 From bd215bc8a900d5cec340e965a31543c1bf4a9134 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 15:35:31 -0500 Subject: [PATCH 20/85] checking parent dirs --- opt/cs50/lib/help50/make | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 1e281ce..7cde67d 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -30,11 +30,13 @@ if [[ "$output" =~ $regex ]]; then if [[ ! -f "$c" ]]; then # Search recursively for .c file - paths=$(find * -name "$c" 2> /dev/null) + pushd "$(cd && pwd)" > /dev/null + paths=$(find $(pwd) -name "$c" 2> /dev/null) lines=$(echo "$paths" | grep -c .) + popd > /dev/null echo -n "There isn't a file called \`${c}\` in your current directory." if [[ "$lines" -eq 1 ]]; then # If unambiguous - d=$(dirname "$paths") + d=$(realpath --relative-to=. "$(dirname "$paths")") echo " Did you mean to \`cd ${d}\` first?" else echo From a69df221e1435cfb52089b8435cdec7f79c1083e Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 16:34:38 -0500 Subject: [PATCH 21/85] added _find helper --- etc/profile.d/help50.sh | 23 +++++++++++++++++++++-- opt/cs50/lib/help50/make | 12 ++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 377dc68..5bd4601 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -9,7 +9,7 @@ fi # Ignore duplicates (but not commands that begin with spaces) export HISTCONTROL="ignoredups" -function _help50 () { +function _help50() { # Exit status of last command local status=$? @@ -73,7 +73,7 @@ function _help50 () { # Try to get help for helper in "$HELPERS"/*; do if [[ -f "$helper" && -x "$helper" ]]; then - local help=$("$helper" <<< "$typescript") + local help=$(. $helper <<< "$typescript") if [[ -n "$help" ]]; then break fi @@ -90,6 +90,25 @@ function _help50 () { truncate -s 0 "$SCRIPT" } +function _find() { + + # In $1 is path to find + if [[ $# -ne 1 ]]; then + return + fi + + # Find absolute paths of any $1 relative to `cd` + pushd "$(cd && pwd)" > /dev/null + paths=$(find $(pwd) -name "$1" 2> /dev/null) + popd > /dev/null + + # Resolve absolute paths to relative paths + local line + while IFS= read -r path; do + realpath --relative-to=. "$(dirname "$path")" + done <<< "$paths" +} + if ! type _helpful >/dev/null 2>&1; then function _helpful() { echo "$1" diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 7cde67d..599127d 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -30,17 +30,13 @@ if [[ "$output" =~ $regex ]]; then if [[ ! -f "$c" ]]; then # Search recursively for .c file - pushd "$(cd && pwd)" > /dev/null - paths=$(find $(pwd) -name "$c" 2> /dev/null) - lines=$(echo "$paths" | grep -c .) - popd > /dev/null + path=$(_find "$c") echo -n "There isn't a file called \`${c}\` in your current directory." - if [[ "$lines" -eq 1 ]]; then # If unambiguous - d=$(realpath --relative-to=. "$(dirname "$paths")") - echo " Did you mean to \`cd ${d}\` first?" + if [[ ! -z "$path" ]]; then + echo " Did you mean to \`cd $path\` first?" else echo fi - exit + exit fi fi From ebaec05f73538101e58c80e603e2fe575f89096e Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 17:03:39 -0500 Subject: [PATCH 22/85] renamed helper --- etc/profile.d/help50.sh | 40 +++++++++++++++++++++++++++++++--------- opt/cs50/lib/help50/make | 4 ++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 5bd4601..2c0f1c8 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -90,23 +90,45 @@ function _help50() { truncate -s 0 "$SCRIPT" } -function _find() { +function _search() { # In $1 is path to find if [[ $# -ne 1 ]]; then return fi - # Find absolute paths of any $1 relative to `cd` - pushd "$(cd && pwd)" > /dev/null + # Find any $1 in descendants paths=$(find $(pwd) -name "$1" 2> /dev/null) - popd > /dev/null + if [[ -z "$paths" ]]; then + + # Find any $1 in ancestors + local dir="$(dirname "$(pwd)")" + while [[ "$dir" != "/" ]]; do + paths=$(find "$dir" -maxdepth 1 -name "$1") + if [[ -z "$paths" ]]; then + dir=$(dirname "$dir") + else + break + fi + done + if [[ -z "$paths" ]]; then - # Resolve absolute paths to relative paths - local line - while IFS= read -r path; do - realpath --relative-to=. "$(dirname "$path")" - done <<< "$paths" + # Find any $1 relative to `cd` + pushd "$(cd && pwd)" > /dev/null + paths=$(find $(pwd) -name "$1" 2> /dev/null) + popd > /dev/null + fi + fi + + # Count paths + count=$(echo "$paths" | grep -c .) + + # If just one + if [[ "$count" -eq 1 ]]; then + + # Resolve absolute path to relative path + realpath --relative-to=. "$(dirname "$paths")" + fi } if ! type _helpful >/dev/null 2>&1; then diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 599127d..1cfc8ed 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -30,8 +30,8 @@ if [[ "$output" =~ $regex ]]; then if [[ ! -f "$c" ]]; then # Search recursively for .c file - path=$(_find "$c") - echo -n "There isn't a file called \`${c}\` in your current directory." + path=$(_search "$c") + echo -n "There isn't a file called \`${c}\` in your current directory. You are in \`$(basename "$PWD")\`." if [[ ! -z "$path" ]]; then echo " Did you mean to \`cd $path\` first?" else From bba678cc8b338975e60552339fdef89de30b3f00 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 17:06:16 -0500 Subject: [PATCH 23/85] added helper --- opt/cs50/lib/help50/bash | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index de1198c..6c298b0 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -12,6 +12,11 @@ if [[ "$output" =~ $regex ]]; then fi fi +regex="bash: \./(.*): Is a directory" +if [[ "$output" =~ $regex ]]; then + echo "Cannot execute a directory. Did you mean to \`cd\` into \`${BASH_REMATCH[1]}\` instead?" +fi + regex="bash: (./.*\.py): Permission denied" if [[ "$output" =~ $regex ]]; then From 1a38952a63779bbd7dfc7f59f14ae3ba205b75b2 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 17:17:05 -0500 Subject: [PATCH 24/85] added helper --- opt/cs50/lib/help50/bash | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 6c298b0..ceaa0cd 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -26,3 +26,13 @@ if [[ "$output" =~ $regex ]]; then exit fi fi + +regex="bash: /.(.*): No such file or directory" +if [[ "$output" =~ $regex ]]; then + + # If file exists + if [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`./${BASH_REMATCH[1]}\`?" + exit + fi +fi From 501a59dc2084b63fa7890ea610e7b2091507d532 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 18 Jan 2024 21:33:04 -0500 Subject: [PATCH 25/85] added helpers --- opt/cs50/lib/help50/make | 14 +++++++------- opt/cs50/lib/help50/python | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) create mode 100755 opt/cs50/lib/help50/python diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 1cfc8ed..7f9c913 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -22,18 +22,18 @@ if [[ "$output" =~ $regex ]]; then fi -regex="No rule to make target '(.*)'" +regex="make: \*\*\* No rule to make target '(.*)'" if [[ "$output" =~ $regex ]]; then # If no .c file for target - c="${BASH_REMATCH[1]}.c" - if [[ ! -f "$c" ]]; then + file="${BASH_REMATCH[1]}.c" + if [[ ! -f "$file" ]]; then # Search recursively for .c file - path=$(_search "$c") - echo -n "There isn't a file called \`${c}\` in your current directory. You are in \`$(basename "$PWD")\`." - if [[ ! -z "$path" ]]; then - echo " Did you mean to \`cd $path\` first?" + dir=$(_search "$file") + echo -n "There isn't a file called \`$file\` in your current directory." + if [[ ! -z "$dir" ]]; then + echo " Did you mean to \`cd $dir\` first?" else echo fi diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python new file mode 100755 index 0000000..2474501 --- /dev/null +++ b/opt/cs50/lib/help50/python @@ -0,0 +1,38 @@ +#!/bin/bash + +output=$(cat) + +regex="AttributeError: module 'cs50' has no attribute '(get_(float|int|string)|SQL)'$" +if [[ "$output" =~ $regex ]]; then + if [[ -f cs50.py ]]; then + echo "You have a file called \`cs50.py\` that is \"shadowing\" CS50's own. Best to rename that file with \`mv\`." + exit + fi +fi + +regex='^\s*(import string\s*$|from\s+string\s+import\s+)' +if echo "$output" | grep -Pq "$regex"; then + if [[ -f string.py ]]; then + echo "You have a file called \`string.py\` that is \"shadowing\" Python's own. Best to rename that file with \`mv\`." + exit + fi +fi + +regex="python: can't open file '(.*\.py)': \[Errno 2\] No such file or directory" +if [[ "$output" =~ $regex ]]; then + + # Relative path from $PWD + path=$(realpath --relative-to=. "${BASH_REMATCH[1]}") + + # If .py is in $PWD + if [[ -n "$path" && "$path" == $(basename "$path") ]]; then + dir=$(_search "$path") + echo -n "There isn't a file called \`$path\` in your current directory." + if [[ ! -z "$dir" ]]; then + echo " Did you mean to \`cd $dir\` first?" + else + echo + fi + exit + fi +fi From 383f4a25a5a4c6066df0388479abf9299f217e01 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 31 Jan 2024 16:00:25 -0500 Subject: [PATCH 26/85] tidied formatting, filesystem --- etc/profile.d/help50.sh | 89 ++++++++++++-------------------------- opt/cs50/bin/_ansi | 13 ------ opt/cs50/bin/_sure | 14 ------ opt/cs50/bin/http-server | 2 + opt/cs50/bin/sqlite3 | 2 + opt/cs50/lib/cli | 72 ++++++++++++++++++++++++++++++ opt/cs50/lib/help50/make | 2 + opt/cs50/lib/help50/python | 2 + 8 files changed, 108 insertions(+), 88 deletions(-) delete mode 100755 opt/cs50/bin/_ansi delete mode 100755 opt/cs50/bin/_sure create mode 100644 opt/cs50/lib/cli diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 2c0f1c8..e353059 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,3 +1,5 @@ +. /opt/cs50/lib/cli + # Directory with helpers HELPERS="/opt/cs50/lib/help50" @@ -11,28 +13,28 @@ export HISTCONTROL="ignoredups" function _help50() { - # Exit status of last command + # Get exit status of last command local status=$? - # Append to history right away - history -a - - # Parse command line in case we want ${argv[0]} - read -a argv <<< $(history 1 | cut -c 8-) + # Get last command, independent of user's actual history + histfile=/tmp/help50.$$.history + HISTFILE=$histfile history -a + local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') + rm --force $histfile # If no typescript yet - if [[ -z "$SCRIPT" ]]; then + if [[ -z $TYPESCRIPT ]]; then # Use this shell's PID as typescript's name, exporting so that subshells know script is already running - export SCRIPT="/tmp/help50.$$" + export TYPESCRIPT=/tmp/help50.$$.typescript # Make a typescript of everything displayed in terminal (without using exec, which breaks sudo); # --append avoids `bash: warning: command substitution: ignored null byte in input`; # --quiet suppresses `Script started...` - script --append --command "bash --login" --flush --quiet "$SCRIPT" + script --append --command "bash --login" --flush --quiet $TYPESCRIPT # Remove typescript before exiting this shell - rm --force "$SCRIPT" + rm --force $TYPESCRIPT # Now exit this shell too exit @@ -40,10 +42,15 @@ function _help50() { # If last command erred (and not ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html - if [[ $status -ne 0 && $status -ne 148 ]]; then + if [[ $status -ne 0 && $status -ne 148 && ! "$command" =~ ^\./ ]]; then + + # Ignore ./* if executable file + if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then + break + fi # Read typescript from disk - local typescript=$(cat "$SCRIPT") + local typescript=$(cat $TYPESCRIPT) # Remove script's own output (if this is user's first command) typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}') @@ -71,9 +78,9 @@ function _help50() { typescript=$(echo "$typescript" | col -bp) # Try to get help - for helper in "$HELPERS"/*; do - if [[ -f "$helper" && -x "$helper" ]]; then - local help=$(. $helper <<< "$typescript") + for helper in $HELPERS/*; do + if [[ -f $helper && -x $helper ]]; then + local help=$($helper <<< "$typescript") if [[ -n "$help" ]]; then break fi @@ -82,63 +89,23 @@ function _help50() { if [[ -n "$help" ]]; then # If helpful _helpful "$help" elif [[ $status -ne 0 ]]; then # If helpless - _helpless "$text" + _helpless "$typescript" fi fi # Truncate typescript - truncate -s 0 "$SCRIPT" -} - -function _search() { - - # In $1 is path to find - if [[ $# -ne 1 ]]; then - return - fi - - # Find any $1 in descendants - paths=$(find $(pwd) -name "$1" 2> /dev/null) - if [[ -z "$paths" ]]; then - - # Find any $1 in ancestors - local dir="$(dirname "$(pwd)")" - while [[ "$dir" != "/" ]]; do - paths=$(find "$dir" -maxdepth 1 -name "$1") - if [[ -z "$paths" ]]; then - dir=$(dirname "$dir") - else - break - fi - done - if [[ -z "$paths" ]]; then - - # Find any $1 relative to `cd` - pushd "$(cd && pwd)" > /dev/null - paths=$(find $(pwd) -name "$1" 2> /dev/null) - popd > /dev/null - fi - fi - - # Count paths - count=$(echo "$paths" | grep -c .) - - # If just one - if [[ "$count" -eq 1 ]]; then - - # Resolve absolute path to relative path - realpath --relative-to=. "$(dirname "$paths")" - fi + truncate -s 0 "$TYPESCRIPT" } +# Default helpers if ! type _helpful >/dev/null 2>&1; then function _helpful() { - echo "$1" + local output=$(_ansi "$1") + echo -e "\033[7m${output}\033[27m" # Reverse video } fi - if ! type _helpless >/dev/null 2>&1; then - function _helpless() { :; } + function _helpless() { :; } # Silent fi export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" diff --git a/opt/cs50/bin/_ansi b/opt/cs50/bin/_ansi deleted file mode 100755 index fc7734b..0000000 --- a/opt/cs50/bin/_ansi +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# If command-line arguments -if [[ -t 0 ]]; then - input="$*" - -# If standard input -else - input=$(cat) -fi - -# https://www.gnu.org/software/termutils/manual/termutils-2.0/html_chapter/tput_1.html#SEC8 -echo "$input" | sed "s/\`\([^\`]*\)\`/$(tput smso)\1$(tput rmso)/g" diff --git a/opt/cs50/bin/_sure b/opt/cs50/bin/_sure deleted file mode 100755 index 74f688a..0000000 --- a/opt/cs50/bin/_sure +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -if [[ $# -ne 1 ]]; then - exit 1 -fi -prompt=$(echo "$1" | _ansi) -while true; do - read -p "$prompt [y/N] " -r - if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then - exit 0 - elif [[ "${REPLY,,}" =~ ^(n|no)$ ]]; then - exit 1 - fi -done diff --git a/opt/cs50/bin/http-server b/opt/cs50/bin/http-server index 7bbcd9e..ea82a17 100755 --- a/opt/cs50/bin/http-server +++ b/opt/cs50/bin/http-server @@ -1,5 +1,7 @@ #!/bin/bash +. /opt/cs50/lib/cli + # Default options a="-a 0.0.0.0" c="-c-1" diff --git a/opt/cs50/bin/sqlite3 b/opt/cs50/bin/sqlite3 index 80557d8..b2dd85e 100755 --- a/opt/cs50/bin/sqlite3 +++ b/opt/cs50/bin/sqlite3 @@ -1,5 +1,7 @@ #!/bin/bash +. /opt/cs50/lib/cli + # If data is coming from stdin (pipe or redirection) if [[ -p /dev/stdin || ! -t 0 ]]; then /usr/local/bin/sqlite3 -nullvalue NULL -table "$@" < /dev/stdin diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli new file mode 100644 index 0000000..9c1384d --- /dev/null +++ b/opt/cs50/lib/cli @@ -0,0 +1,72 @@ +function _ansi() { + + # If command-line arguments + if [[ -t 0 ]]; then + input="$*" + + # If standard input + else + input=$(cat) + fi + + # Format backticks as bold + bold=$(printf '\033[1m') + normal=$(printf '\033[22m') + echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" +} + +function _search() { + + # In $1 is path to find + if [[ $# -ne 1 ]]; then + return + fi + + # Find any $1 in descendants + paths=$(find $(pwd) -name "$1" 2> /dev/null) + if [[ -z "$paths" ]]; then + + # Find any $1 in ancestors + local dir="$(dirname "$(pwd)")" + while [[ "$dir" != "/" ]]; do + paths=$(find "$dir" -maxdepth 1 -name "$1") + if [[ -z "$paths" ]]; then + dir=$(dirname "$dir") + else + break + fi + done + if [[ -z "$paths" ]]; then + + # Find any $1 relative to `cd` + pushd "$(cd && pwd)" > /dev/null + paths=$(find $(pwd) -name "$1" 2> /dev/null) + popd > /dev/null + fi + fi + + # Count paths + count=$(echo "$paths" | grep -c .) + + # If just one + if [[ "$count" -eq 1 ]]; then + + # Resolve absolute path to relative path + realpath --relative-to=. "$(dirname "$paths")" + fi +} + +function _sure() { + if [[ $# -ne 1 ]]; then + return 1 + fi + prompt=$(echo "$1" | _ansi) + while true; do + read -p "$prompt [y/N] " -r + if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then + return 0 + elif [[ "${REPLY,,}" =~ ^(n|no)$ ]]; then + return 1 + fi + done +} diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 7f9c913..f23c37e 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -1,5 +1,7 @@ #!/bin/bash +. /opt/cs50/lib/cli + output=$(cat) regex="make: Nothing to be done for '(.*)'" diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python index 2474501..8248449 100755 --- a/opt/cs50/lib/help50/python +++ b/opt/cs50/lib/help50/python @@ -1,5 +1,7 @@ #!/bin/bash +. /opt/cs50/lib/cli + output=$(cat) regex="AttributeError: module 'cs50' has no attribute '(get_(float|int|string)|SQL)'$" From 72c3d53b0927872a4e88654d858b90ed3e6d6081 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 31 Jan 2024 17:32:25 -0500 Subject: [PATCH 27/85] capping size of help50 input --- etc/profile.d/help50.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index e353059..191ea1a 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,3 +1,8 @@ +# If root +if [ "$(whoami)" == "root" ]; then + return +fi + . /opt/cs50/lib/cli # Directory with helpers @@ -5,7 +10,13 @@ HELPERS="/opt/cs50/lib/help50" # Disable yes, lest users type it at prompt if command -v yes &> /dev/null; then - alias yes=":" + function yes() { + if [[ -t 0 ]]; then + : + else + command yes + fi + } fi # Ignore duplicates (but not commands that begin with spaces) @@ -55,6 +66,9 @@ 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) + # Remove any line continuations from command line local lines="" while IFS= read -r line || [[ -n "$line" ]]; do From eb6647d97206c232373e8717d44f618d641af0c7 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 2 Feb 2024 01:09:04 -0500 Subject: [PATCH 28/85] improved non-greedy matching --- opt/cs50/lib/help50/bash | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index ceaa0cd..224dec0 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -12,12 +12,13 @@ if [[ "$output" =~ $regex ]]; then fi fi -regex="bash: \./(.*): Is a directory" +regex="bash: \./([^:]*): Is a directory" if [[ "$output" =~ $regex ]]; then echo "Cannot execute a directory. Did you mean to \`cd\` into \`${BASH_REMATCH[1]}\` instead?" + exit fi -regex="bash: (./.*\.py): Permission denied" +regex="bash: (\./.*\.py): Permission denied" if [[ "$output" =~ $regex ]]; then # If file exists @@ -27,7 +28,7 @@ if [[ "$output" =~ $regex ]]; then fi fi -regex="bash: /.(.*): No such file or directory" +regex="bash: /\.([^:]*): No such file or directory" if [[ "$output" =~ $regex ]]; then # If file exists From 26facb72471dc4fb24766a26b3309c593b8774d7 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 2 Feb 2024 01:16:59 -0500 Subject: [PATCH 29/85] added helper for .c --- opt/cs50/lib/help50/bash | 10 ++++++++++ tests/baz.c | 0 tests/y.c | 0 3 files changed, 10 insertions(+) create mode 100644 tests/baz.c create mode 100644 tests/y.c diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 224dec0..556b6e3 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -18,6 +18,16 @@ if [[ "$output" =~ $regex ]]; then exit fi +regex="bash: \./(([^:]*)\.c): Permission denied" +if [[ "$output" =~ $regex ]]; then + + # If file exists + if [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`make ${BASH_REMATCH[2]}\` and then \`./${BASH_REMATCH[2]}\`?" + exit + fi +fi + regex="bash: (\./.*\.py): Permission denied" if [[ "$output" =~ $regex ]]; then diff --git a/tests/baz.c b/tests/baz.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/y.c b/tests/y.c new file mode 100644 index 0000000..e69de29 From 45cb7f30429a9c8334b347749b3e2a988373bf6b Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Sat, 27 Apr 2024 22:12:30 -0400 Subject: [PATCH 30/85] use arm64 github runner for building arm image --- .github/workflows/main.yml | 41 ++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4c83cdf..c7abe02 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,11 +1,9 @@ on: push + jobs: - build: + build-amd64: runs-on: ubuntu-latest-64-cores steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -37,6 +35,41 @@ jobs: run: | docker push cs50/cli:amd64 + build-arm64: + needs: build-amd64 + runs-on: ubuntu-latest-64-cores-arm + steps: + - name: Install Docker + run: | + export DEBIAN_FRONTEND=noninteractive + sudo apt update + sudo apt install -y ca-certificates curl + sudo install -m 0755 -d /etc/apt/keyrings + sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt update + sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + sudo usermod -aG docker $USER + sudo apt install -y acl + sudo setfacl --modify user:$USER:rw /var/run/docker.sock + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Install Python + run: | + sudo apt install -y python3 + - name: Build for linux/arm64 uses: docker/build-push-action@v3 with: From 372761f60d628806486c5e6ef70d3253521f7ddc Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Sat, 27 Apr 2024 22:45:20 -0400 Subject: [PATCH 31/85] build amd64 and arm64 docker images concurrently --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7abe02..e1c742c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,6 @@ jobs: docker push cs50/cli:amd64 build-arm64: - needs: build-amd64 runs-on: ubuntu-latest-64-cores-arm steps: - name: Install Docker @@ -87,6 +86,10 @@ jobs: run: | docker push cs50/cli:arm64 + finalize: + needs: [build-amd64, build-arm64] + runs-on: ubuntu-latest + steps: - name: Create multi-arch manifest and push to Docker Hub if: ${{ github.ref == 'refs/heads/main' }} run: | @@ -111,4 +114,4 @@ jobs: workflow_id: 'main.yml', ref: 'main' }); - } + } \ No newline at end of file From b9a5ee3b741fd882e2d68517fcb803411a6a1982 Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Sat, 27 Apr 2024 22:55:10 -0400 Subject: [PATCH 32/85] create canary build --- .github/workflows/main.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1c742c..9e5e478 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,7 +26,9 @@ jobs: BUILDARCH=amd64 load: true platforms: linux/amd64 - tags: cs50/cli:amd64 + tags: | + cs50/cli:amd64 + cs50/cli:canary-amd64 cache-from: type=registry,ref=cs50/cli:amd64-buildcache cache-to: type=registry,ref=cs50/cli:amd64-buildcache,mode=max @@ -35,6 +37,10 @@ jobs: run: | docker push cs50/cli:amd64 + - name: Push linux/amd64 build to Docker Hub (canary) + run: | + docker push cs50/cli:canary-amd64 + build-arm64: runs-on: ubuntu-latest-64-cores-arm steps: @@ -77,7 +83,9 @@ jobs: BUILDARCH=arm64 load: true platforms: linux/arm64 - tags: cs50/cli:arm64 + tags: | + cs50/cli:arm64 + cs50/cli:canary-arm64 cache-from: type=registry,ref=cs50/cli:arm64-buildcache cache-to: type=registry,ref=cs50/cli:arm64-buildcache,mode=max @@ -86,10 +94,20 @@ jobs: run: | docker push cs50/cli:arm64 + - name: Push linux/arm64 build to Docker Hub (canary) + run: | + docker push cs50/cli:canary-arm64 + finalize: needs: [build-amd64, build-arm64] runs-on: ubuntu-latest steps: + - name: Log into Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Create multi-arch manifest and push to Docker Hub if: ${{ github.ref == 'refs/heads/main' }} run: | @@ -98,6 +116,13 @@ jobs: --amend cs50/cli:arm64 docker manifest push cs50/cli:latest + - name: Create multi-arch manifest and push to Docker Hub (canary) + run: | + docker manifest create cs50/cli:canary \ + --amend cs50/cli:canary-amd64 \ + --amend cs50/cli:canary-arm64 + docker manifest push cs50/cli:canary + - name: Re-deploy depdendents if: ${{ github.ref == 'refs/heads/main' }} uses: actions/github-script@v6 From 2919d87c1eb8203c261509af0071c34fc1674ea7 Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Sat, 27 Apr 2024 23:02:17 -0400 Subject: [PATCH 33/85] updated workflow actions versions --- .github/workflows/main.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9e5e478..35ba217 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,10 +5,10 @@ jobs: runs-on: ubuntu-latest-64-cores steps: - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Log into Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -19,7 +19,7 @@ jobs: python-version: '3.11' - name: Build for linux/amd64 - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: build-args: | VCS_REF=${{ github.sha }} @@ -63,10 +63,10 @@ jobs: sudo setfacl --modify user:$USER:rw /var/run/docker.sock - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Log into Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -76,7 +76,7 @@ jobs: sudo apt install -y python3 - name: Build for linux/arm64 - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: build-args: | VCS_REF=${{ github.sha }} @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Log into Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} From f5b22c4a337d5006ea9e91b86e3007457ff129e6 Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Sun, 28 Apr 2024 18:09:51 -0400 Subject: [PATCH 34/85] added comments --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35ba217..2aff497 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,7 +44,7 @@ jobs: build-arm64: runs-on: ubuntu-latest-64-cores-arm steps: - - name: Install Docker + - name: Install Docker (remove once Docker is pre-installed on arm64 runners) run: | export DEBIAN_FRONTEND=noninteractive sudo apt update @@ -71,7 +71,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Install Python + - name: Install Python (replace with setup-python once available on arm64 runners) run: | sudo apt install -y python3 From fe134545288daa58ce2d9d49fd1fc4cbd6189125 Mon Sep 17 00:00:00 2001 From: "yulai.linda@gmail.com" Date: Thu, 2 May 2024 12:09:00 -0400 Subject: [PATCH 35/85] updated actions/github-script to version v7 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2aff497..a57c645 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -125,7 +125,7 @@ jobs: - name: Re-deploy depdendents if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/github-script@v6 + uses: actions/github-script@v7 with: github-token: ${{ secrets.DEPLOY50_PAT }} script: | From 8556c5311f423ec778da7c6a875c10c0f9427f05 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 6 May 2024 15:26:52 -0400 Subject: [PATCH 36/85] WIP --- etc/profile.d/help50.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 191ea1a..9976340 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -39,7 +39,8 @@ function _help50() { # Use this shell's PID as typescript's name, exporting so that subshells know script is already running export TYPESCRIPT=/tmp/help50.$$.typescript - # Make a typescript of everything displayed in terminal (without using exec, which breaks sudo); + # Make a typescript of everything displayed in terminal, + # without using exec, which causes sudo to hang, or tee, which triggers "Output is not a terminal" in vim; # --append avoids `bash: warning: command substitution: ignored null byte in input`; # --quiet suppresses `Script started...` script --append --command "bash --login" --flush --quiet $TYPESCRIPT @@ -51,9 +52,9 @@ function _help50() { exit fi - # If last command erred (and not ctl-z) + # If last command erred (and is not ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html - if [[ $status -ne 0 && $status -ne 148 && ! "$command" =~ ^\./ ]]; then + if [[ $status -ne 0 && $status -ne 148 && ]]; then # Ignore ./* if executable file if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then @@ -122,4 +123,9 @@ if ! type _helpless >/dev/null 2>&1; then function _helpless() { :; } # Silent fi -export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" +if [[ ! "$PROMPT_COMMAND" =~ ^_help50; ]]; then + export PROMPT_COMMAND="_help50;${PROMPT_COMMAND:+; $PROMPT_COMMAND}" +fi + +# TODO: +# help on, off From 5f11fefcb9338657ab551eebaeac5c1bb6720616 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 6 May 2024 15:58:08 -0400 Subject: [PATCH 37/85] WIP --- etc/profile.d/help50.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 9976340..aab3f0d 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -3,7 +3,9 @@ if [ "$(whoami)" == "root" ]; then return fi -. /opt/cs50/lib/cli +# TEMP +#. /opt/cs50/lib/cli +. opt/cs50/lib/cli # Directory with helpers HELPERS="/opt/cs50/lib/help50" @@ -54,7 +56,7 @@ function _help50() { # If last command erred (and is not ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html - if [[ $status -ne 0 && $status -ne 148 && ]]; then + if [[ $status -ne 0 && $status -ne 148 ]]; then # Ignore ./* if executable file if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then @@ -123,9 +125,10 @@ if ! type _helpless >/dev/null 2>&1; then function _helpless() { :; } # Silent fi -if [[ ! "$PROMPT_COMMAND" =~ ^_help50; ]]; then - export PROMPT_COMMAND="_help50;${PROMPT_COMMAND:+; $PROMPT_COMMAND}" -fi +#if [[ ! "$PROMPT_COMMAND" =~ ^_help50\; ]]; then + #export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" +#fi +export PROMPT_COMMAND=_help50 # TODO: # help on, off From a91969f972d4a5b0f953af7197ca7a1c274e9c4a Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 6 May 2024 20:21:02 -0400 Subject: [PATCH 38/85] WIP2 --- .../help50.sh => opt/cs50/bin/help50 | 81 +++++++++++++++++-- 1 file changed, 76 insertions(+), 5 deletions(-) rename etc/profile.d/help50.sh => opt/cs50/bin/help50 (72%) mode change 100644 => 100755 diff --git a/etc/profile.d/help50.sh b/opt/cs50/bin/help50 old mode 100644 new mode 100755 similarity index 72% rename from etc/profile.d/help50.sh rename to opt/cs50/bin/help50 index aab3f0d..4f34d29 --- a/etc/profile.d/help50.sh +++ b/opt/cs50/bin/help50 @@ -1,14 +1,85 @@ +#!/bin/bash + # If root -if [ "$(whoami)" == "root" ]; then - return +if [[ `id -u` -eq 0 ]]; then + exit 1 fi -# TEMP -#. /opt/cs50/lib/cli +function _start() { + + # If already helping + if [[ -n "$HELP50" ]]; then + exit 0 + fi + + # PID of shell to help + export HELP50="$PPID" + + # Start `script` in background + set -o monitor + script --append --command "bash --login && exit 1" --flush --quiet --return "/tmp/help50.$HELP50.typescript" & + + # Remember PID of `script`, in case user wants to turn off + echo $! > "/tmp/help50.$HELP50.pid" + + # Foreground `script`, without echoing command + fg > /dev/null + + # If `script` exited on its own, as via ctl-d or `logout`, then kill parent shell + if [[ $? -ne 0 ]]; then + kill -SIGHUP "$PPID" + fi +} + +function _stop() { + + # If not helping + if [[ -z "$HELP50" ]]; then + exit 0 + fi + + # Kill `script` + pid=$(cat "/tmp/help50.$HELP50.pid") + kill -SIGTERM "$pid" + + # No longer helping + rm --force "/tmp/help50.$HELP50.pid" + unset HELP50 +} + +# Parse argument +if [[ "$1" == "off" ]]; then + _stop +elif [[ "$1" == "on" ]]; then + _start +elif [[ "$1" == "status" ]]; then + if [[ -n "$HELP50" ]]; then + echo "Running" + exit 0 + else + echo "Not running" + exit 1 + fi +else + echo "Usage: $0 [off|on|status]" + exit 1 +fi + +exit 0 + + + + + +# Helpers +# TODO +#. /opt/cs50/lib/help50 . opt/cs50/lib/cli # Directory with helpers -HELPERS="/opt/cs50/lib/help50" +# TODO +#HELPERS="/opt/cs50/lib/help50" +HELPERS="/mnt/opt/cs50/lib/help50" # Disable yes, lest users type it at prompt if command -v yes &> /dev/null; then From 73601b99cbb7620e1454dcde6d108d8488fe40e9 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 6 May 2024 21:46:09 -0400 Subject: [PATCH 39/85] WIP2 --- etc/profile.d/help50.sh | 116 ++++++++++++++++++++++++++++++++++++++++ opt/cs50/bin/help50 | 60 +++++++++++---------- 2 files changed, 148 insertions(+), 28 deletions(-) create mode 100644 etc/profile.d/help50.sh diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh new file mode 100644 index 0000000..473fa8b --- /dev/null +++ b/etc/profile.d/help50.sh @@ -0,0 +1,116 @@ +# Helpers +# TODO +#. /opt/cs50/lib/help50 +#. opt/cs50/lib/cli + +if [[ -z "$HELP50" ]]; then + return +fi + +echo HEREEEEEE + +# Directory with helpers +# TODO +#HELPERS="/opt/cs50/lib/help50" +HELPERS="/mnt/opt/cs50/lib/help50" + +# Disable yes, lest users type it at prompt +if command -v yes &> /dev/null; then + function yes() { + if [[ -t 0 ]]; then + : + else + command yes + fi + } +fi + +# Ignore duplicates (but not commands that begin with spaces) +export HISTCONTROL="ignoredups" + +function _help50() { + + # Get exit status of last command + local status=$? + + # Get last command, independent of user's actual history + histfile=/tmp/help50.$$.history + HISTFILE=$histfile history -a + local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') + rm --force $histfile + + # If last command erred (and is not ctl-z) + # https://tldp.org/LDP/abs/html/exitcodes.html + if [[ $status -ne 0 && $status -ne 148 ]]; then + + # Ignore ./* if executable file + if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then + break + fi + + # Read typescript from disk + local typescript=$(cat /tmp/help50.$HELP50.typescript) + + # 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) + + # Remove any line continuations from command line + local lines="" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ -z $done && $line =~ \\$ ]]; then + lines+="${line%\\}" + else + lines+="$line"$'\n' + local done=1 + fi + done <<< "$typescript" + typescript="$lines" + + # Remove command line from typescript + typescript=$(echo "$typescript" | sed '1d') + + # Remove ANSI characters + typescript=$(echo "$typescript" | ansi2txt) + + # Remove control characters + # https://superuser.com/a/237154 + typescript=$(echo "$typescript" | col -bp) + + # Try to get help + for helper in $HELPERS/*; do + if [[ -f $helper && -x $helper ]]; then + local help=$($helper <<< "$typescript") + if [[ -n "$help" ]]; then + break + fi + fi + done + if [[ -n "$help" ]]; then # If helpful + _helpful "$help" + elif [[ $status -ne 0 ]]; then # If helpless + _helpless "$typescript" + fi + fi + + # Truncate typescript + truncate -s 0 "/tmp/help50.$HELP50.typescript" +} + +# Default helpers +if ! type _helpful >/dev/null 2>&1; then + function _helpful() { + local output=$(_ansi "$1") + echo -e "\033[7m${output}\033[27m" # Reverse video + } +fi +if ! type _helpless >/dev/null 2>&1; then + function _helpless() { :; } # Silent +fi + +#if [[ ! "$PROMPT_COMMAND" =~ ^_help50\; ]]; then + #export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" +#fi +export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 4f34d29..37a8a43 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -5,7 +5,23 @@ if [[ `id -u` -eq 0 ]]; then exit 1 fi -function _start() { +function _off() { + + # If not helping + if [[ -z "$HELP50" ]]; then + exit 0 + fi + + # Kill `script` + pid=$(cat "/tmp/help50.$HELP50.pid") + kill -SIGTERM "$pid" + + # No longer helping + rm --force /tmp/help50.$HELP50.* + unset HELP50 +} + +function _on() { # If already helping if [[ -n "$HELP50" ]]; then @@ -24,44 +40,32 @@ function _start() { # Foreground `script`, without echoing command fg > /dev/null + status=$? - # If `script` exited on its own, as via ctl-d or `logout`, then kill parent shell - if [[ $? -ne 0 ]]; then - kill -SIGHUP "$PPID" - fi -} + # No longer helping + rm --force /tmp/help50.$HELP50.* -function _stop() { + # If `script` was killed, in which case `exit 1` above won't execute + if [[ $status -ne 1 ]]; then - # If not helping - if [[ -z "$HELP50" ]]; then - exit 0 - fi + # Without this, prompt ends up below and to right of "Session terminated, killing shell... ...killed." + echo -e "\r" - # Kill `script` - pid=$(cat "/tmp/help50.$HELP50.pid") - kill -SIGTERM "$pid" + # Else if `script` exited on its own, as via ctl-d or `logout` + else - # No longer helping - rm --force "/tmp/help50.$HELP50.pid" - unset HELP50 + # Kill parent shell, since user presumably wants to exit + kill -SIGHUP "$PPID" + fi } # Parse argument if [[ "$1" == "off" ]]; then - _stop + _off elif [[ "$1" == "on" ]]; then - _start -elif [[ "$1" == "status" ]]; then - if [[ -n "$HELP50" ]]; then - echo "Running" - exit 0 - else - echo "Not running" - exit 1 - fi + _on else - echo "Usage: $0 [off|on|status]" + echo "Usage: $0 [off|on]" exit 1 fi From d6fc53add8deabe971fd75cae17f6e1d9eac0b8a Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 7 May 2024 09:55:27 -0400 Subject: [PATCH 40/85] WIP3 --- opt/cs50/bin/help50 | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 37a8a43..4958686 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -6,19 +6,11 @@ if [[ `id -u` -eq 0 ]]; then fi function _off() { - - # If not helping - if [[ -z "$HELP50" ]]; then - exit 0 - fi - - # Kill `script` - pid=$(cat "/tmp/help50.$HELP50.pid") - kill -SIGTERM "$pid" - - # No longer helping - rm --force /tmp/help50.$HELP50.* - unset HELP50 + for pid in /tmp/help50.*.pid + do + kill -SIGTERM $(cat "$pid") + done + touch /tmp/help50.lock } function _on() { @@ -28,12 +20,15 @@ function _on() { exit 0 fi - # PID of shell to help - export HELP50="$PPID" + # Remove any lockfile + rm --force /tmp/help50.lock + + # PID of parent shell to help + local HELP50="$PPID" # Start `script` in background set -o monitor - script --append --command "bash --login && exit 1" --flush --quiet --return "/tmp/help50.$HELP50.typescript" & + HELP50="$HELP50" script --append --command "bash --login && exit 1" --flush --quiet --return "/tmp/help50.$HELP50.typescript" & # Remember PID of `script`, in case user wants to turn off echo $! > "/tmp/help50.$HELP50.pid" @@ -55,7 +50,7 @@ function _on() { else # Kill parent shell, since user presumably wants to exit - kill -SIGHUP "$PPID" + kill -SIGHUP "$HELP50" fi } From 0c96044da76ba4900c440d048f7ab4cb7514dbc9 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 7 May 2024 16:09:49 -0400 Subject: [PATCH 41/85] WIP4 --- etc/profile.d/help50.sh | 25 ++--- opt/cs50/bin/help50 | 221 +++++++++++----------------------------- 2 files changed, 69 insertions(+), 177 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 473fa8b..be8592a 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -1,18 +1,13 @@ -# Helpers -# TODO -#. /opt/cs50/lib/help50 -#. opt/cs50/lib/cli - +# If not started if [[ -z "$HELP50" ]]; then return fi -echo HEREEEEEE - # Directory with helpers -# TODO -#HELPERS="/opt/cs50/lib/help50" -HELPERS="/mnt/opt/cs50/lib/help50" +HELPERS="/opt/cs50/lib/help50" + +# Library +. /opt/cs50/lib/cli # Disable yes, lest users type it at prompt if command -v yes &> /dev/null; then @@ -96,7 +91,7 @@ function _help50() { fi # Truncate typescript - truncate -s 0 "/tmp/help50.$HELP50.typescript" + truncate -s 0 /tmp/help50.$HELP50.typescript } # Default helpers @@ -107,10 +102,10 @@ if ! type _helpful >/dev/null 2>&1; then } fi if ! type _helpless >/dev/null 2>&1; then - function _helpless() { :; } # Silent + #function _helpless() { :; } # Silent + function _helpless() { + echo TODO + } fi -#if [[ ! "$PROMPT_COMMAND" =~ ^_help50\; ]]; then - #export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" -#fi export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 4958686..a643da2 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -5,33 +5,42 @@ if [[ `id -u` -eq 0 ]]; then exit 1 fi -function _off() { - for pid in /tmp/help50.*.pid - do - kill -SIGTERM $(cat "$pid") - done +function _disable() { touch /tmp/help50.lock } -function _on() { +function _is-enabled() { + if [[ -f /tmp/help50.lock ]]; then + echo disabled + return 1 + else + echo enabled + return 0 + fi +} + +function _enable() { + rm --force /tmp/help50.lock +} + +function _start() { # If already helping if [[ -n "$HELP50" ]]; then - exit 0 + return 0 fi - # Remove any lockfile - rm --force /tmp/help50.lock - # PID of parent shell to help - local HELP50="$PPID" + local HELP50=$PPID - # Start `script` in background + # Start `script` in background, using ; instead of && for command, + # else if user logs out (as via ctl-d) after a non-0 command, bash exits with 127 set -o monitor - HELP50="$HELP50" script --append --command "bash --login && exit 1" --flush --quiet --return "/tmp/help50.$HELP50.typescript" & + HELP50=$HELP50 script --command "bash --login ; exit 1" --flush --quiet --return /tmp/help50.$HELP50.typescript & # Remember PID of `script`, in case user wants to turn off - echo $! > "/tmp/help50.$HELP50.pid" + echo $! > /tmp/help50.$HELP50.pid + ps aux # Foreground `script`, without echoing command fg > /dev/null @@ -40,165 +49,53 @@ function _on() { # No longer helping rm --force /tmp/help50.$HELP50.* + # Temporary + if [[ $status -eq 150 ]]; then + echo "[$status]" + echo "Sorry, something's wrong! Please let sysadmins@cs50.harvard.edu know." + # If `script` was killed, in which case `exit 1` above won't execute - if [[ $status -ne 1 ]]; then + elif [[ $status -ne 1 ]]; then # Without this, prompt ends up below and to right of "Session terminated, killing shell... ...killed." echo -e "\r" + echo "[[$status]]" # Else if `script` exited on its own, as via ctl-d or `logout` else # Kill parent shell, since user presumably wants to exit - kill -SIGHUP "$HELP50" + echo "[[[$status]]]" + kill -SIGHUP $HELP50 fi } -# Parse argument -if [[ "$1" == "off" ]]; then - _off -elif [[ "$1" == "on" ]]; then - _on -else - echo "Usage: $0 [off|on]" - exit 1 -fi - -exit 0 - - - - - -# Helpers -# TODO -#. /opt/cs50/lib/help50 -. opt/cs50/lib/cli - -# Directory with helpers -# TODO -#HELPERS="/opt/cs50/lib/help50" -HELPERS="/mnt/opt/cs50/lib/help50" - -# Disable yes, lest users type it at prompt -if command -v yes &> /dev/null; then - function yes() { - if [[ -t 0 ]]; then - : - else - command yes - fi - } -fi - -# Ignore duplicates (but not commands that begin with spaces) -export HISTCONTROL="ignoredups" - -function _help50() { - - # Get exit status of last command - local status=$? - - # Get last command, independent of user's actual history - histfile=/tmp/help50.$$.history - HISTFILE=$histfile history -a - local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') - rm --force $histfile - - # If no typescript yet - if [[ -z $TYPESCRIPT ]]; then - - # Use this shell's PID as typescript's name, exporting so that subshells know script is already running - export TYPESCRIPT=/tmp/help50.$$.typescript - - # Make a typescript of everything displayed in terminal, - # without using exec, which causes sudo to hang, or tee, which triggers "Output is not a terminal" in vim; - # --append avoids `bash: warning: command substitution: ignored null byte in input`; - # --quiet suppresses `Script started...` - script --append --command "bash --login" --flush --quiet $TYPESCRIPT - - # Remove typescript before exiting this shell - rm --force $TYPESCRIPT - - # Now exit this shell too - exit - fi - - # If last command erred (and is not ctl-z) - # https://tldp.org/LDP/abs/html/exitcodes.html - if [[ $status -ne 0 && $status -ne 148 ]]; then - - # Ignore ./* if executable file - if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then - break - fi - - # Read typescript from disk - local typescript=$(cat $TYPESCRIPT) - - # 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) - - # Remove any line continuations from command line - local lines="" - while IFS= read -r line || [[ -n "$line" ]]; do - if [[ -z $done && $line =~ \\$ ]]; then - lines+="${line%\\}" - else - lines+="$line"$'\n' - local done=1 - fi - done <<< "$typescript" - typescript="$lines" - - # Remove command line from typescript - typescript=$(echo "$typescript" | sed '1d') - - # Remove ANSI characters - typescript=$(echo "$typescript" | ansi2txt) - - # Remove control characters - # https://superuser.com/a/237154 - typescript=$(echo "$typescript" | col -bp) - - # Try to get help - for helper in $HELPERS/*; do - if [[ -f $helper && -x $helper ]]; then - local help=$($helper <<< "$typescript") - if [[ -n "$help" ]]; then - break - fi - fi - done - if [[ -n "$help" ]]; then # If helpful - _helpful "$help" - elif [[ $status -ne 0 ]]; then # If helpless - _helpless "$typescript" - fi - fi - - # Truncate typescript - truncate -s 0 "$TYPESCRIPT" +function _stop() { + for pid in /tmp/help50.*.pid + do + kill -SIGTERM $(cat "$pid") + done } -# Default helpers -if ! type _helpful >/dev/null 2>&1; then - function _helpful() { - local output=$(_ansi "$1") - echo -e "\033[7m${output}\033[27m" # Reverse video - } -fi -if ! type _helpless >/dev/null 2>&1; then - function _helpless() { :; } # Silent -fi - -#if [[ ! "$PROMPT_COMMAND" =~ ^_help50\; ]]; then - #export PROMPT_COMMAND="_help50${PROMPT_COMMAND:+; $PROMPT_COMMAND}" -#fi -export PROMPT_COMMAND=_help50 - -# TODO: -# help on, off +# Parse argument +case "$1" in + disable) + _disable + ;; + enable) + _enable + ;; + is-enabled) + _is-enabled + ;; + start) + _start + ;; + stop) + _stop + ;; + *) + echo "Usage: $0 [disable|enable|is-enabled|start|stop]" + exit 1 + ;; +esac From 72dd67f793db0989910e2bfea1e71c15f9124475 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 10:19:34 -0400 Subject: [PATCH 42/85] WIP5 --- opt/cs50/bin/help50 | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index a643da2..ddd9712 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -6,11 +6,11 @@ if [[ `id -u` -eq 0 ]]; then fi function _disable() { - touch /tmp/help50.lock + touch /var/tmp/help50.lock } function _is-enabled() { - if [[ -f /tmp/help50.lock ]]; then + if [[ -f /var/tmp/help50.lock ]]; then echo disabled return 1 else @@ -20,7 +20,7 @@ function _is-enabled() { } function _enable() { - rm --force /tmp/help50.lock + rm --force /var/tmp/help50.lock } function _start() { @@ -36,18 +36,19 @@ function _start() { # Start `script` in background, using ; instead of && for command, # else if user logs out (as via ctl-d) after a non-0 command, bash exits with 127 set -o monitor - HELP50=$HELP50 script --command "bash --login ; exit 1" --flush --quiet --return /tmp/help50.$HELP50.typescript & + HELP50=$HELP50 script --command "bash --login ; exit 1" --flush --quiet --return /var/tmp/help50.$HELP50.typescript & + local pid=$! # Remember PID of `script`, in case user wants to turn off - echo $! > /tmp/help50.$HELP50.pid - ps aux + echo $pid > /var/tmp/help50.$HELP50.pid # Foreground `script`, without echoing command - fg > /dev/null - status=$? + #fg > /dev/null + wait + local status=$? # No longer helping - rm --force /tmp/help50.$HELP50.* + rm --force /var/tmp/help50.$HELP50.* # Temporary if [[ $status -eq 150 ]]; then @@ -71,7 +72,7 @@ function _start() { } function _stop() { - for pid in /tmp/help50.*.pid + for pid in /var/tmp/help50.*.pid do kill -SIGTERM $(cat "$pid") done From d5a938cffb87505af3218a607bf9b9de7ec00e0d Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 11:21:44 -0400 Subject: [PATCH 43/85] WIP6 --- opt/cs50/bin/help50 | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index ddd9712..138a28e 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -6,11 +6,11 @@ if [[ `id -u` -eq 0 ]]; then fi function _disable() { - touch /var/tmp/help50.lock + touch /tmp/help50.lock } function _is-enabled() { - if [[ -f /var/tmp/help50.lock ]]; then + if [[ -f /tmp/help50.lock ]]; then echo disabled return 1 else @@ -20,7 +20,7 @@ function _is-enabled() { } function _enable() { - rm --force /var/tmp/help50.lock + rm --force /tmp/help50.lock } function _start() { @@ -36,46 +36,31 @@ function _start() { # Start `script` in background, using ; instead of && for command, # else if user logs out (as via ctl-d) after a non-0 command, bash exits with 127 set -o monitor - HELP50=$HELP50 script --command "bash --login ; exit 1" --flush --quiet --return /var/tmp/help50.$HELP50.typescript & - local pid=$! - - # Remember PID of `script`, in case user wants to turn off - echo $pid > /var/tmp/help50.$HELP50.pid - - # Foreground `script`, without echoing command - #fg > /dev/null - wait + HELP50=$HELP50 script --append --command "bash --login ; exit 1" --flush --quiet --return /tmp/help50.$HELP50.typescript local status=$? # No longer helping - rm --force /var/tmp/help50.$HELP50.* - - # Temporary - if [[ $status -eq 150 ]]; then - echo "[$status]" - echo "Sorry, something's wrong! Please let sysadmins@cs50.harvard.edu know." + rm --force /tmp/help50.$HELP50.* # If `script` was killed, in which case `exit 1` above won't execute - elif [[ $status -ne 1 ]]; then + if [[ $status -ne 1 ]]; then # Without this, prompt ends up below and to right of "Session terminated, killing shell... ...killed." echo -e "\r" - echo "[[$status]]" # Else if `script` exited on its own, as via ctl-d or `logout` else # Kill parent shell, since user presumably wants to exit - echo "[[[$status]]]" kill -SIGHUP $HELP50 fi } function _stop() { - for pid in /var/tmp/help50.*.pid - do - kill -SIGTERM $(cat "$pid") - done + local ppid=$(ps -o ppid= -p $$) # bash --login + local gppid=$(ps -o ppid= -p $ppid) # sh -c + local ggppid=$(ps -o ppid= -p $gppid) # script + kill -SIGTERM $ggppid } # Parse argument From f96d245a11e9edce2fccbbea5c4db2b08821d831 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 11:33:32 -0400 Subject: [PATCH 44/85] WIP7 --- etc/profile.d/cli.sh | 7 ++++++- etc/profile.d/help50.sh | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/etc/profile.d/cli.sh b/etc/profile.d/cli.sh index 779b08e..9727517 100644 --- a/etc/profile.d/cli.sh +++ b/etc/profile.d/cli.sh @@ -1,5 +1,5 @@ # If not root -if [ "$(whoami)" != "root" ]; then +if [ `id -u` -ne 0 ]; then # $PATH export PATH="/opt/cs50/bin":"/opt/bin":"$PATH" @@ -60,4 +60,9 @@ if [ "$(whoami)" != "root" ]; then # Valgrind export VALGRIND_OPTS="--memcheck:leak-check=full --memcheck:show-leak-kinds=all --memcheck:track-origins=yes" + + # Start help50 if enabled + if help50 is-enabled > /dev/null; then + help50 start + fi fi diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index be8592a..676e905 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -34,9 +34,9 @@ function _help50() { local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') rm --force $histfile - # If last command erred (and is not ctl-z) + # If last command erred (and is not ctl-c or ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html - if [[ $status -ne 0 && $status -ne 148 ]]; then + if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then # Ignore ./* if executable file if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then From 4b19fc42e3d353d91885cc1e4ac85e6d68e30b28 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 12:41:03 -0400 Subject: [PATCH 45/85] added WORKDIR --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1090923..aeff5be 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ rebuild: docker build --build-arg TAG=$(TAG) --build-arg VCS_REF=$(shell git rev-parse HEAD) --no-cache --tag cs50/cli:$(TAG) . run: - docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt cs50/cli:$(TAG) bash --login || true + docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --env WORKDIR=/mnt --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt cs50/cli:$(TAG) bash --login || true squash: depends docker-squash --tag cs50/cli:$(TAG) cs50/cli:$(TAG) From 28014567eb7e0c01438af246b26a8626a2cc8def Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 12:54:51 -0400 Subject: [PATCH 46/85] WIP7 --- etc/profile.d/help50.sh | 7 ++----- opt/cs50/bin/help50 | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 676e905..f33f7c6 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -98,14 +98,11 @@ function _help50() { if ! type _helpful >/dev/null 2>&1; then function _helpful() { local output=$(_ansi "$1") - echo -e "\033[7m${output}\033[27m" # Reverse video + echo -e "\033[33m${output}\033[39m" # Yellow } fi if ! type _helpless >/dev/null 2>&1; then - #function _helpless() { :; } # Silent - function _helpless() { - echo TODO - } + function _helpless() { :; } # Silent fi export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 138a28e..7e46f6d 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -57,9 +57,9 @@ function _start() { } function _stop() { - local ppid=$(ps -o ppid= -p $$) # bash --login - local gppid=$(ps -o ppid= -p $ppid) # sh -c - local ggppid=$(ps -o ppid= -p $gppid) # script + local ppid=$(ps -o ppid= -p $$) # bash --login + local gppid=$(ps -o ppid= -p $ppid) # sh -c + local ggppid=$(ps -o ppid= -p $gppid) # script kill -SIGTERM $ggppid } From 35dd708fc8fe2125732848f094f83d43fa86242d Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 15:16:49 -0400 Subject: [PATCH 47/85] WIP8 --- etc/profile.d/help50.sh | 24 ++++++++++++++---------- opt/cs50/bin/help50 | 7 +++++++ opt/cs50/lib/cli | 36 ++++++++++-------------------------- opt/cs50/lib/help50/bash | 8 ++++++-- opt/cs50/lib/help50/cd | 19 +++++++++++++++++++ 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index f33f7c6..490e09f 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -10,15 +10,19 @@ HELPERS="/opt/cs50/lib/help50" . /opt/cs50/lib/cli # Disable yes, lest users type it at prompt -if command -v yes &> /dev/null; then - function yes() { - if [[ -t 0 ]]; then - : - else - command yes - fi - } -fi +function yes() { + if [[ -t 0 ]]; then + _alert "That was a rhetorical question. :)" + else + command yes + fi +} +alias y=yes + +# Don't override `n`, though +function no() { + _alert "That was a rhetorical question. :)" +} # Ignore duplicates (but not commands that begin with spaces) export HISTCONTROL="ignoredups" @@ -98,7 +102,7 @@ function _help50() { if ! type _helpful >/dev/null 2>&1; then function _helpful() { local output=$(_ansi "$1") - echo -e "\033[33m${output}\033[39m" # Yellow + _alert "$output" } fi if ! type _helpless >/dev/null 2>&1; then diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 7e46f6d..1120587 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -57,6 +57,13 @@ function _start() { } function _stop() { + + # If not helping + if [[ -z "$HELP50" ]]; then + return 0 + fi + + # Kill grandparent process (i.e., `script` itself) local ppid=$(ps -o ppid= -p $$) # bash --login local gppid=$(ps -o ppid= -p $ppid) # sh -c local ggppid=$(ps -o ppid= -p $gppid) # script diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index 9c1384d..0ea76be 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -1,3 +1,7 @@ +function _alert() { + echo -e "\033[33m${1}\033[39m" # Yellow +} + function _ansi() { # If command-line arguments @@ -10,8 +14,8 @@ function _ansi() { fi # Format backticks as bold - bold=$(printf '\033[1m') - normal=$(printf '\033[22m') + local bold=$(printf '\033[1m') + local normal=$(printf '\033[22m') echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" } @@ -22,31 +26,11 @@ function _search() { return fi - # Find any $1 in descendants - paths=$(find $(pwd) -name "$1" 2> /dev/null) - if [[ -z "$paths" ]]; then - - # Find any $1 in ancestors - local dir="$(dirname "$(pwd)")" - while [[ "$dir" != "/" ]]; do - paths=$(find "$dir" -maxdepth 1 -name "$1") - if [[ -z "$paths" ]]; then - dir=$(dirname "$dir") - else - break - fi - done - if [[ -z "$paths" ]]; then - - # Find any $1 relative to `cd` - pushd "$(cd && pwd)" > /dev/null - paths=$(find $(pwd) -name "$1" 2> /dev/null) - popd > /dev/null - fi - fi + # Find any $1 in descendants of $WORKDIR + paths=$(find "$WORKDIR" -name "$1" -printf "%T+ %p\n" | sort -nr | awk '{print $2}' 2> /dev/null) # Count paths - count=$(echo "$paths" | grep -c .) + local count=$(echo "$paths" | grep -c .) # If just one if [[ "$count" -eq 1 ]]; then @@ -60,7 +44,7 @@ function _sure() { if [[ $# -ne 1 ]]; then return 1 fi - prompt=$(echo "$1" | _ansi) + local prompt=$(echo "$1" | _ansi) while true; do read -p "$prompt [y/N] " -r if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 556b6e3..e0be583 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -2,6 +2,7 @@ output=$(cat) +# touch foo.py && foo.py regex="bash: (.*\.py): command not found" if [[ "$output" =~ $regex ]]; then @@ -12,12 +13,14 @@ if [[ "$output" =~ $regex ]]; then fi fi +# mkdir foo && ./foo regex="bash: \./([^:]*): Is a directory" if [[ "$output" =~ $regex ]]; then - echo "Cannot execute a directory. Did you mean to \`cd\` into \`${BASH_REMATCH[1]}\` instead?" + echo "Cannot execute a directory. Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" exit fi +# touch foo.c && ./foo.c regex="bash: \./(([^:]*)\.c): Permission denied" if [[ "$output" =~ $regex ]]; then @@ -28,7 +31,7 @@ if [[ "$output" =~ $regex ]]; then fi fi -regex="bash: (\./.*\.py): Permission denied" +regex="bash: \./(.*\.py): Permission denied" if [[ "$output" =~ $regex ]]; then # If file exists @@ -38,6 +41,7 @@ if [[ "$output" =~ $regex ]]; then fi fi +# touch foo && /.foo regex="bash: /\.([^:]*): No such file or directory" if [[ "$output" =~ $regex ]]; then diff --git a/opt/cs50/lib/help50/cd b/opt/cs50/lib/help50/cd index a9bf588..c451e02 100755 --- a/opt/cs50/lib/help50/cd +++ b/opt/cs50/lib/help50/cd @@ -1 +1,20 @@ #!/bin/bash + +. /opt/cs50/lib/cli + +output=$(cat) + +# mkdir -p foo/bar && cd bar +regex="cd: (.*): No such file or directory" +if [[ "$output" =~ $regex ]]; then + + # Search recursively for directory + dir="${BASH_REMATCH[1]}" + parent=$(_search "$dir") + echo -n "There isn't a directory called \`$dir\` in your current directory." + if [[ ! -z "$parent" ]]; then + echo " Did you mean to \`cd $parent\` first?" + else + echo + fi +fi From 29ddbfdf11223a1df267de984eb196c5b7cbede5 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 19:57:21 -0400 Subject: [PATCH 48/85] removed Makefile check --- opt/cs50/bin/make | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/opt/cs50/bin/make b/opt/cs50/bin/make index ca6cbd7..1548007 100755 --- a/opt/cs50/bin/make +++ b/opt/cs50/bin/make @@ -3,18 +3,14 @@ # If a single target and not an option if [[ $# -eq 1 ]] && [[ "$1" != -* ]]; then - # If no Makefile - if [[ ! -f Makefile && ! -f makefile ]]; then + # If target ends with .c or is a directory + if [[ "$1" == *?.c || -d "$1" ]]; then - # If target ends with .c or is a directory - if [[ "$1" == *?.c || -d "$1" ]]; then + # Don't suppress "Nothing to be done" with --silent + /usr/bin/make "$1" - # Don't suppress "Nothing to be done" with --silent - /usr/bin/make "$1" - - # Else make exits with 0 - exit 1 - fi + # Else make exits with 0 + exit 1 fi fi From 2ec2530183efb278f4175519db207a4a16cc1350 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 19:57:35 -0400 Subject: [PATCH 49/85] WIP8 --- etc/profile.d/help50.sh | 31 ++++++++++++++++--------------- opt/cs50/lib/cli | 15 ++++++++++----- opt/cs50/lib/help50/bash | 11 +++++++++++ opt/cs50/lib/help50/cd | 4 ++-- opt/cs50/lib/help50/make | 8 ++++---- opt/cs50/lib/help50/python | 2 +- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 490e09f..c38c649 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -9,21 +9,6 @@ HELPERS="/opt/cs50/lib/help50" # Library . /opt/cs50/lib/cli -# Disable yes, lest users type it at prompt -function yes() { - if [[ -t 0 ]]; then - _alert "That was a rhetorical question. :)" - else - command yes - fi -} -alias y=yes - -# Don't override `n`, though -function no() { - _alert "That was a rhetorical question. :)" -} - # Ignore duplicates (but not commands that begin with spaces) export HISTCONTROL="ignoredups" @@ -38,6 +23,11 @@ function _help50() { local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') rm --force $histfile + # Remove aliases + for name in n no y yes; do + unalias $name 2> /dev/null + done + # If last command erred (and is not ctl-c or ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then @@ -98,9 +88,20 @@ function _help50() { truncate -s 0 /tmp/help50.$HELP50.typescript } +function _question() { + _alert "That was a rhetorical question. :)" +} + # Default helpers if ! type _helpful >/dev/null 2>&1; then function _helpful() { + + # Intercept accidental invocation of `yes` and `n`, which are actual programs + for name in n no y yes; do + alias $name=_question + done + + # Output help local output=$(_ansi "$1") _alert "$output" } diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index 0ea76be..a119ede 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -19,15 +19,20 @@ function _ansi() { echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" } -function _search() { - - # In $1 is path to find - if [[ $# -ne 1 ]]; then +function _find() { + + # Usage + if [[ $# -eq 1 ]]; then # Files AND directories + local path="$1" + elif [[ $# -eq 3 && "$1" == "-type" && "$2" =~ ^[df]$ ]]; then # Files OR directories + local type="$1 $2" + local path="$3" + else return fi # Find any $1 in descendants of $WORKDIR - paths=$(find "$WORKDIR" -name "$1" -printf "%T+ %p\n" | sort -nr | awk '{print $2}' 2> /dev/null) + paths=$(find "$WORKDIR" -name "$path" -printf "%T+ %p\n" $type | sort -nr | awk '{print $2}' 2> /dev/null) # Count paths local count=$(echo "$paths" | grep -c .) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index e0be583..4288552 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -13,6 +13,17 @@ if [[ "$output" =~ $regex ]]; then fi fi +# mkdir foo && bar +regex="bash: (.*): command not found" +if [[ "$output" =~ $regex ]]; then + + # If directory exists + if [[ -d "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" + exit + fi +fi + # mkdir foo && ./foo regex="bash: \./([^:]*): Is a directory" if [[ "$output" =~ $regex ]]; then diff --git a/opt/cs50/lib/help50/cd b/opt/cs50/lib/help50/cd index c451e02..0722752 100755 --- a/opt/cs50/lib/help50/cd +++ b/opt/cs50/lib/help50/cd @@ -10,10 +10,10 @@ if [[ "$output" =~ $regex ]]; then # Search recursively for directory dir="${BASH_REMATCH[1]}" - parent=$(_search "$dir") + parent=$(_find -type d "$dir") echo -n "There isn't a directory called \`$dir\` in your current directory." if [[ ! -z "$parent" ]]; then - echo " Did you mean to \`cd $parent\` first?" + echo " Did you mean to run \`cd $parent\` first?" else echo fi diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index f23c37e..347f51b 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -9,7 +9,7 @@ if [[ "$output" =~ $regex ]]; then # If target is a directory if [[ -d "${BASH_REMATCH[1]}" ]]; then - echo "Cannot run \`make\` on a directory. Did you mean to \`cd ${BASH_REMATCH[1]}\` first?" + echo "Cannot run \`make\` on a directory. Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" exit fi @@ -17,7 +17,7 @@ if [[ "$output" =~ $regex ]]; then if [[ "${BASH_REMATCH[1]}" == *?.c ]]; then base="${BASH_REMATCH[1]%.c}" if [[ -n "$base" && ! -d "$base" ]]; then - echo "Did you mean to \`make ${base}\`?" + echo "Did you mean to run \`make ${base}\`?" exit fi fi @@ -32,10 +32,10 @@ if [[ "$output" =~ $regex ]]; then if [[ ! -f "$file" ]]; then # Search recursively for .c file - dir=$(_search "$file") + dir=$(_find -type f "$file") echo -n "There isn't a file called \`$file\` in your current directory." if [[ ! -z "$dir" ]]; then - echo " Did you mean to \`cd $dir\` first?" + echo " Did you mean to run \`cd $dir\` first?" else echo fi diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python index 8248449..8bc7a30 100755 --- a/opt/cs50/lib/help50/python +++ b/opt/cs50/lib/help50/python @@ -28,7 +28,7 @@ if [[ "$output" =~ $regex ]]; then # If .py is in $PWD if [[ -n "$path" && "$path" == $(basename "$path") ]]; then - dir=$(_search "$path") + dir=$(_find -type f "$path") echo -n "There isn't a file called \`$path\` in your current directory." if [[ ! -z "$dir" ]]; then echo " Did you mean to \`cd $dir\` first?" From db691ca7a62d7bcaee0c274e477da6e4b80a2958 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 21:20:39 -0400 Subject: [PATCH 50/85] WIP --- etc/profile.d/help50.sh | 7 ++++--- opt/cs50/lib/help50/bash | 6 ++++++ opt/cs50/lib/help50/clang | 14 ++++++++++++++ tests/bar.c | 1 - tests/baz.c | 0 tests/foo/baz.c | 0 tests/foo/foo2/foo3/x.c | 0 tests/foo/qux | 0 tests/foo/y.c | 0 tests/y.c | 0 10 files changed, 24 insertions(+), 4 deletions(-) create mode 100755 opt/cs50/lib/help50/clang delete mode 100644 tests/bar.c delete mode 100644 tests/baz.c delete mode 100644 tests/foo/baz.c delete mode 100644 tests/foo/foo2/foo3/x.c delete mode 100755 tests/foo/qux delete mode 100644 tests/foo/y.c delete mode 100644 tests/y.c diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index c38c649..a32ba2a 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -17,10 +17,10 @@ function _help50() { # Get exit status of last command local status=$? - # Get last command, independent of user's actual history + # Get last command line, independent of user's actual history histfile=/tmp/help50.$$.history HISTFILE=$histfile history -a - local argv0=$(HISTFILE=$histfile history 1 | cut -c 8- | awk '{print $1}') + local argv=$(HISTFILE=$histfile history 1 | cut -c 8-) # Could technically contain multiple commands, separated by ; or && rm --force $histfile # Remove aliases @@ -33,7 +33,8 @@ function _help50() { if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then # Ignore ./* if executable file - if [[ "$argv" =~ ^\./ && -f "$argv" && -x "$argv" ]]; then + local argv0=$(echo "$argv" | awk '{print $1}') # Assume for simplicity it's just a single command + if [[ "$argv0" =~ ^\./ && -f "$argv0" && -x "$argv0" ]]; then break fi diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 4288552..61521b7 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -17,6 +17,12 @@ fi regex="bash: (.*): command not found" if [[ "$output" =~ $regex ]]; then + # If typo + if [[ "${BASH_REMATCH[1]}" == "1s" ]]; then + echo "Did you mean to run \`ls\` (which starts with a lowercase L)?" + exit + fi + # If directory exists if [[ -d "${BASH_REMATCH[1]}" ]]; then echo "Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" diff --git a/opt/cs50/lib/help50/clang b/opt/cs50/lib/help50/clang new file mode 100755 index 0000000..8c7aa69 --- /dev/null +++ b/opt/cs50/lib/help50/clang @@ -0,0 +1,14 @@ +#!/bin/bash + +output=$(cat) + +# touch helpers.c && make helpers +regex="undefined reference to \`main'" +if [[ "$output" =~ $regex ]]; then + regex="make: \*\*\* \[: (.*)\] Error 1" + if [[ "$output" =~ $regex ]]; then + file="${BASH_REMATCH[1]}.c" + echo "Looks like \`$file\` does not have a \`main\` function. Did you mean to \`make\` something else?" + exit + fi +fi diff --git a/tests/bar.c b/tests/bar.c deleted file mode 100644 index b552c8e..0000000 --- a/tests/bar.c +++ /dev/null @@ -1 +0,0 @@ -int main(void) {} diff --git a/tests/baz.c b/tests/baz.c deleted file mode 100644 index e69de29..0000000 diff --git a/tests/foo/baz.c b/tests/foo/baz.c deleted file mode 100644 index e69de29..0000000 diff --git a/tests/foo/foo2/foo3/x.c b/tests/foo/foo2/foo3/x.c deleted file mode 100644 index e69de29..0000000 diff --git a/tests/foo/qux b/tests/foo/qux deleted file mode 100755 index e69de29..0000000 diff --git a/tests/foo/y.c b/tests/foo/y.c deleted file mode 100644 index e69de29..0000000 diff --git a/tests/y.c b/tests/y.c deleted file mode 100644 index e69de29..0000000 From 1b88177109c3a8f21457880e101d0bbe2f08e6d1 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 21:22:29 -0400 Subject: [PATCH 51/85] WIP --- etc/profile.d/cli.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/etc/profile.d/cli.sh b/etc/profile.d/cli.sh index 9727517..29be1a1 100644 --- a/etc/profile.d/cli.sh +++ b/etc/profile.d/cli.sh @@ -61,8 +61,8 @@ if [ `id -u` -ne 0 ]; then # Valgrind export VALGRIND_OPTS="--memcheck:leak-check=full --memcheck:show-leak-kinds=all --memcheck:track-origins=yes" - # Start help50 if enabled - if help50 is-enabled > /dev/null; then - help50 start - fi + ## TODO: Start help50 if enabled + # if help50 is-enabled > /dev/null; then + # help50 start + # fi fi From 9e770456f9743ca6eaf6f326034351e1456dcb59 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 8 May 2024 23:35:05 -0400 Subject: [PATCH 52/85] removed TAG from Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 15f61ad..2d9b392 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ rebuild: docker build --build-arg VCS_REF=$(shell git rev-parse HEAD) --no-cache --tag $(IMAGE) . run: - docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --env WORKDIR=/mnt --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt cs50/cli:$(TAG) bash --login || true + docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --env WORKDIR=/mnt --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt cs50/cli bash --login || true squash: depends docker-squash --tag $(IMAGE) $(IMAGE) From 6aa50eec88ca76bd98ec8dfda7e4f4d1924a4231 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 9 May 2024 00:16:34 -0400 Subject: [PATCH 53/85] removed hardcoding of typescript path --- etc/profile.d/help50.sh | 6 +++--- opt/cs50/bin/help50 | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index a32ba2a..b4c3438 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -18,7 +18,7 @@ function _help50() { local status=$? # Get last command line, independent of user's actual history - histfile=/tmp/help50.$$.history + histfile=$(mktemp) HISTFILE=$histfile history -a local argv=$(HISTFILE=$histfile history 1 | cut -c 8-) # Could technically contain multiple commands, separated by ; or && rm --force $histfile @@ -39,7 +39,7 @@ function _help50() { fi # Read typescript from disk - local typescript=$(cat /tmp/help50.$HELP50.typescript) + local typescript=$(cat $HELP50) # Remove script's own output (if this is user's first command) typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}') @@ -86,7 +86,7 @@ function _help50() { fi # Truncate typescript - truncate -s 0 /tmp/help50.$HELP50.typescript + truncate -s 0 $HELP50 } function _question() { diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 1120587..f6ca0fc 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -30,17 +30,17 @@ function _start() { return 0 fi - # PID of parent shell to help - local HELP50=$PPID + # Uniquely identify typescript using PID of parent shell to help + local HELP50=/tmp/help50.$PPID # Start `script` in background, using ; instead of && for command, # else if user logs out (as via ctl-d) after a non-0 command, bash exits with 127 set -o monitor - HELP50=$HELP50 script --append --command "bash --login ; exit 1" --flush --quiet --return /tmp/help50.$HELP50.typescript + HELP50=$HELP50 script --append --command "bash --login ; exit 1" --flush --quiet --return $HELP50 local status=$? # No longer helping - rm --force /tmp/help50.$HELP50.* + rm --force $HELP50 # If `script` was killed, in which case `exit 1` above won't execute if [[ $status -ne 1 ]]; then @@ -52,7 +52,7 @@ function _start() { else # Kill parent shell, since user presumably wants to exit - kill -SIGHUP $HELP50 + kill -SIGHUP $PPID fi } From 3eae3bde8a9075ff5273ce0c66b45c28a9f0f4cd Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 9 May 2024 14:57:03 -0400 Subject: [PATCH 54/85] omitting dot directories --- opt/cs50/lib/cli | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index a119ede..8705de1 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -31,8 +31,8 @@ function _find() { return fi - # Find any $1 in descendants of $WORKDIR - paths=$(find "$WORKDIR" -name "$path" -printf "%T+ %p\n" $type | sort -nr | awk '{print $2}' 2> /dev/null) + # Find $path in descendants of $WORKDIR, excluding hidden directories + paths=$(find "$WORKDIR" -not -path "*/.*" -name "$path" -printf "%T+ %p\n" $type | sort -nr | awk '{print $2}' 2> /dev/null) # Count paths local count=$(echo "$paths" | grep -c .) From e43789b74b11f6a6fb6328f0fb6ec83c534fca07 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 9 May 2024 14:57:13 -0400 Subject: [PATCH 55/85] added helpers --- opt/cs50/lib/help50/bash | 8 ++++++++ opt/cs50/lib/help50/cd | 7 +++++++ opt/cs50/lib/help50/make | 7 ++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 61521b7..4bd0fa1 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -37,6 +37,14 @@ if [[ "$output" =~ $regex ]]; then exit fi +# touch foo && cd foo +regex="bash: cd: (.*): Not a directory" +if [[ "$output" =~ $regex ]]; then + file="${BASH_REMATCH[1]}" + echo "Looks like you're trying to change directories, but \`$file\` isn't a directory." + exit +fi + # touch foo.c && ./foo.c regex="bash: \./(([^:]*)\.c): Permission denied" if [[ "$output" =~ $regex ]]; then diff --git a/opt/cs50/lib/help50/cd b/opt/cs50/lib/help50/cd index 0722752..8fa9262 100755 --- a/opt/cs50/lib/help50/cd +++ b/opt/cs50/lib/help50/cd @@ -18,3 +18,10 @@ if [[ "$output" =~ $regex ]]; then echo fi fi + +# cd.. || cd. +regex="bash: cd\.\.?: command not found" +if [[ "$output" =~ $regex ]]; then + echo "Did you mean to run \`cd ..\` instead?" + exit +fi diff --git a/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make index 347f51b..db79c38 100755 --- a/opt/cs50/lib/help50/make +++ b/opt/cs50/lib/help50/make @@ -9,7 +9,7 @@ if [[ "$output" =~ $regex ]]; then # If target is a directory if [[ -d "${BASH_REMATCH[1]}" ]]; then - echo "Cannot run \`make\` on a directory. Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" + echo "Cannot run \`make\` on a directory. Did you mean to run \`cd ${BASH_REMATCH[1]}\` instead?" exit fi @@ -17,7 +17,7 @@ if [[ "$output" =~ $regex ]]; then if [[ "${BASH_REMATCH[1]}" == *?.c ]]; then base="${BASH_REMATCH[1]%.c}" if [[ -n "$base" && ! -d "$base" ]]; then - echo "Did you mean to run \`make ${base}\`?" + echo "Did you mean to run \`make ${base}\` instead?" exit fi fi @@ -28,7 +28,8 @@ regex="make: \*\*\* No rule to make target '(.*)'" if [[ "$output" =~ $regex ]]; then # If no .c file for target - file="${BASH_REMATCH[1]}.c" + file="${BASH_REMATCH[1]}" + [[ "$file" == *.c ]] || file="$file.c" if [[ ! -f "$file" ]]; then # Search recursively for .c file From f2b776a977ac9e5217f52e5ad9e7112c85839553 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 10 May 2024 14:07:59 -0400 Subject: [PATCH 56/85] removed ls helper --- opt/cs50/lib/help50/ls | 1 - 1 file changed, 1 deletion(-) delete mode 100755 opt/cs50/lib/help50/ls diff --git a/opt/cs50/lib/help50/ls b/opt/cs50/lib/help50/ls deleted file mode 100755 index a9bf588..0000000 --- a/opt/cs50/lib/help50/ls +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash From 7a6c0516513f57e9c251fc1622d8beccecbc62cf Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 10 May 2024 14:12:50 -0400 Subject: [PATCH 57/85] handling lack of -type in _find --- opt/cs50/lib/cli | 1 + 1 file changed, 1 insertion(+) diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index 8705de1..14ee660 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -24,6 +24,7 @@ function _find() { # Usage if [[ $# -eq 1 ]]; then # Files AND directories local path="$1" + local type="" elif [[ $# -eq 3 && "$1" == "-type" && "$2" =~ ^[df]$ ]]; then # Files OR directories local type="$1 $2" local path="$3" From 7c3c7db27375b5f48c5dc42179d8782de4716882 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Fri, 10 May 2024 14:36:48 -0400 Subject: [PATCH 58/85] improved, documented Python helpers --- opt/cs50/lib/help50/python | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python index 8bc7a30..cc00030 100755 --- a/opt/cs50/lib/help50/python +++ b/opt/cs50/lib/help50/python @@ -4,7 +4,8 @@ output=$(cat) -regex="AttributeError: module 'cs50' has no attribute '(get_(float|int|string)|SQL)'$" +# touch cs50.py && python -c "import cs50; cs50.get_int" && python -c "from cs50 import get_int" +regex="AttributeError: module 'cs50' has no attribute '.*'|ImportError: cannot import name '.*' from 'cs50'" if [[ "$output" =~ $regex ]]; then if [[ -f cs50.py ]]; then echo "You have a file called \`cs50.py\` that is \"shadowing\" CS50's own. Best to rename that file with \`mv\`." @@ -12,14 +13,25 @@ if [[ "$output" =~ $regex ]]; then fi fi -regex='^\s*(import string\s*$|from\s+string\s+import\s+)' -if echo "$output" | grep -Pq "$regex"; then +# touch re.py && python -c "import re; re.search" && python -c "from re import search" +regex="AttributeError: module 're' has no attribute '.*'|ImportError: cannot import name '.*' from 're'" +if [[ "$output" =~ $regex ]]; then + if [[ -f re.py ]]; then + echo "You have a file called \`re.py\` that is \"shadowing\" Python's own. Best to rename that file with \`mv\`." + exit + fi +fi + +# touch re.py && python -c "import string; string.digits" && python -c "from string import digits" +regex="AttributeError: module 'string' has no attribute '.*'|ImportError: cannot import name '.*' from 'string'" +if [[ "$output" =~ $regex ]]; then if [[ -f string.py ]]; then echo "You have a file called \`string.py\` that is \"shadowing\" Python's own. Best to rename that file with \`mv\`." exit fi fi +# mkdir foo && touch foo/bar.py && python bar.py regex="python: can't open file '(.*\.py)': \[Errno 2\] No such file or directory" if [[ "$output" =~ $regex ]]; then From f5cffb7c90ad06c4a670b4d31e37a699c7a34b29 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 12 May 2024 23:55:48 -0400 Subject: [PATCH 59/85] removed break from CLI library --- etc/profile.d/cli.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/profile.d/cli.sh b/etc/profile.d/cli.sh index 29be1a1..d321990 100644 --- a/etc/profile.d/cli.sh +++ b/etc/profile.d/cli.sh @@ -62,7 +62,7 @@ if [ `id -u` -ne 0 ]; then export VALGRIND_OPTS="--memcheck:leak-check=full --memcheck:show-leak-kinds=all --memcheck:track-origins=yes" ## TODO: Start help50 if enabled - # if help50 is-enabled > /dev/null; then - # help50 start - # fi + if help50 is-enabled > /dev/null; then + help50 start + fi fi From 42c2a858f84f51e3c113076eea0a921d634b7112 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 12 May 2024 23:55:55 -0400 Subject: [PATCH 60/85] added _trap --- etc/profile.d/help50.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index b4c3438..8971b4b 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -35,7 +35,7 @@ function _help50() { # Ignore ./* if executable file local argv0=$(echo "$argv" | awk '{print $1}') # Assume for simplicity it's just a single command if [[ "$argv0" =~ ^\./ && -f "$argv0" && -x "$argv0" ]]; then - break + return fi # Read typescript from disk @@ -112,3 +112,16 @@ if ! type _helpless >/dev/null 2>&1; then fi export PROMPT_COMMAND=_help50 + +# touch foo.c && touch foo && ./foo +function _trap() { + if [[ "$BASH_COMMAND" =~ ^\./(.*)$ ]]; then + local src="${BASH_REMATCH[1]}.c" + local dst="${BASH_REMATCH[1]}" + if [[ "$src" -nt "$dst" ]]; then + local output=$(_ansi "It looks like \`$src\` has changed. Did you mean to run \`make $dst\` again?") + _alert "$output" + fi + fi +} +trap _trap DEBUG From f6e2329958c436d790178c5227b98ed86d2e8e2f Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sat, 18 May 2024 11:00:37 -0700 Subject: [PATCH 61/85] added helper for "syntax error near unexpected token" --- opt/cs50/lib/help50/bash | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 4bd0fa1..dcc2729 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -76,3 +76,10 @@ if [[ "$output" =~ $regex ]]; then exit fi fi + +# int main(void) || do { +regex="bash: syntax error near unexpected token \`.*'" +if [[ "$output" =~ $regex ]]; then + echo "Did you mean to type that in a file instead of your terminal window?" + exit +fi From 3192a87e5f5cc712b96f35d77362297ccd2cc4ba Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 24 Jun 2024 11:29:48 -0400 Subject: [PATCH 62/85] added detection of backslash --- opt/cs50/lib/help50/bash | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index dcc2729..796d8c6 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -13,20 +13,28 @@ if [[ "$output" =~ $regex ]]; then fi fi -# mkdir foo && bar +# mkdir foo && (foo || 1s || .\foo) regex="bash: (.*): command not found" if [[ "$output" =~ $regex ]]; then + # If directory exists + if [[ -d "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" + exit + fi + # If typo if [[ "${BASH_REMATCH[1]}" == "1s" ]]; then echo "Did you mean to run \`ls\` (which starts with a lowercase L)?" exit fi - # If directory exists - if [[ -d "${BASH_REMATCH[1]}" ]]; then - echo "Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" - exit + # If backslash instead of forward slash + if [[ "${BASH_REMATCH[1]}" =~ ^\.(.*) ]]; then + if [[ -f "./${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`./${BASH_REMATCH[1]}\`, with a forward slash instead?" + exit + fi fi fi From aa21d92b56e17cb8577b8b6a9233bd4c61fa5005 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 2 Jul 2024 19:08:16 -0400 Subject: [PATCH 63/85] detecting miscapitalized commands --- opt/cs50/lib/help50/bash | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 796d8c6..defd38b 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -29,6 +29,13 @@ if [[ "$output" =~ $regex ]]; then exit fi + # If uppercase + argv0="${BASH_REMATCH[1],,}" # Lowercase it + if command -v "$argv0" &> /dev/null; then + echo "Did you mean to run \`$argv0\`, in lowercase instead?" + exit + fi + # If backslash instead of forward slash if [[ "${BASH_REMATCH[1]}" =~ ^\.(.*) ]]; then if [[ -f "./${BASH_REMATCH[1]}" ]]; then From 52bce6759c9561777f71b2804bf3930f1df36a98 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 2 Jul 2024 22:39:45 -0400 Subject: [PATCH 64/85] passing $1, $2, etc. to helpers now --- etc/profile.d/help50.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 8971b4b..a805c1e 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -72,7 +72,7 @@ function _help50() { # Try to get help for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then - local help=$($helper <<< "$typescript") + local help=$($helper $argv <<< "$typescript") if [[ -n "$help" ]]; then break fi From 1fdf4a5283aa38a2bd97d83e8758ea44ec51052f Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Tue, 2 Jul 2024 22:39:59 -0400 Subject: [PATCH 65/85] added helper for, e.g., "check 50" --- opt/cs50/lib/help50/bash | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index defd38b..4f748de 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -36,6 +36,12 @@ if [[ "$output" =~ $regex ]]; then exit fi + # If CS50 command + if [[ "${BASH_REMATCH[1]}" =~ ^(check|style|submit)$ && "$2" == "50" ]]; then + echo "Did you mean to run \`${BASH_REMATCH[1]}50\`, without a space, instead?" + exit + fi + # If backslash instead of forward slash if [[ "${BASH_REMATCH[1]}" =~ ^\.(.*) ]]; then if [[ -f "./${BASH_REMATCH[1]}" ]]; then From fb3c69dc08f10cda5f1b3ef8203fdff3e4692b4f Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 11 Aug 2024 17:42:59 -0400 Subject: [PATCH 66/85] fixed _sure --- opt/cs50/lib/cli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index 14ee660..f757b99 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -55,7 +55,7 @@ function _sure() { read -p "$prompt [y/N] " -r if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then return 0 - elif [[ "${REPLY,,}" =~ ^(n|no)$ ]]; then + else return 1 fi done From 6baca23e70930a5f1fc00496df12635131a87373 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 11 Aug 2024 17:48:15 -0400 Subject: [PATCH 67/85] installing file command --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index b0415a4..d5b42dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -184,6 +184,7 @@ RUN apt update && \ dos2unix \ dnsutils `# For nslookup` \ expect `# For help50` \ + file `# For help50` \ fonts-noto-color-emoji `# For render50` \ gdb \ git \ From bbd48209eb1921dfafad5cef77f4e3569d5c3910 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 11 Aug 2024 17:48:27 -0400 Subject: [PATCH 68/85] improved DEBUG trap --- etc/profile.d/help50.sh | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index a805c1e..99ff86c 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -23,7 +23,7 @@ function _help50() { local argv=$(HISTFILE=$histfile history 1 | cut -c 8-) # Could technically contain multiple commands, separated by ; or && rm --force $histfile - # Remove aliases + # Remove any of these aliases for name in n no y yes; do unalias $name 2> /dev/null done @@ -113,15 +113,21 @@ fi export PROMPT_COMMAND=_help50 -# touch foo.c && touch foo && ./foo function _trap() { + + # touch foo.c && make foo && touch foo.c && ./foo if [[ "$BASH_COMMAND" =~ ^\./(.*)$ ]]; then local src="${BASH_REMATCH[1]}.c" local dst="${BASH_REMATCH[1]}" - if [[ "$src" -nt "$dst" ]]; then - local output=$(_ansi "It looks like \`$src\` has changed. Did you mean to run \`make $dst\` again?") - _alert "$output" + if [[ -f "$src" && $(file --brief --mime-type "$src") == "text/x-c" ]]; then + if [[ -x "$dst" && $(file --brief --mime-type "$dst") == "application/x-pie-executable" ]]; then + _helpful "It looks like \`$src\` has changed. Did you mean to run \`make $dst\` again?" + fi fi fi } + +# If the command run by the DEBUG trap returns a non-zero value, the next command is skipped and not executed +shopt -s extdebug + trap _trap DEBUG From 84d7819d67f7b97043451307e99f85d84b50a003 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sun, 11 Aug 2024 17:48:45 -0400 Subject: [PATCH 69/85] added Python helper --- opt/cs50/lib/help50/python | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python index cc00030..1d8b3fd 100755 --- a/opt/cs50/lib/help50/python +++ b/opt/cs50/lib/help50/python @@ -31,14 +31,26 @@ if [[ "$output" =~ $regex ]]; then fi fi -# mkdir foo && touch foo/bar.py && python bar.py +# mkdir -p foo/bar && touch foo/bar/baz.py && python baz.py regex="python: can't open file '(.*\.py)': \[Errno 2\] No such file or directory" if [[ "$output" =~ $regex ]]; then # Relative path from $PWD path=$(realpath --relative-to=. "${BASH_REMATCH[1]}") - # If .py is in $PWD + # If command was `python baz.py` (i.e., without a dirname) + if [[ -n "$path" && "$path" == $(basename "$path") ]]; then + dir=$(_find -type f "$path") + echo -n "There isn't a file called \`$path\` in your current directory." + if [[ ! -z "$dir" ]]; then + echo " Did you mean to \`cd $dir\` first?" + else + echo + fi + exit + fi + + # If command was `python bar/baz.py` (i.e., with a dirname) if [[ -n "$path" && "$path" == $(basename "$path") ]]; then dir=$(_find -type f "$path") echo -n "There isn't a file called \`$path\` in your current directory." From 21148f633830d45fa138cf06ee5b439e910f7d34 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 12 Aug 2024 15:28:08 -0400 Subject: [PATCH 70/85] removed DEBUG trap --- etc/profile.d/help50.sh | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 99ff86c..6fb5079 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -22,19 +22,34 @@ function _help50() { HISTFILE=$histfile history -a local argv=$(HISTFILE=$histfile history 1 | cut -c 8-) # Could technically contain multiple commands, separated by ; or && rm --force $histfile + local argv0=$(echo "$argv" | awk '{print $1}') # Assume for simplicity it's just a single command # Remove any of these aliases for name in n no y yes; do unalias $name 2> /dev/null done + # If last command was ./* + # touch foo.c && make foo && touch foo.c && ./foo + if [[ "$argv" =~ ^\./(.*)$ ]]; then + local src="${BASH_REMATCH[1]}.c" + local dst="${BASH_REMATCH[1]}" + if [[ -f "$src" && $(file --brief --mime-type "$src") == "text/x-c" ]]; then + if [[ -x "$dst" && $(file --brief --mime-type "$dst") == "application/x-pie-executable" ]]; then + if [[ "$src" -nt "$dst" ]]; then + _helpful "It looks like \`$src\` has changed. Did you mean to run \`make $dst\` again?" + fi + fi + fi + fi + # If last command erred (and is not ctl-c or ctl-z) # https://tldp.org/LDP/abs/html/exitcodes.html if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then # Ignore ./* if executable file - local argv0=$(echo "$argv" | awk '{print $1}') # Assume for simplicity it's just a single command if [[ "$argv0" =~ ^\./ && -f "$argv0" && -x "$argv0" ]]; then + echo XXX return fi @@ -72,6 +87,7 @@ function _help50() { # Try to get help for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then + echo "[$helper]" local help=$($helper $argv <<< "$typescript") if [[ -n "$help" ]]; then break @@ -112,22 +128,3 @@ if ! type _helpless >/dev/null 2>&1; then fi export PROMPT_COMMAND=_help50 - -function _trap() { - - # touch foo.c && make foo && touch foo.c && ./foo - if [[ "$BASH_COMMAND" =~ ^\./(.*)$ ]]; then - local src="${BASH_REMATCH[1]}.c" - local dst="${BASH_REMATCH[1]}" - if [[ -f "$src" && $(file --brief --mime-type "$src") == "text/x-c" ]]; then - if [[ -x "$dst" && $(file --brief --mime-type "$dst") == "application/x-pie-executable" ]]; then - _helpful "It looks like \`$src\` has changed. Did you mean to run \`make $dst\` again?" - fi - fi - fi -} - -# If the command run by the DEBUG trap returns a non-zero value, the next command is skipped and not executed -shopt -s extdebug - -trap _trap DEBUG From 50b0adc38676130f7ed1703eb36336610d1b70bf Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Mon, 12 Aug 2024 15:28:44 -0400 Subject: [PATCH 71/85] removed check for executable in ./ --- etc/profile.d/help50.sh | 6 ------ 1 file changed, 6 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 6fb5079..84b99af 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -47,12 +47,6 @@ function _help50() { # https://tldp.org/LDP/abs/html/exitcodes.html if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then - # Ignore ./* if executable file - if [[ "$argv0" =~ ^\./ && -f "$argv0" && -x "$argv0" ]]; then - echo XXX - return - fi - # Read typescript from disk local typescript=$(cat $HELP50) From 3f09407c90f487081786df699212d3e08bc7b5a7 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Wed, 14 Aug 2024 14:44:05 -0400 Subject: [PATCH 72/85] added _fold --- opt/cs50/lib/cli | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index f757b99..dcba5e3 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -16,7 +16,7 @@ function _ansi() { # Format backticks as bold local bold=$(printf '\033[1m') local normal=$(printf '\033[22m') - echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" + echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" | _fold } function _find() { @@ -46,6 +46,22 @@ function _find() { fi } +function _fold() { + + # If command-line arguments + if [[ -t 0 ]]; then + input="$*" + + # If standard input + else + input=$(cat) + fi + + # Wrap long lines + local cols=$(tput cols) + echo "$input" | fold --spaces --width=$cols +} + function _sure() { if [[ $# -ne 1 ]]; then return 1 From 56d8fb7af466a2113772772cb9932eecbbeb32b7 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Thu, 15 Aug 2024 11:29:55 -0400 Subject: [PATCH 73/85] style --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index d5b42dd..eaad0d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM ubuntu:24.04 as builder +FROM ubuntu:24.04 AS builder # Build-time variables From 3e9ca9e4965017ab28731bf97092f923fe23ed74 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sat, 5 Oct 2024 18:45:21 -0400 Subject: [PATCH 74/85] WIP --- Dockerfile | 1 + etc/profile.d/help50.sh | 3 +-- opt/cs50/lib/cli | 2 +- opt/cs50/lib/help50/bash | 14 +++++++++++--- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index eaad0d2..e87539b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -186,6 +186,7 @@ RUN apt update && \ expect `# For help50` \ file `# For help50` \ fonts-noto-color-emoji `# For render50` \ + fzf `# For help50` \ gdb \ git \ git-lfs \ diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 84b99af..65a63e5 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -81,7 +81,6 @@ function _help50() { # Try to get help for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then - echo "[$helper]" local help=$($helper $argv <<< "$typescript") if [[ -n "$help" ]]; then break @@ -100,7 +99,7 @@ function _help50() { } function _question() { - _alert "That was a rhetorical question. :)" + _alert "That was a rhetorical question. <3" } # Default helpers diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index dcba5e3..9db45f2 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -32,7 +32,7 @@ function _find() { return fi - # Find $path in descendants of $WORKDIR, excluding hidden directories + # Find $path in descendants of $WORKDIR, excluding hidden directories, most recently modified first paths=$(find "$WORKDIR" -not -path "*/.*" -name "$path" -printf "%T+ %p\n" $type | sort -nr | awk '{print $2}' 2> /dev/null) # Count paths diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash index 4f748de..bf00e0b 100755 --- a/opt/cs50/lib/help50/bash +++ b/opt/cs50/lib/help50/bash @@ -67,7 +67,7 @@ if [[ "$output" =~ $regex ]]; then fi # touch foo.c && ./foo.c -regex="bash: \./(([^:]*)\.c): Permission denied" +regex="bash: \./((.*)\.c): Permission denied" if [[ "$output" =~ $regex ]]; then # If file exists @@ -77,6 +77,7 @@ if [[ "$output" =~ $regex ]]; then fi fi +# touch foo.py && ./foo.py regex="bash: \./(.*\.py): Permission denied" if [[ "$output" =~ $regex ]]; then @@ -87,11 +88,18 @@ if [[ "$output" =~ $regex ]]; then fi fi +# echo "int main(void) {}" > foo && ./foo +regex="bash: \./([^\.]*): Permission denied" +if [[ "$output" =~ $regex ]]; then + if [[ $(file --brief --mime-type "${BASH_REMATCH[1]}") == "text/x-c" ]]; then + echo "Did you mean to give \`"${BASH_REMATCH[1]}"\` a name of \`"${BASH_REMATCH[1]}".c\` (and then compile it with \`make\`) instead?" + exit + fi +fi + # touch foo && /.foo regex="bash: /\.([^:]*): No such file or directory" if [[ "$output" =~ $regex ]]; then - - # If file exists if [[ -f "${BASH_REMATCH[1]}" ]]; then echo "Did you mean to run \`./${BASH_REMATCH[1]}\`?" exit From 8b8ae7830a294b6d337f1e7b9318ac5cb8789f54 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sat, 5 Oct 2024 19:50:15 -0400 Subject: [PATCH 75/85] WIP --- etc/profile.d/cli.sh | 2 +- etc/profile.d/help50.sh | 9 +++++++-- opt/cs50/bin/help50 | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/etc/profile.d/cli.sh b/etc/profile.d/cli.sh index d321990..9727517 100644 --- a/etc/profile.d/cli.sh +++ b/etc/profile.d/cli.sh @@ -61,7 +61,7 @@ if [ `id -u` -ne 0 ]; then # Valgrind export VALGRIND_OPTS="--memcheck:leak-check=full --memcheck:show-leak-kinds=all --memcheck:track-origins=yes" - ## TODO: Start help50 if enabled + # Start help50 if enabled if help50 is-enabled > /dev/null; then help50 start fi diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 65a63e5..6e801be 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -92,13 +92,15 @@ function _help50() { elif [[ $status -ne 0 ]]; then # If helpless _helpless "$typescript" fi + else + _helped fi # Truncate typescript truncate -s 0 $HELP50 } -function _question() { +function _rhetorical() { _alert "That was a rhetorical question. <3" } @@ -108,7 +110,7 @@ if ! type _helpful >/dev/null 2>&1; then # Intercept accidental invocation of `yes` and `n`, which are actual programs for name in n no y yes; do - alias $name=_question + alias $name=_rhetocial done # Output help @@ -116,6 +118,9 @@ if ! type _helpful >/dev/null 2>&1; then _alert "$output" } fi +if ! type _helped >/dev/null 2>&1; then + function _helped() { :; } # Silent +fi if ! type _helpless >/dev/null 2>&1; then function _helpless() { :; } # Silent fi diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index f6ca0fc..7b88dc5 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -56,6 +56,16 @@ function _start() { fi } +function _status() { + if [[ -n "$HELP50" ]]; then + echo started + return 0 + else + echo stopped + return 1 + fi +} + function _stop() { # If not helping @@ -84,11 +94,14 @@ case "$1" in start) _start ;; + status) + _status + ;; stop) _stop ;; *) - echo "Usage: $0 [disable|enable|is-enabled|start|stop]" + echo "Usage: $0 [disable|enable|is-enabled|start|status|stop]" exit 1 ;; esac From c6fe70f7804a9878ec1013bc342a10f1cb7ecf39 Mon Sep 17 00:00:00 2001 From: "David J. Malan" Date: Sat, 5 Oct 2024 20:12:27 -0400 Subject: [PATCH 76/85] WIP --- etc/profile.d/help50.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 6e801be..04dca38 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -105,6 +105,9 @@ function _rhetorical() { } # Default helpers +if ! type _helped >/dev/null 2>&1; then + function _helped() { :; } # Silent +fi if ! type _helpful >/dev/null 2>&1; then function _helpful() { @@ -118,9 +121,6 @@ if ! type _helpful >/dev/null 2>&1; then _alert "$output" } fi -if ! type _helped >/dev/null 2>&1; then - function _helped() { :; } # Silent -fi if ! type _helpless >/dev/null 2>&1; then function _helpless() { :; } # Silent fi From 11563e1cd0e6c7c874886f7a9c31dfd18213dcd4 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:12:18 -0400 Subject: [PATCH 77/85] Fix help50 bugs found in review - cli.sh: only start help50 in interactive shells with a terminal, else non-interactive login shells (bash --login -c) hang on script - lib/cli: _ansi and _fold take arguments by $#, not -t 0, so messages aren't dropped when stdin is redirected; _fold falls back to 80 columns - valgrind: source lib and use _alert/_ansi instead of undefined _help - help50.sh: fix _rhetocial typo; cap _helpless payload at 8 KiB - help50/python: handle python dir/file.py, use realpath --canonicalize-missing - Dockerfile: install bsdextrautils explicitly for col --- Dockerfile | 1 + etc/profile.d/cli.sh | 4 ++-- etc/profile.d/help50.sh | 6 ++++-- opt/cs50/bin/valgrind | 4 +++- opt/cs50/lib/cli | 13 +++++++------ opt/cs50/lib/help50/python | 11 ++++++----- 6 files changed, 23 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index c7e01e6..3ff4866 100644 --- a/Dockerfile +++ b/Dockerfile @@ -188,6 +188,7 @@ RUN apt update && \ apt install --no-install-recommends --no-install-suggests --yes \ astyle \ bash-completion \ + bsdextrautils `# For col, for help50` \ build-essential `# dpkg-dev, libc, gcc, g++, make, etc.` \ ca-certificates \ clang \ diff --git a/etc/profile.d/cli.sh b/etc/profile.d/cli.sh index 6a8dd61..d4efd2b 100644 --- a/etc/profile.d/cli.sh +++ b/etc/profile.d/cli.sh @@ -62,8 +62,8 @@ if [ `id -u` -ne 0 ]; then # Valgrind export VALGRIND_OPTS="--memcheck:leak-check=full --memcheck:show-leak-kinds=all --memcheck:track-origins=yes" - # Start help50 if enabled - if help50 is-enabled > /dev/null; then + # Start help50 if enabled (only for interactive shells with a terminal, else `script` would hang, e.g., in `bash --login -c`) + if [[ $- == *i* && -t 0 ]] && help50 is-enabled > /dev/null; then help50 start fi fi diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 04dca38..05cddd4 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -90,7 +90,9 @@ function _help50() { if [[ -n "$help" ]]; then # If helpful _helpful "$help" elif [[ $status -ne 0 ]]; then # If helpless - _helpless "$typescript" + + # 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)" fi else _helped @@ -113,7 +115,7 @@ if ! type _helpful >/dev/null 2>&1; then # Intercept accidental invocation of `yes` and `n`, which are actual programs for name in n no y yes; do - alias $name=_rhetocial + alias $name=_rhetorical done # Output help diff --git a/opt/cs50/bin/valgrind b/opt/cs50/bin/valgrind index dd19aa8..f318aab 100755 --- a/opt/cs50/bin/valgrind +++ b/opt/cs50/bin/valgrind @@ -1,8 +1,10 @@ #!/bin/bash +. /opt/cs50/lib/cli + # If run on Python program if [[ "$1" == "python" || "$1" == *.py ]]; then - echo "$(_help "Afraid \`valgrind\` does not support Python programs!")" + _alert "$(_ansi "Afraid \`valgrind\` does not support Python programs!")" exit 1 fi diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli index 9db45f2..6df5353 100644 --- a/opt/cs50/lib/cli +++ b/opt/cs50/lib/cli @@ -4,8 +4,8 @@ function _alert() { function _ansi() { - # If command-line arguments - if [[ -t 0 ]]; then + # If command-line arguments (don't test -t 0, else arguments are ignored whenever stdin is redirected) + if [[ $# -gt 0 ]]; then input="$*" # If standard input @@ -48,8 +48,8 @@ function _find() { function _fold() { - # If command-line arguments - if [[ -t 0 ]]; then + # If command-line arguments (don't test -t 0, else arguments are ignored whenever stdin is redirected) + if [[ $# -gt 0 ]]; then input="$*" # If standard input @@ -57,8 +57,9 @@ function _fold() { input=$(cat) fi - # Wrap long lines - local cols=$(tput cols) + # Wrap long lines (default to 80 columns if no terminal, e.g., when piped) + local cols=$(tput cols 2> /dev/null) + [[ "$cols" =~ ^[0-9]+$ ]] || cols=80 echo "$input" | fold --spaces --width=$cols } diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python index 1d8b3fd..c47be5d 100755 --- a/opt/cs50/lib/help50/python +++ b/opt/cs50/lib/help50/python @@ -35,8 +35,8 @@ fi regex="python: can't open file '(.*\.py)': \[Errno 2\] No such file or directory" if [[ "$output" =~ $regex ]]; then - # Relative path from $PWD - path=$(realpath --relative-to=. "${BASH_REMATCH[1]}") + # Relative path from $PWD (file doesn't exist, so don't require it to) + path=$(realpath --canonicalize-missing --relative-to=. "${BASH_REMATCH[1]}") # If command was `python baz.py` (i.e., without a dirname) if [[ -n "$path" && "$path" == $(basename "$path") ]]; then @@ -51,11 +51,12 @@ if [[ "$output" =~ $regex ]]; then fi # If command was `python bar/baz.py` (i.e., with a dirname) - if [[ -n "$path" && "$path" == $(basename "$path") ]]; then - dir=$(_find -type f "$path") + if [[ -n "$path" ]]; then + base=$(basename "$path") + dir=$(_find -type f "$base") echo -n "There isn't a file called \`$path\` in your current directory." if [[ ! -z "$dir" ]]; then - echo " Did you mean to \`cd $dir\` first?" + echo " Did you mean to run \`python $dir/$base\` instead?" else echo fi From 89610ded80bac817be5543e49f5843fbf35e14d2 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:12:18 -0400 Subject: [PATCH 78/85] Add smoke test for built image tests/smoke.sh checks a built image under timeouts: non-interactive login shells exit, help50 deps are installed, wrappers print with stdin redirected. Run via make smoke, and in CI before pushing to Docker Hub. --- .github/workflows/main.yml | 14 ++++++++++++++ Makefile | 5 ++++- tests/smoke.sh | 28 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100755 tests/smoke.sh diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a96ce3e..c87ff42 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,13 @@ jobs: cs50/cli:amd64 cs50/cli:canary-amd64 + - name: Check out tests + uses: actions/checkout@v7 + + - name: Smoke-test linux/amd64 build + run: | + bash tests/smoke.sh cs50/cli:canary-amd64 + - name: Push linux/amd64 build to Docker Hub if: ${{ github.ref == 'refs/heads/main' }} run: | @@ -68,6 +75,13 @@ jobs: cs50/cli:arm64 cs50/cli:canary-arm64 + - name: Check out tests + uses: actions/checkout@v7 + + - name: Smoke-test linux/arm64 build + run: | + bash tests/smoke.sh cs50/cli:canary-arm64 + - name: Push linux/arm64 build to Docker Hub if: ${{ github.ref == 'refs/heads/main' }} run: | diff --git a/Makefile b/Makefile index 2d9b392..f000d0e 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,10 @@ rebuild: docker build --build-arg VCS_REF=$(shell git rev-parse HEAD) --no-cache --tag $(IMAGE) . run: - docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --env WORKDIR=/mnt --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt cs50/cli bash --login || true + docker run --env LANG=$(LANG) --env LOCAL_WORKSPACE_FOLDER="$(PWD)" --env WORKDIR=/mnt --interactive --publish-all --rm --security-opt seccomp=unconfined --tty --volume "$(PWD)":/mnt --volume /var/run/docker.sock:/var/run/docker-host.sock --workdir /mnt $(IMAGE) bash --login || true + +smoke: + bash tests/smoke.sh $(IMAGE) squash: depends docker-squash --tag $(IMAGE) $(IMAGE) diff --git a/tests/smoke.sh b/tests/smoke.sh new file mode 100755 index 0000000..41ac041 --- /dev/null +++ b/tests/smoke.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Smoke-tests a built cs50/cli image. Usage: tests/smoke.sh [IMAGE] +# Each check has a timeout so that a regression that hangs the shell fails loudly. + +set -o errexit -o nounset -o pipefail + +IMAGE="${1:-cs50/cli}" +run() { timeout 60 docker run --rm "$@"; } + +echo "Checking $IMAGE" + +echo "- non-interactive login shell exits (help50 must not start without a terminal)" +run "$IMAGE" bash --login -c 'echo ok' | grep -qx ok +echo true | run --interactive "$IMAGE" bash --login + +echo "- runtime dependencies of help50 are installed" +run "$IMAGE" bash --login -c 'for c in ansi2txt col file script fold; do command -v "$c" > /dev/null || { echo "missing $c" >&2; exit 1; }; done' + +echo "- help50 controller is the Bash version, on PATH, and enabled by default" +run "$IMAGE" bash --login -c 'test "$(type -P help50)" = /opt/cs50/bin/help50 && help50 is-enabled && test "$(help50 status)" = stopped' > /dev/null + +echo "- wrappers print their message even when stdin is redirected" +run "$IMAGE" bash --login -c 'valgrind python x.py < /dev/null; test $? -eq 1' 2>&1 | grep -q 'does not support Python' + +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 "OK" From 637dd8c9ff3d3adc30c745323a5306c00290b755 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Mon, 21 Sep 2026 00:57:49 -0400 Subject: [PATCH 79/85] Ignore terminal echo before the command line in typescripts script records everything on the pty, including tab-completion listings, history recall, and redrawn prompts. _help50 dropped only the first line as the command, so after tab-completing a command with no output, the leftover echo was treated as its output and passed to _helpless. Now drop everything through the first line that ends with the command as recorded in history, joining backslash continuations and their PS2 prompts; fall back to dropping the first logical line if the command isn't found. --- etc/profile.d/help50.sh | 47 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 05cddd4..4805c89 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -56,21 +56,6 @@ function _help50() { # Cap typescript at MIN(1K lines, 1M bytes), else `read` is slow typescript=$(echo "$typescript" | head -n 1024 | cut -b 1-1048576) - # Remove any line continuations from command line - local lines="" - while IFS= read -r line || [[ -n "$line" ]]; do - if [[ -z $done && $line =~ \\$ ]]; then - lines+="${line%\\}" - else - lines+="$line"$'\n' - local done=1 - fi - done <<< "$typescript" - typescript="$lines" - - # Remove command line from typescript - typescript=$(echo "$typescript" | sed '1d') - # Remove ANSI characters typescript=$(echo "$typescript" | ansi2txt) @@ -78,6 +63,38 @@ function _help50() { # https://superuser.com/a/237154 typescript=$(echo "$typescript" | col -bp) + # Remove everything through the command line itself, as echoed by the terminal. + # It's usually the first line, but tab completion, history browsing, etc. echo + # more before it (e.g., a listing of completions, then a redrawn prompt), which + # would otherwise be mistaken for the command's output. So look for the first + # line that ends with the command (per history), joining any line continuations + # (and dropping their PS2 prompts) along the way; if not found, fall back to + # removing just the first (logical) line. + local command="${argv%"${argv##*[![:space:]]}"}" # Right-trimmed + local after_first="" after_command="" logical="" first="" found="" + while IFS= read -r line || [[ -n "$line" ]]; do + [[ -n "$found" ]] && after_command+="$line"$'\n' + [[ -n "$first" ]] && after_first+="$line"$'\n' + [[ -n "$found" ]] && continue + [[ -n "$logical" ]] && line="${line#"$PS2"}" + if [[ "$line" =~ \\$ ]]; then + logical+="${line%\\}" + continue + fi + logical+="$line" + first=1 + local trimmed="${logical%"${logical##*[![:space:]]}"}" + if [[ -n "$command" && "$trimmed" == *"$command" ]]; then + found=1 + fi + logical="" + done <<< "$typescript" + if [[ -n "$found" ]]; then + typescript="$after_command" + else + typescript="$after_first" + fi + # Try to get help for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then 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 80/85] 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 81/85] 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" From 3992a5ada941ac61dfdda807e97de5492ae5aebd Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 22 Sep 2026 02:36:29 -0400 Subject: [PATCH 82/85] Run `help50 COMMAND` as though COMMAND were typed directly Students used to run `help50 make foo`. Help now arrives automatically after any failed command, so run COMMAND with the same exit status and let the prompt hook handle the rest: help50 make foo behaves exactly like make foo. Builtins go through bash -c with the shell's error wording preserved; an unknown command fails with the shell's own "command not found" message so helpers match it. Bare help50 prints usage (exit 0) explaining the new behaviour. The subcommands (start/stop/...) are unchanged; only they require non-root. --- opt/cs50/bin/help50 | 56 ++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 7b88dc5..c534430 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -1,10 +1,5 @@ #!/bin/bash -# If root -if [[ `id -u` -eq 0 ]]; then - exit 1 -fi - function _disable() { touch /tmp/help50.lock } @@ -80,28 +75,43 @@ function _stop() { kill -SIGTERM $ggppid } +# Run a command as though typed directly. Students used to run `help50 make foo`; help now +# arrives automatically after any failed command, so just run the command, with the same +# exit status, and let the shell's prompt hook handle the rest. +function _run() { + if [[ -n "$(type -P "$1")" ]]; then + exec "$@" + elif [[ -n "$(type -t "$1")" ]]; then # Builtin or keyword, e.g., cd + exec bash -c "$(printf '%q ' "$@")" 2> >(sed 's/^bash: line [0-9]*: /bash: /' >&2) + else # Fail the way the shell would, so that helpers recognize it + echo "bash: $1: command not found" >&2 + exit 127 + fi +} + +function _usage() { + echo "Usage: help50 COMMAND [ARGS...]" + echo + echo "Runs COMMAND. If it fails, help appears automatically, as it now does for any" + echo "command you run, so you can also just run COMMAND directly." + echo + echo "Other usage: help50 [disable|enable|is-enabled|start|status|stop]" +} + # Parse argument case "$1" in - disable) - _disable - ;; - enable) - _enable - ;; - is-enabled) - _is-enabled - ;; - start) - _start - ;; - status) - _status + disable|enable|is-enabled|start|status|stop) + + # Not for root + if [[ `id -u` -eq 0 ]]; then + exit 1 + fi + _${1} ;; - stop) - _stop + ""|-h|--help) + _usage ;; *) - echo "Usage: $0 [disable|enable|is-enabled|start|status|stop]" - exit 1 + _run "$@" ;; esac From 1b189f730b440d25f07ba293a2d8bbae6a031ae8 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 22 Sep 2026 08:40:03 -0400 Subject: [PATCH 83/85] fix(review): make help50 COMMAND match a direct run, and test it Within a help50 session, define help50 as a shell function that evals COMMAND in the calling shell, so aliases (rm -i), functions, and cd behave exactly as they would directly, and no stderr is rewritten through an unwaited sed. In the script (outside a session), route path-shaped names (./foo.c, ./dir) through bash -c so the shell's own Permission denied / Is a directory errors reach the helpers instead of a synthesized "command not found"; wait for sed before exiting; pass -- to type so option-like commands don't leak usage noise. In the prompt hook, treat `help50 COMMAND` as COMMAND when deriving argv, so helpers that look at positional words (e.g., `check 50`) and the ./ re-make hint still fire. Smoke-test the passthrough: exit statuses, exact error text for unknown, option-like, builtin, and path-shaped commands, usage, sudo, the in-shell function, and the hook's argv handling. --- etc/profile.d/help50.sh | 22 ++++++++++++++++++++++ opt/cs50/bin/help50 | 16 ++++++++++++---- tests/smoke.sh | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 7e9eded..3d21308 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -22,6 +22,14 @@ function _help50() { HISTFILE=$histfile history -a local argv=$(HISTFILE=$histfile history 1 | cut -c 8-) # Could technically contain multiple commands, separated by ; or && rm --force $histfile + + # If run via `help50 COMMAND`, it's COMMAND that ran, so that's what helpers should see + if [[ "$argv" =~ ^help50[[:space:]]+([^[:space:]].*)$ ]]; then + local wrapped="${BASH_REMATCH[1]}" + if ! [[ "$wrapped" =~ ^(disable|enable|is-enabled|start|status|stop|-h|--help)([[:space:]]|$) ]]; then + argv="$wrapped" + fi + fi local argv0=$(echo "$argv" | awk '{print $1}') # Assume for simplicity it's just a single command # Remove any of these aliases @@ -132,6 +140,20 @@ function _rhetorical() { _alert "That was a rhetorical question. <3" } +# Run `help50 COMMAND` in this very shell, as though COMMAND had been typed directly, so that +# aliases (e.g., rm -i), functions, and builtins like cd behave exactly as they would have; +# /opt/cs50/bin/help50 handles the rest (and COMMAND too, outside of a help50 session) +function help50() { + case "$1" in + disable|enable|is-enabled|start|status|stop|""|-h|--help) + command help50 "$@" + ;; + *) + eval "$(printf '%q ' "$@")" + ;; + esac +} + # 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 diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index c534430..2bf77d3 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -77,12 +77,20 @@ function _stop() { # Run a command as though typed directly. Students used to run `help50 make foo`; help now # arrives automatically after any failed command, so just run the command, with the same -# exit status, and let the shell's prompt hook handle the rest. +# exit status, and let the shell's prompt hook handle the rest. (Within a help50 session, +# /etc/profile.d/help50.sh defines a `help50` function that runs the command in the shell +# itself instead, so that aliases, functions, and `cd` behave as they would directly.) function _run() { - if [[ -n "$(type -P "$1")" ]]; then + if [[ -n "$(type -P -- "$1")" ]]; then exec "$@" - elif [[ -n "$(type -t "$1")" ]]; then # Builtin or keyword, e.g., cd - exec bash -c "$(printf '%q ' "$@")" 2> >(sed 's/^bash: line [0-9]*: /bash: /' >&2) + elif [[ "$1" == */* || -n "$(type -t -- "$1")" ]]; then # Path, builtin, or keyword, e.g., ./foo.c, cd + + # Let bash itself report errors (e.g., Permission denied, Is a directory, No such + # file or directory), as it would have directly, so that helpers recognize them + bash -c "$(printf '%q ' "$@")" 2> >(sed 's/^bash: line [0-9]*: /bash: /' >&2) + local status=$? + wait $! 2> /dev/null # For sed to finish, lest its output arrive after the prompt + exit $status else # Fail the way the shell would, so that helpers recognize it echo "bash: $1: command not found" >&2 exit 127 diff --git a/tests/smoke.sh b/tests/smoke.sh index 10d4a36..c8511cf 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -46,6 +46,42 @@ run "$IMAGE" bash --login -c ' set -o history; history -s ./slow; set +o history false; _help50 test "$(cat /tmp/cmd)" = ./slow && test ! -s /tmp/output || exit 1 + + # A command run via help50 is reported as the command itself + : > "$HELP50" + set -o history; history -s "help50 ./slow"; set +o history + false; _help50 + test "$(cat /tmp/cmd)" = ./slow || exit 1 +' + +echo "- help50 COMMAND runs COMMAND, with its exit status" +run "$IMAGE" bash --login -c 'help50 true && ! help50 false && test "$(help50 echo x)" = x' +run "$IMAGE" bash --login -c 'help50 valgrind python x.py < /dev/null; test $? -eq 1' 2>&1 | grep -q 'does not support Python' + +echo "- help50 COMMAND fails as COMMAND would have, so that helpers recognize the error" +run "$IMAGE" bash --login -c 'out=$(help50 1s 2>&1); test $? -eq 127 && test "$out" = "bash: 1s: command not found"' +run "$IMAGE" bash --login -c 'out=$(help50 --version 2>&1); test $? -eq 127 && test "$out" = "bash: --version: command not found"' +run "$IMAGE" bash --login -c 'out=$(help50 cd nothere 2>&1); test $? -eq 1 && test "$out" = "bash: cd: nothere: No such file or directory"' +run "$IMAGE" bash --login -c 'cd "$(mktemp -d)" && touch foo.c && out=$(help50 ./foo.c 2>&1); test $? -eq 126 && test "$out" = "bash: ./foo.c: Permission denied"' +run "$IMAGE" bash --login -c 'cd "$(mktemp -d)" && mkdir foo && out=$(help50 ./foo 2>&1); test $? -eq 126 && test "$out" = "bash: ./foo: Is a directory"' + +echo "- help50 alone prints usage and exits 0" +run "$IMAGE" bash --login -c 'help50 | grep -q "^Usage: help50 COMMAND" && help50 -h > /dev/null && help50 --help > /dev/null' + +echo "- root (as via sudo) may run help50 COMMAND but not its subcommands" +run "$IMAGE" bash --login -c 'test "$(sudo help50 echo x)" = x && test -z "$(sudo help50 is-enabled 2>&1)"' + +echo "- within a help50 session, help50 COMMAND runs in the shell itself, so cd and aliases apply" +run "$IMAGE" bash --login -c ' + export HELP50=$(mktemp) + . /etc/profile.d/help50.sh + shopt -s expand_aliases + alias rm="echo aliased" + help50 cd /tmp && test "$PWD" = /tmp || exit 1 + test "$(help50 rm x)" = "aliased x" || exit 1 + help50 false; test $? -eq 1 || exit 1 + out=$(help50 cd nothere 2>&1); test $? -eq 1 && [[ "$out" == *"cd: nothere: No such file or directory" ]] || exit 1 + help50 is-enabled > /dev/null && test "$(help50 status)" = started || exit 1 ' echo "OK" From 0c07b8269bb3426749251888f1b3a41b57b8e68b Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:18:06 +0100 Subject: [PATCH 84/85] Bound the prompt hook's cost, and add a kill switch - Read the typescript bounded (first 64K + last 1M) instead of the whole file into a variable, so the prompt after a failed command no longer scales with how much it printed: 35 MB took 2.4 s, now 0.1 s, and 350 MB would have taken 24 s. - Run each helper under timeout (5 s, then SIGKILL), so a slow or stuck helper cannot stall the prompt; today's helpers can't block, but the framework accepts helpers in any language. - HELP50_DISABLED in the environment disables help50 at login and is reported by help50 is-enabled/status. Set as an organization-wide Codespaces secret, it turns help50 off for everyone at their next login without rebuilding an image; set by one user, it's a persistent personal opt-out. Smoke tests cover all three. --- etc/profile.d/help50.sh | 15 +++++++++++---- opt/cs50/bin/help50 | 8 +++++++- tests/smoke.sh | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 3d21308..03fcc3b 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -55,8 +55,15 @@ function _help50() { # https://tldp.org/LDP/abs/html/exitcodes.html if [[ $status -ne 0 && $status -ne 130 && $status -ne 148 ]]; then - # Read typescript from disk - local typescript=$(cat $HELP50) + # Read typescript from disk, bounded: at most the first 64K (where the command line is + # echoed) and the last 1M (where errors tend to be), so that a program that printed a + # great deal before failing doesn't stall the prompt while the whole file is read + local typescript + if [[ $(stat -c %s "$HELP50" 2> /dev/null || echo 0) -gt $((65536 + 1048576)) ]]; then + typescript=$(head -c 65536 "$HELP50"; echo; echo "[... output omitted ...]"; tail -c 1048576 "$HELP50") + else + typescript=$(cat "$HELP50") + fi # Remove script's own output (if this is user's first command) typescript=$(echo "$typescript" | sed '1{/^Script started on .*/d}') @@ -110,10 +117,10 @@ function _help50() { typescript="$after_first" fi - # Try to get help + # Try to get help, giving each helper a few seconds at most, lest a slow or stuck helper stall the prompt for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then - local help=$($helper $argv <<< "$typescript") + local help=$(timeout -k 1 5 $helper $argv <<< "$typescript") if [[ -n "$help" ]]; then break fi diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index 2bf77d3..e1c9615 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -5,7 +5,13 @@ function _disable() { } function _is-enabled() { - if [[ -f /tmp/help50.lock ]]; then + + # Kill switch: set in the environment (e.g., an organization-wide Codespaces secret) to + # turn help50 off for everyone at their next login, without rebuilding an image + if [[ -n "$HELP50_DISABLED" ]]; then + echo "disabled (HELP50_DISABLED is set)" + return 1 + elif [[ -f /tmp/help50.lock ]]; then echo disabled return 1 else diff --git a/tests/smoke.sh b/tests/smoke.sh index c8511cf..e89f54e 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -54,6 +54,43 @@ run "$IMAGE" bash --login -c ' test "$(cat /tmp/cmd)" = ./slow || exit 1 ' +echo "- the prompt hook stays fast after a failed command printed a huge amount of output" +run "$IMAGE" bash --login -c ' + export HELP50=$(mktemp) + _helpless() { printf "%s" "$1" > /tmp/output; } + . /etc/profile.d/help50.sh + + # 35 MB (4 million lines) of output, then an error, as script(1) records it. + # Reading the whole file into a variable took ~2.4 s here and scaled linearly. + { printf "$ ./huge\r\n"; seq 1 4000000 | sed "s/$/\r/"; printf "Error: boom\r\n"; } > "$HELP50" + size=$(stat -c %s "$HELP50") + set -o history; history -s ./huge; set +o history + start=$(date +%s%N); false; _help50; elapsed=$(( ($(date +%s%N) - start) / 1000000 )) + echo " hook took ${elapsed} ms for a ${size}-byte typescript" + test "$elapsed" -lt 1000 && + test "$(tail -n 1 /tmp/output)" = "Error: boom" || exit 1 +' + +echo "- a helper that hangs cannot stall the prompt" +run --user root "$IMAGE" bash --login -c ' + printf "#!/bin/bash\ncat > /dev/null\nsleep 60\n" > /opt/cs50/lib/help50/zz_hang && chmod 755 /opt/cs50/lib/help50/zz_hang + su ubuntu -c "bash --login -c '"'"' + export HELP50=\$(mktemp); . /etc/profile.d/help50.sh + printf \"\$ ./x\\r\\nsome error\\r\\n\" > \"\$HELP50\" + set -o history; history -s ./x; set +o history + start=\$(date +%s); false; _help50; elapsed=\$(( \$(date +%s) - start )) + echo \"hook took \${elapsed} s with a hung helper\"; test \"\$elapsed\" -lt 15 + '"'"'" +' + +echo "- HELP50_DISABLED in the environment keeps help50 from starting, and says so" +run "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled' +run --env HELP50_DISABLED=1 "$IMAGE" bash --login -c ' + out=$(help50 is-enabled); test $? -eq 1 && [[ "$out" == *HELP50_DISABLED* ]] || exit 1' +# In an interactive shell on a pty (script provides one), help50 starts by default but not when disabled +run "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^started' +run --env HELP50_DISABLED=1 "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^stopped' + echo "- help50 COMMAND runs COMMAND, with its exit status" run "$IMAGE" bash --login -c 'help50 true && ! help50 false && test "$(help50 echo x)" = x' run "$IMAGE" bash --login -c 'help50 valgrind python x.py < /dev/null; test $? -eq 1' 2>&1 | grep -q 'does not support Python' From 888add19ae0e8685015e8e176721ccb2c5cf41d5 Mon Sep 17 00:00:00 2001 From: Rongxin Liu <10591665+rongxin-liu@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:51:23 +0100 Subject: [PATCH 85/85] Treat false-y HELP50_DISABLED as unset, and document ctl-c under timeout - HELP50_DISABLED=0 (or false, no, off, case-insensitively) now counts as unset, so that an admin who sets the org secret to 0 to turn help50 back on gets what they asked for, rather than every student staying disabled with no error. The is-enabled message now shows the value and says to unset it. Smoke tests cover the false-y values and that the lock file is still honored when the environment doesn't disable. - Note in the prompt hook that timeout runs each helper in its own process group, so ctl-c no longer reaches a stuck helper; the timeout itself is the bound. --foreground would restore ctl-c but stop timeout from killing the helper's children, which would give back the hang this is meant to remove. --- etc/profile.d/help50.sh | 6 +++++- opt/cs50/bin/help50 | 16 +++++++++++----- tests/smoke.sh | 8 +++++++- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/etc/profile.d/help50.sh b/etc/profile.d/help50.sh index 03fcc3b..13684c8 100644 --- a/etc/profile.d/help50.sh +++ b/etc/profile.d/help50.sh @@ -117,7 +117,11 @@ function _help50() { typescript="$after_first" fi - # Try to get help, giving each helper a few seconds at most, lest a slow or stuck helper stall the prompt + # Try to get help, giving each helper a few seconds at most, lest a slow or stuck helper stall + # the prompt. Note that timeout runs the helper in its own process group, so ctl-c at the + # terminal no longer reaches the helper (it did before); the timeout itself is the bound. + # Not --foreground, which would restore ctl-c but stop timeout from killing the helper's + # children, so an orphaned child holding stdout open could stall the prompt indefinitely. for helper in $HELPERS/*; do if [[ -f $helper && -x $helper ]]; then local help=$(timeout -k 1 5 $helper $argv <<< "$typescript") diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 index e1c9615..f296249 100755 --- a/opt/cs50/bin/help50 +++ b/opt/cs50/bin/help50 @@ -7,11 +7,17 @@ function _disable() { function _is-enabled() { # Kill switch: set in the environment (e.g., an organization-wide Codespaces secret) to - # turn help50 off for everyone at their next login, without rebuilding an image - if [[ -n "$HELP50_DISABLED" ]]; then - echo "disabled (HELP50_DISABLED is set)" - return 1 - elif [[ -f /tmp/help50.lock ]]; then + # turn help50 off for everyone at their next login, without rebuilding an image. Values + # that read as false (0, false, no, off) count as unset, so that setting the secret to 0 + # re-enables help50 just as deleting it would, rather than silently keeping it off + case "${HELP50_DISABLED,,}" in + ""|0|false|no|off) ;; + *) + echo "disabled (HELP50_DISABLED=$HELP50_DISABLED; unset it to re-enable)" + return 1 + ;; + esac + if [[ -f /tmp/help50.lock ]]; then echo disabled return 1 else diff --git a/tests/smoke.sh b/tests/smoke.sh index e89f54e..3431339 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -86,7 +86,13 @@ run --user root "$IMAGE" bash --login -c ' echo "- HELP50_DISABLED in the environment keeps help50 from starting, and says so" run "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled' run --env HELP50_DISABLED=1 "$IMAGE" bash --login -c ' - out=$(help50 is-enabled); test $? -eq 1 && [[ "$out" == *HELP50_DISABLED* ]] || exit 1' + out=$(help50 is-enabled); test $? -eq 1 && [[ "$out" == *HELP50_DISABLED=1* && "$out" == *unset* ]] || exit 1' +# Values that read as false count as unset, so that setting the secret to 0 re-enables help50, as deleting it would +for value in 0 false FALSE no off ""; do + run --env HELP50_DISABLED="$value" "$IMAGE" bash --login -c 'help50 is-enabled | grep -qx enabled' +done +# The lock file (help50 disable) is still honored when the environment doesn't disable +run --env HELP50_DISABLED=0 "$IMAGE" bash --login -c 'help50 disable && ! help50 is-enabled && help50 enable && help50 is-enabled' > /dev/null # In an interactive shell on a pty (script provides one), help50 starts by default but not when disabled run "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^started' run --env HELP50_DISABLED=1 "$IMAGE" bash -c 'echo "help50 status; exit" | script -qc "bash --login -i" /dev/null' | grep -q '^stopped'