Batch file to change directory run from within powershell does nothing
Asked Answered
A

3

6

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.

Adolescence answered 16/4, 2019 at 11:25 Comment(4)
You can change the logic and instead of executing dev.bat inside the powershell, your dev.bat can at the end execute powershell itsefl in directory you want.Bodice
If you open a Command Prompt window and enter CD /? you will be able to read the usage information for the command to change directory. When changing drives, you use the /D option, when just changing paths, you omit the /D. In C:\dev.bat you should use. CD /D "W:\dev", (the doublequotes aren't necessary, but are best practice).Bethune
At the PowerShell prompt, to change directory you use Set-Location, (which has an alias CD). The method of doing that is generally Set-Location -Path "W:\dev", (CD "W:\dev" in its shortened form).Bethune
/d works only from within CMD and not from within powershell itself. It also only saves me one line in my bat - the point is, CD inside a bat doesn't affect the location in the powershell sessions. I also want to just type dev and not Set-Location W:\my\possibly\long\path...Adolescence
H
8

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.

Hedelman answered 16/4, 2019 at 11:48 Comment(0)
W
0

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

Whacking answered 3/7, 2021 at 13:44 Comment(0)
A
-1

@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>
Adolescence answered 16/4, 2019 at 12:58 Comment(4)
The idea to create custom quick-CD functions on demand is a neat one, but I recommend against calling them aliases, given that in PowerShell aliases can only ever be aliases for command names, and cannot not wrap entire calls with arguments; there's no reason to involve alias functionality at all, so the generator function can be greatly simplified - please see my updated answer (though note that my solution is focused just on creating custom-CD functions and therefore accepts just the target path, not the whole Set-Location call - it's easy to generalize, though).Hedelman
While I appreciate the question and the idea behind your solution, I think the terminology confusion and the convoluted implementation of the generator function are ultimately more harmful than beneficial to future readers.Hedelman
You are right that I have added more abstraction than is strictly required to solve my problem and that your answer is the correct answer to why what I was trying can't work. My focus is on a quick, pragmatic, it just works, way to alias command shortcuts (in the non-technical sense) and not necessarily a long function which, if correctly imported, will do amazing things. I therefore disagree with your opinion of not using aliases here: that's exactly what Aliases are for.Adolescence
What you're looking for is a command wrapper; how that is technically implemented varies by language: in PowerShell, you need a function - and only a function, whereas in Bash you can do it with an alias (within limits). Using the term alias loosely, even though it has a distinct technical meaning in PowerShell, is ill-advised. The shortest expression of your intent is a function such as function dev { Set-Location W:\Dev } - no alias needed; it is what the New-QuickCD function in my answer creates - no unnecessary detours via auto-named functions and aliases.Hedelman

© 2022 - 2024 — McMap. All rights reserved.