排序文本 sort
locale 對排序的影響
重音位置不同的法文單字
1 2 3 4 5 6 |
$ cat french côte cote coté côté |
查看字元在 ISO 的八進位數值
1 2 3 4 5 6 |
$ man iso_8859_1 Oct Dec Hex Char Description ──────────────────────────────────────────────────────────────────── 351 233 E9 é LATIN SMALL LETTER E WITH ACUTE 364 244 F4 ô LATIN SMALL LETTER O WITH CIRCUMFLEX |
分別用傳統 ASCII 和 Canadian-French(系統必須先安裝法文)排序,結果不同
1 2 3 4 5 6 7 8 9 10 11 12 |
$ LC_ALL=C sort french cote coté côte côté $ LC_ALL=fr_CA.utf8 sort french cote côte coté côté |
注意空白
以 -k
指定排序字段時
- 如未以
-t
指定分隔符號,預設以空白分隔並忽略開頭與結尾的空白 - 如果指定
-t
,則開頭與結尾的空白不會被忽略,例如 ” -X- ” 以-t " "
分隔,會被分成三個字段: 空白, “-X-” 及空白 - 如果僅指定一個字段編號,意思是「從該字段開始,一直比對到行的結尾」
-k{n},{m}
格式代表「從第 n 個字段開始,至第 m 個字段結尾」-k{n.i},{m.j}
格式代表「從第 n 個字段第 i 個字元開始,至第 m 個字段第 j 個字元結尾」
穩定性
紀錄內的排序字段都相同,但輸出與輸出不一致,代表 sort 並不穩定。GNU 實現了 coreutils 套件,可透過 --stable
彌補這個不足
1 2 3 4 5 6 7 8 9 10 11 |
$ sort -t_ -k1,1 -k2,2 << EOF > one_two > one_two_three > one_two_four > one_two_five > EOF one_two one_two_five one_two_four one_two_three |
排除重複
使用 -u
排除重複是基於 key 而非整筆紀錄,如果是後者,則可以搭配 uniq
工具使用
格式化文本 fmt
- fmt 並不包含在 POSIX 範疇,可透過安裝 coreutils 使用
- fmt 不同版本,可能會有不同的行為
範例:將短的行合併成長的行
1 2 3 4 5 6 |
$ sed -n '100000,100010p' /usr/share/dict/words | fmt -w 30 washcloths washed washer washer's washers washerwoman washerwoman's washerwomen washes washing washing's |
管道
UNIX 工具使用原則就是:想清楚這個問題該如何劃分成更簡單的工作,每個部份是不是已有現成的工具解決。
在 UNIX 下管理相關的文件,大部分都是簡易的文本文件(無須特殊工具即可編輯閱讀)。這些文件通常放在標準目錄 /etc
底下。例如密碼文件(passwd)、群組文件(group)、文件系統加載表(fstab/vfstab)、主機文件(hosts)、默認的 shell 啟動文件(profile)、系統啟動與關機的 shell 腳本
範例:解析 /etc/passwd 的使用者訊息,製作成辦公室名冊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#! /bin/sh # note1 umask 077 # note2 PERSON=/tmp/pd.key.person.$$ OFFICE=/tmp/pd.key.office.$$ TELEPHONE=/tmp/pd.key.telephone.$$ USER=/tmp/pd.key.user.$$ # note3 trap "exit 1" HUP INT PIPE QUIT TERM trap "rm -f $PERSON $OFFICE $TELEPHONE $USER" EXIT awk -F: '{ print $1 ":" $5 }' > $USER # note4 sed -e 's=/.*==' \ -e 's=^\([^:]*\):\(.*\) \([^ ]*\)=\1:\3, \2=' < $USER | sort > $PERSON sed -e 's=\([^:]*\):[^/]*/\([^/]*\)/.*=\1:\2=' < $USER | sort > $OFFICE sed -e 's=\([^:]*\):[^/]*/[^/]*/\([^/]*\)=\1:\2=' < $USER | sort > $TELEPHONE join -t: $PERSON $OFFICE | # note5 join -t: - $TELEPHONE | cut -d: -f 2- | sort -t: -k1,1 -k2,2 -k3,3 | # note6 awk -F: '{ printf("%-39s\t%s\t%s\n", $1, $2, $3) }' |
Input
1 2 3 4 |
john:x:12502:1000:John Hancock/OSD212/555-0022:/home/john:/bin/bash ben:x:12502:1000:Ben Franklin/KNS321/555-0044:/home/ben:/bin/bash jones:x:12502:1000:Adrian W. Jones/BMD19/555-0095:/home/jones:/bin/bash |
Output
1 2 3 4 |
Franklin, Ben KNS321 555-0044 Hancock, John OSD212 555-0022 Jones, Adrian W. BMD19 555-0095 |
Notes
- 在開放程序應養成一個好習慣:僅允許需要用到這個文件的用戶或進程訪問它
- 為了避免程序多個實例同時執行導致名稱衝突,使用內置的變量
$$
程序編號當後綴 - 使用
trap
監聽工作中止訊號,清理暫存檔 - 這裡使用
=
來做分隔符 -
在管道中代表「上一個」管道的輸出%-39s%
顯示向左對齊的 39 個字元長度
輸出 HTML 格式
輸出 HTML 格式,所有內容都是可以打印的 ASCII 文字,除了標誌符(identitfier)<
、>
,必須以特殊編碼呈現,稱為實體(entities);實體以 &
開頭,&
也必須轉換成實體
1 2 3 4 |
sed -e 's=&=\&=g' \ -e 's=<=\<=g' \ -e 's=>=\>=g' \ |
範例:過濾單詞出現的頻率
1 2 3 4 5 6 7 8 9 10 11 |
#! /bin/sh TOP=${1:-25} tr -cs 'A-Za-z\' '\n' | tr A-Z a-z | sort | uniq -c | sort -k1,1nr -k2 | sed "$TOP"q |
變量、判斷、重複動作
- 變量可以用於管理程序狀態,流程控制的功能造就程序語言:如果只有命令語句,是不可能完成任何工作的
- POSIX Shell 為內嵌(inline)算術提供了一種標記法,稱為算術展開(arithmetic expansion)。會對
$((...))
的算術表達式進行計算,再將結果放回命令的文本內容
設定、打印環境變數
- POSIX 標準支援結合 export 與變量宣告,如
export FOO=bar
,許多商用 UNIX 系統裡的 /bin/sh 仍不兼容 POSIX,最保險的export
方式是先定義變量再 export123FOO=barexport FOO - 使用
env
打印環境變數不會正確地為變數值加上引號,需要此功能的話請使用export -p
-
env -i
會重新初始化程序的環境變數,僅傳遞命令行指定的變數 -
myvar=
賦值並不會將 myvar 刪除,unset myvar
才會完全刪除變量
參數展開
- 參數展開是 Shell 在提供變量值在程序中使用的過程,例如
echo ===${msg}===
- 在默認情況下,未定義的變量會展開成 null (空字串),開發時須特別留意,如
rm -fr /$MYDIR
這種寫法會引發災難
替換運算符
${count-0}
: count 不存在則回傳 0,反之回傳 count 變量的值${count=0}
: count 不存在則將變數 count 的值設為 0${count?"undefined!"}
: count 不存在則顯示count: undefined!
並退出;省略 message 會出現默認訊息parameter null or not set
${count+1}
: count 存在則回傳 1 (true) 反之回傳 null
以上運算符都可以選用冒號前綴 :
,代表「存在且非 null 」
模式匹配運算符
- ${variable#pattern}: 若模式匹配於變量值的開頭,則刪除匹配的最短部份,回傳剩下的部份
- ${variable##pattern}: 若模式匹配於變量值的開頭,則刪除匹配的最長部份,回傳剩下的部份
- ${variable%pattern}: 若模式匹配於變量值的結尾,則刪除匹配的最短部份,回傳剩下的部份
- ${variable%%pattern}: 若模式匹配於變量值的結尾,則刪除匹配的最長部份,回傳剩下的部份
1 2 3 4 5 6 7 |
PATH=/home/tolstoy/home/long.file.name echo ${PATH#/*/} # /tolstoy/home/long.file.name echo ${PATH##/*/} # long.file.name echo ${PATH%.*} # /home/tolstoy/long.file echo ${PATH%%.*} # /home/tolstoy/long |
位置參數
- 當整數大於 9 時,應該以
{}
括起來,如${10}
特殊變量
$#
: 傳遞到 Shell 腳本或函式的參數總數量$*
、$@
:一次表示所有命令行參數,可以把命令行參數傳遞給腳本或函式"$*"
:將所有命令行參數視為單個字串,等同於 “$1 $2 …”"$@"
:將所有命令行參數視為獨立字串,等同於 “$1” “$2″;這是將參數傳送給其他程序的最佳方式,因為保留了所有內嵌在每個參數裡的任何空白$?
:前一命令的退出狀態$$
:程序的 process IDHOME
PATH
PWD
LANG
LC_ALL
shift
命令是用來截去 (lops off) 來自列表的位置參數,由左開始,一旦執行,$1 的初始值會永遠消失
1 2 3 4 5 6 |
while [ $# != 0] do # do something with $1 shift done |
算術展開
- Shell 的算術運算元與 C 的差不多
- 以
$(())
語法包裝,特殊符號不需要以反斜槓轉義 - 可利用
()
包裝子表達式 - 關係運算元如
{<, <=, ==, !=}
回傳值是 1 或 0 - 注意
i++
跟++i
的差別 - 注意
{|, &}
及{||, &&}
的區別 &&
及||
是快捷 (short-circuit) 運算元,當已判斷出整個語句的真偽時,會立即停止繼續執行命令
1 2 3 |
$ echo $((1 + (1<2)) 2 |
退出狀態
if else 語句
- Algol 68 最有名的地方在於,使用方括號作為開始與結束的關鍵字來組織語句
- 不要過度簡化而使用
&&
及||
取代 if 語句
1 2 3 4 5 6 7 8 9 10 |
if pipeline then # statement if true elif pipeline then # statement if true else # statement if all fails fi |
範例
- 將驚嘆號置於管道前,表達否定
- 有些舊的腳本使用冒號(:)命令,代表不做任何事
1 2 3 4 5 6 7 |
if ! grep pattern myfile > /dev/null then echo "pattern not found" else : fi |