【问题标题】:C# Powershell in Runspace - How can i get "Format-List" to work?运行空间中的 C# Powershell - 如何让“格式列表”工作?
【发布时间】:2021-06-07 16:01:03
【问题描述】:

我目前正在用 C# 为我们的 Exchange Online 租户编写一个联系人管理器,分别使用“System.Management.Automation”和“System.Management.Automation.Runspaces”中的 Powershell 命令和运行空间。将联系人添加到 GAL 时效果很好。但我一直在编辑联系人。

我需要使用 Powershell 命令获取联系方式。我可以执行的代码如下:

var command = new PSCommand();
command.AddCommand("Get-Contact");
command.AddParameter("Identity", "someContact");

但是:这当然只给了我联系人姓名。我需要扩展该命令。我需要执行的等效本机 Powershell-Command 如下所示:

Get-Contact -Identity "someContact" | Format-List

当我尝试从上面以某种方式将该“格式列表”添加到命令方案中时 - 例如像这样:

var command = new PSCommand();
command.AddCommand("Get-Contact");
command.AddParameter("Identity", "someContact");
command.AddCommand("Format-List");

我得到一个异常,告诉我“格式列表”不是 Cmdlet 的名称或任何东西......我还尝试使用 AddParameter 甚至 AddArgument 添加它 - 这些都不起作用,我总是以错误结束.

使用 Google,我在 Stackoverflow 上找到了线程,人们通过“AddScript()”命令传递了一个脚本。但是当我做这样的事情时:

AddScript("Get-Contact -Identity 'someContact' | Format-List");

它告诉我,由于 Remote-Powershell 正在无语言模式下运行,因此无法识别语法。我不知道,如果可能的话,如何更改该语言模式。

以下是我用来在我们的 Exchange Online 租户上执行 Remote-Powershell-Commands 的完整代码:

        // sPass Variable in SecureString umwandeln (Passwort muss ein SecureString
        // sein, sonst wird es von WSManConnectionInfo nicht akzeptiert!)
        SecureString ssPass = new NetworkCredential("", sPass).SecurePassword;

        // Exchange Online Credentials vorbereiten
        PSCredential credential = new PSCredential(sUserAndMail, ssPass);

        // Connection zu Exchange Online mit der URL vorbereiten und Authentication Mode auf Basic setzen
        WSManConnectionInfo wsEOConnInfo = new WSManConnectionInfo(new Uri(sURI), sSchema, credential);
        wsEOConnInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
        wsEOConnInfo.IdleTimeout = 60000;

        // Runspace erstellen, in dem die Powershell-Befehle ausgeführt werden
        using (Runspace runspace = RunspaceFactory.CreateRunspace(wsEOConnInfo))
        {
            // Connection herstellen
            runspace.Open();

            // Prüfen, ob Connection existiert. Wenn ja, Commands ausführen
            if (runspace.RunspaceStateInfo.State == RunspaceState.Opened)
            {
                PowerShell ps = PowerShell.Create();

                // Zunächst die ExecutionPolicy auf RemoteSigned für den aktuellen Benutzer setzen. 
                // Andernfalls stehen die MailContact-Befehle der Remote-Powershell nicht zur Verfügung.
                ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", "RemoteSigned").AddParameter("Scope", "CurrentUser");
                ps.Invoke();

                // Nun den Befehl auf Basis des Use-Cases zusammenstellen
                switch (SearchCaseValue)
                {
                    case 1:
                    {
                        // Hier den Befehl zum Suchen des Kontakts auf Basis des Namens
                        var command = new PSCommand();
                        command.AddCommand("Get-Contact");
                        command.AddParameter("Identity", "*" + tb_SearchTerm.Text + "*");

                        // Kommando zusammensetzen und die Ausführung in diesem Runspace festlegen
                        ps.Commands = command;
                        ps.Runspace = runspace;

                        // Kommando ausführen
                        try
                        {
                            // Den Output des Invokes einer Collection zum Zugriff auf die Inhalte zuweisen
                            Collection<PSObject> psOutput = ps.Invoke();

                            // Einen neuen StringBuilder instantiieren
                            var sb = new System.Text.StringBuilder();

                            // Wenn der Inhalt der Collection nicht leer ist, dann jede Zeile des Powershell-Ouputs
                            // aus der Collection in neue Zeilen des StringBuilders schreiben
                            foreach (PSObject outputItem in psOutput)
                            {
                                lb_SearchResults.Items.Add(outputItem.ToString());
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message.ToString());
                        }

                        // Hintergrundfarbe der Listbox ändern, da nun Ergebnisse darin angezeigt werden
                        // und den Button zum Reset der Ergebnisse aktivieren
                        lb_SearchResults.BackColor = Color.White;
                        panel_SearchInProgress.Visible = false;
                        bt_ResetSearchResults.Enabled = true;

                        // Runspace Schließen
                        runspace.Close();
                        break;
                    }
                    case 2:
                    {
                        // Hier den Befehl zum Suchen des Kontakts auf Basis der eMail-Adresse erstellen
                        var command = new PSCommand();
                        command.AddCommand("Get-Contact");
                        command.AddParameter("Filter", "((WindowsEmailAddress -like '*" + tb_SearchTerm.Text + "*'))");

                        // Kommando zusammensetzen und die Ausführung in diesem Runspace festlegen
                        ps.Commands = command;
                        ps.Runspace = runspace;

                        // Kommando ausführen
                        try
                        {
                            // Den Output des Invokes einer Collection zum Zugriff auf die Inhalte zuweisen
                            Collection<PSObject> psOutput = ps.Invoke();

                            // Einen neuen StringBuilder instantiieren
                            var sb = new System.Text.StringBuilder();

                            // Wenn der Inhalt der Collection nicht leer ist, dann jede Zeile des Powershell-Ouputs
                            // aus der Collection in neue Zeilen des StringBuilders schreiben
                            foreach (PSObject outputItem in psOutput)
                            {
                                lb_SearchResults.Items.Add(outputItem.ToString());
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message.ToString());
                        }

                        // Hintergrundfarbe der Listbox ändern, da nun Ergebnisse darin angezeigt werden
                        // und den Button zum Reset der Ergebnisse aktivieren
                        lb_SearchResults.BackColor = Color.White;
                        panel_SearchInProgress.Visible = false;
                        bt_ResetSearchResults.Enabled = true;

                        // Runspace Schließen
                        runspace.Close();
                        break;
                    }
                }    
            }
            // Runspace schließen, falls nicht bereits geschehen. Wichtig, da in Exchange Online
            // nur maximal 3 Runspaces (Connections) gleichzeitig offen sein dürfen!
            runspace.Dispose();
        }

我希望您发现此摘录有助于解决问题。对不起那里的德国评论。我需要知道我在做什么,你知道吗?! :-)

那么...你能告诉我如何在不使用脚本的情况下将“格式列表”传递给远程 Powershell 吗?

非常感谢您提前提供的帮助! 史蒂芬

【问题讨论】:

  • Get-Contact cmdlet 将在 Microsoft 的服务器上运行 - 而 Microsoft 显然不希望你只能运行 任何东西 ,因此它们只允许您执行一组限制性命令,而不能运行任意代码块(因此是“NoLanguage”模式)。如果您想进一步处理/格式化它,您必须将远程运行空间的输出反馈到本地运行空间

标签: c# powershell runspace


【解决方案1】:

注意:

  • 以下部分展示了如何在本地中进一步处理从远程PowerShell运行空间获得的对象 em> 运行空间,在这种情况下,出于安全原因,这是必需的。

    • 这里,Format-List 命令必须在本地应用,但请注意,通常不需要调用Format-*用于处理从 PowerShell SDK 调用返回的对象的 cmdlet - 请参阅下一点;如果要创建对象的 for-display, string 表示,则只需要 Format-*,就像在 PowerShell 控制台(终端)中看到的那样。
  • 底部部分讨论了如何处理从 PowerShell SDK 调用返回的对象一般

    • 事实证明,直接将输出对象作为 数据 工作是 Steffen 真正想要的。

Mathias R. Jessen 在评论中提供了关键指针:

  • 出于安全原因,您的远程运行空间在language mode 和允许您执行的特定cmdlet 方面都受到限制

    • NoLanguage 模式禁止使用任何类型的 PowerShell 代码,从而排除了使用 .AddScript() 方法的可能性。
    • 貌似不允许使用Format-List cmdlet。
  • 如果您确实需要将Format-List 应用于远程运行空间的输出,则需要使用second PowerShell 实例来本地 执行Format-List,将远程运行空间的输出传递给它 - 见下文。

    • 正如其他人所暗示的那样,Format-List 仅在您想要输出对象的 for-display-onlystring 表示时才需要,就像通常一样显示在 PowerShell 控制台(终端)中。

      • 否则,处理数据,只需直接使用返回的对象及其属性,如底部所示。
    • 另外,Format-List 本身并不输出字符串,而是输出包含格式化指令对象;要将后者转换为它们编码的格式化字符串表示,请将它们传递给Out-String cmdlet。

一个简化的例子:

PowerShell psRemote = PowerShell.Create();
// Set up the remote runspace ...

PowerShell psLocal = PowerShell.Create();
// NO setup required for a local runspace.

using (psRemote)
using (psLocal) 
{

  // Get output from the remote runspace.
  var remoteOutput = psRemote.AddCommand("Get-Date").Invoke();

  // Pass the output to the local runspace for display formatting.
  foreach (var o in psLocal
                     .AddCommand("Format-List")
                     .AddCommand("Out-String")
                     .Invoke(remoteOutput)) 
  {
    // Print each object's display representation (a single, multi-line string)
    // To get the representation *line by line*, insert `.AddParameter("Stream")`
    // before the .Invoke()
    Console.WriteLine(o);
  }

}

直接使用来自 PowerShell SDK 调用的输出对象:

Muhammad Arsalan Altaf's answer 显示了一种处理从非泛型.Invoke() 方法调用返回的PSObject 实例集合的方法,使用PSObject 类型的反射成员,例如.Members.Properties

但是,由于PSObject 实现了IDynamicMetaObjectProvider 接口,您可以通过dynamic 变量 使用DLR,这大大简化了事情

代替:

foreach (PSObject outputItem in ps.Invoke())
{                                
     string name = outputItem.Properties["Name"].Value;
     // ...
}

感谢输入枚举变量dynamic,您可以这样做:

foreach (dynamic outputItem in ps.Invoke())
{                                
     string name = outputItem.Name; // use direct property access via the DLR
     // ...
}

一般最好使用.Invoke()方法的通用形式(例如,ps.Invoke&lt;FileInfo&gt;(),以便获得早期绑定的静态类型。

但是,这并不总是一种选择,即:

  • 如果输出对象是PSObject类型的动态对象,则适用于:

    • [pscustomobject] 实例,它们是动态构造的自定义对象,最终由PSObject 实现。
    • 通过remoting 返回的对象,如您的情况,原始.NET 类型标识通常丢失PSObject 实例用于模拟 原始类型 - 请参阅this answer,了解 PowerShell 用于远程处理的基于 XML 的序列化的概述以及何时丢失类型保真度的说明。
  • 如果输出对象不是全部相同类型

    • 但是,您可以稍后使用.BaseObject 属性使用as 运算符或switch 表达式或语句来获取包装在PSObject 实例中的强类型对象。

以下示例代码说明了对象处理方法

  • 您可以将此代码编译为控制台应用程序,前提是您已添加对 PowerShell SDK NuGet 包的引用 - 请参阅 this answer
using System;
using System.IO;
using System.Management.Automation;

namespace demo
{

  class ConsoleApp
  {
    static void Main(string[] args)
    {

      using (var ps = PowerShell.Create())
      {

        // Use `dynamic` to enumerate the *Collection<PSObject>* instance that is 
        // returned from the non-generic .Invoke() call.
        // This is necessary for:
        //   - [pscustomobject] instances
        //   - "rehydrated" object instances received via *remoting* that have
        //     lost their original type identity ([psobject] == [pscustomobject])
        //   - multiple objects that don't all have the same type.
        foreach (dynamic o in ps.AddScript("[pscustomobject] @{ Foo = 42 }").Invoke())
        {
          // Note: Trying to access a nonexistent property quietly returns null.
          Console.WriteLine($"Dynamic: {o.Foo}"); // -> 42
        }

        ps.Commands.Clear();

        // If the return objects *all have the same type* (other than PSObject),
        // use the generic form of the .Invoke() method and specify that time <T>
        // This returns a *Collection<T>* instance, the members of which you
        // access with early binding, as usual.
        foreach (DateTime o in ps.AddCommand("Get-Date").Invoke<DateTime>())
        {
          Console.WriteLine($"Static: {o.Year}"); // -> this year
        }

        ps.Commands.Clear();

        // *Hybrid approach* for *non-PSCustomObjects* of *non-uniform type*
        // Work with Collection<PSObject>, but use `.BaseObject as <T>`
        // to work with statically typed objects.
        // Here, a `switch` expression (C# 8+) is used, but note that with `as`, when `<T>` is a *value type*,
        // `as <T>?` must be used, i.e. a *nullable* type.
        foreach (PSObject o in ps.AddCommand("Get-Date").AddStatement().AddCommand("Get-Item").AddArgument("~").AddStatement().AddCommand("Get-Location").Invoke())
        {
          Console.WriteLine(
            o.BaseObject switch
            {
              DateTime dt => $"DateTime: {dt}",
              DirectoryInfo fi => $"DirectoryInfo: {fi}",
              _ => $"Other ({o.BaseObject.GetType().FullName}): {o.BaseObject}"
            }
          );
        }
      }

    }
  }
}

上面打印的内容如下:

Dynamic: 42
Static: 2021
DateTime: 3/10/2021 10:39:36 AM
DirectoryInfo: /Users/jdoe
Other (System.Management.Automation.PathInfo): /Users/jdoe/Desktop

【讨论】:

    【解决方案2】:

    ps.Invoke() 返回的PSObject 包含对象的所有属性。您可以获得所有属性的值。 试试这个

    ICollection<PSObject> psOutput = ps.Invoke();
    foreach (PSObject outputItem in psOutput)
    {                                
         var name = outputItem.Members["Name"].Value.ToString();
         var distinguishedName = outputItem.Members["DistinguishedName"].Value.ToString();
         var displayName = outputItem.Members["DisplayName"].Value.ToString();
         var lName = outputItem.Members["LastName"].Value.ToString();
    
    }
    

    只需输入您要获取的属性名称,它就会返回该属性的值。

    【讨论】:

    • 谢谢。这就是解决方案!我不知道我需要的只是在那个对象里面。现在它完全有道理。谢谢!
    • 伟大的指针。请注意,虽然使用System.Management.Automation.PSObject 的成员(例如.Members.Properties)绝对是一种选择,但使用允许直接成员访问的dynamic 变量要方便得多(例如,foreach (dynamic outputItem in psOutput) { var name = outputItem.Name; } )
    【解决方案3】:

    帮助我了解为什么需要将联系人对象传递给Format-Table。这基本上是在破坏接触对象本身。 Format-Table 只是将 PSObject 布局到 PS Host 的一种方式,一旦将对象传递给此函数,该对象将丢失其所有属性和方法。

    我将向您展示我对 AD 用户对象的含义的示例。

    没有格式表:

    PS C:\> $aduser=Get-ADuser -Filter *|select -First 1
    
    PS C:\> $aduser.GetType()
    
    IsPublic IsSerial Name                                     BaseType                                                         
    -------- -------- ----                                     --------                                                         
    True     False    ADUser                                   Microsoft.ActiveDirectory.Management.ADAccount                   
    
    PS C:\> $aduser.psobject.Properties.Name
    DistinguishedName
    Enabled
    GivenName
    Name
    ObjectClass
    ObjectGUID
    SamAccountName
    SID
    Surname
    UserPrincipalName
    PropertyNames
    AddedProperties
    RemovedProperties
    ModifiedProperties
    PropertyCount
    

    使用格式表:

    PS C:\> $aduser=Get-ADuser -Filter *|select -First 1|format-table
    
    PS C:\> $aduser.GetType()
    
    IsPublic IsSerial Name                                     BaseType                                                         
    -------- -------- ----                                     --------                                                         
    True     True     Object[]                                 System.Array                                                     
    
    PS C:\> $aduser.psobject.Properties.name
    Count
    Length
    LongLength
    Rank
    SyncRoot
    IsReadOnly
    IsFixedSize
    IsSynchronized
    

    我希望这是有道理的。

    【讨论】:

    • 确实如此。我不知道命令结果的输出与实际 PSObject 的内容不同。这很有意义。谢谢。
    猜你喜欢
    • 1970-01-01
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-03
    • 2020-11-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多