PowerShell mkdir alias + Set-StrictMode -Version 2. Strange bug. Why? - powershell

It's something unbelievable. This is a PowerShell code snippet in test.ps1 file:
Set-StrictMode -Version 2
mkdir c:\tmp\1 # same with 'md c:\tmp\1'
Start cmd.exe, navigate to folder with test.ps1 script and run it:
c:\tmp>powershell ".\test.ps1"
This produces the following error:
The variable '$_' cannot be retrieved because it has not been set.
At line:50 char:38
+ $steppablePipeline.Process($_ <<<< )
+ CategoryInfo : InvalidOperation: (_:Token) [], ParentContainsEr
rorRecordException
+ FullyQualifiedErrorId : VariableIsUndefined
Why?
It works when started from PowerShell console but not cmd.exe. I discovered this bug in much larger script. It was a WTF moment.
What is wrong with this simple script?

Even though a workaround has already been found, I thought people might be interested in an explanation.
As to why it behaves differently in the shell versus cmd.exe, see Powershell 2.0 - Running scripts for the command line call vs. from the ISE
As mentioned in the reference, there is a difference between the following two commands:
powershell ".\test.ps1"
powershell -File ".\test.ps1"
When using the first syntax, it seems to mess with scope, causing the Set-StrictMode command to modify the strict mode for functions defined at the global scope.
This triggers a bug (or an incorrect assumption, perhaps) in the definition of the mkdir function.
The function makes use of the GetSteppablePipeline method to proxy the pipeline for the New-Item cmdlet. However, the author neglected to account for the fact that the PROCESS section is still executed even when there is nothing in the pipeline. Thus, when the PROCESS section is reached, the $_ automatic variable is not defined. If strict mode is enabled, an exception will occur.
One way for Microsoft to account for this would be to replace following line:
$steppablePipeline.Process($_)
with the following:
if (test-path Variable:Local:_) {
$steppablePipeline.Process($_)
}
I admit that this may not be the best way to fix it, but the overhead would be negligible. Another option would be to somehow test if the pipeline is empty in the BEGIN section, and then set $_ to $null.
Either way, if you run your scripts with the "powershell.exe -File filename" syntax, then you won't need to worry about it.

It looks like a bug (in PowerShell).

Related

Executing a powershell script from another script with $PSScriptRoot?

I'm wondering what I'm missing here. I have a powershell script that calls another script with some parameters to execute as a way to keep things tidy. Here is what works:
C:\Scripts\Project\coolscript.ps1 -projname 'my.project' -domain 'work'
I want others to be able to use this script without having to change anything, so I thought I could make the path relative instead of the full one starting from C: so I thought I could execute the script like this:
$pathname = $PSScriptRoot + '\coolscript.ps1'
$pathname -projname 'my.project' -domain 'work'
however I get an error that says 'unexpected token in expression or statement for everything after $pathname
ANy ideas what I'm missing? Thank you
Use the Call operator (&) as follows:
& $pathname -projname 'my.project' -domain 'work'
Call operator &
Runs a command, script, or script block. The call operator, also known as the "invocation operator," lets you run commands that are
stored in variables and represented by strings or script blocks. The
call operator executes in a child scope. For more about scopes, see
about_scopes.

Powershell function call causes missing function error using powershell v7 on windows 10

I wrote a script to build all .net projects in a folder.
Issue
The issue is I am getting a missing function error when I call Build-Sollution.
What I tried
I made sure that function was declared before I used it so I am not really sure why it saids that it is not defined.
I am new to powershell but I would think a function calling another functions should work like this?
Thanks in advance!
Please see below for the error message and code.
Error Message
Line |
3 | Build-Sollution $_
| ~~~~~~~~~~~~~~~
The term 'Build-Sollution' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
Build-Sollution:
Code
param (
#[Parameter(Mandatory=$true)][string]$plugin_path,
[string]$depth = 5
)
$plugin_path = 'path/to/sollutions/'
function Get-Sollutions {
Get-ChildItem -File -Path $plugin_path -Include *.sln -Recurse
}
function Build-Sollution($solution) {
dotnet build $solution.fullname
}
function Build-Sollutions($solutions) {
$solutions | ForEach-Object -Parallel {
Build-Sollution $_
}
}
$solutions_temp = Get-Sollutions
Build-Sollutions $solutions_temp
From PowerShell ForEach-Object Parallel Feature | PowerShell
Script blocks run in a context called a PowerShell runspace. The runspace context contains all of the defined variables, functions and loaded modules.
...
And each runspace must load whatever module is needed and have any variable be explicitly passed in from the calling script.
So in this case, the easiest solution is to define Build-Sollution inside Build-Sollutions
As for this...
I am new to powershell but I would think a function calling another
functions should work like this?
... you cannot use the functions until you load your code into memory. You need to run the code before the functions are available.
If you are in the ISE or VSCode, if the script is not saved, Select All and hit use the key to run. In the ISE use F8 Selected, F5 run all. In VSCode, F8 run selected, crtl+F5 run all. YOu can just click the menu options as well.
If you are doing this from the consolehost, the run the script using dot sourcing.
. .\UncToYourScript.ps1
It's ok to be new, we all started somewhere, but it's vital that you get ramped up first. so, beyond what I address here, be sure to spend time on Youtube and search for Beginning, Intermediate, Advanced PowerShell for videos to consume. There are tons of free training resources all over the web and using the built-in help files would have given you the answer as well.
about_Scripts
SCRIPT SCOPE AND DOT SOURCING Each script runs in its own scope. The
functions, variables, aliases, and drives that are created in the
script exist only in the script scope. You cannot access these items
or their values in the scope in which the script runs.
To run a script in a different scope, you can specify a scope, such as
Global or Local, or you can dot source the script.
The dot sourcing feature lets you run a script in the current scope
instead of in the script scope. When you run a script that is dot
sourced, the commands in the script run as though you had typed them
at the command prompt. The functions, variables, aliases, and drives
that the script creates are created in the scope in which you are
working. After the script runs, you can use the created items and
access their values in your session.
To dot source a script, type a dot (.) and a space before the script
path.
See also:
'powershell .net projects build run scripts'
'powershell build all .net projects in a folder'
Simple build script using Power Shell
Update
As per your comments below:
Sure the script should be saved, using whatever editor you choose.
The ISE does not use PSv7 by design, it uses WPSv5x and earlier.
The editor for PSv7 is VSCode. If you run a function that contains another function, you have explicitly loaded everything in that call, and as such it's available.
However, you are saying, you are using PSv7, so, you need to run your code in the PSv7 consolehost or VSCode, not the ISE.
Windows PowerShell (powershell.exe and powershell_ise.exe) and PowerShell Core (pwsh.exe) are two different environments, with two different executables, designed to run side-by-side on Windows, but you do have to explicitly choose which to use or write your code to branch to a code segment to execute relative to the host you started.
For example, let's say I wanted to run a console command and I am in the ISE, but I need to run that in Pwsh. I use a function like this that I have in a custom module autoloaded via my PowerShell profiles:
# Call code by console executable
Function Start-ConsoleCommand
{
[CmdletBinding(SupportsShouldProcess)]
[Alias('scc')]
Param
(
[string]$ConsoleCommand,
[switch]$PoSHCore
)
If ($PoSHCore)
{Start-Process pwsh -ArgumentList "-NoExit","-Command &{ $ConsoleCommand }" -PassThru -Wait}
Else {Start-Process powershell -ArgumentList "-NoExit","-Command &{ $ConsoleCommand }" -PassThru -Wait}
}
All this code is doing is taking whatever command I send it and if I use the PoSHCore switch...
scc -ConsoleCommand 'SomeCommand' -PoSHCore
... it will shell out to PSCore, run the code, otherwise, it just runs from the ISE>
If you want to use the ISE with PSv7 adn not do the shell out thing, you need to force the ISE to use PSv7 to run code. See:
Using PowerShell Core 6 and 7 in the Windows PowerShell ISE

What does hyphen/dash parameter mean to PowerShell?

I've found out that, if I pass only a dash to an argument of PowerShell 5.1 script on Windows 10, like this:
powershell.exe -File Test.ps1 -
I get a strange error message saying:
C:\path\Test.ps1 : Cannot process argument because the value of argument "name" is not valid. Change the value o f the "name" argument and run the operation again.
CategoryInfo : InvalidArgument: (:) [Test.ps1], PSArgumentException
FullyQualifiedErrorId : Argument,Test.ps1
The Test.ps1 is only:
echo "foo"
The actual problem I face though is that, when the script declares any mandatory parameter:
param (
[Parameter(Mandatory)]
$value
)
echo "foo"
Then executing the script the same way (with - argument) does nothing at all. No output. No error message. It just hangs for a few seconds. And then a control returns to a command prompt.
C:\path>powershell.exe -File Test.ps1 -
C:\path>_
What does the - mean to PowerShell (5.1)?
On the contrary, with PowerShell 2.0 on Windows 7, I get script usage in this case:
C:\path>powershell.exe -File Test.ps1 -
Test.ps1 [-value] <Object> [-Verbose] [-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <ActionPreference>] [-ErrorVariable <String>] [-WarningVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>]
C:\path>_
What makes sense (a missing mandatory parameter).
And without the mandatory parameter declaration, the script works (prints it output):
C:\path>powershell.exe -File Test.ps1 -
foo
C:\path>_
The behavior should be considered a bug - something that starts with - but isn't a valid parameter name should be passed as a positional argument rather than reporting an error.
The bug affects:
Windows PowerShell (as of v5.1.18362.145) - it is unclear if a fix will ever be made.
As you state, whether you (a) get the error message (... the value of argument "name" is not valid ...) or (b) the - is quietly ignored depends on whether your parameter has a parameter attribute such as [Parameter(Mandatory)][1] and/or your param() block has a [CmdletBinding()] attribute (if so, (b) applies).
PowerShell Core 6.x - that is, the problem will be fixed in v7 (current as of this writing: v7.0.0-preview.3); I don't know if 6.2.2, the stable version current as of this writing, will be fixed - we'll see what happens to the bug report on GitHub you've filed.
As for a workaround (works analogously in PowerShell Core):
Use -Command instead of -File.
While that changes the semantics of how the command line is parsed[2], in simple cases such as this one the difference won't matter:
C:\> powershell -Command ./Test.ps1 - # note the "./"
Note the ./, because using -Command (-c) makes PowerShell parse the arguments as if they were PowerShell code, and the usual restrictions re executing scripts by filename only apply (to prevent accidental execution of a file in the current directory, you need a path component to explicitly signal that intent, hence prefix ./ or .\ is needed).
If your script file path needed quoting, you'd have to use quoting and prepend &, the call operator; e.g.:
C:\> powershell -Command "& \"./Test.ps1\" -"
[1] Adding a [Parameter()] attribute to a declared parameter implicitly makes the enclosing script/function an advanced one, in which case different parsing rules apply. The [CmdletBinding[] attribute, which is applied to a param(...) block as a whole, explicitly marks a script / function as an advanced one.
[2] See this answer for the differences between how -File and -Command arguments are parsed.
This isn't an answer but I'm curious as well. If you've already figured it out I'd be interested in what you found. Otherwise, in case it helps, Powershell -h says everything after -file is the script and any arguments passed to it.
-File
Runs the specified script in the local scope ("dot-sourced"), so that the
functions and variables that the script creates are available in the
current session. Enter the script file path and any parameters.
File must be the last parameter in the command, because all characters
typed after the File parameter name are interpreted
as the script file path followed by the script parameters.
Tokenizer reads it in as a CommandArgument.
Powershell >> $Errors = $Null
Powershell >> [System.Management.Automation.PSParser]::Tokenize("powershell -file test.ps1 -", [ref]$Errors)[3]
Content : -
Type : CommandArgument
Start : 26
Length : 1
StartLine : 1
StartColumn : 27
EndLine : 1
EndColumn : 28
So it seems like the issue is further up the chain but I couldn't find a simple way to call the Parser functions to test.
I do see that there's a case that shouldn't occur where the error is swallowed and null returned which might cause it to just stop like it does in your example

Script runs in ISE but not in Powershell

I have a powershell script. This script runs perfectly fine if I open it in Powershell ISE, however, if I right click on the file and click 'run with powershell' the script throws an error.
Furthermore, I read in previous threads that the following execution pattern solved the issue for some people:
powershell -STA -File script.ps1
In this case this didn't solve the issue, however it did allow me to read the error:
At C:\Users\sancarn\AppData\Local\Temp\script.ps1:20 char:20
+ $parent = [System.Windows.Forms.TreeNode]$global:database.Ite ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unable to find type [System.Windows.Forms.TreeNode].
At C:\Users\sancarn\AppData\Local\Temp\script.ps1:27 char:36
+ ... [void]$node.nodes.add([System.Windows.Forms.TreeNode]::new(" ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unable to find type [System.Windows.Forms.TreeNode].
At C:\Users\sancarn\AppData\Local\Temp\script.ps1:33 char:45
+ ... PSCustomObject]IWDBGetChildren([System.Windows.Forms.TreeNode]$node) ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Unable to find type [System.Windows.Forms.TreeNode].
+ CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : TypeNotFound
This having been said, I'm not sure I can really do anything about this error specifically... I already load System.Windows.Forms and System.Drawing... Does anyone have any idea how to execute this file properly?
Edit
Other attempts at trying to fix the issue:
powershell -NoExit -STA -File script.ps1
powershell -NoExit -STA -File script.ps1 -Scope Global
Edit 2
I have also tried adding:
Add-Type -AssemblyName System.Windows.Forms
To the top of the powershell script. The issue remains unresolved however.
Edit:
Not sure why this is being flagged as a duplicate after
this answer already has a recommended answer and
the recommended answer demonstrates why this is different.
...
As #PetSerAl said in his comment, the answer is in https://stackoverflow.com/a/34637458
Every PowerShell script is completely parsed before the first statement in the script is executed. An unresolvable type name token inside a class definition is considered a parse error. To solve your problem, you have to load your types before the class definition is parsed, so the class definition has to be in a separate file.
Ultimately to resolve the issue I need to either load my class definitions as a separate file, or store my declarations in a header file, which calls the class definitions (and the rest of the script).
I am amazed this was even an issue but it works now, so that's good...
Edit
In my case it is a bit different to the post linked by the solution author. I'm actually building a ruby library for executing Powershell on windows machines. Currently I don't write data to a file and I can't guarantee that a here-string/invoke-expression will work.
Invoke-Expression #"
$anotherString=#"
hello world!
"#
"# <--- Powershell will throw an error here!
Instead I've decided to encode the body if a header is present, and then decode and execute at runtime
require 'base64'
if header!=""
encoded_body = Base64.strict_encode64(body.encode("utf-16le"))
body = <<-END_TRANSFORMATION
#{header}
$encoded_body = "#{encoded_body}"
Invoke-Expression $([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($encoded_body)))
END_TRANSFORMATION
end
So far this has appeared to work in all cases, doesn't need external files, and will work even if users use here-doc strings in their Powershell scripts!

Error: "the term is not recognised as the name of a cmdlet"

I have written a basic script to execute and send an email with an attachment, I have tested this in PowerShell. However when I try to create this as a task scheduler I get the following error:
However my exact same code executes without issue in PowerShell.
This is my view in task scheduler:
What am I missing? I have set the execution policy so that is not an issue and I've tested my code and there is no issue there.
Use the -File parameter instead:
-File "C:\path\to\your.ps1"
PowerShell's call operator (&) works only inside PowerShell, so you would need to put it inside the argument string:
"& 'C:\path\to\your.ps1'"
While that would work too, it has certain disadvantages, most importantly that it will return only 0 (success) or 1 (failure) as the exit code, but not the actual exit code of the script.
Your argument needs to include the -Command parameter, not just the code you want to run. Also, your code to run needs to be a string, not raw Powershell code, or the command line will try to interpret it. Set the argument to:
-Command '&"C:\Users\Martyn\Documents\Powershell Scripts\Working.ps1"'
Or, you can try the -File parameter directly, although there are known issues with it:
-File "C:\Users\Martyn\Documents\Powershell Scripts\Working.ps1"
If anyone else is getting this error, you need to install ProcessMitigation into PowerShell before you can use it. By default, it's not there.
Steps to install:
Run Powershell with administrative rights.
Run Save-Module -Name ProcessMitigations -Path <path> command in PS to download the component. Provide actual path instead of to download.
Run Install-Module -Name ProcessMitigations in PS to install it.
More here.