Creating a Wrapper Bash Script to Replace the Drush Launcher

I run a shared hosting web server for a small number of clients. This web server is home to multiple content management systems including several different instances of Drupal.

To help myself and my users maintain their Drupal sites, every user on the server should be able to access Drush and use it on their own Drupal sites. Ideally this would be accomplished by simply typing drush in the command line ensuring it is easily accessible to both myself and my clients (though, mostly just for me).

Historically, the standard way of installing Drush required users to download Drush (the drush.phar file), set its execute permissions and then move it to /usr/local/bin/drush. This enabled the Drush command to be automatically globally accessible from the command line for every user. When Drupal shifted to using Composer to manage dependencies including Drush itself, Drushor more accurately, the Drush Launcherwould detect the presence of Drush in the Composer vendor directory and use that binary instead. This made managing multiple Drupal sites with multiple different versions of Drupal and Drush easy.

Then Drush 12 came out, dropping support for Drush Launcher, resulting in errors like:

The Drush launcher could not find a local Drush in your Drupal site.
Please add Drush with Composer to your project.

This change in compatibility was likely due to the community desperately doing what it could to move on from Drupal 7 and the older versions of Drush. I appreciate the motivations and believe that we are better for it. However this approach and recommended means of "installation," or, more aptly, means of making Drush globally accessible seemed not very globally accessible to my use case at all. The use case and installation process for a beloved and arguably mandatory tool should be consistentnot being radically different between major versions without providing a means to ensure that expected operations and functionality continue as such. One should not be required to type in ./vendor/bin/drush just to use Drush. One should also not be required to edit their $PATH variable in either their ~/.bashrc, ~/.bash_profile, or ~/.zshrc files to point to a binary installed by Composer for a specific site which could be one of many sites a single user could have, for all users who needs Drush access. (Link) This greatly reduces the flexibility that the previous implementation of Drush afforded those of us who run or use shared web hosting services. It simply isn't compatible with a user having more than one Drupal site.

One might say, "If you keep your Drupal instances up to date, you won't have this problem." While this is true, I believe this recommended approach contradicts the recommended process of having one Drush binary per Drupal site. The whole point of this is to ensure that your site is in alignment and stays in alignment with its dependencies, all of its dependencies, including Drush.

Consider performing a major upgrade to Drupal. In order to succeed at this, one typically must fully remove Drush from the project otherwise Composer will fail with an ambiguous error about dependencies. If one were to complete the upgrade of the site whose Drush binary is included in $PATH variable, and move onto another site one may find themselves running into issues with compatibility between the new version of Drush and the Drupal site they are trying to update. This makes the whole process crazy-making, especially when one starts to troubleshoot the Drush issues, checking Composer to find out that the correct version of Drush for the respective Drupal version is already installed. To overcome this, the user would have to play musical $PATH variables, and close and re-open their terminal to switch between Drush versions. This would also further complicate any upgrade troubleshooting processes that require users to switch back and forth between sites for more complex setups.

Googling this problem, the answers I could find were generally unhelpful. It seemed like guidance and recommendations from the community were generally geared towards single-user, single-site hosting circumstances. To overcome this, I produced a bash script that acts as a wrapper for Drush, emulating the previous functionality of the Drush Launcher's ability to select and use the site's respective instance of the Drush binary.

This wrapper will traverse the file system upwards looking for an instance of /vendor/bin/drush stopping at / and showing an error if it does not find an instance of the Drush binary. This ensures that the drush command will work regardless of where you are within your project's directories.

To install this "Lite Drush Launcher" simply create a file at /usr/local/bin/drush, with the following contents:

#!/usr/bin/env bash

SEARCH_DIR="$(pwd)"
while [ "$SEARCH_DIR" != "/" ]; do
  DRUSH_EXEC="$SEARCH_DIR/vendor/bin/drush"
  if [ -x "$DRUSH_EXEC" ]; then
    if [[ "$1" == "completion" || "$1" == "_completion" ]]; then
      exec "$DRUSH_EXEC" "$@"
    fi
    exec "$DRUSH_EXEC" "$@"
  fi
  SEARCH_DIR="$(dirname "$SEARCH_DIR")"
done

# Suppress error output if being sourced for shell completion
if [[ "$1" == "completion" || "$1" == "_completion" ]]; then
  exit 0
fi

echo "Error: No local Drush instance found." >&2
exit 1

Once you've created the file, make use it is executable with the command sudo chmod +x /usr/local/bin/drush.

BONUS: Drush Completion in Bash

If you were paying attention to the previous block of code you may have noticed that there was a line in there specific to error suppression for shell completion.

While researching this issue, I stumbled across the ability for Drush to support command line completion. Not a game changer per-se because the completion seems, incomplete, but definitely welcome none the less. The practice of relying on one Drush binary included for a single site could also have negative consequences for this functionality as well as it relates to other sites.

To enable autocompletion in bash, make sure that bash-completion is installed on your system and create a file at /etc/bash_completion.d/drush with the following contents:

# Dynamic Drush completion with prompt-based rebinding and file completion support

# Track the last known Drush exec path and completion function
__drush_last_fn=""
__drush_last_path=""

# Find nearest vendor/bin/drush upward from current directory
_find_drush_exec() {
  local dir="$PWD"
  while [ "$dir" != "/" ]; do
    if [ -x "$dir/vendor/bin/drush" ]; then
      echo "$dir/vendor/bin/drush"
      return
    fi
    dir="$(dirname "$dir")"
  done
}

# Dynamically (re)bind or unbind Drush completion based on current directory
_drush_auto_bind_completion() {
  local drush_exec=$(_find_drush_exec)

  if [ -z "$drush_exec" ]; then
    if [ -n "$__drush_last_fn" ]; then
      complete -r drush 2>/dev/null
      __drush_last_fn=""
      __drush_last_path=""
    fi
    return
  fi

  if [ "$__drush_last_path" = "$drush_exec" ]; then
    return
  fi

  local script
  script="$("$drush_exec" completion bash 2>/dev/null)"
  local fn_name
  fn_name=$(echo "$script" | grep -oP '^_[^()]+(?=\(\))' | head -n1)

  if [[ -n "$fn_name" ]]; then
    eval "$script"
    if declare -F "$fn_name" >/dev/null 2>&1; then
      complete -o default -F "$fn_name" drush
      __drush_last_fn="$fn_name"
      __drush_last_path="$drush_exec"
    fi
  fi
}

# Bootstrap once at shell load
_drush_auto_bind_completion

# Re-evaluate after each command prompt (e.g., after 'cd')
if [[ "$PROMPT_COMMAND" != *"_drush_auto_bind_completion"* ]]; then
  PROMPT_COMMAND="_drush_auto_bind_completion${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
fi

After doing this, when you are on the project directory of a site that has Drush installed, pressing tab while typing in a command will cause bash to complete the command for you.

For example: when you type drush sta <tab><tab> it will autocomplete to drush status. Nice!