批处理文件在PowerShell中运行无法更改目录,没有任何反应

6

我在我的PATH路径下有一个小的"dev.bat"批处理文件,可以用来切换到我在W:\上的开发项目目录。这在CMD中正常工作,但是在PowerShell(或PWSH)中运行时却出现问题。

在PowerShell中运行其他 .bat 文件没有问题。

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:\>

不,cmd /c dev.bat 没有任何影响。

你可以更改逻辑,而不是在powershell中执行dev.bat,你的dev.bat最后可以在你想要的目录中执行powershell。 - j123b567
如果您打开命令提示符窗口并输入CD /?,则可以阅读更改目录的命令使用信息。更改驱动器时,使用**/D选项,仅更改路径时,省略/D。在C:\dev.bat中,您应该使用CD /D "W:\dev"**, _(双引号不是必需的,但最好是最佳实践)_。 - Compo
在 PowerShell 提示符下,要更改目录,您可以使用 Set-Location 命令 (它有一个别名 CD). 通常的做法是 Set-Location -Path "W:\dev" _(缩写形式为 CD "W:\dev")_。 - Compo
/d 只能在 CMD 中使用,而不能在 PowerShell 中使用。它只能为我的批处理文件节省一行代码 - 问题是,在批处理文件中使用 CD 命令不会影响 PowerShell 会话中的位置。我也想只输入 dev 而不是 Set-Location W:\my\possibly\long\path... - Marc
3个回答

8

当在PowerShell中运行时,批处理文件总是在(cmd.exe子进程[1]中运行,因为PowerShell本身不理解批处理语言。

更改子进程中的工作目录仅限于该子进程(及其自己的子级),对调用进程没有影响;子进程无法更改调用进程的工作目录

您唯一的选择是:

  • 使您的批处理文件回显(打印)所需的工作目录
  • 在PowerShell中捕获该路径并将其传递给Set-Location

如果您不想更改批处理文件,请使用以下解决方法:

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')

如果完全不需要批处理文件,并且您只想要一种方便的方法来创建自定义函数以更改到预定义位置(工作目录),请将以下函数放置在您的$PROFILE文件中:
# 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
}

注意:

  • 生成的函数使用Push-Location而不是Set-Location,以便轻松返回到以前的位置并使用Pop-Location (popd)。

  • 为了方便起见,在创建时生成的函数也通过Invoke-Expression[2]在当前会话中定义,因此您无需重新加载(点源)$PROFILE或在调用新生成的函数之前打开一个新会话。

  • 盲目地使用>>添加到$PROFILE意味着如果您重新定义一个函数,则新定义将生效,但过时的先前定义将留存在文件中,需要手动清理;每个生成的函数后面放置的注释# quick-CD function旨在促进此过程-有关更新就地旧定义的更复杂版本的New-QuickCD请参见底部部分。

  • 您可以通过各种方式使函数更加健壮和方便:使参数强制、验证路径的存在性(默认情况下)、将路径解析为绝对路径-同样,请参见底部部分。

例如,要创建一个名为dev的函数,用于切换到W:\dev,则需要调用:

# 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.

更强大、灵活的New-QuickCD函数:

它比上一个版本有以下改进:

  • 让参数为必填项。
  • 验证目标目录路径是否存在。
  • 定义支持-PrintOnly开关的功能,仅打印函数的目标目录而不改变目录。
  • 首先将相对路径解析为绝对路径,这样您可以运行 New-QuickCD foo . 来定义一个切换到当前位置绝对路径的函数。
  • 当您重新定义一个函数时,以前的定义会自动更新:
    • 为了启用此功能,整个$PROFILE都被重写,使用>重定向操作符。
    • 删除函数,仍然需要手动编辑$PROFILE
  • 附带基于注释的帮助; 例如,运行help New-QuickCD -Examples
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] 相比之下,当从cmd.exe调用批处理文件时,它们在进程中运行,类似于PowerShell在进程中运行其*.ps1脚本的方式。另一方面,像Bash这样的POSIX类Shell默认情况下会在子进程中运行其脚本,除非使用源代码(., source)。

[2] 尽管这是使用Invoke-Expression的一种安全方法,但通常应该避免使用


1

另一种简单的方法是创建一个dev.ps1(PowerShell脚本文件),而不是批处理文件,其中包含以下代码Set-Location -Path "W:\dev"

注意:即使在PowerShell上运行,批处理文件也将在CMD进程上作为子进程运行。


-1
@mkelement是正确的:从路径中的.bat文件没有简单的方法来做到这一点-那是老派的。适当的PowerShell方式是创建一个别名到一个函数,它可以做你想要的事情。
借鉴this answer我的解决方案是:
步骤1:创建一个可重用的函数来制作别名:
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

重新启动 PowerShell(以加载上述函数),然后按照以下步骤定义您的dev快捷方式:

步骤2:创建一个dev快捷方式/别名,使您能够到达您想要的位置:

PS> myAlias dev "Set-Location W:\dev"

步骤三:愉快地使用 dev

PS C:\> dev
PS W:\dev>

创建自定义快速CD函数的想法是一个不错的主意,但我建议不要称它们为别名,因为在PowerShell中,别名只能是命令名称的别名,不能用参数包装整个调用;根本没有理由涉及别名功能,因此生成器函数可以大大简化-请参见我的更新答案(请注意,我的解决方案仅专注于创建自定义CD函数,因此仅接受目标路径,而不是整个“Set-Location”调用-不过很容易推广)。 - mklement0
虽然我很欣赏你的问题和解决方案背后的_想法_,但我认为术语混淆和生成器函数的复杂实现对未来读者更有害无益。 - mklement0
你说得对,我添加了比解决问题所需的更多的抽象概念,而你的答案是为什么我尝试的方法行不通的正确答案。我的重点是快速、实用、只要能工作就行的方式来创建命令快捷方式(在非技术意义上),而不一定是一个长函数,如果正确导入,将会做出惊人的事情。因此,我不同意你不在这里使用别名的观点:这正是别名的用途。 - Marc
你需要的是一个“命令包装器”;如何在技术上实现则因语言而异:在PowerShell中,你需要一个“函数”——只需要一个函数,而在Bash中,你可以使用一个“别名”(有限制)。虽然在PowerShell中,“别名”有着明确的技术含义,但松散地使用这个术语是不明智的。最简短表达你意图的方式是创建一个函数,例如 function dev { Set-Location W:\Dev }——不需要别名;这就是我回答中的 New-QuickCD 函数所创建的——没有通过自动命名函数和别名进行不必要的绕路。 - mklement0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接