刚接触Linux的人,在终端里敲下的第一批命令里,大概率有mkdir和touch。这两个命令简单到看一眼help就会用,但真正到了写脚本、部署服务、维护服务器的时候,很多人才发现,自己对它们的理解其实停留在表面。mkdir不只是"建个文件夹",touch也不只是"新建一个文件",它们背后牵扯到权限体系、时间戳语义、路径解析、shell展开机制,甚至能直接影响你在生产环境里排查问题的速度。这篇内容就从实际使用角度出发,把Linux里创建文件夹和文件这件事彻底讲透。如果你是刚入门Linux的初学者,可以把它当一份完整的实操手册;如果你已经用了一段时间Linux,里面关于umask、特殊字符、批量创建、排查思路的部分,也许能帮你补上一些平时没留意的细节。
1. 想清楚再动手:目录和文件的定位差异
很多教程会直接扔给你一堆命令让背,但我不太赞成这种学法。在敲mkdir和touch之前,先把"目录"和"文件"这两个概念在脑子里的位置摆正,后续操作会顺很多。
1.1 目录与文件:一个是容器,一个是内容
Linux里有一个经典说法:一切皆文件。目录其实也是一种文件,但它是一种特殊的文件,里面记录的并不是我们通常理解的"数据内容",而是一张表,这张表维护着"文件名到inode编号"的映射关系。inode才是真正指向磁盘数据块的索引节点。你可以把目录想象成一个抽屉柜,柜子本身不存东西,但每个抽屉里放了什么、标签上写的是什么,它记得清清楚楚。而我们平时说的"文件",是真正承载数据的实体,是抽屉里那份实际存在的资料。
这个区别带来的直接后果就是:mkdir和touch虽然都算"创建类"命令,但职责完全不同。mkdir(make directory)创建的是容器,用来组织和管理文件;touch创建的是内容承载体,用来存放数据。明白了这一点,你就不会在需要建目录的时候用touch,也不会在需要占位文件的时候傻乎乎用mkdir。
1.2 什么时候用mkdir,什么时候用touch
按我自己的习惯来分,大概是这样的:
- 需要搭建目录层级、项目骨架、归档结构,用
mkdir。 - 需要生成配置文件、日志占位、锁文件、空文件占位,用
touch。
拿实际场景举例。你接手一个Java项目,发现代码里要写日志到logs目录,但程序启动直接报No such file or directory。这时候你要做的是mkdir -p logs,而不是touch logs。反过来,你写一个shell脚本,需要判断某个锁文件/tmp/app.lock是否存在,不存在就创建,这时用touch /tmp/app.lock就非常合适,因为它天然满足"存在就跳过,不存在就创建"的语义。
还有一类高频场景:很多服务启动时要求配置文件必须存在,哪怕里面是空的也认。最常见的做法就是touch /etc/xxx.conf生成一个空配置占位,等服务首次启动再往里面写内容。这里选touch而不是>重定向的原因,我后面会细讲,简单说就是touch在文件已存在时不会报错、也不会清空内容,非常安全。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. mkdir:从单层创建到递归创建的完整拆解
mkdir是Linux里最基础的命令之一,但它有几个很重要的细节,恰恰是很多人容易忽略的。
2.1 最简单的mkdir用法
bash复制mkdir test
这条命令会在当前目录下创建test目录。看起来简单到不能再简单,但有一个点必须注意:如果test目录已经存在,mkdir会直接报错:
code复制mkdir: cannot create directory 'test': File exists
这个报错不只是让终端变难看的问题,它会导致命令的退出码(exit code)变成非零值。在shell脚本里,这可能会触发set -e直接中断脚本执行,也可能让CI流程判定失败。所以写脚本时,要么先判断目录是否存在,要么干脆用-p参数,让命令在目录已存在时静默通过。关于-p,下面马上说。
2.2 -p递归创建为什么是高频用法
bash复制mkdir -p a/b/c
这条命令会一次把a、a/b、a/b/c三层目录全部创建出来。-p全称是--parents,它有两个作用:一是递归创建父目录,二是在目录已存在时不报错、直接跳过。
为什么说-p是写脚本的标配?原因有三个。
第一,省事。不用一层层mkdir a、mkdir a/b、mkdir a/b/c,一条命令解决,可读性也好。
第二,幂等。同样的命令执行十遍和执行一遍效果一样,退出码都是0。这一点在部署脚本、初始化脚本里极其重要,因为脚本往往会在"全新环境"、"部分初始化环境"、"重复执行环境"里各跑一遍,只有幂等才能保证不出问题。
第三,配合其他命令构建目录树时是标准姿势。比如你写一个部署脚本,里面有cp、rsync、tar解压之类的操作,提前用mkdir -p把目标目录结构准备好,是最稳妥的做法。
实际开发中,部署脚本里最常见的一种写法是:
bash复制mkdir -p /opt/app/{logs,conf,bin,lib}
这一条命令会创建四个目录。花括号展开(brace expansion)是shell的一个特性,我后面会专门展开讲,但这里你已经能看出-p加花括号的威力了。
2.3 -m权限参数与umask的关系
bash复制mkdir -m 755 test
-m(--mode)可以在创建目录的同时指定权限。这个参数看起来很简单,但牵扯出一个很多教程没讲透的概念:umask。
如果你不指定-m,mkdir出来的目录权限到底是多少?答案是:取决于当前shell的umask值。Linux里目录默认权限基准是777,然后减去umask值,就是最终权限。一般Linux发行版默认umask是022,所以mkdir默认创建出来的目录权限是777 - 022 = 755(rwxr-xr-x)。但如果你的umask被改成077,那么mkdir出来的目录权限就变成700。这会导致什么后果?如果脚本在某个改了umask的环境下运行,创建出来的目录其他用户完全进不去,服务进程可能就启动失败或者无法写日志了。
-m参数的意义就在于,它可以无视当前umask,强制指定目录权限。比如:
bash复制mkdir -m 700 secret_dir
不管umask是什么,这个目录创建出来就是700,只有创建者自己能进。在安全敏感的部署场景里,这种精确控制很重要。
顺带提一嘴,touch创建文件的默认权限也受umask影响,但基准值不同。文件默认基准是666,减去umask 022后就是644(rw-r--r--)。这也是为什么touch出来的文件默认没有执行权限的原因。理解了umask的机制,你对"为什么Linux默认建出来的文件是644、目录是755"这个问题,就有了根本性的答案。
3. touch:不只是"新建文件"那么简单
touch这个命令名字起得很妙,字面意思是"碰一下"。它的核心功能其实是修改文件的时间戳,创建空文件只是它的一个副作用。但恰恰是这个副作用,让它成为Linux里最高频的命令之一。
3.1 touch的基础用法
bash复制touch file.txt
在file.txt不存在时,这条命令会创建一个0字节的空文件;在文件已存在时,它会更新这个文件的访问时间(atime)和修改时间(mtime)为当前时间,而且不改变文件内容。
这里有一个很多人忽略的关键区别:touch不会像重定向符号>那样清空已有文件的内容。> file.txt也会创建一个空文件,但如果文件已存在,它会把文件内容直接截断成0字节。这是个破坏性操作,我见过不止一次有人在生产环境里手滑用>重定向把配置文件写空了。而touch更新的是元数据(metadata),不动数据本身。所以在需要"确保文件存在但绝不破坏已有内容"的场景里,touch是绝对安全的选择。
3.2 修改时间戳的实际应用场景
理解了touch是改时间戳的,很多进阶用法就顺理成章了。时间戳在工程实践里比你想的重要得多,这里说三个我实际遇到的场景。
场景一:构建系统的增量编译。
make这类构建工具,核心原理就是比较源文件和目标文件的时间戳,如果源文件比目标文件新,就认为需要重新编译。所以在调试构建规则时,touch source.c是常用的手段,用来强制make认为源码发生变化,从而触发重新编译。这个操作比改文件内容、再改回去要方便安全得多。
场景二:日志切割和轮转测试。
日志轮转工具(比如logrotate)会依据文件的时间戳判断日志是否过期。要测试轮转规则是否生效,你不需要真的等一个月,用touch就能快速制造出"很久以前"的文件:
bash复制touch -d "2024-01-01 00:00:00" old.log
-d参数后面跟一个日期字符串,可以精确指定时间。这样一条命令,就把old.log的修改时间变成了半年前,日志轮转脚本一跑,这个文件就会被正确处理。
场景三:备份和增量同步。
rsync、tar这类工具默认依赖mtime判断文件是否变更。有时从备份恢复文件后,所有文件时间都是"当前时间",会导致下一次增量备份时误判全部文件为已修改,把整个数据又同步一遍,白白浪费带宽和磁盘。这时候可以用touch -r参考一个已知正确时间的文件,批量恢复时间戳:
bash复制touch -r reference_file target_file
-r(--reference)表示"把target_file的时间改成和reference_file一样"。这个用法在批量处理时配合find,效果非常好。
touch还有一个常用参数是-t,可以按固定格式指定时间戳:
bash复制touch -t 202401011200.00 file.txt
这个格式是[[CC]YY]MMDDhhmm[.ss],上面的例子就是把file.txt的修改时间设置为2024年1月1日12点00分。注意这个时间是本地时区,而且-t和-d都可以指定时间,但-d更人性化,能接受"2024-01-01 12:00"这种可读格式,日常用-d更方便。
3.3 touch也可以批量创建文件
touch最经典的批量用法是和花括号展开配合:
bash复制touch file{1..10}.txt
一条命令创建file1.txt到file10.txt一共10个文件。这在初始化测试环境、制造一堆临时文件时特别高效。
还有一个我在项目初始化时几乎必用的组合:先建目录,再放占位文件。
bash复制mkdir -p project/src project/docs
touch project/README.md project/src/.gitkeep project/docs/.gitkeep
这里出现了一个约定俗成的占位文件:.gitkeep。为什么要它?因为Git本身不跟踪空目录,如果目录里没有任何文件,提交到远端之后,别人clone下来,这个目录就消失了。放一个.gitkeep进去,既保留了目录,又明确告诉后来者:"这个目录不是忘了删,是故意留空的。"这个技巧几乎所有用Git管理项目的人都会遇到,建议直接记住。
4. 进阶操作:批量创建与花式组合
前文多次提到了花括号展开,这一节系统展开讲,顺便介绍几个更进阶的批量组合方式。这些技巧在纯手工敲命令时未必用得上,但一旦你开始写部署脚本、初始化脚本,它们就是提升效率的关键。
4.1 花括号展开批量创建
Bash和Zsh都支持花括号展开(brace expansion),这是shell层面的一个强大特性。它会在命令真正执行前,把{}里的内容展开成多个参数。
bash复制mkdir -p /tmp/demo/{src,bin,conf,logs,data}
touch /tmp/demo/{README.md,LICENSE,Makefile}
第一行会创建/tmp/demo/src、/tmp/demo/bin、/tmp/demo/conf、/tmp/demo/logs、/tmp/demo/data五个目录。第二行会在/tmp/demo下创建三个文件。两条命令合起来,一个项目的基本骨架就出来了。
花括号展开还能嵌套,并且支持数字区间和字母区间:
bash复制touch photo{001..100}.jpg
mkdir -p project/{src/{main,test},docs,scripts}
第二条命令看起来复杂,展开后实际是:
code复制project/src/main
project/src/test
project/docs
project/scripts
注意{001..100}这种写法是带前导零的数字区间,生成的文件名会是photo001.jpg到photo100.jpg,在排序时非常友好。如果写成{1..100},生成的是photo1.jpg、photo2.jpg……photo100.jpg,排序时photo10.jpg会排在photo2.jpg前面,遇到需要按文件名顺序处理的脚本时容易出问题。
这里有一个重要提醒:花括号展开在Bash、Zsh中可用,但在sh(很多系统默认是Dash)中不支持。如果你的shell脚本开头写的是#!/bin/sh,然后运行环境Ubuntu的/bin/sh又指向Dash,那么脚本里的花括号不会被展开,命令会原样执行,导致创建出带花括号字面量的奇怪目录名。这是跨环境脚本最常踩的坑之一。稳妥的做法是:脚本里不用花括号展开,或者显式把shebang写成#!/bin/bash。
4.2 find与xargs的组合
比花括号展开更复杂的批量创建需求,通常要配合find命令。比如,你需要在/home/user/project目录下所有子目录里都放一个README.md:
bash复制find /home/user/project -type d -exec touch {}/README.md \;
-type d表示只找目录,-exec后面跟要执行的命令,{}是找到的路径的占位符,\;表示命令结束。这个写法简单直接,但也有隐患:如果目录名里有空格或者换行,find默认的输出格式可能会出问题。
更稳健的写法是配合xargs:
bash复制find /home/user/project -type d -print0 | xargs -0 -I {} touch {}/README.md
这里的-print0让find用空字符(\0)而不是换行符来分隔路径,xargs -0就按空字符来切分输入,-I {}则指定了占位符。这三个参数组合起来,能保证文件名里的空格、换行、特殊字符都不会被错误解析。
这个问题我在实际项目里碰到过不止一次。曾经有个同事写了个日志收集脚本,用find遍历目录后做后续处理,结果某个用户目录下有个文件夹叫"my data",脚本一跑,路径被拆成了两截,硬生生在别的路径下创建了一堆错误文件。从那以后,凡是要遍历文件名的脚本,我一律使用-print0搭配xargs -0,再也没出过这类问题。
4.3 创建带空格或特殊字符的文件名
Linux文件系统允许文件名里带空格、中文、甚至换行符。这种灵活性是好事,但也容易让命令行新手栽跟头。
创建一个带空格的文件:
bash复制touch "my file.txt"
注意这里的引号不能省略。如果写成touch my file.txt,shell会把它解析成两个参数,实际创建的是my和file.txt两个文件。所以遇到空格,必须用引号把整个文件名包起来。
文件名里有$、*、?这类特殊字符时更要小心。$会被当成变量前缀,*和?会被当成通配符。解决办法是加引号,或者用反斜杠转义:
bash复制touch "\$weird*file.txt"
touch \$weird\*file.txt
两种方式效果一样,看个人习惯。
还有一个更隐蔽的情况:文件名以中划线开头,比如-f。直接执行touch -f,shell会把它解析成touch的-f参数(force),而不是文件名。解决办法是在命令后面加--来终止参数解析:
bash复制touch -- -f
--这个双横线在很多GNU命令里都有,表示"后面的内容全部是参数,不再解析选项"。这个小技巧救过我很多次,尤其是处理以-开头的临时文件时。
5. 实战中常踩的坑与排查思路
基础命令也有不少暗坑。这一节把我自己踩过的、帮别人排查过的问题集中整理一下。每个问题我都会给出完整的排查链路,希望能帮你建立一套清晰的排查思路。
5.1 权限不足:Permission denied
这是最经典的报错。在普通用户下执行mkdir /root/test,会直接返回:
code复制mkdir: cannot create directory '/root/test': Permission denied
排查链路大概是这样:
- 先用
id确认当前用户身份,看是不是真的没有权限。 - 用
ls -ld /root看目标父目录的权限,确认写权限是否缺失。 - 确认无误后,有两种选择:要么用
sudo mkdir /root/test提权执行,要么换个有权限的目录。
这个问题的变体往往更隐蔽。比如某个父目录你确实有写权限,但它的上一层目录设置了sticky位(sticky bit),/tmp目录就是典型。sticky位的作用是:在这个目录下,只有文件的所有者(或者root)才能删除或重命名文件,即使其他人对目录有写权限也不行。在企业多用户服务器上,这种权限问题非常常见。你不能光看报错就盲目sudo,得先想清楚:到底是真没权限,还是sticky位在起作用?结合ls -ld的输出,基本一次就能定位。
5.2 文件已存在时的表现差异
同一个"创建"操作,在不同工具里的表现完全不同。我把它们列出来,方便对比:
| 操作 | 文件已存在时的表现 | 风险等级 |
|---|---|---|
touch file |
不报错,只更新时间戳,内容不变 | 无风险 |
mkdir dir |
报错File exists,除非加-p |
低 |
> file |
清空文件内容为0字节 | 高,破坏性 |
>> file |
追加内容,不覆盖 | 低 |
echo > file |
清空文件并写入新内容 | 高,破坏性 |
我见过最惨的一次事故:运维同事想往nginx配置里追加一行配置,手滑把>>写成了>,一条echo "xxx" > /etc/nginx/nginx.conf执行下去,整个配置瞬间变成0字节。如果当时用的是>>,或者哪怕先cp备份一份,都不会造成这种级别的事故。
注意:只要涉及覆盖写入的场景,养成先备份或先确认内容的习惯,能避免绝大多数低级事故。
顺便说说touch和>的核心区别:touch更新的是元数据,>操作的是文件内容。touch在文件不存在时创建空文件,在文件存在时保持内容原样;>在文件不存在时创建空文件,在文件存在时直接截断。所以,"确保文件存在且不破坏内容"这个需求,请一律用touch。
5.3 路径写错导致命令静默失败
有些错误不会立刻报出来,而是"操作看起来成功了,结果文件不在你预期位置"。这类问题排查起来比较费劲。
典型场景1:相对路径与当前目录不匹配。你在/var/log下执行touch app.log,然后切到/home/user,发现app.log不在预期位置。原因很简单——touch是在它执行的当前目录下创建文件,而不是在你"心里想"的目录下。排查时先pwd确认当前位置,再执行命令,就不会有这种误解。
典型场景2:环境变量为空导致路径改变。执行touch $APP_HOME/logs.txt,如果APP_HOME没有设置,shell会把变量展开成空字符串,命令变成touch /logs.txt,直接在根目录下创建文件。这种情况更隐蔽,因为命令没报错,文件也确实创建了,只是位置不对。排查思路是:先echo $APP_HOME确认变量是否为空,再执行命令;更保险的做法是脚本里加set -u,遇到未定义变量直接报错。
典型场景3:文件名以点开头,是隐藏文件。touch .abc创建成功后,ls默认不显示隐藏文件,很多人第一反应是"创建失败"。其实文件就在那里,用ls -a就能看到。别笑,这个问题在Linux新手里出现的频率远超你想象。
5.4 特殊字符与转义问题
前面4.3节讲了一些特殊字符的处理,这里再补充两个容易忽略的细节。
第一,文件系统层面的大小写敏感。Linux的ext4、xfs文件系统默认区分大小写,所以File.txt和file.txt是两个完全不同的文件。但在Windows上,NTFS默认不区分大小写。同一个项目在Linux上开发,拿到Windows上跑,有时候会因为大小写混用导致文件找不到。这种跨平台问题在项目交接时特别容易爆发,值得提前留意。
第二,花括号展开在不同shell下的表现差异。前面已经详细说过,Bash能用、Dash不能用。这里再强调一遍:#!/bin/sh的脚本在Ubuntu上跑,遇到花括号就凉了。排查这类问题,先看脚本的shebang,再确认/bin/sh软链接指向谁,基本就能定位。
6. 从命令到习惯:目录结构设计的一点私人体会
最后这部分不谈命令参数了,聊点我自己的经验。
用mkdir和touch用久了你会发现,创建文件夹和文件这个动作本身,背后其实是对项目结构的规划能力。同样是建一个项目骨架,新手喜欢先创建一堆空文件夹,老手则是先想清楚这个目录树要支撑什么样的工作流,再动手。
比如,需要被版本控制的目录,要注意不要在里面放临时文件,避免污染Git仓库;不需要版本控制的目录,要配合.gitignore来管理。这一点从创建第一个文件夹的时候就应该想清楚。再比如,日志目录要有明确的命名规则和轮转策略,临时目录要记得定期清理。mkdir创建出来的不应该只是一个个文件夹,而应该是一套可预期、可维护的结构。
还有一个值得养成的习惯:在脚本里用mkdir -p而不是裸mkdir,因为脚本可能在全新环境、部分初始化环境、已存在目录的环境里反复执行,只有-p才能保证幂等。同样,touch配合-p的目录创建,可以做到"目录和文件都不存在就创建,存在就跳过",这是初始化类脚本最常见的模式。
我自己的做法是:把常用命令组合写成一个init.sh,每次开新项目先跑一遍,比手工一条条敲省心太多。脚本内容大致是:
bash复制#!/bin/bash
set -euo pipefail
PROJECT_NAME=${1:-myapp}
mkdir -p "$PROJECT_NAME"/{src/{main,test},docs,scripts,conf,logs}
touch "$PROJECT_NAME"/{README.md,LICENSE,.gitignore}
touch "$PROJECT_NAME"/src/main/.gitkeep "$PROJECT_NAME"/src/test/.gitkeep
这里解释一下set -euo pipefail:-e是遇到错误就退出,-u是变量未定义时报错,-o pipefail是管道中任何一个命令失败都算整体失败。这三个参数配合起来,能让脚本在出问题时立即暴露,而不是一路静默执行到更严重的地步。这段脚本建议直接复制保存,以后新项目直接改项目名就能用。
再说说touch的一个容易被低估的价值:作为调试工具。很多时候你需要快速制造一些文件来验证脚本逻辑,touch配合花括号展开是最快的。比如测试一个日志清理脚本,先touch log{01..50}.log制造50个文件,再修改其中部分文件的时间戳,模拟不同时间的日志,看清理脚本是否按预期工作。这些场景不需要真的写内容,空文件完全够用。
要说的就这么多。mkdir和touch是两个再基础不过的命令,但把基础命令吃透、用对、养成习惯,整个Linux的使用体验都会不一样。下次在终端里敲下这两个命令的时候,希望你能想起这篇内容里那些不起眼但关键的小细节。
