+<?php
+//V2.9b
+define('AOP_NOTUSED', 0);
+define('AOP_NOTREQUIRED', 1);
+define('AOP_REQUIRED', 2);
+
+class ArgsOptsProcessor
+{
+ protected array $ProcessedOptions;
+ protected int $CounterOptId = 0;
+ protected array $ShortOpts = [];
+ protected array $LongOpts = [];
+ protected array $IdOpts = [];
+ protected array $DescriptionOpts = [];
+ protected array $ValueModeOpts = [];
+ protected array $DefaultValueOpts = [];
+ protected array $ConflictGroups = [];
+ protected array $IsRequiredList = [];
+ protected array $IsOneList = [];
+ protected array $UseOnlyFirstValue = [];
+ protected array $CallbackOpts = [];
+ protected array $CallbackOptsOrder = [];
+ protected string $ProgrammDescription = '';
+ protected string $HelpTextTop = '';
+ protected string $HelpTextBottom = '';
+ protected bool $UseOnlyFirstArg = false;
+ protected $CallbackArgs;
+ protected bool $InternalHelp = false;
+ protected int $ScreenWidth;
+ protected int $ErrorExitCode = 0;
+ protected string $ErrorAddText = '';
+
+ public function HelpTopic()
+ {
+ $WidthScreen = $this->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;
+ }
+}