PageRenderTime 49ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Automation/ServiceUtils/Options.php

https://bitbucket.org/asohn_fandi/er-devops
PHP | 397 lines | 222 code | 46 blank | 129 comment | 47 complexity | 404819946703bbcae4e63a3f9a4bba03 MD5 | raw file
  1. <?php
  2. namespace Fie\Automation\ServiceUtils;
  3. use Fie\Automation\Service;
  4. final class Options
  5. {
  6. private $processed = false;
  7. private $knownOptionsGlobal = ['help', 'verbose', 'debug'];
  8. private $knownOptionsGlobalAPI = ['help', 'verbose'];
  9. private $knownOptionsBySection = [];
  10. private $options = [];
  11. private $flagValues = [];
  12. public function __construct() {
  13. if (Service::isApi()) {
  14. $this->knownOptionsGlobal = $this->knownOptionsGlobalAPI;
  15. }
  16. }
  17. /**
  18. * @return array
  19. */
  20. public function getKnownOptionsGlobal() {
  21. return $this->knownOptionsGlobal;
  22. }
  23. /**
  24. * @return array
  25. */
  26. final public static function getArgv() {
  27. $argv = array_key_exists('argv', $GLOBALS) ? $GLOBALS['argv'] : [];
  28. if (count($argv) > 0 && strpos($argv[0], 'phpunit') > -1) {
  29. return [];
  30. }
  31. if (count($argv) > 1 && strpos($argv[1], 'phpunit') > -1) {
  32. return [];
  33. }
  34. return $argv;
  35. }
  36. /**
  37. * Return raw assoc array of service options.
  38. *
  39. * @return array
  40. */
  41. public function getRawOptions() {
  42. return $this->options;
  43. }
  44. /**
  45. * Return raw assoc array of service flags.
  46. *
  47. * @return array
  48. */
  49. public function getRawFlagValues() {
  50. return $this->flagValues;
  51. }
  52. /**
  53. * @param array $knownOptionsBySection
  54. * @return Options $this
  55. */
  56. public function setKnownOptions(array $knownOptionsBySection = []) {
  57. $this->knownOptionsBySection = $knownOptionsBySection;
  58. return $this;
  59. }
  60. /**
  61. * Parse all web query string key/value pairs and command line arguments into one array.
  62. *
  63. * @param array|null $options
  64. * @throws OptionsException
  65. * @return Options $this
  66. */
  67. public function processOptions(?array $options = null) {
  68. if ($this->processed) {
  69. return $this;
  70. }
  71. $this->processed = true;
  72. if (isset($options)) {
  73. $params = [];
  74. foreach ($options as $option => $value) {
  75. if (strlen($value)) {
  76. $params[] = "--$option=$value";
  77. } else {
  78. $params[] = "--$option";
  79. }
  80. }
  81. $this->parseCommandLineOptions($params);
  82. } else {
  83. $argv = self::getArgv(); # assign to variable because array_splice() is destructive.
  84. if (Service::isPhpUnit()) {
  85. $commandLineOptions = $argv;
  86. } else {
  87. # drop script_name and service_name. Assumes script_name is not executable (e.g. `php script_name service_name`)
  88. $commandLineOptions = count($argv) > 1 ? array_splice($argv, 2) : []; // @codeCoverageIgnore
  89. }
  90. $queryStringOptions = $this->parseWebQueryStringIntoCliParameters();
  91. $this->parseCommandLineOptions(array_merge($queryStringOptions, $commandLineOptions));
  92. if (!array_key_exists('verbose', $this->flagValues)) {
  93. $this->flagValues['verbose'] = 0;
  94. }
  95. if ($this->getOptionFlag('debug')) {
  96. # debug implies verbose
  97. $this->options['verbose'] = null;
  98. $this->flagValues['verbose']++;
  99. }
  100. }
  101. if (!$this->getOptionFlag('help')) {
  102. $this->ensureKnownOptions();
  103. }
  104. $this->sanitizeOptionValues();
  105. return $this;
  106. }
  107. /**
  108. * Get value of option.
  109. *
  110. * @param string $optionName
  111. * @param string|null $default
  112. * @return string
  113. */
  114. public function getOption(string $optionName, ?string $default = null) {
  115. $optionName = strtolower($optionName);
  116. return array_key_exists($optionName, $this->options) ? $this->options[$optionName] : $default;
  117. }
  118. /**
  119. * Get boolean value of option.
  120. *
  121. * @param string $optionName
  122. * @param bool|null $default
  123. * @return bool|null
  124. */
  125. public function getOptionBoolean(string $optionName, ?bool $default = null) {
  126. $optionName = strtolower($optionName);
  127. $bool = array_key_exists($optionName, $this->options) ? strtolower($this->options[$optionName]) : null;
  128. if ($bool === 'true') {
  129. return true;
  130. } elseif ($bool === 'false') {
  131. return false;
  132. } else {
  133. return $default;
  134. }
  135. }
  136. /**
  137. * Get bool value of a given option.
  138. *
  139. * @param string $flagName
  140. * @return bool
  141. */
  142. public function getOptionFlag(string $flagName) {
  143. $flagName = strtolower($flagName);
  144. return array_key_exists($flagName, $this->options);
  145. }
  146. /**
  147. * Get the integer of a given option.
  148. *
  149. * @param string $flagName
  150. * @return int
  151. */
  152. public function getOptionFlagValue(string $flagName) {
  153. $flagName = strtolower($flagName);
  154. return array_key_exists($flagName, $this->flagValues) ? $this->flagValues[$flagName] : 0;
  155. }
  156. /**
  157. * Check service options for a given flag name and exit with error if it does not exist.
  158. *
  159. * @param string $flagName
  160. * @throws OptionsException
  161. * @return bool
  162. */
  163. public function requireFlag(string $flagName) {
  164. $flagName = strtolower($flagName);
  165. if (!array_key_exists($flagName, $this->options)) {
  166. $this->missingFlag([$flagName]);
  167. }
  168. return true;
  169. }
  170. /**
  171. * Check service options for a given option name and exit with error if value is missing.
  172. *
  173. * @param string $optionName
  174. * @param string $optionValuePlaceholder
  175. * @throws OptionsException
  176. * @return string|int value of option
  177. */
  178. public function requireOption(string $optionName, string $optionValuePlaceholder = 'VALUE') {
  179. $optionName = strtolower($optionName);
  180. if (!array_key_exists($optionName, $this->options) || strlen($this->options[$optionName]) === 0) {
  181. $this->missingOption([$optionName], $optionValuePlaceholder);
  182. }
  183. return $this->options[$optionName];
  184. }
  185. /**
  186. * Check service options and exit with error if value is missing
  187. *
  188. * @param array $optionNames
  189. * @param string $optionValuePlaceholder
  190. * @throws OptionsException
  191. * @return string|int value of option
  192. */
  193. public function requireOneOption(array $optionNames, string $optionValuePlaceholder = 'VALUE') {
  194. $lcOptionNames = array_map('strtolower', $optionNames);
  195. $lcOptionNames = array_filter($lcOptionNames); # remove empty elements
  196. $lcOneOption = array_filter($lcOptionNames, function ($optionName) {
  197. return !empty($this->options[$optionName]);
  198. });
  199. $lcOneOption = array_values($lcOneOption); # reindex
  200. if (count($lcOneOption) != 1) {
  201. $this->missingOption($optionNames, $optionValuePlaceholder);
  202. }
  203. /** @var string[] $lcOneOption */
  204. return $this->options[ $lcOneOption[0] ];
  205. }
  206. /**
  207. * Report missing flag.
  208. *
  209. * @param array $flagNames
  210. * @throws OptionsException
  211. */
  212. private function missingFlag(array $flagNames) {
  213. throw new OptionsException("Missing flag '" . join("' or '", $flagNames) . "'");
  214. }
  215. /**
  216. * Report missing option.
  217. *
  218. * @param array $optionNames
  219. * @param string $optionValue
  220. * @throws OptionsException
  221. */
  222. private function missingOption(array $optionNames, string $optionValue) {
  223. throw new OptionsException("Missing key/value pair " . join("=$optionValue or ", $optionNames) . "=$optionValue");
  224. }
  225. /**
  226. * PHP's getopt() will stop parsing command line arguments when it encounters something that is't defined by
  227. * its $options or $longopts. This one doesn't care and we'll ensure options are defined later in ensureKnownOptions()
  228. *
  229. * Parses $GLOBALS['argv'] (or other array, i.e. $_GET) for parameters and assigns them to an array.
  230. *
  231. * Supports:
  232. * -e
  233. * -e <value>
  234. * --long-param
  235. * --long-param=<value>
  236. * --long-param <value>
  237. * <value>
  238. *
  239. * @param array $params List of parameters to parse
  240. * @return Options $this
  241. */
  242. private function parseCommandLineOptions(array $params) {
  243. $options = [];
  244. $flagValues = [];
  245. for ($position = 0; $position < count($params); $position++) {
  246. $original = $params[$position];
  247. $descriptor = $params[$position];
  248. if ($descriptor{0} != '-') {
  249. # param doesn't belong to any option. `<value>`
  250. $options[] = $original;
  251. continue;
  252. }
  253. $descriptor = substr($descriptor, 1);
  254. $value = null;
  255. if ($descriptor{0} == '-') {
  256. # long-opt. `--<param>`
  257. $descriptor = substr($descriptor, 1);
  258. if (strpos($original, '=') !== false) {
  259. # value specified inline. `--<param>=<value>`
  260. list($descriptor, $value) = explode('=', $descriptor, 2);
  261. if ($value === '') {
  262. $value = null;
  263. }
  264. }
  265. }
  266. # check if next parameter is a descriptor or a value. long param `--<param>` or short param `-<param>`
  267. if ($value === null && array_key_exists($position+1, $params) && $params[$position+1]{0} != '-') {
  268. $value = $params[++$position];
  269. }
  270. $descriptor = strtolower($descriptor);
  271. $options[$descriptor] = $value;
  272. if ($value === null) {
  273. @$flagValues[$descriptor]++; # suppress warning, index will be defined if it doesn't exist.
  274. }
  275. }
  276. $this->options = $options;
  277. $this->flagValues = $flagValues;
  278. return $this;
  279. }
  280. /**
  281. * Convert query string into command line arguments
  282. *
  283. * @return array
  284. */
  285. private function parseWebQueryStringIntoCliParameters() {
  286. $queryString = array_key_exists('QUERY_STRING', $_SERVER) ? $_SERVER['QUERY_STRING'] : '';
  287. $options = [];
  288. if (strlen($queryString) == 0) {
  289. return $options;
  290. }
  291. $kvPairs = explode('&', $queryString);
  292. foreach ($kvPairs as $kvPair) {
  293. $kvPair = strpos($kvPair, '=') > -1 ? explode('=', $kvPair) : [$kvPair, ''];
  294. list($key, $value) = $kvPair;
  295. $key = ltrim($key, '-');
  296. if (strlen($key) == 1) {
  297. $options[] = "-$key";
  298. } else {
  299. if (strlen($value) > 0) {
  300. $value = "=$value";
  301. }
  302. $options[] = "--$key$value";
  303. }
  304. }
  305. return $options;
  306. }
  307. /**
  308. * Fail if we encounter an unknown option.
  309. *
  310. * @throws OptionsException
  311. * @return Options $this
  312. */
  313. private function ensureKnownOptions() {
  314. if (count($this->knownOptionsBySection) == 0) {
  315. return $this;
  316. }
  317. $knownOptions = [];
  318. foreach ($this->knownOptionsBySection as $sectionName => $options) {
  319. foreach ($options as $optionName => $optionDesc) {
  320. if (!is_int($optionName) && is_string($optionDesc)) {
  321. $optionName = preg_replace('/=.*$/', '', $optionName);
  322. $knownOptions[] = strtolower($optionName);
  323. }
  324. }
  325. }
  326. $knownOptions = array_unique(array_merge($this->knownOptionsGlobal, $knownOptions));
  327. foreach (array_keys($this->options) as $optionName) {
  328. if (strlen($optionName) > 0 && !in_array($optionName, $knownOptions)) {
  329. throw new OptionsException(sprintf("Unknown option '%s'", $optionName));
  330. }
  331. }
  332. return $this;
  333. }
  334. /**
  335. * Manipulate values of options to be a single string without whitespace for safe use in shell commands.
  336. *
  337. * @return $this
  338. */
  339. private function sanitizeOptionValues() {
  340. foreach ($this->options as $descriptor => $value) {
  341. $value = filter_var($value, FILTER_SANITIZE_STRING);
  342. while (preg_match('/%\d\d/', $value)) {
  343. $value = urldecode($value);
  344. }
  345. $value = trim($value);
  346. $value = preg_replace('/ .*$/', '', $value);
  347. $this->options[$descriptor] = $value;
  348. }
  349. return $this;
  350. }
  351. }