function test{
for($a=0;$a -le 2;$a++){
if($a -eq 1){break}
}
}
#----outside----
write-output $a
How to call a variable from a function and not to use return to get $a ?
Aside from using scopes as described in Rob's answer, there is also the possibility to send a parameter by reference.
This means that inside the function, a reference to the original variable is used, so whatever the function does with it, the original value is affected.
The downside is that when using references, you must use the Value property of the System.Management.Automation.PSReference type to access/alter your data as in the example below:
function test ([ref]$a) {
for ($a.Value = 0; $a.Value -le 2; $a.Value++){
if ($a.Value -eq 1) {break}
}
}
#----outside----
$a = 0
test ([ref]$a) # the parameter needs to be wrapped inside brackets and prefixed by [ref]
Write-Output $a
When sending a parameter that is an object type, like for instance a Hashtable, then by default it is always passed to the function by reference and for those you don't use the [ref] accellerator.
Have you tried to set the variable global?
$var="Test"
function test()
{
$global:var="blub"
}
test
$var
or
$a = 0
function test{
for($global:a=0;$global:a -le 2;$global:a++){
if($global:a -eq 1){break}
}
}
test
write-output $a
Related
So I am trying to start-job from a module I wrote.
Copy-Modules.psm1
function startcopy([string] $ShowToCopy) {
if (-not($ShowToCopy)) { return "No name provided. Doing nothing." }
} else { return "Name Provided $ShowToCopy" }
}
in the main script I am calling it as follows:
$Copyname = "test"
Start-Job -Name "copy1" -InitializationScript { Import-Module -Name .\Copy-Modules.psm1 } -ScriptBlock {startcopy} -ArgumentList $Copyname
However the arguments never seems to go through. No matter how I format or pass the argument with switch or without I always get the result No name provided. Doing nothing.
The simplest solution - assuming you need no other functions from your Copy-Modules.psm1 module - is to pass your function's body as Start-Job's -ScriptBlock argument:
Start-Job -Name "copy1" -ScriptBlock $function:startcopy -ArgumentList $Copyname
$function:startcopy uses namespace variable notation to get the startcopy's body as a script block.
Note:
This obviates the need to define your startcopy function in the scope of the background job (which is an independent session in a child process that knows nothing about the caller's state), which is what your -InitializationScript script block does.
The only limitation of this approach is that the script block won't be named, i.e. the original function name is lost, and $MyInvocation.MyCommand.Name inside the function returns the empty string.
As for what you tried:
It is the script block as a whole that receives the (invariably positional arguments passed to -ArgumentList, which you'll have to pass on explicitly to any commands called inside the script block, using the automatic $args variable:
$Copyname = "test"
# Note the use of $args[0]
Start-Job -Name "copy1" `
-InitializationScript { Import-Module -Name .\Copy-Modules.psm1 } `
-ScriptBlock { startcopy $args[0] } -ArgumentList $Copyname
Say I have JSON like:
{
"a" : {
"b" : 1,
"c" : 2
}
}
Now ConvertTo-Json will happily create PSObjects out of that. I want to access an item I could do $json.a.b and get 1 - nicely nested properties.
Now if I have the string "a.b" the question is how to use that string to access the same item in that structure? Seems like there should be some special syntax I'm missing like & for dynamic function calls because otherwise you have to interpret the string yourself using Get-Member repeatedly I expect.
No, there is no special syntax, but there is a simple workaround, using iex, the built-in alias[1] for the Invoke-Expression cmdlet:
$propertyPath = 'a.b'
# Note the ` (backtick) before $json, to prevent premature expansion.
iex "`$json.$propertyPath" # Same as: $json.a.b
# You can use the same approach for *setting* a property value:
$newValue = 'foo'
iex "`$json.$propertyPath = `$newValue" # Same as: $json.a.b = $newValue
Caveat: Do this only if you fully control or implicitly trust the value of $propertyPath.
Only in rare situation is Invoke-Expression truly needed, and it should generally be avoided, because it can be a security risk.
Note that if the target property contains an instance of a specific collection type and you want to preserve it as-is (which is not common) (e.g., if the property value is a strongly typed array such as [int[]], or an instance of a list type such as [System.Collections.Generic.List`1]), use the following:
# "," constructs an aux., transient array that is enumerated by
# Invoke-Expression and therefore returns the original property value as-is.
iex ", `$json.$propertyPath"
Without the , technique, Invoke-Expression enumerates the elements of a collection-valued property and you'll end up with a regular PowerShell array, which is of type [object[]] - typically, however, this distinction won't matter.
Note: If you were to send the result of the , technique directly through the pipeline, a collection-valued property value would be sent as a single object instead of getting enumerated, as usual. (By contrast, if you save the result in a variable first and the send it through the pipeline, the usual enumeration occurs). While you can force enumeration simply by enclosing the Invoke-Expression call in (...), there is no reason to use the , technique to begin with in this case, given that enumeration invariably entails loss of the information about the type of the collection whose elements are being enumerated.
Read on for packaged solutions.
Note:
The following packaged solutions originally used Invoke-Expression combined with sanitizing the specified property paths in order to prevent inadvertent/malicious injection of commands. However, the solutions now use a different approach, namely splitting the property path into individual property names and iteratively drilling down into the object, as shown in Gyula Kokas's helpful answer. This not only obviates the need for sanitizing, but turns out to be faster than use of Invoke-Expression (the latter is still worth considering for one-off use).
The no-frills, get-only, always-enumerate version of this technique would be the following function:
# Sample call: propByPath $json 'a.b'
function propByPath { param($obj, $propPath) foreach ($prop in $propPath.Split('.')) { $obj = $obj.$prop }; $obj }
What the more elaborate solutions below offer: parameter validation, the ability to also set a property value by path, and - in the case of the propByPath function - the option to prevent enumeration of property values that are collections (see next point).
The propByPath function offers a -NoEnumerate switch to optionally request preserving a property value's specific collection type.
By contrast, this feature is omitted from the .PropByPath() method, because there is no syntactically convenient way to request it (methods only support positional arguments). A possible solution is to create a second method, say .PropByPathNoEnumerate(), that applies the , technique discussed above.
Helper function propByPath:
function propByPath {
param(
[Parameter(Mandatory)] $Object,
[Parameter(Mandatory)] [string] $PropertyPath,
$Value, # optional value to SET
[switch] $NoEnumerate # only applies to GET
)
Set-StrictMode -Version 1
# Note: Iteratively drilling down into the object turns out to be *faster*
# than using Invoke-Expression; it also obviates the need to sanitize
# the property-path string.
$props = $PropertyPath.Split('.') # Split the path into an array of property names.
if ($PSBoundParameters.ContainsKey('Value')) { # SET
$parentObject = $Object
if ($props.Count -gt 1) {
foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
}
$parentObject.($props[-1]) = $Value
}
else { # GET
$value = $Object
foreach ($prop in $props) { $value = $value.$prop }
if ($NoEnumerate) {
, $value
} else {
$value
}
}
}
Instead of the Invoke-Expression call you would then use:
# GET
propByPath $obj $propertyPath
# GET, with preservation of the property value's specific collection type.
propByPath $obj $propertyPath -NoEnumerate
# SET
propByPath $obj $propertyPath 'new value'
You could even use PowerShell's ETS (extended type system) to attach a .PropByPath() method to all [pscustomobject] instances (PSv3+ syntax; in PSv2 you'd have to create a *.types.ps1xml file and load it with Update-TypeData -PrependPath):
'System.Management.Automation.PSCustomObject',
'Deserialized.System.Management.Automation.PSCustomObject' |
Update-TypeData -TypeName { $_ } `
-MemberType ScriptMethod -MemberName PropByPath -Value { #`
param(
[Parameter(Mandatory)] [string] $PropertyPath,
$Value
)
Set-StrictMode -Version 1
$props = $PropertyPath.Split('.') # Split the path into an array of property names.
if ($PSBoundParameters.ContainsKey('Value')) { # SET
$parentObject = $this
if ($props.Count -gt 1) {
foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
}
$parentObject.($props[-1]) = $Value
}
else { # GET
# Note: Iteratively drilling down into the object turns out to be *faster*
# than using Invoke-Expression; it also obviates the need to sanitize
# the property-path string.
$value = $this
foreach ($prop in $PropertyPath.Split('.')) { $value = $value.$prop }
$value
}
}
You could then call $obj.PropByPath('a.b') or $obj.PropByPath('a.b', 'new value')
Note: Type Deserialized.System.Management.Automation.PSCustomObject is targeted in addition to System.Management.Automation.PSCustomObject in order to also cover deserialized custom objects, which are returned in a number of scenarios, such as using Import-CliXml, receiving output from background jobs, and using remoting.
.PropByPath() will be available on any [pscustomobject] instance in the remainder of the session (even on instances created prior to the Update-TypeData call [2]); place the Update-TypeData call in your $PROFILE (profile file) to make the method available by default.
[1] Note: While it is generally advisable to limit aliases to interactive use and use full cmdlet names in scripts, use of iex to me is acceptable, because it is a built-in alias and enables a concise solution.
[2] Verify with (all on one line) $co = New-Object PSCustomObject; Update-TypeData -TypeName System.Management.Automation.PSCustomObject -MemberType ScriptMethod -MemberName GetFoo -Value { 'foo' }; $co.GetFoo(), which outputs foo even though $co was created before Update-TypeData was called.
This workaround is maybe useful to somebody.
The result goes always deeper, until it hits the right object.
$json=(Get-Content ./json.json | ConvertFrom-Json)
$result=$json
$search="a.c"
$search.split(".")|% {$result=$result.($_) }
$result
You can have 2 variables.
$json = '{
"a" : {
"b" : 1,
"c" : 2
}
}' | convertfrom-json
$a,$b = 'a','b'
$json.$a.$b
1
stuck at this one trying to store function output in a variable:
function AD-prompt($Text)
{
do
{
$in = read-host -prompt "$Text"
}
while($in -eq "")
}
calling the function with
$type = AD-prompt "Sample Text"
does not store anything in $type - only when i remove the entire do-while loop it works. it seems the function output is empty as the read-host output is stored in the $in variable, but i have no idea how to solve this - i didnt find another way to loop the read-host aswell sadly.
You need to return $in from your function by outputting it. You can do this by putting it on a line on its own after your loop:
function AD-prompt($Text)
{
do
{
$in = read-host -prompt "$Text"
}
while($in -eq "")
$in
}
It seems that PowerShell adds an additional variable to the return value of a function.
The function subfoo2 itself delivers the correct values, but as soon as PowerShell jumps back to the postion where I called the function (in foo1), value contains the value of an other variable ($msg)
(Have a look at the comments in the code)
writeMessageLog($msg){
...
Add-Content $msg
...
}
subfoo2{
writeMessageLog($msg)
return $UserArrayWithValues #During Debug, $Array is fine (1)
}
foo1{
$var = subfoo2 $UserArray # $var has now the value of $msg and $UserArrayWithValues (2)
#do something with var
}
Realcode:
function WriteLog
{
param ( [string] $severity , $msgNumber, [string] $msg )
...
$msgOut = $date + ... + $msg
Add-Content $msgout ( $msgOut )
...
}
function getFeatures
{
writelog 'I' 1002 $true $true "Load Features"
$Features = importCsv -pPath $FeatureDefintionFilePath
Writelog 'I' 1000 $true $true "Features Loaded"
return $Features # $Features has value as expected (1)
}
function GetUserFeatures ($pUserObject)
{
$SfBFeatures = ""
$SfBFeatures = getFeatures #SfBFeaures has Value of $msg and $Features (2)
...
}
Do I use the functions/return values wrong? What could lead to such behavior? Is it an issue if i call a function within a function?
If I remove $msgOut = $date + ... + $msg in writeMessageLog, the values are fine.
I'm pretty lost right now, and have no ideas where this comes from. Any ideas welcome.
This is how powershell works, basically everything that you print out will be returned as the function output. So don't output extra stuff. To force something to not output stuff you can do:
$null = some-command_that_outputs_unwanted_things
since everybody is obsessed with Out-Null I'll add this link showing several other ways to do that.
Within a function, everything you don't assign or pipe to a consuming cmdlet will get put to the pipeline and returned from the function - even if you don't explicit return it. In fact the return keyword doesn't do anything in PowerShell so the following is equivalent:
function Test-Func
{
"Hello World"
}
function Test-Func
{
return "Hello World"
}
So it looks like your writeMessageLog puts anything on the pipeline thus you have to either assign the value to anything:
$notUsed = writeMessageLog($msg)
or (prefered) pipe it to the Out-Null cmdlet:
writeMessageLog($msg) | Out-Null
Is there a way to create a parameter in a PowerShell function where you have to call it in order to have it considered?
An example given by commandlet (the bold being what I want to do):
Invoke-Command -computername Server01 -Scriptblock {...}
Here is an example of what I want to do with the function
Function DoStuff($computername, -arg2, -domain $domain)
Test-parameter(-domain) if (-domain -eq $true) {
use $domain
}
Else {
$domain = "Domain1"
}
test-parameter($arg2) {
if ($arg2 -eq $true) {
Do something
}
else {
Do the opposite
}
}
So in summary:
If "-arg2" is present, I want something to happen in the script. If "-Domain" is present and has an argument with it, I want that to be used rather then the set argument.
Powershell provides a lot of built-in support for common parameter scenarios, including mandatory parameters, optional parameters, "switch" (aka flag) parameters, and "parameter sets."
By default, all parameters are optional. The most basic approach is to simply check each one for $null, then implement whatever logic you want from there. This is basically what you have already shown in your sample code.
If you want to learn about all of the special support that Powershell can give you, check out these links:
about_Functions
about_Functions_Advanced
about_Functions_Advanced_Parameters
I don't think your question is very clear, this code assumes that if you're going to include the -domain parameter, it's always 'named' (i.e. dostuff computername arg2 -domain domain); this also makes the computername parameter mandatory.
Function DoStuff(){
param(
[Parameter(Mandatory=$true)][string]$computername,
[Parameter(Mandatory=$false)][string]$arg2,
[Parameter(Mandatory=$false)][string]$domain
)
if(!($domain)){
$domain = 'domain1'
}
write-host $domain
if($arg2){
write-host "arg2 present... executing script block"
}
else{
write-host "arg2 missing... exiting or whatever"
}
}
Not sure I understand the question correctly.
From what I gather, you want to be able to assign a value to Domain if it is null and also what to check if $args2 is supplied and according to the value, execute a certain code?
I changed the code to reassemble the assumptions made above.
Function DoStuff($computername, $arg2, $domain)
{
if($domain -ne $null)
{
$domain = "Domain1"
}
if($arg2 -eq $null)
{
}
else
{
}
}
DoStuff -computername "Test" -arg2 "" -domain "Domain2"
DoStuff -computername "Test" -arg2 "Test" -domain ""
DoStuff -computername "Test" -domain "Domain2"
DoStuff -computername "Test" -arg2 "Domain2"
Did that help?