Linux 上的 Shebang 是在那裡定義的?

Posted by 每特17劃 on 2022-08-02

Linux 上的 Shebang 是在那裡定義的?

在 Linux 的指令執行檔的開頭,常常會看到以 #! 字串開頭的宣告,像是:

  • #!/bin/bash
  • #!/usr/bin/env python3
  • #!/bin/sh

這個開頭宣告的作用,是告訴系統要用什麼指令來執行該指令執行檔。 例如: #!/usr/bin/env python3 開頭的,就會以 /usr/bin/env python3 這個指令來執行。

不過,這開頭的 #! 這一行是那裡讀取並判讀的呢? 是 Bash? glibc? … or kernel?

答案是,直接由 Linux kernel 處理的,其對應的 source code 是在

fs/binfmt_script.c 裡的 load_script() 函式:

static int load_script(struct linux_binprm *bprm)
{
	const char *i_name, *i_sep, *i_arg, *i_end, *buf_end;
	struct file *file;
	int retval;

	/* Not ours to exec if we don't start with "#!". */
	if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
		return -ENOEXEC;

(出處: https://github.com/torvalds/linux/blob/v5.18/fs/binfmt_script.c#L41)

同時,Linux kernel config 有個設定選項 BINFMT_SCRIPT ,來決定是否支援 #! 這樣的用法。

(出處: https://github.com/torvalds/linux/blob/v5.18/fs/Kconfig.binfmt#L99)

可以使用下列指令來檢查系統的支援狀況:

zless /proc/config | grep -e "^CONFIG_BINFMT_SCRIPT="

預期會得到:

CONFIG_BINFMT_SCRIPT=y

一般來說,絕大多數的 Linux 系統都內建支援 #! 的語法。 但還是遇有一些 embedded system 或是資安考量的系統,會拿掉這個設定。 若在設定 at , cron 的排程設定時,發現程式未如預期執行時,可順便檢查這一個設定。

Q: 關於 Shebang 的歷史?

可參考維基百科的說明:

https://en.wikipedia.org/wiki/Shebang_(Unix)

Q: Linux 上大約何時支援?

從官方的 git 紀錄,支援 #! 宣告的功能,至少在 v2.6.12 時就己經存在:

https://github.com/torvalds/linux/blob/v2.6.12/fs/binfmt_script.c#L26

實際時間應該更早。 不過現下已經很少遇到這類舊系統,所以暫時沒再往前追溯。

Q: 為什麼有的會用 #!/usr/bin/env 開頭?

以 Bash 為例,有些系統會放在 /bin/bash 的位置,有些會放在 /usr/bin/bash。 若我的 script 寫 #!/bin/bash ,那遇到放在 /usr/bin/bash 位置的系統,就會路徑不符而失效。 所以,就有了這一支 env 的程式,他在所有系統都固定放在 /usr/bin/env 這個位置。 當用下述的指令時:

/usr/bin/env bash

env 就會根據該系統的環境設定(ex: $PATH, /etc/environment, …),去找出對應的 bash 的位置去執行。 如果一來,用上述的宣告在橫跨不同系統時,都能執行,而不需要再調整開頭的 Shebang 路徑。

進一步的資訊,可參考 Shebang (Unix) - Wikipedia 的 “Portability » Program location” 一節。

2024-04-07 創意用法

偶然看到 用 shebang 掛起 Docker instance – Gea-Suan Lin’s BLOG 一文,提到 Shebang 的某種創意用法:

adtac/595b5823ef73b329167b815757bbce9f 利用 Shebang 的用法,把 Dockerfile 的檔案開頭設爲

#!/usr/bin/env -S bash -c "docker run -p 8080:8080 -it --rm \$(docker build --progress plain -f \$0 . 2>&1 | tee /dev/stderr | grep -oP 'sha256:[0-9a-f]*')"

同時,將 Dockerfile 改爲可執行檔 (ex: chmod +x Dockerfile) 。之後即可下 ./Dockerfile 指令去執行它。