Read Me
- 本文是以英文版<bash cookbook> 為基礎整理的筆記,力求脫水
- 2018.01.21 更新完【中級】。內容包括工具、函數、中斷及時間處理等進階主題。
- 本系列其他兩篇,與之互為參考
- 【基礎】內容涵蓋bash語法等知識點。傳送門
- 【高級】內容涉及腳本安全、bash定制、參數設定等高階內容。傳送門
- 所有代碼在本機測試通過
- Debian GNU/Linux 9.2 (stretch)
- GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)
- 2018.01.21 更新 【四】工具.sed流處理 【六】日期與時間
約定格式
# 注釋:前導的$表示命令提示符
# 注釋:無前導的第二+行表示輸出# 例如:
$ 命令 參數1 參數2 參數3 # 行內注釋
輸出_行一
輸出_行二$ cmd par1 par1 par2 # in-line comments
output_line1
output_line2
復制代碼
四、工具
UNIX(Linux)喜歡小而美,不喜歡大而雜
grep 搜索字符串
在當前路徑的所有c后綴文件中,查找printf字符串
$ grep printf *.c
both.c: printf("Std Out message.\n", argv[0], argc-1);
both.c: fprintf(stderr, "Std Error message.\n", argv[0], argc-1);
good.c: printf("%s: %d args.\n", argv[0], argc-1);
somio.c: // we'll use printf to tell us what we
somio.c: printf("open: fd=%d\n", iod[i]);
復制代碼
當然,也可以像這樣,指定不同的搜索路徑
$ grep printf ../lib/*.c ../server/*.c ../cmd/*.c */*.c
復制代碼
搜索結果的默認輸出格式為“文件名 冒號 匹配行”
可以通過**-h
**開關隱藏(hide)文件名
$ grep -h printf *.c
printf("Std Out message.\n", argv[0], argc-1);
fprintf(stderr, "Std Error message.\n", argv[0], argc-1);
printf("%s: %d args.\n", argv[0], argc-1);// we'll use printf to tell us what weprintf("open: fd=%d\n", iod[i]);
復制代碼
或者,不顯示匹配行,而只是用**-c
**開關進行對匹配次數進行計數(count)
$ grep -c printf *.c
both.c:2
good.c:1
somio.c:2
復制代碼
或者,只是簡單地列出(list)含搜索項的文件清單,可以用**-l
**開關
$ grep -l printf *.c
both.c
good.c
somio.c
復制代碼
文件清單可視為一個不包含重復項的集合,便于后續處理,比如
$ rm -i $(grep -l 'This file is obsolete' * )
復制代碼
有時候,只需要知道是否滿足匹配,而不關心具體的內容,可以使用**-q
**靜默(quiet)開關
$ grep -q findme bigdata.file
$ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi
nope
復制代碼
也可以把輸出重定向進/dev/null位桶,一樣實現靜默的效果。位桶(bit bucket)就相當于“位的垃圾桶”,一個有去無回的比特黑洞
$ grep findme bigdata.file >/dev/null
$ if [ $? -eq 0 ] ; then echo yes ; else echo nope ; fi
nope
復制代碼
經常,你更希望搜索時忽略(ignore)大小寫,這時可以用**-i
**開關
$ grep -i error logfile.msgs # 匹配ERROR, error, eRrOr..
復制代碼
很多時候,搜索范圍并不是來自文件,而是管道
$ 命令1 | 命令2 | grep
復制代碼
舉個例。將gcc編譯的報錯信息從標準錯誤(STDERR, 2)重定向到標準輸出(STDOUT,1),再通過管道傳給grep進行篩選
$ gcc bigbadcode.c 2>&1 | grep -i error
復制代碼
多個grep命令可以串聯,以不斷地縮小搜索范圍
grep 關鍵字1 | grep 關鍵字2 | grep 關鍵字3
復制代碼
比如,與**!!
**(復用上一條命令)組合使用,可以實現強大的增量式搜索
$ grep -i 李 專輯/*
... 世界上有太多人姓李 ...
$ !! | grep -i 四
grep -i 李 專輯/* | grep -i 四
... 叫李四的也不少 ...
$ !! | grep -i "饒舌歌手"
grep -i 李 專輯/* | grep -i 四 | grep -i "饒舌歌手"
李四, 饒舌歌手 <lsi@noplace.org>
復制代碼
-v
開關用來反轉(reverse)搜索關鍵字
$ grep -i dec logfile | grep -vi decimal | grep -vi decimate
復制代碼
按關鍵字'dec'匹配,但不要匹配'decimal',也不要匹配'decimate'。因為這里的dec意思是december
...
error on Jan 01: not a decimal number
error on Feb 13: base converted to Decimal
warning on Mar 22: using only decimal numbers
error on Dec 16 : 匹配這一行就對了
error on Jan 01: not a decimal number
...
復制代碼
像上邊這樣“要匹配這個,但不要包含那個”...是非常笨重的,就像在純手工地對密碼進行暴力破解。
仔細觀察規律,匹配關鍵字的模式,才是正解。
$ grep 'Dec [0-9][0-9]' logfile
復制代碼
[0-9][0-9]匹配dec后邊的一位或兩位數日期。如果日期是一位數,syslog會在數字后加個空格補齊格式。所以為了考慮進這種情況,改寫如下,
$ grep 'Dec [0-9 ][0-9]' logfile
復制代碼
對于包含空格等敏感字符的表達式,總是用單引號'...'對表達式進行包裹是個良好的習慣。這樣可以避免很多不必要的語法歧義。
當然,用反斜杠**\
**對空格取消轉義(escaping)也行。但考慮到可讀性,還是建議用單引號對。
$ grep Dec\ [0-9\ ][0-9] logfile
復制代碼
結合正則表達式(Re),可以實現更復雜的匹配。
正則表達式【簡表】
. # 任意一個字符
.... # 任意四個字符
A. # 大寫A,跟一個任意字符
* # 零個或任意一個字符
A* # 零個或任意多個大寫A
.* # 零個或任意個任意字符,甚至可以是空行
..* # 至少包含一個空行以外的任意字符
^ # 行首
$ # 行尾
^$ # 空行
\ # 保留各符號的本義
[字符集合] # 匹配方括號內的字符集合
[^字符集合] # 不匹配方括號內的字符集合
[AaEeIiOoUu] # 匹配大小寫元音字母
[^AaEeIiOoUu] # 匹配不包括大小寫元音的任意字母
\{n,m\} # 重復,最少n次,最多m次
\{n\} # 重復,正好n次
\ {n,\} # 重復,至少n次
A\{5\} # AAAAA
A\{5,\} # 至少5個大寫A
復制代碼
舉個實用的例子:匹配社保編號 SSN
$ grep '[0-9]\{3\}-\{0,1\}[0-9]\{2\}-\{0,1\}[0-9]\{4\}' datafile
復制代碼
這么長的正則,寫的人很爽,讀的人崩潰。所以也被戲稱為Write Only.
為了寫給人看,一定要加個注釋的。
為了講解清楚,來做個斷句
[0-9]\{3\} # 先匹配任意三位數
-\{0,1\} # 零或一個橫杠
[0-9]\{2\} # 再跟任意兩位數
-\{0,1\} # 零或一個橫杠
[0-9]\{4\} # 最后是任意四位數
復制代碼
還有一些z字頭工具,可以直接對壓縮文件進行字符串的查找和查看處理。比如zgrep, zcat, gzcat等。一般系統會預裝有
$ zgrep 'search term' /var/log/messages*
復制代碼
特別是zcat,會盡可能地去還原破損的壓縮文件,而不像其他工具,對“文件損壞”只會一味的報錯。
$ zcat /var/log/messages.1.gz
復制代碼
awk 變色龍
awk是一門語言,是perl的先祖,是一頭怪獸,是一只變色龍(chameleon)。
作為(最)強大的文本處理引擎,awk博大精深,一本書都講不完。這里只能挑些最常用和基礎的內容來講。
首先,以下三種傳文件給awk的方式等效:
$ awk '{print $1}' 輸入文件 # 作為參數
$ awk '{print $1}' < 輸入文件 # 重定向
$ cat 輸入文件 | awk '{print $1}' # 管道
復制代碼
對于格式化的文本,比如ls -l
的輸出,awk對各列從1開始編號,依次遞增。不是從0,因為$0表示整行。最后一列,記為NF。空格被默認作各列的分隔符,也可以通過-F
開關進行自定義。
$1 | $2 | $3 | ... | $NF | |
---|---|---|---|---|---|
首列 | 第二列 | 第三列 | ... | 尾列 | |
$0 | 整行 |
$ ls -l
total 4816
drwxr-xr-x 4 jimhs jimhs 4096 Nov 26 02:10 backup
drwxr-xr-x 3 jimhs jimhs 4096 Nov 24 08:20 bash
...
$
$ ls -l| awk '{print $1, $NF}' # 打印第一行和最后一行
total 4816
drwxr-xr-x backup
drwxr-xr-x bash
...
復制代碼
注意到,第五列是文件大小,可以對其大小求和,并作為結果輸出
$ ls -l | awk '{sum += $5} END {print sum}'
復制代碼
ls -l
輸出的第一行,是一個total匯總。也正因為該行并沒有“第五列”,所以對上邊的{sum += $5}沒有影響。
但實際上,嚴格來講,應該對這樣的特例做預處理,即,刪掉該行。
首先想到的:可以用之前介紹grep時的**-v
**翻轉開關,來去除含'total'的那行
$ ls -l | grep -v '^total' | awk '{sum += $5} END {print sum}'
復制代碼
另一種方法是:在awk腳本內,先用正則定位到total行(第一行),找到后立即執行緊跟的{getline}句塊,因為getline用來接收新的輸入行,這樣就順利跳過了total行,而進入了{sum += $5}句塊。
$ ls -l | awk '/^total/{getline} {sum += $5} END {print sum}'
復制代碼
也就是說,作為awk腳本,各結構塊擺放的順序是相當重要的。
一個完整的awk腳本可以允許多個大括號{}包裹的結構。END前綴的結構體,表示待其他所有語句執行完后,執行一次。與之相對的,是BEGIN前綴,會在任何輸入被讀取之前執行,通常用來進行各種初始化。
作為可編程的語言,awk部分借用了c語言的語法。
可以像這樣,將結構寫成多行
$ awk '{
> for (i=NF; i>0; i--) {
> printf "%s ", $i;
> }
> printf "\n"
> }'
復制代碼
也可以把整個結構體塞進一行內
$ awk '{for (i=NF; i>0; i--) {printf "%s ", $i;} printf "\n" }'
復制代碼
以上腳本,將各列逆序輸出:
drwxr-xr-x 4 jimhs jimhs 4096 Nov 26 02:10 backup
變成了
backup 02:10 26 Nov 4096 jimhs jimhs 4 drwxr-xr-x
復制代碼
對于復雜的腳本,可以單獨寫成一個.awk后綴的文件
#
# 文件名: asar.awk
#
NF > 7 { # 觸發計數語句塊的邏輯,即該行的項數要大于7user[$3]++ # ls -l的第3個變量是用戶名}
END {for (i in user){printf "%s owns %d files\n", i, user[i]}}
復制代碼
然后通過**-f
**文件開關來引用(file)
$ ls -lR /usr/local | awk -f asar.awk
bin owns 68 files
albing owns 1801 files
root owns 13755 files
man owns 11491 files
復制代碼
這個腳本asar.awk,遞歸地遍歷/usr/local路徑,并統計各用戶名下的文件數量。
注意:其中用于自增時計數的user[]數組,它的索引是$3,即用戶名,而不是整數。這樣的數組也叫作關聯數組(associative arrays) ,或稱為映射(map),或者是哈希表(hashes)。
至于怎么做的關聯、映射、哈希,這些技術細節,awk都在幕后自行處理了。
這樣的數組,肯定是無法用整數作為索引去遍歷了。
所以,awk為此專門定制了一條優雅的for...in...的語法
for (i in user)
復制代碼
這里,i會去遍歷整個關聯數組user,本例是[bin , albing , man , root]。再強調一下,重點是會遍歷“整個”。至于遍歷“順序”,你沒法事先指定,也沒必要關心。
下邊的hist.awk腳本,在asar.awk的基礎上,加了格式化輸出和直方圖的功能。也借這個稍復雜的例子,說明awk腳本中函數的定義和調用:
#
# 文件名: hist.awk
#
function max(arr, big)
{big = 0;for (i in user){if (user[i] > big) { big=user[i];}}return big
}NF > 7 {user[$3]++}
END {# for scalingmaxm = max(user);for (i in user){#printf "%s owns %d files\n", i, user[i]scaled = 60 * user[i] / maxm ;printf "%-10.10s [%8d]:", i, user[i]for (i=0; i<scaled; i++) {printf "#";}printf "\n";}}
復制代碼
本例中還用到了printf的格式化輸出,這里不展開說明。
awk內的算術運算默認都是浮點型的,除非通過調用內建函數int(),顯式指定為整型。
本例中做的是浮點運算,所以,只要變量scaled不為零,for循環體就至少會執行一次,類似下邊的"bin"一行,雖然寥寥68個文件,也還是會顯示一格#
$ ls -lR /usr/local | awk -f hist.awk
bin [ 68]:#
albing [ 1801]:#######
root [ 13755]:##################################################
man [ 11491]:##########################################
復制代碼
至于各用戶名輸出時的排列順序,如前所述,是由建立哈希時的內在機制決定的,你無法干預。
如果非要干預(比如希望按字典序,或文件數量)排列的話,可以這樣實現:將腳本結構一分為二,將第一部分的輸出先送給sort做排序,然后再通過管道送給打印直方圖的第二部分。
最后,再通過一個小例子,結束awk的介紹。
這個簡短的腳本,打印出包含關鍵字的段落:
$ cat para.awk
/關鍵字/ { flag=1 }
{ if (flag == 1) { print $0 } }
/^$/ { flag=0 }
$
$ awk -f para.awk < 待搜索的文件
復制代碼
段落(paragraph),是指兩個空行之間所有的文本。空行表示段落的結束
/^$/
會匹配空行。但是,對那些含有空格的“空行”,更精確的匹配是像這樣:
/^[:blank:]*$/
復制代碼
sed 流處理
@2018.01.20
sed
即流編輯器(stream editor)。
可以這么來簡單區分,sed
對文本是按行掃描。awk
則是按列掃描。
sed
本身就是一個龐大的話題,所以原著******并未過多涉及。
最近看***<Linux命令速查手冊>***(第2版)的時候,發現書中引用的一個鏈接內容不錯,中文翻得也不錯。
所以放在此處,供有求知欲的讀者參考。
并特此感謝原作者Eric Pement,及譯者Joe Hong。
- USEFUL ONE-LINE SCRIPTS FOR SED (Unix stream editor)
- SED單行腳本快速參考(Unix 流編輯器)
cut uniq sort 切割 去重 排序
處理格式化數據,經常涉及一系列組合操作:切割、去重、排序。
先看個例子,統計系統里各個shell的頻次
$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
$
$ cut -d':' -f7 /etc/passwd | sort | uniq -c | sort -rn17 /bin/false # 禁止登錄16 /usr/sbin/nologin # 禁止登錄2 /bin/bash1 /bin/sync
復制代碼
將管道拆開,逐項來看:
cut -d':' -f7 /etc/passwd # 以冒號為分隔符,取第七個字段
sort # 預排序
uniq -c # 去重,合計歸總
sort -rn # 由大到小,再次排序
復制代碼
對于cut
命令, 常用**-d
分隔符(delimiter)開關來做列向切割。tab制表符是默認的分隔符。切割后的各列通過-f
**域(field)開關來索引。這點與awk
的$1...$NF
類似。
$ cat ipaddr.list
10.0.0.20 # lanyard
192.168.0.2 # laptop
10.0.0.5 # mainframe
192.168.0.4 # office
10.0.0.2 # sluggish
192.168.0.12 # speedy
復制代碼
**-f2
和$2
**等效,都能取到第二列
$ cut -d'#' -f2 < ipaddr.list
$
$ awk -F'#' '{print $2}' < ipaddr.list
復制代碼
對于列寬度固定的格式化數據,比如ps -l
或ls -l
的輸出,可以用**-c
**列索引(column)開關來定位。第一列索引號是1,依次遞增。
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 9148 9143 0 80 0 - 5322 - pts/0 00:00:00 bash
0 R 1000 9536 9148 0 80 0 - 7466 - pts/0 00:00:00 ps
...
復制代碼
字符區間[12,15]是PID列,
$ ps -l | cut -c12-15
PID
9148
9536
...
復制代碼
也可以用開區間,例如用[67,)取CMD至末尾
$ ps -l | cut -c67-
CMD
bash
ps
...
復制代碼
怎么取出下邊方括號內的數據列?
$ cat delimited_data
Line [l1].
Line [l2].
Line [l3].
復制代碼
當然,優雅的解法,肯定是用awk
+正則
不過,用cut
也行:先剪掉左括號,再剪掉右括號。簡單直接。
$ cut -d'[' -f2 delimited_data | cut -d']' -f1
l1
l2
l3
復制代碼
回到本章最開始的例子,了解一下“去重”。
$ 預排序 | uniq -c | 再次排序
復制代碼
uniq
適用于預排序過的序列。**-c
開關意思是計數(count),將預排序后的各個相鄰的重復項匯總計數。還有一個-d
**開關,用于列出重復項(duplicate)。
當uniq
接收到兩個文件作為參數時,第二個文件被用來接收輸出,里邊原有的內容會被覆蓋掉。
$ uniq -d file.in file.out
復制代碼
如果不需要計數,可以用sort
的**-u
**開關去重(unique):
cut -d':' -f7 /etc/passwd | sort -u
復制代碼
sort
命令,有三個開關最為常用:
-r
逆序(reverse)
$ sort -r
復制代碼
-f
混雜(fold),忽略大小寫,即“將大小寫混為一體”
$ sort -f# GNU長格式參數的等效寫法:
$ sort -–ignore-case
復制代碼
-n
數字(number) 將排序對象視為數字
舉個例子:對ip地址排序
$ cat ipaddr.list
10.0.0.20 # lanyard
192.168.0.2 # laptop
10.0.0.5 # mainframe
192.168.0.4 # office
10.0.0.2 # sluggish
192.168.0.12 # speedy
復制代碼
用前邊介紹過的cut
,先去掉注釋列
$ cut -d# -f1 ipaddr.list
10.0.0.20
192.168.0.2
10.0.0.5
192.168.0.4
10.0.0.2
192.168.0.12
復制代碼
$ !! | sort -t . -k 1,1n -k 2,2n -k 3,3n -k 4,4n
10.0.0.2
10.0.0.5
10.0.0.20
192.168.0.2
192.168.0.4
192.168.0.12
復制代碼
先用**-t
**指定 域分隔符(field seperator),這里是點號。分隔出四個域。
-k 1,1n
,用人話表達,就是"從第一個域(1)的首,直至(,)第一個域(1)的尾,按數字(n)排序"。后邊的2、3、4以此類推。
這是新式的POSIX風格的寫法。如果按舊式(已廢止),要寫成這樣
$ sort -t. +0n -1 +1n -2 +2n -3 +3n -4
復制代碼
一樣丑。舊式寫法就不多介紹了。
sort
的排序行為,會受**本地化設置(locale setting)**的影響。所以,如果你發現排序行為跟預期不符,最好先檢查一下該設置。
最后,再介紹一個概念,穩定排序(stable sort)。
現在, 我們只希望對第四個數域進行排序:
$ sort -t. -k4n ipaddr.list
10.0.0.2 # sluggish
192.168.0.2 # laptop
192.168.0.4 # office
10.0.0.5 # mainframe
192.168.0.12 # speedy
10.0.0.20 # lanyard
復制代碼
對比原始的ip列表。可以看到,雖然laptop和sluggish行的第四個數是相等的,但排序后sluggish被提到了前邊
$ cat ipaddr.list
...
192.168.0.2 # laptop
...
10.0.0.2 # sluggish
...
復制代碼
這是因為,sort默認會進行last-resort comparison的操作:如果分不出大小,就用其他域值來輔助判斷,進行終極的比較。
這種行為,可以通過**-s
**開關(stable)禁用
$ sort -t. -s -k4n ipaddr.list
192.168.0.2 # laptop
10.0.0.2 # sluggish
...
復制代碼
tr wc 轉換 統計
將分號全部替換成逗號
$ tr ';' ',' <源文件 >目標文件
復制代碼
這個是tr(translate)命令最原始的用法。分號和逗號是一對一的替換關系
也可以進行多對一的替換,逗號','會被展開成';:.!?'的長度
$ tr ';:.!?' ',' <源文件 >目標文件
復制代碼
一對多呢?這樣寫是沒有意義的。';:.!?'長出來的部分都會被截斷
$ tr ',' ';:.!?' <源文件 >目標文件
復制代碼
作為文字轉換和替換工具,tr不如sed功能豐富,至少tr不支持正則表達式,所以限制了使用范圍。
但是,tr也內置了一些能處理字符范圍的語法。
比如,大小寫的轉換
$ tr 'A-Z' 'a-z' <源文件 >目標文件
復制代碼
$ tr '[:upper:]' '[:lower:]' <源文件 >目標文件
復制代碼
總之記住一點,保證替換和被替換目標長度(或范圍)的一致。否則tr會自動去做補齊和截斷,這可能并不是你所期望的。
ROT-13也稱為回轉13,誕生于古羅馬。通過字母移位實現簡單的加解密。
密文 = ROT13(明文) 明文 = ROT13(密文)
$ cat /tmp/joke
Q: Why did the chicken cross the road?
A: To get to the other side.
復制代碼
$ tr 'A-Za-z' 'N-ZA-Mn-za-m' < /tmp/joke
D: Jul qvq gur puvpxra pebff gur ebnq?
N: Gb trg gb gur bgure fvqr.
復制代碼
$ !! | tr 'A-Za-z' 'N-ZA-Mn-za-m'
Q: Why did the chicken cross the road?
A: To get to the other side.
復制代碼
DOS/Windows,一行結束的標志是"回車"+"換行",兩個字符。Linux,只有一個字符,"換行"。
可以通過開關**-d
**進行刪除(delete)
$ tr -d '\r' <dos文件 >linux文件
復制代碼
這樣,所有的回車鍵都被刪除了。包括行末和行內的。很少會有回車鍵出現在“行內”(inline),但這也是可能的。為了避免誤刪,可以考慮用更專業的轉換工具,比如dos2unix或unix2dos
總結一下除了回車鍵之外的轉義字符:
轉義字符【簡表】
轉義符 | 描述 |
---|---|
\ooo | 1-3個八進制數 |
\\ | 反斜杠自身 |
\a | 嗶 |
\b | 退格 |
\f | 換頁 |
\n | 換行 |
\r | 回車 |
\t | 制表(水平) |
\v | 制表(垂直) |
wc用于字數統計(word count)
$ wc data_file
5 15 60 data_file# 統計行數
$ wc -l data_file
5 data_file# 統計詞數
$ wc -w data_file
15 data_file# 統計字符(字節)數
$ wc -c data_file
60 data_file# 60字節,與ls的結果一致
$ ls -l data_file
-rw-r--r-- 1 jp users 60B Dec 6 03:18 data_file
復制代碼
如果希望將統計的結果作為變量,
這樣是不行的
data_file_lines=$(wc -l "$data_file")
復制代碼
因為你會得到"5 data_file",而不是數字5
可以用awk將5提取出來
data_file_lines=$(wc -l "$data_file" | awk '{print $1}')
復制代碼
find locate slocate 查找
如何在大海里撈針?
所謂文件夾(folders),是圖形用戶界面(GUI)里的通俗叫法。更專業(BIGE)的名稱,叫做子目錄(subdirectories)
先從最基本的find開始
在當前路徑(.)查找所有的mp3文件,然后移動到~/songs
$ find . -name '*.mp3' -print -exec mv '{}' ~/songs \;
復制代碼
與前邊介紹的各種單字符命令開關(-d
, -n
)不同,find
使用謂語(predicates)來修飾各種行為,比如上邊的-name
,-print
,-exec
,對應名稱,打印,執行。花括號用于接收找到的文件。
文件名有怪異(odd)字符怎么辦?UNIX玩家眼里,任何非小寫、非數字都是怪異的,比如大寫、空格、各種標點、頭上帶音調的字母等等。
$ find . -name '*.mp3' -print0 | xargs -i -0 mv '{}' ~/songs
復制代碼
-print0
告訴find
,用空字符\0
作為各個文件的分割符。同樣,使用-0
告訴xargs
,管道前邊傳過來的數據使用\0
做為分隔符。
因為mv
移動完一個文件,才能移動下一個。所以,還特別使用了-i
開關,讓參數(mp3文件)一個一個的傳進花括號內。
對于可以一次處理一批文件的命令,比如chmod
,可以批量修改很多文件的權限。這時,xargs
會把管道傳過來的文件流, 一次性的傳給chmod處理。這樣效率就很高。
$ find some_directory -type f -print0 | xargs -0 chmod 0644
復制代碼
如果這樣寫,處理效率就低了
$ find some_directory -type f -print0 | xargs -i -0 chmod 0644 '{}'
復制代碼
繼續討論mp3文件。
如果當前路徑有些mp3文件只是鏈接、而原始文件在其他地方,find
會默認忽略。
為了將這些非當前路徑的mp3也包括進來,可以加個謂語-follow
(跟蹤)
$ find . -follow -name '*.mp3' -print0 | xargs -i -0 mv '{}' ~/songs
復制代碼
如果mp3后綴名可能是大寫:MP3,可以使用-iname
(ignore case)忽略大小寫
$ find . -follow -iname '*.mp3' -print0 | xargs -i -0 mv '{}' ~/songs
復制代碼
find
不支持-iname
? 那就只能這樣寫了:
$ find . -follow -name '*.[Mm][Pp]3' -print0 | xargs -i -0 mv '{}' ~/songs
復制代碼
也可以按修改時間(modification time)查找。+
大于,-
小于,無符號表示“正好”
大于90天
$ find . -name '*.jpg' -mtime +90 -print
復制代碼
還可以用邏輯與-a
(and)、或-o
(or)做組合搜索
大于7天,并且,小于14天
$ find . -mtime +7 -a -mtime -14 -print
復制代碼
大于14天的text文件,或者,小于14天的txt文件
$ find . -mtime +14 -name '*.text' -o \( -mtime -14 -name '*.txt' \) -print
復制代碼
上邊的一對圓括號是必須的,因為相鄰的兩個謂語-name
和-print
,等效于一個邏輯與-a
的組合。
所以,為了消除歧義,必須加上括號。且因為括號在bash語法中有其他特殊含義,所以還必要用反斜杠\取消轉義。
也可以先指定文件類型,縮小查找的范圍
查找文件名中包含關鍵字python的目錄
$ find . -type d -name '*python*' -print
復制代碼
文件類型【簡表】
符號 | 描述 |
---|---|
b | 塊文件 |
c | 字符文件 |
d | 目錄 |
p | 管道文件,fifo |
f | 普通文件 |
l | 符號鏈接 |
s | 套接字 |
D | Solaris專用,“門” |
-size
表示按文件大小查找。加減號用法同-mtime
大于3M
$ find . -size +3000k -print
復制代碼
文件大小的單位,除了k
,也可以是c
,表示字節。b
或者留空,表示塊(block),一個塊通常是512字節,根據不同的文件系統而定。
如果只依稀記得要找的文件中包含某個特殊的詞,比如'basher',且該文件是txt文本,也確信就在當前路徑,那么可以這樣,直接用grep
grep -i basher *.txt
復制代碼
開關**-i
**表示忽略大小寫(ignore case)
如果文件可能藏在當前路徑的某個子目錄下,可以用*
通配符
grep -i basher */*.txt
復制代碼
如果不靈,那就該find
命令上場了
find . -name '*.txt' -exec grep -Hi basher '{}' \;
復制代碼
其中的-H
顯示文件名。最后的\;
表示該組命令結束,不加也行,這里只是預防如果你在后邊還要跟些其他語句。其他的命令參數,在前邊都介紹過了。
如果搜索范圍過大,find
的效率是很低的,因為它對整個查找范圍用的窮舉搜索。
locate
命令,速度就快很多。因為它的搜索,建立在索引上。
當然,前提是索引存在,且是新的。
這一點,操作系統會做索引的維護工作,比如通過cron job
來完成
索引的內容,一般是整個文件系統內各文件的名稱和位置,不會深入到文件內部,所以不支持按內容搜索。
slocate
, 除了提供文件名和路徑信息,還包含權限。用戶只能搜索到自己名下的文件。出于安全考慮,locate
命令一般會鏈接到slocate
。
tar gzip 壓縮 解壓
AR, ARC, ARJ, BIN, BZ2, CAB, CAB, JAR, CPIO, DEB, HQX, LHA, LZH, RAR, RPM, UUE, ZOO
在傳統的UNIX語境中,存檔、打包(archiving, combining),和壓縮(compressing)是兩碼事,對應不同的工具。而在windows,是不做區分的。
首先,tar
(tape archive)生成包(tarball),然后,再通過gzip
, bzip2
等工具生成類似tarball.tar.Z, tarball.tar.gz, tarball.tgz, or tarball.tar.bz2等格式的壓縮包,當然也包括流行的zip格式。
tarball.tar.Z是最原始的UNIX壓縮包格式。現在更常見的,是gzip,bzip2等。
tarball.tar.gz是這樣生成的:
$ tar cf 包.tar 要打包的文件路徑 # 打包
$ gzip 包.tar # 壓縮
復制代碼
GNU tar兼備壓縮的功能,可以一步到位
$ tar czf 包.tgz 要打包的文件路徑
復制代碼
為了與windows兼容,也經常會使用zip格式
$ zip -r 壓縮包 要打包的文件路徑
復制代碼
zip和unzip由InfoZip提供,用于大多數UNIX平臺。**-l
開關用于UNIX的換行符向DOS兼容,-ll
**用于反向兼容。更多細節,請參考使用手冊。
紅帽的RPM(Red Hat Package Manager),其實是加了頭部的CPIO文件
$ rpm2cpio some.rpm | cpio -i
復制代碼
Debian的.deb文件,其實是gzipped或bzipped格式的ar包,可以通過標準的ar, gunzip或bunzip2來解包。
windows平臺的WinZip, PKZIP, FilZip,以及7-Zip等,也支持眾多的壓縮格式。
解壓之前,最好先用file
命令查看壓縮包的格式,再決定使用哪種解壓工具
$ file 文件.*
文件.1: GNU tar archive
文件.2: gzip compressed data, from Unix
$
$ gunzip 文件.2
gunzip: 文件.2: unknown suffix -- ignored
$
$ mv 文件.2 文件.2.gz
$ gunzip 文件.2.gz
$
$ file 文件.2
文件.2: GNU tar archive
復制代碼
拆包之前,最好先用tar -t
查看路徑表。預先了解拆包時各個文件的去向
$ tar tf some.tar | awk -F/ '{print $1}' | sort -u
復制代碼
最后,強烈建議,每次用tar打包時,使用相對路徑,而不是絕對路徑。這樣,拆包時文件的去向是可控的。用絕對路徑的話,會有覆蓋掉該路徑下原始文件的風險。
五、加分技能
daemon-ize 守護進程
守護進程,沒有控制臺,但常駐后臺
首先,守護進程(daemon)不是像這樣,用個&
就能簡單實現的
$ ./daemonscript.sh &
復制代碼
特別是當你用SSH遠程操作時。如果此時你登出,SSH還會一直掛在那,傻等那個“后臺”腳本結束。而那個腳本,是不會結束的。
正解一:
$ nohup ./daemonscript.sh 0<&- 1>/dev/null 2>&1 &
復制代碼
分解來看:
nohup # 告訴腳本,不接收控制臺登出時傳過來的hangup信號
./daemonscript.sh # 腳本名稱
0<&- # 關閉STDIN(0)
1>/dev/null # 丟棄STDOUT(1)
2>&1 # 丟棄STDERR(2)
& # 后臺運行
復制代碼
正解二:
nohup mydaemonscript >>/var/log/myadmin.log 2>&1 <&- &
復制代碼
分解來看:
nohup
./daemonscript.sh
>>/var/log/some.log # STDOUT(1) 追加寫進日志
2>&1 # STDERR(2) 追加寫進日志
<&- # 關閉 STDIN(0)
&
復制代碼
注意一個細節: 在正解二中,對于標準文件描述符(0,1,2),0和1的符號可以省略不寫,2必須顯式聲明。關于順序,1必須在2之前聲明。0的位置隨意。
source . $include 代碼復用
先看一個配置文件,這里定義了三個參數
$ cat myprefs.cfg
SCRATCH_DIR=/var/tmp
IMG_FMT=png
SND_FMT=ogg
$
復制代碼
對于通用的參數設置,或代碼片段,可以放在單獨的文件中,供其他腳本復用。
復用的方法有三種,逐一介紹:
方法一,使用bash的source
命令
source $HOME/myprefs.cfg
cd ${SCRATCH_DIR:-/tmp}
echo 你常用的圖片格式是:$IMG_FMT
echo 你常用的音樂格式是:$SND_FMT
復制代碼
方法二,POSIX風格的單點號.
. $HOME/myprefs.cfg
復制代碼
點號很容易被漏看
方法三,類c語言
$include $HOME/myprefs.cfg
復制代碼
注意:include
前邊的$
不是命令提示符。并且,請保證被復用的文件可讀、可執行。
代碼復用,是bash腳本一個既強大又危險的功能。因為它讓你少寫代碼的同時,也讓你的腳本,對外部代碼敞開了大門。
開頭那個配置文件,只是一些常量聲明。也叫做被動語句
主動語句呢?
$ cat myprefs.cfg
SCRATCH_DIR=/var/tmp
IMG_FMT=$(cat $HOME/myimage.pref)
if [ -e /media/mp3 ]
thenSND_FMT=mp3
elseSND_FMT=ogg
fi
echo config file loaded
$
復制代碼
看到沒?邏輯判斷,cat
命令,echo
命令
只要符合語法規范就行,任意發揮。
function 函數的定義、傳值、返回
函數請務必在使用前定義,否則會收到類似command not found
的錯誤提示。
先定義
function usage ( )
{printf "usage: %s [ -a | - b ] file1 ... filen\n" $0 > &2
}
復制代碼
后使用
if [ $# -lt 1]
thenusage
fi
復制代碼
定義的方式可以很靈活,
function usage ( )
{...
}function usage {...
}usage ( ) {...
}usage ( )
{...
}
復制代碼
以上四種寫法都對。不過,關鍵字function
或()
,至少要保留一樣。
建議保留function
,既一目了然,也方便像這樣grep '^function' script
進行函數查找
重定向也可以放在外邊,把整個函數的輸出都傳給STDERR(2)
function usage ( )
{printf "usage: %s [ -a | - b ] file1 ... filen\n" $0
} > &2
復制代碼
如何傳參給函數?如何使用返回的結果?
# 定義函數:
function max ( )
{ ... }# 傳參給函數:
max 128 $SIM
max $VAR $CNT
復制代碼
參數緊跟在函數名后,用空格隔開,不需要像其他語言那樣使用括號
函數的返回呢?
先看完整的函數定義:
function max ( )
{local HIDNif [ $1 -gt $2 ]thenBIGR=$1elseBIGR=$2fiHIDN=5
}
復制代碼
執行結果保存在(非局部變量)BIGR中。HIDN是局部變量
所以,可以像這樣在函數外部訪問返回值
echo $BIGR
復制代碼
或者,也可以讓函數把返回值先吐到屏幕
function max ( )
{if [ $1 -gt $2 ]thenecho $1elseecho $2fi
}
復制代碼
然后,在外部用$()
接收屏幕的內容
BIGR=$(max 128 $SIM)
復制代碼
第一種方法通過變量綁定了返回結果(coupling),比較呆板。
后一種解除了綁定(de-coupling),如果返回值很多的話,從屏幕接收完還需要多一步拆解的動作,又會比較麻煩。
孰優孰劣,自己權衡吧
在函數的生命周期,可訪問到一個叫$FUNCNAME
的內置變量,這個變量以數組的形式,保存當前函數調用棧(call stack)的所有信息。[0]是函數名自身,[1], [2]...是各個參數。棧頂是main
。
捕獲中斷 trap kill
中斷信號是獵物,trap是陷阱
trap -l
和kill -l
可以列出(list)所有中斷,種類和數量因各操作系統而異。
$ trap -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
...
復制代碼
腳本被中斷時,返回值(exit status)是128+中斷編號。比如,ABRT對應134(128+6)。
特別值得一提的,是-SIGKILL ( -9 )
,它不會被任何陷阱捕獲,相當于絕殺。腳本會被KILL
信號即刻殺死,不會做任何退出前的現場清理工作。所以,請謹慎使用。
此外,還有三個偽中斷信號沒有列出。主要用在一些特定的場合。做個簡單介紹,更多細節請參考bash手冊。
DEBUG
,調試信號,類似于EXIT
,通常放在待調試的語句之前。RETURN
,返回信號,在函數調用或外部source (.)
引用完成時,主語句恢復執行時觸發。ERR
,異常信號,在某條命令崩潰時觸發。
trap
命令的基本用法
trap [-lp] 參數 中斷信號
復制代碼
-l
前邊介紹過了。-p
打印當前設置的陷阱和它們的句柄(handlers)。
參數是一條自定義的語句或函數。中斷可以是一條或多條。
trap ' echo "你逮到我了! $?" ' ABRT EXIT HUP INT TERM QUIT
復制代碼
參數也可以是空字符串(null string),表示忽略后邊列出的中斷。
參數也可以留空,或只寫一個減號-
,表示按系統缺省處理。
trap USR1
trap - USR1
復制代碼
POSIX,這里具體指的是1003.2標準,會對trap
和kill
的行為產生一些影響。
做個小實驗:
$ set -o posix # 首先,打開posix開關$ kill -l
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 ...
復制代碼
可以發現,所有中斷標識的前綴SIG和編號,都消失了。
而且,posix對缺省參數,有更嚴格的格式要求:
$ trap USR1 # 格式錯誤。參數不能為空
trap: usage: trap [-lp] [[arg] signal_spec ...]$ trap - USR1 # 格式正確。留空的話,至少要寫個減號占位$ set +o posix # 關閉posix開關
復制代碼
POSIX風格這種顯式的表達形式,有助于消除語法歧義。
最后,來看個完整的例子:一個殺不死的腳本。
#!/bin/bash -
#名稱:hard_to_kill.sh# 先定義一個陷阱函數
function trapped {if [ "$1" = "USR1" ]; thenecho "[$?] 被你的$1陷阱逮到了!"exitelseecho "[$?] 逃過$1陷阱,滅哈哈哈~~~"fi
}# 設置哪些中斷信號需要捕獲
trap "trapped ABRT" ABRT
trap "trapped EXIT" EXIT
trap "trapped HUP" HUP
trap "trapped INT" INT
trap "trapped KILL" KILL # 如前所述,這條語句是無效的
trap "trapped QUIT" QUIT
trap "trapped TERM" TERM
trap "trapped USR1" USR1 # 能殺死腳本的,只有USR1# 陷阱設置完畢,開個無限循環
while ((1)); do:
done
復制代碼
這個腳本比較有趣,讀者可以在自己機子上跑跑。通過發送kill -USR1
信號或萬能的-KILL
信號退出。
關于別名 alias
在bash交互控制臺,你可以通過alias來設置各種命令的別名。
alias很聰明,能避免定義出現死循環
$ alias ls='ls -a'
$ alias echo='echo ~~~'
復制代碼
不帶參數時,列出所有。有些默認的別名,預定義在bash的配置文件中(比如~/.bashrc)
$ alias
grep='grep --color=auto'
l='ls -CF'
la='ls -A'
ll='ls -l'
ls='ls --color=auto'
復制代碼
alias的實現機制,就是簡單的文本替換,且具有高優先級。
比如,用一個字母h,列出主目錄的文件
$ alias h='ls $HOME'#或者
$ alias h='ls ~'
復制代碼
這里注意要用單引號,表示$HOME變量在使用時才展開,而不是定義時。
unalias用于解除別名
$ unalias h
復制代碼
如果別名多到你自己都看不懂,可以用-a
,刪除當前會話的所有(all)別名
$ unalias -a
復制代碼
但如果unalias
自身也是個別名,怎么辦?
可以用反斜杠\
,禁止對別名(如果存在)的展開
$ \unalias -a
復制代碼
另外,不能像這樣使用位置變量$1
,因為它在當前語境沒有任何意義,除非是放在一個函數里邊。$HOME
不一樣,它是環境變量。
$ alias dr='mkdir $1 && cd $1'
復制代碼
如果語境中存在重名,但只想使用原生的bash內置命令,可以用builtin修飾:
# builtin 命令 命令的參數
$ builtin echo test
復制代碼
內置,是指寫在bash源碼里邊,隨之啟動并常駐內存的那些最基本的命令,cd, exit等。
原生,與用戶自定義相對。
如果語境中存在重名,但只想使用原生的外部命令,可以用command修飾:
# command 命令 命令的參數
$ command awk -f asar.awk
復制代碼
外部,一般體型較大,不隨bash啟動,在使用時才從硬盤調入內存的命令,grep, awk等。
如果搞不清楚哪些對哪些,可以先用type
(-a
)查看命令的類別
# exit是原生的內置命令
$ type exit
exit is a shell builtin# ls既是自定義別名,也是外部命令
$ type -a ls
ls is aliased to 'ls --color=auto'
ls is /bin/ls
復制代碼
對于外部命令,你也可以添加絕對路徑前綴來繞過別名
$ /bin/ls
復制代碼
前提是你得知道準確的路徑。否則,還是用command
好了,它會從$PATH
中讀取路徑。當然,如果路徑不對,command
也枉然。
最后再看個例子。這里,用戶自定義了一個同名的cd
函數:用三個點號...
替代常規的寫法../..
,返回上上級目錄。在函數內部,就使用了builtin
,來引用重名的內置命令cd
function cd ()
{if [[ $1 = "..." ]]thenbuiltin cd ../..elsebuiltin cd $1fi
}
復制代碼
六、日期與時間
strftime格式【全表】
格式 | 描述 |
---|---|
%% | 百分號,字面 |
%a | 星期,簡 (Sun..Sat) |
%A | 星期,全 (Sunday..Saturday) |
%B | 月,全 (January..December) |
%b | 月,簡 %h (MMM Jan..Dec) |
%c | 日期 時間,本地缺省 |
%C | 年,兩位 (CC 00..99) |
%d | 天,兩位 (DD 01..31) |
%D | 日期 %m/%d/%y (MM/DD/YY) 【注1】 |
%e | 天 (D 1..31) |
%F | 日期 %Y-%m-%d (CCYY-MM-DD) 【注2】 |
%g | 年,兩位,對應%V 周數 (YY) |
%G | 年,四位,對應%V 周數 (CCYY) |
%H | 小時,全天,兩位 (HH 00..23) |
%h | 月,簡 %b (MMM Jan..Dec) |
%I | 小時,半天,兩位 (hh 01..12) |
%j | 天,三位 (001..366) |
%k | 小時,全天 (H 0..23) |
%l | 小時,半天 (h 1..12) |
%m | 月,兩位 (MM 01..12) |
%M | 分,兩位 (MM 00..59) |
%n | 新行,字面 |
%N | 納秒,九位 (000000000..999999999) [GNU] |
%p | 半天,大寫 (AM/PM) |
%P | 半天,小寫 (am/pm) [GNU] |
%r | 時間,半天 %I:%M:%S (hh:MM:SS AM/PM) |
%R | 小時:分,兩位 %H:%M (HH:MM) |
%s | 秒數,UTC元時間(1970年1月1日零時)至今 |
%S | 秒,兩位 (SS 00..61) 【注3】 |
%t | 制表符,字面 |
%T | 時間 %H:%M:%S (HH:MM:SS) |
%u | 周一-周日 (1..7) |
%U | 周數 (周日-周六) (00..53) |
%v | 日期,非標準 %e-%b-%Y (D-MMM-CCYY) |
%V | 周數 (周日-周六) (01..53) 【注4】 |
%w | 周日-周六 (0..6) |
%W | 周數 (周一-周日) (00..53) |
%x | 日期,本地最優 |
%X | 時間,本地最優 |
%y | 年,兩位 (YY 00..99) |
%Y | 年,四位 CCYY |
%z | UTC時區 ISO 8601格式 [-]hhmm |
%Z | 時區名稱 |
【注1】只有美國才用MM/DD/YY。其他地方都是DD/MM/YY,所以這個格式有歧義,應避免使用。建議用%F
替代,因為它是公認的標準格式,且表達清晰。
【注2】CCYY-MM-DD符合ISO 8601標準; HP-UX系統是個例外,它的月份用英文全稱表示。
【注3】秒數的區間之所以是00-61,而不是00-59,是考慮到存在周期性的閏秒和雙閏秒。
【注4】根據ISO 8601,包含1月1日的星期,如果它在新年的天數至少有四天,則被視為新年的第一周,否則被歸為上一年的第53周。而它的下一周則是新年的第一周。對應的年份通過%G
獲得。
格式化時間
先聲明幾個環境變量
$ STRICT_ISO_8601='%Y-%m-%dT%H:%M:%S%z' # 【注1】
$ ISO_8601='%Y-%m-%d %H:%M:%S %Z' # 可讀性更強的ISO-8601
$ ISO_8601_1='%Y-%m-%d %T %Z' # %T等于%H:%M:%S
$ DATEFILE='%Y%m%d%H%M%S' # 用于在文件名內嵌入時間戳
復制代碼
【注1】: ISO
ISO 8601的優點:
- 使用廣泛,歧義少
- 更易讀,且便于awk和cut做切割處理
- 不論是用于文件名或時間序列,都能正確排序
加號+
雖然可以放在變量聲明里,但因為有些系統對這個加號的位置比較挑剔,所以還是建議在每次用到變量時才顯式加入。
$ date "+$ISO_8601"
2018-01-21 01:16:53 EST
復制代碼
GNU awk可以直接使用strftime函數
$ gawk "BEGIN {print strftime(\"$ISO_8601\")}"
2018-01-21 01:21:14 EST
復制代碼
小寫的%z
不是標準的時區寫法,用于GNU date
命令。因系統而異。
$ date "+$STRICT_ISO_8601"
2018-01-21T01:21:54-0500
復制代碼
GNU date
支持-d
參數,用于指定任意的時間。不是每個版本都支持。
$ date -d '2034-01-21' "+$ISO_8601"
2034-01-21 00:00:00 EST
復制代碼
MM/DD/YY
、DD/MM/YY
、M/D/YY
或D/M/YY
都是帶有歧義的日期格式,不建議使用。
$ date "+程序啟動于: $ISO_8601"
程序啟動于: 2018-01-21 01:28:14 EST
復制代碼
二十四小時制比十二小時制表述更清晰,也便于做時間切割
$ printf "%b" "程序啟動于: $(date "+$ISO_8601")\n"
程序啟動于: 2018-01-21 01:29:20 EST
復制代碼
在文件名內嵌入時間戳
$ echo "可以這樣重命名文件: mv file.log file_$(date +$DATEFILE).log"
可以這樣重命名文件: mv file.log file_20180121012750.log
復制代碼
時區,閏年以及夏令時等的轉換,是個及其復雜的話題和技術活,不建議讀者自行操作,而應該交給相關的命令或工具去做。
格式化任意時間
-d
參數可以通過字符串形式,指定任意時間,功能異常強大。
$ date '+%Y-%m-%d %H:%M:%S %z'
2018-01-21 02:25:47 -0500$ date -d 'today' '+%Y-%m-%d %H:%M:%S %z'
2018-01-21 02:26:05 -0500$ date -d 'yesterday' '+%Y-%m-%d %H:%M:%S %z'
2018-01-20 02:26:32 -0500$ date -d 'tomorrow' '+%Y-%m-%d %H:%M:%S %z'
2018-01-22 02:26:56 -0500$ date -d 'Monday' '+%Y-%m-%d %H:%M:%S %z'
2018-01-22 00:00:00 -0500$ date -d 'this Monday' '+%Y-%m-%d %H:%M:%S %z'
2018-01-22 00:00:00 -0500$ date -d 'last Monday' '+%Y-%m-%d %H:%M:%S %z'
2018-01-15 00:00:00 -0500$ date -d 'next Monday' '+%Y-%m-%d %H:%M:%S %z'
2018-01-22 00:00:00 -0500$ date -d 'last week' '+%Y-%m-%d %H:%M:%S %z'
2018-01-14 02:29:12 -0500$ date -d 'next week' '+%Y-%m-%d %H:%M:%S %z'
2018-01-28 02:29:35 -0500$ date -d '2 weeks' '+%Y-%m-%d %H:%M:%S %z'
2018-02-04 02:30:03 -0500$ date -d '-2 weeks' '+%Y-%m-%d %H:%M:%S %z'
2018-01-07 02:30:35 -0500$ date -d '2 weeks ago' '+%Y-%m-%d %H:%M:%S %z'
2018-01-07 02:30:59 -0500$ date -d '+4 days' '+%Y-%m-%d %H:%M:%S %z'
2018-01-25 02:31:24 -0500$ date -d '-6 days' '+%Y-%m-%d %H:%M:%S %z'
2018-01-15 02:31:32 -0500$ date -d '2000-01-01 +12 days' '+%Y-%m-%d %H:%M:%S %z'
2000-01-13 00:00:00 -0500$ date -d '3 months 1 day' '+%Y-%m-%d %H:%M:%S %z'
2018-04-22 03:32:40 -0400
復制代碼
設置默認時間【腳本】
看個完整的例子。用于自定義生成跨度一周的日期區間。可傳給SQL做查詢,生成定期匯報等。
#!/usr/bin/env bash
# 使用正午時間,是為了避免如果腳本在午夜運行,多幾秒就會使得多算一天的錯誤
START_DATE=$(date -d 'last week Monday 12:00:00' '+%Y-%m-%d')while [ 1 ]; doprintf "%b" "開始日期:$START_DATE, 是否正確? (Y/新日期) "read answer# ENTER, "Y" or "y"以外的輸入被視為待驗證日期# 日期格式: CCYY-MM-DDcase "$answer" in[Yy]) break;;[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])START_DATE="$answer"printf "%b" "用$answer覆寫$START_DATE [ok]\n";;*)printf "%b" "日期格式有誤,請重試\n";;esac
doneEND_DATE=$(date -d "$START_DATE +7 days" '+%Y-%m-%d')echo "START_DATE: $START_DATE"
echo "END_DATE: $END_DATE"
復制代碼
cron時間設置【腳本】
cron用于執行定時的計劃任務。下面是些簡單的時間設置。
# Vixie Cron
# 分 時 天 月 星期天
# 0-59 0-23 1-31 1-12 0-7# 第一個星期三 @ 23:00
00 23 1-7 * Wed [ "$(date '+%a')" == "Wed" ] && 命令 參數# 第二個星期四 @ 23:00
00 23 8-14 * Thu [ "$(date '+%a')" == "Thu" ] && 命令# 第三個星期五 @ 23:00
00 23 15-21 * Fri [ "$(date '+%a')" == "Fri" ] && 命令# 第四個星期六 @ 23:00
00 23 22-27 * Sat [ "$(date '+%a')" == "Sat" ] && 命令# 第五個星期日 @ 23:00
00 23 28-31 * Sun [ "$(date '+%a')" == "Sun" ] && 命令
復制代碼
要注意的是,每個月的最后一周不一定是滿的,如下表所示。
一月 2018
日 | 一 | 二 | 三 | 四 | 五 | 六 |
---|---|---|---|---|---|---|
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 |
所以如果你指定了第五個星期五,一定要知道自己在做什么。
epoch 元秒
表達及轉換
基本概念
- 元時 epoch: 1970年1月1日零時零分零秒,1970-01-01T00:00:00
- 元秒 epoch seconds: 是從元時至今的總秒數。
“現在”的元秒表示
$ date '+%s'
1516522891
復制代碼
任意時間點
$ date -d '2034-01-21 12:00:00 +0000' '+%s'
2021457600
復制代碼
將元秒轉換為可讀的形式
$ EPOCH='1516522891'$ date -d "1970-01-01 UTC $EPOCH seconds" +"%Y-%m-%d %T %z"
2018-01-21 03:21:31 -0500$ date --utc --date "1970-01-01 $EPOCH seconds" +"%Y-%m-%d %T %z"
2018-01-21 08:21:31 +0000
復制代碼
運算
下邊這個元秒運算的例子簡單易懂。
CORRECTION='172800' # 修正值設為兩天# 。。獲取bad_date的代碼。。bad_date='Jan 2 05:13:05' # 系統日志的時間格式# 先轉換為元秒
bad_epoch=$(date -d "$bad_date" '+%s')# 修正
good_epoch=$(( bad_epoch + $CORRECTION ))# 再轉換為可讀形式
good_date=$(date -d "1970-01-01 UTC $good_epoch seconds")# ISO格式
good_date_iso=$(date -d "1970-01-01 UTC $good_epoch seconds" +'%Y-%m-%d %T')echo "錯誤日期: $bad_date"
echo "錯誤元秒: $bad_epoch"
echo "修正: +$CORRECTION"
echo "正確元秒: $good_epoch"
echo "正確日期: $good_date"
echo "正確日期_iso: $good_date_iso"# 。。good_date用于后續代碼。。
復制代碼
元秒換算 【全表】
秒 | 分 | 時 | 天 |
---|---|---|---|
60 | 1 | ||
300 | 5 | ||
600 | 10 | ||
3,600 | 60 | 1 | |
18,000 | 300 | 5 | |
36,000 | 600 | 10 | |
86,400 | 1,440 | 24 | 1 |
172,800 | 2,880 | 48 | 2 |
604,800 | 10,080 | 168 | 7 |
1,209,600 | 20,160 | 336 | 14 |
2,592,000 | 43,200 | 720 | 30 |
31,536,000 | 525,600 | 8,760 | 365 |