Shell Overview === ###### tags: `OS / Ubuntu / shell script` ###### tags: `OS`, `Ubuntu`, `linux`, `command`, `shell script`, `sh`, `bash`, `shebang`, `hashband`, `string`, `if-else`, `for-loop`, `makefile`, `指令串接` <br> [TOC] <br> # Linux: Shell script ## 術語 ### bash [bash (Bourne-Again Shell) ](https://www.796t.com/content/1546346172.html) ### dash [dash (Debian Almquist Shell)](https://www.796t.com/content/1546346172.html) ### sheband (hashband) - `#`: hash - `!`: bang [ʃəˋbæŋ] - https://tw.dictionary.search.yahoo.com/search?p=bang - int. 砰! - vi. 發出砰的一聲;砰砰作響; - vt. 砰地敲(或推,扔);猛擊,猛撞; - shebang 是什麼? [ChatGPT] shebang(也稱為 hashbang)是一種在腳本文件的第一行使用的特殊註釋語法,用於指定解釋器的路徑。它的格式是以井號(#)開頭,後面跟著一個嘆號(!),然後是解釋器的路徑。在執行腳本時,操作系統會讀取 shebang 行,並使用指定的解釋器來解釋執行該腳本。例如,一個使用 shebang 的 Python 腳本可能會在第一行寫上 #!/usr/bin/python,以指定使用 Python 解釋器來運行該腳本。這樣就可以直接執行腳本文件而無需顯式地調用解釋器。 - 範例1 ```bash #! /bin/bash ``` - 範例2: `echo_test.sh` ```bash #! /bin/bash echo -e 'a\nb\nc' ``` ``` $ sh echo_test.sh $ bash echo_test.sh $ cdmod +x echo_test.sh; ./echo_test.sh ``` - 範例3: `echo_test.sh` ```bash #! /bin/sh echo -e 'a\nb\nc' ``` ``` $ sh echo_test.sh $ bash echo_test.sh $ cdmod +x echo_test.sh; ./echo_test.sh ``` - 參考資料 - [Shell Scripting for Beginners – How to Write Bash Scripts in Linux](https://www.freecodecamp.org/news/shell-scripting-crash-course-how-to-write-bash-scripts-in-linux/) - [Python 文件頂部的 #!/user/bin/env python 是什麼意思](https://ithelp.ithome.com.tw/articles/10309816) <br> <hr> <br> ## 模式 ### 登入 Shell vs. 非登入 Shell - ### 登入 Shell: 由登入系統時啟動(例如,透過 SSH 或控制台)。 會讀取 `/etc/profile`、`~/.bash_profile`、`~/.bash_login`、`~/.profile` 等配置文件。 - ### 非登入 Shell: 由登入 Shell 或其他程序啟動(例如,您在登入 Shell 中執行 `bash`)。 會讀取 `~/.bashrc` 配置文件。 - ### status 差異 - **登入 Shell:** ```bash $ shopt ... login_shell on ... ``` - **非登入 Shell:** ```bash $ shopt ... login_shell off ... ``` - ### cmd 差異 - **登入 Shell:** ```bash $ echo $0 -bash $ ps -p $$ -o args= -bash ``` ``` $ p() { basename "$(pwd)"; } -bash: syntax error near unexpected token `(' ``` - **非登入 Shell:** ```bash $ bash $ echo $0 bash $ ps -p $$ -o args= bash ``` ``` $ p() { basename "$(pwd)"; } $ p ``` <br> <hr> <br> ## 入門 - [Shell Scripting for Beginners – How to Write Bash Scripts in Linux](https://www.freecodecamp.org/news/shell-scripting-crash-course-how-to-write-bash-scripts-in-linux/) <br> <hr> <br> ## 特殊變數 ### 教學資料 - ### [Shell特殊变量:Shell $0, $#, $*, $@, $?, $$和命令行参数](http://c.biancheng.net/cpp/view/2739.html) | 变量 | 含义 | |----|----| | `$0` | 当前脚本的文件名 | | `$n` | 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。 | | `$#` | 传递给脚本或函数的参数个数。 | | `$*` | 传递给脚本或函数的所有参数。 | | `$@` | 传递给脚本或函数的所有参数。被双引号(" ")包含时,与 $* 稍有不同,下面将会讲到。 | | `$?` | 上个命令的退出状态,或函数的返回值。 | | `$$` | 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。 | - :warning: **`$*` 和 `$@` 的区别** - `$*` 把所有的參數,打包成 1 個,可以說是 all-in-one - ### [Bash wait Command with Examples](https://phoenixnap.com/kb/bash-wait-command) | 变量 | 含义 | |----|----| | `&` | ampersand sign (and 符號), indicats a background job | | `$!` | 最後背景程序的 PID | | `$?` | 最後程序的離開狀態 | ![](https://i.imgur.com/aANn59Z.png) - 比較 `$$` 和 `$!` - `$$` 是當前 SHELL 程序的 PID - `$!` 是最後一個背景程序的 PID - ### [操作 `${@}`](https://stackoverflow.com/questions/1537673) 1. open new file and edit it: vim r.sh: ``` echo "params only 2 : ${@:2:1}" echo "params 2 and 3 : ${@:2:2}" echo "params all from 2: ${@:2:99}" echo "params all from 2: ${@:2}" ``` 2. run it: ``` $ chmod u+x r.sh $ ./r.sh 1 2 3 4 5 6 7 8 9 10 ``` 3. the result is: ``` params only 2 : 2 params 2 and 3 : 2 3 params all from 2: 2 3 4 5 6 7 8 9 10 params all from 2: 2 3 4 5 6 7 8 9 1 ``` ### 實作範例 - `myshell.sh` ```sh echo "Hello, myshell!" echo '----------------------' echo "\$@: $@" echo "\$*: $*" echo "\$#: $#" echo "\$0: $0" echo "\$1: $1" echo "\$2: $2" echo "\$3: $3" echo "\$4: $4" echo "\$5: $5" echo "\$?: $?" echo "\$$: $$" echo '----------------------' echo "\${1}: ${1}" echo '${1:+"$@"}:' ${1:+"$@"} echo '${1:+"$*"}:' ${1:+"$*"} echo '${1:+"$#"}:' ${1:+"$#"} echo '----------------------' echo 'for var in $@' for var in $@ do echo "$var" done echo echo 'for var in '"'"'$@'"'"'' for var in '$@' do echo "$var" done echo echo 'for var in "$@"' for var in "$@" do echo "$var" done echo '----------------------' echo 'for var in $*' for var in $* do echo "$var" done echo echo 'for var in '"'"'$*'"'"'' for var in '$*' do echo "$var" done echo echo 'for var in "$*"' for var in "$*" do echo "$var" done ``` - ### 測試1:`sh myshell.sh aa 'bb \" cc' "dd \" ee" ff` ``` Hello, myshell! ---------------------- $@: aa bb \" cc dd " ee ff $*: aa bb \" cc dd " ee ff $#: 4 $0: myshell.sh $1: aa $2: bb \" cc $3: dd " ee $4: ff $5: $?: 0 $$: 3374206 ---------------------- ${1}: aa ${1:+"$@"}: aa bb \" cc dd " ee ff ${1:+"$*"}: aa bb \" cc dd " ee ff ${1:+"$#"}: 4 ---------------------- for var in $@ <--- 展開不正確 aa bb \" cc dd " ee ff for var in '$@' $@ for var in "$@" aa bb \" cc dd " ee ff ---------------------- for var in $* <--- 展開不正確 aa bb \" cc dd " ee ff for var in '$*' $* for var in "$*" aa bb \" cc dd " ee ff ``` - :warning: `'$@'` `'$*'` 是純字串 - :warning: `$@` `$*` 正確用法應該是要加雙引號 - :warning: `"$*"` 用途不明 - :warning: 底下有沒有加 `: (colon)`,目前測起來沒有差別 ```sh echo '${1+"$@"}:' ${1+"$@"} echo '${1+"$*"}:' ${1+"$*"} echo '${1+"$#"}:' ${1+"$#"} ``` - ### 測試2:`sh myshell.sh` ``` Hello, myshell! ---------------------- $@: $*: $#: 0 $0: myshell.sh $1: $2: $3: $4: $5: $?: 0 $$: 3374612 ---------------------- ${1}: ${1:+"$@"}: ${1:+"$*"}: ${1:+"$#"}: ---------------------- for var in $@ for var in '$@' $@ for var in "$@" ---------------------- for var in $* for var in '$*' $* for var in "$*" ``` - ### 測試3:透過 `bash -c ...` 測試 `$@` 和 `$*` 之間的差異 [![](https://i.imgur.com/rC7S76P.png)](https://i.imgur.com/rC7S76P.png) [![](https://i.imgur.com/Upwppe3.png)](https://i.imgur.com/Upwppe3.png) - ### 參考資料 - [How to escape single quotes within single quoted strings](https://stackoverflow.com/questions/1250079) - [${1:+"$@"} in /bin/sh](https://stackoverflow.com/questions/154625/) - If `$1` exists and is not an empty string, then substitute the quoted list of arguments. 如果 `$1` 存在且不是空字串,則替換成引用的參數清單 - [How to iterate over arguments in a Bash script](https://stackoverflow.com/questions/255898) - [Add arguments to 'bash -c'](https://unix.stackexchange.com/questions/144514/) <br> <hr> <br> ## 重導參數研究 (forward parameters) > #dockerfile, forward parameters, forward arguments, ### 實作1 - ### `Dockerfile` ```dockerfile= FROM ubuntu:18.04 COPY preshell.sh /usr/local/bin/preshell RUN chmod +x /usr/local/bin/preshell ENTRYPOINT ["preshell"] COPY myshell.sh /usr/local/bin/myshell RUN chmod +x /usr/local/bin/myshell CMD ["bash"] ``` - ### `preshell.sh` ```bash #!/bin/bash echo 'raw info:' echo '---------------------------------' echo "command: $@" echo "\$0: $0" echo "\$1: $1" echo "\$2: $2" echo "\$3: $3" echo "\$4: $4" echo "\$5: $5" echo "(more, total=$#)" echo # get the user's script name script=$1 # skip the script name, then get the remaining arguments # i.e. preshell.sh echo a b c # => preshell.sh a b c (where echo is dropped) shift # escape chars for \ and " (where \ goes first) args="" for arg in "$@" do # replace all occurrences, use ${parameter//pattern/string}: arg=${arg//\\/\\\\} arg=${arg//\"/\\\"} args="$args \"$arg\"" done # pattern: # sh -c '$0 "$@"' $script $arg1 $arg2 $arg3 ... # e.g. # sh -c '$0 "$@"' echo aa 'bb' "cc" 'dd ee' "ff gg" 'hh'"'"'"ii' "jj'\"kk" # sh -c '$0 "$@"' echo -e "WX\bYZ\tZ\nA" # where: # - ' in 'hh'"ii' string needs to be escaped as '""' # - skip_arg0 does nothing # - if you use $0 as script name, you will get preshell.sh instead of $script # cmd="sh -c '$script "'"$@"'"' skip_arg0 $args" echo "\$script: $script" echo "\$args: $args" echo "\$cmd: $cmd" echo ">>>>>>>>>>>" echo "\$cmd: $cmd" eval $cmd echo "<<<<<<<<<<<" echo "exiting" exit ``` - `$@` 就是指 args (argument list),不包含 $0 ![](https://i.imgur.com/jXLPkl6.png) - ### `myshell.sh` ```sh #!/bin/sh echo "Hello, myshell!" echo '----------------------' echo "\$@: $@" echo "\$*: $*" echo "\$#: $#" echo "\$0: $0" echo "\$1: $1" echo "\$2: $2" echo "\$3: $3" echo "\$4: $4" echo "\$5: $5 (more)" echo "\$#: $#" echo "\$?: $?" echo "\$$: $$" echo '----------------------' echo "\${1}: ${1}" echo '${1:+"$@"}:' ${1:+"$@"} echo '${1:+"$*"}:' ${1:+"$*"} echo '${1:+"$#"}:' ${1:+"$#"} echo '${1+"$@"}:' ${1+"$@"} echo '${1+"$*"}:' ${1+"$*"} echo '${1+"$#"}:' ${1+"$#"} echo '----------------------' echo 'for var in "$@"' for var in "$@" do echo "$var" done echo '----------------------' echo 'for var in "$*"' for var in "$*" do echo "$var" done ``` - ### 操作流程 ``` $ docker build -t ubuntu-test:18.04 ./ ``` ``` $ docker run --rm -it ubuntu-test:18.04 ls -ls ``` ``` $ docker run --rm -it ubuntu-test:18.04 myshell aa 'bb' "cc" 'dd ee' "ff gg" 'hh'" ' "' " ii' "jj ' \" kk" ``` - ### debug 專用 `tmp.sh` ```bash #!/bin/bash script=echo echo $@ args="" for arg in "$@" do # replace all occurrences, use ${parameter//pattern/string}: arg=${arg//\\/\\\\} arg=${arg//\"/\\\"} args="$args \"$arg\"" done echo $args cmd="sh -c '$script "'"$@"'"' bash $args" echo $cmd eval $cmd ``` 執行方式:`$ bash tmp.sh a b c` 只能使用 bash,因為有使用到 find-replace 用法 (sh 無提供) ### 參考資料 - [Add arguments to 'bash -c'](https://unix.stackexchange.com/questions/144514/) :+1: :+1: :100: ``` /bin/bash -c 'echo "$0" "$1"' foo bar /bin/bash -c 'echo "$@"' bash foo bar /usr/bin/env -- "ls" "-l" ``` 執行結果 ``` foo bar foo bar (ls -l 略) ``` 若加入 `shift` 指令,則: ``` /bin/bash -c 'shift; echo "$0" "$1"' foo bar /bin/bash -c 'shift; echo "$@"' bash foo bar ``` 執行結果 ``` foo bar ``` - [Joining bash arguments into single string with spaces](https://unix.stackexchange.com/questions/197792) ### 實作2 (更簡單) - [[官方][Docker] ENTRYPOINT](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#entrypoint) ``` #!/bin/bash set -e if [ "$1" = 'postgres' ]; then chown -R postgres "$PGDATA" if [ -z "$(ls -A "$PGDATA")" ]; then gosu postgres initdb fi exec gosu postgres "$@" fi exec "$@" ``` <br> <hr> <br> ## 變數範圍 - [Set a parent shell's variable from a subshell](https://stackoverflow.com/questions/15541321/set-a-parent-shells-variable-from-a-subshell) ```bash a=3 (a=4) echo $a #=3 ``` ``` a=3 { a=4;} echo $a #=4 ``` - [how to export VARs from a subshell to a parent shell?](https://serverfault.com/questions/37796/how-to-export-vars-from-a-subshell-to-a-parent-shell) <br> <hr> <br> ## 方法參數 ### file exist or not exist ```bash= if [ -e filename ]; then echo 'exist'; else echo 'not exist'; fi # 等效於 if test -e filename; then echo 'exist'; else echo 'not exist'; fi ``` ```bash= if [ -f azure-parabricks-test_key.pem ]; then echo 'exist' else echo 'NOT exist' fi ``` ```bash= if [ -z azure-parabricks-test_key.pem ]; then echo 'NOT exist' else echo 'exist' fi ``` - [How to Check if a File or Directory Exists in Bash](https://linuxize.com/post/bash-check-if-file-exists/) :+1: :100: - [What does "if [ -e $name ]" mean? Where $name is a path to a directory](https://unix.stackexchange.com/questions/127743/) <br> ### variable exist or not exist - unset, set (empty, non-rmpty)? - `${var+x}`: 當 `var` 有任何指派(=)動作,就會有 `x`(字串) - `${var:+x}`: 當 `var` 有任何指派(=)動作 & 長度大於0,就會有 `x`(字串) - `[ -z $var ]`: 當 `var` 有任何指派(=)動作 & trim()後長度大於0,就會是 false,其餘為 true `[ -z ${var} ] && echo echo unset_or_empty || echo not_empty` - `$ if __condition__; then echo "unset [${p}][${p+HOME}][${p:+HOME}]"; else echo "set [$p][${p+HOME}][${p:+HOME}]"; fi` - 條件測試:`-z ${p}` | condition | -z -> true | -z -> false | | --------- | ---------------- | ----------- | | unset p | unset [][][] | | | p= | unset [][HOME][] | | | p='' | unset [][HOME][] | | | p="" | unset [][HOME][] | | | p=' ' | unset [ ][HOME][HOME] | | | p=" " | unset [ ][HOME][HOME] | | | p="param" | | set [param][HOME][HOME] | - 條件測試:`-z ${p+x}` | condition | -z -> true | -z -> false | | --------- | ---------------- | ----------- | | unset p | unset [][][] | | | p= | | set [][HOME][] | | p='' | | set [][HOME][] | | p="" | | set [][HOME][] | | p=' ' | | set [ ][HOME][HOME] | | p=" " | | set [ ][HOME][HOME] | | p="param" | | set [param][HOME][HOME] | - 條件測試:`-z ${p:+x}` | condition | -z -> true | -z -> false | | --------- | ---------------- | ----------- | | unset p | unset [][][] | | | p= | unset [][HOME][] | | | p='' | unset [][HOME][] | | | p="" | unset [][HOME][] | | | p=' ' | | set [ ][HOME][HOME] | | p=" " | | set [ ][HOME][HOME] | | p="param" | | set [param][HOME][HOME] | - [How to check if a variable is set in Bash](https://stackoverflow.com/questions/3601515) 正確方式 ``` if [ -z ${var+x} ]; then echo "var is unset"; else echo "var is set to '$var'"; fi ``` 錯誤方式 ``` if [ -z "$var" ]; then echo "var is blank"; else echo "var is set to '$var'"; fi ``` - "$var" 無法區分:[1]變數未被設定 [2]變數是空值 > it doesn't distinguish between a variable that is unset and a variable that is set to the empty string. - [2.6.2 Parameter Expansion](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02) - [How To Bash Shell Find Out If a Variable Is Empty Or Not](https://www.cyberciti.biz/faq/unix-linux-bash-script-check-if-variable-is-empty/) > #if-else, 三元運算子 ``` [[ ! -z "$var" ]] && echo "Not empty" || echo "Empty" ``` - [Linux Shell指令碼攻略:shell中各種括號()、(())、[]、[[]]、{}的作用](https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/544464/) - 特殊替換 `${var:-string},${var: string},${var:=string},${var:?string}` - 模式匹配替換 `${var%pattern},${var%%pattern},${var#pattern},${var##pattern}` - 字串提取和替換 `${var:num},${var:num1:num2},${var/pattern/pattern},${var//pattern/pattern}` ### 教學 - ### [linux 下shell中if的“-e,-d,-f”是什么意思](https://blog.csdn.net/superbfly/article/details/49274889) 文件表达式 `-e filename` 如果 filename存在,则为真 `-d filename` 如果 filename为目录,则为真 `-f filename` 如果 filename为常规文件,则为真 `-L filename` 如果 filename为符号链接,则为真 `-r filename` 如果 filename可读,则为真 `-w filename` 如果 filename可写,则为真 `-x filename` 如果 filename可执行,则为真 `-s filename` 如果文件长度不为0,则为真 `-h filename` 如果文件是软链接,则为真 `filename1 -nt filename2` 如果 filename1比 filename2新,则为真。 `filename1 -ot filename2` 如果 filename1比 filename2旧,则为真。 <br> <hr> <br> ## 語法 ### if-then-else-fi ```sh= h=/home/tj if [ $h = "/home/tj" ]; then echo 'exist' else echo 'not exist' fi ``` - 如果沒有 ';' 會有 error `tmp.sh: 5: tmp.sh: Syntax error: "else" unexpected (expecting "then")` 亦可改成:`then` 切到下一行 ```sh= h=/home/tj if [ $h = "/home/tj" ] then echo 'exist' else echo 'not exist' fi ``` ```sh= h=/home/tj if [ $h = "/home/tj" ]; then echo 'exist' else echo 'not exist' fi ``` - [Shell test命令(Shell [])详解,附带所有选项及说明](http://c.biancheng.net/view/2742.html) <br> ### if-statement | 條件 | 說明 | 範例結果 | |--------------------|----|---------| | `-n "$VAR"` | 判斷 `$VAR` 是否「非空」 | `"abc"` → ✅;`""` → ❌ | | `-z "$VAR"` | 判斷 `$VAR` 是否「為空」 | `""` → ✅;`"abc"` → ❌ | | `"$VAR" = "value"` | 判斷是否等於特定值 | `"abc" = "abc"` → ✅ | | 符號 | 含義 | 推測來源 | |------|-----------------------|------------------------| | `-n` | **non-zero length** | `n` = nonempty 或 non-null | | `-z` | **zero length** | `z` = zero-length | ```bash= # 判斷是否有傳 CMD 參數 if [ -n "$(CMD)" ]; then echo "你有傳 CMD=$(CMD)" else echo "你沒有傳 CMD" fi ``` <br> <br> <hr> <br> ### bash vs sh - [运行shell脚本时报错"\[\[ : not found"解决方法](https://www.cnblogs.com/han-1034683568/p/7211392.html) - bash与sh是有区别的,两者是不同的命令,且bash是sh的增强版,而"[[]]"是bash脚本中的命令,因此在执行时,使用sh命令会报错,将sh替换为bash命令即可: <br> <hr> <br> ## 特殊語法 ### 槽狀用法 `'basename $(pwd)'` ```bash= alias dirname='basename `pwd`' # case1 alias dirname='basename $(pwd)' # case2 alias dirname="basename `pwd`" # case3 alias dirname="basename $(pwd)" # case4 ``` ```bash= # case5 dirname() { basename $(pwd) } ``` - **雙引號**:命令替換在**定義別名時**發生。 - **單引號**:命令替換*不會**在定義或執行別名時發生。 - **建議**:使用函數比別名更靈活,特別是當涉及到命令替換和參數時。 <br> ### 數值計算 - 操作範例 ```bash # 透過 $((運算式)) $ echo $(((1+2)*3)) 9 $ echo $((5/3)) 1 ``` ```bash $ s=1 $ e=12 $ echo $(( e - s )) 11 $ echo $(( ( e - s ) / 10 )) 1 $ echo $(( ( e - s ) / 5 )) 2 ``` - 時間戳記 timestamp ```baash date && start_time=`date +%s` && echo "start_time:" $start_time sleep 12 # do something date && end_time=`date +%s` && echo "end_time:" $end_time total=$((end_time - start_time)) time_h=$((total/3600)) time_m=$(( (total - time_h*3600) / 60 )) time_s=$(( total - time_h*3600 - time_m*60 )) echo "Total time: ${time_h}h ${time_m}m ${time_s}s" ``` <br> <hr> <br> ## 回傳值 ### Unix 訊號 - [[wiki] Unix訊號](https://zh.wikipedia.org/wiki/Unix%E4%BF%A1%E5%8F%B7) ### exited with code ### - [Question: My batch job exited with code ###. What does that mean?](http://www.bu.edu/tech/files/text/batchcode.txt) :+1: > 資料來源:[C 語言為什麼是 int main() { ... }](https://blog.moli.rocks/2016/12/15/why-should-main-return-in-c/) - the system sets the exit code to 128 + \<signal number\> - 137: 128 + SIGKILL(9) - [[FreeBSD] preferable exit codes for programs](https://www.freebsd.org/cgi/man.cgi?query=sysexits&apropos=0&sektion=0&manpath=FreeBSD+4.3-RELEASE&format=html) <br> ### 11 (SIGSEGV) - Parabricks / pbrun germline: Received signal: 11 ![](https://i.imgur.com/7EntlNW.png) <br> ### 137 - **[Docker-compose exit code is 137 when there is no OOM exception](https://stackoverflow.com/questions/59296801/)** - According to this post, the exit code of 137 can be due to two main issues. - The container received a docker stop and the app is not gracefully handling SIGTERM - The container has run out of memory (OOM). - **[What causes a container to exit with code 137?](https://janetpanic.com/what-causes-a-container-to-exit-with-code-137/)** - What causes error 137 in Docker > Error 137 in Docker denotes that the container was ‘KILL’ed by ‘oom-killer’ (Out of Memory). This happens when there isn’t enough memory in the container for running the process. - How to resolve error 137 in Docker > So the first option is to identify the cause of this memory usage. - **[Process finished with exit code 137 in PyCharm](https://stackoverflow.com/questions/43268156)** - Exit code 137 means that your process was killed by (signal 9) `SIGKILL` <br> <hr> <br> ## 指令串接 ## 指令串接/討論 ## `;` 與 `&&` 的差異與使用時機 - ### gpt-4o (2025/04/16) 在 Makefile 或 shell 腳本中,`;` 與 `&&` 都可用來串接多個指令,但它們的行為略有不同。 --- ### 🧠 差異比較 | 比較項目 | `;` (semicolon) | `&&` (and operator) | |----------|------------------|----------------------| | 執行順序 | 逐行執行,不管前面成功或失敗 | **僅在前一行成功時才執行下一行** | | 錯誤處理 | 不管 exit code,後面仍執行 | 若前一行失敗(exit code ≠ 0),**中止** | | 常見用途 | 簡單串接,不關心錯誤 | 嚴謹控制流程、防止錯誤後續執行 | --- ### 🔍 範例比較 **使用 `;`(不管前一行成敗)** ```bash echo hello; false; echo world ``` 輸出: ``` hello world ``` 即使 `false` 會回傳錯誤(exit code 1),`echo world` 還是執行了。 --- **使用 `&&`(前一行成功才繼續)** ```bash echo hello && false && echo world ``` 輸出: ``` hello ``` 這裡 `false` 失敗了,導致 `echo world` 不會執行。 --- ### ✅ 在 Makefile 中的應用 你原本的程式碼如下: ```makefile run-app: export RUN_MODE=$(RUN_MODE); \ export SERVER_URL=$(SERVER_URL); \ export SHOW_SWAGGER=$(SHOW_SWAGGER); \ export SERVER_PORT=$(SERVER_PORT); \ $(IMAGE_DIR)/entrypoint.sh ``` 因為你只是要「設定一連串環境變數後,執行一個腳本」,用 `;` 是可以接受的,也比較常見。 但如果你希望當某個 `export` 失敗就中止,那可以改用 `&&`: ```makefile run-app: export RUN_MODE=$(RUN_MODE) && \ export SERVER_URL=$(SERVER_URL) && \ export SHOW_SWAGGER=$(SHOW_SWAGGER) && \ export SERVER_PORT=$(SERVER_PORT) && \ $(IMAGE_DIR)/entrypoint.sh ``` 不過實際上 `export` 幾乎不會失敗(除非語法錯),所以 `;` 就夠用了。 --- ### ✅ 若想更嚴謹控制流程(可選) 可以改用 `&&`,讓任一 export 失敗時就終止流程: ```makefile run-app: export RUN_MODE=$(RUN_MODE) && \ export SERVER_URL=$(SERVER_URL) && \ export SHOW_SWAGGER=$(SHOW_SWAGGER) && \ export SERVER_PORT=$(SERVER_PORT) && \ $(IMAGE_DIR)/entrypoint.sh ``` --- ### ✅ 更簡潔寫法(推薦) 直接將變數寫在同一行執行: ```makefile run-app: RUN_MODE=$(RUN_MODE) SERVER_URL=$(SERVER_URL) \ SHOW_SWAGGER=$(SHOW_SWAGGER) SERVER_PORT=$(SERVER_PORT) \ $(IMAGE_DIR)/entrypoint.sh ``` 這樣會在 **同一個 shell 環境中執行**,不需逐行 export,簡潔有效。 <br> <hr> <br> ## 特殊操作 ### eval 使用 **單引號** 或 **雙引號** 皆可 ``` cmd='echo pid=$$' eval $cmd ``` ``` cmd="echo pid=$$" eval $cmd ``` 執行結果 ``` pid=862639 ``` - [Run a string as a command within a Bash script](https://stackoverflow.com/questions/2355148/) <br> ### shift - [How do I forward parameters to other command in bash script?](https://stackoverflow.com/a/1537687/4359712) `shifttest.sh`: ``` #!/bin/bash echo $1 shift echo $1 $2 ``` `shifttest.sh 1 2 3` produces ``` 1 2 3 ``` - If you forward the arguments as `$1` without quoting them as `"$1"`, then the shell will perform word splitting, so e.g. `foo bar` will be forwarded as `foo` and `bar` separately. - [shell: "can't shift that many" error](https://superuser.com/questions/897148/) > 因為沒有多餘的參數可以讓你 shift ```sh #tmp.sh echo '$@: '"$@" echo "\$#: $#" shift echo '$@: '"$@" ``` 執行結果: ``` $ sh tmp.sh $@: $#: 0 tmp.sh: 3: shift: can't shift that many ``` ``` $ sh tmp.sh 1 $@: 1 $#: 1 $@: ``` ``` $ sh tmp.sh 1 2 3 $@: 1 2 3 $#: 3 $@: 2 3 ``` 解決辦法: ```sh= echo '$@: '"$@" echo "\$#: $#" if [ $# -gt 0 ]; then echo "do shift" shift fi echo '$@: '"$@" ``` <br> ### 字串 string - ### [Replace one substring for another string in shell script](https://stackoverflow.com/questions/13210880) - ### 字串串接 ```bash x=123 y=abc z=$x$y # z=${x}y, z=$x${y}, z=${x}${y} echo $z ``` - [How to concatenate string variables in Bash](https://stackoverflow.com/questions/4181703) - ### 子字串 > `${變數:起始索引:長度}` ```bash x=0123456789 echo '${x:0:3}: '${x:0:3} # 012 echo '${x:7:3}: '${x:7:3} # 789 echo '${x:5:100}: '${x:5:100} # 56789 ``` 去頭 > `#$%` 的 `#` > 語法:`${x` + `#` + `*`(萬用字元) + `尋找字元` + `}` ```bash x=0123456789 echo ${x#*3} # 砍掉 0123,只留 456789 x=0011223344 echo ${x#*3} # 砍掉 0011223 (not greedy),只留 344 echo ${x##*3} # 砍掉 00112233 (greedy),只留 44 ``` 去尾 > `#$%` 的 `%` > 語法:`${x` + `%` + `*`(萬用字元) + `尋找字元` + `}` ```bash x=0123456789 echo ${x%3*} # 砍掉 3456789,只留 012 x=0011223344 echo ${x%3*} # 砍掉 344 (not greedy),只留 0011223 ``` - [Extract substring in Bash](https://stackoverflow.com/questions/428109) - ### 格式化 ```bash x=123 printf "%06d\n" $x printf "%6d\n" $x printf "%-6d\n" $x ``` ![](https://i.imgur.com/6ULWFnE.png) ```bash x=0123456789 printf "%.0s\n" $x printf "%.2s\n" $x # 01 printf "%.5s\n" $x # 01234 ``` - [Linux printf command](https://www.computerhope.com/unix/uprintf.htm) - ### 路徑處理 > 見「子字串」處理 - 取出檔案名稱:`${X##*/}` - `#` 表示從前面開始找 - `#` 第二個`#`表示貪婪 - `*` 表示從前面開始吃掉任意字元,直到 `/` (最後一個,後面已經沒有 `/`) - 取出當前路徑:`${X%/*}` - `%` 表示從後面開始找 - `*` 表示從後面開始吃掉任意字元,直到 `/` (後面數來第一個,不貪婪) - 完整範例 ```bash X=/home/tj/Downloads/docker/Dockerfile echo "檔案名稱:${X##*/}" echo "當前路徑:${X%/*}" echo "當前路徑:${0}" ``` 執行結果: ``` 檔案名稱:Dockerfile 當前路徑:/home/tj/Downloads/docker 當前路徑:bash ``` - 參考資料 - [bash - how to find current shell command](https://stackoverflow.com/a/5077898/4359712) - ### 字串比較 ``` if [ $h = "/home/tj" ]; then echo "case1:$h" else echo "case2" fi ``` - 會有 error `tmp.sh: 3: [: =: unexpected operator` - 解決方法:要加雙引號 ``` if [ "$h" = "/home/tj" ]; then echo "case1:$h" else echo "case2" fi ``` - [shell腳本報錯:"[: =: unary operator expected"](https://www.796t.com/content/1507882933.html) - 究其原因,是因為如果變量STATUS值為空,那麽就成了 [ = "OK"] ,顯然 [ 和 "OK" 不相等並且缺少了 [ 符號,所以報了這樣的錯誤。 - 其他參考資料 - [Compare a string using sh shell]() <br> <hr> <br> ## 注意事項 ### 傳遞環境變數 傳遞環境變數,需以雙引號傳遞,不能用單引號 ```sh $ python test.py --PATH='$HOME' <--- 失敗 ['test.py', '--PATH=$HOME'] $ python test.py --PATH='${HOME}' <--- 失敗 ['test.py', '--PATH=${HOME}'] $ python test.py --PATH="$HOME" ['test.py', '--PATH=/home/tj'] $ python test.py --PATH="${HOME}" ['test.py', '--PATH=/home/tj'] ``` <br> <hr> <br> ## 完整教學 - [[ITREAD] Shell 教程](https://www.itread01.com/study/linux-shell.html) - [[鳥哥] 第十二章、學習 Shell Scripts](https://linux.vbird.org/linux_basic/centos7/0340bashshell-scripts.php) <br> <hr> <br> # Linux: Makefile ## 變數範圍 - [Makefile 語法簡介](https://sites.google.com/site/mymakefile/makefile-yu-fa-jian-jie) :+1: :100: - [How to abort makefile if variable not set?](https://stackoverflow.com/questions/10858261/) ## API - [Append date and time to an environment variable in linux makefile](https://stackoverflow.com/questions/1859113) ```makefile $(shell operation) ``` ```makefile= LOGPATH = logs LOGFILE = $(LOGPATH)/$(shell date --iso=seconds) test_logfile: echo $(LOGFILE) sleep 2s echo $(LOGFILE) ``` <br> {%hackmd vaaMgNRPS4KGJDSFG0ZE0w %}