使用参数和凭据从PowerShell启动.ps1脚本,并使用变量获取输出



Hello Stack社区:)

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

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

还有一个类似的问题Link。但答案是使用文件作为2个PS脚本之间的通信方式。我只是想避免访问冲突@更新:主脚本将启动其他一些脚本。因此,如果同时由多个用户执行,那么使用文件的解决方案可能会很棘手。

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参数传递给它。

提前感谢!

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. 在所述进程中创建运行空间
  3. 在所述进程外运行空间中执行代码

我在下面列出了这样一个函数,请参阅内联注释以了解详细信息:

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:pathtoscript1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}

注:

  • 以下解决方案适用于任何外部程序,并且始终将输出捕获为文本

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

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

注:

  • 由于以不同用户身份运行,这仅在Windows上受支持(自.NET Core 3.1起),但在这两个PowerShell版本中都受支持。

  • 由于需要以不同的用户身份运行并且需要捕获输出,.WindowStyle不能用于运行命令hidden(因为使用.WindowStyle需要.UseShellExecute$true,这与这些要求不兼容);然而,由于所有输出都被捕获,因此将.CreateNoNewWindow设置为$true会有效地导致隐藏执行。

  • 下面只捕获stdout输出。如果您也想捕获stderr输出,则需要通过事件捕获它,因为在$ps.StandardOutput.ReadToEnd()旁边使用$ps.StandardError.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格式表示输出的能力,该格式允许将输出反序列化为富对象,尽管类型保真度有限

# 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

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

您可以执行以下操作,将参数传递给ps1脚本。

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

& C:scriptsdest.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

最新更新