diff --git a/Dockerfile b/Dockerfile index 6cc5f54..d866ff8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -180,12 +180,16 @@ RUN apt update && \ ca-certificates \ clang \ clang-format \ + colorized-logs `# For help50` \ coreutils `# For fold` \ cowsay \ curl \ dos2unix \ dnsutils `# For nslookup` \ + expect `# For help50` \ + file `# For help50` \ fonts-noto-color-emoji `# For render50` \ + fzf `# For help50` \ gdb \ git \ git-lfs \ @@ -253,7 +257,6 @@ RUN pip3 install --no-cache-dir \ cs50 \ Flask \ Flask-Session \ - help50 \ pytest \ render50 \ setuptools \ diff --git a/Makefile b/Makefile index 728d8eb..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)" --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 + 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) 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 new file mode 100644 index 0000000..04dca38 --- /dev/null +++ b/etc/profile.d/help50.sh @@ -0,0 +1,128 @@ +# If not started +if [[ -z "$HELP50" ]]; then + return +fi + +# Directory with helpers +HELPERS="/opt/cs50/lib/help50" + +# Library +. /opt/cs50/lib/cli + +# 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 line, independent of user's actual 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 + 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 + + # Read typescript from disk + 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}') + + # 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 $argv <<< "$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 + else + _helped + fi + + # Truncate typescript + truncate -s 0 $HELP50 +} + +function _rhetorical() { + _alert "That was a rhetorical question. <3" +} + +# Default helpers +if ! type _helped >/dev/null 2>&1; then + function _helped() { :; } # Silent +fi +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=_rhetocial + done + + # Output help + local output=$(_ansi "$1") + _alert "$output" + } +fi +if ! type _helpless >/dev/null 2>&1; then + function _helpless() { :; } # Silent +fi + +export PROMPT_COMMAND=_help50 diff --git a/opt/cs50/bin/help50 b/opt/cs50/bin/help50 new file mode 100755 index 0000000..7b88dc5 --- /dev/null +++ b/opt/cs50/bin/help50 @@ -0,0 +1,107 @@ +#!/bin/bash + +# If root +if [[ `id -u` -eq 0 ]]; then + exit 1 +fi + +function _disable() { + touch /tmp/help50.lock +} + +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 + return 0 + fi + + # 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 $HELP50 + local status=$? + + # No longer helping + rm --force $HELP50 + + # If `script` was killed, in which case `exit 1` above won't execute + if [[ $status -ne 1 ]]; then + + # Without this, prompt ends up below and to right of "Session terminated, killing shell... ...killed." + echo -e "\r" + + # 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 $PPID + fi +} + +function _status() { + if [[ -n "$HELP50" ]]; then + echo started + return 0 + else + echo stopped + return 1 + fi +} + +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 + kill -SIGTERM $ggppid +} + +# Parse argument +case "$1" in + disable) + _disable + ;; + enable) + _enable + ;; + is-enabled) + _is-enabled + ;; + start) + _start + ;; + status) + _status + ;; + stop) + _stop + ;; + *) + echo "Usage: $0 [disable|enable|is-enabled|start|status|stop]" + exit 1 + ;; +esac diff --git a/opt/cs50/bin/http-server b/opt/cs50/bin/http-server index a0f1c26..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" @@ -9,22 +11,16 @@ port="-p 8080" options="--no-dotfiles" t="-t0" -# Formatting -bold=$(tput bold) -normal=$(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 - 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 "Are you sure you want to serve ${bold}${1}${normal} 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/make b/opt/cs50/bin/make index 4658e3e..1548007 100755 --- a/opt/cs50/bin/make +++ b/opt/cs50/bin/make @@ -1,23 +1,18 @@ #!/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 target ends with .c or is a directory + if [[ "$1" == *?.c || -d "$1" ]]; then -# Run make -if [[ -d "$1" ]]; then - echo "$1 is a directory" - exit 1 -else - /usr/bin/make -B -s $* + # Don't suppress "Nothing to be done" with --silent + /usr/bin/make "$1" + + # Else make exits with 0 + exit 1 + fi fi + +# Don't echo recipes +/usr/bin/make --always-make --silent "$@" diff --git a/opt/cs50/bin/sqlite3 b/opt/cs50/bin/sqlite3 index 3c1c0e7..b2dd85e 100755 --- a/opt/cs50/bin/sqlite3 +++ b/opt/cs50/bin/sqlite3 @@ -1,8 +1,6 @@ #!/bin/bash -# Formatting -bold=$(tput bold) -normal=$(tput sgr0) +. /opt/cs50/lib/cli # If data is coming from stdin (pipe or redirection) if [[ -p /dev/stdin || ! -t 0 ]]; then @@ -12,8 +10,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 - 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 @@ -21,13 +18,11 @@ 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 - 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 "Are you sure you want to create ${bold}$1${normal}? [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/bin/valgrind b/opt/cs50/bin/valgrind index 60a6429..dd19aa8 100755 --- a/opt/cs50/bin/valgrind +++ b/opt/cs50/bin/valgrind @@ -1,12 +1,8 @@ #!/bin/bash -# Formatting -bold=$(tput bold) -normal=$(tput sgr0) - # If run on Python program if [[ "$1" == "python" || "$1" == *.py ]]; then - echo "Afraid ${bold}valgrind${normal} does not support Python programs!" + echo "$(_help "Afraid \`valgrind\` does not support Python programs!")" exit 1 fi diff --git a/opt/cs50/lib/cli b/opt/cs50/lib/cli new file mode 100644 index 0000000..9db45f2 --- /dev/null +++ b/opt/cs50/lib/cli @@ -0,0 +1,78 @@ +function _alert() { + echo -e "\033[33m${1}\033[39m" # Yellow +} + +function _ansi() { + + # If command-line arguments + if [[ -t 0 ]]; then + input="$*" + + # If standard input + else + input=$(cat) + fi + + # Format backticks as bold + local bold=$(printf '\033[1m') + local normal=$(printf '\033[22m') + echo "$input" | sed "s/\`\\([^\`]*\\)\`/${bold}\\1${normal}/g" | _fold +} + +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" + else + return + fi + + # 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 + local 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 _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 + fi + local prompt=$(echo "$1" | _ansi) + while true; do + read -p "$prompt [y/N] " -r + if [[ "${REPLY,,}" =~ ^(y|yes)$ ]]; then + return 0 + else + return 1 + fi + done +} diff --git a/opt/cs50/lib/help50/bash b/opt/cs50/lib/help50/bash new file mode 100755 index 0000000..bf00e0b --- /dev/null +++ b/opt/cs50/lib/help50/bash @@ -0,0 +1,114 @@ +#!/bin/bash + +output=$(cat) + +# touch foo.py && foo.py +regex="bash: (.*\.py): command not found" +if [[ "$output" =~ $regex ]]; then + + # If file exists + if [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`python ${BASH_REMATCH[1]}\`?" + exit + fi +fi + +# 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 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 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 + echo "Did you mean to run \`./${BASH_REMATCH[1]}\`, with a forward slash instead?" + exit + fi + fi +fi + +# mkdir foo && ./foo +regex="bash: \./([^:]*): Is a directory" +if [[ "$output" =~ $regex ]]; then + echo "Cannot execute a directory. Did you mean to run \`cd ${BASH_REMATCH[1]}\`?" + 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 + + # 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 + +# touch foo.py && ./foo.py +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 + 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 [[ -f "${BASH_REMATCH[1]}" ]]; then + echo "Did you mean to run \`./${BASH_REMATCH[1]}\`?" + 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 diff --git a/opt/cs50/lib/help50/cd b/opt/cs50/lib/help50/cd new file mode 100755 index 0000000..8fa9262 --- /dev/null +++ b/opt/cs50/lib/help50/cd @@ -0,0 +1,27 @@ +#!/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=$(_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 run \`cd $parent\` first?" + else + 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/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/opt/cs50/lib/help50/make b/opt/cs50/lib/help50/make new file mode 100755 index 0000000..db79c38 --- /dev/null +++ b/opt/cs50/lib/help50/make @@ -0,0 +1,45 @@ +#!/bin/bash + +. /opt/cs50/lib/cli + +output=$(cat) + +regex="make: Nothing to be done for '(.*)'" +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]}\` instead?" + exit + fi + + # If target ends with .c + 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}\` instead?" + exit + fi + fi + +fi + +regex="make: \*\*\* No rule to make target '(.*)'" +if [[ "$output" =~ $regex ]]; then + + # If no .c file for target + file="${BASH_REMATCH[1]}" + [[ "$file" == *.c ]] || file="$file.c" + if [[ ! -f "$file" ]]; then + + # Search recursively for .c 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 run \`cd $dir\` first?" + else + echo + fi + exit + fi +fi diff --git a/opt/cs50/lib/help50/python b/opt/cs50/lib/help50/python new file mode 100755 index 0000000..1d8b3fd --- /dev/null +++ b/opt/cs50/lib/help50/python @@ -0,0 +1,64 @@ +#!/bin/bash + +. /opt/cs50/lib/cli + +output=$(cat) + +# 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\`." + exit + fi +fi + +# 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 -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 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." + if [[ ! -z "$dir" ]]; then + echo " Did you mean to \`cd $dir\` first?" + else + echo + fi + exit + fi +fi