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 7b88dc5..2bf77d3 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,51 @@ 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. (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 + exec "$@" + 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 + 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 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"