-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
command.inc
1940 lines (1833 loc) · 76.8 KB
/
command.inc
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
use Drush\Log\LogLevel;
/**
* @defgroup dispatching Command dispatching functions.
* @{
*
* These functions handle command dispatching, and can
* be used to programatically invoke drush commands in
* different ways.
*/
/**
* Invokes a Drush API call, including all hooks.
*
* Executes the specified command with the specified arguments on the currently
* bootstrapped site using the current option contexts. Note that it will not
* bootstrap any further than the current command has already bootstrapped;
* therefore, you should only invoke commands that have the same (or lower)
* bootstrap requirements.
*
* Commands execute with the same options that the user provided on the
* commandline. If you need to invoke another Drush command with options you
* specify, use drush_invoke_process() instead.
*
* @param string $command
* The command to invoke.
* @param array $arguments
* An array of argument to pass into the command.
*
* @return mixed|false
* The return value from drush_dispatch() or FALSE on error.
*
* @see drush_invoke_process()
*/
function drush_invoke($command, $arguments = array()) {
// Convert a standalone argument to a single-element array.
if (!is_array($arguments)) {
$arguments = array($arguments);
}
$commands = drush_get_commands();
if (array_key_exists($command, $commands)) {
$command = $commands[$command];
// Drush overloads the 'arguments' element, which contains the help string
// for the allowed arguments for the command when fetched, and is fixed up
// by _drush_prepare_command() to contain the actual commandline arguments
// during dispatching.
$command['arguments'] = array();
return drush_dispatch($command, $arguments);
}
else {
return drush_set_error('DRUSH_COMMAND_NOT_FOUND', dt("The drush command '!command' could not be found.", array('!command' => $command)));
}
}
/**
* Invoke a command in a new process, targeting the site specified by
* the provided site alias record.
*
* @param array $site_alias_record
* The site record to execute the command on. Use '@self' to run on the current site.
* @param string $command_name
* The command to invoke.
* @param array $commandline_args
* The arguments to pass to the command.
* @param array $commandline_options
* The options (e.g. --select) to provide to the command.
* @param mixed $backend_options
* TRUE - integrate errors
* FALSE - do not integrate errors
* array - @see drush_backend_invoke_concurrent
* There are also several options that _only_ work when set in
* this parameter. They include:
* 'invoke-multiple'
* If $site_alias_record represents a single site, then 'invoke-multiple'
* will cause the _same_ command with the _same_ arguments and options
* to be invoked concurrently (e.g. for running concurrent batch processes).
* 'concurrency'
* Limits the number of concurrent processes that will run at the same time.
* Defaults to '4'.
* 'override-simulated'
* Forces the command to run, even in 'simulated' mode. Useful for
* commands that do not change any state on the machine, e.g. to fetch
* database information for sql-sync via sql-conf.
* 'interactive'
* Overrides the backend invoke process to run commands interactively.
* 'fork'
* Overrides the backend invoke process to run non blocking commands in
* the background. Forks a new process by adding a '&' at the end of the
* command. The calling process does not receive any output from the child
* process. The fork option is used to spawn a process that outlives its
* parent.
*
* @return
* If the command could not be completed successfully, FALSE.
* If the command was completed, this will return an associative
* array containing the results of the API call.
* @see drush_backend_get_result()
*
* Do not change the signature of this function! drush_invoke_process
* is one of the key Drush APIs. See http://drupal.org/node/1152908
*/
function drush_invoke_process($site_alias_record, $command_name, $commandline_args = array(), $commandline_options = array(), $backend_options = TRUE) {
if (is_array($site_alias_record) && array_key_exists('site-list', $site_alias_record)) {
list($site_alias_records, $not_found) = drush_sitealias_resolve_sitespecs($site_alias_record['site-list']);
if (!empty($not_found)) {
drush_log(dt("Not found: @list", array("@list" => implode(', ', $not_found))), LogLevel::WARNING);
return FALSE;
}
$site_alias_records = drush_sitealias_simplify_names($site_alias_records);
foreach ($site_alias_records as $alias_name => $alias_record) {
$invocations[] = array(
'site' => $alias_record,
'command' => $command_name,
'args' => $commandline_args,
);
}
}
else {
$invocations[] = array(
'site' => $site_alias_record,
'command' => $command_name,
'args' => $commandline_args);
$invoke_multiple = drush_get_option_override($backend_options, 'invoke-multiple', 0);
if ($invoke_multiple) {
$invocations = array_fill(0, $invoke_multiple, $invocations[0]);
}
}
return drush_backend_invoke_concurrent($invocations, $commandline_options, $backend_options);
}
/**
* Given a command record, dispatch it as if it were
* the original command. Executes in the currently
* bootstrapped site using the current option contexts.
* Note that drush_dispatch will not bootstrap any further than the
* current command has already bootstrapped; therefore, you should only invoke
* commands that have the same (or lower) bootstrap requirements.
*
* @param command
* A full $command such as returned by drush_get_commands(),
* or a string containing the name of the command record from
* drush_get_commands() to call.
* @param arguments
* An array of argument values.
*
* @see drush_topic_docs_topic().
*/
function drush_dispatch($command, $arguments = array()) {
drush_set_command($command);
$return = FALSE;
if ($command) {
// Add arguments, if this has not already been done.
// (If the command was fetched from drush_parse_command,
// then you cannot provide arguments to drush_dispatch.)
if (empty($command['arguments'])) {
_drush_prepare_command($command, $arguments);
}
// Add command-specific options, if applicable.
drush_command_default_options($command);
// Test to see if any of the options in the 'cli' context
// are not represented in the command structure.
if ((_drush_verify_cli_options($command) === FALSE) || (_drush_verify_cli_arguments($command) === FALSE)) {
return FALSE;
}
// Give command files an opportunity to alter the command record
drush_command_invoke_all_ref('drush_command_alter', $command);
// Include and validate command engines.
if (drush_load_command_engines($command) === FALSE) {
return FALSE;
}
// Do tilde expansion immediately prior to execution,
// so that tildes are passed through unchanged for
// remote commands and other redispatches.
drush_preflight_tilde_expansion($command);
// Call the callback function of the active command.
$return = call_user_func_array($command['callback'], $command['arguments']);
}
// Add a final log entry, just so a timestamp appears.
drush_log(dt('Command dispatch complete'), LogLevel::NOTICE);
return $return;
}
/**
* Entry point for commands into the drush_invoke() API
*
* If a command does not have a callback specified, this function will be called.
*
* This function will trigger $hook_drush_init, then if no errors occur,
* it will call drush_invoke() with the command that was dispatch.
*
* If no errors have occured, it will run $hook_drush_exit.
*/
function drush_command() {
$args = func_get_args();
$command = drush_get_command();
foreach (drush_command_implements("drush_init") as $name) {
$func = $name . '_drush_init';
if (drush_get_option('show-invoke')) {
drush_log(dt("Calling global init hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP);
}
call_user_func_array($func, $args);
_drush_log_drupal_messages();
}
if (!drush_get_error()) {
$result = _drush_invoke_hooks($command, $args);
}
if (!drush_get_error()) {
foreach (drush_command_implements('drush_exit') as $name) {
$func = $name . '_drush_exit';
if (drush_get_option('show-invoke')) {
drush_log(dt("Calling global exit hook: !func", array('!name' => $name, '!func' => $func . '()')), LogLevel::BOOTSTRAP);
}
call_user_func_array($func, $args);
_drush_log_drupal_messages();
}
}
}
/**
* Invoke Drush API calls, including all hooks.
*
* This is an internal function; it is called from drush_dispatch via
* drush_command, but only if the command does not specify a 'callback'
* function. If a callback function is specified, it will be called
* instead of drush_command + _drush_invoke_hooks.
*
* Executes the specified command with the specified arguments on the
* currently bootstrapped site using the current option contexts.
* Note that _drush_invoke_hooks will not bootstrap any further than the
* current command has already bootstrapped; therefore, you should only invoke
* commands that have the same (or lower) bootstrap requirements.
*
* Call the correct hook for all the modules that implement it.
* Additionally, the ability to rollback when an error has been encountered is also provided.
* If at any point during execution, the drush_get_error() function returns anything but 0,
* drush_invoke() will trigger $hook_rollback for each of the hooks that implement it,
* in reverse order from how they were executed. Rollbacks are also triggered any
* time a hook function returns FALSE.
*
* This function will also trigger pre_$hook and post_$hook variants of the hook
* and its rollbacks automatically.
*
* HOW DRUSH HOOK FUNCTIONS ARE NAMED:
*
* The name of the hook is composed from the name of the command and the name of
* the command file that the command definition is declared in. The general
* form for the hook filename is:
*
* drush_COMMANDFILE_COMMANDNAME
*
* In many cases, drush commands that are functionally part of a common collection
* of similar commands will all be declared in the same file, and every command
* defined in that file will start with the same command prefix. For example, the
* command file "pm.drush.inc" defines commands such as "pm-enable" and "pm-disable".
* In the case of "pm-enable", the command file is "pm", and and command name is
* "pm-enable". When the command name starts with the same sequence of characters
* as the command file, then the repeated sequence is dropped; thus, the command
* hook for "pm-enable" is "drush_pm_enable", not "drush_pm_pm_enable".
*
* There is also a special Drupal-version-specific naming convention that may
* be used. To hide a commandfile from all versions of Drupal except for the
* specific one named, add a ".dVERSION" after the command prefix. For example,
* the file "views.d8.drush.inc" defines a "views" commandfile that will only
* load with Drupal 8. This feature is not necessary and should not be used
* in contrib modules (any extension with a ".module" file), since these modules
* are already version-specific.
*
* @param command
* The drush command to execute.
* @param args
* An array of arguments to the command OR a single non-array argument.
* @return
* The return value will be passed along to the caller if --backend option is
* present. A boolean FALSE indicates failure and rollback will be intitated.
*
* This function should not be called directly.
* @see drush_invoke() and @see drush_invoke_process()
*/
function _drush_invoke_hooks($command, $args) {
// If someone passed a standalone arg, convert it to a single-element array
if (!is_array($args)) {
$args = array($args);
}
// Include the external command file used by this command, if there is one.
drush_command_include($command['command-hook']);
// Generate the base name for the hook by converting all
// dashes in the command name to underscores.
$hook = str_replace("-", "_", $command['command-hook']);
// Call the hook init function, if it exists.
// If a command needs to bootstrap, it is advisable
// to do so in _init; otherwise, new commandfiles
// will miss out on participating in any stage that
// has passed or started at the time it was discovered.
$func = 'drush_' . $hook . '_init';
if (function_exists($func)) {
drush_log(dt("Calling drush command init function: !func", array('!func' => $func)), LogLevel::BOOTSTRAP);
call_user_func_array($func, $args);
_drush_log_drupal_messages();
if (drush_get_error()) {
drush_log(dt('The command @command could not be initialized.', array('@command' => $command['command-hook'])), LogLevel::ERROR);
return FALSE;
}
}
$rollback = FALSE;
$completed = array();
$available_rollbacks = array();
$all_available_hooks = array();
// Iterate through the different hook variations
$variations = array($hook . "_pre_validate", $hook . "_validate", "pre_$hook", $hook, "post_$hook");
foreach ($variations as $var_hook) {
// Get the list of command files.
// We re-fetch the list every time through
// the loop in case one of the hook function
// does something that will add additional
// commandfiles to the list (i.e. bootstrapping
// to a higher phase will do this).
$list = drush_commandfile_list();
// Make a list of function callbacks to call. If
// there is a 'primary function' mentioned, make sure
// that it appears first in the list, but only if
// we are running the main hook ("$hook"). After that,
// make sure that any callback associated with this commandfile
// executes before any other hooks defined in any other
// commandfiles.
$callback_list = array();
if (($var_hook == $hook) && ($command['primary function'])) {
$callback_list[$command['primary function']] = $list[$command['commandfile']];
}
else {
$primary_func = ($command['commandfile'] . "_" == substr($var_hook . "_",0,strlen($command['commandfile']) + 1)) ? sprintf("drush_%s", $var_hook) : sprintf("drush_%s_%s", $command['commandfile'], $var_hook);
$callback_list[$primary_func] = $list[$command['commandfile']];
}
// We've got the callback for the primary function in the
// callback list; now add all of the other callback functions.
unset($list[$command['commandfile']]);
foreach ($list as $commandfile => $filename) {
$func = sprintf("drush_%s_%s", $commandfile, $var_hook);
$callback_list[$func] = $filename;
}
// Run all of the functions available for this variation
$accumulated_result = NULL;
foreach ($callback_list as $func => $filename) {
if (function_exists($func)) {
$all_available_hooks[] = $func . ' [* Defined in ' . $filename . ']';
$available_rollbacks[] = $func . '_rollback';
$completed[] = $func;
drush_log(dt("Calling hook !hook", array('!hook' => $func)), LogLevel::DEBUG);
try {
$result = call_user_func_array($func, $args);
drush_log(dt("Returned from hook !hook", array('!hook' => $func)), LogLevel::DEBUG);
}
catch (Exception $e) {
drush_set_error('DRUSH_EXECUTION_EXCEPTION', (string) $e);
}
// If there is an error, break out of the foreach
// $variations and foreach $callback_list
if (drush_get_error() || ($result === FALSE)) {
$rollback = TRUE;
break 2;
}
// If result values are arrays, then combine them all together.
// Later results overwrite earlier results.
if (isset($result) && is_array($accumulated_result) && is_array($result)) {
$accumulated_result = array_merge($accumulated_result, $result);
}
else {
$accumulated_result = $result;
}
_drush_log_drupal_messages();
}
else {
$all_available_hooks[] = $func;
}
}
// Process the result value from the 'main' callback hook only.
if ($var_hook == $hook) {
$return = $accumulated_result;
if (isset($return)) {
drush_handle_command_output($command, $return);
}
}
}
// If no hook functions were found, print a warning.
if (empty($completed)) {
$default_command_hook = sprintf("drush_%s_%s", $command['commandfile'], $hook);
if (($command['commandfile'] . "_" == substr($hook . "_",0,strlen($command['commandfile'])+ 1))) {
$default_command_hook = sprintf("drush_%s", $hook);
}
$dt_args = array(
'!command' => $command['command-hook'],
'!default_func' => $default_command_hook,
);
$message = "No hook functions were found for !command. The primary hook function is !default_func(). Please implement this function. Run with --show-invoke to see all available hooks.";
$return = drush_set_error('DRUSH_FUNCTION_NOT_FOUND', dt($message, $dt_args));
}
if (drush_get_option('show-invoke')) {
// We show all available hooks up to and including the one that failed (or all, if there were no failures)
drush_log(dt("Available drush_invoke() hooks for !command: !available", array('!command' => $command['command-hook'], '!available' => "\n" . implode("\n", $all_available_hooks))), LogLevel::OK);
}
if (drush_get_option('show-invoke') && !empty($available_rollbacks)) {
drush_log(dt("Available rollback hooks for !command: !rollback", array('!command' => $command['command-hook'], '!rollback' => "\n" . implode("\n", $available_rollbacks))), LogLevel::OK);
}
// Something went wrong, we need to undo.
if ($rollback) {
if (drush_get_option('confirm-rollback', FALSE)) {
// Optionally ask for confirmation, --yes and --no are ignored from here on as we are about to finish this process.
drush_set_context('DRUSH_AFFIRMATIVE', FALSE);
drush_set_context('DRUSH_NEGATIVE', FALSE);
$rollback = drush_confirm(dt('Do you want to rollback? (manual cleanup might be required otherwise)'));
}
if ($rollback) {
foreach (array_reverse($completed) as $func) {
$rb_func = $func . '_rollback';
if (function_exists($rb_func)) {
call_user_func_array($rb_func, $args);
_drush_log_drupal_messages();
drush_log(dt("Changes made in !func have been rolled back.", array('!func' => $func)), LogLevel::DEBUG);
}
}
}
$return = FALSE;
}
if (isset($return)) {
return $return;
}
}
/**
* Convert the structured output array provided from the Drush
* command into formatted output. Output is only printed for commands
* that define 'default-format' &/or 'default-pipe-format'; all
* other commands are expected to do their own output.
*/
function drush_handle_command_output($command, $structured_output) {
// If the hook already called drush_backend_set_result,
// then return that value. If it did not, then the return
// value from the hook will be the value returned from
// this routine.
$return = drush_backend_get_result();
if (empty($return)) {
drush_backend_set_result($structured_output);
}
// We skip empty strings and empty arrays, but note that 'empty'
// returns TRUE for the integer value '0', but we do want to print that.
// Only handle output here if the command defined an output format
// engine. If no engine was declared, then we presume that the command
// handled its own output.
if ((!empty($structured_output) || ($structured_output === 0))) {
// If the command specifies a default pipe format and
// returned a result, then output the formatted output when
// in --pipe mode.
$formatter = drush_get_outputformat();
if (!$formatter && is_string($structured_output)) {
$formatter = drush_load_engine('outputformat', 'string');
}
if ($formatter) {
if ($formatter === TRUE) {
return drush_set_error(dt('No outputformat class defined for !format', array('!format' => $format)));
}
if ((!empty($command['engines']['outputformat'])) && (!in_array($formatter->engine, $command['engines']['outputformat']['usable']))) {
return $formatter->format_error(dt("The command '!command' does not produce output in a structure usable by this output format.", array('!command' => $command['command'])));
}
// Add any user-specified options to the metadata passed to the formatter.
$metadata = array();
$metadata['strict'] = drush_get_option('strict', FALSE);
if (isset($formatter->engine_config['options'])) {
$machine_parsable = $formatter->engine_config['engine-info']['machine-parsable'];
if (drush_get_option('full', FALSE)) {
if (isset($formatter->engine_config['fields-full'])) {
$formatter->engine_config['fields-default'] = $formatter->engine_config['fields-full'];
}
else {
$formatter->engine_config['fields-default'] = array_keys($formatter->engine_config['field-labels']);
}
}
elseif ((drush_get_context('DRUSH_PIPE') || $machine_parsable) && isset($formatter->engine_config['fields-pipe'])) {
$formatter->engine_config['fields-default'] = $formatter->engine_config['fields-pipe'];
}
// Determine the --format, and options relevant for that format.
foreach ($formatter->engine_config['options'] as $option => $option_info) {
$default_value = isset($formatter->engine_config[$option . '-default']) ? $formatter->engine_config[$option . '-default'] : FALSE;
if (($default_value === FALSE) && array_key_exists('default', $option_info)) {
$default_value = $option_info['default'];
}
if (isset($option_info['list'])) {
$user_specified_value = drush_get_option_list($option, $default_value);
}
else {
$user_specified_value = drush_get_option($option, $default_value);
}
if ($user_specified_value !== FALSE) {
if (array_key_exists('key', $option_info)) {
$option = $option_info['key'];
}
$metadata[$option] =$user_specified_value;
}
}
}
if (isset($metadata['fields']) && !empty($metadata['fields'])) {
if (isset($formatter->engine_config['field-labels'])) {
$formatter->engine_config['field-labels'] = drush_select_fields($formatter->engine_config['field-labels'], $metadata['fields'], $metadata['strict']);
}
}
$output = $formatter->process($structured_output, $metadata);
if (drush_get_context('DRUSH_PIPE')) {
drush_print_pipe($output);
}
else {
drush_print($output);
}
}
}
}
/**
* Fail with an error if the user specified options on the
* command line that are not documented in the current command
* record. Also verify that required options are present.
*/
function _drush_verify_cli_options($command) {
// Start out with just the options in the current command record.
$options = _drush_get_command_options($command);
// Skip all tests if the command is marked to allow anything.
// Also skip backend commands, which may have options on the commandline
// that were inherited from the calling command.
if (($command['allow-additional-options'] === TRUE)) {
return TRUE;
}
// If 'allow-additional-options' contains a list of command names,
// then union together all of the options from all of the commands.
if (is_array($command['allow-additional-options'])) {
$implemented = drush_get_commands();
foreach ($command['allow-additional-options'] as $subcommand_name) {
if (array_key_exists($subcommand_name, $implemented)) {
$options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name]));
}
}
}
// Also add in global options
$options = array_merge($options, drush_get_global_options());
// Add a placeholder option so that backend requests originating from prior versions of Drush are valid.
$options += array('invoke' => '');
// Now we will figure out which options in the cli context
// are not represented in our options list.
$cli_options = array_keys(drush_get_context('cli'));
$allowed_options = _drush_flatten_options($options);
$allowed_options = drush_append_negation_options($allowed_options);
$disallowed_options = array_diff($cli_options, $allowed_options);
if (!empty($disallowed_options)) {
$unknown = count($disallowed_options) > 1 ? dt('Unknown options') : dt('Unknown option');
if (drush_get_option('strict', TRUE)) {
$msg = dt("@unknown: --@options. See `drush help @command` for available options. To suppress this error, add the option --strict=0.", array('@unknown' => $unknown, '@options' => implode(', --', $disallowed_options), '@command' => $command['command']));
return drush_set_error('DRUSH_UNKNOWN_OPTION', $msg);
}
}
// Next check to see if all required options were specified,
// and if all specified options with required values have values.
$missing_required_options = array();
$options_missing_required_values = array();
foreach ($command['options'] as $option_name => $option) {
if (is_array($option) && !empty($option['required']) && drush_get_option($option_name, NULL) === NULL) {
$missing_required_options[] = $option_name;
}
// Note that drush_get_option() will return TRUE if an option
// was specified without a value (--option), as opposed to
// the string "1" is --option=1 was used.
elseif (is_array($option) && !empty($option['value']) && ($option['value'] == 'required') && drush_get_option($option_name, NULL) === TRUE) {
$options_missing_required_values[] = $option_name;
}
}
if (!empty($missing_required_options) || !empty($options_missing_required_values)) {
$missing_message = '';
if (!empty($missing_required_options)) {
$missing = count($missing_required_options) > 1 ? dt('Missing required options') : dt('Missing required option');
$missing_message = dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $missing_required_options)));
}
if (!empty($options_missing_required_values)) {
if (!empty($missing_message)) {
$missing_message .= " ";
}
$missing = count($options_missing_required_values) > 1 ? dt('Options used without providing required values') : dt('Option used without a value where one was required');
$missing_message .= dt("@missing: --@options.", array('@missing' => $missing, '@options' => implode(', --', $options_missing_required_values)));
}
return drush_set_error(dt("!message See `drush help @command` for information on usage.", array('!message' => $missing_message, '@command' => $command['command'])));
}
return TRUE;
}
function drush_append_negation_options($allowed_options) {
$new_allowed = $allowed_options;
foreach ($allowed_options as $option) {
$new_allowed[] = 'no-' . $option;
}
return $new_allowed;
}
function _drush_verify_cli_arguments($command) {
// Check to see if all of the required arguments
// are specified.
if ($command['required-arguments']) {
$required_arg_count = $command['required-arguments'];
if ($required_arg_count === TRUE) {
$required_arg_count = count($command['argument-description']);
}
if (count($command['arguments']) < $required_arg_count) {
$missing = $required_arg_count > 1 ? dt('Missing required arguments') : dt('Missing required argument');
$required = array_slice(array_keys($command['argument-description']), 0, $required_arg_count);
return drush_set_error(dt("@missing: @required. See `drush help @command` for information on usage.", array(
'@missing' => $missing,
'@required' => implode(", ", $required),
'@command' => $command['command'],
)));
}
}
return TRUE;
}
/**
* Return the list of all of the options for the given
* command record by merging the 'options' and 'sub-options'
* records.
*/
function _drush_get_command_options($command) {
drush_command_invoke_all_ref('drush_help_alter', $command);
$options = $command['options'];
foreach ($command['sub-options'] as $group => $suboptions) {
$options = array_merge($options, $suboptions);
}
return $options;
}
/**
* Return the list of all of the options for the given
* command record including options provided by engines and additional-options.
*/
function drush_get_command_options_extended($command) {
drush_merge_engine_data($command);
// Start out with just the options in the current command record.
$options = _drush_get_command_options($command);
// If 'allow-additional-options' contains a list of command names,
// then union together all of the options from all of the commands.
if (is_array($command['allow-additional-options'])) {
$implemented = drush_get_commands();
foreach ($command['allow-additional-options'] as $subcommand_name) {
if (array_key_exists($subcommand_name, $implemented)) {
$options = array_merge($options, _drush_get_command_options($implemented[$subcommand_name]));
}
}
}
return $options;
}
/**
* Return the array keys of $options, plus any 'short-form'
* representations that may appear in the option's value.
*/
function _drush_flatten_options($options) {
$flattened_options = array();
foreach($options as $key => $value) {
// engine sections start with 'package-handler=git_drupalorg',
// or something similar. Get rid of everything from the = onward.
if (($eq_pos = strpos($key, '=')) !== FALSE) {
$key = substr($key, 0, $eq_pos);
}
$flattened_options[] = $key;
if (is_array($value)) {
if (array_key_exists('short-form', $value)) {
$flattened_options[] = $value['short-form'];
}
}
}
return $flattened_options;
}
/**
* Get the options that were passed to the current command.
*
* This function returns an array that contains all of the options
* that are appropriate for forwarding along to drush_invoke_process.
*
* @return
* An associative array of option key => value pairs.
*/
function drush_redispatch_get_options() {
$options = array();
// Add in command-specific and alias options, but for global options only.
$options_soup = drush_get_context('specific') + drush_get_context('alias');
$global_option_list = drush_get_global_options(FALSE);
foreach ($options_soup as $key => $value) {
if (array_key_exists($key, $global_option_list)) {
$options[$key] = $value;
}
}
// Local php settings should not override sitealias settings.
$cli_context = drush_get_context('cli');
unset($cli_context['php'], $cli_context['php-options']);
// Pass along CLI parameters, as higher priority.
$options = $cli_context + $options;
$options = array_diff_key($options, array_flip(drush_sitealias_site_selection_keys()));
unset($options['command-specific']);
unset($options['path-aliases']);
// If we can parse the current command, then examine all contexts
// in order for any option that is directly related to the current command
$command = drush_parse_command();
if (is_array($command)) {
foreach (drush_get_command_options_extended($command) as $key => $value) {
$value = drush_get_option($key);
if (isset($value)) {
$options[$key] = $value;
}
}
}
// If --bootstrap-to-first-arg is specified, do not
// pass it along to remote commands.
unset($options['bootstrap-to-first-arg']);
return $options;
}
/**
* @} End of "defgroup dispatching".
*/
/**
* @file
* The drush command engine.
*
* Since drush can be invoked independently of a proper Drupal
* installation and commands may operate across sites, a distinct
* command engine is needed.
*
* It mimics the Drupal module engine in order to economize on
* concepts and to make developing commands as familiar as possible
* to traditional Drupal module developers.
*/
/**
* Parse console arguments.
*/
function drush_parse_args() {
$args = drush_get_context('argv');
$command_args = NULL;
$global_options = array();
$target_alias_name = NULL;
// It would be nice if commandfiles could somehow extend this list,
// but it is not possible. We need to parse args before we find commandfiles,
// because the specified options may affect how commandfiles are located.
// Therefore, commandfiles are loaded too late to affect arg parsing.
// There are only a limited number of short options anyway; drush reserves
// all for use by drush core.
static $arg_opts = array('c', 'u', 'r', 'l', 'i');
// Check to see if we were executed via a "#!/usr/bin/env drush" script
drush_adjust_args_if_shebang_script($args);
// Now process the command line arguments. We will divide them
// into options (starting with a '-') and arguments.
$arguments = $options = array();
for ($i = 1; $i < count($args); $i++) {
$opt = $args[$i];
// We set $command_args to NULL until the first argument that is not
// an alias is found (the command); we put everything that follows
// into $command_args.
if (is_array($command_args)) {
$command_args[] = $opt;
}
// Is the arg an option (starting with '-')?
if (!empty($opt) && $opt{0} == "-" && strlen($opt) != 1) {
// Do we have multiple options behind one '-'?
if (strlen($opt) > 2 && $opt{1} != "-") {
// Each char becomes a key of its own.
for ($j = 1; $j < strlen($opt); $j++) {
$options[substr($opt, $j, 1)] = TRUE;
}
}
// Do we have a longopt (starting with '--')?
elseif ($opt{1} == "-") {
if ($pos = strpos($opt, '=')) {
$options[substr($opt, 2, $pos - 2)] = substr($opt, $pos + 1);
}
else {
$options[substr($opt, 2)] = TRUE;
}
}
else {
$opt = substr($opt, 1);
// Check if the current opt is in $arg_opts (= has to be followed by an argument).
if ((in_array($opt, $arg_opts))) {
// Raising errors for missing option values should be handled by the
// bootstrap or specific command, so we no longer do this here.
$options[$opt] = $args[$i + 1];
$i++;
}
else {
$options[$opt] = TRUE;
}
}
}
// If it's not an option, it's a command.
else {
$arguments[] = $opt;
// Once we find the first argument, record the command args and global options
if (!is_array($command_args)) {
// Remember whether we set $target_alias_name on a previous iteration,
// then record the $target_alias_name iff this arguement references a valid site alias.
$already_set_target = is_string($target_alias_name);
if (!$already_set_target && drush_sitealias_valid_alias_format($opt)) {
$target_alias_name = $opt;
}
// If an alias record was set on a previous iteration, then this
// argument must be the command name. If we set the target alias
// record on this iteration, then this is not the command name.
// If we've found the command name, then save $options in $global_options
// (all options that came before the command name), and initialize
// $command_args to an array so that we will begin storing all args
// and options that follow the command name in $command_args.
if ($already_set_target || (!is_string($target_alias_name))) {
$command_args = array();
$global_options = $options;
}
}
}
}
// If no arguments are specified, then the command will
// be either 'help' or 'version' (the latter if --version is specified)
// @todo: it would be handy if one could do `drush @remote st --help` and
// have that show help for st. Today, that shows --help for help command!
if (!count($arguments)) {
if (array_key_exists('version', $options)) {
$arguments = array('version');
}
else {
$arguments = array('help');
}
}
if (is_array($command_args)) {
drush_set_context('DRUSH_COMMAND_ARGS', $command_args);
}
drush_set_context('DRUSH_GLOBAL_CLI_OPTIONS', $global_options);
// Handle the "@shift" alias, if present
drush_process_bootstrap_to_first_arg($arguments);
drush_set_arguments($arguments);
drush_set_config_special_contexts($options);
drush_set_context('cli', $options);
return $arguments;
}
/**
* Pop an argument off of drush's argument list
*/
function drush_shift() {
$arguments = drush_get_arguments();
$result = NULL;
if (!empty($arguments)) {
// The php-script command uses the DRUSH_SHIFT_SKIP
// context to cause drush_shift to skip the 'php-script'
// command and the script path argument when it is
// called from the user script.
$skip_count = drush_get_context('DRUSH_SHIFT_SKIP');
if (is_numeric($skip_count)) {
for ($i = 0; $i < $skip_count; $i++) {
array_shift($arguments);
}
$skip_count = drush_set_context('DRUSH_SHIFT_SKIP', 0);
}
$result = array_shift($arguments);
drush_set_arguments($arguments);
}
return $result;
}
/**
* Special checking for "shebang" script handling.
*
* If there is a file 'script.php' that begins like so:
* #!/path/to/drush
* Then $args will be:
* /path/to/drush /path/to/script userArg1 userArg2 ...
* If it instead starts like this:
* #!/path/to/drush --flag php-script
* Then $args will be:
* /path/to/drush "--flag php-script" /path/to/script userArg1 userArg2 ...
* (Note that execve does not split the parameters from
* the shebang line on whitespace; see http://en.wikipedia.org/wiki/Shebang_%28Unix%29)
* When drush is called via one of the "shebang" lines above,
* the first or second parameter will be the full path
* to the "shebang" script file -- and if the path to the
* script is in the second position, then we will expect that
* the argument in the first position must begin with a
* '@' (alias) or '-' (flag). Under ordinary circumstances,
* we do not expect that the drush command must come before
* any argument that is the full path to a file. We use
* this assumption to detect "shebang" script execution.
*/
function drush_adjust_args_if_shebang_script(&$args) {
if (drush_has_bash()) {
// The drush.launcher script may add --php or --php-options at the
// head of the argument list; skip past those.
$base_arg_number = 1;
while (substr($args[$base_arg_number], 0, 5) == '--php') {
++$base_arg_number;
}
if (_drush_is_drush_shebang_script($args[$base_arg_number])) {
// If $args[1] is a drush "shebang" script, we will insert
// the option "--bootstrap-to-first-arg" and the command
// "php-script" at the beginning of @args, so the command
// line args become:
// /path/to/drush --bootstrap-to-first-arg php-script /path/to/script userArg1 userArg2 ...
drush_set_option('bootstrap-to-first-arg', TRUE);
array_splice($args, $base_arg_number, 0, array('php-script'));
drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE);
}
elseif (((strpos($args[$base_arg_number], ' ') !== FALSE) || (!ctype_alnum($args[$base_arg_number][0]))) && (_drush_is_drush_shebang_script($args[$base_arg_number + 1]))) {
// If $args[2] is a drush "shebang" script, we will insert
// the space-exploded $arg[1] in place of $arg[1], so the
// command line args become:
// /path/to/drush scriptArg1 scriptArg2 ... /path/to/script userArg1 userArg2 ...
// If none of the script arguments look like a drush command,
// then we will insert "php-script" as the default command to
// execute.
$script_args = explode(' ', $args[$base_arg_number]);
$has_command = FALSE;
foreach ($script_args as $script_arg) {
if (preg_match("/^[a-z][a-z0-9-]*$/",$script_arg)) {
$has_command = TRUE;
}
}
if (!$has_command) {
$script_args[] = 'php-script';
}
array_splice($args, 1, $base_arg_number, $script_args);
drush_set_context('DRUSH_SHEBANG_SCRIPT', TRUE);
}
}
}
/**
* Process the --bootstrap-to-first-arg option, if it is present.
*
* This option checks to see if the first user-provided argument is an alias
* or site specification; if it is, it will be shifted into the first argument
* position, where it will specify the site to bootstrap. The result of this
* is that if your shebang line looks like this:
*
* #!/path/to/drush --bootstrap-to-first-arg php-script
*
* Then when you run that script, you can optionally provide an alias such
* as @dev as the first argument (e.g. $ ./mydrushscript.php @dev scriptarg1
* scriptarg2). Since this is the behavior that one would usually want,
* it is default behavior for a canonical script. That is, a script
* with a simple shebang line, like so:
*
* #!/path/to/drush
*
* will implicitly have "--bootstrap-to-first-arg" and "php-script" prepended, and will therefore
* behave exactly like the first example. To write a script that does not
* use --bootstrap-to-first-arg, then the drush command or at least one flag must be explicitly
* included, like so:
*
* #!/path/to/drush php-script
*/
function drush_process_bootstrap_to_first_arg(&$arguments) {
if (drush_get_option('bootstrap-to-first-arg', FALSE)) {
$shift_alias_pos = 1 + (drush_get_context('DRUSH_SHEBANG_SCRIPT') === TRUE);
if (count($arguments) >= $shift_alias_pos) {