Shell技巧:用$_自动变量让mkdir与cd一步到位,目录名只写一次
2026/9/17 3:59:03 网站建设 项目流程

前阵子整理 shell 学习笔记,翻到去年 11 月 8 号的条目,里面只有一行命令和一段标注:mkdir demo; cd "$_",旁边写着五个字——只写一遍目录名。今天不打算把这行命令贴出来就完事,而是把背后的变量机制、封装思路、跨 shell 差异和实际踩坑一起讲透。

先把这个需求说清楚:在终端里创建一个新目录,然后马上切进去。你可能会说,这不是mkdircd两条命令的事吗?确实,但问题是目录名要敲两遍。目录名短还好,一旦是一串带日期、带项目编号的长路径,重复敲两遍不但费手,还特别容易敲错。这篇内容主要面向刚上手 shell 的新手,也适合写了很久但一直没细究过$_这个自动变量的运维和研发同学。围绕的核心问题只有一个:怎么把"创建并进入目录"合成一句,目录名只输入一次。

1. "建完就进"这个动作,藏着你没注意的成本

1.1 哪些场景每天都在触发这个操作

先别急着看命令,想想你一天里到底有多少次在建目录之后紧接着就要进去。

最常见的就是初始化项目。无论是新建一个 Go 模块、Python 项目,还是随手开一个临时目录做测试,动作基本都是mkdir然后cd。再比如手动部署的时候,你需要在/data/www下面建一个带发布日期的目录,然后进去拉代码或者解压包。还有 CI/CD 脚本的本地复现环节,脚本里经常是先建目录再进入目录执行后续命令,你在本地调试时也得手动走一遍同样的流程。

这些场景有一个共同点:目录名往往不是固定的,而是带着时间戳、项目编号、分支名这类信息。目录名一旦长起来,重复敲一遍的痛苦指数是翻倍的。

1.2 目录名写两遍的代价到底有多大

有人觉得,多敲一次名字而已,能有多大事?我遇到过的情况是:正在敲一个长目录名,手一抖少打了一个字母,mkdir成功了,但cd进去发现不对,又得cd ..退回来重新进。还有一次是在生产服务器上,目录路径特别长,我复制粘贴的时候没注意行尾多了个空格,结果mkdir建出来的目录名带了一个看不见的空格,后面所有引用这个路径的脚本全部报错,排查了十分钟。

这不是手速问题,是重复输入必然带来的出错概率问题。一次命令里同一个参数要写两遍,等于把出错的概率翻了一倍。对于交互式操作来说,优化点很明确:能不能让目录名只输入一遍,让命令自己去复用?这也是$_在这条场景里最有价值的天然优势。

2. 先看最笨但最安全的写法:&&链式命令

在学习阶段,几乎所有教程都会教你这个写法:

mkdir demo && cd demo

这个写法本身没有问题,而且它有一个非常关键的优势:&&是短路操作符,只有mkdir执行成功了,cd才会执行。如果目标目录所在的父目录不存在、当前用户没有写权限,mkdir返回非零退出码,cd压根不会跑。这种“前面失败,后面就不做”的语义,在脚本里是非常安全的行为。

但问题也很直白:demo这个目录名你写了两次。目录名短的时候还好,一旦变长就很折磨人。我们看一个稍微真实点的路径:

mkdir -p /data/www/releases/2024-11-08/frontend-web cd /data/www/releases/2024-11-08/frontend-web

这样的命令你在终端里敲一遍就知道,第二行基本靠复制粘贴,而复制粘贴长路径恰恰是另一个事故源头。如果把这个命令写进脚本,情况也没好到哪去:一旦目录路径改了,脚本里mkdircd两处都要同步改,漏改一处就是故障。

顺便说一句,&&不是唯一的选择,有时候你会看到用分号的版本:

mkdir demo; cd demo

分号只表示“按顺序执行”,它不关心第一条命令到底成功没有。如果mkdir因为目录已存在而报错,分号后面的cd还是会执行,而且恰好能在目录已存在的情况下进入目录。这个特性后面还会提到,它决定了你选择哪种方式时的行为差异。

3. 用$_只写一遍目录名:原理与正确姿势

3.1$_到底是什么,它是怎么被赋值的

$_在 Bash 中是一个自动变量,它的含义是“上一条命令的最后一个参数”。注意这里的“最后一个参数”是在完成展开之后的值,不是你在键盘上敲的原始字符串。

举个例子:

echo hello shell echo "$_"

第二条echo会输出什么?答案是shell。因为第一条命令echo hello shell的最后一个参数是shell,执行完毕后,_就被设置成shell。这就是$_最直观的用法。

有人会问:这和按向上方向键然后改命令有什么区别?区别在于,$_是可以在脚本里、在当前这条命令行里直接引用的,它不是历史记录的堆栈操作,而是一个“当前状态”的变量。对于 shell 来说,记录上一条命令的最后一个参数,本质上是为了方便你快速引用,只是很多人没有意识到这个变量还能这样组合。

3.2 组合命令的执行时序

回到标题里的核心命令:

mkdir demo; cd "$_"

这里我用的是分号而不是&&。为什么分号也能生效?关键在于执行时序。

虽然这一整行会被 shell 先解析,但$_的展开并不是在“整行被读取”的那一刻统一完成的,而是在每一条命令即将执行的时候完成的。执行顺序是这样的:

  1. shell 先执行mkdir demo,命令结束后,_被更新为demo
  2. shell 继续执行cd "$_",在这个命令的参数展开阶段,_的值已经是demo,于是等价于执行cd demo

如果你用的是&&,逻辑也是一样的,只是多了一层“mkdir成功才执行cd”的保障。所以:

mkdir demo && cd "$_"

这个写法同样没问题,而且在目录创建失败时,不会执行后面的cd,更安全。

结合前面的长路径场景:

mkdir -p /data/www/releases/2024-11-08/frontend-web && cd "$_"

一次输入,两次使用,目录名只写了一遍。mkdir接收的最后一个参数是那一长串路径,cd自然也是进入那个路径。

3.3 别忘了给$_加双引号

这里有一个很容易踩的坑:如果目录名里带空格,cd $_cd "$_"行为完全不同。

mkdir "my project" cd "$_" # 正确,进入名为 my project 的目录 cd $_ # 错误,被拆成两个参数,报错

不加引号,Bash 会把$_展开后的结果做单词拆分,遇到空格就断开了。我见过不少人在这个坑里栽过跟头,所以在$_的使用场景里,双引号不是可选项,而是标配。

3.4 别把$_!$混为一谈

搜索资料的时候,很多人会看到另一个写法:!$

mkdir demo cd !$

!$是历史展开(history expansion)的语法,含义是“历史记录中上一条命令的最后一个单词”。它和$_看起来很像,但有两处关键区别。

对比项$_!$
类型自动变量,命令执行后更新历史展开,读取输入时替换
在非交互式脚本中可用(但需注意初始值)默认关闭
是否可嵌入双引号可以,"$_"不行,历史展开在引号内外行为不同
典型推荐度更推荐用于脚本和函数封装更适合交互式终端的临时用法

另外补充一个交互式终端里的实用快捷键:Alt+.(按 Alt 再按点号),可以快速补全上一条命令的最后一个参数。在mkdir之后顺手按一下Alt+.,也能实现“目录名只敲一遍”,而且不需要记$_。如果你还没习惯用$_,这个快捷键是最容易上手的替代方案。

4. 把技巧固化下来:一个稳妥可复用的mkcd函数

4.1 为什么需要函数而不是每次手敲

手敲mkdir xxx; cd "$_"这件事,顺手归顺手,但它有几个不舒服的地方。

首先是$_这个变量虽然在交互式 shell 里很可靠,但你没法保证中间有没有别的命令插进来改掉它的值。其次是这种一行式命令没有错误处理:万一mkdir失败,结果是未知的。再就是多参数的情况很尴尬,比如你输入了mkdir a b两个目录,cd只会进入最后一个。这些边角问题,用一个封装函数解决最干净。

我自己在.bashrc里放了一个mkcd函数,用了很多年。它解决的不只是“少写一遍目录名”,而是把整个“建目录并进入”的流程规范化了。

4.2 一个完整的 mkcd 实现

mkcd() { if [ "$#" -ne 1 ]; then printf 'usage: mkcd <directory>\n' >&2 return 2 fi if ! mkdir -p -- "$1"; then printf 'mkcd: failed to create directory: %s\n' "$1" >&2 return 1 fi if ! cd -- "$1"; then printf 'mkcd: failed to enter directory: %s\n' "$1" >&2 return 1 fi }

拆开讲几个关键点:

"$#" -ne 1是参数数量检查。函数只接受一个目录参数,参数多了少了都有明确提示。这一点很重要,因为mkdir a b; cd "$_"会直接进入b,而用户可能以为自己在a里,这种静默成功的错误最危险。

mkdir -p -- "$1"中的-p表示递归创建父目录,同时目录已存在时不报错。--表示后面的内容不再解析为选项。这两个点在前面已经解释过,加上之后函数能容忍多级路径、以-开头的目录名。

if ! mkdir ...是判断退出码。mkdir失败时函数直接返回,不会继续执行cd。同理,if ! cd ...处理的是创建成功但进入失败的情况,比如目录名中间有权限问题。

这个版本的函数在 Bash 和 zsh 下都能正常运行,我个人更推荐在交互式环境里直接使用这个函数,而不是反复手敲mkdir xxx; cd "$_"

4.3 安装到 .bashrc 还是 .zshrc

把上面的函数体复制进~/.bashrc(Bash 用户)或者~/.zshrc(zsh 用户),保存后执行source ~/.bashrc重新加载配置,就能立刻使用mkcd了。

如果你和我一样同时维护多台机器,建议把这个函数单独放到一个shell-functions文件里,然后在.bashrc.zshrc中统一source。这样换新机器的时候,一条命令就能把整套自定义函数带过去。

# ~/.bashrc 或 ~/.zshrc source ~/.config/shell/my-functions.sh

这里唯一要注意的是,有些环境变量管理器(比如 oh-my-zsh)可能会自带一个mkcd插件,如果你的函数和它冲突,加载顺序会导致其中一个被覆盖。遇到这种情况,给函数换一个名字,比如mkdircd,或者干脆删掉插件默认的那个,都是可行的。

5. 实战踩坑记录:$_在哪些场景下会"失灵"

5.1 函数内部依赖$_的风险

网上还有一种写法是直接把$_写进函数里:

mkcd_bad() { mkdir "$1" cd "$_" }

这个函数在大多数情况下能工作,因为函数体里第一条命令是mkdir,执行完后_被更新为$1的值。但它非常脆弱,原因有两个。

第一,如果在mkdir之前函数里还有其他任何赋值、打印、调用其他内置命令,_的值都可能被改掉。第二,如果mkdir因为某种原因没有接收到参数,_的更新规则可能不生效,那么cd "$_"就会退回到这个 shell 会话之前的值,结果完全不可控。

做一个负责任的封装,就应该直接使用函数参数$1,而不是依赖变量状态。这是我在代码评审里见过很多次的问题:能用参数解决的,不要依赖隐式状态。隐式状态看起来少敲了几个字,但它的行为取决于上下文,时间一长没人能说清楚它到底在什么情况下会出问题。

5.2 不同 shell 的行为差异

$_并不是所有 shell 都提供同样语义的变量。

Shell_变量的行为是否适合这个技巧
Bash交互式和脚本中均可用,更新规则明确适合,推荐
zsh上一条命令的最后一个参数,另有$LASTARG同义适合,基本兼容
dash(POSIX sh)标准未规定,依赖实现不建议依赖
fish没有这个语义的_变量不适合,应改用函数参数
PowerShell$_是管道当前对象,语义完全不同不能直接套用

所以mkdir xxx; cd "$_"这个技巧,严格来说是一个 Bash/zsh 技巧。如果你需要写跨 shell 的 POSIX 脚本,不要依赖$_,老老实实把参数传两遍或者用变量存下来。

5.3 边界情况:目录已存在怎么办

mkdir在“目录已存在”的情况下会返回非零退出码。这时你之前用的连接符就决定了行为:

  • mkdir demo && cd demo:目录存在时,cd不会执行,停在外面。
  • mkdir demo; cd demo:目录存在时,mkdir报错但cd照样执行,最终进入目录。

如果你希望“无论目录是否已存在,都进入该目录”,那分号版本更符合需求。用mkdir -p demo; cd "$_"也是同样的效果,因为-pmkdir在目录已存在时不再报错,退出码为 0,这样配上&&也能进入目录。

这里没有标准答案,取决于你想要的交互感受。我自己更偏向mkdir -pcd,因为实际工作中“目录可能在,也可能不在”的情况很常见,-p直接帮我把两种场景统一了。

5.4 多参数与特殊路径的几个真实案例

再列几个我在使用中遇到过的边界情况:

  • mkdir /tmp/a/b/c && cd "$_"-p创建多级目录后,$_是整个/tmp/a/b/c,可以正常进入。
  • mkdir "-newdir" && cd "$_":没有--时,mkdir会把-newdir当选项解析,命令直接报错。这也是我在函数里强调加--的原因。
  • mkdir "my project" && cd "$_":必须写成cd "$_",不能写成cd $_,原因前面已经讲过,空格会被拆分。
  • mkdir因为权限失败,$_仍然是那个路径,cd会尝试执行并再次失败。这不算$_的问题,但如果不用if判断,错误信息会连续出现两条,刚上手的人容易被绕晕。

6. 这个思路还能延伸到哪些场景

6.1 自动创建并进入临时目录

$_在临时目录场景特别舒服:

mktemp -d /tmp/myapp.XXXXXX cd "$_"

mktemp -d会输出一个随机生成的临时目录路径,这个路径你没法提前预知,更不可能手动重复输入。但是cd "$_"可以直接把上一条命令的最后一个参数作为目标路径。这个组合比先执行再复制粘贴输出路径要快得多。

6.2 git clone 之后进入目录

git clone也是一个经典的“建目录并进入”场景。不过这里有个隐藏的细节:

git clone git@github.com:user/repo.git mydir && cd "$_"

git clone的最后一个参数是目录名mydir,所以cd "$_"正好进入目标目录。但如果你省略了目录名,只写仓库地址,那么最后一个参数是 URL,直接cd "$_"会发现进不去,因为实际生成的目录名是 URL 去掉了.git后缀。这个逻辑不难推,但在脚本里容易忽略。建议在脚本里显式指定克隆目录名,再用$_进入,既清晰又不会出错。

6.3 其他 shell 的等效做法

$_技巧在 fish 和 PowerShell 里不能直接迁移,但“把建目录和进目录封装成一个函数”的思路是一致的。

fish 版本:

function mkcd mkdir -p $argv and cd $argv end

PowerShell 版本:

function mkcd { param([string]$Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null Set-Location -Path $Path }

Windows 自带的 cmd 里可以用 doskey 宏:

doskey mkcd=mkdir $* & cd /d $*

$*在 doskey 宏里代表用户输入的完整内容,所以mkdircd引用的是同一段输入。这些都是顺着“只输入一遍目录名”的思路在不同终端里落地的方案,本质上没有区别。

7. 围绕$_的最后一个补充

最后再补充一个实用小技巧。$_这个变量在调试和日志输出时也很有用,比如你想在脚本里打印“最后处理的是哪个文件”,可以在命令后面直接写:

echo "$_"

但要注意,如果在脚本开头、任何命令执行之前使用$_,它的值是启动当前脚本时的路径,不是空值。这可能会让第一次接触的人误以为“脚本里$_有问题”。其实不是,只是这个变量在启动初期有特殊初始值,等第一条命令执行完就会被覆盖。

在实际使用中,我个人的习惯是:交互式命令随手用$_,需要写成可复用工具时用函数参数。这两种方式并行不悖,但边界要清楚。$_解决了“少敲一遍”的效率问题,函数封装解决了“稳定可靠”的质量问题,两者加起来才是完整方案。

如果在实际使用中遇到$_没有按预期输出、目录切不进去的情况,先检查两件事:一是用的 shell 是不是 bash 或 zsh,二是命令里有没有用双引号把$_包住。这两个检查基本能覆盖九成以上的异常。

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

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

立即咨询