which command fails with a literal tilde because it searches for executables in $PATH directories, not user home directories, while type succeeds because it analyzes the shell’s internal state and performs tilde expansion itself.
This distinction explains why your terminal behavior changes depending on how you write the path. When you type ~/.local/bin/python, the shell expands this to /home/username/.local/bin/python before passing it to any utility. If you use which ~/.local/bin/python, the which binary receives the string ~/.local/bin/python as an argument. Since which looks for files in the system-wide search paths defined by the environment variable, it cannot find a file named literally containing a tilde character in those standard directories. Conversely, type is a shell builtin. It understands that ~ refers to the current user's home directory and resolves the path internally before reporting whether the command exists.
How Shell Expansion Works
Understanding this difference requires looking at the order of operations in a Unix shell like Bash. When you enter a command line, the shell processes several stages before executing the final program. One of the earliest stages is tilde expansion. This means that whenever the shell sees a~ at the beginning of a word, it replaces it with the value of the $HOME environment variable.
For example, if your home directory is /home/alice, the shell transforms ~/scripts into /home/alice/scripts. This transformation happens before the command name and its arguments are determined. Therefore, external commands like ls, cat, or which never see the tilde character. They only see the fully resolved absolute path. This is why ls ~ works perfectly; the shell hands ls the argument /home/alice, and ls lists that directory.
However, this automatic expansion does not happen inside all contexts. It primarily occurs when the tilde is unquoted and at the start of a word. If you quote the tilde, such as "~", the shell treats it as a literal string. This nuance is critical when debugging why some tools behave unexpectedly with relative or symbolic references.
Why Which Fails With Tilde
Thewhich command is an external executable, typically located at /usr/bin/which. Its sole purpose is to locate the first executable file in your $PATH that matches the given name. It does not perform shell expansions. When you run which ~/bin/script.sh, the shell has already expanded the path, so which receives /home/user/bin/script.sh as an argument.
If the script is not in a directory listed in $PATH, which will return nothing or indicate that the command was not found. More importantly, which is designed to find commands by name, not by full path. If you pass a full path to which, it checks if that specific file exists and is executable. However, many users mistakenly believe which should work with just the filename part of a home-relative path. Because which relies on $PATH, and $PATH rarely includes ~/bin by default (it usually includes /usr/bin, /usr/local/bin, etc.), using which with home-relative paths often leads to confusion.
To verify if a file exists, use the test command [ -f ~/script.sh ] instead of which. To check if a command is in your path, use which python3. Mixing these concepts causes the errors described in the original problem.
Why Type Succeeds
Thetype command is different because it is a shell builtin. Builtins are commands executed by the shell itself, not by launching a separate process. Because the shell is running the command, it has access to its own parsing logic, including tilde expansion. When you run type ~/bin/script.sh, the shell expands the tilde, then checks its internal database of known commands, aliases, functions, and keywords.
If the path points to a valid executable file, type will report that it is a file. If it is a function, it reports that. This makes type much more powerful for introspection than which. It tells you exactly what the shell *will* do when you type that command. For developers working with custom environments, type is the preferred tool for debugging command resolution issues.
Another related builtin is command -v. It behaves similarly to type but is more portable across different shells like sh or dash. Using command -v ~/bin/script.sh will also succeed where which might fail or give misleading results.
Best Practices for Path Handling
When writing scripts or troubleshooting Ubuntu systems, always be aware of which tool you are using. If you need to verify if an executable is in your system path, usewhich. If you need to know what the shell will execute for a specific command, including those in your home directory, use type or command -v.
Avoid using which to check for the existence of arbitrary files. It is strictly for finding executables in $PATH. For general file existence checks, use [ -e filename ] or test -e filename. This prevents confusion between "is this file here?" and "is this command in my path?".
Additionally, consider adding ~/bin to your $PATH if you store personal executables there. This allows which to find them naturally. You can add this to your .bashrc or .profile with the line export PATH="$HOME/bin:$PATH". Once added, which will correctly locate scripts in your home directory without needing full paths.
For more complex setup issues involving custom binaries, you might encounter undefined symbols or sourcing errors. See Troubleshooting Undefined Symbol After Sourcing ROS for help with library linking issues that often accompany custom path configurations.
Frequently Asked Questions
Can I use which with quoted tildes?
No, quoting the tilde prevents shell expansion, so which "~" passes the literal string to the command. Since no file named literally ~ exists in your $PATH directories, it will always fail.
Is type faster than which?
Yes, because type is a shell builtin, it avoids the overhead of spawning a new process. Which must launch an external binary, making it slightly slower, though the difference is negligible for single commands.
Why does which sometimes show multiple results?
Which only shows the first match in your $PATH. If you want to see all matches, use the -a flag, like which -a python. This helps diagnose shadowing issues where one version hides another.
How do I add my home bin folder to the path permanently?
Add export PATH="$HOME/bin:$PATH" to your ~/.bashrc file and then run source ~/.bashrc. This ensures every new shell session includes your personal scripts in the search path.
What is the difference between type and command -v?
They are nearly identical in bash, but command -v is POSIX compliant and works in other shells like dash or sh. Use command -v for maximum portability in cross-shell scripts.