How do I keep function and variable names from colliding? - powershell

I am making a script to install several programs.
Install.ps1
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$here\includes\script1.ps1"
. "$here\includes\script2.ps1"
Write-Host "Installing program 1"
Install-ProgramOne
Write-Host "Installing program 2"
Install-ProgramTwo
script1.ps1
param (
[string] $getCommand = "msiexec /a program1.msi /q"
)
function Get-Command {
$getCommand
}
function Install-ProgramOne {
iex $(Get-Command)
}
script2.ps1
param (
[string] $getCommand = "msiexec /a program2.msi /q"
)
function Get-Command {
$getCommand
}
function Install-ProgramTwo {
iex $(Get-Command)
}
The $getCommand variable will get overwritten when both files are included.
There are namespaces in C# and modules in Ruby, but I cannot figure out how to keep namespaces separate in Powershell.

The $getCommand variable is not a variable per-se but a parameter. A parameter that has a default value specified. That said, it isn't a great idea to have script parameters for a dot sourced script file. These type of files usually just include a library of functions and shared/global variables.
A better approach in V2 and higher is to use a module. A module is a container of variables and functions in which you control what is exported and what is private. This is what I would do with your two scripts:
script1.psm1
# private to this module
$getCommand = "msiexec /a program1.msi /q"
function Get-Command {
$getCommand
}
function Install-ProgramOne {
iex $(Get-Command)
}
Export-ModuleMember -Function Install-ProgramOne
script2.psm1
# private to this module
$getCommand = "msiexec /a program2.msi /q"
function Get-Command {
$getCommand
}
function Install-ProgramTwo {
iex $(Get-Command)
}
Export-ModuleMember -Function Install-ProgramTwo
The use like so:
Import-Module $PSScriptRoot\script1.psm1
Import-Module $PSScriptRoot\script2.psm1
Install-ProgramOne
Install-ProgramTwo

You are "dot sourcing" your scripts instead of running them. This basically means "dump everything into the GLOBAL namespace". If you simply run the scripts instead of dot-sourcing them, then they each get their own local scope. In general, I think scripts with parameters should be run, not dot-sourced.
The problem with not dot-sourcing is that the functions you are declaring, by default, will go out of the scope when the script completes. To avoid this, you can define your function like this instead:
function global:Install-ProgramOne
{
}
And then merely run the script instead of dot-sourcing, and $getcommand will be local to each script you run.

Related

Is there any way to test functions in a PowerShell script without executing the script?

I would like to define stand-alone functions in my PowerShell script and be able to Pester test the functions without executing the rest of the script. Is there any way to do this without defining the functions in a separate file?
In the following pseudocode example, how do I test functionA or functionB without executing mainFunctionality?
script.ps1:
functionA
functionB
...
mainFunctionality
script.Tests.ps1:
BeforeAll {
. $PSScriptRoot/script.ps1 # This will execute the mainFunctionality, which is what I want to avoid
}
Describe 'functionA' {
# ... tests
}
I believe in Python, you can do this by wrapping your "mainFunctionality" inside this condition, so I am looking for something similar in Powershell.
if __name__ == '__main__':
mainFunctionality
Ref: What does if __name__ == "__main__": do?
Using the PowerShell Abstract Syntax Tree (AST) to just grab functionA and invoke it:
$ScriptBlock = {
function functionA {
Write-Host 'Do something A'
}
function functionB {
Write-Host 'Do something A'
}
function mainFunctionality {
# Do something
functionA
# Do something
functionB
# Do something
}
mainFunctionality
}
Using NameSpace System.Management.Automation.Language
$Ast = [Parser]::ParseInput($ScriptBlock, [ref]$null, [ref]$null) # or: ParseFile
$FunctionA = $Ast.FindAll({
$Args[0] -is [ScriptBlockAst] -and $Args[0].Parent.Name -eq 'functionA'
}, $True)
Invoke-Expression $FunctionA.EndBlock
Do something A
You could use $MyInvocation.PSCommandPath to determine who invoked your script, its the closest I can think of to Python's if __name__ == '__main__':. This property will give you the absolute path of the caller. From there you can extract the script name, i.e. with Path.GetFileName and after you can determine what you want to do, for example, call mainFunctionality if the caller's name equals to main.ps1 or call mainFunctionality if the caller's name is not equal to script.Tests.ps1.
Here is a short example.
myScript.ps1
function A {
"I'm function A"
}
function B {
"I'm function B"
}
function mainFunctionality {
"I'm function mainFunctionality"
}
A # Calls A
B # Calls B
# Call `mainFunctionality` only if my caller's name is `main.ps1`
if([System.IO.Path]::GetFileName($MyInvocation.PSCommandPath) -eq 'main.ps1') {
mainFunctionality
}
Then if calling myScript.ps1 from main.ps1 you would see:
I'm function A
I'm function B
I'm function mainFunctionality
And if calling myScript.ps1 from anywhere else (console or other script with a different name) you would see:
I'm function A
I'm function B
Yes, you can use the Invoke-Expression cmdlet to test functions in a PowerShell script without executing the script. This cmdlet allows you to execute a string as if it were a command.
For example, if you have a function called Test-Function in your script, you can use the following command to test it: Invoke-Expression -Command "Test-Function"
function functionA {
# Do something
}
function functionB {
# Do something
}
function mainFunctionality {
# Do something
functionA
# Do something
functionB
# Do something
}
mainFunctionality
Yes, you can test functions in a PowerShell script without executing the rest of the script. To do this, you can use the Invoke-Pester command to run specific tests in the script. For example, if you wanted to test the functions functionA and functionB, you could use the following command:
Invoke-Pester -Script .\MyScript.ps1 -TestName functionA,functionB
This will execute the tests for the specified functions without executing the rest of the script.

Module scope gets duplicated for every script

I have a module, and script which runs simple user scripts. I want to keep user script as simple as possible, and that's why I use Import-Module with -Global flag. I have a problem with module "private" variable. In my case I have 2 copies of this variable. Can I achieve only one copy?
Below is simple example. You can run in by placing 3 files in the same folder and executing ScriptRunner.ps1.
Module.psm1
function Invoke-UserScript
{
param($Path)
$Script:UserScriptFailed = $false
& $Path
return $Script:UserScriptFailed
}
function New-Something
{
$Script:UserScriptFailed = $true
}
function Write-Var
{
Write-Host "Write-Var output: $Script:UserScriptFailed"
}
Export-ModuleMember -Function Invoke-UserScript
Export-ModuleMember -Function New-Something
Export-ModuleMember -Function Write-Var
ScriptRunner.ps1
Set-Location $PSScriptRoot
Import-Module -Name (Resolve-Path '.\Module.psm1') -Global
$failed = Invoke-UserScript -Path '.\UserScript.ps1'
Write-Output "ScriptRunner output: $failed"
UserScript.ps1
New-Something
Write-Var
In my example function New-Something sets UserScriptFailed to $true. But once UserScript.ps1 finishes, ScriptRunner.ps1 sees $false value.
Output:
Write-Var output: True
ScriptRunner output: False
You could try to dot source the script you want to check:
function Invoke-UserScript
{
param($Path)
$Script:UserScriptFailed = $false
# Sourcing may add the functions to the current scope
. $Path
& $Path
return $Script:UserScriptFailed
}

Setting variables for batch files in Powershell

I have a batch file named bar.cmd with a single line: ECHO %Foo%.
How can I set Foo in a Powershell script so that when I call & .\bar.cmd, it will print Bar?
To set an environment variable in PowerShell:
Set-Item Env:foo "bar"
or
$env:foo = "bar"
If you want to do it the other way around:
When you run cmd.exe to execute a shell script (.bat or .cmd file) in PowerShell, the variable gets set in that running instance of cmd.exe but is lost when that cmd.exe instance terminates.
Workaround: Run the cmd.exe shell script and output any environment variables it sets, then set those variables in the current PowerShell session. Below is a short PowerShell function that can do this for you:
# Invokes a Cmd.exe shell script and updates the environment.
function Invoke-CmdScript {
param(
[String] $scriptName
)
$cmdLine = """$scriptName"" $args & set"
& $Env:SystemRoot\system32\cmd.exe /c $cmdLine |
Select-String '^([^=]*)=(.*)$' | ForEach-Object {
$varName = $_.Matches[0].Groups[1].Value
$varValue = $_.Matches[0].Groups[2].Value
Set-Item Env:$varName $varValue
}
}

Recursive -Verbose in Powershell

Is there an easy way to make the -Verbose switch "passthrough" to other function calls in Powershell?
I know I can probably search $PSBoundParameters for the flag and do an if statement:
[CmdletBinding()]
Function Invoke-CustomCommandA {
Write-Verbose "Invoking Custom Command A..."
if ($PSBoundParameters.ContainsKey("Verbose")) {
Invoke-CustomCommandB -Verbose
} else {
Invoke-CustomCommandB
}
}
Invoke-CustomCommandA -Verbose
It seems rather messy and redundant to do it this way however... Thoughts?
One way is to use $PSDefaultParameters at the top of your advanced function:
$PSDefaultParameterValues = #{"*:Verbose"=($VerbosePreference -eq 'Continue')}
Then every command you invoke with a -Verbose parameter will have it set depending on whether or not you used -Verbose when you invoked your advanced function.
If you have just a few commands the do this:
$verbose = [bool]$PSBoundParameters["Verbose"]
Invoke-CustomCommandB -Verbose:$verbose
I began using KeithHill's $PSDefaultParameterValues technique in some powershell modules. I ran into some pretty surprising behavior which I'm pretty sure resulted from the effect of scope and $PSDefaultParameterValues being a sort-of global variable. I ended up writing a cmdlet called Get-CommonParameters (alias gcp) and using splat parameters to achieve explicit and terse cascading of -Verbose (and the other common parameters). Here is an example of how that looks:
function f1 {
[CmdletBinding()]
param()
process
{
$cp = &(gcp)
f2 #cp
# ... some other code ...
f2 #cp
}
}
function f2 {
[CmdletBinding()]
param()
process
{
Write-Verbose 'This gets output to the Verbose stream.'
}
}
f1 -Verbose
The source for cmdlet Get-CommonParameters (alias gcp) is in this github repository.
How about:
$vb = $PSBoundParameters.ContainsKey('Verbose')
Invoke-CustomCommandB -Verbose:$vb

How to properly use the -verbose and -debug parameters in a custom cmdlet

By default, any named function that has the [CmdletBinding()] attribute accepts the -debug and -verbose (and a few others) parameters and has the predefined $debug and $verbose variables. I'm trying to figure out how to pass them on to other cmdlet's that get called within the function.
Let's say I have a cmdlet like this:
function DoStuff() {
[CmdletBinding()]
PROCESS {
new-item Test -type Directory
}
}
If -debug or -verbose was passed into my function, I want to pass that flag into the new-item cmdlet. What's the right pattern for doing this?
$PSBoundParameters isn't what you're looking for. The use of the [CmdletBinding()] attribute allows the usage of $PSCmdlet within your script, in addition to providing a Verbose flag. It is in fact this same Verbose that you're supposed to use.
Through [CmdletBinding()], you can access the bound parameters through $PSCmdlet.MyInvocation.BoundParameters. Here's a function that uses CmdletBinding and simply enters a nested prompt immediately in order examine the variables available inside the function scope.
PS D:\> function hi { [CmdletBinding()]param([string] $Salutation) $host.EnterNestedPrompt() }; hi -Salutation Yo -Verbose
PS D:\>>> $PSBoundParameters
____________________________________________________________________________________________________
PS D:\>>> $PSCmdlet.MyInvocation.BoundParameters
Key Value
--- -----
Salutation Yo
Verbose True
So in your example, you would want the following:
function DoStuff `
{
[CmdletBinding()]
param ()
process
{
new-item Test -type Directory `
-Verbose:($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent -eq $true)
}
}
This covers -Verbose, -Verbose:$false, -Verbose:$true, and the case where the switch is not present at all.
Perhaps it sounds strange, but there isn't any easy way for a cmdlet to know its verbose or debug mode. Take a look at the related question:
How does a cmdlet know when it really should call WriteVerbose()?
One not perfect, but practically reasonable, option is to introduce your own cmdlet parameters (for example, $MyVerbose and $MyDebug) and use them in the code explicitly:
function DoStuff {
[CmdletBinding()]
param
(
# Unfortunately, we cannot use Verbose name with CmdletBinding
[switch]$MyVerbose
)
process {
if ($MyVerbose) {
# Do verbose stuff
}
# Pass $MyVerbose in the cmdlet explicitly
New-Item Test -Type Directory -Verbose:$MyVerbose
}
}
DoStuff -MyVerbose
UPDATE
When we need only a switch (not, say, a verbosity level value) then the approach with $PSBoundParameters is perhaps better than proposed in the first part of this answer (with extra parameters):
function DoStuff {
[CmdletBinding()]
param()
process {
if ($PSBoundParameters['Verbose']) {
# Do verbose stuff
}
New-Item Test -Type Directory -Verbose:($PSBoundParameters['Verbose'] -eq $true)
}
}
DoStuff -Verbose
It's all not perfect anyway. If there are better solutions then I would really like to know them myself.
There is no need. PowerShell already does this as the code below proves.
function f { [cmdletbinding()]Param()
"f is called"
Write-Debug Debug
Write-Verbose Verbose
}
function g { [cmdletbinding()]Param()
"g is called"
f
}
g -Debug -Verbose
The output is
g is called
f is called
DEBUG: Debug
VERBOSE: Verbose
It is not done as direct as passing -Debug to the next cmdlet though. It is done through the $DebugPreference and $VerbrosePreference variables. Write-Debug and Write-Verbose act like you would expect, but if you want to do something different with debug or verbose you can read here how to check for yourself.
Here's my solution:
function DoStuff {
[CmdletBinding()]
param ()
BEGIN
{
$CMDOUT = #{
Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false };
Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false }
}
} # BEGIN ENDS
PROCESS
{
New-Item Example -ItemType Directory #CMDOUT
} # PROCESS ENDS
END
{
} #END ENDS
}
What this does different from the other examples is that it will repsect "-Verbose:$false" or "-Debug:$false". It will only set -Verbose/-Debug to $true if you use the following:
DoStuff -Verbose
DoStuff -Verbose:$true
DoStuff -Debug
DoStuff -Debug:$true
You could build a new hash table based on the bound debug or verbose parameters and then splat it to the internal command. If you're just specifying switches (and aren't passing a false switch, like $debug:$false) you can just check for the existence of debug or verbose:
function DoStuff() {
[CmdletBinding()]
PROCESS {
$HT=#{Verbose=$PSBoundParameters.ContainsKey'Verbose');Debug=$PSBoundParameters.ContainsKey('Debug')}
new-item Test -type Directory #HT
}
}
If you want to pass the parameter value it's more complicated, but can be done with:
function DoStuff {
[CmdletBinding()]
param()
PROCESS {
$v,$d = $null
if(!$PSBoundParameters.TryGetValue('Verbose',[ref]$v)){$v=$false}
if(!$PSBoundParameters.TryGetValue('Debug',[ref]$d)){$d=$false}
$HT=#{Verbose=$v;Debug=$d}
new-item Test -type Directory #HT
}
}
The best way to do it is by setting the $VerbosePreference. This will enable the verbose level for the entire script. Do not forget to disable it by the end of the script.
Function test
{
[CmdletBinding()]
param($param1)
if ($psBoundParameters['verbose'])
{
$VerbosePreference = "Continue"
Write-Verbose " Verbose mode is on"
}
else
{
$VerbosePreference = "SilentlyContinue"
Write-Verbose " Verbose mode is Off"
}
# <Your code>
}
You can set the VerbosePreference as a global variable on starting your script and then check for the global variable in your custom cmdlet.
Script:
$global:VerbosePreference = $VerbosePreference
Your-CmdLet
Your-CmdLet:
if ($global:VerbosePreference -eq 'Continue') {
# verbose code
}
Checking explicitly for 'Continue' allows the script to be equal to -verbose:$false when you call the CmdLet from a script that doesn't set the global variable (in which case it's $null)
You do not have to do any checks or comparisons. Even though -Verbose (and -Debug) are of type [switch], they seem to understand not just $true and $false but also their preference variable. The preference variable also gets inherited correctly to all child functions that are called. I tried this on Powershell version 7.3.2 and it works as expected.
function Parent {
[CmdletBinding()]param()
Child
}
function Child {
[CmdletBinding()]param()
New-Item C:\TEST\SomeDir -Force -ItemType Directory -Verbose:$VerbosePreference -Debug:$DebugPreference
}
Parent -Verbose
Parent -Debug
I think this is the easiest way:
Function Test {
[CmdletBinding()]
Param (
[parameter(Mandatory=$False)]
[String]$Message
)
Write-Host "This is INFO message"
if ($PSBoundParameters.debug) {
Write-Host -fore cyan "This is DEBUG message"
}
if ($PSBoundParameters.verbose) {
Write-Host -fore green "This is VERBOSE message"
}
""
}
Test -Verbose -Debug