Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Custom Drush commands

Your own Drush command runs module code from the terminal: a data import, a mailing, a clean-up. It is more convenient than a page in the panel when the work is long or has to run on a schedule. A command is a class method with an attribute.

Where a command lives

Drush looks for module commands in the src/Drush/Commands folder. Create the folders and the file by hand or with the drush generate drush:command-file command.

The first command

// web/modules/custom/blog/src/Drush/Commands/BlogCommands.php
namespace Drupal\blog\Drush\Commands;
 
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
 
final class BlogCommands extends DrushCommands
{
    #[CLI\Command(name: 'blog:subscribers')]
    public function subscribers(): void
    {
        $count = \Drupal::database()
            ->select('blog_subscribers')
            ->countQuery()
            ->execute()
            ->fetchField();
 
        $this->io()->writeln("Subscribers: $count");
    }
}

The CLI\Command attribute sets the command name. By convention it starts with the module name and a colon. The file name must end in Commands.php.

vendor/bin/drush cr
vendor/bin/drush blog:subscribers

The command prints the number of rows in the subscribers table, for example "Subscribers: 3".

Arguments

An argument is a required value after the command name. It arrives in a method parameter:

// web/modules/custom/blog/src/Drush/Commands/BlogCommands.php
#[CLI\Command(name: 'blog:unsubscribe')]
#[CLI\Argument(name: 'email', description: 'The subscriber address')]
public function unsubscribe(string $email): void
{
    $deleted = \Drupal::database()
        ->delete('blog_subscribers')
        ->condition('email', $email)
        ->execute();
 
    $this->io()->success("Rows deleted: $deleted");
}
vendor/bin/drush blog:unsubscribe anna@example.com

The command deletes the subscription and prints a green message. Without an address Drush reports that an argument is missing.

Options

An option is an optional switch with two hyphens. Options arrive in the last method parameter, the $options array, which holds the default values:

// web/modules/custom/blog/src/Drush/Commands/BlogCommands.php
#[CLI\Command(name: 'blog:list')]
#[CLI\Option(name: 'limit', description: 'How many addresses to show')]
public function list(array $options = ['limit' => 10]): void
{
    $emails = \Drupal::database()
        ->select('blog_subscribers', 's')
        ->fields('s', ['email'])
        ->range(0, (int) $options['limit'])
        ->execute()
        ->fetchCol();
 
    foreach ($emails as $email) {
        $this->io()->writeln($email);
    }
}
vendor/bin/drush blog:list --limit=3

Without the option the command shows ten addresses, with it — three.

Output

MethodWhat it prints
$this->io()->writeln('…')A line of text
$this->io()->success('…')A success message
$this->io()->error('…')An error message
$this->io()->table($headers, $rows)A table
$this->io()->confirm('…')A yes or no question, returns true or false
$this->logger()->notice('…')A notice-level log entry

Help

The descriptions from the attributes go into the help:

vendor/bin/drush help blog:unsubscribe
vendor/bin/drush list --filter=blog

The first command shows the arguments and options, the second all commands of the module.

Common mistake

Drush says the command is not defined. Check the path and the namespace: the file is in src/Drush/Commands, the namespace is Drupal\blog\Drush\Commands, the file name ends in Commands.php, and the module is enabled. Older guides put commands into src/Commands and register them in a drush.services.yml file — modern Drush versions do not need that. After adding the file run drush cr.

Thanks for reading!

Donate to the site — we work hard for you.