Call a PowerShell script in a new, clean PowerShell instance (from within another script) - powershell

I have many scripts. After making changes, I like to run them all to see if I broke anything. I wrote a script to loop through each, running it on fresh data.
Inside my loop I'm currently running powershell.exe -command <path to script>. I don't know if that's the best way to do this, or if the two instances are totally separate from each other.
What's the preferred way to run a script in a clean instance of PowerShell? Or should I be saying "session"?

Using powershell.exe seems to be a good approach but with its pros and cons, of course.
Pros:
Each script is invoked in a separate clean session.
Even crashes do not stop the whole testing process.
Cons:
Invoking powershell.exe is somewhat slow.
Testing depends on exit codes but 0 does not always mean success.
None of the cons is mentioned is a question as a potential problem.
The demo script is below. It has been tested with PS v2 and v3. Script names
may include special characters like spaces, apostrophes, brackets, backticks,
dollars. One mentioned in comments requirement is ability to get script paths
in their code. With the proposed approach scripts can get their own path as
$MyInvocation.MyCommand.Path
# make a script list, use the full paths or explicit relative paths
$scripts = #(
'.\test1.ps1' # good name
'.\test 2.ps1' # with a space
".\test '3'.ps1" # with apostrophes
".\test [4].ps1" # with brackets
'.\test `5`.ps1' # with backticks
'.\test $6.ps1' # with a dollar
'.\test ''3'' [4] `5` $6.ps1' # all specials
)
# process each script in the list
foreach($script in $scripts) {
# make a command; mind &, ' around the path, and escaping '
$command = "& '" + $script.Replace("'", "''") + "'"
# invoke the command, i.e. the script in a separate process
powershell.exe -command $command
# check for the exit code (assuming 0 is for success)
if ($LastExitCode) {
# in this demo just write a warning
Write-Warning "Script $script failed."
}
else {
Write-Host "Script $script succeeded."
}
}

If you're on PowerShell 2.0 or higher, you can use jobs to do this. Each job runs in a separate PowerShell process e.g.:
$scripts = ".\script1.ps1", ".\script2.ps1"
$jobs = #()
foreach ($script in $scripts)
{
$jobs += Start-Job -FilePath $script
}
Wait-Job $jobs
foreach ($job in $jobs)
{
"*" * 60
"Status of '$($job.Command)' is $($job.State)"
"Script output:"
Receive-Job $job
}
Also, check out the PowerShell Community Extensions. It has a Test-Script command that can detect syntax errors in a script file. Of course, it won't catch runtime errors.

One tip for PowerShell V3 users: we (the PowerShell team) added a new API on the Runspace class called ResetRunspace(). This API resets the global variable table back to the initial state for that runspace (as well as cleaning up a few other things). What it doesn't do is clean out function definitions, types and format files or unload modules. This allows the API to be much faster. Also note that the Runspace has to have been created using an InitialSessionState object, not a RunspaceConfiguration instance. ResetRunspace() was added as part of the Workflow feature in V3 to support parallel execution efficiently in a script.

The two instances are totally separate, because they are two different processes. Generally, it is not the most efficient way to start a Powershell process for every script run. Depending on the number of scripts and how often you re-run them, it may be affecting your overall performance. If it's not, I would leave everything AS IS.
Another option would be to run in the same runspace (this is a correct word for it), but clean everything up every time. See this answer for a way to do it. Or use below extract:
$sysvars = get-variable | select -Expand name
function remove-uservars {
get-variable |
where {$sysvars -notcontains $_.name} |
remove-variable
}

Related

In powershell spawn notepad++ when file to open has spaces in it

$npp = "C:\Program Files\Notepad++\notepad++.exe";
$myfiles = #(
"C:\bad boys\file1.txt",
"C:\bad boys\file2.txt",
"C:\bad boys\file3.txt"
)
foreach ($file in $myfiles) {
Start-Process -FilePath $npp -ArgumentList "$file" -PassThru -NoNewWindow | out-null
}
This almost works... except, It doesn't open in notepad++ because it sees the space in the file name and thinks this is where the file path ends... thus, i am unable to open my file list. Any Ideas how to fix? What i get instead is notepad++ asking many times if I want to create the file "C:\bad"
tl;dr
While Joel Coehoorn's helpful answer provides an effective solution to your Start-Process problem (which stems from the bug detailed below), you can simplify your code to:
foreach ($file in $myfiles) {
# Note: | Out-Null is a trick that makes calling *GUI* applications
# *synchronous* (makes PowerShell wait for them to exit).
& $npp $file | Out-Null
}
You're seeing a long-standing bug in Start-Process that causes it to blindly space-concatenate its -ArgumentList (-Args) arguments without using required embedded double-quoting for arguments with spaces when forming the single string encoding all arguments that is passed to the target executable behind the scenes.
See GitHub issue #5576, which also discusses that a fix will require a new parameter so as not to break backward compatibility.
For that reason, the required embedded double-quoting must be performed manually as shown in Joel's answer.
When passing multiple arguments, it is ultimately easier to pass a single string to -ArgumentList, with embedded double-quoting as necessary - essentially by formulating a string similar to how you would pass multiple arguments from cmd.exe:
E.g., if you were to pass two file paths with spaces to Notepad++ at once, you would do:
Start-Process -Wait -FilePath $npp -ArgumentList "`"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt`""
Alternatively, since your argument string doesn't require string interpolation, you could use a verbatim (single-quoted) string instead, which avoids the need for escaping the embedded " as `":
Start-Process -Wait -FilePath $npp -ArgumentList '"C:\bad boys\file1.txt`" `"C:\bad boys\file2.txt"'
Using a here-string is yet another option that avoids the need to escape, and can additionally make the call more readable (also works with single quotes (#'<newline>...<newline>'#):
Start-Process -Wait -FilePath $npp -ArgumentList #"
"C:\bad boys\file1.txt" "C:\bad boys\file2.txt"
"#
Also note the overall simplification of the Start-Process call:
Use of -Wait to ensure synchronous execution (waiting for Notepad++ to exit before continuing).
It looks like this is what you tried to do by combining -PassThru with piping to Out-Null, but that doesn't actually work, because that only waits for Start-Process itself to exit (which itself - unlike the launched process - executes synchronously anyway).
The omission of the unnecessary -NoNewWindow parameter, which only applies to starting console applications (in order to prevent opening a new console window); Notepad++ is a GUI application.
Note that the only good reason to use Start-Process here - rather than direct invocation - is the need for synchronous execution: Start-Process -Wait makes launching GUI applications synchronous (too), whereas with direct invocation only console applications execute synchronously.
If you didn't need to wait for Notepad++ to exit, direct invocation would make your quoting headaches would go away, as the required embedded quoting is then automatically performed behind the scenes:[1]
foreach ($file in $myfiles) {
& $npp $file # OK, even with values with spaces
}
However, the | Out-Null trick can be used effectively in direct invocation to make calling GUI applications synchronous[2], which leads us to the solution at the top:
foreach ($file in $myfiles) {
& $npp $file | Out-Null # Wait for Notepad++ to exit.
}
[1] However, up to at least PowerShell 7.2.x, other quoting headaches can still arise, namely with empty-string arguments and arguments whose values contain " chars. - see this answer.
[2] Out-Null automatically makes PowerShell wait for the process in the previous pipeline segment to exit, so as to ensure that all input can be processed - and it does so irrespective of whether the process is a console-subsystem or GUI-subsystem application. Since GUI applications are normally detached from the calling console and therefore produce no output there, Out-Null has no ill effects. In the rare event that a GUI application does explicitly attach to the calling console and produce output there, you can use | Write-Output instead (which also works if there's no output, but is perhaps more confusing).
Try quotes around the file paths within the string data:
$myfiles = #(
"`"C:\bad boys\file.txt`"",
"`"C:\bad boys\file2.txt`"",
"`"C:\bad boys\file3.txt`""
)

Why working script fails when running in the background?

I have such script (here simplified):
while ($true)
{
Write-Host "Looping..."
Write-Host "Looping..." 6>> trash.txt
Start-Sleep -s 1
}
when I run it directly it works, but when I run it in the background:
Start-Job { .\sleeper.ps1 }
for a second it is seen as Running but shortly after as Failed and indeed file "trash.txt" is not created at all, so even one iteration is not executed.
What is wrong here?
I think the main issue is around the $PWD and -FilePath param, but I will list some info on Write-Host too:
Start-Job should be run with the -FilePath parameter. This is because {} is a ScriptBlock object, which is by default taken by the -ScriptBlock parameter, which you do not want. Example solution to that line: Start-Job -FilePath ./script.ps1
The $PWD or present working directory is, by default, the home directory / user profile of the current user when executed in a PowerShell job (Linux: $HOME // Windows: $Home/Documents). You can test this by executing a job simply with that variable (it may be $ENV:PWD on Windows?). Either way, it is likely not the same as the directory you are executing this in. trash.txt is being made and appended to, but it is being made in a different directory than your current directory. You will want your script to explicitly include an absolute path to the file being created, or give the script parameters that allow you to input the path at execution. Here is another StackOverflow article where a user had similar struggles, with two good solutions where one uses $args in the script and another uses the -InitializationScript parameter of Start-Job to set the $PWD: PowerShell Start-Job Working Directory
Often, Write-Host is low-priority as a selected output vs. using Write-Output / Write-Verbose / Write-Warning / Write-Error. More information about this can be found here: https://www.jsnover.com/blog/2013/12/07/write-host-considered-harmful/ - though, newer versions of PowerShell have added an information stream which I believe may make Write-Host output more accessible. More information on streams can be found with help about_Redirection

What is shortest possible way to download script from HTTP and run it with parameters using Powershell?

I have a PowerShell script file stored in an internal artifact server. The script URL is http://company-server/bootstrap.ps1.
What is a concise way to download that script and execute with a custom parameter?
I want to send such a command to users, who will copy-paste it over and over, so it must be a single-line and should be short.
What I currently have works, but it is long and unwieldy:
$c=((New-Object System.Net.WebClient).DownloadString('http://company-server/bootstrap.ps1'));Invoke-Command -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList 'RunJob'
I am wondering if there is shorter way to do this.
Note: From a code golf perspective, the solutions below could be shortened further, by eliminating insignificant whitespace; e.g., &{$args[0]}hi instead of & { $args[0] } hi. However, in the interest of readability such whitespace was kept.
A short formulation of a command that downloads a script via HTTP and executes it locally, optionally with arguments is probably this, taking advantage of:
alias irm for Invoke-RestMethod, in lieu of (New-Object System.Net.WebClient).DownloadString()
omitting quoting where it isn't necessary
relying on positional parameter binding
& ([scriptblock]::Create((irm http://company-server/bootstrap.ps1))) RunJob
RunJob is the OP's custom argument to pass to the script.
An even shorter, but perhaps more obscure approach is to use iex, the built-in alias for Invoke-Expression, courtesy of this GitHub comment.
iex "& { $(irm http://company-server/bootstrap.ps1) } RunJob"
As an aside: in general use, Invoke-Expression should be avoided.
The command uses an expandable string ("...", string interpolation) to create a string with the remote script's content enclosed in a script block { ... }, which is then invoked in a child scope (&). Note how the arguments to pass to the script must be inside "...".
However, there is a general caveat (which doesn't seem to be a problem for you): if the script terminates with exit, the calling PowerShell instance is exited too.
There are two workarounds:
Run the script in a child process:
powershell { iex "& { $(irm http://company-server/bootstrap.ps1) } RunJob" }
Caveats:
The above only works from within PowerShell; from outside of PowerShell, you must use powershell -c "..." instead of powershell { ... }, but note that properly escaping embedded double quotes, if needed (for a URL with PS metacharacters and/or custom arguments with, say, spaces), can get tricky.
If the script is designed to modify the caller's environment, the modifications will be lost due to running in a child process.
Save the script to a temporary file first:
Note: The command is spread across multiple lines for readability, but it also works as a one-liner:
& {
$f = Join-Path ([IO.Path]::GetTempPath()) ([IO.Path]::GetRandomFileName() + '.ps1');
irm http://company-server/bootstrap.ps1 > $f;
& $f RunJob;
ri $f
}
The obvious down-side is that the command is much longer.
Note that the command is written with robustness and cross-platform compatibility in mind, so that it also works in PowerShell Core, on all supported platforms.
Depending on what platforms you need to support / what assumptions you're willing to make (e.g., that the current dir. is writeable), the command can be shortened.
Potential future enhancements
GitHub issue #5909, written as of PowerShell Core 6.2.0-preview.4 and revised as of PowerShell Core 7.0, proposes enhancing the Invoke-Command (icm) cmdlet to greatly simplify download-script-and-execute scenarios, so that you could invoke the script in question as follows:
# WISHFUL THINKING as of PowerShell Core 7.0
# iwr is the built-in alias for Invoke-WebRequest
# icm is the built-in alias for Invoke-Command.
iwr http://company-server/bootstrap.ps1 | icm -Args RunJob
GitHub issue #8835 goes even further, suggesting an RFC be created to introduce a new PowerShell provider that allows URLs to be used in places where only files were previously accepted, enabling calls such as:
# WISHFUL THINKING as of PowerShell Core 7.0
& http://company-server/bootstrap.ps1 RunJob
However, while these options are very convenient, there are security implications to consider.
Here is a shorter solution (158 chars.)
$C=(New-Object System.Net.WebClient).DownloadString("http://company-server/bootstrap.ps1");icm -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 121
$C=(curl http://company-server/bootstrap.ps1).content;icm -ScriptBlock ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 108
$C=(curl http://company-server/bootstrap.ps1).content;icm ([Scriptblock]::Create($c)) -ArgumentList "RunJob"
Here is 98
$C=(iwr http://company-server/bootstrap.ps1).content;icm -sc([Scriptblock]::Create($c)) -ar RunJob
Thanks to Ansgar Wiechers

Powershell Invoke-Expressions pauses

I wrote a Powershell script that uses Steam's command line tool to login and check for updates for a community server I am running. See below:
$steamcmdFolder = 'C:\download\steam'
$steamcmdExec = $steamcmdFolder+"\steamcmd.exe"
$forceinstall = 'force_install_dir'+$steamcmdFolder
$appupdate = 'app_update 258550'
$cmdOutput = "$steamcmdExec +login anonymous"
do {
Write-Host Checking for an update....
Invoke-Expression $cmdOutput
Invoke-expression $forceinstall
Invoke-expression $appupdate
}
while ($Update = 1)
The Invoke-Expression lines are individual command-line statements I want executed in the order I have them. For some reason, the first Invoke-Expression works fine but the others do not -- everything just stops. I can type in the value of $forceinstall on the PowerShell command-line and it works as expected. But why can't I do this using PowerShell? Any suggestions are welcome!
If you convert the other two lines down to what they are, it seems like they are not real commands.
#Invoke-expression $forceinstall
Invoke-Expression "force_install_dirC:\download\steam"
#Invoke-expression $appupdate
Invoke-Expression "app_update 258550"
Looking into the SteamCMD documents, it appears that you might want to change it to be a single line command.
Invoke-Expression "steamcmd +login anonymous +force_install_dir C:\download\steam +app_update 258550 +quit"

Conditionally piping to Out-Null

I'm writing a PowerShell script to msbuild a bunch of solutions. I want to count how many solutions build successfully and how many fail. I also want to see the compiler errors, but only from the first one that fails (I'm assuming the others will usually have similar errors and I don't want to clutter my output).
My question is about how to run an external command (msbuild in this case), but conditionally pipe its output. If I'm running it and haven't gotten any failures yet, I don't want to pipe its output; I want it to output directly to the console, with no redirection, so it will color-code its output. (Like many programs, msbuild turns off color-coding if it sees that its stdout is redirected.) But if I have gotten failures before, I want to pipe to Out-Null.
Obviously I could do this:
if ($SolutionsWithErrors -eq 0) {
msbuild $Path /nologo /v:q /consoleloggerparameters:ErrorsOnly
} else {
msbuild $Path /nologo /v:q /consoleloggerparameters:ErrorsOnly | Out-Null
}
But it seems like there's got to be a way to do it without the duplication. (Okay, it doesn't have to be duplication -- I could leave off /consoleloggerparameters if I'm piping to null anyway -- but you get the idea.)
There may be other ways to solve this, but for today, I specifically want to know: is there a way to run a command, but only pipe its output if a certain condition is met (and otherwise not pipe it or redirect its output at all, so it can do fancy stuff like color-coded output)?
You can define the output command as a variable and use either Out-Default or Out-Null:
# set the output command depending on the condition
$output = if ($SolutionsWithErrors -eq 0) {'Out-Default'} else {'Out-Null'}
# invoke the command with the variable output
msbuild $Path /nologo /v:q /consoleloggerparameters:ErrorsOnly | & $output
UPDATE
The above code loses MSBuild colors. In order to preserve colors and yet avoid
duplication of code this approach can be used:
# define the command once as a script block
$command = {msbuild $Path /nologo /v:q /consoleloggerparameters:ErrorsOnly}
# invoke the command with output depending on the condition
if ($SolutionsWithErrors -eq 0) {& $command} else {& $command | Out-Null}
is there a way to run a command, but only pipe its output if a certain condition is met (and otherwise not pipe it or redirect its output at all, so it can do fancy stuff like color-coded output)?
There is no such a way built-in, more likely. But it can be implemented with a function and the function is reused as such a way:
function Invoke-WithOutput($OutputCondition, $Command) {
if ($OutputCondition) { & $Command } else { $null = & $Command }
}
Invoke-WithOutput ($SolutionsWithErrors -eq 0) {
msbuild $Path /nologo /v:q /consoleloggerparameters:ErrorsOnly
}