diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb1f52a..751e7b7 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,11 +11,11 @@ jobs: if: "!contains(github.event.head_commit.message, 'skip ci')" name: PHP ${{ matrix.php-versions }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.php-versions == '8.1' }} + continue-on-error: ${{ matrix.php-versions >= '8.4' }} strategy: fail-fast: false matrix: - php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] + php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] os: [ubuntu-latest, windows-latest] steps: @@ -24,7 +24,7 @@ jobs: run: git config --system core.autocrlf false; git config --system core.eol lf - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up PHP ${{ matrix.php-versions }} uses: shivammathur/setup-php@v2 @@ -32,9 +32,6 @@ jobs: php-version: ${{ matrix.php-versions }} ini-values: date.timezone=Europe/Berlin - - name: Setup Problem Matchers for PHP - run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" - - name: Validate composer.json and composer.lock run: composer validate @@ -43,7 +40,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2.1.3 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -51,7 +48,7 @@ jobs: - name: Install dependencies run: > - curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.5.3.sh > xp-run && + curl -sSL https://baltocdn.com/xp-framework/xp-runners/distribution/downloads/e/entrypoint/xp-run-8.8.0.sh > xp-run && composer install --prefer-dist && echo "vendor/autoload.php" > composer.pth diff --git a/ChangeLog.md b/ChangeLog.md index 4a00ab3..c564369 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,8 @@ Commands ChangeLog ## ?.?.? / ????-??-?? +* Added PHP 8.2, 8.3 and 8.4 to the test matrix - @thekid + ## 11.0.0 / 2021-10-21 * Implemented xp-framework/rfc#341, dropping compatibility with XP 9 diff --git a/src/main/php/util/cmd/ParamString.class.php b/src/main/php/util/cmd/ParamString.class.php index 2bae90f..20d43e5 100755 --- a/src/main/php/util/cmd/ParamString.class.php +++ b/src/main/php/util/cmd/ParamString.class.php @@ -1,4 +1,6 @@ setParams(null === $list ? $_SERVER['argv'] : $list); } + /** + * Parses a command line using UN*X-style quoting + * + * @param ?string $line + * @return self + */ + public static function parse($line) { + return new self('' === ($line ?? '') ? [] : CommandLine::$UNIX->parse($line)); + } + /** * Set the parameter string * diff --git a/src/main/php/xp/command/CmdRunner.class.php b/src/main/php/xp/command/CmdRunner.class.php index 907787f..3ce0a7e 100755 --- a/src/main/php/xp/command/CmdRunner.class.php +++ b/src/main/php/xp/command/CmdRunner.class.php @@ -1,9 +1,9 @@ getSimpleName()."\n\n"; $text= ''; } else { - @list($headline, $text)= explode("\n", $comment, 2); - $markdown= '# '.ltrim($headline, ' #')."\n\n"; + $p= strcspn($comment, ".\n"); + $markdown= '# '.ltrim(substr($comment, 0, $p), ' #')."\n\n"; + $text= substr($comment, $p); } $markdown.= "- Usage\n ```sh\n$ xp cmd ".Commands::nameOf($class); @@ -122,7 +123,7 @@ protected function listCommands() { $commandsIn= function($package) { $markdown= ''; foreach ($package->getClasses() as $class) { - if ($class->isSubclassOf('util.cmd.Command') && !Modifiers::isAbstract($class->getModifiers())) { + if ($class->isSubclassOf(Command::class) && !Modifiers::isAbstract($class->getModifiers())) { $markdown.= ' $ xp cmd '.$class->getSimpleName()."\n"; } } @@ -143,6 +144,61 @@ protected function listCommands() { Help::render(self::$err, $markdown, []); } + /** + * Starts repl + * + * @param util.cmd.Config $config + * @return void + */ + protected function startRepl($config) { + foreach (Commands::allPackages() as $package) { + self::$out->writeLine("\e[33m@", $package, "\e[0m"); + } + self::$out->writeLine('XP Command REPL. Use "ls" to list commands, "exit" to exit'); + self::$out->writeLine("\e[36m", str_repeat('═', 72), "\e[0m"); + foreach ($config->sources() as $source) { + self::$out->writeLine('Config: ', $source); + } + + $prompt= (getenv('USERNAME') ?: getenv('USER') ?: posix_getpwuid(posix_geteuid())['name']).'@'.gethostname(); + $exit= 0; + do { + self::$out->write("\n\e[", 0 === $exit ? '44' : '41', ";1;37m", $prompt, " cmd \$\e[0m "); + + $command= $args= null; + sscanf(self::$in->readLine(), "%[^ ] %[^\r]", $command, $args); + switch ($command) { + case 'exit': return; + case null: $exit= 0; break; + case 'ls': + foreach (array_merge(Commands::allPackages(), [Package::forName(null)]) as $package) { + foreach ($package->getClasses() as $class) { + if ($class->isSubclassOf(Command::class) && !Modifiers::isAbstract($class->getModifiers())) { + self::$out->write("* \e[37m", $class->getSimpleName(), "\e[0m"); + if ($comment= $class->getComment()) { + self::$out->writeLine(" - \e[3m", substr($comment, 0, strcspn($comment, ".\n")), "\e[0m"); + } else { + self::$out->writeLine(); + } + } + } + } + $exit= 0; + break; + + // Treat any other input as a command name + default: + try { + $exit= $this->runCommand($command, ParamString::parse($args), $config); + } catch (\Throwable $e) { + self::$err->writeLine(Throwable::wrap($e)); + $exit= 255; + } + break; + } + } while (true); + } + /** * Main method * @@ -152,12 +208,6 @@ protected function listCommands() { */ public function run(ParamString $params, Config $config= null) { - // No arguments given - show our own usage - if ($params->count < 1) { - $this->selfUsage(); - return 1; - } - // Configure properties $config || $config= new Config(); @@ -181,10 +231,10 @@ public function run(ParamString $params, Config $config= null) { break 2; } - // Sanity check + // Without class: Start REPL if (!$params->exists($offset)) { - self::$err->writeLine('*** Missing classname'); - return 1; + $this->startRepl($config); + return 0; } // Use default path for config if no sources set diff --git a/src/test/php/util/cmd/unittest/CmdRunnerTest.class.php b/src/test/php/util/cmd/unittest/CmdRunnerTest.class.php index a6d8028..f51dcb6 100755 --- a/src/test/php/util/cmd/unittest/CmdRunnerTest.class.php +++ b/src/test/php/util/cmd/unittest/CmdRunnerTest.class.php @@ -8,9 +8,9 @@ class CmdRunnerTest extends AbstractRunnerTest { /** @return xp.command.AbstractRunner */ protected function runner() { return new CmdRunner(); } - #[Test, Values([[[]], [['-?']]])] - public function selfUsage($args) { - $return= $this->runWith($args); + #[Test] + public function selfUsage() { + $return= $this->runWith(['-?']); $this->assertEquals(1, $return); $this->assertOnStream($this->err, 'xp cmd [class]'); $this->assertEquals('', $this->out->bytes()); @@ -35,4 +35,11 @@ public function longClassUsage() { $this->assertEquals('', $this->out->bytes()); $this->assertFalse($command->wasRun()); } + + #[Test] + public function startRepl() { + $return= $this->runWith([], 'exit'); + $this->assertEquals(0, $return); + $this->assertOnStream($this->out, 'XP Command REPL'); + } } \ No newline at end of file