1#!/usr/bin/env php 2<?php 3 4use dokuwiki\Extension\CLIPlugin; 5use dokuwiki\Extension\PluginController; 6use dokuwiki\plugin\dev\LangProcessor; 7use dokuwiki\plugin\dev\Skeletor; 8use dokuwiki\plugin\dev\SVGIcon; 9use splitbrain\phpcli\Exception as CliException; 10use splitbrain\phpcli\Options; 11 12/** 13 * @license GPL2 14 * @author Andreas Gohr <andi@splitbrain.org> 15 */ 16class cli_plugin_dev extends CLIPlugin 17{ 18 /** 19 * Register options and arguments on the given $options object 20 * 21 * @param Options $options 22 * @return void 23 */ 24 protected function setup(Options $options) 25 { 26 $options->useCompactHelp(); 27 $options->setHelp( 28 "CLI to help with DokuWiki plugin and template development.\n\n" . 29 "Run this script from within the extension's directory." 30 ); 31 32 $options->registerCommand('init', 'Initialize a new plugin or template in the current (empty) directory.'); 33 $options->registerCommand('addTest', 'Add the testing framework files and a test. (_test/)'); 34 $options->registerArgument('test', 'Optional name of the new test. Defaults to the general test.', false, 35 'addTest'); 36 $options->registerCommand('addConf', 'Add the configuration files. (conf/)'); 37 $options->registerCommand('addLang', 'Add the language files. (lang/)'); 38 39 $types = PluginController::PLUGIN_TYPES; 40 array_walk( 41 $types, 42 function (&$item) { 43 $item = $this->colors->wrap($item, $this->colors::C_BROWN); 44 } 45 ); 46 47 $options->registerCommand('addComponent', 'Add a new plugin component.'); 48 $options->registerArgument('type', 'Type of the component. Needs to be one of ' . join(', ', $types), true, 49 'addComponent'); 50 $options->registerArgument('name', 'Optional name of the component. Defaults to a base component.', false, 51 'addComponent'); 52 53 $options->registerCommand('deletedFiles', 'Create the list of deleted files based on the git history.'); 54 $options->registerCommand('rmObsolete', 'Delete obsolete files.'); 55 56 $prefixes = array_keys(SVGIcon::SOURCES); 57 array_walk( 58 $prefixes, 59 function (&$item) { 60 $item = $this->colors->wrap($item, $this->colors::C_BROWN); 61 } 62 ); 63 64 $options->registerCommand('downloadSvg', 'Download an SVG file from a known icon repository.'); 65 $options->registerArgument('prefix:name', 66 'Colon-prefixed name of the icon. Available prefixes: ' . join(', ', $prefixes), true, 'downloadSvg'); 67 $options->registerArgument('output', 'File to save, defaults to <name>.svg in current dir', false, 68 'downloadSvg'); 69 $options->registerOption('keep-ns', 'Keep the SVG namespace. Use when the file is not inlined into HTML.', 'k', 70 false, 'downloadSvg'); 71 72 $options->registerCommand('cleanSvg', 'Clean a existing SVG files to reduce their file size.'); 73 $options->registerArgument('files...', 'The files to clean (will be overwritten)', true, 'cleanSvg'); 74 $options->registerOption('keep-ns', 'Keep the SVG namespace. Use when the file is not inlined into HTML.', 'k', 75 false, 'cleanSvg'); 76 77 $options->registerCommand('cleanLang', 78 'Clean language files from unused language strings. Detecting which strings are truly in use may ' . 79 'not always correctly work. Use with caution.'); 80 } 81 82 /** @inheritDoc */ 83 protected function main(Options $options) 84 { 85 $args = $options->getArgs(); 86 87 switch ($options->getCmd()) { 88 case 'init': 89 return $this->cmdInit(); 90 case 'addTest': 91 $test = array_shift($args); 92 return $this->cmdAddTest($test); 93 case 'addConf': 94 return $this->cmdAddConf(); 95 case 'addLang': 96 return $this->cmdAddLang(); 97 case 'addComponent': 98 $type = array_shift($args); 99 $component = array_shift($args); 100 return $this->cmdAddComponent($type, $component); 101 case 'deletedFiles': 102 return $this->cmdDeletedFiles(); 103 case 'rmObsolete': 104 return $this->cmdRmObsolete(); 105 case 'downloadSvg': 106 $ident = array_shift($args); 107 $save = array_shift($args); 108 $keep = $options->getOpt('keep-ns'); 109 return $this->cmdDownloadSVG($ident, $save, $keep); 110 case 'cleanSvg': 111 $keep = $options->getOpt('keep-ns'); 112 return $this->cmdCleanSVG($args, $keep); 113 case 'cleanLang': 114 return $this->cmdCleanLang(); 115 default: 116 $this->error('Unknown command'); 117 echo $options->help(); 118 return 0; 119 } 120 } 121 122 /** 123 * Get the extension name from the current working directory 124 * 125 * @throws CliException if something's wrong 126 * @param string $dir 127 * @return string[] name, type 128 */ 129 protected function getTypedNameFromDir($dir) 130 { 131 $pdir = fullpath(DOKU_PLUGIN); 132 $tdir = fullpath(tpl_incdir() . '../'); 133 134 if (strpos($dir, $pdir) === 0) { 135 $ldir = substr($dir, strlen($pdir)); 136 $type = 'plugin'; 137 } elseif (strpos($dir, $tdir) === 0) { 138 $ldir = substr($dir, strlen($tdir)); 139 $type = 'template'; 140 } else { 141 throw new CliException('Current directory needs to be in plugin or template directory'); 142 } 143 144 $ldir = trim($ldir, '/'); 145 146 if (strpos($ldir, '/') !== false) { 147 throw new CliException('Current directory has to be main extension directory'); 148 } 149 150 return [$ldir, $type]; 151 } 152 153 /** 154 * Interactively ask for a value from the user 155 * 156 * @param string $prompt 157 * @param bool $cache cache given value for next time? 158 * @return string 159 */ 160 protected function readLine($prompt, $cache = false) 161 { 162 $value = ''; 163 $default = ''; 164 $cachename = getCacheName($prompt, '.readline'); 165 if ($cache && file_exists($cachename)) { 166 $default = file_get_contents($cachename); 167 } 168 169 while ($value === '') { 170 echo $prompt; 171 if ($default) echo ' [' . $default . ']'; 172 echo ': '; 173 174 $fh = fopen('php://stdin', 'r'); 175 $value = trim(fgets($fh)); 176 fclose($fh); 177 178 if ($value === '') $value = $default; 179 } 180 181 if ($cache) { 182 file_put_contents($cachename, $value); 183 } 184 185 return $value; 186 } 187 188 /** 189 * Create the given files with their given content 190 * 191 * Ignores all files that already exist 192 * 193 * @param array $files A File array as created by Skeletor::getFiles() 194 */ 195 protected function createFiles($files) { 196 foreach ($files as $path => $content) { 197 if(file_exists($path)) { 198 $this->error($path . ' already exists'); 199 continue; 200 } 201 202 io_makeFileDir($path); 203 file_put_contents($path, $content); 204 $this->success($path . ' created'); 205 } 206 } 207 208 /** 209 * Delete the given file if it exists 210 * 211 * @param string $file 212 */ 213 protected function deleteFile($file) 214 { 215 if (!file_exists($file)) return; 216 if (@unlink($file)) { 217 $this->success('Delete ' . $file); 218 } 219 } 220 221 /** 222 * Run git with the given arguments and return the output 223 * 224 * @throws CliException when the command can't be run 225 * @param string ...$args 226 * @return string[] 227 */ 228 protected function git(...$args) 229 { 230 $args = array_map('escapeshellarg', $args); 231 $cmd = 'git ' . join(' ', $args); 232 $output = []; 233 $result = 0; 234 235 $this->info($cmd); 236 $last = exec($cmd, $output, $result); 237 if ($last === false || $result !== 0) { 238 throw new CliException('Running git failed'); 239 } 240 241 return $output; 242 } 243 244 // region Commands 245 246 /** 247 * Intialize the current directory as a plugin or template 248 * 249 * @return int 250 */ 251 protected function cmdInit() 252 { 253 $dir = fullpath(getcwd()); 254 if ((new FilesystemIterator($dir))->valid()) { 255 throw new CliException('Current directory needs to be empty'); 256 } 257 258 [$base, $type] = $this->getTypedNameFromDir($dir); 259 $user = $this->readLine('Your Name', true); 260 $mail = $this->readLine('Your E-Mail', true); 261 $desc = $this->readLine('Short description'); 262 263 $skeletor = new Skeletor($type, $base, $desc, $user, $mail); 264 $skeletor->addBasics(); 265 $this->createFiles($skeletor->getFiles()); 266 267 try { 268 $this->git('init'); 269 } catch (CliException $e) { 270 $this->error($e->getMessage()); 271 } 272 273 return 0; 274 } 275 276 /** 277 * Add test framework 278 * 279 * @param string $test Name of the Test to add 280 * @return int 281 */ 282 protected function cmdAddTest($test = '') 283 { 284 $skeletor = Skeletor::fromDir(getcwd()); 285 $skeletor->addTest($test); 286 $this->createFiles($skeletor->getFiles()); 287 return 0; 288 } 289 290 /** 291 * Add configuration 292 * 293 * @return int 294 */ 295 protected function cmdAddConf() 296 { 297 $skeletor = Skeletor::fromDir(getcwd()); 298 $skeletor->addConf(is_dir('lang')); 299 $this->createFiles($skeletor->getFiles()); 300 return 0; 301 } 302 303 /** 304 * Add language 305 * 306 * @return int 307 */ 308 protected function cmdAddLang() 309 { 310 $skeletor = Skeletor::fromDir(getcwd()); 311 $skeletor->addLang(is_dir('conf')); 312 $this->createFiles($skeletor->getFiles()); 313 return 0; 314 } 315 316 /** 317 * Add another component to the plugin 318 * 319 * @param string $type 320 * @param string $component 321 */ 322 protected function cmdAddComponent($type, $component = '') 323 { 324 $skeletor = Skeletor::fromDir(getcwd()); 325 $skeletor->addComponent($type, $component); 326 $this->createFiles($skeletor->getFiles()); 327 return 0; 328 } 329 330 /** 331 * Generate a list of deleted files from git 332 * 333 * @link https://stackoverflow.com/a/6018049/172068 334 */ 335 protected function cmdDeletedFiles() 336 { 337 if (!is_dir('.git')) throw new CliException('This extension seems not to be managed by git'); 338 339 $output = $this->git('log', '--no-renames', '--pretty=format:', '--name-only', '--diff-filter=D'); 340 $output = array_map('trim', $output); 341 $output = array_filter($output); 342 $output = array_unique($output); 343 $output = array_filter($output, function ($item) { 344 return !file_exists($item); 345 }); 346 sort($output); 347 348 if (!count($output)) { 349 $this->info('No deleted files found'); 350 return 0; 351 } 352 353 $content = "# This is a list of files that were present in previous releases\n" . 354 "# but were removed later. They should not exist in your installation.\n" . 355 join("\n", $output) . "\n"; 356 357 file_put_contents('deleted.files', $content); 358 $this->success('written deleted.files'); 359 return 0; 360 } 361 362 /** 363 * Remove files that shouldn't be here anymore 364 */ 365 protected function cmdRmObsolete() 366 { 367 $this->deleteFile('_test/general.test.php'); 368 $this->deleteFile('.travis.yml'); 369 370 return 0; 371 } 372 373 /** 374 * Download a remote icon 375 * 376 * @param string $ident 377 * @param string $save 378 * @param bool $keep 379 * @return int 380 * @throws Exception 381 */ 382 protected function cmdDownloadSVG($ident, $save = '', $keep = false) 383 { 384 $svg = new SVGIcon($this); 385 $svg->keepNamespace($keep); 386 return (int)$svg->downloadRemoteIcon($ident, $save); 387 } 388 389 /** 390 * @param string[] $files 391 * @param bool $keep 392 * @return int 393 * @throws Exception 394 */ 395 protected function cmdCleanSVG($files, $keep = false) 396 { 397 $svg = new SVGIcon($this); 398 $svg->keepNamespace($keep); 399 400 $ok = true; 401 foreach ($files as $file) { 402 $ok = $ok && $svg->cleanSVGFile($file); 403 } 404 return (int)$ok; 405 } 406 407 /** 408 * @return int 409 */ 410 protected function cmdCleanLang() 411 { 412 $lp = new LangProcessor($this); 413 414 $files = glob('./lang/*/lang.php'); 415 foreach ($files as $file) { 416 $lp->processLangFile($file); 417 } 418 419 $files = glob('./lang/*/settings.php'); 420 foreach ($files as $file) { 421 $lp->processSettingsFile($file); 422 } 423 424 return 0; 425 } 426 427 //endregion 428} 429