【问题标题】:Powershell test for noninteractive mode非交互模式的 Powershell 测试
【发布时间】:2021-09-24 03:47:00
【问题描述】:

我有一个脚本,可以手动运行,也可以由计划任务运行。我需要以编程方式确定我是在 -noninteractive 模式(通过计划任务运行时设置)还是正常模式下运行。我已经用谷歌搜索了,我能找到的最好的方法是添加一个命令行参数,但是我没有任何可行的方法来处理计划任务,我也不能合理地期望用户在运行它时添加参数手动。 非交互模式是否设置了某种变量或我可以在脚本中检查的东西?

编辑: 实际上,我无意中回答了自己的问题,但我将其留在这里以供后代使用。

我在脚本中插入了一个读取主机以向用户询问某些内容,当它在非交互模式下运行时,繁荣,终止错误。将其放入 try/catch 块中,并根据我所处的模式执行操作。

不是最漂亮的代码结构,但它可以工作。如果其他人有更好的方法,请添加它!

【问题讨论】:

    标签: powershell


    【解决方案1】:

    我不喜欢任何其他答案作为完整的解决方案。 [Environment]::UserInteractive 报告用户是否是交互式的,而不是具体的进程是否是交互式的。该 api 对于检测您是否在服务中运行很有用。这是我处理这两种情况的解决方案:

    function Assert-IsNonInteractiveShell {
        # Test each Arg for match of abbreviated '-NonInteractive' command.
        $NonInteractive = [Environment]::GetCommandLineArgs() | Where-Object{ $_ -like '-NonI*' }
    
        if ([Environment]::UserInteractive -and -not $NonInteractive) {
            # We are in an interactive shell.
            return $false
        }
    
        return $true
    }
    

    【讨论】:

    • [Environment]::UserInteractive 完全符合我的预期和想要的。在 ISE 和 Powershell 控制台下,它返回 true,作为计划任务,它返回 false。谢谢。
    【解决方案2】:

    您可以检查如何使用 Get-WmiObject 为 WMI 对象调用 powershell:

    (gwmi win32_process | ? { $_.processname -eq "powershell.exe" }) | select commandline
    
    #commandline
    #-----------
    #"C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe" -noprofile -NonInteractive
    

    更新:2020-10-08

    从 PowerShell 3.0 开始,此 cmdlet 已被 Get-CimInstance 取代

    (Get-CimInstance win32_process -Filter "ProcessID=$PID" | ? { $_.processname -eq "pwsh.exe" }) | select commandline
    
    #commandline
    #-----------
    #"C:\Program Files\PowerShell\6\pwsh.exe"
    

    【讨论】:

    • 你领先我几分钟。
    • 在运行多个 PowerShell.exe 实例时避免误报:gwmi -Class Win32_Process -Filter "ProcessID=$PID" | Select -Expand CommandLine
    • 作为你想要的布尔值((gwmi win32_process | ? { $_.processname -eq "powershell.exe" }).commandline -match "-NonInteractive")
    • 我不信任这些命令行参数。如果我从非交互式 Windows 会话(如服务)通过常规批处理文件(也可以交互式运行)运行 PowerShell 脚本会怎样?那么谁来设置 -NonInteractive 选项呢?我在 C# 测试 Environment.UserInteractiveGetConsoleWindow 和测试 std 句柄以进行重定向方面进行了完整的测试。但是 P/Invoke 在 PS 中似乎不实用。
    • WMI 是一种非常昂贵的方法来实现这一点,并且使用 PowerShell 而不是 WMI 来过滤进程会增加成本。总体来说,这有点像跑到外面看窗外的灯是否亮着。
    【解决方案3】:

    交互性测试应该同时考虑流程和用户。可以使用带有轻量级-like 条件的简单过滤器来寻找-NonInteractive(至少-noni)powershell 开关以确定进程交互性(非常类似于@VertigoRay 的脚本):

    function Test-Interactive
    {
        <#
        .Synopsis
            Determines whether both the user and process are interactive.
        #>
    
        [CmdletBinding()] Param()
        [Environment]::UserInteractive -and
            !([Environment]::GetCommandLineArgs() |? {$_ -ilike '-NonI*'})
    }
    

    这避免了 WMI、流程探索、命令式混乱、双重否定命名甚至完整的正则表达式的开销。

    【讨论】:

    • 我喜欢您提供的简洁性,但我不确定您为什么要添加 [CmdletBinding()] 的开销和一个空的 Param() 块。使用 Measure-Command 并且您发布的代码需要 6 毫秒,而我发布的代码运行需要 5.3 毫秒。很遗憾您主张轻量级,但您的代码运行时间始终更长(尽管不到 1 毫秒)。无论如何,我喜欢你的猫剥皮过程的简洁......所以+1......我调整了我的 GitHub 以效仿;但是我让我的答案更加冗长以供学习。现在,我的 GitHub 需要 3 毫秒才能运行……谢谢! ;)
    • 我还更改了我的示例和 GitHub 以使用 -like 而不是 -ilike ... 因为 PowerShell 默认情况下不区分大小写。出于某种原因,使用-ilike 会为执行时间增加相当一致的 0.3 毫秒。既然我们谈论的是轻量级和速度,就把它扔在那里。
    • @VertigoRay [CmdletBinding()] Param() 是获取常用参数的习惯,我猜这里并不需要。我想-like 工作正常,我可能是为了防止在配置文件脚本中更改默认的区分大小写,但这看起来不太可能(或不可能?为什么甚至有-ilike?)。为什么要如此积极地优化速度(我只是为了简单)?这应该每个脚本只运行一次,而不是循环或频繁运行。编译后的 cmdlet 会更快!
    • 我完全同意。使用 powershell,我们正在处理不同程度的缓慢。我以为您是在追求速度,因为您提到了其他选项的开销;我喜欢比较优化它的方法。如果您想要简单,则必须记住,powershell 为新程序员提供了较低的入门门槛。我觉得你布置函数的方式对新手来说不太可读。当然,简单是相对的。
    • 在现代 PowerShell 中,您应该跳过 Where-Object,直接写:-and !([Environment]::GetCommandLineArgs() -match '-noni')
    【解决方案4】:

    我想在此处提供更新的答案,因为在 .NET Core(运行 microsoft/nanoserver 的容器)和 .NET Full(运行 microsoft/windowsservercore 的容器)之间,[Environment]::UserInteractive 的行为似乎不同。

    虽然[Environment]::UserInteractive 将在“常规”Windows 中返回TrueFalse,但它将在“nanoserver”中返回$null

    如果您想要一种方法来检查交互模式而不考虑值,请将此检查添加到您的脚本中:

    ($null -eq [Environment]::UserInteractive -or [Environment]::UserInteractive)

    编辑:要回答为什么不检查真实性的评论,请考虑以下假设的真值表:

    left  | right  | result 
    =======================
    $null | $true  | $false
    $null | $false | $true (!) <--- not what you intended
    

    【讨论】:

    • 出于好奇,你为什么不能测试[Environment]::UserInteractive的真实性? $null 应该评估为 $False
    • 我可能误解了一些特定于powershell的东西,但是AIUI,您应该始终将$null放在比较操作的左侧。有关更多信息,请参阅我编辑的答案
    • 无论如何,[Environment]::UserInteractive 不会告诉你 PowerShell 正在交互式运行...
    • 根据旧测试,这个doesn't do anything.
    • 而不是-or,您可以只使用if (-not [Environment]::UserInteractive) 来实现相同的效果,如果环境变量是$null$false,这将起作用
    【解决方案5】:

    -Noninteractive 开关用于启动 PowerShell 提示符时,这将返回一个布尔值。

    [Environment]::GetCommandLineArgs().Contains('-NonInteractive')
    

    【讨论】:

    • 我认为这里的最佳答案。在 powershell 脚本步骤中,UserInteractive 无法在 nix 章鱼服务器上运行。
    • 如果用户将选项缩写为 -NonI,则会失败。
    【解决方案6】:

    我认为这个问题需要更彻底的评估。

    • “交互式”表示 shell 作为 REPL 运行 - 一个连续的读取-执行-打印循环。

    • “非交互式”表示 shell 正在执行脚本、命令或脚本块,并在执行后终止

    如果 PowerShell 使用 -Command-EncodedCommand-File 中的任何一个选项运行,则它是非交互式的。不幸的是,您也可以运行 选项的脚本 (pwsh script.ps1),因此没有可靠的方法来检测 shell 是否是交互式的。

    那么我们是不是运气不好?不,幸运的是 PowerShell 会自动添加选项,我们可以测试 PowerShell 是否运行脚本块或通过 ssh 运行以执行命令 (ssh user@host command)。

    function IsInteractive {
        # not including `-NonInteractive` since it apparently does nothing
        # "Does not present an interactive prompt to the user" - no, it does present!
        $non_interactive = '-command', '-c', '-encodedcommand', '-e', '-ec', '-file', '-f'
    
        # alternatively `$non_interactive [-contains|-eq] $PSItem`
        -not ([Environment]::GetCommandLineArgs() | Where-Object -FilterScript {$PSItem -in $non_interactive})
    }
    
    

    现在在您的 PowerShell 配置文件中测试它是否处于交互模式,因此当您执行脚本、命令或脚本块时配置文件不会运行(您仍然必须记住运行 pwsh -f script.ps1 - 而不是 pwsh script.ps1

    if (-not (IsInteractive)) {
        exit
    }
    

    【讨论】:

    • 您也可以添加[Environment]::CommandLine -match '\.ps1'。想不出会返回意外结果的场景。
    【解决方案7】:

    我想出了一个现有的和经过验证的 C# 代码的高级端口,它使用相当多的 P/Invoke 来确定所有极端情况。这段代码在我的PowerShell Build Script 中使用,用于协调围绕 Visual Studio 项目的多个构建任务。

    # Some code can be better expressed in C#...
    #
    Add-Type @'
    using System;
    using System.Runtime.InteropServices;
    
    public class Utils
    {
        [DllImport("kernel32.dll")]
        private static extern uint GetFileType(IntPtr hFile);
    
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetStdHandle(int nStdHandle);
    
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetConsoleWindow();
    
        [DllImport("user32.dll")]
        private static extern bool IsWindowVisible(IntPtr hWnd);
    
        public static bool IsInteractiveAndVisible
        {
            get
            {
                return Environment.UserInteractive &&
                    GetConsoleWindow() != IntPtr.Zero &&
                    IsWindowVisible(GetConsoleWindow()) &&
                    GetFileType(GetStdHandle(-10)) == 2 &&   // STD_INPUT_HANDLE is FILE_TYPE_CHAR
                    GetFileType(GetStdHandle(-11)) == 2 &&   // STD_OUTPUT_HANDLE
                    GetFileType(GetStdHandle(-12)) == 2;     // STD_ERROR_HANDLE
            }
        }
    }
    '@
    
    # Use the interactivity check somewhere:
    if (![Utils]::IsInteractiveAndVisible)
    {
        return
    }
    

    【讨论】:

    • 仅供参考,这在 Powershell ISE 中不起作用(至少在 PSVersion:5.1.14409.1012 上)。如果您将所有方法都转换为 public,您会看到失败的部分是:[Utils]::IsWindowVisible([Utils]::GetConsoleWindow())。这当然是一些 ISE 错误/功能 :)。作为参考,列出了 ISE 和常规 Powershell 之间的一些差异here。没有看到任何关于 IsWindowVisible() 的信息,但我怀疑它属于该列表。
    • @PetruZaharia 这个我没试过,而且这个代码在 ISE 中甚至没有意义,但它可能找不到它的控制台窗口。
    • 是的,唯一的缺陷是它假设所有 PowerShell 都托管在控制台中(但是,这里的所有其他答案都假设 PowerShell.exe,所以......)。
    【解决方案8】:
    C:\> powershell -NoProfile -NoLogo -NonInteractive -Command "[Environment]::GetCommandLineArgs()"
    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
    -NoProfile
    -NoLogo
    -NonInteractive
    -Command
    [Environment]::GetCommandLineArgs()
    

    【讨论】:

    • 请将粘贴的代码缩进为代码 - 并解释您的答案为何有效。
    【解决方案9】:

    实现两个脚本,一个是手动启动的core.ps1,一个是带参数启动core.ps1的scheduled.ps1。

    【讨论】:

    • 谢谢丹。这是一个好主意,但是我们设置整个计划任务的方式使得除了我之外的任何人都变得有点复杂和困惑(甚至在我忘记这个一周并回到它之后的我)。
    • 这行得通吗? powershell.exe -noexit script.ps1 -Argument1 {arg1}-Argument2 {arg2}
    • 想出了一种我可以做到的方法,但给你答案复选标记,因为你的答案在技术上是一个答案,而不是我使用的那个:)
    【解决方案10】:
    powerShell -NonInteractive { Get-WmiObject Win32_Process -Filter "Name like '%powershell%'" | select-Object CommandLine }
    
    powershell -Command { Get-WmiObject Win32_Process -Filter "Name like '%powershell%'" | select-Object CommandLine }
    

    在第一种情况下,您将获得“-NonInteractive”参数。后者你不会。

    【讨论】:

      【解决方案11】:

      脚本:IsNonInteractive.ps1

      function Test-IsNonInteractive()
      {
          #ref: http://www.powershellmagazine.com/2013/05/13/pstip-detecting-if-the-console-is-in-interactive-mode/
          #powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
          return [bool]([Environment]::GetCommandLineArgs() -Contains '-NonInteractive')
      }
      
      Test-IsNonInteractive
      

      示例用法(来自命令提示符)

      pushd c:\My\Powershell\Scripts\Directory
      ::run in non-interactive mode
      powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
      ::run in interactive mode
      powershell -File .\IsNonInteractive.ps1
      popd
      

      更多参与示例PowerShell脚本

      #script options
      $promptForCredentialsInInteractive = $true
      
      #script starts here
      
      function Test-IsNonInteractive()
      {
          #ref: http://www.powershellmagazine.com/2013/05/13/pstip-detecting-if-the-console-is-in-interactive-mode/
          #powershell -NoProfile -NoLogo -NonInteractive -File .\IsNonInteractive.ps1
          return [bool]([Environment]::GetCommandLineArgs() -Contains '-NonInteractive')
      }
      
      function Get-CurrentUserCredentials()
      {
          return [System.Net.CredentialCache]::DefaultCredentials
      }
      function Get-CurrentUserName()
      {
          return ("{0}\{1}" -f $env:USERDOMAIN,$env:USERNAME)
      }
      
      $cred = $null
      $user = Get-CurrentUserName
      
      if (Test-IsNonInteractive) 
      {
          $msg = 'non interactive'
          $cred = Get-CurrentUserCredentials
      } 
      else 
      {
          $msg = 'interactive'
          if ($promptForCredentialsInInteractive) 
          {
              $cred = (get-credential -UserName $user -Message "Please enter the credentials you wish this script to use when accessing network resources")
              $user = $cred.UserName
          } 
          else 
          {
              $cred = Get-CurrentUserCredentials
          }
      }
      
      $msg = ("Running as user '{0}' in '{1}' mode" -f $user,$msg)
      write-output $msg
      

      【讨论】:

      • 如果 [Environment]::UserInteractive工作,那将是不错的。我提交了一个错误,因为这让我难过: @987654321 @
      • 我现在在我的代码中使用您的解决方案,即使它不完全有效。例如,powershell.exe -NonInt -c "blah" 将作为 -NonInteractive 运行,但您的检查将失败。
      • @VertigoRay 它看起来确实像[Environment]::UserInteractive 工作正常,也许不是你期望的那样。我在计划的任务中使用了此功能,并且正确显示了“ false”。如果我手动运行它并打开窗口,它将报告“ true”。谢谢你的评论。
      • @B_Dubb86 您是否将其作为系统帐户运行?如果您查看我之前链接的对connect.microsoft.com/PowerShell/feedbackdetail/view/1588843 的唯一评论,您可能会明白为什么您的评论有效。为了节省您的点击次数,引用以下内容:“Environment.UserInteractive 报告用户是否是交互式的,而不是具体说明进程是否是交互式的。该 api 对于检测您是否在服务中运行很有用。”
      猜你喜欢
      • 2023-04-05
      • 1970-01-01
      • 2019-08-31
      • 1970-01-01
      • 2017-06-05
      • 2021-12-07
      • 2021-12-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多