Skip to content

Commit cd0291f

Browse files
authored
Check in the UnixCommandNotFound feedback provider (#1)
1 parent 25db008 commit cd0291f

File tree

7 files changed

+399
-0
lines changed

7 files changed

+399
-0
lines changed

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
bin/
2+
obj/
3+
.ionide/
4+
project.lock.json
5+
*-tests.xml
6+
/debug/
7+
/staging/
8+
/Packages/
9+
*.nuget.props
10+
11+
# VSCode directories that are not at the repository root
12+
/**/.vscode/
13+
14+
# Ignore binaries and symbols
15+
*.pdb
16+
*.dll
17+

build.ps1

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[CmdletBinding(DefaultParameterSetName = 'Build')]
2+
param(
3+
[Parameter(ParameterSetName = 'Build')]
4+
[ValidateSet('Debug', 'Release')]
5+
[string] $Configuration = 'Debug',
6+
7+
[Parameter(ParameterSetName = 'Bootstrap')]
8+
[switch] $Bootstrap
9+
)
10+
11+
Import-Module "$PSScriptRoot/tools/helper.psm1"
12+
13+
if ($Bootstrap) {
14+
Write-Log "Validate and install missing prerequisits for building ..."
15+
Install-Dotnet
16+
return
17+
}
18+
19+
$srcDir = Join-Path $PSScriptRoot 'src'
20+
dotnet publish -c $Configuration $srcDir
21+
22+
Write-Host "`nThe module 'command-not-found' is published to 'bin\command-not-found'`n" -ForegroundColor Green

src/FeedbackProvider.cs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System.Diagnostics;
2+
using System.Management.Automation;
3+
using System.Management.Automation.Subsystem;
4+
using System.Management.Automation.Subsystem.Feedback;
5+
using System.Management.Automation.Subsystem.Prediction;
6+
7+
namespace Microsoft.PowerShell.FeedbackProvider;
8+
9+
public sealed class UnixCommandNotFound : IFeedbackProvider, ICommandPredictor
10+
{
11+
private readonly Guid _guid;
12+
private List<string>? _candidates;
13+
14+
internal UnixCommandNotFound(string guid)
15+
{
16+
_guid = new Guid(guid);
17+
}
18+
19+
Dictionary<string, string>? ISubsystem.FunctionsToDefine => null;
20+
21+
public Guid Id => _guid;
22+
23+
public string Name => "cmd-not-found";
24+
25+
public string Description => "The built-in feedback/prediction source for the Unix command utility.";
26+
27+
#region IFeedbackProvider
28+
29+
private static string? GetUtilityPath()
30+
{
31+
string cmd_not_found = "/usr/lib/command-not-found";
32+
bool exist = IsFileExecutable(cmd_not_found);
33+
34+
if (!exist)
35+
{
36+
cmd_not_found = "/usr/share/command-not-found/command-not-found";
37+
exist = IsFileExecutable(cmd_not_found);
38+
}
39+
40+
return exist ? cmd_not_found : null;
41+
42+
static bool IsFileExecutable(string path)
43+
{
44+
var file = new FileInfo(path);
45+
return file.Exists && file.UnixFileMode.HasFlag(UnixFileMode.OtherExecute);
46+
}
47+
}
48+
49+
public FeedbackItem? GetFeedback(string commandLine, ErrorRecord lastError, CancellationToken token)
50+
{
51+
if (Platform.IsWindows || lastError.FullyQualifiedErrorId != "CommandNotFoundException")
52+
{
53+
return null;
54+
}
55+
56+
var target = (string)lastError.TargetObject;
57+
if (target is null)
58+
{
59+
return null;
60+
}
61+
62+
if (target.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
63+
{
64+
return null;
65+
}
66+
67+
string? cmd_not_found = GetUtilityPath();
68+
if (cmd_not_found is not null)
69+
{
70+
var startInfo = new ProcessStartInfo(cmd_not_found);
71+
startInfo.ArgumentList.Add(target);
72+
startInfo.RedirectStandardError = true;
73+
startInfo.RedirectStandardOutput = true;
74+
75+
using var process = Process.Start(startInfo);
76+
if (process is not null)
77+
{
78+
string? header = null;
79+
List<string>? actions = null;
80+
81+
while (true)
82+
{
83+
string? line = process.StandardError.ReadLine();
84+
if (line is null)
85+
{
86+
break;
87+
}
88+
89+
if (line == string.Empty)
90+
{
91+
continue;
92+
}
93+
94+
if (line.StartsWith("sudo ", StringComparison.Ordinal))
95+
{
96+
actions ??= new List<string>();
97+
actions.Add(line.TrimEnd());
98+
}
99+
else if (actions is null)
100+
{
101+
header = line;
102+
}
103+
}
104+
105+
if (actions is not null && header is not null)
106+
{
107+
_candidates = actions;
108+
109+
var footer = process.StandardOutput.ReadToEnd().Trim();
110+
return string.IsNullOrEmpty(footer)
111+
? new FeedbackItem(header, actions)
112+
: new FeedbackItem(header, actions, footer, FeedbackDisplayLayout.Portrait);
113+
}
114+
}
115+
}
116+
117+
return null;
118+
}
119+
120+
#endregion
121+
122+
#region ICommandPredictor
123+
124+
public bool CanAcceptFeedback(PredictionClient client, PredictorFeedbackKind feedback)
125+
{
126+
return feedback switch
127+
{
128+
PredictorFeedbackKind.CommandLineAccepted => true,
129+
_ => false,
130+
};
131+
}
132+
133+
public SuggestionPackage GetSuggestion(PredictionClient client, PredictionContext context, CancellationToken cancellationToken)
134+
{
135+
if (_candidates is not null)
136+
{
137+
string input = context.InputAst.Extent.Text;
138+
List<PredictiveSuggestion>? result = null;
139+
140+
foreach (string c in _candidates)
141+
{
142+
if (c.StartsWith(input, StringComparison.OrdinalIgnoreCase))
143+
{
144+
result ??= new List<PredictiveSuggestion>(_candidates.Count);
145+
result.Add(new PredictiveSuggestion(c));
146+
}
147+
}
148+
149+
if (result is not null)
150+
{
151+
return new SuggestionPackage(result);
152+
}
153+
}
154+
155+
return default;
156+
}
157+
158+
public void OnCommandLineAccepted(PredictionClient client, IReadOnlyList<string> history)
159+
{
160+
// Reset the candidate state.
161+
_candidates = null;
162+
}
163+
164+
public void OnSuggestionDisplayed(PredictionClient client, uint session, int countOrIndex) { }
165+
166+
public void OnSuggestionAccepted(PredictionClient client, uint session, string acceptedSuggestion) { }
167+
168+
public void OnCommandLineExecuted(PredictionClient client, string commandLine, bool success) { }
169+
170+
#endregion;
171+
}
172+
173+
public class Init : IModuleAssemblyInitializer, IModuleAssemblyCleanup
174+
{
175+
private const string Id = "47013747-CB9D-4EBC-9F02-F32B8AB19D48";
176+
177+
public void OnImport()
178+
{
179+
var feedback = new UnixCommandNotFound(Id);
180+
SubsystemManager.RegisterSubsystem(SubsystemKind.FeedbackProvider, feedback);
181+
SubsystemManager.RegisterSubsystem(SubsystemKind.CommandPredictor, feedback);
182+
}
183+
184+
public void OnRemove(PSModuleInfo psModuleInfo)
185+
{
186+
SubsystemManager.UnregisterSubsystem<ICommandPredictor>(new Guid(Id));
187+
SubsystemManager.UnregisterSubsystem<IFeedbackProvider>(new Guid(Id));
188+
}
189+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
8+
9+
<!-- Disable deps.json generation -->
10+
<GenerateDependencyFile>false</GenerateDependencyFile>
11+
12+
<!-- Deploy the produced assembly -->
13+
<PublishDir>..\bin\command-not-found</PublishDir>
14+
</PropertyGroup>
15+
16+
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
17+
<!-- Disable PDB generation for the Release build -->
18+
<DebugSymbols>false</DebugSymbols>
19+
<DebugType>None</DebugType>
20+
</PropertyGroup>
21+
22+
<ItemGroup>
23+
<PackageReference Include="System.Management.Automation" Version="7.4.0-preview.2">
24+
<ExcludeAssets>contentFiles</ExcludeAssets>
25+
<PrivateAssets>All</PrivateAssets>
26+
</PackageReference>
27+
<Content Include="command-not-found.psd1;ValidateOS.psm1">
28+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
29+
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
30+
</Content>
31+
</ItemGroup>
32+
33+
</Project>

src/ValidateOS.psm1

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function Test-Utility([string] $path) {
2+
$target = [System.IO.FileInfo]::new($path)
3+
$mode = [System.IO.UnixFileMode]@('OtherExecute', 'GroupExecute', 'UserExecute')
4+
$target.Exists -and $target.UnixFileMode.HasFlag($mode)
5+
}
6+
7+
if (!$IsLinux -or (
8+
!(Test-Utility "/usr/lib/command-not-found") -and
9+
!(Test-Utility "/usr/share/command-not-found/command-not-found"))) {
10+
$exception = [System.PlatformNotSupportedException]::new(
11+
"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'.")
12+
$err = [System.Management.Automation.ErrorRecord]::new($exception, "PlatformNotSupported", "InvalidOperation", $null)
13+
throw $err
14+
}

src/command-not-found.psd1

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# Module manifest for module 'command-not-found'
3+
#
4+
5+
@{
6+
ModuleVersion = '0.1.0'
7+
GUID = '47013747-CB9D-4EBC-9F02-F32B8AB19D48'
8+
Author = 'PowerShell'
9+
CompanyName = "Microsoft Corporation"
10+
Copyright = "Copyright (c) Microsoft Corporation."
11+
Description = "Provide feedback on the 'CommandNotFound' error stemmed from running an executable on Linux platform."
12+
PowerShellVersion = '7.4'
13+
14+
NestedModules = @('ValidateOS.psm1', 'PowerShell.CommandNotFound.Feedback.dll')
15+
FunctionsToExport = @()
16+
CmdletsToExport = @()
17+
VariablesToExport = '*'
18+
AliasesToExport = @()
19+
}

0 commit comments

Comments
 (0)