How to save function in powershell? - powershell

Hi so i've tried to make my first function. A simple one that will restart powershell in windows terminal. And it's working.
function Restart-PowerShell{
Get-Process -Id $PID | Select-Object -ExpandProperty Path | ForEach-Object { Invoke-Command { & "$_" } -NoNewScope }
But when i restart powershell the function disappear, it doesn't save it. How can i save it? I've tried to look around but couldn't find any way to save

You save PowerShell functions by simply putting their definition in a file on disk:
"function Restart-PowerShell {${function:Restart-PowerShell}}" |Set-Content Restart-PowerShell.ps1
This will write it to a file Restart-PowerShell.ps1 in the current directory.
Next time you need to re-use your function, it's as simple as dot-sourcing the file:
PS ~> . .\path\to\Restart-PowerShell.ps1
PS ~> Restart-PowerShell # now the function is available in the current session

Mathias R. Jessen's helpful answer shows how to save your function's definition to a custom file that you can dot-source on demand in future sessions.
However, there is a file that is dot-sourced automatically when a PowerShell session starts (unless explicitly suppressed with -NoProfile via the PowerShell CLI): your $PROFILE file, and that's where customizations of your sessions - such as custom functions and aliases - are typically stored.
Therefore, if you add your function to your $PROFILE file, it automatically becomes available in future sessions too.
You can open $PROFILE in your text editor or, building on Mathias' technique, add the function programmatically, as follows, which ensures on-demand creation of the file and its parent directory (on a pristine machine, neither exists):
# Make sure the $PROFILE file exists.
If (-not (Test-Path $PROFILE)) { $null = New-Item -Force $PROFILE }
# Append the function definition to it.
#"
function Restart-PowerShell {
${function:Restart-PowerShell}
}
"# | Add-Content $PROFILE
Note: To reload your profile mid-session after having modified $PROFILE, use . $PROFILE.

Related

Powershell: Why can't I add aliases in functions?

I tried adding several aliases in my powershell profile script. For organization reasons I wanted to keep those in a function which I would call at the end of said profile. I discovered that, while the function can be called from inside or outside the script without any problems, the aliases don't apply to my current powershell session. Only when I add them one at a time in the profile script, they would be usable.
This is my script as it is right now
function Populate-Aliases() {
New-Alias grep Select-String
New-Alias touch New-Item
New-Alias lsa dir -Force
}
Populate-Aliases
I'm also certain the script is executed when I create a new ps session, as is proven by inserting any output in the function. It's just the aliases that don't apply to my session.
I tried creating the aliases via function in the profile script, which didn't work. I also tried declaring a function from within the terminal as such:
function al(){New-Alias lsa dir -Force}
al
lsa
This did also not work which leads me to believe that I'm making some kind of mistake or creating aliases in functions is not supported (which I could not quite understand why that would be the case).
Creating an alias via New-Alias in the cli works without any problem. Also just adding the New-Alias statement to the profile script works, when it is not enclosed in a function.
-Scope
Specifies the scope in which this alias is valid. The default value is Local. For more information, see about_Scopes.
This means that, by default, the concerned alias is only available in the scope of the function:
function Test {
New-Alias Show Write-Host
Show 'This works'
}
Test
Show 'but this does not work'
Unless you will set the -scope to global:
function Test {
New-Alias -Scope Global Show Write-Host
Show 'This works'
}
Test
Show 'And this works too'
To complement iRon's helpful answer:
While a function name such as Populate-Aliases - or perhaps better, using an approved verb, Add-CustomAliases - does suggest modification of the caller's state, it is generally better to let the caller make the choice to have its state modified, by using . , the dot-sourcing operator, which executes the specified function or script directly in the caller's scope rather than in a child scope (as is the default and as happens when you use &, the call operator).
Thus, you could leave your function as-is and simply invoke it in your $PROFILE file as follows:
# Dot-source the function call, so that it runs directly in the current scope
# (which inside $PROFILE is the *global* scope), causing the aliases to
# become globally defined.
. Populate-Aliases
Note that this technique also allows you to out-source the alias definitions to a script file; say you place them in an CustomAliases.ps1 file alongside your $PROFILE file, you can then define them globally as follows:
# Ditto, via an external .ps1 file.
. $PSScriptRoot/CustomAliases.ps1
The only challenge is that not using . for invocation then becomes effectively a quiet no-op. The function's / script's comment-based help could make that clear, but you can also implement a runtime check to enforce dot-sourced invocation:
function Add-CustomAliases {
# Ensure that the function was invoked with dot-sourcing.
if ($MyInvocation.InvocationName -ne '.') {
throw "Please invoke this function dot-sourced."
}
New-Alias grep Select-String
New-Alias touch New-Item
New-Alias lsa dir -Force
}
. Add-CustomAliases # OK
Add-CustomAliases # Throws an error, due to using dot-sourcing.
Note: With the script-file implementation, an extended check is necessary for robustness (see this answer).
# Content of CustomAliases.ps1
# Ensure that the script was invoked with dot-sourcing.
if (-not ($MyInvocation.InvocationName -eq '.' -or $MyInvocation.Line -eq '')) {
throw "Please invoke this script dot-sourced."
}
New-Alias grep Select-String
New-Alias touch New-Item
New-Alias lsa dir -Force

doskey to print `$env:Path -split ';'` (in PowerShell), except that doesn't work

Entering $env:path at the prompt correctly returns:
C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps;...
Entering $env:Path -split ';' at the prompt correctly returns:
C:\WINDOWS\system32
C:\WINDOWS
C:\WINDOWS\System32\WindowsPowerShell\v1.0\
C:\WINDOWS\system32\config\systemprofile\AppData\Local\Microsoft\WindowsApps
...
Entering doskey envpath=$env:Path -split ';' at the prompt causes some pretty weird stuff that I've asked elsewhere about cleaning up, but among other things appears to evaluate $env:Path at the time I'm defining the macro.
However the desired result is for it to evaluate $env:Path later, at the time I'm running the macro. How do I create a macro to do that?
Don't try to use doskey.exe in PowerShell: To get it to work at all, you'd have to unload the module that handles interactive command-line editing in PowerShell, PSReadLine - which would take away a lot of useful features. See this answer for background information.
Instead:
Define the desired functionality as a function.
Add that function to your $PROFILE file, so that it is available by default in future PowerShell sessions.
The following demonstrates this technique:
# Make sure the $PROFILE file exists.
if (-not (Test-Path $PROFILE)) {
$null = New-Item -Force $PROFILE
}
# Add the desired functionality as a function definition.
#'
function envpath { $env:Path -split ';' }
'# | Add-Content $PROFILE
Note the use of a (verbatim) here-string (#'<newline>...<newline>'#), which makes embedding a function definition in a string syntactically easier, since no escaping of embedded quote characters is needed.
envpath will then be defined in future PowerShell sessions (unless the session was created with the CLI's -NoProfile switch).

Batch file to change directory run from within powershell does nothing

I have a small "dev.bat" batch file on my PATH which I run to switch to my development project directory in W:\. This works fine from CMD but not when run from PowerShell (or PWSH).
I have no other problems running .bat files from PowerShell.
PS C:\> type C:\dev.bat
W:
CD W:\dev
PS C:\> dev.bat
me#computer C:\
> W:
me#computer W:\dev
> CD W:\dev
PS C:\> echo "Why did dev.bat not change directory??"
Why did dev.bat not change directory??
PS C:\> W:
PS W:\>
No, cmd /c dev.bat makes no difference.
When run from PowerShell, batch files invariably run in a (cmd.exe) child process[1], given that PowerShell itself doesn't understand the batch language.
Changing the working directory in a child process is limited to that child process (and its own children), and has no effect on the calling process; a child process cannot change the calling process' working directory.
Your only option is to:
have your batch file echo (print) the desired working directory
capture that path in PowerShell and pass it to Set-Location
If you don't want to change your batch file, use the following workaround:
Set-Location -LiteralPath (cmd /c 'dev.bat >NUL && cd')
# Or if you want to use the 'cd' alias for Set-Location and
# are confident that path never has "[" characters in it (so that
# it can't be mistaken for a wildcard expression):
cd (cmd /c 'dev.bat >NUL && cd')
If batch files needn't be involved at all, and you just want a convenient way to create custom functions that change to a predefined location (working directory), place the following function in your $PROFILE file:
# Helper function to place in $PROFILE, which generates custom quick-cd
# functions, based on a function name and target directory path.
function New-QuickCD ($Name, $LiteralPath) {
$funcDef = #"
function global:$Name { Push-Location -LiteralPath "$LiteralPath" } # quick-CD function
"#
Invoke-Expression $funcDef # define in current session too
$funcDef >> $PROFILE # append to $PROFILE
}
Note:
The generated functions use Push-Location rather than Set-Location to enable easy returning to the previous location with Pop-Location (popd).
For convenience, generated functions are also defined in the current session via Invoke-Expression[2] on creation, so you don't have to reload (dot-source) $PROFILE or open a new session before you can call the newly generated function.
Blindly appending to $PROFILE with >> means that if you redefine a function, the new definition will take effect, but the obsolete previous one will linger in the file, requiring manual cleanup; the comment # quick-CD function placed after each generated function is meant to facilitate that - see the bottom section for a more sophisticated version of New-QuickCD that updates old definitions in place.
You can make the function more robust and convenient in a variety of ways: making the parameters mandatory, verifying the path's existence (by default), resolving the path to an absolute one - again, see the bottom section.
E.g., to create a function named dev that switches to W:\dev, you'd then call:
# Generate function 'dev', which switches to 'W:\dev',
# append it to your $PROFILE file, and also define it in this session:
New-QuickCD dev W:\dev
# Call it:
dev # changes the current location to W:\dev; use 'popd' to return.
More robust, flexible New-QuickCD function:
It improves on the above version as follows:
It makes the parameters mandatory.
It verifies the existence of the target directory path.
It defines the functions with support for a -PrintOnly switch that merely prints the function's target directory, without changing to it.
It resolves a relative path to an absolute one first, so that you can run New-QuickCD foo . to define a function that switches to the absolute path of the current location.
When you redefine a function, the previous definition is automatically updated:
In order to enable this functionality $PROFILE is rewritten as a whole, using the > redirection operator.
To remove functions, you must still edit $PROFILE manually.
It comes with comment-based help; run help New-QuickCD -Examples, for instance.
function New-QuickCD {
<#
.SYNOPSIS
Creates a custom quick-CD function.
.DESCRIPTION
Creates a custom quick-CD function and appends it your $PROFILE file.
Such a function changes to a fixed location (directory) stored inside the
function, specified at creation time to allow for quickly changing to
frequently used directories using a short name.
For convenience, a newly created function is also defined for the running
session (not just for all future sessions).
The quick-CD functions use Push-Location to change location, which
enables you to easily return to the previously active location with
Pop-Location (popd).
To determine what location a given quick-CD function *would* change to,
invoke it with the -PrintOnly switch.
.PARAMETER FunctionName
The name of the quick-CD function to define.
.PARAMETER DirectoryPath
The literal path of the directory the quick-CD function should change to.
If given a relative path, it is resolved to an absolute one first.
For convenience, you may specify a *file* path, in which case that file's
parent path is used.
.NOTES
Your $PROFILE file is recreated every time you use this function, using the
> redirection operator, so as to support updating functions in place.
To *remove* a quick-CD function, edit $PROFILE manually.
.EXAMPLE
New-QuickCD dev W:\dev
Adds a 'dev' function to $PROFILE, which on invocation changes the current
location to W:\dev
* Call just 'dev' to change to W:\dev. Use popd to return to the previous
location.
* Call 'dev -PrintOnly' to print what location function 'dev' *would*
change to.
.EXAMPLE
New-QuickCD proj .
Adds a 'proj' function to $PROFILE, which on invocation changes to the
the location that is current at the time of calling New-QuickCd.
#>
param(
[Parameter(Mandatory)] [string] $FunctionName,
[Parameter(Mandatory)] [string] $DirectoryPath
)
Set-StrictMode -Version 1; $ErrorActionPreference = 'Stop'
# Resolve the path to a full path. Fail if it doesn't exist.
$fullPath = (Resolve-Path -ErrorAction Stop -LiteralPath $DirectoryPath).Path
# As a courtesy, if the path is a *file*, we use its parent path instead.
if (Test-Path -PathType Leaf $fullPath) {
$fullPath = [IO.Path]::GetDirectoryName($fullPath)
}
# Define a comment that identifies the functions we add to $PROFILE as
# quick-CD functions.
$idComment = '<# quick-CD function generated with New-QuickCD #>'
# Generate the new function's source code...
# * on a *single line*, which enables easy filtering when updating $PROFILE below
# * with a distinctive comment at the end of the line that identifies the
# function as a quick-CD function.
# * with the global: scope specifier, which makes it easier to call the
# same definition with Invok-Expression to make the function available in the
# current session too.
$newFuncDef = #"
$idComment function global:$FunctionName { param([switch] `$PrintOnly) if (`$PrintOnly) { "$fullPath" } else { Push-Location -LiteralPath "$fullPath" } }
"#
# ... define it in the current session (doing this *before* updating $PROFILE ensures early exit if the function name is invalid)
Invoke-Expression $newFuncDef
# ... and update $PROFILE:
# Get the current content of $PROFILE
[string] $currentProfileContent = if (Test-Path -LiteralPath $PROFILE) { Get-Content -Raw -LiteralPath $PROFILE }
# Try to replace an existing definition.
$newProfileContent = $currentProfileContent -replace ('(?m)^{0} function global:{1} .+$' -f [regex]::Escape($idComment), [regex]::Escape($FunctionName)), $newFuncDef
if (-not $currentProfileContent -or $newProfileContent -ceq $currentProfileContent) { # Profile didn't exist or nothing was replaced -> we must append the new definition.
$newProfileContent = $newProfileContent.TrimEnd() + [Environment]::NewLine * 2 + $newFuncDef
}
# Write the file.
$newProfileContent > $PROFILE
}
[1] By contrast, batch files run in-process when invoked from cmd.exe, analogous to how PowerShell runs its *.ps1 scripts in-process.
POSIX-like shells such as Bash, on the other hand, by default run their scripts in a child process, except when sourcing is used (., source)
[2] While this is a safe use of Invoke-Expression, it should generally be avoided.
Another simple way to do that is create a dev.ps1 (PowerShell Script file) rather batch file, whith the follow code Set-Location -Path "W:\dev"
Note: batch file runs on CMD process as a child process even if you runs it on powershell
#mkelement is correct: there is no simple way to do this from a .bat file on your path - that is old school. The proper PowerShell way is to create an alias to a function which does what you want.
Borrowing from this answer my solution is:
Step 1: Create a reusable function to make an alias:
PS> echo 'function myAlias {
$g=[guid]::NewGuid();
$alias = $args[0]; $commands = $args[1];
echo "function G$g { $commands }; New-Alias -Force $alias G$g">>$profile
};'>>$profile
Re-start powershell (to load the above function) and then define your dev shortcut as follows:
Step 2: Create a dev shortcut/alias which gets you where you want to be:
PS> myAlias dev "Set-Location W:\dev"
Step 3: Happily use dev
PS C:\> dev
PS W:\dev>

How do I call a DOSkey Alias from Powershell?

I have DOSKey Alias that I setup like so (Aliases in Windows command prompt) calling it from the registry, and I'd like to be able to run it from powershell; is there some way to do this? All it really does is add something to the %PATH% environment variable.
If you don't have to use DOSKey and would just like to alias a command, you can use the built-in Set-Alias command like so:
Set-Alias -Name np -Value notepad++
That will, while the current powershell window is open, alias np to notepad++. Which also lets you open files with np temp.txt.
If you'd like to persist that, you could add it to your profile. You can do that by editing your Microsoft.PowerShell_profile.ps1, a shorthand is:
notepad $profile
Feel free to use notepad++ or any other editor you might have in your PATH. You can add the Set-Alias line to the bottom of the file and save and close it.
When you then open another powershell instance, it will have the example np alias available.
Below is code that will copy your DOSKEY macros into PowerShell aliases. This might or might not make sense for some commands which already have PowerShell aliases or do not work in the same way in PowerShell.
Note that you must source the command into the current shell.
PS C:\src\t> type .\Get-DoskeyMacros.ps1
& doskey /MACROS |
ForEach-Object {
if ($_ -match '(.*)=(.*)') {
Set-Alias -Name $Matches[1] -Value $Matches[2]
}
}
PS C:\src\t> . .\Get-DoskeyMacros.ps1
NOTES: (ht mklement0)
This code presumes that the doskey aliases have already been defined, which is not likely in a normal PowerShell session. However, you can start the PowerShell session from a cmd session that has them defined or run a batch file from PowerShell that defines them.
A potential translation-to-alias problem is that doskey macros support parameters (e.g., $*), whereas PowerShell aliases do not.
#lit's script didn't work for me.
Below is a script that worked. It copies the generated Set-Alias commands to the $profile script.
doskey /MACROS |
ForEach-Object {
if ($_ -match '(.*)=(.*)') {
echo ('Set-Alias -Name '+$Matches[1]+' -Value '+$Matches[2])
}
} > $profile
Notes:
Like the original script, this must be executed in a PowerShell session started from a cmd session.
The $profile file didn't exist for me. I first had to create it, along with the containing folder.

How to reload user profile from script file in PowerShell

I want to reload my user profile from a script file. I thought that dot sourcing it from within the script file would do the trick, but it doesn't work:
# file.ps1
. $PROFILE
However, it does work if I dot source it from PowerShell's interpreter.
Why do I want to do this?
I run this script every time I update my profile and want to test it, so I'd like to avoid having to restart PowerShell to refresh the environment.
If you want to globally refresh your profile from a script, you will have to run that script "dot-sourced".
When you run your script, all the profile script runs in a "script" scope and will not modify your "global" scope.
In order for a script to modify your global scope, it needs to be "dot-source" or preceded with a period.
. ./yourrestartscript.ps1
where you have your profile script "dot-sourced" inside of "yourrestartscript.ps1". What you are actually doing is telling "yourrestartscript" to run in the current scope and inside that script, you are telling the $profile script to run in the script's scope. Since the script's scope is the global scope, any variables set or commands in your profile will happen in the global scope.
That doesn't buy you much advantage over running
. $profile
So, the approach that you marked as the answer may work inside the Powershell command prompt, but it doesn't work inside PowerShell ISE (which, to me, provides a superior PowerShell session) and probably won't work right in other PowerShell environments.
Here's a script that I have been using for a while, and it has worked very well for me in every environment. I simply put this function into my Profile.ps1 at ~\Documents\WindowsPowerShell, and whenever I want to reload my profile, I dot-source the function, i.e.
. Reload-Profile
Here's the function:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
. $_
}
}
}
& $profile
works to reload the profile.
If your profile sets aliases or executes imports which fail then you will see errors because they were already set in the previous loading of the profile.
Why are you trying to do this?
Because it is likely to create duplicates (appends to $env:path) and problems with setting constant/readonly objects causing errors.
There was a thread on this topic recently on microsoft.public.windows.powershell.
If you are trying to reset the state of the session there is no way to do this, even using an inner scope ($host.EnterNestedPrompt()) because of the ability to set variables/aliases/... at "all scope".
I found this workaround:
#some-script.ps1
#restart profile (open new powershell session)
cmd.exe /c start powershell.exe -c { Set-Location $PWD } -NoExit
Stop-Process -Id $PID
A more elaborated version:
#publish.ps1
# Copy profile files to PowerShell user profile folder and restart PowerShell
# to reflect changes. Try to start from .lnk in the Start Menu or
# fallback to cmd.exe.
# We try the .lnk first because it can have environmental data attached
# to it like fonts, colors, etc.
[System.Reflection.Assembly]::LoadWithPartialName("System.Diagnostics")
$dest = Split-Path $PROFILE -Parent
Copy-Item "*.ps1" $dest -Confirm -Exclude "publish.ps1"
# 1) Get .lnk to PowerShell
# Locale's Start Menu name?...
$SM = [System.Environment+SpecialFolder]::StartMenu
$CurrentUserStartMenuPath = $([System.Environment]::GetFolderPath($SM))
$StartMenuName = Split-Path $CurrentUserStartMenuPath -Leaf
# Common Start Menu path?...
$CAD = [System.Environment+SpecialFolder]::CommonApplicationData
$allUsersPath = Split-Path $([System.Environment]::GetFolderPath($CAD)) -Parent
$AllUsersStartMenuPath = Join-Path $allUsersPath $StartMenuName
$PSLnkPath = #(Get-ChildItem $AllUsersStartMenuPath, $CurrentUserStartMenuPath `
-Recurse -Include "Windows PowerShell.lnk")
# 2) Restart...
# Is PowerShell available in PATH?
if ( Get-Command "powershell.exe" -ErrorAction SilentlyContinue ) {
if ($PSLnkPath) {
$pi = New-Object "System.Diagnostics.ProcessStartInfo"
$pi.FileName = $PSLnkPath[0]
$pi.UseShellExecute = $true
# See "powershell -help" for info on -Command
$pi.Arguments = "-NoExit -Command Set-Location $PWD"
[System.Diagnostics.Process]::Start($pi)
}
else {
# See "powershell -help" for info on -Command
cmd.exe /c start powershell.exe -Command { Set-Location $PWD } -NoExit
}
}
else {
Write-Host -ForegroundColor RED "Powershell not available in PATH."
}
# Let's clean up after ourselves...
Stop-Process -Id $PID
This is only a refinement of the two line script in guillermooo's answer above, which did not get the new PowerShell window into the correct directory for me. I believe this is because $PWD is evaluated in the new PowerShell window's context, which is not the value we want set-location to process.
function Restart-Ps {
$cline = "`"/c start powershell.exe -noexit -c `"Set-Location '{0}'" -f $PWD.path
cmd $cline
Stop-Process -Id $PID
}
By rights it shouldn't work, as the command line it spits out is malformed, but it seems to do the job and that's good enough for me.
since I stumbled onto this several years later, I thought to add that you can use the invocation operator: & to load your profile with the default variable to your profile: $profile.
so, if your session somehow fails to load your profile (happens to me with cmder/conemu) just type:
& $profile
I used this to troubleshoot what profile was taking forever to load.
Start Run:
powershell_ise -noprofile
Then i ran this:
function Reload-Profile {
#(
$Profile.AllUsersAllHosts,
$Profile.AllUsersCurrentHost,
$Profile.CurrentUserAllHosts,
$Profile.CurrentUserCurrentHost
) | % {
if(Test-Path $_){
Write-Verbose "Running $_"
$measure = Measure-Command {. $_}
"$($measure.TotalSeconds) for $_"
}
}
}
. Reload-Profile
Thank you #Winston Fassett for getting me closer to finding my issue.
Pseudo Alias (simulate keys)
If you just want a function to work like an alias in the console, just simulate the key presses to get around having to use the dot source.
# when "reload" is typed in the terminal, the profile is reloaded
# use sendkeys to send the enter key to the terminal
function reload {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait(". $")
[System.Windows.Forms.SendKeys]::SendWait("PROFILE")
[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")
}
screenshot of it working