经常写shell,那么shell如何被解析的呢?
一、sed的经典示例
$符号在shell中解析为变量,但是在sed中代表文件的最后一行。
如何显示/etc/passwd 的倒数第三行
redirect]# sed -n '$-2p' /etc/passwd
这个明显是不行的,sed内部有一个行号计数器,一行一行读取直到最后一行 ,$才是最后一行的行号。
如何解决?
先用wc -l计数,然后变量传进去再打印倒数第三行。
redirect]# line=25 redirect]# sed -n "${line}p" /etc/passwd
注意不能用单引号,单引号属于强引用,无法将变量解析。
如何要同时显示最后一行和倒数第三行?
redirect]# sed -n "${line}p;$p" /etc/passwd tcpdump:x:72:72::/:/sbin/nologin
这样为何只显示了倒数第三行内容呢?
第二个$ 属于sed的最后一行,不应该暴露给shell解析。
redirect]# sed -n "${line}p;"'$p' /etc/passwd tcpdump:x:72:72::/:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
这样也是可以的
redirect]# sed -n "${line}p;\$p" /etc/passwd tcpdump:x:72:72::/:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
redirect]# sed -n "${line}""p;\$p" /etc/passwd tcpdump:x:72:72::/:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib redirect]# sed -n ${line}"p;\$p" /etc/passwd tcpdump:x:72:72::/:/sbin/nologin rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
二、awk的经典示例
使用awk输出hello world,在hello后增加单引号
redirect]# awk 'BEGIN{print "hello world"}' hello world
redirect]# awk 'BEGIN{print "hello'"'"' world"}' hello' world 这样拆解开来看 'BEGIN{print "hello' "'" ' world"}'
redirect]# awk "BEGIN{print \"hello' world\"}" hello' world 047是单引号的ASSIC值 redirect]# awk 'BEGIN{print "hello\047 world"}' hello' world # print 中双引号的值都保留给awk awk -v q="'" 'BEGIN{print "hello"q" world"}' hello' world
三、shell解析的基本步骤
大括号扩展
生成数字
to_delete]# echo {1..10} 1 2 3 4 5 6 7 8 9 10 [root@to_delete]# [root@to_delete]# echo {a..e} a b c d e
批量创建文件:
to_delete]# touch /tmp/{a..d}.log
波浪线扩展
echo ~ 输出家目录
echo ~+ 输出当前目录,和pwd等效
echo ~- 输出上一级目录
变量替换扩展
不仅可以解释变量,也可以解释变量表达式。
例如截取字符s之前的内容。
%%s* 贪婪删除从左到右。
~]# echo $name ninesun ~]# echo ${name%%s*} nine
二次单词拆分:
~]# echo $(echo -e "hello\nworld") hello world ~]# echo "$(echo -e "hello\nworld")" hello world
算术扩展
to_delete]# a=4 [root@to_delete]# echo $((a+5)) 9
文件通配符扩展
shopt 命令用于显示和设置shell中的行为选项,通过这些选项以增强shell易用性。
shopt [-psu] [optname …]
-s 开启某个选项.
-u 关闭某个选项.
-p 列出所有可设置的选项.
1、如何解决*无法匹配以.开头的文件?
dotglob If set, bash includes filenames beginning with a `.' in the results of pathname expansion.
tmp]# ls /root/*ssh ls: cannot access '/root/*ssh': No such file or directory # 匹配不到 [root@tmp]# shopt -s dotglob [root@tmp]# ls /root/*ssh authorized_keys
2、如何解决递归到子目录进行匹配?
直接搜索,找不到子目录fork中的文件。
shopt -s globstar 开启递归搜索文件通配符扩展
globstar If set, the pattern ** used in a pathname expansion context will match all files and zero or more directories and subdirectories. If the pattern is followed by a /, only directories and subdirectories match.
grep -l "printf" **/*.c
**代表递归当前目录
-l 只列出文件名,而不是内容。这也是grep比较好用的参数.
tmp]# shopt -s globstar [root@tmp]# grep -l "printf" *.c fork.c [root@tmp]# grep -l "printf" **/*.c fork.c test/fork_1.c [root@tmp]# grep -l "printf" */*.c test/fork_1.c # ** 不仅仅递归一层目录,多级子目录同样可以递归. tmp]# grep -l "printf" **/*.c fork.c fork/fork_2/fork_2.c test/fork_1.c
c
test/fork_1.c
引号去除
cat "/proc/self/cmdline"
搜索命令
fork + exec
fork一个子bash进程,在子bash进程中加载exec替换子进程bash并执行起来
执行命令
子bash的进程退出码交还给父进程。
shell解析命令行的流程
看了这个流程你会发现shell编程中单引号、双引号的真正含义。
例如为何 echo "~" 和 echo ~的输出会不同
echo "{1..10}" 和echo {1..10}的输出结果会不同?
示例
name=longshuai a=24 echo -e "some files:" ~/i* "\nThe date:$(date +%F)\n$name's age is $((a+4))" >/tmp/a.log
解释如上的shell脚本
eval命令的用法
\$ 被当成普通的$符号而不是变量的标识。
$a 替换为hello
eval echo $hello 此时eval不执行自己,就变成了echo $hello
[root@hadoop100 ~]#a=hello You have mail in /var/spool/mail/root [root@hadoop100 ~]# [root@hadoop100 ~]#hello=ninesun [root@hadoop100 ~]# [root@hadoop100 ~]#echo $a hello [root@hadoop100 ~]#echo \$a $a [root@hadoop100 ~]# [root@hadoop100 ~]#eval echo \$a hello [root@hadoop100 ~]# [root@hadoop100 ~]#eval echo \$$a ninesun [root@hadoop100 ~]#eval echo $$a 3671a [root@hadoop100 ~]# [root@hadoop100 ~]# [root@hadoop100 ~]#eval echo $\$a 3671a [root@hadoop100 ~]#eval echo \$$a ninesun
命令行处理步骤
--update 2022年2月11日09:40:54
要想成为真正的 shell 脚本编程专家(或者为了调试一些棘手的问题),你需要理解命令行处理过程涉及的各个步骤,尤其是这些步骤的先后顺序。shell 从 STDIN 或脚本中读取的每一行被称为管道,因为其中包含了由零个或多个管道字符(|)分隔的一个或多个命令。
对于读入的管道,shell 会将其分解成命令,设置管道的 I/O,然后对每个命令执行下列操作。将命令分割成由一组固定的元字符(空格、制表符、换行符、;、(、)、<、>、|、&)分隔的词法单元(token)。
词法单元的类型包括单词、关键字、I/O 重定向、分号。
检查每个命令的第一个词法单元是否为不带引号或反斜线的关键字。如果属于起始关键字,例如 if 或其他控制结构的开头、function、{、(,那么该命令属于复合命令。
shell 会在内部为其完成相关准备工作,读取下一个命令,并重头开始这个过程。如果关键字不是复合命令的开头(例如,then、else、do 这种属于控制结构“中间”的部分;fi、done 这种属于控制结构“结束”的部分;或者是逻辑运算符),则 shell 会提示语法错误。
对照别名列表检查每个命令的第一个单词。如果有匹配,将单词替换成别名定义,然后返回第 1 步;否则,继续往下进行第 4 步。
这种方式允许出现递归别名,也允许为关键字定义别名(例如,alias aslongas=while 或 alias procedure=function)。
执行花括号扩展。例如,将 a{b,c} 扩展为 ab ac。
如果波浪线位于单词的开头,将其替换成用户的主目录($HOME)。例如,将 ~user 替换成 user 的主目录。
对以美元符号($)起始的表达式执行参数(变量)替换。
对形如 $(string) 的表达式执行命令替换。
对形如 $((string)) 的算术表达式进行求值。
将命令行中经过参数替换、命令替换、算术求值得到的结果再次分割成一系列单词。
这次使用 $IFS 所包含的字符作为分隔符,不再使用第 1 步中的那组元字符。对出现的 *、?、[] 执行路径扩展,也就是通配符扩展。
将第一个单词作为命令,按照下列顺序查找其来源:先按照函数名,再作为内建命令,接着作为 $PATH 所包含目录中的文件。设置好 I/O 重定向和其他事宜后,执行该命令。
步骤着实不少,甚至这还不是全部!在继续往下进行之前,我们应该先用一个示例来明晰这个过程。假设要执行下列命令:
alias ll = "ls -l"
再进一步假设用户 alice 的主目录(/home/alice)有一个名为 .hist537 的文件,还有一个双美元符号变量,其值为2357(记住)
保存的是进程 ID,这个数字在所有运行的进程中是唯一的)。现在我们来看看 shell 是如何处理下列命令的。
ll $(type -path cc) ~alice/.*$(($$%1000) ll $(type -pathcc) ~alice/.*$(($$%1000)) 将其分割成一系列单词。 ll 并非关键字,是小写l,因此第 2 步什么都不做 1。 ls -l $(type -path cc) ~alice/.*$(($$%1000)) 将别名 ll 替换成 ls -l。 然后shell 重复第 1~3 步; 第 2 步会将 ls -l 分割为 2 个单词。ls-l$(type -pathcc) ~alice/.*$(($$%1000)) 什么都不做。 ls -l $(type -path cc) /home/alice/.*$(($$%1000)) 将~alice 扩展成 /home/alice。 ls -l $(type -path cc) /home/alice/.*$((2537%1000)) 将 $$ 替换成 2537。 ls -l /usr/bin/cc/home/alice/.*$((2537%1000)) 对 type -path cc 执行命令替换。 ls -l /usr/bin/cc/home/alice/.*537 对算术表达式 2537%1000 求值。 ls-l /usr/bin/cc/home/alice/.*537 什么都不做。 ls -l /usr/bin/cc/home/alice/.hist537 将通配符表达式 .*537 替换成文件名。在 /usr/bin 中找到命令 ls。 执行包含选项 -l 和两个参数的 /usr/bin/ls。 1这里提到的第 x 步对应前文中相应编号的步骤。 ——译者注尽管这些步骤相当直观,但只是部分而已。还有 5 种方式可以改变该流程:引用;使用 command、builtin、enable;使用高级命令 eval
单引号('')能够完全绕过包括别名在内的第 1~10 步。单引号中的所有字符全部保持不变。单引号中不能再出现单引号,就算在其之前加上反斜线(\)也没用。双引号("")能够绕过第 1~4 步以及第 9~10 步。也就是说,它会忽略管道字符、别名、波浪号替换、通配符扩展以及通过分隔符(如空白字符)将双引号中的内容分割成一系列单词。但是,双引号仍会执行参数替换、命令替换以及算术表达式求值。双引号中可以出现双引号,在其之前加上反斜线(\)即可。另外,必须使用反斜线对具有特殊含义的 $、'(古老的命令替换分隔符)、\ 进行转义。表 C-1 给出了一个简单的示例,其中演示了引用是如何工作的。假设执行的语句是 person=hatter,用户 alice 的主目录是 /home/alice