找出使用最频繁的Shell命令

使用最频繁 TOP 10命令:

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'| sort -rn | nl | head

先上成品,然后,我们一步一步分析这行语句都涉及到了命令,它们又是如何拼凑到一块儿完成我们的目的的。

Shell

首先我们需要明确一下 shell 的定义,大家可能经常听到 shell ,shell 脚本这些词,那么 shell 到底是什么呢?

Shell 翻译成中文的意思就是壳,和操作系统内核的核相对应,就是一个基于内核的可操作界面。可操作性界面又分为两种:

  • 图形界面类:Graphical User Interface GUI

    • Windows Exploer
    • GNOME:Unbuntu 17.0 所使用的GUI
    • KDE
  • 命令行类:Comand Line Interface CLI

    • Windows CMD
    • Windows Power Shell
    • SH: Bourne Shell
    • ZSH: Z Shell,Mac Catalina 版本后的默认 Shell

而狭义上的 shell 则是指 POSIX 制定的一种命令行解释器,有时候也指这种命令行解释器所解释的语言,就是我们通常意义上说的 shell 脚本。

POSIX 是一系列操作系统接口的集合,主流的操作系统都有实现它。而 POSIX 给出的 shell 也是一个接口,具体的实现也是有很多种,比如:zsh、bash、fish等。

在 GUI 系统里面,通常也会提供命令行的能力,提供这种能力的软件也就是我们所熟知的:终端 Termial。

history

hisotry 是一条 shell 内置命令,用于列出所有的历史命令。内置命令则意味着和 ps curl chmod 等已一个单独的二进制文件形式存在的用户命令不同,history 是直接由 shell 内置实现的,我们用定位二进制文件位置的:whereis 来查找一下 hisotry,就会发现并不存在这个文件:whereis history

不过在 zsh 里面,我们可以用 which 查看 history 的实现:

$which history
history: aliased to omz_history

$which omz_history
omz_history () {
	zparseopts -E c=clear l=list
	if [[ -n "$clear" ]]
	then
		echo -n >| "$HISTFILE"
		echo History file deleted. Reload the session to see its effects. >&2
	elif [[ -n "$list" ]]
	then
		builtin fc "$@"
	else
		builtin fc "$@" -l 1
	fi
}

可以看到 history 在 zsh 中是omz_history的别名,而继续分析这个omz_history我们可以发现它只是一个函数,看到这个函数我们就可以大概理解了,history其实就是把 $HISTFILE 这个文件里的东西全部读出来处理一下而已,这个文件默认是使用的用户目录下的.zsh_history,我们可以通过 cat ~/.zsh_history直接查看。

不过由于 hisotry 是每个 shell 内置的,各自的实现方式都有所不同,所以历史在不同的 shell 中是彼此不可见的。

我们截取一下 history 的输出,大概是形如:序号+空格+命令这样的形式:

$history | head -10
1 ls
2 cd ../
3 golint
4 which ls
...

那么要找到使用最频繁的Shell命令,第一步肯定是把用过哪些命令全部列出来,不加任何参数的 hisotry就可以实现这个事情。

awk

awk 是一个强大的文本处理工具,命名于三位创始人 Alfred Aho,Peter Weinberger, 和 Brian Kernighan的名字首字母。awk 按行处理文本,默认通过空格和TAB分隔变量,具有大量高级特性,已经复杂到可以称其为一门语言了。

命令格式:awk [参数] [脚本] [要处理的数据]

一个简单的例子,history 输出时只输出命令本身,忽略序号和参数:

$history| awk '{print $2}'
ls
cd
golint
which
...

通过 Linux 管道将 history 的输出传给 awk;

print 是 awk 的一个内置命令,类似于 echo,用于输出文本;

awk 是按行来处理文本的,而一行内则会默认根据空格和TAB分隔,$0 代表这一行的完整文本,而 $1 $2 则代表分隔后的第1、2个变量,可以通过 -F 来指定不同的分隔符。我们这里的 $1 是序号,而 $2 则是命令,后面还有各种命令对应的参数。

awk 语法相当强大,甚至有一本书专门去介绍,想详细了解的话网上的其他资料应该也非常丰富,这里就不展开了。

回到我们的目标,我们可以这样统计命令出现的频率:

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'
  1. 遍历所有行,map[command]++ 命令每出现一次,对应 map 中的 key 就加一。$2 代表命令,而 [] 则是一种类似哈希表的语法,我们把这个类似哈希表的变量命名为 map。
  2. END 表示在整个文件所有行处理完后才执行后面的逻辑
  3. 从 map 中取出每个命令及其出现次数,除以总数得到频率。NR 代表总数,是 awk 中的一个内置变量,类似的内置变量还有:NF 当前行元素的个数、FS 指定的分隔符、FILENAME 文件名等。

输出大概如下:

2 0.0182133% pwdttplay
1 0.00910664% kpwd
1 0.00910664% opt
1205 10.9735% ttplay
1 0.00910664% tpq
140 1.27493% cat
1 0.00910664% cttplay
1 0.00910664% mak
1 0.00910664% GA
87 0.792278% man
1 0.00910664% httptest
6 0.0546398% source
...

现在我们不仅得到了使用过的历史命令而且还得到了它们的频率和次数,但现在它们还是无序的,不能直观的找到最频繁的那些命令,接下来我们需要排个序。

sort

sort 是一个可以用于排序的命令,会将输入按照行进行排序,不过默认是按照字符顺序由小到大排序的。我们需要最大的那几个数在前面,所以可以使用 -r 参数让其降序排序,而按照字符顺序排序则会出现:10 比 2 小的问题,所以还需要一个参数:-n,来让其按照数字大小比较:

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'| sort -rn

输出:

606 5.5141% cd
475 4.32211% go
454 4.13103% f
364 3.3121% curl
361 3.2848% ls
358 3.25751% git
231 2.10191% grep
212 1.92903% rm
159 1.44677% ping
158 1.43767% j
153 1.39217% dig
140 1.27389% etcdctl
...

nl

现在我们的输出虽然有次数了,但是没有明确的名次,不够直观,我们可以通过 nl(line numbering filter)命令给每一行加上序号:

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'| sort -rn | nl

输出:

     1	606 5.5141% cd
     2	475 4.32211% go
     3	454 4.13103% f
     4	364 3.3121% curl
     5	361 3.2848% ls
     6	358 3.25751% git
     7	231 2.10191% grep
     8	212 1.92903% rm
     9	159 1.44677% ping
    10	158 1.43767% j
    11	153 1.39217% dig
    12	140 1.27389% etcdctl
    ...

大多数情况下,我们只关注 TOP 几的命令,而我们上面的输出几乎会打满整个终端,这个时候我们可以通过 head 命令来只查看前 N 行,head 默认输出前 10 行,可以通过 -n参数指定大小。

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'| sort -rn | nl |head

输出:

     1	606 5.5141% cd
     2	475 4.32211% go
     3	454 4.13103% f
     4	364 3.3121% curl
     5	361 3.2848% ls
     6	358 3.25751% git
     7	231 2.10191% grep
     8	212 1.92903% rm
     9	159 1.44677% ping
    10	158 1.43767% j

总结

最终我们的脚本是这样的:

history | awk '{map[$2]++} END { for (a in map )print map[ a ]" " map[ a ]/NR*100 "% " a }'| sort -rn | nl |head

通过以上命令的组合,我们就可以成功的找出最频繁使用的shell命令,这个时候我们可以针对一些常用的命令做一些优化,来提高我的效率,比如我经常使用 grep -nr 来查找当前目录的文件,输的多了实在觉得很麻烦,我就把它做成了一个 iTerm 的快捷键,实现了一键输入。