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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions etc/profile.d/help50.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 41 additions & 23 deletions opt/cs50/bin/help50
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
#!/bin/bash

# If root
if [[ `id -u` -eq 0 ]]; then
exit 1
fi

function _disable() {
touch /tmp/help50.lock
}
Expand Down Expand Up @@ -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
36 changes: 36 additions & 0 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading