Compartilhar via


Como criar um provedor de comentários

O PowerShell 7.4 introduziu o conceito de provedores de comentários. Um provedor de comentários é um módulo do PowerShell que implementa a interface IFeedbackProvider para fornecer sugestões de comando com base em tentativas de execução de comando do usuário. O provedor é acionado quando há uma execução bem-sucedida ou falha. Os provedores de feedback utilizam informações sobre o sucesso ou fracasso para fornecer retorno.

Pré-requisitos

Para criar um provedor de comentários, você deve atender aos seguintes pré-requisitos:

  • Instalar o PowerShell 7.4 ou superior
    • Você deve habilitar o recurso experimental PSFeedbackProvider para habilitar o suporte para provedores de comentários e preditores. Para obter mais informações, consulte Usando recursos experimentais.
  • Instalar o SDK do .NET 8 – 8.0.0 ou superior
    • Consulte a página Baixar .NET 8.0 para obter a versão mais recente do SDK.

Visão geral de um provedor de comentários

Um provedor de comentários é um módulo binário do PowerShell que implementa a interface System.Management.Automation.Subsystem.Feedback.IFeedbackProvider. Essa interface declara os métodos para obter comentários com base na entrada da linha de comando. A interface de comentários pode fornecer sugestões com base no êxito ou falha do comando invocado pelo usuário. As sugestões podem ser o que você quiser. Por exemplo, você pode sugerir maneiras de resolver um erro ou práticas melhores, como evitar o uso de aliases. Para obter mais informações, consulte O que são Provedores de Comentários? postagem no blog.

O diagrama a seguir mostra a arquitetura de um provedor de comentários:

Diagrama da arquitetura do provedor de comentários.

Os exemplos a seguir orientam você pelo processo de criação de um provedor de comentários simples. Além disso, você pode registrar o provedor na interface de previsão de comandos para adicionar sugestões de feedback à experiência do preditor de linha de comando. Para obter mais informações sobre preditores, consulte Usando previsores no PSReadLine e Como criar um preditor de linha de comando.

Etapa 1 – Criar um novo projeto de biblioteca de classes

Use o seguinte comando para criar um novo projeto no diretório do projeto:

dotnet new classlib --name MyFeedbackProvider

Adicione uma referência de pacote para o pacote System.Management.Automation ao arquivo .csproj. O exemplo a seguir mostra o arquivo de .csproj atualizado:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Management.Automation" Version="7.4.0-preview.3">
        <ExcludeAssets>contentFiles</ExcludeAssets>
        <PrivateAssets>All</PrivateAssets>
    </PackageReference>
  </ItemGroup>
</Project>

Nota

Você deve alterar a versão do assembly System.Management.Automation para corresponder à versão da visualização do PowerShell que você pretende usar. A versão mínima é 7.4.0-preview.3.

Etapa 2 – Adicionar a definição de classe para seu provedor

Altere o nome do arquivo Class1.cs para corresponder ao nome do seu provedor. Este exemplo usa myFeedbackProvider.cs. Esse arquivo contém as duas classes principais que definem o provedor de comentários. O exemplo a seguir mostra o modelo básico para as definições de classe.

using System.Management.Automation;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
using System.Management.Automation.Language;

namespace myFeedbackProvider;

public sealed class myFeedbackProvider : IFeedbackProvider, ICommandPredictor
{

}

public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{

}

Etapa 3 – Implementar a classe Init

A classe Init registra e cancela o registro do provedor de comentários com o gerente do subsistema. O método OnImport() é executado quando o módulo binário está sendo carregado. O método OnRemove() é executado quando o módulo binário está sendo removido. Este exemplo registra o fornecedor de feedback e o subsistema do preditor de comandos.

public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
    private const string Id = "<ADD YOUR GUID HERE>";

    public void OnImport()
    {
        var feedback = new myFeedbackProvider(Id);
        SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, feedback);
        SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, feedback);
    }

    public void OnRemove(PSModuleInfo psModuleInfo)
    {
        SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
        SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
    }
}

Substitua o valor do espaço reservado <ADD YOUR GUID HERE> por um Guid exclusivo. Você pode gerar um Guid usando o cmdlet New-Guid.

New-Guid

O Guid é um identificador exclusivo para seu provedor. O provedor deve ter uma ID exclusiva para ser registrado no subsistema.

Etapa 4 – Adicionar membros da classe e definir o construtor

O código a seguir implementa as propriedades definidas nas interfaces, adiciona os membros de classe necessários e cria o construtor para a classe myFeedbackProvider.

/// <summary>
/// Gets the global unique identifier for the subsystem implementation.
/// </summary>
private readonly Guid _guid;
public Guid Id => _guid;

/// <summary>
/// Gets the name of a subsystem implementation, this will be the name displayed when triggered
/// </summary>
public string Name => "myFeedbackProvider";

/// <summary>
/// Gets the description of a subsystem implementation.
/// </summary>
public string Description => "This is very simple feedback provider";

/// <summary>
/// Default implementation. No function is required for a feedback provider.
/// </summary>
Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;

/// <summary>
/// Gets the types of trigger for this feedback provider.
/// </summary>
/// <remarks>
/// The default implementation triggers a feedback provider by <see cref="FeedbackTrigger.CommandNotFound"/> only.
/// </remarks>
public FeedbackTrigger Trigger => FeedbackTrigger.All;

/// <summary>
/// List of candidates from the feedback provider to be passed as predictor results
/// </summary>
private List<string>? _candidates;

/// <summary>
/// PowerShell session used to run PowerShell commands that help create suggestions.
/// </summary>
private PowerShell _powershell;

internal myFeedbackProvider(string guid)
{
    _guid = new Guid(guid); // Save guid
    _powershell = PowerShell.Create(); // Create PowerShell instance
}

Etapa 5 – Criar o método GetFeedback()

O método GetFeedback usa dois parâmetros, context e token. O parâmetro context recebe as informações sobre o gatilho para que você possa decidir como responder com sugestões. O parâmetro token é usado para cancelamento. Essa função retorna um FeedbackItem que contém a sugestão.

/// <summary>
/// Gets feedback based on the given commandline and error record.
/// </summary>
/// <param name="context">The context for the feedback call.</param>
/// <param name="token">The cancellation token to cancel the operation.</param>
/// <returns>The feedback item.</returns>
public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
{
    // Target describes the different kinds of triggers to activate on,
    var target = context.Trigger;
    var commandLine = context.CommandLine;
    var ast = context.CommandLineAst;

    // defining the header and footer variables
    string header;
    string footer;

    // List of the actions
    List<string>? actions = new List<string>();

    // Trigger on success code goes here

    // Trigger on error code goes here

    return null;
}

A imagem a seguir mostra como esses campos são usados nas sugestões exibidas ao usuário.

Captura de tela de exemplos de provedores de comentários

Criar sugestões para um disparo Bem-sucedido

Para uma invocação bem-sucedida, queremos expandir todos os aliases usados na última execução. Usando CommandLineAst, identificamos todos os comandos com aliases e criamos uma sugestão para utilizar o nome do comando totalmente qualificado.

// Trigger on success
if (target == FeedbackTrigger.Success)
{
    // Getting the commands from the AST and only finding those that are Commands
    var astCmds = ast.FindAll((cAst) => cAst is CommandAst, true);

    // Inspect each of the commands
    foreach(var command in astCmds)
    {

        // Get the command name
        var aliasedCmd = ((CommandAst) command).GetCommandName();

        // Check if its an alias or not, if so then add it to the list of actions
        if(TryGetAlias(aliasedCmd, out string commandString))
        {
            actions.Add($"{aliasedCmd} --> {commandString}");
        }
    }

    // If no alias was found return null
    if(actions.Count == 0)
    {
        return null;
    }

    // If aliases are found, set the header to a description and return a new FeedbackItem.
    header = "You have used an aliased command:";
    // Copy actions to _candidates for the predictor
    _candidates = actions;

    return new FeedbackItem(header, actions);
}

Implementar o método TryGetAlias()

O método TryGetAlias() é uma função auxiliar privada que retorna um valor booliano para indicar se o comando é um alias. No construtor de classe, criamos uma instância do PowerShell que podemos usar para executar comandos do PowerShell. O método TryGetAlias() usa essa instância do PowerShell para invocar o método GetCommand para determinar se o comando é um alias. O objeto AliasInfo retornado por GetCommand contém o nome completo do comando aliased.

/// <summary>
/// Checks if a command is an alias.
/// </summary>
/// <param name="command">The command to check if alias</param>
/// <param name="targetCommand">The referenced command by the aliased command</param>
/// <returns>True if an alias and false if not</returns>
private bool TryGetAlias(string command, out string targetCommand)
{
    // Create PowerShell runspace as a session state proxy to run GetCommand and check
    // if its an alias
    AliasInfo? pwshAliasInfo =
        _powershell.Runspace.SessionStateProxy.InvokeCommand.GetCommand(command, CommandTypes.Alias) as AliasInfo;

    // if its null then it is not an aliased command so just return false
    if(pwshAliasInfo is null)
    {
        targetCommand = String.Empty;
        return false;
    }

    // Set targetCommand to referenced command name
    targetCommand = pwshAliasInfo.ReferencedCommand.Name;
    return true;
}

Criar sugestões para um disparo com Falha

Quando uma execução de comando falha, queremos sugerir que o usuário execute Get-Help para obter mais informações sobre como usar o comando.

// Trigger on error
if (target == FeedbackTrigger.Error)
{
    // Gets the command that caused the error.
    var erroredCommand = context.LastError?.InvocationInfo.MyCommand;
    if (erroredCommand is null)
    {
        return null;
    }

    header = $"You have triggered an error with the command {erroredCommand}. Try using the following command to get help:";

    actions.Add($"Get-Help {erroredCommand}");
    footer = $"You can also check online documentation at https://learn.microsoft.com/en-us/powershell/module/?term={erroredCommand}";

    // Copy actions to _candidates for the predictor
    _candidates = actions;
    return new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
}

Etapa 6 – Enviar sugestões para o preditor de linha de comando

Outra maneira que o seu provedor de feedback pode aprimorar a experiência do usuário é fornecer sugestões de comando para a interface ICommandPredictor. Para obter mais informações sobre como criar um preditor de linha de comando, consulte Como criar um preditor de linha de comando.

O código a seguir implementa os métodos necessários da interface ICommandPredictor para adicionar o comportamento de previsão ao seu provedor de comentários.

  • CanAcceptFeedback() - Esse método retorna um valor booliano que indica se o preditor aceita um tipo específico de comentários.
  • GetSuggestion() - Esse método retorna um objeto SuggestionPackage que contém as sugestões a serem exibidas pelo preditor.
  • OnCommandLineAccepted() - Esse método é chamado quando uma linha de comando é aceita para execução.
/// <summary>
/// Gets a value indicating whether the predictor accepts a specific kind of feedback.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="feedback">A specific type of feedback.</param>
/// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
{
    return feedback switch
    {
        PredictorFeedbackKind.CommandLineAccepted => true,
        _ => false,
    };
}

/// <summary>
/// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
/// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
/// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
public SuggestionPackage GetSuggestion(
    PredictionClient client,
    PredictionContext context,
    CancellationToken cancellationToken)
{
    if (_candidates is not null)
    {
        string input = context.InputAst.Extent.Text;
        List<PredictiveSuggestion>? result = null;

        foreach (string c in _candidates)
        {
            if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
            {
                result ??= new List<PredictiveSuggestion>(_candidates.Count);
                result.Add(new PredictiveSuggestion(c));
            }
        }

        if (result is not null)
        {
            return new SuggestionPackage(result);
        }
    }

    return default;
}

/// <summary>
/// A command line was accepted to execute.
/// The predictor can start processing early as needed with the latest history.
/// </summary>
/// <param name="client">Represents the client that initiates the call.</param>
/// <param name="history">History command lines provided as references for prediction.</param>
public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
{
    // Reset the candidate state once the command is accepted.
    _candidates = null;
}

Etapa 7 – Criar o provedor de comentários

Agora você está pronto para criar e começar a usar seu provedor de comentários! Para criar o projeto, execute o seguinte comando:

dotnet build

Esse comando cria o módulo do PowerShell como um arquivo DLL no seguinte caminho da pasta do projeto: bin/Debug/net8.0/myFeedbackProvider

Você pode encontrar o erro error NU1101: Unable to find package System.Management.Automation. ao criar em computadores Windows. Para corrigir isso, adicione um arquivo nuget.config ao diretório do projeto e adicione o seguinte:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
  <disabledPackageSources>
    <clear />
  </disabledPackageSources>
</configuration>

Usando um provedor de comentários

Para testar seu novo provedor de comentários, importe o módulo compilado para sua sessão do PowerShell. Isso pode ser feito importando a pasta descrita após a criação ter sido bem-sucedida:

Import-Module ./bin/Debug/net8.0/myFeedbackProvider

Depois de estar satisfeito com o módulo, você deverá criar um manifesto do módulo, publicá-lo na Galeria do PowerShell e instalá-lo em seu $Env:PSModulePath. Para obter mais informações, consulte Como criar um manifesto de módulo. Você pode adicionar o comando Import-Module ao script $PROFILE para que o módulo esteja disponível na sessão do PowerShell.

Você pode obter uma lista de provedores de comentários instalados usando o seguinte comando:

Get-PSSubsystem -Kind FeedbackProvider
Kind              SubsystemType      IsRegistered Implementations
----              -------------      ------------ ---------------
FeedbackProvider  IFeedbackProvider          True {general}

Nota

Get-PSSubsystem é um cmdlet experimental que foi introduzido no PowerShell 7.1 Você deve habilitar o recurso experimental PSSubsystemPluginModel para usar esse cmdlet. Para obter mais informações, consulte Usando recursos experimentais.

A captura de tela a seguir mostra algumas sugestões de exemplo do novo provedor.

Captura de tela dos gatilhos do provedor de comentários de êxito e erro

Veja a seguir um GIF mostrando como a integração do preditor funciona com o novo provedor.

GIF do sistema de previsão trabalhando com o provedor de comentários

Outros provedores de comentários

Criamos outro provedor de comentários que pode ser usado como uma boa referência para exemplos mais profundos.

comando não encontrado

O provedor de comentários command-not-found utiliza a ferramenta de utilitário command-not-found em sistemas Linux para fornecer sugestões quando os comandos nativos são tentados a ser executados, mas estão ausentes. Você pode encontrar o código no Repositório GitHub ou pode baixar por conta própria na Galeria do PowerShell .

Adaptador do PowerShell

O Microsoft.PowerShell.PowerShellAdapter é um provedor de comentários que ajuda você a converter saídas de texto de comandos nativos em objetos do PowerShell. Ele detecta "adaptadores" em seu sistema e sugere que você os use ao usar o comando nativo. Você pode saber mais sobre os adaptadores do PowerShell na postagem do blog Provedor de Comentários do Adaptador do PowerShell. Você também pode encontrar o código no Repositório GitHub ou pode baixar por conta própria na Galeria do PowerShell .

Apêndice – Código de implementação completo

O código a seguir combina os exemplos anteriores para localizar a implementação completa da classe de provedor.

using System.Management.Automation;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;
using System.Management.Automation.Language;

namespace myFeedbackProvider;

public sealed class myFeedbackProvider : IFeedbackProvider, ICommandPredictor
{
    /// <summary>
    /// Gets the global unique identifier for the subsystem implementation.
    /// </summary>
    private readonly Guid _guid;
    public Guid Id => _guid;

    /// <summary>
    /// Gets the name of a subsystem implementation, this will be the name displayed when triggered
    /// </summary>
    public string Name => "myFeedbackProvider";

    /// <summary>
    /// Gets the description of a subsystem implementation.
    /// </summary>
    public string Description => "This is very simple feedback provider";

    /// <summary>
    /// Default implementation. No function is required for a feedback provider.
    /// </summary>
    Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;

    /// <summary>
    /// Gets the types of trigger for this feedback provider.
    /// </summary>
    /// <remarks>
    /// The default implementation triggers a feedback provider by <see cref="FeedbackTrigger.CommandNotFound"/> only.
    /// </remarks>
    public FeedbackTrigger Trigger => FeedbackTrigger.All;

    /// <summary>
    /// List of candidates from the feedback provider to be passed as predictor results
    /// </summary>
    private List<string>? _candidates;

    /// <summary>
    /// PowerShell session used to run PowerShell commands that help create suggestions.
    /// </summary>
    private PowerShell _powershell;

    // Constructor
    internal myFeedbackProvider(string guid)
    {
        _guid = new Guid(guid); // Save guid
        _powershell = PowerShell.Create(); // Create PowerShell instance
    }

    #region IFeedbackProvider
    /// <summary>
    /// Gets feedback based on the given commandline and error record.
    /// </summary>
    /// <param name="context">The context for the feedback call.</param>
    /// <param name="token">The cancellation token to cancel the operation.</param>
    /// <returns>The feedback item.</returns>
    public FeedbackItem? GetFeedback(FeedbackContext context, CancellationToken token)
    {
        // Target describes the different kinds of triggers to activate on,
        var target = context.Trigger;
        var commandLine = context.CommandLine;
        var ast = context.CommandLineAst;

        // defining the header and footer variables
        string header;
        string footer;

        // List of the actions
        List<string>? actions = new List<string>();

        // Trigger on success
        if (target == FeedbackTrigger.Success)
        {
            // Getting the commands from the AST and only finding those that are Commands
            var astCmds = ast.FindAll((cAst) => cAst is CommandAst, true);

            // Inspect each of the commands
            foreach(var command in astCmds)
            {

                // Get the command name
                var aliasedCmd = ((CommandAst) command).GetCommandName();

                // Check if its an alias or not, if so then add it to the list of actions
                if(TryGetAlias(aliasedCmd, out string commandString))
                {
                    actions.Add($"{aliasedCmd} --> {commandString}");
                }
            }

            // If no alias was found return null
            if(actions.Count == 0)
            {
                return null;
            }

            // If aliases are found, set the header to a description and return a new FeedbackItem.
            header = "You have used an aliased command:";
            // Copy actions to _candidates for the predictor
            _candidates = actions;

            return new FeedbackItem(header, actions);
        }

        // Trigger on error
        if (target == FeedbackTrigger.Error)
        {
            // Gets the command that caused the error.
            var erroredCommand = context.LastError?.InvocationInfo.MyCommand;
            if (erroredCommand is null)
            {
                return null;
            }

            header = $"You have triggered an error with the command {erroredCommand}. Try using the following command to get help:";

            actions.Add($"Get-Help {erroredCommand}");
            footer = $"You can also check online documentation at https://learn.microsoft.com/en-us/powershell/module/?term={erroredCommand}";

            // Copy actions to _candidates for the predictor
            _candidates = actions;
            return new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
        }
        return null;
    }

    /// <summary>
    /// Checks if a command is an alias.
    /// </summary>
    /// <param name="command">The command to check if alias</param>
    /// <param name="targetCommand">The referenced command by the aliased command</param>
    /// <returns>True if an alias and false if not</returns>
    private bool TryGetAlias(string command, out string targetCommand)
    {
        // Create PowerShell runspace as a session state proxy to run GetCommand and check
        // if its an alias
        AliasInfo? pwshAliasInfo =
            _powershell.Runspace.SessionStateProxy.InvokeCommand.GetCommand(command, CommandTypes.Alias) as AliasInfo;

        // if its null then it is not an aliased command so just return false
        if(pwshAliasInfo is null)
        {
            targetCommand = String.Empty;
            return false;
        }

        // Set targetCommand to referenced command name
        targetCommand = pwshAliasInfo.ReferencedCommand.Name;
        return true;
    }
    #endregion IFeedbackProvider

    #region ICommandPredictor

    /// <summary>
    /// Gets a value indicating whether the predictor accepts a specific kind of feedback.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="feedback">A specific type of feedback.</param>
    /// <returns>True or false, to indicate whether the specific feedback is accepted.</returns>
    public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
    {
        return feedback switch
        {
            PredictorFeedbackKind.CommandLineAccepted => true,
            _ => false,
        };
    }

    /// <summary>
    /// Get the predictive suggestions. It indicates the start of a suggestion rendering session.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="context">The <see cref="PredictionContext"/> object to be used for prediction.</param>
    /// <param name="cancellationToken">The cancellation token to cancel the prediction.</param>
    /// <returns>An instance of <see cref="SuggestionPackage"/>.</returns>
    public SuggestionPackage GetSuggestion(
        PredictionClient client,
        PredictionContext context,
        CancellationToken cancellationToken)
    {
        if (_candidates is not null)
        {
            string input = context.InputAst.Extent.Text;
            List<PredictiveSuggestion>? result = null;

            foreach (string c in _candidates)
            {
                if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
                {
                    result ??= new List<PredictiveSuggestion>(_candidates.Count);
                    result.Add(new PredictiveSuggestion(c));
                }
            }

            if (result is not null)
            {
                return new SuggestionPackage(result);
            }
        }

        return default;
    }

    /// <summary>
    /// A command line was accepted to execute.
    /// The predictor can start processing early as needed with the latest history.
    /// </summary>
    /// <param name="client">Represents the client that initiates the call.</param>
    /// <param name="history">History command lines provided as references for prediction.</param>
    public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
    {
        // Reset the candidate state once the command is accepted.
        _candidates = null;
    }

    #endregion;
}

public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
    private const string Id = "<ADD YOUR GUID HERE>";

    public void OnImport()
    {
        var feedback = new myFeedbackProvider(Id);
        SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, feedback);
        SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, feedback);
    }

    public void OnRemove(PSModuleInfo psModuleInfo)
    {
        SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
        SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
    }
}