How to get the file directory for a custom cmdlet - powershell

The way our business is set up, our custom cmdlets are spread out across the network in several different larger files. We refer to these files in the usual "Microsoft.PowerShell_profile.ps1"
Is there something I can run within Powershell to find where a cmdlet is running from instead of manually going through all the files referenced in "Microsoft.PowerShell_profile.ps1" to find it?
E.g. if I have written a cmdlet called Get-UserExpiry and it is saved in C:\User\Name\Documents\CustomCmds.ps1, and I include that file path in Microsoft.PowerShell_profile.ps1, is there a command I can use to find that file path if all I know is the cmdlet name?

Get-Command is what you need. That being said, depending on the command you are testing and the type of the command (external application, function, cmdlet, profile function), the command path won't always be assigned to the same property / subproperty.
Here's a way to get the path no matter where it is disclosed.
Function definition
Here we check the possible locations of the path based on the Get-Command result, filter out everything that is $null or empty and pick the first result we get.
Function Get-CommandLocation($Command) {
$definition = (Get-Command -Name $Command)
#(
$definition.Module.Path
$definition.Source
$definition.ScriptBlock.File
) | Where-Object { $_ } | Select-Object -First 1
}
Examples
Get-CommandLocation Get-Item # Native cmdlet
# Value obtained from (Get-Command -Name 'Get-Item').Module.Path
# Return C:\Windows\system32\WindowsPowerShell\v1.0\Modules\Microsoft.PowerShell.Management\Microsoft.PowerShell.Management.psd1
Get-CommandLocation Edit-Profile # Custom profile function (Test with a function in your profile (if any))
# Value obtained from(Get-Command -Name 'Edit-Profile').ScriptBlock.File
# Return C:\Users\SagePourpre\Documents\WindowsPowerShell\Microsoft.VSCode_profile.ps1
Get-CommandLocation New-ExternalHelp # PlatyPS module downloaded from the gallery
# Value obtained from (Get-Command New-ExternalHelp).Module.Path
# Return C:\Program Files\WindowsPowerShell\Modules\platyPS\0.14.2\platyPS.psm1
Get-CommandLocation -command cmd # External Program
# Value located in (Get-Command -Name 'cmd').Source
# Return C:\Windows\system32\cmd.exe

Related

How can I use an Alias as function parameter, In PowerShell?

I have an Alias named "github", for example.
Set-Alias -Name github -Value "C:\UserName\Folder\github"
Also I have a function named "goto", for example:
Function goto ( [string] alias ) { Set-Location -Path $alias }
How can I use this function on my terminal passing as parameter an alias.
> goto github
Output: `cannnot find "CurrentLocation\github" folder, cause the function not resolve the alias as parameter
An Alias, in simple terms, is just an association to a cmdlet, function, script file, or executable program. When we Set-Alias, what's happening is that a new AliasInfo instance is added to the Alias: PSDrive - See about Alias Provider for details.
So, to recap, when we do:
Set-Alias -Name github -Value "C:\UserName\Folder\github"
What's actually happening is that the the alias with name github is being associated with the function with name C:\UserName\Folder\github, a fun but useless demonstration:
PS /> Get-Alias github
CommandType Name Version Source
----------- ---- ------- ------
Alias github -> C:\UserName\Folder\github
PS /> ${function:C:\UserName\Folder\github} = { 'hello' }
PS /> C:\UserName\Folder\github
hello
PS /> github
hello
In my opinion, an easy workaround for what you're looking for would be to have one function (github) that outputs a string containing the path you want to access and a second function (goto) that receives the pipeline input, for this we use a non-advanced function that leverages the automatic variable $input:
function goto { Set-Location -Path $input }
function github { 'path\to\something' }
github | goto
Set-Alias would also be a valid use case for Set-Location since this cmdlet already accepts values from pipeline:
Set-Alias -Name goto -Value Set-Location
function github { 'path\to\something' }
github | goto
Alternatively, JPBlanc suggested, in a now deleted answer:
function goto ([string] $alias) { Set-Location -Path $alias }
function githubpath { "C:\UserName\Folder\github" }
Which would also work for a positional argument but mandates the use of the grouping operator (..):
goto (github)
Why ?
Without the grouping operator, the function goto would see github as an argument and NOT as a separated function which we want to invoke.
The operator forces the github function to be invoked first or, in other words, to take precedence and to produce it's output before the invocation of the goto function.

Creating powershell modules from multiple files, referencing with module

I creating a PowerShell script module using separate source files. What is the canonical way to reference source functions internal to the module from other internal source files?
For example if my module is created from PS source code in files "foo" and "bar"; and a function in "foo" needs to call a function in "bar", what is the best way to do that?
It doesn't seem like dot-sourcing would be a good idea. Nor does making the component files ("foo" and "bar") psm1 files. Is this the idea behind the "ScriptsToProcess" field in the psd1 file?
Am I thinking about this wrong (non-"PowerShelly")? Should I just dump everything into a single psm1?
I've personally followed the practice laid out by RamblingCookieMonster in his blog here: http://ramblingcookiemonster.github.io/Building-A-PowerShell-Module/
Which is to organise your functions in to separate .ps1 files under sub-folders \Public and \Private. Public contains the functions the user should be able to call directly, Private is for the functions that are only used internally by your module.
Then in the .psm1 file you load the functions via a loop and dot sourcing as follows:
#Get public and private function definition files.
$Public = #( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = #( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue )
#Dot source the files
Foreach($import in #($Public + $Private))
{
Try
{
. $import.fullname
}
Catch
{
Write-Error -Message "Failed to import function $($import.fullname): $_"
}
}
# Here I might...
# Read in or create an initial config file and variable
# Export Public functions ($Public.BaseName) for WIP modules
# Set variables visible to the module and its functions only
Export-ModuleMember -Function $Public.Basename
Source of this example: https://github.com/RamblingCookieMonster/PSStackExchange/blob/db1277453374cb16684b35cf93a8f5c97288c41f/PSStackExchange/PSStackExchange.psm1
You should then also explicitly list your Public function names in your .psd1 module manifest file under the FunctionsToExport setting. Doing this allows these functions to be discoverable and the module to be auto-loaded when they are used.
Since I recently had to do this myself, I am sharing my solution. I have recently started grouping functions in psm1 files. These can be compiled into a single module with a single manifest.
This allows me to have groups of functions that can be packaged with multiple modules.
Write-BarFunctions.psm1
Function Write-Bar {
return "Bar"
}
Function Write-Baz {
return "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)
}
Which are combined into a single module like this:
(formatted for readability)
New-ModuleManifest
-Path .\Write-FooBarBazCombos
-NestedModules #('.\FooFunctions\Write-FooFunctions.psm1', '.\BarFunctions\Write-BarFunctions.psm1')
-Guid (New-Guid)
-ModuleVersion '1.0.0.0'
-Description 'demonstrate multiple psm1 files as 1 powershell module with 1 powershell module manifest'
-PowerShellVersion $PSVersionTable.PSVersion.ToString()
-FunctionsToExport #('Write-Foo', 'Write-Bar','Write-FooBar', 'Write-FooBarBaz')
PowerShell output:
PS C:\LWC\scripting-misc\module-manifest-multiple-files-example> New-ModuleManifest -Path .\Write-FooBarBazCombos.psd1
-NestedModules #('.\Write-FooFunctions.psm1', '.\Write-BarFunctions.psm1') -Guid (New-Guid) -ModuleVersion '1.0.0.0' -D
escription 'demonstrate multiple psm1 files as 1 powershell module with 1 powershell module manifest' -PowerShellVersio
n $PSVersionTable.PSVersion.ToString() -FunctionsToExport #('Write-Foo', 'Write-Bar','Write-FooBar', 'Write-FooBarBaz')
PS C:\LWC\scripting-misc\module-manifest-multiple-files-example> Import-Module .\Write-FooBarBazCombos.psd1
PS C:\LWC\scripting-misc\module-manifest-multiple-files-example> Get-Command -Module Write-FooBarBazCombos
CommandType Name Version Source
----------- ---- ------- ------
Function Write-Bar 1.0.0.0 Write-FooBarBazCombos
Function Write-Foo 1.0.0.0 Write-FooBarBazCombos
Function Write-FooBar 1.0.0.0 Write-FooBarBazCombos
Function Write-FooBarBaz 1.0.0.0 Write-FooBarBazCombos
note that Write-Baz is not exposed in the imported module as it is excluded from the FunctionsToExport parameter so Write-FooBarBaz will error (intentional to show behavior).
PS C:\LWC\scripting-misc\module-manifest-multiple-files-example> Write-FooBar
FooBar
What you're left with in the directory:
PS C:\LWC\scripting-misc\module-manifest-multiple-files-example> Get-ChildItem | Select-Object Name
Name
----
Write-BarFunctions.psm1
Write-FooBarBazCombos.psd1
Write-FooFunctions.psm1
Addendum - I expanded on this answer in another question - here:
https://stackoverflow.com/a/56171985/7710456
#Ryan
I similarly assumed that dot sourcing wasn't the best choice here, but I'm not so sure anymore. I've used the NestedModules approach as well, but have run up against a specific problem. I've asked the question here:
PowerShell module, call function in NestedModule from another NestedModule
In summary I find that the PrimaryModule can call any function in any NestedModule. But one NestedModule is not able to call a function in another NestedModule.
Splitting your code out into many logical files is Developer 101 basics. So I'm really surprised there isn't a standard way of handling this.
Any help here much appreciated. Please read the linked question, it gives plenty of detail. Is the consensus that dot sourcing has to be used? Because I'm finding the module manifest way of splitting out the code very limiting.

PowerShell: provide parameters in a file

Is there a way to provide powershell parameters with a file?
At the moment I have a script which is called My_Script.ps1. To start this script I have to provide the right parameters in the command:
.\My_Script.ps1 -param1="x" -param2="x" -param3="x" -param4="x" -param5="x" -param6="x" ...
This works but it isn't a very easy way to start the script. Is it possible in powershell to use a file in which you store your parameters and to use that file when you start the script?
Example
In My_Script.ps1 I add something like:
Param(
[string]$File="Path/to/file"
)
In my file I have something like
param1="x"
param2="x"
param3="x"
param4="x"
...
To execute the script you can edit the file and just start the script with .\My_Script.ps1
Another option:
Just use a ps1 file as config file and define your variables as you would do in your main script
$Param1 = "Value"
$Param2 = 42
Then you can use dot-sourcing or import-module to get the data from the config file
. .\configfile.ps1
or
Import-Module .\Configfile.ps1
afterwards you can just use the variables
In addition to splatting you can create variables from = separated values in a file.
param1=foo
param2=bar
param3=herp
param4=derp
Don't quote the values. The parameter names should be valid for a variable (no spaces etc.)
PowerShell 3 and newer:
(Get-Content c:\params.ini -raw | ConvertFrom-StringData).GetEnumerator() |
ForEach { Set-Variable $_.name $_.value }
PowerShell 2:
([IO.File]::ReadAllText('c:\params.ini') | ConvertFrom-StringData).GetEnumerator() |
ForEach { Set-Variable $_.name $_.value }
The code creates variables in current scope. It's possible to create in a global/script/parent scope.
You can use this blog posting
for a start and declare your parameters in an ini-like format.
For sure you could also use a csv-like format and work with import-csv cmdlet.

How to pass output from a PowerShell cmdlet to a script?

I'm attempting to run a PowerShell script with the input being the results of another PowerShell cmdlet. Here's the cross-forest Exchange 2013 PowerShell command I can run successfully for one user by specifying the -Identity parameter:
.\Prepare-MoveRequest.ps1 -Identity "user#domain.com" -RemoteForestDomainController "dc.remotedomain.com" $Remote -UseLocalObject -OverwriteLocalObject -Verbose
I want to run this command for all MailUsers. Therefore, what I want to run is:
Get-MailUser | select windowsemailaddress | .\Prepare-MoveRequest.ps1 -RemoteForestDomainController "dc.remotedomain.com" $Remote -LocalForestDomainController "dc.localdomain.com" -UseLocalObject -OverwriteLocalObject -Verbose
Note that I removed the -Identity parameter because I was feeding it from each Get-MailUser's WindowsEmailAddress property value. However, this returns with a pipeline input error.
I also tried exporting the WindowsEmailAddress property values to a CSV, and then reading it as per the following site, but I also got a pipeline problem: http://technet.microsoft.com/en-us/library/ee861103(v=exchg.150).aspx
Import-Csv mailusers.csv | Prepare-MoveRequest.ps1 -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote
What is the best way to feed the windowsemailaddress field from each MailUser to my Prepare-MoveRequest.ps1 script?
EDIT: I may have just figured it out with the following foreach addition to my Import-Csv option above. I'm testing it now:
Import-Csv mailusers.csv | foreach { Prepare-MoveRequest.ps1 -Identity $_.windowsemailaddress -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote }
You should declare your custom function called Prepare-MoveRequest instead of simply making it a script. Then, dot-source the script that declares the function, and then call the function. To accept pipeline input into your function, you need to declare one or more parameters that use the appropriate parameter attributes, such as ValueFromPipeline or ValueFromPipelineByPropertyName. Here is the official MSDN documentation for parameter attributes.
For example, let's say I was developing a custom Stop-Process cmdlet. I want to stop a process based on the ProcessID (or PID) of a Windows process. Here is what the command would look like:
function Stop-CustomProcess {
# Specify the CmdletBinding() attribute for our
# custom advanced function.
[CmdletBinding()]
# Specify the PARAM block, and declare the parameter
# that accepts pipeline input
param (
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int] $Id
)
# You must specify the PROCESS block, because we want this
# code to execute FOR EACH process that is piped into the
# cmdlet. If we do not specify the PROCESS block, then the
# END block is used by default, which only would run once.
process {
Write-Verbose -Message ('Stopping process with PID: {0}' -f $ID);
# Stop the process here
}
}
# 1. Launch three (3) instances of notepad
1..3 | % { notepad; };
# 2. Call the Stop-CustomProcess cmdlet, using pipeline input
Get-Process notepad | Stop-CustomProcess -Verbose;
# 3. Do an actual clean-up
Get-Process notepad | Stop-Process;
Now that we've taken a look at an example of building the custom function ... once you've defined your custom function in your script file, dot-source it in your "main" script.
# Import the custom function into the current session
. $PSScriptRoot\Prepare-MoveRequest.ps1
# Call the function
Get-MailUser | Prepare-MoveRequest -RemoteForestDomainController dc.remotedomain.com $Remote -LocalForestDomainController dc.localdomain.com -UseLocalObject -OverwriteLocalObject -Verbose;
# Note: Since you've defined a parameter named `-WindowsEmailAddress` that uses the `ValueFromPipelineByPropertyName` attribute, the value of each object will be bound to the parameter, as it passes through the `PROCESS` block.
EDIT: I would like to point out that your edit to your post does not properly handle parameter binding in PowerShell. It may achieve the desired results, but it does not teach the correct method of binding parameters in PowerShell. You don't have to use the ForEach-Object to achieve your desired results. Read through my post, and I believe you will increase your understanding of parameter binding.
My foreach loop did the trick.
Import-Csv mailusers.csv | foreach { Prepare-MoveRequest.ps1 -Identity $_.windowsemailaddress -RemoteForestDomainController DC.remotedomain.com -RemoteForestCredential $Remote }

How to check if a cmdlet exists in PowerShell at runtime via script

I have a PowerShell script that needs to run under multiple hosts (PowerGUI, PowerShell ISE, etc...), but I am having an issue where sometimes a cmdlet doesn't exist under one of the hosts. Is there a way to check to see if a cmdlet exists so that I can wrap the code in an if block and do something else when it does not exist?
I know I could use the $host.name to section the code that is suppose to run on each host, but I would prefer to use Feature Detection instead in case the cmdlet ever gets added in the future.
I also could use a try/catch block, but since it runs in managed code I assume there is away to detect if a cmdlet is installed via code.
Use the Get-Command cmdlet to test for the existence of a cmdlet:
if (Get-Command $cmdName -errorAction SilentlyContinue)
{
"$cmdName exists"
}
And if you want to ensure it is a cmdlet (and not an exe or function or script) use the -CommandType parameter e.g -CommandType Cmdlet
This is a simple function to do what you're like to do :)
function Check-Command($cmdname)
{
return [bool](Get-Command -Name $cmdname -ErrorAction SilentlyContinue)
}
How to use (for example):
if (Check-Command -cmdname 'Invoke-WebRequest')
{
Invoke-WebRequest $link -OutFile $destination
}
else
{
$webclient.DownloadFile($link, $destination)
}
If the command is in Verb-Noun form then you can use Get-Command with Verb and Noun parameters.
# Usage:
if (Get-Command -Verb Invoke -Noun MyCommand) {
# cmdlet Invoke-MyCommand exists
}
Get-Command -Verb Get -Noun Item
# Output:
# CommandType Name Version Source
# ----------- ---- ------- ------
# Cmdlet Get-Item 7.0.0.0 #Microsoft.PowerShell.Management
Get-Command -Verb Take -Noun One
# No output.
function Take-One { [CmdletBinding()]param() }
Get-Command -Verb Take -Noun One
# Output:
# CommandType Name Version Source
# ----------- ---- ------- ------
# Function Take-One
Tested on Windows PowerShell 5.1 and PowerShell Core 7.0.
Edit 2020-11-09 Additional example. Also usage example (adapted from Keith Hill answer).