system-commandline-cli

Par github · awesome-copilot

Utilisez cette compétence lors de l'ajout, de la modification ou de la révision de commandes CLI dans un projet .NET construit avec System.CommandLine. Les déclencheurs incluent : la création d'une nouvelle commande CLI, l'ajout d'options ou d'arguments, le câblage de gestionnaires de commandes, l'enregistrement de sous-commandes, la construction de groupes de commandes, ou toute décision d'architecture concernant la structure des commandes CLI. Utilisez-la également lorsque l'utilisateur mentionne `System.CommandLine`, `CommandBase`, `SetAction`, `ParseResult`, `RootCommand`, `subcommand`, ou demande à ajouter un verbe à la CLI. N'utilisez PAS cette compétence pour du code C# général, des API web, du travail sur l'interface utilisateur ou des projets sans CLI.

npx skills add https://github.com/github/awesome-copilot --skill system-commandline-cli

System.CommandLine CLI Developer Skill

Vous travaillez sur une application CLI .NET construite avec System.CommandLine v2.x.x, ciblant .NET 8 ou ultérieur ou toute implémentation .NET Standard 2.0, incluant .NET Framework 4.6.1 ou ultérieur et .NET Core 2.0 ou ultérieur. Suivez strictement ces règles et patrons lors de la création ou de la modification de commandes CLI.


Vue d'ensemble de l'architecture

<CLI Project>/
├── Program.cs                       # Point d'entrée et invocation des commandes
└── Commands/
    ├── CommandBase.cs               # Classe de base pour toutes les commandes
    ├── GlobalOptions.cs             # Définit les options globales pour la CLI
    ├── RootCommand.cs               # Enregistre les commandes de premier niveau
    └── <Group>/                     # Un dossier par groupe de commandes
        ├── <Group>Command.cs        # Commande parente qui enregistre ses enfants
        └── <Group><Verb>Command.cs  # Commande feuille avec son gestionnaire

RÈGLE 1 — Préférer une classe CommandBase spécifique au projet

Préférez définir une CommandBase abstraite spécifique au projet qui hérite de System.CommandLine.Command. Les commandes concrètes doivent hériter de cette classe de base pour que le comportement partagé et les conventions restent centralisés.

internal abstract class CommandBase : Command
{
    protected CommandBase(string name, string? description = null)
        : base(name, description)
    {
    }
}

internal sealed class MyCommand : CommandBase
{
    public MyCommand()
        : base("command-name", "Texte d'aide affiché dans --help")
    {
        this.SetAction(CommandHandler);
    }

    private async Task<int> CommandHandler(
        ParseResult parseResult,
        CancellationToken cancellationToken)
    {
        // implémentation
        return 0;
    }
}

Lorsque le projet possède déjà une classe de base de commande, préservez ses conventions établies. Sinon, introduisez-en une lorsque les commandes ont besoin d'un comportement partagé ; les applications simples peuvent hériter directement de Command lorsqu'une classe de base n'ajoute aucune valeur significative.


RÈGLE 2 — Options et arguments

Définir des options

private readonly Option<string> _myOption;

// Dans le constructeur :
_myOption = new Option<string>("--my-option")
{
    Description = "Description claire de ce que cette option fait",
    Required = true,   // ou false
};
_myOption.Aliases.Add("-m");       // Ajouter un alias court
this.Options.Add(_myOption);

Définir des arguments (positionnels)

private readonly Argument<string> _fileArgument;

// Dans le constructeur :
_fileArgument = new Argument<string>("file")
{
    Description = "Chemin vers le fichier d'entrée"
};
this.Arguments.Add(_fileArgument);

Lire les valeurs dans les gestionnaires

// Option/argument requis — utilisez GetValue :
var value = parseResult.GetValue(_myOption);

RÈGLE 3 — Patron de gestionnaire de commande

Les gestionnaires sont des méthodes async câblées via SetAction :

this.SetAction(CommandHandler);

private async Task<int> CommandHandler(ParseResult parseResult, CancellationToken cancellationToken)
{
    // 1. Lire les valeurs des options/arguments
    // 2. Charger les paramètres de session (si nécessaire)
    // 3. Valider la configuration en amont — échouer rapidement avec un message clair
    // 4. Exécuter la logique métier
    // 5. Afficher les résultats avec Console

    return 0; // ou code de sortie non-zéro
}

RÈGLE 4 — Groupe de commandes (parent avec sous-commandes)

Une commande de groupe enregistre les enfants mais n'appelle pas SetAction :

internal class MyGroupCommand : CommandBase
{
    public MyGroupCommand()
        : base("mygroup", "Gère les ressources my-group")
    {
        this.Subcommands.Add(new MyGroupListCommand());
        this.Subcommands.Add(new MyGroupCreateCommand());
        this.Subcommands.Add(new MyGroupDeleteCommand());
    }
}

Une commande peut définir à la fois une action et des sous-commandes lorsque l'invocation directe a un comportement significatif.


RÈGLE 5 — Enregistrement

  • Commandes de premier niveau → enregistrez dans RootCommand.cs :

    this.Subcommands.Add(new MyGroupCommand());
  • Sous-commandes → enregistrez dans le constructeur de la commande parente :

    this.Subcommands.Add(new MyGroupCreateCommand());

RÈGLE 6 — Confirmation de l'utilisateur pour les opérations destructrices

Console.WriteLine("Êtes-vous sûr de vouloir supprimer X ? Cette action ne peut pas être annulée. (yes/no)");
var confirmation = Console.ReadLine();
if (confirmation?.ToLower() != "yes" && confirmation?.ToLower() != "y")
{
    Console.WriteLine("Opération annulée.");
    return 0;
}

RÈGLE 7 — Logique de commande

La logique de chaque commande doit se trouver dans une ou plusieurs classes de service qui implémentent des interfaces. La commande reçoit les interfaces via injection de dépendances (DI), pas les implémentations concrètes. Le gestionnaire de commande ne doit pas contenir de logique métier. Le gestionnaire de commande doit être mince, responsable uniquement de :

  1. Analyser l'entrée
  2. Valider la configuration
  3. Appeler la méthode de service
  4. Afficher les résultats

La classe de service doit être injectée dans le constructeur de la commande via DI, pas instanciée directement.


RÈGLE 8 — Injection de dépendances

Les services sont enregistrés dans Program.cs :

serviceCollection.TryAddSingleton<IMyService, MyServiceImpl>();

Ajoutez une extension de commodité dans ServiceProviderExtensions.cs :

public static IMyService GetMyService(this ServiceProvider provider)
    => provider.GetRequiredService<IMyService>();

RÈGLE 9 — Conventions de nommage

Élément Convention Exemple
Nom de commande CLI lowercase kebab-case agent create, set show
Classe de commande PascalCase + suffixe Command AgentCreateCommand
Champ d'option _camelCaseOption (private readonly) _projectNameOption
Nom long d'option --kebab-case --project-name
Alias court d'option -x (1-2 caractères) -p, -id, -md
Champ d'argument _camelCaseArgument _fileArgument
Namespace MyProject.Commands.<Group> MyProject.Commands.Agent
Dossier Commands/<Group>/ Commands/Agent/

RÈGLE 10 — Visibilité

  • Toutes les classes de commande sont internal.

RÈGLE 11 — Options globales et validation

Définissez les options partagées par l'ensemble de l'arborescence des commandes une seule fois dans GlobalOptions.cs. Réutilisez la même instance Option<T> lors de l'enregistrement, de la validation et de la lecture de l'option.

internal static class GlobalOptions
{
    public static readonly Option<string> EndpointOption = CreateEndpointOption();

    private static Option<string> CreateEndpointOption()
    {
        var option = new Option<string>(...);

        // ajouter la description de l'option, les alias et le drapeau Required
        // Ajouter la validation à la collection Validators de l'option

        return option;
    }
}

Utiliser les options globales dans une commande

Exposez l'analyse ou la conversion répétée à travers des assistants CommandBase protégés :

/// <summary>Résout le point de terminaison validé à partir de l'option globale.</summary>
protected Uri GetEndpoint(ParseResult parseResult)
{
    var baseUrl = parseResult.GetValue(GlobalOptions.EndpointOption)!;
    return new Uri(baseUrl);
}

/// <summary>Résout la clé optionnelle à partir de l'option globale.</summary>
protected string? GetKey(ParseResult parseResult)
    => parseResult.GetValue(GlobalOptions.KeyOption);

Consommez ces assistants à partir du gestionnaire de la commande feuille. La commande ne doit pas ajouter les options globales à sa propre collection Options ; l'enregistrement récursif sur la racine les rend déjà disponibles dans son ParseResult.

private async Task<int> CommandHandler(
    ParseResult parseResult,
    CancellationToken cancellationToken)
{
    var endpoint = GetEndpoint(parseResult);
    var key = GetKey(parseResult);

    ...

    return 0;
}

Lisez une option globale directement dans un gestionnaire feuille uniquement lorsqu'aucune logique de conversion ou de secours partagée n'est nécessaire. Toujours utiliser le symbole statique GlobalOptions ; ne jamais créer un second Option<T> avec les mêmes alias.

Suivez ces exigences :

  1. Définissez Recursive = true pour que l'option soit acceptée pour chaque commande descendante.
  2. Ajoutez chaque option globale exactement une fois à RootCommand.Options ; ne la dupliquez pas sur les commandes feuilles.
  3. Lisez les valeurs via le symbole partagé, par exemple parseResult.GetValue(GlobalOptions.Endpoint), de préférence derrière un assistant CommandBase.
  4. Ajoutez la validation à la collection Validators de l'option pour que l'entrée invalide devienne une erreur d'analyse et le gestionnaire de commande ne soit pas invoqué. Ne comptez pas sur les exceptions de new Uri(...) ou des services en aval.
  5. Validez les options de point de terminaison comme des URI http ou https absolues non vides. Rejetez les schémas non supportés, les URI relatives, les chaînes de requête et les fragments car l'ajout d'un chemin de point de terminaison fixe changerait leur signification.
  6. Pour les options de secret optionnelles comme --key, autorisez l'omission mais rejetez une valeur vide ou contenant uniquement des espaces fournie explicitement. Validez la valeur sans la journaliser, l'afficher, la découper ou la modifier d'une autre manière.
  7. Séparez la validation de la dérivation.
  8. Utilisez des messages de validation stables et exploitables qui nomment l'option et le format accepté.
  9. Testez les options globales via l'analyseur racine, incluant la valeur par défaut, les valeurs valides explicites, les valeurs invalides, et la plaçabilité avant et après une sous-commande représentative. Vérifiez que l'entrée invalide empêche l'exécution du gestionnaire.

RÈGLE 12 — Liste de contrôle pour les nouvelles commandes

Lors de la création d'une nouvelle commande, vérifiez :

  1. ✅ Hérite de la classe de base de commande du projet lorsqu'une existe ou fournit un comportement partagé significatif
  2. ✅ Le constructeur passe name, description à la base
  3. ✅ Toutes les options ont Description, Required
  4. ✅ Gestionnaire câblé via this.SetAction(CommandHandler)
  5. ✅ Signature du gestionnaire : async Task<int> CommandHandler(ParseResult, CancellationToken)
  6. ✅ Commande enregistrée dans la commande parente (RootCommand ou commande de groupe)
  7. ✅ La classe est internal
  8. ✅ Fichier placé dans le dossier Commands/<Group>/
  9. ✅ Namespace correspond au dossier : MyProject.CLI.Commands.<Group>

Skills similaires