Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
bin/
obj/
.ionide/
project.lock.json
*-tests.xml
/debug/
/staging/
/Packages/
*.nuget.props

# VSCode directories that are not at the repository root
/**/.vscode/

# Ignore binaries and symbols
*.pdb
*.dll

22 changes: 22 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[CmdletBinding(DefaultParameterSetName = 'Build')]
param(
[Parameter(ParameterSetName = 'Build')]
[ValidateSet('Debug', 'Release')]
[string] $Configuration = 'Debug',

[Parameter(ParameterSetName = 'Bootstrap')]
[switch] $Bootstrap
)

Import-Module "$PSScriptRoot/tools/helper.psm1"

if ($Bootstrap) {
Write-Log "Validate and install missing prerequisits for building ..."
Install-Dotnet
return
}

$srcDir = Join-Path $PSScriptRoot 'src'
dotnet publish -c $Configuration $srcDir

Write-Host "`nThe module 'command-not-found' is published to 'bin\command-not-found'`n" -ForegroundColor Green
189 changes: 189 additions & 0 deletions src/FeedbackProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
using System.Diagnostics;
using System.Management.Automation;
using System.Management.Automation.Subsystem;
using System.Management.Automation.Subsystem.Feedback;
using System.Management.Automation.Subsystem.Prediction;

namespace Microsoft.PowerShell.FeedbackProvider;

public sealed class UnixCommandNotFound : IFeedbackProvider, ICommandPredictor
{
private readonly Guid _guid;
private List<string>? _candidates;

internal UnixCommandNotFound(string guid)
{
_guid = new Guid(guid);
}

Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;

public Guid Id => _guid;

public string Name => "cmd-not-found";

public string Description => "The built-in feedback/prediction source for the Unix command utility.";

#region IFeedbackProvider

private static string? GetUtilityPath()
{
string cmd_not_found = "/usr/lib/command-not-found";
bool exist = IsFileExecutable(cmd_not_found);

if (!exist)
{
cmd_not_found = "/usr/share/command-not-found/command-not-found";
exist = IsFileExecutable(cmd_not_found);
}

return exist ? cmd_not_found : null;

static bool IsFileExecutable(string path)
{
var file = new FileInfo(path);
return file.Exists && file.UnixFileMode.HasFlag(UnixFileMode.OtherExecute);
}
}

public FeedbackItem? GetFeedback(string commandLine, ErrorRecord lastError, CancellationToken token)
{
if (Platform.IsWindows || lastError.FullyQualifiedErrorId != "CommandNotFoundException")
{
return null;
}

var target = (string)lastError.TargetObject;
if (target is null)
{
return null;
}

if (target.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
{
return null;
}

string? cmd_not_found = GetUtilityPath();
if (cmd_not_found is not null)
{
var startInfo = new ProcessStartInfo(cmd_not_found);
startInfo.ArgumentList.Add(target);
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;

using var process = Process.Start(startInfo);
if (process is not null)
{
string? header = null;
List<string>? actions = null;

while (true)
{
string? line = process.StandardError.ReadLine();
if (line is null)
{
break;
}

if (line == string.Empty)
{
continue;
}

if (line.StartsWith("sudo ", StringComparison.Ordinal))
{
actions ??= new List<string>();
actions.Add(line.TrimEnd());
}
else if (actions is null)
{
header = line;
}
}

if (actions is not null && header is not null)
{
_candidates = actions;

var footer = process.StandardOutput.ReadToEnd().Trim();
return string.IsNullOrEmpty(footer)
? new FeedbackItem(header, actions)
: new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
}
}
}

return null;
}

#endregion

#region ICommandPredictor

public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
{
return feedback switch
{
PredictorFeedbackKind.CommandLineAccepted => true,
_ => false,
};
}

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;
}

public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
{
// Reset the candidate state.
_candidates = null;
}

public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) { }

public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) { }

public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { }

#endregion;
}

public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
{
private const string Id = "47013747-CB9D-4EBC-9F02-F32B8AB19D48";

public void OnImport()
{
var feedback = new UnixCommandNotFound(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));
}
}
33 changes: 33 additions & 0 deletions src/PowerShell.CommandNotFound.Feedback.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<!-- Disable deps.json generation -->
<GenerateDependencyFile>false</GenerateDependencyFile>

<!-- Deploy the produced assembly -->
<PublishDir>..\bin\command-not-found</PublishDir>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<!-- Disable PDB generation for the Release build -->
<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Management.Automation" Version="7.4.0-preview.2">
<ExcludeAssets>contentFiles</ExcludeAssets>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<Content Include="command-not-found.psd1;ValidateOS.psm1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>

</Project>
14 changes: 14 additions & 0 deletions src/ValidateOS.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
function Test-Utility([string] $path) {
$target = [System.IO.FileInfo]::new($path)
$mode = [System.IO.UnixFileMode]@('OtherExecute', 'GroupExecute', 'UserExecute')
$target.Exists -and $target.UnixFileMode.HasFlag($mode)
}

if (!$IsLinux -or (
!(Test-Utility "/usr/lib/command-not-found") -and
!(Test-Utility "/usr/share/command-not-found/command-not-found"))) {
$exception = [System.PlatformNotSupportedException]::new(
"This module only works on Linux and depends on the utility 'command-not-found' to be available under the folder '/usr/lib' or '/usr/share/command-not-found'.")
$err = [System.Management.Automation.ErrorRecord]::new($exception, "PlatformNotSupported", "InvalidOperation", $null)
throw $err
}
19 changes: 19 additions & 0 deletions src/command-not-found.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#
# Module manifest for module 'command-not-found'
#

@{
ModuleVersion = '0.1.0'
GUID = '47013747-CB9D-4EBC-9F02-F32B8AB19D48'
Author = 'PowerShell'
CompanyName = "Microsoft Corporation"
Copyright = "Copyright (c) Microsoft Corporation."
Description = "Provide feedback on the 'CommandNotFound' error stemmed from running an executable on Linux platform."
PowerShellVersion = '7.4'

NestedModules = @('ValidateOS.psm1', 'PowerShell.CommandNotFound.Feedback.dll')
FunctionsToExport = @()
CmdletsToExport = @()
VariablesToExport = '*'
AliasesToExport = @()
}
Loading