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>
Related
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
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.
I have the following string
"C:\ProgramData\Package
Cache{6b95042e-f763-4850-9136-d004dd0d0a9b}\AzInfoProtection.exe"
/uninstall
I need to execute the above string as below
First-line
cd C:\ProgramData\Package Cache\{6b95042e-f763-4850-9136-d004dd0d0a9b}
The second line (note there is no exe)
AzInfoProtection /uninstall
Variables are generally executed like below in PowerShell
Invoke-Expression $cmd
But how to split the above string into multiple lines for execution. Then I need to remove the quote and then exe.
It's a bit hard to understand the ask here but I think I follow. Let me know if I'm off base or misunderstanding what you're trying to do.
$commandString = '"C:\ProgramData\Package Cache{6b95042e-f763-4850-9136-d004dd0d0a9b}\AzInfoProtection.exe" /uninstall'
# Get command parent directory
if( $commandString -match '^".*?"' ) {
$runInDir = Split-Path -Parent $Matches[0]
}
# Change directories (use the location stack for easy traversal)
Push-Location $runInDir
# Run program
Invoke-Expression $commandString
# Change back to previous directory
Pop-Location
This works by checking if the string starts with a quote-enclosed string (escaped quotes should not need to be handled within filepaths), and if so gets the first match from the $Matches object. $Matches is an automatic variable which is populated whenever you get a $True result using the [-match operator][1]. With the command path extracted, we use Split-Path to get the parent container relative to the filepath.
Then use Push-Location to change directories. Push-Location works like Set-Location (aliased to cd) except it tracks the directories you leave and enter as a stack. Its sibling cmdlet Pop-Location is used further on to return to the previous location.
Finally, we use Invoke-Expression to run your command. After this completes use Pop-Location to return to the previous directory. Keep the following in mind:
You should take note that the use of Invoke-Expression is often implemented insecurely, and so you should consider heeding the warning on the documentation I've linked to and consider parameterizing your command if your $commandString is actually populated from a generated file, provided by a parameter, or another other outside source.
Note: You mentioned this in your question:
The second line (note there is no exe)
Windows doesn't care if you omit the extension for executable types when executing them. You can run AzInfoProtection.exe with or without the .exe at the end. So unless I'm missing something this detail doesn't have any bearing on how this code works.
To run the string you can pipe it to cmd to run it using:
$commandString | cmd
I can remove an alias like so:
Remove-Item Alias:wget
Then trying the alias gives the expected result:
PS > wget
wget : The term 'wget' is not recognized as the name of a cmdlet, function,
script file, or operable program.
However, if I put the same into a script,
PS > cat wget.ps1
Remove-Item Alias:wget
wget
it gives unexpected result
cmdlet Invoke-WebRequest at command pipeline position 1
Supply values for the following parameters:
Uri:
The following seems to work
If (Test-Path Alias:wget) {Remove-Item Alias:wget}
If (Test-Path Alias:wget) {Remove-Item Alias:wget}
wget
The first line removes the Script level alias, and the second removes the Global level alias. Note that both need to be tested, because if the Global doesn't exist the Script isn't created.
Also this misbehaves in that it modifies the parent without being dot sourced.
Alternatively you can just dot source your original and that would work
. .\wget.ps1
The reason behind this is scope...
There are four scopes: local, script, private, and global
The rules for variables, functions and aliases say that if they are not defined in the current scope then PowerShell will search the parent scopes.
The default for an alias is set to allscope, which make it visible from any child scopes and also they are inherited into any new child scope (I think that would be a fair definition).
Get-Alias -Name wget | select Name,options
Name Options
wget AllScope
Once you remove the alias that is the script scope (scope 0), it will then find the alias from the global/parent scope (scope 1).
When you dot source it, you are just saying to run the script in the calling/global scope in the first place, so you are removing the Global alias by default.
Try these...
E.g #1.
1.1) Remove the alias from the global scope.
Remove-Item -Path Alias:\wget
1.2) Create a new one (global scope) and make it private.
New-Alias -Name wget -Value dir -Scope private
1.3) Now the alias is not visible from the nested scope.
So try run the script and it will not be found.
E.g #2.
2.1) Remove the alias from the global scope.
Remove-Item -Path Alias:\wget
2.2) Create a new one and make it AllScope
New-Alias -Name wget -Value dir -Option AllScope
2.3) Now run your script and it will work fine (using the new alias dir from the parent scope)
You could try the same with variables as well. They should be easier to demo (and play around with) since you can more easily play with the scope parameter when you use New/Get/Set/Remove -scope 1 or scope 0 OR use the scope modifiers, for example, $global:a.
Get-Command -Noun variable -ParameterName scope
Clearly, some of the other answers are on-track and have identified the fact that the alias is present in several scopes.
But if you're trying to invoke wget.exe rather than Invoke-WebRequest (or wget.ps1 ) in some script, the simplest answer might be to put the ".exe" extension on the command name when you invoke it. That way you don't have to fiddle with the aliases.
It looks like when you run the script from the PowerShell console it inherits the alias drive from that session. So if wget is there it works, and if it's not it fails. If instead you run the script in a new process it gets removed normally.
Here's the code I used for my demo:
Remove-Item -Path Alias:\wget -Force
wget
Start-Sleep -Seconds 100
The start-sleep will keep your console from closing immediately. Try running that script in a new process. I used the run prompt and PowerShell itself by typing `powershell.exe -file pathtoscript '
I have a PowerShell script that does some stuff using the script’s current directory. So when inside that directory, running .\script.ps1 works correctly.
Now I want to call that script from a different directory without changing the referencing directory of the script. So I want to call ..\..\dir\script.ps1 and still want that script to behave as it was called from inside its directory.
How do I do that, or how do I modify a script so it can run from any directory?
Do you mean you want the script's own path so you can reference a file next to the script? Try this:
$scriptpath = $MyInvocation.MyCommand.Path
$dir = Split-Path $scriptpath
Write-host "My directory is $dir"
You can get a lot of info from $MyInvocation and its properties.
If you want to reference a file in the current working directory, you can use Resolve-Path or Get-ChildItem:
$filepath = Resolve-Path "somefile.txt"
EDIT (based on comment from OP):
# temporarily change to the correct folder
Push-Location $dir
# do stuff, call ant, etc
# now back to previous directory
Pop-Location
There's probably other ways of achieving something similar using Invoke-Command as well.
There are answers with big number of votes, but when I read your question, I thought you wanted to know the directory where the script is, not that where the script is running. You can get the information with powershell's auto variables
$PSScriptRoot # the directory where the script exists, not the
# target directory the script is running in
$PSCommandPath # the full path of the script
For example, I have a $profile script that finds a Visual Studio solution file and starts it. I wanted to store the full path, once a solution file is started. But I wanted to save the file where the original script exists. So I used $PsScriptRoot.
If you're calling native apps, you need to worry about [Environment]::CurrentDirectory not about PowerShell's $PWD current directory. For various reasons, PowerShell does not set the process' current working directory when you Set-Location or Push-Location, so you need to make sure you do so if you're running applications (or cmdlets) that expect it to be set.
In a script, you can do this:
$CWD = [Environment]::CurrentDirectory
Push-Location $MyInvocation.MyCommand.Path
[Environment]::CurrentDirectory = $PWD
## Your script code calling a native executable
Pop-Location
# Consider whether you really want to set it back:
# What if another runspace has set it in-between calls?
[Environment]::CurrentDirectory = $CWD
There's no foolproof alternative to this. Many of us put a line in our prompt function to set [Environment]::CurrentDirectory ... but that doesn't help you when you're changing the location within a script.
Two notes about the reason why this is not set by PowerShell automatically:
PowerShell can be multi-threaded. You can have multiple Runspaces (see RunspacePool, and the PSThreadJob module) running simultaneously withinin a single process. Each runspace has it's own $PWD present working directory, but there's only one process, and only one Environment.
Even when you're single-threaded, $PWD isn't always a legal CurrentDirectory (you might CD into the registry provider for instance).
If you want to put it into your prompt (which would only run in the main runspace, single-threaded), you need to use:
[Environment]::CurrentDirectory = Get-Location -PSProvider FileSystem
This would work fine.
Push-Location $PSScriptRoot
Write-Host CurrentDirectory $CurDir
I often used the following code to import a module which sit under the same directory as the running script. It will first get the directory from which powershell is running
$currentPath=Split-Path ((Get-Variable
MyInvocation -Scope
0).Value).MyCommand.Path
import-module "$currentPath\sqlps.ps1"
I made a one-liner out of #JohnL's solution:
$MyInvocation.MyCommand.Path | Split-Path | Push-Location
Well I was looking for solution for this for a while, without any scripts just from CLI. This is how I do it xD:
Navigate to folder from which you want to run script (important thing is that you have tab completions)
..\..\dir
Now surround location with double quotes, and inside them add cd, so we could invoke another instance of powershell.
"cd ..\..\dir"
Add another command to run script separated by ;, with is a command separator in powershell
"cd ..\..\dir\; script.ps1"
Finally Run it with another instance of powershell
start powershell "cd..\..\dir\; script.ps1"
This will open new powershell window, go to ..\..\dir, run script.ps1 and close window.
Note that ";" just separates commands, like you typed them one by one, if first fails second will run and next after, and next after... If you wanna keep new powershell window open you add -noexit in passed command . Note that I first navigate to desired folder so I could use tab completions (you couldn't in double quotes).
start powershell "-noexit cd..\..\dir\; script.ps1"
Use double quotes "" so you could pass directories with spaces in names e.g.,
start powershell "-noexit cd '..\..\my dir'; script.ps1"