Add exec cmdlet for bash compatibility (#16462)

This commit is contained in:
Steve Lee
2022-01-04 14:09:27 -08:00
committed by GitHub
parent 0834c65877
commit a9b14a13fd
6 changed files with 218 additions and 2 deletions
@@ -26,6 +26,7 @@ namespace System.Management.Automation
internal const string PSRemotingSSHTransportErrorHandling = "PSRemotingSSHTransportErrorHandling";
internal const string PSCleanBlockFeatureName = "PSCleanBlock";
internal const string PSAMSIMethodInvocationLogging = "PSAMSIMethodInvocationLogging";
internal const string PSExecFeatureName = "PSExec";
#endregion
@@ -138,6 +139,9 @@ namespace System.Management.Automation
new ExperimentalFeature(
name: PSAMSIMethodInvocationLogging,
description: "Provides AMSI notification of .NET method invocations."),
new ExperimentalFeature(
name: PSExecFeatureName,
description: "Add 'exec' built-in command on Linux and macOS"),
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
@@ -4634,7 +4634,7 @@ end {
const ScopedItemOptions ReadOnly_AllScope = ScopedItemOptions.ReadOnly | ScopedItemOptions.AllScope;
const ScopedItemOptions ReadOnly = ScopedItemOptions.ReadOnly;
return new SessionStateAliasEntry[] {
var builtInAliases = new List<SessionStateAliasEntry> {
new SessionStateAliasEntry("foreach", "ForEach-Object", string.Empty, ReadOnly_AllScope),
new SessionStateAliasEntry("%", "ForEach-Object", string.Empty, ReadOnly_AllScope),
new SessionStateAliasEntry("where", "Where-Object", string.Empty, ReadOnly_AllScope),
@@ -4801,6 +4801,15 @@ end {
// - do not use AllScope - this causes errors in profiles that set this somewhat commonly used alias.
new SessionStateAliasEntry("sls", "Select-String"),
};
#if UNIX
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSExecFeatureName))
{
builtInAliases.Add(new SessionStateAliasEntry("exec", "Switch-Process"));
}
#endif
return builtInAliases.ToArray();
}
}
@@ -5439,6 +5448,13 @@ end {
cmdlets.Add("Get-PSSubsystem", new SessionStateCmdletEntry("Get-PSSubsystem", typeof(Subsystem.GetPSSubsystemCommand), helpFile));
}
#if UNIX
if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSExecFeatureName))
{
cmdlets.Add("Switch-Process", new SessionStateCmdletEntry("Switch-Process", typeof(SwitchProcessCommand), helpFile));
}
#endif
foreach (var val in cmdlets.Values)
{
val.SetPSSnapIn(psSnapInInfo);
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
using System.Management.Automation;
using System.Runtime.InteropServices;
using Dbg = System.Management.Automation.Diagnostics;
#if UNIX
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// Implements a cmdlet that allows use of execv API.
/// </summary>
[Experimental(ExperimentalFeature.PSExecFeatureName, ExperimentAction.Show)]
[Cmdlet(VerbsCommon.Switch, "Process", HelpUri = "https://go.microsoft.com/fwlink/?linkid=2181448")]
public sealed class SwitchProcessCommand : PSCmdlet
{
/// <summary>
/// Get or set the command and arguments to replace the current pwsh process.
/// </summary>
[Parameter(Position = 0, Mandatory = false, ValueFromRemainingArguments = true)]
public string[] WithCommand { get; set; } = Array.Empty<string>();
/// <summary>
/// Execute the command and arguments
/// </summary>
protected override void EndProcessing()
{
if (WithCommand.Length == 0)
{
return;
}
// execv requires command to be full path so resolve command to first match
var command = this.SessionState.InvokeCommand.GetCommand(WithCommand[0], CommandTypes.Application);
if (command is null)
{
ThrowTerminatingError(
new ErrorRecord(
new CommandNotFoundException(
string.Format(
System.Globalization.CultureInfo.InvariantCulture,
CommandBaseStrings.NativeCommandNotFound,
command
)
),
"CommandNotFound",
ErrorCategory.InvalidArgument,
WithCommand
)
);
}
var execArgs = new string?[WithCommand.Length + 1];
// execv convention is the first arg is the program name
execArgs[0] = command.Name;
for (int i = 1; i < WithCommand.Length; i++)
{
execArgs[i] = WithCommand[i];
}
// need null terminator at end
execArgs[execArgs.Length - 1] = null;
int exitCode = Exec(command.Source, execArgs);
if (exitCode < 0)
{
ThrowTerminatingError(
new ErrorRecord(
new Exception(
string.Format(
System.Globalization.CultureInfo.InvariantCulture,
CommandBaseStrings.ExecFailed,
Marshal.GetLastPInvokeError(),
string.Join(" ", WithCommand)
)
),
"ExecutionFailed",
ErrorCategory.InvalidOperation,
WithCommand
)
);
}
}
/// <summary>
/// The `execv` POSIX syscall we use to exec /bin/sh.
/// </summary>
/// <param name="path">The path to the executable to exec.</param>
/// <param name="args">
/// The arguments to send through to the executable.
/// Array must have its final element be null.
/// </param>
/// <returns>
/// An exit code if exec failed, but if successful the calling process will be overwritten.
/// </returns>
[DllImport("libc",
EntryPoint = "execv",
CallingConvention = CallingConvention.Cdecl,
CharSet = CharSet.Ansi,
SetLastError = true)]
private static extern int Exec(string path, string?[] args);
}
}
#endif
@@ -222,4 +222,10 @@ Reviewed by TArcher on 2010-07-20
<data name="UseOfDeprecatedCommandWarning" xml:space="preserve">
<value>The {0} is obsolete. {1}</value>
</data>
<data name="ExecFailed" xml:space="preserve">
<value>Exec call failed with errorno {0} for command line: {1}</value>
</data>
<data name="NativeCommandNotFound" xml:space="preserve">
<value>Command '{0}' was not found. The specified command must be an executable.</value>
</data>
</root>
@@ -0,0 +1,74 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
Describe 'Switch-Process tests for Unix' -Tags 'CI' {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if (-not [ExperimentalFeature]::IsEnabled('PSExec') -or $IsWindows)
{
$PSDefaultParameterValues['It:Skip'] = $true
return
}
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
It 'Exec alias should map to Switch-Process' {
$alias = Get-Command exec
$alias | Should -BeOfType [System.Management.Automation.AliasInfo]
$alias.Definition | Should -BeExactly 'Switch-Process'
}
It 'Exec by itself does nothing' {
exec | Should -BeNullOrEmpty
}
It 'Exec given a cmdlet should fail' {
{ exec Get-Command } | Should -Throw -ErrorId 'CommandNotFound,Microsoft.PowerShell.Commands.SwitchProcessCommand'
}
It 'Exec given an exe should work' {
$id, $uname = pwsh -noprofile -noexit -outputformat text -command { $pid; exec uname }
Get-Process -Id $id -ErrorAction Ignore| Should -BeNullOrEmpty
$uname | Should -BeExactly (uname)
}
It 'Exec given an exe and arguments should work' {
$id, $uname = pwsh -noprofile -noexit -outputformat text -command { $pid; exec uname -a }
Get-Process -Id $id -ErrorAction Ignore| Should -BeNullOrEmpty
$uname | Should -BeExactly (uname -a)
}
It 'Exec will replace the process' {
$sleep = Get-Command sleep -CommandType Application | Select-Object -First 1
$p = Start-Process pwsh -ArgumentList "-noprofile -command exec $($sleep.Source) 90" -PassThru
Wait-UntilTrue {
($p | Get-Process).Name -eq 'sleep'
} -timeout 60000 -interval 100 | Should -BeTrue
}
}
Describe 'Switch-Process for Windows' {
BeforeAll {
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
if (-not $IsWindows)
{
$PSDefaultParameterValues['It:Skip'] = $true
return
}
}
AfterAll {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
It 'Switch-Process should not be available' {
Get-Command -Name Switch-Process -ErrorAction Ignore | Should -BeNullOrEmpty
}
It 'Exec alias should not be available' {
Get-Alias -Name exec -ErrorAction Ignore | Should -BeNullOrEmpty
}
}
@@ -16,7 +16,8 @@ $script:cmdletsToSkip = @(
"Get-ExperimentalFeature",
"Enable-ExperimentalFeature",
"Disable-ExperimentalFeature",
"Get-PSSubsystem"
"Get-PSSubsystem",
"Switch-Process"
)
function UpdateHelpFromLocalContentPath {