Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
useful-scripts/bin/find-in-jars
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
executable file
409 lines (356 sloc)
12.6 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# @Function | |
# Find files in the jar files under specified directory, search jar files recursively(include subdirectory). | |
# | |
# @Usage | |
# $ find-in-jars 'log4j\.properties' | |
# # search file log4j.properties/log4j.xml at zip root | |
# $ find-in-jars '^log4j\.(properties|xml)$' | |
# $ find-in-jars 'log4j\.properties$' -d /path/to/find/directory | |
# $ find-in-jars '\.properties$' -d /path/to/find/dir1 -d path/to/find/dir2 | |
# $ find-in-jars 'Service\.class$' -e jar -e zip | |
# $ find-in-jars 'Mon[^$/]*Service\.class$' -s ' <-> ' | |
# | |
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-find-in-jars | |
# @author Jerry Lee (oldratlee at gmail dot com) | |
# | |
# NOTE about Bash Traps and Pitfalls: | |
# | |
# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! | |
# for example: readonly var1=$(echo value1) | |
# local var2=$(echo value1) | |
# | |
# Because the combination make exit code of assignment to be always 0, | |
# aka. the exit code of command in subshell is discarded. | |
# tested on bash 3.2.57/4.2.46 | |
# | |
# solution is separation of var declaration and assignment: | |
# var1=$(echo value1) | |
# readonly var1 | |
# local var2 | |
# var2=$(echo value1) | |
set -eEuo pipefail | |
# NOTE: DO NOT declare var PROG as readonly in ONE line! | |
PROG="$(basename "$0")" | |
readonly PROG | |
readonly PROG_VERSION='2.5.0-dev' | |
################################################################################ | |
# util functions | |
################################################################################ | |
# NOTE: $'foo' is the escape sequence syntax of bash | |
readonly ec=$'\033' # escape char | |
readonly eend=$'\033[0m' # escape end | |
readonly nl=$'\n' # new line | |
# How to delete line with echo? | |
# https://unix.stackexchange.com/questions/26576 | |
# | |
# terminal escapes: http://ascii-table.com/ansi-escape-sequences.php | |
# In particular, to clear from the cursor position to the beginning of the line: | |
# echo -e "\033[1K" | |
# Or everything on the line, regardless of cursor position: | |
# echo -e "\033[2K" | |
readonly clear_line=$'\033[2K\r' | |
redEcho() { | |
# -t check: is a terminal device? | |
[ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" | |
} | |
# Getting console width using a bash script | |
# https://unix.stackexchange.com/questions/299067 | |
# | |
# NOTE: DO NOT declare columns var as readonly in ONE line! | |
[ -t 2 ] && columns=$(stty size | awk '{print $2}') | |
printResponsiveMessage() { | |
if ! $show_responsive || [ ! -t 2 ]; then | |
return | |
fi | |
local message="$*" | |
# http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html | |
echo -n "$clear_line${message:0:columns}" >&2 | |
} | |
clearResponsiveMessage() { | |
if ! $show_responsive || [ ! -t 2 ]; then | |
return | |
fi | |
echo -n "$clear_line" >&2 | |
} | |
die() { | |
clearResponsiveMessage | |
redEcho "Error: $*" >&2 | |
exit 1 | |
} | |
usage() { | |
local -r exit_code="${1:-0}" | |
(($# > 0)) && shift | |
# shellcheck disable=SC2015 | |
[ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout | |
(($# > 0)) && redEcho "$*$nl" >$out | |
cat >$out <<EOF | |
Usage: ${PROG} [OPTION]... PATTERN | |
Find files in the jar files under specified directory, | |
search jar files recursively(include subdirectory). | |
The pattern default is *extended* regex. | |
Example: | |
${PROG} 'log4j\.properties' | |
# search file log4j.properties/log4j.xml at zip root | |
${PROG} '^log4j\.(properties|xml)$' | |
${PROG} 'log4j\.properties$' -d /path/to/find/directory | |
${PROG} '\.properties$' -d /path/to/find/dir1 -d path/to/find/dir2 | |
${PROG} 'Service\.class$' -e jar -e zip | |
${PROG} 'Mon[^$/]*Service\.class$' -s ' <-> ' | |
Find control: | |
-d, --dir the directory that find jar files. | |
default is current directory. this option can specify | |
multiply times to find in multiply directories. | |
-e, --extension set find file extension, default is jar. this option | |
can specify multiply times to find multiply extension. | |
-E, --extended-regexp PATTERN is an extended regular expression (*default*) | |
-F, --fixed-strings PATTERN is a set of newline-separated strings | |
-G, --basic-regexp PATTERN is a basic regular expression | |
-P, --perl-regexp PATTERN is a Perl regular expression | |
-i, --ignore-case ignore case distinctions | |
Output control: | |
-a, --absolute-path always print absolute path of jar file | |
-s, --separator specify the separator between jar file and zip entry. | |
default is \`!'. | |
-L, --files-not-contained-found | |
print only names of JAR FILEs NOT contained found | |
-l, --files-contained-found | |
print only names of JAR FILEs contained found | |
-R, --no-find-progress do not display responsive find progress | |
Miscellaneous: | |
-h, --help display this help and exit | |
-V, --version display version information and exit | |
EOF | |
exit "$exit_code" | |
} | |
progVersion() { | |
echo "$PROG $PROG_VERSION" | |
exit | |
} | |
################################################################################ | |
# parse options | |
################################################################################ | |
declare -a dirs=() | |
declare -a extensions=() | |
declare -a args=() | |
separator='!' | |
regex_mode=-E | |
use_absolute_path=false | |
show_responsive=true | |
only_print_file_name=false | |
while (($# > 0)); do | |
case "$1" in | |
-d | --dir) | |
dirs=(${dirs[@]:+"${dirs[@]}"} "$2") | |
shift 2 | |
;; | |
-e | --extension) | |
extensions=(${extensions[@]:+"${extensions[@]}"} "$2") | |
shift 2 | |
;; | |
-E | --extended-regexp) | |
regex_mode=-E | |
shift | |
;; | |
-F | --fixed-strings) | |
regex_mode=-F | |
shift | |
;; | |
-G | --basic-regexp) | |
regex_mode=-G | |
shift | |
;; | |
-P | --perl-regexp) | |
regex_mode=-P | |
shift | |
;; | |
-i | --ignore-case) | |
ignore_case_option=-i | |
shift | |
;; | |
-a | --absolute-path) | |
use_absolute_path=true | |
shift | |
;; | |
# support the legacy typo option name --seperator for compatibility | |
-s | --separator | --seperator) | |
separator="$2" | |
shift 2 | |
;; | |
-L | --files-not-contained-found) | |
only_print_file_name=true | |
print_matched_files=false | |
shift | |
;; | |
-l | --files-contained-found) | |
only_print_file_name=true | |
print_matched_files=true | |
shift | |
;; | |
-R | --no-find-progress) | |
show_responsive=false | |
shift | |
;; | |
-h | --help) | |
usage | |
;; | |
-V | --version) | |
progVersion | |
;; | |
--) | |
shift | |
args=(${args[@]:+"${args[@]}"} "$@") | |
break | |
;; | |
-*) | |
usage 2 "${PROG}: unrecognized option '$1'" | |
;; | |
*) | |
args=(${args[@]:+"${args[@]}"} "$1") | |
shift | |
;; | |
esac | |
done | |
readonly separator regex_mode ignore_case_option use_absolute_path only_print_file_name print_matched_files show_responsive args | |
# shellcheck disable=SC2178 | |
dirs=${dirs:-.} | |
# shellcheck disable=SC2178 | |
readonly extensions=${extensions:-jar} | |
(("${#args[@]}" == 0)) && usage 1 "Missing file pattern!" | |
(("${#args[@]}" > 1)) && usage 1 "More than 1 file pattern: ${args[*]}" | |
readonly pattern="${args[0]}" | |
declare -a tmp_dirs=() | |
for d in "${dirs[@]}"; do | |
[ -e "$d" ] || die "file $d(specified by option -d) does not exist!" | |
[ -d "$d" ] || die "file $d(specified by option -d) exists but is not a directory!" | |
[ -r "$d" ] || die "directory $d(specified by option -d) exists but is not readable!" | |
# convert dirs to Absolute Path if has option -a, --absolute-path | |
$use_absolute_path && tmp_dirs=(${tmp_dirs[@]:+"${tmp_dirs[@]}"} "$(cd "$d" && pwd)") | |
done | |
# set dirs to Absolute Path | |
$use_absolute_path && dirs=("${tmp_dirs[@]}") | |
readonly dirs | |
# convert extensions to find -iname options | |
find_iname_options=() | |
for e in "${extensions[@]}"; do | |
(("${#find_iname_options[@]}" == 0)) && | |
find_iname_options=(-iname "*.$e") || | |
find_iname_options=("${find_iname_options[@]}" -o -iname "*.$e") | |
done | |
readonly find_iname_options | |
################################################################################ | |
# Check the existence of command for listing zip entry! | |
################################################################################ | |
__prepareCommandToListZipEntries() { | |
# `zipinfo -1`/`unzip -Z1` is ~25 times faster than `jar tf`, find zipinfo/unzip command first. | |
# | |
# How to list files in a zip without extra information in command line | |
# https://unix.stackexchange.com/a/128304/136953 | |
if command -v zipinfo &>/dev/null; then | |
command_to_list_zip_entries=(zipinfo -1) | |
is_use_zip_cmd_to_list_zip_entries=true | |
elif command -v unzip &>/dev/null; then | |
command_to_list_zip_entries=(unzip -Z1) | |
is_use_zip_cmd_to_list_zip_entries=true | |
elif [ -n "$JAVA_HOME" ]; then | |
# search jar command under JAVA_HOME | |
if [ -f "$JAVA_HOME/bin/jar" ]; then | |
[ -x "$JAVA_HOME/bin/jar" ] || die "found \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executable!" | |
command_to_list_zip_entries=("$JAVA_HOME/bin/jar" tf) | |
elif [ -f "$JAVA_HOME/../bin/jar" ]; then | |
[ -x "$JAVA_HOME/../bin/jar" ] || die "found \$JAVA_HOME/../bin/jar($JAVA_HOME/../bin/jar) is NOT executable!" | |
command_to_list_zip_entries=("$JAVA_HOME/../bin/jar" tf) | |
fi | |
is_use_zip_cmd_to_list_zip_entries=false | |
elif command -v jar &>/dev/null; then | |
# search jar command under PATH | |
command_to_list_zip_entries=(jar tf) | |
is_use_zip_cmd_to_list_zip_entries=false | |
else | |
die "NOT found command to list zip entries: zipinfo, unzip or jar!" | |
fi | |
readonly command_to_list_zip_entries is_use_zip_cmd_to_list_zip_entries | |
} | |
__prepareCommandToListZipEntries | |
listZipEntries() { | |
local zip_file="$1" msg | |
if $is_use_zip_cmd_to_list_zip_entries; then | |
# How to check if zip file is empty in bash | |
# https://superuser.com/questions/438878 | |
msg="$("${command_to_list_zip_entries[@]}" -t "$zip_file" 2>&1)" || { | |
# NOTE: | |
# if list emtpy zip file by zipinfo/unzip command, | |
# exit code is 1, and print 'Empty zipfile.' | |
if [ "$msg" != 'Empty zipfile.' ]; then | |
clearResponsiveMessage | |
redEcho "fail to list zip entries of $zip_file, ignored: $msg" >&2 | |
fi | |
return 0 | |
} | |
fi | |
"${command_to_list_zip_entries[@]}" "$zip_file" || { | |
clearResponsiveMessage | |
redEcho "fail to list zip entries of $zip_file, ignored!" >&2 | |
return 0 | |
} | |
} | |
################################################################################ | |
# find logic | |
################################################################################ | |
searchJarFiles() { | |
printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." | |
local jar_files total_jar_count | |
jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" | |
[ -n "$jar_files" ] || die "No ${extensions[*]} file found!" | |
total_jar_count="$(echo "$jar_files" | wc -l)" | |
# delete white space | |
# because the output of mac system command `wc -l` contains white space! | |
total_jar_count="${total_jar_count//[[:space:]]/}" | |
echo "$total_jar_count" | |
echo "$jar_files" | |
} | |
__outputResultOfJarFile() { | |
local jar_file="$1" file | |
if $only_print_file_name; then | |
local matched=false | |
# NOTE: Do NOT use -q flag with grep: | |
# With the -q flag the grep program will stop immediately when the first line of data matches. | |
# Normally you shouldn't use -q in a pipeline like this | |
# unless you are sure the program at the other end can handle SIGPIPE. | |
# more info see: | |
# - https://stackoverflow.com/questions/19120263/why-exit-code-141-with-grep-q | |
# - https://unix.stackexchange.com/questions/305547/broken-pipe-when-grepping-output-but-only-with-i-flag | |
# - http://www.pixelbeat.org/programming/sigpipe_handling.html | |
if grep $regex_mode ${ignore_case_option:-} -c -- "$pattern" &>/dev/null; then | |
matched=true | |
fi | |
if [ $print_matched_files != $matched ]; then | |
return | |
fi | |
clearResponsiveMessage | |
[ -t 1 ] && echo "${ec}[1;35m${jar_file}${eend}" || echo "${jar_file}" | |
else | |
{ | |
# Prevent grep from exiting in case of no match | |
# https://unix.stackexchange.com/questions/330660 | |
# shellcheck disable=SC2086 | |
grep $regex_mode ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern" || true | |
} | while read -r file; do | |
clearResponsiveMessage | |
[ -t 1 ] && | |
echo "${ec}[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" || | |
echo "${jar_file}${separator}${file}" | |
done | |
fi | |
} | |
findInJarFiles() { | |
[ -t 1 ] && local -r grep_color_option='--color=always' | |
local counter=1 total_jar_count jar_file | |
read -r total_jar_count | |
while read -r jar_file; do | |
printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" | |
listZipEntries "${jar_file}" | __outputResultOfJarFile "${jar_file}" | |
done | |
clearResponsiveMessage | |
} | |
searchJarFiles | findInJarFiles |