Search code examples
phpsymfonycommand-line-argumentsdocopt

Merge two string of CLI options


How can I merge two strings that contain conventional CLI options (i.e. anything getopt() would parse correctly.)?

The first is pulled from a config file, and the second is from Symfony's Console\Input\getArgument(). I could also get the second one from $GLOBALS['argv']. I want to combine them, so that I can launch another program with them.

Either string could contain short options, long options, both with or without values.

Example

e.g., config.yml contains

phpunit_arguments: -d xdebug.mode=off --columns=115

...and then the user can call my program with php {filename} --columns=95 --debug. I want to merge those strings, so that I end up with:

-d xdebug.mode=off --columns=95 --debug

The columns from the first string was overridden by the one from the second.

Failed Attempt 1: Converting to arrays

If I can get those strings into arrays like the following, then I can just use array_merge_recursive():

array(
    '-d xdebug.mode' => 'off',
    '--columns'      => 115
)

array(
     '--columns' => 95,
     '--debug'
)

...but to do that I need a parser that understand CLI options.

I've looked at the following, but none seem to be designed to take an arbitrary string and return a parsed array.

Failed Attempt 2: Concatenating

I tried just concatenating the two strings instead of merging them, and that technically works, but it creates UX problems. My program displays the args to the users, and concat'd string would contain duplicates, which would be confusing for some. The program also accepts input while it's running, and regenerates the options; over time, appending to the prior string would snowball and worse the confusion/UX.

e.g., after setting groups a few times, it'd end up as

-d xdebug.mode=off --columns=95 --debug --groups=database --groups=file --groups=console


Solution

  • You can create your own function that will smart merge configuration parameters and CLI arguments. Using a single regular expression, we can extract pre-signs like - or --, command names and equality character with the value.

    Merging arguments using regular expressions and loops

    Please read inline comments

    <?php
    
    echo "PHP ".phpversion().PHP_EOL;
    
    // $config = yaml_parse_file('config.yml');
    
    // Load a JSON file instead of YAML, because repl.it
    // does not have php_yaml extension enabled
    $json = file_get_contents("config.json");
    $config = json_decode($json, TRUE);
    
    $phpunit_arguments = explode(' ', $config['phpunit_arguments']);
    
    echo "phpunit_arguments:\n";
    print_r($phpunit_arguments);
    
    // Merge the params
    $params = mergeConfigParameters($phpunit_arguments);
    
    // Concatenate and print all params
    echo "cli args:\n";
    echo implode(' ', $params).PHP_EOL;
    
    function mergeConfigParameters($config_params) {
      $argv = $GLOBALS['argv'];
      array_shift($argv); // Remove script file from CLI arguments
    
      if (empty($argv)) {
        // If we run the file without CLI arguments,
        // apply some fake argv arguments for demontration
        $argv = [
          '-d',
          'xdebug.mode=on',
          '--columns=95',
          '--debug',
          '--groups=database',
          '--groups=file',
          '--groups=console'
        ];
      }
    
      echo "argv:\n";
      print_r($argv);
    
      // Merge all parameters, CLI arguments and from config
      $all_params = array_merge($config_params, $argv);
    
      echo "all_params:\n";
      print_r($all_params);
    
      // We'll save all the params here using assoc array
      // to identify and handle/override duplicate commands
      $params = [];
    
      foreach ($all_params as $param) {
        // This regex will match everything:
        // -d
        // xdebug.mode=off
        // --columns=95
        // and create 4 groups:
        // 1: the pre char(s), - or --
        // 2: the cmd, actual command
        // 3: the eq char, =
        // 4: the value
        if (preg_match('/^(-[-]?)?([\w.]+)(=?)(.*)/', $param, $matches)) {
          // Destructure matches
          [, $pre, $cmd, $eq, $value] = $matches;
          $param = [
            'pre' => $pre,
            'cmd' => $cmd,
            'eq' => $eq,
            'value' => $value
          ];
    
          // If the command is set, merge it with the previous,
          // else add it to $params array
          if (isset($params[$cmd])) {
            $params[$cmd] = array_merge($params[$cmd], $param);
          } else {
            $params[$cmd] = $param;
          }
        }
      }
    
      $merged = [];
    
      // Loop throu all unique params and re-build the commands
      foreach ($params as $param) {
        [
          'pre' => $pre,
          'cmd' => $cmd,
          'eq' => $eq,
          'value' => $value
        ] = $param;
    
        if (!empty($pre)) {
          $cmd = $pre.$cmd;
        }
    
        if (!empty($eq)) {
          $cmd .= $eq;
    
          if (!empty($value)) {
            $cmd .= $value;
          }
        }
    
        $merged[] = $cmd;
      }
    
      echo "merged:\n";
      print_r($merged);
    
      // Finally we have all unique commands compiled again
      return $merged;
    }
    

    Result

    Running this command:

    php main.php -d xdebug.mode=on --columns=95 --debug --groups=database --groups=file --groups=console
    

    with this config.yml:

    phpunit_arguments: -d xdebug.mode=off --columns=115 --number=1234
    

    will output this:

    PHP 7.2.24-0ubuntu0.18.04.7
    phpunit_arguments:
    Array
    (
        [0] => -d
        [1] => xdebug.mode=off
        [2] => --columns=115
        [3] => --number=1234
    )
    argv:
    Array
    (
        [0] => -d
        [1] => xdebug.mode=on
        [2] => --columns=95
        [3] => --debug
        [4] => --groups=database
        [5] => --groups=file
        [6] => --groups=console
    )
    all_params:
    Array
    (
        [0] => -d
        [1] => xdebug.mode=off
        [2] => --columns=115
        [3] => --number=1234
        [4] => -d
        [5] => xdebug.mode=on
        [6] => --columns=95
        [7] => --debug
        [8] => --groups=database
        [9] => --groups=file
        [10] => --groups=console
    )
    merged:
    Array
    (
        [0] => -d
        [1] => xdebug.mode=on
        [2] => --columns=95
        [3] => --number=1234
        [4] => --debug
        [5] => --groups=console
    )
    cli args:
    -d xdebug.mode=on --columns=95 --number=1234 --debug --groups=console
    

    So we can see that the arguments -d xdebug.mode=off and --columns=115 have been merged and override be the CLI arguments -d xdebug.mode=on --columns=95 and also we have only the last one --groups=console set.

    Run it

    You can check this parsing working online onto this repl.it.