How can I create function check with $args - powershell

I declare all variable in var.ps1 like this:
$a = "aa"
$b = "bb"
In the second script check.ps1 I try to create a function to check if the argument passed exist in var.ps1 something like this:
check "$a" "$b" "$c"
I need to use args in my function.
Could you please give me any suggestion?

It's unclear why you want this and I'd probably solve it another way, but to answer your question, you can try the sample below.
Remember to use literal strings (single quotes) for values with a leading $ to avoid them being treated as a variable. If not, PowerShell will try to replace the variables with it's value (or nothing if it's not defined), which means that Check-Variables won't get the variable names. The following solution accepts both 'a' and '$a'.
Var.ps1
$a = "aa"
$b = "bb"
Check.ps1
#Dot-sourcing var.ps1 which is located in the same folder as Check.ps1
#Loads variables into the current scope so every function in Check.ps1 can access them
. "$PSScriptRoot\var.ps1"
function Check-Variables {
foreach ($name in $args) {
#Remove leading $
$trimmedname = $name -replace '^\$'
if(-not (Get-Variable -Name $trimmedname -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: Variable `$$trimmedname is not defined"
}
}
}
Check-Variables '$a' "b" '$c'
Demo:
PS> C:\Users\frode\Desktop\Check.ps1
ERROR: Variable $c is not defined

Related

How does $_ get set in a ScriptBlock?

I'm looking to have a function in script where I can use a ScriptBlock passed in as either a predicate or with Where-Object.
I can write
cat .\.gitignore | Where-Object { $_.contains('pp') }
and this works; as does:
$f = { $_.contains('pp') }; cat .gitignore | Where-Object $f
however trying
$f.Invoke( 'apple' )
results in
MethodInvocationException: Exception calling "Invoke" with "1" argument(s): "You cannot call a method on a null-valued expression.
Whereas I expected True. So clearly $_ wasn't set.
Likewise
$ff = { echo "args: $args`nauto: $_" }; $ff.Invoke( 'apple' )
outputs
args: apple
auto:
So $_ is clearly not getting set.
'apple' | %{ $_.contains('pp') }
Works, but I want the scriptblock to be a variable and
$f = { $_.contains('pp') }; 'apple' | %$f
Is a compile error.
tl;dr: So how do I set/pass the value of $_ inside a scriptblock I am invoking?
Note, this answer only covers how does $_ gets populated in the context of a process block of a script block. Other use cases can be found in the about_PSItem documentation.
In the context of a process block of a Script Block, the $_ ($PSItem) variable is automatically populated and represents each element coming from the pipeline, i.e.:
$f = { process { $_.contains('pp') }}
'apple' | & $f # True
You can however achieve the same using InvokeWithContext method from the ScriptBlock Class:
$f = { $_.contains('pp') }
$f.InvokeWithContext($null, [psvariable]::new('_', 'apple')) # True
Do note, this method always returns Collection`1. Output is not enumerated.
Worth noting as zett42 points out, the scoping rules of script blocks invoked via it's methods or via the call operator & still apply.
Script Blocks are able to see parent scope variables (does not include Remoting):
$foo = 'hello'
{ $foo }.Invoke() # hello
But are not able to update them:
$foo = 'hello'
{ $foo = 'world' }.Invoke()
$foo # hello
Unless using a scope a modifier (applies only to Value Types):
$foo = 'hello'
{ $script:foo = 'world' }.Invoke()
$foo # world
Or via the dot sourcing operator .:
$foo = 'hello'
. { $foo = 'world' }
$foo # world
# still applies with pipelines!
$foo = 'hello'
'world' | . { process { $foo = $_ }}
$foo # world
See about Scopes for more details.
Using the .Invoke() method (and its variants, .InvokeReturnAsIs() and .InvokeWithContext()) to execute a script block in PowerShell code is best avoided, because it changes the semantics of the call in several respects - see this answer for more information.
While the PowerShell-idiomatic equivalent is &, the call operator, it is not enough here, given that you want want the automatic $_ variable to be defined in your script block.
The easiest way to define $_ based on input is indeed ForEach-Object (one of whose built-in aliases is %):
$f = { $_.contains('pp') }
ForEach-Object -Process $f -InputObject 'apple' # -> $true
Note, however, that -InputObject only works meaningfully for a single input object (though you may pass an array / collection in which case $_ then refers to it as a whole); to provide multiple ones, use the pipeline:
'apple', 'pear' | ForEach-Object $f # $true, $false
# Equivalent, with alias
'apple', 'pear' | % $f
If, by contrast, your intent is simply for your script block to accept arguments, you don't need $_ at all and can simply make your script either formally declare parameter(s) or use the automatic $args variable which contains all (unbound) positional arguments:
# With $args: $args[0] is the first positional argument.
$f = { $args[0].contains('pp') }
& $f 'apple'
# With declared parameter.
$f = { param([string] $fruit) $fruit.contains('pp') }
& $f 'apple'
For more information about the parameter-declaration syntax, see the conceptual about_Functions help topic (script blocks are basically unnamed functions, and only the param(...) declaration style can be used in script blocks).
I got it to work by wrapping $f in () like
$f = { $_.contains('pp') }; 'apple' | %($f)
...or (thanks to #zett42) by placing a space between the % and $ like
$f = { $_.contains('pp') }; 'apple' | % $f
Can even pass in the value from a variable
$f = { $_.contains('pp') }; $a = 'apple'; $a | %($f)
Or use it inside an If-statement
$f = { $_.contains('pp') }; $a = 'apple'; If ( $a | %($f) ){ echo 'yes' }
So it appears that $_ is only set by having things 'piped' (aka \) into it? But why this is and how it works, and if this can be done through .invoke() is unknown to me. If anyone can explain this please do.
From What does $_ mean in PowerShell? and the related documentation, it seems like $PSItem is indeed a better name since it isn't like Perl's $_

How to escape a string variable that might contain characters needing escaping (such as a $) in PowerShell

Context
I have this small logic to automate databases restore:
$sourceServer = "PROD"
$targetServer = "DEV"
$backupHistory = Get-DbaDbBackupHistory -SqlInstance $sourceServer -LastFull | Select-Object Path
foreach ($backup in $backupHistory)
{
$path = $($backup.Path)
Restore-DbaDatabase -SqlInstance $targetServer -Path $path
}
$path might contain a $ character, for instance \\Backups\Server$Instance\DB.bak.
In these cases, when I use the $path variable as a parameter for the restore operation, the $Instance portion of the variable gets evaluated by PowerShell, which transforms it into an empty string, so, the value for my $path variable is now \\Backups\Server\DB.bak
Question
Is there any way to instruct PowerShell to use the string variable "as is" without evaluating it? I assume this would similar to escape the variable content, but I have not found a way to achieve this.
If you have $someVar have value with powershell special characters, you should not care they can expand into some variables. Normally they will not.
$Computername = 'SuperServer'
$Temp = 'TempFolder'
$someVar = 'C:\$Temp\$Computername' # Note that I use single quotes
New-Item -ItemType Directory -Path $someVar # Will create C:\$Temp\$Computername , not C:\TempFolder\SuperServer
When you use string variable with special symbols in value, they remain just symbols. They are not expanded into variable values.
When you write string literals in single quotes, nothing is expanded.
When you write string literals in double quotes, everything is expanded.
$Var1 = 'Text'
$Var2 = 12
$ComboVar1 = '$Var1 $Var2' # => == "$Var1 $Var2"
$ComboVar2 = "$Var1 $Var2" # => == "Text 12"
Write-Host $ComboVar1 # => "$Var1 $Var2"
Write-Host $ComboVar2 # => "Text 12"
Please note that $ComboVar2 is expanded when value is set, not when outpout is done!

Powershell function being passed too many parameters

I'm writing a set of PowerShell scripts to monitor the size of various folders. I've run into an error, and I've got no idea what's causing it.
Here is the code, with Write-Host showing what I am expecting and what the variables $ip and $loc actually contain:
function getDriveLetter($ip) {
Write-Host $ip # prints: 192.168.10.10 myfolder1\myfolder2\
# expected: 192.168.10.10
switch($ip) {
"192.168.10.10" {return "E`$"; break}
"192.168.10.20" {return "D`$"; break}
default {"Unknown"; break}
}
}
function getFullPath($loc,$folder) {
Write-Host $loc # prints: 192.168.10.10 myfolder1\myfolder2\
# expected: 192.168.10.10
$drive = getDriveLetter("$loc")
$str = "\\$loc\$drive\DATA\$folder"
return $str
}
function testPath($loc,$folder) {
$mypath = getFullPath("$loc","$folder")
if (Test-Path $mypath) {
return $true
} else {
return $false
}
}
When I run the command:
testPath("192.168.10.10","myfolder1\myfolder2\")
I'm getting a "False" result, but if I run:
Test-Path "\\192.168.10.10\E`$\DATA\myfolder1\myfolder2\"
The command returns True (as it should).
What have I missed? I've tried forcing the variables to be set with:
$mypath = getFullPath -loc "$loc" -folder "$folder"
but there's no change. If it changes anything, this is on Powershell version 4.
I'd suggest that you review the syntax of PowerShell a bit more, because there's many mistakes in there. PowerShell is quite different from C# and you seem to make a lot of assumptions. :)
First of all, that's not how you call PowerShell functions. Also not sure why you added quotes around the parameters? They are redundant. If you fix the function calls your code should function as expected.
$mypath = getFullPath $loc $folder
Then there's a semicolon in your switch statement, which is also wrong. Then, you don't have to escape the $ if you just use ''. The break is also redundant because return exits the function in this case.
"192.168.10.10" { return 'E$' }
Also, one interesting thing about PowerShell: You could just get rid of the return in getFullPath:
function getFullPath($loc, $folder) {
$drive = getDriveLetter($loc)
"\\$loc\$drive\DATA\$folder"
}
PowerShell returns uncaptured output, which is important to be aware of, it can be the cause of many obscure bugs.
The problem is in how you are calling your functions. Function arguments are space delimited in PowerShell, and do not use parentheses to enclose the arguments.
getFullPath $loc $folder
When you wrap arguments in parentheses, you are creating an array containing two values, and passing that array as the first argument.
getFullPath($loc, $folder)
This line passes an array containing two strings #($loc, $folder) as the first argument, and then, because there are no other arguments on the line, it passes $null to the second. Inside the function, the array is then joined to be used as a string, which is the behavior you observed.
The problem is how you pass the parameters to the functions.
See more details on above link:
How do I pass multiple parameters into a function in PowerShell?
function getDriveLetter() {
param($ip)
switch($ip) {
"192.168.0.228" {return "E`$"; break}
"192.168.10.20" {return "D`$"; break}
default {"Unknown"; break}
}
}
function getFullPath() {
param($loc, $folder)
$drive = getDriveLetter -ip $loc
$str = "\\$loc\$drive\DATA\$folder"
return $str
}
function testPath() {
param($loc, $folder)
$mypath = getFullPath -loc $loc -folder $folder
if (Test-Path $mypath) {
return $true
} else {
return $false
}
}
testPath -loc "192.168.10.10" -param "myfolder1\myfolder2\"

Setting a global PowerShell variable from a function where the global variable name is a variable passed to the function

I need to set a global variable from a function and am not quite sure how to do it.
# Set variables
$global:var1
$global:var2
$global:var3
function foo ($a, $b, $c)
{
# Add $a and $b and set the requested global variable to equal to it
$c = $a + $b
}
Call the function:
foo 1 2 $global:var3
End result:
$global:var3 is set to 3
Or if I called the function like this:
foo 1 2 $global:var2
End result:
$global:var2 is set to 3
I hope this example makes sense. The third variable passed to the function is the name of the variable it is to set.
You can use the Set-Variable cmdlet. Passing $global:var3 sends the value of $var3, which is not what you want. You want to send the name.
$global:var1 = $null
function foo ($a, $b, $varName)
{
Set-Variable -Name $varName -Value ($a + $b) -Scope Global
}
foo 1 2 var1
This is not very good programming practice, though. Below would be much more straightforward, and less likely to introduce bugs later:
$global:var1 = $null
function ComputeNewValue ($a, $b)
{
$a + $b
}
$global:var1 = ComputeNewValue 1 2
As simple as:
$A="1"
function changeA2 () { $global:A="0"}
changeA2
$A
I ran across this question while troubleshooting my own code.
So this does NOT work...
$myLogText = ""
function AddLog ($Message)
{
$myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This APPEARS to work, but only in the PowerShell ISE:
$myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $myLogText
This is actually what works in both ISE and command line:
$global:myLogText = ""
function AddLog ($Message)
{
$global:myLogText += ($Message)
}
AddLog ("Hello")
Write-Host $global:myLogText
You'll have to pass your arguments as reference types.
#First create the variables (note you have to set them to something)
$global:var1 = $null
$global:var2 = $null
$global:var3 = $null
#The type of the reference argument should be of type [REF]
function foo ($a, $b, [REF]$c)
{
# add $a and $b and set the requested global variable to equal to it
# Note how you modify the value.
$c.Value = $a + $b
}
#You can then call it like this:
foo 1 2 [REF]$global:var3
The first suggestion in latkin's answer seems good, although I would suggest the less long-winded way below.
PS c:\temp> $global:test="one"
PS c:\temp> $test
one
PS c:\temp> function changet() {$global:test="two"}
PS c:\temp> changet
PS c:\temp> $test
two
His second suggestion however about being bad programming practice, is fair enough in a simple computation like this one, but what if you want to return a more complicated output from your variable? For example, what if you wanted the function to return an array or an object? That's where, for me, PowerShell functions seem to fail woefully. Meaning you have no choice other than to pass it back from the function using a global variable. For example:
PS c:\temp> function changet([byte]$a,[byte]$b,[byte]$c) {$global:test=#(($a+$b),$c,($a+$c))}
PS c:\temp> changet 1 2 3
PS c:\temp> $test
3
3
4
PS C:\nb> $test[2]
4
I know this might feel like a bit of a digression, but I feel in order to answer the original question we need to establish whether global variables are bad programming practice and whether, in more complex functions, there is a better way. (If there is one I'd be interested to here it.)
#zdan. Good answer. I'd improve it like this...
I think that the closest you can come to a true return value in PowerShell is to use a local variable to pass the value and never to use return as it may be 'corrupted' by any manner of output situations
function CheckRestart([REF]$retval)
{
# Some logic
$retval.Value = $true
}
[bool]$restart = $false
CheckRestart( [REF]$restart)
if ( $restart )
{
Restart-Computer -Force
}
The $restart variable is used either side of the call to the function CheckRestart making clear the scope of the variable. The return value can by convention be either the first or last parameter declared. I prefer last.
Set the variable as a global variable outside of the function and then set the value inside of the function.

Function return value in PowerShell

I have developed a PowerShell function that performs a number of actions involving provisioning SharePoint Team sites. Ultimately, I want the function to return the URL of the provisioned site as a String so at the end of my function I have the following code:
$rs = $url.ToString();
return $rs;
The code that calls this function looks like:
$returnURL = MyFunction -param 1 ...
So I am expecting a String, however it's not. Instead, it is an object of type System.Management.Automation.PSMethod. Why is it returning that type instead of a String type?
PowerShell has really wacky return semantics - at least when viewed from a more traditional programming perspective. There are two main ideas to wrap your head around:
All output is captured, and returned
The return keyword really just indicates a logical exit point
Thus, the following two script blocks will do effectively the exact same thing:
$a = "Hello, World"
return $a
 
$a = "Hello, World"
$a
return
The $a variable in the second example is left as output on the pipeline and, as mentioned, all output is returned. In fact, in the second example you could omit the return entirely and you would get the same behavior (the return would be implied as the function naturally completes and exits).
Without more of your function definition I can't say why you are getting a PSMethod object. My guess is that you probably have something a few lines up that is not being captured and is being placed on the output pipeline.
It is also worth noting that you probably don't need those semicolons - unless you are nesting multiple expressions on a single line.
You can read more about the return semantics on the about_Return page on TechNet, or by invoking the help return command from PowerShell itself.
This part of PowerShell is probably the most stupid aspect. Any extraneous output generated during a function will pollute the result. Sometimes there isn't any output, and then under some conditions there is some other unplanned output, in addition to your planned return value.
So, I remove the assignment from the original function call, so the output ends up on the screen, and then step through until something I didn't plan for pops out in the debugger window (using the PowerShell ISE).
Even things like reserving variables in outer scopes cause output, like [boolean]$isEnabled which will annoyingly spit a False out unless you make it [boolean]$isEnabled = $false.
Another good one is $someCollection.Add("thing") which spits out the new collection count.
With PowerShell 5 we now have the ability to create classes. Change your function into a class, and return will only return the object immediately preceding it. Here is a real simple example.
class test_class {
[int]return_what() {
Write-Output "Hello, World!"
return 808979
}
}
$tc = New-Object -TypeName test_class
$tc.return_what()
If this was a function the expected output would be
Hello World
808979
but as a class the only thing returned is the integer 808979. A class is sort of like a guarantee that it will only return the type declared or void.
As a workaround I've been returning the last object in the array that you get back from the function... It is not a great solution, but it's better than nothing:
someFunction {
$a = "hello"
"Function is running"
return $a
}
$b = someFunction
$b = $b[($b.count - 1)] # Or
$b = $b[-1] # Simpler
All in all, a more one-lineish way of writing the same thing could be:
$b = (someFunction $someParameter $andAnotherOne)[-1]
I pass around a simple Hashtable object with a single result member to avoid the return craziness as I also want to output to the console. It acts through pass by reference.
function sample-loop($returnObj) {
for($i = 0; $i -lt 10; $i++) {
Write-Host "loop counter: $i"
$returnObj.result++
}
}
function main-sample() {
$countObj = #{ result = 0 }
sample-loop -returnObj $countObj
Write-Host "_____________"
Write-Host "Total = " ($countObj.result)
}
main-sample
You can see real example usage at my GitHub project unpackTunes.
The existing answers are correct, but sometimes you aren't actually returning something explicitly with a Write-Output or a return, yet there is some mystery value in the function results. This could be the output of a builtin function like New-Item
PS C:\temp> function ContrivedFolderMakerFunction {
>> $folderName = [DateTime]::Now.ToFileTime()
>> $folderPath = Join-Path -Path . -ChildPath $folderName
>> New-Item -Path $folderPath -ItemType Directory
>> return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
Directory: C:\temp
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2/9/2020 4:32 PM 132257575335253136
True
All that extra noise of the directory creation is being collected and emitted in the output. The easy way to mitigate this is to add | Out-Null to the end of the New-Item statement, or you can assign the result to a variable and just not use that variable. It would look like this...
PS C:\temp> function ContrivedFolderMakerFunction {
>> $folderName = [DateTime]::Now.ToFileTime()
>> $folderPath = Join-Path -Path . -ChildPath $folderName
>> New-Item -Path $folderPath -ItemType Directory | Out-Null
>> # -or-
>> $throwaway = New-Item -Path $folderPath -ItemType Directory
>> return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
True
New-Item is probably the more famous of these, but others include all of the StringBuilder.Append*() methods, as well as the SqlDataAdapter.Fill() method.
You need to clear output before returning. Try using Out-Null. That's how powershell return works. It returns not the variable you wanted, but output of your whole function. So your example would be:
function Return-Url
{
param([string] $url)
. {
$rs = $url.ToString();
return
} | Out-Null
return $rs
}
$result = Return-Url -url "https://stackoverflow.com/questions/10286164/function-return-value-in-powershell"
Write-Host $result
Write-Host $result.GetType()
And result is:
https://stackoverflow.com/questions/10286164/function-return-value-in-powershell
System.String
Credits to https://riptutorial.com/powershell/example/27037/how-to-work-with-functions-returns
It's hard to say without looking at at code. Make sure your function doesn't return more than one object and that you capture any results made from other calls. What do you get for:
#($returnURL).count
Anyway, two suggestions:
Cast the object to string:
...
return [string]$rs
Or just enclose it in double quotes, same as above but shorter to type:
...
return "$rs"
Luke's description of the function results in these scenarios seems to be right on. I only wish to understand the root cause and the PowerShell product team would do something about the behavior. It seems to be quite common and has cost me too much debugging time.
To get around this issue I've been using global variables rather than returning and using the value from the function call.
Here's another question on the use of global variables:
Setting a global PowerShell variable from a function where the global variable name is a variable passed to the function
The following simply returns 4 as an answer. When you replace the add expressions for strings it returns the first string.
Function StartingMain {
$a = 1 + 3
$b = 2 + 5
$c = 3 + 7
Return $a
}
Function StartingEnd($b) {
Write-Host $b
}
StartingEnd(StartingMain)
This can also be done for an array. The example below will return "Text 2"
Function StartingMain {
$a = ,#("Text 1","Text 2","Text 3")
Return $a
}
Function StartingEnd($b) {
Write-Host $b[1]
}
StartingEnd(StartingMain)
Note that you have to call the function below the function itself. Otherwise, the first time it runs it will return an error that it doesn't know what "StartingMain" is.