持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情
虽然命令行现在已经 50 岁了,但我们仍然不知道该怎么称呼它。 命令行、shell、终端、bash、prompt 还是控制台? 我们将其称为命令行以保持一致。
为了清晰起见,本文将重点讨论 UNIX 风格(Linux 和 Mac)的命令行,而忽略其他命令行(如: Windows 的命令处理器和 PowerShell)。我们观察到,目前大多数数据科学家都在使用基于 UNIX 的系统。
命令行是什么?
命令行是计算机的基于文本的界面。 您可以将其视为操作系统的“揭开面纱”。 有些人误以为它只是过去的遗物,但不要上当。 现代命令行以前所未有的方式让人震惊!
过去,基于文本的输入和输出就是你所得到的。 就像第一批汽车一样,第一批操作系统甚至没有引擎盖可以弹出。 一切都在眼前。 在这种环境下,所谓的 REPL(读取-评估-打印循环)方法是与计算机交互的自然方式。
REPL 意味着您输入一个命令,然后按回车键,该命令会立即被评估。 它不同于通常用于更复杂程序的 edit-run-debug 或 edit-compile-run-debug 循环。
命令行一般遵循“让每个程序做好一件事”的 UNIX 哲学,因此,基本命令非常简单。 基本前提是你可以通过组合这些简单的程序来做复杂的事情。
为什么要使用命令行?
世界上几乎所有的编程语言都比命令行更强大,而且大多数点击式 GUI 更易于学习。你为什么还要费心在命令行上做任何事情?
第一个原因是速度。一切都在您的指尖。要告诉计算机执行简单的任务,例如:下载文件、重命名具有特定前缀的一堆文件夹或对 CSV 文件执行 SQL 查询,你真的无法击败命令行的敏捷性。学习曲线就在那里,但是一旦你内化了一组基本命令,它就像魔术一样。
第二个原因是没有限制。无论您当前使用什么堆栈、平台或技术,您都可以从命令行与之交互。它就像万物之间的粘合剂。它也无处不在。哪里有电脑,哪里就有命令行。
第三个原因是自动化。与 GUI 界面不同,在命令行中完成的一切最终都可以自动化。指令与计算机之间的歧义为零。在基于 GUI 的工具中,您浪费生命的所有那些重复点击都可以在命令行环境中自动化。
第四个原因是可扩展性。与 GUI 不同,命令行是非常模块化的。简单的命令是为无数场景创建复杂功能的完美构建块,并且生态系统在 50 年后仍在增长。命令行就在这里。
第五个原因是没有其他选择。通常,第三方服务的一些更晦涩或前沿的功能可能根本无法通过 GUI 访问,只能使用 CLI(命令行界面)使用。
命令行是如何工作的?
命令行的工作方式大致分为四层:
- Terminal = 获取键盘输入的应用程序将其传递给正在运行的程序(例如:shell)并返回结果。由于当今所有现代计算机都具有图形用户界面 (GUI),因此,终端是您与其他基于文本的堆栈之间必不可少的 GUI 前端层。
- Shell = 解析终端应用程序传递的按键并处理正在运行的命令和程序的程序。它的工作基本上是找到程序的位置,处理变量之类的事情,并使用 TAB 键提供精美的补全。有不同的选项,例如:Bash、Dash、Zsh 和 Fish,仅举几例。所有的内置命令和选项集略有不同。
- Command = 与操作系统交互的计算机程序。常见的示例是:
ls、mkdir和rm等命令。有些是预先内置到 shell 中的,有些是在磁盘上编译的二进制程序,有些是文本脚本,还有一些是指向另一个命令的别名,但归根结底,它们都只是计算机程序。 - 操作系统 = 执行所有其他程序的程序。它处理与 CPU、硬盘和网络等所有硬件的直接交互。
提示符和波浪号
每个人的命令行看起来都略有不同。
但是,通常有一个共同点:提示符,可能由美元符号 ($) 表示。 它是状态结束位置以及您可以开始输入命令的位置的视觉提示。
在我的电脑上,命令行显示:
juha@ubuntu:~/hello$
juha 是我的用户名,ubuntu 是我的计算机名,~/hello 是我当前的工作目录。
那个波浪号 (~) 字符是怎么回事? 当前目录是 ~/hello 是什么意思?
波浪号是主目录的简写,是您所有个人文件的地方。 我的主目录是 /home/juha,所以我当前的工作目录是 /home/juha/hello,它是 ~/hello 的简写。 (约定 ~username 通常指某人的主目录;~juha 指我的主目录,依此类推。)
从现在开始,我们将在提示符中省略除美元符号之外的所有其他内容,以使我们的示例更简洁。
命令的剖析
早些时候,我们将命令简单地描述为与操作系统交互的计算机程序。 虽然正确,但让我们更具体一些。
当您在提示符后键入内容并按回车键时,shell 程序将尝试解析并执行它。 比方说:
$ generate million dollars
generate: command not found
shell 程序获取第一个完整的单词 generate 并认为它是一个命令。
剩下的两个词,million和dollars,被解释为两个单独的参数(有时称为arguments)。
现在,负责促进执行的 shell 程序开始寻找生成命令。 有时它是磁盘上的文件,有时是其他文件。 我们将在下一章详细讨论这一点。
在我们的示例中,没有找到名为 generate 的命令,我们最终得到一条错误消息(这是预期的)。
让我们运行一个实际有效的命令:
$ df --human-readable
Filesystem Size Used Avail Use% Mounted on
sysfs 0 0 0 - /sys
proc 0 0 0 - /proc
udev 16G 0 16G 0% /dev
...
在这里,我们使用"--human-readable"选项运行命令"df"(disk free 的缩写)。
通常在缩写选项前使用"-"(破折号),在长格式前使用"--"(双破折号)。 (这些约定随着时间的推移而演变;有关更多信息,请参阅此博客文章。)
例如,这些是完成同一件事:
$ df -h
$ df --human-readable
您通常还可以在单个破折号后合并多个缩写选项。
df -h -l -a
df -hla
注意:格式最终由每个命令决定,所以不要假设这些规则是通用的。
由于空格或反斜杠等某些字符具有特殊含义,因此最好将字符串参数包含在引号中。但是,对于类似 bash 的 shell,单引号 (') 和双引号 (") 之间是有区别的。单引号从字面上理解所有内容,而双引号允许 shell 程序解释变量等内容。例如:
$ testvar=13
$ echo "$testvar"
13
$ echo '$testvar'
$testvar
如果您想了解所有可用选项,通常可以使用 --help 参数获取列表:
df --help
提示:在命令行中键入的常见内容是长文件路径。大多数 shell 程序都提供 TAB 键来自动完成路径或命令,以避免重复输入。
不同类型的命令
有五种不同类型的命令:二进制、脚本、内置、函数和别名。
我们可以将它们分为两类,基于文件的和虚拟的。
二进制和脚本命令是基于文件的,并通过创建新进程(一个操作系统的概念用于新程序)来执行。基于文件的命令往往更复杂和重量级。
内置命令、函数和别名是虚拟的,它们在现有的 shell 进程中执行。这些命令大多简单轻量。
二进制文件是经典的可执行程序文件。它包含只有操作系统才能理解的二进制指令。如果你尝试用文本编辑器打开它,你会得到乱码。二进制文件是通过将源代码编译成可执行的二进制文件来创建的。例如,Python 解释器命令 python 是一个二进制可执行文件。
对于二进制命令,shell 程序负责从文件系统中找到与命令名称匹配的实际二进制文件。不过,不要指望 shell 在你的机器上到处寻找命令。相反,shell 依赖于一个名为 $PATH 的环境变量,它是一个以冒号分隔 (:) 的路径列表以进行迭代。始终选择第一个匹配项。
要检查您当前的 $PATH,请尝试以下操作:
$ echo $PATH
如果你想知道某个命令的二进制文件在哪里,你可以调用 which 命令。
$ which python
/home/juha/.pyenv/shims/python
现在您知道在哪里可以找到文件,您可以使用文件实用程序来确定文件的一般类型。
$ file /home/juha/.pyenv/shims/pip
/home/juha/.pyenv/shims/pip: Bourne-Again shell script text executable, ASCII text
$ file /usr/bin/python3.9
/usr/bin/python3.9: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, stripped
脚本是包含人类可读程序的文本文件。 Python、R 或 Bash 脚本是一些常见示例,您可以将其作为命令执行。
通常我们不会将 Python 脚本作为命令执行,而是使用如下解释器:
$ python hello.py
Hello world
这里python是命令,hello.py只是它的一个参数。 (如果您查看 python --help 所说的内容,您会发现它对应于变体“文件:从脚本文件读取的程序”,这在这里确实有意义。)
但是我们也可以直接执行 hello.py 作为命令:
$ ./hello.py
Hello world
为此,我们需要两件事。首先,hello.py 的第一行需要使用特殊的#! 定义一个脚本解释器符号。
#!/usr/bin/env python3
print("Hello world")
这 #! 表示法告诉操作系统哪个程序知道如何解释文件中的文本,并且有许多很酷的昵称,例如 shebang、hashbang 或我最喜欢的 hash-pling!
我们需要的第二件事是将文件标记为可执行文件。您可以使用 chmod(更改权限)命令执行此操作: chmod u+x hello.py 将为拥有用户设置为可执行标志。
内置命令是硬编码到 shell 程序本身的简单命令。 cd, echo, alias 和 pwd等命令通常是内置命令。
如果您运行help命令(这也是一个内置命令!),您将获得所有内置命令的列表。
函数就像用户定义的额外内置函数。例如:
$ hello() { echo 'hello, world'; }
可以用作命令:
$ hello
hello, world
如果你想列出所有当前可用的函数,你可以调用(在类 Bash 的 shells 中):
$ declare -F
别名就像宏。更复杂命令的简写或替代名称。
例如,您希望新命令 showerr 列出最近的系统错误:
$ alias showerr="cat /var/log/syslog"
$ showerr
Apr 27 10:49:20 juha-ubuntu gsd-power[2484]: failed to turn the kbd backlight off: GDBus.Error:org.freedesktop.UPower.GeneralError: error writing brightness
. . .
由于函数和别名不是物理文件,因此它们在关闭终端后不会持久存在,通常定义在所谓的配置文件 ~/.bash_profile 或 ~/.bashrc 文件中,这些文件在新的交互或登录 shell 时执行启动。一些发行版还支持 ~/.bash_aliases 文件。
如果您想获取当前为您的 shell 活动的所有别名的列表,您可以不带任何参数调用 alias 命令。
将命令组合在一起
计算机上发生的几乎所有事情都发生在进程内部。二进制和脚本命令总是启动一个新进程。内置、函数和别名搭载在现有 shell 程序的进程上。
进程是用于运行命令(程序)实例的操作系统概念。每个进程都有一个 ID、它有自己的保留内存空间和在您的系统上执行操作的安全权限。每个进程还具有标准输入 (stdin)、标准输出 (stdout) 和标准错误 (stderr) 流。
这些流是什么?它们只是任意的数据流。没有指定编码,这意味着它可以是任何东西。文本、视频、音频、摩尔斯电码,无论命令的作者觉得合适。归根结底,您的计算机只是一台美化的数据转换机器。因此,就像函数一样,每个进程都有输入和输出是有道理的。将输出流与错误流分开也是有意义的。如果您的输出流是视频,那么您不希望基于文本的错误消息的字节与您的视频字节混合。
默认情况下,stdout 和 stderr 流通过管道返回到您的终端,但这些流可以重定向到文件或通过管道成为另一个进程的输入。在命令行中,这是通过使用特殊的重定向运算符(|、>、<、>>)来完成的。
让我们从一个例子开始。 curl 命令下载一个 URL 并将其标准输出定向回终端作为默认值。
$ curl https://filesamples.com/samples/document/csv/sample1.csv
"May", 0.1, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0
"Jun", 0.5, 2, 1, 1, 0, 0, 1, 1, 2, 2, 0, 1
"Jul", 0.7, 5, 1, 1, 2, 0, 1, 3, 0, 2, 2, 1
"Aug", 2.3, 6, 3, 2, 4, 4, 4, 7, 8, 2, 2, 3
"Sep", 3.5, 6, 4, 7, 4, 2, 8, 5, 2, 5, 2, 5
假设我们只想要前三行。我们可以通过使用管道运算符 (|) 将两个命令通过管道组合在一起来做到这一点。通过管道传输,将第一个命令 (curl) 的标准输出作为第二个命令 (head) 的标准输入。第二个命令(head)的标准输出仍然作为默认输出到终端。
$ curl https://filesamples.com/samples/document/csv/sample1.csv | head -n 3
"May", 0.1, 0, 0, 1, 1, 0, 0, 0, 2, 0, 0, 0
"Jun", 0.5, 2, 1, 1, 0, 0, 1, 1, 2, 2, 0, 1
"Jul", 0.7, 5, 1, 1, 2, 0, 1, 3, 0, 2, 2, 1
通常,您需要磁盘上的数据而不是终端上的数据。我们可以通过使用 > 操作符将最后一个命令(head)的标准输出重定向到一个名为 foo.csv 的文件中来实现这一点。
$ curl https://filesamples.com/samples/document/csv/sample1.csv | head -n 3 > foo.csv
最后,一个进程在结束时总是返回一个值。当返回值为零 (0) 时,我们将其解释为成功执行。如果它返回任何其他数字,则表示执行有错误并提前退出。例如,任何未被 try/except 捕获的 Python 异常都会以非零代码退出 Python 解释器。
您可以使用 $? 变量检查先前执行的命令的返回值是什么?
$ curl http://fake-url
curl: (6) Could not resolve hostmm
$ echo $?
6
以前我们将两个命令与流一起通过管道传输,这意味着它们并行运行。当我们使用 && 运算符将两个命令组合在一起时,命令的返回值很重要。这意味着我们要等待上一个命令成功,然后再继续下一个。例如:
cp /tmp/apple.png /tmp/usedA.png && cp /tmp/apple.png /tmp/usedB.png && rm /tmp/apple.png
这里我们尝试将文件 /tmp/apple 复制到两个不同的位置,最后删除原始文件。使用 && 运算符意味着 shell 程序检查每个命令的返回值,并在它移动之前断言它为零(成功)。这可以防止我们在最后意外删除文件。
如果您对编写更长的 shell 脚本感兴趣,那么现在是绕道而行到 Bash“严格模式”领域的好时机,以免让您头疼。
像老板一样管理数据科学项目
通常,当数据科学家冒险进入命令行时,这是因为他们使用了第三方服务或云运营商提供的 CLI(命令行界面)工具。常见示例包括从 AWS S3 下载数据、在 Spark 集群上执行一些代码或构建用于生产的 Docker 镜像。
总是手动记住并一遍又一遍地键入这些命令并不是很有用。从团队合作和版本控制的角度来看,这不仅是痛苦的,而且也是一种不好的做法。人们应该始终记录神奇的食谱。
为此,我们建议使用自 1976 年以来的经典之一 make 命令。它是一个简单、普遍且强大的命令,最初是为编译源代码而创建的,但可以被武器化以执行和记录任意脚本。
使用 make 的默认方法是在项目的根目录中创建一个名为 Makefile 的文本文件。您应该始终将此文件提交到您的版本控制系统中。
让我们创建一个只有一个“target”的非常简单的 Makefile。由于编译源代码的历史,它们被称为目标,但您应该将目标视为一项任务。
Makefile
hello:
echo "Hello world!"
现在,还记得我们说过这是 1976 年的经典吗?好吧,它并非没有怪癖。您必须非常小心地使用制表符缩进该 echo 语句,而不是任何数量的空格。如果您不这样做,您将收到“缺少分隔符”错误。
要执行我们的“hello”目标(或任务),我们调用:
$ make hello
echo "Hello world!"
Hello world!
注意: make 如何打印出食谱而不仅仅是输出。您可以使用 -s 参数来限制输出。
$ make -s hello
Hello world!
接下来,让我们添加一些有用的东西,比如下载我们的训练数据。
Makefile
hello:
echo "Hello world!"
get-data:
mkdir -p .data
curl <https://filesamples.com/samples/document/csv/sample1.csv>
> .data/sample1.csv
echo "Downloaded .data/sample1.csv"
现在我们可以下载我们的示例训练数据:
$ make -s get-data
Downloaded .data/sample1.csv
(顺便说一句:我们的读者中经验更丰富的 Makefile 向导会注意到 get-data 应该真正命名为 .data/sample1.csv 以利用 Makefile 的速记和数据依赖性。)
最后,我们将看一个示例,说明数据科学项目中的简单 Makefile 可能是什么样子,这样我们就可以演示如何在 make 中使用变量并获得更多启发:
Makefile
DOCKER_IMAGE := mycompany/myproject
VERSION := $(shell git describe --always --dirty --long)
default:
echo "See readme"
init:
pip install -r requirements.txt
pip install -r requirements-dev.txt
cp -u .env.template .env
build-image:
docker build .
-f ./Dockerfile
-t $(DOCKER_IMAGE):$(VERSION)
push-image:
docker push $(DOCKER_IMAGE):$(VERSION)
pin-dependencies:
pip install -U pip-tools
pip-compile requirements.in
pip-compile requirements-dev.in
upgrade-dependencies:
pip install -U pip pip-tools
pip-compile -U requirements.in
pip-compile -U requirements-dev.in
这个示例 Makefile 将允许您的团队成员在克隆存储库后初始化他们的环境,在他们引入新库时固定依赖项,并部署具有良好版本标签的新 docker 镜像。
如果您始终在代码存储库中提供一个不错的 Makefile 以及编写良好的自述文件,它将使您的同事能够使用命令行并始终如一地重现您的每个项目的魔力。
原文链接:What every data scientist should know about the command line