WP_CLI

SynopsisParser{}WP-CLI 1.0

Generate a synopsis from a command's PHPdoc arguments. Turns something like "<object-id>..." into [ optional=>false, type=>positional, repeating=>true, name=>object-id ]

No Hooks.

Usage

$SynopsisParser = new SynopsisParser();
// use class methods

Methods

  1. private static classify_token( $token )
  2. private static is_optional( $token )
  3. private static is_repeating( $token )
  4. public static parse( $synopsis )
  5. public static render( &$synopsis )

SynopsisParser{} code WP-CLI 2.8.0-alpha

class SynopsisParser {

	/**
	 * @param string $synopsis A synopsis
	 * @return array List of parameters
	 */
	public static function parse( $synopsis ) {
		$tokens = array_filter( preg_split( '/[\s\t]+/', $synopsis ) );

		$params = [];
		foreach ( $tokens as $token ) {
			$param = self::classify_token( $token );

			// Some types of parameters shouldn't be mandatory
			if ( isset( $param['optional'] ) && ! $param['optional'] ) {
				if ( 'flag' === $param['type'] || ( 'assoc' === $param['type'] && $param['value']['optional'] ) ) {
					$param['type'] = 'unknown';
				}
			}

			$param['token'] = $token;
			$params[]       = $param;
		}

		return $params;
	}

	/**
	 * Render the Synopsis into a format string.
	 *
	 * @param array $synopsis A structured synopsis. This might get reordered
	 *                        to match the parsed output.
	 * @return string Rendered synopsis.
	 */
	public static function render( &$synopsis ) {
		if ( ! is_array( $synopsis ) ) {
			return '';
		}
		$bits               = [
			'positional' => '',
			'assoc'      => '',
			'generic'    => '',
			'flag'       => '',
		];
		$reordered_synopsis = [
			'positional' => [],
			'assoc'      => [],
			'generic'    => [],
			'flag'       => [],
		];
		foreach ( $bits as $key => &$value ) {
			foreach ( $synopsis as $arg ) {
				if ( empty( $arg['type'] )
					|| $key !== $arg['type'] ) {
					continue;
				}

				if ( empty( $arg['name'] ) && 'generic' !== $arg['type'] ) {
					continue;
				}

				if ( 'positional' === $key ) {
					$rendered_arg = "<{$arg['name']}>";

					$reordered_synopsis['positional'] [] = $arg;
				} elseif ( 'assoc' === $key ) {
					$arg_value = isset( $arg['value']['name'] ) ? $arg['value']['name'] : $arg['name'];
					$arg_value = "=<{$arg_value}>";

					if ( ! empty( $arg['value']['optional'] ) ) {
						$arg_value = "[{$arg_value}]";
					}

					$rendered_arg = "--{$arg['name']}{$arg_value}";

					$reordered_synopsis['assoc'] [] = $arg;
				} elseif ( 'generic' === $key ) {
					$rendered_arg = '--<field>=<value>';

					$reordered_synopsis['generic'] [] = $arg;
				} elseif ( 'flag' === $key ) {
					$rendered_arg = "--{$arg['name']}";

					$reordered_synopsis['flag'] [] = $arg;
				}
				if ( ! empty( $arg['repeating'] ) ) {
					$rendered_arg = "{$rendered_arg}...";
				}
				if ( ! empty( $arg['optional'] ) ) {
					$rendered_arg = "[{$rendered_arg}]";
				}
				$value .= "{$rendered_arg} ";
			}
		}
		$rendered = '';
		foreach ( $bits as $v ) {
			if ( ! empty( $v ) ) {
				$rendered .= $v;
			}
		}

		$synopsis = array_merge(
			$reordered_synopsis['positional'],
			$reordered_synopsis['assoc'],
			$reordered_synopsis['generic'],
			$reordered_synopsis['flag']
		);

		return rtrim( $rendered, ' ' );
	}

	/**
	 * Classify argument attributes based on its syntax.
	 *
	 * @param string $token
	 * @return array
	 */
	private static function classify_token( $token ) {
		$param = [];

		list( $param['optional'], $token )  = self::is_optional( $token );
		list( $param['repeating'], $token ) = self::is_repeating( $token );

		$p_name  = '([a-z-_0-9]+)';
		$p_value = '([a-zA-Z-_|,0-9]+)';

		if ( '--<field>=<value>' === $token ) {
			$param['type'] = 'generic';
		} elseif ( preg_match( "/^<($p_value)>$/", $token, $matches ) ) {
			$param['type'] = 'positional';
			$param['name'] = $matches[1];
		} elseif ( preg_match( "/^--(?:\\[no-\\])?$p_name/", $token, $matches ) ) {
			$param['name'] = $matches[1];

			$value = substr( $token, strlen( $matches[0] ) );

			// substr returns false <= PHP 5.6, and '' PHP 7+
			if ( false === $value || '' === $value ) {
				$param['type'] = 'flag';
			} else {
				$param['type'] = 'assoc';

				list( $param['value']['optional'], $value ) = self::is_optional( $value );

				if ( preg_match( "/^=<$p_value>$/", $value, $matches ) ) {
					$param['value']['name'] = $matches[1];
				} else {
					$param = [
						'type' => 'unknown',
					];
				}
			}
		} else {
			$param['type'] = 'unknown';
		}

		return $param;
	}

	/**
	 * An optional parameter is surrounded by square brackets.
	 *
	 * @param string $token
	 * @return array
	 */
	private static function is_optional( $token ) {
		if ( '[' === substr( $token, 0, 1 ) && ']' === substr( $token, -1 ) ) {
			return [ true, substr( $token, 1, -1 ) ];
		}

		return [ false, $token ];
	}

	/**
	 * A repeating parameter is followed by an ellipsis.
	 *
	 * @param string $token
	 * @return array
	 */
	private static function is_repeating( $token ) {
		if ( '...' === substr( $token, -3 ) ) {
			return [ true, substr( $token, 0, -3 ) ];
		}

		return [ false, $token ];
	}
}