Shell and Bash Concepts


Shell 是使用者與作業系統溝通的介面,人機介面的形式分成 CLI 與 GUI 兩種。CLI (Command Line Interface) 讓一堆指令可以透過 指令搞 (command script) 變成自動化 程序 (procedure),提高工作效能,這樣的指令搞我們把它稱為 shell scripts,中文通常翻譯成 程式化腳本Shell 指令搞

Shell 本身也是一個程式,但是他並沒有一個表準的型態,不同作業系統使用的也不盡相同。Unix-like 上最基本的 Shell 叫做 sh,全名是 Bouren Shell,是個 POSIX 標準。Bash 全名是 Bourne Again Shell,是 sh 的 superset。sh 與 bash 有點像 vi vs vim 的關係。

Unix / Linux 上很常用 Bash Script 做一些工作,本文整理 Linux 上的 Shell 和 Bash Script 重要的概念 … 等 外在因子

  • 註一:Shell 中文為 ,由法國計算機科學家 Louis Pouzin 在 1964-1965 年間提出來的概念,後來實作在 Multics 上,而 Multics 後來催促 Unix 的誕生。
  • 註二:中文的 程序 一詞,有時候指的是作業系統的 process 概念,是個實際上佔有資源的單位;有時候也指的是 procedure,指的是一段 作業流程 (workflow),後面描述視狀況會增加英文註記。

Shell Concepts

Shell 是什麼?

Shell 是作業系統的專有名詞,指的是使用者與作業系統核心 (Kernel) 溝通的橋樑,負責將使用者要執行的指令,帶給 Kernel 處理。而 Kernel 則是負責跟硬體溝通,可以說是作業系統核心的前置處理器 (Pre-processoer)。

Shell 分成兩種,一種是處理 CLI (Command Line Interface),一種是處理 GUI (Graphic User Interface)。現今流行的兩大主流作業系統個別的 Shell 如下整理:

Unix-Like:

  • Linux 底下 CLI 最常見的有 bash、zsh、csh、tsh … 等,而 GUI 則是 X Window
  • macOS 的 Shell 則由 Finder、SystemUIServer、Dock、Mission Controller 組成。
    • iTerm2, terminal.app 不是 Shell, 他們都是應用程式.
    • macOS 的 CLI Shell 則是 bash

Microsoft:

  • MS-DOS 底下的 command.com
  • Windows 則使用 Windows Shell,屬於 GUI Shell,它則包含了 Start Menu、Explorer、Task Bar … 等
    • cmd.exe 則是 Windows 底下的 cli shell.

Shebang

Shell Script 使用 #! 告訴 shell 這個 script 使用哪個 interpreter 解析。#! 這個組合符號稱為 Shebang,Shebang 的 syntax:

1
#!interpreter [optional-arg]

規則描述如下:

  1. Shebang 必須在 script 的第一行
  2. 必須使用 shebang #! 作為開頭
  3. Shebang 後面可以接一個空格 (space),但不是必要的
  4. Shebang 後面接 interpreter binary 的絕對路徑,像是 /bin/sh/bin/bash
  5. Interpreter 可以額外帶入參數

舉例:

  • #!/bin/bash: 使用 bash 作為 interpreter
    • #! /bin/bash Shebang 與 interpreter 空一個是可以的。
    • #!/bin/sh: 使用 sh 作為 interpreter
  • #!/usr/bin/env bash: 使用 env 自動找到 bash 的位置。
  • #!/bin/bash -x: 開啟 bash trace 模式,等於在 script 裡面使用 set +
    • #!/bin/bash -v: 開啟 bash verbose 模式
    • #!/bin/bash -xv: 同時開啟 bash trace 和 verbose 模式
  • #!/bin/false: 啥都不做,同時回傳 non-zero 的 exit code.

執行 Shell Script

可以透過以下兩種方式執行 Shell Script:

  1. 使用 Shebang 指定的 interpreter 解譯 script: ./script.sh
    • 前提是檔案的模式為 executable (可執行)
  2. 如果想忽略 Shebang 的宣告,使用指定的 interpreter 解譯 script: bash script.sh
    • 如果原本 Shebang 宣告使用 #!/bin/sh,那麼就會被改成使用 bash
  3. 使用 #!/usr/bin/env xxx
    • 不同 unix-like 環境的 shell interpreter 路徑不見得一致,如果要考慮可移植性的話,shebang 宣告成 #!/usr/bin/env bash,runtime 自動找到 interpreter 的位置。

IPC: pipes, stdin, stdout, stderr

兩個 process 之間需要溝通時,就需要有機制配合,這個過程稱為 Inter-Process Communication (IPC)。Shell Programming 的 Standard In / Out / Error 是所有程序 (Process) 對外溝通的標準介面,然後透過 Pipe (管線) 可以串接這三個資料源頭,或者透過 檔案 (File)

先整理三個 Standard 的基本概念:

  • 標準輸入 (standard input, stdin, 代碼為 0): 程式執行所需要的輸入資料
  • 標準輸出 (standard output, stdout, 代碼為 1): 程式正常執行過程中,產生的輸出資料。
  • 標準錯誤輸出 (standard error output, stderr, 代碼為 2): 程式發生錯誤時,給使用者用的訊息、或呈現程式狀態用的訊息。

Process 之間的溝通過程中會有以下狀況:

  1. stdin: 接收其他 Process 提供的資訊,當作 stdin,而這個 stdin 可能是由另一個 Process 的 stdout or stderr 提供。
  2. stdout: 產生輸出結果,給下一個 Process 使用,或者寫到檔案。
  3. stderr: 將錯誤結果,給下一個 Process 使用,或者寫到檔案。
  4. 上述的過程,在 Unix 裡通常都會透過 pipe 作為溝通的 管道
    • unix 的 pipe 分成 anonymous pipe 和 named pipe 兩種
      • 經常看到的 a | b 就是 anonymous pipe,專屬 process
      • 透過 mkfifo /tmp/test 可以建立 named pipe,很多 process 可以共用

通常他們三個在 pipe 過程可以透過 Terminal (Console) 或者寫入檔案,常用的如下:

  • 大於 >
    • 代表左邊指令的結果,輸出並 覆寫 到右邊檔案位置
    • 例如:cmd > filename
    • 這個做法檔案會被清空,然後重新寫入
  • 兩個大於 >>
    • 代表左邊指令的結果,輸出並 附加 到右邊檔案位置
    • 例如:cmd >> filename
    • 不會清空檔案內容,直接附加在最後
  • 小於 <:
    • 把右邊的內容當作 input,送給左邊的 cli
    • 範例:cmd < list

Unix 裡透過 pipe (|) 讓 stdin, stdout, stderr 相互傳導,下表整理詳細的

下圖整理自 How do I save terminal output to a file? 中網友的回答:

註:IPC 的通訊實作方式很多種,包含 Shared Memory, Message Passing, Sockets, RPC … 等。pipe 是 unix 上第一個實作的 IPC 介面,在 Unix / Windows 有個別的實作,整理如下:

  • unix:
    • anonymous pipe: 只能在同一台機器上的不同 process 使用。
    • named pipe: call FIFO, communicate in same machine
  • windows:
    • ordinary pipes: called anonymous pipe
    • named pipes: Bi-directional, 可以跨機器溝通。

上述詳細可以參閱 Operating System Concepts 第三章的介紹。

Shell Variables

在 POSIX 定義以下變數應該被初始化的,這些變數都是 built-in 的,整理如下:

  • ENV
  • HOME
  • IFS: Input Field Separators
  • LANG
  • LC_*: LC_ALL, LC_COLLATE, LC_CTYPE, LC_MESSAGE
  • LINENO
  • NLSPATH
  • PATH
  • PPID
  • PS1, PS2, PS4
  • PWD

上述變數詳細參閱: 2.5.3 Shell Variables

Special Shell Variables

除了上一段提到的變數,還有一些特殊符號構成的變數:

  • $1 - $9: 參數的位置
  • $0: 目前 script 的名稱
  • $?: 上一個 command 的 exit status
  • $#: 被呼叫的 script 的參數位置
  • $$: 目前的程序的 PID (Process ID)
  • $!: 最後一個在背景執行的 process id
  • $-: 使用 set 設定的 flag
  • $*: script 所有的參數 (args) 列表
  • $@: 同上

Bash Concepts

Bash 全名 Bourne-Again Shell,是大多數 linux 預設的 shell,底下整理一些使用 Bash 重要的觀念。

註:除了 bash,其他老牌子 shell 還有 sh, csh, ksh … 等,新一代的有 dash、rbash、tmux … 等。可以透過 cat /etc/shells 看到目前作業系統支援哪一些 shell。

初始程序:Login Shell, Non-Login Shell

初始程序 (init process) 是電腦系統很重要的課題,很多地方都有初始程序,像是電腦主機版 BIOS 開機程序、Bootloader、作業系統開機程序 (pid=0)、K8s pod 的初始程序、Docker container 初始程序、登入程序、瀏覽器開啟程序、網路連線初始程序 (TCP Handshaking) …

Linux 的登入程序是寫 shell script 時需要了解的,剛開始在用 cron job 跑 script 時很容易遇到讀不到環境變數的問題,其實就是 shell 的初始程序的問題。

Shell 初始程序分成 1) login shell 和 2) non-login shell 兩種,兩種都有各自所屬的初始程序 (procedure),這些程序稱為 rc (run command) 程序。常見的 cron job 拿不到環境變數就是初始程序不一樣造成;另一個常見的例子就是用 ssh 跑 remote procedure ,這個過程也有個 login 的過程,同樣的也會遇到 shell login 初始程序的問題。

註一:centos/redhat 與 debian/ubuntu 的 crontab 對於 login shell 有不同的初始方式,所以有些人可能使用 crontab 沒遇到上述問題。

login shell 的 rc 流程如下:

  1. /etc/profile
  2. /etc/profile.d/*.sh
  3. ~/.bash_profile
  4. ~/.bashrc (invoked by $HOME/.bash_profile)
  5. /etc/bashrc

而 non-login shell 只有後面的三個:

  1. ~/.bash_profile
  2. ~/.bashrc (invoked by $HOME/.bash_profile)
  3. /etc/bashrc

了解這些次序之後,可以從原始碼中了解登入初始的細節。

crontab 與 ssh, su 切換身份時,先確認他們是用什麼方式登入。

使用 su 切換身份

延續前面提到的問題,以下整理使用 su 要注意的觀念。

SysOp 常需要用 sudo 配合 su 切換身份到 root,很常看到下這樣的指令:

1
2
~$ sudo su
root@lab99:/home/ubuntu#

之後,就開始直接用 root 身份做事情,同樣的切換方法,有時候也會看到以下的切法:

1
2
~$ sudo su -
root@lab99:~#

這兩個有什麼差別?注意切換之後的 #提示訊息 (prompt) 以及現在位置 (pwd),觀察兩者 #環境變數 (env) 的差異。
寫 script 需要直接切換身份執行該身份的指令,通常會是:

1
2
~$ su <username> -c "your_script.sh or cli"
~$ su - <username> -c "your_script.sh or cli"

這兩個 script 可能會得到不同的結果,所以因為初始程序的不一樣,造成可用環境變數不一樣,然後 script 在 runtime 很容易造成問題。
這兩個切換方式的唯一差別,就在後者的 su 多了 –login 參數 (shortcut as -),這個參數就是切換時使用 login shell 方式登入。前者是 non-login shell,只有完成部分程序,所以看起來少了一些東西;後者就如同真的使用 root 登入一樣。

其實 su 的 manual 也建議通常要帶 --login 參數,也就是使用 login shell 方式切換,避免因為 non-login shell 造成額外的問題。摘錄原文如下:

For backward compatibility, su defaults to not change the current directory and to only set the environment variables HOME and SHELL (plus USER and LOGNAME if the target user is not root). It is recommended to always use the –login option (instead of its shortcut -) to avoid side effects caused by mixing environments.

如何知道現在是哪一種方式?

除了 prompt 可以觀察,要用什麼方式知道現在的 login shell?可以透過以下方式確認現在是用哪一種方式:

1
2
3
4
5
6
7
8
9
10
~$ echo $0
-bash # 有 hyphen - 開頭,是 login shell
~$ sudo su -
~$ echo $0
-su # sudo su - 切換 login shell
~$ echo $0
bash # 沒有 hyphen - 開頭,是 non login shell
~$ sudo su
~$ echo $0
su # sudo su 切換 non login shell

Bash Built-in Commands

Shell Script 作為使用者與 Kernel 的介面,使用上經常會配合其他工具,像是 sed, awk, find, ls, … 等,一起使用,透過這樣的方式與系統溝通,這些工具都可以在 /bin/, /usr/bin 底下找到實際的執行檔。但有些指令不存在於上述位置,而是由 bash 內建提供的,整理常用的如下:

  • alias
  • bind
  • declare
  • echo
  • enable
  • help
  • let
  • local
  • logout
  • printf
  • read
  • readarray
  • source
  • type
  • typeset
  • ulimit
  • unalias

上述詳細參見:Bash Builtin Commands

除了 bash 內建的, sh 也內建一些指令,不難發現,這些都是構成一個 程式語言 的基礎元素,整理如下:

  1. : (a colon)
  2. . (a period)
  3. break
  4. cd
  5. continue
  6. eval
  7. exec
  8. exit
  9. export
  10. getopts
  11. hash
  12. pwd
  13. readonly
  14. return
  15. shift
  16. test
  17. [
  18. times
  19. trap
  20. umask
  21. unset

上述詳細參見: Bourne Shell Builtins

IFS

IFS 是 Internal Field Separator 的縮寫,是 bash 的特殊變數,用在做字串分割 (splitting) 時做為切割字元 (delimiter),預設值是 space, tab 與 newline 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# macOS
~$ echo "[${IFS}]"
[
]
echo -n "$IFS" | od -a -t x1 -c
0000000 sp ht nl nul
20 09 0a 00
\t \n \0
0000004

# Ubuntu 18.04
~$ echo "[${IFS}]"
[
]
~$ echo -n "$IFS" | od -a -t x1 -c
0000000 sp ht nl
20 09 0a
\t \n
0000003

od 是 Octal Dump, unix 底下顯示檔案內容格式的工具,可以顯示八進位、十六進位等格式。


Q and A

sh vs bash 的差異?

如開頭提到,sh vs bash 大概就是 vi vs vim 的差別。前者是基本的實作,包含所有 built-in 的必要功能,後者則多了一些延伸功能。

下圖是網友整理的 sh, csh, ksh, bash, tcsh 功能比較表:


資料來源:Difference between sh and bash

從 macOS (v10.15.7) 看看兩者差異:

1
2
3
4
ls -al /bin/sh
-rwxr-xr-x 1 root wheel 31440 Sep 22 08:30 /bin/sh
ls -al /bin/bash
-r-xr-xr-x 1 root wheel 623472 Sep 22 08:30 /bin/bash

從 ubuntu 18.04 查看:

1
2
~# ls -l /bin/sh
lrwxrwxrwx 1 root root 4 Jul 25 2018 /bin/sh -> dash

dash 是 ubuntu 預設的 shell,全名是 Debian Almquist Shell

這年代還需要 Shell Programming?

雲端流行的時代,很多高階工具 (像是 AWS CDK)、宣告式 (Ansible、IaC、K8s) 概念流行,有人就會問:

還需要寫 Shell Scripts?

這問題應該要這樣問:

你是開發應用程式?還是系統程式?

先解釋什麼是 應用程式,什麼是 系統程式

  • 應用程式:給終端使用者使用 (End-User) 的程式,跟提供人機介面,商業邏輯打交道。
    1. 舉凡各種 Mobile APP、Web APP = Frontend + Backend
    2. Desktop APP,像是 Slack、Browser、Git Client、Final Cut .. etc.
  • 系統程式:給應用程式開發者使用的,他主要會跟作業系統德核心 (Kernel) 打交道,打交道有兩種方式:
    1. 直接透過 System Call 跟作業系統 Kernel 打交道,像是寫驅動程式、處理網路封包、處理 I/O、處理影像 …
    2. 間接跟核心打交道:透過 Shell Script 與系統工具整合,像是使用 ToolChain 執行應用程式編譯,最常見的就是編譯 Android APP。
    3. 整合應用:管理應用程式生命週期,像是 Linux 的初始應用程式 sysvinit,還有 build 過程的 scripts.

一開始提到 Shell 人機介面的形式分成 CLI 與 GUI 兩種:GUI & CLI。GUI 對人的操作來講雖然很方便,但是以工作效率話來講,卻是很沒效率的。Linux 一開始就是以 Bash 為主,後來也有 X-Window 的 GUI 標準。微軟作業系統一開始則以 GUI 為主,如果要自動化配置,就要仰賴像是 autoit 這種圖形控制器來自動畫。但是後來微軟弄出 PowerShell,用於自動化組態管理,提高效率,其目的跟 autoit / bash 其實是類似的。


延伸閱讀

站內延伸

參考資料




Comments