shell 程式設計簡記

凪风sama發表於2024-06-25

1. 環境變數

環境變數是指作業系統中記錄一些配置資訊的變數,這些變數在不同的程式之間共享,可以被作業系統或者 shell 指令碼讀取和修改。

環境變數也可以類比為各個語言中的全域性變數,其作用域是全域性的,所有的程式碼段或者說作用域都可以直接訪問到這個變數。

1.1 檢視環境變數

檢視你環境變數的命令是 printenvenv

env # 檢視全部環境變數,只有這一種方式
printenv # 檢視全部環境變數
printenv [變數名] # 檢視指定環境變數的值

比如

SHELL=/bin/bash
ROS_VERSION=2
SESSION_MANAGER=local/ruby:@/tmp/.ICE-unix/1804,unix/ruby:/tmp/.ICE-unix/1804
QT_ACCESSIBILITY=1
COLORTERM=truecolor
XDG_CONFIG_DIRS=/etc/xdg/xdg-ubuntu:/etc/xdg
XDG_MENU_PREFIX=gnome-
GNOME_DESKTOP_SESSION_ID=this-is-deprecated
GTK_IM_MODULE=fcitx
PKG_CONFIG_PATH=:/usr/local/lib/pkgconfig
ROS_PYTHON_VERSION=3
LANGUAGE=zh_CN:zh:en_US:en
QT4_IM_MODULE=fcitx
MANDATORY_PATH=/usr/share/gconf/ubuntu.mandatory.path
LC_ADDRESS=zh_CN.UTF-8
GNOME_SHELL_SESSION_MODE=ubuntu
LC_NAME=zh_CN.UTF-8
SSH_AUTH_SOCK=/run/user/1000/keyring/ssh
XMODIFIERS=@im=fcitx
DESKTOP_SESSION=ubuntu
PATH=/opt/ros/foxy/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/local/cuda-11.8/bin

上面就是使用 printenv 命令檢視到的環境變數。可以看出其中指定了很多系統重要的變數,如SHELL表明了bash所在的路徑,因此我們可以直接訪問到bash而不用指定路徑。而DESKTOP_SESSION則表明當前的發行版是Ubuntu。

1.2 設定環境變數

設定環境變數的命令是 export
在設定環境變數前,先明確shell中的變數如何書寫。

ENV=value # 等號左邊為變數名,右邊為變數值

這樣就在shell中宣告瞭一個變數。注意在一個shell中宣告的變數,只對當前shell有效,不會影響其他shell。此時該變數只是普通的臨時變數,不會被儲存到環境變數中。

檢視shell中的變數可以使用 echo 命令,echo可以將變數或者常量的值輸出到終端。

echo "test echo"
test echo

因此也可以用來檢視變數的值。

echo $ENV # 檢視變數值
# 注意變數前要加$來取出其值

使用export來將變數匯出為環境變數

export ENV # 將變數匯出為環境變數

# 或者直接在export命令中指定變數名和值
export ENV=value

這樣就可以在其他程式中使用該變數了。已經匯出的環境變數可以直接全域性賦值更改

ENV=new_value # 全域性變數賦值

但是需要注意的是,對於使用命令列export匯出的環境變數僅對該次會話生效,當關閉終端或者重新登入後,環境變數就會失效。如果想永久生效,需要修改系統配置檔案。

1.3 修改配置檔案來設定環境變數

在linux系統啟動時,會自動按序讀取一些配置檔案並從中執行相應的shell命令。因此,我們只需要在這些配置檔案中新增export命令,即可設定永久的環境變數。

在ubuntu(不同發行版有所不同)下,比較常用的有如下幾個配置檔案:

  • /etc/profile.d目錄下的.sh檔案和~/.bashrc 檔案 對使用者的shell進行初始化和環境變數設定
  • ./.bash_profile.d目錄下的檔案和./bash_login 檔案用於個別使用者的shell環境初始化 使用者登入時會讀取內容

因此我們可以在這些檔案中新增export命令來設定環境變數。

這裡在選擇在/etc/profile.d/目錄下新建一個檔案,叫做source_env.sh,並新增如下內容:

image

然後儲存退出,執行

source /etc/profile.d/source_env.sh
# 這樣可以直接生效,不需要重啟系統

接著使用env或者printenv命令檢視環境變數,就可以看到我們剛才設定的環境變數。

image
image

可以看到環境變數MYTEST已經被設定成功,並且可以被其他程式使用。

推薦使用新建檔案的方式來新增自己的環境變數,這樣可以避免對系統的其他部分造成影響,更加美觀以及方便管理。

1.4 常用環境變數

對於比較常用的環境變數,這裡給出一個,PATH環境變數

我們使用printenv檢視PATH環境變數的值

printenv PATH
/usr/local/bin:/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/local/cuda-11.8/bin

可以看到其中是一串以冒號分隔的目錄路徑,這些目錄包含了一些系統命令的可執行檔案路徑,如lscd等常用命令,正是由於這些路徑被記錄在PATH環境變數中,我們才可以直接在命令列中使用這些命令。

可以使用witch + 空格 + 命令名 來檢視某個命令的具體路徑

which ls
/bin/ls

可以看到ls命令的具體路徑是/bin/ls,而這個路徑正是PATH環境變數中第一個目錄/usr/bin中的一個。

因此在使用一些命令時報出command not found錯誤,可能是因為該命令的路徑沒有被新增到PATH環境變數中。可以使用locate命令來搜尋帶有某個關鍵字的檔案路徑,然後手動新增到PATH環境變數中。

locate ls # locate會搜尋系統中所有檔名包含ls字串的檔案路徑
/bin/ls 

然後將該路徑新增到PATH環境變數中即可。

1.5 命令的別名

在shell中可以使用alias(翻譯過來就是別名)來檢視當前目錄下所有命令的別名,也可以設定新的別名。

檢視當前shell下所有命令的別名

alias

設定新的別名

alias 別名='原命令'

比如

alias ll='ls -l' # 設定ll命令的別名為ls -l
alias lll = 'ls -alh' # 設定lll命令的別名為ls -alh

這樣就可以使用新的別名來代替原命令,如ll命令等價於ls -llll命令等價於ls -alh

但是注意,設定的別名只對當前shell有效,不會影響其他shell。依舊可以將其加入到配置檔案中,讓其永久生效。

若要移除別名,可以使用unalias命令。

unalias 別名

比如

unalias ll

這樣ll命令的別名就被移除了。
若想移除該shell下所有別名,可以使用unalias -a命令。

unalias -a

當然,unalias命令僅在該shell下有效,不會影響其他shell。

2. 輸入與輸出

linux最核心的思想就是一切皆檔案,因此輸入輸出的控制也是由對應的檔案來實現的。

2.1 終端的標準輸入輸出

在linux中,我們使用終端(terminal)來與計算機進行互動。終端的輸入輸出可以分為兩種:

  • 標準輸入輸出(stdin/stdout):stdin(標準輸入)是指鍵盤輸入,stdout(標準輸出)是指終端輸出。
  • 錯誤輸出(stderr):stderr(標準錯誤輸出)是指終端輸出錯誤資訊。

開啟一個終端,執行如下命令:

ls  -l /dev/{stdin,stdout,stderr}

lrwxrwxrwx 1 root root 15 6月  19  2024 /dev/stderr -> /proc/self/fd/2
lrwxrwxrwx 1 root root 15 6月  19  2024 /dev/stdin -> /proc/self/fd/0
lrwxrwxrwx 1 root root 15 6月  19  2024 /dev/stdout -> /proc/self/fd/1

可以看到在該終端下的stdin,stdout,stderr軟連結到了/proc/self/fd/0,1,2三個檔案上。而繼續執行如下命令:

ls -l /proc/self/fd
lrwxrwxrwx 1 root root 0 6月  19  2024 /proc/self -> 10810

可以看到/proc/self目錄下有一個軟連結,指向當前程序的程序號。因此這樣就可以知道當前程序的標準輸入輸出檔案號。

2.2 管道

linux中的管道(pipe)是實現程序間通訊的一種方式,它允許將一個程序的輸出作為另一個程序的輸入。

其語法如下:

命令1 | 命令2

其中,命令1的輸出會作為命令2的輸入。

以上一節的例子為例,我們可以編寫一個簡單的shell來嘗試管道的功能

# in pipe.sh
echo "該程序的程序號為$$"   # echo $$ 輸出當前終端的程序號
# in get.sh
read line # read + 變數名 可以從標準輸入讀取內容並賦值給變數
echo "讀取到的行是:$line"

編寫完這兩個shell檔案後賦予執行許可權

chmod +x pipe.sh get.sh

然後執行

./pipe.sh |./get.sh

可以看到終端輸出

該程序的程序號為10810

在使用管道符時,linux會隱式的建立一個臨時的管道檔案用來儲存命令的輸出,此時標準輸出會軟鏈到管道檔案上,而標準輸入則會軟鏈到管道檔案中讀取內容。

當然,我們也可以顯式的建立管道檔案,稱為具名管道

mkfifo  管道名 # 建立一個管道檔案

這個管道檔案是程序間通用的,可以被多個程序共享來讀取。

可以使用cat命令或者echo >> 來讀取和寫入管道檔案。

2.3 檔案重定向

我們再理清2.1節中的一些概念。

  • 對於每個終端,會有屬於自己的程序號,並將其的輸入輸出以及錯誤輸出檔案號記錄在/proc/self目錄下。
  • 而對於每個程序,其輸入輸出以及錯誤輸出檔案號被分配為0, 1, 2三個檔案。
  • 在不加更改的情況下,0, 1, 2都會軟鏈到/dev/stdin, /dev/stdout, /dev/stderr三個檔案上,來實現終端上的。

而linux中檔案的重定向可以理解為可以不將0, 1, 2描述符連結到標準的輸入輸出{stdin,stdout,stderr}三兄弟,而是將其連線到其他位置上。

linux中的重定向簡單的分為種

  • 輸出重定向,包含輸出和錯誤輸出
  • 輸入重定向

2.3.1 輸出重定向

輸出重定向分為兩種:

  • 覆蓋輸出(覆蓋掉原來的內容)
  • 追加輸出(在原來的內容後面追加)
命令 > 檔名  # 覆蓋輸出
命令 >> 檔名 # 追加輸出

比如123.txt檔案內容為123

cat 123.txt > 456.txt
cat 456.txt
123
cat 123.txt > 456.txt
cat 456.txt
123
cat 123.txt >> 456.txt
cat 456.txt
123
123

cat命令可以讀取檔案內容,並將其標準輸出到終端,此時檔案符1軟鏈至stdout

1->stdout

但是此時我們使用重定向符號,改變了檔案符1的定向,在執行時相當於

1->456.txt

執行後檔案符1重新軟鏈到stdout。這就是輸出重定向。

2.3.X 錯誤輸出重定向

有時我們並不期望命令或可執行檔案的標準輸出,而是希望檢測其執行狀態,獲取其退出時的錯誤輸出,因此需要將錯誤輸出重定向到檔案。

錯誤輸出重定向的語法如下:

命令 2> 檔名
命令 2>> 檔名 # 追加錯誤輸出
# 也就是在標準輸出符後面加上2,表示將錯誤輸出重定向到檔案

同時shell支援將錯誤輸出與標準輸出一同重定向到同一個檔案,語法如下:

命令 &> 檔名

2.3.2 輸入重定向

輸入重定向也有兩種

  • 覆蓋輸入(覆蓋掉原來的內容)
  • 從標準輸入讀取內容,直到遇到結束符
命令 < 檔名  # 覆蓋輸入
命令 << 結束符 # 從標準輸入讀取內容,直到遇到結束符

給出一個shell檔案 read.sh

read line
echo "讀取到的行是:$line"

前面說過,read命令旨在從終端的標準輸入中讀取內容,並將其賦值給變數。

賦予read.sh許可權後,執行如下命令

chmod +x read.sh
read.sh < 123.txt
讀取到的行是:123

可以看到,read命令讀取了123.txt檔案的內容,並將其賦值給變數line。

與輸出重定向類似,輸入重定向會改變檔案符0的定向。
在重定向前,檔案符0軟鏈至stdin

0->stdin

在重定向後,檔案符0重新軟鏈至123.txt

0->123.txt

因此,read命令不再從stdin中讀取內容,而是從123.txt檔案中讀取內容。

還有一種用法就是從標準輸入讀取內容直到遇到結束符,並將其作為命令的輸入。

cat << 結束符 # 從標準輸入讀取內容直到遇到結束符

比如

cat << EOF
hello 
1
2
EOF
# 讀取結束
# 輸出到終端
hello 
1
2

藉助此種方法可以有這樣的作用

cat << EOF > 123.txt

首先我們將輸入重定向到直到遇到EOF才結束輸入,然後將輸出重定向到123.txt檔案,這樣就實現了檢測關鍵字結束輸入並將輸入儲存到指定檔案。

錯誤輸出重定向的語法用到的時候再去搜吧。

補充一個管道與輸入重定向的錯誤區分。

# 假設存在a.txt且為空

echo "hello" >> a.txt
# 將echo的輸出重定向到a.txt檔案,並追加到檔案末尾

echo "hello" | a.txt
# 管道運算子允許一個命令的輸出作為另一個命令的輸入,但是a.txt並沒有接受輸入的屬性或語句,因此會報錯

# 可以這樣寫
echo "hello" | cat >> a.txt

最後提一嘴,輸入輸出的重定向指的是將命令的標準輸入輸出重定向到檔案,是命令與檔案間的操作,因此不存在命令間的重定向,想要實現兩個命令間的通訊請使用管道。

2.3.3 命令展開

命令展開是指將命令的輸出作為引數傳遞給其他命令的一種方式。

其中常用的展開有下面幾種

  • 萬用字元展開
ls * # 實際上是將*展開為當前目錄的所有檔名作為引數傳遞給ls命令
ls ~ # 實際上是將~展開為家目錄的絕對路徑作為引數傳遞給ls命令
# echo命令使用*會列印當前目錄的所有檔案,使用~會列印家目錄的絕對路徑
  • 算數表示式展開,$((表示式))
echo $((3+5)) # 輸出8
  • 變數展開,$變數名
name="Ruby"
echo $name # 輸出Ruby
# 變數的訪問實際上也是一種展開
  • 花括號展開,{字元1,字元2,字元3,...}
echo {a,b,c} # 輸出a b c

# 使用花括號+字串
echo alpha_{a,b,c}.txt # 輸出alpha_a.txt alpha_b.txt alpha_c.txt
# 可以這樣使用
touch alpha_{a,b,c}.txt # 建立三個檔案,alpha_a.txt alpha_b.txt alpha_c.txt

# 同時花括號支援範圍展開
echo {1..10} # 輸出1 2 3 4 5 6 7 8 9 10
echo {A..Z}  # 輸出A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
# 實際上是在{字元1..字元2}的範圍內使用Ascill碼進行範圍展開

# 結合這個用法可以批次建立連續檔案
touch 2024-{1..12}.txt
# 建立了2024-1到2024-12的12個檔案
  • 命令展開
echo `ls *`
echo `cat 123.txt`
# or 
echo $(ls *)
echo $(cat 123.txt)
# 這種方法會將命令的輸出展開為字串,因此可以賦值操作
result=$(ls *) #or result=`ls *`
echo $result
  • 引號內展開

在shell中,單引號括起來的字串相當於python中的原始字串,只會原樣輸出引號內的字元,不會進行任何格式化操作。而使用雙引號括起來的字串則會進行變數展開、命令展開、算數表示式展開等操作。

name="Ruby"
echo 'Hello, $name!' # 輸出Hello, $name!
echo "Hello, $name!" # 輸出Hello, Ruby!
echo "`ls /root/a.txt`" 
# or echo $(ls /root/a.txt)
# 輸出/root/a.txt
# 在雙引號中可以使用\對特殊字元進行轉義
echo "Hello, \$name!" # 輸出Hello, $name!

注意,無論單雙引號,都不支援萬用字元以及花括號展開。因此類似於echo "ls *"會輸出ls *而不是列印當前目錄的所有檔案。

3. shell指令碼程式設計

3.1 一個簡單的shell指令碼

如前面所見,shell指令碼的副檔名一般設為.sh,即便不加此副檔名也可以執行,但是約定使用.sh來標識該本件為一個shell指令碼檔案。

幾乎所有在命令列中能執行的shell命令都可以在shell指令碼中使用。

給出一個簡單的shell指令碼,test.sh

指令碼的第一行一般使用固定格式#!bin/bash#!加指令碼直譯器的絕對路徑來指定使用哪個直譯器來執行指令碼。

#!/bin/bash

echo "Hello,World!"

然後賦予執行許可權並執行

chmod +x test.sh
# 這裡有兩種執行shell指令碼的方法

# 1. 直接執行
./test.sh

# 2. 指定直譯器執行
bash test.sh

# 輸出
Hello,World!

第一種方法中,會根據指令碼第一行指定的直譯器來對指令碼進行解釋執行,而第二種方法則是無論指定直譯器為何,都將使用bash直譯器來執行指令碼。

3.2 變數

shell中的變數和python感覺差不多。

變數的定義為

# 變數名=變數值, 如

name="RubyRose" 
# 變數名的規範與大多語言相同
# 注意變數名與變數值間的等於號不能有空格

通常使用全大寫的變數來表示一個常量,如

PI=3.141592653589793

想要使用echo輸出變數值到終端,只需在變數名前加$符號

echo $name
# 若不加$,echo會預設將所有輸入作為字串輸出

echo name
# 輸出name

同時可以直接對變數名重新賦值來更改其值

name="Ruby"

對於變數有幾個關鍵字

# readonly用來指定一個變數是隻讀不可改的,僅在宣告時賦一次值
readonly name
name="else" # 這裡會報錯,因為name已經被宣告為只讀不可改

# unset用來刪除一個變數,或者說來重置一個變數,變數的值會變為空
unset name
echo $name # 輸出為空

值得注意的是,在賦值時,不加引號也會預設值為字串值

alpha=rose
# 等價於
alpha="rose"

也就是說,shell中預設的變數型別為字串。

3.3 字串操作

在shell的列印輸出(echo)中,會遇到一些格式化字串的需求

name=Ruby

# 1. 字串拼接,使用雙引號
echo "Hello, $name!"
# 輸出Hello, Ruby!

# 有時會有這樣的需求
echo "Hello, $nameBBB"
# 此時變數名與字串重合,而我們並沒有定義nameBBB變數,輸出空。
# 因此可以使用{}包裹變數名來解決這個問題
echo "Hello, ${name}BBB"


# 2. 原始字串,使用單引號
echo 'Hello, $name!'
# 輸出Hello, $name!,相當於python中的r"name"

# 3. 獲取字串長度
echo ${#name}
# 輸出4

3.4 變數型別指定

shell中變數型別有三種

3.2節中已經講過,shell中的變數預設為字串型別,也就是說在不使用關鍵字指定的前提下,無法進行數值運算。

num1=10
num2=20
sum=$num1+$num2
echo $sum 
# 輸出字串10+20

因此在宣告一些變數時,可以使用delcare關鍵字來指定變數的型別。

使用時的語法如下

declare [+/-] [變換屬性] 變數名

# - 表示為變數增加屬性
# + 表示為變數刪除屬性
# 變換屬性有如下幾種

# -i 整型
# -l 小寫字母
# -u 大寫字母
# -a 陣列型別
# -A 關聯陣列型別(HashMap)
# -x 環境變數
# -r 只讀變數
# -g 全域性變數,shell中不加指定也預設為全域性變數
# -p 顯示變數型別

一些演示例子如下

  1. 宣告整型變數
declare -i num=10
declare -i num_=20
declare -i sum=$num+$num_
echo $sum # 輸出30

值得一提的是,shell會將賦值儘量的轉換到賦值物件所需要的型別,因此對於一個已經宣告的整型變數,允許如下操作。

declare -i num
str1=10
str2=20 # 記得嗎,str1和str2預設為字串
num=$str1+$str2
echo $num # 輸出30

同理也有

declare -i num=10
declare -i num_=10
str=$num+$num_ # str預設字串,因此整型轉換到字串進行字串拼接
echo $str # 輸出10+10
  1. 宣告大小寫字母變數
declare -l Lowname=RUBY
declare -u Upname=ruby
echo $Lowname # 輸出ruby
echo $Upname # 輸出RUBY

shell會將賦值的字串根據變數宣告時的屬性來僅保留大寫或者小寫

  1. 宣告陣列以及關聯陣列

陣列也是shell中的一種基本型別,其可以使用declare -a來宣告,亦可以直接宣告,訪問其元素與正常陣列類似。

# 元素用小括號包裹,用空格分隔
declare -a arr=(1 2 3 4 5)
# or
arr=(1 2 3 4 5)
# or 直接宣告
arr[0]=1 # 這裡直接宣告瞭一個陣列,只有一個元素1


# 訪問元素
echo $arr # 陣列名指向第一個元素,輸出1
echo ${arr[2]} # 輸出1
echo ${arr[@]} or echo ${arr[*]} # 輸出1 2 3 4 5

# 陣列長度
echo ${#arr[@]} # 輸出5,語法與輸出字串長度類似

# 新增元素
arr[5]=6 # 直接從最後一個元素的後面的索引賦值即可新增
echo ${arr[*]} # 輸出1 2 3 4 5 6
# or
arr+=(6 7 8) # 也可以使用+=來新增元素或者一段陣列
echo ${arr[*]} # 輸出1 2 3 4 5 6 6 7 8

# 陣列拼接
arr1=(1 2 3)
arr2=(4 5 6)
arr3=(${arr1[*]} ${arr2[*]}) # 注意有空格
echo ${arr3[*]} # 輸出1 2 3 4 5 6

關於關聯陣列,shell中沒有直接的宣告方式,需要使用declare -A來宣告,其用法與python中的Dict類似,賦值時以[鍵]=值的方式進行。

# 元素用小括號包裹,用空格分隔
declare -A arr=([id1]=11 [id2]=12 [id3]=13)
# or
declare -A arr
arr[id1]=11
arr[id2]=12
arr[id3]=13
# 即關聯陣列支援宣告後直接使用鍵值對來新增元素,對於已經存在的鍵,會覆蓋原值,對於不存在的鍵,會新增鍵值對。

# 訪問元素
echo ${arr[id1]} # 輸出11
echo ${arr[@]} or echo ${arr[*]} # 輸出11 12 13,即只會輸出值

# 想要遍歷輸出關聯陣列的鍵,可以加`!`
echo ${!arr[*]} # 輸出id1 id2 id3

# 關聯陣列長度
echo ${#arr[@]} # 輸出3
  1. 宣告環境變數
declare -x MYENV=123
echo $MYENV # 輸出123
printenv MYENV # 輸出123,說明確實匯出到了環境變數
  1. 宣告只讀變數
declare -r name=ruby
# 等價於 readonly name=ruby
name=else # 報錯,不可修改
  1. 顯示變數型別
declare -p [...變數名] # 輸出變數型別,可跟多個變數名

可以透過組合使用這些關鍵字來宣告各種變數型別。

declare -ai arr=(1 2 3)
# or
declare -a -i arr=(1 2 3)
# 宣告一個整型陣列arr
arr[0]+=1
echo ${arr[0]} # 輸出2

上述的宣告後的變數可以使用+[屬性]來取消其屬性。

declare -l Lowname
Lowname=RUBY
echo $Lowname # 輸出ruby
declare +l Lowname # 取消小寫字母屬性
Lowname=BIG
echo $Lowname # 輸出BIG,屬性失效

但是注意,+r選項並不能取消變數的只讀屬性,對一個只讀變數使用+r選項會報錯。

也可以使用typeset命令來宣告變數型別,其語法與declare基本一致。

3.4.X 算數運算

因為shell中變數型別預設為字串,因此無法直接進行數值運算。

a=10
b=20
sum=$a+$b
echo $sum # 輸出字串10+20,而非30

shell中提供了一些命令來進行數值運算。

在此之前,簡單瞭解一下內建命令與外部命令的區別。

  • 內建命令是在編譯時與shell一起編譯生成可執行檔案的命令,自身已整合在shell內部,不需要訪問外部的可執行檔案來執行。
  • 外部命令則是指需要呼叫外部可執行檔案來執行的命令,如ls、cp、mv等。

可以透過type命令來檢視某個命令是否為內建命令,而且使用which命令來檢視內建命令時,一般找不到其路徑,因為已經整合在shell內部。

type mv
mv 是 /usr/bin/mv # 說明mv是外部命令
which mv
/usr/bin/mv

type cd
cd 是 shell 內建
which cd 
# 輸出空,內建命令找不到路徑

下面開始介紹常用的算數運算命令。

  1. 括號實現
# 使用$((...))來包裹算數表示式
result=$((10+20))
echo $result # 輸出30

#or 
num1=10
num2=20
result=$(($num1*$num2))
echo $result # 輸出200


#支援整數加(+),減(-),乘(*),除(/),乘方(**),取模(%)運算

#也支援位運算如左移(<<)、右移(>>)、按位與(&)、按位或(|)、按位異或(^)、邏輯非(~)

僅可以實現整數運算,非整除會截斷為整數。

  1. expr命令
# expr命令可以進行算數運算,其語法如下
expr [數] [運算子] [數]

# 其中數字和運算子各作為引數傳入,需要用空格分隔。

# such as
expr 10 + 20 # 輸出30
expr 10 / 20 # 輸出0,因為除法運算結果為浮點數
expr 5 * 2   # 報錯,因為*號是shell中的特殊字元,可以使用跳脫字元\來表示
expr 5 \* 2  # 輸出10,因為\*表示*號本身

# 也可以賦值給變數
sum = $(expr 10 + 20)
# or
sun = `expr 10 + 20`
echo $sum # 輸出30

# 注意,expr實現的運算僅有+-*/%

  1. bc命令
    在終端中輸入bc
bc

會出現以下
image

可以看出bc是linux中的一個可執行程式,用來進行算術運算。
其中支援加(+)減(-)乘(*)除(/)乘方(^)以及取非(!)運算。
但是僅可以執行表示式,不能直接賦值給變數。

1+2
3
5^2
25
!0
1
!5
0
quit # 輸入quit退出bc

上面的方法僅支援整數運算。想要進行浮點數運算,可以使用-l選項來實現。

bc -l

然後就可以使用浮點數運算了。

6 / 4
1.50000000000000000000
quit
# 小數位有點多,可以直接在bc中使用scale命令來指定小數位數,直接截斷,非四捨五入
bc -l
2/3
.666666666666666666666
scale=2 # 指定小數位數為2
2/3
.66
quit

在浮點數運算模式下bc提供了一些函式計算

s(x) 計算 sin(x),以下x皆為弧度表示
c(x) 計算 cos(x)
a(x) 計算arctangent(x)
l(x) 計算ln(x)
e(x) 計算e的x次方,其中e為自然底數
x^y 計算x的y次方
sqrt(x) 計算根號下x
# 直接傳入即可

可以看見bc執行後預設由標準輸入讀取表示式然後將結果到標準輸出,因此可以使用管道向其輸入表示式。

echo '1+2' | bc # 輸出3
echo '6/4' | bc -l # 輸出1.5000000000
# 多條命令使用分號;隔開
echo '1+2;2/3' | bc -l
3.0000000000
.66666666666

# 使用這個方法可以指定小數位數
echo 'scale=2;2/3' | bc -l
.66

# 在使用一些重定向就可以實現使用bc賦值
echo '1+2' | bc >> a.txt
# 將bc的輸出重定向到a.txt檔案末尾

# 也可以將bc的輸出賦值給變數
result=$(echo '1+2' | bc)
# or 
result=`echo '1+2' | bc`

可以看到使用$([命令/命令表示式])or` [表示式] `的方法來將命令的輸出賦值給變數。

  1. let
    let命令一般與賦值操作結合實現數值運算。
# 使用let來進行數值運算
num1=10
num2=20
let sum=$num1+$num2
echo $sum # 輸出30
# let 支援省略$來進行數值運算,則上述等同
let sum=num1+num2
echo $sum # 輸出30

let支援加(+)減(-)乘(*)除(/)乘方(**)取模(%)運算
不支援位運算。
支援+=,-=等類似運算,支援自增自減運算。

num=0
let num++ # 自增
echo $num # 輸出1
let num-- # 自減
echo $num # 輸出0
let num+=10 # 加法賦值
echo $num # 輸出10
# 對於並非使用let宣告的變數,也可以使用let來進行數值運算
num=10
let num+=10
echo $num # 輸出20

# 對於非數字的變數,let預設其值為0
alpha="b"
let alpha++ # 自增
echo $alpha # 輸出1
alpha="b"
let alpha+=10
echo $alpha # 輸出10

3.5 命令狀態及其狀態運算

3.5.1 命令狀態碼

在shell中,每條命令執行完畢後,都會返回一個退出狀態(exit status)碼,用來表示命令執行成功與否。其取值範圍為0-255,0表示成功,非0表示失敗,因此在使用退出狀態碼時大部分時間僅需要關注0與非0即可。

當執行完命令後,可以透過echo $?來列印出上一條命令的退出狀態碼。

ls a.txt # a.txt存在
echo $? # 輸出0

ls b.txt # b.txt不存在
echo $? # 輸出1

注意的是,與大多數語言不同,shell中表示成功的退出狀態碼為0(true),表示失敗的退出狀態碼為非0(false)。

在編寫shell指令碼時,執行指令碼後的退出狀態碼預設為指令碼最後一個語句的退出狀態碼。

#!/bin/bash
ls a.txt
ls b.txt

此指令碼執行後,如果b.txt不存在,則退出狀態碼為1,否則為0。

當然,我們也可以透過exit命令來指定退出狀態碼。

#!/bin/bash
ls a.txt    
exit 0 # 表示成功

這樣不論a.txt是否存在,指令碼都會返回0作為退出狀態碼。0可以換成其他數字。

需要注意的是,exit後的語句不會執行,指令碼執行到exit即結束。

3.5.2 test測試命令

test命令是一個用於條件判斷的命令,其語法如下

test [表示式] -[選項] [表示式]
# or
test -[選項] [表示式]

其中選項有如下(真的很多)

# 數值比較,左邊相對於右邊

-eq	    #等於則為真(equal)
-ne	    #不等於則為真(not equal)
-gt	    #大於則為真(greater than)
-ge	    #大於等於則為真(greater than or equal)
-lt	    #小於則為真(less than)
-le	    #小於等於則為真(less than or equal)
# such as
test 10 -eq 10 # 該語句的狀態碼為0
# 字串比較,左邊在右邊中出現則為真

=	    #等於則為真
!=	    #不相等則為真
-z 字串 	#字串的長度為零則為真
-n 字串 	#字串的長度不為零則為真
# such as
test "abc" = "abc" # 該語句的狀態碼為0
name=Ruby
test -z "$name" # 該語句的狀態碼為0,因為name的長度為3
test -n "$name" # 該語句的狀態碼為0,因為name的長度不為0
# 這裡建議將字串用雙引號括起來,防止空變數名的情況發生。
如
test -n $non # 由於non變數為空,相當於test -z ,而該語句的狀態碼為0,因此本該輸出0,實際輸出1。
# 檔案測試,檔案測試命令用於測試檔案是否存在、是否可讀、是否可寫、是否可執行等。
-e 檔名	#如果檔案存在則為真
-r 檔名	#如果檔案存在且可讀則為真
-w 檔名	#如果檔案存在且可寫則為真
-x 檔名	#如果檔案存在且可執行則為真
-s 檔名	#如果檔案存在且至少有一個字元則為真
-d 檔名	#如果檔案存在且為目錄則為真
-f 檔名	#如果檔案存在且為普通檔案則為真
-c 檔名	#如果檔案存在且為字元型特殊檔案則為真
-b 檔名	#如果檔案存在且為塊特殊檔案則為真
# such as
# 假設a.txt存在且可讀
test -e a.txt # 該語句的狀態碼為0,因為a.txt存在
test -r a.txt # 該語句的狀態碼為0,因為a.txt存在且可讀

上述這些選項前可以將作為引數傳入來對其邏輯取反,即本來

test ! -e a.txt # 此時若a.txt存在,則該語句的狀態碼為1,否則為0
#等價與[ ! -e a.txt ]
test "abc" != "abc" # 狀態碼為1,因為"abc"等於"abc",只有不相等時才為真,狀態嗎為0。

這些選項之後用到再來查閱即可,太多了不可能一下記住。

當然shell提供了一種關於test更簡單的寫法

[ [表示式] -[選項] [表示式] ]
# or
[ -[選項] [表示式] ]
# 注意中括號與語句間均有一個空格
# such as
[ "abs" = "abs" ]
[ -z "$name" ]

3.5.3 邏輯運算子

與C++以及Python等語言類似,shell提供了一些邏輯運算子來進行條件判斷,包括邏輯與(&&)、邏輯或(||)以及邏輯非(!)。

在瞭解shell中的邏輯運算子前,先了解兩個shell中的內建命令truefalse

true # 該命令的退出狀態碼恆為0
false # 該命令的退出狀態碼恆為非0
  1. 邏輯與
[表示式] && [表示式]
# or
[ [表示式] -a [表示式] ] # -a = and

該運算子用於判斷兩個表示式是否都為真,如果兩個表示式的退出狀態碼都為0,則返回退出狀態碼為0,否則返回退出狀態碼為1,相應的,也會有邏輯短路的現象。

[ "abc" = "def" -a touch yes.txt ]
# 等價與 [ "abc" = "def" ] && touch yes.txt

# 顯然字串abc不等於def,因此無論後面是什麼,都會返回1,同時後面的表示式也不會被執行,因此不會建立yes.txt檔案。
  1. 邏輯或
[表示式] || [表示式]
# or
[ [表示式] -o [表示式] ] # -o = or

該運算子用於判斷兩個表示式是否至少有一個為真,如果兩個表示式的退出狀態碼有一個為0,則返回退出狀態碼為0,否則返回退出狀態碼為1,相應的,也會有邏輯短路的現象。

[ "abc" = "abc" -o touch yes.txt ]
# 等價與 [ "abc" = "abc" ] || touch yes.txt
# 顯然字串abc等於abc,無論後面是什麼,都會返回0,因此後面的表示式touch yes.txt不會被執行,不會建立yes.txt檔案。  
  1. 邏輯非
! [表示式]
# or
[ ! [表示式] ]
[ [表示式] ! -[選項] [表示式]]

該運算子用於對錶達式取反,如果表示式的退出狀態碼為0,則返回退出狀態碼為1,否則返回退出狀態碼為0。

[! -e a.txt ]
# 等價與 -e a.txt 為假,即a.txt不存在,則返回0,否則返回1。
!ls a.txr
# 若a.txt存在,則返回1,否則返回0。
[ "abc" != "abc" ]
# 顯然abc等於abc,!=不成立,因此返回1。

透過上述的邏輯運算子,我們可以寫出一些複雜的條件判斷語句,應用其短路特性達到if-else的效果。

[ -e a.txt ] && rm a.txt
# 若a.txt存在,則刪除a.txt,否則什麼都不做。

[ ! -e a.txt ] && touch a.txt
# 若a.txt不存在,則建立a.txt,否則什麼都不做。

在考慮這幾個運算子間的優先順序時,直接用小括號最省腦子。

3.6 條件語句

3.6.1 if-else語句

shell中提供if-else來實現條件語句,當條件為真(退出狀態碼為0)時執行if塊中的命令,否則執行else塊中的命令。
其語法如下
最簡單的if-else語句如下

if [條件]
then
    # 條件為真時執行的程式碼
fi  # 結束if語句

# shell中可以使用;來在一行分割命令,那麼上面可以這樣寫
if [條件]; then echo "條件為真"; fi

帶有else的if-else語句如下

if [條件]
then
    # 條件判斷1為真時執行的程式碼
else
    # 條件判斷1為假時執行的程式碼
fi # 結束if語句

或者帶有elif的if-else語句如下

if [條件1]
then
    # 條件判斷1為真時執行的程式碼
elif [條件2]
then
    # 條件判斷2為真時執行的程式碼
else
    # 條件判斷1和2都為假時執行的程式碼
fi # 結束if語句

多層巢狀的if-else使用縮排區分

if [條件1]
then
    if [條件2]
    then
        # 條件判斷2為真時執行的程式碼
    fi
else
    if [條件3]
    then
        # 條件判斷3為真時執行的程式碼
    fi
fi # 結束if語句

注意這裡的條件為條件塊,即使用如下

if [ 5 -gt 3 ]
    echo "5大於3"
else
    echo "5不大於3"
fi
# 可以組合邏輯運算子
num=10
if [ $num -gt 3 -a $num -lt 15 ]
    echo "num大於3且小於15"
else
    echo "num不大於3或大於15"
fi
上面等價與
if [ $num -gt 3 ] && [ $num -lt 15 ]
    echo "num大於3且小於15"
else
    echo "num不大於3或大於15"
fi

對於一些複雜的條件判斷,可以使用邏輯運算子的短路特性來簡化程式碼,這裡就不舉例子了。

3.6.2 case語句

case語句類似與C++中的switch語句,用於多分支條件判斷。

case 變數 in
值1)
    # 變數等於值1時執行的程式碼
    ;;
值2)    
    # 變數等於值2時執行的程式碼
    ;;
*)
    # 變數不等於值1或2時執行的程式碼
esac

即對於每種模式,我們使用值)來充當型別case,然後使用;;來表示該模式結束。由於shell中沒有default關鍵字,因此使用萬用字元*來匹配所有其他情況,最後使用esac來結束case語句。

很有意思的是,if-else和case語句的結束符都是其反寫,即fiesac

這裡提一嘴,shell中的匹配與正規表示式幾乎一致。

case語句也支援使用|來用一種模式匹配多個情況。

read -p "請輸入數字:" num # read的-p選項用於提示使用者輸入
case $num in
1|2|3)
    echo "你輸入的數字是1、2、3中的一個"
    ;;
4|5|6)
    echo "你輸入的數字是4、5、6中的一個"
    ;;
*)
    echo "你輸入的數字不是1、2、3、4、5、6中的任何一個"
esac

同時可以使用正規表示式來匹配

read -p "請輸入::" str
case $str in
[0-9]*)
    echo "你輸入的是數字"
    ;;
[a-zA-Z]*)
    echo "你輸入的是字母"
    ;;
*)
    echo "你輸入的不是數字也不是字母"
esac

3.7 迴圈語句

3.7.1 for迴圈

shell中提供了兩種for的寫法,一種是類似python的迭代for,一種是類似C語言條件for。

迭代for的語法如下

for 變數 in 值1 值2 值3...
do
   # 迴圈體
done
# 值1,2,3...也可為可迭代物件或者其他使用空格或者tab分隔的值,亦可以使用一些展開的語法來實現。

# such as 
for i in 1 2 5 
do 
   echo $i
done
# 輸出1 2 5

for i in *
do 
   echo "$i"
done
# 列印當前目錄下所有檔名

for i in {1..10}
do
   echo $i
done
# 列印1到10

條件for的語法如下

for (( 初始值; 條件; 步進值 ))
do
   # 迴圈體
done
# 初始值,條件,步進值均為數字,亦可使用一些變數來代替,注意使用雙層小括號。

# such as 
for ((i=1; i<=10; i++))
do
   echo $i
done
# 輸出1 2 3 4 5 6 7 8 9 10

也可以用來訪問陣列或者關聯陣列

arr=(1 2 3 4 5)
for val in ${arr[*]}
do
    echo $val
done
# 輸出1 2 3 4 5
declare -A assoc_arr
assoc_arr[key1]=value1
assoc_arr[key2]=value2
for key in ${!assoc_arr[@]}
do
    echo $key : ${assoc_arr[$key]}
done
# 輸出key1 : value1 key2 : value2

3.7.2 while與until迴圈

shell中提供了while和until迴圈,用於迴圈執行一段程式碼,直到條件為真或假。

其中while迴圈和until迴圈可以認為是兩個相反的例子,while迴圈僅有表示式的退出狀態碼為0時(即為真或者說執行成功)去執行迴圈體,而until迴圈則是表示式的退出狀態碼為0時(即為真或者說執行成功)退出執行。

while [ 條件 ]
do
    # 迴圈體
done

until [ 條件 ]
do
    # 迴圈體
done

# such as
let cnt=0
while [ $cnt -le 5 ] # 迴圈條件為cnt小於等於5
do
    echo $cnt
    let cnt++
done
# 輸出0 1 2 3 4 5

let cnt=0
until [ $cnt -ge 5 ] # 退出迴圈條件為cnt大於等於5
do
    echo $cnt
    let cnt++
done
# 輸出0 1 2 3 4 5

3.7.3 迴圈控制語句break與continue

shell提供了向c/c++中類似的break和continue語句,用於控制迴圈的執行。

break用於跳出當前迴圈,continue用於跳過當前迴圈的剩餘語句,並開始下一次迴圈。

let cnt=0
while [ $cnt -le 5]
do 
    echo $cnt
    let cnt++
    if [ $cnt -eq 3 ] # 若cnt等於3
    then
        break # 跳出當前迴圈
    fi
done
# 輸出0 1 2
# 上面的if-else語句可以使用邏輯短路來簡化
[ $cnt -eq 3 ] && break

# contine用於跳過當前迴圈的剩餘語句,並開始下一次迴圈
let cnt=0
while [ $cnt -le 5 ]
do
    let cnt++
    [ $cnt -eq 3 ] && continue # 若cnt等於3,則跳過當前迴圈的剩餘語句,並開始下一次迴圈
    echo $cnt
done
# 輸出0 1 2 4 5

3.8 指令碼傳參

類似於ls a.txt,ls實際上也是一個可執行程式,我們向其中傳入了a.txt作為引數。

在自己編寫的shell中,我們可以使用$n來表示傳遞給指令碼或函式的引數,其中n為引數的位置,從0開始。

# t.sh
#!/bin/bash
echo "第一個引數為:$1"
echo "第二個引數為:$2"
echo "第三個引數為:$3"

chmod +x t.sh # 使指令碼可執行
./t.sh a b c # 執行指令碼,並傳入引數a,b,c
# 輸出
# 第一個引數為:a
# 第二個引數為:b
# 第三個引數為:c

其中還有一些特殊符號

$0 執行指令碼時提供的路徑
$# 傳遞給指令碼或函式的引數個數
$@ 傳遞給指令碼或函式的所有引數
# t.sh
#!/bin/bash
for i in $@
do
    echo $i
done
# 輸出所有引數
./t.sh "hello world" 123
# 輸出
# hello 
# world
# 123
# 這是由於hello world雖然是一個引數,但是未加雙引號的情況下引數會依據空白字元重新分割。因此需要使用雙引號將$@括起來。

for i in "$@"
do
    echo $i
done
# 輸出所有引數
./t.sh "hello world" 123
# 輸出
# hello world
# 123
# "$@"會將每個引數使用雙引號括起來,因此不會出現空格被遍歷出來。

3.9 函式

shell中提供了函式的概念,可以將一系列命令封裝成一個函式,然後在其他地方呼叫。

函式的寫法有三種

# 第一種,使用function關鍵字來定義函式,同時函式名後面加上()
function 函式名() {
    # 函式體
}

# 第二種,省略function關鍵字,直接使用函式名加()
函式名() {
    # 函式體
}

# 第三種,使用關鍵字function來定義函式,省略函式名後的()
function 函式名{
    # 函式體
}

# 這三種寫法的花括號都可以另起一行
func()
{
    echo "Hello, World!"
}

# 呼叫函式直接使用函式名即可
func
# 輸出
# Hello, World!

shell中的函式也可以傳參和使用返回值來傳遞資料。

其中傳參的寫法與向指令碼傳參的形式類似,而非像其他語言中在小括號中先宣告引數。

func()
{
    echo "第一個引數為:$1" # 使用$n的形式獲取第n個引數
    echo "第二個引數為:$2"
    echo "第三個引數為:$3"

    echo "所有引數為:$#" # 使用$#獲取引數個數
    for i in "$@" # 使用@或*的形式獲取所有引數
    do 
        echo $i 
    done
}

而函式的返回值可以透過return關鍵字來實現,執行完函式後使用$?來獲取函式的退出狀態碼,即其返回值。

func()
{
    let sum=0
    for num in "$@"
    do
        let sum=sum+num
    done
    return $sum # 返回函式的返回值
}
func {1..5}
echo "函式的返回值為:$?" # 輸出函式的返回值
# 輸出
# 函式的返回值為:15

但是使用return僅能返回一個int值,因為本質上執行函式就是執行一個封裝好的命令,返回實際上是返回了一個退出狀態碼,範圍在0-255之間。因此大於255的返回值會溢位。

由於函式類似於命令,因此可以使用命令的展開來實現任意無限制的返回值。

func()
{
    let sum=0
    for num in "$@"
    do
        let sum=sum+num
    done
    echo "$sum"
}
res=`func {1..100}`
#or
res=$(func {1..100})
echo "函式的返回值為:$res"
# 輸出 
# 函式的返回值為:5050

使用這種方法的返回可以不限制返回值的型別。

函式中的變數作用域預設是全域性的,可以透過local關鍵字來宣告區域性變數。

而使用命令展開獲取返回值時,即使不使用local宣告,也無法修改和訪問函式內部的變數。

3.10 grep命令

grep命令是linux中非常常用的命令,用於在指定文字中查詢能夠匹配上模式的行。模式可以是字串,亦可以是正規表示式。匹配時區分大小寫。

grep [選項] [模式] [檔案]
其中常用的選項有
-i:忽略大小寫進行匹配。
-v:反向查詢,只列印不匹配的行。
-n:顯示匹配行的行號。
-r:遞迴查詢子目錄中的檔案。
-l:只列印匹配的檔名。
-c:只列印匹配的行數。
-w:匹配整個單詞。
-q:靜默模式,不列印任何資訊。可以透過退出狀態來判斷是否匹配到。
-B 數字 :列印匹配行之前的n行
-A 數字 :列印匹配行之後的n行
-C 數字 :列印匹配行前後各n行

# 當grep匹配到行時,退出狀態碼為0,否則為1。
# 匹配到多個行時,會將所有匹配的行列印出來。
a.txt
cat1
cat2
cat3
cat4

grep "cat" a.txt
# 輸出
# cat1
# cat2
# cat3
# cat4
echo "$?" # 輸出0

grep "dog" a.txt
echo "$?" # 輸出1

grep -w "cat" a.txt # 匹配整個單詞
echo "$?"  # 輸出1

使用管道可以結合其他命令的輸出來使用grep

cat a.txt | grep "cat"
# 作用與 grep "cat" a.txt 相同,這裡使用管道將cat的輸出作為grep的輸入

ls * | grep *.txt
# 找到當前目錄下所有以.txt結尾的檔名

相關文章