-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmodule_builder.drush.inc
More file actions
1194 lines (1044 loc) · 43.6 KB
/
module_builder.drush.inc
File metadata and controls
1194 lines (1044 loc) · 43.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* @file
* Module builder drush commands.
*/
/**
* Implementation of hook_drush_init().
*
* Include common code.
*/
function module_builder_drush_init() {
$command_info = drush_get_command();
if ($command_info['commandfile'] == 'module_builder') {
module_builder_drush_init_helper();
}
}
/**
* Load the file for the factory class and set the environment on it.
*
* Helper for hook_drush_init() and completion callbacks.
*/
function module_builder_drush_init_helper() {
// Set up the MB factory.
\DrupalCodeBuilder\Factory::setEnvironmentLocalClass('Drush')
->setCoreVersionNumber(drush_drupal_version());
}
/**
* Implementation of hook_drush_command().
*/
function module_builder_drush_command() {
$items = array();
$items['mb-build'] = array(
'callback' => 'drush_module_builder_callback_build_module',
'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. Use '.' to specify the current folder name.",
'hooks' => "Short names of hooks, separated by spaces.",
'presets:' => "Names of preset groups of hooks, e.g., 'block'." . "\n" .
"Use the 'presets:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b presets: block'.",
'plugins:' => "Sets of properties for one ore more plugins. " .
"Each set is separated with a ':', and contains: \n" .
" - the plugin type (this is the suffix of the plugin manager service ID)\n" .
" - the plugin name\n" .
" - (optional) a list of comma-separated service IDs to be injected into the plugin.\n" .
"Use the 'plugins:' marker to separate this from prior commands. Example: 'mymodule hook_a hook_b plugins: block alpha : block beta current_user'.",
'routes:' => "Menu paths, separated by spaces. " . "\n" .
"Use the 'routes:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b routes: module/path module/otherpath'.",
'perms:' => "Sets of properties for one or more permissions. " .
"Each set is separated with a ':', and contains: \n" .
" - the permission machine name\n" .
" - (optional) the permission description\n" .
"Use the 'perms:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b perms: 'administer my module' 'access my module-Access my Module description'. Quote strings that contain spaces.",
'theme:' => "Theme hooks, without the initial 'theme_', separated by spaces. " . "\n" .
"Use the 'theme:' marker to separate this from prior commands, eg 'mymodule hook_a hook_b perms: my_themable'.",
'services:' => "IDs of services, separated by spaces. " . "\n",
'settings_form!' => "Add this argument to add a settings form to the module.",
'api!' => "Add this argument to add an api.php file to the module.",
'readme!' => "Add this argument to add a README file to the module.",
'tests!' => "Add this argument to add a Simpletest test case file to the module.",
),
// Commented out, as only the first argument is required.
// TODO: figure out how to specify this!
//'required-arguments' => TRUE,
'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 module components to generate:
- 'info' makes the info file.
- 'readme' makes README file.
- 'tests' makes the tests folder and test case file.
- 'module', 'install' make the foo.module or foo.install file respectively.
- 'FILE': If custom modules define other files to output, you can request those too, omitting the module root name part and any .inc extension, eg 'views' for 'foo.views.inc.
- 'code' generates code files as needed: the module and install files, and any files requested by hooks.
- 'all' generates everything, including any code files needed by the requested hooks.
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.",
'skip' => "Developer option: specifies a comma-separate list of property names to skip prompting for in interactive mode.",
),
'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-component'] = array(
'callback' => 'drush_module_builder_callback_build_component',
'aliases' => array('mbc'),
'description' => "Generate a Drupal component, such as a module or profile.",
'arguments' => array(
'component type' => "The type of component, e.g., 'module'.",
),
'options' => array(
'write' => 'Write the component to the current site codebase. Will prompt to overwrite existing files; use yes to force. Use quiet to suppress output to the terminal.',
),
);
$items['mb-download'] = array(
'callback' => 'drush_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' => 'drush_module_builder_callback_data_list',
'description' => "List the hooks module_builder knows about.",
'arguments' => array(
'modules' => '(optional) Names of modules, separated by spaces.',
),
'options' => array(
'raw' => "Outputs the raw debug hook data.",
),
);
return $items;
}
/**
* Implementation of hook_drush_help().
*/
function module_builder_drush_help($section) {
switch ($section) {
case 'drush:mb-build':
return dt("Generates and optionally writes module code with the specified components.\n" .
"This can run in one of two modes:\n" .
" - Interactive mode: You are presented with a prompt for each value. " .
"This is the default mode.\n" .
" - Direct mode: Enter all values as a single command. Enable this with the --noi option.\n" .
"Interactive mode will accept any direct mode-style parameters passed in on the initial command.");
}
}
/**
* Handle a sanity exception from the library and output a message.
*
* @param ModuleBuilder\Exception\SanityException $e
* A sanity exception object.
*/
function module_builder_handle_sanity_exception($e) {
$failed_sanity_level = $e->getFailedSanityLevel();
switch ($failed_sanity_level) {
case 'data_directory_exists':
$message = "The component data directory could not be created or is not writable.";
break;
case 'component_data_processed':
$message = "No component data was found. Run 'drush mb-download' to process component data from documentation files.";
break;
}
drush_set_error(DRUSH_APPLICATION_ERROR, $message);
}
/**
* Determines which mode we are in: interactive or direct.
*
* @return
* TRUE if we are in interactive mode, FALSE for direct.
*/
function module_builder_determine_interactive_mode() {
// Interactive mode is the default, overridden by the non-interactive option.
$interactive = !drush_get_option(array('non-interactive', 'noi'));
// This is a shortcut for developing, if you have --noi forced in your drush
// config and need to switch back to interactive for testing.
if (drush_get_option(array('interactive'))) {
$interactive = TRUE;
}
return $interactive;
}
/**
* Module builder drush command callback.
*
* Form:
* $drush mb machine_name hookA hookB hookC
* perms: permission_name
* routes: path/to/thing another/path
* theme: my_themeable another_themeable
* readme! settings-form! tests!
* Hook names may be short or long, e.g. both 'help' and 'hook_help' are
* allowed. Parameters ending in a ':' are markers, which introduce a new type
* of parameter, such as routes and plugins. Parameters ending in '!' are
* booleans which add a single component such as a README file.
*/
function drush_module_builder_callback_build_module() {
$commands = func_get_args();
// Check settings before we start. This sort of wastes the potential of using
// exceptions, but it's polite to warn the user of problems before they've
// spent ages typing in all the hook names in interactive mode.
// Get our task handler. This performs a sanity check on the environment which
// throws an exception.
try {
$mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module');
}
catch (\DrupalCodeBuilder\Exception\SanityException $e) {
// If the problem is that the hooks need downloading, we can recover from this.
if ($e->getFailedSanityLevel() == 'component_data_processed') {
if (drush_confirm(dt('No hook definitions found. Would you like to download them now?'))) {
// Download the hooks so we can move on.
$success = drush_module_builder_callback_hook_download();
if (!$success) {
drush_set_error(DRUSH_APPLICATION_ERROR, 'Problem downloading hook data.');
return;
}
// Get the task handler that we were trying to get in the first place.
$mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', 'module');
}
}
// Otherwise, fail.
else {
drush_set_error(DRUSH_APPLICATION_ERROR, $e->getMessage());
return;
}
}
// Extra drush-specific component data info for the module component.
// This defines command prefixes and drush options.
// This gets merged with the component data info returned from the Module
// generator.
$component_info_drush_extra = array(
'root_name' => array(
'drush_value_process' => 'module_builder_drush_component_root_name_process',
),
'hooks' => array(
'command_prefix' => 'hooks',
// The list is huge, so bypass showing the options.
'drush_no_option_list' => TRUE,
),
'module_hook_presets' => array(
'command_prefix' => 'presets',
),
'readable_name' => array(
'drush_option' => 'name',
),
'short_description' => array(
'drush_option' => 'desc',
),
'module_help_text' => array(
'drush_option' => 'helptext',
),
'module_dependencies' => array(
'drush_option' => 'dep',
),
'module_package' => array(
'drush_option' => 'package',
),
'permissions' => array(
'command_prefix' => 'perms',
),
'services' => array(
'command_prefix' => 'services',
),
'plugins' => array(
'command_prefix' => 'plugins',
),
'forms' => array(
'command_prefix' => 'forms',
),
'theme_hooks' => array(
'command_prefix' => 'theme',
),
'router_items' => array(
'command_prefix' => 'routes',
),
);
$component_data_extra = array();
// Extra component data for the build list and the bare code options.
// 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';
}
// If we are writing or going, all.
elseif (drush_get_option(array('write', 'go'))) {
$build = 'all';
}
// Otherwise, outputting to terminal: only module
else {
$build = 'code';
}
}
// Make a list out of the build option string. This may of course have only
// one item in it.
$build_list = explode(' ', $build);
// Multi build: set a single string to switch on below.
if (count($build_list) > 1) {
$build = 'code';
}
// Set the build list in the module data.
// TODO: move all the above to a helper function!
$component_data_extra['requested_build'] = array_fill_keys($build_list, TRUE);
// The 'bare code' option. This doesn't fully work yet, as 'add' doesn't
// fully work yet! TODO!
$bare_code = drush_get_option('add');
$component_data_extra['bare_code'] = $bare_code;
// Insert the 'hooks' prefix into the commands array, after the module name.
// This privileges the hooks: they don't need a prefix and all commands up to
// another prefix are taken to be hooks.
array_splice($commands, 1, 0, 'hooks:');
// Call the main function to do the work of gathering data from the user and
// building the files and optionally writing them.
drush_module_builder_build_component($commands, 'module', $component_info_drush_extra, $component_data_extra);
// Enable the module if requested.
if (drush_get_option('go')) {
pm_module_manage(array(array_shift($commands)), TRUE);
}
}
/**
* Command argument complete callback: mb-build.
*
* Debug this with, for example:
* $ drush --early=includes/complete.inc --complete-debug drush mb foo ini
* and comment out cache in drush_complete_get().
*/
function module_builder_module_builder_callback_build_complete() {
// We're too early in the drush bootstrap for this to be called, apparently.
// So do it ourselves.
module_builder_drush_init_helper();
// Add our environment handler, and check hook data is ready.
try {
// Drupal bootstrap isn't ready enough to check environment, because the
// file API isn't loaded. This isn't bad, because here it's us, the caller,
// that knows we can't pass the check.
\DrupalCodeBuilder\Factory::getEnvironment()->skipSanityCheck(TRUE);
$mb_task_handler_report = \DrupalCodeBuilder\Factory::getTask('ReportHookData');
}
catch (\DrupalCodeBuilder\Exception\SanityException $e) {
// Just do nothing if we're not ready: the actual command will deal with
// error output to the user.
return;
}
$data = $mb_task_handler_report->getHookDeclarations();
$complete = array();
foreach ($data as $hook_name => $hook_data) {
$complete[] = $hook_name;
// Add the short name too.
$complete[] = substr($hook_name, 5);
// TODO: in anticipation of us doing callbacks too, don't just chop off the
// front!
//if (preg_match('/^hook_/'))
}
return array(
'values' => $complete,
);
}
/**
* Build a generic requested component.
*
* @param $component_type
* The type of the component, e.g. 'module'.
*/
function drush_module_builder_callback_build_component($component_type = NULL) {
$commands = func_get_args();
// Shift off the component name.
array_shift($commands);
// Determine whether we're in interactive mode.
$interactive = !drush_get_option(array('non-interactive', 'noi'));
// This is a shortcut for developing, if you have --noi forced in your drush
// config and need to switch back to interactive for testing.
if (drush_get_option(array('interactive'))) {
$interactive = TRUE;
}
if (empty($component_type)) {
if ($interactive) {
$component_type = drush_prompt("Enter a component name", 'module', TRUE);
}
else {
throw new Exception("No component name given.");
}
}
drush_module_builder_build_component($commands, $component_type);
}
/**
* Builds a Drupal component.
*
* This is a common helper for Drush command callbacks, that each deal with a
* single type of component, e.g. modules or themes.
*
* This hands over to module_builder_drush_output_code() to output the code.
*
* @param $commands
* The commands array; the original parameters from the Drush command callback.
* @param $component_type
* The component type, e.g., 'module'.
* @param $component_info_drush_extra = array()
* An array to merge into the component data info array. This may contain
* additional information specific to drush. Possible keys for each property
* info array are:
* - 'command_prefix': A string that when suffixed with ':' acts as a marker
* for this property in the commands array. All commands after this marker
* taken as data for this property, until another marker is found or until
* the end of the commands array. For example, the following list of
* commands contains hooks and hook presets:
* 'hooks: menu init presets: node field-widget'
* - 'drush_option': A string giving the drush option to take this property's
* value from.
* - 'drush_no_option_list': Suppress the output of an option list for this
* property. Useful for a property whose list of options is very large.
* - 'drush_value_process': A callable to apply processing to the property's
* value before passing it to the generator. This receives the value as its
* parameter.
* @param $component_data = array()
* Initial component data, into which data obtained from the user will be
* added. Command callbacks may wish to gather extra data, or add defaults.
*/
function drush_module_builder_build_component($commands, $component_type, $component_info_drush_extra = array(), $component_data = array()) {
// Get the Generator task, specifying the component we want so we get a
// sanity check based on that and our environment.
try {
$mb_task_handler_generate = \DrupalCodeBuilder\Factory::getTask('Generate', $component_type);
}
catch (\DrupalCodeBuilder\Exception\SanityException $e) {
module_builder_handle_sanity_exception($e);
return;
}
// Set the base type.
$component_data['base'] = $component_type;
// Get the component data info, and add in extra info specific to Drush.
$component_data_info = $mb_task_handler_generate->getRootComponentDataInfo();
// Remove extra property info that's for properties we don't know about (such
// as ones that only apply to certain versions of Drupal).
foreach ($component_info_drush_extra as $property_name => $property_extra_info) {
if (!isset($component_data_info[$property_name])) {
unset($component_info_drush_extra[$property_name]);
}
}
$component_data_info = array_merge_recursive($component_data_info, $component_info_drush_extra);
// Developer option: skip specified properties. This allows quicker manual
// testing of interactive mode.
$skip = array_fill_keys(explode(',', drush_get_option('skip')), TRUE);
$component_data_info = array_diff_key($component_data_info, $skip);
// Split the commands array into:
// - plain commands
// - a nested array of prefixed commands
// - an array of boolean flags.
// E.g. given 'foo bar hooks: biz presets: bax qux!', we want:
// - $commands as just 'foo bar'
// - hooks as 'biz'
// - presets as 'bax'
// - boolean flags as 'qux'
$plain_commands = array();
$prefixed_commands = array();
$boolean_commands = array();
foreach ($commands as $command) {
if (strlen($command) > 1 && substr($command, -1) == ':') {
// This is a preset marker.
$prefix_marker = substr($command, 0, -1);
// Set the array up and move on.
$prefixed_commands[$prefix_marker] = array();
continue;
}
if (substr($command, -1) == '!') {
// This is a boolean flag.
$boolean_flag = substr($command, 0, -1);
$boolean_commands[$boolean_flag] = TRUE;
continue;
}
if (isset($prefix_marker)) {
// Continue taking commands for this prefix until we find another prefix
// marker or run out of commands.
$prefixed_commands[$prefix_marker][] = $command;
}
else {
$plain_commands[] = $command;
}
}
// Determine whether we're in interactive mode.
$interactive = !drush_get_option(array('non-interactive', 'noi'));
// This is a shortcut for developing, if you have --noi forced in your drush
// config and need to switch back to interactive for testing.
if (drush_get_option(array('interactive'))) {
$interactive = TRUE;
}
// Build the component data array from the given commands.
// Work through the component data info, assembling the component data array
// Each property info needs to be prepared, so iterate by reference.
foreach ($component_data_info as $property_name => &$property_info) {
// Prepare the single property: get options, default value, etc.
$mb_task_handler_generate->prepareComponentDataProperty($property_name, $property_info, $component_data);
// Initialize our value from the default that's been set by
// prepareComponentDataProperty(). We try various things to set it.
$value = $component_data[$property_name];
// Keep track of whether the value has come from the user or is still the
// default.
$user_specified = FALSE;
// If the property is required, and there are plain command parameters
// remaining, take one of those.
if (!$user_specified && $property_info['required'] && $plain_commands) {
$value = array_shift($plain_commands);
$user_specified = TRUE;
}
// If the property has a prefix, and the commands included that prefix,
// then take the commands for that prefix.
if (!$user_specified && isset($property_info['command_prefix']) && !empty($prefixed_commands[$property_info['command_prefix']])) {
$value = $prefixed_commands[$property_info['command_prefix']];
$user_specified = TRUE;
}
// If the property can be set with a command-line option, check that.
if (!$user_specified && isset($property_info['drush_option'])) {
$drush_option_value = drush_get_option($property_info['drush_option']);
if (!empty($drush_option_value)) {
$value = $drush_option_value;
$user_specified = TRUE;
}
}
// Boolean commands.
if ($property_info['format'] == 'boolean') {
if (isset($boolean_commands[$property_name])) {
$value = TRUE;
$user_specified = TRUE;
}
}
// Process direct mode values for compound properties into the expected
// format.
if ($user_specified && ($property_info['format'] == 'compound')) {
// A compound property in direct input mode uses a ':' to separate the
// different child items. So for example:
// plugins: block alpha : block beta
$child_property_names = array_keys($property_info['properties']);
$child_items = [];
$delta = 0;
foreach ($value as $child_value) {
if ($child_value == ':') {
// This starts a new delta.
$delta++;
// Restore the list of child property names.
$child_property_names = array_keys($property_info['properties']);
// Move on to the next single value.
continue;
}
// Still here: take the value.
$child_property_name = array_shift($child_property_names);
// Split array properties on a comma.
// TODO: either document this or rethink it.
if ($property_info['properties'][$child_property_name]['format'] == 'array') {
$child_value = explode(',', $child_value);
}
$child_items[$delta][$child_property_name] = $child_value;
// Defaults for other child properties will be filled in by the
// process stage.
}
$value = $child_items;
}
// If we're not in interactive mode, there's nothing more to do for this
// command other than use the default value. That's already set in the
// component data.
if (!$user_specified && $interactive) {
// Prompt the user for a property we've not already got user input for.
// Turn empty defaults into a value that Drush won't output.
$default = empty($component_data[$property_name]) ? NULL : $component_data[$property_name];
if ($property_info['format'] == 'compound') {
// For compound properties, allow the user to enter as many items as
// they like, prompting for all the child properties for each item.
// Entering an empty value for the first child property ends the
// handling of the compound property.
$delta = 0;
$child_property_names = array_keys($property_info['properties']);
while (TRUE) {
// Initialize a new child item so a default value can be placed
// into it.
$value[$delta] = [];
foreach ($property_info['properties'] as $child_property_name => &$child_property_info) {
// Prepare the child property so we get defaults.
// (The call to prepare the compound property will have already
// filled in defaults, but it's safe to call this again.)
$mb_task_handler_generate->prepareComponentDataProperty($child_property_name, $child_property_info, $value[$delta]);
// Turn empty defaults into a value that Drush won't output.
$default = empty($value[$delta][$child_property_name]) ? NULL : $value[$delta][$child_property_name];
// The first child property gets special treatment: don't propose a
// default for it, and force it to be non-required. This is because
// otherwise it would be impossible to leave the loop of collecting
// child items, as leaving this property empty is the way for the
// user to cause that.
$first_child = ($child_property_name == $child_property_names[0]);
if ($first_child) {
$default = '';
}
// Add the parent label so we can use it in prompts.
$child_property_info['parent_label'] = $property_info['label'];
$child_value = module_builder_drush_interactive_prompt($child_property_name, $child_property_info, $component_data[$child_property_name], $default, $delta, $first_child);
// If the first property is empty, stop collecting child items.
if ($first_child && empty($child_value)) {
// Bail on both the while and the foreach loops: XKCD dinosaur!
// Remove the child item we created for this delta, as nothing's
// been put in it.
unset($value[$delta]);
goto endcompound;
}
$value[$delta][$child_property_name] = $child_value;
}
$delta++;
}
endcompound:
}
else {
$value = module_builder_drush_interactive_prompt($property_name, $property_info, $component_data, $default);
}
} // End interactive.
// Split up the value if it should be an array.
if ($property_info['format'] == 'array' && !is_array($value)) {
$value = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
}
// Process compound properties into the expected format.
if ($property_info['format'] == 'compound' && !is_array($value)) {
$value = preg_split('/\s+/', $value, -1, PREG_SPLIT_NO_EMPTY);
// For now, a compound property is input in direct mode as a flat array,
// where we take each array element to be the first child property.
$child_properties = $property_info['properties'];
$first_child_property = array_shift(array_keys($child_properties));
$items = [];
foreach ($value as $single_value) {
$items[][$first_child_property] = $single_value;
// Defaults for other child properties will be filled in by the
// process stage.
}
$value = $items;
}
// Perform any processing specific to drush.
if (isset($property_info['drush_value_process'])) {
$callback = $property_info['drush_value_process'];
$value = $callback($value);
}
// Set the value in the component data array.
$component_data[$property_name] = $value;
}
// Generate the component.
$files = $mb_task_handler_generate->generateComponent($component_data);
$component_dir = module_builder_get_component_folder($component_type, $component_data['root_name']);
// Finally, output the files!
module_builder_drush_output_code($component_dir, $files);
}
/**
* Prompt the user for a single value.
*
* This can be used recursively for child properties.
*
* @param $property_name
* The name of the property to prompt for.
* @param $property_info
* The info array for a single component property.
* @param $component_data
* The component data array, or a sub-array of it, such that the property's
* value will be placed in the top level of this array.
* @param $default
* The default value for the property.
* @param $delta
* (optional) If the property is a child property, the delta of the current
* component.
* @param $first_child
* (optional) If the property is a child property, TRUE to indicate it's the
* first of the children. Defaults to FALSE.
*
* @return
* The value obtained from the user.
*/
function module_builder_drush_interactive_prompt($property_name, $property_info, $component_data, $default, $delta = NULL, $first_child = FALSE) {
// Show the user the available options, unless told not to.
$numeric_options = FALSE;
if (isset($property_info['options']) && empty($property_info['drush_no_option_list'])) {
$numeric_options = TRUE;
drush_print("Select from the following options. Enter either option index number or their value.");
$option_by_index = array();
$i = 1;
foreach ($property_info['options'] as $option => $label) {
$option_by_index[$i] = $option;
drush_print("[$i] $option: $label", 2);
$i++;
}
if (!empty($property_info['options_allow_other'])) {
drush_print("[...] Values not on this list may also be entered.", 2);
}
}
// If this is the first child of a compound component, introduce the current
// delta.
if ($first_child) {
$human_delta = $delta + 1;
drush_print("{$property_info['parent_label']}: $human_delta");
}
// Make a prompt string, letting the user know if the input is optional
// so they don't waste time typing.
switch ($property_info['format']) {
case 'array':
$prompt = "Enter the {$property_info['label']}, as a space separated list";
break;
case 'boolean':
$prompt = "Do you want a {$property_info['label']}";
$prompt .= ' ' . "(y/n)";
// Set the default.
$default = empty($default) ? 'n' : 'y';
break;
case 'string':
$prompt = "Enter the {$property_info['label']}";
}
if (empty($property_info['required']) && empty($component_data[$property_name])) {
$prompt .= ' ' . "(optional)";
}
if ($first_child) {
$prompt .= ' ' . "(leave blank to finish entering {$property_info['parent_label']})";
}
// Hack the 'required' property for first child, so the user can enter an
// empty string and exit the child properties loop.
$required = $property_info['required'];
if ($first_child) {
$required = FALSE;
}
// Indent child properties to make the structure clearer.
if (!is_null($delta)) {
$prompt = ' ' . $prompt;
}
$value = drush_prompt($prompt, $default, $required);
// Split a space-separated list into an array now rather than later, so the
// numeric options can be converted.
if ($property_info['format'] == 'array') {
if (empty($value)) {
$value = [];
}
else {
$value = preg_split('@\s+@', $value);
}
}
// Convert boolean property input to an actual boolean.
if ($property_info['format'] == 'boolean') {
if (in_array(strtolower($value), ['no', 'n'])) {
$value = FALSE;
}
}
// Callback to convert a value given as an index in a list of options into
// the actual option value.
$convert_index_to_value_callback = function($item) use ($option_by_index) {
if (is_numeric($item)) {
return $option_by_index[$item];
}
else {
return $item;
}
};
// Convert a numeric option index value into the actual option value.
if ($numeric_options) {
// We have either an array of values or a single one.
if (is_array($value)) {
$value = array_map($convert_index_to_value_callback, $value);
}
else {
$value = $convert_index_to_value_callback($value);
}
}
return $value;
}
/**
* Process the input for component base name for filesystem shorthands.
*
* This allows for the following in the component name:
* - A single '.' specifies the current folder.
* - A trailing slash is ignored, thus allowing autocompletion of folder names.
*
* @param $component_root_name
* The given component root name.
*
* @return
* The processed component root name.
*/
function module_builder_drush_component_root_name_process($component_root_name) {
// Trim a final '/' from the module machine name, to allow use of tab
// autocompletion on the command line.
if (substr($component_root_name, -1) == '/') {
$component_root_name = substr($component_root_name, 0, -1);
}
// An input machine name given as '.' means use the current folder as the
// component name, thus write to the current folder.
if ($component_root_name == '.') {
$component_root_name = basename(drush_get_context('DRUSH_OLDCWD'));
}
return $component_root_name;
}
/**
* Get the folder where a generated component's files should be written to.
*
* @param $component_type
* The type of the component. One of 'module' or 'theme'.
* @param $component_name
* The component name.
*
* @return
* The full system path for the component's folder, without a trailing slash.
*/
function module_builder_get_component_folder($component_type, $component_name) {
$drupal_root = drush_get_context('DRUSH_DRUPAL_ROOT');
// First try: if the component exists, we write there: nice and simple.
// In Drupal 8, drupal_get_filename() triggers an error for a component that
// doesn't exist, so bypass that with a dummy error handler.
set_error_handler(function() {}, E_USER_WARNING);
$component_path = @drupal_get_path($component_type, $component_name);
restore_error_handler();
if (!empty($component_path)) {
return $drupal_root . '/' . $component_path;
}
// Third try: 'parent' option was given.
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 {
// If there's no dot, assume that an existing module is meant.
// (Would anyone enter a complete path for this??? If we do need this,
// then consider recursing into this for the parent path??)
$module_dir .= drupal_get_path($component_type, $parent_dir) . '/';
}
return $module_dir . $component_name;
}
// Fourth and final try: build it based on the module folder structure.
// There is probably a proper way to do this but it's Sunday morning and
// I want this to just work and so brute force appeals.
require_once DRUSH_BASE_PATH . '/commands/pm/download.pm.inc';
$module_dir = _pm_download_destination($component_type);
// Some versions of Drush don't give us the trailing /.
if (substr($module_dir, -1, 1) != '/') {
$module_dir .= '/';
}
if ($component_type == 'module') {
// 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;
}
}
}
// $module_dir should now be a full path to the parent of the destination
// folder, with a trailing slash.
$module_dir .= $component_name;
return $module_dir;
}
/**
* Output generated text, to terminal or to file.
*
* @param $component_dir
* The base folder for the component. May or may not exist.
* @param $filename
* The array of files to write. Keys are filenames relative to the
* $component_dir, values are strings for the file contents.
*/
function module_builder_drush_output_code($component_dir, $files) {