一、前言
通常,用户控制计算机的方式有图形化界面(GUI,Graphical User Interface)和命令行界面(CLI,Command Line Interface)两种。然而,真正能够控制计算机硬件(如 CPU、内存、显示器等)的只有操作系统内核(Kernel)。因此,图形化界面和命令行只是架设在用户和内核之间的一种桥梁。
由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没必要),因此需要开发一个程序。用户直接使用这个程序,程序的作用就是接收用户的操作(输入),进行简单处理,然后传递给内核,这样用户就可以间接使用操作系统了。Shell 就是这样的一个程序。在用户和内核之间增加一层「代理」,既能简化操作,又能保证内核的安全,何乐而不为呢?
因此,Shell 并不是操作系统内核的一部分。
以上内容部分摘自 Shell 是什么。
二、Shell 的含义
Shell 的英文原意是「外壳」,与之相对的是内核(Kernel)。但具体来说,Shell 这个词有多种含义。
-
Shell 是一个应用程序,提供一个与用户对话的环境,该环境只有一个命令提示符(通常是
$
或#
),让用户输入命令。这种环境被称为命令行环境(CLI)。Shell 接收到用户输入的命令,将命令传递给操作系统执行,并将结果返回给用户。 -
Shell 是一个命令解析器,解析用户输入的命令,它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 写出各种小程序,又被称为脚本(Script)。这些脚本通过 Shell 解析器解析执行,注意不是编译。
-
Shell 是一个工具箱,提供了各种小工具,供用户方便地使用操作系统的功能。
以上摘自 Bash 简介。
三、Shell 的种类
Shell 又很多种,只要能给用户提供命令行环境的程序,都可以看作是 Shell。在历史上,主要 Shell 有这些:
- Bourne Shell(sh)
- Bourne Again shell(bash)
- C Shell(csh)
- TENEX C Shell(tcsh)
- Korn shell(ksh)
- Z Shell(zsh)
- Friendly Interactive Shell(fish)
更详细可看网道和 Wikiwand。目前最常用的 Shell 是 bash。
四、Shell 解析器
各操作系统通常会内置多种 Shell 解析器,且有一个默认的 Shell。比如 Linux 操作系统是 bash,Windows 操作系统是 PowerShell,Mac 操作系统是 bash 或 zsh。
以 macOS 为例,不同版本下其内置默认 Shell 如下:
- OS X 10.2 及以下版本默认 Shell 为 csh。
- OS X 10.3 ~ macOS 10.14 版本默认 Shell 为 bash。
- 自 macOS 10.15 起默认 Shell 为 zsh。
关于如何从 bash 切换至 zsh,或者在不修改默认 Shell 的情况下使用其他 Shell,可参考:在 Mac 上将 zsh 用作默认 Shell。
一些命令:
- 查看当前操作系统默认 Shell
$ echo $SHELL
/bin/zsh
- 查看当前使用的 Shell(不一定是默认 Shell)
$ ps
PID TTY TIME CMD
13766 ttys000 0:00.25 /bin/zsh -l
14879 ttys000 0:04.45 node /usr/local/Cellar/yarn/1.22.19/libexec/bin/yarn.j
14880 ttys000 0:10.75 gulp start --project qualcomm-202206
14886 ttys000 0:00.06 open -W http://localhost:3000
15495 ttys001 0:00.37 /bin/zsh -l
16701 ttys002 0:00.63 -zsh
18509 ttys003 0:00.26 -zsh
一般来说,ps 命令(显示当前系统的进程状态)结果的「倒数第二行」是当前正在使用的 Shell。
- 查看当前操作系统的所有的 Shell
$ cat /etc/shells
# List of acceptable shells for chpass(1).
# Ftpd will not allow users to connect who are not using
# one of these shells.
/bin/bash
/bin/csh
/bin/dash
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh
- 进入或退出 Shell
当前我的默认 Shell 是 zsh,如果要进入 bash 环境,执行一些命令,然后再退出。举个例子:
$ bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$ echo $NVM_DIR
/Users/frankie/.nvm
bash-3.2$ exit
exit
其中 bash
命令表示进入 bash Shell 环境,echo $NVM_DIR
表示输出 $NVM_DIR
环境变量,然后执行 exit
命令退出 bash Shell 环境。
五、Shell 脚本
脚本(Script)是指包含一系列命令的文本文件。利用 Shell 解析器便可执行的脚本,被称为 Shell 脚本。
举个例子,一个最简单的 Shell 脚本 hello.sh
:
#!/bin/bash
echo "Hello Shell"
- 脚本首行
#!
是一个约定标记(称为 Shebang 行),用于指定执行该脚本的 Shell 解析器,通常是/bin/bash
或/bin/sh
。#!
与解析器之间的「空格」可有可无。- 文件顶部的 Shebang 行是非必需的。若无,则要在执行脚本时手动指定,比如
/bin/sh ./hello.sh
或bash ./hello.sh
。- 若手动指定解析器,那么将会使用所指定的解析器去执行该脚本,而脚本内指定的解析器会被忽略。
- Shell 脚本文件的扩展名是非必需的,甚至可以命名为奇奇怪怪的扩展名,不影响脚本的执行。但是...按照习惯通常会命名为
.sh
。
但需要注意的是,如果 Shell 解析器并没有放在 /bin
,这样脚本叫无法执行了。为了确保稳定性,可以写成这样:
#!/usr/bin/env bash
原因是 env
命令一定是在 /bin/bin
目录下,它返回对应 Shell 解析器的位置。
如果开发过一些 Node 相关工具(比如 vue-cli),你一定看到过这样的 Shebang 行,用于指定解析器为 Node。
#!/usr/bin/env node
也可以观察一下项目的 node_modules/.bin
目录下的所有可执行文件,它们几乎都是以 #!/usr/bin/env node
开头的,其作用是正确地查找对应解析器所在路径,以避免用户没有安装在默认路径下导致无法正常执行脚本的问题。如果你是写 Python 的话,一定见过 #!/usr/bin/env python
...
如果平常想要看下 Node 安装目录的话,可以使用 which
命令,比如:
$ which node
/Users/frankie/.nvm/versions/node/v16.15.0/bin/node
六、NPM 脚本与 Shell
在前端项目中,我们通常会在 package.json
中定义一系列的脚本命令,比如:
{
"scripts": {
"test": "jest"
}
}
当我们在终端输入 npm run test
命令时,就可以执行对应的测试脚本。当我们在项目根目录或者用户根目录等路径下直接执行 jest
命令,抛出错误:
$ jest
zsh: command not found: jest
原因是执行命令的当前目录或 PATH
(系统环境变量)目录都不存在一个名为 jest
的可执行文件。那么 npm run jest
内部又偷偷做了哪些工作呢,为什么它能准确找到可执行文件 jest
所在的目录呢?
假设我们有一个 my-app
包,如下:
{
"name": "my-app",
"version": "1.0.0",
"bin": {
"myapp": "./cli.js"
}
}
其中有一个 bin
字段(详见),表示该包有一个名为 myapp
的可执行文件。在执行 npm install my-app
时,除了将包下载至 node_modules
目录下,还会创建一个 cli.js
文件的软链接(symlink)至 node_modules/.bin
目录,该可执行文件的名称就是对应的键名 myapp
。若以 "bin": "./cli.js"
形式配置,名称则为其包名 my-app
。若全局安装,则会被链接至全局目录。
以 jest 为例,可执行文件 node_modules/.bin/jest
其实是 node_modules/jest/bin/jest.js
的软链接,因此真正被执行的是后者。
如果 package.json
中定义的命令是这样:
{
"test": "./node_modules/.bin/jest"
}
或这样:
{
"test": "./node_modules/jest/bin/jest.js"
}
都非常容易理解,但观感来说,它又长又臭,还丑。那么 { "test": "jest" }
又是如何找到目标可执行文件的呢?
其内在奥秘在于 npm run
命令。当执行此命令时,会创建一个 Shell 子进程,然后在这个 Shell 中执行指定的脚本命令。换句话说,只要是 Shell 可以运行的命令,都可以写在 NPM 脚本里面。
比较特别的是,npm run
创建的 Shell 进程,会将当前目录的 node_modules/.bin
子目录加入到 PATH
环境变量,在执行结束之后,再将 PATH
环境变量恢复原样。
推荐下阮一峰老师的这篇文章:npm scripts 使用指南。
我们来验证下,假设有这样一个项目,且有一个 test.sh
脚本用于输出 PATH
环境变量:
{
"name": "simple-test",
"version": "1.0.0",
"scripts": {
"test": "source ./test.sh"
}
}
#!/bin/bash
# test.sh
echo $PATH
其中 source
命令表示执行一个脚本,也可用其简写形式 .
表示,即 . ./test.sh
。我们也常用此命令来刷新环境变量,比如 source ~/.zshrc
,更多可看文章。
下面为了方便对比,执行前后都输出一下 PATH
环境变量的值:
可以发现,在 npm run
执行期间,PATH
环境变量发生了变化,截图标记部分为临时新增的环境变量,其中包括项目所在路径 simple-test/node_modules/.bin
,执行完之后又变为了最初的模样。
至于为什么 simple-test
逐层往上添加 node_modules/.bin
目录,我猜是跟 Node 逐层查找模块有关系,具体没去细究。
因此,我们甚至可以在 bin
字段去声明一些命令:
{
"name": "simple-test",
"version": "1.0.0",
"scripts": {
"test": "source ./test.sh"
},
"bin": {
"hello": "./hello.sh"
}
}
然后安装此包后,就可以在 script
中使用 hello
命令了。
七、Shell 配置文件
由于很多高级编程语言,有着丰富的第三方库,因此可选择的配置文件格式有很多,比如 JSON、XML、YAML 等等。对于 Shell 而言,其配置文件多是 key=value
形式的文本文件,等号两边不应有「空格」。
Shell 配置文件本身就是一种特殊的 Shell 脚本,只是没有用 .sh
扩展名而已。前面提到过 Shell 脚本扩展名是非必需的。当 Shell 被启动时,会执行对应 Shell 的配置文件中的命令,通常是配置当前 Shell 的环境,比如 alias
、PATH
等。
Shell 配置文件可分为「系统级别」和「用户级别」。当 Shell 启动时,会先执行系统级别的配置文件(如果存在的话),然后再执行用户级别的配置文件(如果存在的话),因此用户级别的配置文件优先级更高。另外,系统级别的配置对所有用户生效,而用户级别仅对当前用户生效,可以理解为继承关系。
- 系统级别配置文件一般位于
/etc
目录下,比如/etc/profile
、/etc/bashrc
等。- 用户级别配置文件一般位于
~
(用户目录)下,比如~/.profile
、~/.bashrc
、~/.bash_profile
、~/.zshrc
等,通常是隐藏性文件。
以 macOS 为例,通常修改 Shell 配置都是在用户级别的配置文件上操作,比如 ~/.bash_profile
或 ~/.zshrc
,取决于你在使用哪一种 Shell 解析器。
常见 Shell 是如何读取配置的,附一张图,源自 Shell startup scripts:
图中涉及了几个概念 interactive 和 non-interactive、login 和 non-login,分别表示是否为交互式 Shell、是否为登录式 Shell。它们在读取配置文件上会有所区别,比如,非交互式和非登录式的 zsh 不会读取 ~/.zshrc
配置文件。
以下说明,摘自此处:
Login Shell:是指该 Shell 被启动时用于用户登录。
Non-login Shell:是指用户已登录下启动的那些 Shell,被自动执行的 Shell 也属于非登录式 Shell,它们的执行通常与用户登录无关。
Interactive Shell:是指可以让用户通过键盘进行交互的 Shell。 平常在使用的 CLI 都是交互式 Shell。
Non-interactive Shell:是指被自动执行的脚本, 通常不会请求用户输入,输出也一般会存储在日志文件中。
如果在使用 zsh,可阅读下这篇大而全的配置指引:zsh wiki。
八、环境变量与 Shell 变量
本小节内容大部分摘自文章:设置与查看Linux系统中的环境变量。
在 Linux/Unix 系统中,分为「环境变量」和「Shell 变量」两种变量。它们都是区分大小写的,因此 HOME
和 home
是两个不同的变量。
- 环境变量:通常在 Shell 配置文件中以
key=value
形式实现,它在整个系统范围内都可用,并被所有子进程和 Shell 继承。通常以大写形式命名,比如PATH
等。- Shell 变量:专门用于设置或定义它们的 Shell 中的变量,每一种 Shell 解析器都有一组属于自己的内部 Shell 变量。在编写 Shell 脚本时常用于跟踪临时数据。
使用 env
或 printenv
命令(不带其他参数时),可以显示所有环境变量。若查看单个环境变量的值,可以使用 printenv
或 echo
命令。
$ printenv PATH
/Users/frankie/.nvm/versions/node/v16.14.0/bin:/usr/local/sbin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
$ echo $PATH
/Users/frankie/.nvm/versions/node/v16.14.0/bin:/usr/local/sbin:/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
echo
命令中的变量名前需要添加前缀 $
,否则当做输出一个字符串。
使用 set
命令,可以显示所有变量(包括环境变量和自定义变量),以及所有的 Shell 函数。
8.1 环境变量
单个值的环境变量,形式看起来是这样的:
KEY=value1
如果变量的值中含有空格,则需要将值放在引号中,比如:KEY="value with spaces"
。
多个值的环境变量,形式是这样的:
KEY=value1:value2:value3
每个值之间以分号 :
作为分隔符。可以理解为类似于高级编程语言中的数组。
8.2 Shell 变量
Shell 变量声明语法如下:
variable=value
等号左边为「变量名」,右边为变量值,同样地,若变量值含有空格,需要将值放在引号中。需要注意的是,在 Shell 中没有数据类型的概念,所有变量值都是字符串。
Shell 变量除了可以在 Shell 脚本文件中声明、使用,也是可以直接在 Shell 会话中操作的,但是该变量的声明周期仅在会话被销毁之前,而且不能被其他新的 Shell 会话环境中读取。若用浏览器来比喻的话,每个 Shell 会话就是两个标签的应用,应用之间的变量是不可共享的。
比如:
$ myname=frankie
$ echo $myname
frankie
由于用户创建的 Shell 变量,仅可在当前 Shell 中使用,若要传递给子 Shell,需要使用 export 命令。这样输出的变量,对于子 Shell 来说就是环境变量。但注意其他非子 Shell,是不能读取到的。
$ export myname=frankie
$ myage=20
$ bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
bash-3.2$ echo $myname
frankie
bash-3.2$ echo $myage
bash-3.2$
其他就不展开了,不然就跑偏了,有兴趣可看阮一峰老师的文章:Bash 变量。
8.3 常见变量
常见环境变量:
变量名 | 含义 |
---|---|
USER | 当前登录用户。 |
PWD | 当前工作目录。 |
OLDPWD | 上一个工作目录。 |
PATH | 系统查找指令时会检查的目录列表。当用户输入一个指令时,系统将会按此目录列表的顺序检索目录,以寻找相应的可执行文件。 |
LANG | 当前的语言和本地化设置,包括字符编码。 |
HOME | 当前用户的主目录。 |
SHELL | 当前系统的默认 Shell。 |
TERM | 终端类型名,即终端仿真器所用的协议。 |
常见的 Shell 变量:
变量名 | 含义 |
---|---|
COLUMNS | 用于设置绘制到屏幕上的输出信息的宽的列数。 |
HOSTNAME | 主机名。 |
PS1 | 命令提示符样式。 |
PS2 | 输入多行命令时,命令提示符样式。 |
8.4 特殊变量
变量名 | 含义 |
---|---|
$? |
上一个命令的退出码,用于判断上一个命令是否执行成功。返回值为 0 表示命令执行成功,否则执行失败。 |
$$ |
当前 Shell 的进程 ID。 |
$_ |
上一个命令的最后一个参数。 |
$! |
最近一个后台执行的异步命令的进程 ID。 |
$0 |
在 CLI 直接执行时,表示当前 Shell 的名称。在 Shell 脚本执行时,表示当前脚本名称。 |
$@ 、$#
|
$@ 表示脚本的参数值;$# 表示脚本的参数数量。 |
8.5 变量持久化
若不希望每次启动新的 Shell 会话时,都必须重新设置重要的变量,则需要将变量写入配置文件中。以 macOS 为例,最常见的是配置文件是 ~/.bash_profile
或 ~/.zshrc
,具体取决于你的使用了哪一种 Shell 解析器,请参考前面的第七节内容。
比如我在 ~/.zshrc
中配置 nvm 的目录,可以添加这样一行配置:
export NVM_DIR="$HOME/.nvm"
其中 $HOME
本身就是一个环境变量(表示当前用户主目录),假设我的主目录是 /Users/frankie
,那么变量 NVM_DIR
的值就是 /Users/frankie/.nvm
。
平常配置最多的可能是 PATH 变量,假设安装了一个工具 A,然后需要配置该工具的可执行文件 aaa
的路径(假设为 ~/.aaa/bin
),可以这样:
export PATH=$PATH:$HOME/.aaa/bin
如此 /Users/frankie/.aaa/bin
就会被添加至环境变量 PATH 中,这样我们就可以在任何目录下执行 aaa
命令了。
记得配置完,要使用类似 source ~/.zshrc
的命令刷新变量,使其在已启动的 Shell 进程中生效,否则只在新打开的 Shell 进程中生效。因为每次启动一个新的 Shell 进程都会先读取对应的 Shell 配置文件,然后这个刷新命令本质上就是执行了一个 Shell 脚本而已,这些内容在前一节提到过了。
未完待续...
网友评论