CopyShell.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <?php
  2. App::uses('Folder', 'Utility');
  3. App::uses('File', 'Utility');
  4. App::uses('AppShell', 'Console/Command');
  5. /** Valid characters: letters,numbers,underscores,hyphens only */
  6. if (!defined('VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE')) {
  7. define('VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE', '/^[\da-zA-Z_-]+$/');
  8. }
  9. if (!defined('NL')) {
  10. define('NL', PHP_EOL);
  11. }
  12. if (!defined('WINDOWS')) {
  13. define('WINDOWS', substr(PHP_OS, 0, 3) === 'WIN' ? true : false);
  14. }
  15. /**
  16. * A Copy Shell to update changes to your life site.
  17. * It can also be used for backups and other "copy changes only" services.
  18. *
  19. * Using a PPTP tunnel it is also highly secure while being as fast as ftp tools
  20. * like scp or rsnyc or sitecopy can possibly be.
  21. *
  22. * tested on Console:
  23. * - Windows (XP, Vista, Win7, Win8)
  24. *
  25. * tested with:
  26. * - FTP update (updating a remote server)
  27. *
  28. * does not work with:
  29. * - SFTP (although supported in newest version there are errors)
  30. *
  31. * based on:
  32. * - sitecopy [linux] (maybe switch to "rsync" some time...? way more powerful and reliable!)
  33. *
  34. * @author Mark Scherer
  35. * @license MIT
  36. */
  37. class CopyShell extends AppShell {
  38. public $scriptFolder = null;
  39. public $sitecopyFolder = null;
  40. public $sitecopyFolderName = 'copy';
  41. public $configCustomFile = 'sitecopy.txt'; // inside /config
  42. public $configGlobalFile = 'sitecopy_global.txt'; // inside /config
  43. public $configCount = 0; # total count of all configs (all types)
  44. const TYPE_APP = 0;
  45. const TYPE_CAKE = 1;
  46. const TYPE_PLUGIN = 2;
  47. const TYPE_VENDOR = 3;
  48. const TYPE_CUSTOM = 4;
  49. public $types = array(
  50. self::TYPE_APP => 'app',
  51. self::TYPE_CAKE => 'cake',
  52. self::TYPE_VENDOR => 'vendor',
  53. self::TYPE_PLUGIN => 'plugin',
  54. self::TYPE_CUSTOM => 'custom'
  55. );
  56. public $matches = array(
  57. self::TYPE_CAKE => 'lib/Cake',
  58. self::TYPE_VENDOR => 'vendors', # in root dir
  59. self::TYPE_PLUGIN => 'plugins', # in root dir
  60. );
  61. public $type = self::TYPE_APP;
  62. public $configName = null; # like "test" in "app_test" or "123" in "custom_123"
  63. // both typeName and configName form the "site" name: typeName_configName
  64. public $configCustom = array(); # configFile Content
  65. public $configGlobal = array(); # configFile Content
  66. public $tmpFolder = null;
  67. public $tmpFile = null;
  68. public $logFile = null;
  69. public $localFolder = APP;
  70. public $remoteFolder = null;
  71. /**
  72. * CopyShell::startup()
  73. *
  74. * @return void
  75. */
  76. public function startup() {
  77. $this->scriptFolder = dirname(__FILE__) . DS;
  78. $this->sitecopyFolder = $this->scriptFolder . $this->sitecopyFolderName . DS;
  79. $this->tmpFolder = TMP . 'cache' . DS . 'copy' . DS;
  80. /*
  81. TODO - garbage clean up of log file
  82. if (file_exists($this->tmpFolder.'log.txt') && (int)round(filesize($this->tmpFolder.'log.txt')/1024) > 2000) { # > 2 MB
  83. unlink($this->tmpFolder.'log.txt');
  84. }
  85. //echo (int)round(filesize($this->tmpFolder.'log.txt')/1024);
  86. */
  87. parent::startup();
  88. }
  89. /**
  90. * Main method to run updates
  91. * to use params you need to explicitly call "Tools.Copy main -params"
  92. *
  93. * @return void
  94. */
  95. public function run() {
  96. $configContent = $this->getConfigs();
  97. // change type if param given
  98. if (!empty($this->params['cake'])) { # cake core
  99. $this->type = static::TYPE_CAKE;
  100. } elseif (!empty($this->params['vendors'])) {
  101. $this->type = static::TYPE_VENDOR;
  102. } elseif (!empty($this->params['plugins'])) {
  103. $this->type = static::TYPE_PLUGIN;
  104. }
  105. $this->out($this->types[$this->type]);
  106. // find all mathing configs to this type
  107. $configs = array();
  108. if (!empty($configContent)) {
  109. $configs = $this->getConfigNames($configContent);
  110. }
  111. $this->out('' . count($configs) . ' of ' . $this->configCount . ' configs match:');
  112. $this->out('');
  113. $connections = array();
  114. if (!empty($configs)) {
  115. //$connections = array_keys($configs); # problems with 0 (mistake in shell::in())
  116. foreach ($configs as $key => $config) {
  117. $this->out(($key + 1) . ': ' . $config);
  118. $connections[] = $key + 1;
  119. }
  120. } else {
  121. $this->out('No configs found in /config/' . $this->configCustomFile . '!');
  122. }
  123. if (false) {
  124. /*
  125. if (count($connections) == 1) {
  126. $connection = 1;
  127. } elseif (isset($this->args[0])) {
  128. $tryConnection = array_search($this->args[0], $configs);
  129. if ($tryConnection !== false) {
  130. $connection = $tryConnection+1;
  131. }
  132. */
  133. } else {
  134. array_unshift($connections, 'q', 'h');
  135. $connection = $this->in(__('Use Sitecopy Config ([q] to quit, [h] for help)') . ':', $connections, 'q');
  136. }
  137. $this->out('');
  138. if (empty($connection) || $connection === 'q') {
  139. return $this->error('Aborted!');
  140. }
  141. if ($connection === 'h') {
  142. $this->help();
  143. return;
  144. }
  145. if (in_array($connection, $connections) && is_numeric($connection)) {
  146. $configuration = $this->getConfig($configs[$connection - 1], $configContent);
  147. //$this->typeName :: $this->types[$this->type]
  148. $configName = explode('_', $configs[$connection - 1], 2);
  149. if (!empty($configName[1])) {
  150. $this->configName = $configName[1];
  151. } else {
  152. return $this->error('Invalid config name \'' . $configs[$connection - 1] . '\'');
  153. }
  154. }
  155. // allow c, v and p only with app configs -> set params (by splitting app_configName into app and configName)
  156. if ($this->type > 3 || $this->type > 0 && $configName[0] !== 'app') {
  157. return $this->error('"-c" (-cake), "-v" (-vendor) and "-p" (-plugin) only possible with app configs (not with custom ones)');
  158. }
  159. if (empty($configuration)) {
  160. return $this->error('Error...');
  161. }
  162. $this->out('... Config \'' . $this->types[$this->type] . '_' . $this->configName . '\' selected ...');
  163. $hasLocalPath = false;
  164. $this->out('');
  165. // display global content (if available)
  166. if (!empty($this->configGlobal)) {
  167. //$this->out('GLOBAL CONFIG:');
  168. foreach ($this->configGlobal as $c) {
  169. if ($rF = $this->isRemotePath($c)) {
  170. $this->remoteFolder = $rF;
  171. } elseif ($this->isLocalPath($c)) {
  172. $hasLocalPath = true;
  173. }
  174. //$this->out($c);
  175. }
  176. }
  177. // display custom content
  178. //$this->out('CUSTOM CONFIG (may override global config):');
  179. $this->credentials = array();
  180. foreach ($configuration as $c) {
  181. if ($rF = $this->isRemotePath($c)) {
  182. $this->remoteFolder = $rF;
  183. } elseif ($lF = $this->isLocalPath($c)) {
  184. $this->localFolder = $lF;
  185. $hasLocalPath = true;
  186. } elseif ($cr = $this->areCredentials($c)) {
  187. $this->credentials[] = $cr;
  188. }
  189. }
  190. // "vendor" or "cake"? -> change both localFolder and remoteFolder and add them to to the config array
  191. if ($this->type > 0) {
  192. $configuration = $this->getConfig($this->types[$this->type], $configContent);
  193. //pr($configuration);
  194. $folder = $this->types[$this->type];
  195. if (!empty($this->matches[$this->type])) {
  196. $folder = $this->matches[$this->type];
  197. }
  198. // working with different OS - best to always use / slash
  199. $this->localFolder = dirname($this->localFolder) . DS . $folder;
  200. $this->localFolder = str_replace(DS, '/', $this->localFolder);
  201. $this->remoteFolder = dirname($this->remoteFolder) . DS . $folder;
  202. $this->remoteFolder = str_replace(DS, '/', $this->remoteFolder);
  203. foreach ($this->credentials as $c) {
  204. $configuration[] = $c;
  205. }
  206. $configuration[] = $this->localFolder;
  207. $configuration[] = $this->remoteFolder;
  208. }
  209. /*
  210. if (!$hasLocalPath) {
  211. // add the automatically found app folder as local path (default if no specific local path was given)
  212. $localPath = 'local '.TB.TB.$this->localFolder;
  213. $this->out($localPath);
  214. $configuration[] = $localPath;
  215. }
  216. */
  217. $this->tmpFile = 'config_' . $this->types[$this->type] . '_' . $this->configName . '.tmp';
  218. $this->logFile = 'log_' . $this->types[$this->type] . '_' . $this->configName . '.txt';
  219. // create tmp config file (adding the current APP path, of no local path was given inside the config file)
  220. $File = new File($this->tmpFolder . $this->tmpFile, true, 0770);
  221. //$File->open();
  222. $configTotal = array();
  223. // extract "side xyz" from config, add global and then the rest of custom
  224. $configTotal[] = 'site ' . $this->types[$this->type] . '_' . $this->configName;//$configuration[0];
  225. unset($configuration[0]);
  226. foreach ($this->configGlobal as $c) {
  227. $configTotal[] = $c;
  228. }
  229. foreach ($configuration as $c) {
  230. $configTotal[] = $c;
  231. }
  232. foreach ($configTotal as $key => $val) {
  233. $this->out($val);
  234. }
  235. $File->write(implode(NL, $configTotal), 'w', true);
  236. while (true) {
  237. $this->out('');
  238. $this->out('Type: ' . $this->types[$this->type]);
  239. $this->out('');
  240. $allowedActions = array('i', 'c', 'l', 'f', 'u', 's');
  241. if (isset($this->args[1])) {
  242. $action = strtolower(trim($this->args[1]));
  243. $this->args[1] = null; # only the first time
  244. } elseif (isset($this->args[0])) {
  245. if (mb_strlen(trim($this->args[0])) === 1) {
  246. $action = strtolower(trim($this->args[0]));
  247. }
  248. $this->args[0] = null; # only the first time
  249. }
  250. if (empty($action) || !in_array($action, $allowedActions)) {
  251. $action = strtolower($this->in(__('Init, Catchup, List, Fetch, Update, Synch (or [q] to quit)') . ':', array_merge($allowedActions, array('q')), 'l'));
  252. }
  253. if ($action === 'q') {
  254. return $this->error('Aborted!');
  255. }
  256. if (in_array($action, $allowedActions)) {
  257. // synch can destroy local information that might not have been saved yet, so confirm
  258. if ($action === 's') {
  259. $continue = $this->in(__('Local files might be overridden... Continue?'), array('y', 'n'), 'n');
  260. if (strtolower($continue) !== 'y' && strtolower($continue) !== 'yes') {
  261. $action = '';
  262. continue;
  263. }
  264. }
  265. $options = array();
  266. $options[] = '--show-progress';
  267. if (!empty($this->params['force'])) {
  268. $options[] = '--keep-going';
  269. }
  270. $name = $this->types[$this->type] . '_' . $this->configName;
  271. $this->_execute($name, $action, $options);
  272. }
  273. $action = '';
  274. }
  275. }
  276. /**
  277. * Only main functions covered - see "sitecopy --help" for more information
  278. */
  279. protected function _execute($config = null, $action = null, $options = array()) {
  280. $options[] = '--debug=ftp,socket --rcfile=' . $this->tmpFolder . $this->tmpFile .
  281. ' --storepath=' . $this->tmpFolder . ' --logfile=' . $this->tmpFolder . $this->logFile;
  282. if (!empty($action)) {
  283. if ($action === 'i') {
  284. $options[] = '--initialize';
  285. } elseif ($action === 'c') {
  286. $options[] = '--catchup';
  287. } elseif ($action === 'l') {
  288. $options[] = '--list';
  289. } elseif ($action === 'f') {
  290. $options[] = '--fetch';
  291. } elseif ($action === 'u') {
  292. $options[] = '--update';
  293. } elseif ($action === 's') {
  294. $options[] = '--synchronize';
  295. }
  296. }
  297. #last
  298. if (!empty($config)) {
  299. $options[] = $config;
  300. //pr($options);
  301. }
  302. $this->_exec(false, $options);
  303. // "Job Done"-Sound for the time comsuming actions (could be other sounds as well?)
  304. if ($action === 'f' || $action === 'u') {
  305. $this->_beep();
  306. }
  307. $this->out('... done ...');
  308. }
  309. /**
  310. * @param string $line
  311. * @return bool If local path
  312. */
  313. protected function isLocalPath($line) {
  314. if (mb_strlen($line) > 7 && trim(mb_substr($line, 0, 6)) === 'local') {
  315. $config = trim(str_replace('local ', '', $line));
  316. if (!empty($config)) {
  317. return $config;
  318. }
  319. }
  320. return false;
  321. }
  322. /**
  323. * @return string path on success, boolean FALSE otherwise
  324. */
  325. protected function isRemotePath($line) {
  326. if (mb_strlen($line) > 8 && trim(mb_substr($line, 0, 7)) === 'remote') {
  327. $config = trim(str_replace('remote ', '', $line));
  328. if (!empty($config)) {
  329. return $config;
  330. }
  331. }
  332. return false;
  333. }
  334. /**
  335. * @return string path on success, boolean FALSE otherwise
  336. */
  337. protected function areCredentials($line) {
  338. if (mb_strlen($line) > 8) {
  339. if (trim(mb_substr($line, 0, 7)) === 'server') {
  340. $config = trim(str_replace('server ', '', $line));
  341. } elseif (trim(mb_substr($line, 0, 9)) === 'username') {
  342. $config = trim(str_replace('username ', '', $line));
  343. } elseif (trim(mb_substr($line, 0, 9)) === 'password') {
  344. $config = trim(str_replace('password ', '', $line));
  345. }
  346. if (!empty($config)) {
  347. return $config;
  348. }
  349. }
  350. return false;
  351. }
  352. /**
  353. * Make a small sound to inform the user accustically about the success.
  354. *
  355. * @return void
  356. */
  357. protected function _beep() {
  358. if ($this->params['silent']) {
  359. return;
  360. }
  361. // seems to work only on windows systems - advantage: sound does not need to be on
  362. $File = new File($this->scriptFolder . 'files' . DS . 'beep.bat');
  363. $sound = $File->read();
  364. system($sound);
  365. // seems to work on only on windows xp systems + where sound is on
  366. //$sound = 'sndrec32 /play /close "'.$this->scriptFolder.'files'.DS.'notify.wav';
  367. //system($sound);
  368. if (WINDOWS) {
  369. } else {
  370. exec('echo -e "\a"');
  371. }
  372. }
  373. /**
  374. * @return bool Success
  375. */
  376. protected function _exec($silent = true, $options = array()) {
  377. // make sure, folder exists
  378. $Folder = new Folder($this->tmpFolder, true, 0770);
  379. $f = (WINDOWS ? $this->sitecopyFolder : '') . 'sitecopy ';
  380. $f .= implode(' ', $options);
  381. if (!empty($this->params['debug'])) {
  382. $this->hr();
  383. $this->out($f);
  384. $this->hr();
  385. return true;
  386. }
  387. if ($silent !== false) {
  388. $res = exec($f);
  389. return $res === 0;
  390. }
  391. $res = system($f);
  392. return $res !== false;
  393. }
  394. /**
  395. * Displays help contents
  396. *
  397. * @return void
  398. */
  399. public function help() {
  400. $this->hr();
  401. $this->out("Usage: cake copy <arg1> <arg2>...");
  402. $this->hr();
  403. $this->out('Commands [arg1]:');
  404. $this->out("\tName of Configuration (only neccessarry if there are more than 1)");
  405. $this->out("\nCommands [arg2]:");
  406. $this->out("\ti: Init (Mark all files and directories as not updated)");
  407. $this->out("\tc: Catchup (Mark all files and directories as updated)");
  408. $this->out("\tl: List (Show differences)");
  409. $this->out("\tf: Fetch (Get differences)");
  410. $this->out("\tu: Update (Copy local content to remote location)");
  411. $this->out("\ts: Synch (Get remote content and override local files");
  412. $this->out("");
  413. $continue = $this->in(__('Show script help, too?'), array('y', 'n'), 'y');
  414. if (strtolower($continue) === 'y' || strtolower($continue) === 'yes') {
  415. // somehow does not work yet (inside cake console anyway...)
  416. $this->_exec(false, array('--help'));
  417. $this->out('');
  418. $this->_exec(false, array('--version'));
  419. }
  420. return $this->_stop();
  421. }
  422. /**
  423. * Read out config file and parse it to an array
  424. */
  425. protected function getConfigs() {
  426. // global file (may be present)
  427. $File = new File($this->localFolder . 'config' . DS . $this->configGlobalFile);
  428. if ($File->exists()) {
  429. $File->open('r');
  430. $content = (string)$File->read();
  431. $content = explode(NL, $content);
  432. if (!empty($content)) {
  433. $configGlobal = array();
  434. foreach ($content as $line => $c) {
  435. $c = trim($c);
  436. if (!empty($c)) {
  437. $configGlobal[] = $c;
  438. }
  439. }
  440. $this->configGlobal = $configGlobal;
  441. }
  442. }
  443. // custom file (must be present)
  444. $File = new File($this->localFolder . 'config' . DS . $this->configCustomFile);
  445. if (!$File->exists()) {
  446. return $this->error('No config file present (/config/' . $this->configCustomFile . ')!');
  447. }
  448. $File->open('r');
  449. // Read out configs
  450. $content = $File->read();
  451. if (empty($content)) {
  452. return array();
  453. }
  454. $content = explode(NL, $content);
  455. if (empty($content)) {
  456. return array();
  457. }
  458. $configContent = array();
  459. foreach ($content as $line => $c) {
  460. $c = trim($c);
  461. if (!empty($c)) {
  462. $configContent[] = $c;
  463. }
  464. }
  465. return $configContent;
  466. }
  467. /**
  468. * Get a list with available configs
  469. *
  470. * @param array $content
  471. * checks on whether all config names are valid!
  472. */
  473. protected function getConfigNames($content) {
  474. $configs = array();
  475. foreach ($content as $c) {
  476. if (mb_strlen($c) > 6 && trim(mb_substr($c, 0, 5)) === 'site') {
  477. $config = trim(str_replace('site ', '', $c));
  478. if (!empty($config)) {
  479. if (!$this->isValidConfigName($config)) {
  480. return $this->error('Invalid Config Name \'' . $config . '\' in /config/' . $this->configCustomFile . '!' . NL . 'Allowed: [app|custom]+\'_\'+{a-z0-9-} or [cake|vendor|plugin]');
  481. }
  482. if ($this->typeMatchesConfigName($config, $this->type)) {
  483. $configs[] = $config;
  484. }
  485. $this->configCount++;
  486. }
  487. }
  488. }
  489. return $configs;
  490. }
  491. /**
  492. * Makes sure nothing strange happens if there is an invalid config name
  493. * (like updating right away on "cake copy u", if u is supposed to be the config name...)
  494. */
  495. protected function isValidConfigName($name) {
  496. $reservedWords = array('i', 'c', 'l', 'f', 'u', 's');
  497. if (in_array($name, $reservedWords)) {
  498. return false;
  499. }
  500. if (!preg_match(VALID_ALPHANUMERIC_HYPHEN_UNDERSCORE, $name)) {
  501. return false;
  502. }
  503. if ($name !== 'cake' && $name !== 'vendor' && $name !== 'plugin' && substr($name, 0, 4) !== 'app_' && substr($name, 0, 7) !== 'custom_') {
  504. return false;
  505. }
  506. return true;
  507. }
  508. /**
  509. * Makes sure type matches config name (app = only app configs, no cake or vendor or custom configs!)
  510. *
  511. * @return string type on success, otherwise boolean false
  512. */
  513. protected function typeMatchesConfigName($name, $type) {
  514. if (array_key_exists($type, $this->types) && $name !== 'cake' && $name !== 'vendor' && $name !== 'plugin') {
  515. $splits = explode('_', $name, 2); # cake_eee_sss = cake & eee_sss
  516. if (!empty($splits[0]) && in_array(trim($splits[0]), $this->types)) {
  517. return true;
  518. }
  519. }
  520. return false;
  521. }
  522. /**
  523. * Return the specific config of a config name
  524. *
  525. * @param string $config name
  526. * @param array $content
  527. * @return array
  528. */
  529. protected function getConfig($config, $content) {
  530. $configs = array();
  531. $started = false;
  532. foreach ($content as $c) {
  533. if (mb_strlen($c) > 6 && substr($c, 0, 5) === 'site ') {
  534. $currentConfig = trim(str_replace('site ', '', $c));
  535. if (!empty($currentConfig) && $currentConfig == $config) {
  536. // start
  537. if (!$started) {
  538. // prevent problems with 2 configs with the same alias (but shouldnt happen anyway)
  539. $currentConfig = null;
  540. }
  541. $started = true;
  542. }
  543. }
  544. if ($started && !empty($currentConfig)) {
  545. // done
  546. break;
  547. }
  548. if ($started) {
  549. $configs[] = $c;
  550. }
  551. }
  552. return $configs;
  553. }
  554. public function getOptionParser() {
  555. $subcommandParser = array(
  556. 'options' => array(
  557. /*
  558. 'plugin' => array(
  559. 'short' => 'g',
  560. 'help' => 'The plugin to update. Only the specified plugin will be updated.'),
  561. 'default' => ''
  562. ),
  563. 'dry-run'=> array(
  564. 'short' => 'd',
  565. 'help' => 'Dry run the update, no files will actually be modified.'),
  566. 'boolean' => true
  567. ),
  568. 'log'=> array(
  569. 'short' => 'l',
  570. 'help' => 'Log all ouput to file log.txt in TMP dir'),
  571. 'boolean' => true
  572. ),
  573. */
  574. 'silent' => array(
  575. 'short' => 's',
  576. 'help' => 'Silent mode (no beep sound)',
  577. 'boolean' => true
  578. ),
  579. 'vendors' => array(
  580. 'short' => 'e',
  581. 'help' => 'ROOT/vendor',
  582. 'boolean' => true
  583. ),
  584. 'cake' => array(
  585. 'short' => 'c',
  586. 'help' => 'ROOT/lib/Cake',
  587. 'boolean' => true
  588. ),
  589. 'app' => array(
  590. 'short' => 'a',
  591. 'help' => 'ROOT/app',
  592. 'boolean' => true
  593. ),
  594. 'plugins' => array(
  595. 'short' => 'p',
  596. 'help' => 'ROOT/plugin',
  597. 'boolean' => true
  598. ),
  599. 'custom' => array(
  600. 'short' => 'u',
  601. 'help' => 'Custom path',
  602. 'boolean' => true
  603. ),
  604. 'force' => array(
  605. 'short' => 'f',
  606. 'help' => 'Force (keep going regardless of errors)',
  607. 'boolean' => true
  608. ),
  609. 'debug' => array(
  610. 'help' => 'Debug output only',
  611. 'boolean' => true
  612. )
  613. )
  614. );
  615. return parent::getOptionParser()
  616. ->description("A shell to quickly upload modified files (diff) only.")
  617. ->addSubcommand('run', array(
  618. 'help' => 'Update',
  619. 'parser' => $subcommandParser
  620. ));
  621. }
  622. }