图片描述

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匹配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本身就是一个庞大的话题,所以原著<Bash Cookbook>并未过多涉及。

最近看<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

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

转义字符【简表】

转义符 描述
\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 ]
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
%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/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

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

jimhs
17 声望1 粉丝