首页 Shell 特殊命令
文章
取消

Shell 特殊命令

常用内建命令

  • ::总是返回真(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'

区别如图:

shell 执行命令的流程

  1. 处理管道|
  2. alias 替换,将命令别名替换为真正的命令;
  3. brace 替换,将ls {a,b}.txt解析为ls a.txt b.txt
  4. ~ 替换,将~替换为当前登录用户的家目录;
  5. 变量替换,处理${}变量引用;
  6. 命令替换,处理$()命令替换;
  7. 算数表达式运算,处理$(())$[]的内容;
  8. glob 扩展,也就是 shell 通配符(***?[][^]);
  9. 查找可执行文件,优先级依次降低: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

lsoflist 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

xxdod是二进制查看器;

  • 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,指定输出格式,cASCII字符、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.shB.sh两个脚本,如何优雅的在A.sh中执行B.sh脚本(反之亦然)?

初学者可能会使用./B.sh,但是,这条语句执行的是当前工作目录下的 B 脚本(因为./是相对路径),并非与 A 脚本同一目录下的 B 脚本!除非我们当前就在脚本所在目录,否则执行到该语句时就会失败。

当前工作目录(current working directorycwd)是 进程 的一个属性,使用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,此时$0PATH中对应目录拼上/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 &)
本文由作者按照 CC BY 4.0 进行授权

Shell 特殊运算符

Shell 参数解析