Unix简介

Unix简介

Unix 是一种「操作系统」(Operating System,OS),即介于用户或用户程序与硬件之间的软件层。它负责文件和屏幕输出,并确保许多进程可以并存在一个系统上。但是,它不会立即对用户可见。我们使用 Unix的大多数时候,我们都是在输入由shell 的解释器执行的命令。Shell使实际操作系统调用。可能一共有几种Unix shell 可用,但在本教程中,我们假设我们是使用 sh 或 bash shell,尽管许多命令对于现有的各种 shell 是通用的。

本教程的大部分内容都适用于任何类 Unix 平台,但是Unix并非只有一个:

  • 传统的有几种主要风格的Unix:ATT 和 BSD。苹果的达尔文更接近BSD;IBM 和 HP有自己版本的Unix,Linux 就是另一个变体。它们之间存在着巨大的差异,因此我们正在学习本教程时,可能很长时间看不到它们。
  • Linux 中有各种 Linux 发行版,例如 Red Hat和Ubuntu。这些主要是在系统文件的组织存在不同,因此不必对它们担心。
  • 正如刚才所提到的,有不同的shell,它们确实有很大不同。在这里我们会学习 bash shell,它是旧的sh shell 的改进版本。由于种种原因,bash 优于 csh 和 tcsh shell。其他shell是 ksh和zsh,它们是本身是对bash shell的改进。

文件及相关

目的:了解由存储文件目录组成的 Unix 文件系统,了解用于显示数据文件的可执行文件和命令。

查看文件

目的:学习用于显示文件内容的命令。

命令 作用
ls 列出当前目录下的文件和文件夹
touch 创建新文件或者更新已存在的文件的时间
cat > filename 将文本输入文件
cp 拷贝文件
mv 重命名文件
rm 删除文件
file 声明文件类型
cat filename 展示文件
head,tail 展示部分文件
less,more 渐进展示文件

ls

没有任何参数,ls 命令为您提供了当前位置中的文件列表。

练习 21.1 输入ls会展示什么?

预期结果:如果目录中有文件,则将列出它们:如果没有,则不会给出输出结果。这是标准的 Unix 行为: 没有输出并不意味着出了问题,只意味着没有什么可报告的。

练习 21.2 如果 ls 命令显示有文件,则选择其中一个文件上输入 ls name。使用一个选项,例如, ls - s name, 我们可以得到更多的文件名称。

注释 21:如果我们指定了不存在的文件的名称,我们将得到报错信息。

cat

cat 命令通常用于显示文件,但它也可用于创建一些简单的内容。

练习 21.3:输入 cat > newfilename(我们可以选择任何文件名)然后输入一些文本。使用Control-d 独占一行来表示终止,现在使用cat来展示文件内容:cat newfilename

预期结果:在 cat 的第一次使用中,文本被从终端串联到一个文件。在第二种情况下,文件被 cat 到终端输出。我们在屏幕上应该可以准确地看到我们输入到文件中的内容。

注意事项:请确保在最后一行的输入是 Control-d 。如果我们卡住了, Control-c 通常可以解决。试试这个:用 cat > filename 创建一个文件然后再在某一行的中间按 Control-c 。我们的文件的内容是什么?

注释 22:我们经常会看到用ˆD的符号代替 Control -d。大写字母是出于历史原因:我们使用控制键和小写字母。

man

unix命令的主要来源(尽管并不总是最容易理解的)是man命令,意思是 “手册”。通过这种方式获得的描述被称为手册页。

练习 21.4 阅读ls命令的手册页:man ls。找出一些文件的大小和时间/日期,例如,我们刚刚创建的文件。预期的结果。我们是否发现了ls-sandls-loptions?第一个选项列出了每个文件的大小,通常以千字节为单位,另一个选项给出了关于一个文件的各种信息,包括我们以后要学习的东西。

预期结果:我们找到 ls -sls -l 选项了吗?第一个选项列出了每个文件的大小通常是以千字节为单位,另一个选项给出了关于文件的各种信息,包括我们以后要学习的东西。

注意事项: man 命令使我们进入一个 可以查看长文本文件的模式。这种查看器在Unix系统中很常见(它可以作为 moreor 系统命令使用),所以记住以下导航方式。使用空格键前进,使用 u 键后退。用 g 键到文本的开头,用 G 键到文本的结尾。用 q 键退出查看器。如果我们被卡住了, Control-c 会让我们离开。

注释 15:有一些与文件相关的日期,对应着内容的变化、权限的变化和任何形式的访问。stat 命令给出了所有这些日期。

注释 16:如果我们已经知道我们要找的是什么命令,我们可以用man来获取关于它的在线信息。如果我们忘记了某个命令的名字,man -k关键字可以帮助我们找到它。

touch

touch 命令用于创建一个空文件,或者更新一个已经存在的文件的时间戳。使用 ls -l 命令来确认这一行为。

cp, mv, rm

cp 命令可以用来复制一个文件(或目录,见下文):cp file1 file2 复制了一个 file1 的副本,并将其命名为 file2 。

练习 21.5 使用cp file1 file2来复制一个文件。确认这两个文件有相同的内容。如果我们改变原始文件,复制的文件会发生什么吗?

预期结果:我们应该看到,如果原始文件改变或被删除,副本不会发生变化。

注意事项:如果 file2 已经存在,我们会得到一个报错信息。

一个文件可以用 mv 重命名,mv 的意思是 “move”。

练习 21.6 重命名一个文件。如果目标名称已经存在,会发生什么?

文件可以用 rm 删除。这个命令很危险,原因是没有撤消命令。

head, tail

还有更多的命令用于显示一个文件、一个文件的部分内容或关于一个文件的信息。

练习 21.7 执行 ls /usr/share/wordsls /usr/share/dict/words 来确认我们的系统中存在一个带有字的文件存在于我们的系统中。现在用这个文件试验一下headtailmorewc命令。

预期结果:head 显示文件的前几行, tail 显示最后一行,而 more 使用的是与 man 页相同的查看器。阅读这些命令的man页并尝试增加和减少输出的数量。wc(’word count’)命令报告一个文件中的字数、字符数和行数。

另一个有用的命令是 file:它告诉我们正在处理的是什么类型的文件。

练习 21.8 对不同的 ‘foo’ 做 file foo :一个文本文件,一个目录,或/bin/ls命令。

预期结果:有些信息我们可能无法理解,但要注意的词是要注意的是 “文本”、”目录 “或 “可执行”。在这一点上,建议我们学会使用文本编辑器,如 emacs 或 vi 。

目录

目的:在这里我们将学习Unix目录树,如何操作它,以及如何使用它。

命令 作用
ls 列出目录的内容
mkdir 创建新的目录
cd 改变目录
pwd 展示当前工作目录

一个unix文件系统是一棵目录树,其中一个目录是文件或更多目录的容器。我们将以如下方式显示目录

  1. / .........................................目录树的根
  2. bin........................................二进制程序
  3. home.......................................用户目录的地址

Unix目录树的根部用斜线表示。执行ls / 来查看根目录下有哪些文件和目录看看根目录下有哪些文件和目录。注意,根目录不是我们重新启动我们的个人电脑时开始的位置,也不是我们登录服务器时开始的位置,而是登录到服务器时开始的位置。

练习 21.9 查找我们当前工作目录的命令是pwd。我们的主目录是我们在登录时的工作目录,找出我们的主目录。

预期结果:我们通常会看到类似 /home/yourname/Users/yourname 的东西,结果取决于系统。

输入 ls 来查看工作目录的内容。在本节的显示中,目录名称后面会有一个斜杠:dir/,但这个字符不是其名称的一部分。我们可以通过使用 ls -F 来获得这种输出,我们可以通过在我们的会话开始时声明 alias ls = ls -F来告诉我们的shell持续

  1. /home/you/
  2. --adirectory/
  3. --afile

建立一个新目录的命令是mkdir

练习 21.10mkdir newdir建立一个新目录,用ls查看当前目录

预期结果:我们应该看到这种结构:

  1. /home/you/
  2. newdir/...............................新目录
  • cd (‘change directory’)命令是进入另一个目录,也就是把它变成我们的工作目录。它可以在以下方面使用:
  • cd 没有任何参数, cd 将我们带到我们的主目录。
  • cd <绝对路径> 一个绝对路径从目录树的根部开始,也就是说,从 /. 开始 cd 命令带我们到那个位置。
  • cd <相对路径> 一个相对路径是指不从根开始的路径。这种形式的的 cd 命令将我们带到<我们当前的地址>/<相对路径>。

练习 21.11 使用 cd newdir ,用 pwd 查出我们在目录树中的位置。用ls 确认确认该目录是空的。我们将如何使用绝对路径到达这个位置?

预期结果: pwd 应该告诉我们 /home/you/newdir,然后 ls 没有输出。意味着没有任何东西可以列出。绝对路径是 /home/you/newdir 。

练习 21.12 让我们在这个目录下快速创建一个文件:touch onefile,和另一个目录:mkdir otherdir。做 ls 并确认有一个新的文件和目录。

预期结果:我们应该会得到如下:

  1. /home/you/
  2. newdir/..............................我们在这
  3. onefile
  4. otherdir/

ls 命令有一个非常有用的选项:通过 ls -a ,我们可以看到我们的普通文件和隐藏文件,隐藏文件的名称以点头。在我们的新目录中进行ls -a操作,我们会发现有以下文件:

  1. /home/you/
  2. newdir/..............................我们在这
  3. .
  4. ..
  5. onefile
  6. otherdir/

单点是当前目录,双点是后一级的目录。

练习 21.13 预测我们在 cd ./otherdir/..之后的位置,并检查我们的判断是否正确。

预期结果:单点将我们送到当前目录,所以这并没有改变任何东西。 otherdir 部分使该子目录成为我们的当前工作目录。最后,.. 回到了上一级,换句话说,这个命令让我们回到我们开始的地方。

由于我们的主目录是一个特殊的地方,所以有一些cd的快捷方式:cd不带参数、cd ˜cd $HOME都能让我们回到我们的home目录。

进入我们的主目录,在那里做 ls newdir 来检查我们创建的第一个目录的内容,而不需要去那里。

练习 21.14 ls ...是做什么的?

预期结果:回顾一下,.. 表示树上的一级目录:我们应该看到我们自己的主目录,以及任何其他用户的目录。

练习 21.15 我们能用ls来查看其他用户的主目录的内容吗?在前面的练习中我们看到我们的系统中是否存在其他用户。如果是的话,就用 ls .../thatotheruser

预期结果:如果这是我们的私人电脑,我们可能可以查看其他用户的目录内容。其他用户的目录内容。如果这是一台大学的计算机,那么另一个目录很可能是受保护的。保护权限问题将在下一节讨论—我们会得到:. . / otheruser: Permission denied 尝试用 cd 移动到别人的主目录,起作用吗?

我们可以用 cp 复制一个目录,但我们需要添加一个标志,以表明我们递归地复制了内容:cp -r。在我们的home目录中建立另一个目录 somedir ,这样我们就有了以下结构:

  1. /home/you/
  2. newdir/.............................you have been working in this one
  3. somedir/.....................................you just created this one

cp -r newdir somedircp -r newdir thirddir的区别是什么 (thirddir 是一个不存在的目录名)

权限

目的:在这一节中,我们将了解到如何给系统中的各种用户提供对我们的文件执行(或不执行)各种事务的权限。

Unix文件,包括目录,都有权限,表明 “谁可以对这个文件做什么”。可以对文件进行的操作 可以对一个文件进行的操作分为三类。

  • 读 r:对文件的任何访问(显示、获取文件的信息),但不改变文件。
  • 写 w:对文件的访问,改变其内容,或甚至其元数据,如 “修改日期”。
  • 执行 x:如果文件是可执行的,可以运行它;如果它是一个目录,可以进入它。有可能访问一个文件的人也被分为三类。
  • 用户 u:拥有该文件的人。
  • 组 g:所有者所属的用户组。
  • 其他 o:其他所有人。 这九种权限是按顺序呈现的
user group other
rwx rwx rwx

例如,rw-r--r--意味着所有者可以读写一个文件,所有者的组和其他人都只能读文件。

权限也是以三比特为一组的数字表示的,r=4,w=2,x=1

rwx.
421
常见的代码是7 = rwx 和 6 = rw 。我们会发现许多文件的权限是755,这代表每个人都可以运行的可执行文件,但只有所有者可以改变。或者644代表一个每个人都可以看到的数据文件,但同样只有所有者可以改变。我们可以用chmod命令来设置权限:
  1. chmod file # just one file
  2. chmod -R directory # directory, recursively

示例如下:

  1. chmod 766 file # set to rwxrw-rw-
  2. chmod g+w file # give group write permission
  3. chmod g=rx file # set group permissions
  4. chod o-w file # take away write permission from others
  5. chmod o= file # take away all permissions from others.
  6. chmod g+r,o-x file # give group read permission # remove other execute permission

man 页给出了所有的选项。

练习 21.16 建立一个文件 foo ,并执行 chmod u-r foo 。我们现在能检查它的内容吗?让这个文件再次可读,这次使用数字代码。现在让我们的同学也能读到这个文件,检查方法是让他们中的一个人查看文件内容。

预期结果:当我们把文件设置为不可读时,我们仍然可以 ls 文件,但是不能 cat 文件:cat 时会给出权限拒绝 的信息。

创建一个com文件,内容如下:

  1. #!/bin/sh
  2. echo "Hello world!"

这是一个合法的shell脚本。当我们输入 ./com 时会发生什么?我们能使这个脚本可执行吗?在这三个权限类别中,“我们”和“其他人”指的是谁很清楚。那么“组”呢?我们将在第20.10节中讨论这个问题。

注释 17 :还有更多不明显的权限。例如,setuid 位声明程序应该以创建者的权限运行,而不是执行它的用户的权限。这对系统实用程序,如 passwdmkdir,它们改变了密码文件和目录结构,因此需要 root 权限。多亏了 setuid 位,一个用户才可以运行这些程序,这些程序被设计为用户只能分别对自己的密码文件和自己的目录进行修改。编码 setuid 位是用 chmod 设置的: chmod 4ugo 文件。

我们已经看到 ls filename 给我们提供了关于那个文件的信息,而 ls 给我们提供了当前目录下的所有文件。为了查看名字上有某些条件的文件,存在通配符机制。通配符机制如下:

* 可代表任意数量的字符。
可代表任意单个字符。

例如:

  1. %% ls
  2. s sk ski skiing skill
  3. %% ls ski
  4. ski skiing skill

第二个选项列出了所有文件名以 ski 开头,后面是任意数量的其他字符的文件’。下面我们会看到,在不同的情况下,ski的意思是 “ sk后面有任意数量的i字符”。现象令人困惑,但事实就是这样的。

文本搜索和正则表达式

目的:在本节中,我们将学习如何在文件中搜索文本。

对于这一部分,我们至少需要一个包含一定数量文本的文件。例如,我们可以从以下网站获得随机的文本:http://www.lipsum.com/feed/html。

grep 命令可以用来搜索文件中的文本表达式。

练习 21.17 在我们的文本文件中用 grep q yourfile 搜索字母 q,并在我们目录的所有文件中用 grep q搜索它。尝试一些其他的搜索。

预期结果:在第一种情况下,我们得到一个包含q的所有行的列表;在第二种情况下, grep 还报告了在哪个文件名中发现了匹配的内容:qfile :这一行有 q 在里面。

注意事项:如果我们要找的字符串没有出现,grep 将根本不输出任何东西。请记住,如果没有什么可报告的,这是Unix命令的标准行为。除了搜索字面字符串外,我们还可以寻找更多的一般表达式。

除了搜索字面字符串外,我们还可以寻找更多的一般表达式。

字符 作用
ˆ 行首
$ 行末
. 任意字符
* 任意数量的字符
[xyz] 任意字符例如 xyz

这看起来像我们刚才看到的通配符机制(第20.1.4节),但它有细微的不同。与上面的例子相比:

  1. %%
  2. cat s
  3. sk
  4. ski
  5. skill
  6. skiing
  7. %% grep "ski*" s
  8. sk
  9. ski
  10. skill
  11. skiing

在第二种情况下,我们搜索一个由sk和任何数量的i字符组成的字符串,包括零个字符 的字符串。

还有一些例子:我们可以找到

  • 所有包含字母 “q” 的行,用 grep q yourfile
  • 所有以’a’开头的行,用 grep "ˆa" yourfile(如果我们的搜索字符串包含特殊字符,最好使用引号将其括起来)。
  • 所有以数字结尾的行,用 grep "[0-9]$" yourfile

练习 21.18 构建搜索字符串,用于查找

  • 以大写字母开头的行,以及
  • 恰好包含一个字符的行。

预期结果:对于第一种情况,使用范围字符 [ ] ,对于第二种情况,使用句号来匹配任何字符。

练习 21.19 添加几行 x = 1, x = 2, x = 3(也就是说,在x和等号之间有不同数量的空格)到我们的测试文件中。并使用 grep 命令来搜索所有对 x 的赋值。

上表中的字符有特殊含义。如果我们想搜索那个实际的字符,我们必须对它进行转义。

练习 21.20 做一个测试文件,其中有 abc 和 a.c,分别在不同的行中。尝试使用命令 grep"a.c" filegrep a\\.c filegrep "a\.c " file

预期结果:我们会看到,句号需要转义,搜索字符串需要加引号。如果没有这两种情况,我们会看到 grep 也能找到 abc 字符串。

用 sed 编辑流

Unix有各种工具用于逐行处理文本文件。流媒体编辑器 sed 就是一个例子。如果我们使用过 vi 编辑器,我们可能已经习惯了像s/foo/bar/ 这样的语法来进行修改。有了 sed ,我们可以在命令行上做这些事情。比如说:

  1. sed 's/foo/bar/' myfile > mynewfile

将把替代命令 s/foo/bar/ 应用到 myfile 的每一行。输出会显示在我们的所以我们应该在一个新文件中捕获它;关于输出重定向的更多信息,请参见第20.3.2节。

用 cut 的方式切起线条

另一个编辑行的工具是cut,它将切行并显示它的某些部分。例如:

  1. cut -c 2-5 myfile

将显示myfile中每一行的第2-5位的字符,制作一个测试文件并验证这个例子。

也许更有用的是,我们可以给cut一个分割符,让它在出现分割符的情况下分割一个行。例如,我们的系统很可能有一个/etc/passwd文件,其中包含用户的信息,以每一行都由冒号分隔的字段组成。例如:

  1. daemon::1:1:System Services:/var/root:/usr/bin/false
  2. nobody::-2:-2:Unprivileged User:/var/empty:/usr/bin/false
  3. root::0:0:System Administrator:/var/root:/bin/sh

第七个也是最后一个字段是用户的登录shell;/bin/false表示用户无法登录。

我们可以用以下方法显示用户和他们的登录shell。

  1. cut -d ":" -f 1,7 /etc/passwd

这告诉 cut使用冒号作为分隔符,并输出字段1和7。

其他有用的命令:tar

tar命令代表“磁带归档”,也就是说,它最初是用来将文件打包到磁带上的。(“存档”部分来自于ar命令。)现在,它被用来将文件打包在一起,以便在网站上发布:如果您想发布一个包含数百个文件的库,它将它们捆绑到一个文件中。

最常见的两个选项是for

  1. 创建tar文件:
  1. tar fc package.tar directory_with_stuff

声明一个tar file create

  1. 解压tar文件:
  1. tar fx package.tar
  2. # this creates the directory that was packaged

文本文件通常可以被压缩到很大程度,所以为gzip添加z压缩是一个好主意:

  1. tar fcz package.tar.gz directory_with_stuff
  2. tar fx package.tar.gz

命名为“gzip”文件包。tgz也很常见。

命令的执行

搜索路径

目的:在这一节中,我们将学习Unix如何决定当我们输入一个命令名时,Unix是如何决定做什么的。如果我们输入了ls这样的命令,shell并不只是依靠一个命令列表:它实际上会去搜索一个名为ls的程序。这意味着我们可以有多个同名的不同命令。这意味着我们可以有多个同名的不同命令,哪一个被执行取决于哪一个先被找到。

练习 21.21 我们可能认为的 “Unix命令”往往只是系统目录中的可执行文件,输入 which ls, 然后输入ls -l 查看结果。

预期结果:ls 的位置是类似于 /bin/ls 的东西。如果我们输入了 ls 命令,我们会看到它可能是由 root 拥有的。它的可执行位可能是为所有用户设置的。unix搜索命令的位置是搜索路径,它被存储在环境的变量(更多细节见下 PATH。

练习 22.22 输入 echo $PATH。我们能找到 cd 的位置吗?是否有其他命令在相同的位置吗?当前目录 ‘.’ 在路径中吗?如果没有,做 export PATH=".:$PATH"。现在创建一个可执行文件 cd(见上面的基础知识),然后执行 cd 。

预期结果:路径将是一个用冒号分隔的目录列表。

例如,/usr/bin:/usr/local/bin:/usr/X11R6/bin。如果工作目录在路径中,它可能会在最后。/usr/X11R6/bin:. 但通常情况下工作目录不在路径中。如果我们在路径的开头加上’.’,unix会在系统命令之前找到本地的 cd 命令。

有些人认为将工作目录放在路径中是一种安全风险。如果我们的目录是可写的,有人可以在我们的目录中放置一个名为cd 的恶意脚本(或任何其他系统命令),而我们就会在不知不觉中执行它。

可以将我们自己的命令定义为现有命令的别名。

练习 21.23 使用 alias chdir=cd,确保现在 chdir 的工作方式和 cd 一样。使用 alias rm='rm -i' ; 在手册中查找它的含义。有些人认为这个别名是个好主意;我们能明白为什么吗?

预期结果: rm-i“交互式 “选项使命令每一次删除之前都要求确认。由于 unix 没有一个需要明确清空的垃圾桶(如Windows或Mac OS上的),这可能是一个好主意。

命令序列

重定向

目的:在本节中,我们将学习如何将一条命令输入另一条命令,以及如何将命令与输入和输出文件连接起来。

到目前为止,我们所使用的unix命令都是从我们的键盘上,或从一个名为 的文件;它们的输出到了我们的屏幕上。还有其他的可能性,即从文件中提供输入或将输出存储在一个文件中的其他可能性。

命令排序

有多种方法可以在一个命令行上有多个命令。

简单排序

首先,我们可以输入:

  1. command1 ; command2

如果我们多次重复相同的两个命令,这很方便:我们只需要向上述的方式一次就可以重复这两个命令。但是有一个问题,如果我们输入:

  1. cc -o myprog myprog.c ; ./myprog

然后编译失败,程序仍然会被执行,如果存在旧版本的可执行文件,则使用该版本的存在。这是非常令人困惑的。

一个更好的方法是:

  1. cc -o myprog myprog.c && ./myprog

这条命令只有在第一条命令成功的情况下才会执行第二条命令。

管道

不从文件中获取输入,也不向文件发送输出,而是将两个命令连接在一起,使第二个命令将第一个命令的输出作输入。这方面的语法是 cmdone | cmdtwo。这就是所谓的管道。例如,grep a yourfile | grep b可以找到所有同时包含a和b的行。

练习 21.24 构造一个管道,计算文件中包含字符串th的行数。使用wc命令(参见上面)进行计数。

反引用

还有一些组合命令的方法。假设我们想把wc的结果以一种很好地呈现。输入以下命令:

  1. echo The line count is wc -l foo

其中 foo 是一个现有文件的名称。获得实际行数输出的方法是通过反引用:

  1. echo The line count is wc -l foo

在反引用之间的任何内容都会在命令行的其他部分被评估之前被执行。

练习 21.25 这里使用wc的方式是,它打印文件名。你能找到防止这种情况发生的方法吗?

还有另一种无序评估机制:

  1. echo "There are $( cat Makefile | wc -l ) lines"

子shell中分组

假如我们想把输出重定向应用于一连串的几个命令中:

  1. configure ; make ; make install > installation.log 2>&1

这只抓住了最后一条命令。例如,我们可以把这三条命令放在一个子shell里,然后捕获它的输出。

  1. ( configure ; make ; make install ) > installation.log 2>&1

这种机制使嵌套命令成为可能,但出于兼容性和遗留目的,当不需要嵌套时,倒引号可能仍然更可取。

退出状态

命令可能会失败。如果我们在命令行上键入一条命令,我们会看到错误,并在键入下一条命令时采取相应的行动,当我们输入下一条命令时,我们就会采取相应的行动。当这个失败的命令发生在一个脚本中时,我们必须告诉脚本如何采取相应的行动。为此,我们可以使用命令的退出状态:这是一个值(成功时为零,否则为非零)存储在一个内部变量中,我们可以用$?访问它。

示例:假设我们有一个不可写的目录如下

  1. [testing] ls -ld nowrite/
  2. dr-xr-xr-x 2 eijkhout 506 68 May 19 12:32 nowrite//
  3. [testing] cd nowrite/

然后写

  1. [nowrite] cat ../newfile
  2. #!/bin/bash
  3. touch $1 echo "Created file: $1"
  4. [nowrite] newfile myfile
  5. bash: newfile: command not found
  6. [nowrite] ../newfile myfile
  7. touch: myfile: Permission denied
  8. Created file: myfile
  9. [nowrite] ls
  10. [nowrite]

脚本将报告说文件被创建了,尽管它并没有被创建。

改进的脚本:

  1. [nowrite] cat ../betterfile
  2. #!/bin/bash touch $1
  3. if [ $? -eq 0 ] ; then
  4. echo "Created file: $1"
  5. else
  6. echo "Problem creating file: $1"
  7. fi
  8. [nowrite] ../betterfile myfile
  9. touch: myfile: Permission denied
  10. Problem creating file: myfile

进程和工作

命令 简介
ps 列出(所有)进程
kill 杀死一个进程
CTRL -c 杀死前台的工作
CTRL -z 挂起前台的工作
jobs 给出所有工作的状态
fg 将最后一个暂停的工作带到前台
fg %3 将一个特定的工作带到前台
bg 在后台运行最后一个暂停的工作

Unix操作系统可以同时运行许多程序,方法是在列表中轮流运行,每次只给每个程序几分之一的时间。每次只给每个程序几分之一秒的时间来运行。命令ps可以告诉我们当前正在运行的所有程序。

练习 21.26 输入 ps 。目前有多少个程序在运行?默认情况下,ps只给出我们明确启动的程序我们明确启动的程序。使用ps guwax 可以获得所有正在运行的程序的详细列表。有多少个程序正在运行?有多少属于根用户,有多少属于我们?

预期结果:要计算属于某个用户的程序,可以用管道将 ps 命令通过一个适当的grep ,然后将其输送到 wc

在这个长长的 ps 列表中,第二列包含了进程号。有时,拥有这些进程号是很有用的:如果一个程序行为不正常,我们可以使用以下命令杀死:

  1. kill 123456

其中,12345 是进程号。

上面解释过的 cut 命令可以从一行中剪切某些位置:输入 ps guwax | cut -c 10-14 要获得所有运行进程的动态信息,可以使用top命令,通过阅读手册来了解如何按CPU使用率对输出进行排序。

在shell中启动的进程被称为 jobsjob(unix)。除了进程号之外,它们还有也有一个作业号。现在我们将探讨如何操作作业。

当我们输入一条命令并按下返回键时,该命令在其运行期间成为前台的进程。其他同时运行的东西都是后台进程。

制作一个可执行文件hello,内容如下:

  1. \#!/bin/sh
  2. while [ 1 ] ; do
  3. sleep 2
  4. date
  5. done

然后输入 ./hello

练习 21.27 输入 Control-z 。这个命令会将前台进程暂停,它将给我们一个数字,如 [1] 或 [2] ,表明它是第一个或第二个被暂停或置于后台的程序。现在输入 bg ,把进程放到后台。确认没有前台进程,点击返回键,然后输入 ls 命令。

预期结果:在我们把一个进程放到后台后,终端又可以接受前台命令。如果我们点击回车键,我们应该看到命令提示符。然而。后台进程仍然不断地产生输出。

练习 21.28 输入 jobs 来查看当前会话中的进程。如果我们刚才放在后台的进程是1号,输入fg %1,确认它又是一个前台进程。

预期结果:如果 shell 在前台执行一个程序,它将不接受命令输入,所以点击返回键应该只产生空行。

练习 21.29 当我们使 hello 脚本再次成为前台进程时,我们可以用Control-c 杀死它。尝试这样:再次启动该脚本,这次是以 ./hello & 的形式,这样就可以立即把它放到后台。我们也应该得到类似于 [1] 12345 的输出,它告诉我们这是我们放在后台的第一个工作,而 12345 是它的进程ID。用 kill %1杀死这个脚本。再次启动它,通过它的进程号。

预期结果:使用进程号的 kill 12345 命令通常足以来杀死一个正在运行的程序,但有时需要使用 kill -9 12345

Shell 自定义

上面提到 ls -F 是查看哪些文件是常规文件、可执行文件或目录的一个简单方法。通过输入别名ls='ls -F',ls 命令每次被调用时都会自动扩展为 ls -F。如果我们想在每个登录会话中都有这种行为,我们可以将别名命令添加到我们的.profile 文件中。除了sh/bash 之外,其他 shells 也有其他的文件可以进行这样的自定义。

输入/输出重定向

目的:在本节中,我们将学习如何将一个命令提供给另一个命令,以及如何将命令连接到输入和输出文件。 到目前为止,我们使用的unix命令都是从我们的键盘或命令行上命名的文件中获取输入;他们的输出到你的屏幕上。从文件中提供输入或将输出存储在文件中还有其他可能性。

输入重定向

grep命令有两个参数,第二个参数是一个文件名。我们也可以写 grep string < yourfile,其中的小于号意味着输入将来自命名的文件,即 yourfile,这就是所谓的输入重定向。

标准文件

Unix有三个标准文件来处理输入和输出:

标准文件 目的
stdin 为进程提供输入的文件。
stdout 是写入进程输出的文件。
stderr 是写入错误输出的文件。

在交互式会话中,所有三个文件都连接到用户终端。使用输入或输出重定向意味着将输入或输出发送到与终端不同的文件。

输出重定向

反之,grep string yourfile > outfile 会把通常进入终端的内容重定向输出到 outfile 。如果输出文件不存在,就会被创建,否则就会被覆盖掉。(如果要追加,使用 grep text yourfile >> outfile )。

练习 21.30 从上一节中选取一个 grep 命令,并将其输出发送到一个文件。检查该文件的内容是否与之前出现在屏幕上的内容相同。搜索一个文件中没有出现的字符串,并将其输出发送到一个文件中。这对输出文件意味着什么?

预期结果:搜索一个不在文件中出现的字符串,没有终端输出。如果我们把这个 grep 的输出重定向到一个文件,会给出一个零大小的文件。使用 lswc 命令进行检查。

有时我们想运行程序,但忽略输出。为此,您可以将输出重定向到系统空设备:/dev/null

  1. yourprogram >/dev/null

下面是一些有用的习语:

习语 含义
program 2>/dev/null 只发送错误到空设备
program >/dev/null 2>&1 将输出发送到dev-null,并输出错误。注意违反直觉的规范顺序!
` program 2>&1 less` 发送输出和错误到更少

Shell 环境变量

在上面我们遇到了PATH,它是一个shell或环境变量的示例。这些变量是shell所知道的,shell所运行的所有程序都可以使用它们。虽然PATH是一个内置变量,但您也可以定义自己的变量,并在shell脚本中使用它们。

Shell变量大致分为以下几类:

  • 特定于shell的变量,如HOME或PATH。
  • 特定于某些程序的变量,例如TEX/LATEX的TEXINPUTS。
  • 自己定义的变量;见下。
  • 控制结构定义的变量,例如for;见下文。

我们可以通过键入env查看shell已知的所有变量的完整列表。

注释 25:这并不包括你自己定义的变量,除非你导出它们;见下文。

练习 21.31 通过输入 echo $HOME,检查 HOME 变量的值。也可以通过 grep 管道输入 env 来查找 HOME 的值。

shell变量的使用

我们可以通过在shell变量前面加上一个美元符号来获得它的值。输入以下内容并检查输出:

  1. echo x
  2. echo $x
  3. x=5
  4. echo x
  5. echo $x

可以看到,shell将所有内容都视为一个字符串,除非我们通过在名称前面加上$来显式地告诉它取一个变量的值。以前没有定义的变量将打印为空白字符串。Shell变量可以通过多种方式设置。与其他编程语言一样,最简单的方法是赋值。当进行下一个练习时,最好记住shell是一种基于文本的语言。

练习 21.32 在命令行上输入 a=5 。这就定义了一个变量 a ;通过使用echo命令检查其值。

预期结果:shell 将通过输入的数值 5 来响应。

注意事项:注意等号周围不要有空格;也要注意用美元符号来打印该值。

导出变量

这样设置的变量会被我们在这个 shell 中发出的所有后续命令所知,但不会被我们启动的新 shell 中的的命令所知。为此,我们需要使用导出命令。重现以下内容会话(方括号内为命令提示):

  1. [] a=20
  2. [] echo $a
  3. 20
  4. [] /bin/bash
  5. [] echo $a
  6. [] exit
  7. exit
  8. [] export a=21
  9. [] /bin/bash
  10. [] echo $a
  11. 21
  12. [] exit

我们也可以临时设置一个变量。重现这个场景:

  1. 找到一个没有数值的环境变量:

    1. [] echo
    2. $b []
  1. 写一个简短的shell脚本来打印这个变量:

    1. [] cat > echob
    2. #!/bin/bash
    3. echo $b

    当然,还要使其可执行:chmod +x echob 。

  1. 现在调用该脚本,在其前面设置变量b:

    1. [] b=5 ./echob
    2. 5

    我们设置值的语法,作为一个前缀而不使用单独的命令,设置值只为这一条命令。

  2. 证明该变量仍然是未定义的:

    1. [] echo $b
    2. []

    也就是说,我们定义了这个变量只是为了执行一个单一的命令。

控制结构

像任何好的编程系统一样,shell 有一些控制结构。它们的语法需要一点时间来适应习惯(不同的 shell 有不同的语法;在本教程中我们只讨论 bash shell)。

条件语句

bash shell 的条件语句可以被称为 if,它可以写成几行上:

  1. if [ $PATH = "" ] ; then
  2. echo "Error: path is empty"
  3. fi

或在单行上:

  1. if [ wc -l file -gt 100 ] ; then echo "file too long" ; fi

有一些测试被定义,例如 -f somefile 测试一个文件是否存在。更改我们的脚本,使它在文件不存在时报告 -1。

这方面的语法是很微妙的:

  • if 和 elif 后面是一个条件,后面是一个分号
  • 条件的括号周围需要有空格
  • else 的 then 后面没有分号

练习 21.33 Bash 条件语句有一个 elif 关键字。但我们仍然可以写出以下序列 else if,如:

  1. if [ something ] ; then
  2. foo
  3. else if [ something_else ] ; then
  4. bar
  5. fi

我们能预测这里的错误是什么吗?

循环

一个for循环的形式如下:

  1. for var in listofitems ; do
  2. something with $var
  3. done

这将以以下方式工作:

  • 对于 listofitems 中的每个项目,变量 var 被设置为该项目
  • 并且循环主体被执行。

举一个简单的例子:

  1. [] for x in a b c ; do echo $x ; done
  2. a
  3. b
  4. c

有一个更有意义的例子,以下是我们如何对所有的 .c 文件进行备份:

  1. or cfile in .c ; do
  2. cp $cfile $cfile.bak
  3. done

Shell变量可以以多种方式进行操作。执行下面的命令,我们就会发现可以删除变量的尾部字符:

  1. [] a=b.c
  2. [] echo ${a%.c}
  3. b

以此为提示,写一个循环,将所有的 .c 文件重命名为 .x 文件。

上面的结构很容易在单词上进行循环,比如 ls 的输出。要进行数字循环,可以使用命令 seq

  1. [shell:474] seq 1 5
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5

循环处理一个数字序列,通常如下:

  1. for i in seq 1 ${HOWMANY}‘ ; do echo $i ; done

注意回车键,这是为了在评估循环之前执行 seq 命令所必需的。

脚本

unix shells 也是编程环境,我们将在本节中进一步了解 unix 的这一方面。

如何执行脚本

可以编写 unix shell 命令的程序:首先,我们需要知道如何把一个程序放在一个文件中,并让它被执行。做一个包含以下两行的文件 script1:

  1. #!/bin/bash
  2. echo "hello world"

并在命令行上输入 ./script1 。结果如何?使该文件可执行,然后再试一次。

为了编写我们想从任何地方调用的脚本,人们通常会把它们放在主目录下的 bin 目录中。然后我们会把这个目录添加到我们的搜索路径中,包含在 PATH 中;见第 20.3.1 节。

脚本参数

我们可以用选项和参数来调用一个shell脚本:

  1. ./my_script -a file1 -t -x file2 file3

现在我们将学习如何在我们的脚本中纳入这一功能。

首先,所有的命令行参数和选项都可以在脚本中作为变量 $1 , $2 等等,而命令行参数的数量可以用 $# 来表示:

  1. #!/bin/bash
  2. echo "The first argument is $1"
  3. echo "There were $# arguments in all"

官方定义:

变量 含义
$# 参数个数
$0 脚本名称
$1,$2,… 参数
$ ,$@ 所有参数的列表

练习 21.34 编写一个脚本,将一个文件名作为输入参数,并报告该文件中有多少个行。

编辑我们的脚本以测试文件是否少于10行(使用 foo -lt bar 测试),如果是的话,就使用 cat 查看该文件。提示:我们需要在测试中使用反引号。在我们的脚本中添加一个测试,如果我们在没有任何参数的情况下调用它,它将给出一个有用的信息。

解析参数的标准方法是使用 shift 命令,它将从参数列表中弹出第一个参数。然后,依次解析参数包括查看 $1 、移动和查看新的 $1

代码:

  1. // arguments.sh
  2. while [ $# -gt 0 ] ; do
  3. echo "argument: $1"
  4. shift
  5. done

练习 21.35 写一个 say.sh 脚本,输出它的文本参数。然而,如果我们调用它:

  1. ./say.sh -n 7 "Hello world"

它应该是按照我们指定的次数来输出的。使用选项-u:

  1. ./say.sh -u -n 7 "Goobye cruel world"

应该以大写字母输出该信息。确保参数的顺序并不重要,并对任何不被识别的选项给出一个错误信息。变量 $@$ 在双引号方面有不同的行为。比方说,我们评估 myscript “1 2” 3, 然后

  • 使用 $* 是去掉引号后的参数列表:myscript 1 2 3。
  • 使用 “$*” 是去除引号后的参数列表:myscript “1 2 3”。
  • 使用 “$@” 保留了引号:myscript “1 2” 3。

扩展

shell 对命令行进行各种扩展,即用不同的文本替换命令行的一部分。

花括号扩展:

  1. [] echo a{b,cc,ddd}e
  2. abe acce addde

例如,这可以用来删除一些基本文件名的所有扩展名:

  1. [] rm tmp.{c,s,o} # delete tmp.c tmp.s tmp.o

波浪号扩展给出了我们自己的,或别人的主目录:

  1. [] echo ˜
  2. /share/home/00434/eijkhout
  3. [] echo ˜eijkhout
  4. /share/home/00434/eijkhout

参数扩展给出了 shell 变量的值:

  1. [] x=5
  2. [] echo $x
  3. 5

未定义的变量不会出现错误信息:

  1. [] echo $y

在参数扩展方面有许多变化。上面我们已经看到,我们可以剥离尾部的字符:

  1. [] a=b.c
  2. [] echo ${a%.c}
  3. b

以下是处理未定义变量的方法:

  1. [] echo ${y:-0}
  2. 0

反引号机制被称为命令替换。它允许我们对一条命令的一部分进行评估,并将其作为另一条命令的输入。例如,如果我们想查看文件是什么类型,请执行 ls 命令:

  1. [] file which ls

这首先评估了 which ls,给出了 /bin/ls,然后评估file /bin/ls。另一个例子,这里我们反引用整个管道,并对结果做一个测试:

  1. [] echo 123 > w
  2. [] cat w
  3. 123
  4. [] wc -c w
  5. 4 w
  6. [] if [ cat w | wc -c -eq 4 ] ; then echo four ; fi
  7. four

Unix shell 编程在很大程度上是面向文本操作的,但也有可能进行算术。算术替换告诉 shell 将一个参数扩展作为一个数字来处理:

  1. [] x=1
  2. [] echo $((x2))
  3. 2

整数范围可按以下方式使用:

  1. [] for i in {1..10} ; do echo $i ; done
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. 8
  10. 9
  11. 10

启动文件

在本教程中,我们已经看到了几种自定义 shell 行为的机制。比如说,通过设置 PATH变量,我们可以扩展 shell 寻找可执行文件的位置。其他环境变量我们可以为自己的目的引入。这些定制的变量中的许多需要适用于每个会话,所以我们可以在任何会话开始时读取 shell 启动文件。

在启动文件中最普遍的事是定义 alias

  1. alias grep=’grep -i
  2. alias ls=’ls -F

并设置一个自定义的命令行提示。

不巧的是,有几个启动文件的读取是一个复杂的情况功能。这里有一个很好的常识性指南:

  • 有一个 .profile 文件,除了读取.bashrc文件之外什么都不做:

    1. # ˜/.profile
    2. if [ -f ˜/.bashrc ]; then
    3. source ˜/.bashrc
    4. fi
  • 我们的.bashrc文件会进行实际的自定义:

  1. \# ˜/.bashrc
  2. \# make sure your path is updated
  3. if [ -z "$MYPATH" ]; then
  4. export MYPATH=1
  5. export PATH=$HOME/bin:$PATH
  6. fi

交互式Shell

Unix的交互式使用,与编写脚本不同,是用户和shell之间复杂的对话。我们作为用户输入一行按下回车,然后 shell 试着解释。有以下几种情况:

  • 我们输入行只包含一个完整的命令,例如 ls foo :shell 将执行这个命令。
  • 我们可以在一行中输入一个以上的命令,用分号隔开:mkdir foo ; cd foo,shell 将按顺序执行这些命令。
  • 我们的输入行不是一个完整的命令,例如 while [1]。 shell 会识别出识别出还有更多的命令,并使用一个不同的提示来显示它正在等待命令的剩余部分。
  • 我们的输入行是一个合法的命令,但我们想在第二行输入更多的内容。在这种情况下在这种情况下,我们可以用一个反斜杠字符来结束我们的输入行, shell 将知道它需要暂缓执行我们的命令。但实际上,反斜杠会隐藏(逃避)返回。

当shell通过使用我们的一个或多个输入行或只使用一个输入行的一部分来收集要执行的命令行时,如刚才所述,它将对命令行进行扩展。然后,它将把命令行解释为一个命令和参数,并继续用找到的参数来调用该命令。

有一些微妙的地方。如果我们输入 ls .c,那么 shell 会重新识别通配符并将其扩展为一个命令行,例如 ls foo.c bar.c 。注意,ls 不接受 .c 作为参数!在我们想让 unix 命令接收一个带有通配符的参数情况下,我们需要转义它,这样 shell 就不会将它扩展。例如,find . -name \.c 将使 shell 调用带有参数 .-name .c 的 find 。

系统和其他用户

Unix是一个多用户操作系统。因此,即使我们在自己的个人设备上使用它,我们也是一个有账户的用户,偶尔需要输入我们的用户名和密码。

如果我们是在我们的个人设备上,我们可能是唯一登录的用户。在大学机器或其他服务器上,经常会有其他用户,这里有一些与他们有关的命令。

命令 作用
whoami 显示我们的登录名
who 显示当前登录的其他用户
finger otheruser 获取另一个用户的信息;我们可以在这里指定一个用户的登录名,或他们的真实姓名,或系统知道的其他识别信息。他们的真实姓名,或系统知道的其他识别信息。
top 哪些进程正在系统上运行;使用 top -u 来获得这个排序的 cpu 数量时间。(在 Linux 上,也可以尝试使用 vmstat 命令)。
uptime 距离上一次重启的时间

当我们的账户被创建时,我们的系统管理员会把我们分配到一个或多个组。(如果我们管理自己的机器,我们会在一些默认的组中;请继续阅读,以便将自己添加到更多的组种)。

groups 命令告诉我们所在的组,ls -l 告诉我们每个文件属于哪个组。与 chmod 类似,我们可以使用 chgrp 来改变一个文件所属的组,以便与该组中的用户共享。

创建一个新的组,或将一个用户添加到一个组,需要系统权限。创建一个组命令如下:

  1. sudo groupadd new_group_name

添加一个用户到一个组命令如下:

  1. sudo usermod -a -G thegroup theuser

超级用户

即使我们拥有我们的设备,也尽可能地使用普通用户的帐户工作,只有在严格需要时才使用 root 权限( root 账户也被称为超级用户)。如果我们有 root 权限,我们也可以用它来 “成为另一个用户”,用他们的的特权,使用 sudo(’superuser do‘)命令。

  • 以另一个用户的身份执行一个命令:

    1. sudo -u otheruser command arguments
  • 以根用户身份执行命令:

    1. sudo command arguments
  • 成为另一个用户:

    1. sudo su - otheruser
  • 成为超级用户:

    1. sudo su -

其他系统:SSH 和 SCP

没有人是一座孤岛,没有电脑也是如此。有时我们想用一台计算机,例如我们的笔记本电脑,连接到另一台电脑,例如一台超级计算机。

如果我们已经在一台 Unix 计算机上,我们可以用 ’安全 shell‘的命令 ssh 登录到另一台计算机。它是旧的 “远程 shell “命令 rsh 的一个更安全的变种:

  1. ssh yourname@othermachine.otheruniversity.edu

其中 yourname 可以省略,如果我们在两台机器上有相同的名字的话。

如果只想从一台设备复制一个文件到另一台设备,我们可以使用 “安全复制” scp,这是 “远程复制” rcp 的一个安全变体。scp 命令的语法与 cp 很相似,只是源文件或目标文件有一个设备前缀。

要将一个文件从当前设备复制到另一台设备,请输入:

  1. scp localfile yourname@othercomputer:otherdirectory

其中 yourname 也可以省略,otherdirectory 可以是一个绝对路径,也可以是一个相对于我们的主目录的路径的相对路径。

  1. # absolute path:
  2. scp localfile yourname@othercomputer:/share/
  3. # path relative to your home directory:
  4. scp localfile yourname@othercomputer:mysubdirectory

把目标路径留空,会把文件放在远程主目录下:

  1. scp localfile yourname@othercomputer:

注意这个命令结尾的冒号:如果我们把它漏掉,就会得到一个名称中带有 ‘at’的本地文件。我们也可以从远程设备上复制一个文件,例如,要复制一个文件,保留名称:

  1. scp yourname@othercomputer:otherdirectory/otherfile .

sed 和 awk 工具

除了相当小的实用工具,如 tr 和 cut ,Unix 还有一些更强大的工具,在本节中我们将看到两个对文本文件进行逐行转换的工具。

sed

流媒体编辑器sed就像一个远程控制的编辑器,用一个命令行接口做简单的行编辑。大多数时候,我们会按以下方式使用sed:

  1. cat somefile | sed s/abc/def/:g > newfile

(这里使用 cat 并不是严格意义上的需要。) s/abc/def/ 部分的效果是在每一行中用 def 替换 abc; :g 修改器将其应用于每一行中的每一个实例,而不仅仅是第一行。

  • 如果我们有一个以上的编辑,我们可以用:

    1. sed -e s/one/two/’ -e s/three/four/’
  • 如果一个编辑只需要在某些行上进行,我们可以通过在编辑的前缀加上匹配字符串。比如说:

    1. sed ’/ˆa/s/b/c/’

    只对以a开头的行进行编辑(见第20.2节正则表达式)。

  • 传统上,sed 只能在一个流中起作用,所以输出文件总是必须与输入不同。GNU版本,即Linux系统的标准版本,有一个标志-i,可以就地进行编辑:

    ​ sed -e ’s/ab/cd/’ -e ’s/ef/gh/’ -i thefile

awk

awk 工具也是对每一行进行操作,但它可以说是有一个内存。一个 awk 程序由一连串的对子组成,每个对子由一个匹配字符串和一个行为组成。最简单的 awk 程序是:

  1. cat somefile | awk ’{ print }’

其中匹配字符串被省略,意味着所有的行都是匹配的,并且动作是打印该行。awk 将每一行分成由空白分隔的字段,awk 的一个常见应用是打印某个字段:

  1. awk ’{print $2}’ file

输出每一行的第二个字段。

假设我们想打印一个 Fortran 程序中的所有子程序,可以通过以下方法完成:

  1. awk ’/subroutine/ {print}’ yourfile.f

练习 21.36 建立一个命令管道,在每个子程序的标题下只输出子程序的名称。为此,我们首先用 sed 把括号换成空格,然后用 awk 来打印子程序名称域。

awk 有一些变量,它可以记住一些东西。例如,我们可以将每一行的第二个字段打印出来,而不是只打印的第二个字段,我们可以把它们做成一个列表,然后再输出:

  1. cat myfile | awk BEGIN {v="Fields:"} {v=v " " $2} END {print v}’

作为使用变量的另一个例子,下面是如何如何 BEGIN 和 END 行之间的所有行:

  1. cat myfile | awk ’/END/ {p=0} p==1 {print} /BEGIN/ {p=1}

练习 21.37 与 BEGIN 和 END 匹配的位置可能看起来很奇怪。重新安排 awk 程序,进行测试,并解释我们得到的结果。

复习题

练习 21.38 假设我们是一个教授,为提交作业编写一个脚本:如果一个学生调用这个脚本,它就会把学生的文件复制到某个标准位置。

  1. submit_homework myfile.txt

为了简单起见,我们通过建立一个提交目录和两个不同的文件 student1.txt 和 student2.txt 进行模拟。然后

  1. submit_homework student1.txt
  2. submit_homework student2.txt

在提交的目录中应该有这两个文件的副本。开始写一个简单的脚本;如果我们用错了方法,它应该给出一个有用的信息。

尝试检测一个学生是否作弊。探索 diff 命令,看看提交的文件是否与已提交的文件相同:循环查看所有已提交的文件然后

  1. 首先输出所有的差异;
  2. 计算差异个数;
  3. 测试该计数是否为零。

现在通过捕捉作弊学生是否随机插入了一些空格来完善我们的测试。对于一个更难的测试:尝试检测作弊的学生是否插入了换行。这不可能不能用 diff 来做,但我们可以尝试用 tr 来删除换行线。