常用内建命令
:
:总是返回真(0),不产生输出,因此经常用来清空文件;true
:和:
一样,总是返回真(0),不产生输出,常用于构建无限循环;false
:和true
相反,总是返回假(1),不产生输出;test
:用于条件测试,基本同[
;[
:用于条件测试,需使用]
结尾;[[
:用于条件测试,需使用]]
结尾,支持正则、通配符;
eval
语法:eval COMMAND
,用于执行 COMMAND 字符串所指代的命令。
是不是觉得奇怪,直接运行 COMMAND 不行吗,为何要多此一举,加个 eval 呢?
确实。一般情况下,我们是用不到 eval 的,我们完全可以直接运行 COMMAND;
但有一种情况除外:需要动态生成命令并执行它,这时候 eval 就能大显身手了。
你可能会想,不是可以使用bash -c "COMMAND"
吗,这也可以动态生成命令并执行呀。
这确实可行,但如果需要改变当前进程的数据呢?这种方式只会改变子进程中的数据!
为了深入理解 eval,我们先来了解 shell 中命令的执行流程,在 shell 中,一个命令有 3 种写法:
- 直接写
Normal Command
; - 放在双引号中
"Command"
; - 放在单引号中
'Command'
。
区别如图:
- 处理管道
|
- alias 替换,将命令别名替换为真正的命令;
- brace 替换,将
ls {a,b}.txt
解析为ls a.txt b.txt
; - ~ 替换,将
~
替换为当前登录用户的家目录; - 变量替换,处理
${}
变量引用; - 命令替换,处理
$()
命令替换; - 算数表达式运算,处理
$(())
或$[]
的内容; - glob 扩展,也就是 shell 通配符(
*
、**
、?
、[]
、[^]
); - 查找可执行文件,优先级依次降低:function、built-in、$PATH 变量;
放在单引号中的命令执行流程最为简单,直接查找命令,然后执行;
而没有引号或双引号中的命令则会进行很多解析步骤。
回到 eval
上面了解了 shell 执行一个命令的具体流程和细节;很好,eval 也会进行一样的步骤,来解析一条命令!
为避免当前 shell 的解析污染 eval 的解析,建议用单引号将命令包起来,即eval 'cmd arg1 arg2 ...'
。
mktemp
mktemp
,用于创建临时文件,并打印出 tmp 文件所在的路径,参数如下。
-d
,创建临时文件夹而不是临时文件;-u
,仅仅打印 tmp 文件的路径,而不创建(不安全);-q
,在创建文件失败或其他错误时,不输出错误信息;--suffix=SUFF
,指定文件后缀;-p
,指定文件夹,默认为$TMPDIR
,如果该变量为空则使用/tmp
目录;
pgrep
pgrep
命令,可以理解为processes grep
,也就是使用正则表达式来查找与进程相关的信息,如:
pgrep 'nginx'
查找所有运行的 nginx 进程,并打印出它们的 PID;pgrep -c 'nginx'
不打印 PID,而是统计进程数目;
pgrep
的常用参数:
-i
,忽略大小写;-v
,反向匹配;-c
,统计匹配到的数量;-l
,显示 process_name(不带参数);-a
,显示 full_command_line(带启动参数);-w
,显示 TID(线程 ID);-d
,指定分隔符,默认为换行符;-f
,使用 full_process_name 去匹配(带启动参数);-P
,查找给定 PPID(父进程 ID)下的进程;-x
,只查找与 command_name 完全匹配的进程;-o
,查找最先(oldest)启动的进程;-n
,查找最后(newest)启动的进程;-F
,从给定的 pid 文件中查找进程;
lsof
lsof
即list open files
,因为在 Unix 中一切皆文件,因此 lsof 支持查找普通文件、套接字文件等;
使用详解:
lsof
,显示系统打开的所有文件;lsof filename
,显示打开了指定文件的进程;lsof +d dirname
,显示指定目录下所有被打开的文件;lsof +D dirname
,显示指定目录下所有被打开的文件(递归);lsof -c command
,显示指定进程打开的文件(根据名称);lsof -p pid
,显示指定进程打开的文件(根据 PID);lsof -u username/uid
,显示指定用户打开的文件;lsof -g group_id
,显示指定用户组 ID 打开的文件;lsof -d fd/type
,显示打开了指定 fd 的进程;lsof -R
,显示父进程 PID(PPID);lsof -n
,显示 ip 而不是 hostname;lsof -i [46][tcp|udp][@host|addr][:svc_name|port]
,按照给定条件查找;lsof cond1 -a cond2
,加了 -a 参数说明须同时满足 cond1 和 cond2,默认为 OR 或。
文件描述符:
cwd
:current work dirctory;txt
:program text (code and data);ltx
:shared library text (code and data);mem
:memory-mapped file;mmap
:memory-mapped device;pd
:parent directory;rtd
:root directory;0u
:标准输入文件1u
:标准输出文件2u
:标准错误文件
文件状态:
r
:只读模式;w
:只写模式;u
:读写模式;空格
:文件状态未知且文件未被锁定;-
:文件状态未知且文件已被锁定;
文件类型:
IPv4
:IPv4 套接字文件;IPv6
:IPv6 套接字文件;unix
:Unix 套接字文件;BLK
:块设备;CHR
:字符设备;DIR
:文件夹;REG
:普通文件;LINK
:符号链接文件;FIFO
:管道文件;
xxd、od
xxd
和od
是二进制查看器;
xxd
支持二进制、十六进制;od
支持八进制、十进制、十六进制。
xxd
命令,常用参数:
-b
,以二进制格式查看文件内容,默认为十六进制;-e
,以小端字节序显示(不建议),默认为大端字节序;-g
,设置每个显示单位的长度(字节),默认为 2 字节;-u
,以十六进制显示时,使用大写的 ABCDEF;-i
,以 C 语言风格显示(十六进制,前缀 0x);-l
,只输出前 n 个字符;
xxd
命令举例:
xxd -g1 /etc/resolv.conf
(十六进制)xxd -b /etc/resolv.conf
(二进制)
od
命令,常用参数:
-A
,指定地址进制,取值[odxn]
,o
八进制(default)、d
十进制、x
十六进制、n
不显示地址栏;-t
,指定输出格式,c
ASCII字符、oN
八进制,可指定单位长度 N、xN
十六进制,可指定单位长度 N。
od
命令举例:
od -An -to1 /etc/resolv.conf
(八进制)od -An -tc /etc/resolv.conf
(ASCII 字符 + 转义字符)
tail 技巧
tail -n3
:显示最后 3 行;tail -n+2
:显示第 2 行至最后 1 行(从 1 开始);tail -c3
:显示最后 3 字节;tail -c+2
:显示第 2 字节至最后 1 字节(从 1 开始)。
区分tab和空格
有时候我们从肉眼上很难区分 tab 和连续空格,不过我们可以借助 cat、sed 来区分:
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
$ echo -e 'www.zfl9.com\twww.baidu.com\nwww.zfl9.com www.baidu.com'
www.zfl9.com www.baidu.com
www.zfl9.com www.baidu.com
$ echo -e 'www.zfl9.com\twww.baidu.com\nwww.zfl9.com www.baidu.com' | sed -n 'l'
www.zfl9.com\twww.baidu.com$
www.zfl9.com www.baidu.com$
$ cat --help
Usage: cat [OPTION]... [FILE]...
Concatenate FILE(s) to standard output.
With no FILE, or when FILE is -, read standard input.
-n, --number 在每个输出行前打印行号
-b, --number-nonblank 覆盖选项 -n,只显示非空行的行号(空行不计数)
-s, --squeeze-blank 抑制重复空行的输出(缩减为一个空行)
-E, --show-ends 显示行尾符 '$'
-T, --show-tabs 显示制表符 '^I'
-v, --show-nonprinting 显示不可打印字符 '^'、`M-`,除了 LFD、TAB
-A, --show-all 等同于 -vET(不可打印字符、行尾符、制表符)
-e 等同于 -vE(不可打印字符、行尾符)
-t 等同于 -vT(不可打印字符、制表符)
$ echo -e 'www.zfl9.com\twww.baidu.com\nwww.zfl9.com www.baidu.com'
www.zfl9.com www.baidu.com
www.zfl9.com www.baidu.com
$ echo -e 'www.zfl9.com\twww.baidu.com\nwww.zfl9.com www.baidu.com' | cat -A
www.zfl9.com^Iwww.baidu.com$
www.zfl9.com www.baidu.com$
dirname、basename
dirname path
:获取 path 路径字符串的父目录部分dirname /a/b/c
输出/a/b
dirname /a/b/c/
输出/a/b
dirname foo/bar/baz
输出foo/bar
basename path
:获取 path 路径字符串的文件名部分basename /a/b/c
输出c
basename /a/b/c/
输出c
basename foo/bar/baz
输出baz
进入脚本所在目录
假设同一目录下有A.sh
、B.sh
两个脚本,如何优雅的在A.sh
中执行B.sh
脚本(反之亦然)?
初学者可能会使用./B.sh
,但是,这条语句执行的是当前工作目录下的 B 脚本(因为./
是相对路径),并非与 A 脚本同一目录下的 B 脚本!除非我们当前就在脚本所在目录,否则执行到该语句时就会失败。
当前工作目录(
current working directory
,cwd
)是 进程 的一个属性,使用fork
创建进程时,新进程会继承当前进程的 cwd。cwd 主要用于解析相对路径(也就是不以/
开头的路径),相对路径都是相对于 cwd 的,换句话说,cwd 是相对路径的起点。
当我们键入./A.sh
时,会通过fork
创建新进程,由这个新进程来执行 A 脚本;因此 A 脚本所在进程的 cwd 默认是我们当前所在目录。而当 A 脚本进程执行到./B.sh
时,发现这是个相对路径,因此以 cwd 为起点,寻找并打开相应文件,然后尝试执行。
问题已经清楚,我们需要在A.sh
中,将 cwd 改为脚本所在目录,才能让./B.sh
正确执行。
我们可以从$0
变量入手,因为它存储着当前脚本的路径,首先了解A
脚本是如何启动的:
- 通过绝对路径,即
/path/to/A.sh
,那么$0
为此绝对路径 - 通过相对路径,即
path/to/A.sh
,那么$0
为此相对路径 - 假设脚本目录在
PATH
环境变量中,还可以直接A.sh
,此时$0
为PATH
中对应目录拼上/A.sh
,该路径要么是绝对路径,要么是相对路径,所以可以套用上面两种情况
因此可以在A.sh
中使用cd $(dirname "$0")
来切到脚本所在目录,无论是相对还是绝对路径都能适应。
守护进程启动方式
nohup command args... </dev/null &>>/var/log/proc.log &
setsid command args... </dev/null &>>/var/log/proc.log
command args... </dev/null &>>/var/log/proc.log & disown
(command args... </dev/null &>>/var/log/proc.log &)