powershell confused about array loop, foreach method, string? - powershell

I'm trying to export all CA certificates to a directory in Base64 format, I'm new to powershell, since I'm used to doing scripts with bash. Somehow I'm not seeing something that feels like it should be obvious.
This is my line so far,
#(Get-ChildItem -path Cert:\Localmachine\ca).ForEach({Export-Certificate -Type CERT -FilePath "C:\ssl\certs.d\$_.Thumbprint" -Cert "Cert:\LocalMachine\ca\$_.Thumbprint"})
I appreciate any help, as I'm trying to learn how to be idiomatic in PS4.

This line of code contains 3 issues:
First. String interpolation with object property. PS parser doesn't understand "$var.Property", it only understands $expression within "string". But since it's expression, and not just variable name, you can make PS evaluate your line with "$(something to evaluate)". In other words, your -FilePath should be:
-FilePath "C:\ssl\certs.d\$($_.Thumbprint)"
Second. Working with objects. PS underneath is full-blown .Net Framework. Even though many objects are represented in output in a simple, predefined way, actually they are |ed to output as complete live objects. According to MSDN, the -Cert parameter is a <Certificate>, not a string pointing to a certificate, so your -Cert should be simply
-Cert $_
Third. Arrays. Get-ChildItem underneath is nothing more than DirectoryInfo.GetFileSystemInfos() which returns an array of objects. So ideally, you don't need to wrap it with anything and it's possible to simply pipe it further (Get-ChildItem | Foreach-Object{...}). But many people have different tastes on PS syntax, so the form of (gci).ForEach({...}) (without #) has the right to live as well. But what you are doing in form of #(...) is creating a new array of one item being the array returned to you by gci. So technically, it just shouldn't work. It will though, because PS saves you from such mistakes automatically: in PS you can work with array of 1 item in the same way as with this item directly (unless explicitly specified opposite). To illustrate,
#(4).Length # returns 1
#(#(2,3)).Length # returns 2
#(,#(2,3)).Length #returns 1
Thus, your current syntax for Get-ChildItem is error-prone and relies on automatic PS error-handling sugar. I recommend to either remove # in the beginning, or to rewrite in form of
Get-ChildItem -...... | Foreach-Object {...}

Related

PowerShell Get-VHD "is not an existing virtual hard disk file"

When creating a new VM in Hyper-V, to keep things organized, I use a particular naming convention when creating the associated VHDX files. The naming convention is the VMs FQDN followed by the SCSI controller attachment point followed by what the name of the drive is called or used for inside of the VM. I encapsulate the SCSI and Name parameters inside smooth and square brackets respectively. I find this tends to make things a little bit easier from a human perspective to match the VHDX files in Hyper-V to what the VM sees internally when needing to do maintenance tasks. It has also helped with scripting in the past. An example file name would look as follows...
servername.example.com(0-0)[OS].vhdx
This has worked well for quite some time, but recently I tried to run some PowerShell commands against the VHDX files and ran across a problem. Apparently the square brackets for the internal VM name are being parsed as RegEx or something inside of the PowerShell commandlet (I'm honestly just guessing on this). When I try to use Get-VHD on a file with the above naming convention it spits out an error as follows:
Get-VHD : 'E:\Hyper-V\servername.example.com\Virtual Hard Disks\servername.example.com(0-0)[OS].vhdx' is not an existing virtual hard disk file.
At line:1 char:12
+ $VhdPath | Get-VHD
+ ~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [Get-VHD], VirtualizationException
+ FullyQualifiedErrorId : InvalidParameter,Microsoft.Vhd.PowerShell.Cmdlets.GetVHD
If I simply rename the VHDX file to exclude the "[OS]" portion of the naming convention the command works properly. The smooth brackets for the SCSI attachment point don't seem to bother it. I've tried doing a replace command to add a backtick ''`'' in front of the brackets to escape them, but the same error results. I've also tried double backticks to see if passing in a backtick helped... that at least showed a single backtick in the error it spat out. Suspecting RegEx, I tried the backslash as an escape character too... which had the interesting effect of converting all the backslashes in the file path into double backslashes in the error message. I tried defining the path variable via single and double quotes without success. I've also tried a couple of different ways of obtaining it via pipeline such as this example...
((Get-VM $ComputerName).HardDrives | Select -First 1).Path | Get-VHD
And, for what it's worth, as many VMs as I am attempting to process... I need to be able to run this via pipeline or some other automation scriptable method rather than hand coding a reference to each VHDX file.
Still thinking it may be something with RegEx, I attempted to escape the variable string with the following to no avail:
$VhdPathEscaped = [System.Text.RegularExpressions.Regex]::Escape($VhdPath)
Quite frankly, I'm out of ideas.
When I first ran across this problem was when I tried to compact a VHDX file with PowerShell. But, since the single VM I was working with needed to be offline for that function to run anyway, rather than fight the error with the VHDX name, I simply renamed it, compacted it, and reset the name back. However, for the work I'm trying to do now, I can't afford to take the VM offline as this script is going to run against a whole fleet of live VMs. So, I need to know how to properly escape those characters so the Get-VHD commandlet will accept those file names.
tl;dr:
A design limitation of Get-VHD prevents it from properly recognizing VHD paths that contain [ and ] (see bottom section for details).
Workaround: Use short (8.3) file paths assuming the file-system supports them:
$fso = New-Object -ComObject Scripting.FileSystemObject
$VhdPath |
ForEach-Object { $fso.GetFile((Convert-Path -LiteralPath $_)) } |
Get-VHD
Otherwise, your only options are (as you report, in your case the VHDs are located on a ReFS file-system, which does not support short names):
Rename your files (and folders, if applicable) to not contain [ or ].
Alternatively, if you can assume that your VHDs are attached to VMs, you can provide the VM(s) to which the VHD(s) of interests are attached as input to Get-VHD, via Get-VM (you may have to filter the output down to only the VHDs of interest):
(Get-VM $vmName).Id | Get-VHD
Background information:
It looks like Get-VHD only has a -Path parameter, not also a -LiteralPath parameter, which looks like a design flaw:
Having both parameters is customary for file-processing cmdlets (e.g. Get-ChildItem):
-Path accepts wildcard expressions to match potentially multiple files by a pattern.
-LiteralPath is used to pass literal (verbatim) paths, to be used as-is.
What you have is a literal path that happens to look like a wildcard expression, due to use of metacharacters [ and ]. In wildcard contexts, these metacharacter must normally be escaped - as `[ and `] - in order to be treated as literals, which the following (regex-based) -replace operation ensures[1] (even with arrays as input).
Unfortunately, this appears not to be enough for Get-VHD. (Though you can verify that it works in principle by piping to Get-Item instead, which also binds to -Path).
Even double `-escaping (-replace '[][]', '``$&') doesn't work (which is - unexpectedly required in come cases - see GitHub issue #7999).
# !! SHOULD work, but DOES NOT
# !! Ditto for -replace '[][]', '``$&'
$VhdPath -replace '[][]', '`$&' | Get-VHD
Note: Normally, a robust way to ensure that a cmdlet's -LiteralPath parameter is bound by pipeline input is to pipe the output from Get-ChildItem or Get-Item to it.[2] Given that Get-VHD lacks -LiteralPath, this is not an option, however:
# !! DOES NOT HELP, because Get-VHD has no -LiteralPath parameter.
Get-Item -LiteralPath $VhdPath | Get-VHD
[1] See this regex101.com page for an explanation of the regex ($0 is an alias of $& and refers to the text captured by the match at hand, i.e. either [ or ]). Alternatively, you could pass all paths individually to the [WildcardPattern]::Escape() method (e.g., [WildcardPattern]::Escape('a[0].txt') yields a`[0`].txt.
[2] See this answer for the specifics of how this binding, which happens via the provider-supplied .PSPath property, works.
Ok... So, I couldn't get the escape characters to be accepted by Get-VHD... be it by hand or programmatically. I gave it a go of passing it on the pipeline using Get-ChildItem too without success. However... I did manage to find an alternative for my particular use case. In addition to a path to a VHDX file, the Get-VHD command will also accept vmid, and disknumber as parameters. So, not that it's the way I wanted to go about obtaining what I need (because this method spits out info on all the attached drives), I can still manage to accomplish the task at hand by using the following example:
Get-VM $ComputerName | Select-Object -Property VMId | Get-VHD
By referencing them in this manner the Get-VHD commandlet is happy. This works for today's problem only because the VHDX files in question are attached to VMs. However, I'll still need to figure out about referencing unattached files at some point in the future. Which... Maybe ultimately require a slow and painful renaming of all the VHDX files to not use the square brackets in their name.

Get-ChildItem with Multiple Paths via Variable

This one stumps me a bit. I generally feel pretty advanced in powershell but I simply dont understand the nuance of this one.
This works
$LogFiles = Get-ChildItem -Path c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log
Yet what I want to do (and doesnt work) is this:
$LogsToGather = "c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log"
$LogFiles = Get-ChildItem -Path "$($LogsToGather)" -Recurse
I have tried making the VAR an array, I have tried a number of things with making string. I was able to write around the issue but I am uniquely interested in understanding what data type -path is accepting with that common delineation and be able to create it dynamically.
It seems like a trick that the cmdlet accepts comma delineation. Can it be recreated using some sort of array, hashtable, etc..?
Anyone know?
Yes, $LogsToGather must be an array of strings for your command to work:
$LogsToGather = 'c:\windows\temp\*.log', 'c:\temp\*.log', 'C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log'
Note how the array elements, separated by ,, must be quoted individually (see bottom section).
Get-Help with -Parameter is a quick way to examine the data type a given parameter expects:
PS> Get-Help Get-ChildItem -Parameter Path
-Path <String[]>
Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (`.`).
Required? false
Position? 0
Default value Current directory
Accept pipeline input? True (ByPropertyName, ByValue)
Accept wildcard characters? false
String[] represents an array ([]) of [string] (System.String) instances - see about_Command_Syntax.
For more information on Get-ChildItem, see the docs.
As for what you tried:
$LogsToGather = "c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log"
This creates a single string that, when passed to Get-ChildItem, is as a whole interpreted as a single path, which obviously won't work.
Note that specifying the elements of an array unquoted, as in:
Get-ChildItem -path c:\windows\temp\*.log, c:\temp\*.log, ...
is only supported if you pass an array as a command argument, not in the context of creating an array with an expression such as $LogsToGather = 'foo', 'bar', ..
The reason is that PowerShell has two fundamental parsing modes - argument mode and expression mode, as explained in this answer,

Is there a functional difference between -OutVariable and variable assignment?

I came across the following bizarre behavior and I was hoping someone could point me to some documentation that can explain why this behavior happens or what is causing it.
I have tried looking at the documentation for New-SelfSignedCertificate to see if there was a remark explaining this or a parameter that "forced" it to complete but didn't find anything useful. https://learn.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps
I also tried searching google for OutVariable vs assignment but didn't find anything helpful either.
Consider the following functions:
function Why-DoesThisFail {
$cert = New-SelfSignedCertificate -Type Custom -Subject "CN=test" -KeyAlgorithm "RSA" -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" -NotAfter (Get-Date).AddDays(1) -KeySpec Signature `
-KeyExportPolicy NonExportable;
# Note that this outputs fine.
Write-Host $cert.Thumbprint;
# Prints nothing
Get-ChildItem -Path "Cert:\CurrentUser\My\$cert.Thumbprint";
}
function Why-DoesThisPass {
New-SelfSignedCertificate -Type Custom -Subject "CN=test" -KeyAlgorithm "RSA" -KeyLength 2048 `
-CertStoreLocation "Cert:\CurrentUser\My" -NotAfter (Get-Date).AddDays(1) -KeySpec Signature `
-KeyExportPolicy NonExportable -OutVariable cert;
# Note that this outputs fine.
Write-Host $cert.Thumbprint;
# Prints Cert as expected
Get-ChildItem -Path "Cert:\CurrentUser\My\$cert.Thumbprint";
}
Notice that the only difference between the 2 functions is one is using variable assignment and one is using the OutVariable. Is this a behavior of Powershell itself in how it is handling OutVariable vs assignment; or is this because of something that New-SelfSignedCertificate is doing behind the covers? It feels almost as if the Certificate isn't being registered on the machine until after the New-SelfSignedCertificate completes and it doesn't complete for some reason when variable assignment is used. Note that after the function completes and control is returned to Powershell, you can successfully run the last line (replacing the thumbprint with the one written from Write-Host) and retrieve the certificate from the machine.
I'm puzzled.
Thanks for any help!
Your issue, is not with any confusion or context with this?
-OutVariable and variable assignment?
It's because you are using the line in the first one wrong. Double quotes do coerce variable expansion, but in your case you need to change the string interpolation for that dotted $cert variable. So, you are having a misunderstanding about string expansion.
This...
Get-ChildItem -Path "Cert:\CurrentUser\My\$cert.Thumbprint";
... should be done this way.
Get-ChildItem -Path "Cert:\CurrentUser\My\$($cert.Thumbprint)"
... otherwise, PowerShell does not know what it is. Also, those semi-colon(s) is never really needed in PowerShell proper. It is a special marker separating distinct code segments. If you had this all on one line, that's at thing, but not how you are using it here.
If you are doing this in the ISE or VSCode, you'll see the color coding change to reflect the which will be expanded and which will not.
See this article.
Powershell String Expansion
Variable Interpolation, or Two Types of Strings Strings with single
quotes (') are literal strings, meaning there are no special
characters; every character in the string will output as itself.
Strings with double quotes (") in powershell will expand variables and
escape characters inside the string; this is referred to as
interpolation.
And this one, though it talks to Here-Strings, it still covers the point I am trying to make here.
Variable expansion in strings and here-strings
Now as for your post question...
Is there a functional difference between -OutVariable and variable
assignment?
... The send result of either is the same. However, this is the deal...
You can store the output of a command in a variable and at the sametime have this output on the screen. This what Out-Variable also does, whereas variable assignment does no output. Yet it can, if you use variable squeezing. So, this...
Get-ChildItem -OutVariable files
... this ...
($files = Get-ChildItem)
... will do the same thing. Assign the results to the variable, and output to the screen at the same time.
See this ---
3 ways to store and display PowerShell Variable simultaneously
Using -OutVariable parameter
PowerShell Variable squeezing Using
Tee-Object Cmdlet
Lastly, avoid the use of Write-Host, unless you are doing text screen coloring. PowerShell, will output to the screen is the default, unless you tell it otherwise. So, this
Write-Host $cert.Thumbprint
Write-Host 'Hello'
... and this …
$cert.Thumbprint
'Hello'
... will do the same thing.
If you must use Write-*, because of your chosen coding practice, then choose Write-Output.
Update for Andrei Odegov point / pointer
Yes, they just output hello to the screen. Try it.
Write-Host 'hello'
# results
hello
'Hello'
# results
Hello
As for your link ...
Is your last statement that Write-Host "Hello" and "Hello" do the same
thing, correct? Look at stackoverflow.com/questions/8755497/…
... these all do the same thing. Output to the screen.
$count = 5
write-host "count=" + $count
# results
count= + 5
write-host "count = $count"
# results
count = 5
"count = $count"
# results
count = 5
"count=" + $count
# Results
count=5
That first one is why string concatenation can be, well, has issues. I am not the only one who thinks so. See this video.
From the authors ofThe Big book of 'The PowerShell Gotcha's'
Don't Concatenate Strings
https://devops-collective-inc.gitbook.io/the-big-book-of-powershell-gotchas
https://www.youtube.com/results?search_query=don%27t+concatenate+string

How can I move from Windows traditional command line to the modern PowerShell?

I was used to a few command line tricks in Windows that increased my productivity a lot.
Now I am told that I should move to PowerShell because it's more POWERful. It took me a while to get a little bit hang of it (objects, piping, etc.), and there are a lot of great tutorials on how to get a few things done.
However, some (relatively) basic trick still puzzle me. For instance, what is the equivalent of the FOR structure in PowerShell?
For example,
FOR %i IN (*.jpg) DO Convert %i -resize 800x300 resized/%i
The above line takes all of photos in a folder and uses the ImageMagick's Convert tool to resize the images and restores the resized imaged in a sub-folder called RESIZED.
In PowerShell I tried the command:
Dir ./ | foreach {convert $_.name -resize 800x300 resized/$_name}
This can't work despite all of the googling around I did. What is missing?
Note that / rather than \ is used as the path separator in this answer, which works on Windows too and makes the code compatible with the cross-platform PowerShell Core editions.
tl;dr:
$convertExe = './convert' # adjust path as necessary
Get-ChildItem -File -Filter *.jpg | ForEach-Object {
& $convertExe $_.Name -resize 800x300 resized/$($_.Name)
}
Read on for an explanation and background information.
The equivalent of:
FOR %i IN (*.jpg)
is:
Get-ChildItem -File -Filter *.jpg
or, with PowerShell's own wildcard expressions (slower, but more powerful):
Get-ChildItem -File -Path *.jpg # specifying parameter name -Path is optional
If you're not worried about restricting matches to files (as opposed to directories), Get-Item *.jpg will do too.
While dir works as a built-in alias for Get-ChildItem, I recommend getting used to PowerShell's own aliases, which follow a consistent naming convention; e.g., PowerShell's own alias for Get-ChildItem is gci
Also, in scripts it is better to always use the full command names - both for readability and robustness.
As you've discovered, to process the matching files in a loop you must pipe (|) the Get-ChildItem command's output to the ForEach-Object cmdlet, to which you pass a script block ({ ... }) that is executed for each input object, and in which $_ refers to the input object at hand.
(foreach is a built-in alias for ForEach-Object, but note that there's also a foreach statement, which works differently, and it's important not to confuse the two.)
There are 2 pitfalls for someone coming from the world of cmd.exe (batch files):
In PowerShell, referring to an executable by filename only (e.g., convert) does not execute an executable by that name located in the current directory, for security reasons.
Only executables in the PATH can be executed by filename only, and unless you've specifically placed ImageMagick's convert.exe in a directory that comes before the SYSTEM32 directory in the PATH, the standard Windows convert.exe utility (whose purpose is to convert FAT disk volumes to NTFS) will be invoked.
Use Get-Command convert to see what will actually execute when you submit convert; $env:PATH shows the current value of the PATH environment variable (equivalent of echo %PATH%).
If your custom convert.exe is indeed in the current directory, invoke it as ./convert - i.e., you must explicitly reference its location.
Otherwise (your convert.exe is either not in the PATH at all or is shadowed by a different utility) specify the path to the executable as needed, but note that if you reference that path in a variable or use a string that is single- or double-quoted (which is necessary if the path contains spaces, for instance), you must invoke with &, the call operator; e.g.,
& $convertExe ... or & "$HOME/ImageMagic 2/convert" ...
PowerShell sends objects through the pipeline, not strings (this innovation is at the heart of PowerShell's power). When you reference and object's property or an element by index as part of a larger string, you must enclose the expression in $(...), the subexpression operator:
resized/$($_.Name) - Correct: property reference enclosed in $(...)
resized/$_.Name - !! INCORRECT - $_ is stringified on its own, followed by literal .Name
However, note that a stand-alone property/index reference or even method call does not need $(...); e.g., $_.Name by itself, as used in the command in the question, does work, and retains its original type (is not stringified).
Note that a variable without property / index access - such as $_ by itself - does not need $(...), but in the case at hand $_ would expand to the full path. For the most part, unquoted tokens that include variable references are treated like implicitly double-quoted strings, whose interpolation rules are summarized in this answer of mine; however, many additional factors come into play, which are summarized here; edge cases are highlighted in this question.
At the end of the day, the safest choice is to double-quote strings that contain variable references or subexpressions:
"resized/$($_.Name)" - SAFEST
Use:
Get-ChildItem | foreach {convert $_.name -resize 800x300 resized/$($_.name)}
Or, perhaps, you need to pass the full name (with path), also showing a shorter syntax (using aliases):
gci | % {convert $_.fullname -resize 800x300 resized/$($_.name)}
Also, you might want to supply the full path to the executable.
Revised based on comments given below
There are many applications with the name "Convert". If I do
Get-Command Convert
on my computer. It shows me an app that is part of the Windows system. If PowerShell is running the wrong app on you, it's never going to work.
The solution will be to point PowerShell at the convert tool inside the ImageMagick program folder. A Google search on "ImageMagick PowerShell" will lead you to lots of people who have faced the same problem as you.

Check for File in multiple directories with an IF statement Powershell

I am facing some problems in powershell. I want to be able to let a powershell command search for multiple directories.
With the name being a variable like "$VM_DISK=VM_DISK.vhdx" and let powershell search in that manor so that if that file exists in a folder such as C:\VM_DISK\ it exit the script.
I have already tried the "Get-Childitem" but it doesn't seem to work when I put my variable in it. Here is an example:
$VM_DISK= "Example.vhdx"
$search=Get-ChildItem -Path C:\VM_DISK\* -Filter $VM_DISK -Recurse
if ($search -eq $VM_DISK) {write-host "Goodbye!" exit} else {write-host "Continue"}
I just cant seem to figure out why this isn't working, hope some can figure it out.
You need to alter your if statement.
if ($search.Name -contains $VM_Disk)
This way you are comparing an Array of names (which is what you want, names of objects, not objects) to a name of particular object (to a string, basically).
This makes little sense in your case, tbh. Since $search would always include $VM_Disk or would be null if nothing was found.
So the proper way to test would be if ($search) (just like Mathias advised). Which would test if anything was returned. Which, basically equals what you are trying to do.