#!/usr/bin/env php getCode(), $ex->getMessage() )); } if (!is_array($data)) { throw new \RuntimeException('Input JSON must decode to an object/array'); } return $data; } function verboseLog(bool $enabled, string $message): void { if ($enabled) fwrite(STDERR, "[verbose] $message".PHP_EOL); } $version = '0.4'; $actions = [ 'show', 'write', 'help' ]; $settings = ['key' => '', 'value' => '']; /* Flags and Argument configuration {{{*/ $collection = (new Input\InputCollection()) ->add( Input\InputTypeFactory::build('LongOption')->name('help')->short('h') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL) ->description('Display help text') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('verbose')->short('v') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL) ->description('Print debug details to STDERR') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('in')->short('i') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) ->description('Path to a JSON data file to read (for write)') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('mode')->short('m') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) ->description('Set a mode') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('appPath')->short('a') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) ->description('Path where the config/ directory for the conf files is located, defaults to the working dir') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('pkgPath')->short('p') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) ->description('Path where the config/ directory of the package conf files is located, defaults to the working dir') ) // }}} ->add( Input\InputTypeFactory::build('LongOption')->name('siteDir') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL | Input\AbstractInputType::FLAG_VALUE_REQUIRED) ->description('Site/instance directory below config/. Accepts owner_xyz or config/owner_xyz') ) // }}} ->add( Input\InputTypeFactory::build('Argument')->name('action') // {{{ ->flags(AbstractInputType::FLAG_REQUIRED) ->description( 'one of:' ."\n\t".'show reads configurations' ."\n\t".'write writes configurations' ) ->validator(new Input\Validator( function (AbstractInputType $input, AbstractInputHandler $context) use ($actions) { $action = $context->find('action'); if ( !in_array($action, $actions) ) { throw new \Exception("'$action' not found"); } return $action; } )) ) // }}} ->add( Input\InputTypeFactory::build('Argument')->name('prefix') // {{{ ->flags(AbstractInputType::FLAG_REQUIRED) ->description( 'the settings prefix' ) /* ->validator(new Input\Validator( function (AbstractInputType $input, AbstractInputHandler $context) use ($actions) { $action = $context->find('action'); if ( !in_array($action, $actions) { throw new \Exception("'$action' not found"); } return $action; } )) */ ) // }}} ->add( Input\InputTypeFactory::build('Argument')->name('setting') // {{{ ->flags(AbstractInputType::FLAG_OPTIONAL) ->description( 'the settings you want to work on. With action "write" you can pass the value to set as JSON after an equal sign.' ) ->validator(new Input\Validator( function (AbstractInputType $input, AbstractInputHandler $context) { $setting = $context->find('setting'); $action = $context->find('action'); if ($setting === null || $setting === '') { return ['key' => '', 'value' => null]; } $setting = explode('=', $setting); $settings['key'] = $setting[0]; if ($action === 'write') { $value = $setting[1] ?? null; if (! (isset($value) || $context->find('in'))) { throw new \Exception('You need a value to write'); } $specialValues = [ 'true', 'false', 'null' ]; if (! (in_array($value, $specialValues) || is_numeric($value) || strpbrk($value, '[{":') !== false )) { $value = "\"$value\""; } try { $settings['value'] = json_decode($value, true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException $ex) { throw new \Exception( sprintf( 'The value does not appear to be a valid JSON string. Returned: %s: %s', $ex->getCode(), $ex->getMessage() )); } } return $settings; } )) ) // }}} ; // }}} /* Parse Input, usage, unkown flags, Help {{{*/ $usage = Cli\manpage( basename(__FILE__), $version, 'read write settings', $collection, Colour::FG_GREEN, Colour::FG_WHITE, [ 'Examples' => 'Basic usage:' .PHP_EOL .'cfg show VeruA db:host' .PHP_EOL .'cfg write VeruA \'db:host="newHost"\'' .PHP_EOL .PHP_EOL .'Advance usage:' .PHP_EOL .'cfg write extension_name \'module:enabled=true\'' .PHP_EOL .PHP_EOL .'#Instance write:' .PHP_EOL .'cfg write extension_name \'module:enabled=true\' --siteDir=owner_xyz' .PHP_EOL .PHP_EOL .'#Batch write from JSON file:' .PHP_EOL .'cfg write extension_name --siteDir=owner_xyz -i /tmp/extension.json' .PHP_EOL .PHP_EOL .'#Batch write from JSON string:' .PHP_EOL .'cfg write extension_name \'module={"enabled":true,"timeout":30,"label":"example"}\' --siteDir=owner_xyz' .PHP_EOL .PHP_EOL .'#Read merged value for an instance:' .PHP_EOL .'cfg show extension_name module:enabled --siteDir=owner_xyz' .PHP_EOL ] ).PHP_EOL; // Get the supplied input. Passing the collection will make the handler bind values // and validate the input according to our collection try { $argv = Input\InputHandlerFactory::build('Argv', $collection); } catch (\Exception $ex) { echo $usage; if (isset($argv[1])) { if ($argv[1] == '-h' || $argv[1] == '--help' || $argv[1] == 'help') { exit(0); } } fwrite(STDERR, $ex->getMessage().PHP_EOL); exit(1); } // show help if ($argv->find( 'help' ) || $argv->find('action') == 'help') { echo $usage; exit(0); } //}}} $prefix = $argv->find('prefix'); $verbose = (bool)$argv->find('verbose'); $appPath = $argv->find('appPath'); if (!$appPath) $appPath = getcwd().'/'; $appPath = rtrim($appPath, '/').'/'; /* $it = new RecursiveDirectoryIterator($appPath); foreach(new RecursiveIteratorIterator($it) as $file) { $configDir = $file->getPath(); if ($file->isDir() && $file->getFilename() == '.' && basename($configDir) == 'config') { echo "found config dir: $configDir\n"; } } */ $mode = ($argv->find('mode') == '') ? null : $argv->find('mode'); $cfg = (new Settings([], $mode))->appPath($appPath)->prefix($prefix); // pkgPath points to package defaults (e.g. .default.conf.php) if ($pkgPath = $argv->find('pkgPath')) $cfg->pkgPath(rtrim($pkgPath, '/').'/'); $site = null; $siteFlag = 0x01; $siteInput = $argv->find('siteDir'); if ($siteInput !== null && $siteInput !== false) { $siteInput = trim((string)$siteInput); // Accept both "owner_xyz" and "config/owner_xyz" and normalize to site key. if (str_starts_with($siteInput, $appPath.'config/')) { $siteInput = substr($siteInput, strlen($appPath.'config/')); } elseif (str_starts_with($siteInput, 'config/')) { $siteInput = substr($siteInput, strlen('config/')); } $siteInput = trim($siteInput, '/'); if ($siteInput === '') { fwrite(STDERR, 'Option --siteDir is empty.'.PHP_EOL); exit(1); } // Block directory traversal and hidden-dot segments. if (str_contains($siteInput, '..') || preg_match('~(^|/)\.(?:/|$)~', $siteInput)) { fwrite(STDERR, "Invalid directory in --siteDir: '$siteInput'.".PHP_EOL); exit(1); } // Allow only predictable path segments for instance directories. foreach (explode('/', $siteInput) as $part) { if ($part === '' || !preg_match('/^[A-Za-z0-9._-]+$/', $part)) { fwrite(STDERR, "Invalid directory name segment '$part' in --siteDir.".PHP_EOL); exit(1); } } $site = $siteInput; // Reuse util-settings site resolution: config//.conf.php $cfg->site($site); verboseLog($verbose, "siteDir resolved to '$site'"); } try { if (is_readable($cfg->buildFileName('default'))) { $cfg->load(); } elseif (is_readable($cfgFile = $cfg->buildFileName())) { $cfg->load(require($cfgFile)); } } catch (\Throwable $e) { fwrite(STDERR, "Error: ".$e->getMessage().PHP_EOL); exit(1); } verboseLog($verbose, 'config bootstrap loaded'); $result = $cfg; $settings = $argv->find('setting') ?? $settings; if ($settings['key']) { foreach (explode(':', $settings['key']) as $setting) { try { $result = $result->{$setting}; } catch (\OutOfRangeException $e) { fwrite(STDERR, $e->getMessage().PHP_EOL); exit(1); } } } else $result = $cfg; if ($result instanceof Settings) $result = $result->toArray(); switch ($argv->find('action')) { case 'show': verboseLog($verbose, "show action for prefix '$prefix'"); $out = (is_string($result)) ? $result : json_encode($result, JSON_PRETTY_PRINT); echo $out.PHP_EOL; break; case 'write': $inputFile = $argv->find('in'); // Write needs exactly one payload source: // either SETTING (key=value) or --in (JSON file). // This avoids ambiguous input precedence and empty writes. if ($inputFile && $settings['key'] !== '') { fwrite(STDERR, 'Please use either SETTING or --in, not both.'.PHP_EOL); exit(1); } if (!$inputFile && $settings['key'] === '') { fwrite(STDERR, 'Nothing to write: provide SETTING or --in.'.PHP_EOL); exit(1); } $path = ($settings['key'] !== '') ? explode(':', $settings['key']) : []; verboseLog($verbose, 'write source: '.($inputFile ? "--in ($inputFile)" : 'SETTING')); if ($inputFile) { try { $setting2write = readJsonInputFile($inputFile); } catch (\Throwable $e) { fwrite(STDERR, $e->getMessage().PHP_EOL); exit(1); } } else { $setting2write = $settings['value']; } verboseLog($verbose, 'write path: '.json_encode($path)); while ( ! empty($path)) { $setting2write = [array_pop($path) => $setting2write]; } $writeType = ($site !== null) ? $siteFlag : null; $file = $cfg->buildFileName($writeType); verboseLog($verbose, "write target: $file"); if (is_readable($file)) { $setting2write = array_replace_recursive(require($file), $setting2write); copy($file, "$file.bak"); verboseLog($verbose, "existing config merged from: $file"); } $targetDir = dirname($file); if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { fwrite(STDERR, "Can not create directory: $targetDir".PHP_EOL); exit(1); } $writeCfg = $cfg->create($setting2write); verboseLog($verbose, 'payload prepared for writer'); try { (new SettingsWriter($writeCfg, '', $writeType))->write(); echo "Written modified settings to: $file".PHP_EOL; } catch (\Exception $e) { fwrite(STDERR, $e->getMessage().PHP_EOL); exit(1); } break; } /* jEdit buffer local properties {{{ * :folding=explicit:collapseFolds=1: }}}*/