使用PowerShell启动带参数和凭据的.ps1脚本,并通过变量获取输出。

11

你好 Stack Community :)

我有一个简单的目标。我想从另一个 PowerShell 脚本启动一些 PowerShell 脚本,但有三个条件:

  1. 我必须传递凭据(执行连接到具有特定用户的数据库)
  2. 它必须使用某些参数
  3. 我想将输出传递给一个变量

类似的问题在此链接中有提到。但是回答是使用文件作为两个 PowerShell 脚本之间通信的方法。我只想避免访问冲突。@更新:主脚本将启动其他几个脚本。因此,如果执行将在同时由多个用户执行,则使用文件的解决方案可能会很棘手。

Script1.ps1 是应具有字符串输出的脚本。(仅为了清楚起见,这是虚构脚本,真实脚本有150行,所以我只是想举一个例子)

param(  
[String]$DeviceName
)
#Some code that needs special credentials
$a = "Device is: " + $DeviceName
$a

ExecuteScripts.ps1 应该以上述3个条件调用那个脚本。

我尝试了多种解决方案,例如这一个:

$arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName
$output = Start-Process powershell -ArgumentList $arguments -Credential $credentials
$output 

我从中没有得到任何输出,也不能直接调用这个脚本

&C:\..\script1.ps1 -ClientName PCPC

因为我无法将-Credential参数传递给它。

先行感谢!


如果这只是关于访问冲突的问题:为每次调用创建唯一的文件名会解决你的问题,对吧? - mklement0
1
如果这是唯一的方法,我会坚持使用这个解决方案。只需生成随机文件名,检查是否存在这样的文件...我将从我的Java代码执行6到10个脚本,并且每次我使用或其他人使用我的应用程序时都需要6到10个文件。因此,这也涉及到性能问题。 - Dmytro
4个回答

5

Start-Process 将是我在 PowerShell 中调用 PowerShell 的“最后手段选择”,特别是因为所有 I/O 都会变成字符串而不是(反序列化的)对象。

有两个备选方案:

1. 如果用户是本地管理员且已配置 PSRemoting

如果可以使用本地机器上的远程会话(不幸的是仅限于本地管理员),我肯定会选择 Invoke-Command

$strings = Invoke-Command -FilePath C:\...\script1.ps1 -ComputerName localhost -Credential $credential

$strings将包含结果。


2. 如果用户不是目标系统上的管理员

您可以通过以下方式编写自己的“仅限本地Invoke-Command”:

  1. 使用不同的登录名创建PowerShellProcessInstance
  2. 在该进程中创建一个runspace
  3. 在该进程外的runspace中执行代码

我在下面组合了这样一个函数,有关步骤说明,请参见内联注释:

function Invoke-RunAs
{
    [CmdletBinding()]
    param(
        [Alias('PSPath')]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        ${FilePath},

        [Parameter(Mandatory = $true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential},

        [Alias('Args')]
        [Parameter(ValueFromRemainingArguments = $true)]
        [System.Object[]]
        ${ArgumentList},

        [Parameter(Position = 1)]
        [System.Collections.IDictionary]
        $NamedArguments
    )

    begin
    {
        # First we set up a separate managed powershell process
        Write-Verbose "Creating PowerShellProcessInstance and runspace"
        $ProcessInstance = [System.Management.Automation.Runspaces.PowerShellProcessInstance]::new($PSVersionTable.PSVersion, $Credential, $null, $false)

        # And then we create a new runspace in said process
        $Runspace = [runspacefactory]::CreateOutOfProcessRunspace($null, $ProcessInstance)
        $Runspace.Open()
        Write-Verbose "Runspace state is $($Runspace.RunspaceStateInfo)"
    }

    process
    {
        foreach($path in $FilePath){
            Write-Verbose "In process block, Path:'$path'"
            try{
                # Add script file to the code we'll be running
                $powershell = [powershell]::Create([initialsessionstate]::CreateDefault2()).AddCommand((Resolve-Path $path).ProviderPath, $true)

                # Add named param args, if any
                if($PSBoundParameters.ContainsKey('NamedArguments')){
                    Write-Verbose "Adding named arguments to script"
                    $powershell = $powershell.AddParameters($NamedArguments)
                }

                # Add argument list values if present
                if($PSBoundParameters.ContainsKey('ArgumentList')){
                    Write-Verbose "Adding unnamed arguments to script"
                    foreach($arg in $ArgumentList){
                        $powershell = $powershell.AddArgument($arg)
                    }
                }

                # Attach to out-of-process runspace
                $powershell.Runspace = $Runspace

                # Invoke, let output bubble up to caller
                $powershell.Invoke()

                if($powershell.HadErrors){
                    foreach($e in $powershell.Streams.Error){
                        Write-Error $e
                    }
                }
            }
            finally{
                # clean up
                if($powershell -is [IDisposable]){
                    $powershell.Dispose()
                }
            }
        }
    }

    end
    {
        foreach($target in $ProcessInstance,$Runspace){
            # clean up
            if($target -is [IDisposable]){
                $target.Dispose()
            }
        }
    }
}

然后像这样使用:
$output = Invoke-RunAs -FilePath C:\path\to\script1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}

2

rcv.ps1

param(
    $username,
    $password
)

"The user is:  $username"
"My super secret password is:  $password"

从另一个脚本执行:

.\rcv.ps1 'user' 'supersecretpassword'

输出:

The user is:  user
My super secret password is:  supersecretpassword

澄清一下:意图不仅仅是传递凭据,而是以凭据所标识的用户身份来运行。 - mklement0
1
@mklement0,感谢您的澄清,因为通过不同的问题迭代,这对我来说并不清楚。 - thepip3r

2
注意:
  • 以下解决方案适用于任何外部程序,并且始终以文本形式捕获输出

  • 调用另一个 PowerShell 实例将其输出捕获为富对象(有限制),请参见底部部分的变体解决方案或考虑 Mathias R. Jessen 的有用答案,该答案使用 PowerShell SDK

这是一个概念验证,基于直接使用 .NET 类型 System.Diagnostics.ProcessSystem.Diagnostics.ProcessStartInfo 来在内存中捕获进程输出(如你所述,在你的问题中,Start-Process 不是一个选项,因为它只支持将输出捕获到文件中,如 此答案 所示):

注意:

由于以不同的用户身份运行,这仅在 Windows 上受支持(截至 .NET Core 3.1),但在两个 PowerShell 版本中都有。由于需要以不同的用户身份运行并需要捕获输出,无法使用 `.WindowStyle` 来隐藏运行命令(因为使用 `.WindowStyle` 需要将 `.UseShellExecute` 设置为 `$true`,这与这些要求不兼容)。然而,由于所有输出都被捕获,将 `.CreateNoNewWindow` 设置为 `$true` 实际上会导致隐藏执行。下面仅捕获了标准输出 (`stdout`) 的输出。如果您想捕获 `stderr` 输出,您需要通过事件来捕获它,因为同时使用 `$ps.StandardError.ReadToEnd()` 和 `$ps.StandardOutput.ReadToEnd()` 可能会导致死锁。
# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # For demo purposes, use a simple `cmd.exe` command that echoes the username. 
  # See the bottom section for a call to `powershell.exe`.
  FileName = 'cmd.exe'
  Arguments = '/c echo %USERNAME%'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # (user@doamin.com), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output.
# By reading to the *end*, this implicitly waits for (near) termination
# of the process.
# Do NOT use $ps.WaitForExit() first, as that can result in a deadlock.
$stdout = $ps.StandardOutput.ReadToEnd()

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

"Running ``cmd /c echo %USERNAME%`` as user $($cred.UserName) yielded:"
$stdout

上述内容类似于以下内容,显示该过程已成功使用给定的用户身份运行:
Running `cmd /c echo %USERNAME%` as user jdoe yielded:
jdoe

由于您正在调用另一个 PowerShell 实例,您可能希望利用 PowerShell CLI 以 CLIXML 格式表示输出的能力,这允许将输出反序列化为 丰富的对象,尽管类型保真度有限,如 this related answer 中所解释的。
# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # Invoke the PowerShell CLI with a simple sample command
  # that calls `Get-Date` to output the current date as a [datetime] instance.
  FileName = 'powershell.exe'
  # `-of xml` asks that the output be returned as CLIXML,
  # a serialization format that allows deserialization into
  # rich objects.
  Arguments = '-of xml -noprofile -c Get-Date'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # (user@doamin.com), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output, in CLIXML format,
# stripping the `#` comment line at the top (`#< CLIXML`)
# which the deserializer doesn't know how to handle.
$stdoutCliXml = $ps.StandardOutput.ReadToEnd() -replace '^#.*\r?\n'

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

# Use PowerShell's deserialization API to 
# "rehydrate" the objects.
$stdoutObjects = [Management.Automation.PSSerializer]::Deserialize($stdoutCliXml)

"Running ``Get-Date`` as user $($cred.UserName) yielded:"
$stdoutObjects
"`nas data type:"
$stdoutObjects.GetType().FullName

以上输出类似于以下内容,显示由Get-Date输出的[datetime]实例(System.DateTime)被反序列化为以下内容:
Running `Get-Date` as user jdoe yielded:

Friday, March 27, 2020 6:26:49 PM

as data type:
System.DateTime

0

以下是将参数传递给ps1脚本的方法。

第一个脚本可以是origin.ps1,我们在其中编写:

& C:\scripts\dest.ps1 Pa$$w0rd parameter_a parameter_n

目标脚本 dest.ps1 可以包含以下代码来捕获变量

$var0 = $args[0]
$var1 = $args[1]
$var2 = $args[2]
Write-Host "my args",$var0,",",$var1,",",$var2

结果将会是:

my args Pa$$w0rd, parameter_a, parameter_n

1
主要目标是将所有条件合并为一个执行。我必须传递参数和凭据! - Dmytro
我认为PowerShell不允许您像这样传递参数$arguments = "C:..\script1.ps1" + " -ClientName" + $DeviceName。您应该考虑删除“-”。 - Andy McRae
1
话虽如此,Start-Process确实可以使用参数和凭据执行脚本,但它不会将输出保存到变量中。如果我尝试访问$output变量,它是NULL。来自@mklement0的另一个想法是将输出保存到文件中。但在我的情况下,这将导致一个地方有大量来自不同用户和不同脚本创建的文件。 - Dmytro
@AndyMcRae,"C:\..\script1.ps1" + " -ClientName " + $DeviceName 构建了一个用来传递给 -ArgumentList 参数的 字符串 表示一个 PowerShell 命令,在新的进程中通过 Start-Process 传递给 powershell.exe,这个过程工作得非常好(假设脚本路径中没有嵌入空格)。Dmytro 的挑战是以_不同的用户身份运行_ 脚本,因此需要使用 Start-Process -Credential,同时收集该脚本的输出信息_在内存中_而不是在_文件_中(这可以通过 -RedirectStandardOutput-RedirectStandardError 来实现)。 - mklement0
好的,现在我明白那个问题的要点了。虽然我猜已经有人回答过了。 - Andy McRae
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接