跳转至

操作文件系统的shell命令

引言

上一章我们学习了shell命令的"Hello world",从echo开始入门了shell命令。因为shell是用来帮助人类操作操作系统的,所以从这一章开始我们将根据操作系统内核的不同组成部分来学习操作相应部分的命令。

文件系统简介

使用计算机的时候,文件是我们一定离不开的东西。文件本质上就是一块持久化(durable)的数据。持久化就是指电脑关机之后数据仍然存在,不会丢失。为了实现持久化,文件必须要存储在持久化设备当中,比如磁盘,固态硬盘等。这些物理设备是计算机中的重要资源,并且对于普通用户来说直接操作它们既繁琐又困难。

因此为了管理这些持久化存储设备,并让用户能更加便利地保存数据,操作系统提供了文件系统(file system)这个概念,并把磁盘上的数据抽象成文件(file)。向磁盘中读写数据的操作,也就被简化成了读写文件系统中的文件。

为了方便操作系统管理和用户使用,几乎所有的文件系统都会把文件组织成一个类似树状的结构(不知道是什么的可以点这个链接,或者如果你希望更简单但不那么严谨准确地了解树的大致概念的话可以看附录C),叶子结点是文件,其他的节点都是文件夹(文件夹本质上也是一种文件)。对于Linux的文件系统来说,文件系统的文件一定是一棵树,对于其他操作系统来说不一定。

虽说这里我说Linux文件系统像是一棵树,我们有办法能够看到这棵树吗?当然有。你可以尝试下面这个命令

tree /
这条命令会将整个Linux文件系统打印成一棵树的样子,因此他会运行很长的时间,输出很多的东西。如果这样你感到不耐烦,也可以试试这个
tree /tmp
这条指令只会打印/tmp文件夹下的内容,会短很多。等下一章你学过了文件路径的知识之后,你可以尝试在tree后面加上任意一个文件夹的路径,它都会将这文件夹下的所有内容以一棵树的形式打印出来。

注1:这里我说类似树状的结构,是因为文件系统通常还会提供链接的功能,这些内容你会在第13章中学到。

注2:对于Windows操作系统的文件系统来说,更像是每个盘符下有一棵树。

查看当前文件夹的内容——ls

基础用法

说起文件系统的命令,我第一个想到的就是lsls是GNU coreutils中的一个软件。它的作用是查看当前文件夹下里的内容,也就是列出有哪些文件和文件夹。对于习惯了图形化界面的我们来说,文件系统就是在窗口中打开的文件夹,一眼就能看出有哪些文件。因此初次使用shell的同学可能会感到很不习惯。但是对于熟练使用shell的人来说,进入文件夹后第一件事就是输入ls来查看有哪些东西,这已经成为了一种条件反射。

现在大家可以尝试在自己的shell界面输入

ls
如果你是从零开始跟着文档操作的话,现在你的文件夹下面应该只有一个叫做test.txt的文件。也就是输出只有一个test.txt。如果你的文件夹下面没有文件,那么ls什么也不会输出。

现在你可以尝试使用第二章的那条指令,换几个不同的文件名(你也可以更换echo输出的内容),在你的文件夹下面再创建几个文件,然后使用ls看看结果。

根据你的shell和终端的不同,ls的输出结果可能会根据文件的类型有不同的颜色。

展示其他文件夹的内容

ls命令的最末尾可以使用一个位置参数,表示ls展示的文件夹的路径,如果没有这个参数就表示展示当前文件夹。例如,

ls /dev

查看更详细的信息

但是,ls的输出实在是太简单了,只有文件名。相比之下,图形界面的系统的的文件系统会输出更多详细的信息,例如文件的大小,最近一次修改时间等等。ls能查看更详细的信息吗?当然可以,你可以尝试下面这个命令

ls -l
这里我们使用-l参数(list的缩写),可以让ls输出更详细的信息。我们可以看到,输出信息的第一行是这个文件夹中所有文件的总大小,从第二行开始每个文件占一行,大概是这样的
-rw-r--r-- 1 root root 14 Oct 27 10:43 test.txt
这里第一列表示文件类型和文件权限信息。第一个字符表示文件的类型-表示普通文件,d(directory)表示文件夹,其他的文件类型我们现在不用关心,之后会提到。从第二个字符开始,每三个字符一组,同一组的三个字符分别表示文件的r(read)读权限,w(write)写权限,和x(excute)执行权限。三个组从前到后则分别表示文件所有者的权限,文件所有者所在的用户组的权限和其他用户的权限。用户组就是linux系统中管理权限的一种方式,可以将一些用户分成一个组,组内的用户拥有相同的权限。如果没有相应的权限,这里就会使用-表示。另外,在其他用户的执行权限的位置上,除了x-还可能会出现t。这是一个特殊的权限,通常用于文件夹,表示没有删除文件夹和文件夹中文件的权限(但是有创建文件的能力)。

以"ex"开头的单词人们总是喜欢用"x"而不是"e"来缩写,这很奇怪。或许是因为"ex"读音像"x"。

对于普通文件来说,读写权限很好理解,执行权限就是用户(组)是否有权限将这个文件作为一个可执行二进制文件执行。对于文件夹来说,这些文件权限含义有些不同,读权限指的是用户(组)是否有权限读取文件夹的内容(也就是对于没有读权限的文件夹,你无法使用ls),写权限指的是你是否能在这个文件夹中创建,删除文件和修改文件名,执行权限则指的是你是否能进入这个文件夹(也就是能否使用下面要讲到的cd)。

对于上面这个例子来说,第一列表示的信息是:这个文件是一个普通文件,对于所有者来说有读写权限,对于所有者所在的用户组(的其他用户)和其他用户来说只有读的权限。

第二列表示文件的硬链接数量。硬链接类似于Windows系统的快捷方式。文件系统的这个概念会在第13章讲到,没有其他硬链接的文件这里就是1。第三列表示文件的所有者。第四列表示文件所有者所属的用户组。第五列表示文件大小,默认使用字节为单位,但是你可以使用-h参数让这里的输出使用人类可读的单位(K-\(10^3\), M-\(10^6\), G-\(10^9\), T-\(10^{12}\) 等)。还有其他的参数可以改变文件大小使用的单位。第六列和第七列表示文件最近修改时间的月和日,第八列也表示的最近修改时间,当最近修改时间比较近的的时候这里表示具体时间,当最近修改时间不在这一年的时候这里使用年份。最后第九列就是文件名。

隐藏文件

一般的文件都会提供隐藏文件的功能,很有趣的是,在linux系统中,用文件名来区分一个文件是否是隐藏文件。你可以尝试使用下面这个命令

echo "hidden" > .hidden
然后你再使用ls,你会发现,ls并没有列出.hidden这个文件。这就是因为linux系统把所有文件名是.开头的文件都当做了隐藏文件。隐藏文件只是不显示,但是是存在的。

当然,ls也是提供了显示隐藏文件的功能的,也就是参数-a(-all的缩写)。你可以尝试使用这个命令

ls -a
你会发现输出结果中出现了.hidden。同时,你可能注意到了,输出结果中还出现了两个奇怪的文件...,它们两个是两个特殊的文件,所有的文件夹都拥有,具体内容将会在下一章学到。

如果你不想在你的输出中显示这两个特殊的文件,你可以使用-A(--almost-all的缩写形式)。

你也许会好奇,如果同时使用-a-A会怎么样?遇到这样的问题,很自然的想法应当是亲自去试试。你可以尝试ls -aAls -Aa(这里使用了命令行参数的缩写,还记得吗)。

很神奇,两个结果不同。也许你已经能发现了,当两个参数的功能存在冲突的时候,在参数顺序中排在后面的参数会覆盖排在前面的参数的行为。也就是,前一章中讲到的选项参数和标志参数的顺序无所谓其实不完全正确。

按输出排序

如果一个文件夹下面有很多的文件,我们可能会希望能将输出的文件按某种方式排序,ls当然也提供了这种功能。默认情况下ls按照文件名的字典序排序。可以使用不同的参数让ls按不同的规则排序: * -S:将文件按大小排序。(Size的缩写) * -t:将文件按最近修改时间排序。(time的缩写) * -v:在默认排序的基础上,对于相似文件名中只有数字不同的,按照这个数字排序,因为人们通常使用相同名称加数字表示文件版本,也就是按照文件版本来排序了。(version的缩写) * -X:按照文件后缀的字典序排序。(eXtension的缩写,又是"ex"缩写成"x"的例子) * --group-directories-first:将文件夹放在文件前面,再分别排序。 * -U:强制使用默认排序。(不知道是什么的缩写)

还有--sort参数,用法是--sort=xxx其中将xxx替换为下面这些单词分别有不同效果。 * none:效果与-U相同。 * size:效果与-S相同。 * time:效果与-t相同。 * version:效果与-v相同。 * extension:效果与-X相同。 * width:按照文件名的宽度排序。~~(强迫症狂喜)~~

最后,使用-r(reverse的缩写)可以将排序顺序从升序变成降序。

进入其他文件夹——cd

到现在为止,我们的所有操作都只在同一个文件夹下面进行。但是文件系统当然不止一个文件夹,因此我们应该有进入和操作其他文件夹的能力。为了表示我们的位置和我们要操作的文件和文件夹的位置,大部分操作系统中提供文件路径这个概念,由于内容比较多且比较复杂,更详细的介绍会在下一章中。bash中提供给我们进入其他文件夹的命令就是cd,这是一个shell内置的命令。当前所在的文件夹实际上不是操作系统或者文件系统的一个状态,而且shell的一个状态,因此只能由shell自己来实现这个功能。cd的用法也比较简单,它只接受一个位置参数,表示要进入的文件夹。例如:

cd /tmp
表示进入路径为/tmp的文件夹。另外有一个常用的命令
cd ..
表示进入上一级文件夹。实际上..可以理解为是一个特殊的路径,具体的内容将会在下一章讲解。

创建文件夹——mkdir

上面展示的命令主要还是读取文件系统的内容,下面将要展示的两个命令让你可以使用shell修改文件系统的内容。第一个就是mkdir,这是GNU coreutils中的一个软件,它的作用是创建一个文件夹。

基本用法

mkdir接受一个在最末尾的位置参数,表示要创建的文件夹的路径,例如:

mkdir ./testdir
这条命令会在当前文件夹下创建一个叫testdir的文件夹。你可以使用ls看到这个文件夹,也可以使用cd ./testdir进入这个文件夹。

不基本的用法

这里介绍mkdir的一个常用选项参数--parents/-p。因为很难概括这个选项的作用,所以取了这个小标题。它的作用主要有两个。首先,如果你使用mkdir尝试创建一个已经存在的文件夹,那么它会给你报一个错误,但是如果加上了-p,那么就不会报错,但是什么也不会做。另外,如果你试图创建的文件夹的路径上的上一级文件夹不存在,那么它会自动创建上一级文件夹,然后再创建这个文件夹。

修改文件的最近修改时间——touch

touch是GNU coreutils的一个软件,它的作用是修改文件的最后修改时间。名字挺形象的,对吧。

基本用法

touch接受一个在最末尾的位置参数,表示要修改时间的文件路径,例如:

touch test.txt
执行完这条命令之后你再使用ls -l查看test.txt的信息,你会发现它的最近修改时间变成了你执行这条命令的时间。

touch命令还有一个作用,如果你后面的文件路径下不存在这个文件,它会创建一个新的文件,有的时候我们使用这个命令来创建一个新文件。如果你不想要这种行为,而是希望touch在文件不存在的时候报错告诉你,那么你可以加上-c选项。

touch还支持修改文件的最近修改时间到指定的时间,使用-d或者-t或者--time,这个功能的具体用法你可以自己去查阅相关的资料。

查找文件——find

find是GNU coreutils的一个软件,它的功能是在文件系统中查找文件。

基本用法

find接受一个在最开头的位置参数,表示要查找的文件夹路径,后面可以使用参数表示查找的限制。最常用的是-name(没有缩写形式),表示要查找的文件名称。

find . -name test.txt
这里.表示要查找的是当前文件夹(包括当前文件夹下面的文件夹),也就是在当前文件夹以及所有子文件夹里面查找名为test.txt的文件。它会输出符合要求的文件的路径。

你可能会觉得这个命令的参数形式比较奇怪,明明是全称形式,但是只有一个-。这是因为这里的参数只是看起来像是选项参数,但其实它是位置参数。后面我们可以看到,允许使用多次相同的参数来表示复杂的查找逻辑。把这里的-name看作是提示后面紧跟着的参数的类型而不是选项参数的名称更为合理。

-name查找的文件名称中可以包含通配符*,用来表示这个位置可以匹配任何0个或多个字符。例如:

find . -name "*.txt"
表示在当前文件夹下查找所有以.txt为后缀名的文件。也就是前面*的位置上是任意的0个或多个字符都满足我们的要求。最好使用"把匹配的字符串包裹起来,以防它们不能正常起作用。

了解正则表达式的同学可能会觉得这个很熟悉,但是要注意,-name不支持正则表达式匹配,它只支持*?[]这三种通配符。其中?表示1个任意字符.[]表示可以匹配字符集合,例如:[ab]pple可以匹配applebpple

find查找的基本逻辑

find查找文件的本质是运行后面的查找语句(或许像是SQL,不知道这是什么的同学可以忽略)。find的查找语句是由下面五种类型的基本语句组成的: 1. 测试语句(Tests): 可以判断一个文件是否拥有某种属性并返回truefalse,例如:-empty可以判断一个文件是否为空。前面提到的-name也是一种测试语句,测试一个文件名是否匹配它后面的那个字符串。 2. 动作语句(Actions): 可以对文件执行某种操作,通常根据执行操作的成功与否,返回truefalse,例如:-print可以打印文件的名称。 3. 全局选项(Global options): 设置查找行为并且对整个命令中的全部查找都有效,例如:-depth要求查找过程按深度优先搜索的顺序进行。 4. 局部选项(Positional options): 设置查找的行为,但是只对紧跟在它后面的那个语句有效。 5. 操作符(Operators): 将其他语句的结果合并的语句,例如:-or表示逻辑或。

另外,find可以使用括号对于语句进行组合。但是要注意,()是两个单独的参数,虽然在bash中没有问题,但是在fish或者其他的shell中可能需要用引号将()包括起来,就像下面的例子中那样。

find的语句种类很多,功能很全面,这里我随意举一个例子展示一下find的强大功能。更多的信息请大家根据需要自己去搜索。

find . -path "./testdir/*/test/*" "(" -name "test[1-9].*" -or -name "test1[0-9].*" ")" -not -empty
这个命令表示在当前文件夹下查找满足如下条件的文件: 1. 文件的路径匹配./testdir/*/test/* 2. 文件的名称是test1test19并且后缀名任意 3. 文件非空

小结

这一章介绍了一部分操作linux文件系统的命令,包括用于查看文件夹内容的ls,用于进入其他文件夹的cd,用于创建文件夹的mkdir,用于修改文件的最近修改路径的touch和用于查找文件的find。其中一些命令拥有很多的功能和参数,在这个文档中无法详尽地介绍,需要大家自己去搜索相关自己以及自己操作尝试。

在这一章里,我们也看到,使用linux文件系统时最重要的一个概念——文件路径,我们将在下一章详细讨论这个问题。