一个简单的 PowerShell REPL 脚本

复制代码
class REPLCommand {
  [string] $Name
  [scriptblock] $Action
  [string] $Description

  REPLCommand() {
    $this.Name = $this.GetType().Name
    $this.Action = {}
    $this.Description = ""
  }

  REPLCommand([string] $name, [scriptblock] $action, [string] $description) {
    $this.Name = $name
    $this.Action = $action
    $this.Description = $description
  }
  
  [object] Execute([string] $input) {
    return Invoke-Command -ScriptBlock $this.Action -ArgumentList $input
  }

  [SimpleREPL] Repl() {
    return [SimpleREPL]::GetInstance()
  }

  [string] GetValue([string] $originValue, [string] $defaultVariable) {
    return $this.GetValue($originValue, $defaultVariable, "")
  }

  [string] GetValue([string] $originValue, [string] $defaultVariable, [string] $defaultValue) {
    if ($originValue) {
      return $originValue
    }
      
    $value = $this.Repl().GetVariable($defaultVariable)
    if ($value) {
      return $value
    }

    return $defaultValue
  }
}

class Echo: REPLCommand {
  Echo() {
    $this.Description = "回显输入内容。"
    $this.Action = {
      param([string] $input)
      Write-Host $input
    }
  }
}

class Exit: REPLCommand {
  Exit() {
    $this.Description = "退出 REPL。"
    $this.Action = {
      param([string] $input)
      [SimpleREPL]::GetInstance().Running = false
    }    
  }
}

class Quit: REPLCommand {
  Quit() {
    $this.Description = "退出 REPL。"
    $this.Action = {
      param([string] $input)
      [SimpleREPL]::GetInstance().Running = false
    }    
  }
}

class Clear: REPLCommand {
  Clear() {
    $this.Description = "清空屏幕。"
    $this.Action = {
      Clear-Host
    }
  }
}

class Help: REPLCommand {
  Help() {
    $this.Description = "展示帮助信息。"
    $this.Action = {
      param([string] $input)

      if ($input -and $this.Repl().Commands.ContainsKey($input)) {
        $command = $this.Repl().Commands[$input]
        Write-Host ("{0,-8} {1}" -f $command.Name.ToLower(), $command.Description)
        return
      }

      $this.Repl().Commands.GetEnumerator() | Sort-Object Name | ForEach-Object {
        Write-Host ("{0,-8} {1}" -f $_.Value.Name.ToLower(), $_.Value.Description)
      }
    }
  }
}

class Time: REPLCommand {
  Time() {
    $this.Description = "显示系统时间。"
    $this.Action = {
      param([string] $input)
      Write-Host "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
    }
  }
}

class PSVersion: REPLCommand {
  PSVersion() {
    $this.Description = "显示 PowerShell 版本。"
    $this.Action = {
      param([string] $input)
      Write-Host $PSVersionTable.PSVersion.ToString()
    }
  }
}

class Set: REPLCommand {
  Set() {
    $this.Description = "设置变量值。用法:set a 1"
    $this.Action = {
      param([string] $input)
      $parts = $input.Trim() -split '\s+', 2
      $var = $parts[0]
      $value = $parts[1]
      $this.Repl().SetVariable($var, $value)
    }
  }
}

class Get: REPLCommand {
  Get() {
    $this.Description = "查看变量值。用法:get [variable]"
    $this.Action = {
      param([string] $input)
      $parts = $input.Trim() -split '\s+'
      if (-not $name) {
        $this.Repl().Variables.GetEnumerator() | Sort-Object Name | ForEach-Object {
          Write-Host ("{0,-8} {1}" -f $_.Key, $_.Value)
        }
        return
      }

      $name = $parts[0]
      $value = $this.Repl().GetVariable($name)
      Write-Host $value
    }
  }
}

class Jsonrpc2: REPLCommand {
  Jsonrpc2() {
    $this.Description = "发送 JSON-RPC2 请求。用法:jsonrpc2 [url] [method] [params] [id]。如果没有传入参数,从变量表中获取对应的值。"
    $this.Action = {
      param([string] $input)
      $parts = $input.Trim() -split '\s+', 4
      $url = $this.GetValue($parts[0], "url")
      $method = $this.GetValue($parts[1], "method")
      $params = $this.GetValue($parts[2], "params")
      $id = $this.GetValue($parts[3], "id", "1")

      if ((-not $url) -or (-not $method)) {
        Write-Host "错误:缺少 url 和 method 参数。"
        return
      }
      $params = $params | ConvertFrom-Json -AsHashtable

      $response = ($this.InvokeJsonRpc2($url, $method, $params, $id) | ConvertTo-Json).Trim()
      Write-Host $response
    }
  }

  [object] InvokeJsonRpc2([string] $url, [string] $method, [object] $params, [string] $id) {
    # 构建 JSON-RPC 2.0 请求对象
    $req = @{
      jsonrpc = "2.0"
      method = $Method
      id = $Id
      params = @{}
    }
    
    # 如果提供了 params,则添加到请求中
    if ($null -ne $Params) {
      $req.params = $Params
    }
    
    # 转换为 JSON 字符串
    $body = $req | ConvertTo-Json

    Write-Verbose "Sending JSON-RPC request: $body"
    
    try {
      return Invoke-RestMethod -Uri $url -Method POST -ContentType "application/json" -Body $body
    }
    catch {
      Write-Error "JSON-RPC call failed: $($_.Exception.Message)"
      throw
    }
  }
}

class Save: REPLCommand {
  Save() {
    $this.Description = "将变量值保存到 .repl.var.json 文件。"
    $this.Action = {
      param([string] $input)
      $this.Repl().VariableFile.Write($this.Repl().Variables)
    }
  }
}

class Load: REPLCommand {
  Load() {
    $this.Description = "从 .repl.var.json 文件加载变量值。"
    $this.Action = {
      param([string] $input)
      $table = $this.Repl().VariableFile.Read()
      foreach ($key in $table.Keys) {
        $this.Repl().Variables[$key] = $table[$key]
      }
    }
  }
}

class Config: REPLCommand {
  Config() {
    $this.Description = "查看或修改配置,并保存到 .repl.cfg.json 文件。"
    $this.Action = {
      param([string] $input)

      if ($this.Repl().Configs.Count -eq 0) {
        $this.Repl().Configs = $this.Repl().ConfigFile.Read()
      }

      $parts = $input.Trim() -split '\s+', 2

      $name = $parts[0]
      $value = $parts[1]

      if (-not $name) {
        $this.Repl().Configs.GetEnumerator() | Sort-Object Name | ForEach-Object {
          Write-Host ("{0,-8} {1}" -f $_.Key, $_.Value)
        }
        return
      }

      if (-not $value) {
        $value = $this.Repl().Configs[$name]
        Write-Host $value
        return
      }

      $this.Repl().Configs[$name] = $value
      $this.Repl().ConfigFile.Write($this.Repl().Configs)
    }
  }
}

class Script: REPLCommand {
  Script() {
    $this.Description = "显示 repl 脚本。"
    $this.Action = {
      param([string] $input)

      $files = Get-ChildItem -Filter *.repl
      foreach ($file in $files) {
        Write-Host $file.BaseName
      }
    }
  }
}

class Run: REPLCommand {
  Run() {
    $this.Description = "执行 repl 脚本。"
    $this.Action = {
      param([string] $input)

      $scriptFileName = $input
      if (-not $input.EndsWith(".repl")) {
        $scriptFileName += ".repl"
      }

      $lines = Get-Content $scriptFileName | ForEach-Object {
        ($_ -replace '#.*$').Trim()
      } | Where-Object { $_ -ne "" }

      $list = [Collections.Generic.List[string]]$lines
      $this.Repl().ExecuteBatch($list)
    }
  }
}

class ReplJsonFile {
  [string]$FilePath

  ReplJsonFile([string]$customPath) {
    $this.FilePath = $customPath
  }

  [hashtable] Read() {
    if (-not (Test-Path $this.FilePath)) {
      return @{}
    }
    
    try {
      $jsonContent = Get-Content -Path $this.FilePath -Raw -ErrorAction Stop
      $hashtable = $jsonContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop
      return $hashtable
    }
    catch {
      throw "读取文件失败: $($_.Exception.Message)"
    }
  }

  [void] Write([hashtable]$data) {
    try {
      $jsonContent = $data | ConvertTo-Json -Depth 10 -Compress
      $jsonContent | Set-Content -Path $this.FilePath -Encoding UTF8 -Force
    }
    catch {
      throw "写入文件失败: $($_.Exception.Message)"
    }
  }

  [void] Clear() {
    if (Test-Path $this.FilePath) {
      Remove-Item $this.FilePath -Force
    }
  }

  [bool] Exists() {
    return (Test-Path $this.FilePath)
  }
}

class SimpleREPL {
  hidden static [SimpleREPL] $instance = $null
  
  # 获取单例实例
  static [SimpleREPL] GetInstance() {
    if (-not [SimpleREPL]::instance) {
      [SimpleREPL]::instance = [SimpleREPL]::new()
    }
    return [SimpleREPL]::instance
  }

  [hashtable] $Commands = @{}
  [bool] $Running = $false
  [hashtable] $Variables = @{}
  [hashtable] $Configs = @{}
  [ReplJsonFile] $VariableFile
  [ReplJsonFile] $ConfigFile

  hidden SimpleREPL() {
    $this.ScanCommand()

    $path = Join-Path (Get-Location) ".repl.var.json"
    $this.VariableFile = [ReplJsonFile]::new($path)
    $path = Join-Path (Get-Location) ".repl.cfg.json"
    $this.ConfigFile = [ReplJsonFile]::new($path)
    $this.Configs = $this.ConfigFile.Read()
  }

  # 防止克隆
  hidden [SimpleREPL] Clone() {
    return $this
  }

  [string] getConfig([string] $name) {
    return $this.Configs[$name]
  }

  [boolean] isConfigMatch([string] $name, [string] $target) {
    return $this.getConfig($name).Equals($target)
  }

  [void] SetVariable([string] $var, [string] $value) {
    $this.Variables[$var] = $value
  }

  [string] GetVariable([string] $var) {
    return $this.Variables[$var]
  }

  [void] ScanCommand() {
    # 获取当前 AppDomain 中的所有类型。
    $allTypes = [AppDomain]::CurrentDomain.GetAssemblies() | 
      ForEach-Object { $_.GetTypes() } |
      Where-Object { $_.IsClass -and !$_.IsAbstract } |
      Group-Object FullName | ForEach-Object { $_.Group[0] }

    # 扫描 REPLCommand 派生类并注册。
    foreach ($type in $allTypes) {
      if ($type.BaseType -and $type.BaseType.Name -eq "REPLCommand") {
        try {
          $command = New-Object -TypeName $type.FullName
          $this.RegisterCommand($command)
        } catch {
          # ignore
        }
      }
    }
  }

  [void] RegisterCommand([REPLCommand] $command) {
    $LowerName = $command.GetType().Name.ToLower()
    $this.Commands[$LowerName] = $command
  }
  
  [void]RegisterCommand([string]$name, [scriptblock]$action, [string]$description) {
    $command = [REPLCommand]::new($name, $action, $description)
    $this.Commands[$name] = $command
  }
  
  [hashtable] ParseInput([string] $input) {
    $parts = $input.Trim() -split '\s+', 2
    $commandName = $parts[0].ToLower()
    $arguments = $parts[1]
    
    return @{
      Command = $commandName
      Arguments = $arguments
    }
  }
  
  [void] Run() {
    $this.Running = $true
    
    Write-Host "PowerShell REPL" -ForegroundColor Cyan
    Write-Host "输入 'help' 查看可用命令" -ForegroundColor Yellow
    Write-Host "输入 'quit' 或 'exit' 退出" -ForegroundColor Yellow
    
    while ($this.Running) {
      try {
        $input = Read-Host "repl"
        
        if ([string]::IsNullOrWhiteSpace($input)) {
          continue
        }
     
        $this.Execute($input)
      } catch {
        Write-Host "执行错误: $($_.Exception.Message)" -ForegroundColor Red
      }
    }
  }

  [void] Execute([string] $input) {
    $startTime = Get-Date

    $measureTime = $false
    if ($this.isConfigMatch("measure.time", "on")) {
      $measureTime = $true
    }

    try {
      $parsed = $this.ParseInput($input)
      $commandName = $parsed.Command.ToLower()
      $arguments = $parsed.Arguments
      
      if ($this.Commands.ContainsKey($commandName)) {
        $this.Commands[$commandName].Execute($arguments)
        return
      } 

      # 如果不是注册的命令,尝试作为 PowerShell 命令执行
      try {
        $result = (Invoke-Expression $input | Out-String).Trim()
        if ($result) {
          Write-Host $result
        }
      } catch {
        Write-Host "错误: 未知命令 '$commandName'" -ForegroundColor Red
        Write-Host "输入 'help' 查看可用命令" -ForegroundColor Yellow
      }
    } finally {
      if ($measureTime) {
        Write-Host "----------------"
        $endTime = Get-Date
        $duration = New-TimeSpan -Start $startTime -End $endTime
        $milliseconds = $duration.TotalMilliseconds
        $min = [int]($milliseconds / 60000)
        $sec = [int]($milliseconds / 1000)
        $millis = [int]($milliseconds % 1000)
          
        $timeMessage = ""
        if ($min -gt 0) {
          $timeMessage += "$min 分"
        }
        if ($sec -gt 0) {
          $timeMessage += " $sec 秒"
        }

        if ($timeMessage.length -ne 0) {
          $timeMessage += " "
        }
        
        $timeMessage += "$millis 毫秒"
        Write-Host "耗时 $timeMessage"
      }
    }
  }

  [void] ExecuteBatch([System.Collections.Generic.List[string]] $batch) {
    foreach ($line in $batch) {
      if ([string]::IsNullOrWhiteSpace($line)) {
        continue
      }

      $this.Execute($line)
    }
  }
}

# 使用示例和演示
function Start-REPL {
  [SimpleREPL]::GetInstance().Run()
}

Start-REPL

# repl 脚本示例

set url http://localhost:8080/rpc
set method system/info
jsonrpc2

优点

  • 提供丰富的内置命令(echo、help、time、set/get等)
  • 支持变量持久化(save/load命令)
  • 支持配置文件管理(config命令)
  • 支持脚本批量执行(run命令)
  • 代码架构设计良好,便于添加新命令
  • 实现了 JSON-RPC 2.0 基础功能

缺点

  • 错误处理不够完善
  • 安全性考虑不足
  • 缺乏请求/响应日志记录功能
  • 配置系统相对简单,缺乏类型验证
相关推荐
guojikun16 天前
一键配置 Web 前端开发环境(PowerShell 自动化脚本)
windows·web前端·powershell
WarPigs17 天前
Powershell笔记
脚本·powershell
struggle202523 天前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
竹等寒1 个月前
Powershell 管理 后台/计划 作业(六)
服务器·windows·网络安全·powershell
charlee442 个月前
CMake构建学习笔记21-通用的CMake构建脚本
cmake·powershell·构建
点云SLAM4 个月前
Windows CMD(命令提示符)中最常用的命令汇总和实战示例
windows·microsoft·visual studio·powershell·cmd命令提示符·bat文件·win环境批处理
等不到来世4 个月前
npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1
npm·powershell
Ronin-Lotus4 个月前
上位机知识篇---Prompt&PowerShell Prompt
prompt·powershell
Мартин.4 个月前
Operation Blackout 2025: Smoke & Mirrors
powershell