Dot-slash revisited

There is a popular post “About the Use of Dot-Slash in Commands” at linfo.org that explains why ./ is used to run a program in the current directory.

However, there is an inaccuracy that bothers me:

When some text is typed into a shell and then the ENTER key is pressed, the shell assumes that it is a command. The shell immediately checks to see if the first string (i.e., sequence of characters) in that text is a built-in command or the absolute path (i.e., location relative to the root directory) to an executable.

Bash does not check “builtin vs absolute path”. It first checks whether the command name contains any ‘/’, i.e. whether it is a pathname, absolute or relative. For example, ./my_script, subdir/my_script, and /usr/bin/ls all count as “pathname”. The function that does the check is defined in general.c:

/* Return 1 if STRING is an absolute program name; it is absolute if it
   contains any slashes.  This is used to decide whether or not to look
   up through $PATH. */
int
absolute_program (const char *string)
{
#ifndef __MSYS__
  return ((char *)mbschr (string, '/') != (char *)NULL);
#else
  return ((char *)mbschr (string, '/') != (char *)NULL ||
	  (char *)mbschr (string, '\\') != (char *)NULL);
#endif
}

where mbschr() is Bash’s multibyte-aware version of strchr().

If the name contains a slash, Bash treats it as a pathname and executes that file using shell_execve() which can be found in execute_cmd.c:

/* Call execve(), handling interpreting shell scripts, and handling
   exec failures. */
int
shell_execve (char *command, char **args, char **env)
{
  int i;
  execve (command, args, env);
  i = errno;   /* error from execve() */
  /* ... shebang/ENOEXEC handling and error paths ... */
}

If the name does not contain a slash, Bash resolves the name in this order:

  1. Function
    If a shell function with that name exists, Bash executes it. Functions run in the current shell unless the command is in a pipeline or background job, in which case a subshell may be used. Because they share the shell environment, functions can directly mutate shell state.

  2. Builtin
    If a builtin command matches, Bash executes it. Builtins also run in the current shell (or a subshell in pipelines). Special builtins are resolved before functions, while ordinary builtins come after functions in the search order.

  3. $PATH
    If no function or builtin matches, Bash searches the directories in $PATH. This search (implemented in findcmd.c) yields a full pathname if found. Bash then calls shell_execve() to run the external program, the same mechanism used for names that already contained a slash.

If nothing is found, Bash tries the optional command_not_found_handle function, otherwise it prints “command not found” and returns 127.

Flow: flow chart


TL;DR: ./ is needed because Bash only looks in $PATH for names without /, and the current directory (.) is not in $PATH by default (primarily to prevent path hijacking/shadowing, e.g. a repo or writable dir dropping a fake ls, as the linfo.org post explains). A bare my_script goes through function → builtin → $PATH and fails, while ./my_script has a slash, so Bash treats it as a pathname and executes it directly.

References