Powershell - clarification about foreach - powershell

I am learning powershell and I need someone to give me an initial push to get me through the learning curve. I am familiar with programming and dos but not powershell.
What I would like to do is listing all files from my designated directory and pushing the filenames into an array. I am not very familiar with the syntax and when I tried to run my test I was asked about entering parameters.
Could someone please enlighten me and show me the correct way to get what I want?
This is what powershell asked me:
PS D:\ABC> Test.ps1
cmdlet ForEach-Object at command pipeline position 2
Supply values for the following parameters:
Process[0]:
This is my test:
[string]$filePath = "D:\ABC\*.*";
Get-ChildItem $filePath | foreach
{
$myFileList = $_.BaseName;
write-host $_.BaseName
}
Why was ps asking about Process[0]?
I would want to ps to list all the files from the directory and pipe the results to foreach where I put each file into $myFileList array and print out the filename as well.

Don't confuse foreach (the statement) with ForEach-Object (the cmdlet). Microsoft does a terrible job with this because there is an alias of foreach that points to ForEach-Object, so when you use foreach you have to know which version you're using based on how you're using it. Their documentation makes this worse by further conflating the two.
The one you're trying to use in your code is ForEach-Object, so you should use the full name of it to differentiate it. From there, the issue is that the { block starts on the next line.
{} is used in PowerShell for blocks of code related to statements (like while loops) but is also used to denote a [ScriptBlock] object.
When you use ForEach-Object it's expecting a scriptblock, which can be taken positionally, but it must be on the same line.
Conversely, since foreach is a statement, it can use its {} on the next line.
Your code with ForEach-Object:
Get-ChildItem $filePath | ForEach-Object {
$myFileList = $_.BaseName;
write-host $_.BaseName
}
Your code with foreach:
$files = Get-ChildItem $filePath
foreach ($file in $Files)
{
$myFileList = $file.BaseName;
write-host $file.BaseName
}

Related

Read value of variable in .ps1 and update the same variable in another .ps1

I'm trying to find an efficient way to read the value of a string variable in a PowerShell .ps1 file and then update the same variable/value in another .ps1 file. In my specific case, I would update a variable for the version # on script one and then I would want to run a script to update it on multiple other .ps1 files. For example:
1_script.ps1 - Script I want to read variable from
$global:scriptVersion = "v1.1"
2_script.ps1 - script I would want to update variable on (Should update to v1.1)
$global:scriptVersion = "v1.0"
I would want to update 2_script.ps1 to set the variable to "v1.1" as read from 1_script.ps1. My current method is using get-content with a regex to find a line starting with my variable, then doing a bunch of replaces to get the portion of the string I want. This does work, but it seems like there is probably a better way I am missing or didn't get working correctly in my tests.
My Modified Regex Solution Based on Answer by #mklement0 :
I slightly modified #mklement0 's solution because dot-sourcing the first script was causing it to run
$file1 = ".\1_script.ps1"
$file2 = ".\2_script.ps1"
$fileversion = (Get-Content $file1 | Where-Object {$_ -match '(?m)(?<=^\s*\$global:scriptVersion\s*=\s*")[^"]+'}).Split("=")[1].Trim().Replace('"','')
(Get-Content -Raw $file2) -replace '(?m)(?<=^\s*\$global:scriptVersion\s*=\s*")[^"]+',$fileversion | Set-Content $file2 -NoNewLine
Generally, the most robust way to parse PowerShell code is to use the language parser. However, reconstructing source code, with modifications after parsing, may situationally be hampered by the parser not reporting the details of intra-line whitespace - see this answer for an example and a discussion.[1]
Pragmatically speaking, using a regex-based -replace solution is probably good enough in your simple case (note that the value to update is assumed to be enclosed in "..." - but matching could be made more flexible to support '...' quoting too):
# Dot-source the first script in order to obtain the new value.
# Note: This invariably executes *all* top-level code in the script.
. .\1_script.ps1
# Outputs to the display.
# Append
# | Set-Content -Encoding utf8 2_script.ps1
# to save back to the input file.
(Get-Content -Raw 2_script.ps1) -replace '(?m)(?<=^\s*\$global:scriptVersion\s*=\s*")[^"]+', $global:scriptVersion
For an explanation of the regex and the ability to experiment with it, see this regex101.com page.
[1] Syntactic elements are reported in terms of line and column position, and columns are character-based, meaning that spaces and tabs are treated the same, so that a difference of, say, 3 character positions can represent 3 spaces, 3 tabs, or any mix of it - the parser won't tell you. However, if your approach allows keeping the source code as a whole while only removing and splicing in certain elements, that won't be a problem, as shown in iRon's helpful answer.
To compliment the helpful answer from #mklement0. In case your do go for the PowerShell abstract syntax tree (AST) class, you might use the Extent.StartOffset/Extent.EndOffset properties to reconstruct your script:
Using NameSpace System.Management.Automation.Language
$global:scriptVersion = 'v1.1' # . .\Script1.ps1
$Script2 = { # = Get-Content -Raw .\Script2.ps1
[CmdletBinding()]param()
begin {
$global:scriptVersion = "v1.0"
}
process {
$_
}
end {}
}.ToString()
$Ast = [Parser]::ParseInput($Script2, [ref]$null, [ref]$null)
$Extent = $Ast.Find(
{
$args[0] -is [AssignmentStatementAst] -and
$args[0].Left.VariablePath.UserPath -eq 'global:scriptVersion' -and
$args[0].Operator -eq 'Equals'
}, $true
).Right.Extent
-Join (
$Script2.SubString(0, $Extent.StartOffset),
$global:scriptVersion,
$Script2.SubString($Extent.EndOffset)
) # |Set-Content .\Script2.ps1

Getting files in a directory that has over 7 million items using powershell

This is currently what I am trying to execute.
$folderPath = 'M:\abc\WORKFORCE\Media\Attachments'
Write-Host "Executing Script..."
foreach ($file in Get-ChildItem $folderPath -file)
{
# execute code
}
However when I execute the powershell script it freezes on me. It's been this way for an hour now. I'm assuming it might be because the directory has over 8 million items in it. Is there a more efficient way to move these items? Is waiting my only option? Or is it not possible to do this at all with powershell because of how large the directory is?
When you do not need any information except file name, you should use [System.IO.Directory]::EnumerateFiles($folderPath, '*')
EnumerateFiles returns IEnumerable[String].
IEnumerable is a special type that can be used in foreach statements. It does not loads information into memory, but instead it gets next item only when requested. It works almost immediately.
So, your code will be
$filesIEnumerable = [System.IO.Directory]::EnumerateFiles($folderPath,'*')
foreach ($fullName in $filesIEnumerable) {
# code here
$fileName = [System.IO.Path]::GetFileName($fullName)
# more code here
}
In case you want to keep in-memory all list of files instead of iterating once ( for example you need to iterate several times ), EnumerateFiles is still a faster and requires less memory than Get-ChildItem because it does not get any extended file attributes:
$files = #([System.IO.Directory]::EnumerateFiles($folderPath,'*'))
Look further about EnumerateFiles at learn.microsoft.com
Without further explanation of what the end-goal of the script is; there can not really be a solution to this question.
However, a tip on performance, can be given.
Original script:
$folderPath = 'M:\abc\WORKFORCE\Media\Attachments'
Write-Host "Executing Script..."
foreach ($file in Get-ChildItem $folderPath -file)
{
# execute code
}
Suggested approach:
$files = Get-ChildItem 'M:\abc\WORKFORCE\Media\Attachments' -file
$DestinationPath = 'F:\DestinationFolder'
Write-Host "Executing Script..."
$Files | ForEach-Object {
# execute code
# Write-Verbose "Moving $_.Name"
# Move-Item -Destination $DestinationPath
}
That being said, it looks like filimonic's take on an answer has a superior speed to its execution, than my suggestion.
( To expand on that, check this thread)

Sensitive word search with powershell

I am somewhat new to PowerShell so any help I would appreciate.
I am trying to put a PS script together so that I can search a file for sensitive words before transferring it from one network to another. Like 'Classified' and multiple other words that I can add to a word bank in a text file vice updating the code every time
Right now I am forced to use PS 2 windows 7 and server 2008
Select-String -Path e:\transfer_folder\*.* -pattern Classified,restricted
Then I can get an output for any hits on the list of words so that I can find them. I am trying to speed up my searching through hundreds of pages of documents with what I like to call a dirty word search so I do not put something that should not go on the wrong network.
You've got the right idea. The -Pattern tack in powershell can usually be called on to work with regular expressions. If you've never worked with regular expressions, take a look at this beginner's guide to using regex pattern matching. What you probably want is a set of variables that you can use to dynamically pick out those sensitive keywords.
The short and simple answer is that you want to use a pipe to separate your options for pattern, and pass it in as a string.
Select-String -Path e:\transfer_folder\*.* -pattern "Classified|Restricted"
Also, you might want to think about doing this at the file level rather than just importing all of your stuff in willynilly like that. I would go for something like:
$files = #(Get-ChildItem -Path E:\transfer_folder\ -Filter "*.txt|*.etc").FullName
(The # symbol means that you get your output as an array. The .FullName means that you're only selecting the FullName field from the object that's being produced by the command.)
Then you can process each file individually, like:
Foreach ($file in $files) {
Write-Host "Processing $file"
echo (Select-String -Path $file -Pattern $pattern)
}
One of the reasons that I love powershell is how comparatively easy it is to perform these types of matching operations. If you dig into Regex, you'll notice that you can represent "OR" as "|". So you have two options to do this logically:
Just hard write it out
$pattern = "Classified|Forbidden|Death|Danger"
Do it dynamically
Scripting is all about not having to do things more than once, right? So you'll probably want to encapsulate this in a function or something. Or maybe you want to get your words from a text file? You can be like:
(might take some tweaking)
function Get-ForbiddenWords ([string[]]$words, [string]$folder) {
ForEach ($word in $words) {
$pattern += "$word|"
}
#remove trailing pipe
$pattern -replace ".$"
$files = #(Get-ChildItem -Path $folder -Filter "*.txt|*.etc").FullName
Foreach ($file in $files) {
Write-Host "Processing $file"
echo (Select-String -Path $file -Pattern $pattern)
}
}
Now you can put this in your powershell profile and invoke it with
Get-ForbiddenWords -words secret dangerous whatever -folder E:\transfer_folder\

How would I specify a directory to run a PowerShell script that would edit file extensions?

I am new to PowerShell and new to IT. I've been asked by my boss to write a PowerShell script that will identify filenames that have no file extension and then change them to .PDF files. After doing some research online I've found a script that had a similar purpose and tried to tailor it to my needs:
$proj_files = Get-ChildItem | Where-Object {$_.Extension -eq "."}
ForEach ($file in $proj_files) {
$filenew = $file.Name + ".pdf"
Rename-Item $file $filenew
}
My first question is does the logic in this script make sense? Is "Extension -eq "." the correct syntax to specify a filename with no extension? My other thought was to use Extension -eq "null" or something similar. If I do need to use a null value, what would that look like? My other question is how would I specify a given directory for this script to search through, or would I even need to? My thought here would be to specify the path under Get-ChildItem, like so: $proj_files = Get-ChildItem -Path C:\Users\mthomas\Documents | Where-Object {$_.Extension -eq ".'} Does that seem correct? I am hesitant to test this out before getting a second opinion because I don't want to change every file extension on my computer or something stupid like that. Anyhow, thanks everyone for the help.
You can do something like the following to find files in a directory without an extension, and rename them to have a PDF extension:
$directory = "C:\Path\To\Directory"
Get-ChildItem -File $directory | Where-Object { -Not $_.Extension } | Foreach-Object {
$_ | Rename-Item -NewName "$($_.Name).pdf"
}
Let's break this down
$directory = "C:\Path\To\Directory"
This is where we set the directory we want to locate files without extensions in. It doesn't have to be set as a static variable but since you are just getting your feet wet with Powershell this keeps it simple.
Get-ChildItem -File $directory
Get-ChildItem is the cmdlet which is used to list directory contents (also aliased to gci, ls, and dir). -File tells it to only list files, and $directory references the directory we want to search from, which we set above. Note that Get-ChildItem might behave differently depending on the provider (for example, you can also use Get-ChildItem on a registry key), but if you are working with a filesystem path you do not need to worry about additional providers for this case.
|
Passes the previous output down the pipeline. This is a common operator in Powershell, but basically you can string commands together using it. You can read more about the pipeline at https://learn.microsoft.com/en-us/powershell/scripting/getting-started/fundamental/understanding-the-windows-powershell-pipeline?view=powershell-6
Where-Object { -Not $_.Extension }
Where-Object evaluates a condition on one or more items, and filters out items that do not meet the condition. Since Get-ChildItem can return one or more files, we use the -Not operator in the ScriptBlock (denoted by {} and make sure that there is no extension on the file. $_, or $PSItem, is a special variable used by the pipeline, in this case $_ equals each item returned by Get-ChildItem. The Extension property exists on files returned by Get-ChildItem, and will be blank, or evaluated as $False. So filtering on -Not $_.Extension is the same as saying to only match objects that are missing a file extension. Where-Object can be read about in more detail here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/where-object?view=powershell-6
Foreach-Object { SCRIPTBLOCK }
Similar to Where-Object, but runs code for each object in the pipeline rather than evaluating and filtering out objects which don't match a condition. In this case, we pipe the each file without an extension to Rename-Item, which I'll break down further below. More information on Foreach-Object can be read about here: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/foreach-object?view=powershell-6
$_ | Rename-Item -NewName "$($_.Name).pdf"
Rename the current file in the Foreach-Object block to the new name with .pdf appended. The "$( ... )" is called a sub-expression, which is a string interpolation technique that lets you run a command within a string, and make its output part of the string. You could achieve the same effect by doing $_ | Rename-Item -NewName ( $_.Name + ".pdf" ) which just adds a .pdf to the end of the current name.
Summary
The pipeline is a very powerful tool in Powershell, and is key to writing efficient and less bloated scripts. It might seem complex at first, but the more you use it the less daunting it will seem. I highly suggest reading the additional documentation I linked to above as it should help fill in any gaps I may have missed in my explanations above.
To simplify the breakdown above, the command does this, in this order: Gets all files in the specified directory, selects only the files that do not have an extension, then renames each file found without an extension to have a .pdf at the end.
The logic in the script - the overall shape - makes understandable sense, but is not right for it to work as you intend.
Testing on my computer here:
new-item -ItemType File -Name 'test'
get-item test | format-list *
get-item test | foreach { $_.extension; $_.Extension.length; $_.extension.GetType().name }
a file with no extension shows up with an empty string (blank content, length 0, type String, so your where-object { $_.Extension -eq "." } needs to be looking for "" instead of ".".
But:
Get-ChildItem | Where-Object { $_.Extension -eq '' }
shows me some folders as well, because they also have no extension in their name, so you might want Get-ChildItem -File to restrict it to just files.
how would I specify a given directory for this script to search through, or would I even need to?
It would run in the current directory, whichever shows up in your prompt PS C:\wherever> so if you need it to run somewhere else, yes you'd need to change to that folder or specify in get-childitem -LiteralPath 'c:\path\to\wherever'. You haven't mentioned subfolders, if you need those included, get-childitem -Recurse switch as well.
Speaking of subfolders, your $filenew = $file.Name + ".pdf" only makes sense in the current directory, I think it would work better if you used the full filename including path, so they definitely get renamed in the same place they were found $filenew = $file.FullName + ".pdf"
Is "Extension -eq "." the correct syntax to specify a filename with no extension?
Being careful here, what you wrote in your question was correct syntax but incorrect string content. What you've written here with quotes on the left of Extension is incorrect syntax.
My other thought was to use Extension -eq "null" or something similar. If I do need to use a null value, what would that look like?
And being careful here, "null" is not a null value, it's a string containing the four letter word 'null'.
You don't need to use a null value here, normally if you do it looks like $null, but in this case you could use where-object { [string]::IsNullOrEmpty($_.Extension) } but there's no benefit to it, I think.
And, as a stylistic choice, both "" and '' are strings, but "" can contain variables and sub-expressions, so if you have plain text it's a neat habit to use '' for it because it makes it clear to the reader that you intend there to be nothing special happening in this string.
Then your code, with parameter names given, looks more like:
$proj_files = Get-ChildItem -LiteralPath 'C:\Users\mthomas\Documents' |
Where-Object {$_.Extension -eq '.'}
foreach ($file in $proj_files)
{
$filenew = $file.FullName + '.pdf'
Rename-Item -LiteralPath $file.FullName -NewName $filenew
}
If you want to see what it will do, use -WhatIf on the end of Rename-Item:
Rename-Item -LiteralPath $file.FullName -NewName $filenew -WhatIf
Then it won't make the changes, just tell you what it would do.
I am hesitant to test this out before getting a second opinion because I don't want to change every file extension on my computer or something stupid like that
Sensible. But internet people are going to tell you to test their code before running it, because ultimately it's your responsibility to safeguard your files, rather than trust random code from the internet, so having test folders, having a spare machine, having a good backup, playing with PowerShell in pieces until you are happy with what they do, they're all good habits to get into as well.

Replace text between quotes in XML

I'm trying to find a way to change unknown text (could be anything) in an XML file for a printer migration.
The text that is in question is
PrintProcessor="hpcpp111"
The Print Processor section could contain anything as this varies depending on printer model, type and driver. I would prefer to use powershell if possible as I am trying to understand scripting and how it all works, but I find it a little confusing. I may need to manually edit thousands of these files as we are migrating 5,500 printers.
I have found some code that gets it close for example
function Reset-InfoPathTemplateLink {
Param(
[string]$FilePath,
[string]$FileExtension,
[string]$OldPath,
[string]$NewPath
)
$files = Get-ChildItem $FilePath -Filter $FileExtension
foreach ($file in $files) {
(Get-Content $file.fullname) |
ForEach-Object {$_ -replace $OldPath,$NewPath} |
Set-Content $file.fullname
} #end foreach
} #end function
and if I dot-source it and then run this command
Reset-InfoPathTemplateLink -FilePath "c:\test2" -FileExtension ".xml" -OldPath "PrintProcessor=""" -NewPath "PrintProcessor='"WinPrint"'"
The WinPrint gets added to the beginning of the current print processor, which isnt ideal. If anyone has tips on how to get this to work, or if they can suggest a better method of doing this I would appreciate it
this is how you can use PowerShell to replace a pattern within a text file:
$path="c:\myPath.txt"
(Get-Content $path) -replace '(PrintProcessor=")(.*)(")','$1WinPrint$3' | Set-Content $path