跳转至

最开始的shell命令与shell的基本操作

引言

上一章的最后我们写出了我们的第一个bash命令,这一章,我们将从那条命令开始,介绍我们的第一个命令echo

注:上一章我们也提到了,通常我们在shell中完成任务的方式是运行其他软件,但是也有一些shell的内置命令,它们并不能算是软件。为了简单,在本文档中我将不会区分,把它们都称之为命令,因为他们用起来几乎没有区别。

最开始的shell命令——echo

echo是属于GNU coreutils的软件,它的基本功能就是在屏幕上打印一段文字。把bash命令看作是一种编程语言的话,echo和其他语言的中print或者类似的函数地位差不多,但功能更简单一些。

echo最基础的功能就像下面这样:

echo "Hello world!"
输入这行命令之后,屏幕上应该会输出Hello world!(在下一行,因为你输入了一个回车表示一个命令的结束)。一般开始学习一门语言的时候我们都会从输出"Hello world"开始,我们的bash学习也遵照这样的一个传统。(这似乎已经成为一个梗了)

如果你已经有其他编程语言的基础,你肯定会很自然地产生一个问题。我们想要输出一些键盘上打不出来的字符该怎么做呢?换句话说,echo是否支持转义字符呢?

我们可以先做一个尝试:

echo "Hello\n world!"
使用这条命令会发生什么?Helloworld之间并没有如愿出现换行符,而是原封不动地打印了一个\n

看上去我们还差了一点什么。我们缺少的东西叫做命令行参数。这个概念我们将在下一节仔细介绍,这里我们先尝试使用一下。

echo -e "Hello\n world!"
这条命令中我们增加了一个-e(请注意空格哦)。输入这条指令然后回车...输出结果中确实出现了换行。因此我们知道了,echo确实可以使用转义字符,但是需要命令行参数-e

另外,命令行参数-E可以强制让echo不进行转义,但这是echo的默认行为,所以几乎没什么用。

最后,echo还支持两种使用ascii码的转义: 1. \0NNN:三个N表示最多三位的八进制数。 2. \xHH:两个H表示最多两位的十六进制数。

小彩蛋: 尝试下面这两个指令

echo -e "\a"
你应该会听到一个音效(取决于你的操作系统)。
echo -e "\x1b[31mHello world!\x1b[0m"
你应该会看到一个红色的Hello world!。如果你想知道这是怎么做到的,去了解一下escape code

命令行参数

如果你学过什么是函数调用,你会知道很多函数在使用的时候需要传入参数(argument)。我在这里并不想讨论argument和parameter的区别,所以我们就统一称为参数。

命令行参数(command line argument),名字看上去就跟函数参数类似。实际上作用也类似,函数参数是在程序中给函数传入数据的,命令函参数则是在shell中给命令(实际上是软件)传入数据用的。命令行参数主要有两种作用:传入数据和控制命令的行为。

命令行参数主要有三种形式:位置参数(positional argument), 选项参数(optional argument), 标志参数(flag argument)。下面我将先介绍命令行参数的基本形式,然后分别介绍这三种形式的命令行参数。

命令行参数的基本形式

命令行参数是跟在命令行命令后面的,以空格间隔的一系列字符串,每两个空格之间会被认为是一个参数。这就是为什么我上面说到需要注意空格。如果你的一个参数中不得不包括空格,可以用引号(通常'"都行)把它包裹起来。

位置参数

位置参数(positional argument)是在命令行中有特定位置的参数,也就是说,cli程序通过参数的位置来区分这些参数。这里所说的位置,指的是参数的顺序。因此,对于位置参数来说,顺序是很重要的。另外,后面我们会学到的选项参数和标志参数是不区分顺序的,甚至个数都是可变的,因此位置参数和其他的参数不能掺杂在一起使用。通常情况下,位置参数在所有参数的最前面,或者所有参数的最后面。在最前面的位置参数必须有确定的个数,在最后面的位置参数个数可以是不定的。

再拿出前面的例子:

echo -e "Hello\n world!"
这里的"Hello\n world!就是位置参数,它传入的是echo要打印的信息。同时可以注意到,我使用引号包裹了这个参数,这是因为这个参数中出现了空格。我们可以进一步做一个尝试,将-e放到最后会发生什么呢?
echo "Hello\n world!!" -e
你会发现,-e没有起作用,但是却被打印了出来,这是因为-e被认为是一个位置参数,并且被echo作为了要打印的信息。也就是说,这里的位置参数个数是不定的,凡是在echo参数最末尾的位置参数都会被认为是要打印的信息,多个参数会被一起打印并且中间会用一个空格间隔。

利用上面我们得到的信息,尝试尝试着修改一下我们原本的命令,因为如果有多个参数,参数之间就会被加上空格,因此Hello\nworld!之间的空格是不是可以省略掉呢?这样我们是不是就可以不用加引号了呢?我们可以尝试一下下面这个命令。

echo -e Hello\n world!
看上去不太成功。这是因为echo的转义字符必须要在引号内才能生效。也就是改成下面这个样子就能成功了。
echo -e "Hello\n" world!

注意:如果你使用的不是bash,而是其他的shell,上面这些可能指令的行为可能不同,因为其他的shell可能会内置自己版本的echo指令,它们的行为和echo类似,但是在一些特殊的情况下会有不同。感兴趣的同学可以自行去探索它们的行为。

选项参数

选项参数(optional argument)通常是两个或者更多的参数,以--xxx或者-x开头,后面接一个或者更多个参数(都是以空格间隔的)。选项参数中开头那个--xxx-x表示后面给出的参数属于哪个选项,后面接着的是参数的具体内容。它和位置参数功能上类似,但是因为前面有指定选项的标志,因此选项参数不需要有固定的顺序。并且有默认值的选项参数可以省略, 通常情况下相同选项的选项参数只能出现一次。

Python的同学可能会觉得熟悉,Python的函数也支持选项参数。例如:print("Hello world", end="!")。这里的end="!"就是选项参数。

上面给出的选项参数的开头有两种形式,这是因为通常情况下,命令行参数的选项中-后面只能接一个字母,具体原因在后面命令行参数的缩写中会提到。但是只用一个字母又很容易冲突,因此使用--,后面可以接一个单词。很多时候常用的选项参数会同时有--xxx的全称形式和-x的简称形式。

例如,很多需要传入文件名的命令都会有--file /path/to/file这个选项,它也常常可以用-f /path/to/file这样的简称形式。

标志参数

标志参数(flag argument)和选项参数很像,但是它通常只有一个参数作为选项,没有后面的内容,也就是单独的形如--xxx-x的形式。标志参数相当于是给程序传入了一个布尔值,有这个参数表示true,没有这个参数表示false,通常用于表示某项功能是否开启。习惯上同一个标志参数只出现一次,但是有的时候可能会使用同时出现多次的相同标志参数用于表示一些特殊含义。

还是用上面的例子:

echo -e "Hello\n world!`
其中的-e就是标志参数,表示开启转义字符的功能。它只有简称形式,没有全称形式。

标志参数也不区分顺序,而且通常标志参数和选项参数可以混杂在一起使用。

后面在学习了很多指令之后你们会发现,很多选项参数和标志参数已经成为了一种不成文的约定,例如:-v是"verbosely"的缩写,作为标志参数表示更输出更详细的执行过程。

命令行参数的缩写

目前我们学习的命令还比较简单,但是后面我们可能会遇到一些命令,需要数量非常多的参数。尤其是当我们需要很多标志参数的时候,我们需要连着输入很多的-和空格,很麻烦。因此大部分命令都会支持命令行参数的缩写,允许我们将多个标志参数连在一起。

命令行参数的缩写形式是这样的-xyz这里xyz分别表示了三个不同的参数,可以把他们连在一起,省略中间的空格和-。这里举一个后面会讲到的例子:

ls -l -a
可以缩写为
ls -la
另外需要注意的是,只有拥有简称形式的参数可以缩写,缩写的顺序一样无所谓。这也就是为什么简称形式只能是一个字母,因为这样才能在连在一起的情况下区分开它们。但是还有一种特殊情况,缩写的参数,最后一个可以是选项参数,例如:
git -a -m "first commit"
它可以缩写为
git -am "first commit"
但是不能缩写为
git -ma "first commit"
因为选项参数必须在最后一个,并且后面紧跟选项的具体内容。

大部分命令都会有很多命令行参数,但是除了位置参数,很多参数都因为有默认值而可以省略。这个文档中介绍的命令一般也只会介绍它的常用的参数和重要的参数。其余的参数自己去查阅网络上的资料,或者使用第13章介绍的方式自行探索。

类似echo的指令——printf

printf指令也GNU coreutils的一个软件。并且很多shell都会内置自己的printf指令,它们的功能与bash中的可能会有细节上的不同。下面介绍的内容只在bash中正确,在其他的shell中不保证正确。

会C语言的同学一定会觉得这个名字十分熟悉。是的,正如你们想的那样,这个命令和C语言的printf十分相似。并且它的能力完全覆盖echo。下面就是使用printf的例子:

printf "Hello\n world\n"
这里我们可以看到它与echo的两点不同: 1. printf不需要使用-e就可以进行转义。 2. echo的输出会额外添加一个换行,但是printf不会,需要我们手动添加一个换行符。如果你去掉这个换行符,你会发现world和下一个命令行提示符挤在了一起。

但是,它还有比echo功能强得多的地方,就是它支持和C语言一样的格式化输出功能,并且它的格式化模板和C语言也几乎一样。例如:

printf "%05d %03d\n" 42 42
这里给不熟悉C语言的同学解释一下,这里的第一个字符串是格式化的模板,其中%开头的是格式化参数,后面的参数会被依次根据规则进行格式化并替换进前面的字符串,最后将整个替换后的字符串输出。这里%05d表示将后面的第一个参数看做十进制整数,并且不足5位的话前面用0补齐到5位数,因此模板字符串中%05d会被替换成00042。后一个类似,它的意思是不足3位的话用0补齐到3位数。

它和C语言中的printf也存在一些区别: 1. printf命令的模板和填充的参数都用命令行的位置参数传入,因此用空格间隔。和C语言一样,格式化模板字符串只能有一个,但是不一定要用"包裹,符合命令行位置参数的要求就可以。 2. printf中使用%s的话,后面替换的字符串不会进行转义,如果需要转义的话要使用%b。 3. 命令行命令中不存在指针,因此不能使用%p. 4. 有一个特殊的模板%q表示将后面的参数格式化成可以用作命令行参数的形式,其中的字符会被转换为转义字符的形式。

实际上可能还有一些细节我没有提及,大家可以自己去探索。

shell的基本操作

经过这一章前面的内容,如果大家都跟文档内容操作的话,应该已经执行过几个命令了。在实际使用的shell的时候,我猜肯定有人已经产生了一些操作上的疑问,像是"为什么我不能用鼠标移动光标?"之类的。因此这里我简单讲解一下shell的一些基本操作方式。

首先我来解释一下,为什么shell的操作方式和我们遇到过的其他图形化的界面如此不同呢,我认为主要有两个原因: 1. shell刚出现的早期,还没有出现鼠标,因此人们只能用键盘操作。 2. 对于程序员来说,只使用键盘效率是更高的,因为经常在鼠标和键盘上切换会花费额外的时间。

因此shell可以完全没有鼠标操作。当然,因为现在电脑基本都有图形化界面,所以一些鼠标操作也是可以在shell上进行的,比如可以使用鼠标滑动来选中文字,但是你只能使用Ctrl-C来复制。

另外,对于使用Windows terminal的同学,有一个快捷的鼠标操作,在界面上点击右键可以粘贴。

因为shell的操作相关的内容比较琐碎,所以我这里就分条列举了。(下面使用Ctrl键的地方在Mac电脑上使用Command键)

  • 在输入命令的时候如果中间出现小的错误可以使用左右键在一行中左右移动光标进行修改。光标通常是实心长方形,它相当于光标在长方形的左边缘,也就是它覆盖的字符前面。
  • 即使光标不在一行的最末尾,直接按回车也可以执行整条命令。
  • 使用上下键可以在历史记录中切换,如果你想要执行某个之前执行过的命令,这会很方便。
  • 直接按回车shell会换行并重新打印一遍提示符。
  • 如果你的命令输入了一部分,但是你不想执行这条命令了,你可以按Ctrl-C,shell会换行并重新打印一遍提示符,但不会执行命令。
  • 如果一条命令在执行过程中你想要终止它,可以按Ctrl-C(可能没有用)或者Ctrl-D(一定有用),具体原理可以看第9章。因为shell也是一个软件,所以在没有命令执行的时候按Ctrl-D可以退出shell,如果你的终端只有一个shell,那么终端也会关闭。
  • 因为终端拥有图形化界面,所以你可以使用鼠标滚轮翻看之前的执行情况,如果是没有图形化界面的终端,那么离开屏幕的内容就永远消失了。

小结

这一章介绍了两个用于在屏幕上输出文字的命令作为我们bash命令的"Hello world"。另外,我们还介绍了命令行参数的知识,这对于cli程序和命令行命令的使用来说十分重要,后面也会经常用到,必须熟悉。最后我们介绍了shell的一些基本操作,这些操作可以让你更丝滑地使用shell,如果你经常使用就会自然地变得很熟练。