Do you often create custom (shell) scripts on your Drupal sites? Then you must know how often it can be painful to write and maintain such shell scripts. In this article, you'll learn to write custom site-wide Drush commands and use them instead of custom shell scripts where applicable.

Most complex custom scripts become hard to maintain unless you have a team-member who is well-versed in shell commands and syntax. Despite having such knowledge, such custom scripts often end up needing some tooling like logging, formatting, accepting user input, arguments, parameters, and flags.

Using site-wide Drush commands allows the use of cool features that come with the Symfony Console component instead of reinventing the wheel in all custom scripts.

Commandfiles that are installed in a Drupal site and are not bundled inside a Drupal module are called “site-wide” commandfiles.

In this article, we’ll take a look at how to write such a site-wide Drush command to replace a fairly simple Bash script.

Two-minute version

  • As Drupal developers, maintaining PHP scripts is easier.
  • Site-wide Drush commands are not bundled inside a module.
  • Create command files at drush/Commands/*Commands.php
  • Define commands as PHP class methods using annotations.
  • Use Symfony Console features in your commands.
  • Run shell commands using passthru() or equivalent.
  • See the source code for the examples used in this article.


Not everybody on a Drupal project is used to Shell scripting. Also, as Drupal developers, one finds it easier to work with PHP. Here’s a simple script:

#!/usr/bin/env php

echo "Hello world" . PHP_EOL;

This file can then be run from the terminal like a shell script. However, it is challenging if you want to do some fancy stuff like, for example:

  • Define arguments
  • Define options
  • Format messages, e.g. showing colorful output.
  • Handle logging and verbosity

Using sitewide Drush commands,  you have access to the feature-rich Symfony Console library which makes it easier to write and maintain custom scripts.

You can run shell commands using PHP’s passthru() or its equivalents.

Additionally, these commands can easily be discovered with drush list and their documentation can easily be accessed with the --help flag.

$ drush list | grep custom
  custom:hello-human (chh)             Display "Hello {name}".                                                
  custom:hello-world (chw)             Display "Hello world!".


To serve as an example, we’ll implement a custom script using Drush that does the following:

  • Displays Hello world! by default.
  • Displays Hello {name}! when a name is specified.
  • Displays an informal greeting if an --informal flag is passed.
  • Requires at least one name.
  • Displays additional names, if any.
  • Displays log messages when a --verbose flag is present.


The only thing you need is a standard Drupal 9+ project with Drush 10 or higher. You can install this all using Composer.

Basic commands

Site-wide Drush commands are defined as PHP classes in one of the following locations:

  • drush/Commands/*Commands.php
  • drush/Commands/Foo/*Commands.php, where Foo is a namespace.

For a basic example, here’s how we define a custom:hello-world command.

# File: PROJECT/drush/Commands/CustomCommands.php

namespace Drush\Commands;

class CustomHelloCommands extends DrushCommands {

   * Display "Hello world!".
   * @command custom:hello-world
   * @aliases chw
  public function helloWorld(): void {
    $this->output()->writeln('Hello world!');


If you run drush custom:hello-world from within your project, you should get a nice Hello world! Let’s take a look at some important parts of the code:

  • The namespace must be set to: Drush\Commands.
  • The class must extend DrushCommands.
  • The @command annotation makes Drush discover the command.
  • The @aliases allows your command to be run as drush chw instead of having to type drush custom:hello-world.
$ drush custom:hello-world
Hello world!

Advanced commands

In this section, we’ll create a command that takes some required arguments, optional arguments, and an option to modify the command’s behavior. Here’s a preview of the command in action.

$ drush custom:hello-human --informal Jigarius Jerry Fabiola Cartman
What up Jigarius.
Looks like you're not alone!
What up Jerry, Fabiola, and Cartman.

We implement this command by adding a method in the same class defined in the Basic Example above.

  * Display "Hello {name}".
  * @param string $name
  *   Name of the primary person to greet.
  * @param string[] $others
  *   Names of other people to greet.
  * @command custom:hello-human
  * @aliases chh
  * @option $informal Say "What up" instead of "Hello".
  * @usage custom:hello-human Anya
  * @usage custom:hello-human --informal Jerry
  * @usage custom:hello-human --verbose John Jane Gene
 public function helloHuman(string $name, array $others): void {
   $names = array_merge([$name], $others);
   $greeting = $this->input()->getOption('informal') ? 'What up' : 'Hello';
   // ...

Let’s analyze what’s going on in this command.


The drush custom:hello-human command needs at least one argument to work. This has been defined with string $name. Additionally, it takes a variadic argument array $others. It can be any number of values. Variadic arguments are defined as an array after all other arguments.


Options are used for passing optional instructions or flags which usually alter the behavior of a command. In our example, an --informal option has been defined with the @option annotation. If it is set, then an informal greeting is used instead of a formal Hello.

There are some options that Symfony Console adds by default. For example, --verbose displays some log messages that would otherwise be hidden.

What’s next

On this page

Never miss an article

Follow me on LinkedIn or Twitter and enable notifications.