ConfigCommand.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\Command;
  12. use Symfony\Component\Console\Input\InputInterface;
  13. use Symfony\Component\Console\Input\InputArgument;
  14. use Symfony\Component\Console\Input\InputOption;
  15. use Symfony\Component\Console\Output\OutputInterface;
  16. use Composer\Config;
  17. use Composer\Factory;
  18. use Composer\Json\JsonFile;
  19. use Composer\Json\JsonManipulator;
  20. /**
  21. * @author Joshua Estes <Joshua.Estes@iostudio.com>
  22. * @author Jordi Boggiano <j.boggiano@seld.be>
  23. */
  24. class ConfigCommand extends Command
  25. {
  26. /**
  27. * @var Composer\Json\JsonFile
  28. */
  29. protected $configFile;
  30. /**
  31. * {@inheritDoc}
  32. */
  33. protected function configure()
  34. {
  35. $this
  36. ->setName('config')
  37. ->setDescription('Set config options')
  38. ->setDefinition(array(
  39. new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'),
  40. new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'),
  41. new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'),
  42. new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'),
  43. new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'),
  44. new InputArgument('setting-key', null, 'Setting key'),
  45. new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'),
  46. ))
  47. ->setHelp(<<<EOT
  48. This command allows you to edit some basic composer settings in either the
  49. local composer.json file or the global config.json file.
  50. To edit the global config.json file:
  51. <comment>%command.full_name% --global</comment>
  52. To add a repository:
  53. <comment>%command.full_name% repositories.foo vcs http://bar.com</comment>
  54. You can add a repository to the global config.json file by passing in the
  55. <info>--global</info> option.
  56. To edit the file in an external editor:
  57. <comment>%command.full_name% --edit</comment>
  58. To choose your editor you can set the "EDITOR" env variable.
  59. To get a list of configuration values in the file:
  60. <comment>%command.full_name% --list</comment>
  61. You can always pass more than one option. As an example, if you want to edit the
  62. global config.json file.
  63. <comment>%command.full_name% --edit --global</comment>
  64. EOT
  65. )
  66. ;
  67. }
  68. /**
  69. * {@inheritDoc}
  70. */
  71. protected function initialize(InputInterface $input, OutputInterface $output)
  72. {
  73. if ($input->getOption('global') && 'composer.json' !== $input->getOption('file')) {
  74. throw new \RuntimeException('--file and --global can not be combined');
  75. }
  76. // Get the local composer.json, global config.json, or if the user
  77. // passed in a file to use
  78. $this->configFile = $input->getOption('global')
  79. ? (Factory::createConfig()->get('home') . '/config.json')
  80. : $input->getOption('file');
  81. $this->configFile = new JsonFile($this->configFile);
  82. // initialize the global file if it's not there
  83. if ($input->getOption('global') && !$this->configFile->exists()) {
  84. touch($this->configFile->getPath());
  85. $this->configFile->write(array('config' => new \ArrayObject));
  86. }
  87. if (!$this->configFile->exists()) {
  88. throw new \RuntimeException('No composer.json found in the current directory');
  89. }
  90. }
  91. /**
  92. * {@inheritDoc}
  93. */
  94. protected function execute(InputInterface $input, OutputInterface $output)
  95. {
  96. // Open file in editor
  97. if ($input->getOption('editor')) {
  98. $editor = getenv('EDITOR');
  99. if (!$editor) {
  100. if (defined('PHP_WINDOWS_VERSION_BUILD')) {
  101. $editor = 'notepad';
  102. } else {
  103. foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
  104. if (exec('which '.$candidate)) {
  105. $editor = $candidate;
  106. break;
  107. }
  108. }
  109. }
  110. }
  111. system($editor . ' ' . $this->configFile->getPath() . (defined('PHP_WINDOWS_VERSION_BUILD') ? '': ' > `tty`'));
  112. return 0;
  113. }
  114. // List the configuration of the file settings
  115. if ($input->getOption('list')) {
  116. $this->listConfiguration($this->configFile->read(), $output);
  117. return 0;
  118. }
  119. if (!$input->getArgument('setting-key')) {
  120. return 0;
  121. }
  122. // If the user enters in a config variable, parse it and save to file
  123. if (array() !== $input->getArgument('setting-value') && $input->getOption('unset')) {
  124. throw new \RuntimeException('You can not combine a setting value with --unset');
  125. }
  126. if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) {
  127. throw new \RuntimeException('You must include a setting value or pass --unset to clear the value');
  128. }
  129. $values = $input->getArgument('setting-value'); // what the user is trying to add/change
  130. // handle repositories
  131. if (preg_match('/^repos?(?:itories)?\.(.+)/', $input->getArgument('setting-key'), $matches)) {
  132. if ($input->getOption('unset')) {
  133. return $this->manipulateJson('removeRepository', $matches[1], function (&$config, $repo) {
  134. unset($config['repositories'][$repo]);
  135. });
  136. }
  137. if (2 !== count($values)) {
  138. throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com');
  139. }
  140. return $this->manipulateJson(
  141. 'addRepository',
  142. $matches[1],
  143. array(
  144. 'type' => $values[0],
  145. 'url' => $values[1],
  146. ), function (&$config, $repo, $repoConfig) {
  147. $config['repositories'][$repo] = $repoConfig;
  148. }
  149. );
  150. }
  151. // handle config values
  152. $uniqueConfigValues = array(
  153. 'process-timeout' => array('is_numeric', 'intval'),
  154. 'vendor-dir' => array('is_string', function ($val) { return $val; }),
  155. 'bin-dir' => array('is_string', function ($val) { return $val; }),
  156. 'notify-on-install' => array(
  157. function ($val) { return true; },
  158. function ($val) { return $val !== 'false' && (bool) $val; }
  159. ),
  160. );
  161. $multiConfigValues = array(
  162. 'github-protocols' => array(
  163. function ($vals) {
  164. if (!is_array($vals)) {
  165. return 'array expected';
  166. }
  167. foreach ($vals as $val) {
  168. if (!in_array($val, array('git', 'https', 'http'))) {
  169. return 'valid protocols include: git, https, http';
  170. }
  171. }
  172. return true;
  173. },
  174. function ($vals) {
  175. return $vals;
  176. }
  177. ),
  178. );
  179. $settingKey = $input->getArgument('setting-key');
  180. foreach ($uniqueConfigValues as $name => $callbacks) {
  181. if ($settingKey === $name) {
  182. if ($input->getOption('unset')) {
  183. return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) {
  184. unset($config['config'][$key]);
  185. });
  186. }
  187. list($validator, $normalizer) = $callbacks;
  188. if (1 !== count($values)) {
  189. throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300');
  190. }
  191. if (true !== $validation = $validator($values[0])) {
  192. throw new \RuntimeException(sprintf(
  193. '"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  194. $values[0]
  195. ));
  196. }
  197. return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values[0]), function (&$config, $key, $val) {
  198. $config['config'][$key] = $val;
  199. });
  200. }
  201. }
  202. foreach ($multiConfigValues as $name => $callbacks) {
  203. if ($settingKey === $name) {
  204. if ($input->getOption('unset')) {
  205. return $this->manipulateJson('removeConfigSetting', $settingKey, function (&$config, $key) {
  206. unset($config['config'][$key]);
  207. });
  208. }
  209. list($validator, $normalizer) = $callbacks;
  210. if (true !== $validation = $validator($values)) {
  211. throw new \RuntimeException(sprintf(
  212. '%s is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  213. json_encode($values)
  214. ));
  215. }
  216. return $this->manipulateJson('addConfigSetting', $settingKey, $normalizer($values), function (&$config, $key, $val) {
  217. $config['config'][$key] = $val;
  218. });
  219. }
  220. }
  221. }
  222. protected function manipulateJson($method, $args, $fallback)
  223. {
  224. $args = func_get_args();
  225. // remove method & fallback
  226. array_shift($args);
  227. $fallback = array_pop($args);
  228. $contents = file_get_contents($this->configFile->getPath());
  229. $manipulator = new JsonManipulator($contents);
  230. // try to update cleanly
  231. if (call_user_func_array(array($manipulator, $method), $args)) {
  232. file_put_contents($this->configFile->getPath(), $manipulator->getContents());
  233. } else {
  234. // on failed clean update, call the fallback and rewrite the whole file
  235. $config = $this->configFile->read();
  236. array_unshift($args, $config);
  237. call_user_func_array($fallback, $args);
  238. $this->configFile->write($config);
  239. }
  240. }
  241. /**
  242. * Display the contents of the file in a pretty formatted way
  243. *
  244. * @param array $contents
  245. * @param OutputInterface $output
  246. * @param string|null $k
  247. */
  248. protected function listConfiguration(array $contents, OutputInterface $output, $k = null)
  249. {
  250. foreach ($contents as $key => $value) {
  251. if ($k === null && !in_array($key, array('config', 'repositories'))) {
  252. continue;
  253. }
  254. if (is_array($value)) {
  255. $k .= preg_replace('{^config\.}', '', $key . '.');
  256. $this->listConfiguration($value, $output, $k);
  257. if (substr_count($k,'.') > 1) {
  258. $k = str_split($k,strrpos($k,'.',-2));
  259. $k = $k[0] . '.';
  260. } else {
  261. $k = null;
  262. }
  263. continue;
  264. }
  265. $output->writeln('[<comment>' . $k . $key . '</comment>] <info>' . $value . '</info>');
  266. }
  267. }
  268. }