Zsh 開發指南(第十九篇 腳本實例講解)
導讀
本文將講解一些比較簡單的 zsh 腳本實例。
實例一:複製一個目錄的目錄結構
功能:
將一個目錄及它下邊的所有目錄複製到另一個目錄中(即創建同名目錄),但不複製目錄下的其他類型文件。
例子:
src 的目錄結構:src├── a├── b│ ├── 1.txt│ └── 2│ └── 3.txt├── c.txt├── d├── e f│ └── g│ └── 4.txt└── g h -> e f要構造一個 dst 目錄,只包含 src 下的目錄,內容如下:dst└── src ├── a ├── b │ └── 2 ├── d └── e f └── g
思路:
- 首先需要先將 src 目錄下的目錄名篩選出來,可以用 **/*(/) 匹配。
- 然後用 mkdir -p 在 dst 目錄中創建對應的目錄。
# 參數 1:src 目錄# 參數 2:待創建的 dst 目錄#!/bin/zshfor i ($1/**/*(/)) { # -p 參數是遞歸創建目錄,這樣不用考慮目錄的創建順序 mkdir -p $2/$i}
實例二:尋找不配對的文件
功能:
需要當前目錄下有一些 .txt 和 .txt.md5sum 的文件,需要尋找出沒有對應的 .md5sum 文件的 .txt 文件。(實際的場景是尋找已經下載完成的文件,未下載完的文件都對應某個帶後綴的文件。)
例子:
當前目錄的所有文件:aa.txtbb.txtbb.txt.md5sumcc dd.txtcc dd.txt.md5sumee ff.txt.md5sumgg.txthh ii.txt需要找出沒有對應 .md5sum 的 .txt 文件:aa.txtgg.txthh ii.txt
思路:
- 找到所有 .md5sum 文件,然後把文件名中的 .md5sum 去掉,即為那些需要排除的 .txt 文件(a)。
- 所有的文件,排除掉 .m5sum 文件,再排除掉 a,即結果。
實現:
#!/bin/zshall_files=(*)bad_files=(*.md5sum)bad_files+=(${bad_files/.md5sum})# 數組差集操作echo ${all_files:|bad_files}
實例三:用 sed 批量重命名文件
功能:
用形如 sed 命令的用法批量重命名文件。
例子:
# 實現 renamex 命令,接受的第一個參數為 sed 的主體參數,其餘參數是文件列表# 效果是根據 sed 對文件名的修改重命名這些文件% tree.├── aaa_aaa.txt├── aaa.txt├── ccc.txt└── xxx ├── aaa bbb.txt └── bbb ccc.txt% renamex s/aaa/bbb/g **/*aaa_aaa.txt -> bbb_bbb.txtaaa.txt -> bbb.txtxxx/aaa bbb.txt -> xxx/bbb bbb.txt% tree.├── bbb_bbb.txt├── bbb.txt├── ccc.txt└── xxx ├── bbb bbb.txt └── bbb ccc.txt
思路:
- 要找出所有的文件名,然後用 sed 替換成新文件名。
- 如果文件名有變化,用 mv 命令移動
實現:
#!/bin/zsh(($+2)) || { echo Usage: renamex s/aaa/bbb/g *.txt return}for name ($*[2,-1]) { local new_name="$(echo $name | sed $1)" [[ $name == $new_name ]] && continue mv -v $name $new_name}
實例四:根據文件的 md5 刪除重複文件
功能:
刪除當前目錄以及子目錄下所有的重複文件(根據 md5 判斷,不是很嚴謹)。
思路:
- 用 md5sum 命令計算所有文件的 md5。
- 使用哈希表判斷 md5 是否重複,刪除哈希表裡已經有 md5 的後續文件。
實現:
#!/bin/zsh# D 是包含以 . 開頭的隱藏文件local files=("${(f)$(md5sum **/*(.D))}")local files_to_delete=()local -A md5sfor i ($files) { # 取前 32 位,即 md5 的長度 local md5=$i[1,32] if (($+md5s[$md5])) { # 取 35 位之後的內容,即文件路徑,md5 後邊有兩個空格 files_to_delete+=($i[35,-1]) } else { md5s[$md5]=1 }}(($#files_to_delete)) && rm -v $files_to_delete
實例五:轉換 100 以內的漢字數字為阿拉伯數字
功能:
轉換 100 以內的漢字數字為阿拉伯數字,如六十八轉換成 68。
思路:
- 建一個哈希表存放漢字與數字的對應關係。
- 比較麻煩的是「十」,在不同的位置,轉換成的數字不同,需要分別處理。
實現:
#!/bin/zshlocal -A table=(零 0一 1二 2三 3四 4五 5六 6七 7八 8九 9)local resultif [[ $1 == 十 ]] { result=一零} elif [[ $1 == 十* ]] { result=${1/十/一}} elif [[ $1 == *十 ]] { result=${1/十/零}} elif [[ $1 == *十* ]] { result=${1/十}} else { result=$1}for i ({1..$#result}) { result[i]=$table[$result[i]] if [[ -z $result[i] ]] { echo error return 1 }}echo $result運行結果:% ./convert 一1% ./convert 十10% ./convert 十五15% ./convert 二十20% ./convert 五十六56% ./convert 一百error
實例六:為帶中文漢字數字的文件名重命名成以對應數字開頭
功能:
見下邊例子。
例子:
當前目錄有如下文件:Zsh-開發指南(第一篇-變數和語句).mdZsh-開發指南(第七篇-數值計算).mdZsh-開發指南(第三篇-字元串處理之轉義字元和格式化輸出).mdZsh-開發指南(第九篇-函數和腳本).mdZsh-開發指南(第二篇-字元串處理之常用操作).mdZsh-開發指南(第五篇-數組).mdZsh-開發指南(第八篇-變數修飾語).mdZsh-開發指南(第六篇-哈希表).mdZsh-開發指南(第十一篇-變數的進階內容).mdZsh-開發指南(第十七篇-使用-socket-文件和-TCP-實現進程間通信).mdZsh-開發指南(第十三篇-管道和重定向).mdZsh-開發指南(第十九篇-腳本實例講解).mdZsh-開發指南(第十二篇-[[-]]-的用法).mdZsh-開發指南(第十五篇-進程與作業控制).mdZsh-開發指南(第十八篇-更多內置模塊的用法).mdZsh-開發指南(第十六篇-alias-和-eval-的用法).mdZsh-開發指南(第十四篇-文件讀寫).mdZsh-開發指南(第十篇-文件查找和批量處理).mdZsh-開發指南(第四篇-字元串處理之通配符).md需要重命名成這樣:01_Zsh-開發指南(第一篇-變數和語句).md02_Zsh-開發指南(第二篇-字元串處理之常用操作).md03_Zsh-開發指南(第三篇-字元串處理之轉義字元和格式化輸出).md04_Zsh-開發指南(第四篇-字元串處理之通配符).md05_Zsh-開發指南(第五篇-數組).md06_Zsh-開發指南(第六篇-哈希表).md07_Zsh-開發指南(第七篇-數值計算).md08_Zsh-開發指南(第八篇-變數修飾語).md09_Zsh-開發指南(第九篇-函數和腳本).md10_Zsh-開發指南(第十篇-文件查找和批量處理).md11_Zsh-開發指南(第十一篇-變數的進階內容).md12_Zsh-開發指南(第十二篇-[[-]]-的用法).md13_Zsh-開發指南(第十三篇-管道和重定向).md14_Zsh-開發指南(第十四篇-文件讀寫).md15_Zsh-開發指南(第十五篇-進程與作業控制).md16_Zsh-開發指南(第十六篇-alias-和-eval-的用法).md17_Zsh-開發指南(第十七篇-使用-socket-文件和-TCP-實現進程間通信).md18_Zsh-開發指南(第十八篇-更多內置模塊的用法).md19_Zsh-開發指南(第十九篇-腳本實例講解).md
思路:
- 首先需要寫將漢字數字轉成阿拉伯數字的函數。
- 然後需要從文件名中截取漢字數字,然後轉成阿拉伯數字。
- 拼接文件名,然後移動文件。
實現:
#!/bin/zsh# 轉換數字的邏輯和上一個實例一樣local -A table=(零 0一 1二 2三 3四 4五 5六 6七 7八 8九 9)convert() { local result if [[ $1 == 十 ]] { result=一零 } elif [[ $1 == 十* ]] { result=${1/十/一} } elif [[ $1 == *十 ]] { result=${1/十/零} } elif [[ $1 == *十* ]] { result=${1/十} } else { result=$1 } for i ({1..$#result}) { result[i]=$table[$result[i]] if [[ -z $result[i] ]] { echo error return 1 } } echo $result}for i (Zsh*.md) { # -Z 2 是為了在前邊補全一個 0 # 把文件名「第」之前和「篇」之後的全部去除 local -Z 2 num=$(convert ${${i#*第}%篇*}) mv -v $i ${num}_$i}
實例七:統一壓縮解壓工具
功能:
Linux 下常用的壓縮、歸檔格式眾多,參數各異,寫一個用法統一的壓縮解壓工具,用於創建、解壓 .zip .7z .tar .tgz .tbz2 .txz .tar.gz .tar.bz2 .tar.xz .cpio .ar .gz .bz2 .xz 等文件。(類似 atool,但 atool 很久沒更新了,一些新的格式不支持,沒法定製。而且是用 perl 寫的,很難看懂。所以還是決定自己寫一個,只覆蓋 atool的一部分常用功能。)
例子:
# a 用於創建壓縮文件% a a.tgz dir1 file1 file2dir1/file1file2# al 用於列出壓縮文件中的文件列表% al a.tgzdrwxr-xr-x goreliu/goreliu 0 2017-09-13 11:23 dir1/-rw-r--r-- goreliu/goreliu 3 2017-09-13 11:23 file1-rw-r--r-- goreliu/goreliu 3 2017-09-13 11:23 file2# x 用於解壓文件% x a.tgzdir1/file1file2a.tgz -> a# 如果解壓後的文件名或目錄名中當前目錄下已經存在,則解壓到隨機目錄% x a.tgzdir1/file1file2a.tgz -> /tmp/test/x-c4I
思路:
- 壓縮文件時,根據傳入的文件名判斷壓縮文件的格式。
- 解壓和查看壓縮文件內容時,根據傳入的文件名和 file 命令結果判斷壓縮文件的格式。
- 為了復用代碼,多個命令整合到一個文件,然後 ln -s 成多個命令。
實現:
#!/bin/zshget_type_by_name() { case $1 { (*.zip|*.7z|*.jar) echo 7z ;; (*.rar|*.iso) echo 7z_r ;; (*.tar|*.tgz|*.txz|*.tbz2|*.tar.*) echo tar ;; (*.cpio) echo cpio ;; (*.cpio.*) echo cpio_r ;; (*.gz) echo gz ;; (*.xz) echo xz ;; (*.bz2) echo bz2 ;; (*.lzma) echo lzma ;; (*.lz4) echo lz4 ;; (*.ar) echo ar ;; (*) return 1 ;; }}get_type_by_file() { case $(file -bz $1) { (Zip *|7-zip *) echo 7z ;; (RAR *) echo 7z_r ;; (POSIX tar *|tar archive) echo tar ;; (*cpio archive*) echo cpio ;; (*gzip *) echo gz ;; (*XZ *) echo xz ;; (*bzip2 *) echo bz2 ;; (*LZMA *) echo lzma ;; (*LZ4 *) echo lz4 ;; (current ar archive) echo ar ;; (*) return 1 ;; }}(($+commands[tar])) || alias tar=bsdtar(($+commands[cpio])) || alias cpio=bsdcpiocase ${0:t} { (a) (($#* >= 2)) || { echo Usage: $0 target files/dirs return 1 } case $(get_type_by_name $1) { (7z) 7z a $1 $*[2,-1] ;; (tar) tar -cavf $1 $*[2,-1] ;; (cpio) find $*[2,-1] -print0 | cpio -H newc -0ov > $1 ;; (gz) gzip -cv $*[2,-1] > $1 ;; (xz) xz -cv $*[2,-1] > $1 ;; (bz2) bzip2 -cv $*[2,-1] > $1 ;; (lzma) lzma -cv $*[2,-1] > $1 ;; (lz4) lz4 -cv $2 > $1 ;; (ar) ar rv $1 $*[2,-1] ;; (*) echo $1: error return 1 ;; } ;; (al) (($#* >= 1)) || { echo Usage: $0 files return 1 } for i ($*) { case $(get_type_by_name $i || get_type_by_file $i) { (7z|7z_r) 7z l $i ;; (tar) tar -tavf $i ;; (cpio|cpio_r) cpio -itv < $i ;; (gz) zcat $i ;; (xz) xzcat $i ;; (bz2) bzcat $i ;; (lzma) lzcat $i ;; (lz4) lz4cat $i ;; (ar) ar tv $i ;; (*) echo $i: error ;; } } ;; (x) (($#* >= 1)) || { echo Usage: $0 files return 1 } for i ($*) { local outdir=${i%.*} [[ $outdir == *.tar ]] && { outdir=$outdir[1, -5] } if [[ -e $outdir ]] { outdir="$(mktemp -d -p $PWD x-XXX)" } else { mkdir $outdir } case $(get_type_by_name $i || get_type_by_file $i) { (7z|7z_r) 7z x $i -o$outdir ;; (tar) tar -xavf $i -C $outdir ;; (cpio|cpio_r) local file_path=$i [[ $i != /* ]] && file_path=$PWD/$i cd $outdir && cpio -iv < $file_path && cd .. ;; (gz) zcat $i > $outdir/$i[1,-4] ;; (xz) xzcat $i > $outdir/$i[1,-4] ;; (bz2) bzcat $i > $outdir/$i[1,-5] ;; (lzma) lzcat $i > $outdir/$i[1,-6] ;; (lz4) lz4cat $i > $outdir/$i[1,-5] ;; (ar) local file_path=$i [[ $i != /* ]] && file_path=$PWD/$i cd $outdir && ar x $file_path && cd .. ;; (*) echo $i: error ;; } local files=$(ls -A $outdir) if [[ -z $files ]] { rmdir $outdir } elif [[ -e $outdir/$files && ! -e $files ]] { mv -v $outdir/$files . && rmdir $outdir echo $i " -> " $files } else { echo $i " -> " $outdir } } ;; (*) echo error return 1 ;;}
實例八:方便並發運行命令的工具
功能:
我們經常會遇到在循環里批量處理文件的場景(比如將所有 jpg 圖片轉換成 png 圖片),那麼就會遇到一個麻煩:如果在前台處理文件,那同一時間只能處理一個,效率太低;如果在後台處理文件,那麼瞬間就會啟動很多個進程,佔用大量資源,系統難以承受。我們希望的是在同一時間最多同時處理固定數量(比如 10 個)的文件,如果已經達到了這個數量,那麼就先等一會,直到有退出的進程後再繼續。parallel 命令中在一定程度上能滿足這個需求,但用起來太麻煩。
例子:
# rr 是一個函數(可放在 .zshrc 中),直接 rr 加命令即可使用# 命令中支持變數、alias、重定向等等,格式上和直接輸入命令沒有區別% rr sleep 5[4] 5031% rr sleep 5[5] 5032# 如果不加參數,則顯示當前運行的進程數、最大進程並發數和運行中進程的進程號# 默認最大進程並發數是 10% rrrunning/max: 2/10pid: 5031 5032# 5 秒之後,運行結束% rrrunning/max: 0/10# 用 -j 來指定最大進程並發數,指定一次即可,如需修改可再次指定# 可以只調整最大進程並發數而不運行命令% rr -j2 sleep 10[4] 5035% rr sleep 10[5] 5036# 超過了最大進程並發數,等待,並且每一秒檢查一次是否有進程退出# 如果有進程退出,則繼續在後台運行當前命令% rr sleep 10running/max: 2/2, wait 1s ...pid: 5035 5036running/max: 2/2, wait 1s ...pid: 5035 5036[4] - done $*[4] 5039# 實際使用場景,批量將 jpg 圖片轉換成 png 圖片,gm 是 graphicsmagick 中的命令# 轉換圖片格式比較耗時,順序執行的話需要很久% for i (*.jpg) { rr gm convert $i ${i/jpg/png} }[4] 5055[5] 5056[6] 5057[7] 5058[8] 5059[9] 5060[10] 5061[11] 5062[12] 5063[13] 5064running/max: 10/10, wait 1s ...pid: 5060 5061 5062 5063 5064 5055 5056 5057 5058 5059running/max: 10/10, wait 1s ...pid: 5060 5061 5062 5063 5064 5055 5056 5057 5058 5059[11] done $*[5] done $*[5] 5067[12] done $*[11] 5068[6] done $*[6] 5069[12] 5070running/max: 10/10, wait 1s ...pid: 5070 5060 5061 5064 5055 5067 5068 5069 5058 5059[13] - done $*[4] done $*[4] 5072[13] 5073running/max: 10/10, wait 1s ...pid: 5070 5060 5072 5061 5073 5067 5068 5069 5058 5059[5] done $*[6] done $*[5] 5075[6] 5076running/max: 10/10, wait 1s ...pid: 5070 5060 5072 5061 5073 5075 5076 5068 5058 5059...
思路:
- 需要在全局變數里記錄最大進程並發數和當前運行的進程(哈希表)。
- 每運行一個進程,將對應的進程號放入哈希表中。
- 如果當前運行進程數達到最大進程並發數,則循環檢查哈希表裡的進程是否退出。
實現:
rr() { (($+max_process)) || typeset -g max_process=10 (($+running_process)) || typeset -gA running_process=() [[ $1 == -j<1-> ]] && { max_process=${1[3,-1]} shift } (($# == 0)) && { for i (${(k)running_process}) { [[ -e /proc/$i ]] || unset "running_process[$i]" } echo "running/max: $#running_process/$max_process" (($#running_process > 0)) && echo "pid: ${(k)running_process}" return } while ((1)) { local running_process_num=$#running_process if (($running_process_num < max_process)) { $* & running_process[$!]=1 return } for i (${(k)running_process}) { [[ -e /proc/$i ]] || unset "running_process[$i]" } (($#running_process == $running_process_num)) && { echo "running/max: $running_process_num/$max_process, wait 1s ..." echo "pid: ${(k)running_process}" sleep 1 } }}
總結
本文講解了幾個比較實用的 zsh 腳本,後續可能會補充更多個。
更新歷史
2017.09.13:新增「實例七:統一壓縮解壓工具」和「實例八:方便並發運行命令的工具」。
本文不再更新,全系列文章在此更新維護:github.com/goreliu/zshguide
付費解決 Windows、Linux、Shell、C、C++、AHK、Python、JavaScript、Lua 等領域相關問題,靈活定價,歡迎諮詢,微信 ly50247。
推薦閱讀: