I want to import external function from file, not converting it to a module (we have hundreds of file-per-function, so treat all them as modules is overkill).
Here is code explanation. Please notice that I have some additional logic in Import-Function like adding scripts root folder and to check file existence and throw special error, to avoid this code duplication in each script which requires that kind of import.
C:\Repository\Foo.ps1:
Function Foo {
Write-Host 'Hello world!'
}
C:\InvocationTest.ps1:
# Wrapper func
Function Import-Function ($Name) {
# Checks and exception throwing are omitted
. "C:\Repository\$name.ps1"
# Foo function can be invoked in this scope
}
# Wrapped import
Import-Function -Name 'Foo'
Foo # Exception: The term 'Foo' is not recognized
# Direct import
. "C:\Repository\Foo.ps1"
Foo # 'Hello world!'
Is there any trick, to dot source to global scope?
You can't make the script run in a parent scope, but you can create a function in the global scope by explicitly scoping it.
Would something like this work for you?
# Wrapper func
Function Import-Function ($Path) {
# Checks and exception throwing are omitted
$script = Get-Content $Path
$Script -replace '^function\s+((?!global[:]|local[:]|script[:]|private[:])[\w-]+)', 'function Global:$1'
.([scriptblock]::Create($script))
}
The above regex only targets root functions (functions left justified; no white space to left of the word function). In order to target all functions, regardless of spacing (including sub-functions), change the $Script -replace line to:
$Script -replace '^\s*function\s+((?!global[:]|local[:]|script[:]|private[:])[\w-]+)','function Global:$1'
You can change the functions that are defined in the dot-sourced files so that they are defined in the global scope:
function Global:Get-SomeThing {
# ...
}
When you dot source that from within a function, the function defined in the dot sourced file will be global. Not saying this is best idea, just another possibility.
Just dot-source the function as well:
. Import-Function -Name 'Foo'
Foo # Hello world!
I can't remember a way to run a function in global scope right now. You could do something like this:
$name = "myscript"
$myimportcode= {
# Checks and exception throwing are omitted
. .\$name.ps1
# Foo function can be invoked in this scope
}
Invoke-Expression -Command $myimportcode.ToString()
When you convert the scriptblock to a string .ToString(), the variable will expand.
Related
I’m writing a Powershell script to call a file convert function(to execute ANYTRAN file).
I was told to put param() in a try-catch by my boss but that seems to cause an error.
Then how can I catch the error from param()?
I think it’s possible to use if statement in a parent shell.
Please give me some advice.
Below is the code.
$ErrorActionPreference = "Stop"
try{
#-----------------------------------------------------------
# 初期処理
#-----------------------------------------------------------
#---# 環境変数定義(define common env)
#---& ".\commonEnv.ps1"
# 共通関数インクルード(include common func)
. (Resolve-Path ".\commonFunc.ps1").path
# 引数取得(get parameter)
Param(
$inFile
,$outFile
,$flgZeroByte
)
Your issue is not in params passing . "param", unless the syntax is incorrect, logically can't crash (and you can't handle whether it could, unless with error handling at scope higher than the definition of function) .
function sc1 {
param ( $v_f = '' )
process {
Write-Host $v_f
}
}
sc1 '& 2'
I'm trying to write a function that will print a user-supplied greeting addressed to a user-supplied name. I want to use expanding strings the way I can in this code block:
$Name = "World"
$Greeting = "Hello, $Name!"
$Greeting
Which successfully prints Hello, World!. However, when I try to pass these strings as parameters to a function like so,
function HelloWorld
{
Param ($Greeting, $Name)
$Greeting
}
HelloWorld("Hello, $Name!", "World")
I get the output
Hello, !
World
Upon investigation, Powershell seems to be ignoring $Name in "Hello, $Name!" completely, as running
HelloWorld("Hello, !", "World")
Produces output identical to above. Additionally, it doesn't seem to regard "World" as the value of $Name, since running
function HelloWorld
{
Param ($Greeting, $Name)
$Name
}
HelloWorld("Hello, $Name!", "World")
Produces no output.
Is there a way to get the expanding string to work when passed in as a function parameter?
In order to delay string interpolation and perform it on demand, with then-current values, you must use $ExecutionContext.InvokeCommand.ExpandString()[1] on a single-quoted string that acts as a template:
function HelloWorld
{
Param ($Greeting, $Name)
$ExecutionContext.InvokeCommand.ExpandString($Greeting)
}
HelloWorld 'Hello, $Name!' 'World' # -> 'Hello, World!'
Note how 'Hello, $Name!' is single-quoted to prevent instant expansion (interpolation).
Also note how HelloWorld is called with its arguments separated with spaces, not ,, and without (...).
In PowerShell, functions are invoked like command-line executables - foo arg1 arg2 - not like C# methods - foo(arg1, arg2) - see Get-Help about_Parsing.
If you accidentally use , to separate your arguments, you'll construct an array that a function sees as a single argument.
To help you avoid accidental use of method syntax, you can use Set-StrictMode -Version 2 or higher, but note that that entails additional strictness checks.
Note that since PowerShell functions by default also see variables defined in the parent scope (all ancestral scopes), you could simply define any variables that the template references in the calling scope instead of declaring individual parameters such as $Name:
function HelloWorld
{
Param ($Greeting) # Pass the template only.
$ExecutionContext.InvokeCommand.ExpandString($Greeting)
}
$Name = 'World' # Define the variable(s) used in the template.
HelloWorld 'Hello, $Name!' # -> 'Hello, World!'
Caveat: PowerShell string interpolation supports full commands - e.g., "Today is $(Get-Date)" - so unless you fully control or trust the template string, this technique can be security risk.
Ansgar Wiechers proposes a safe alternative based on .NET string formatting via PowerShell's -f operator and indexed placeholders ({0}, {1}, ...):
Note that you can then no longer apply transformations on the arguments as part of the template string or embed commands in it in general.
function HelloWorld
{
Param ($Greeting, $Name)
$Greeting -f $Name
}
HelloWorld 'Hello, {0}!' 'World' # -> 'Hello, World!'
Pitfalls:
PowerShell's string expansion uses the invariant culture, whereas the -f operator performs culture-sensitive formatting (snippet requires PSv3+):
$prev = [cultureinfo]::CurrentCulture
# Temporarily switch to culture with "," as the decimal mark
[cultureinfo]::CurrentCulture = 'fr-FR'
# string expansion: culture-invariant: decimal mark is always "."
$v=1.2; "$v"; # -> '1.2'
# -f operator: culture-sensitive: decimal mark is now ","
'{0}' -f $v # -> '1,2'
[cultureinfo]::CurrentCulture = $prev
PowerShell's string expansion supports expanding collections (arrays) - it expands them to a space-separated list - whereas the -f operator only supports scalars (single values):
$arr = 'one', 'two'
# string expansion: array is converted to space-separated list
"$var" # -> 'one two'
# -f operator: array elements are syntactically treated as separate values
# so only the *first* element replaces {0}
'{0}' -f $var # -> 'one'
# If you use a *nested* array to force treatment as a single array-argument,
# you get a meaningless representation (.ToString() called on the array)
'{0}' -f (, $var) # -> 'System.Object[]'
[1] Surfacing the functionality of the $ExecutionContext.InvokeCommand.ExpandString() method in a more discoverable way, namely via an Expand-String cmdlet, is the subject of GitHub feature-request issue #11693.
Your issue occurs because the $Name string replacement is happening outside of the function, before the $Name variable is populated inside of the function.
You could do something like this instead:
function HelloWorld
{
Param ($Greeting, $Name)
$Greeting -replace '\$Name',$Name
}
HelloWorld -Greeting 'Hello, $Name!' -Name 'World'
By using single quotes, we send the literal greeting of Hello, $Name in and then do the replacement of this string inside the function using -Replace (we have to put a \ before the $ in the string we're replace because $ is a regex special character).
I have a Powershell module and in the manifest I have declared the primary module and two nested modules.
The structure of the module is as follows:
- [dir] Pivot.DockerAdmin
- [manifest] Pivot.DockerAdmin.psd1
- [main module file] Pivot.DockerAdmin.psm1
- [nested script] DockerfileScripts.ps1
- [nested script] DockerCliScripts.ps1
What works
The Primary Module (Pivot.DockerAdmin.psm1) can call functions in the Nested Module files (both DockerfileScripts.ps1, DockerCliScripts.ps1), without a problem. Note, there's no specific logic to include these files, other than the entry in the manifest file.
What does NOT work
One Nested Module script file (DockerfileScripts.ps1) cannot call functions in the other Nested Module script file (DockerCliScripts.ps1).
The nested modules are just simple script files. So in effect, I'm using the NestedModule concept to logically group some functions in other files.
The module is setup correctly. I'm confident about this, because I even have Pester tests running on a build box without any special treatment.
I expect to be able to call a function in a nested module from another nested module, in the same way the primary module can call functions in any nested module, but this fails with an unrecognised command error.
If this is not possible are there any recommendations around organising script files within PS modules, so that a similar division of scripts / separation of concerns is possible?
So if you look at the example I posted here:
https://stackoverflow.com/a/55064995/7710456
I'll expand on it a bit.
I took another look at it and created a module manifest for all of the modules, and all of those modules need to follow standards for PowerShell modules (in a folder with the same name as the PowerShell module, in a location that is present in the PSModulePath)
Write-BazFunctions.psm1:
Function Write-Baz {
return "Baz"
}
Write-BarFunctions.psm1:
Function Write-Bar {
return "Bar"
}
Function Write-BarBaz {
$bar = Write-Bar;
$baz = Write-Baz;
return ("{0}{1}" -f $bar, $baz)
}
Write-FooFunctions.psm1
Function Write-Foo {
return "Foo"
}
Function Write-FooBar {
$foo = Write-Foo
$bar = Write-Bar
return ("{0}{1}" -f $foo, $bar)
}
Function Write-FooBarBaz {
$foobar = Write-FooBar
$baz = Write-Baz
return ("{0}{1}" -f $foobar, $baz)
}
Function Write-FooBazBar {
$foo = Write-Foo
$bar = Write-Bar
$baz = Write-Baz
return ("{0}{1}{2}" -f $foo, $bar, $baz)
}
Now - differences. In the manifest for Write-BarFunctions (note required, not nested):
RequiredModules = #('Write-BazFunctions')
note another difference from my original answer linked above I was targeting the psm1 files directly, instead, reference them just by the module name.
Once I did this, I was able to import Write-FooFunctions and all of the functions came available. Since Write-BarBaz in Write-BarFunctions calls Write-Baz from Write-BazFunctions you can see that this will allow for nested modules to reference one another.
Good afternoon
Unfortunately, PowerShell is not able to detect the ParameterSet by the Parameter Types, for example: If the 2nd Parameter is passed as a Int, then select ParameterSet1, otherwise use ParameterSet2.
Therefore I would like to manually detect the passed Parameter-Combinations.
Is it possible to get the list of passed parameters in DynamicParam, something like this?:
Function Log {
[CmdletBinding()]
Param ()
DynamicParam {
# Is is possible to access the passed Parameters?,
# something like that:
If (Args[0].ParameterName -eq 'Message') { … }
# Or like this:
If (Args[0].Value -eq '…') { … }
}
…
}
Thanks a lot for any help and light!
Thomas
This first finding was wrong!:
"I've found the magic, by using $PSBoundParameters we can access the passed parameters."
This is the correct but very disappointing answer:
It's very annoying and unbelievable, but it looks like PowerShell does not pass any information about the dynamically passed arguments.
The following example used the New-DynamicParameter function as defined here:
Can I make a parameter set depend on the value of another parameter?
Function Test-DynamicParam {
[CmdletBinding()]
Param (
[string]$FixArg
)
DynamicParam {
# The content of $PSBoundParameters is just
# able to show the Params declared in Param():
# Key Value
# --- -----
# FixArg Hello
# Add the DynamicParameter str1:
New-DynamicParameter -Name 'DynArg' -Type 'string' -HelpMessage 'DynArg help'
# Here, the content of $PSBoundParameters has not been adjusted:
# Key Value
# --- -----
# FixArg Hello
}
Begin {
# Finally - but too late to dynamically react! -
# $PSBoundParameters knows all Parameters (as expected):
# Key Value
# --- -----
# FixArg Hello
# DynArg World
}
Process {
…
}
}
# Pass a fixed and dynamic parameter
Test-DynamicParam -FixArg 'Hello' -DynArg 'World'
I have a main script that I am running. What it does is read through a directory filled with other powershell scripts, dot includes them all and runs a predefined method in each made up of the first portion of the dot delimited file name. Example:
Run master.ps1
Master.ps1 dot sources .\resource\sub.ps1
Sub.ps1 has defined a function called 'dosub'
Master.ps1 runs 'dosub' using Invoke-Expression
Also defined in sub.ps1 is the function 'saysomething'. Implemented in'dosub' is a call to 'saysomething'.
My problem is I keep getting the error:
The term 'saysomething' is not recognized as the name of a cmdlet,
function, script file, or operable program. Check the spelling of the
name, or if a path was included, verify that the path is correct and
try again.
Why can't the method 'dosub' find the method 'saysomething' which is defined in the same file?
master.ps1:
$handlersDir = "handlers"
$handlers = #(Get-ChildItem $handlersDir)
foreach ( $handler in $handlers ) {
. .\$handlersDir\$handler
$fnParts = $handler.Name.split(".")
$exp = "do" + $fnParts[0]
Invoke-Expression $exp
}
sub.ps1:
function saysomething() {
Write-Host "I'm here to say something!"
}
function dosub() {
saysomething
Write-Host "In dosub!"
}
Your code works on my system. However you can simplify it a bit:
$handlersDir = "handlers"
$handlers = #(Get-ChildItem $handlersDir)
foreach ( $handler in $handlers )
{
. .\$handlersDir\$handler
$exp = "do" + $handler.BaseName
Write-Host "Calling $exp"
& $exp
}
Note the availability of the BaseName property. You also don't need to use Invoke-Expression. You can just call the named command ysing the call (&) operator.
What you have given works as needed. You probably don't have the directories etc proper on your machine. Or you are running something else and posting a different ( working!) code here.
You can also make following corrections:
. .\$handlersDir\$handler
instead of above you can do:
. $handler.fullname
Instead the splitting of the filename you can do:
$exp = "do" + $handler.basename