全部博文(1015)
分类: Java
2016-08-13 23:41:07
做Java开发,经常要查找某个包或者类在哪个jar文件里,下面用Bash来做一个小脚本。实现这个简单脚本的过程中,遇到了几个陷阱,比较典型,所以就记录一下。
为便于测试,后面的操作都在本地Maven的Repository目录下操作。
$ cd ~/.m2/repository
JDK自带的jar命令,有一个选项"-t",可以列出jar文件中的内容,所以要检查一个jar文件是否包含指定的包或者类,只需要结合grep命令就可以。
$ jar -tf javax/enterprise/cdi-api/1.0-SP4/cdi-api-1.0-SP4.jar | grep -i Stereotype javax/enterprise/inject/Stereotype.class
如果要从指定目录下所有jar文件中查找,利用find命令找出目录下所有的jar文件,可以写出下面的命令:
$ find . -type f -name '*.jar' -exec jar -tf {} | grep -i Stereotype \;
find: -exec: no terminating ";" or "+" grep: ;: No such file or directory
命令执行失败,这个不难理解,Shell解析上面命令的时候,碰到管道符号"|"时,就认为find命令结束了,而这时候的find命令是不完整的,所以报第一行错误,而本来属于find命令的"\;",对独立的grep命令是没有意义的,所以有第二行错误。
调整一下上面的命令:
$ find . -type f -name '*.jar' -exec jar -tf {} \; | grep -i Stereotype
javax/enterprise/inject/Stereotype.class
这样可以判断一个目录下的jar文件是否包含指定的包或类,但是没有打印jar文件名,而且这种方式也不可能打印出jar文件名,因为管道后的grep命令只和标准输入打交道。
再回到上面那个失败的命令,怎么让find命令的exec执行由管道连接的多个命令?即在Shell解析整个find命令时,"|"只被看做普通符号,而在执行exec指定的命令中,"|"表示管道,也就是说需要二次解析。提到二次解析,eval命令是很直接的选择:
$ find . -name '*.jar' -exec eval 'jar -tf {} | grep -i Stereotype' \;
find: eval: No such file or directory
...
find: eval: No such file or directory
失败了,找不到eval。eval是Shell内置的命令,在Shell外是找不到eval的,从上面的结果看,find命令的exec选项跟Shell应该没啥关系,所以找不到eval命令。既然exec跟Shell没关系,那可以让exec起一个bash实例:
$ find . -name '*.jar' -exec bash -c 'jar -tf {} | grep -i Stereotype' \;
javax/enterprise/inject/Stereotype.class
可以工作,但还是没打印jar文件的名字。查一下grep命令的选项,可以组合"-H"和"--label"选项来实现:
$ find . -name '*.jar' -exec bash -c 'jar -tf {} | grep -iH --label {} Stereotype' \;
./javax/enterprise/cdi-api/1.0-SP4/cdi-api-1.0-SP4.jar:javax/enterprise/inject/Stereotype.class
和预期的一样,可以满足要求,接下来就是整理成脚本,方便使用了。
脚本在简单的前提下,应该提供尽可能灵活和丰富的特性。这个查找功能虽然简单,还是有两个明显的潜在需求:
指定多个包或者类,因为使用grep,只要遵守grep的正则表达式,就可以支持;查找多个目录或文件,find命令也支持。所以设计脚本的命令接口时,意识到这两个潜在需求,就很容易支持。
脚本的命令接口定义如下:
# name 必须指定,包名或者类名,可以用'.'或者'/'来分隔,遵守grep的正则表达式 # path 可选,也可以指定多个,可以是目录,也可以是jar文件,如果没指定,默认值为当前目录 # 返回码 如果找到,返回0;没找到,返回1;参数错误,返回2。 jargrep.sh [path ...]
返回码主要是为了便于被其他脚本调用,其他脚本可以根据返回码判断指定的包名或类名是否存在。根据前面的分析和实验结果,脚本实现如下:
#!/bin/bash if [ $# -lt 1 ]; then echo "Usage: $0 name [path ...]"; exit 2; fi name=${1//./\/}; shift;
path=${@:-.};
find $path -name '*.jar' -exec bash -c "jar -tf {} | grep -iH --label {} '$name'" \;
测试上面的脚本:
# 默认当前目录. $ ./jargrep.sh 'javax/enterprise/inject/Stereotype' ./javax/enterprise/cdi-api/1.0-SP4/cdi-api-1.0-SP4.jar:javax/enterprise/inject/Stereotype.class
$ echo $? 0 # 指定多个包名或类名 $ ./jargrep.sh 'Stereotype\|MethodSorters' ./javax/enterprise/cdi-api/1.0-SP4/cdi-api-1.0-SP4.jar:javax/enterprise/inject/Stereotype.class
./junit/junit/4.11/junit-4.11.jar:org/junit/runners/MethodSorters.class
$ echo $? 0 # 指定多个目录或文件 $ ./jargrep.sh 'org.hamcrest.CoreMatchers' junit/junit/4.10/ junit/junit/4.8.1/junit-4.8.1.jar
junit/junit/4.10//junit-4.10.jar:org/hamcrest/CoreMatchers.class junit/junit/4.8.1/junit-4.8.1.jar:org/hamcrest/CoreMatchers.class
$ echo $? 0 # 找不到指定的类 $ ./jargrep.sh Stereotype junit/junit/4.10 $ echo $? 0
当找不到指定的名字时,返回值是0而不是1,这是因为find命令执行总是成功的,exec选项的结果没办法对find的返回值产生影响。另外,如果采用find结合xargs的方式,也不会有实质性差别,都不太可能根据多个Bash子实例的执行结果来决定返回结果。虽然用单行命令解决这个问题是比较困难的,但这对Shell来说并不复杂:用find命令找到一个文件列表,然后从列表中依次读出文件并检查文件是否满足要求。
解决返回值问题前,再做一个实验。如果把"jar -tf {} | grep -iH --label {} '$name'" 放在一个独立的脚本里,find的exec是可以直接调用的,不需要起新Bash实例,在前面的分析和实验里,没有这么做。在脚本里,可以考虑把这组语句放到一个函数里,毕竟函数是脚本内的脚本,把代码中的find语句改为下面的代码:
function check-jar() {
jar -tf "$1" | grep -iH --label "$1" "$name" }
find $path -name '*.jar' -exec check-jar {} \;
可读性好了很多,如果可以工作,返回值的问题也会很容易解决。执行脚本:
$ ./jargrep.sh Stereotype
find: check-jar: No such file or directory
...
find: check-jar: No such file or directory
失败了,exec找不到"check-jar",稍微想想,exec连Shell内置的命令eval都找不到,又怎么可能找得到自定义脚本里的函数呢?当然如果执行"export -f check-jar",并再让exec起新Bash实例也可以做到,但对这个问题来说,实在没这个必要。
利用循环和上面定义的check-jar函数,修改脚本如下:
status=1;
find $path -name "*.jar" -print0 | while read -r -d '' jarfile; do check-jar "$jarfile" && status=0; done exit $status;
如果check-jar成功过,就把status置为0,验证一下:
$ ./jargrep.sh 'javax/enterprise/inject/Stereotype' ./javax/enterprise/cdi-api/1.0-SP4/cdi-api-1.0-SP4.jar:javax/enterprise/inject/Stereotype.class $ echo $? 1
很奇怪,返回码没有被置为0。通过调查,确定原因是管道后面的while语句是在一个子Shell中运行的,这个子Shell可以访问脚本中的变量和函数,但是对变量的改动是不会反映到脚本中的,所以返回码一直都是1。知道了原因,调整一下语句,利用Bash的"Process Substitution"特性,改动后的脚本如下:
status=1; while read -r -d '' jarfile; do check-jar "$jarfile" && status=0; done < <(find $path -name '*.jar' -print0) exit $status;
再测试脚本,满足预先设定的目标。完整的代码如下:
#!/bin/bash if [ $# -lt 1 ]; then echo "Usage: $0 name [path ...]"; exit 2; fi name=${1//./\/}; shift;
path=${@:-.}; function check-jar() {
jar -tf "$1" | grep -iH --label "$1" "$name";
}
status=1; while read -r -d '' jarfile; do check-jar "$jarfile" && status=0; done < <(find $path -type f -name '*.jar' -size +22c -print0) exit $status;