Command Line Parser Concept (#6099)

* Adds logic for validation

* Adds validation to do-install

* Adds help to do-install

* Adds validation & help to reconfigure

* Adds validation to check.translation

* Adds validation to manipulate.translation

* Small fixes to help texts

* Refactors language option validation

* Adds default options to validation

* Fixes validation with regex

* Refactors readAs functions

* Updates to new regex validation format

* Fixes typing around default values

* Adds file extension validation

* Restandardises validation & parsing typing around array of strings

* Adds NotOneOf validation

* Adds ArrayOfString read as

* Refactors existing validation

* Adds validation throughout cli

* Removes unused file

* Adds new CL parser with goal of wrapping CLI behaviour

* Hides parsing and validation

* Rewites CL parser to make better use of classes

* Rolls out new parser across CL

* Fixes error during unknown option check

* Fixes misnamed property calls

* Seperates validations into more appropriate locations

* Adds common boolean forms to validation

* Moves CommandLineParser and Option classes into their own files

* Fixes error when validating Int type

* Rewrites appendTypedValues -> appendTypedValidValues now filters invalid values from output

* Renames  ->  for clarity

* Adds some docs clarifying option defaults and value taking behaviour

* Refactors getUsageMessage for readability

* Minor formatting changes

* Adds tests for CommandLineParser

* Adds more tests

* Adds minor fixs

* Reconfigure now correctly updates config

* More fixes to reconfigure

* Fixes required files for CommandLineParserTest

* Use .php extension for PHP file

* PHPStan ignore instead of wrong typing

* Refactors to support php 7.4

* Moves away from dynamic properties by adding 'Definintions' to all commands

* Renames target to definition for clarity

* Stops null from being returned as a valid value in a certain edge case

* Adds PHPStan ignore instead of incorrect typing

* Refactors tests to take account of new typing solution

* Marks file as executable

* Draft CLI rework

* Finish rewrite as object-oriented

* Fix PHPStan ignore and make more strongly typed

* Rename class Option to CliOption

* Light renaming + anonymous classes

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
This commit is contained in:
Kasimir Cash 2024-02-28 12:23:28 +00:00 committed by GitHub
parent 5de794ee0f
commit 4b29e666b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1153 additions and 620 deletions

104
cli/CliOption.php Normal file
View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
final class CliOption {
public const VALUE_NONE = 'none';
public const VALUE_REQUIRED = 'required';
public const VALUE_OPTIONAL = 'optional';
private string $longAlias;
private ?string $shortAlias;
private string $valueTaken = self::VALUE_REQUIRED;
/** @var array{type:string,isArray:bool} $types */
private array $types = ['type' => 'string', 'isArray' => false];
private string $optionalValueDefault = '';
private ?string $deprecatedAlias = null;
public function __construct(string $longAlias, ?string $shortAlias = null) {
$this->longAlias = $longAlias;
$this->shortAlias = $shortAlias;
}
/** Sets this option to be treated as a flag. */
public function withValueNone(): self {
$this->valueTaken = static::VALUE_NONE;
return $this;
}
/** Sets this option to always require a value when used. */
public function withValueRequired(): self {
$this->valueTaken = static::VALUE_REQUIRED;
return $this;
}
/**
* Sets this option to accept both values and flag behavior.
* @param string $optionalValueDefault When this option is used as a flag it receives this value as input.
*/
public function withValueOptional(string $optionalValueDefault = ''): self {
$this->valueTaken = static::VALUE_OPTIONAL;
$this->optionalValueDefault = $optionalValueDefault;
return $this;
}
public function typeOfString(): self {
$this->types = ['type' => 'string', 'isArray' => false];
return $this;
}
public function typeOfInt(): self {
$this->types = ['type' => 'int', 'isArray' => false];
return $this;
}
public function typeOfBool(): self {
$this->types = ['type' => 'bool', 'isArray' => false];
return $this;
}
public function typeOfArrayOfString(): self {
$this->types = ['type' => 'string', 'isArray' => true];
return $this;
}
public function deprecatedAs(string $deprecated): self {
$this->deprecatedAlias = $deprecated;
return $this;
}
public function getValueTaken(): string {
return $this->valueTaken;
}
public function getOptionalValueDefault(): string {
return $this->optionalValueDefault;
}
public function getDeprecatedAlias(): ?string {
return $this->deprecatedAlias;
}
public function getLongAlias(): string {
return $this->longAlias;
}
public function getShortAlias(): ?string {
return $this->shortAlias;
}
/** @return array{type:string,isArray:bool} */
public function getTypes(): array {
return $this->types;
}
/** @return string[] */
public function getAliases(): array {
$aliases = [
$this->longAlias,
$this->shortAlias,
$this->deprecatedAlias,
];
return array_filter($aliases);
}
}

247
cli/CliOptionsParser.php Normal file
View File

@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
abstract class CliOptionsParser {
/** @var array<string,CliOption> */
private array $options = [];
/** @var array<string,array{defaultInput:?string[],required:?bool,aliasUsed:?string,values:?string[]}> */
private array $inputs = [];
/** @var array<string,string> $errors */
public array $errors = [];
public string $usage = '';
public function __construct() {
global $argv;
$this->usage = $this->getUsageMessage($argv[0]);
$this->parseInput();
$this->appendUnknownAliases($argv);
$this->appendInvalidValues();
$this->appendTypedValidValues();
}
private function parseInput(): void {
$getoptInputs = $this->getGetoptInputs();
$this->getoptOutputTransformer(getopt($getoptInputs['short'], $getoptInputs['long']));
$this->checkForDeprecatedAliasUse();
}
/** Adds an option that produces an error message if not set. */
protected function addRequiredOption(string $name, CliOption $option): void {
$this->inputs[$name] = [
'defaultInput' => null,
'required' => true,
'aliasUsed' => null,
'values' => null,
];
$this->options[$name] = $option;
}
/**
* Adds an optional option.
* @param string $defaultInput If not null this value is received as input in all cases where no
* user input is present. e.g. set this if you want an option to always return a value.
*/
protected function addOption(string $name, CliOption $option, string $defaultInput = null): void {
$this->inputs[$name] = [
'defaultInput' => is_string($defaultInput) ? [$defaultInput] : $defaultInput,
'required' => null,
'aliasUsed' => null,
'values' => null,
];
$this->options[$name] = $option;
}
private function appendInvalidValues(): void {
foreach ($this->options as $name => $option) {
if ($this->inputs[$name]['required'] && $this->inputs[$name]['values'] === null) {
$this->errors[$name] = 'invalid input: ' . $option->getLongAlias() . ' cannot be empty';
}
}
foreach ($this->inputs as $name => $input) {
foreach ($input['values'] ?? $input['defaultInput'] ?? [] as $value) {
switch ($this->options[$name]->getTypes()['type']) {
case 'int':
if (!ctype_digit($value)) {
$this->errors[$name] = 'invalid input: ' . $input['aliasUsed'] . ' must be an integer';
}
break;
case 'bool':
if (filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) === null) {
$this->errors[$name] = 'invalid input: ' . $input['aliasUsed'] . ' must be a boolean';
}
break;
}
}
}
}
private function appendTypedValidValues(): void {
foreach ($this->inputs as $name => $input) {
$values = $input['values'] ?? $input['defaultInput'] ?? null;
$types = $this->options[$name]->getTypes();
if ($values) {
$validValues = [];
$typedValues = [];
switch ($types['type']) {
case 'string':
$typedValues = $values;
break;
case 'int':
$validValues = array_filter($values, static fn($value) => ctype_digit($value));
$typedValues = array_map(static fn($value) => (int) $value, $validValues);
break;
case 'bool':
$validValues = array_filter($values, static fn($value) => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null);
$typedValues = array_map(static fn($value) => (bool) filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE), $validValues);
break;
}
if (!empty($typedValues)) {
// @phpstan-ignore-next-line (change to `@phpstan-ignore property.dynamicName` when upgrading to PHPStan 1.11+)
$this->$name = $types['isArray'] ? $typedValues : array_pop($typedValues);
}
}
}
}
/** @param array<string,string|false>|false $getoptOutput */
private function getoptOutputTransformer($getoptOutput): void {
$getoptOutput = is_array($getoptOutput) ? $getoptOutput : [];
foreach ($getoptOutput as $alias => $value) {
foreach ($this->options as $name => $data) {
if (in_array($alias, $data->getAliases(), true)) {
$this->inputs[$name]['aliasUsed'] = $alias;
$this->inputs[$name]['values'] = $value === false
? [$data->getOptionalValueDefault()]
: (is_array($value)
? $value
: [$value]);
}
}
}
}
/**
* @param array<string> $userInputs
* @return array<string>
*/
private function getAliasesUsed(array $userInputs, string $regex): array {
$foundAliases = [];
foreach ($userInputs as $input) {
preg_match($regex, $input, $matches);
if(!empty($matches['short'])) {
$foundAliases = array_merge($foundAliases, str_split($matches['short']));
}
if(!empty($matches['long'])) {
$foundAliases[] = $matches['long'];
}
}
return $foundAliases;
}
/**
* @param array<string> $input List of user command-line inputs.
*/
private function appendUnknownAliases(array $input): void {
$valid = [];
foreach ($this->options as $option) {
$valid = array_merge($valid, $option->getAliases());
}
$sanitizeInput = $this->getAliasesUsed($input, $this->makeInputRegex());
$unknownAliases = array_diff($sanitizeInput, $valid);
if (empty($unknownAliases)) {
return;
}
foreach ($unknownAliases as $unknownAlias) {
$this->errors[$unknownAlias] = 'unknown option: ' . $unknownAlias;
}
}
/**
* Checks for presence of deprecated aliases.
* @return bool Returns TRUE and generates a deprecation warning if deprecated aliases are present, FALSE otherwise.
*/
private function checkForDeprecatedAliasUse(): bool {
$deprecated = [];
$replacements = [];
foreach ($this->inputs as $name => $data) {
if ($data['aliasUsed'] !== null && $data['aliasUsed'] === $this->options[$name]->getDeprecatedAlias()) {
$deprecated[] = $this->options[$name]->getDeprecatedAlias();
$replacements[] = $this->options[$name]->getLongAlias();
}
}
if (empty($deprecated)) {
return false;
}
fwrite(STDERR, "FreshRSS deprecation warning: the CLI option(s): " . implode(', ', $deprecated) .
" are deprecated and will be removed in a future release. Use: " . implode(', ', $replacements) .
" instead\n");
return true;
}
/** @return array{long:array<string>,short:string}*/
private function getGetoptInputs(): array {
$getoptNotation = [
'none' => '',
'required' => ':',
'optional' => '::',
];
$long = [];
$short = '';
foreach ($this->options as $option) {
$long[] = $option->getLongAlias() . $getoptNotation[$option->getValueTaken()];
$long[] = $option->getDeprecatedAlias() ? $option->getDeprecatedAlias() . $getoptNotation[$option->getValueTaken()] : '';
$short .= $option->getShortAlias() ? $option->getShortAlias() . $getoptNotation[$option->getValueTaken()] : '';
}
return [
'long' => array_filter($long),
'short' => $short
];
}
private function getUsageMessage(string $command): string {
$required = ['Usage: ' . basename($command)];
$optional = [];
foreach ($this->options as $name => $option) {
$shortAlias = $option->getShortAlias() ? '-' . $option->getShortAlias() . ' ' : '';
$longAlias = '--' . $option->getLongAlias() . ($option->getValueTaken() === 'required' ? '=<' . strtolower($name) . '>' : '');
if ($this->inputs[$name]['required']) {
$required[] = $shortAlias . $longAlias;
} else {
$optional[] = '[' . $shortAlias . $longAlias . ']';
}
}
return implode(' ', $required) . ' ' . implode(' ', $optional);
}
private function makeInputRegex() : string {
$shortWithValues = '';
foreach ($this->options as $option) {
if (($option->getValueTaken() === 'required' || $option->getValueTaken() === 'optional') && $option->getShortAlias()) {
$shortWithValues .= $option->getShortAlias();
}
}
return $shortWithValues === ''
? "/^--(?'long'[^=]+)|^-(?<short>\w+)/"
: "/^--(?'long'[^=]+)|^-(?<short>(?(?=\w*[$shortWithValues])[^$shortWithValues]*[$shortWithValues]|\w+))/";
}
}

119
cli/_cli.php Normal file → Executable file
View File

@ -6,11 +6,12 @@ if (php_sapi_name() !== 'cli') {
}
const EXIT_CODE_ALREADY_EXISTS = 3;
const REGEX_INPUT_OPTIONS = '/^-{2}|^-{1}/';
require(__DIR__ . '/../constants.php');
require(LIB_PATH . '/lib_rss.php'); //Includes class autoloader
require(LIB_PATH . '/lib_install.php');
require_once(__DIR__ . '/CliOption.php');
require_once(__DIR__ . '/CliOptionsParser.php');
Minz_Session::init('FreshRSS', true);
FreshRSS_Context::initSystem();
@ -73,119 +74,3 @@ function performRequirementCheck(string $databaseType): void {
fail($message);
}
}
/**
* Parses parameters used with FreshRSS' CLI commands.
* @param array{'long':array<string,string>,'short':array<string,string>,'deprecated':array<string,string>} $parameters
* Matrix of 'long': map of long option names as keys and their respective getopt() notations as values,
* 'short': map of short option names as values and their equivalent long options as keys, 'deprecated': map of
* replacement option names as keys and their respective deprecated option names as values.
* @return array{'valid':array<string,string>,'invalid':array<string>} Matrix of 'valid': map of of all known
* option names used and their respective values and 'invalid': list of all unknown options used.
*/
function parseCliParams(array $parameters): array {
global $argv;
$longOptions = [];
$shortOptions = '';
foreach ($parameters['long'] as $name => $getopt_note) {
$longOptions[] = $name . $getopt_note;
}
foreach ($parameters['deprecated'] as $name => $deprecatedName) {
$longOptions[] = $deprecatedName . $parameters['long'][$name];
}
foreach ($parameters['short'] as $name => $shortName) {
$shortOptions .= $shortName . $parameters['long'][$name];
}
$options = getopt($shortOptions, $longOptions);
$valid = is_array($options) ? $options : [];
array_walk($valid, static fn(&$option) => $option = $option === false ? '' : $option);
/** @var array<string,string> $valid */
checkForDeprecatedOptions(array_keys($valid), $parameters['deprecated']);
$valid = replaceOptions($valid, $parameters['short']);
$valid = replaceOptions($valid, $parameters['deprecated']);
$invalid = findInvalidOptions(
$argv,
array_merge(array_keys($parameters['long']), array_values($parameters['short']), array_values($parameters['deprecated']))
);
return [
'valid' => $valid,
'invalid' => $invalid
];
}
/**
* @param array<string> $options
* @return array<string>
*/
function getOptions(array $options, string $regex): array {
$longOptions = array_filter($options, static function (string $a) use ($regex) {
return preg_match($regex, $a) === 1;
});
return array_map(static function (string $a) use ($regex) {
return preg_replace($regex, '', $a) ?? '';
}, $longOptions);
}
/**
* Checks for presence of unknown options.
* @param array<string> $input List of command line arguments to check for validity.
* @param array<string> $params List of valid options to check against.
* @return array<string> Returns a list all unknown options found.
*/
function findInvalidOptions(array $input, array $params): array {
$sanitizeInput = getOptions($input, REGEX_INPUT_OPTIONS);
$unknownOptions = array_diff($sanitizeInput, $params);
if (0 === count($unknownOptions)) {
return [];
}
fwrite(STDERR, sprintf("FreshRSS error: unknown options: %s\n", implode (', ', $unknownOptions)));
return $unknownOptions;
}
/**
* Checks for presence of deprecated options.
* @param array<string> $optionNames Command line option names to check for deprecation.
* @param array<string,string> $params Map of replacement options as keys and their respective deprecated
* options as values.
* @return bool Returns TRUE and generates a deprecation warning if deprecated options are present, FALSE otherwise.
*/
function checkForDeprecatedOptions(array $optionNames, array $params): bool {
$deprecatedOptions = array_intersect($optionNames, $params);
$replacements = array_map(static fn($option) => array_search($option, $params, true), $deprecatedOptions);
if (0 === count($deprecatedOptions)) {
return false;
}
fwrite(STDERR, "FreshRSS deprecation warning: the CLI option(s): " . implode(', ', $deprecatedOptions) .
" are deprecated and will be removed in a future release. Use: "
. implode(', ', $replacements) . " instead\n");
return true;
}
/**
* Switches items in a list to their provided replacements.
* @param array<string,string> $options Map with items to check for replacement as keys.
* @param array<string,string> $replacements Map of replacement items as keys and the item they replace as their values.
* @return array<string,string> Returns $options with replacements.
*/
function replaceOptions(array $options, array $replacements): array {
$updatedOptions = [];
foreach ($options as $name => $value) {
$replacement = array_search($name, $replacements, true);
$updatedOptions[$replacement ? $replacement : $name] = $value;
}
return $updatedOptions;
}

View File

@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
'password' => ':',
'api-password' => ':',
'language' => ':',
'email' => ':',
'token' => ':',
'purge-after-months' => ':',
'feed-min-articles-default' => ':',
'feed-ttl-default' => ':',
'since-hours-posts-per-rss' => ':',
'max-posts-per-rss' => ':',
],
'short' => [],
'deprecated' => [
'api-password' => 'api_password',
'purge-after-months' => 'purge_after_months',
'feed-min-articles-default' => 'feed_min_articles_default',
'feed-ttl-default' => 'feed_ttl_default',
'since-hours-posts-per-rss' => 'since_hours_posts_per_rss',
'max-posts-per-rss' => 'max_posts_per_rss',
],
];
if (!isset($isUpdate)) {
$isUpdate = false;
} elseif (!$isUpdate) {
$parameters['long']['no-default-feeds'] = ''; //Only for creating new users
$parameters['deprecated']['no-default-feeds'] = 'no_default_feeds';
}
$GLOBALS['options'] = parseCliParams($parameters);
if (!empty($options['invalid']) || empty($options['valid']['user'])) {
fail('Usage: ' . basename($_SERVER['SCRIPT_FILENAME']) .
" --user username ( --password 'password' --api-password 'api_password'" .
" --language en --email user@example.net --token 'longRandomString'" .
($isUpdate ? '' : ' --no-default-feeds') .
" --purge-after-months 3 --feed-min-articles-default 50 --feed-ttl-default 3600" .
" --since-hours-posts-per-rss 168 --max-posts-per-rss 400 )");
}
function strParam(string $name): ?string {
global $options;
return isset($options['valid'][$name]) ? strval($options['valid'][$name]) : null;
}
function intParam(string $name): ?int {
global $options;
return isset($options['valid'][$name]) && ctype_digit($options['valid'][$name]) ? intval($options['valid'][$name]) : null;
}
$values = array(
'language' => strParam('language'),
'mail_login' => strParam('email'),
'token' => strParam('token'),
'old_entries' => intParam('purge-after-months'), //TODO: Update with new mechanism
'keep_history_default' => intParam('feed-min-articles-default'), //TODO: Update with new mechanism
'ttl_default' => intParam('feed-ttl-default'),
'since_hours_posts_per_rss' => intParam('since-hours-posts-per-rss'),
'max_posts_per_rss' => intParam('max-posts-per-rss'),
);
$values = array_filter($values);

View File

@ -5,21 +5,23 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':'
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
parent::__construct();
}
};
if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
fail('Usage: ' . basename(__FILE__) . " --user username");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$username = cliInitUser($cliOptions->user);
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
fwrite(STDERR, 'FreshRSS actualizing user “' . $username . "”…\n");
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();

View File

@ -8,38 +8,39 @@ require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/i18n/I18nUsageValidator.php';
require_once __DIR__ . '/../constants.php';
$i18nFile = new I18nFile();
$i18nData = new I18nData($i18nFile->load());
$cliOptions = new class extends CliOptionsParser {
/** @var array<int,string> $language */
public array $language;
public string $displayResult;
public string $help;
public string $displayReport;
$parameters = [
'long' => [
'display-result' => '',
'help' => '',
'language' => ':',
'display-report' => '',
],
'short' => [
'display-result' => 'd',
'help' => 'h',
'language' => 'l',
'display-report' => 'r',
],
'deprecated' => [],
];
public function __construct() {
$this->addOption('language', (new CliOption('language', 'l'))->typeOfArrayOfString());
$this->addOption('displayResult', (new CliOption('display-result', 'd'))->withValueNone());
$this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
$this->addOption('displayReport', (new CliOption('display-report', 'r'))->withValueNone());
parent::__construct();
}
};
$options = parseCliParams($parameters);
if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (isset($cliOptions->help)) {
checkHelp();
}
if (array_key_exists('language', $options['valid'])) {
$languages = [$options['valid']['language']];
$i18nFile = new I18nFile();
$i18nData = new I18nData($i18nFile->load());
if (isset($cliOptions->language)) {
$languages = $cliOptions->language;
} else {
$languages = $i18nData->getAvailableLanguages();
}
$displayResults = array_key_exists('display-result', $options['valid']);
$displayReport = array_key_exists('display-report', $options['valid']);
$displayResults = isset($cliOptions->displayResult);
$displayReport = isset($cliOptions->displayReport);
$isValidated = true;
$result = [];
@ -122,5 +123,5 @@ DESCRIPTION
-r, --display-report display completion report.
HELP;
exit;
exit();
}

View File

@ -1,38 +1,93 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$isUpdate = false;
require(__DIR__ . '/_update-or-create-user.php');
$cliOptions = new class extends CliOptionsParser {
public string $user;
public string $password;
public string $apiPassword;
public string $language;
public string $email;
public string $token;
public int $purgeAfterMonths;
public int $feedMinArticles;
public int $feedTtl;
public int $sinceHoursPostsPerRss;
public int $maxPostsPerRss;
public bool $noDefaultFeeds;
$username = $GLOBALS['options']['valid']['user'];
if (!FreshRSS_user_Controller::checkUsername($username)) {
fail('FreshRSS error: invalid username “' . $username .
'”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addOption('password', (new CliOption('password')));
$this->addOption('apiPassword', (new CliOption('api-password'))->deprecatedAs('api_password'));
$this->addOption('language', (new CliOption('language')));
$this->addOption('email', (new CliOption('email')));
$this->addOption('token', (new CliOption('token')));
$this->addOption(
'purgeAfterMonths',
(new CliOption('purge-after-months'))->typeOfInt()->deprecatedAs('purge_after_months')
);
$this->addOption(
'feedMinArticles',
(new CliOption('feed-min-articles-default'))->typeOfInt()->deprecatedAs('feed_min_articles_default')
);
$this->addOption(
'feedTtl',
(new CliOption('feed-ttl-default'))->typeOfInt()->deprecatedAs('feed_ttl_default')
);
$this->addOption(
'sinceHoursPostsPerRss',
(new CliOption('since-hours-posts-per-rss'))->typeOfInt()->deprecatedAs('since_hours_posts_per_rss')
);
$this->addOption(
'maxPostsPerRss',
(new CliOption('max-posts-per-rss'))->typeOfInt()->deprecatedAs('max_posts_per_rss')
);
$this->addOption(
'noDefaultFeeds',
(new CliOption('no-default-feeds'))->withValueNone()->deprecatedAs('no_default_feeds')
);
parent::__construct();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$usernames = listUsers();
if (preg_grep("/^$username$/i", $usernames)) {
fail('FreshRSS warning: username already exists “' . $username . '”', EXIT_CODE_ALREADY_EXISTS);
}
$username = $cliOptions->user;
echo 'FreshRSS creating user “', $username, "”…\n";
$values = [
'language' => $cliOptions->language ?? null,
'mail_login' => $cliOptions->email ?? null,
'token' => $cliOptions->token ?? null,
'old_entries' => $cliOptions->purgeAfterMonths ?? null,
'keep_history_default' => $cliOptions->feedMinArticles ?? null,
'ttl_default' => $cliOptions->feedTtl ?? null,
'since_hours_posts_per_rss' => $cliOptions->sinceHoursPostsPerRss ?? null,
'max_posts_per_rss' => $cliOptions->maxPostsPerRss ?? null,
];
$values = array_filter($values);
$ok = FreshRSS_user_Controller::createUser(
$username,
empty($options['valid']['email']) ? '' : $options['valid']['email'],
empty($options['valid']['password']) ? '' : $options['valid']['password'],
$GLOBALS['values'],
!isset($options['valid']['no-default-feeds'])
isset($cliOptions->email) ? $cliOptions->email : null,
$cliOptions->password ?? '',
$values,
!isset($cliOptions->noDefaultFeeds)
);
if (!$ok) {
fail('FreshRSS could not create user!');
}
if (!empty($options['valid']['api-password'])) {
if (isset($cliOptions->apiPassword)) {
$username = cliInitUser($username);
$error = FreshRSS_api_Controller::updatePassword($options['valid']['api-password']);
$error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error !== false) {
fail($error);
}

View File

@ -5,21 +5,20 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
parent::__construct();
}
};
if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
fail('Usage: ' . basename(__FILE__) . " --user username");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$username = cliInitUser($cliOptions->user);
echo 'FreshRSS optimizing database for user “', $username, "”…\n";

View File

@ -5,29 +5,27 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
parent::__construct();
}
};
if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
fail('Usage: ' . basename(__FILE__) . " --user username");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = $options['valid']['user'];
$username = $cliOptions->user;
if (!FreshRSS_user_Controller::checkUsername($username)) {
fail('FreshRSS error: invalid username “' . $username . '”');
fail('FreshRSS error: invalid username: ' . $username . "\n");
}
$usernames = listUsers();
if (!preg_grep("/^$username$/i", $usernames)) {
fail('FreshRSS error: username not found “' . $username . '”');
if (!FreshRSS_user_Controller::userExists($username)) {
fail('FreshRSS error: user not found: ' . $username . "\n");
}
if (strcasecmp($username, FreshRSS_Context::systemConf()->default_user) === 0) {
fail('FreshRSS error: default user must not be deleted: “' . $username . '”');
}

View File

@ -7,74 +7,91 @@ if (file_exists(DATA_PATH . '/applied_migrations.txt')) {
fail('FreshRSS seems to be already installed!' . "\n" . 'Please use `./cli/reconfigure.php` instead.', EXIT_CODE_ALREADY_EXISTS);
}
$parameters = [
'long' => [
'environment' => ':',
'base-url' => ':',
'language' => ':',
'title' => ':',
'default-user' => ':',
'allow-anonymous' => '',
'allow-anonymous-refresh' => '',
'auth-type' => ':',
'api-enabled' => '',
'allow-robots' => '',
'disable-update' => '',
'db-type' => ':',
'db-host' => ':',
'db-user' => ':',
'db-password' => ':',
'db-base' => ':',
'db-prefix' => '::',
],
'short' => [],
'deprecated' => [
'base-url' => 'base_url',
'default-user' => 'default_user',
'allow-anonymous' => 'allow_anonymous',
'allow-anonymous-refresh' => 'allow_anonymous_refresh',
'auth-type' => 'auth_type',
'api-enabled' => 'api_enabled',
'allow-robots' => 'allow_robots',
'disable-update' => 'disable_update',
],
];
$cliOptions = new class extends CliOptionsParser {
public string $defaultUser;
public string $environment;
public string $baseUrl;
public string $language;
public string $title;
public bool $allowAnonymous;
public bool $allowAnonymousRefresh;
public string $authType;
public bool $apiEnabled;
public bool $allowRobots;
public bool $disableUpdate;
public string $dbType;
public string $dbHost;
public string $dbUser;
public string $dbPassword;
public string $dbBase;
public string $dbPrefix;
$configParams = [
'environment' => 'environment',
'base-url' => 'base_url',
'language' => 'language',
'title' => 'title',
'default-user' => 'default_user',
'allow-anonymous' => 'allow_anonymous',
'allow-anonymous-refresh' => 'allow_anonymous_refresh',
'auth-type' => 'auth_type',
'api-enabled' => 'api_enabled',
'allow-robots' => 'allow_robots',
'disable-update' => 'disable_update',
];
public function __construct() {
$this->addRequiredOption('defaultUser', (new CliOption('default-user'))->deprecatedAs('default_user'));
$this->addOption('environment', (new CliOption('environment')));
$this->addOption('baseUrl', (new CliOption('base-url'))->deprecatedAs('base_url'));
$this->addOption('language', (new CliOption('language')));
$this->addOption('title', (new CliOption('title')));
$this->addOption(
'allowAnonymous',
(new CliOption('allow-anonymous'))->withValueOptional('true')->deprecatedAs('allow_anonymous')->typeOfBool()
);
$this->addOption(
'allowAnonymousRefresh',
(new CliOption('allow-anonymous-refresh'))->withValueOptional('true')->deprecatedAs('allow_anonymous_refresh')->typeOfBool()
);
$this->addOption('authType', (new CliOption('auth-type'))->deprecatedAs('auth_type'));
$this->addOption(
'apiEnabled',
(new CliOption('api-enabled'))->withValueOptional('true')->deprecatedAs('api_enabled')->typeOfBool()
);
$this->addOption(
'allowRobots',
(new CliOption('allow-robots'))->withValueOptional('true')->deprecatedAs('allow_robots')->typeOfBool()
);
$this->addOption(
'disableUpdate',
(new CliOption('disable-update'))->withValueOptional('true')->deprecatedAs('disable_update')->typeOfBool()
);
$this->addOption('dbType', (new CliOption('db-type')));
$this->addOption('dbHost', (new CliOption('db-host')));
$this->addOption('dbUser', (new CliOption('db-user')));
$this->addOption('dbPassword', (new CliOption('db-password')));
$this->addOption('dbBase', (new CliOption('db-base')));
$this->addOption('dbPrefix', (new CliOption('db-prefix'))->withValueOptional());
parent::__construct();
}
};
$dBconfigParams = [
'db-type' => 'type',
'db-host' => 'host',
'db-user' => 'user',
'db-password' => 'password',
'db-base' => 'base',
'db-prefix' => 'prefix',
];
$options = parseCliParams($parameters);
if (!empty($options['invalid']) || empty($options['valid']['default-user']) || !is_string($options['valid']['default-user'])) {
fail('Usage: ' . basename(__FILE__) . " --default-user admin ( --auth-type form" .
" --environment production --base-url https://rss.example.net --allow-robots" .
" --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled" .
" --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
" --db-base freshrss --db-prefix freshrss_ --disable-update )");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
fwrite(STDERR, 'FreshRSS install…' . "\n");
$values = [
'default_user' => $cliOptions->defaultUser ?? null,
'environment' => $cliOptions->environment ?? null,
'base_url' => $cliOptions->baseUrl ?? null,
'language' => $cliOptions->language ?? null,
'title' => $cliOptions->title ?? null,
'allow_anonymous' => $cliOptions->allowAnonymous ?? null,
'allow_anonymous_refresh' => $cliOptions->allowAnonymousRefresh ?? null,
'auth_type' => $cliOptions->authType ?? null,
'api_enabled' => $cliOptions->apiEnabled ?? null,
'allow_robots' => $cliOptions->allowRobots ?? null,
'disable_update' => $cliOptions->disableUpdate ?? null,
];
$dbValues = [
'type' => $cliOptions->dbType ?? null,
'host' => $cliOptions->dbHost ?? null,
'user' => $cliOptions->dbUser ?? null,
'password' => $cliOptions->dbPassword ?? null,
'base' => $cliOptions->dbBase ?? null,
'prefix' => $cliOptions->dbPrefix ?? null,
];
$config = array(
'salt' => generateSalt(),
'db' => FreshRSS_Context::systemConf()->db,
@ -88,10 +105,26 @@ if (file_exists($customConfigPath)) {
}
}
foreach ($configParams as $param => $configParam) {
if (isset($options['valid'][$param])) {
$isFlag = $parameters['long'][$param] === '';
$config[$configParam] = $isFlag ? true : $options['valid'][$param];
foreach ($values as $name => $value) {
if ($value !== null) {
switch ($name) {
case 'default_user':
if (!FreshRSS_user_Controller::checkUsername($value)) {
fail('FreshRSS invalid default username! default_user must be ASCII alphanumeric');
}
break;
case 'environment':
if (!in_array($value, ['development', 'production', 'silent'], true)) {
fail('FreshRSS invalid environment! environment must be one of { development, production, silent }');
}
break;
case 'auth_type':
if (!in_array($value, ['form', 'http_auth', 'none'], true)) {
fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
}
break;
}
$config[$name] = $value;
}
}
@ -99,23 +132,10 @@ if ((!empty($config['base_url'])) && is_string($config['base_url']) && Minz_Requ
$config['pubsubhubbub_enabled'] = true;
}
foreach ($dBconfigParams as $dBparam => $configDbParam) {
if (isset($options['valid'][$dBparam])) {
$config['db'][$configDbParam] = $options['valid'][$dBparam];
}
}
$config['db'] = array_merge($config['db'], array_filter($dbValues));
performRequirementCheck($config['db']['type']);
if (!FreshRSS_user_Controller::checkUsername($options['valid']['default-user'])) {
fail('FreshRSS error: invalid default username “' . $options['valid']['default-user']
. '”! Must be matching ' . FreshRSS_user_Controller::USERNAME_PATTERN);
}
if (isset($options['valid']['auth-type']) && !in_array($options['valid']['auth-type'], ['form', 'http_auth', 'none'], true)) {
fail('FreshRSS invalid authentication method (auth-type must be one of { form, http_auth, none })');
}
if (file_put_contents(join_path(DATA_PATH, 'config.php'),
"<?php\n return " . var_export($config, true) . ";\n") === false) {
fail('FreshRSS could not write configuration file!: ' . join_path(DATA_PATH, 'config.php'));

View File

@ -5,21 +5,20 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
parent::__construct();
}
};
if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
fail('Usage: ' . basename(__FILE__) . " --user username > /path/to/file.opml.xml");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$username = cliInitUser($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");

View File

@ -5,26 +5,23 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
'filename' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
public string $filename;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addRequiredOption('filename', (new CliOption('filename')));
parent::__construct();
}
};
if (!empty($options['invalid'])
|| empty($options['valid']['user']) || empty($options['valid']['filename'])
|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
) {
fail('Usage: ' . basename(__FILE__) . ' --user username --filename /path/to/db.sqlite');
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$filename = $options['valid']['filename'];
$username = cliInitUser($cliOptions->user);
$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');

View File

@ -5,31 +5,31 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
'max-feed-entries' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
public int $maxFeedEntries;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addOption('maxFeedEntries', (new CliOption('max-feed-entries'))->typeOfInt(), '100');
parent::__construct();
}
};
if (!empty($options['invalid']) || empty($options['valid']['user']) || !is_string($options['valid']['user'])) {
fail('Usage: ' . basename(__FILE__) . " --user username ( --max-feed-entries 100 ) > /path/to/file.zip");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (!extension_loaded('zip')) {
fail('FreshRSS error: Lacking php-zip extension!');
}
$username = cliInitUser($options['valid']['user']);
$username = cliInitUser($cliOptions->user);
fwrite(STDERR, 'FreshRSS exporting ZIP for user “' . $username . "”…\n");
$export_service = new FreshRSS_Export_Service($username);
$number_entries = empty($options['valid']['max-feed-entries']) ? 100 : intval($options['valid']['max-feed-entries']);
$number_entries = $cliOptions->maxFeedEntries;
$exported_files = [];
// First, we generate the OPML file

View File

@ -5,27 +5,24 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
'filename' => ':',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
public string $filename;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addRequiredOption('filename', (new CliOption('filename')));
parent::__construct();
}
};
if (!empty($options['invalid'])
|| empty($options['valid']['user']) || empty($options['valid']['filename'])
|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
) {
fail('Usage: ' . basename(__FILE__) . " --user username --filename /path/to/file.ext");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$username = cliInitUser($cliOptions->user);
$filename = $cliOptions->filename;
$filename = $options['valid']['filename'];
if (!is_readable($filename)) {
fail('FreshRSS error: file is not readable “' . $filename . '”');
}

View File

@ -5,27 +5,25 @@ require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$parameters = [
'long' => [
'user' => ':',
'filename' => ':',
'force-overwrite' => '',
],
'short' => [],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $user;
public string $filename;
public string $forceOverwrite;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addRequiredOption('filename', (new CliOption('filename')));
$this->addOption('forceOverwrite', (new CliOption('force-overwrite'))->withValueNone());
parent::__construct();
}
};
if (!empty($options['invalid'])
|| empty($options['valid']['user']) || empty($options['valid']['filename'])
|| !is_string($options['valid']['user']) || !is_string($options['valid']['filename'])
) {
fail('Usage: ' . basename(__FILE__) . ' --user username --force-overwrite --filename /path/to/db.sqlite');
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($options['valid']['user']);
$filename = $options['valid']['filename'];
$username = cliInitUser($cliOptions->user);
$filename = $cliOptions->filename;
if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
fail('Only *.sqlite files are supported!');
@ -34,7 +32,7 @@ if (pathinfo($filename, PATHINFO_EXTENSION) !== 'sqlite') {
echo 'FreshRSS importing database from SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$clearFirst = array_key_exists('force-overwrite', $options['valid']);
$clearFirst = isset($cliOptions->forceOverwrite);
$ok = $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_IMPORT, $clearFirst);
if (!$ok) {
echo 'If you would like to clear the user database first, use the option --force-overwrite', "\n";

View File

@ -6,70 +6,65 @@ require_once __DIR__ . '/i18n/I18nData.php';
require_once __DIR__ . '/i18n/I18nFile.php';
require_once __DIR__ . '/../constants.php';
$parameters = [
'long' => [
'action' => ':',
'help' => '',
'key' => ':',
'language' => ':',
'origin-language' => ':',
'revert' => '',
'value' => ':',
],
'short' => [
'action' => 'a',
'help' => 'h',
'key' => 'k',
'language' => 'l',
'origin-language' => 'o',
'revert' => 'r',
'value' => 'v',
],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
public string $action;
public string $key;
public string $value;
public string $language;
public string $originLanguage;
public string $revert;
public string $help;
$options = parseCliParams($parameters);
public function __construct() {
$this->addRequiredOption('action', (new CliOption('action', 'a')));
$this->addOption('key', (new CliOption('key', 'k')));
$this->addOption('value', (new CliOption('value', 'v')));
$this->addOption('language', (new CliOption('language', 'l')));
$this->addOption('originLanguage', (new CliOption('origin-language', 'o')));
$this->addOption('revert', (new CliOption('revert', 'r'))->withValueNone());
$this->addOption('help', (new CliOption('help', 'h'))->withValueNone());
parent::__construct();
}
};
if (!empty($options['invalid']) || array_key_exists('help', $options['valid'])) {
manipulateHelp();
exit();
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (!array_key_exists('action', $options['valid'])) {
error('You need to specify the action to perform.');
if (isset($cliOptions->help)) {
manipulateHelp();
}
$data = new I18nFile();
$i18nData = new I18nData($data->load());
switch ($options['valid']['action']) {
switch ($cliOptions->action) {
case 'add' :
if (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid']) && array_key_exists('language', $options['valid'])) {
$i18nData->addValue($options['valid']['key'], $options['valid']['value'], $options['valid']['language']);
} elseif (array_key_exists('key', $options['valid']) && array_key_exists('value', $options['valid'])) {
$i18nData->addKey($options['valid']['key'], $options['valid']['value']);
} elseif (array_key_exists('language', $options['valid'])) {
if (isset($cliOptions->key) && isset($cliOptions->value) && isset($cliOptions->language)) {
$i18nData->addValue($cliOptions->key, $cliOptions->value, $cliOptions->language);
} elseif (isset($cliOptions->key) && isset($cliOptions->value)) {
$i18nData->addKey($cliOptions->key, $cliOptions->value);
} elseif (isset($cliOptions->language)) {
$reference = null;
if (array_key_exists('origin-language', $options['valid'])) {
$reference = $options['valid']['origin-language'];
if (isset($cliOptions->originLanguage)) {
$reference = $cliOptions->originLanguage;
}
$i18nData->addLanguage($options['valid']['language'], $reference);
$i18nData->addLanguage($cliOptions->language, $reference);
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
case 'delete' :
if (array_key_exists('key', $options['valid'])) {
$i18nData->removeKey($options['valid']['key']);
if (isset($cliOptions->key)) {
$i18nData->removeKey($cliOptions->key);
} else {
error('You need to specify the key to delete.');
exit;
}
break;
case 'exist':
if (array_key_exists('key', $options['valid'])) {
$key = $options['valid']['key'];
if (isset($cliOptions->key)) {
$key = $cliOptions->key;
if ($i18nData->isKnown($key)) {
echo "The '{$key}' key is known.\n\n";
} else {
@ -83,16 +78,16 @@ switch ($options['valid']['action']) {
case 'format' :
break;
case 'ignore' :
if (array_key_exists('language', $options['valid']) && array_key_exists('key', $options['valid'])) {
$i18nData->ignore($options['valid']['key'], $options['valid']['language'], array_key_exists('revert', $options['valid']));
if (isset($cliOptions->language) && isset($cliOptions->key)) {
$i18nData->ignore($cliOptions->key, $cliOptions->language, isset($cliOptions->revert));
} else {
error('You need to specify a valid set of options.');
exit;
}
break;
case 'ignore_unmodified' :
if (array_key_exists('language', $options['valid'])) {
$i18nData->ignore_unmodified($options['valid']['language'], array_key_exists('revert', $options['valid']));
if (isset($cliOptions->language)) {
$i18nData->ignore_unmodified($cliOptions->language, isset($cliOptions->revert));
} else {
error('You need to specify a valid set of options.');
exit;
@ -122,6 +117,7 @@ ERROR;
*/
function manipulateHelp(): void {
$file = str_replace(__DIR__ . '/', '', __FILE__);
echo <<<HELP
NAME
$file
@ -144,17 +140,17 @@ DESCRIPTION
select the origin language (only for add language action)
EXAMPLES
Example 1: add a language. It adds a new language by duplicating the referential.
Example 1: add a language. Adds a new language by duplicating the reference language.
php $file -a add -l my_lang
php $file -a add -l my_lang -o ref_lang
Example 2: add a new key. It adds the key for all supported languages.
Example 2: add a new key. Adds a key to all supported languages.
php $file -a add -k my_key -v my_value
Example 3: add a new value. It adds a new value for the selected key in the selected language.
Example 3: add a new value. Sets a new value for the selected key in the selected language.
php $file -a add -k my_key -v my_value -l my_lang
Example 4: delete a key. It deletes the selected key from all supported languages.
Example 4: delete a key. Deletes the selected key from all supported languages.
php $file -a delete -k my_key
Example 5: format i18n files.
@ -170,11 +166,12 @@ Example 8: ignore all unmodified keys. Adds IGNORE comments to all unmodified ke
php $file -a ignore_unmodified -l my_lang
Example 9: revert ignore on all unmodified keys. Removes IGNORE comments from all unmodified keys in the selected language.
Warning: will also revert individually added unmodified keys.
Warning: will also revert individually added IGNOREs on unmodified keys.
php $file -a ignore_unmodified -r -l my_lang
Example 10: check if a key exist.
php $file -a exist -k my_key\n\n
php $file -a exist -k my_key
HELP;
exit();
}

View File

@ -3,133 +3,120 @@
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$parameters = [
'long' => [
'environment' => ':',
'base-url' => ':',
'language' => ':',
'title' => ':',
'default-user' => ':',
'allow-anonymous' => '',
'allow-anonymous-refresh' => '',
'auth-type' => ':',
'api-enabled' => '',
'allow-robots' => '',
'disable-update' => '',
'db-type' => ':',
'db-host' => ':',
'db-user' => ':',
'db-password' => ':',
'db-base' => ':',
'db-prefix' => '::',
],
'short' => [],
'deprecated' => [
'base-url' => 'base_url',
'default-user' => 'default_user',
'allow-anonymous' => 'allow_anonymous',
'allow-anonymous-refresh' => 'allow_anonymous_refresh',
'auth-type' => 'auth_type',
'api-enabled' => 'api_enabled',
'allow-robots' => 'allow_robots',
'disable-update' => 'disable_update',
],
];
$cliOptions = new class extends CliOptionsParser {
public string $defaultUser;
public string $environment;
public string $baseUrl;
public string $language;
public string $title;
public bool $allowAnonymous;
public bool $allowAnonymousRefresh;
public string $authType;
public bool $apiEnabled;
public bool $allowRobots;
public bool $disableUpdate;
public string $dbType;
public string $dbHost;
public string $dbUser;
public string $dbPassword;
public string $dbBase;
public string $dbPrefix;
$configParams = [
'environment',
'base-url',
'language',
'title',
'default-user',
'allow-anonymous',
'allow-anonymous-refresh',
'auth-type',
'api-enabled',
'allow-robots',
'disable-update',
];
public function __construct() {
$this->addOption('defaultUser', (new CliOption('default-user'))->deprecatedAs('default_user'));
$this->addOption('environment', (new CliOption('environment')));
$this->addOption('baseUrl', (new CliOption('base-url'))->deprecatedAs('base_url'));
$this->addOption('language', (new CliOption('language')));
$this->addOption('title', (new CliOption('title')));
$this->addOption(
'allowAnonymous',
(new CliOption('allow-anonymous'))->withValueOptional('true')->deprecatedAs('allow_anonymous')->typeOfBool()
);
$this->addOption(
'allowAnonymousRefresh',
(new CliOption('allow-anonymous-refresh'))->withValueOptional('true')->deprecatedAs('allow_anonymous_refresh')->typeOfBool()
);
$this->addOption('authType', (new CliOption('auth-type'))->deprecatedAs('auth_type'));
$this->addOption(
'apiEnabled',
(new CliOption('api-enabled'))->withValueOptional('true')->deprecatedAs('api_enabled')->typeOfBool()
);
$this->addOption(
'allowRobots',
(new CliOption('allow-robots'))->withValueOptional('true')->deprecatedAs('allow_robots')->typeOfBool()
);
$this->addOption(
'disableUpdate',
(new CliOption('disable-update'))->withValueOptional('true')->deprecatedAs('disable_update')->typeOfBool()
);
$this->addOption('dbType', (new CliOption('db-type')));
$this->addOption('dbHost', (new CliOption('db-host')));
$this->addOption('dbUser', (new CliOption('db-user')));
$this->addOption('dbPassword', (new CliOption('db-password')));
$this->addOption('dbBase', (new CliOption('db-base')));
$this->addOption('dbPrefix', (new CliOption('db-prefix'))->withValueOptional());
parent::__construct();
}
};
$dBconfigParams = [
'db-type' => 'type',
'db-host' => 'host',
'db-user' => 'user',
'db-password' => 'password',
'db-base' => 'base',
'db-prefix' => 'prefix',
];
$options = parseCliParams($parameters);
if (!empty($options['invalid'])) {
fail('Usage: ' . basename(__FILE__) . " --default-user admin ( --auth-type form" .
" --environment production --base-url https://rss.example.net --allow-robots" .
" --language en --title FreshRSS --allow-anonymous --allow-anonymous-refresh --api-enabled" .
" --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123" .
" --db-base freshrss --db-prefix freshrss_ --disable-update )");
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
fwrite(STDERR, 'Reconfiguring FreshRSS…' . "\n");
foreach ($configParams as $param) {
if (isset($options['valid'][$param])) {
switch ($param) {
case 'allow-anonymous-refresh':
FreshRSS_Context::systemConf()->allow_anonymous_refresh = true;
break;
case 'allow-anonymous':
FreshRSS_Context::systemConf()->allow_anonymous = true;
break;
case 'allow-robots':
FreshRSS_Context::systemConf()->allow_robots = true;
break;
case 'api-enabled':
FreshRSS_Context::systemConf()->api_enabled = true;
break;
case 'auth-type':
if (in_array($options['valid'][$param], ['form', 'http_auth', 'none'], true)) {
FreshRSS_Context::systemConf()->auth_type = $options['valid'][$param];
} else {
fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
}
break;
case 'base-url':
FreshRSS_Context::systemConf()->base_url = (string) $options['valid'][$param];
break;
case 'default-user':
if (FreshRSS_user_Controller::checkUsername((string) $options['valid'][$param])) {
FreshRSS_Context::systemConf()->default_user = (string) $options['valid'][$param];
} else {
$values = [
'default_user' => $cliOptions->defaultUser ?? null,
'environment' => $cliOptions->environment ?? null,
'base_url' => $cliOptions->baseUrl ?? null,
'language' => $cliOptions->language ?? null,
'title' => $cliOptions->title ?? null,
'allow_anonymous' => $cliOptions->allowAnonymous ?? null,
'allow_anonymous_refresh' => $cliOptions->allowAnonymousRefresh ?? null,
'auth_type' => $cliOptions->authType ?? null,
'api_enabled' => $cliOptions->apiEnabled ?? null,
'allow_robots' => $cliOptions->allowRobots ?? null,
'disable_update' => $cliOptions->disableUpdate ?? null,
];
$dbValues = [
'type' => $cliOptions->dbType ?? null,
'host' => $cliOptions->dbHost ?? null,
'user' => $cliOptions->dbUser ?? null,
'password' => $cliOptions->dbPassword ?? null,
'base' => $cliOptions->dbBase ?? null,
'prefix' => $cliOptions->dbPrefix ?? null,
];
$systemConf = FreshRSS_Context::systemConf();
foreach ($values as $name => $value) {
if ($value !== null) {
switch ($name) {
case 'default_user':
if (!FreshRSS_user_Controller::checkUsername($value)) {
fail('FreshRSS invalid default username! default_user must be ASCII alphanumeric');
}
break;
case 'disable-update':
FreshRSS_Context::systemConf()->disable_update = true;
break;
case 'environment':
if (in_array($options['valid'][$param], ['development', 'production', 'silent'], true)) {
FreshRSS_Context::systemConf()->environment = $options['valid'][$param];
} else {
if (!in_array($value, ['development', 'production', 'silent'], true)) {
fail('FreshRSS invalid environment! environment must be one of { development, production, silent }');
}
break;
case 'language':
FreshRSS_Context::systemConf()->language = (string) $options['valid'][$param];
break;
case 'title':
FreshRSS_Context::systemConf()->title = (string) $options['valid'][$param];
case 'auth_type':
if (!in_array($value, ['form', 'http_auth', 'none'], true)) {
fail('FreshRSS invalid authentication method! auth_type must be one of { form, http_auth, none }');
}
break;
}
// @phpstan-ignore-next-line (change to `@phpstan-ignore property.dynamicName` when upgrading to PHPStan 1.11+)
$systemConf->$name = $value;
}
}
$db = FreshRSS_Context::systemConf()->db;
foreach ($dBconfigParams as $dBparam => $configDbParam) {
if (isset($options['valid'][$dBparam])) {
$db[$configDbParam] = $options['valid'][$dBparam];
}
}
/** @var array{'type':string,'host':string,'user':string,'password':string,'base':string,'prefix':string,
* 'connection_uri_params':string,'pdo_options':array<int,int|string|bool>} $db */
$db = array_merge(FreshRSS_Context::systemConf()->db, array_filter($dbValues));
performRequirementCheck($db['type']);
FreshRSS_Context::systemConf()->db = $db;
FreshRSS_Context::systemConf()->save();

View File

@ -1,26 +1,85 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
$isUpdate = true;
require(__DIR__ . '/_update-or-create-user.php');
$cliOptions = new class extends CliOptionsParser {
public string $user;
public string $password;
public string $apiPassword;
public string $language;
public string $email;
public string $token;
public int $purgeAfterMonths;
public int $feedMinArticles;
public int $feedTtl;
public int $sinceHoursPostsPerRss;
public int $maxPostsPerRss;
$username = cliInitUser($GLOBALS['options']['valid']['user']);
public function __construct() {
$this->addRequiredOption('user', (new CliOption('user')));
$this->addOption('password', (new CliOption('password')));
$this->addOption('apiPassword', (new CliOption('api-password'))->deprecatedAs('api_password'));
$this->addOption('language', (new CliOption('language')));
$this->addOption('email', (new CliOption('email')));
$this->addOption('token', (new CliOption('token')));
$this->addOption(
'purgeAfterMonths',
(new CliOption('purge-after-months'))->typeOfInt()->deprecatedAs('purge_after_months')
);
$this->addOption(
'feedMinArticles',
(new CliOption('feed-min-articles-default'))->typeOfInt()->deprecatedAs('feed_min_articles_default')
);
$this->addOption(
'feedTtl',
(new CliOption('feed-ttl-default'))->typeOfInt()->deprecatedAs('feed_ttl_default')
);
$this->addOption(
'sinceHoursPostsPerRss',
(new CliOption('since-hours-posts-per-rss'))->typeOfInt()->deprecatedAs('since_hours_posts_per_rss')
);
$this->addOption(
'maxPostsPerRss',
(new CliOption('max-posts-per-rss'))->typeOfInt()->deprecatedAs('max_posts_per_rss')
);
parent::__construct();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
$username = cliInitUser($cliOptions->user);
echo 'FreshRSS updating user “', $username, "”…\n";
$values = [
'language' => $cliOptions->language ?? null,
'mail_login' => $cliOptions->email ?? null,
'token' => $cliOptions->token ?? null,
'old_entries' => $cliOptions->purgeAfterMonths ?? null,
'keep_history_default' => $cliOptions->feedMinArticles ?? null,
'ttl_default' => $cliOptions->feedTtl ?? null,
'since_hours_posts_per_rss' => $cliOptions->sinceHoursPostsPerRss ?? null,
'max_posts_per_rss' => $cliOptions->maxPostsPerRss ?? null,
];
$values = array_filter($values);
$ok = FreshRSS_user_Controller::updateUser(
$username,
empty($options['valid']['email']) ? null : $options['valid']['email'],
empty($options['valid']['password']) ? '' : $options['valid']['password'],
$GLOBALS['values']);
isset($cliOptions->email) ? $cliOptions->email : null,
$cliOptions->password ?? '',
$values);
if (!$ok) {
fail('FreshRSS could not update user!');
}
if (!empty($options['valid']['api_password'])) {
$error = FreshRSS_api_Controller::updatePassword($options['valid']['api_password']);
if (isset($cliOptions->apiPassword)) {
$error = FreshRSS_api_Controller::updatePassword($cliOptions->apiPassword);
if ($error) {
fail($error);
}

View File

@ -5,45 +5,38 @@ require(__DIR__ . '/_cli.php');
const DATA_FORMAT = "%-7s | %-20s | %-5s | %-7s | %-25s | %-15s | %-10s | %-10s | %-10s | %-10s | %-10s | %-10s | %-5s | %-10s\n";
$parameters = [
'long' => [
'user' => ':',
'header' => '',
'json' => '',
'human-readable' => '',
],
'short' => [
'human-readable' => 'h',
],
'deprecated' => [],
];
$cliOptions = new class extends CliOptionsParser {
/** @var array<int,string> $user */
public array $user;
public string $header;
public string $json;
public string $humanReadable;
$options = parseCliParams($parameters);
public function __construct() {
$this->addOption('user', (new CliOption('user'))->typeOfArrayOfString());
$this->addOption('header', (new CliOption('header'))->withValueNone());
$this->addOption('json', (new CliOption('json'))->withValueNone());
$this->addOption('humanReadable', (new CliOption('human-readable', 'h'))->withValueNone());
parent::__construct();
}
};
if (!empty($options['invalid'])) {
fail('Usage: ' . basename(__FILE__) . ' (--human-readable --header --json --user username --user username …)');
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
if (empty($options['valid']['user'])) {
$users = listUsers();
} elseif (is_array($options['valid']['user'])) {
/** @var array<string> $users */
$users = $options['valid']['user'];
} else {
/** @var array<string> $users */
$users = [$options['valid']['user']];
}
$users = $cliOptions->user ?? listUsers();
sort($users);
$formatJson = isset($options['valid']['json']);
$formatJson = isset($cliOptions->json);
$jsonOutput = [];
if ($formatJson) {
unset($options['valid']['header']);
unset($options['valid']['human-readable']);
unset($cliOptions->header);
unset($cliOptions->humanReadable);
}
if (array_key_exists('header', $options['valid'])) {
if (isset($cliOptions->header)) {
printf(
DATA_FORMAT,
'default',
@ -92,7 +85,7 @@ foreach ($users as $username) {
'lang' => FreshRSS_Context::userConf()->language,
'mail_login' => FreshRSS_Context::userConf()->mail_login,
);
if (isset($options['valid']['human-readable'])) { //Human format
if (isset($cliOptions->humanReadable)) { //Human format
$data['last_user_activity'] = date('c', $data['last_user_activity']);
$data['database_size'] = format_bytes($data['database_size']);
}

View File

@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
require_once __DIR__ . '/../../cli/CliOption.php';
require_once __DIR__ . '/../../cli/CliOptionsParser.php';
final class CliOptionsOptionalTest extends CliOptionsParser {
public string $string = '';
public int $int = 0;
public bool $bool = false;
/** @var array<int,string> $arrayOfString */
public array $arrayOfString = [];
public string $defaultInput = '';
public string $optionalValue = '';
public bool $optionalValueWithDefault = false;
public string $defaultInputAndOptionalValueWithDefault = '';
public function __construct() {
$this->addOption('string', (new CliOption('string', 's'))->deprecatedAs('deprecated-string'));
$this->addOption('int', (new CliOption('int', 'i'))->typeOfInt());
$this->addOption('bool', (new CliOption('bool', 'b'))->typeOfBool());
$this->addOption('arrayOfString', (new CliOption('array-of-string', 'a'))->typeOfArrayOfString());
$this->addOption('defaultInput', (new CliOption('default-input', 'i')), 'default');
$this->addOption('optionalValue', (new CliOption('optional-value', 'o'))->withValueOptional());
$this->addOption('optionalValueWithDefault', (new CliOption('optional-value-with-default', 'd'))->withValueOptional('true')->typeOfBool());
$this->addOption('defaultInputAndOptionalValueWithDefault',
(new CliOption('default-input-and-optional-value-with-default', 'e'))->withValueOptional('optional'),
'default'
);
$this->addOption('flag', (new CliOption('flag', 'f'))->withValueNone());
parent::__construct();
}
}
final class CliOptionsOptionalAndRequiredTest extends CliOptionsParser {
public string $required = '';
public string $string = '';
public int $int = 0;
public bool $bool = false;
public string $flag = '';
public function __construct() {
$this->addRequiredOption('required', new CliOption('required'));
$this->addOption('string', new CliOption('string', 's'));
$this->addOption('int', (new CliOption('int', 'i'))->typeOfInt());
$this->addOption('bool', (new CliOption('bool', 'b'))->typeOfBool());
$this->addOption('flag', (new CliOption('flag', 'f'))->withValueNone());
parent::__construct();
}
}
class CliOptionsParserTest extends TestCase {
public function testInvalidOptionSetWithValueReturnsError(): void {
$result = $this->runOptionalOptions('--invalid=invalid');
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
}
public function testInvalidOptionSetWithoutValueReturnsError(): void {
$result = $this->runOptionalOptions('--invalid');
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
}
public function testValidOptionSetWithValidValueAndInvalidOptionSetWithValueReturnsValueForValidOptionAndErrorForInvalidOption(): void {
$result = $this->runOptionalOptions('--string=string --invalid=invalid');
self::assertEquals('string', $result->string);
self::assertEquals(['invalid' => 'unknown option: invalid'], $result->errors);
}
public function testOptionWithValueTypeOfStringSetOnceWithValidValueReturnsValueAsString(): void {
$result = $this->runOptionalOptions('--string=string');
self::assertEquals('string', $result->string);
}
public function testOptionWithRequiredValueTypeOfIntSetOnceWithValidValueReturnsValueAsInt(): void {
$result = $this->runOptionalOptions('--int=111');
self::assertEquals(111, $result->int);
}
public function testOptionWithRequiredValueTypeOfBoolSetOnceWithValidValueReturnsValueAsBool(): void {
$result = $this->runOptionalOptions('--bool=on');
self::assertEquals(true, $result->bool);
}
public function testOptionWithValueTypeOfArrayOfStringSetOnceWithValidValueReturnsValueAsArrayOfString(): void {
$result = $this->runOptionalOptions('--array-of-string=string');
self::assertEquals(['string'], $result->arrayOfString);
}
public function testOptionWithValueTypeOfStringSetMultipleTimesWithValidValueReturnsLastValueSetAsString(): void {
$result = $this->runOptionalOptions('--string=first --string=second');
self::assertEquals('second', $result->string);
}
public function testOptionWithValueTypeOfIntSetMultipleTimesWithValidValueReturnsLastValueSetAsInt(): void {
$result = $this->runOptionalOptions('--int=111 --int=222');
self::assertEquals(222, $result->int);
}
public function testOptionWithValueTypeOfBoolSetMultipleTimesWithValidValueReturnsLastValueSetAsBool(): void {
$result = $this->runOptionalOptions('--bool=on --bool=off');
self::assertEquals(false, $result->bool);
}
public function testOptionWithValueTypeOfArrayOfStringSetMultipleTimesWithValidValueReturnsAllSetValuesAsArrayOfString(): void {
$result = $this->runOptionalOptions('--array-of-string=first --array-of-string=second');
self::assertEquals(['first', 'second'], $result->arrayOfString);
}
public function testOptionWithValueTypeOfIntSetWithInvalidValueReturnsAnError(): void {
$result = $this->runOptionalOptions('--int=one');
self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors);
}
public function testOptionWithValueTypeOfBoolSetWithInvalidValuesReturnsAnError(): void {
$result = $this->runOptionalOptions('--bool=bad');
self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
}
public function testOptionWithValueTypeOfIntSetMultipleTimesWithValidAndInvalidValuesReturnsLastValidValueSetAsIntAndError(): void {
$result = $this->runOptionalOptions('--int=111 --int=one --int=222 --int=two');
self::assertEquals(222, $result->int);
self::assertEquals(['int' => 'invalid input: int must be an integer'], $result->errors);
}
public function testOptionWithValueTypeOfBoolSetMultipleTimesWithWithValidAndInvalidValuesReturnsLastValidValueSetAsBoolAndError(): void {
$result = $this->runOptionalOptions('--bool=on --bool=good --bool=off --bool=bad');
self::assertEquals(false, $result->bool);
self::assertEquals(['bool' => 'invalid input: bool must be a boolean'], $result->errors);
}
public function testNotSetOptionWithDefaultInputReturnsDefaultInput(): void {
$result = $this->runOptionalOptions('');
self::assertEquals('default', $result->defaultInput);
}
public function testOptionWithDefaultInputSetWithValidValueReturnsCorrectlyTypedValue(): void {
$result = $this->runOptionalOptions('--default-input=input');
self::assertEquals('input', $result->defaultInput);
}
public function testOptionWithOptionalValueSetWithoutValueReturnsEmptyString(): void {
$result = $this->runOptionalOptions('--optional-value');
self::assertEquals('', $result->optionalValue);
}
public function testOptionWithOptionalValueDefaultSetWithoutValueReturnsOptionalValueDefault(): void {
$result = $this->runOptionalOptions('--optional-value-with-default');
self::assertEquals(true, $result->optionalValueWithDefault);
}
public function testNotSetOptionWithOptionalValueDefaultAndDefaultInputReturnsDefaultInput(): void {
$result = $this->runOptionalOptions('');
self::assertEquals('default', $result->defaultInputAndOptionalValueWithDefault);
}
public function testOptionWithOptionalValueDefaultAndDefaultInputSetWithoutValueReturnsOptionalValueDefault(): void {
$result = $this->runOptionalOptions('--default-input-and-optional-value-with-default');
self::assertEquals('optional', $result->defaultInputAndOptionalValueWithDefault);
}
public function testRequiredOptionNotSetReturnsError(): void {
$result = $this->runOptionalAndRequiredOptions('');
self::assertEquals(['required' => 'invalid input: required cannot be empty'], $result->errors);
}
public function testOptionSetWithDeprecatedAliasGeneratesDeprecationWarningAndReturnsValue(): void {
$result = $this->runCommandReadingStandardError('--deprecated-string=string');
self::assertEquals('FreshRSS deprecation warning: the CLI option(s): deprecated-string are deprecated ' .
'and will be removed in a future release. Use: string instead',
$result
);
$result = $this->runOptionalOptions('--deprecated-string=string');
self::assertEquals('string', $result->string);
}
public function testAlwaysReturnUsageMessageWithUsageInfoForAllOptions(): void {
$result = $this->runOptionalAndRequiredOptions('');
self::assertEquals('Usage: cli-parser-test.php --required=<required> [-s --string=<string>] [-i --int=<int>] [-b --bool=<bool>] [-f --flag]',
$result->usage,
);
}
private function runOptionalOptions(string $cliOptions = ''): CliOptionsOptionalTest {
$command = __DIR__ . '/cli-parser-test.php';
$className = CliOptionsOptionalTest::class;
$result = shell_exec("CLI_PARSER_TEST_OPTIONS_CLASS='$className' $command $cliOptions 2>/dev/null");
$result = is_string($result) ? unserialize($result) : new CliOptionsOptionalTest();
/** @var CliOptionsOptionalTest $result */
return $result;
}
private function runOptionalAndRequiredOptions(string $cliOptions = ''): CliOptionsOptionalAndRequiredTest {
$command = __DIR__ . '/cli-parser-test.php';
$className = CliOptionsOptionalAndRequiredTest::class;
$result = shell_exec("CLI_PARSER_TEST_OPTIONS_CLASS='$className' $command $cliOptions 2>/dev/null");
$result = is_string($result) ? unserialize($result) : new CliOptionsOptionalAndRequiredTest();
/** @var CliOptionsOptionalAndRequiredTest $result */
return $result;
}
private function runCommandReadingStandardError(string $cliOptions = ''): string {
$command = __DIR__ . '/cli-parser-test.php';
$className = CliOptionsOptionalTest::class;
$result = shell_exec("CLI_PARSER_TEST_OPTIONS_CLASS='$className' $command $cliOptions 2>&1");
$result = is_string($result) ? explode("\n", $result) : '';
return is_array($result) ? $result[0] : '';
}
}

24
tests/cli/cli-parser-test.php Executable file
View File

@ -0,0 +1,24 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/../../vendor/autoload.php');
require(__DIR__ . '/CliOptionsParserTest.php');
$optionsClass = getenv('CLI_PARSER_TEST_OPTIONS_CLASS');
if (!is_string($optionsClass) || !class_exists($optionsClass)) {
die('Invalid test static method!');
}
switch ($optionsClass) {
case CliOptionsOptionalTest::class:
$options = new CliOptionsOptionalTest();
break;
case CliOptionsOptionalAndRequiredTest::class:
$options = new CliOptionsOptionalAndRequiredTest();
break;
default:
die('Unknown test static method!');
}
echo serialize($options);