超哥 入門
推薦閱讀
https://effective-shell.com
一、基礎#
shell 的作用是
- 解釋執行使用者輸入的命令或程序等
- 使用者輸入一條命令,shell 就解釋一條
- 鍵盤輸入命令,Linux 給予響應的方式,稱之為互動式
shell 的作用_1
shell 是一塊包裹著系統核心的殼,處於操作系統的最外層,與使用者直接對話,把使用者的輸入,解釋給操作系統,然後處理操作系統的輸出結果,輸出到螢幕給予使用者看到結果。
從我們登入 Linux,輸入帳號密碼到進入 Linux 互動式介面,所有的操作,都是交給 shell 解釋並執行
shell 的作用_2
1.1 什麼是 shell 腳本#
當命令或者程序語句寫在文件中,我們執行文件,讀取其中的代碼,這個程序文件就稱之為 shell 腳本。
在 shell 腳本裡定義多條 Linux 命令以及循環控制語句,然後將這些 Linux 命令一次性執行完畢,執行腳本文件的方式稱之為,非互動式方式。
windows 中存在 *.bat
批處理腳本
Linux 中常用 *.sh
腳本文件
shell 腳本規則
在 Linux 系統中,shell 腳本或者稱之為 (bash shell 程序)通常都是 vim 編輯,由 Linux 命令、bash shell 指令、邏輯控制語句 和 註釋信息組成。
Shebang#
電腦程序中,shebang 指的是出現在文本文件的第一行前兩個字符 #!
在 Unix 系統中,程序會分析 shebang 後面的內容,作為解釋器的指令,例如
- 以
#! /bin/sh
開頭的文件,程序在執行的時候會調用 /bin/sh ,也就是 bash 解釋器 - 以
#!/usr/bin/python
開頭的文件,代表指定 python 解釋器去執行 - 以
#!/usr/bin/env
解釋器名稱,是一種在不同平台上都能正確找到解釋器的辦法
注意事項:
- 如果腳本未指定 shebang ,腳本執行的時候,默認用當前 shell 去解釋腳本,即
$SHELL
- 如果 shebang 指定了可執行的解釋器,如
/bin/bash
/usr/bin/python
,腳本在執行時,文件名會作為參數傳遞給解釋器 - 如果
#!
指定的解釋程序沒有可執行權限,則會報錯 "bad interpreter: Permission denied" - 如果
#!
指定的解釋程序不是一個可執行文件,那麼指定的解釋程序會被忽略,轉而交給當前的 SHELL 去執行這個腳本。 - 如果
#!
指定的解釋程序不存在,那麼會報錯 "bad interpreter: No such file or directory" #!
之後的解釋程序,需要寫其絕對路徑 (如︰#!/bin/bash
),它是不會自動到$PATH
中尋找解釋器的- 如果你使用
bash test.sh
這樣的命令來執行腳本,那麼#!
這一行將會被忽略掉,解釋器當然是用命令行中顯式指定的 bash
#! /bin/bash
# Date: xxx
# Author: xxx
# Blog: xxx
shell 腳本語言很適合處理純文本類型數據,且 Linux 的哲學思想就是一切皆文件,如日誌、配置文件、文本、網頁文件,大多數都是純文本類型的,因此 shell 可以方便的進行文本處理,好比強大的 Linux 三劍客 (grep、sed、awk)
腳本語言#
shell 腳本語言屬於一種 弱類型語言 無需聲明變量類型,直接定義使用
強類型語言,必須先定義變量類型,確定是數字、字符串等,之後再賦予同類型的值
# Linux 默認 shell
[root@localhost ~]# echo $SHELL
/bin/bash
# Centos7 支持的 shell
[root@localhost ~]# cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash
/bin/tcsh
/bin/csh
默認的 sh 解釋器
[root@localhost ~]# ll /usr/bin/sh
lrwxrwxrwx. 1 root root 4 Jul 18 19:54 /usr/bin/sh -> bash
1.2 bash#
歷史命令
[root@localhost ~]# echo $HISTSIZE
1000
[root@localhost ~]# echo $HISTFILE
/root/.bash_history
history
-c 清除歷史
-r $HISTFILE 恢復歷史
[root@localhost ~]# history -c
[root@localhost ~]# history
1 history
[root@localhost ~]# history -r ~/.bash_history
!歷史ID # 快速執行歷史命令
!! # 執行上次的命令
bash 特性
- 文件路徑 tab 鍵補全
- 命令補全
- 快捷鍵
ctrl + a, e, u, k, l
- 通配符
- 命令歷史
- 命令別名
- 命令行展開
1.3 shell 變量#
變量定義與賦值,注意 == 變量與值之間不得有空格 ==
- 變量類型,bash 默認把所有變量都認為是字符串
- bash 變量是弱類型,無需事先聲明類型,是將聲明和賦值同時進行
變量替換 / 引用
name="This is test!"
echo ${name}
echi $name # 可以省略花括號
單引號變量,不識別特殊語法
雙引號變量,能識別特殊符號
變量名規則
- 名稱定義要做到見名知意,切按照規則來,切不得引用保留關鍵字 (help 檢查保留字)
- 只能包含數字、字母、下劃線
- 不能以數字開頭
- 不能用標點符號
- 變量名嚴格區分大小寫
變量的作用域
- 本地變量,只針對當前的 shell 進程
pstree 檢查進程樹
環境變量:也稱為全局變量,針對當前 shell 以及其任意子進程,環境變量也分 自定義、內置
兩種環境變量
局部變量:針對在 shell 函數 或是 shell 腳本 中定義
位置參數變量:用於 shell 腳本中傳遞的參數
特殊變量:shell 內置的特殊功效變量
$?
- 0 : 成功
- 1 - 255 : 錯誤碼
自定義變量
- 變量賦值:varName=value
- 變量引用:
${varName}
,$varName
""
,變量名會替換變量值
單引號變量,不識別特殊語法
雙引號變量,能識別特殊符號
-
每次調用 bash/sh 解釋器執行腳本,都会開啟一個子 shell,因此不保留當前的 shell 變量,通過 pstree 命令檢查進程樹
-
調用 source 或者點符號,在當前 shell 環境加載腳本,因此保留變量
# 1. 開啟子 shell 的執行方式
cat make_vars.sh
name="test"
echo $name
aa
bash make_vars.sh
echo $name
aa
# 2. 不開啟子 shell 的執行方式
`Linux 命令`
反引號
echo dir=`ls`
echo $dir
xxxx xxx
shell 變量面試題
cat test.sh
user1=`whoami`
sh test.sh
echo $user1
B
A. 當前用戶
B. 空
1.4 環境變量設置#
環境變量一般指的是用 export 內置命令導出的變量,用於定義 shell 的運行環境、保證 shell 命令的正確執行
shell 通過環境變量確定登錄的用戶名、PATH 路徑、文件系統等各種應用
環境變量可以在命令行中臨時創建,但是用戶退出 shell 終端,變量即丟失,如要永久生效,需要修改環境變量配置文件
- 用戶個人配置文件
~/.bash_profile
、~/.bashrc
遠程登錄用戶特有文件 - 全局配置文件
/etc/profile
、/etc/bashrc
,且系統建議最好創建在/etc/profile.d/
而非直接修改主文件,修改全局配置文件,影響所有登錄系統的用戶
檢查系統環境變量的命令
- set : 當前 shell 中的所有變量,包括全局變量、局部變量,包含函數
- env : 只顯示全局變量
- declare : 輸出所有的變量,如同 set
- export : 顯示和設置環境變量值
撤銷環境變量
- unset 變量名,刪除變量或函數
設置只讀變量
- readonly ,只有 shell 結束,只讀變量失效
系統保留環境變量關鍵字
bash 內嵌了諸多環境變量,用於定義 bash 的工作環境
export | awk -F '[ :=]' '{print $3}'
bash 多命令執行
ls /data/;cd /tmp/;cd /home;cd /data
環境變量初始化與加載順序
ssh 登錄 linux 後,系統啟動一個 bash shell
/etc/profile : 全局環境變量文件,並從 /etc/profile.d 目錄的配置文件中搜集 shell 的設置
讀取 /etc/profile.d 目錄下的腳本
運行 $HOME/.bash_profile (用戶環境變量文件)
運行 $HOME/.bashrc
運行 /etc/bashrc
1.5 特殊變量#
特殊參數變量
shell 的特殊變量,用在如腳本,函數傳遞參數使用,有如下特殊的,位置參數變量
$0 獲取 shell 腳本文件名,以及腳本路徑
$n 獲取 shell 腳本的第 n 個參數,n 在 1~9 之間,如 $1, $2,$9 ,大於 9 則需要寫,${10},參數空格隔開
$# 獲取執行的 shell 腳本後面的參數總個數
$* 獲取 shell 腳本所有參數,不加引號等同於 $@ 作用,加上引號 "$*" 作用是接收所有參數為單個字符串,"$1 $2...."
$@ 不加引號,效果同上,加引號,是接收所有參數為獨立字符串,如 "$1” “$2” “$3” ..., 空格保留
for_variables.sh
#! /bin/bash
for var in "$*"
do
echo "$var"
done
for var in "$@"
echo "$var"
done
bash for_variables.sh test ha 1 2 3 4
特殊狀態變量
$? 上一次命令執行狀態返回值,0 正確,非 0 失敗
$$ 當前 shell 腳本的進程號
$! 上一次後台進程的 PID
$_ 再次之前執行的命令,最後一個參數
man bash 搜索 Special Parameters
echo
-n 不換行輸出
-e 解析字符串中的特殊字符
\n 換行
\r 回車
\t 制表
\b 退格
printf
eval
執行多個命令
eval ls;cd /tmp
exec
不創建子進程,執行後續命令,執行完畢後,自動 exit
expr
expr 函數名
1.6 shell 子串#
${變量} 返回變量值
${#變量} 返回變量長度,字符長度
${變量:Offset} 返回變量 Offset 數值之後的字符
${變量:Offset:length} 提取 Offset 之後的 length 限制的字符
# 刪除
${變量#word} 從變量開頭,刪除最短匹配的 word 子串
${變量##word} 從變量開頭,刪除最長匹配的 word
${變量%word} 從變量結尾刪除最短的 word
${變量%%word} 從變量結尾開始刪除最長匹配的 word
# 替換
${變量/pattern/string}用 string 代替第一個匹配的 pattern
${變量//pattern/string}用 string 代替所有的 pattern
# #指定字符內容截取
a*c 匹配開頭為 a,中間任意個字符,結尾為 c 的字符串 (大小寫敏感)
seq -s ":" 10
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${#char} &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|wc -L &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;expr length "${char}" &>/dev/null;done
time for n in {1...10000};do char=`seq -s "testtt" 100`;echo ${char}|awk "{print length($0)}" &>/dev/null;done
shell 編程,儘量使用 Linux 內置的命令,內置的操作,和內置的函數,效率最高
儘可能的減少管道符的操作
批量修改文件名
for file_name in `ls *.jpg`;do mv $file_name `echo ${file_name//_finished/}` done;
1.7 shell 擴展變量#
${parameter:-word} 如果 parameter 變量值為空,返回 word 字符串
${parameter:=word} 如果 parameter 變量為空,則 word 替代變量值,且返回其值
${parameter:?word} 如果 parameter 變量為空,word 當作 stderr 輸出,否則輸出變量值。用於設置變量為空導致錯誤時,返回的錯誤信息
${parameter:+word} 如果 parameter 變量為空,什麼都不做,否則 word 返回
stdout 1
stderr 2
應用:數據備份,刪除過期數據
find xargs
# 刪除 7 天以上過期數據
find path -name "" -type "文件類型" -mtime +7 | xargs rm -f
dir_path="/data/my_data"
find ${dir_path} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f
find ${dir_path:=/data/my_data} -name '*.tar.gz' -type f -mtime +7 | xargs rm -f
1.8 父子 shell#
父子 shell_1
pstree
ps -ef
-e 列出所有進程的信息,如同 -A 選項
-f 顯示 UID,PID,PPID
ps -ef --forest
父子 shell_2
創建進程列表 (創建子 shell)
# shell 進程列表
(cd ~; pwd; ls; cd /tmp; ls)
檢測是否在子 shell 環境中
# 如果是 0,就是在當前 shell 環境中執行的,否則就是開辟子 shell 去運行的
echo $BASH_SUBSHELL
(cd ~; echo $BASH_SUBSHELL)
子 shell 嵌套
(pwd; echo $BASH_SUBSHELL)
(pwd; (echo $BASH_SUBSHELL))
利用括號,開啟子 shell 的理念,在 shell 腳本開發中,經常會用子 shell 進行多進程的處理提高程序並發執行效率
1.9 內置命令、外置命令#
內置命令:在系統啟動時就加載入內存,常駐內存,執行效率更高,但是佔用資源
外置命令:使用者需要從硬碟中讀取程序文件,再讀入內存加載
type cd
type ls
外置命令 特點:一定會開啟子進程執行
ps -f --forest
內置命令是 shell 的一部分,不需要單獨去某個文件,系統啟動後,便被加載到內存中
# 查看所有內置命令
compgen -b
總結#
echo "username: ${USER}"
echo "UID: $UID"
echo "User home: " $HOME
${} 取出變量值
$() 在括號中執行命令,得到執行結果
`` 同上
() 開啟子 shell,執行
$vars 取出變量值
二、shell 開發#
2.1 數學運算#
Shell 算術運算符
算術運算符 | 說明 / 含義 |
---|---|
+、- | 加法(或正號)、減法(或負號) |
*、/、% | 乘法、除法、取餘(取模) |
** | 幂運算 |
++、-- | 自增和自減,可以放在變量的前面也可以放在變量的後面 |
!、&&、|| | 邏輯非(取反)、邏輯與(and)、邏輯或(or) |
<、<=、>、>= | 比較符號(小於、小於等於、大於、大於等於) |
==、!=、= | 比較符號(相等、不相等;對於字符串,= 也可以表示相當於) |
<<、>> | 向左移位、向右移位 |
~、|、 &、^ | 按位取反、按位或、按位與、按位異或 |
=、+=、-=、*=、/=、%= | 賦值運算符,例如 a+=1 相當於 a=a+1,a-=1 相當於 a=a-1 |
Shell 中常用的六種數學計算方式
運算操作符 / 運算命令 | 說明 |
---|---|
(( )) | 用於整數運算,效率很高,推薦使用。語法層面 |
let | 用於整數運算,和 (()) 類似。 |
$[] | 用於整數運算,不如 (()) 靈活。 |
expr | 可用於整數運算,也可以處理字符串。比較麻煩,需要注意各種細節,不推薦使用。 |
bc | Linux 下的一個計算器程序,可以處理整數和小數。Shell 本身只支持整數運算,想計算小數就得使用 bc 這個外部的計算器。 |
declare -i | 將變量定義為整數,然後再進行數學運算時就不會被當做字符串了。功能有限,僅支持最基本的數學運算(加減乘除和取餘),不支持邏輯運算、自增自減等,所以在實際開發中很少使用。 |
2.1.1 雙小括號#
$(()) # $ 符號必須
# .sh 腳本中可不寫 echo
a=10
b=2
echo $(($a - $b))
echo $(($a > $b))
2.1.2 運算腳本#
#! /bin/bash
print_usage() {
echo "請輸入數字!!!"
exit 1
}
read -p "請輸入第一個數字: " firstnum
if [ -n "`echo` $firstnum | sed 's/[0-9]//g'" ]
then
print_usage
fi
if [ "${operator}" != "+" ] && [ "${operator}" != "-" ] && [ "${operator}" != "*" ] && [ "${operator}" != "/" ]
then
echo "只能輸入 +-*/"
exit 2
fi
read -p "請輸入第二個數字: " secondnum
if [ -n "`echo` $secondnum | sed 's/[0-9]//g'" ]
then
print_usage
fi
echo "${firstnum}${operator}${secondnum} 結果是:$((${firstnum}${operator}${secondnum}))"
2.1.3 檢測 nginx 服務是否運行#
while true;
do
wget --timeout=${timeout} --tries=1 http://xxx.com/ -q -O /dev/null
if [ $? -ne 0 ]
then
let fails+=1
else
let success+=1
fi
if [ $success -ge 1 ]
then
echo "正常"
exit 0
fi
if [ ${fails} -ge 2 ];then
echo "錯誤"
exit 2
fi
done
-ne 不等於
-ge 大於等於
vim 定位另一個括號,shift + %
2.1.4 expr#
expr 1 + 2
# 模式匹配
expr aa.png ":" ".*"
expr aa.png ":" ".p"
# 判斷 jpg 文件
expr bb.jpg ":" ".*\.jpg" # 6
expr bb.jpghhfff ":" ".*\.jpg" # 也能匹配成功 # 6
判斷文件名後綴是否合法
#!/bin/bash
if expr "$1" ":" ".*\.jpg" &> /dev/null
then
echo "這是一個 jpg!"
else
echo "不是 jpg!"
fi
找出長度不大於 2 的單詞
#!/bin/bash
for str1 in This is a str, you know?
do
if [ `expr length str1` -le 2 ]
then
echo $str1
done
2.1.5 bc, awk, 中括號計算#
bc 交互式計算
計算 1~100 的和
echo {1..100} | tr " " "+" | bc
seq -s "+" 100 | bc
echo $((`seq -s "+" 100`)) # 效率最高
seq -s "+" 100 | xargs expr
echo "2.2 1.1" | awk '{print ($1 + $2)}'
num=5
res=$[num+4]
echo $res
res=$[num*4]
echo $res
2.2 shell 條件測試#
條件測試語法 | 說明 |
---|---|
test <測試表達式> | 利用 test 命令進行條件測試表達式的方法。test 命令和 <測試表達式> 之間至少有一個空格 |
[ <測試表達式> ] | 透過 [](單中括號) 進行條件測試表達式的方法,和 test 命令的用法相同,這是老男孩推薦的方法。[] 的邊界和內容之間至少有一個空格 |
[[ <測試表達式> ]] | 透過 [](雙中括號) 進行條件測試表達式的方法,是比 test 和 [] 更新的語法格式。[[]] 的邊界和內容之間至少有一個空格 |
((<測試表達式>)) | 這是透過 (()) 進行條件測試表達式的方法,一般用於 if 語句裡。(()) 兩端不需要有空格 |
2.2.1 test 條件測試#
-e 判斷該文件是否存在,(普通文件,目錄),存在就為真(0),否則為假(1)
echo $?
結合 && 和 || 使用
文件類型
選 項 | 作 用 |
---|---|
-b filename | 判斷文件是否存在,並且是否為塊設備文件 |
-c filename | 判斷文件是否存在,並且是否為字符設備文件 |
-d filename | 判斷文件是否存在,並且是否為目錄文件(常用) |
-e filename | 判斷文件是否存在(常用) |
-f filename | 判斷文件是否存在,並且是否為普通文件(常用) |
-L filename | 判斷文件是否存在,並且是否為符號鏈接文件 |
-p filename | 判斷文件是否存在,並且是否為管道文件 |
-s filename | 判斷文件是否存在,並且是否為非空 |
-S filename | 判斷該文件是否存在,並且是否為套接字文件 |
文件權限判斷
選 項 | 作 用 |
---|---|
-r filename | 判斷文件是否存在,並且是否擁有讀權限 |
-w filename | 判斷文件是否存在,並且是否擁有寫權限 |
-x filename | 判斷文件是否存在,並且是否擁有執行權限 |
-u filename | 判斷文件是否存在,並且是否擁有 SUID 權限 |
-g filename | 判斷文件是否存在,並且是否擁有 SGID 權限 |
-k filename | 判斷該文件是否存在,並且是否擁有 SBIT 權限 |
文件比較
選 項 | 作 用 |
---|---|
filename1 -nt filename2 | 判斷 filename1 的修改時間是否比 filename2 的新 |
filename -ot filename2 | 判斷 filename1 的修改時間是否比 filename2 的舊 |
filename1 -ef filename2 | 判斷 filename1 是否和 filename2 的 inode 号一致,可以理解為兩個文件是否為同一個文件。這個判斷用於判斷硬鏈接是很好的方法 |
-z 字符串為空就為真,否則為假
-n 反過來的,字符串是有內容就為真,否則為假
2.2.2 [ ] 條件測試#
test 和 [] 的作用一樣
[ ]
中前後有空格
條件測試中使用變量,必須使用雙引號
""
[ -f "${file1}" ]
[[ -f "${file1}" ]]
2.2.3 變量測試#
使用雙引號
2.2.4 字符串比較測試#
常用字符串測試操作符 | 說明 |
---|---|
-n "字符串" | 若字符串的長度不為 0,則為真。n 可以理解為 no zero |
-z "字符串" | 若字符串的長度為 0,則為真。z 可以理解為 zero 的縮寫 |
"串 1" = "串 2" | 若字符串 1 等於字符串 2,則為真。可使用 "== "代替"=" |
"串 1" != "串 2" | 若字符串 1 不等於字符串 2,則為真。但不能用 !== 代替 "!=" |
變量要有雙引號
= 兩邊要有空格
2.2.5 數值比較測試#
在 [] 和 test 中使用 | 在 (()) 和 [[]] 中使用的比較符號 | 說明 |
---|---|---|
-eq | == 或 = | 相等 (equeal) |
-ne | != | 不等 (not equal) |
-gt | > | 大於 (greater than) |
-lt | < | 小於 (less than) |
-ge | >= | 大於等於 (greater than or equal) |
-le | <= | 大於小於 (less than or equal) |
-a | && | and |
-o | || | or |
! | ! | 非 |
一般使用
[]
[[]]
也支持 -eq 等
2.2.6 邏輯運算#
read -p "請輸入一個字符:" var1
[ "$var1" -eq "1" ] && {
echo $var1
exit 0
}
[ "$var2" = "2" ] && {
echo $var1
exit 0
}
[ "$var1" != "2" -a "$var1" != "1" ] && {
echo "請輸入 1 或 2!!"
exit 1
}
# 正則表達式
[[ $num =~ [1-3] ]]
總結#
邏輯與 -a
邏輯或 -o
[[]]
無法使用 >=
和 <=
,只能使用 -ge
或 -le
測試表達式符號 | [] | test | [[]] | (()) |
---|---|---|---|---|
邊界為是否需要空格 | 需要 | 需要 | 不需要 | 需要 |
邏輯操作符 | !, -a, -o | !, -a, -o | !, &&, || | !, &&, || |
整數比較操作符 | -eq, -gt, -lt, -ge, -le | -eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <= | -eq, -gt, -lt, -ge, -le 或 =, >, <,>=, <= | =, >, <, >=, <= |
字符串比較操作符 | =, ==, != | =, ==, != | =, ==, != | =, ==, != |
是否支持通配符匹配 | 不支持 | 不支持 | 支持 | 不支持 |
最常用 []
2.3 腳本開發#
開發內存檢測腳本#
# 第二行,最後一個值
free -m | awk 'NR==2 {print $NF}'
#! /bin/bash
FreeMem=`free -m | awk 'NR==2 {print $NF}'`
CHARS="當前內存是 $FreeMem"
if [ "$FreeMem" -lt "100" ]; then
echo $CHARS|tee /tmp/message.txt
# mail -s 標題,收件人 < 內容
mail -s "`date +%F-%T`$CHARS" [email protected] < /tmp/message.txt
fi
mysql 監控腳本#
mysql 監控腳本流程
# 本地
netstat -tunlp | grep mysql | wc -l
ss -tunlp | grep mysql | wc -l
lsof -i tcp:3380 | wc -l
# 遠程
nmap 127.0.0.1 -p 3380 | grep open | wc -l
# telnet
echo -e "\n" | telnet 127.0.0.1 3380 2>/dev/null | grep Connected | wc -l
Rsync 啟停腳本開發#
2.4 函數#
function xx() {
}
function xx{
}
xx(){
}
美化相關
lsb_functions="/lib/lsb/init-functions"
if test -f $lsb_functions;then
. $lsb_functions
else
init_functions="/etc/init.d/functions"
if test -f $init_functions;then
. $init_functions
fi
log_success_msg() {
echo " 成功! $@"
}
log_failure_msg() {
echo " 錯誤! $@"
}
fi
log_success_msg echo "test"
函數返回值#
shell 的函數只能返回 整數值,如果想讓函數返回字符串,一般有兩種方法:將返回值賦值給一個字符串變量、輸出返回值,在函數調用處使用變量賦值。