在jar文件中查找包和类

做Java开发,经常要查找某个包或者类在哪个jar文件里,下面用Bash来做一个小脚本。实现这个简单脚本的过程中,遇到了几个陷阱,比较典型,所以就记录一下。

为便于测试,后面的操作都在本地Maven的Repository目录下操作。

$ cd ~/.m2/repository

1. 分析和实验

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
和预期的一样,可以满足要求,接下来就是整理成脚本,方便使用了。

2. 编写脚本

脚本在简单的前提下,应该提供尽可能灵活和丰富的特性。这个查找功能虽然简单,还是有两个明显的潜在需求:

  • 指定多个包或者类
  • 查找多个目录或文件

指定多个包或者类,因为使用grep,只要遵守grep的正则表达式,就可以支持;查找多个目录或文件,find命令也支持。所以设计脚本的命令接口时,意识到这两个潜在需求,就很容易支持。

脚本的命令接口定义如下:

# name      必须指定,包名或者类名,可以用'.'或者'/'来分隔,遵守grep的正则表达式
# path      可选,也可以指定多个,可以是目录,也可以是jar文件,如果没指定,默认值为当前目录
# 返回码     如果找到,返回0;没找到,返回1;参数错误,返回2。 
jargrep.sh <name> [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;

转载于:https://my.oschina.net/mononite/blog/149235

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值