美文网首页
浅谈 Shell

浅谈 Shell

作者: 越前君 | 来源:发表于2022-06-21 21:41 被阅读0次
    配图源自 Freepik

    一、前言

    通常,用户控制计算机的方式有图形化界面(GUI,Graphical User Interface)和命令行界面(CLI,Command Line Interface)两种。然而,真正能够控制计算机硬件(如 CPU、内存、显示器等)的只有操作系统内核(Kernel)。因此,图形化界面和命令行只是架设在用户和内核之间的一种桥梁。

    由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没必要),因此需要开发一个程序。用户直接使用这个程序,程序的作用就是接收用户的操作(输入),进行简单处理,然后传递给内核,这样用户就可以间接使用操作系统了。Shell 就是这样的一个程序。在用户和内核之间增加一层「代理」,既能简化操作,又能保证内核的安全,何乐而不为呢?

    因此,Shell 并不是操作系统内核的一部分。

    以上内容部分摘自 Shell 是什么

    二、Shell 的含义

    Shell 的英文原意是「外壳」,与之相对的是内核(Kernel)。但具体来说,Shell 这个词有多种含义。

    1. Shell 是一个应用程序,提供一个与用户对话的环境,该环境只有一个命令提示符(通常是 $#),让用户输入命令。这种环境被称为命令行环境(CLI)。Shell 接收到用户输入的命令,将命令传递给操作系统执行,并将结果返回给用户。

    2. Shell 是一个命令解析器,解析用户输入的命令,它支持变量、条件判断、循环操作等语法,所以用户可以用 Shell 写出各种小程序,又被称为脚本(Script)。这些脚本通过 Shell 解析器解析执行,注意不是编译。

    3. 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

    一些命令:

    1. 查看当前操作系统默认 Shell
    $ echo $SHELL
    /bin/zsh
    
    1. 查看当前使用的 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。

    1. 查看当前操作系统的所有的 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
    
    1. 进入或退出 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.shbash ./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 的环境,比如 aliasPATH 等。

    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 变量」两种变量。它们都是区分大小写的,因此 HOMEhome 是两个不同的变量。

    • 环境变量:通常在 Shell 配置文件中以 key=value 形式实现,它在整个系统范围内都可用,并被所有子进程和 Shell 继承。通常以大写形式命名,比如 PATH 等。
    • Shell 变量:专门用于设置或定义它们的 Shell 中的变量,每一种 Shell 解析器都有一组属于自己的内部 Shell 变量。在编写 Shell 脚本时常用于跟踪临时数据。

    使用 envprintenv 命令(不带其他参数时),可以显示所有环境变量。若查看单个环境变量的值,可以使用 printenvecho 命令。

    $ 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 脚本而已,这些内容在前一节提到过了。

    未完待续...

    相关文章

      网友评论

          本文标题:浅谈 Shell

          本文链接:https://www.haomeiwen.com/subject/qaqevrtx.html