用 shell 腳本做命令行工具擴展
問題的提出
公司開發機與遠程伺服器之間有嚴格的隔離策略,不能直接使用 ssh 登錄,而必需通過跳板機。這樣一來,本地與伺服器之間的一些文件傳輸變得非常不便。經過諮詢,運維教了我一招:
$ nc -l 8080 > filename
$ nc yunhai.xxxxxxxx.xxxxx.com 8080 < filename
使用 nc 建立連接來傳輸文件,第一行命令在伺服器執行,第二行命令在本地執行,它的參數就是遠程伺服器的 host,運行完成後,將文件從本地拷貝到遠程。這樣確實可以工作,而且 8080 是運維允許通行的埠,上面的命令是合規的。反過來想將文件從遠程拷貝到本地,只需要將上面的重定向符換個方向即可,順序不變,仍是先在伺服器運行第一行命令:
$ nc -l 8080 < filename
$ nc yunhai.xxxxxxxx.xxxxx.com 8080 > filename
從伺服器拷貝大量經過選擇的文件,運維還教了另外一個辦法來實現:
$ python -m SimpleHTTPServer 8080
本地打開瀏覽器訪問 yunhai.xxxxxxxx.xxxxx.com:8080 就能實現類似 ftp 一樣的功能啦:
凡是在這條命令啟動目錄下面的文件,都可以通過點擊上面的鏈接下載,子目錄的話則會展開。與 nc 不同,每次下載不再是「一鎚子買賣」,你可以一直下一直下……這又一次體現了 python 的強大 (雖然我不怎麼用)。
這篇文章寫到這裡似乎就可以結束了,然而我要說的是,上面的工具都不能滿足我的需求。因為我不只是需要一個跨跳板機傳輸文件的工具 (其實用 secretcrt 的 rz/sz 就挺好,不過我司未購買,禁止員工安裝盜版),還想要一個跨多台機器存儲和共享文件的機制。例如我本身是在 mac 上開發,還有一台 windows 測試筆記型電腦,遠程 linux 伺服器目前有一台,但是將來很可能會擴展……想想將來要在這麼多機器上找到並傳輸一個文件我就頭大。
就在我一愁莫展的時候,安全組的同事提供了一個基於企業網盤的命令行工具,可以通過命令行的方式上傳下載文件,在 mac 上還有桌面端可以用。首先它是合規的,其次它上傳、下載的文件位於企業網盤你的個人賬戶下的一個特定目錄,其它人沒有許可權看不到,而你在開發機上通過瀏覽器登錄時,如果之前已經登錄過公司的帳號,就會 SSO 無感登錄:
然後就可以在瀏覽器里查看、編輯、上傳、下載文件啦。另一方面,在伺服器使用命令行也可以 SSO 免登錄直接上傳下載:
$ bst_tool --help
當前用戶:yunhai
Bxxxx Secure Transmission tool. Version x.x.x.x.
Usage:
bst_tool COMMAND [flags] [options]
where COMMAND is one of:
ls 列出指定目錄下的文件
get 下載企業網盤中的文件到本地
put 上傳本地文件到企業網盤
delete 刪除企業網盤中的文件
version 顯示當前版本號
Find more detail by:
bst_tool COMMAND -help
這個工具濃縮的都是精華,只提供列出 (ls)、下載 (get)、上傳 (put)、刪除 (delete) 這四大基本操作,首先來看 ls:
$ bst_tool ls /init
當前用戶:yunhai
total size: 7
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ FILEID (7) │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ 406869750 │ yunhai │ 535.00B │ 2021-04-13 21:24:00 │ file │ init_bst.sh │
│ 406850107 │ yunhai │ 820.00B │ 2021-04-13 19:35:24 │ file │ create_user.sh │
│ 406842658 │ yunhai │ 14.28KB │ 2021-04-13 19:28:05 │ file │ bash_bst.txt │
│ 404998873 │ yunhai │ 409.00B │ 2021-04-07 20:25:16 │ file │ build_cmake.sh │
│ 402750998 │ yunhai │ 15.73MB │ 2021-03-31 16:24:10 │ file │ cmake-3.20.0.zip │
│ 402361471 │ yunhai │ 289.00B │ 2021-03-30 20:41:32 │ file │ build_glibc.sh │
│ 402359162 │ yunhai │ 2.85MB │ 2021-03-30 20:33:41 │ file │ global-6.6.5.tar.gz │
│─────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
如果給 ls 的參數是一個文件,那麼它只列出這個文件的詳細資訊:
$ bst_tool ls /init/create_user.sh
當前用戶:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│────────────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│────────────────│
│ 406850107 │ yunhai │ 820.00B │ 2021-04-13 19:35:24 │ file │ create_user.sh │
│───────────│──────────│──────────│─────────────────────│──────│────────────────│
至於遞歸列出目錄中的子目錄什麼的,想都不要想了,沒有。get 每次可以下載一個文件:
$ bst_tool get /init/create_user.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/cnblogs/create_user.sh
上面這個腳本就是我在新機器上必跑的一個腳本,用來創建非 root 用戶,並修改一些常用的配置。如果你本地已經有一個同名文件,bst 會貼心的給出提示:
$ bst_tool get /init/create_user.sh
當前用戶:yunhai
錯誤:本地路徑 /home/yunh/code/cnblogs/create_user.sh 指向一個已存在文件,請先自行確認並刪除已存在文件,或者選擇其他路徑
這種情況下是不會覆蓋本地文件的,防止意外丟失數據。如果就是要覆蓋,那麼先刪除一下就好啦~
有新的文件需要上傳時,可以使用 put 子命令:
$ bst_tool put score.txt /data/
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 415268968
FilePath: /data/score.txt
Time: 2021-05-06 15:54:31
如果不指定目標文件名,自動使用本地文件名;如果指定的目標目錄不存在,自動遞歸創建路徑中的每個目錄;那如果遠程目標已經存在了呢?
$ bst_tool put create_user.sh /init/
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 415278480
FilePath: /init/create_user(1).sh
Time: 2021-05-06 16:05:29
好傢夥,自動重命名了,命名方式是名稱後跟序號,這也是一種保護數據的思路。
一些臨時文件用完以後,可以刪除:
$ bst_tool delete /data/score.txt
當前用戶:yunhai
deleting the 1th filePath: /data/score.txt
delete result: 成功
在介紹的過程中,我相信你已經了解到了這個小工具的幾個先天不足:
- 不支援遞歸列出,想要知道一個文件有沒有在遠程目錄、在哪個子目錄下面,很難;
- 不支援遞歸下載,想要一次性下載一個目錄,很難;
- 不支援覆蓋下載,想要將遠程文件備份到本地固定目錄,很難;
- 不支援覆蓋上傳,想要將一個文件的修改版本上傳到同一個位置,很難。
作為日常使用的一部分,能用是不夠的,必需要好用!作為 shell 資深用戶,看不慣就改是我們的座右銘,這次就拿它來開刀~
問題的解決
柿子先檢軟的捏,這個覆蓋上傳、覆蓋下載看起來挺容易,通過預先探測來得知文件是否存在,如果存在了給用戶一個告警,並讓用戶 (就是我) 選擇是放棄還是繼續覆蓋就可以了,當用戶選擇覆蓋時,將已存在後台的文件先刪除,再上傳,完事兒~
文件是否存在
在正式開始之前,我們先看一下針對以下幾種情況,bst_tool ls 的效果,因為這個會影響我們後面的判斷邏輯。
- 目錄存在且不為空
- 文件存在
- 目錄存在但為空
- 文件或目錄不存在
$ bst_tool ls /data
當前用戶:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 419245020 │ yunhai │ 283.00B │ 2021-05-14 15:44:24 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
$ bst_tool ls /data/data
當前用戶:yunhai
total size: 1
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 419245020 │ yunhai │ 283.00B │ 2021-05-14 15:44:24 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
$ bst_tool ls /empty
當前用戶:yunhai
total size: 0
$ bst_tool ls /data/data.txt
當前用戶:yunhai
total size: 0
這裡分別列出了 /data 目錄、/data/data 文件、/empty 目錄、/data/data.txt 文件,最後一個是不存在的文件。可以看到前兩組和後兩組的差別比較大,關鍵字就是 total size: 0 這行,為 0 表示不存在或空目錄,不為 0 表示存在;但是這樣就萬事大吉了嗎?今天這個例子我舉得比較巧,相信細心的人已經看出來了,/data 目錄和 /data/data 文件的輸出完全一樣!巧就巧在它們名稱相同、而且目錄下只有一個同名文件,這種場景下,第二個 ls 是輸出文件的詳細資訊;第一個 ls 是輸出目錄下的文件的詳細資訊、而它剛好就是這個文件,所以輸出內容是難辨彼此。
現在關鍵點就聚焦在一個項到底是文件還是目錄了,可能有的人會說,用 TYPE 欄位唄,然而上面的例子中 TYPE 都是 file 的區分不出來。當然啦,一般場景下目錄會有多個文件,totoal size > 1 的話必然是一個目錄,只是這樣並不嚴謹。現在考慮一下 bst 工具中還有什麼能幫到我,除了 ls 外就剩 put / get / delete,三種了,put 是我們要執行的命令,delete 是萬萬不行滴,於是考查一下 get 呢:
$ bst_tool get /data/data
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/data
$ bst_tool get /data
當前用戶:yunhai
no file named 'data' found in directory '/'
哈哈,真金不怕火煉,這一下目錄露餡了 —— get 只能下載文件。可以通過試下載到臨時目錄來判斷到底是文件還是目錄,這不失為一個終極手段。於是就有了下面這段腳本:
1 # $1: dest path
2 #
3 # return value:
4 # 0: not exist
5 # 1: file exist
6 # 2: dir exist but empty
7 # 3: dir exist and not empty
8 bsttool_query_path()
9 {
10 local dst=$1
11 local res=$(bst_tool ls "$dst")
12 local line=$(echo "$res" | sed -n '2p')
13 local str=${line/"total size"//}
14 local download="/tmp/bst.tmp"
15 local ret=0
16 if [ "$str" != "$line" ]; then
17 # normal return
18 ret=$(echo "$line" | awk '{print $NF}')
19 if [ $ret -eq 0 ]; then
20 ret=2
21 elif [ $ret -gt 1 ]; then
22 ret=3 # a dir
23 else # size=1
24 # note here we can NOT using type file/dir to determine,
25 # for example, there is a path /tmp/tmp pointed to a file
26 # when we ls /tmp, it report one item named 'tmp' with type file,
27 # but actually it is a dir.
28 # so here we use another way to determine, that is, download it,
29 # you can NOT download a dir.
30 echo "dir or file exist, try download to check"
31 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
32 line=$(bst_tool get "$dst" "$download" | sed -n '2p')
33 str=${line/"download success"//}
34 if [ "$str" != "$line" ]; then # string replace changed: download success
35 # is a file
36 ret=1
37 else
38 # is a dir
39 ret=3
40 fi
41 fi
42 else
43 # server response error, no such path
44 ret=0
45 fi
46
47 return $ret
48 }
下面做個簡單說明:
- line 13,16 : 判斷是否訪問 server 出錯
- line 18:提取 total size 大小
- line 19-20:為 0 表示不存在或空目錄
- line 21-22:大於 1 為非空目錄
- line 31-32:試下載,為防止下載衝突,提前清理臨時文件
- lline 33:提取 download success 關鍵資訊
- line 34-36: 下載成功,文件
- line 37-39:下載失敗,目錄
- line 43-44:server 訪問出錯,可以認為文件不存在
注意這裡驗證一行中是否包含特定字元串的方法,使用了 shell 字元串替換語法:$(line/”character string”//),如果找到並替換成功 (替換為空其實就是刪除),得到的字元串肯定會和原串不同;否則沒有變化。
上面這些邏輯封裝成 shell 函數 bsttool_get_path,調用時提供一個路徑參數、然後根據返回值來判斷一個路徑的屬性。不過這個演算法有個不太好的地方,就是遇到上面那種分辨不清的場景而待檢查文件又特別大時,需要消耗不少無謂的網路頻寬和時間,但是也沒有辦法,工具本身提供的功能和資訊太少了,我們這邊可以做的優化就是:不要一上來就用這個試下載去判斷,而是其它方法都已經窮盡、迫不得已時才請它出山。
覆蓋上傳
能確認文件屬性後,就可以開始正文了,再回顧一下上一節中待上傳文件可能的四種狀態及對覆蓋上傳的影響:
- 文件存在:提示用戶
- 文件或目錄不存在:直接上傳
- 目錄存在但為空:直接上傳,最終文件將位於目錄下
- 目錄存在且不為空:需要繼續判斷目錄下有無同名文件
- 文件存在:提示用戶
- 文件或目錄不存在:直接上傳
- 目錄存在:直接上傳,最終將和該目錄並列位於父目錄之下
1 # $1: dest path
2 #
3 # return value
4 # 0: user allow and delete existing remote file
5 # 1: user disallow and do nothing
6 bsttool_duplicate_warning()
7 {
8 local resp
9 echo -n "dest file exist, do you want to cover it? (n/y) "
10 read resp
11 case "$resp" in
12 "y"|"Y")
13 # remove remote reource first
14 bst_tool delete "$1"
15 return 0
16 ;;
17 *)
18 return 1
19 ;;
20 esac
21 }
22
23 # $1: local file path
24 # $2: remote file path (optional)
25 bsttool_replace ()
26 {
27 if [ $# -lt 1 ]; then
28 echo "Usage: bstput src [dst]"
29 return 1
30 fi
31
32 local src=$1
33 local dst="/"
34 if [ $# -gt 1 ]; then
35 dst=$2
36 fi
37
38 # first check dest existence
39 local resp
40 local name=${src##*/} # remove path part, want name only
41 if [ -z "$name" ]; then
42 name="$src"
43 fi
44
45 local path=$dst/$name
46 bsttool_query_path "$dst"
47 local res=$?
48 case $res in
49 0)
50 # not exist
51 echo "dir/file not exist, start uploading"
52 ;;
53 1)
54 # file exist
55 bsttool_duplicate_warning "$dst"
56 if [ $? -ne 0 ]; then
57 return 1
58 fi
59 ;;
60 2)
61 # empty dir exist
62 echo "dir exist but empty, start uploading"
63 ;;
64 3)
65 # dir exist and not empty
66 # try file again
67 echo "dir exist, try file"
68 bsttool_query_path "$path"
69 res=$?
70 if [ $res -eq 1 ]; then
71 # dest file exist, warning...
72 bsttool_duplicate_warning "$path"
73 if [ $? -ne 0 ]; then
74 return 1
75 fi
76 fi
77 ;;
78 *)
79 echo "should not reach here !!"
80 return 1
81 ;;
82 esac
83
84 bst_tool put "$src" "$dst"
85 return 0
86 }
下面做個簡單說明:
- line 1-21:衝突提醒,並獲取用戶輸入,如果用戶確認覆蓋,則在 put 前調用 delete 刪除之;
- line 27-36: 進入正文,檢查並獲取輸入參數;
- line 38-47: 檢查目標文件是否存在及屬性;
- line 49-52: 不存在,可以上傳;
- line 53-59: 存在,調提醒函數獲取用戶輸入,如果用戶拒絕覆蓋,退出;否則繼續;
- line 60-63:目錄存在但為空,可以上傳;
- line 64-69:目錄存在且不為空,繼續判斷子目錄;
- line 70-76:子目錄中存在同名文件,調提醒函數獲取用戶輸入,如果用戶拒絕覆蓋,退出;否則繼續;
- line 84: 如果能走到這裡,說明前面沒有文件名稱衝突、或用戶同意覆蓋文件且後台文件已被清理,執行上傳。
為了簡化調用,還可以使用 alias 重命名上面的 shell function:
# alias
alias bstput=bsttool_replace
alias bstget='bst_tool get'
alias bstdel='bst_tool delete'
alias bstls='bst_tool ls'
將上面的腳本保存在 ~/.bash_bst 下並在 shell 配置文件中寫入這樣一行配置:
source ~/.bash_bst
這樣我就可以在命令行使用 bstxx 系列命令代替笨重的 bst_tool xxx 了 (後者仍可用),而且這套 alias 拓展了原命令的功能,使用 bstput 就可以實現覆蓋上傳啦,下面是執行效果:
$ bstput data /data
dir or file exist, try download to check
dir exist, try file
dir or file exist, try download to check
dest file exist, do you want to cover it? (n/y) y
當前用戶:yunhai
deleting the 1th filePath: /data/data
delete result: 成功
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 419402686
FilePath: /data/data
Time: 2021-05-14 19:49:20
將本地的 data 文件上傳到後台 /data,而後台現在已經有 /data/data 的文件,所以這裡判斷出來有衝突,提示用戶是否覆蓋,得到授權後,刪除後台文件後上傳成功。
整個過程調用了兩次 bsttool_query_path,第一次針對 /data (total size == 1),確定它是一個目錄;第二次針對 /data/data (total size == 1),確定它是一個文件。
覆蓋下載
覆蓋下載就相對簡單多了,因為要判斷是否重複的文件位於本地,可以動用的手段就豐富了。下面直接上腳本:
1 # $1: local path
2 #
3 # return value
4 # 0: user allow and delete existing local file
5 # 1: user disallow and do nothing
6 bsttool_existence_warning()
7 {
8 local resp
9 echo -n "local file exist, do you want to cover it? (n/y) "
10 read resp
11 case "$resp" in
12 "y"|"Y")
13 # remove local reource first
14 rm "$1"
15 return 0
16 ;;
17 *)
18 return 1
19 ;;
20 esac
21 }
22
23
24 # $1: remote file path
25 # $2: local file path (optional)
26 bsttool_fetch ()
27 {
28 if [ $# -lt 1 ]; then
29 echo "Usage: bstget remote [local]"
30 return 1
31 fi
32
33 local remote=$1
34 local name=${remote##*/} # remove path part, want name only
35 if [ -z "$name" ]; then
36 name="$remote"
37 fi
38
39 local local="./"
40 if [ $# -gt 1 ]; then
41 local=$2
42 fi
43
44 # check dest existence
45 if [ -e "$local" ]; then
46 if [ -d "$local" ]; then
47 # for dir, test sub file existence
48 if [ -e "$local/$name" -a ! -d "$local/$name" ]; then
49 # dest file exist, warning...
50 bsttool_existence_warning "$local/$name"
51 if [ $? -ne 0 ]; then
52 return 1
53 fi
54 fi
55 else
56 #elif [ -f "$local" ]; then
57 # dest file exist, warning...
58 bsttool_existence_warning "$local"
59 if [ $? -ne 0 ]; then
60 return 1
61 fi
62 fi
63 fi
64
65 bst_tool get "$remote" "$local"
66 return 0
67 }
下面做個簡單說明:
- line 6-21:本地文件存在時輸出的警告資訊,如果用戶同意覆蓋,調用 rm 移除本地同名文件並返回 0,否則返回 1;
- line 28-42:進入正文,檢查並獲取輸入參數,當用戶未提供本地路徑或提供的本地路徑是個目錄時,需要取遠程文件名作為本地文件名,所以這裡有截取本地路徑名的操作;
- line 45:檢查本地文件是否存在,注意這裡使用 -e 來檢查所有文件類型;
- line 46:本地目錄存在,繼續檢查目錄;
- line 48-54:目錄下有同名文件存在 (如果是目錄則沒關係,可以共存),調提醒函數獲取用戶輸入,如果用戶拒絕覆蓋,退出;否則繼續;
- line 57-61:本地文件存在,調提醒函數獲取用戶輸入,如果用戶拒絕覆蓋,退出;否則繼續;
- line 65:執行下載,此時不會衝突。
同 bstput,加入以下內容來簡化命令調用:
alias bstget=bsttool_fetch
下面是執行效果:
$ bstget /data/data
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/data
$ bstget /data/data data
local file exist, do you want to cover it? (n/y) n
$ bstget /data/data test
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/test/data
$ bstget /data/data test
local file exist, do you want to cover it? (n/y) n
$ bstget /data/data test/data
local file exist, do you want to cover it? (n/y) y
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/test/data
第一次下載時都是沒有衝突的,當再次下載時就會提醒了,可以看到針對文件、目錄的情況都能正常處理。
遞歸列出
軟柿子捏完了,再來看遞歸列出、遞歸下載。它們能否實現的關鍵就在於能否區分遠程路徑為目錄,因為對目錄需要遞歸調用 shell 函數做遍歷。這裡如果使用之前判斷遠程文件屬性的 bsttool_query_path 函數就有點兒太重了,其實使用 TYPE 欄位就足夠了,因為我們只是將文件羅列出來就夠了,不需要上傳或下載。那如何獲取文件的 TYPE 欄位呢?讓我們先來看一下 bst_tool ls 的輸出:
$ bstls /ollvm
當前用戶:yunhai
total size: 12
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ FILEID (12) │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
│ 423324279 │ yunhai │ 289.00B │ 2021-05-24 14:28:33 │ file │ build_glibc.sh │
│ 423324280 │ yunhai │ 15.73MB │ 2021-05-24 14:28:33 │ file │ cmake-3.20.0.zip │
│ 423324281 │ yunhai │ 409.00B │ 2021-05-24 14:28:33 │ file │ build_cmake.sh │
│ 423324283 │ yunhai │ 14.28KB │ 2021-05-24 14:28:33 │ file │ bash_bst.txt │
│ 423324284 │ yunhai │ 820.00B │ 2021-05-24 14:28:33 │ file │ create_user.sh │
│ 423324278 │ yunhai │ 2.85MB │ 2021-05-24 14:28:33 │ file │ global-6.6.5.tar.gz │
│ 423324285 │ yunhai │ 535.00B │ 2021-05-24 14:28:33 │ file │ init_bst.sh │
│ 405005475 │ yunhai │ │ 2021-04-07 20:49:02 │ dir │ tutor │
│ 405004793 │ yunhai │ │ 2021-04-07 20:41:01 │ dir │ 81 │
│ 404965309 │ yunhai │ │ 2021-04-07 18:19:08 │ dir │ 8 │
│ 404943904 │ yunhai │ │ 2021-04-07 17:46:10 │ dir │ 4 │
│ 404938470 │ yunhai │ │ 2021-04-07 17:37:02 │ dir │ common │
│──────────────│──────────│──────────│─────────────────────│──────│─────────────────────│
可以看到目錄的 TYPE 為 dir,普通文件為 file;除了這兩個欄位,為了給用戶列出文件詳情,我們還需要獲取 FILENAME / FILESIZE / FILEID 三個欄位,為此最好是一次性從輸出中獲取它們,以分隔符劃分並獲取各個欄位的辦法,我第一個想到的就是 awk:
$ bstls /ollvm | sed -n '6p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, $4, $6, $7)}'
423324279 289.00B file build_glibc.sh
這段腳本先提取輸出中的一行 (sed -n ‘6p’),這裡數據從第6行開始,所以必需大於等於6;然後使用豎線分隔該行各個欄位並通過 $n 列印需要的欄位,注意這裡的豎線不是普通的 『|』,而是更大更長的豎線,這個我真不知道怎麼從鍵盤上敲出來,最後還是從 bst 的輸出中複製的才搞定。由於我們只關心第 1 / 3 / 5 / 6 列,所以下標選擇了 2 / 4 / 6 / 7,這是因為分隔後下標為 1 的第一列對應是個空列,需要跳過。如果我們用這個腳本跑一下目錄,它能否正確輸出呢?
$ bstls /ollvm | sed -n '14p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, $4, $6, $7)}'
405004793 dir 81
第一個目錄位於 14 行,所以需要更新 sed 參數為 14p,後面的不變。可以看到由於目錄沒有 FILESIZE 欄位,導致輸出後只有三項,這樣一來當我們繼續提出的時候,就會少了一列,和上面文件的格式不統一了,有什麼辦法可以為空欄位補零的嗎?答案是使用 awk 的條件判斷語句:
$ bstls /ollvm | sed -n '14p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}'
405004793 0 dir 81
awk 中的可以用 match 表達式來進行正則匹配,如果第 4 列滿足文件大小的正則表達式 ([a-zA-Z0-9.]+),那麼就使用對應的欄位,否則使用 0 代替。可以看到新的輸出中包含了4 個欄位,第 2 個欄位正確的補零了。ok,有了這個基礎,再怎麼將它們賦值給 shell 的變數呢?最簡單的辦法,還是使用 awk,將想要賦值的欄位 print 出來,類似這樣:
$ filesize=`bstls /ollvm | sed -n '7p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf $2}'`
$ echo $filesize
15.73MB
這次以第 7 行的文件數據為例,將之前的 awk 輸出再經過 awk 過濾一次,得到的值賦值給 shell 變數,再列印變數即可,可以看到列印出的結果是符合預期的。由於這裡使用的是默認的空格和 TAB 鍵分隔,所以不需要特別指定 awk 的分隔符,從這裡也可以看出來上面對目錄大小為空的處理是必要的,不然空列會直接被忽略,後面的欄位就對不上了。但是這樣一個一個 print 的缺點是效率太低了,提取 4 個欄位就需要執行 4 次賦值,有沒有辦法一次提取 4 個欄位呢?答案就是使用 eval:
$ eval `bstls /ollvm | sed -n '7p' | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}'`
$ echo -e "fid=$fid\tsize=$size\ttype=$type\tname=$name"
fid=423324280 size=15.73MB type=file name=cmake-3.20.0.zip
eval 命令接收一個字元串,並將這個串按 shell 語法去解釋並應用於當前 shell;所以問題的關鍵變成如何構造一個 shell 語句來實現多個欄位的同時賦值,於是就有了後面這個 awk 語句:
awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}'
它將我們之前得到的輸出以 shell 變數賦值的方式輸出出來,再交給 eval 去『評估』,這樣就完成了整個賦值過程,於是我們看到,在後面 echo 語句中列印這些變數時,得到了正確的輸出。 ok,有了這些做鋪墊就可以正式亮出遞歸列出的程式碼了:
1 # $1: remote path
2 # $2: depth
3 bsttool_list_recur ()
4 {
5 local remote=$1
6 local depth=$(($2+1))
7 local res=$(bst_tool ls "$remote")
8 local line=$(echo "$res" | sed -n '2p')
9 local str=${line/"total size"//}
10 local ret=0
11 if [ "$str" != "$line" ]; then
12 # normal return
13 ret=$(echo "$line" | awk '{print $NF}')
14 else
15 # server response error
16 ret=0
17 fi
18
19 # 6: skip header
20 local max=$(($ret + 6 - 1))
21 local fid=""
22 local size="" # has K/M/G postfix
23 local type=""
24 local name=""
25 echo "$remote [$depth-$ret]:"
26 for n in $(seq 6 $max)
27 do
28 # why sed -n "$np" don't work ?
29 line=$(echo "$res" | sed -n "$n"'p')
30 # row: fid, user, size, timestamp, type, name
31 # if size is empty (for dir, all spaces), using 0 instead
32 # do awk twice to remove redundant space
33 #
34 # add quotation for assignment is important, otherwise we will get errors on bracket (etc) in value
35 eval $(echo "$line" | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=\"%s\";size=\"%s\";type=\"%s\";name=\"%s\";\n", $1,$2,$3,$4)}')
36 if [ "$type" == "dir" ]; then
37 # do for sub dirs
38 local path=""
39 if [ ${remote:0-1} == "/" ]; then
40 path="$remote$name"
41 else
42 path="$remote/$name"
43 fi
44
45 bsttool_list_recur "$path" $depth
46 else
47 echo -e "$fid:\t$size,\t$name"
48 fi
49 done
50
51 return 0
52 }
53
54 # $1: remote path
55 bsttool_list_all ()
56 {
57 local remote="/"
58 if [ $# -gt 0 ]; then
59 remote=$1
60 fi
61
62 bsttool_list_recur "$remote" 0
63 }
下面做個簡單說明:
- line 7:調用 bst_tool ls 列出遠程根目錄;
- line 8-17:獲取輸出文件項數量,如果後台應答有錯誤,則按沒有輸出處理;
- line 20-26:聲明變數,跳過表頭,準備遍歷文件項;
- line 29:截取某一行文件數據;
- line 35:獲取該行中各個欄位並賦值給變數;
- line 36-45:對於目錄項,遞歸列出其中文件;
- line 47:對於文件項,直接列出詳情;
- line 55-63:處理初始情況 (確定根目錄、給定深度),開始遞歸。
同上,加入以下內容來簡化命令調用:
alias bstrls=bsttool_list_all
在 bst 和 ls 之間有一個 ‘r’ 字元,表示遞歸。下面是執行效果:
$ bstrls /ollvm
/ollvm [1-12]:
423324279: 289.00B, build_glibc.sh
423324280: 15.73MB, cmake-3.20.0.zip
423324281: 409.00B, build_cmake.sh
423324283: 14.28KB, bash_bst.txt
423324284: 820.00B, create_user.sh
423324278: 2.85MB, global-6.6.5.tar.gz
423324285: 535.00B, init_bst.sh
/ollvm/tutor [2-5]:
405006643: 1.32KB, build_ollvm.sh
405005476: 819.00B, build_skeleton.sh
404994390: 310.00B, test.c
403551141: 248.28KB, llvm-pass-tutorial-dev.zip
403531758: 24.79KB, llvm-pass-tutorial.tar.gz
/ollvm/81 [2-5]:
405073802: 2.38KB, copy_ollvm81.sh
405004794: 1.81KB, build_llvm81.sh
404314927: 44.50KB, PassManagerBuilder.cpp
403560689: 12.22MB, cfe-8.0.1.src.tar.xz
403560381: 29.07MB, llvm-8.0.1.src.tar.xz
/ollvm/8 [2-3]:
406677215: 1.66KB, copy_ollvm8.sh
406677122: 531.00B, build_ollvm8.sh
403203783: 83.16MB, obfuscator-llvm-8.0.zip
/ollvm/4 [2-3]:
404951254: 1.30KB, copy_ollvm4.sh
404943908: 320.00B, build_ollvm4.sh
402300052: 180.29MB, obfuscator.tar.gz
/ollvm/common [2-7]:
406687742: 3.45KB, compare_instructions.sh
406687716: 1.21KB, inject_so.sh
406678282: 899.00B, clone_netdisk.sh
406678068: 401.00B, download_ndk.sh
404942152: 181.00B, build_p2psrc.sh
404940645: 748.00B, clone_p2psrc.sh
402360890: 819.91MB, ndkr20.zip
對於目錄,沒有列出 FILEID 和 FILESIZE 欄位,代之以目錄深度和文件數量 ([x-x]);文件項的話個人覺得反而比原版命令清爽一些,嗯就這樣了。使用 bstrls + grep,查找一個文件在不在後台就變得容易多了,不過總體執行速度堪憂,考慮到一個命令下去,底層執行了 N 多次 bst_tool 命令,情有可原~
遞歸下載
剩下最後一根硬骨頭了,不過有了遞歸列出的基礎,遞歸下載也沒那麼難了。與覆蓋下載遇到相同的問題是需要提前判斷文件是否已經在本地存在,防止意外覆蓋數據;不同的點是,遞歸下載可能會有多次覆蓋提醒,如果每次都要讓用戶選擇,那也不是不行,畢竟有 yes 這種工具,不過這種工具是提前設置好了 yes 或 no 的選項,沒有辦法隨機應變,想要靈活性與便利性都具備,最好還是自己處理選項,最簡單的辦法是用大寫字母表示應用全部,小寫表示只應用一次:
- y:允許一次
- Y:全部允許
- n:禁止一次
- N:全部禁止
那麼這個選項變數能作為 shell 函數的參數傳遞嗎?答案是不能,因為雖然可以向下傳遞,但是如果用戶在深層調用中改變了選擇,這個變化卻不能向上傳遞,畢竟 shell 函數參數只能作為輸入參數,不能像 c / c++ / java 那種高級程式設計語言一樣可以有輸出參數,如果使用 shell 函數的 return 語句作為輸出,那麼它本身的返回碼又不能用了。綜合考慮,最後用戶輸入的選項作為全局變數,在遞歸開始前設置為默認值,在遞歸過程中改變,以便影響後續的判斷過程。
另外一個不同點是,隨著目錄的深入、函數的遞歸,需要維護好當前工作目錄,保證每次創建文件時所處的目錄是正確的,當目錄遍歷結束返回上層時 (或函數遞歸調用結束返回調用點),需要返回上級目錄。對於遍歷過程,直接 cd .. 就可以了,但是對於首次進入遞歸,需要保存當前工作目錄 (PWD),因為下載目錄可能是個多級目錄,一次 cd .. 是回不來的。
1 # global variable
2 # we can NOT pass these as recursive function parameters,
3 # as shell function can NOT pass modification back to callee.
4 #
5 # cover flag (1:no once 2:no all 3:yes once 4:yes all)
6 g_cover_flag=1
7
8 # return value
9 # 0: invalid input
10 # 1: no once
11 # 2: no all
12 # 3: yes once
13 # 4: yes all
14 bsttool_duplicate_dir_warning()
15 {
16 local resp
17 echo -n "dest file/dir exist, do you want to cover it? (n/N/y/Y) "
18 read resp
19 case "$resp" in
20 "n")
21 return 1
22 ;;
23 "N")
24 return 2
25 ;;
26 "y")
27 return 3
28 ;;
29 "Y")
30 return 4
31 ;;
32 *)
33 return 0
34 ;;
35 esac
36 }
37
38 # $1: remote path
39 # $2: local path (optional)
40 bsttool_get_all ()
41 {
42 if [ $# -lt 1 ]; then
43 echo "Usage: bstrget remote [local]"
44 return 1
45 fi
46
47 local remote=$1
48 local local=""
49 local type=0
50 g_cover_flag=1 # no once
51 local dirold=""
52 if [ $# -gt 1 ]; then
53 local=$2
54 else
55 # get name part
56 local tmp=""
57 if [ ${remote:0-1} == "/" ]; then
58 # cut tailing '/'
59 tmp="${remote:0:$((${#remote}-1))}"
60 else
61 tmp="$remote"
62 fi
63
64 local="${tmp##*/}"
65 fi
66
67 if [ -z "$local" -o "$local" == "/" ]; then
68 # default name
69 local="setup"
70 fi
71
72 bsttool_query_path "$remote"
73 type=$?
74 case $type in
75 0)
76 # not exist
77 echo "remote not exist"
78 return 1
79 ;;
80 1)
81 # file exist
82 # do nothing, leave file to bsttool_get_recur
83 ;;
84 2)
85 # empty dir exist
86 echo "nothing to download"
87 return 1
88 ;;
89 3)
90 # dir exist
91 if [ ! -d "$local" ]; then
92 mkdir -p "$local"
93 else
94 bsttool_duplicate_dir_warning
95 g_cover_flag=$?
96 # invalid or no (all)
97 if [ $g_cover_flag -eq 0 -o $g_cover_flag -eq 1 -o $g_cover_flag -eq 2 ]; then
98 return 1
99 fi
100 fi
101
102 dirold="$PWD"
103 cd "$local"
104 ;;
105 *)
106 echo "should not reach here !"
107 return 1
108 ;;
109 esac
110 111 bsttool_get_recur "$remote" "$local" 0
112 if [ ! -z "$dirold" ]; then
113 cd "$dirold"
114 fi
115 return 0
116 }
下面做個簡單說明:
- line 6:全局用戶覆蓋選項變數;
- line 14-36:用於獲取用戶選擇的提醒函數;
- line 42-70:進入正文,獲取並檢查輸入參數,如果沒有提供本地下載路徑,默認為當前目錄;文件名為遠程路徑的文件名部分,如果遠程路徑為根目錄,本地下載目錄默認為 setup;
- line 72-73:查詢遠程路徑屬性,bsttool_query_path 在之前的覆蓋上傳中有過介紹;
- line 75-79:遠程路徑不存在,直接退出,退出碼為 1;
- line 80-83:遠程文件存在,什麼也不做,留給後面的遞歸函數處理;
- line 84-88:遠程目錄為空,直接退出,退出碼為 1;
- line 89-104:遠程目錄存在且不為空,如果本地路徑不存在,遞歸創建之;否則提示用戶是否覆蓋該目錄,如果用戶選擇否,直接退出,退出碼為 1。如果用戶選擇覆蓋或目錄是新創建的,則記錄舊目錄,切換到下載目錄,準備開始遞歸下載;
- line 105-108:後台出錯,直接退出,退出碼為 1;
- line 111:啟動遞歸下載;
- line 112-114:恢復初始工作目錄。
重頭戲都放在了 bsttool_get_recur 中:
1 # $1: remote path
2 # $2: local path
3 # $3: depth
4 bsttool_get_recur ()
5 {
6 local remote=$1
7 local local=$2
8 local depth=$(($3+1))
9
10 local res=$(bst_tool ls "$remote")
11 local line=$(echo "$res" | sed -n '2p')
12 local str=${line/"total size"//}
13 local ret=0
14 if [ "$str" != "$line" ]; then
15 # normal return
16 ret=$(echo "$line" | awk '{print $NF}')
17 else
18 # server response error
19 ret=0
20 fi
21
22 # 6: skip header
23 local max=$(($ret + 6 - 1))
24 local fid=""
25 local size="" # has K/M/G postfix
26 local type=""
27 local name=""
28 local path=""
29 local filesize=0
30 echo "$remote [$depth-$ret]:"
31 for n in $(seq 6 $max)
32 do
33 # why sed -n "$np" don't work ?
34 line=$(echo "$res" | sed -n "$n"'p')
35 # row: fid, user, size, timestamp, type, name
36 # if size is empty (for dir, all spaces), using 0 instead
37 # do awk twice to remove redundant space
38 eval $(echo "$line" | awk -F'│' '{printf ("%s\t%s\t%s\t%s\n", $2, match($4, /[a-zA-Z0-9.]+/)?$4:"0", $6, $7)}' | awk '{printf ("fid=%s;size=%s;type=%s;name=%s;\n", $1,$2,$3,$4)}')
39 if [ "$type" == "dir" ]; then
40 if [ -d "$name" ]; then
41 echo "dir exist: $name"
42 if [ $g_cover_flag -eq 1 -o $g_cover_flag -eq 3 ]; then
43 bsttool_duplicate_dir_warning
44 g_cover_flag=$?
45 fi
46
47 # do NOT rm dirs even if user select cover it !
48 # just merge files into it ..
49 #
50 # if not yes once or all, then nothing to do
51 if [ $g_cover_flag -ne 3 -a $g_cover_flag -ne 4 ]; then
52 echo "nothing to do"
53 continue
54 fi
55 else
56 mkdir "$name"
57 fi
58
59 cd "$name"
60 # do for sub dirs
61 if [ ${remote:0-1} == "/" ]; then
62 path="$remote$name"
63 else
64 path="$remote/$name"
65 fi
66
67 bsttool_get_recur "$path" "$name" $depth
68 cd ..
69 else
70 echo -e "$fid:\t$size,\t$name"
71 if [ -f "$name" ]; then
72 echo "file exist: $name"
73 if [ $g_cover_flag -eq 1 -o $g_cover_flag -eq 3 ]; then
74 bsttool_duplicate_dir_warning
75 g_cover_flag=$?
76 fi
77
78 if [ $g_cover_flag -eq 3 -o $g_cover_flag -eq 4 ]; then
79 echo "remove old file before download"
80 rm "$name"
81 else
82 # no need to download
83 echo "nothing to do"
84 continue
85 fi
86 fi
87
88 if [ ${remote:0-1} == "/" ]; then
89 path="$remote$name"
90 else
91 path="$remote/$name"
92 fi
93
94 bst_tool get "$path" "$name"
95 fi
96 done
97
98 return 0
99 }
前面獲取資訊這部分和遞歸列出非常類似,就不贅述了,下面主要對下載這一部分做個簡單說明 (line 39+):
- line 39-54:如果是目錄,且本地已存在,且用戶之前未選擇全部應用 (no once 或 yes once),則提醒用戶是否覆蓋,如果用戶選擇了不覆蓋,或者之前用戶選擇的是 no all,則跳過該目錄,繼續處理下個文件項;
- line 56:如果目錄不存在,則創建之;
- line 59-68:切換到新目錄,構建新的路徑,調用自身遞歸處理目錄中的文件項,處理完畢後回退到上級目錄;
- line 70-86:輸出下載文件的詳細資訊,如果文件已存在,且用戶之前未選擇全部應用 (no once 或 yes once),則提示用戶是否覆蓋文件,如果用戶選擇了不覆蓋,或者之前用戶選擇的是 no all,則跳過該文件,繼續處理下個文件項。否則刪除本地同名文件,防止之後下載時產生衝突;
- line 88-94:構建下載文件路徑,啟動 bst_tool 下載文件。
在使用這個工具做文件備份的時候,我發現一個新的需求點,就是我只希望備份自己寫的腳本文件,一些安裝包、壓縮包等較大的文件可以從網上下載,沒必要備份,但是這個工具一次性全下載下來了,既佔用空間,又浪費頻寬。自然而然想到的解決方案就是通過文件尺寸來過濾下載項,只有小於閾值的文件才被下載,和覆蓋選項一樣,我們希望它能在靈活性和便利性上能達到兼顧,於是依葫蘆畫瓢,再整一個下載限制的選項:
1 # global variable
2 # we can NOT pass these as recursive function parameters,
3 # as shell function can NOT pass modification back to callee.
4 #
5 # cover flag (1:no once 2:no all 3:yes once 4:yes all)
6 g_cover_flag=1
7 # size limit flag (1:no once 2:no all 3:yes once 4:yes all)
8 g_limit_flag=1
9 g_size_limit=1 # MB
10
11 # return value
12 # 0: invalid input
13 # 1: no once
14 # 2: no all
15 # 3: yes once
16 # 4: yes all
17 bsttool_huge_file_warning()
18 {
19 local resp
20 echo -n "do you want to continue download this large file? (n/N/y/Y) "
21 read resp
22 case "$resp" in
23 "n")
24 return 1
25 ;;
26 "N")
27 return 2
28 ;;
29 "y")
30 return 3
31 ;;
32 "Y")
33 return 4
34 ;;
35 *)
36 return 0
37 ;;
38 esac
39 }
40
41 # $1: remote path
42 # $2: local path (optional)
43 bsttool_get_all ()
44 {
45 ……
46 g_limit_flag=1 # no once
47 g_size_limit=$((1024*1024)) # in bytes
48 bsttool_get_recur "$remote" "$local" 0
49 if [ ! -z "$dirold" ]; then
50 cd "$dirold"
51 fi
52 }
它由兩個選項組成,其中 g_limit_flag 表示用戶的選擇,通過調用 bsttool_huge_file_warning 詢問用戶獲取,後者和覆蓋選項幾乎完全相同; g_size_limit 表示文件尺寸閾值,這個目前固定為 1 MB,後續可更改為通過環境變數設置。有了變數的定義和初值,就可以在遞歸函數中進行判斷了:
1 echo -e "$fid:\t$size,\t$name"
2 # contains KB?
3 local kfactor=${size/"KB"//}
4 if [ "$kfactor" != "$size" ]; then
5 kfactor=1024
6 else
7 kfactor=1
8 fi
9
10 # contains MB?
11 local mfactor=${size/"MB"//}
12 if [ "$mfactor" != "$size" ]; then
13 mfactor=$((1024*1024))
14 else
15 mfactor=1
16 fi
17
18 # contains GB?
19 local gfactor=${size/"GB"//}
20 if [ "$gfactor" != "$size" ]; then
21 gfactor=$((1024*1024*1024))
22 else
23 gfactor=1
24 fi
25
26 # should we support TB?
27 filesize=$(awk -v val="$size" -v k="$kfactor" -v m="$mfactor" -v g="$gfactor" 'BEGIN{ printf ("%d", strtonum(val)*k*m*g) }')
28 if [ $filesize -gt $g_size_limit ]; then
29 echo "file too huge: $g_limit_flag"
30 if [ $g_limit_flag -eq 1 -o $g_limit_flag -eq 3 ]; then
31 bsttool_huge_file_warning
32 g_limit_flag=$?
33 fi
34
35 # if not yes once or all, then nothing to do
36 if [ $g_limit_flag -ne 3 -a $g_limit_flag -ne 4 ]; then
37 # no need to download
38 echo "nothing to do"
39 continue
40 fi
41 fi
下面做個簡單說明:
- line 1:當下載的文件類型為普通文件時,已經提取到了文件的尺寸資訊,不過這個資訊是以各種不同單位結尾的字元串,單位有 KB/MB/GB;
- line 2-27:將它們統一轉換為位元組為單位,這裡使用 awk strtonum 來將字元串轉為 double 精度數字,strtonum 會自動忽略不能轉換為數字的單位部分。另外通過檢查有無單位關鍵字來確定使用的乘積因子,最後使用 awk 的乘法來獲取最終以位元組為單位 double 精度的乘積結果。這裡有幾點需要注意:
- 使用 strtonum 將字元串轉換為數字;
- 向 awk 傳遞 shell 變數 (-v);
- awk 的乘除默認是 double 精度的,反而想要整數乘除結果會比較費勁;
- line 28-41:當文件尺寸換算為位元組數超過閾值時,且用戶之前未選擇全部應用 (no once 或 yes once),則提示用戶是否下載大文件,如果用戶選擇了不下載,或者之前用戶選擇的是 no all,則跳過該文件,繼續處理下個文件項。
同上,加入以下內容來簡化命令調用:
alias bstrget=bsttool_get_all
ok,至此一個比較實用的備份腳本工具做好了,我們用它來備份一下剛才的目錄:
$ bstrget /ollvm
/ollvm [1-12]:
423324279: 289.00B, build_glibc.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/build_glibc.sh
423324280: 15.73MB, cmake-3.20.0.zip
file too huge: 1
do you want to continue download this large file? (n/N/y/Y) n
nothing to do
423324281: 409.00B, build_cmake.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/build_cmake.sh
423324283: 14.28KB, bash_bst.txt
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/bash_bst.txt
423324284: 820.00B, create_user.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/create_user.sh
423324278: 2.85MB, global-6.6.5.tar.gz
file too huge: 1
do you want to continue download this large file? (n/N/y/Y) y
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/global-6.6.5.tar.gz
423324285: 535.00B, init_bst.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/init_bst.sh
/ollvm/tutor [2-5]:
405006643: 1.32KB, build_ollvm.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/build_ollvm.sh
405005476: 819.00B, build_skeleton.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/build_skeleton.sh
404994390: 310.00B, test.c
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/test.c
403551141: 248.28KB, llvm-pass-tutorial-dev.zip
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/llvm-pass-tutorial-dev.zip
403531758: 24.79KB, llvm-pass-tutorial.tar.gz
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/tutor/llvm-pass-tutorial.tar.gz
/ollvm/81 [2-5]:
405073802: 2.38KB, copy_ollvm81.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/copy_ollvm81.sh
405004794: 1.81KB, build_llvm81.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/build_llvm81.sh
404314927: 44.50KB, PassManagerBuilder.cpp
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/81/PassManagerBuilder.cpp
403560689: 12.22MB, cfe-8.0.1.src.tar.xz
file too huge: 3
do you want to continue download this large file? (n/N/y/Y) N
nothing to do
403560381: 29.07MB, llvm-8.0.1.src.tar.xz
file too huge: 2
nothing to do
/ollvm/8 [2-3]:
406677215: 1.66KB, copy_ollvm8.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/8/copy_ollvm8.sh
406677122: 531.00B, build_ollvm8.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/8/build_ollvm8.sh
403203783: 83.16MB, obfuscator-llvm-8.0.zip
file too huge: 2
nothing to do
/ollvm/4 [2-3]:
404951254: 1.30KB, copy_ollvm4.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/4/copy_ollvm4.sh
404943908: 320.00B, build_ollvm4.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/4/build_ollvm4.sh
402300052: 180.29MB, obfuscator.tar.gz
file too huge: 2
nothing to do
/ollvm/common [2-7]:
406687742: 3.45KB, compare_instructions.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/compare_instructions.sh
406687716: 1.21KB, inject_so.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/inject_so.sh
406678282: 899.00B, clone_netdisk.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/clone_netdisk.sh
406678068: 401.00B, download_ndk.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/download_ndk.sh
404942152: 181.00B, build_p2psrc.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/build_p2psrc.sh
404940645: 748.00B, clone_p2psrc.sh
當前用戶:yunhai
download success. local path: /home/yunh/code/bstext/ollvm/common/clone_p2psrc.sh
402360890: 819.91MB, ndkr20.zip
file too huge: 2
nothing to do
輸出中高亮的部分表示我實際輸入的選擇,第一個 15 MB 多的文件 no once;第二個 3 MB 多的文件 yes once;第三個 12 MB 多的文件 no all。之後凡是大於 1 MB 的文件就自動跳過了,可以看到 file too huge 輸出的就是。檢查下載目錄後,確實如此:
$ ls -lhR ollvm
ollvm:
total 3.0M
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 4
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 8
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 81
-rw-rw-r-- 1 yunh yunh 15K May 24 20:49 bash_bst.txt
-rw-rw-r-- 1 yunh yunh 409 May 24 20:49 build_cmake.sh
-rw-rw-r-- 1 yunh yunh 289 May 24 20:48 build_glibc.sh
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 common
-rw-rw-r-- 1 yunh yunh 820 May 24 20:49 create_user.sh
-rw-rw-r-- 1 yunh yunh 2.9M May 24 20:49 global-6.6.5.tar.gz
-rw-rw-r-- 1 yunh yunh 535 May 24 20:49 init_bst.sh
drwxrwxr-x 2 yunh yunh 4.0K May 24 20:49 tutor
ollvm/4:
total 8.0K
-rw-rw-r-- 1 yunh yunh 320 May 24 20:49 build_ollvm4.sh
-rw-rw-r-- 1 yunh yunh 1.4K May 24 20:49 copy_ollvm4.sh
ollvm/8:
total 8.0K
-rw-rw-r-- 1 yunh yunh 531 May 24 20:49 build_ollvm8.sh
-rw-rw-r-- 1 yunh yunh 1.7K May 24 20:49 copy_ollvm8.sh
ollvm/81:
total 56K
-rw-rw-r-- 1 yunh yunh 1.9K May 24 20:49 build_llvm81.sh
-rw-rw-r-- 1 yunh yunh 2.4K May 24 20:49 copy_ollvm81.sh
-rw-rw-r-- 1 yunh yunh 45K May 24 20:49 PassManagerBuilder.cpp
ollvm/common:
total 24K
-rw-rw-r-- 1 yunh yunh 181 May 24 20:49 build_p2psrc.sh
-rw-rw-r-- 1 yunh yunh 899 May 24 20:49 clone_netdisk.sh
-rw-rw-r-- 1 yunh yunh 748 May 24 20:49 clone_p2psrc.sh
-rw-rw-r-- 1 yunh yunh 3.5K May 24 20:49 compare_instructions.sh
-rw-rw-r-- 1 yunh yunh 401 May 24 20:49 download_ndk.sh
-rw-rw-r-- 1 yunh yunh 1.3K May 24 20:49 inject_so.sh
ollvm/tutor:
total 292K
-rw-rw-r-- 1 yunh yunh 1.4K May 24 20:49 build_ollvm.sh
-rw-rw-r-- 1 yunh yunh 819 May 24 20:49 build_skeleton.sh
-rw-rw-r-- 1 yunh yunh 249K May 24 20:49 llvm-pass-tutorial-dev.zip
-rw-rw-r-- 1 yunh yunh 25K May 24 20:49 llvm-pass-tutorial.tar.gz
-rw-rw-r-- 1 yunh yunh 310 May 24 20:49 test.c
perfect! 如果我重新執行一遍上面的命令,還可以看到覆蓋選項的使用,這裡出於篇幅考慮就不再羅列了。
後記
其實還可以實現目錄的遞歸上傳功能,技術上不存在任何障礙,只是對我來說意義不大,就沒有做。
在測試的過程中, 我還發現一個腳本的 bug,就是當目錄中包含兩個同名文件時 (一個是普通文件,一個是目錄),則在 bst_tool ls name 時,將優先輸出目錄的內容,和目錄是否為空、目錄和文件的創建先後順序都無關。那麼在之前 bsttool_query_path 中對遠程路徑進行判斷時,就有可能出問題 (將上傳的目標文件理解為目錄)。例如有下面的文件結構:
$ bst_tool ls /tmp/
當前用戶:yunhai
total size: 2
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ FILEID │ USERNAME │ FILESIZE │ TIME │ TYPE │ FILENAME │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
│ 424833297 │ yunhai │ │ 2021-05-26 19:45:06 │ dir │ data │
│ 424829696 │ yunhai │ 16.11KB │ 2021-05-26 19:41:06 │ file │ data │
│───────────│──────────│──────────│─────────────────────│──────│──────────│
當我想將文件上傳到 /tmp 並且命名為 data 時,我會發現實際上傳的路徑是 /tmp/data/xxx 而不是覆蓋 /tmp/data:
$ bstput foo /tmp/data
dir exist but empty, start uploading
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 424841196
FilePath: /tmp/data/foo
Time: 2021-05-26 19:55:53
原因就是我上面說的。這個問題對 bst_tool 也存在,使用同樣的參數調用 bst_tool put 會得到下面的輸出:
$ bst_tool put foo /tmp/data
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 424842183
FilePath: /tmp/data/foo(1)
Time: 2021-05-26 19:57:07
可見是上傳到了同一個位置。那就沒有辦法上傳到 /tmp/data 了嗎? 如果將上傳文件提前重命名為目標文件名,再將遠程路徑改為文件的直屬目錄路徑,是不是就可以了呢?
$ mv foo data
$ bstput data /tmp
dir exist, try file
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 424844325
FilePath: /tmp/data(1)
Time: 2021-05-26 20:00:12
結果是令人失望的,本來期望 bstput 會提醒我們有同名文件衝突是否覆蓋呢,結果直接上傳並重命名了,這就是我開頭說的 bug。讓我們來分析一下為什麼是這個樣子:
- 當 bsttool_query_path 以 /tmp 為參數進行檢查時,發現它是一個非空目錄;
- 繼續檢查它下面是不是有名叫 /tmp/data 的文件,之前我們說過,bst_tool ls /tmp/data 時優先列出目錄內容,於是我們認為這裡有一個同名目錄;
- 由於文件可以和目錄同名,於是我們認為不影響,就繼續調用 bst_tool put 去做上傳;
- 而實際上傳後發現已經有這樣一個文件了,於是將上傳文件改名。
可以看出來問題的關鍵就是,當 bst_tool ls xxx 告訴你這是一個目錄時,有可能是不成立的。此時還可能存在一個同名的文件,從而引發上傳衝突。那這個問題怎麼解決呢?好辦,其實還是用之前 ls 解決不了時上 get 的套路,如果後台僅有一個目錄,此時 bst_tool get xxx 會報錯,否則那個同名文件會被下載下來,這樣就能知道有沒有同名文件了。下面是修補程式程式碼:
1 # normal return
2 ret=$(echo "$line" | awk '{print $NF}')
3 if [ $ret -eq 0 ]; then
4 ret=2
5 elif [ $ret -gt 1 ]; then
6 # a dir
7 #ret=3
8 # at this point, we are NOT sure there is no file with same name exist,
9 # as bst_tool ls list directory with preference than file,
10 # so here we need a download try...
11 echo "dir exist, try download to check existence of file with same name"
12 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
13 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
14 str=${line/"download success"//}
15 if [ "$str" != "$line" ]; then # string replace changed: download success
16 # has same name file
17 ret=1
18 else
19 # only dir
20 ret=3
21 fi
22 else # size=1
23 # note here we can NOT using type file/dir to determine,
24 # for example, there is a path /tmp/tmp pointed to a file
25 # when we ls /tmp, it report one item named 'tmp' with type file,
26 # but actually it is a dir.
27 # so here we use another way to determine, that is, download it,
28 # you can NOT download a dir.
29 echo "dir or file exist, try download to check"
30 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
31 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
32 str=${line/"download success"//}
33 if [ "$str" != "$line" ]; then # string replace changed: download success
34 # is a file
35 ret=1
36 else
37 # is a dir
38 ret=3
39 fi
40 fi
原來的程式碼就是 line 7 了,當 bst_tool ls 返回 size 大於 1 後直接判斷為目錄 (3) 返回; 現在加了 line 11-21,用於繼續判斷有無同名文件。可以看到修補程式程式碼和已有的 line 29-39 程式碼非常類似,所以也可以將它們合併在一起,最終得到:
1 # normal return
2 ret=$(echo "$line" | awk '{print $NF}')
3 if [ $ret -eq 0 ]; then
4 ret=2
5 elif [ $ret -gt 1 ]; then
6 # a dir
7 ret=3
8 # at this point, we are NOT sure there is no file with same name exist,
9 # as bst_tool ls list directory with preference than file,
10 # so here we need a download try...
11 echo "dir exist, try download to check existence of file with same name"
12 else # size=1
13 ret=3
14 # note here we can NOT using type file/dir to determine,
15 # for example, there is a path /tmp/tmp pointed to a file
16 # when we ls /tmp, it report one item named 'tmp' with type file,
17 # but actually it is a dir.
18 # so here we use another way to determine, that is, download it,
19 # you can NOT download a dir.
20 echo "dir or file exist, try download to check"
21 fi
22
23 if [ $ret -eq 3 ]; then
24 rm "$download" > /dev/null 2>&1 # otherwise bst_tool will fail
25 line=$(bst_tool get "$remote" "$download" | sed -n '2p')
26 str=${line/"download success"//}
27 if [ "$str" != "$line" ]; then # string replace changed: download success
28 # is a file
29 ret=1
30 else
31 # is a dir
32 ret=3
33 fi
34 fi
這裡為了避免將 size == 0 的場景也包含進來,在 size > 0 (> 1 及 == 1) 時將 ret 的值臨時設置為 3,之後再通過下載來判斷。下面是加入修補程式後腳本的執行情況:
$ bstput data /tmp
dir exist, try download to check existence of file with same name
dir exist, try file
dir exist, try download to check existence of file with same name
remote file exist, do you want to cover it? (n/y) y
當前用戶:yunhai
deleting the 1th filePath: /tmp/data
delete result: 成功
當前用戶:yunhai
uploading... 100.00 %
upload success:
FileId: 425166451
FilePath: /tmp/data
Time: 2021-05-27 14:56:45
可以看到這回能正確的上傳了。不過這裡也有一些額外代價,看第一行輸出,本來檢查完 /tmp 是目錄就該結束了,但是依照新的邏輯,需要確認沒有一個叫 /tmp 的同名文件,於是又去試下載 /tmp,增加了額外的非必要請求。不過權衡正反兩個方面,為了正確性做的這點性能犧牲還是值得的。
除了腳本的 bug,其實細心的人已經發現,這個工具及其配套的後台也有問題,就拿最開始企業網盤在瀏覽器里那張截圖來說吧,裡面怎麼出現了兩個同名目錄 (/tmp) ? 其實就是我不斷的測試,不知怎麼著觸發了後台的 bug,導致出現了兩個一模一樣的目錄項,在其中一個裡面修改內容,另一個也會跟著變化 (很明確不是文件名重複這樣簡單的問題)。聯繫過相關負責人,給的結論是這個東西已經停止維護,甚至準備下線了,所以也不再接收新的 bug report,當時差點暈倒,得,將就用吧~
結語
做這個命令擴展腳本花了不少心血,不過可能由於工具本身不是開源的緣故,能拿過來直接用的可能性比較低,甚至想跑一跑都沒有環境 (與公司帳號系統綁定)。不過原理都是相通的,腳本本身可以做為一種參考,這裡給出腳本的 github 地址供觀摩:
[email protected]:goodpaperman/bstext.git
如果有幸能跑起來 (說明咱們是一個公司的?),可以定義以下環境變數來改變腳本的行為:
- BST_TOOL_VERBOSE:打開調試輸出,可以看到更多中間細節;
- BST_TOOL_GET_HUGE:下載閾值,單位為 MB,大於此值的文件均略過,不設置的話默認為 1 MB 。
參考
[1]. Shell判斷文件或目錄是否存在
[2]. shell 字元串包含
[4]. shell腳本中如何使用alias
[5]. Linux_shell自動輸入y或yes
[6]. awk使用shell變數及shell使用awk中的變數
[7]. Shell高級語法:awk配合eval實現快速變數