From: direct Date: Thu, 17 Apr 2025 14:06:41 +0000 (+1000) Subject: Добавлена библиотека для обработки параметров запуска cli X-Git-Url: http://git.ultra-x.su/?a=commitdiff_plain;h=a503109c67308a6d42b93aa7dec952bbb365176d;p=dev Добавлена библиотека для обработки параметров запуска cli --- diff --git a/lib/ArgsOptsProc.php b/lib/ArgsOptsProc.php new file mode 100644 index 0000000..b2a2f66 --- /dev/null +++ b/lib/ArgsOptsProc.php @@ -0,0 +1,626 @@ +GetScreenWidth(); + global $argv; + + $this->PrintWithWordWrap($this->ProgrammDescription); + $this->PrintWithWordWrap($this->HelpTextTop); + $OptionIds = array_keys($this->DescriptionOpts); + $diff = array_intersect($this->LongOpts, $OptionIds); + $NoShortOption = false; + $NoLongOption = false; + if (count(array_intersect($this->ShortOpts, $OptionIds)) == 0) + { + $NoShortOption = true; + } + if (count($diff) == 0) + { + $NoLongOption = true; + $MaxlenParamName = 0; + + } + else + { + $MaxlenParamName = max(array_map('strlen', array_keys($diff))) + 2; + } + + $pregsize = $WidthScreen - $MaxlenParamName - (($NoShortOption)?0:4) - 6; + if (!$NoShortOption || !$NoLongOption) + { + echo 'options:', PHP_EOL; + foreach ($this->DescriptionOpts as $OptionId => $Description) + { + preg_match_all('/(.{0,'.$pregsize.'})(\ |$)/', $Description, $parts); + + if ($NoShortOption) + { + $ShortOptionText = ''; + $OptionDelimiter = ''; + } + else + { + $ShortOptionText = ($this->IdOpts[$OptionId]['ShortOpts'] === null)? " " : '-'.$this->IdOpts[$OptionId]['ShortOpts']; + $OptionDelimiter = ($this->IdOpts[$OptionId]['ShortOpts'] === null || $this->IdOpts[$OptionId]['LongOpts'] === null)? ' ' : ', '; + } + if ($NoLongOption) + { + $LongOptionText = ''; + $OptionDelimiter = ''; + } + else + { + $LongOptionText = (($this->IdOpts[$OptionId]['LongOpts'] === null)? str_repeat(' ', $MaxlenParamName):'--' . str_pad($this->IdOpts[$OptionId]['LongOpts'],$MaxlenParamName - 2)); + } + $OptionText = " " . $ShortOptionText . $OptionDelimiter . $LongOptionText . " "; + $FF = true; + foreach ($parts[1] as $part) + { + if ($part == '') + { + continue; + } + if ($FF) + { + echo $OptionText; + $FF = false; + } + else + { + echo str_repeat(' ', strlen($OptionText)); + } + echo $part, PHP_EOL; + } + } + } + echo PHP_EOL; + $this->PrintWithWordWrap($this->HelpTextBottom); + + } + + protected function GetIdOption(string $option): ?int + { + if ($option == '') + { + return null; + } + + if (isset($this->ShortOpts[$option])) + { + return $this->ShortOpts[$option]; + } + if (isset($this->LongOpts[$option])) + { + return $this->LongOpts[$option]; + } + + return null; + } + + protected function GetOptionById(int $IdOption): ?array + { + $OptionName = null; + if (isset($this->IdOpts[$IdOption]['ShortOpts'])) + { + $OptionName['Short'] = $this->IdOpts[$IdOption]['ShortOpts']; + } + if (isset($this->IdOpts[$IdOption]['LongOpts'])) + { + $OptionName['Long'] = $this->IdOpts[$IdOption]['LongOpts']; + } + return $OptionName; + } + + public function DeclareOption(?string $ShortOptionName, ?string $LongOptionName = null): bool + { + //контроль входных данных + if (!is_null($ShortOptionName) && strlen($ShortOptionName) != 1) + { + return false; + } + if (!is_null($LongOptionName) && strlen($LongOptionName) < 2) + { + return false; + } + if (isset($this->ShortOpts[$ShortOptionName]) || isset($this->LongOpts[$LongOptionName])) + { + return false; + } + if (is_null($ShortOptionName) && is_null($LongOptionName)) + { + return false; + } + if (!is_null($ShortOptionName)) + { + $this->ShortOpts[$ShortOptionName] = $this->CounterOptId; + } + if (!is_null($LongOptionName)) + { + $this->LongOpts[$LongOptionName] = $this->CounterOptId; + } + $this->IdOpts[$this->CounterOptId]['ShortOpts'] = $ShortOptionName; + $this->IdOpts[$this->CounterOptId]['LongOpts'] = $LongOptionName; + $this->CounterOptId++; + return true; + } + + public function SetErrorExitCode(int $code): void + { + $this->ErrorExitCode = $code; + } + + public function SetErrorAddText(string $text): void + { + $this->ErrorAddText = $text; + } + + public function SetProgrammDescription(string $Description): void + { + $this->ProgrammDescription = $Description; + } + + public function SetHelpTextTop(string $text = ''): void + { + $this->HelpTextTop = $text; + } + + public function SetHelpTextBottom(string $text = ''): void + { + $this->HelpTextBottom = $text; + } + + public function SetOptionDescription(string $Option, string $Description): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + $this->DescriptionOpts[$IdOption] = $Description; + return true; + } + + public function SetOptionValueMode(string $Option, int $ValueMode = AOP_NOTUSED): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + if ($ValueMode < 0 || $ValueMode > 2) + { + return false; + } + $this->ValueModeOpts[$IdOption] = $ValueMode; + return true; + } + + public function SetOptionDefaultValue(string $Option, ?string $Value = null): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + if (!isset($this->ValueModeOpts[$IdOption])) + { + $this->ValueModeOpts[$IdOption] = AOP_NOTUSED; + } + if (is_null($Value)) + { + $Value = false; + } + switch ($this->ValueModeOpts[$IdOption]) + { + case AOP_NOTUSED: + $this->DefaultValueOpts[$IdOption] = false; + break; + case AOP_NOTREQUIRED: + $this->DefaultValueOpts[$IdOption] = $Value; + break; + case AOP_REQUIRED: + if ($Value !== false) + { + $this->DefaultValueOpts[$IdOption] = $Value; + } + else + { + return false; + } + break; + } + return true; + } + + public function SetOptionUseOnlyFirstValue(string $Option, bool $val = true): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + $this->UseOnlyFirstValue[$IdOption] = $val; + return true; + } + + public function SetOptionConfilctGroup(string $Option, string $ConflictGroup): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + if ($ConflictGroup == '') + { + return false; + } + $this->ConflictGroups[$ConflictGroup][] = $IdOption; + return true; + } + + public function SetOptionIsRequired(string $Option, string $RequiredGroup = 'default'): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + $this->IsRequiredList[$RequiredGroup][] = $IdOption; + return true; + } + + public function SetOptionIsOne(string $Option): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + $this->IsOneList[] = $IdOption; + return true; + } + + public function SetOptionCallbackHandler(string $Option, callable $CallbackHandler = null, int $Order = 255): bool + { + $IdOption = $this->GetIdOption($Option); + if (is_null($IdOption)) + { + return false; + } + $this->CallbackOpts[$IdOption] = $CallbackHandler; + $this->CallbackOptsOrder[$IdOption] = $Order; + return true; + } + + public function UseInternalHelp(bool $OnlyShort = false): void + { + if (!$this->InternalHelp) + { + + $long = ($OnlyShort)? null: 'help'; + $this->InternalHelp = true; + $this->DeclareOption('h', $long); + $this->SetOptionDescription('h', 'show this help topic and quit'); + $this->SetOptionCallbackHandler('h', [$this, 'HelpTopic']); + $this->SetOptionUseOnlyFirstValue('h'); + } + } + + public function RunProcessing(): array + { + if (isset($this->ProcessedOptions)) + { + return $this->ProcessedOptions; + } + global $argc, $argv; + //формирование доступных опций + $ShortOptsList = ''; + foreach ($this->ShortOpts as $Option => $IdOption) + { + $ShortOptsList .= $Option; + if (isset($this->ValueModeOpts[$IdOption])) + { + switch ($this->ValueModeOpts[$IdOption]) + { + case AOP_NOTUSED: + break; + case AOP_NOTREQUIRED: + $ShortOptsList .= '::'; + break; + case AOP_REQUIRED: + $ShortOptsList .= ':'; + break; + } + + } + } + $LongOptsList = []; + foreach ($this->LongOpts as $Option => $IdOption) + { + if (isset($this->ValueModeOpts[$IdOption])) + { + switch ($this->ValueModeOpts[$IdOption]) + { + case AOP_NOTUSED: + break; + case AOP_NOTREQUIRED: + $Option .= '::'; + break; + case AOP_REQUIRED: + $Option .= ':'; + break; + } + } + $LongOptsList[] = $Option; + } + unset($Option, $IdOption); + + //базовый парсинг + $ParsedOptions = getopt($ShortOptsList, $LongOptsList,$index); + //приведение разноимённых опций но под одним ID к сокращенной опции с объединением значений + foreach ($this->IdOpts as $IdOption => $Options) + { + if (!is_null($Options['ShortOpts']) && !is_null($Options['LongOpts'])) + { + if (isset($ParsedOptions[$Options['ShortOpts']]) && isset($ParsedOptions[$Options['LongOpts']])) + { + if (!is_array($ParsedOptions[$Options['ShortOpts']])) + { + $TempShortValue = $ParsedOptions[$Options['ShortOpts']]; + $ParsedOptions[$Options['ShortOpts']] = []; + $ParsedOptions[$Options['ShortOpts']][] = $TempShortValue; + } + + if (!is_array($ParsedOptions[$Options['LongOpts']])) + { + $TempLongValue = $ParsedOptions[$Options['LongOpts']]; + $ParsedOptions[$Options['LongOpts']] = []; + $ParsedOptions[$Options['LongOpts']][] = $TempLongValue; + } + $ParsedOptions[$Options['ShortOpts']] = array_merge($ParsedOptions[$Options['ShortOpts']], $ParsedOptions[$Options['LongOpts']]); + unset($ParsedOptions[$Options['LongOpts']]); + } + } + } + //докидывание опций, которые не были указаны но объявлены как имеющие значение по умолчанию + foreach ($this->DefaultValueOpts as $IdOption => $Value) + { + if (!isset($ParsedOptions[$this->IdOpts[$IdOption]['ShortOpts']]) && !isset($ParsedOptions[$this->IdOpts[$IdOption]['LongOpts']])) + { + if (!is_null($this->IdOpts[$IdOption]['ShortOpts'])) + { + $ParsedOptions[$this->IdOpts[$IdOption]['ShortOpts']] = $Value; + } + elseif (!is_null($this->IdOpts[$IdOption]['LongOpts'])) + { + $ParsedOptions[$this->IdOpts[$IdOption]['LongOpts']] = $Value; + } + } + } + + //докидывание аргументов, как отдельной опции + for ($i = $index; $i < $argc; $i++) + { + $ParsedOptions['!ARGS'][] = $argv[$i]; + } + if ($this->UseOnlyFirstArg && isset($ParsedOptions['!ARGS'])) + { + $ParsedOptions['!ARGS'] = $ParsedOptions['!ARGS'][0]; + } + + //обработка опции "использовать только одно значение" гарантирует что значение опции будет использовано первое найденное и не будет массивом значений + foreach ($ParsedOptions as $Option => $Values) + { + $IdOption = $this->GetIdOption($Option); + if (isset($this->UseOnlyFirstValue[$IdOption]) && is_array($Values)) + { + if ($this->UseOnlyFirstValue[$IdOption]) + { + $ParsedOptions[$Option] = $Values[0]; + } + } + } + + //проверяем используется и вызывается ли встроенный help + if ($this->InternalHelp &&(isset($ParsedOptions['h']) || isset($ParsedOptions['help']))) + { + $this->HelpTopic(); + exit(); + } + + // проверка на пропущенность обязательных параметров + + foreach ($this->IsRequiredList as $RequiredGroup => $IdOptions) + { + foreach ($IdOptions as $IdOption) + { + if ($RequiredGroup == 'default' && !isset($ParsedOptions[$this->IdOpts[$IdOption]['ShortOpts']]) && !isset($ParsedOptions[$this->IdOpts[$IdOption]['LongOpts']])) + { + $OptionById = $this->GetOptionById($IdOption); + $OptPrintname = (count($OptionById) == 1)? '-' . $OptionById['Short'] : '-' . $OptionById['Short'] .', --'. $OptionById['Long']; + fwrite(STDERR, "Required parameter {$OptPrintname} is missing!".PHP_EOL); + fwrite(STDERR, $this->ErrorAddText); + exit($this->ErrorExitCode); + } + if ($RequiredGroup != 'default' && !isset($ParsedOptions[$this->IdOpts[$IdOption]['ShortOpts']]) && !isset($ParsedOptions[$this->IdOpts[$IdOption]['LongOpts']])) + { + $NFF = true; + } + else + { + $NFF = false; + break; + } + } + if ($RequiredGroup != 'default' && $NFF) + { + $OptPrintname = ''; + $OptPrintnameSeparator = ''; + foreach ($IdOptions as $IdOption) + { + $OptionById = $this->GetOptionById($IdOption); + $OptPrintname .= (count($OptionById) == 1)? $OptPrintnameSeparator. '-' . $OptionById['Short'] : $OptPrintnameSeparator . '-' . $OptionById['Short'] .', --'. $OptionById['Long']; + $OptPrintnameSeparator = ' or '; + } + fwrite(STDERR, "Required parameter {$OptPrintname} is missing!".PHP_EOL); + fwrite(STDERR, $this->ErrorAddText); + exit($this->ErrorExitCode); + } + unset($NFF); + } + + //проверка на всякие ограничения + //проверка на запрещенность нескольких значений для опции + foreach ($ParsedOptions as $Option => $Values) + { + $IdOption = $this->GetIdOption($Option); + if (array_search($IdOption, $this->IsOneList) !== false && is_array($Values)) + { + $OptPrintname = ((strlen($Option) == 1)? '-' : '--').$Option; + fwrite(STDERR, "Parameter {$OptPrintname} must be the one!".PHP_EOL); + fwrite(STDERR, $this->ErrorAddText); + exit($this->ErrorExitCode); + } + } + + //проверка на конфликты опций + $ParsedOptList = array_keys($ParsedOptions); + $ParsedIdOptionList = []; + foreach ($ParsedOptList as $Option) + { + $ParsedIdOptionList [] = $this->GetIdOption($Option); + } + foreach ($this->ConflictGroups as $ConflictGroupOpts) + { + $CollList = array_intersect($ConflictGroupOpts, $ParsedIdOptionList); + if (count($CollList) > 1) + { + $OptPrintname = array_merge(array_keys(array_intersect($this->ShortOpts, $CollList)),array_keys(array_intersect($this->LongOpts, $CollList))); + sort($OptPrintname); + foreach ($OptPrintname as &$Value) + { + $Value = ((strlen($Value) == 1)? '-' : '--').$Value; + } + fwrite(STDERR, "Options " . implode(', ', $OptPrintname) . " can not be used together.".PHP_EOL); + fwrite(STDERR, $this->ErrorAddText); + exit($this->ErrorExitCode); + } + } + // в этом месте надо перебрать опции и вызвать обработчики + $CBC = []; + foreach ($ParsedOptions as $OptName => $Values) + { + $IdOption = $this->GetIdOption($OptName); + if (isset($this->CallbackOpts[$IdOption])) + { + if (is_callable($this->CallbackOpts[$IdOption], true)) + { + $CBC[$this->CallbackOptsOrder[$IdOption]][$IdOption] = $Values; + } + } + } + ksort($CBC); + foreach ($CBC as $IdOptions) + { + foreach ($IdOptions as $IdOption => $Values) + { + call_user_func($this->CallbackOpts[$IdOption], $Values, $ParsedOptions); + } + } + unset($CBC); + + //проверяем есть ли обработчик для аргументов и если есть, то выполняем + if (isset($this->CallbackArgs)) + { + if (is_callable($this->CallbackArgs, true)) + { + $ARGS = (isset($ParsedOptions['!ARGS']))? $ParsedOptions['!ARGS']: null; + call_user_func($this->CallbackArgs, $ARGS, $ParsedOptions); + } + } + //сохраняем обработанные опции + $this->ProcessedOptions = $ParsedOptions; + return $this->ProcessedOptions; + } + + public function SetArgsCallbackHandler(callable $CallbackHandler = null): void + { + $this->CallbackArgs = $CallbackHandler; + } + + public function SetArgsUseOnlyFirstArg(bool $val = true): void + { + $this->UseOnlyFirstArg = $val; + } + + protected function PrintWithWordWrap(string $text): void + { + + if($text == '') + { + return; + } + $pregsize = $this->GetScreenWidth() - 2; + preg_match_all('/(.{0,'.$pregsize.'})(\ |$)/', $text, $parts); + foreach ($parts[1] as $part) + { + if ($part == '') + { + continue; + } + echo $part, PHP_EOL; + } + } + + protected function GetScreenWidth():int + { + if (!isset($this->ScreenWidth)) + { + $WidthScreen = 80; + $stty = (array_map('trim',explode(';', shell_exec('stty -a')))); + foreach ($stty as $value) + { + if (strtok($value,' =') == 'columns') + { + $WidthScreen = (int) strtok(' ='); + break; + } + } + unset($stty, $value); + strtok('', ''); + $this->ScreenWidth = $WidthScreen; + } + return $this->ScreenWidth; + } +}