It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}
Related
Is it possible to create an anonymous Recursive Function in PowerShell? (if yes, how?)
I have a recursive object and using a recursive function to drill down through the properties, like:
$Object = ConvertFrom-Json '
{
"Name" : "Level1",
"Folder" : {
"Name" : "Level2",
"Folder" : {
Name : "Level3"
}
}
}'
Function GetPath($Object) {
$Object.Name
if ($Object.Folder) { GetPath $Object.Folder }
}
(GetPath($Object)) -Join '\'
Level1\Level2\Level3
The function is relative small and only required ones, therefore I would like to directly invoke it as an anonymous function, some like:
(&{
$Object.Name
if ($Object.Folder) { ???? $Object.Folder }
}) -Join '\'
Is this possible in PowerShell?
If yes, how can I (as clean as possible) refer to the current function at ?????
Unfortunately there is not much documentation on this topic but you could execute the anonymous script block via $MyInvocation automatic variable, specifically it's ScriptInfo.ScriptBlock Property.
A simple example:
& {
param([int] $i)
if($i -eq 10) { return $i }
($i++)
& $MyInvocation.MyCommand.ScriptBlock $i
}
# Results in 0..10
Using your current code and Json provided in question:
(& {
param($s)
$s.Name; if ($s.Folder) { & $MyInvocation.MyCommand.ScriptBlock $s.Folder }
} $Object) -join '\'
# Results in Level1\Level2\Level3
Same as the above but using pipeline processing instead:
($Object | & {
process {
$_.Name; if($_.Folder) { $_.Folder | & $MyInvocation.MyCommand.ScriptBlock }
}
}) -join '\'
A bit more code but the same can be accomplished using a Collections.Queue instead of recursion, which is likely to be more resource efficient:
$(
$queue = [System.Collections.Queue]::new()
$queue.Enqueue($object)
while($queue.Count) {
$node = $queue.Dequeue()
$node.Name
if($node.Folder) { $queue.Enqueue($node.Folder) }
}
) -join '\'
#Santiago's helpful answer was exactly where I was initially looking for.
Nevertheless, it doesn't always require a recursive function to crawl through a recursive object.
As in the mcve, I could just have done:
#(
do {
$Object.Name
$Object = $Object.Folder
} while ($Object)
) -Join '\'
$global:af_fp = "C:\Path\to\folder\"
Function function-name {
do things …
$global:af_fp = $global:af_fp + $variableFromDo_things + "_AF.csv"
}
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
Above is the generalized (and abbreviated) script contents for a powershell script.
Every time I run the script in this way, I get the following error:
Add-Content : Could not find a part of the path 'C:\Users\timeuser\Documents\'.
At C:\Users\timeuser\Documents\get_software.ps1:231 char:51
+ ... ware | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\timeuser\Documents\:String) [Add-Content], DirectoryNotFoundException
+ FullyQualifiedErrorId : GetContentWriterDirectoryNotFoundError,Microsoft.PowerShell.Commands.AddContentCommand
When I run
Get-Variable -Scope global
after running the script and seeing the error, the variable af_fp contains exactly the information I am seeking for the file name, however, the error shows the variable contents ending in ':String'.
To confuse me even more, if I comment out the lines containing '$global:...' and re-run the same script, IT ACTUALL RUNS AND SAVES THE FILE USING THE LINE
function-name | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
AS INTENDED. Of course, I had to run the script and watch it error first, then re-run the script with the global variable declaration and update commented out for it to actually work. I want to run the script ONCE and still get the same results.
FYI, I am a complete noob to powershell, but very familiar with the concept of variable scope.....but why is this global not working when initially created and updated, but then work the second time around, when, as far as I can tell, the CONTENT AND SCOPE of the global remains the same...…. any assistance to finding a solution to this small issue would be greatly appreciated; I have tried sooooo may different methods from inquiries through here and on Google...…..
EDIT: not sure why this will matter, because the script ran before as intended when I explicitly typed the parameter for -Path as 'C:\path\to\file'. The ONLY CHANGES MADE to the original, working script (below) were my inclusion of the global variable declaration, the update to the contents of the global variable (near the end of the function), and the attempt to use the global variable as the parameter to -Path, that is why I omitted the script:
'''
$global:af_fp = "C:\Users\timeuser\Documents\"
Function Get-Software {
[OutputType('System.Software.Inventory')]
[Cmdletbinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[String[]]$Computername = $env:COMPUTERNAME
)
Begin {
}
Process {
ForEach ($Computer in $Computername) {
If (Test-Connection -ComputerName $Computer -Count 1 -Quiet) {
$Paths = #("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "SOFTWARE\\Wow6432node\\Microsoft\\Windows\\CurrentVersion\\Uninstall")
ForEach ($Path in $Paths) {
Write-Verbose "Checking Path: $Path"
# Create an instance of the Registry Object and open the HKLM base key
Try {
$reg = [microsoft.win32.registrykey]::OpenRemoteBaseKey('LocalMachine', $Computer, 'Registry64')
}
Catch {
Write-Error $_
Continue
}
# Drill down into the Uninstall key using the OpenSubKey Method
Try {
$regkey = $reg.OpenSubKey($Path)
# Retrieve an array of string that contain all the subkey names
$subkeys = $regkey.GetSubKeyNames()
# Open each Subkey and use GetValue Method to return the required values for each
ForEach ($key in $subkeys) {
Write-Verbose "Key: $Key"
$thisKey = $Path + "\\" + $key
Try {
$thisSubKey = $reg.OpenSubKey($thisKey)
# Prevent Objects with empty DisplayName
$DisplayName = $thisSubKey.getValue("DisplayName")
If ($DisplayName -AND $DisplayName -notmatch '^Update for|rollup|^Security Update|^Service Pack|^HotFix') {
$Date = $thisSubKey.GetValue('InstallDate')
If ($Date) {
Try {
$Date = [datetime]::ParseExact($Date, 'yyyyMMdd', $Null)
}
Catch {
Write-Warning "$($Computer): $_ <$($Date)>"
$Date = $Null
}
}
# Create New Object with empty Properties
$Publisher = Try {
$thisSubKey.GetValue('Publisher').Trim()
}
Catch {
$thisSubKey.GetValue('Publisher')
}
$Version = Try {
#Some weirdness with trailing [char]0 on some strings
$thisSubKey.GetValue('DisplayVersion').TrimEnd(([char[]](32, 0)))
}
Catch {
$thisSubKey.GetValue('DisplayVersion')
}
$UninstallString = Try {
$thisSubKey.GetValue('UninstallString').Trim()
}
Catch {
$thisSubKey.GetValue('UninstallString')
}
$InstallLocation = Try {
$thisSubKey.GetValue('InstallLocation').Trim()
}
Catch {
$thisSubKey.GetValue('InstallLocation')
}
$InstallSource = Try {
$thisSubKey.GetValue('InstallSource').Trim()
}
Catch {
$thisSubKey.GetValue('InstallSource')
}
$HelpLink = Try {
$thisSubKey.GetValue('HelpLink').Trim()
}
Catch {
$thisSubKey.GetValue('HelpLink')
}
$Object = [pscustomobject]#{
#Potential Candidate for AssetID in the TIME system
AssetID = $Computer
#String that contains word or word combinations for the product field of CPE WFN; may also contain the valid values necessary for update, edition, language, sw_edition, target_hw/sw fields as well.
cpeprodinfo = $DisplayName
cpeversion = $Version
InstallDate = $Date
cpevendor = $Publisher
UninstallString = $UninstallString
InstallLocation = $InstallLocation
InstallSource = $InstallSource
HelpLink = $thisSubKey.GetValue('HelpLink')
EstimatedSizeMB = [decimal]([math]::Round(($thisSubKey.GetValue('EstimatedSize') * 1024) / 1MB, 2))
}
$Object.pstypenames.insert(0, 'System.Software.Inventory')
Write-Output $Object
}
}
Catch {
Write-Warning "$Key : $_"
}
}
}
Catch { }
$reg.Close()
}
}
Else {
Write-Error "$($Computer): unable to reach remote system!"
}
$global:af_fp = $global:af_fp + $Computer + "_AF.csv"
}
}
}
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $($af_fp)
'''
IGNORE FORMATTING PLEASE- HAD TROUBLE MAKING INDENTS CORRECTLY FROM COPY-PASTE AND RESTRICTIONS ON SITE FOR CODE BLOCKS.....
NOTE: the ONLY changes I made, that I am asking about, are the global declaration, the global variable update in the function, and the attempt to use the global variable for the -Path parameter....script otherwise runs and will even run WITH THE LAST LINE AS IS if I ran it and errored the first time.....not sure how the addition script will help in any way, shape, or form!
With a little effort, Nasir's solution worked! HOWEVER, I ran across a sample file that had a way of adding to a parameter that inspired me to make a change to my ORIGINAL, that also worked: remove global variable from script entirely and add this code the very end:
$file_suffix = '_AF.csv'
Get-Software | ConvertTo-CSV -NoTypeInformation | Add-Content -Path $env:COMPUTERNAME$file_suffix
In this way, I was able to accomplish exactly what I was setting out to do! Thanks Nasir for your response as well! I was able to also make that work as intended!
Global variables are generally frowned upon, since they often lead to poor scripts, with hard to debug issues.
It seems like your function returns some stuff, which you need to write to a file, the name of which is also generated by the same function. You can try something like this:
function function-name {
param($PathPrefix)
#do things
[pscustomobject]#{"DoThings_data" = $somevariablefromDoThings; "Filename" = "$($PathPrefix)$($variableFromDo_Things)_AF.csv"}
}
function-name -PathPrefix "C:\Path\to\folder\" | Foreach-Object { $_.DoThings_data | Export-Csv -Path $_.Filename -NoTypeInformation }
Or just have your function write the CSV data out and then return the data if you need to further process it outside the function.
Edit: this is just me extrapolating from partial code you have provided. To Lee_Dailey's point, yes, please provide more details.
I have function that updates object in WMI. I want user to be able to specify in parameters only values that he wants to update. How can I do it?
function UpdateObject ([bool] $b1, [bool] $b2, [int] $n1, [string] $s1)
{
$myObject = GetObjectFromWmi #(...)
#(...)
#This is bad. As it overrides all the properties.
$myObject.b1 = $b1
$myObject.b2 = $b2
$myObject.n1 = $n1
$myObject.s1 = $s1
#This is what I was thinking but don't kwow how to do
if(IsSet($b1)) { $myObject.b1 = $b1 }
if(IsSet($b2)) { $myObject.b2 = $b2 }
if(IsSet($n1)) { $myObject.n1 = $n1 }
if(IsSet($s1)) { $myObject.s1 = $s1 }
#(...) Store myObject in WMI.
}
I tried passing $null as as parameter but it get's automaticly converted to $false for bool, 0 for int and empty string for string
What are your suggestions?
Check $PSBoundParameters to see if it contains a key with the name of your parameter:
if($PSBoundParameters.ContainsKey('b1')) { $myObject.b1 = $b1 }
if($PSBoundParameters.ContainsKey('b2')) { $myObject.b2 = $b2 }
if($PSBoundParameters.ContainsKey('n1')) { $myObject.n1 = $n1 }
if($PSBoundParameters.ContainsKey('s1')) { $myObject.s1 = $s1 }
$PSBoundParameters acts like a hashtable, where the keys are the parameter names, and the values are the parameters' values, but it only contains bound parameters, which means parameters that are explicitly passed. It does not contain parameters filled in with a default value (except for those passed with $PSDefaultParameterValues).
Building on briantist's answer, if you know that all the parameters exist as properties on the target object you can simply loop through the $PSBoundParameters hashtable and add them one by one:
foreach($ParameterName in $PSBoundParameters.Keys){
$myObject.$ParameterName = $PSBoundParameters[$ParameterName]
}
If only some of the input parameters are to be passed as property values, you can still specify the list just once, with:
$PropertyNames = 'b1','b2','n1','s1'
foreach($ParameterName in $PSBoundParameters.Keys |Where-Object {$PropertyNames -contains $_}){
$myObject.$ParameterName = $PSBoundParameters[$ParameterName]
}
To save yourself having to create a parameter for each property you may want to change, consider using a hashtable or other object to pass this information to your function.
For example:
function UpdateObject ([hashtable]$properties){
$myObject = GetObjectFromWmi
foreach($property in $properties.Keys){
# without checking
$myObject.$property = $properties.$property
# with checking (assuming members of the wmiobject have MemberType Property.
if($property -in (($myObject | Get-Member | Where-Object {$_.MemberType -eq "Property"}).Name)){
Write-Output "Updating $property to $($properties.$property)"
$myObject.$property = $properties.$property
}else{
Write-Output "Property $property not recognised"
}
}
}
UpdateObject -properties {"b1" = $true; "b2" = $false}
If you want a [Boolean] parameter that you want the user to specify explicitly or omit (rather than a [Switch] parameter which can be present or not), you can use [Nullable[Boolean]]. Example:
function Test-Boolean {
param(
[Nullable[Boolean]] $Test
)
if ( $Test -ne $null ) {
if ( $Test ) {
"You specified -Test `$true"
}
else {
"You specified -Test `$false"
}
}
else {
"You did not specify -Test"
}
}
In this sample function the $Test variable will be $null (user did not specify the parameter), $true (user specified -Test $true), or $false (user specified -Test $false). If user specifies -Test without a parameter argument, PowerShell will throw an error.
In other words: This gives you a tri-state [Boolean] parameter (missing, explicitly true, or explicitly false). [Switch] only gives you two states (present or explicitly true, and absent or explicitly false).
I am trying to get the value from the function which contains 2 values, one is pointed from the current method, and another value is null.
What I expect is to print out the $convertAccountEx value from the function, but it gives me nothing. I have tried to give $null value for $convertAccountEx, but nothing changes.
function ConvertTo-Date(){
Param ($accountEx,$convertAccountEx)
if($accountEx.accountExpires -eq 0){
$convertAccountEx = "Never"
}
else{
$convertAccountEx = [DateTime]::FromFileTime($AccountEx.accountExpires)
}
}
$userObjects = $ADSearch.FindAll()
foreach ($user in $userObjects){
$accountEx = $user.Properties.Item("accountExpires")
ConvertTo-Date -accountEx $accountEx.accountExpires -convertAccountEx $convertAccountEx
$convertAccountEx
}
Your code should look like this:
function ConvertTo-Date(){
Param ($accountEx)
if($accountEx.accountExpires -eq 0){
$convertAccountEx = "Never"
}
else{
$convertAccountEx = [DateTime]::FromFileTime($AccountEx.accountExpires)
}
$convertAccountEx
}
$userObjects = $ADSearch.FindAll()
foreach ($user in $userObjects){
$accountEx = $user.Properties.Item("accountExpires")
ConvertTo-Date -accountEx $accountEx.accountExpires
}
The issue you ran into is called scoping. The idea is, that every variable you create, should be exclusive to its running scope. So the var inside your function is - by definition - a different var than outside, even if they share the name. An easy way to go around this in your example is, to just return the value from you function (by calling it).
What I would advise is: Don't just return the value, but append it to the objects you create. Here a (really) simple example:
function Add-Info {
Param( $user )
$user | Add-Member -NotePropertyName NewProperty -NotePropertyValue 'SomeValue'
$user
}
I want to create a powershell script that accepts dynamic parameters and I also want to iterate through them.
eg:
I call the powershell script in the following manner.
ParametersTest.ps1 -param1 value1 -param2 value2 -param3 value3
And I should be able to access my params inside the script as follows:
for($key in DynamicParams) {
$paramValue = DynamicParams[$key];
}
Is there anyway to do this in powershell? Thanks in advance.
There is nothing built-in like that (essentially you're asking for PowerShell parameter parsing in the absence of any definition of those parameters). You can emulate it, though. With $args you can get at all arguments of the function as an array. You can then iterate that and decompose it into names and values:
$DynamicParams = #{}
switch -Regex ($args) {
'^-' {
# Parameter name
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
$name = $_ -replace '^-'
}
'^[^-]' {
# Value
$value = $_
}
}
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
To iterate over dynamic parameters you can either do something like you wrote
foreach ($key in $DynamicParams.Keys) {
$value = $DynamicParams[$key]
}
(note the foreach, not for, the latter of which cannot work like you wrote it) or just iterate normally over the hash table:
$DynamicParams.GetEnumerator() | ForEach-Object {
$name = $_.Key
$value = $_.Value
}