You are here

module_builder.drush.inc in Module Builder 6.2

Same filename and directory in other branches
  1. 7 drush/module_builder.drush.inc

Module builder drush commands.

IMPORTANT: This file should be identical across versions of Drupal.

File

drush/module_builder.drush.inc
View source
<?php

/**
 * @file
 *  Module builder drush commands.
 *
 * IMPORTANT: This file should be identical across versions of Drupal.
 */

/**
 * Initialization.
 *
 * @todo: is there a hook to move this to?
 */

// Include common code.
include_once dirname(__FILE__) . '/../includes/common.inc';

// Set our environment.
define('MODULE_BUILDER_ENV', 'drush');

/**
 * Implementation of hook_drush_command().
 *
 * In this hook, you specify which commands your
 * drush module makes available, what it does and 
 * description.
 *
 * Notice how this structure closely resembles how 
 * you define menu hooks.
 * 
 * @See drush_parse_command() for a list of recognized keys.
 *
 * @return
 *   An associative array describing your command(s).
 */
function module_builder_drush_command() {
  $items = array();

  // the key in the $items array is the name of the command.
  $items['mb-build'] = array(
    'callback' => 'module_builder_callback_build',
    'description' => "Generate the code for a new Drupal module, including file headers and hook implementations.",
    'arguments' => array(
      'module name' => 'The machine name of the module.',
      'hooks' => 'Short names of hooks, separated by spaces.',
    ),
    'aliases' => array(
      'mb',
    ),
    'options' => array(
      '--noi' => "Disables interactive mode.",
      '--data' => "Location to read hook data. May be absolute, or relative to Drupal files dir. Defaults to 'files/hooks'.",
      '--build' => "Which file type to generate: 'all', 'code', 'info', 'FILE'. " . "'all' generates everything: info and any code files needed by the requested hooks. " . "'code' generates code files as needed. " . "'info' makes just the info file. " . "'module', 'install' make just the foo.module or foo.install files. " . "'If custom modules define other files to output, you can request those too, omitting the module root name part and any .inc extension, eg 'module_builder' for 'foo.module_builder.inc. " . "Default is 'all' if writing new files, 'code' if appending to file or outputting only to terminal.",
      '--write' => 'Write files to sites/all/modules. Will prompt to overwrite existing files; use --yes to force. Use --quiet to suppress output to the terminal.',
      '--go' => 'Write all module files and enable the new module. Take two commands into the shower? Not me.',
      '--add' => "Append hooks to module file. Implies '--write --build=code'. Warning: will not check hooks already exist.",
      '--name' => 'Readable name of the module.',
      '--desc' => 'Description (for the admin module list).',
      '--helptext' => 'Module help text (for the system help).',
      '--dep' => 'Dependencies, separated by spaces, eg "forum views".',
      '--package' => 'Module package.',
      '--parent' => "Name of a module folder to place this new module into; use if this module is to be added to an existing package. Use '.' for the current working directory.",
    ),
    'examples' => array(
      'drush mb my_module menu cron nodeapi' => 'Generate module code with hook_menu, hook_cron, hook_nodeapi.',
      'drush mb my_module --build=info --name="My module" --dep="forum views"' => 'Generate module info with readable name and dependencies.',
      'drush mb my_module menu cron --write --name="My module" --dep="forum views"' => 'Generate both module files, write files and also output to terminal.',
      'drush mb my_module menu cron --write ' => 'Generate module code, write files and also output to terminal.',
      'drush mb my_module menu cron --write --quiet --name="My module" --dep="forum views"' => 'Generate both module files, write files and output nothing to terminal.',
      'drush mb my_module menu cron --add' => 'Generate code for hook_cron and add it to the existing my_module.module file.',
      'drush mb my_module menu cron --write --parent=cck' => 'Generate both module files, write files to a folder my_module inside the cck folder.',
      'drush mb my_module menu cron --write --parent=.' => 'Generate both module files, write files to a folder my_module in the current working directory.',
    ),
  );
  $items['mb-download'] = array(
    'callback' => 'module_builder_callback_hook_download',
    'description' => "Update module_builder hook data.",
    'options' => array(
      '--data' => "Location to save downloaded files. May be absolute, or relative to Drupal files dir. Defaults to 'files/hooks'.",
    ),
    'aliases' => array(
      'mbdl',
    ),
  );
  $items['mb-list'] = array(
    'callback' => 'module_builder_callback_hook_list',
    'description' => "List the hooks module_builder knows about.",
  );
  $items['mb-analyze'] = array(
    'callback' => 'module_builder_callback_hook_analyze',
    'description' => "List the hooks found in a given module.",
    'aliases' => array(
      'mban',
    ),
  );
  $items['mb-dochooks'] = array(
    'callback' => 'module_builder_callback_doc_hooks',
    'description' => "Adds comment headers to hooks that need them in the given module.",
  );
  $items['mb-docparams'] = array(
    'callback' => 'module_builder_callback_doc_params',
    'description' => "Adds params... WIP!",
  );
  $items['mb-debug'] = array(
    'callback' => 'module_builder_callback_debug',
    'description' => "Debug module builder. Does whatever was needed at the time.",
  );
  $items['mb-dir'] = array(
    'callback' => 'module_builder_callback_get_data_dir',
    'description' => "Print the location of the module builder data directory.",
  );
  return $items;
}

/**
 * Implementation of hook_drush_help().
 *
 * This function is called whenever a drush user calls
 * 'drush help <name-of-your-command>'
 *
 * @param
 *   A string with the help section (prepend with 'drush:')
 *
 * @return
 *   A string with the help text for your command.
 */
function module_builder_drush_help($section) {
  switch ($section) {
    case 'drush:mb-build':
      return dt('Generates and optionally writes module code with the specified hooks. ' . 'By default this runs in interactive mode, and will prompt you for each ' . "of the module's properties. Use the --noi option to use as a single command.");
  }
}

/**
 * Module builder drush command callback.
 *
 * Form:
 * $drush mb machine_name hookA hookB hookC
 * where 'hookA' is the short name, ie 'menu' not hook_menu'.
 */
function module_builder_callback_build() {
  $commands = func_get_args();

  // Build the module data.
  $module_data = module_builder_build_data($commands);

  // What to build
  $build = drush_get_option('build');

  // write options:
  // - all -- everything we can do
  // - code -- code files, not info (module + install _ ..?)
  // - info -- only info fole
  // - module -- only module file
  // - install -- only install file
  // - ??? whatever hooks need
  // No build: set nice default.
  if (!$build) {

    // If we are adding, 'code' is implied
    if (drush_get_option('add')) {
      $build = 'code';
    }
    elseif (drush_get_option(array(
      'write',
      'go',
    ))) {
      $build = 'all';
    }
    else {
      $build = 'code';
    }
  }

  //print_r($build);

  // Make a list
  $build_list = explode(' ', $build);

  // Multi build: set a single string to switch on below.
  if (count($build_list) > 1) {
    $build = 'code';
  }

  //print_r($build_list);

  // Build files.
  // Include generating component file.
  module_builder_include('generate');

  // Build module code in all cases bar 'info'.
  if ($build != 'info') {

    // Check hook data file exists.
    if (!_module_builder_check_hook_data()) {
      return drush_set_error("DRUSH_NOT_COMPLETED', 'No hook definitions found. You need to download hook definitions before using this module: see the command 'mbdl'.");
    }
    module_builder_build_module($commands, $module_data, $build_list);
  }

  // Build info code in cases 'info' and 'all'.
  if ($build == 'info' or $build == 'all') {
    module_builder_build_info($commands, $module_data);
  }

  /*
  switch ($build) {
    case 'info':
      // info and stop
      module_builder_callback_info($commands, $module_data);
      break;
    case 'all':
      // info and fall through
      module_builder_callback_info($commands, $module_data);
    case 'code':
      // this is just here to look pretty
    default:
      // anything else, eg module, install etc
      module_builder_callback_module($commands, $module_data, $build_list);
  }
  */
  if (drush_get_option('go')) {
    pm_module_manage(array(
      array_shift($commands),
    ), TRUE);
  }
}

/**
 * Helper function to build the array of module_data.
 */
function module_builder_build_data($commands) {

  // Determine whether we're in interactive mode.
  $interactive = !drush_get_option(array(
    'non-interactive',
    'noi',
  ));

  // Information about the keys we need to build the module data.
  $data = array(
    'module_root_name' => array(
      'commands' => 0,
      'prompt' => dt('module name'),
      'required' => TRUE,
    ),
    // It is essential this key follow the root name, so that the the root
    // name gets to the commands array first.
    'hooks' => array(
      'commands' => 'all',
      'prompt' => dt('required hooks'),
      'required' => TRUE,
    ),
    'module_readable_name' => array(
      'key' => 'name',
      'prompt' => dt('human readable name'),
      'required' => TRUE,
      // A callback to generate the default value of this key.
      // The signature is foo($commands, $module_data)
      'default_callback' => 'module_builder_default_readable_name',
    ),
    'module_short_description' => array(
      'key' => 'desc',
      'prompt' => dt('description'),
      'default' => 'TODO: Description of module',
    ),
    'module_help_text' => array(
      'key' => 'helptext',
      'prompt' => dt('help text'),
    ),
    'module_dependencies' => array(
      'key' => 'dep',
      'prompt' => dt('dependencies'),
    ),
    'module_package' => array(
      'key' => 'package',
      'prompt' => dt('package'),
    ),
  );
  foreach ($data as $name => $definition) {

    // Merge in default values.
    $definition += array(
      'required' => FALSE,
    );

    // First pass: get data from either drush command line options...
    if (isset($definition['key'])) {
      $module_data[$name] = drush_get_option($definition['key']);
    }
    elseif (isset($definition['commands'])) {

      // A numeric value of 'commands' means take that index from the commands array.
      if (is_numeric($definition['commands']) && isset($commands[$definition['commands']])) {
        $module_data[$name] = $commands[$definition['commands']];
        unset($commands[$definition['commands']]);
      }
      else {
        $module_data[$name] = $commands;
      }
    }

    // Second pass: prompt the user for data.
    if ($interactive && empty($module_data[$name])) {
      $value = drush_prompt(dt('Enter the @type', array(
        '@type' => $definition['prompt'],
      )), NULL, $definition['required']);
      if ($value !== FALSE) {
        $module_data[$name] = $value;
      }
    }

    // Third pass: set a default value from the definition or a callback.
    if (empty($module_data[$name])) {
      if (isset($definition['default'])) {
        $module_data[$name] = $definition['default'];
        continue;
      }
      elseif (isset($definition['default_callback'])) {
        $function = $definition['default_callback'];
        $module_data[$name] = $function($commands, $module_data);
      }
    }
  }

  // Extra processing for the hooks array (or not).
  if (!is_array($module_data['hooks'])) {
    $module_data['hooks'] = preg_split('/\\s+/', $module_data['hooks']);
  }

  // Convert the array from numeric to keyed by full hook name.
  foreach ($module_data['hooks'] as $hook_name) {
    $hooks["hook_{$hook_name}"] = TRUE;
  }
  $module_data['hooks'] = $hooks;

  //print_r($module_data);
  return $module_data;
}

/**
 * Callback for generating default for module readable name. 
 */
function module_builder_default_readable_name($commands, $module_data) {
  return ucfirst(str_replace('_', ' ', $module_data['module_root_name']));
}

/**
 * Generates and outputs module code.
 *
 * @param $commands
 *    The drush array of commands.
 * @param $module_data
 *    An array of module data. Passed by reference so file information can
 *    be added by module_builder_generate_module().
 * @param $build_list
 *    An array of requested code files to output
 *    'code' or 'all' means all of them.
 */
function module_builder_build_module($commands, &$module_data, $build_list) {

  //drush_print_r($module_data);

  //exit;

  /*
  $input = drush_input('input?...');
  drush_print("You entered: >$input<");
  */

  // Generate all our code.
  $module_code = module_builder_generate_module($module_data, drush_get_option('add'));
  if (is_null($module_code)) {
    return drush_set_error('DRUSH_NOT_COMPLETED', 'No module code has been generated: perhaps you have specified invalid hook names or hooks this module does not know about.');
  }

  //print_r($build_list);
  if (!in_array($build_list[0], array(
    'code',
    'all',
  ))) {

    // We have keys in module_code that are entire filenames, eg 'foo.install'
    // We have array items in build_list that are sort of file endings, eg 'install'
    // Match them up!
    $requested_files = module_builder_requested_filenames($module_data['module_root_name'], array_keys($module_code), $build_list);
  }
  else {

    // Meh we want the lot.
    $requested_files = array_keys($module_code);
  }

  //print_r($requested_files);
  foreach ($requested_files as $filename) {
    $code = $module_code[$filename];
    module_builder_drush_output_code($module_data['module_root_name'], $filename, $code);
  }
  return;
}

/**
 * Figure out which of $real filenames are being requested in the list of $abbrev'iated ones.
 *
 * @return
 *  A flat array of filenames from $real. Those whose abreviations were not found.
 *  in $abbrev are removed.
 */
function module_builder_requested_filenames($module_root_name, $real, $abbrev) {

  //print_r($real);
  foreach ($real as $r) {
    $p = preg_replace(array(
      "[^{$module_root_name}\\.]",
      // module_name. at start
      '[\\.inc$]',
    ), array(
      '',
      '',
    ), $r);
    $processed[$r] = $p;

    // build an array with the real names as keys
    // and the abbrevs as values
  }

  //print_r($processed);

  //print_r($abbrev);

  // Intersecting thorws away values that are not in $abbrev
  // while keeping the real filename keys.
  $result = array_intersect($processed, $abbrev);

  //print_r($result);

  // We only care about the keys anyway
  return array_keys($result);
}

/**
 * Generates and outputs info file code.
 */
function module_builder_build_info($commands, $module_data) {
  module_builder_include('generate');
  $info_code = module_builder_generate_info_oo($module_data);

  /*
  module_builder_include('generate_info');
  $info_code = module_builder_generate_info($module_data);
  */
  module_builder_drush_output_code($module_data['module_root_name'], $module_data['module_root_name'] . '.info', $info_code);
}

/**
 * Output generated text, to terminal or to file.
 */
function module_builder_drush_output_code($module_root_name, $filename, $code) {

  // Output to terminal
  if (!drush_get_option('quiet')) {
    drush_print("Proposed {$filename}:");
    drush_print_r($code);
  }
  $write = drush_get_option('write');

  // Write to file
  // Add to file option implies write.
  // Write & go option implies write.
  if (drush_get_option(array(
    'write',
    'add',
    'go',
  ))) {
    $module_dir = pm_dl_destination('module');

    // Gee great. Drush HEAD doesn't give us the trailing /.
    if (substr($module_dir, -1, 1) != '/') {
      $module_dir .= '/';
    }

    // Drush tries to put any module into 'contrib' if the folder exists;
    // hack this out and put the code in 'custom'.
    if (substr($module_dir, -8, 7) == 'contrib') {
      $module_dir_custom = substr_replace($module_dir, 'custom', -8, 7);
      if (is_dir($module_dir_custom)) {
        $module_dir = $module_dir_custom;
      }
    }
    if (drush_get_option('parent')) {

      // The --parent option allows the user to specify a location for the new module folder.
      $parent_dir = drush_get_option('parent');
      if (substr($parent_dir, 0, 1) == '.') {

        // An initial . means to start from the current directory rather than
        // the modules folder, which allows submodules to be created where the
        // user is standing.
        $module_dir = drush_get_context('DRUSH_OLDCWD') . '/';

        // Remove both the . and the following /.
        $parent_dir = substr($parent_dir, 2);
        if ($parent_dir) {

          // If there's anything left (since just '.' is a valid option), append it.
          $module_dir .= $parent_dir . '/';
        }
      }
      else {
        $module_dir .= $parent_dir . '/';
      }
    }

    // $module_dir should now be a full path to the parent of the destination
    // folder, with a trailing slash.
    $module_dir .= $module_root_name;
    if (!is_dir($module_dir)) {
      @drush_op('mkdir', $module_dir, 0777);

      //drush_print("Module directory $module_dir created");
    }
    $filepath = $module_dir . '/' . $filename;

    // Add to file option
    // if file doesn't exist, we skip this and silently write it anyway
    if (drush_get_option('add') && file_exists($filepath)) {
      $fh = fopen($filepath, 'a');
      fwrite($fh, $code);
      fclose($fh);
      return;
    }

    // if file exists, ask for whether to overwrite
    if (file_exists($filepath)) {
      if (!drush_confirm(dt('File ' . $filename . ' exists. Do you really want to overwrite?'))) {
        return;
      }
    }
    file_put_contents($filepath, $code);
  }
}

/**
 * Ask the user for input. DOESN'T WORK.
 *
 * @param $msg The question to ask
 * @return The entered string.
 */
function drush_input($msg, $required = FALSE, $indent = 0) {
  print str_repeat(' ', $indent) . (string) $msg . ": ";
  while ($line = trim(fgets(STDIN))) {
    if (!$required or strlen($line) > 0) {
      return $line;
    }
    print 'we never get here wtf?';
    print str_repeat(' ', $indent) . (string) $msg . ": ";
  }
}

/**
 * Callback for downloading hook data.
 */
function module_builder_callback_hook_download() {
  $directory = _module_builder_get_hooks_directory();
  $return = module_builder_update_data();
  if (!$return) {
    return drush_set_error('Problem downloading hooks.');
  }
  else {
    drush_print("Hook files have been downloaded to {$directory} and processed.");
  }
}

/** 
 * Callback to list known hooks.
 */
function module_builder_callback_hook_list() {

  // Include generating component file.
  module_builder_include('process');
  $data = module_builder_get_hook_data();
  $time = module_builder_get_hook_data_last_updated();
  foreach ($data as $file => $hooks) {
    drush_print("Group {$file}:", 2);
    foreach ($hooks as $key => $hook) {
      drush_print($hook['name'] . ': ' . $hook['description'], 4);
    }
  }
  drush_print(t("Hook data retrieved from @dir.", array(
    '@dir' => _module_builder_get_hooks_directory(),
  )));
  drush_print(t("Hook data was processed on @time.", array(
    '@time' => $time,
  )));

  //print_r($data);
}

/**
 * Callback to list hook implementations found in a given module.
 */
function module_builder_callback_hook_analyze() {
  $commands = func_get_args();

  // The first argument is the module machine name.
  $module_root_name = array_shift($commands);

  // Include process component file.
  module_builder_include('process');
  $hooks = module_builder_get_hook_names(_module_builder_get_hooks_directory(), TRUE);
  foreach ($hooks as $key => $hook) {
    $hooks[$key] = $module_root_name . '_' . $hook;
  }
  $module_files = module_builder_get_module_files($module_root_name);

  //drush_print_r($module_files);
  foreach ($module_files as $file) {
    $functions = module_builder_get_functions($file);
    $module_hooks[$file] = array_intersect($functions, $hooks);
  }
  if (drush_get_option('flat')) {
  }
  drush_print_r($module_hooks);
  drush_print_r(array_merge($module_hooks));
}

/** 
 * Callback to add doc headers to existing hooks.
 */
function module_builder_callback_doc_hooks() {
  $commands = func_get_args();

  // The first argument is the module machine name.
  $module_root_name = array_shift($commands);
  $module_files = module_builder_get_module_files($module_root_name);

  // Include component files.
  module_builder_include('process');
  module_builder_include('generate');
  $hook_names = module_builder_get_hook_names('short');
  $pattern = '[(?<! \\* / \\n )' . "function \\ image_gallery _ ( \\w * )  # function declaration: capture hook name\n     ]mx";
  foreach ($module_files as $filename) {
    $code = file_get_contents($filepath . '/' . $filename);

    //print $code;

    // Get functions that have no docs.
    preg_match_all($pattern, $code, $function_names);

    // Get only those that are actual hooks.
    $bad_hooks = array_intersect($function_names[1], $hook_names);

    // For each hook that has no documentation.
    foreach ($bad_hooks as $hook_name) {
      $doc = module_builder_generate_hook_doxy("hook_{$hook_name}");
      $pattern2 = "[(?= function \\ image_gallery _ {$hook_name} )]x";
      $code = preg_replace($pattern2, $doc, $code);
    }
    if (!drush_get_option('quiet')) {
      print $code;
    }
    print 'Added hook documentation headers for: ' . implode(', ', $bad_hooks) . "\n";
    if (!drush_confirm(dt('Are you sure you want to overwrite ' . $filename . '?'))) {
      continue;
    }
    file_put_contents($filepath . '/' . $filename, $code);
  }
}

/**
 * Callback to output the location of the data directory.
 */
function module_builder_callback_get_data_dir() {
  drush_print('Module builder data is in ' . _module_builder_get_hooks_directory());
}

/**
 * WORK IN PROGRESS
 * Add function headers wherever needed with params.
 */
function module_builder_callback_doc_params() {
  $commands = func_get_args();
  print 'wip!!!';
  return;

  // The first argument is the module machine name.
  $module_root_name = array_shift($commands);
  $filepath = drupal_get_path('module', $module_root_name);

  //$old_dir = getcwd();

  //chdir($filepath);
  $files = scandir($filepath);
  foreach ($files as $filename) {
    $ext = substr(strrchr($filename, '.'), 1);
    if (in_array($ext, array(
      'module',
      'install',
      'inc',
    ))) {
      $module_files[] = $filename;
    }
  }

  // Include component files.
  module_builder_include('process');
  module_builder_include('generate');
  $hook_names = module_builder_get_hook_names('short');
  $pattern = '[
      / \\* \\* \\n    # start phpdoc
      \\ \\* \\ ( .* ) \\n  # first line of phpdoc: capture the text
  (?: \\ \\* .* \\n )* # lines of phpdoc
      \\ \\* /  \\n    # end phpdoc
      function \\ ( \\w* ) \\( ( .* ) \\) \\  { # function declaration: capture both entire declaration and name
  ]mx';
  foreach ($module_files as $filename) {
    $code = file_get_contents($filepath . '/' . $filename);

    //print $code;

    // Get functions that have no docs.
    preg_match_all($pattern, $code, $function_names);

    // Get only those that are actual hooks.

    //$bad_hooks = array_intersect($function_names[1], $hook_names);

    // For each hook that has no documentation.
    foreach ($bad_hooks as $hook_name) {
      $doc = module_builder_generate_hook_doxy("hook_{$hook_name}");
      $pattern2 = "[(?= function \\ image_gallery _ {$hook_name} )]x";
      $code = preg_replace($pattern2, $doc, $code);
    }
    if (!drush_get_option('quiet')) {

      // print $code;
    }
    print 'Added hook documentation headers for: ' . implode(', ', $bad_hooks) . "\n";
    if (!drush_confirm(dt('Are you sure you want to overwrite ' . $filename . '?'))) {
      continue;
    }

    //file_put_contents($filepath . '/' .$filename, $code);
  }
}

/**
 * Just for testing stuff on the commandline while developing the module.
 */
function module_builder_callback_debug() {

  /*
  include(dirname(__FILE__) . '/../includes/process.inc');
  include(dirname(__FILE__) . '/../includes/update.inc');

  module_builder_update_documentation();
  */
  return;
}

Functions

Namesort descending Description
drush_input Ask the user for input. DOESN'T WORK.
module_builder_build_data Helper function to build the array of module_data.
module_builder_build_info Generates and outputs info file code.
module_builder_build_module Generates and outputs module code.
module_builder_callback_build Module builder drush command callback.
module_builder_callback_debug Just for testing stuff on the commandline while developing the module.
module_builder_callback_doc_hooks Callback to add doc headers to existing hooks.
module_builder_callback_doc_params WORK IN PROGRESS Add function headers wherever needed with params.
module_builder_callback_get_data_dir Callback to output the location of the data directory.
module_builder_callback_hook_analyze Callback to list hook implementations found in a given module.
module_builder_callback_hook_download Callback for downloading hook data.
module_builder_callback_hook_list Callback to list known hooks.
module_builder_default_readable_name Callback for generating default for module readable name.
module_builder_drush_command Implementation of hook_drush_command().
module_builder_drush_help Implementation of hook_drush_help().
module_builder_drush_output_code Output generated text, to terminal or to file.
module_builder_requested_filenames Figure out which of $real filenames are being requested in the list of $abbrev'iated ones.

Constants

Namesort descending Description
MODULE_BUILDER_ENV