博主日常工作中使用的shell指令碼分享

良知猶存發表於2021-11-15

前言:

今天給大家分享一篇在我工作中常用的一個shell指令碼,裡面有一些我們常用到的shell操作。該指令碼用於本地電腦和伺服器互動上,實現以下功能:

  1. 自動拉取自己個人電腦上的原始碼到伺服器上yocto包的原始碼資料夾。
  2. 自動執行compile 、strip
  3. 自動把編譯好的結果回傳到個人電腦上。
  4. 拷貝執行檔案到機器中

作者:良知猶存

轉載授權以及圍觀:歡迎關注微信公眾號:羽林君

或者新增作者個人微信:become_me


情節介紹:

在工作中,我們經常會遇到需要把修改的程式碼放到伺服器去編譯,然後把編譯好的檔案放到機器板卡對應的檔案。這個過程如果我們使用命令的話,大概有七八條,完成操作完之後 ,大約會花費一分。雖然花費的時間不算多,但是你也會一直關注終端,然後等待輸入命令,需要花費不小精力。但是我們可以寫一些shell指令碼來輔助我們的工作。今天就是給大家分享使用的指令碼,一邊分享使用過程,一邊分享裡面用到的shell技術點。包含,if判斷、switch case條件、字元擷取、遠端呼叫指令碼、兩種免密登陸等。

三者的使用過程為:本地通過指令碼輸入引數選擇使用wifi分配的ip還是網線分配的ip,然後進行ip資料傳參形式 遠端呼叫預先放置好的——伺服器編譯指令碼,伺服器指令碼通過傳入的ip引數,免密scp拷貝本地的檔案到伺服器執行目錄,伺服器執行編譯工作,然後拷貝到本地的指定目錄,最後呼叫本地的expect免密登陸的指令碼,拷貝檔案到機器中。

指令碼一:本地選擇指令碼auto_build.sh

指令碼內容如下:

#/bin/bash
remote_ip=172.160.111.32
remote_hostname=lyn

case $1 in
    1)  echo -e '\033[0;42m Ethernet dhcp \033[0m'
        VAR="eno1"
    ;;
    2)  echo -e '\033[0;46m wireless dhcp \033[0m'
        VAR="wlo1"
    ;;
esac

HOST_IP=$(ifconfig $VAR | grep "inet" | grep -v inet6| awk '{ print $2}' | awk -F: '{print $1}')
echo "parse ip is:" $HOST_IP
if [[ ! -n "${HOST_IP}" ]] ;then
	echo -e "\033[0;31m input local ip \033[0m"
	read local_ip
else
	if [[ ! $(echo "${HOST_IP}" | awk -F. '{printf $1}') == "192"  ]] ;then
		local_ip=$HOST_IP
	else
		echo -e "\033[0;31m error ip \033[0m"
		exit 0
	fi
fi
#exit 0
if [[ $2 == 1 ]];then
	build_opt="all_build"
else
	build_opt=
fi
ssh -t ${remote_hostname}@${remote_ip} "/home/lyn/build.sh ip=${local_ip} ${build_opt}"

這個部分有幾處技術使用:

switch case使用,if else、免密登陸,遠端呼叫指令碼。

首先是一個switch case

此處作用是進行ip地址的篩選,因為的在除錯過程中,我的電腦有時候用網線連線,有時候會去測試房去測試,用wifi連線,這個時候會進行網路ip地址的區分,當我輸入./auto_build.sh 1的時候,指令碼會進行解析eno1網線分配的ip地址,當我輸入./auto_build.sh 2的時候則會解析wlo1wifi分配的ip。

在裡面我還用了顏色列印,進行關鍵詞的標註,如下所示:

關於顏色列印的部分這個是另一個知識,這是一個轉義的實際使用過程,通過特定符號的轉義識別,我們在Linux終端去顯示不同顏色的列印輸出,這個是我們經常使用的操作,例如log等級分級列印時候,error是紅色,正常是綠色,普通是白色等。

顏色列印大致介紹如下:

轉義序列以控制字元'ESC'開頭。該字元的ASCII碼十進位制表示為27,十六進位制表示為0x1B,八進位制表示為033。多數轉義序列超過兩個字元,故通常以'ESC'和左括號'['開頭。該起始序列稱為控制序列引導符(CSI,Control Sequence Intro),通常由 '\033[' 或 '\e[' 代替。

通過轉義序列設定終端顯示屬性時,可採用以下格式:

\033[ Param {;Param;...}m

或

\e[ Param {;Param;...}m

其中,'\033['或'\e['引導轉義序列,'m'表示設定屬性並結束轉義序列。

因此,通過轉義序列設定終端顯示屬性時,常見格式為:

\033[顯示方式;前景色;背景色m輸出字串\033[0m

或\e[顯示方式;前景色;背景色m輸出字串\033[0m

其中 ,'\033[0m'用於恢復預設的終端輸出屬性,否則會影響後續的輸出。

示例:我在此處使用 echo -e '\033[0;42m Ethernet dhcp \033[0m' 進行網線埠ip分配的列印,通過轉義之後,列印顏色為帶背景色的綠色顯示。具體對應的顏色,大家可以看一下小麥老兄寫的這篇文章 printf列印還能這麼玩

注:列印log時候記得echo 要使用 -e引數。

其次還有組合使用命令實現獲取本地ip

HOST_IP=$(ifconfig $VAR | grep "inet" | grep -v inet6| awk '{ print $2}' | awk -F: '{print $1}')

我們一步步檢視執行情況

第一步:ifconfig eno1

lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1 
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.16.30.147  netmask 255.255.255.0  broadcast 172.16.30.255
        inet6 fe80::ca7:d954:67e0:7c60  prefixlen 64  scopeid 0x20<link>
        ether f8:b4:6a:bd:dd:92  txqueuelen 1000  (Ethernet)
        RX packets 3678600  bytes 3470673356 (3.4 GB)
        RX errors 0  dropped 36842  overruns 0  frame 0
        TX packets 2229431  bytes 995696588 (995.6 MB)
        TX errors 0 

我們經常使用ifconfig檢視ip,但是使用ifconfig返回的資料過多,而我們實際使用的部分只是一部分而已。

所以我們使用ifconfig指定裝置查詢ip,篩去無用資訊。

第二步:ifconfig eno1 | grep "inet"

把第一步查詢的資訊通過 | 產生一個管道傳遞給下一個命令,用grep查詢有inet字元的行資料,顯示如下:

lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1  | grep "inet"
        inet 172.16.30.147  netmask 255.255.255.0  broadcast 172.16.30.255
        inet6 fe80::ca7:d954:67e0:7c60  prefixlen 64  scopeid 0x20<link>

因為我們只需要ipv4協議的ip,所以我們要去掉inet6對應的地址

第三步:ifconfig eno1 | grep "inet" | grep -v inet6

使用grep -v命令去掉 inet6 關鍵詞的對應一行資訊

lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1  | grep "inet" | grep -v inet6
        inet 172.16.30.147  netmask 255.255.255.0  broadcast 172.16.30.255

第四步:ifconfig eno1 | grep "inet" | grep -v inet6 | awk '{ print$2}'

使用 awk處理文字檔案的語言進行處理資料,$2 表示預設以空格分割的第二組 ,-F:-F指定分隔符為 ‘ : ’

lyn@lyn:~/Documents/work-data/download_data$ ifconfig eno1  | grep "inet" | grep -v inet6| awk '{ print$2}' 
172.16.30.147

關於grep sed awk的使用大家也可以網上具體檢視一下,但是我們一般使用過程中,grep 更適合單純的查詢或匹配文字, sed 更適合編輯匹配到的文字,awk 更適合格式化文字,對文字進行較複雜格式處理。

這個時候我們從本機得到了ip地址。我們需要進行遠端呼叫伺服器指令碼,並把ip以引數形式傳入。

其次ssh免密登陸和ssh遠端執行任務

首先第一個部分就是ssh免密登陸

本地執行ssh到服務的相關操作命令需要免密,伺服器scp本地檔案也要免密登陸,那麼如何設定我們ssh相關命令操作,無需密碼呢?

SSH分客戶端openssh-client和伺服器openssh-server
如果你只是想登陸別的機器,只需要安裝openssh-client(ubuntu有預設安裝,如果沒有則sudo apt-get install openssh-client),如果要使別的機器登陸本機就需要在本機安裝openssh-server(sudo apt-get install openssh-server)

我們可以使用 ps -e | grep ssh 來檢視對應的openssh-client和openssh-server執行情況,其中ssh是client ,sshd是server,哪個缺我們就使用apt-get install 。

sudo service ssh start 安裝之後可以使用這個命令啟動。

準備好了對應的server和client接下來,把我們生成的rsa公鑰拷貝要對應要登陸的機器,即可免密登陸。

1.客戶端生成公私鑰

ssh-keygen 命令一路回車預設生成

這個命令會在使用者目錄.ssh資料夾下建立公私鑰,id_rsa (私鑰),id_rsa.pub (公鑰)。

2.上傳公鑰到伺服器

ssh-copy-id -i ~/.ssh/id_rsa.pub lyn@172.160.111.32

上面這條命令是寫到伺服器上的ssh目錄下去了

vi  ~/.ssh/authorized_keys

可以看到客戶端寫入到伺服器的 id_rsa.pub (公鑰)內容。

3.測試免密登入
客戶端通過ssh連線遠端伺服器,就可以免密登入了。

ssh lyn@172.160.111.32

第二個部分就是ssh遠端執行伺服器指令碼

有時候我們需要遠端執行一些有互動操作的命令。這個時候我們就可以使用ssh加引數進去進行遠端執行。

格式如下:

遠端執行一個命令

ssh lyn@172.160.111.32 "ls -l"

執行多條命令,使用分號把不同的命令隔起來

ssh lyn@172.160.111.32 "ls;cat test.txt "

遠端執行本地指令碼

ssh lyn@172.160.111.32 < test.sh 

遠端執行本地的指令碼(執行帶有引數的指令碼),需要為 bash
指定 -s 引數:

ssh lyn@172.160.111.32 'bash -s' < test.sh helloworld

執行遠端的指令碼

ssh lyn@172.160.111.32 "/home/lyn/test.sh"

注,此時需要指定指令碼的絕對路徑!

而我們使用的為遠端執行指令碼,最終ssh遠端執行如下:

remote_ip=172.160.111.32
remote_hostname=lyn
local_ip=172.16.30.147
build_opt=
ssh -t ${remote_hostname}@${remote_ip} "/home/lyn/build.sh ip=${local_ip} ${build_opt}"

指令碼二:伺服器編譯指令碼 build.sh

指令碼內容如下:

#!/bin/bash -e
scp_dir=/media/lyn/win_data/lyn_workdata/working/robot-ctl
download_data=/home/lyn/Documents/work-data/download_data

build_dir=/home/lyn/projects/yocto/yocto-build/tmp/work/aarch64-poky-linux/robot-ctl/git-r0/git/
image_dir=/home/lyn/projects/yocto/yocto-build/tmp/work/aarch64-poky-linux/robot-ctl/git-r0/image/robot-ctl/

remote_exec_file_dir=/home/lyn/Documents/work-data/download_data/scp_exec.sh

all_build=No
wifi_src=No
only_scp_robot=No
strip_mode=No
ip_wireless_dhcp=170.160.111.45
ip_ethernet_dhcp=170.160.111.147
local_ip=${ip_wireless_dhcp}
host_name=lyn

date
echo -e "\033[0;31m Loading options.\033[0m"

# Load all the options.

if [ $# -eq 0 ];then
	echo -e "\033[33;5m no argument \033[0m"
fi
for arg in "${@}"
do
    if [[ -n "${arg}" ]] && [[ "${arg}" == "wifi" ]] ; then
        wifi_src="Yes"
        local_ip=${ip_ethernet_dhcp}
		echo -e "local connect robot wifi \n --${wifi_src}\n --ip:${local_ip}."
    fi    
    if [[ -n "${arg}" ]] && [[ "${arg}" == "scp" ]] ; then
        only_scp_robot="Yes"
        local_ip=${ip_ethernet_dhcp}
        echo -e "scp robot local connect robot wifi  \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}."
    fi
    if [[ -n "${arg}" ]] && [[ "${arg}" == "all_build" ]] ; then
        all_build="Yes"
        echo -e "all bulid"
    fi
    

    if [[ -n "${arg}" ]] && [[ "${arg}" == "ip" ]] ; then
        echo -e "\033[32m ip:=\033[0m"
	    read ip_addr
        local_ip=${ip_addr}
        wifi_src="Yes"
        echo -e "scp robot local ip insert  \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}."
    fi   
    if [[ -n "${arg}" ]] && [[ "${arg:0:3}" == "ip=" ]] ; then
        echo -e "\033[32m ip put:=\033[0m" ${arg#*=}
        local_ip=${arg#*=}
    	wifi_src="Yes"
        echo -e "scp robot local ip insert  \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}."
    fi
    if [[ -n "${arg}" ]] && [[ "${arg}" == "debug" ]] ; then
        echo -e "\033[32m need \033[0m" ${arg}
        strip_mode="Yes"
        echo -e "scp robot local ip insert  \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}."
    fi



done
if [  "${only_scp_robot}" == "No" ];then
	if [ ! "${wifi_src}" == "Yes"  ] ; then
	echo -e "local don't connect robot wifi \n --${wifi_src}\n --ip:${local_ip}."
	fi
	

	if [ -d "${build_dir}" ]; then
		cd ${build_dir}


		scp -rp lyn@{local_ip}:${scp_dir}/src \
			 lyn@{local_ip}:${scp_dir}/include \
			 lyn@{local_ip}:${scp_dir}/CMakeLists.txt .


		//ssh ${host_name}@${local_ip} "${remote_scp_code_dir}"
	
		#git pull
	        if [ "${all_build}" == "Yes"  ] ; then
		./../temp/run.do_generate_toolchain_file
		./../temp/run.do_configure
		fi
	
		./../temp/run.do_compile
		./../temp/run.do_install    
	
		#cd /home/lyn/ 
		if [ "${strip_mode}" == "No"  ]; then
			#./strip_x1000.sh
			cd ${image_dir}
			aarch64-linux-gnu-strip pp
		fi
	
	else
		echo -e "\033[0;31m dir is not exist.\033[0m"
	fi
	
	fi
	
	scp ${image_dir}/exec ${host_name}@${local_ip}:${download_data}
	
	if [[ "${wifi_src}" == "Yes" ]] || [[ "${only_scp_robot}" ]]  ; then	
		ssh ${host_name}@${local_ip} "${remote_exec_file_dir}"
	else
		echo -e "no robot wifi\n"
	fi


伺服器執行的指令碼內容比較長,從執行的流程來說,在這個指令碼中,大致為初始化讀取指令碼執行傳入的引數,通過引數配置不同的變數匹配不同機器狀態,緊接著,拷貝本地的檔案到伺服器編譯,伺服器編譯完成之後拷貝可執行檔案到本地,再呼叫本地的指令碼把可執行檔案拷貝到機器對應的目錄。

這個指令碼有些使用技術和第一個指令碼有重合,此處僅說沒有講到的部分。

首先第一個使用的就是 { $# } 和 {$@ }

下面是從菜鳥教程拷貝的shell傳參的特殊字元介紹

引數處理 說明
$# 傳遞到指令碼的引數個數
$* 以一個單字串顯示所有向指令碼傳遞的引數。 如"$*"用「"」括起來的情況、以"$1 $2 … $n"的形式輸出所有引數。
$$ 指令碼執行的當前程式ID號
$! 後臺執行的最後一個程式的ID號
$@ 與$*相同,但是使用時加引號,並在引號中返回每個引數。 如"$@"用「"」括起來的情況、以"$1" "$2" … "$n" 的形式輸出所有引數。
$- 顯示Shell使用的當前選項
$? 顯示最後命令的退出狀態。0表示沒有錯誤,其他任何值表明有錯誤。

我在其中使用了$#用來輔助提醒輸入的引數數量,防止出錯;

if [ $# -eq 0 ];then
	echo -e "\033[33;5m no argument \033[0m"
fi

然後使用$@把傳入的引數一個個解析出了來,進行變數的配置。

for arg in "${@}"
do
    if [[ -n "${arg}" ]] && [[ "${arg}" == "wifi" ]] ; then
        wifi_src="Yes"
        local_ip=${ip_ethernet_dhcp}
	echo -e "local connect robot wifi \n --${wifi_src}\n --ip:${local_ip}."
    fi 
	...
done

其次使用了字串擷取的操作

if [[ -n "${arg}" ]] && [[ "${arg:0:3}" == "ip=" ]] ; then
    echo -e "\033[32m ip put:=\033[0m" ${arg#*=}
    local_ip=${arg#*=}
	wifi_src="Yes"
    echo -e "scp robot local ip insert  \n --scp:${only_scp_robot} --${wifi_src}\n --ip:${local_ip}."
fi

${arg:0:3} 意思為從左邊第0個字元開始,字元的個數為3個

${arg#*=} 意思為 # 號擷取,刪除 '='左邊字元,保留右邊字元。

至於為什麼這麼使用,以及其他使用的介紹,這裡我摘錄其他博主的文章給大家做一個簡單的分享

摘錄自:《shell指令碼字串擷取的8種方法

假設有變數 var=http://www.aaa.com/123.htm.

1. # 號擷取,刪除左邊字元,保留右邊字元。

echo ${var#*//}

其中 var 是變數名,# 號是運算子,*// 表示從左邊開始刪除第一個 // 號及左邊的所有字元
即刪除 http://
結果是 :www.aaa.com/123.htm

2. ## 號擷取,刪除左邊字元,保留右邊字元。

echo ${var##*/}

##*/ 表示從左邊開始刪除最後(最右邊)一個 / 號及左邊的所有字元
即刪除 http://www.aaa.com/

結果是 123.htm

3. %號擷取,刪除右邊字元,保留左邊字元

echo ${var%/*}

%/* 表示從右邊開始,刪除第一個 / 號及右邊的字元

結果是:http://www.aaa.com

4. %% 號擷取,刪除右邊字元,保留左邊字元

echo ${var%%/*}

%%/* 表示從右邊開始,刪除最後(最左邊)一個 / 號及右邊的字元
結果是:http:

**5. 從左邊第幾個字元開始,及字元的個數 **

echo ${var:0:5}

其中的 0 表示左邊第一個字元開始,5 表示字元的總個數。
結果是:http:

6. 從左邊第幾個字元開始,一直到結束。

echo ${var:7}

其中的 7 表示左邊第8個字元開始,一直到結束。
結果是 :www.aaa.com/123.htm

7. 從右邊第幾個字元開始,及字元的個數

echo ${var:0-7:3}

其中的 0-7 表示右邊算起第七個字元開始,3 表示字元的個數。
結果是:123

8. 從右邊第幾個字元開始,一直到結束。

echo ${var:0-7}

表示從右邊第七個字元開始,一直到結束。
結果是:123.htm

注:(左邊的第一個字元是用 0 表示,右邊的第一個字元用 0-1 表示)

此外我們也可以使用awk、cut進行擷取,這裡就不一一列舉了。

指令碼三:本地expect登陸拷貝scp_exec.sh指令碼

指令碼內容如下:

#!/bin/expect

set timeout 30
 
set host 192.168.1.1
set user root
spawn scp /home/lyn/Documents/work-data/download_data/ $user@$host:/opt/lib/exec
 
#spawn ssh $user@$host
expect {
         "*yes/no*"
         {
	  send "yes\r"
  	  expect  "*password:*" { send "123456\r" }
  	 }
	 "*password:*"
	 {
	  send "123456\r"
	 }
	}
expect eof

因為伺服器地址相對固定,並且方便設定ssh公鑰免密登陸,但是機器而言,你需要除錯的機器有很多,我就沒有考慮使用了expect命令。先給大家簡單介紹一下expect:

Expect是等待輸出內容中的特定字元。然後由send傳送特定的相應。其互動流程是:

spawn啟動指定程式 -> expect獲取指定關鍵字 -> send想指定程式傳送指定指令 -> 執行完成, 退出.

首先使用expect 我們需要安裝expect

sudo apt-get install tcl tk expect

因為我寫的這個部分也比較簡單,所以就一點點給大家說明裡面執行細節:

#!/bin/expect

expect的目錄,類似與shell目錄

set timeout 30

set 自定義變數名”:設定超時時間,單位為秒,有些拷貝大檔案的朋友可能會遇到

expect: spawn id exp4 not open這裡沒有等到expect eof是,ssh連線已經關閉了。一般是超時時間太短了,

建議可以直接設定成 timeout -1,這意味著用不超時,拷貝結束之後才會斷開。

set host 192.168.1.1
set user root
spawn scp /home/lyn/Documents/work-data/download_data/ $user@$host:/opt/lib/exec

spawn(expect安裝後的命令)是進入expect環境後才可以執行的expect內部命令,它主要的功能是給ssh執行程式加個殼,用來傳遞互動指令。可以理解為啟動一個新程式 。

**`expect {
         "*yes/no*"
         {
	  send "yes\r"
  	  expect  "*password:*" { send "123456\r" }
  	 }
	 "*password:*"
	 {
	  send "123456\r"
	 }
	}`**

expect {}: 多行期望,從上往下匹配,匹配成功裡面的哪一條,將執行與之的
send命令,注意,這裡面的匹配字串只會執行一個,即匹配到的那個,其餘
的將不會執行,如果想匹配這句命令執行成功後(如登入成功後等待輸入的root
@ubuntu:~#)的其他字元,需要另起一個expect命令,並保證不在expect{}裡
面。

`send "yes\r"

send接收一個字串引數,並將該引數傳送到程式。這裡就是執行互動動作,
與手工輸入密碼的動作等效。 命令字串結尾別忘記加上“\r”,表示“回車
鍵”。

expect eof

expect執行結束, 退出互動程式。

這裡我只是簡單描述了一下我使用expect檔案,更多expect命令學習,有興趣的朋友,可以自行搜尋學習。

結語

這就是我分享我在工作中使用的shell指令碼,如果大家有更好的想法和需求,也歡迎大家加我好友交流分享哈。


作者:良知猶存,白天努力工作,晚上原創公號號主。公眾號內容除了技術還有些人生感悟,一個認真輸出內容的職場老司機,也是一個技術之外豐富生活的人,攝影、音樂 and 籃球。關注我,與我一起同行。

                              ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧  END  ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧

推薦閱讀

【1】在球場上我向人民幣玩家低了頭

【2】嵌入式底層開發的軟體框架簡述

【3】CPU中的程式是怎麼執行起來的 必讀

【4】cartographer環境建立以及建圖測試

【5】設計模式之簡單工廠模式、工廠模式、抽象工廠模式的對比

本公眾號全部原創乾貨已整理成一個目錄,回覆[ 資源 ]即可獲得。

相關文章