Return from function - powershell

I wish to return from the following powershell function if I find a match (for a more complete code sample see my codereview question):
Function Find-Property($fileName, $PropertyName)
{
$shellfolder = Create-ShellFolder $fileName
0..287 | Foreach-Object {
if($PropertyName -eq $shellfolder.GetDetailsOf($null, $_)){ return $_ }
}
}
This code just appears to return from the scope of the if conditional, which is not so useful.
How can I do this? Do I need a labeled break somewhere?

If you wish to use the return statement to exit the function you can use the foreach keyword instead of the ForEach-Object cmdlet. Here's a demo:
function Foo {
foreach ($number in (0..287)) {
$number # Just show our current iteration.
if ($number -eq 50) {
return $number
}
}
}

No need for a label.
function Find-Property($Filename, $PropertyName)
{
$shellfolder = Create-ShellFolder $fileName
0..287 | Where {$PropertyName -eq $shellfolder.GetDetailsOf($null, $_)} |
Foreach {$_;break}
}
Another option is to minorly tweak your original function:
function Find-Property($fileName, $PropertyName)
{
$shellfolder = Create-ShellFolder $fileName
0..287 | Foreach-Object {
if($PropertyName -eq $shellfolder.GetDetailsOf($null, $_)) {$_; break}
}
}

Related

Powershell - Exchange JSON output without needing to write to a file

EDIT: Added Setupconfigfiles.ps1
I'm a bit new to detailed scripting so please bear with me.
I have two Powershell scripts
Setupconfigfiles.ps1 generates JSON output to be fed to an API.
Script2 uses that JSON data to execute API commands.
Script 2 can call setupconfigfiles.ps1 as indicated below and use the output data.
.\SetupConfigFiles.ps1 -type $Type -outfile .\Templist.json
$servers = Get-Content -Raw -Path .\templist.json | ConvertFrom-Json
setupconfigfiles.ps1:
param (
# If this parameter is set, format the output as csv.
# If this parameter is not set, just return the output so that the calling program can use the info
[string]$outfile,
# this parameter can be 'production', 'development' or 'all'
[string]$type
)
enum MachineTypes {
production = 1
development = 2
all = 3
}
$Servers = Get-ADObject -Filter 'ObjectClass -eq "computer"' -SearchBase 'Obfuscated DSN' | Select-Object Name
$output = #()
$count = 0
# Set this to [MachineTypes]::production or [MachineTypes]::development or [MachineTypes]::all
if ($type -eq "all") {
$server_types = [MachineTypes]::all
}
ElseIf ($type -eq "production") {
$server_types = [MachineTypes]::production
}
else {
$server_types = [MachineTypes]::development
}
ForEach ($Server in $Servers)
{
$count = $count + 1
$this_server = #{}
$this_server.hostname = $Server.Name
$this_server.id = $count
$this_server."site code" = $this_server.hostname.substring(1,3)
$this_server."location code" = $this_server.hostname.substring(4,2)
if ($this_server.hostname.substring(7,1) -eq "P") {
$this_server.environment = "Production"
}
ElseIf ($this_server.hostname.substring(7,1) -eq "D") {
$this_server.environment = "Development"
}
Else {
$this_server.environment = "Unknown"
}
if (($server_types -eq [MachineTypes]::production ) -and ($this_server.environment -eq "Production")) {
$output += $this_server
}
ElseIf (($server_types -eq [MachineTypes]::development ) -and ($this_server.environment -eq "Development")) {
$output += $this_server
}
Else {
if ($server_types -eq [MachineTypes]::all ) {
$output += $this_server
}
}
}
if ($outfile -eq "")
{
ConvertTo-Json $output
}
else {
ConvertTo-Json $output | Out-File $outfile
}
How can I do it without needing to write to the Templist.json file?
I've called this many different ways. The one I thought would work is .\SetupConfigFiles.ps1 $servers
Y'all are great. #Zett42 pointed me in a direction and #Mathias rounded it out.
The solution was to change:
"ConvertTo-Json $output" to "Write-Output $output"
Then it's handled in the calling script.
thanks!

Powershell scripting - replace text in files

Given these powershell functions below. I'm trying to replace the version text with another version that I specify. All my paths are correct, and I output both the current version and new version appropriatlely. When i go to replace them, the file indicates that it has changed, but no text was changed.
Alternately, I've found if i specify the exact current version as a string variable and bypass the Get-VersionXXX calls, it replaces the version without issues. Is there anything obvious I'm doing wrong?
function Get-VersionAndDateFromFile([string] $versionFile, [string] $versionString)
{
if (-Not(Test-Path -path $versionFile))
{
return ""
}
else
{
#foreach($line in [System.IO.File]::ReadLines($versionFile))
(Get-Content -Path $versionFile -Raw) | ForEach-Object {
$line = $_
if ($line.Contains($versionString))
{
if ($line.Contains(""""))
{
$tokens = $line.Split('\"')
return $tokens[1]
}
}
}
}
return ""
}
function Get-VersionOnlyFromFile([string] $versionFile, [string] $versionString)
{
[string] $versionAndDate = Get-VersionAndDateFromFile $versionFile $versionStriong
return VersionFromVersionAndDateString $versionAndDate
}
function Get-VersionFromVersionAndDateString([string] $versionAndDateString)
{
if (!$versionAndDateString)
{
return ""
}
else
{
if ($versionAndDateString.Contains(" "))
{
$newTokens = $versionAndDateString.Trim().Split(" ")
return $newTokens[0]
}
else
{
return $versionAndDateString
}
}
}
function ReplaceTextInFile([string] $fullPath, [string] $oldString, [string] $newString)
{
Write-Host "Old " $oldString
Write-Host "New " $newString
((Get-Content -path $fullPath -Raw) -replace $oldString,$newString) | Set-Content -Path $fullPath
}
Calling code:
[string] $newVersionString = "1.2.3.4.6 09-16-2021"
[string] $currentVersionString = Get-VersionAndDateFromFile $pathToVersionFile "SW_VERSION"
ReplaceTextInFile -fullPath $pathToVersionFile -oldString $currentVersionString -newString $newVersionString
finally: file is a header file called Version.h
#define SW_VERSION "1.2.3.4.5 09-14-2021"
Unless explicitly redirected all output in a function is returned to the pipeline. The return command is redundant. As such, you're sending back two things in your function, first is the version number and second is an empty string (lines starting with >>>):
function Get-VersionAndDateFromFile([string] $versionFile, [string] $versionString)
{
if (-Not(Test-Path -path $versionFile))
{
return ""
}
else
{
#foreach($line in [System.IO.File]::ReadLines($versionFile))
(Get-Content -Path $versionFile -Raw) | ForEach-Object {
$line = $_
if ($line.Contains($versionString))
{
if ($line.Contains(""""))
{
$tokens = $line.Split('\"')
>>> return $tokens[1]
}
}
}
}
>>> return ""
}
You convert the output to a string, and by all accounts it looks right, but if you look more closely you'll see there's a space after the date:
PS C:\WINDOWS\system32> ">$currentVersionString<"
>1.2.3.4.5 09-14-2021 <
If we comment out the last return in that function the space goes away, and your replace should work fine.
function Get-VersionAndDateFromFile([string] $versionFile, [string] $versionString)
{
if (-Not(Test-Path -path $versionFile))
{
return ""
}
else
{
#foreach($line in [System.IO.File]::ReadLines($versionFile))
(Get-Content -Path $versionFile -Raw) | ForEach-Object {
$line = $_
if ($line.Contains($versionString))
{
if ($line.Contains(""""))
{
$tokens = $line.Split('\"')
return $tokens[1]
}
}
}
}
# return ""
}
To be honest, it would be much simpler to do this:
function Get-VersionAndDateFromFile([string] $versionFile, [string] $versionString)
{
if (Test-Path -path $versionFile)
{
Get-Content -Path $versionFile -Raw | Where-Object{$_ -match "$versionString\s+""(.+?)"""} | ForEach-Object {
$Matches[1]
}
}
}
Run your Replace Function with these 2 added lines, as shown below...
$old = $oldString -replace '\s','\s';
$old = $old -replace '\s$';
You'll find you're capturing an extra space at the end of your OldString variable. Like so: 1.2.3.4.5\s\s09-16-2021\s
The two lines fix the issue and it does the replace... or you can fix your Get-Version function to capture the correct string.
function ReplaceTextInFile([string] $fullPath, [string] $oldString, [string] $newString)
{
Write-Host "Old " $oldString
Write-Host "New " $newString
$old = $oldString -replace '\s','\s'; #replaces space with \s
$old = $old -replace '\\s$'; #removes trailing space at end of line
$old = $oldString -replace '\s','\s'; $old = $old -replace '\\s$'
((Get-Content -path $fullPath -Raw) -replace $old,$newString) | Set-Content -Path $fullPath
}

Powershell Return only TRUE if All Values are the same

I have the script below to read registry values from a certain key(taking no credit for it). My end goal is to only return TRUE if all the values in the array Match. However I'm not quite getting it as
Example Registry Entry
$array = #()
$regval = Get-Item -Path HKLM:\SOFTWARE\Runner\Event
$regval.GetValueNames() |
ForEach-Object {
$name = $_
$rv.Value
$array += New-Object psobject -Property #{'Value' = $rv.Value }
}
$Matchvalue = 'A'
Foreach ($v in $array){
if ($v -match $Matchvalue){
$true
}
}
Update: I've just tried again and it appears my array is empty. So any tips welcome for me.
How about this:
$regkey = Get-Item HKLM:\SOFTWARE\Runner\Event
$matchPattern = 'A'
$values = $regkey.GetValueNames()
$matchingValues = $values | Where { $regkey.GetValue($_) -match $matchPattern }
# this is going to be true or false
$values.Count -eq $matchingValues.Count
Note that by default, Powershell is case-insensitive. So $matchPattern = 'A' and $matchPattern = 'a' will behave the same.
Here's my attempt to do something like Haskell's "all".
function foldr ($sb, $accum, $list) {
if ($list.count -eq 0) { $accum }
else { & $sb $list[0] (foldr $sb $accum $list[1..$list.length]) }
}
function and ($list) {
foldr { $args[0] -and $args[1] } $true $list
}
function all ($list, $sb) {
and ( $list | foreach $sb )
}
all 1,1,1 { $_ -eq 1 }
True
all 1,2,1 { $_ -eq 1 }
False

Writing an output on a .txt file on Powershell

I found a little script to get all the local groups and members and it's working perfectly but I need to write the output on PowerShell.
Trap {"Error: $_"; Break;}
function EnumLocalGroup($LocalGroup) {
$Group = [ADSI]"WinNT://$strComputer/$LocalGroup,group"
"`r`n" + "Group: $LocalGroup"
$Members = #($Group.psbase.Invoke("Members"))
foreach ($Member In $Members) {
$Name = $Member.GetType().InvokeMember("Name", 'GetProperty', $Null, $Member, $Null)
$Name
}
}
$strComputer = gc env:computername
"Computer: $strComputer"
$computer = [adsi]"WinNT://$strComputer"
$objCount = ($computer.PSBase.Children | Measure-Object).Count
$i = 0
foreach ($adsiObj in $computer.PSBase.Children) {
switch -regex ($adsiObj.PSBase.SchemaClassName) {
"group" {
$group = $adsiObj.Name
EnumLocalGroup $group
}
}
$i++
}
I already tried this:
function EnumLocalGroup($LocalGroup) | Out-File -FilePath "E:\PS\Malik\group.txt"
But the code won't start if I do that. I also tried to use this whole Out-File line at the end of the code after the } but doesn't work either and this is the only solution I find on Internet.
If you want to incorporate logging into a function you need to put it into the function body, e.g.
function EnumLocalGroup($LocalGroup) {
....
$foo = 'something'
$foo # output returned by function
$foo | Add-Content 'log.txt' # output to log file
...
}
or
function EnumLocalGroup($LocalGroup) {
...
$foo = 'something'
$foo | Tee-Object 'log.txt' -Append # output goes to log file and StdOut
...
}
Otherwise you have to do the logging when you call the function:
EnumLocalGroup $group | Add-Content 'C:\log.txt'

How can I get out of the loop once and for all?

I have this file:
function t {
"abcd" -split "" |%{ if ($_ -eq "b") { return; } write-host $_; }
}
$o = #{}
$o |add-member -name t -membertype scriptmethod -value {
"abcd" -split "" |%{ if ($_ -eq "b") { return; } write-host $_; }
}
write-host '-function-'
t;
write-host '-method-'
$o.t();
and if I run it I'll get:
-function-
a
c
d
-method-
a
c
d
as expected. but if what I wanted was 'a', I could replace the return with break. if I do so in the function what I get is:
-function-
a
so the method never even gets called. if I replace it in the method, it won't compile (and complains about calling it). what is going on here?
and what's the proper way to exit the foreach-object loop once and for all?
The simplest solution is to rewrite it as a foreach statement, instead of using the ForEach-Object command, which is particularly hard to stop:
$o | add-member -name t -membertype scriptmethod -value {
foreach($e in "abcd" -split "") {
if ($_ -eq "b") { break }
write-host $_;
}
}
As a side note, the foreach(){} keyword/construct runs a lot faster than piping through ForEach-Object.
For the sake of completeness ...
To stop a pipeline, throw PipelineStoppedException
"abcd" -split "" | % {
if($_ -eq "b") {
throw [System.Management.Automation.PipelineStoppedException]::new()
}
$_
}
You don't use break to stop the ForEach, you use return since it's executed as a lambda on each member of the object anyway.