5 * Provides Configuration Management commands.
8 use Drupal\config\StorageReplaceDataWrapper;
9 use Drush\Log\LogLevel;
10 use Drupal\Core\Config\StorageComparer;
11 use Drupal\Core\Config\ConfigImporter;
12 use Drupal\Core\Config\ConfigException;
13 use Drupal\Core\Config\FileStorage;
14 use Drupal\Component\Utility\NestedArray;
15 use Drush\Config\StorageWrapper;
16 use Drush\Config\CoreExtensionFilter;
17 use Symfony\Component\Yaml\Parser;
20 * Implementation of hook_drush_help().
22 function config_drush_help($section) {
24 case 'meta:config:title':
25 return dt('Config commands');
26 case 'meta:config:summary':
27 return dt('Interact with the configuration system.');
32 * Implementation of hook_drush_command().
34 function config_drush_command() {
35 $deps = array('drupal dependencies' => array('config'));
36 $items['config-get'] = array(
37 'description' => 'Display a config value, or a whole configuration object.',
39 'config-name' => 'The config object name, for example "system.site".',
40 'key' => 'The config key, for example "page.front". Optional.',
42 'required-arguments' => 1,
45 'description' => 'The config storage source to read. Additional labels may be defined in settings.php',
46 'example-value' => 'sync',
47 'value' => 'required',
49 'include-overridden' => array(
50 'description' => 'Include overridden values.',
54 'drush config-get system.site' => 'Displays the system.site config.',
55 'drush config-get system.site page.front' => 'gets system.site:page.front value.',
57 'outputformat' => array(
59 'pipe-format' => 'var_export',
61 'aliases' => array('cget'),
62 'core' => array('8+'),
65 $items['config-set'] = array(
66 'description' => 'Set config value directly. Does not perform a config import.',
68 'config-name' => 'The config object name, for example "system.site".',
69 'key' => 'The config key, for example "page.front".',
70 'value' => 'The value to assign to the config key. Use \'-\' to read from STDIN.',
74 'description' => 'Format to parse the object. Use "string" for string (default), and "yaml" for YAML.',
75 'example-value' => 'yaml',
76 'value' => 'required',
78 // A convenient way to pass a multiline value within a backend request.
80 'description' => 'The value to assign to the config key (if any).',
85 'drush config-set system.site page.front node' => 'Sets system.site:page.front to "node".',
87 'aliases' => array('cset'),
88 'core' => array('8+'),
91 $items['config-export'] = array(
92 'description' => 'Export configuration to a directory.',
93 'core' => array('8+'),
94 'aliases' => array('cex'),
96 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'",
99 'add' => 'Run `git add -p` after exporting. This lets you choose which config changes to sync for commit.',
100 'commit' => 'Run `git add -A` and `git commit` after exporting. This commits everything that was exported without prompting.',
101 'message' => 'Commit comment for the exported configuration. Optional; may only be used with --commit or --push.',
102 'push' => 'Run `git push` after committing. Implies --commit.',
104 'description' => 'The remote git branch to use to push changes. Defaults to "origin".',
105 'example-value' => 'origin',
108 'description' => 'Make commit on provided working branch. Ignored if used without --commit or --push.',
109 'example-value' => 'branchname',
111 'destination' => 'An arbitrary directory that should receive the exported files. An alternative to label argument.',
112 'skip-modules' => 'A list of modules to ignore during export (e.g. to avoid listing dev-only modules in exported configuration).',
115 'drush config-export --skip-modules=devel' => 'Export configuration; do not include the devel module in the exported configuration, regardless of whether or not it is enabled in the site.',
116 'drush config-export --destination' => 'Export configuration; Save files in a backup directory named config-export.',
120 $items['config-import'] = array(
121 'description' => 'Import config from a config directory.',
122 'arguments' => array(
123 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'",
127 'description' => 'Format for displaying proposed changes. Recognized values: list, diff. Defaults to list.',
128 'example-value' => 'list',
131 'description' => 'An arbitrary directory that holds the configuration files. An alternative to label argument',
134 'description' => 'Allows for partial config imports from the source directory. Only updates and new configs will be processed with this flag (missing configs will not be deleted).',
136 'skip-modules' => 'A list of modules to ignore during import (e.g. to avoid disabling dev-only modules that are not enabled in the imported configuration).',
138 'core' => array('8+'),
140 'drush config-import --skip-modules=devel' => 'Import configuration; do not enable or disable the devel module, regardless of whether or not it appears in the imported list of enabled modules.',
142 'aliases' => array('cim'),
145 $items['config-list'] = array(
146 'description' => 'List config names by prefix.',
147 'core' => array('8+'),
148 'aliases' => array('cli'),
149 'arguments' => array(
150 'prefix' => 'The config prefix. For example, "system". No prefix will return all names in the system.',
153 'drush config-list system' => 'Return a list of all system config names.',
154 'drush config-list "image.style"' => 'Return a list of all image styles.',
155 'drush config-list --format="json"' => 'Return all config names as json.',
157 'outputformat' => array(
159 'pipe-format' => 'var_export',
160 'output-data-type' => 'format-list',
164 $items['config-edit'] = $deps + array(
165 'description' => 'Open a config file in a text editor. Edits are imported into active configuration after closing editor.',
166 'core' => array('8+'),
167 'aliases' => array('cedit'),
168 'arguments' => array(
169 'config-name' => 'The config object name, for example "system.site".',
171 'global-options' => array('editor', 'bg'),
172 'allow-additional-options' => array('config-import'),
174 'drush config-edit image.style.large' => 'Edit the image style configurations.',
175 'drush config-edit' => 'Choose a config file to edit.',
176 'drush config-edit --choice=2' => 'Edit the second file in the choice list.',
177 'drush --bg config-edit image.style.large' => 'Return to shell prompt as soon as the editor window opens.',
181 $items['config-delete'] = array(
182 'description' => 'Delete a configuration object.',
183 'core' => array('8+'),
184 'aliases' => array('cdel'),
185 'arguments' => array(
186 'config-name' => 'The config object name, for example "system.site".',
191 $items['config-pull'] = array(
192 'description' => 'Export and transfer config from one environment to another.',
193 // 'core' => array('8+'), Operates on remote sites so not possible to declare this locally.
194 'drush dependencies' => array('config', 'core'), // core-rsync, core-execute.
195 'bootstrap' => DRUSH_BOOTSTRAP_NONE,
196 'aliases' => array('cpull'),
197 'arguments' => array(
198 'source' => 'A site-alias or the name of a subdirectory within /sites whose config you want to copy from.',
199 'target' => 'A site-alias or the name of a subdirectory within /sites whose config you want to replace.',
201 'required-arguments' => TRUE,
202 'allow-additional-options' => array(), // Most options from config-export and core-rsync unusable.
204 'drush config-pull @prod @stage' => "Export config from @prod and transfer to @stage.",
205 'drush config-pull @prod @self --label=vcs' => "Export config from @prod and transfer to the 'vcs' config directory of current site.",
208 'safe' => 'Validate that there are no git uncommitted changes before proceeding',
209 'label' => "A config directory label (i.e. a key in \$config_directories array in settings.php). Defaults to 'sync'",
210 'runner' => 'Where to run the rsync command; defaults to the local site. Can also be "source" or "destination".',
212 'topics' => array('docs-aliases', 'docs-config-exporting'),
219 * Implements hook_drush_help_alter().
221 function config_drush_help_alter(&$command) {
222 // Hide additional-options which are for internal use only.
223 if ($command['command'] == 'config-edit') {
224 $command['options']['source']['hidden'] = TRUE;
225 $command['options']['partial']['hidden'] = TRUE;
230 * Config list command callback
232 * @param string $prefix
233 * The config prefix to retrieve, or empty to return all.
235 function drush_config_list($prefix = '') {
236 $names = \Drupal::configFactory()->listAll($prefix);
239 // Just in case there is no config.
241 return drush_set_error(dt('No config storage names found.'));
244 return drush_set_error(dt('No config storage names found matching @prefix', array('@prefix' => $prefix)));
252 * Config get command callback.
254 * @param $config_name
259 function drush_config_get($config_name, $key = NULL) {
261 return drush_config_get_object($config_name);
264 return drush_config_get_value($config_name, $key);
269 * Config delete command callback.
271 * @param $config_name
274 function drush_config_delete($config_name) {
275 $config =\Drupal::service('config.factory')->getEditable($config_name);
276 if ($config->isNew()) {
277 return drush_set_error('DRUSH_CONFIG_ERROR', 'Configuration name not recognized. Use config-list to see all names.');
285 * Config set command callback.
287 * @param $config_name
292 * The data to save to config.
294 function drush_config_set($config_name, $key = NULL, $data = NULL) {
295 // This hidden option is a convenient way to pass a value without passing a key.
296 $data = drush_get_option('value', $data);
299 return drush_set_error('DRUSH_CONFIG_ERROR', dt('No config value specified.'));
302 $config = \Drupal::configFactory()->getEditable($config_name);
303 // Check to see if config key already exists.
304 if ($config->get($key) === NULL) {
311 // Special flag indicating that the value has been passed via STDIN.
313 $data = stream_get_contents(STDIN);
316 // Now, we parse the value.
317 switch (drush_get_option('format', 'string')) {
319 $parser = new Parser();
320 $data = $parser->parse($data, TRUE);
323 if (is_array($data) && drush_confirm(dt('Do you want to update or set multiple keys on !name config.', array('!name' => $config_name)))) {
324 foreach ($data as $key => $value) {
325 $config->set($key, $value);
327 return $config->save();
331 if ($config->isNew() && drush_confirm(dt('!name config does not exist. Do you want to create a new config object?', array('!name' => $config_name)))) {
334 elseif ($new_key && drush_confirm(dt('!key key does not exist in !name config. Do you want to create a new config key?', array('!key' => $key, '!name' => $config_name)))) {
337 elseif (drush_confirm(dt('Do you want to update !key key in !name config?', array('!key' => $key, '!name' => $config_name)))) {
340 if ($confirmed && !drush_get_context('DRUSH_SIMULATE')) {
341 return $config->set($key, $data)->save();
347 * If provided $destination is not TRUE and not empty, make sure it is writable.
349 function drush_config_export_validate() {
350 $destination = drush_get_option('destination');
351 if ($destination === TRUE) {
352 // We create a dir in command callback. No need to validate.
356 if (!empty($destination)) {
357 $additional = array();
358 $values = drush_sitealias_evaluate_path($destination, $additional, TRUE);
359 if (!isset($values['path'])) {
360 return drush_set_error('config_export_target', 'The destination directory could not be evaluated.');
362 $destination = $values['path'];
363 drush_set_option('destination', $destination);
364 if (!file_exists($destination)) {
365 $parent = dirname($destination);
366 if (!is_dir($parent)) {
367 return drush_set_error('config_export_target', 'The destination parent directory does not exist.');
369 if (!is_writable($parent)) {
370 return drush_set_error('config_export_target', 'The destination parent directory is not writable.');
374 if (!is_dir($destination)) {
375 return drush_set_error('config_export_target', 'The destination is not a directory.');
377 if (!is_writable($destination)) {
378 return drush_set_error('config_export_target', 'The destination directory is not writable.');
385 * Command callback: Export config to specified directory (usually sync).
387 function drush_config_export($destination = NULL) {
388 global $config_directories;
390 // Determine which target directory to use.
391 if ($target = drush_get_option('destination')) {
392 if ($target === TRUE) {
393 // User did not pass a specific value for --destination. Make one.
394 /** @var drush_version_control_backup $backup */
395 $backup = drush_include_engine('version_control', 'backup');
396 $destination_dir = $backup->prepare_backup_dir('config-export');
399 $destination_dir = $target;
400 // It is important to be able to specify a destination directory that
401 // does not exist yet, for exporting on remote systems
402 drush_mkdir($destination_dir);
406 $choices = drush_map_assoc(array_keys($config_directories));
407 unset($choices[CONFIG_ACTIVE_DIRECTORY]);
408 if (!isset($destination) && count($choices) >= 2) {
409 $destination = drush_choice($choices, 'Choose a destination.');
410 if (empty($destination)) {
411 return drush_user_abort();
414 elseif (!isset($destination)) {
415 $destination = CONFIG_SYNC_DIRECTORY;
417 $destination_dir = config_get_config_directory($destination);
420 // Prepare a new branch, if applicable
421 $remote = drush_get_option('push', FALSE);
422 $original_branch = FALSE;
425 // Get the branch that we're on at the moment
426 $result = drush_shell_cd_and_exec($destination_dir, 'git rev-parse --abbrev-ref HEAD');
428 return drush_set_error('DRUSH_CONFIG_EXPORT_NO_GIT', dt("The drush config-export command requires that the selected configuration directory !dir be under git revision control when using --commit or --push options.", array('!dir' => $destination_dir)));
430 $output = drush_shell_exec_output();
431 $original_branch = $output[0];
432 $branch = drush_get_option('branch', FALSE);
434 $branch = $original_branch;
436 if ($branch != $original_branch) {
437 // Switch to the working branch; create it if it does not exist.
438 // We do NOT want to use -B here, as we do NOT want to reset the
439 // branch if it already exists.
440 $result = drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $branch);
442 $result = drush_shell_cd_and_exec($destination_dir, 'git checkout -b %s', $branch);
447 // Do the actual config export operation
448 $result = _drush_config_export($destination, $destination_dir, $branch);
450 // Regardless of the result of the export, reset to our original branch.
451 if ($branch != $original_branch) {
452 drush_shell_cd_and_exec($destination_dir, 'git checkout %s', $original_branch);
458 function _drush_config_export($destination, $destination_dir, $branch) {
459 $commit = drush_get_option('commit');
460 $comment = drush_get_option('message', 'Exported configuration.');
461 $storage_filters = drush_config_get_storage_filters();
462 if (count(glob($destination_dir . '/*')) > 0) {
463 // Retrieve a list of differences between the active and target configuration (if any).
464 if ($destination == CONFIG_SYNC_DIRECTORY) {
465 $target_storage = \Drupal::service('config.storage.sync');
468 $target_storage = new FileStorage($destination_dir);
470 /** @var \Drupal\Core\Config\StorageInterface $active_storage */
471 $active_storage = \Drupal::service('config.storage');
472 $comparison_source = $active_storage;
474 // If the output is being filtered, then write a temporary copy before doing
476 if (!empty($storage_filters)) {
477 $tmpdir = drush_tempdir();
478 drush_copy_dir($destination_dir, $tmpdir, FILE_EXISTS_OVERWRITE);
479 $comparison_source = new FileStorage($tmpdir);
480 $comparison_source_filtered = new StorageWrapper($comparison_source, $storage_filters);
481 foreach ($active_storage->listAll() as $name) {
482 // Copy active storage to our temporary active store.
483 if ($existing = $active_storage->read($name)) {
484 $comparison_source_filtered->write($name, $existing);
489 $config_comparer = new StorageComparer($comparison_source, $target_storage, \Drupal::service('config.manager'));
490 if (!$config_comparer->createChangelist()->hasChanges()) {
491 return drush_log(dt('The active configuration is identical to the configuration in the export directory (!target).', array('!target' => $destination_dir)), LogLevel::OK);
494 drush_print("Differences of the active config to the export directory:\n");
495 $change_list = array();
496 foreach ($config_comparer->getAllCollectionNames() as $collection) {
497 $change_list[$collection] = $config_comparer->getChangelist(NULL, $collection);
499 // Print a table with changes in color, then re-generate again without
500 // color to place in the commit comment.
501 _drush_print_config_changes_table($change_list);
502 $tbl = _drush_format_config_changes_table($change_list);
503 $output = $tbl->getTable();
504 if (!stristr(PHP_OS, 'WIN')) {
505 $output = str_replace("\r\n", PHP_EOL, $output);
507 $comment .= "\n\n$output";
509 if (!$commit && !drush_confirm(dt('The .yml files in your export directory (!target) will be deleted and replaced with the active config.', array('!target' => $destination_dir)))) {
510 return drush_user_abort();
512 // Only delete .yml files, and not .htaccess or .git.
513 $target_storage->deleteAll();
516 // Write all .yml files.
517 $source_storage = \Drupal::service('config.storage');
518 if ($destination == CONFIG_SYNC_DIRECTORY) {
519 $destination_storage = \Drupal::service('config.storage.sync');
522 $destination_storage = new FileStorage($destination_dir);
524 // If there are any filters, then attach them to the destination storage
525 if (!empty($storage_filters)) {
526 $destination_storage = new StorageWrapper($destination_storage, $storage_filters);
528 foreach ($source_storage->listAll() as $name) {
529 $destination_storage->write($name, $source_storage->read($name));
532 // Export configuration collections.
533 foreach (\Drupal::service('config.storage')->getAllCollectionNames() as $collection) {
534 $source_storage = $source_storage->createCollection($collection);
535 $destination_storage = $destination_storage->createCollection($collection);
536 foreach ($source_storage->listAll() as $name) {
537 $destination_storage->write($name, $source_storage->read($name));
541 drush_log(dt('Configuration successfully exported to !target.', array('!target' => $destination_dir)), LogLevel::SUCCESS);
542 drush_backend_set_result($destination_dir);
544 // Commit and push, or add exported configuration if requested.
545 $remote = drush_get_option('push', FALSE);
546 if ($commit || $remote) {
547 // There must be changed files at the destination dir; if there are not, then
548 // we will skip the commit-and-push step
549 $result = drush_shell_cd_and_exec($destination_dir, 'git status --porcelain .');
551 return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git status` failed."));
553 $uncommitted_changes = drush_shell_exec_output();
554 if (!empty($uncommitted_changes)) {
555 $result = drush_shell_cd_and_exec($destination_dir, 'git add -A .');
557 return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git add -A` failed."));
559 $comment_file = drush_save_data_to_temp_file($comment);
560 $result = drush_shell_cd_and_exec($destination_dir, 'git commit --file=%s', $comment_file);
562 return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git commit` failed. Output:\n\n!output", array('!output' => implode("\n", drush_shell_exec_output()))));
565 // Remote might be FALSE, if --push was not specified, or
566 // it might be TRUE if --push was not given a value.
567 if (!is_string($remote)) {
570 $result = drush_shell_cd_and_exec($destination_dir, 'git push --set-upstream %s %s', $remote, $branch);
572 return drush_set_error('DRUSH_CONFIG_EXPORT_FAILURE', dt("`git push` failed."));
577 elseif (drush_get_option('add')) {
578 drush_shell_exec_interactive('git add -p %s', $destination_dir);
582 'destination' => $destination_dir,
587 function drush_config_import_validate() {
588 drush_include_engine('drupal', 'environment');
589 if (drush_get_option('partial') && !drush_module_exists('config')) {
590 return drush_set_error('config_import_partial', 'Enable the config module in order to use the --partial option.');
592 if ($source = drush_get_option('source')) {
593 if (!file_exists($source)) {
594 return drush_set_error('config_import_target', 'The source directory does not exist.');
596 if (!is_dir($source)) {
597 return drush_set_error('config_import_target', 'The source is not a directory.');
603 * Return storage filters to alter config import and export.
605 function drush_config_get_storage_filters() {
606 return drush_command_invoke_all('drush_storage_filters');
610 * Implements hook_drush_storage_filters().
612 function config_drush_storage_filters() {
614 $module_adjustments = drush_get_option('skip-modules');
615 if (!empty($module_adjustments)) {
616 if (is_string($module_adjustments)) {
617 $module_adjustments = explode(',', $module_adjustments);
619 $result[] = new CoreExtensionFilter($module_adjustments);
625 * Command callback. Import from specified config directory (defaults to sync).
627 function drush_config_import($source = NULL) {
628 global $config_directories;
630 // Determine source directory.
631 if ($target = drush_get_option('source')) {
632 $source_dir = $target;
635 $choices = drush_map_assoc(array_keys($config_directories));
636 unset($choices[CONFIG_ACTIVE_DIRECTORY]);
637 if (!isset($source) && count($choices) >= 2) {
638 $source= drush_choice($choices, 'Choose a source.');
639 if (empty($source)) {
640 return drush_user_abort();
643 elseif (!isset($source)) {
644 $source = CONFIG_SYNC_DIRECTORY;
646 $source_dir = config_get_config_directory($source);
649 // Determine $source_storage in partial and non-partial cases.
650 /** @var \Drupal\Core\Config\StorageInterface $active_storage */
651 $active_storage = \Drupal::service('config.storage');
652 if (drush_get_option('partial')) {
653 $source_storage = new StorageReplaceDataWrapper($active_storage);
654 $file_storage = new FileStorage($source_dir);
655 foreach ($file_storage->listAll() as $name) {
656 $data = $file_storage->read($name);
657 $source_storage->replaceData($name, $data);
661 if ($source == CONFIG_SYNC_DIRECTORY) {
662 $source_storage = \Drupal::service('config.storage.sync');
665 $source_storage = new FileStorage($source_dir);
669 // If our configuration storage is being filtered, then attach all filters
670 // to the source storage object. We will use the filtered values uniformly
671 // for comparison, full imports, and partial imports.
672 $storage_filters = drush_config_get_storage_filters();
673 if (!empty($storage_filters)) {
674 $source_storage = new StorageWrapper($source_storage, $storage_filters);
677 /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
678 $config_manager = \Drupal::service('config.manager');
679 $storage_comparer = new StorageComparer($source_storage, $active_storage, $config_manager);
682 if (!$storage_comparer->createChangelist()->hasChanges()) {
683 return drush_log(dt('There are no changes to import.'), LogLevel::OK);
686 if (drush_get_option('preview', 'list') == 'list') {
687 $change_list = array();
688 foreach ($storage_comparer->getAllCollectionNames() as $collection) {
689 $change_list[$collection] = $storage_comparer->getChangelist(NULL, $collection);
691 _drush_print_config_changes_table($change_list);
694 // Copy active storage to the temporary directory.
695 $temp_dir = drush_tempdir();
696 $temp_storage = new FileStorage($temp_dir);
697 $source_dir_storage = new FileStorage($source_dir);
698 foreach ($source_dir_storage->listAll() as $name) {
699 if ($data = $active_storage->read($name)) {
700 $temp_storage->write($name, $data);
703 drush_shell_exec('diff -x %s -u %s %s', '*.git', $temp_dir, $source_dir);
704 $output = drush_shell_exec_output();
705 drush_print(implode("\n", $output));
708 if (drush_confirm(dt('Import the listed configuration changes?'))) {
709 return drush_op('_drush_config_import', $storage_comparer);
713 // Copied from submitForm() at /core/modules/config/src/Form/ConfigSync.php
714 function _drush_config_import(StorageComparer $storage_comparer) {
715 $config_importer = new ConfigImporter(
717 \Drupal::service('event_dispatcher'),
718 \Drupal::service('config.manager'),
720 \Drupal::service('config.typed'),
721 \Drupal::moduleHandler(),
722 \Drupal::service('module_installer'),
723 \Drupal::service('theme_handler'),
724 \Drupal::service('string_translation')
726 if ($config_importer->alreadyImporting()) {
727 drush_log('Another request may be synchronizing configuration already.', LogLevel::WARNING);
731 // This is the contents of \Drupal\Core\Config\ConfigImporter::import.
732 // Copied here so we can log progress.
733 if ($config_importer->hasUnprocessedConfigurationChanges()) {
734 $sync_steps = $config_importer->initialize();
735 foreach ($sync_steps as $step) {
738 $config_importer->doSyncStep($step, $context);
739 if (isset($context['message'])) {
740 drush_log(str_replace('Synchronizing', 'Synchronized', (string)$context['message']), LogLevel::OK);
742 } while ($context['finished'] < 1);
745 drush_log('The configuration was imported successfully.', LogLevel::SUCCESS);
747 catch (ConfigException $e) {
748 // Return a negative result for UI purposes. We do not differentiate
749 // between an actual synchronization error and a failed lock, because
750 // concurrent synchronizations are an edge-case happening only when
751 // multiple developers or site builders attempt to do it without
753 $message = 'The import failed due for the following reasons:' . "\n";
754 $message .= implode("\n", $config_importer->getErrors());
756 watchdog_exception('config_import', $e);
757 return drush_set_error('config_import_fail', $message);
763 * Edit command callback.
765 function drush_config_edit($config_name = '') {
766 // Identify and validate input.
768 $config = \Drupal::configFactory()->get($config_name);
769 if ($config->isNew()) {
770 return drush_set_error(dt('Config !name does not exist', array('!name' => $config_name)));
774 $config_names = \Drupal::configFactory()->listAll();
775 $choice = drush_choice($config_names, 'Choose a configuration.');
776 if (empty($choice)) {
777 return drush_user_abort();
780 $config_name = $config_names[$choice];
781 $config = \Drupal::configFactory()->get($config_name);
785 $active_storage = $config->getStorage();
786 $contents = $active_storage->read($config_name);
788 // Write tmp YAML file for editing
789 $temp_dir = drush_tempdir();
790 $temp_storage = new FileStorage($temp_dir);
791 $temp_storage->write($config_name, $contents);
793 $exec = drush_get_editor();
794 drush_shell_exec_interactive($exec, $temp_storage->getFilePath($config_name));
796 // Perform import operation if user did not immediately exit editor.
797 if (!drush_get_option('bg', FALSE)) {
798 $options = drush_redispatch_get_options() + array('partial' => TRUE, 'source' => $temp_dir);
799 $backend_options = array('interactive' => TRUE);
800 return (bool) drush_invoke_process('@self', 'config-import', array(), $options, $backend_options);
805 * Config pull validate callback
808 function drush_config_pull_validate($source, $destination) {
809 if (drush_get_option('safe')) {
810 $return = drush_invoke_process($destination, 'core-execute', array('git diff --quiet'), array('escape' => 0));
811 if ($return['error_status']) {
812 return drush_set_error('DRUSH_GIT_DIRTY', 'There are uncommitted changes in your git working copy.');
818 * Config pull command callback
820 * @param string $label
821 * The config label which receives the transferred files.
823 function drush_config_pull($source, $destination) {
824 // @todo drush_redispatch_get_options() assumes you will execute same command. Not good.
825 $global_options = drush_redispatch_get_options() + array(
829 // @todo If either call is made interactive, we don't get an $return['object'] back.
830 $backend_options = array('interactive' => FALSE);
831 if (drush_get_context('DRUSH_SIMULATE')) {
832 $backend_options['backend-simulate'] = TRUE;
835 $export_options = array(
836 // Use the standard backup directory on Destination.
837 'destination' => TRUE,
839 drush_log(dt('Starting to export configuration on Target.'), LogLevel::OK);
840 $return = drush_invoke_process($source, 'config-export', array(), $global_options + $export_options, $backend_options);
841 if ($return['error_status']) {
842 return drush_set_error('DRUSH_CONFIG_PULL_EXPORT_FAILED', dt('Config-export failed.'));
845 // Trailing slash assures that transfer files and not the containing dir.
846 $export_path = $return['object'] . '/';
849 $rsync_options = array(
850 'remove-source-files' => TRUE,
852 'exclude-paths' => '.htaccess',
853 'yes' => TRUE, // No need to prompt as destination is always the target config directory.
855 $label = drush_get_option('label', 'sync');
856 $runner = drush_get_runner($source, $destination, drush_get_option('runner', FALSE));
857 drush_log(dt('Starting to rsync configuration files from !source to !dest.', array('!source' => $source, '!dest' => $destination)), LogLevel::OK);
858 // This comment applies similarly to sql-sync's use of core-rsync.
859 // Since core-rsync is a strict-handling command and drush_invoke_process() puts options at end, we can't send along cli options to rsync.
860 // Alternatively, add options like --ssh-options to a site alias (usually on the machine that initiates the sql-sync).
861 $return = drush_invoke_process($runner, 'core-rsync', array("$source:$export_path", "$destination:%config-$label"), $rsync_options);
862 if ($return['error_status']) {
863 return drush_set_error('DRUSH_CONFIG_PULL_RSYNC_FAILED', dt('Config-pull rsync failed.'));
866 drush_backend_set_result($return['object']);
870 * Show and return a config object
872 * @param $config_name
873 * The config object name.
875 function drush_config_get_object($config_name) {
876 $source = drush_get_option('source', 'active');
877 $include_overridden = drush_get_option('include-overridden', FALSE);
879 if ($include_overridden) {
880 // Displaying overrides only applies to active storage.
881 $config = \Drupal::config($config_name);
882 $data = $config->get();
884 elseif ($source == 'active') {
885 $config = \Drupal::service('config.storage');
886 $data = $config->read($config_name);
888 elseif ($source == 'sync') {
889 $config = \Drupal::service('config.storage.sync');
890 $data = $config->read($config_name);
893 return drush_set_error(dt('Unknown value !value for config source.', array('!value' => $source)));
896 if ($data === FALSE) {
897 return drush_set_error(dt('Config !name does not exist in !source configuration.', array('!name' => $config_name, '!source' => $source)));
900 drush_log(dt('Config !name exists but has no data.', array('!name' => $config_name)), LogLevel::NOTICE);
907 * Show and return a value from config system.
909 * @param $config_name
914 function drush_config_get_value($config_name, $key) {
915 $data = drush_config_get_object($config_name);
916 $parts = explode('.', $key);
917 if (count($parts) == 1) {
918 $value = isset($data[$key]) ? $data[$key] : NULL;
921 $value = NestedArray::getValue($data, $parts, $key_exists);
922 $value = $key_exists ? $value : NULL;
925 $returns[$config_name . ':' . $key] = $value;
927 if ($value === NULL) {
928 return drush_set_error('DRUSH_CONFIG_ERROR', dt('No matching key found in !name config.', array('!name' => $config_name)));
936 * Print a table of config changes.
938 * @param array $config_changes
939 * An array of changes keyed by collection.
941 function _drush_format_config_changes_table(array $config_changes, $use_color = FALSE) {
948 $red = "\033[31;40m\033[1m%s\033[0m";
949 $yellow = "\033[1;33;40m\033[1m%s\033[0m";
950 $green = "\033[1;32;40m\033[1m%s\033[0m";
954 $rows[] = array('Collection', 'Config', 'Operation');
955 foreach ($config_changes as $collection => $changes) {
956 foreach ($changes as $change => $configs) {
971 foreach($configs as $config) {
975 sprintf($colour, $change)
980 $tbl = _drush_format_table($rows);
985 * Print a table of config changes.
987 * @param array $config_changes
988 * An array of changes keyed by collection.
990 function _drush_print_config_changes_table(array $config_changes) {
991 $tbl = _drush_format_config_changes_table($config_changes, !drush_get_context('DRUSH_NOCOLOR'));
993 $output = $tbl->getTable();
994 if (!stristr(PHP_OS, 'WIN')) {
995 $output = str_replace("\r\n", PHP_EOL, $output);
998 drush_print(rtrim($output));
1003 * Command argument complete callback.
1005 function config_config_get_complete() {
1006 return _drush_config_names_complete();
1010 * Command argument complete callback.
1012 function config_config_set_complete() {
1013 return _drush_config_names_complete();
1017 * Command argument complete callback.
1019 function config_config_view_complete() {
1020 return _drush_config_names_complete();
1024 * Command argument complete callback.
1026 function config_config_edit_complete() {
1027 return _drush_config_names_complete();
1031 * Command argument complete callback.
1033 function config_config_import_complete() {
1034 return _drush_config_directories_complete();
1038 * Command argument complete callback.
1040 function config_config_export_complete() {
1041 return _drush_config_directories_complete();
1045 * Command argument complete callback.
1047 function config_config_pull_complete() {
1048 return array('values' => array_keys(_drush_sitealias_all_list()));
1052 * Helper function for command argument complete callback.
1055 * Array of available config directories.
1057 function _drush_config_directories_complete() {
1058 drush_bootstrap_max(DRUSH_BOOTSTRAP_DRUPAL_CONFIGURATION);
1059 global $config_directories;
1060 return array('values' => array_keys($config_directories));
1064 * Helper function for command argument complete callback.
1067 * Array of available config names.
1069 function _drush_config_names_complete() {
1070 drush_bootstrap_max();
1071 return array('values' => $storage = \Drupal::service('config.storage')->listAll());