GithubHelp home page GithubHelp logo

annotated-command's Introduction

Consolidation\AnnotatedCommand

Initialize Symfony Console commands from annotated/attributed command class methods.

CI scrutinizer codecov license

Component Status

Currently in use in Robo (1.x+), Drush (9.x+) and Terminus (1.x+).

Motivation

Symfony Console provides a set of classes that are widely used to implement command line tools. Increasingly, it is becoming popular to use annotations to describe the characteristics of the command (e.g. its arguments, options and so on) implemented by the annotated method.

Extant commandline tools that utilize this technique include:

This library provides routines to produce the Symfony\Component\Console\Command\Command from all public methods defined in the provided class.

Note If you are looking for a very fast way to write a Symfony Console-base command-line tool, you should consider using Robo, which is built on top of this library, and adds additional conveniences to get you going quickly. Use g1a/starter to quickly scaffold a new commandline tool. See Using Robo as a Framework. It is possible to use this project without Robo if desired, of course.

Library Usage

This is a library intended to be used in some other project. Require from your composer.json file:

    "require": {
        "consolidation/annotated-command": "^4"
    },

Example Annotated Command Class

The public methods of the command class define its commands, and the parameters of each method define its arguments and options. If a parameter has a corresponding "@option" annotation in the docblock, then it is an option; otherwise, it is an argument.

class MyCommandClass
{
    /**
     * This is the my:echo command
     *
     * This command will concatenate two parameters. If the --flip flag
     * is provided, then the result is the concatenation of two and one.
     *
     * @command my:echo
     * @param string $one The first parameter.
     * @param string $two The other parameter.
     * @param bool $flip The "flip" option
     * @option flip Whether or not the second parameter should come first in the result.
     * @aliases c
     * @usage bet alpha --flip
     *   Concatenate "alpha" and "bet".
     */
    public function myEcho($one, $two, $flip = false)
    {
        if ($flip) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }
}

or via PHP 8 attributes.

    #[CLI\Name(name: 'my:echo', aliases: ['c'])]
    #[CLI\Help(description: 'This is the my:echo command', synopsis: "This command will concatenate two parameters. If the --flip flag\nis provided, then the result is the concatenation of two and one.",)]
    #[CLI\Param(name: 'one', description: 'The first parameter')]
    #[CLI\Param(name: 'two', description: 'The other parameter')]
    #[CLI\Option(name: 'flip', description: 'Whether or not the second parameter should come first in the result.')]
    #[CLI\Usage(name: 'bet alpha --flip', description: 'Concatenate "alpha" and "bet".')]
    public function myEcho($one, $two = '', $flip = false)
    {
        if ($options['flip']) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }

Legacy Annotated Command Methods

The legacy method for declaring commands is still supported. When using the legacy method, the command options, if any, are declared as the last parameter of the methods. The options will be passed in as an associative array; the default options of the last parameter should list the options recognized by the command. The rest of the parameters are arguments. Parameters with a default value are optional; those without a default value are required.

class MyCommandClass
{
    /**
     * This is the my:echo command
     *
     * This command will concatenate two parameters. If the --flip flag
     * is provided, then the result is the concatenation of two and one.
     *
     * @command my:echo
     * @param integer $one The first parameter.
     * @param integer $two The other parameter.
     * @param array $options An option that takes multiple values.
     * @option flip Whether or not the second parameter should come first in the result.
     * @aliases c
     * @usage bet alpha --flip
     *   Concatenate "alpha" and "bet".
     */
    public function myEcho($one, $two, $options = ['flip' => false])
    {
        if ($options['flip']) {
            return "{$two}{$one}";
        }
        return "{$one}{$two}";
    }
}

Option Default Values

The $options array must be an associative array whose key is the name of the option, and whose value is one of:

  • The boolean value false, which indicates that the option takes no value.
  • A string containing the default value for options that may be provided a value, but are not required to.
  • The special value InputOption::VALUE_REQUIRED, which indicates that the user must provide a value for the option whenever it is used.
  • The special value InputOption::VALUE_OPTIONAL, which produces the following behavior:
    • If the option is given a value (e.g. --foo=bar), then the value will be a string.
    • If the option exists on the commandline, but has no value (e.g. --foo), then the value will be true.
    • If the option does not exist on the commandline at all, then the value will be null.
    • If the user explicitly sets --foo=0, then the value will be converted to false.
    • LIMITATION: If any Input object other than ArgvInput (or a subclass thereof) is used, then the value will be null for both the no-value case (--foo) and the no-option case. When using a StringInput, use --foo=1 instead of --foo to avoid this problem.
  • The special value true produces the following behavior:
    • If the option is given a value (e.g. --foo=bar), then the value will be a string.
    • If the option exists on the commandline, but has no value (e.g. --foo), then the value will be true.
    • If the option does not exist on the commandline at all, then the value will also be true.
    • If the user explicitly sets --foo=0, then the value will be converted to false.
    • If the user adds --no-foo on the commandline, then the value of foo will be false.
  • An empty array, which indicates that the option may appear multiple times on the command line.

No other values should be used for the default value. For example, $options = ['a' => 1] is incorrect; instead, use $options = ['a' => '1'].

Default values for options may also be provided via the @default annotation. See hook alter, below.

Hooks

Commandfiles may provide hooks in addition to commands. A commandfile method that contains a @hook annotation is registered as a hook instead of a command. The format of the hook annotation is:

@hook type target

The hook type determines when during the command lifecycle this hook will be called. The available hook types are described in detail below.

The hook target specifies which command or commands the hook will be attached to. There are several different ways to specify the hook target.

  • The command's primary name (e.g. my:command) or the command's method name (e.g. myCommand) will attach the hook to only that command.
  • An annotation (e.g. @foo) will attach the hook to any command that is annotated with the given label.
  • If the target is specified as *, then the hook will be attached to all commands.
  • If the target is omitted, then the hook will be attached to every command defined in the same class as the hook implementation.

There are ten types of hooks in the command processing request flow:

  • Command Event (Symfony)
    • @pre-command-event
    • @command-event
    • @post-command-event
  • Option
    • @pre-option
    • @option
    • @post-option
  • Initialize (Symfony)
    • @pre-init
    • @init
    • @post-init
  • Interact (Symfony)
    • @pre-interact
    • @interact
    • @post-interact
  • Validate
    • @pre-validate
    • @validate
    • @post-validate
  • Command
    • @pre-command
    • @command
    • @post-command
  • Process
    • @pre-process
    • @process
    • @post-process
  • Alter
    • @pre-alter
    • @alter
    • @post-alter
  • Status
    • @status
  • Extract
    • @extract

In addition to these, there are two more hooks available:

The "pre" and "post" varieties of these hooks, where available, give more flexibility vis-a-vis hook ordering (and for consistency). Within one type of hook, the running order is undefined and not guaranteed. Note that many validate, process and alter hooks may run, but the first status or extract hook that successfully returns a result will halt processing of further hooks of the same type.

Each hook has an interface that defines its calling conventions; however, any callable may be used when registering a hook, which is convenient if versions of PHP prior to 7.0 (with no anonymous classes) need to be supported.

Command Event Hook

The command-event hook is called via the Symfony Console command event notification callback mechanism. This happens prior to event dispatching and command / option validation. Note that Symfony does not allow the $input object to be altered in this hook; any change made here will be reset, as Symfony re-parses the object. Changes to arguments and options should be done in the initialize hook (non-interactive alterations) or the interact hook (which is naturally for interactive alterations).

Option Event Hook

The option event hook (OptionHookInterface) is called for a specific command, whenever it is executed, or its help command is called. Any additional options for the command may be added here by calling the addOption method of the provided $command object. Note that the option hook is only necessary for calculating dynamic options. Static options may be added via the @option annotation on any hook that uses them. See the Alter Hook documentation below for an example.

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Command\Command;

/**
 * @hook option some:command
 */
public function additionalOption(Command $command, AnnotationData $annotationData)
{
    $command->addOption(
        'dynamic',
        '',
        InputOption::VALUE_NONE,
        'Option added by @hook option some:command'
    );
}

Initialize Hook

The initialize hook (InitializeHookInterface) runs prior to the interact hook. It may supply command arguments and options from a configuration file or other sources. It should never do any user interaction.

The consolidation/config project (which is used in Robo PHP) uses @hook init to automatically inject values from config.yml configuration files for options that were not provided on the command line.

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    $value = $input->getOption('some-option');
    if (!$value) {
        $input->setOption('some-option', $this->generateRandomOptionValue());
    }
}

You may alter the AnnotationData here by using simple array syntax. Below, we add an additional display field label for a Property List.

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    $annotationData['field-labels'] .= "\n" . "new_field: My new field";
}

Alternately, you may use the set() or append() methods on the AnnotationData class.

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;

/**
 * @hook init some:command
 */
public function initSomeCommand(InputInterface $input, AnnotationData $annotationData)
{
    // Add a line to the field labels.
    $annotationData->append('field-labels', "\n" . "new_field: My new field");
    // Replace all field labels.
    $annotationData->set('field-labels', "one_field: My only field");

}

Interact Hook

The interact hook (InteractorInterface) runs prior to argument and option validation. Required arguments and options not supplied on the command line may be provided during this phase by prompting the user. Note that the interact hook is not called if the --no-interaction flag is supplied, whereas the command-event hook and the init hook are.

use Consolidation\AnnotatedCommand\AnnotationData;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
 * @hook interact some:command
 */
public function interact(InputInterface $input, OutputInterface $output, AnnotationData $annotationData)
{
    $io = new SymfonyStyle($input, $output);

    // If the user did not specify a password, then prompt for one.
    $password = $input->getOption('password');
    if (empty($password)) {
        $password = $io->askHidden("Enter a password:", function ($value) { return $value; });
        $input->setOption('password', $password);
    }
}

Validate Hook

The purpose of the validate hook (ValidatorInterface) is to ensure the state of the targets of the current command are usabe in the context required by that command. Symfony has already validated the arguments and options prior to this hook. It is possible to alter the values of the arguments and options if necessary, although this is better done in the configure hook. A validation hook may take one of several actions:

  • Do nothing. This indicates that validation succeeded.
  • Return a CommandError. Validation fails, and execution stops. The CommandError contains a status result code and a message, which is printed.
  • Throw an exception. The exception is converted into a CommandError.
  • Return false. Message is empty, and status is 1. Deprecated.

The validate hook may change the arguments and options of the command by modifying the Input object in the provided CommandData parameter. Any number of validation hooks may run, but if any fails, then execution of the command stops.

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook validate some:command
 */
public function validatePassword(CommandData $commandData)
{
    $input = $commandData->input();
    $password = $input->getOption('password');

    if (strpbrk($password, '!;$`') === false) {
        throw new \Exception("Your password MUST contain at least one of the characters ! ; ` or $, for no rational reason whatsoever.");
    }
}

Command Hook

The command hook is provided for semantic purposes. The pre-command and command hooks are equivalent to the post-validate hook, and should confirm to the interface (ValidatorInterface). All of the post-validate hooks will be called before the first pre-command hook is called. Similarly, the post-command hook is equivalent to the pre-process hook, and should implement the interface (ProcessResultInterface).

The command callback itself (the method annotated @command) is called after the last command hook, and prior to the first post-command hook.

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook pre-command some:command
 */
public function preCommand(CommandData $commandData)
{
    // Do something before some:command
}

/**
 * @hook post-command some:command
 */
public function postCommand($result, CommandData $commandData)
{
    // Do something after some:command
}

Process Hook

The process hook (ProcessResultInterface) is specifically designed to convert a series of processing instructions into a final result. An example of this is implemented in Robo in the CollectionProcessHook class; if a Robo command returns a TaskInterface, then a Robo process hook will execute the task and return the result. This allows a pre-process hook to alter the task, e.g. by adding more operations to a task collection.

The process hook should not be used for other purposes.

use Consolidation\AnnotatedCommand\CommandData;

/**
 * @hook process some:command
 */
public function process($result, CommandData $commandData)
{
    if ($result instanceof MyInterimType) {
        $result = $this->convertInterimResult($result);
    }
}

Alter Hook

An alter hook (AlterResultInterface) changes the result object. Alter hooks should only operate on result objects of a type they explicitly recognize. They may return an object of the same type, or they may convert the object to some other type.

If something goes wrong, and the alter hooks wishes to force the command to fail, then it may either return a CommandError object, or throw an exception.

use Consolidation\AnnotatedCommand\CommandData;

/**
 * Demonstrate an alter hook with an option
 *
 * @hook alter some:command
 * @option $alteration Alter the result of the command in some way.
 * @usage some:command --alteration
 */
public function alterSomeCommand($result, CommandData $commandData)
{
    if ($commandData->input()->getOption('alteration')) {
        $result[] = $this->getOneMoreRow();
    }

    return $result;
}

If an option needs to be provided with a default value, that may be done via the @default annotation.

use Consolidation\AnnotatedCommand\CommandData;

/**
 * Demonstrate an alter hook with an option that has a default value
 *
 * @hook alter some:command
 * @option $name Give the result a name.
 * @default $name George
 * @usage some:command --name=George
 */
public function nameSomeCommand($result, CommandData $commandData)
{
    $result['name'] = $commandData->input()->getOption('name')

    return $result;
}

Status Hook

DEPRECATED

Instead of using a Status Determiner hook, commands should simply return their exit code and result data separately using a CommandResult object.

The status hook (StatusDeterminerInterface) is responsible for determing whether a command succeeded (status code 0) or failed (status code > 0). The result object returned by a command may be a compound object that contains multiple bits of information about the command result. If the result object implements ExitCodeInterface, then the getExitCode() method of the result object is called to determine what the status result code for the command should be. If ExitCodeInterface is not implemented, then all of the status hooks attached to this command are executed; the first one that successfully returns a result will stop further execution of status hooks, and the result it returned will be used as the status result code for this operation.

If no status hook returns any result, then success is presumed.

Extract Hook

DEPRECATED

See RowsOfFieldsWithMetadata in output-formatters for an alternative that is more flexible for most use cases.

The extract hook (ExtractOutputInterface) is responsible for determining what the actual rendered output for the command should be. The result object returned by a command may be a compound object that contains multiple bits of information about the command result. If the result object implements OutputDataInterface, then the getOutputData() method of the result object is called to determine what information should be displayed to the user as a result of the command's execution. If OutputDataInterface is not implemented, then all of the extract hooks attached to this command are executed; the first one that successfully returns output data will stop further execution of extract hooks.

If no extract hook returns any data, then the result object itself is printed if it is a string; otherwise, no output is emitted (other than any produced by the command itself).

On-Event hook

Commands can define their own custom events; to do so, they need only implement the CustomEventAwareInterface, and use the CustomEventAwareTrait. Event handlers for each custom event can then be defined using the on-event hook.

A handler using an on-event hook looks something like the following:

/**
 * @hook on-event custom-event
 */
public function handlerForCustomEvent(/* arbitrary parameters, as defined by custom-event */)
{
    // do the needful, return what custom-event expects
}

Then, to utilize this in a command:

class MyCommands implements CustomEventAwareInterface
{
    use CustomEventAwareTrait;

    /**
     * @command my-command
     */
    public myCommand($options = [])
    {
        $handlers = $this->getCustomEventHandlers('custom-event');
        // iterate and call $handlers
    }
}

It is up to the command that defines the custom event to declare what the expected parameters for the callback function should be, and what the return value is and how it should be used.

Replace Command Hook

The replace-command (ReplaceCommandHookInterface) hook permits you to replace a command's method with another method of your own.

For instance, if you'd like to replace the foo:bar command, you could utilize the following code:

<?php
class MyReplaceCommandHook  {

  /**
   * @hook replace-command foo:bar
   *
   * Parameters must match original command method.
   */
  public function myFooBarReplacement($value) {
    print "Hello $value!";
  }
}

Output

If a command method returns an integer, it is used as the command exit status code. If the command method returns a string, it is printed.

If the Consolidation/OutputFormatters project is used, then users may specify a --format option to select the formatter to use to transform the output from whatever form the command provides to a string. To make this work, the application must provide a formatter to the AnnotatedCommandFactory. See API Usage below.

Logging

The Annotated-Command project is completely agnostic to logging. If a command wishes to log progress, then the CommandFile class should implement LoggerAwareInterface, and the Commandline tool should inject a logger for its use via the LoggerAwareTrait setLogger() method. Using Robo is recommended.

Access to Symfony Objects

If you want to use annotations, but still want access to the Symfony Command, e.g. to get a reference to the helpers in order to call some legacy code, you may create an ordinary Symfony Command that extends \Consolidation\AnnotatedCommand\AnnotatedCommand, which is a \Symfony\Component\Console\Command\Command. Omit the configure method, and place your annotations on the execute() method.

It is also possible to add InputInterface and/or OutputInterface parameters to any annotated method of a command file (the parameters must go before command arguments).

Parameter Injection

Just as this library will by default inject $input and/or $output at the head of the parameter list of any command function, it is also possible to add a handler to inject other objects as well.

Given an implementation of SymfonyStyleInjector similar to the example below:

use Consolidation\AnnotatedCommand\ParameterInjector

class SymfonyStyleInjector implements ParameterInjector
{
    public function get(CommandData $commandData, $interfaceName)
    {
        return new MySymfonyStyle($commandData->input(), $commandData->output());
    }
}

Then, an instance of 'MySymfonyStyle' will be provided to any command handler method that takes a SymfonyStyle parameter if the SymfonyStyleInjector is registered in your application's initialization code like so:

$commandProcessor->parameterInjection()->register('Symfony\Component\Console\Style\SymfonyStyle', new SymfonyStyleInjector);

The following classes are available to be injected into the command method by default:

  • Symfony\Component\Console\Input\InputInterface
  • Symfony\Component\Console\Output\OutputInterface
  • Consolidation\AnnotatedCommand\AnnotationData
  • Consolidation\OutputFormatters\Options\FormatterOptions

Note that these instances are also available via the CommandData object passed to most command hooks.

Handling Standard Input

Any Symfony command may use the provided StdinHandler to imlement commands that read from standard input.

  /**
   * @command example
   * @option string $file
   * @default $file -
   */
  public function example(InputInterface $input)
  {
      $data = StdinHandler::selectStream($input, 'file')->contents();
  }

This example will read all of the data available from the stdin stream into $data, or, alternately, will read the entire contents of the file specified via the --file=/path option.

For more details, including examples of using the StdinHandle with a DI container, see the comments in StdinHandler.php.

API Usage

If you would like to use Annotated Commands to build a commandline tool, it is recommended that you use Robo as a framework, as it will set up all of the various command classes for you. If you would like to integrate Annotated Commands into some other framework, see the sections below.

Set up Command Factory and Instantiate Commands

To use annotated commands in an application, pass an instance of your command class in to AnnotatedCommandFactory::createCommandsFromClass(). The result will be a list of Commands that may be added to your application.

$myCommandClassInstance = new MyCommandClass();
$commandFactory = new AnnotatedCommandFactory();
$commandFactory->setIncludeAllPublicMethods(true);
$commandFactory->commandProcessor()->setFormatterManager(new FormatterManager());
$commandList = $commandFactory->createCommandsFromClass($myCommandClassInstance);
foreach ($commandList as $command) {
    $application->add($command);
}

You may have more than one command class, if you wish. If so, simply call AnnotatedCommandFactory::createCommandsFromClass() multiple times.

If you do not wish every public method in your classes to be added as commands, use AnnotatedCommandFactory::setIncludeAllPublicMethods(false), and only methods annotated with @command will become commands.

Note that the setFormatterManager() operation is optional; omit this if not using Consolidation/OutputFormatters.

A CommandInfoAltererInterface can be added via AnnotatedCommandFactory::addCommandInfoAlterer(); it will be given the opportunity to adjust every CommandInfo object parsed from a command file prior to the creation of commands.

Command File Discovery

A discovery class, CommandFileDiscovery, is also provided to help find command files on the filesystem. Usage is as follows:

$discovery = new CommandFileDiscovery();
$myCommandFiles = $discovery->discover($path, '\Drupal');
foreach ($myCommandFiles as $myCommandClass) {
    $myCommandClassInstance = new $myCommandClass();
    // ... as above
}

For a discussion on command file naming conventions and search locations, see #12.

If different namespaces are used at different command file paths, change the call to discover as follows:

$myCommandFiles = $discovery->discover(['\Ns1' => $path1, '\Ns2' => $path2]);

As a shortcut for the above, the method discoverNamespaced() will take the last directory name of each path, and append it to the base namespace provided. This matches the conventions used by Drupal modules, for example.

Configuring Output Formatts (e.g. to enable wordwrap)

The Output Formatters project supports automatic formatting of tabular output. In order for wordwrapping to work correctly, the terminal width must be passed in to the Output Formatters handlers via FormatterOptions::setWidth().

In the Annotated Commands project, this is done via dependency injection. If a PrepareFormatter object is passed to CommandProcessor::addPrepareFormatter(), then it will be given an opportunity to set properties on the FormatterOptions when it is created.

A PrepareTerminalWidthOption class is provided to use the Symfony Application class to fetch the terminal width, and provide it to the FormatterOptions. It is injected as follows:

$terminalWidthOption = new PrepareTerminalWidthOption();
$terminalWidthOption->setApplication($application);
$commandFactory->commandProcessor()->addPrepareFormatter($terminalWidthOption);

To provide greater control over the width used, create your own PrepareTerminalWidthOption subclass, and adjust the width as needed.

Other Callbacks

In addition to the hooks provided by the hook manager, there are additional callbacks available to alter the way the annotated command library operates.

Factory Listeners

Factory listeners are notified every time a command file instance is used to create annotated commands.

public function AnnotatedCommandFactory::addListener(CommandCreationListenerInterface $listener);

Listeners can be used to construct command file instances as they are provided to the command factory.

Option Providers

An option provider is given an opportunity to add options to a command as it is being constructed.

public function AnnotatedCommandFactory::addAutomaticOptionProvider(AutomaticOptionsProviderInterface $listener);

The complete CommandInfo record with all of the annotation data is available, so you can, for example, add an option --foo to every command whose method is annotated @fooable.

CommandInfo Alterers

CommandInfo alterers can adjust information about a command immediately before it is created. Typically, these will be used to supply default values for annotations custom to the command, or take other actions based on the interfaces implemented by the commandfile instance.

public function alterCommandInfo(CommandInfo $commandInfo, $commandFileInstance);

annotated-command's People

Contributors

agentrickard avatar alexeyshockov avatar alexpott avatar andypost avatar bwood avatar dependencies[bot] avatar dieterholvoet avatar frol-kr avatar garak avatar grasmash avatar greg-1-anderson avatar gunjanpatel avatar jeroeny avatar joachim-n avatar joestewart avatar kporras07 avatar peter279k avatar rodrigoaguilera avatar stevector avatar sweetchuck avatar weitzman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

annotated-command's Issues

Confusion of setting up a mode and default values

Probably, it could be easily argued that it's confusing because I am new to this library and at the first place didn't know what i was doing back then. However I think that this thing is worth to mention that the way you define the mode and default values of options for the command is a bit confusing.

As an example, if you want to have a default value to be equal 2, there are two ways (as far as I know) of doing so. The first is through the annotation which works fine and the second one is through the default values of arguments of a function which could behave as you may not expect.

Ex.
I started to use this lib with robo framework, where I wanted to run my tests in two threads as a default arg.

public function parallelRunTest(array $opts = ['threads|t' => 2, 'suite|s' => 'api'])

Here I wanted the threads to be equal 2 as default value but it turns out that 2 (phpInputOption::VALUE_REQUIRED) is equal a constant that specifies that this field is required, the other option will work though. So the only way is to do it though the annotation like this:

/**
* @option int $threads Number of threads to use for running the tests
* @default int $threads 2
*/
public function parallelRunTest(array $opts = ['threads|t' => null, 'suite|s' => 'api'])

adding arguments dynamically

I see in the docs I can dynamically add an option in the option hook with:

$command->addOption()

Is there a way to add an argument? Symfony command has an addArgument() method, but I don't see how to get hold of that from somewhere like an interact hook, which is where I'd want to add it based on input.

how does the option hook declare a default value for an extra option?

I'm going by this example in Drush:

  /**
   * When a hook extends a command with additional options, it must
   * implement declare those option(s) in a @hook option like this one.  Doing so will add
   * the option to the help text for the modified command, and will also
   * allow the new option to be specified on the command line.  Without
   * this, Drush will fail with an error when a user attempts to use
   * an unknown option.
   *
   * @hook option sql-sync
   * @option http-sync Copy the database via http instead of rsync.  Value is the url that the existing database dump can be found at.
   * @option http-sync-user Username for the protected directory containing the sql dump.
   * @option http-sync-password Password for the same directory.
   */
  public function optionsetSqlSync() {}

There doesn't seem to be a way for a default value for the extra option to be defined.

Having it set in the $options parameter for the command being enhanced causes a crash.

I think it would make sense to have an $options parameter in the hook function declaration.

Prevent running a hook

I'm programmatically creating a command that will override an annotated command (@command foo) already added in application. But the annotated command has also a validate hook (@hook validate foo). With the new command the hook makes no sense. Is there a way to disable it?

HookReplaceCommandInterface.php does not exist

Steps to reproduce

Read the documentation and clicked on the link to HookReplaceCommandInterface.php

Expected behavior

The file should have opened for viewing and reference

Actual behavior

404

The Acquia BLT docs for replacing a command point to the section of the README with a short example and a link to the interface to reference. This file apparently does not exist anymore. This makes it difficult to know what to do in order to replace a command or if that is even still an option.

@usage descriptions not parsed

The README.md gives the following example implementation of the @Usage annotation:

     * @usage bet alpha --flip
     *   Concatenate "alpha" and "bet".

When executing a command with this annotation, the following is output:

my:cat bet alpha --flip

The description Concatenate "alpha" and "bet". does not appear.

Expand hook system for arbitrary events (e.g. Drush cache-clear types)

I'm looking at porting cache-* commands. How do you suggest we implement the feature of cache-clear where other commandfiles can alter in their own cache types. For example, views alters in the views cache as a selection during cache-clear. Would be nice to do away with the old Drush hook system for this.

README mentions non-existing "configure hook"

The README mentions the following in the Validate hook section:

It is possible to alter the values of the arguments and options if necessary, although this is better done in the configure hook.

But there is no mention of a "configure hook" anywhere in the documentation. Which hook is intended to alter the options before executing the command?

Recurse @hook

Steps to reproduce

/**
     * @hook command bar
     */
    public function foo()
    {
        $this->writeln('foo');
    }

    /**
     * @hook command baz
     */
    public function bar()
    {
        $this->writeln('bar');
    }

    public function baz()
    {
        $this->writeln('baz');
    }

Expected behavior

robo baz should output

foo
bar
baz

And go even further so that Robo could build a big tree of tasks.
In general this task-hook-system could become a bit more detailled.
For example: I have several things to do before the build but they need to be in some semi-specific order (like @hook pre-command build 0 for priority).

But this issue is about recursing all the way to each hook
or finding an alternative way/word for doing so

Actual behavior

Tell us what happens instead

baz
bar

allow init hooks to have an $output parameter

As in the example in the docs, I am using the init hook to set a some option values to defaults if they were not given on the command line.

What I'd like to do when this happens is notify the user, .e.g:

Foo option has been set to 'bar'.

However, the hook doesn't appear to allow an $output parameter.

what order are hooks of the same type called in?

Is there a defined order for hooks of the same type, eg interact?

In particular, within a single commands class, is it:

  • the order they are defined in the code
  • alphabetical by method name
  • arbitrary?

Docs should specify the hook machine names

The readme lists the hooks by a human name, eg 'Option Event Hook', and links to the interface, which is great.

But it doesn't seem to say anywhere that this hook must be annotated as '@hook option'.

These machine names should be in the Readme text.

Maybe they could also be annotated in some way on the interface?

@param name needs to start with $

The name of a @param needs to start with a $. Consider relaxing that. Tripped me up for a while. If not possible, throw an exception when param name is empty to alert the command author.

command discovery produces the wrong namespace

Steps to reproduce

I set up a composer package to provide a Drush command in this way, as explained in the Drush docs:

Nested (e.g. Commandfile is part of a Composer package)

    Filename: $PROJECT_ROOT/drush/Commands/dev_modules/ExampleCommands.php
    Namespace: Drush\Commands\dev_modules

Expected behavior

The command file is picked up.

Actual behavior

The command file is not picked up.

The array of file data returned by discovery has this:

  "/Users/joachim/Sites/_sandbox/drupal-code-builder-drush-9x/CodeBuilderDevCommands.php" => "\Drush\CodeBuilderDevCommands"

The class name that is in the array value is incorrect -- it should be: Drush\Commands\code_builder_commands\CodeBuilderDevCommands

The reason for that is that discoverCommandFilesInLocation() makes assumptions about the namespace based on the filepath:

            $relativePathName = $file->getRelativePathname();
            $relativeNamespaceAndClassname = str_replace(
                ['/', '.php'],
                ['\\', ''],
                $relativePathName
            );

log the command about to be run

See drush-ops/drush#4151

It would be useful if Drush's debug output said the command class and method that was about to be run.

CommandProcessor has LoggerAwareTrait, so it can log, but Drush uses constants from Drush\Log\LogLevel, so I don't know how we do that over here.

Important change in hook replace-command

We ran into an issue with the replace-command hook. As an example, our command class looks like this:

<?php

namespace Drupal\example\Commands;

use Drush\Commands\DrushCommands;

/**
 * A Drush commandfile to overwrite some of the drush commands.
 */
class ExampleCommands extends DrushCommands {

  /**
   * @hook replace-command default-content-access:import-module
   */
  public function contentImportModule($module, $options = ['update-existing' => FALSE]): void {
    print 'Disabled on this site!';
  }

  /**
   * @hook replace-command locale:import
   */
  public function import($langcode, $file, $options = ['type' => self::OPT, 'override' => self::OPT]) {
    print 'Disabled on this site!';
  }

}

This worked just fine for several years now but started to fail yesterday. TBH, I don't know which component has changed to make this fail but the error message is

In InputDefinition.php line 232:
  [Symfony\Component\Console\Exception\LogicException]  
  An option named "type" already exists.                

The stack trace is

  at /var/www/html/vendor/symfony/console/Input/InputDefinition.php:232
 Symfony\Component\Console\Input\InputDefinition->addOption() at /var/www/html/vendor/consolidation/annotated-command/src/AnnotatedCommand.php:206
 Consolidation\AnnotatedCommand\AnnotatedCommand->addOptions() at /var/www/html/vendor/consolidation/annotated-command/src/AnnotatedCommand.php:263
 Consolidation\AnnotatedCommand\AnnotatedCommand->optionsHookForHookAnnotations() at /var/www/html/vendor/consolidation/annotated-command/src/Hooks/Dispatchers/OptionsHookDispatcher.php:31
 Consolidation\AnnotatedCommand\Hooks\Dispatchers\OptionsHookDispatcher->getOptions() at /var/www/html/vendor/consolidation/annotated-command/src/CommandProcessor.php:154
 Consolidation\AnnotatedCommand\CommandProcessor->optionsHook() at /var/www/html/vendor/consolidation/annotated-command/src/AnnotatedCommand.php:255
 Consolidation\AnnotatedCommand\AnnotatedCommand->optionsHook() at /var/www/html/vendor/consolidation/annotated-command/src/Options/AlterOptionsCommandEvent.php:81
 Consolidation\AnnotatedCommand\Options\AlterOptionsCommandEvent->findAndAddHookOptions() at /var/www/html/vendor/consolidation/annotated-command/src/Options/AlterOptionsCommandEvent.php:72
 Consolidation\AnnotatedCommand\Options\AlterOptionsCommandEvent->alterCommandOptions() at /var/www/html/vendor/symfony/event-dispatcher/EventDispatcher.php:214
 Symfony\Component\EventDispatcher\EventDispatcher->doDispatch() at /var/www/html/vendor/symfony/event-dispatcher/EventDispatcher.php:44
 Symfony\Component\EventDispatcher\EventDispatcher->dispatch() at /var/www/html/vendor/consolidation/annotated-command/src/Hooks/Dispatchers/CommandEventHookDispatcher.php:30
 Consolidation\AnnotatedCommand\Hooks\Dispatchers\CommandEventHookDispatcher->callCommandEventHooks() at /var/www/html/vendor/consolidation/annotated-command/src/Hooks/HookManager.php:424
 Consolidation\AnnotatedCommand\Hooks\HookManager->callCommandEventHooks() at /var/www/html/vendor/symfony/event-dispatcher/EventDispatcher.php:214
 Symfony\Component\EventDispatcher\EventDispatcher->doDispatch() at /var/www/html/vendor/symfony/event-dispatcher/EventDispatcher.php:44
 Symfony\Component\EventDispatcher\EventDispatcher->dispatch() at /var/www/html/vendor/symfony/console/Application.php:1007
 Symfony\Component\Console\Application->doRunCommand() at /var/www/html/vendor/symfony/console/Application.php:255
 Symfony\Component\Console\Application->doRun() at /var/www/html/vendor/symfony/console/Application.php:148
 Symfony\Component\Console\Application->run() at /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php:118
 Drush\Runtime\Runtime->doRun() at /var/www/html/vendor/drush/drush/src/Runtime/Runtime.php:49
 Drush\Runtime\Runtime->run() at /var/www/html/vendor/drush/drush/drush.php:72
 require() at /var/www/html/vendor/drush/drush/includes/preflight.inc:18
 drush_main() at phar:///usr/local/bin/drush/bin/drush.php:141
 require() at /usr/local/bin/drush:10

When removing the options parameter from our overwrite function, then it works just fine. But that contradicts the documentation: Parameters must match original command method.

Better handling of help command

Steps to reproduce

drush help is a common mistake when user meant to type drush list. See https://drupal.stackexchange.com/questions/249793/how-to-list-all-the-commands-in-drush-9-aka-drush-help

Expected behavior

Run drush list. If not possible, give better error message.

Actual behavior

Error:

[Symfony\Component\Console\Exception\CommandNotFoundException]                                 
  Command  was not found. Pass --root or a @siteAlias in order to run Drupal-specific commands.  

Debugging

I think things go awry at https://github.com/consolidation/annotated-command/blob/master/src/Options/AlterOptionsCommandEvent.php#L65. $commandToDescribe is null.

clarify in docs how to give an annotation for a hook

@hook type commandname|annotation

The commandname may be the command's primary name (e.g. my:command), it's method name (e.g. myCommand) or any of its aliases.

If an annotation is given instead, then this hook function will run for all commands with the specified annotation.

It would be helpful here to explicitly say that 'annotation' is of the form '@foo' rather than just 'foo'.

Allow alteration of field-labels

Steps to reproduce

Writing a command that simply adds data to another command, and with the PropertyList output, it doesn't seem to have a clear way to add new field data.

Expected behavior

Inside the @hook init, I would expect to be able to alter $annotationData with a setter. e.g.

  /**
   * Registers additional information to domain:info.
   *
   * @hook init domain:info
   */
  public function initDomainInfo(InputInterface $input, AnnotationData $annotationData) {
    $annotationData->set('field-labels', ['new' => 'New field']);
  }

However, in my use case, it's not really set() it's append(), so I'm not certain what the best approach would be.

Actual behavior

Here's the method I have to use now, leveraging exchangeArray to swap out data.

  /**
   * Registers additional information to domain:info.
   *
   * @hook init domain:info
   */
  public function initDomainInfo(InputInterface $input, AnnotationData $annotationData) {
    // To add a field label, we have to swap out the annotation array.
    $iterator = $annotationData->getIterator();
    $new = [];
    // Simple while loop
    while ($iterator->valid()) {
      $new[$iterator->key()] = $iterator->current();
      if ($iterator->key() == 'field-labels') {
        $new[$iterator->key()] .= "\n" . 'domain_access: Domain Access';
      }
      $iterator->next();
    }
    // $annotationData is an \ArrayObject, so we can exchange.
    $annotationData->exchangeArray($new);
  }

System Configuration

n/a

Options provided by 'options' hook don't appear in command help

Steps to reproduce

Implement an options hook:

class StepCommandOptions {

  /**
   * @hook option @stepoption
   * @option global-list Global list blah.
   */
  public function stepOptions();

In the command desiring the options from the hook add this annotation:

  /**
   * @stepoption
   *
   * @command test:step-command
   *
   * @option bool $list List the steps for this command.
   */
  public function stepCommand($options = [
    'list' => FALSE,
  ]) {

In the application file use code like that in Terminus.php to make the hook code available to the application:

/**
   * Add the commands and hooks which are shipped with core Wps
   *
   * @param $container
   */
  private function addBuiltInCommandsAndHooks($container) {

    // Add the built in commands.
    $commands = $this->getCommands([
      'path' => __DIR__ . '/Commands',
      'namespace' => 'Wps\Console\Commands',
    ]);

    // Add the built in hooks
    $hooks = [
      'Wps\Console\Hooks\StepCommandOptions',
    ];
    $this->commands = array_merge($commands, $hooks);
  }

Expected behavior

I should see 'global-list' in the output of wps test:step-command -h

Actual behavior

$ php bin/wps.php test:step-command -h
Usage:
  test:step-command [options]

Options:
      --list            List the steps for this command.
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

System Configuration

Darwin Kernel Version 15.6.0

Allow FormatterOptions to be altered by hooks

Goal

Dynamically alter the way output is formatted in a post-process hook, e.g. to dynamically select the default fields based on the command output.

Expected behavior

The FormatterOptions should be available via a method of CommandData.

Actual behavior

FormatterOptions is created in a protected method CommandProcessor::writeUsingFormatter() immediately before it is used.

Cancel a command via hook.

Forgive me for not using the issue template, but I think this is more a question/idea than a specific bug.

I am wondering if there is some ability to cancel a command via a hook? I am using a library that is using robo, and I am able to hook into a command, and I thought I could use something like the validate hook to check for some condition, and ultimately see if I want to allow my command to run based on certain criteria. If the criteria is met, I would love to be able to cancel the command execution, print a helpful message, but still return a successful exit code.

This does not seem possible with validation, as you can only return a CommandError or throw an exception, both which end up in the whole process being killed.

Thoughts? Thanks in advance for any help or ideas....

Custom help command

Having separate short and long form strings for command information would be useful.

The short, existing string should be displayed when commands are listed. The long form string should be displayed when a user asks for help with the command.

The annotation would look like this:

* @description Short description of the command.
* @help This text is output when the user types `command --help` and is intended to provided more detailed information.

How to set options programmatically in command so they are available to post-command hook?

This is technically a support request, so maybe this is not the right place to file this. I didn't see in the contributing guidelines anything about support requests, or I missed it.

I am using the replace-command hook to override the recipes:multisite:init command provided as part of BLT. I'm also using the post-command hook to run some additional operations. Within the replace-command method, how do I set option values so that they will be available in the post-command hook? In the post-command hook, there is a CommandData object that I can lookup options in, but there doesn't seem to be a corresponding way inside the command itself to set options in a way that they later show up in the CommandData object.

I'm probably missing something obvious. Any help is appreciated.

Thanks,

Sean

Default option values not being properly set

Steps to reproduce

I created a command class with the following function:

public function initialize($options = ['project-dir' => __DIR__, 'gitignore' => TRUE]) {

I ran app category:initialize to run the command.

Expected behavior

The $options['gitignore'] would be set to TRUE

Actual behavior

The $options['gitignore'] was set to FALSE

System Configuration

php 5.5.38, MacOS Sierra

shorter names for options

Is there a way to specify a shorter version of an option?

For example, the way that 'git clean' has both --dry-run and -n.

Symfony 2.8.18 breaks '@hook option'

This arguably is not a bug with annotated-command. I'm just reporting and linking to symfony/console#30 in case this info is useful to others.

Steps to reproduce

  1. Create an application with an annotated command which gets 1 option from the normal @option annotation and another option from a @hook option implementation.
  2. Pin symfony/console to 2.8.17 in your composer.json and run composer update.
  3. Observe that both options work in with your command.
  4. Pin symfony/console to 2.8.18 in your composer.json and run composer update.
  5. Observe that the option implemented with the hook stops working. If you set a break point in your command you can see that the value of the "hooked in" option is not updated when the option is passed.

This problem is also present when I test with symfony/console 3.2.

Notes

Here's the PR for symfony/console 2.8.18.

dsnopek writes:

In the Symfony Console PR it appears the inputBound property is used to prevent $input->bind() from getting called multiple times.
However... this actually seems a little off. I don't know the Symfony Console code very well, but it seems to me that this would fail to bind and probably hide validation errors if the arguments/options failed to validate for some reason. This is because the first time $input->bind() is called, it just swallows any exceptions, but sets the inputBound property regardless. The 2nd time $input->bind() is called it does show validation errors, but it won't actually try to bind a 2nd time because inputBound is set. At least that's my interpretation just from looking at the code.

I'm guessing that 'annotated-command' is updating the commands definition in the event, and was expecting Symfony Console to do the 2nd $input->bind() with the new definition. But since the inputBound property is set, it won't run it the 2nd time.

Firstly, I think this is a bug in Symfony Console. If $input->setInputBound(true) only happens if the first bind is successful, then things work as expected. See this PR:
symfony/console#30

Or, possibly, I think the 'annotated-command' project could bind the $input to the new definition itself rather than depending on Symfony Console to do it later? I'm not sure which project will be easier to convince that there is something to fix.

System Configuration

  • php 5.6
  • symfony/console 2.8.18

README doens't mention * as possible hook target

The README section on hooks doesn't say that the target can be *. This is used in Drush, for instance:

    /**
     * Print druplicon as post-command output.
     *
     * @hook post-command *
     */
    public function druplicon($result, CommandData $commandData)

Allow commands to call other commands

I would like to be able to calls other commands from my annotated command using an annotation. E.g.,

<?php

  /**
   * @command tests:all
   *
   * @calls tests:behat
   * @calls tests:phpunit
   */
  public function testsAll() {
  }

  /**
   * @command tests:behat
   * @validateme
   */
  public function testsBehat() {
    ...
  }

In this example, using @calls tests:behat would cause the tests:behat command to be executed prior the the body of testsAll().

This is better than simply calling $this->testsBehat() from $this->testsAll() because it will invoke the hooks for $this->testsBehat().

Standard naming conventions and search locations for command files

Currently, the CommandFileDiscovery class searches for command files named *Commands.php in directories named CliTools (or src/CliTools).

In Drush, I tried an alternative search pattern, and currently look for command files named "*CommandFile.php" in directories named CommandFiles (or src/CommandFiles).

Ideally, we should decide on a single standard default that both Drush and DrupalConsole can use, so that command files are found consistently. At the moment, I am leaning towards the second option, and calling these literally CommandFiles, as Drush is currently doing. Other conventions are possible, of course; anyone with opinions on the subject should weigh in.

createFinder() should set followLinks() on the symfony finder

Steps to reproduce

I created a symlink to a command folder into my drush command location folder.

Expected behavior

The command should be picked up.

Actual behavior

It's not, because Symfony finder doens't follow symlinks by default.

System Configuration

OS X 10.11.6.

Symfony 5.2

Is there a reason that it's not allowed to use this with symfony/console v 5.2?
"symfony/console": "^4.4.8|~5.1.0",

Single-command app

Steps to reproduce

There's a way to create a single-command tool in the Symfony Console with such line:

$app->setDefaultCommand('my-command', TRUE);

to use the tool like this:

./my-command arguments

Unfortunately, this case is skipped in the Annotated Command.

Expected behavior

The command should be executed normally.

Actual behavior

The first argument, inputted in the command line, is skipped when my-command method is called.

System Configuration

Linux Mint 18.3 64-bit, PHP 7.2.2-3+ubuntu16.04.1+deb.sury.org+1 ( NTS )

@return on plugin command methods is unforgiving

From pantheon-systems/terminus#1503
Added to Jira in BUGS-1138

Expected behavior

The following two command declarations should behave identically:

/**
 * @command foo:bar
 * @field-labels
 *   foo: Foo Label
 *   bar: Bar Label
 * @return PropertyList
 */
public function fooBar() {
  // Returns the following property list in a formatted table.
  return new PropertyList(['foo' => 'foo val', 'bar' => 'bar val']);
}
/**
 * @command foo:bar
 * @field-labels
 *   foo: Foo Label
 *   bar: Bar Label
 * @return PropertyList Comment for myself in code.
 */
public function fooBar() {
  // Returns an unformatted list of the values below.
  return new PropertyList(['foo' => 'foo val', 'bar' => 'bar val']);
}

Actual behavior

When authoring a plugin for terminus 1.x, if my @return declaration in my docblock includes comments after the return type of RowsOfFields or PropertyList, the returned output is not formatted as expected.

Steps to reproduce the behavior

See above.

hook basics need a little improvement

Commandfiles may provide hooks in addition to commands. A commandfile method that contains a @hook annotation is registered as a hook instead of a command.

  1. The first sentence says "in addition to commands" where the second sentence says "instead of". Which one?
  2. I feel it needs to be a mentioned that hooks are loaded on every request. This is weird because normal command classes, as far as I understand these things (which is not that far) are not.

Support @inheritdoc

Is there a way we can support phpdoc inheritance: https://www.phpdoc.org/docs/latest/guides/inheritance.html

All of @inheritdoc, @inheritDoc, {@inheritdoc} and {@inheritDoc} would need to be supported.
The variants with the {} are used to import the docblock of the parent in the current docblock.

We're looking to create a package with a RoboFileBase for our CI tool. Each project can have a RoboFile that extends that class and can overwrite the commands. It would be nice if we could use the docblock inheritance to provide the documentation on the command line.

Document variable amount of arguments

This was briefly discussed on Twitter:
https://twitter.com/sebreghtsjelle/status/835124401616076801

/**
 * Documentation for mynamespace:my-command
 * 
 * @param array $arguments
 *   Variable amount of arguments. Last 2 arguments are SSH username and password.
 *   All remaining (first x) arguments are servers to execute a command on.
 */
public function mynamespaceMyCommand(array $arguments) {
}

How would we document individual arguments for this command?
Might be related to #69.

init hook and command-event

Steps to reproduce

  1. I tried to use the initialize hook per README.md
  2. I tried to use command-event hook

Expected behavior

Both should work

Actual behavior

  1. Initialize hook is actually called init. Lets fix docs.
  2. I was not able to get command-event to fire for a Drush command in quick testing.

System Configuration

OSX 10.11

Actual required PHP version is ^7.0.22 now

Steps to reproduce

  1. Use Composer to install a version of Annotated Command ≤2.4.11 using 5.4.0 ≤ PHP < 7.0.22
  2. Attempt to update to the newest version

Expected behavior

The update should work without trouble on any PHP version ^5.4.0

Actual behavior

The requirement for the dependency phpdocumentor/reflection-docblock ^4.0 is PHP ^7.0; Composer complains and fails accordingly

System Configuration

Mac OS 10.12.6, PHP 5.5.38

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.