Bash Cookbook 学习笔记 【中级】

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 we
    printf("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 {            # 触发计数语句块的逻辑,即该行的项数要大于7
        user[$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 scaling
        maxm = 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。

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 -rn
     17 /bin/false          # 禁止登录
     16 /usr/sbin/nologin   # 禁止登录
      2 /bin/bash
      1 /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 -lls -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

总结一下除了回车键之外的转义字符:

转义字符【简表】

转义符描述
\ooo1-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套接字
DSolaris专用,“门”

-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 ]
then
    SND_FMT=mp3
else
    SND_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]
then
    usage
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 HIDN
    if [ $1 -gt $2 ]
    then
        BIGR=$1
    else
        BIGR=$2
    fi
    HIDN=5
}
复制代码

执行结果保存在(非局部变量)BIGR中。HIDN是局部变量

所以,可以像这样在函数外部访问返回值

echo $BIGR
复制代码

或者,也可以让函数把返回值先吐到屏幕

function max ( )
{
    if [ $1 -gt $2 ]
    then
        echo $1
    else
        echo $2
    fi
}
复制代码

然后,在外部用$()接收屏幕的内容

BIGR=$(max 128 $SIM)
复制代码

第一种方法通过变量绑定了返回结果(coupling),比较呆板。

后一种解除了绑定(de-coupling),如果返回值很多的话,从屏幕接收完还需要多一步拆解的动作,又会比较麻烦。

孰优孰劣,自己权衡吧

在函数的生命周期,可访问到一个叫$FUNCNAME的内置变量,这个变量以数组的形式,保存当前函数调用栈(call stack)的所有信息。[0]是函数名自身,[1], [2]...是各个参数。栈顶是main

捕获中断 trap kill

中断信号是猎物,trap是陷阱

trap -lkill -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标准,会对trapkill的行为产生一些影响。

做个小实验:

$ 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" ]; then
        echo "[$?] 被你的$1陷阱逮到了!"
        exit
    else
        echo "[$?] 逃过$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 = "..." ]]
    then
        builtin cd ../..
    else
        builtin cd $1
    fi
}
复制代码

六、日期与时间

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
%zUTC时区 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/YYDD/MM/YYM/D/YYD/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 ]; do
	printf "%b" "开始日期:$START_DATE, 是否正确? (Y/新日期) "
	read answer
	# ENTER, "Y" or "y"以外的输入被视为待验证日期
	# 日期格式: CCYY-MM-DD
	case "$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
done

END_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

123456
78910111213
14151617181920
21222324252627
28293031

所以如果你指定了第五个星期五,一定要知道自己在做什么。

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用于后续代码。。
复制代码

元秒换算 【全表】

601
3005
60010
3,600601
18,0003005
36,00060010
86,4001,440241
172,8002,880482
604,80010,0801687
1,209,60020,16033614
2,592,00043,20072030
31,536,000525,6008,760365

转载于:https://juejin.im/post/5a75e8fc5188257a5c60424a

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Shell scripting is a way to harness and customize the power of any Unix system, and an essential skill for Unix users, system administrators, and even professional Mac OS X developers. But beneath this simple promise lies a treacherous ocean of variations in Unix commands and standards. This thoroughly revised edition of bash Cookbook teaches shell scripting the way Unix masters practice the craft. Three Unix veterans present a variety of recipes and tricks for all levels of shell programmers so that anyone can become a proficient user of the most common Unix shell—the bash shell—and cygwin or other popular Unix emulation packages. This cookbook is full of useful scripts that readers can use right away, along with examples that explain how to create better scripts. Table of Contents Chapter 1. Beginning Bash Chapter 2. Standard Output Chapter 3. Standard Input Chapter 4. Executing Commands Chapter 5. Basic Scripting: Shell Variables Chapter 6. Shell Logic And Arithmetic Chapter 7. Intermediate Shell Tools I Chapter 8. Intermediate Shell Tools Ii Chapter 9. Finding Files: Find, Locate, Slocate Chapter 10. Additional Features For Scripting Chapter 11. Working With Dates And Times Chapter 12. End-User Tasks As Shell Scripts Chapter 13. Parsing And Similar Tasks Chapter 14. Writing Secure Shell Scripts Chapter 15. Advanced Scripting Chapter 16. Configuring And Customizing Bash Chapter 17. Housekeeping And Administrative Tasks Chapter 18. Working Faster By Typing Less Chapter 19. Tips And Traps: Common Goofs For Novices Appendix A Reference Lists Appendix B Examples Included With Bash Appendix C Command-Line Processing Appendix D Revision Control
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值