☰
Windows终端命令对照:cmd、PowerShell、Git Bash 差异
2026/9/30 20:02:40 网站建设 项目流程

在 cmd 里敲下ls,屏幕回你一句“'ls' 不是内部或外部命令,也不是可运行的程序”,这个瞬间大概是每个既碰 Windows 又碰 Linux 的人共同的入门礼。更让人抓狂的是,同样一条rm -rf ./build,在 Linux 服务器上跑得好好的,换到 Windows 的 PowerShell 里就报错,你以为是权限问题,其实是这套壳根本不认这个写法。这份对照表就是为这种场景准备的:把 cmd、PowerShell、Git Bash、Windows Terminal 这四套常用终端里最容易混淆的命令、语法差异、踩坑点放在一张桌上横向比,让你在切环境的时候不用靠记忆硬扛,而是知道“为什么不一样、该用哪条”。不管你是刚接触 Linux 命令的运维新人,还是常年写前端脚本、顺手在 Windows 上跑 Docker 和 WSL 的开发者,下面这些内容都能直接拿去用。

1. 四种壳的脾气不同:先搞清楚你在跟谁说话

很多人把“终端”当成一个东西,其实这是四层完全不同的概念堆在一起:终端模拟器负责显示和输入,壳(shell)负责解释你敲的命令。Windows Terminal 是前者,cmd、PowerShell、Git Bash 是后者。搞混这一点,后面所有的命令差异都会变成玄学。

1.1 一个终端窗口,三种解释器

Windows Terminal 本身不执行任何命令,它只是一个更现代的窗口容器,可以在同一个界面里开多个标签页,每个标签页挂不同的 profile:一个标签跑 PowerShell,一个跑 cmd,第三个跑 Git Bash,第四个直接进 WSL 里的 Ubuntu。这意味着你的命令到底能不能跑,取决于当前标签挂着哪个壳,而不是“我在 Windows 上”。

cmd 是最老的那套,语法继承自 DOS,参数用/开头,环境变量用%VAR%引用,管道里传的是纯文本。PowerShell 是后来重做的,参数用-开头,命令叫 cmdlet,命名规范是“动词-名词”结构,比如Get-ChildItem,管道里传的是对象而不是文本。Git Bash 是 Git for Windows 附带的一套 MSYS2 环境,里面装的是真正的 GNU 工具链,grep、sed、awk、less、ssh-keygen全都是原版的。WSL 则是内核级别的 Linux,不是模拟。

这四种壳最直观的差别可以看这个对比:

特性cmdPowerShell 5.1PowerShell 7Git Bash
参数前缀/---或--
变量引用%VAR%$env:VAR$env:VAR$VAR
转义字符^反引号反引号\
管道传递文本对象对象文本
默认编码GBKGBKUTF-8UTF-8
内置 grep无无无有

最后一行特别关键:PowerShell 里没有grep。你在网上抄一段ps aux | grep java丢进 PowerShell,它会告诉你grep不是可识别的命令。正确的写法是Get-Process | Where-Object {$_.ProcessName -like "*java*"},或者干脆开一个 Git Bash 标签页。

1.2 PowerShell 那些“假 Unix 别名”是最坑的地方

PowerShell 为了照顾 Unix 用户的习惯,内置了一批别名:ls指向Get-ChildItem,cat指向Get-Content,rm指向Remove-Item,cp指向Copy-Item,mv指向Move-Item,ps指向Get-Process,kill指向Stop-Process。看起来皆大欢喜,实际上是个陷阱。

我用ls -la在 PowerShell 里试过无数次,结果永远是参数绑定失败,因为Get-ChildItem根本没有-l参数,-la也不会被识别成-l -a——PowerShell 不支持把多个单字母参数挤在一起写。想列出隐藏文件得用Get-ChildItem -Force,或者简写ls -Force。同理,rm -rf node_modules在 PowerShell 里必然报错,得写成Remove-Item node_modules -Recurse -Force。

这类“看起来像 Unix、实际不是”的别名,比完全不同的命令名更危险,因为它会给你一种“我懂了”的错觉。我的建议很简单:在 PowerShell 里就老老实实用 cmdlet 全名,或者用规范的别名(gci、gc、ri这类官方短别名),别把 Linux 命令原样搬过来。

1.3 还有一个隐藏地雷:PowerShell 里的curl、where、sc

这三个名字在 PowerShell 5.1 里都被占了别名,而且指向的东西完全不是你想要的。

curl在 PS 5.1 里是Invoke-WebRequest的别名,所以你写curl -o out.zip https://example.com/a.zip会失败,因为-o不是Invoke-WebRequest的参数。解决办法是明确调用可执行文件:curl.exe -o out.zip ...。Windows 10 1803 之后系统自带了curl.exe,这个写法在 cmd、PowerShell、Git Bash 里都通。

where在 PowerShell 里是Where-Object的别名,你想用它找可执行文件的路径(对应 Linux 的which),得写where.exe python。而 cmd 里的where就是找文件的,两个环境同名不同义,切换的时候特别容易翻车。

sc在 PowerShell 里是Set-Content的别名。你想查服务状态写sc query mysql,它会当成往文件里写内容。正确的是sc.exe query mysql,或者用 PowerShell 原生的Get-Service mysql。

需要记住一个通用判断方法:当一个命令的行为诡异得不像它应该有的样子,先跑一遍Get-Command 命令名,看它到底指向什么。

2. 文件与目录操作:路径才是跨平台的第一道坎

文件和目录操作是所有命令行的地基,也是差异最密集的区域。这里最容易被低估的不是命令本身,而是路径规则和引号规则。

2.1 反斜杠、正斜杠与盘符的三重困扰

Windows 用\做路径分隔符,Linux 用/。但实际用起来比这复杂:Windows 的 API 其实两种都认,所以cd C:/Users/me在 cmd 里也能跑;而 Git Bash 用/c/Users/me这种形式表示C:\Users\me,/开头代表的是 MSYS2 的虚拟根目录,不是系统盘根目录。

这里有个很多人第一次都会踩的坑:在 Git Bash 里执行docker run -v /c/Users/me/data:/data ...一般没问题,但如果你写的路径是C:\Users\me\data,MSYS2 会做一次“路径自动转换”,把以/开头的参数当成 Unix 路径去翻译,结果就变成了C:/Program Files/Git/c/Users/me/data这种鬼东西。这是 MSYS 的路径转换机制在作怪,它不是 bug,是设计。

绕开的办法有三种:一是用//c/Users/me/data双斜杠开头,明确告诉它别转;二是设置环境变量MSYS_NO_PATHCONV=1关掉这个转换;三是在 Git Bash 里别写 Windows 风格路径。我自己的习惯是涉及 Docker 挂载目录时优先开一个 PowerShell 标签,用C:\Users\me\data写,省得跟转换规则斗智斗勇。

另一个高频坑是 cmd 里的盘符切换。在 cmd 里cd D:\project不会真的把你切到 D 盘,工作目录还留在 C 盘,得加/d:cd /d D:\project。PowerShell 和 Git Bash 没这个问题,直接cd D:\project或cd /d/project就行。

2.2 常用文件命令横向对照

下面这张表是我自己用得最多的一组映射,基本覆盖日常八成场景:

操作Linux / Git BashcmdPowerShell
列出全部(含隐藏)ls -ladir /aGet-ChildItem -Force
切换目录并换盘cd /d/projectcd /d D:\projectSet-Location D:\project
递归创建目录mkdir -p a/b/cmd a\b\cNew-Item -ItemType Directory -Force a\b\c
删除文件rm filedel fileRemove-Item file
递归强制删除目录rm -rf dirrd /s /q dirRemove-Item dir -Recurse -Force
复制目录cp -r src dstxcopy src dst /E /ICopy-Item src dst -Recurse
移动/重命名mv a bmove a bMove-Item a b
查看文本cat ftype fGet-Content f
分页查看less fmore fGet-Content f | more
查找可执行文件which pythonwhere pythonGet-Command python
软链接ln -s a bmklink b aNew-Item -ItemType SymbolicLink

关于软链接这一行,我得单独啰嗦几句。Git Bash 里的ln -s默认会退化成“复制文件”而不是创建真正的符号链接,因为 MSYS2 在 Windows 上创建符号链接需要特权。想让它真正生效,得设MSYS=winsymlinks:nativestrict环境变量,并且账号要有创建符号链接的权限——要么开管理员,要么在系统设置里打开开发者模式。我自己试过好几次,最后发现直接用 cmd 的mklink最省心,它在管理员权限下稳定工作。

xcopy和robocopy的选择也值得说一句。xcopy src dst /E /I里/E表示包含空目录,/I表示目标不存在时按目录处理,缺了/I它可能会弹出来问你“目标是文件还是目录”,脚本里就卡住了。robocopy src dst /MIR做镜像同步更快更稳,但/MIR会删除目标端多余的文件,我第一次用的时候差点把备份目录清空,这个参数务必在确认路径无误后再用。

2.3 通配符与引号:语法差异最密集的十厘米

通配符看起来都一样,实际上展开时机不同。Linux 的*是 shell 在把参数交给程序之前就展开的,所以rm *.log是 shell 先列出所有匹配文件再传给rm。cmd 里大部分命令自己处理通配符,del *.log是del自己去匹配。PowerShell 的通配符由 cmdlet 处理,语义还分*、?、[a-z]字符集。

引号差异更值得警惕。cmd 里双引号是唯一起作用的引号,^是转义符;PowerShell 里单引号是“字面量”不做变量插值,双引号会做插值,转义符是反引号(键盘左上角那个),\在 PowerShell 里只是路径分隔符,没有任何转义含义;Git Bash 里单引号同样是字面量,双引号允许变量展开,反斜杠是转义符。

看一个具体例子。要输出一段包含变量的文本,三个环境写出来完全不同:

# Git Bash / Linux echo "当前用户: $USER"
:: cmd echo 当前用户: %USERNAME%
# PowerShell Write-Output "当前用户: $env:USERNAME"

再看一个更隐蔽的:在 PowerShell 里想传一个包含反斜杠的 JSON 字符串给curl.exe,你得用单引号包起来,因为双引号里的$会被当成变量开头:

curl.exe -X POST https://api.example.com/v1/items -H "Content-Type: application/json" -d '{"name":"test","path":"C:\\data"}'

这个写法我是被坑过好几次才记住的。在 Bash 里同样的请求,-d后面用单引号包 JSON 也是标准做法,两个环境在这个点上的习惯反倒是统一的。

3. 文本三件套:grep、sed、awk 在 Windows 侧的替身

只要你开始看日志、改配置文件、批量处理数据,grep、sed、awk 这三个工具就是刚需。Windows 这边没有原生的对应物,但各有各的替代方案,能力差距也不小。

3.1 findstr 与 grep 的实际能力差距

cmd 里的findstr是唯一能算得上 grep 血亲的东西,支持正则(虽然是很老的那套正则),支持/s递归、/i忽略大小写、/n显示行号、/v反向匹配。

:: cmd 里递归查找所有 java 文件里包含 NullPointerException 的行 findstr /s /i /n "NullPointerException" *.java

它能用,但限制很明显:不支持 Perl 兼容正则,不支持-A/-B上下文行,不支持只输出匹配部分,多条件现在是靠/c和空格分隔多个字符串。你要在日志里找异常前后十行,findstr直接放弃。

Git Bash 里的grep就是原生的 GNU grep,该有的全有:

grep -rn --include="*.java" "NullPointerException" . grep -A 10 -B 5 "ERROR" app.log grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" access.log | sort -u

所以我给的建议很直接:文本处理尽量在 Git Bash 或者 WSL 里做,findstr只用来应付那种“临时找一下文件里有没有某个字符串”的小活儿。

3.2 PowerShell 的 Select-String 才是正牌替代

PowerShell 里有个被严重低估的命令:Select-String,官方别名sls。它不是文本过滤,而是把每一行匹配结果包成一个 MatchInfo 对象,带文件名、行号、匹配内容,用管道往下传还能继续处理。

# 递归搜索,输出文件名、行号、整行内容 Get-ChildItem -Recurse -Filter *.log | Select-String -Pattern "ERROR|WARN" # 提取日志里所有 IP 地址并去重 Select-String -Path access.log -Pattern "\d+\.\d+\.\d+\.\d+" -AllMatches | ForEach-Object { $_.Matches.Value } | Sort-Object -Unique

比grep强的地方在于对象化。grep给你的是字符串,你还得用cut、awk二次切分;Select-String给你的对象里.LineNumber、.Filename、.Matches都是现成字段。我第一次认真用Select-String处理上千个日志文件的时候,确实有种“早知道就少写一堆正则”的感觉。

但它也有短板:不支持-A/-B上下文行(得自己写逻辑),速度在处理超大文件时不如 GNU grep。真要跑几百 MB 的日志,我还是会切到 Git Bash 用grep。

3.3 sed 和 awk 在四个环境里的生存状态

Git Bash 里sed、awk都是原版的,直接能用:

sed -i 's/127.0.0.1/0.0.0.0/g' config.ini awk -F',' '{sum+=$3} END {print sum}' data.csv

PowerShell 里没有这两个命令,替代方式是-replace运算符和ForEach-Object:

# 相当于 sed -i 's/old/new/g' (Get-Content config.ini) -replace '127\.0\.0\.1','0.0.0.0' | Set-Content config.ini # 相当于简单的 awk 求和 (Import-Csv data.csv | Measure-Object -Property Amount -Sum).Sum

注意-replace用的是 .NET 正则语法,反斜杠需要转义,写\.表示字面点号。还有一点很关键:Set-Content默认写出的编码在 PS 5.1 里是 GBK(或者带 BOM 的 UTF-8,取决于版本),如果这个文件要被 Linux 上的程序读,最好显式指定:Set-Content config.ini -Encoding utf8NoBOM(PowerShell 7)或者用[System.IO.File]::WriteAllText()。

3.4 管道、重定向与空设备的三套写法

这几行符号是跨平台最容易写错的地方:

需求Linux / Git BashcmdPowerShell
管道|||(传对象)
标准输出重定向>>>>>>>>>
标准错误重定向2>2>2>
丢弃输出> /dev/null 2>&1> NUL 2>&1> $null
顺序执行;&;
条件执行&&||&&||仅 PS7 支持

最后一行值得单独说。PowerShell 5.1 完全不支持&&和||,你写git pull && npm install会直接语法报错。这在 5.1 上困扰了我很久,因为很多文档都假设你能用。PS 5.1 的替代写法是:

git pull; if ($?) { npm install }

$?是上一条命令是否成功的布尔值。PowerShell 7 之后&&和||终于能用了,这也是我建议把 PS 5.1 升到 7 的理由之一。升级很简单,winget install --id Microsoft.PowerShell一条命令搞定,装完是两个版本共存,不会覆盖系统的 5.1。

丢弃输出的写法差异也要留意:PowerShell 里> $null只丢掉了成功流的输出,错误流要用2>$null;而且 PS 5.1 里如果错误流输出了内容,命令的退出码判断可能和你想的不一样。

4. 进程、服务与端口:排查线上问题的高频动作

命令行最有价值的用途之一就是排查问题:某个端口被谁占了、某个进程为什么杀不掉、某个服务为什么起不来。这些动作在四个环境里差别很大。

4.1 查进程、杀进程的三套语法

Linux 上ps aux | grep java是最经典的组合。在 Git Bash 里这个命令能跑,但注意:Git Bash 的ps只能看到 MSYS2 环境里的进程,看不到所有 Windows 进程。想查真正的 Windows 进程,ps是靠不住的。

操作Linux / Git BashcmdPowerShell
列出所有进程ps auxtasklistGet-Process
按名字过滤ps aux | grep javatasklist | findstr javaGet-Process *java*
查看端口占用进程ss -tulnpnetstat -anoGet-NetTCPConnection
按 PID 杀进程kill -9 1234taskkill /F /PID 1234Stop-Process -Id 1234 -Force
按名字杀进程pkill -f javataskkill /F /IM java.exeStop-Process -Name java -Force

tasklist的输出列很宽,/FO CSV参数能让它输出 CSV 格式,方便后续处理:tasklist /FI "IMAGENAME eq java.exe" /FO CSV。/FI是过滤器,比管道加findstr更高效,因为它是在系统层面过滤的。

PowerShell 的Get-Process强在可以直接访问进程对象的各种属性:

# 找出占用内存超过 500MB 的进程,按内存倒序 Get-Process | Where-Object { $_.WorkingSet64 -gt 500MB } | Sort-Object WorkingSet64 -Descending | Select-Object Name, Id, @{N='内存MB';E={[math]::Round($_.WorkingSet64/1MB,1)}}

@{N='...';E={...}}这个叫计算属性,是 PowerShell 里非常实用的技巧,本质上是动态构造输出列。用熟了之后你会发现它比awk的字段处理更直观。

4.2 端口占用的完整排查链路

“8080 端口被占了”是开发日常里出现频率极高的问题。三个环境里的完整排查路径如下。

cmd 里的经典两步:

netstat -ano | findstr :8080 tasklist /FI "PID eq 12345"

第一行拿到 PID,第二行查这个 PID 是什么程序。netstat -ano里的-a是显示所有连接和监听端口,-n是以数字形式显示地址和端口(不做 DNS 反查,快很多),-o是显示所属进程 PID,这三个参数缺一不可。

PowerShell 里有更原生的方法,直接一步到位:

Get-NetTCPConnection -LocalPort 8080 | Select-Object LocalAddress, LocalPort, State, OwningProcess, @{N='进程名';E={(Get-Process -Id $_.OwningProcess).ProcessName}}

这个写法我第一次用时觉得很爽,因为它把 PID 到进程名的映射也一起做了。如果只是想确认某个端口通不通,Test-NetConnection比其他环境直观:

Test-NetConnection -ComputerName 192.168.1.100 -Port 3306

Linux 侧对应的是nc -zv 192.168.1.100 3306或者telnet。Git Bash 里nc不一定随附,得看安装的组件,我一般会装个netcat备用,或者干脆在 PowerShell 里做连通性测试,两边都方便。

这里分享一个排查经验:Windows 上端口被占但Get-NetTCPConnection查不到进程名,往往是因为那个进程是以 SYSTEM 权限跑的,普通权限读不到进程信息。这时候需要以管理员身份重开终端再查。我第一次遇到这个问题的时候,对着一个“无名进程”折腾了半小时,最后发现是权限不够。

4.3 服务管理的语义差异

Linux 的systemctl是一套完整体系:status、start、stop、restart、enable、disable,加上journalctl看日志。Windows 这边对应的是“服务(Service)”概念,但管理工具的分布很分散。

操作LinuxcmdPowerShell
查服务状态systemctl status nginxsc query nginxGet-Service nginx
启动服务systemctl start nginxnet start nginxStart-Service nginx
停止服务systemctl stop nginxnet stop nginxStop-Service nginx
设置开机自启systemctl enable nginxsc config nginx start= autoSet-Service nginx -StartupType Automatic
查看服务日志journalctl -u nginx -f事件查看器Get-WinEvent -LogName Application

cmd 的sc config有个特别反直觉的地方:start= auto等号后面必须有一个空格,写成start=auto会报错。这个规则是sc命令的历史遗留,我当初在这上面花了十几分钟才搞明白。

Get-WinEvent是 PowerShell 里查事件日志的利器,配合Where-Object可以做出很强的过滤:

# 查看最近 2 小时的应用错误日志 Get-WinEvent -FilterHashtable @{ LogName='Application'; Level=2; StartTime=(Get-Date).AddHours(-2) } | Select-Object TimeCreated, ProviderName, Message -First 20

-FilterHashtable是服务端过滤,比先取全部再用Where-Object在客户端过滤快几倍,日志量大的时候差距非常明显。

4.4 网络命令的映射关系

网络诊断命令的对应关系整理出来是这样的:

用途Linux / Git BashcmdPowerShell
查看网卡信息ip addr/ifconfigipconfig /allGet-NetIPAddress
测试连通性ping -c 4 hostping -n 4 hostTest-Connection host -Count 4
路由追踪traceroute hosttracert hostTest-NetConnection -TraceRoute
DNS 查询dig/nslookupnslookupResolve-DnsName
发 HTTP 请求curl -v urlcurl.exe -v urlInvoke-RestMethod url
查看路由表ip routeroute printGet-NetRoute

ping的参数差异是最容易记错的:Linux 用-c指定次数,Windows 用-n。我至今偶尔还会在 Windows 上敲ping -c 4,然后看着它把-c当成什么奇怪的东西。

Invoke-RestMethod处理 JSON API 的体验比 curl 好,因为它会自动把 JSON 反序列化成对象,你不用再管道给jq:

$resp = Invoke-RestMethod -Uri "https://api.example.com/v1/users" -Method Get $resp.data | Where-Object { $_.status -eq 'active' } | Select-Object id, name

Git Bash 里对应的写法要配合jq:curl -s https://api.example.com/v1/users | jq '.data[] | select(.status=="active")'。两边都有各自的舒服场景,API 返回结构复杂的时候我更倾向 PowerShell,因为它不需要额外装工具。

5. 编码、权限与环境变量:跨平台协作的隐形战场

前面那些差异看得见,报错了能查到。真正耗时间的是编码、权限、换行符这类“看不见”的问题,因为症状往往诡异:文件内容明明对,程序就是读不出来。

5.1 中文乱码的三层根因

中文乱码在 Windows 命令行里可以拆成三层原因,分清楚才不会瞎试。

第一层是控制台代码页。cmd 默认用 GBK,你type一个 UTF-8 编码的文件,中文就是乱码。临时解决是chcp 65001切到 UTF-8,但要注意切完代码页之后,某些老程序的行为会变,而且这个设置只对当前窗口有效。

第二层是命令行工具的输入输出编码。PowerShell 5.1 里 Python 输出中文乱码是经典案例,因为 Python 按控制台代码页编码输出,而 PowerShell 5.1 的控制台编码是 GBK。有效的修法是显式指定:

# 临时设置当前会话 $env:PYTHONIOENCODING = "utf-8" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8

或者更彻底一点,把这两行写进$PROFILE,每次启动自动生效。PowerShell 7 默认就是 UTF-8,装完之后这类问题基本消失,这也是我劝大家升级的另一个理由。

第三层是文件本身的编码和 BOM。Set-Content在 PS 5.1 里默认写出带 BOM 的 UTF-8,这个 BOM 会让 Linux 上的脚本、配置文件解析出问题——比如 shell 脚本第一行#!/bin/bash前面多了三个不可见字节,执行时报No such file or directory,而文件明明存在。用Format-Hex看一下文件头就能确认:

Format-Hex script.sh | Select-Object -First 2

如果开头看到EF BB BF,那就是 BOM。去掉的办法是用[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding $false)),最后那个$false就是“不写 BOM”。

Git Bash 里创建的文件默认是 UTF-8 无 BOM,这一点反而比 PowerShell 友好。要在两个环境之间来回传脚本文件,尽量在 Git Bash 侧创建和编辑。

5.2 .sh 文件在 Windows 上的换行符灾难

换行符是另一类看不见的坑。Windows 用 CRLF(\r\n),Linux 用 LF(\n)。一个在 Windows 上用记事本编辑过的 shell 脚本,传到 Linux 上执行会报莫名其妙的错误,比如$'\r': command not found。

Git 提供了三层控制手段,我一般按这个顺序配:

# 全局配置:检出时转成 CRLF,提交时转成 LF(Windows 单机开发推荐) git config --global core.autocrlf true # 如果主要在跨平台协作,用 input 更安全:提交转 LF,检出不动 git config --global core.autocrlf input

但最靠谱的做法是在仓库根目录放一个.gitattributes,把规则写死在项目里,不依赖每个人的本地配置:

* text=auto *.sh text eol=lf *.bat text eol=crlf *.png binary

*.sh text eol=lf这行的意思是:不管在什么系统上检出,.sh文件一律用 LF。这条规则救过我不止一次,因为 CI 机器上跑脚本失败,往往就是某个人的编辑器偷偷写入了 CRLF。

如果已经中招了,Git Bash 里有现成的转换工具:dos2unix file.sh和unix2dos file.bat。这两个命令在 Git Bash 里通常自带,没有的话可以用sed -i 's/\r$//' file.sh顶一下。

5.3 chmod 与 Windows 权限模型不是一回事

Linux 的chmod 755是权限位的直接操作,rwxr-xr-x一眼能看懂。Windows 用的是 ACL(访问控制列表),权限项是按用户和组分配的,粒度完全不同。Git Bash 里的chmod只在 MSYS2 的虚拟文件系统里有效,对 NTFS 文件的影响非常有限。

具体表现是什么?你chmod +x script.sh之后,在 Git Bash 里能执行,但用ls -l看权限位可能还是 644。能否执行其实取决于文件扩展名和 Windows 的执行策略,而不是那个权限位。

这个差异会带来一个实际问题:clone 一个 Linux 项目的仓库后,里面的部署脚本没有执行权限。解决办法是用 Git 的命令记录权限位:

git update-index --chmod=+x deploy.sh git commit -m "fix: 给 deploy.sh 添加执行权限"

这样提交之后,别人在 Linux 上 clone 下来权限就是对的。这个命令我在每个跨平台项目里都会用一次,属于典型的“不做也不会立刻出问题,但迟早会出问题”的配置。

真正跟权限强相关的操作,比如创建符号链接、改系统目录、查 SYSTEM 进程,还是得在管理员权限的终端里做。开管理员 PowerShell 的方式:在开始菜单搜索 PowerShell,右键选择“以管理员身份运行”;或者在 Windows Terminal 里用Ctrl+Shift+加数字键打开管理员标签,这个快捷键用的熟能省不少切换时间。

5.4 环境变量的三种写法与作用域陷阱

环境变量的差异是纯粹的语法问题,但作用域问题才是真正的坑。

# Git Bash / Linux:只在当前会话有效 export API_KEY="abc123" echo $API_KEY
:: cmd:只在当前会话有效 set API_KEY=abc123 echo %API_KEY% :: cmd:持久化到用户级别(需要重开终端生效) setx API_KEY "abc123"
# PowerShell:当前会话 $env:API_KEY = "abc123" # 用户级别持久化(等价于 setx 的 .NET 实现) [Environment]::SetEnvironmentVariable("API_KEY", "abc123", "User") # 读取 $env:API_KEY

set和$env:都只影响当前窗口,关掉就没了,这一点和 Linux 的export一致。需要永久生效就用setx或[Environment]::SetEnvironmentVariable,但注意它们不会影响已经打开的终端窗口,必须重开。

有个细节容易被忽略:setx有 1024 字符的长度限制,写超长路径进去会被截断。[Environment]::SetEnvironmentVariable没有这个限制,所以脚本里我更倾向用后者。

另外,PATH变量的操作要特别小心。setx PATH "%PATH%;C:\new\tool"这种写法有个致命问题:它会把你当前的PATH展开、拼接、然后写入,如果当前的PATH里包含引用其他变量的部分(比如%JAVA_HOME%\bin),setx会把它展开成实际路径存进去,看起来没问题,但那些变量后续再改就不生效了,而且总长度一旦超过 1024 就会被截断,可能导致整个PATH损坏。稳妥的做法是只操作用户级PATH,并且用 PowerShell 的 .NET 方法读取和写入:

$userPath = [Environment]::GetEnvironmentVariable("PATH", "User") $newPath = $userPath + ";C:\new\tool\bin" [Environment]::SetEnvironmentVariable("PATH", $newPath, "User")

在动手之前,先把当前值打印出来看一眼,或者先复制一份存到文本里。我见过同事一次setx把PATH写坏,最后只能手动重建的经历,代价不小。

6. 把四套壳拧成一股绳:别名、配置与工作流

理解了差异之后,真正提升效率的是“让每个壳都能跑我习惯的命令”。这一步靠的是别名、配置文件和工作流的统一。

6.1 alias、doskey 与 Set-Alias 的三种玩法

Git Bash 的别名写在~/.bashrc,这是最成熟的一套:

alias ll='ls -alF' alias gs='git status -sb' alias dc='docker compose' # 别名带参数不够用的时候,直接写函数 mkcd() { mkdir -p "$1" && cd "$1"; } # 交互式搜索历史命令 alias h='history | grep'

改完.bashrc之后要source ~/.bashrc让它生效。mkcd这个函数是我用得最多的一个,创建目录并进入,省掉一次输入。

cmd 的别名机制最弱,doskey只在当前窗口有效,想持久化得改注册表的AutoRun项,改动系统级配置风险不小,我不太推荐。要写就写进一个.bat文件,然后在注册表里指向它:

:: cmd 别名(仅当前会话) doskey ll=dir /a doskey gs=git status -sb

PowerShell 的Set-Alias和函数能力最强,配置写在$PROFILE指向的文件里:

# 查看 profile 路径,通常是 Documents\PowerShell\Microsoft.PowerShell_profile.ps1 $PROFILE # 建立 profile 文件(如果不存在) if (!(Test-Path $PROFILE)) { New-Item -Path $PROFILE -ItemType File -Force } # 以下内容写进 profile Set-Alias ll Get-ChildItem function mkcd { param($p) New-Item -ItemType Directory -Force $p | Out-Null; Set-Location $p } function gs { git status -sb } # 让 grep 在 PowerShell 里也能用(调用 Git Bash 的 grep) function grep { & "C:\Program Files\Git\usr\bin\grep.exe" @args }

最后那个函数是我自己的一个小技巧:PowerShell 里没有 grep,但 Git for Windows 装了 grep.exe,直接在 PowerShell 里包一层调用它,就能在 PS 环境里继续用熟悉的 grep 语法。@args是 PowerShell 的“参数透传”语法,把收到的所有参数原样传给目标程序。

要提醒一点:PowerShell 的执行策略默认可能禁止运行脚本,profile 不生效就是这个原因。检查用Get-ExecutionPolicy,改成Set-ExecutionPolicy -Scope CurrentUser RemoteSigned就够了。用-Scope CurrentUser只影响当前用户,比改全局策略稳。

6.2 Windows Terminal 的 profile 配置实操

Windows Terminal 的核心价值是把多个壳收进一个窗口,配置都写在settings.json里,可以用Ctrl+,打开,也可以用Ctrl+Shift+,打开原始 JSON 文件。

一个实用的配置片段:

{ "profiles": { "list": [ { "name": "PowerShell 7", "commandline": "pwsh.exe -NoLogo", "startingDirectory": "D:\\work", "colorScheme": "One Half Dark" }, { "name": "Git Bash", "commandline": "C:\\Program Files\\Git\\bin\\bash.exe -i -l", "icon": "C:\\Program Files\\Git\\mingw64\\share\\git\\git-for-windows.ico" } ] }, "defaultProfile": "{guid-of-powershell7}" }

startingDirectory设成你的工作目录,能省掉每次开窗口都要cd的麻烦。-i -l参数是让 Git Bash 以登录 shell 的方式启动,这样.bashrc和.bash_profile都会被加载,别名才会生效——不加这两个参数,你会发现别名全都不起作用,这是我当初排查了好久才找到的原因。

wt命令本身也很好用,可以从任何终端里启动新标签页:

wt -p "Git Bash" -d D:\work # 在指定目录开一个 Git Bash 标签 wt new-tab pwsh ; split-pane -V # 新建标签页并垂直分屏

如果你的环境需要离线部署 Windows Terminal,官方发布页提供了.msixbundle安装包,但直接装可能会因为缺少依赖框架失败,需要先把 VCLibs 和 UI.Xaml 这两个运行时依赖装上。这类离线场景我在内网环境里遇到过几次,提前把依赖包一起准备好会省很多事。

6.3 在四个壳里看到“同一个世界”

最后想聊一个心态上的调整。我刚开始两头跑的时候,总想着“能不能让 Windows 和 Linux 命令完全统一”,折腾过给 cmd 装 GNU 工具、给 PowerShell 装各种模块,最后发现收益不大,反而让环境越来越难维护。

后来我换了个思路:承认差异,按场景分工。文件批处理、日志分析、Git 操作、写跨平台脚本,一律用 Git Bash,因为工具链最完整;查系统进程、服务、事件日志、网络状态,用 PowerShell,因为它能访问 Windows 的原生对象;跑老旧的.bat脚本、做简单的文件操作,用 cmd,因为它最轻量、兼容性最好。Windows Terminal 负责把这些壳装在一个窗口里,切换就是Ctrl+Tab。

这个分工方式让我的日常工作流变得很清晰:开一个 Windows Terminal 窗口,左边标签是 Git Bash 用来跑命令和 Git 操作,右边是 PowerShell 用来查系统状态和调试 API,需要临时跑个.bat就新开一个 cmd 标签。三个环境各司其职,不用再纠结“为什么这个命令在这里不行”。

7. 几个我踩过并且反复踩的坑

前面讲的都是成体系的差异,这里再补几个零碎但很致命的点,都是我在实际排查里撞过的。

第一个是tar和unzip解压中文名压缩包乱码的问题。Git Bash 里的unzip处理包含中文文件名的 zip 包时经常乱码,因为 zip 格式的编码标记不规范。我的处理方式是改用 Windows 10 自带的tar.exe:tar -xf archive.zip,它是 bsdtar,对编码的处理更宽容。再不行就用 7z,它对中文路径的兼容性最好。这个坑在解压别人打包的素材文件时特别常见。

第二个是关于“需要终端的程序在 Git Bash 里卡住”的问题。有些程序需要真正的 TTY 才能正常交互,比如需要输入密码的命令行工具。在 Git Bash 的 mintty 里跑这类程序,经常表现为没有任何输出,就那么卡着。原因就是 mintty 不是真正的控制台,程序拿不到 TTY。解决办法是用winpty包一层:winpty python interactive_script.py,或者直接切到 PowerShell 里跑。这个现象的根因和 Linux 上某些环境报“需要终端”的错误是同一件事,理解了本质就不会瞎猜了。

第三个是跨文件系统的性能问题。如果你用 WSL,把项目放在/mnt/c/Users/...下面开发,会发现npm install、Git 操作、文件监听都慢得离谱。原因是 Windows 和 Linux 之间的文件系统访问要经过一层转换,代价很高。正确做法是把项目放在 WSL 内部的~/projects下,然后用 VS Code 的 Remote 功能打开。我实测过同一个项目,放在/mnt/c下构建要几分钟,放在 WSL 内部只要几十秒,差距是数量级的。

第四个是脚本里的路径分隔符硬编码。写跨平台脚本时,./scripts/build.sh这种写法在 Windows 上跑不了,scripts\build.bat在 Linux 上也一样。稳妥的做法是用语言本身的路径 API:Python 用pathlib.Path,Node.js 用path.join(),或者在 shell 里用变量拼装。宁可在每个平台上各写一个脚本入口,也不要在代码里做复杂的路径判断。

第五个是history的差异。bash 的history用的是!前缀调用,比如!123执行第 123 条、!!重复上一条、!git执行最近一条以 git 开头的命令。PowerShell 里对应的是Get-History、Invoke-History,!!也支持,但没有 bash 那么丰富的展开语法。cmd 只能用 F7 调出历史列表,相当原始。这个差异影响不大,但从 bash 转过来的人会明显感觉 PowerShell 的历史操作“手感不对”。

最后一个建议:把这张对照表里最常用的二三十条命令做成自己的速查卡片,贴在编辑器旁边,用两周就形成肌肉记忆了。剩余的冷门命令不用背,真正需要的时候回来查就行。命令行这东西,用得越多越顺手,关键是要理解“为什么不一样”,而不是死记“这个环境里该敲什么”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询