# 突破盲點的 bash 使用技巧 bash 是 linux 下最通用的互動式 shell。 在有圖形界面以前,shell 就是 unix 使用者認知中電腦的全部。 本課程將會介紹多種冷門的 bash 使用技巧, 包含迴圈、多工、互動使用技巧、腳本撰寫; 帶領聽眾重新理解 shell 的設計哲學。 其中腳本會以 sh 為主,而互動式技巧則會以 bash 為主。 ---- ## 講者介紹: gholk * linux 使用者,興趣使然的 web 開發者。 * 宣稱只寫 vanilla js ,但其實只是懶得學框架。 * 自認為只是 linux 使用者,所以堅持能用 shell 解決的事情就不動到 c 、 js 等一般語言。 * 非本科生,未來應該不是靠程式過活,所以可以任性地把自己定位為使用者。 --- ## shell 與通用程式語言 * 一般完整的程式語言特性是,內部功能完整,但與外部其它程式溝通困難。 * shell 則是設計用來呼叫、組合所有其它程式的語言。 ---- ### 二者間的取捨 * 用通用程式語言設計的程式,應該行為單純,只專注在主要的功能。 * 在使用時再經由 shell 的包裝,配合 **萬用字元** 、**管道** 、 **迴圈** 等達成各式各樣的功能。 * (因為之前在 node.js 裡處理參數的時候寫得很煩。) --- ## shell 的理念 每個程序都有 stdin stdout stderr , 可以傳入 argv ,繼承環境變數。 shell 的核心功能就是呼叫、組合各程序,達到目的。 除了依賴外部程序, shell 中也能自定義 **函數** 、 **括號命令群組** , 函數也像程序一樣有 std stream argv 環境變數等功能, 而命令群組一樣有 std stream ,但缺少 argv 。 ---- ### 截取 stream 的一部份 ```shell cat some-stream | { head -c 8 >/dev/null # 丟掉開頭的 8 byte foo=$(head -c 4) # 讀取 4 byte # do something echo $(basename $foo) cat # 把剩下的輸入原封不動丟進輸出 } > result # 把結果存起來 ``` ---- ### 在行處理上不適用 以行來處理會漏資料,我不知道為什麼。 ```shell yes hello | nl | { head -n 1 >/dev/null # 丟掉開頭一行 foo=$(head -n 1) # 讀取一行 echo $foo } ``` ---- ### 只能回去用 read ```shell yes hello | nl | { read n for i in `seq 1 n` do read line done } ``` --- ## 管道的縮排應該怎麼寫? 管道符為最後一個字元時,可以省略跳脫結尾換行符的反斜線。 ```sh awk '{sum += $1; print sum}' < file | sed -n '/[13579]$/p' | cut -d ' ' -f 2-4 | sort -n ``` ---- 也有些人覺得應該要寫清楚, 顯式手動跳脫比較好 ~~,就像某些 js 開發者硬要加分號一樣~~ 。 ```sh cat file |\ awk '{sum += $1; print sum}' |\ sed -n '/[13579]$/p' ``` ---- *那就摻在一起作成撒尿牛丸啊!* 這好像是 gnu 風格的縮排? 不確定從哪看來的,但目前我覺得這種寫法最美觀。 ```sh cat file \ | awk '{sum += $1; print sum}' \ | sed -n '/[13579]$/p' ``` --- ## 為什麼要用 cat cat 的好處,統一格式,一律把輸入的檔案放在最開頭。 ```sh cat file \ | awk '{sum += $1; print sum}' \ | sed -n '/[13579]$/p' \ | cut -d ' ' -f 2-4 \ | sort -n ``` ---- 如果不用 cat , 重道向的檔案慣例是寫在第一個命令的結尾處, 後面跟著的命令同樣位置卻是空的,造成不協調感。 ```sh awk '{sum += $1; print sum}' <file \ | sed -n '/[13579]$/p' \ | cut -d ' ' -f 2-4 \ | sort -n ``` ---- ## 重導向符的位置 但其實重導向符的位置不一定要在結尾, 要擺開頭、中間、結尾都可以。 ```sh awk '{sum += $1; print sum}' < file \ | sed -n '/[13579]$/p' ``` ```sh <file awk '{sum += $1; print sum}' \ | sed >result -n '/[13579]$/p' ``` ```sh >df.log df ``` ---- 所以可以寫成這樣,夠反人類吧? 有沒有忽然覺得 cat 比較好看了? ```sh < file \ awk '{sum += $1; print sum}' | sed -n '/[13579]$/p' | cut -d ' ' -f 2-4 | sort -n ``` ---- 如果你記得命令群組的用法,那也可以用在這裡: ```sh { awk '{sum += $1; print sum}' | sed -n '/[13579]$/p' | cut -d ' ' -f 2-4 | sort -n } <file >result ``` ---- 有點像是沒有宣告成函數的寫法: ```sh do_something() { awk '{sum += $1; print sum}' | sed -n '/[13579]$/p' | cut -d ' ' -f 2-4 | sort -n } do_something <file >result ``` ---- ### 同一個檔案不能同時是管道的輸入與輸出 因為 `>` 會清空檔案內容,讓輸入讀不到資料。 ``` do_something <file >file ``` --- ## 組合出一對多的管道 在複雜的情況時,會需要組合多個輸入輸出; 所以為了模擬複雜的情況,介紹幾個程式: ---- ### paste 逐行合併二個檔案 ``` ~$ seq 0 2 8 > even ~$ seq 1 2 9 > odd ~$ paste even odd 0 1 2 3 4 5 6 7 8 9 ``` ---- ### 座標轉換程式 proj4 ```sh <lonlat.txt awk '{print $2,$3,$4}' \ | proj -f %.4f +proj=utm \ > utm.txt ``` ---- ### stdin 只有一個 有時,我們想把多個結果併在同一個檔案裡, 可以用 paste 把二個檔案的每一行串接起來。 ```sh <xyz.txt cs2cs -f %.4f +proj=cart +to +proj=utm \ | paste xyz.txt - >xyz-utm.txt ``` ---- 但如果想要一次併好幾個檔案呢? ```sh ( <xyz.txt cs2cs -f %.4f +proj=cart +to +proj=utm <xyz.txt cs2cs -f %.4f +proj=cart +to +proj=lonlat ) | paste - - # ????? ``` 很遺憾,你只有一個 stdin 可以用。 ---- ### process substitution 這個功能可以讓你把某個命令當作檔案寫入或讀取。 寫入是 `>( )` ,讀取是 `<( )` 。 ```sh diff <(tar -xOf v1.tar main.c) <(tar -xOf v2.tar main.c) ``` ---- 進程替換其實就是把該部份語法, 置換成連結到該命令的管道。 ```shell ~:$ printf 'process substitution: "%s"\n' <(true) process substitution: "/dev/fd/63" ~:$ ll >(true) l-wx------ 1 gholk gholk 64 8月 9 11:39 /dev/fd/63 -> pipe:[109154] ~:$ ll <(true) lr-x------ 1 gholk gholk 64 8月 9 11:39 /dev/fd/63 -> pipe:[109168] ``` ---- 那結合二者,就能把多個程序的輸出結合在一起了! ```sh paste \ <( <xyz.txt awk '{print $2,$3,$4}' \ | cs2cs -f %.4f +proj=cart +to +proj=utm ) \ <( <xyz.txt awk '{print $2,$3,$4}' \ | cs2cs -f %.4f +proj=cart +to +proj=lonlat ) ``` ---- 只是有個小問題,process substitution 是 bash 的專屬功能,sh 不支援。 ---- ### tee and fifo 所以如果要在 sh 中執行,那得再介紹 fifo。 fifo 搭配 tee 就能實現一到多的管道。 ---- ### fifo ```shell mkfifo my-fifo echo hey >my-fifo & cat my-fifo ``` ---- ```flow start=>start: start xyz=>parallel: file with name and xyz coordinate awk=>operation: awk extract xyz part tee=>parallel: tee dulpicate xyz pll=>operation: proj xyz to lonlat putm=>operation: proj xyz to utm paste=>operation: paste merge result=>end: file with name, xyz, utm, lonlat start(right)->xyz xyz(path1, bottom)->awk->tee tee(path3, left)->pll->paste tee(path2, bottom)->putm->paste xyz(path3, right)->paste paste->result ``` ---- ```sh fifo_list='xyz2lonlat xyz2utm lonlat utm' mkfifo $fifo_list # 對 fifo 的寫入在開始讀取前會阻塞所以要丟到背景 awk '{print $2,$3,$4}' <xyz.txt | tee xyz2lonlat > xyz2utm & cs2cs -f %.8f +proj=cart +to +proj=lonlat <xyz2lonlat >lonlat & cs2cs -f %.4f +proj=cart +to +proj=utm <xyz2utm >utm & paste xyz.txt lonlat utm > all.txt rm $fifo_list ``` ---- * fifo 其實是解決輸入輸出流沒有名字的問題。 因為 paste 需要多個輸入,但只用 stdin 只有一個, 所以用多個 fifo 來連接輸入輸出。 * 要實現一對多,最關鍵還是 tee; 其實 tee 也可以用 `>( )` pipe 給多個程式。 ---- ## tee 與 fifo 的阻塞 tee 與 fifo 都是會阻塞的操作。 對 fifo 讀取或寫入時, 如果沒有其它程式同時在寫入或讀取,就會阻塞。 ---- tee 也是會阻塞,如果 tee 寫入的任一個程序 或是檔案阻塞,那 tee 所有的輸出都會阻塞。 --- ## server 當自己家用 ssh 不一定是執行一個互動式的 shell , 也可以直接執行命令。 ```shell ssh user@my.lab.ml ls ``` ---- 如果有一台以上的電腦,但某些程式只裝在特定一台, 可以用 ssh 幾乎無縫接軌取用。 ```sh cat xyz.txt \ | ssh user@my.lab.ml cs2cs +proj=cart +to +proj=lonlat ``` ---- ### 善用別名 ssh 可以用別名代表一台伺服器, 不然每次都要打一長串帳號域名,一點都不像自己家。 ---- ``` # ~/.ssh/config # see `man ssh_config` Host lab HostName my.lab.ml User user ``` ```sh cat xyz.txt \ | ssh lab cs2cs +proj=cart +to +proj=lonlat ``` ---- ### 自動壓縮或手動壓縮 如果資料量比較大時,可以考慮壓縮加速: ```sh gzip --to-stdout xyz.txt \ | ssh lab zcat - \ \| cs2cs +proj=cart +to +proj=lonlat \ \| gzip - \ | zcat - ``` ---- 每次都手動壓也很累,不如直接在 ssh 層使用自動壓縮吧: ``` # ~/.ssh/config # global Compression yes # or only compression in host Host lab HostName my.lab.ml Compression yes ``` ---- ### 善用 stdin 但 ssh 執行遠端電腦上的程式的問題是,只有 stdin 能用。 如果真得要傳複數檔案,可以考慮用 tar 來打包, 但就得執行一長串命令來解開再打包回來了; 還要注意不要丟任何東西到 stdout,不然回來的 tar 會壞掉。 (只能靠 stderr 來 log 了。) ---- 傳送單個 tar 封存: ```shell tar -cf - file1 file2 | ssh lab tar -xf - ``` ---- 傳送 tar 封存並執行達端程式,再把結果用 tar 送回來: ```sh tar -cf - x y z | ssh lab ' tar -xf - paste x y z | cs2cs +proj=cart +to +proj=lonlat > lonlat tar -cf - x y z lonlat rm x y z lonlat ' > result.tar ``` ---- ### ssh key 只是你可能需要用 ssh-key 免密碼登入, 才能達到全家就是你家的方便等級。 如果怕安全問題,可以手動修改 server 上的 `~/.ssh/authorized_keys` ,用完就註解掉該行。 ---- ### ssh control master 或是用 ssh control master 登入一次後 就複用既有的 ssh 連線。 除了在連線存在期間不用重覆登入外, 還可以省下建 tcp 連線的時間。 ```term ~:$ time ssh lab true real 0m1.123s user 0m0.032s sys 0m0.032s ~:$ # using ssh control master ~:$ time ssh lab true real 0m0.030s user 0m0.004s sys 0m0.008s ``` ---- ``` # ~/.ssh/config Host lab HostName my.lab.ml User gholk ControlMaster auto ControlPath ~/.ssh/ssh-control-master-%r@%h:%p ControlPersist 300 ``` ---- 當然,第一次登入還是要密碼, 所以如果要完全自動化,還是得用 ssh key。 只是用 control master 之後, 不管同時執行了幾個 ssh , 都只會跑在同一個 ssh 連線上。 ---- ```sh ssh -N lab # login with password then C-z to background bg # run previous command in background for i in * do # do something with ssh cat $i | sed | ssh lab proj | awk >result-$i done kill %% # kill ssh -N process ``` --- ## 用 disown 讓程式在登出後繼續運行 ```sh ( for tar in *.tar do tar -xf $tar summary mv summary $(basename $tar .tar)-summary done ) >extract-tar.log 2>&1 & disown -h %% exit ``` ```shell tail -f extract-tar.log # or with modern command `less` less +F extract-tar.log ``` ---- ### 放棄難用的 nohup nohup 只能用在執行檔上。 但如果你要用 sh 就另當別論了, disown 只能在 bash 中使用。 ---- 其實在迴圈的 done 後面直接加 `&` 就能丟到背景了, ```sh while sleep 1s do echo dont sleep done & ``` ---- 關於 `&` 的意義,其實比較像 `;` 。 ```shell echo a & echo b echo a ; echo b ``` ---- 加括號是有時要一次執行好幾個命令, 要全部丟到背景就可以用括號包起來成群組, 再把群組丟到背景。 ```sh ( uncompress NCTU0010.19d.Z crx2rnx NCTU0010.19d > NCTU0010.19o xz NCTU0010.19o ) & ``` --- ## 迴圈寫法 有些命令適合放 while 後面。 ``` find -name '*.jpg' | while read file do cp $file /tmp # some other code done ``` ---- 比較醜,但比較好理解的寫法。 ``` find -name '*.jpg' | while true do read file if [ -z "$file" ] then break fi # some other code done ``` ---- 我也常用 sleep : ``` while sleep 1s # or use `true` while true do sleep 1s done ``` ---- ## 平行處理壓縮時間 ```shell for gzip in *.gz do gzip -d $gzip compute-some-thing ${gzip%.gz} done ``` ---- 同時處理二個 ```shell for gzip in *.gz do gzip -d $gzip wait compute-some-thing ${gzip%.gz} & done ``` ---- 我有一個大膽的想法 ```shell for gzip in *.gz do ( gzip -d $gzip compute-some-thing ${gzip%.gz} ) & done ``` ---- 比較保險的平行處理,直接跑二個迴圈。 ```shell for gzip in *[13579].gz do gzip -d $gzip compute-some-thing ${gzip%.gz} done & for gzip in *[24680].gz do gzip -d $gzip compute-some-thing ${gzip%.gz} done & ``` --- ## 編輯以前的命令並執行 很多人應該都知道可以用 ↑ ↓ 來執行以前的命令。 ---- ### 在歷史記錄中搜尋 另一個更好用的是 readline 的 C-r (reverse search) 可以搜尋以前的命令,不用 C-b 一個個找按半天。 1. `C-r` 2. 輸入要搜尋命令包含的字串 3. 每輸入一個字,會即時顯示搜尋到的結果 4. 按 enter 執行,按 ← → 編輯, 再按一次 `C-r` 換下一個匹配的, `C-c` 取消。 ---- ### fc: fix command 這個命令可以開啟編輯器編輯上一個執行的 shell 命令, 編輯完離開後就會執行。 可以用選項控制要編輯第 n 個命令,或是像 `C-r` 一樣搜尋。 但其實 fc 不好用,因為不太可能 *記住* 要改的命令是第幾個, 而且搜尋匹配的第一個結果不一定是你印象中的。 ---- ### 直接開啟編輯器 另一個 readline 快捷鍵是 `C-x C-e` (edit command in editor) , 是直接開啟編輯器編輯目前打到一半的 shell 命令。 主要用在命令打得很長的時候, 只靠 shell 基礎的功能編輯會很痛苦。 編輯器同 fc 預設都是 vi 或看 EDITOR 變數。 所以請至少有一個能在 shell 中使用的編輯器。 ---- ### 取代 fc 的功能 搭配 `C-r` 搜尋的話,就搜到了再按 `C-x C-e` 編輯即可, 互動搜尋的效果會比 fc 盲搜的結果好很多。 ---- ### 為什麼多行命令被壓成一行? 問題在 cmdhist 與 lithist 這二個選項。 ```sh shopt -s cmdhist # save multiple line command in single history entry # but join in single line with `;` shopt -s lithist # keep `\n` instead use `;` ``` ---- ### 多行命令 ``` ~:$ for i in `seq 2 6` > do > echo $i > done 2 3 4 5 6 ``` ---- ### only enable cmdhist ``` ~:$ for i in `seq 2 6`; do echo $i; done ``` ---- ### long multi-line command ` ~:$ for id in $(tail -n +2 csrs.id); do echo $id; curl-csrs-ppp get $id > $id.zip; basename=$(basename $(unzip -l $id.zip | awk '$4 ~ /mari.*pdf/ { print $4 }') .pdf); mv $id.zip $basename.19o.zip; unzip $basename.19o.zip $basename.csv; sleep 20s; done ` ---- ### enable lithist ``` ~:$ for id in $(tail -n +2 csrs.id) do echo $id curl-csrs-ppp get $id > $id.zip basename=$(basename $(unzip -l $id.zip | awk '$4 ~ /mari.*pdf/ { print $4 }') .pdf) mv $id.zip $basename.19o.zip unzip $basename.19o.zip $basename.csv sleep 20s done ``` ---- ### lithist 故障 lithist 有時候會壞掉,多行命令會被分開成一行一行, 可能是因為舊的歷史檔案 `~/.bash_history` 格式亂掉。 像如果歷史檔案的大小超過限制會被截斷,格式就會亂掉。 修正或直接刪掉就會正常。 ---- ### edit function, alias, script https://github.com/GHolk/loco/blob/master/bash_function#L78 --- ## 在腳本中啟動另一個子 shell 當你需要在腳本內在另一支程式內執行一系列命令, 一般是要寫到另一個檔案直接執行。 ``` sftp -b batch.sftp remote-server ``` 但有時候不希望多一個檔案,管理起來會很麻煩, 會想要都寫在同一個同案裡。 ---- ### here doc 比較直覺也比較保險的作法是用 heredoc, 但缺點是變數會被展開,可能需要跳脫。 (如果你的 IDE 有跳脫的快速鍵就不成問題。) ---- ```sh #!/bin/sh rinex=NCTU0010.19o docker exec --interactive --tty gxh bash <<GUEST . /usr/local/GipsyX/rc_GipsyX.sh rinex=inside-docker echo $rinex # NCTU0010.19o echo \$rinex # inside-docker gd2e.py -rnxFile $rinex GUEST ``` ---- ### 用 tail 抓出自己的內容 主要是用 `$0` 會指向檔案本身的技巧,事先算好行數, 但 `$0` 會存完整路徑是 bash 的擴充, sh 中 `$0` 只會存最終檔名。 ---- ```sh #!/bin/sh a=b tail +5 $0 | su -l guest -c sh exit whoami # guest a=c echo $a file=$(echo *.*) ``` ---- 這是 debian 裡 grub-mkconfig 的做法, 因為 grub 腳本中用到了大量的變數,如果一一跳脫可讀性會很差。 ``` #!/bin/sh exec tail -n +3 $0 # This file provides an easy way to add custom menu entries. Simply type the # menu entries you want to add after this comment. Be careful not to change # the 'exec tail' line above. menuentry '[system] shutdown' { halt } ``` ---- ### 用 sed 定位 exit 不用手動計算行數,但要注意不要匹配到奇怪的東西。 ```sh #!/bin/sh sed '1,/^exit$/d' $0 | dbus-run-session sh exit gvfs-mount ftp://my.lab.ml cp .gvfs/ftp/some-file.zip . ``` --- ## 冷門用法 摺疊 ```shell seq 10 | paste - - ``` ---- ### 格式化輸出類似的字串 ```shell seq -f "(%.0f)" 10 ``` ```shell printf "%s\0" * # similar to find -maxdepth 1 -print0 ``` ---- ### 重覆輸出 ```shell printf "yes\n%.0s" `seq 5` ``` ```shell yes "yes" | head -n 5 ``` ---- ### yes 的真正用途 當某些程式執行時會問很多 yes no 的時候, 用 yes 告訴他。 ```shell yes | sudo apt install the-world ``` --- ## ex 批次編輯 雖然 sed 也可以批次編輯, 但有些功能還是要用可以來回跳躍的真正的編輯器比較方便, 而且編輯器還是比較快。 後來發現其實差不多,當檔案太大時,都是卡在硬碟寫入瓶頸。 ---- ```sh for rinex in *.rnx do echo ' 1 /ANT # s/-Unknown-/TPSG3_A1/ w n ' done | ex *.rnx ``` ---- ### 為什麼不是 ed ? ``` ED(1) Unix Programmer's Manual ED(1) NAME ed - text editor SYNOPSIS ed [ - ] [ -x ] [ name ] DESCRIPTION Ed is the standard text editor. ``` ---- ### 有 vi 就有 ex ex 的好處是,太新的發行版不一定有裝 ed , 但一定有裝 vi ,有 vi 就有 ex 。
{"metaMigratedAt":"2023-06-14T23:18:12.133Z","metaMigratedFrom":"YAML","title":"突破盲點的 bash 使用技巧","breaks":false,"description":"bash 是 linux 下最通用的互動式 shell。在有圖形界面以前,shell 就是 unix 使用者認知中電腦的全部。本課程將會介紹多種冷門的 bash 使用技巧,包含迴圈、多工、互動使用技巧、腳本撰寫;帶領聽眾重新理解 shell 的設計哲學。其中腳本會以 sh 為主,而互動式技巧則會以 bash 為主。","contributors":"[{\"id\":\"2a735abd-b3b1-4523-b7a8-aa63bc4fb87c\",\"add\":26139,\"del\":12910}]"}
    3212 views