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:subscribersThe 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.comThe 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=3Without the option the command shows ten addresses, with it — three.
Output
| Method | What 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=blogThe 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.