ConfigCommand.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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\Config\JsonConfigSource;
  18. use Composer\Factory;
  19. use Composer\Json\JsonFile;
  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 Config
  28. */
  29. protected $config;
  30. /**
  31. * @var Composer\Json\JsonFile
  32. */
  33. protected $configFile;
  34. /**
  35. * @var Composer\Config\JsonConfigSource
  36. */
  37. protected $configSource;
  38. /**
  39. * {@inheritDoc}
  40. */
  41. protected function configure()
  42. {
  43. $this
  44. ->setName('config')
  45. ->setDescription('Set config options')
  46. ->setDefinition(array(
  47. new InputOption('global', 'g', InputOption::VALUE_NONE, 'Apply command to the global config file'),
  48. new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'),
  49. new InputOption('auth', 'a', InputOption::VALUE_NONE, 'Affect auth config file (only used for --editor)'),
  50. new InputOption('unset', null, InputOption::VALUE_NONE, 'Unset the given setting-key'),
  51. new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'),
  52. new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json', 'composer.json'),
  53. new InputOption('absolute', null, InputOption::VALUE_NONE, 'Returns absolute paths when fetching *-dir config values instead of relative'),
  54. new InputArgument('setting-key', null, 'Setting key'),
  55. new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'),
  56. ))
  57. ->setHelp(<<<EOT
  58. This command allows you to edit some basic composer settings in either the
  59. local composer.json file or the global config.json file.
  60. To edit the global config.json file:
  61. <comment>%command.full_name% --global</comment>
  62. To add a repository:
  63. <comment>%command.full_name% repositories.foo vcs http://bar.com</comment>
  64. You can add a repository to the global config.json file by passing in the
  65. <info>--global</info> option.
  66. To edit the file in an external editor:
  67. <comment>%command.full_name% --editor</comment>
  68. To choose your editor you can set the "EDITOR" env variable.
  69. To get a list of configuration values in the file:
  70. <comment>%command.full_name% --list</comment>
  71. You can always pass more than one option. As an example, if you want to edit the
  72. global config.json file.
  73. <comment>%command.full_name% --editor --global</comment>
  74. EOT
  75. )
  76. ;
  77. }
  78. /**
  79. * {@inheritDoc}
  80. */
  81. protected function initialize(InputInterface $input, OutputInterface $output)
  82. {
  83. parent::initialize($input, $output);
  84. if ($input->getOption('global') && 'composer.json' !== $input->getOption('file')) {
  85. throw new \RuntimeException('--file and --global can not be combined');
  86. }
  87. $this->config = Factory::createConfig($this->getIO());
  88. // Get the local composer.json, global config.json, or if the user
  89. // passed in a file to use
  90. $configFile = $input->getOption('global')
  91. ? ($this->config->get('home') . '/config.json')
  92. : $input->getOption('file');
  93. $this->configFile = new JsonFile($configFile);
  94. $this->configSource = new JsonConfigSource($this->configFile);
  95. $authConfigFile = $input->getOption('global')
  96. ? ($this->config->get('home') . '/auth.json')
  97. : dirname(realpath($input->getOption('file'))) . '/auth.json';
  98. $this->authConfigFile = new JsonFile($authConfigFile);
  99. $this->authConfigSource = new JsonConfigSource($this->authConfigFile, true);
  100. // initialize the global file if it's not there
  101. if ($input->getOption('global') && !$this->configFile->exists()) {
  102. touch($this->configFile->getPath());
  103. $this->configFile->write(array('config' => new \ArrayObject));
  104. @chmod($this->configFile->getPath(), 0600);
  105. }
  106. if ($input->getOption('global') && !$this->authConfigFile->exists()) {
  107. touch($this->authConfigFile->getPath());
  108. $this->authConfigFile->write(array('http-basic' => new \ArrayObject, 'github-oauth' => new \ArrayObject));
  109. @chmod($this->authConfigFile->getPath(), 0600);
  110. }
  111. if (!$this->configFile->exists()) {
  112. throw new \RuntimeException("File '$configFile' cannot be found in the current directory");
  113. }
  114. }
  115. /**
  116. * {@inheritDoc}
  117. */
  118. protected function execute(InputInterface $input, OutputInterface $output)
  119. {
  120. // Open file in editor
  121. if ($input->getOption('editor')) {
  122. $editor = escapeshellcmd(getenv('EDITOR'));
  123. if (!$editor) {
  124. if (defined('PHP_WINDOWS_VERSION_BUILD')) {
  125. $editor = 'notepad';
  126. } else {
  127. foreach (array('vim', 'vi', 'nano', 'pico', 'ed') as $candidate) {
  128. if (exec('which '.$candidate)) {
  129. $editor = $candidate;
  130. break;
  131. }
  132. }
  133. }
  134. }
  135. $file = $input->getOption('auth') ? $this->authConfigFile->getPath() : $this->configFile->getPath();
  136. system($editor . ' ' . $file . (defined('PHP_WINDOWS_VERSION_BUILD') ? '' : ' > `tty`'));
  137. return 0;
  138. }
  139. if (!$input->getOption('global')) {
  140. $this->config->merge($this->configFile->read());
  141. $this->config->merge(array('config' => $this->authConfigFile->exists() ? $this->authConfigFile->read() : array()));
  142. }
  143. // List the configuration of the file settings
  144. if ($input->getOption('list')) {
  145. $this->listConfiguration($this->config->all(), $this->config->raw(), $output);
  146. return 0;
  147. }
  148. $settingKey = $input->getArgument('setting-key');
  149. if (!$settingKey) {
  150. return 0;
  151. }
  152. // If the user enters in a config variable, parse it and save to file
  153. if (array() !== $input->getArgument('setting-value') && $input->getOption('unset')) {
  154. throw new \RuntimeException('You can not combine a setting value with --unset');
  155. }
  156. // show the value if no value is provided
  157. if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) {
  158. $data = $this->config->all();
  159. if (preg_match('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) {
  160. if (empty($matches[1])) {
  161. $value = isset($data['repositories']) ? $data['repositories'] : array();
  162. } else {
  163. if (!isset($data['repositories'][$matches[1]])) {
  164. throw new \InvalidArgumentException('There is no '.$matches[1].' repository defined');
  165. }
  166. $value = $data['repositories'][$matches[1]];
  167. }
  168. } elseif (strpos($settingKey, '.')) {
  169. $bits = explode('.', $settingKey);
  170. $data = $data['config'];
  171. foreach ($bits as $bit) {
  172. if (isset($data[$bit])) {
  173. $data = $data[$bit];
  174. } elseif (isset($data[implode('.', $bits)])) {
  175. // last bit can contain domain names and such so try to join whatever is left if it exists
  176. $data = $data[implode('.', $bits)];
  177. break;
  178. } else {
  179. throw new \RuntimeException($settingKey.' is not defined');
  180. }
  181. array_shift($bits);
  182. }
  183. $value = $data;
  184. } elseif (isset($data['config'][$settingKey])) {
  185. $value = $this->config->get($settingKey, $input->getOption('absolute') ? 0 : Config::RELATIVE_PATHS);
  186. } else {
  187. throw new \RuntimeException($settingKey.' is not defined');
  188. }
  189. if (is_array($value)) {
  190. $value = json_encode($value);
  191. }
  192. $output->writeln($value);
  193. return 0;
  194. }
  195. $values = $input->getArgument('setting-value'); // what the user is trying to add/change
  196. // handle repositories
  197. if (preg_match('/^repos?(?:itories)?\.(.+)/', $settingKey, $matches)) {
  198. if ($input->getOption('unset')) {
  199. return $this->configSource->removeRepository($matches[1]);
  200. }
  201. if (2 !== count($values)) {
  202. throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com');
  203. }
  204. return $this->configSource->addRepository($matches[1], array(
  205. 'type' => $values[0],
  206. 'url' => $values[1],
  207. ));
  208. }
  209. // handle github-oauth
  210. if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) {
  211. if ($input->getOption('unset')) {
  212. $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]);
  213. $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
  214. return;
  215. }
  216. if ($matches[1] === 'github-oauth') {
  217. if (1 !== count($values)) {
  218. throw new \RuntimeException('Too many arguments, expected only one token');
  219. }
  220. $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
  221. $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], $values[0]);
  222. } elseif ($matches[1] === 'http-basic') {
  223. if (2 !== count($values)) {
  224. throw new \RuntimeException('Expected two arguments (username, password), got '.count($values));
  225. }
  226. $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
  227. $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], array('username' => $values[0], 'password' => $values[1]));
  228. }
  229. return;
  230. }
  231. $booleanValidator = function ($val) { return in_array($val, array('true', 'false', '1', '0'), true); };
  232. $booleanNormalizer = function ($val) { return $val !== 'false' && (bool) $val; };
  233. // handle config values
  234. $uniqueConfigValues = array(
  235. 'process-timeout' => array('is_numeric', 'intval'),
  236. 'use-include-path' => array($booleanValidator, $booleanNormalizer),
  237. 'preferred-install' => array(
  238. function ($val) { return in_array($val, array('auto', 'source', 'dist'), true); },
  239. function ($val) { return $val; }
  240. ),
  241. 'store-auths' => array(
  242. function ($val) { return in_array($val, array('true', 'false', 'prompt'), true); },
  243. function ($val) {
  244. if ('prompt' === $val) {
  245. return 'prompt';
  246. }
  247. return $val !== 'false' && (bool) $val;
  248. }
  249. ),
  250. 'notify-on-install' => array($booleanValidator, $booleanNormalizer),
  251. 'vendor-dir' => array('is_string', function ($val) { return $val; }),
  252. 'bin-dir' => array('is_string', function ($val) { return $val; }),
  253. 'cache-dir' => array('is_string', function ($val) { return $val; }),
  254. 'cache-files-dir' => array('is_string', function ($val) { return $val; }),
  255. 'cache-repo-dir' => array('is_string', function ($val) { return $val; }),
  256. 'cache-vcs-dir' => array('is_string', function ($val) { return $val; }),
  257. 'cache-ttl' => array('is_numeric', 'intval'),
  258. 'cache-files-ttl' => array('is_numeric', 'intval'),
  259. 'cache-files-maxsize' => array(
  260. function ($val) { return preg_match('/^\s*([0-9.]+)\s*(?:([kmg])(?:i?b)?)?\s*$/i', $val) > 0; },
  261. function ($val) { return $val; }
  262. ),
  263. 'discard-changes' => array(
  264. function ($val) { return in_array($val, array('stash', 'true', 'false', '1', '0'), true); },
  265. function ($val) {
  266. if ('stash' === $val) {
  267. return 'stash';
  268. }
  269. return $val !== 'false' && (bool) $val;
  270. }
  271. ),
  272. 'autoloader-suffix' => array('is_string', function ($val) { return $val === 'null' ? null : $val; }),
  273. 'optimize-autoloader' => array($booleanValidator, $booleanNormalizer),
  274. 'prepend-autoloader' => array($booleanValidator, $booleanNormalizer),
  275. 'github-expose-hostname' => array($booleanValidator, $booleanNormalizer),
  276. );
  277. $multiConfigValues = array(
  278. 'github-protocols' => array(
  279. function ($vals) {
  280. if (!is_array($vals)) {
  281. return 'array expected';
  282. }
  283. foreach ($vals as $val) {
  284. if (!in_array($val, array('git', 'https', 'ssh'))) {
  285. return 'valid protocols include: git, https, ssh';
  286. }
  287. }
  288. return true;
  289. },
  290. function ($vals) {
  291. return $vals;
  292. }
  293. ),
  294. 'github-domains' => array(
  295. function ($vals) {
  296. if (!is_array($vals)) {
  297. return 'array expected';
  298. }
  299. return true;
  300. },
  301. function ($vals) {
  302. return $vals;
  303. }
  304. ),
  305. );
  306. foreach ($uniqueConfigValues as $name => $callbacks) {
  307. if ($settingKey === $name) {
  308. if ($input->getOption('unset')) {
  309. return $this->configSource->removeConfigSetting($settingKey);
  310. }
  311. list($validator, $normalizer) = $callbacks;
  312. if (1 !== count($values)) {
  313. throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300');
  314. }
  315. if (true !== $validation = $validator($values[0])) {
  316. throw new \RuntimeException(sprintf(
  317. '"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  318. $values[0]
  319. ));
  320. }
  321. return $this->configSource->addConfigSetting($settingKey, $normalizer($values[0]));
  322. }
  323. }
  324. foreach ($multiConfigValues as $name => $callbacks) {
  325. if ($settingKey === $name) {
  326. if ($input->getOption('unset')) {
  327. return $this->configSource->removeConfigSetting($settingKey);
  328. }
  329. list($validator, $normalizer) = $callbacks;
  330. if (true !== $validation = $validator($values)) {
  331. throw new \RuntimeException(sprintf(
  332. '%s is an invalid value'.($validation ? ' ('.$validation.')' : ''),
  333. json_encode($values)
  334. ));
  335. }
  336. return $this->configSource->addConfigSetting($settingKey, $normalizer($values));
  337. }
  338. }
  339. throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command');
  340. }
  341. /**
  342. * Display the contents of the file in a pretty formatted way
  343. *
  344. * @param array $contents
  345. * @param array $rawContents
  346. * @param OutputInterface $output
  347. * @param string|null $k
  348. */
  349. protected function listConfiguration(array $contents, array $rawContents, OutputInterface $output, $k = null)
  350. {
  351. $origK = $k;
  352. foreach ($contents as $key => $value) {
  353. if ($k === null && !in_array($key, array('config', 'repositories'))) {
  354. continue;
  355. }
  356. $rawVal = isset($rawContents[$key]) ? $rawContents[$key] : null;
  357. if (is_array($value) && (!is_numeric(key($value)) || ($key === 'repositories' && null === $k))) {
  358. $k .= preg_replace('{^config\.}', '', $key . '.');
  359. $this->listConfiguration($value, $rawVal, $output, $k);
  360. if (substr_count($k, '.') > 1) {
  361. $k = str_split($k, strrpos($k, '.', -2));
  362. $k = $k[0] . '.';
  363. } else {
  364. $k = $origK;
  365. }
  366. continue;
  367. }
  368. if (is_array($value)) {
  369. $value = array_map(function ($val) {
  370. return is_array($val) ? json_encode($val) : $val;
  371. }, $value);
  372. $value = '['.implode(', ', $value).']';
  373. }
  374. if (is_bool($value)) {
  375. $value = var_export($value, true);
  376. }
  377. if (is_string($rawVal) && $rawVal != $value) {
  378. $output->writeln('[<comment>' . $k . $key . '</comment>] <info>' . $rawVal . ' (' . $value . ')</info>');
  379. } else {
  380. $output->writeln('[<comment>' . $k . $key . '</comment>] <info>' . $value . '</info>');
  381. }
  382. }
  383. }
  384. }