从 C# 调用 PowerShell 的另一种解决方案






4.88/5 (5投票s)
带有解释的基本 PowerShell 处理程序 C# 类。
引言
从 C# 支持 PowerShell 有点麻烦。在这里,我提供了一个简单的解决方案,而无需深入研究环境的细节。本文旨在成为一个系列文章的基础,该系列文章将通过从 C# 编写的程序调用 PowerShell 来支持各种 Microsoft 环境。
必备组件
所有示例均在 MS Visual Studio Community 2019 的 Windows 10 Pro 21H2 19044.1387 环境中创建、编译和测试。
要成功编译,需要安装 NuGet 包:Microsoft.PowerShell.3.ReferenceAssemblies
。
自定义扩展库中方法的先决条件
您可以使用相应命名空间中已定义的方法和类,但我包含了一些简单的解决方案示例,以供您理解和阐明。您当然可以以不同的方式(更简单或更清晰地)编写它们,但我们不必在此过多纠结。
以下方法在字符串值为空或仅包含空格时返回true
public static bool IsNullEmptyOrWhite(this string sValue) {...}
定义 PowerShell 参数的类,形式为Name
/Value
。它可以被任何方便的dictionary
类替换
public class ParameterPair
{
public string Name { get; set; } = string.Empty;
public object Value { get; set; } = null;
}
Using the Code
在 PowerShell 环境中执行命令
当然,有很多方法可以打开 PowerShell、在其内部发出命令并获取其结果。我将其限制为两种
- 调用脚本,其中所有命令及其数据都必须表示为一行文本
- 调用 Cmdlet,我们一次只能提供一个命令,并且其参数作为
Name
/Value
对传递,其中值可以是任何类型
因此,定义了两个具有不同参数集的RunPS
方法,它们在经过适当的参数处理后,使用统一的ExecutePS
方法。
/// <summary>
/// Basic method of calling PowerShell a script where all commands
/// and their data must be presented as one line of text
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">A single line of text
/// containing commands and their parameters (in text format)</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
//Programmer's Commandment I: Remember to reset your variables
outs = new Collection<PSObject>();
HasError = false;
//Cleanup of PowerShell also due to commandment I
ps.Commands.Clear();
ps.Streams.ClearStreams();
//We put the script into the PowerShell environment
//along with all commands and their parameters
ps.AddScript(psCommand);
//We are trying to execute our command
outs = ExecutePS(ps);
//The method returns true when executed correctly and false
//when some errors have occurred
return !HasError;
}
/// <summary>
/// Method 2 cmdlet call where we can only give one command at a time
/// and its parameters are passed as Name/Value pairs,
/// where values can be of any type
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">Single command with no parameters</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <param name="parameters">A collection of parameter pairs
/// in the form Name/Value</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand,
out Collection<PSObject> outs, params ParameterPair[] parameters)
{
//Programmer's Commandment I: Remember to reset your variables
outs = new Collection<PSObject>();
HasError = false;
if (!psCommand.Contains(' '))
{
//Cleanup of PowerShell also due to commandment I
ps.Commands.Clear();
ps.Streams.ClearStreams();
//We put a single command into the PowerShell environment
ps.AddCommand(psCommand);
//Now we enter the command parameters in the form of Name/Value pairs
foreach (ParameterPair PP in parameters)
{
if (PP.Name.IsNullEmptyOrWhite())
{
LastException = new Exception("E1008:Parameter cannot be unnamed");
return false;
}
if (PP.Value == null) ps.AddParameter(PP.Name);
else ps.AddParameter(PP.Name, PP.Value);
}
//We are trying to execute our command
outs = ExecutePS(ps);
}
//And here we have a special exception
//if we tried to apply the method not to a single command
else LastException = new Exception
("E1007:Only one command with no parameters is allowed");
//The method returns true when executed correctly
//and false when some errors have occurred
return !HasError;
}
/// <summary>
/// Internal method in which we try to execute a script or command with parameters
/// This method does not need to return a fixed value
/// that indicates whether or not the execution succeeded,
/// since the parent methods use the principal properties of the class set in it.
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <returns>A collection of objects that contains the feedback</returns>
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
Collection<PSObject> retVal = new Collection<PSObject>();
//We are trying to execute our script
try
{
retVal = ps.Invoke();
// ps.HadErrors !!! NO!
// The PowerShell environment has a special property that
// indicates in the assumption whether errors have occurred
// unfortunately, most often I have found that despite errors,
// its value is false or vice versa,
// in the absence of errors, it pointed to the truth.
//Therefore, we check the fact that errors have occurred,
//using the error counter in PowerShell.Streams
if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
{
//We create another general exception, but we do not raise it.
LastException = new Exception("E0002:Errors were detected during execution");
//And we write runtime errors to the LastErrors collection
LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
}
}
//We catch script execution errors and exceptions
catch (Exception ex)
{
//And if they do, we create a new general exception but don't raise it
LastException = new Exception("E0001:" + ex.Message);
}
//Returns a collection of results
return retVal;
}
如何使用这样准备的类
首先,我们创建一个空的集合用于存放结果
Collection<PSObject> Results = new Collection<PSObject>();
然后我们需要打开单个 PowerShell 环境
PowerShell ps = PowerShell.Create();
然后尝试执行脚本
if (PS.Basic.RunPS(ps, "Get-Service |
Where-Object {$_.canpauseandcontinue -eq \"True\"}", out Results))
{ Interpreting the results… }
else
{ Interpretation of errors… }
或者带有参数的命令
if (PS.Basic.RunPS(ps, "Get-Service", out Results,
new ParameterPair { Name = "Name", Value = "Spooler"}
))
{ Interpreting the results… }
else
{ Interpretation of errors… }
下一步是解释结果
在每种情况下,我们都会检查 Collection<PSObject>
Results
集合。
例如,对于脚本 "Get-Service ...
",该集合包含基本类型 (Results [0] .BaseObject.GetType()) ServiceController
) 的对象,其中包含属性 "name
"。我们可以通过 Results [0] .Properties ["name"]. Value
来读取它。
结果的解释归结为检查Results
并检索我们感兴趣的相应属性的值。
或者解释错误
当尝试执行 PowerShell 命令时出错时,基类有几个属性和变量用于处理。
由于命令准备不当而导致的执行错误
这些错误可能源于在执行命令之前准备 PowerShell 时出错。例如,我们可能会忘记通过一个空的ps
来打开环境。或者,我们可能会传递一些语法错误而不是正确的脚本/命令。
在这种情况下,在尝试调用ps.Invoke();
时会发生一个异常,您可以在其描述中找到错误的根本原因。在基类中,LastException
变量被定义为消息 "E0001:" + ex.Message
(即,在代码 "E0001
" 前面加上异常描述)。
在错误解释阶段,您可以通过检查 "ErrorCode
" 属性的值(PS.Basic.ErrorCode == 1
)来检查是否发生了此类错误,然后使用 LastException.Message
中的描述进行更详细的错误处理。
命令执行错误
我们还可能在执行完全有效的命令或脚本时遇到错误。例如,当我们指定一个对象的 Identity,而该值在 PowerShell 环境的可视范围中不存在任何对象时。这时我们会收到 "not found" 或 "wrong name" 的错误,但仅仅尝试执行命令不会引起异常。
我们可以在 PSDataCollection<ErrorRecord> LastErrors
集合中找到此类错误。
所引入的类中的错误处理模型将导致 LastException
中出现一个新异常,其描述形式为:"E0002: Errors were detected during execution
"。在通过 "ErrorCode" (PS.Basic.ErrorCode == 2)
检查后,我们可以从集合中读取后续错误,并根据 LastErrors [0] .Exception.Message
中的异常描述确定其原因。与 LastException
一样,现在需要在此基础上进行更详细的错误处理。
C# 下 PowerShell 处理类完整代码
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Management.Automation;
namespace PS
{
public static class Basic
{
/// <summary>
/// The last exception that occurred in the PS.Basic class
/// </summary>
public static Exception LastException = null;
/// <summary>
/// Collection of PowerShell runtime errors
/// </summary>
public static PSDataCollection<ErrorRecord> LastErrors =
new PSDataCollection<ErrorRecord>();
/// <summary>
/// Auxiliary Property that helps to check if there was an error and
/// resets the error state
/// </summary>
public static bool HasError
{
get
{
return LastException != null;
}
set
{
if(!value)
{
LastException = null;
LastErrors = new PSDataCollection<ErrorRecord>();
}
}
}
/// <summary>
/// A helper Property to help you get the error code
/// </summary>
public static int ErrorCode
{
get
{
if (HasError) return int.Parse(LastException.Message.Substring(1, 4));
return 0;
}
}
/// <summary>
/// Basic method of calling PowerShell a script where all commands
/// and their data must be presented as one line of text
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">A single line of text containing commands
/// and their parameters (in text format)</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS
(PowerShell ps, string psCommand, out Collection<PSObject> outs)
{
//Programmer's Commandment I: Remember to reset your variables
outs = new Collection<PSObject>();
HasError = false;
//Cleanup of PowerShell also due to commandment I
ps.Commands.Clear();
ps.Streams.ClearStreams();
//We put the script into the PowerShell environment
//along with all commands and their parameters
ps.AddScript(psCommand);
//We are trying to execute our command
outs = ExecutePS(ps);
//The method returns true when executed correctly and false
//when some errors have occurred
return !HasError;
}
/// <summary>
/// Method 2 cmdlet call where we can only give one command
/// at a time and its parameters are passed as Name/Value pairs,
/// where values can be of any type
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <param name="psCommand">Single command with no parameters</param>
/// <param name="outs">A collection of objects that contains the feedback</param>
/// <param name="parameters">A collection of parameter pairs
/// in the form Name/Value</param>
/// <returns>The method returns true when executed correctly
/// and false when some errors have occurred</returns>
public static bool RunPS(PowerShell ps, string psCommand,
out Collection<PSObject> outs, params ParameterPair[] parameters)
{
//Programmer's Commandment I: Remember to reset your variables
outs = new Collection<PSObject>();
HasError = false;
if (!psCommand.Contains(' '))
{
//Cleanup of PowerShell also due to commandment I
ps.Commands.Clear();
ps.Streams.ClearStreams();
//We put a single command into the PowerShell environment
ps.AddCommand(psCommand);
//Now we enter the command parameters in the form of Name/Value pairs
foreach (ParameterPair PP in parameters)
{
if (PP.Name.IsNullEmptyOrWhite())
{
LastException = new Exception("E1008:Parameter cannot be unnamed");
return false;
}
if (PP.Value == null) ps.AddParameter(PP.Name);
else ps.AddParameter(PP.Name, PP.Value);
}
//We are trying to execute our command
outs = ExecutePS(ps);
}
//And here we have a special exception if we tried
//to apply the method not to a single command
else LastException = new Exception("E1007:Only one command
with no parameters is allowed");
//The method returns true when executed correctly and false
//when some errors have occurred
return !HasError;
}
/// <summary>
/// Internal method in which we try to execute a script or command with parameters
/// This method does not need to return a fixed value that indicates
/// whether or not the execution succeeded,
/// since the parent methods use the principal properties of the class set in it.
/// </summary>
/// <param name="ps">PowerShell environment</param>
/// <returns>A collection of objects that contains the feedback</returns>
private static Collection<PSObject> ExecutePS(PowerShell ps)
{
Collection<PSObject> retVal = new Collection<PSObject>();
//We are trying to execute our script
try
{
retVal = ps.Invoke();
// ps.HadErrors !!! NO!
// The PowerShell environment has a special property
// that indicates in the assumption whether errors have occurred
// unfortunately, most often, I have found that despite errors
// its value is false or vice versa,
// in the absence of errors, it pointed to the truth.
// Therefore, we check the fact that errors have occurred,
// using the error counter in PowerShell.Streams
if (ps.Streams.Error.Count > 0) //czy są błędy wykonania
{
//We create another general exception, but we do not raise it.
LastException = new Exception
("E0002:Errors were detected during execution");
//And we write runtime errors to the LastErrors collection
LastErrors = new PSDataCollection<ErrorRecord>(ps.Streams.Error);
}
}
//We catch script execution errors and exceptions
catch (Exception ex)
{
//And if they do, we create a new general exception but don't raise it
LastException = new Exception("E0001:" + ex.Message);
}
//Returns a collection of results
return retVal;
}
}
/// <summary>
/// Class defining the PowerShell parameter in the form Name/Value.
/// it can be replaced with any convenient dictionary class
/// </summary>
public class ParameterPair
{
public string Name { get; set; } = string.Empty;
public object Value { get; set; } = null;
}
}
历史
- 2021 年 11 月 26 日:文章初稿