TopUser 发表于 2023-7-15 12:25

PowerShell脚本中实现限时读取用户输入

本帖最后由 TopUser 于 2023-7-15 12:27 编辑

又是很久没有发帖了,突然想到之前倒腾PowerShell的时候实现了一个限时读取用户输入的函数,那就再~~水~~一个帖子吧。

# 问题描述

在PowerShell脚本编写的时候,我们会使用`Read-Host`函数来接收用户的输入,但是很多时候我们想要在这个函数上附加一个超时效果,即等待用户在一段时间输入内容,如果超过时间脚本便不再等待。

# 解决方案概述

由于PowerShell可以调用C#代码,所以如果有C#的大佬的话,可能会有一些其他的实现思路,同时也欢迎分享。由于我对于C#了解不多,所以还是从PowerShell的角度来实现。我的实现方式我自认为具有以下特点:

- 支持Window PowerShell和.Net PowerShell

PowerShell有`Window PowerShell`版本,也就是Window自带的PowerShell。还有一种可以跨平台的`.Net Core`版本,官方仓库在[这里](https://github.com/PowerShell/PowerShell)。`Window PowerShell`基本上都没有怎么维护了,所以现在的Window 10/11自带的都是`Window PowerShell`的5.1版本。开发时使用的是`Widnow PowerShell`版本,然后写好之后对于`.Net Core`版本做了一个适配。

- 性能不是很好

本函数为了做到超时终端读取效果,会开启一个进程来读取用户输入,超时再杀死进程,所以性能不高。之前我也试过一些底层的控制IO输入输入的C++接口,使用下来有一些Bug,所以最终才使用这种方式实现。

- 效果完美(自认为)

用户如果在限定时间输入,结果就会返回用户输入,如果超时会有提示,同时返回`NULL`。这个逻辑在调试的时候依旧不存在任何异常情况(这里直接指出是因为之前用了一个C#的方案,在调试的时候会出现一个Bug)。当然,你也可以在理解原理之后自行根据需求修改

# 源码及分析

PowerShell生态本身就不是很好,如果你只是想要在PowerShell中实现这个效果而已,那你完全可以拷贝这个函数直接用即可,不用花时间分析原理。

```powershell
Function Read-HostWithTimeout {
    param(
      $Prompt = "请输入内容: ",
      $PromptBackGroundColor = $Host.UI.RawUI.BackgroundColor,
      $PromptForeGroundColor = $Host.UI.RawUI.ForegroundColor,
      $Timeout = 5000,
      $TimeoutHint = "输入超时",
      $HintBackgroundColor = $Host.UI.RawUI.BackgroundColor,
      $HintForeGroundColor = ::Yellow
    )
    # 在这里调用输出提示的内容而不是在新进程里面提示是因为新进程提示的内容会被计入返回值,这里就不会
    Write-Host $Prompt -ForegroundColor $PromptForeGroundColor -BackgroundColor $PromptBackGroundColor -NoNewline

    $res = try {
            # Windows PowerShell会在超时结束抛出异常,所以这里需要定义异常可捕捉
      $ErrorActionPreference = 'Stop'
      # 新开线程执行超时读取的逻辑,注意,这里跟了一个timeout的参数,这个参数内部可以读取到
      powershell.exe -Command {
            param($Timeout)
            # 在这个进程里面新开一个Powershell线程,线程之间共享控制台对象
            $InitialSessionState = ::CreateDefault()
            $InitialSessionState.Variables.Add(
                ::new(
                  "ThreadContext",
                  @{Host = $Host },
                  "share host between threads"
                )
            )
            $PSThread = ::Create($InitialSessionState)
            $null = $PSThread.AddScript{
                $ThreadContext.Host.UI.ReadLine() # 执行读取控制台操作
            }
            # 开始异步执行创建好的对象,并等待timeout秒无结果之后杀死本进程
            $Job = $PSThread.BeginInvoke()
            if (-not $Job.AsyncWaitHandle.WaitOne($Timeout)) {
                Get-Process -Id $PID | Stop-Process
            }
            else {
                return $PSThread.EndInvoke($Job)
            }
      } -args $Timeout
    }
    catch {
            # Windows PowerShell超时会执行这里的代码
      Write-Host $TimeoutHint -ForegroundColor $HintForeGroundColor -BackgroundColor $HintBackGroundColor
    }
    # .Net Core PowerShell超时会执行这里的代码
    if($null -eq $res -and $PSVersionTable.PSEdition -eq "Core"){
      Write-Host $TimeoutHint -ForegroundColor $HintForeGroundColor -BackgroundColor $HintBackGroundColor
    }
    return $res
}
```

调用示例

```powershell
$res = Read-HostWithTimeout -Timeout 3000
# 超时$res的值就为null
# 有输入$res的值就为用户输入
Write-Host $res
```

eaglexiong 发表于 2023-7-16 19:38

看到代码很精简,支持

Holmewei 发表于 2023-7-17 11:00

感谢大佬分享
页: [1]
查看完整版本: PowerShell脚本中实现限时读取用户输入