From 0a4212d4fc9fb3be42768c066dc5288f5defca3b Mon Sep 17 00:00:00 2001 From: Zava Date: Tue, 2 Feb 2021 13:57:51 +0800 Subject: [PATCH 001/175] + add uniq-lines command --- bin/uniq-lines | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100755 bin/uniq-lines diff --git a/bin/uniq-lines b/bin/uniq-lines new file mode 100755 index 00000000..6368665e --- /dev/null +++ b/bin/uniq-lines @@ -0,0 +1,19 @@ +#!/bin/bash +# @Function +# print uniq line keep order, no sorting required + +outputUniqLines() { + awk '{ + s[$0]++ + } + + END { + for(v in s) { + if (s[v] == 1) { + print v + } + } + }' +} + +cat "$@" | outputUniqLines From d2726e3ba4b878a6cbc290c01ab461c88d78fba3 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 5 Feb 2021 12:55:40 +0800 Subject: [PATCH 002/175] + add .editorconfig --- .editorconfig | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..40d2df19 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 +tab_width = 4 + +[*.yml] +indent_size = 2 + +[*.{md,mkd,markdown}] +trim_trailing_whitespace = false From 9f54749321ceb039c7a60d339e98be61c39d53f2 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 14 Feb 2021 16:13:36 +0800 Subject: [PATCH 003/175] + add .gitignore --- .gitignore | Bin 0 -> 4454 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..a406f26949fb9833d422dbc6e7268e4f93fd4297 GIT binary patch literal 4454 zcmbVPO>^V85#2L>1+vaz#$}S3s;wNHT*j7n%8qx(WqFcKO{P>J2~n_!j|D)<+{6C% zyaq@~uGAhJA7r9IH2ULp_v4>xWh%zb-ulPR_=C5d7g{UR@q1yqL3Hx}P((-g!x%T# z@F=zGMT8oDHAY!9Ry)YmJumL>`5N-TRx39)qS?sRs$%D*8^1QT{RV&d zHJ2|@nxL!+*a?<|3l*YQ^$;P2PA8hJ=wLr9vjI$+UB&7_^=-vkxs$zhh3vY36^`Q9 z?oDq+3qcl_`CqT@62w^1W9Pe(`$bg1a2H(0Jt{Uwf+|JP9JN<=W!ViEf^}3#1nj zk{3K!F0iy*nP=HV-Xi{$#kPlJZ3BL3GnOz_3!~ODL<*>r4Vir~n)kK~%qdskp^Psv zJ-4!#NMzioP_)Y90_(@Li?d&hiZC}s46~brLzbi+eF<%h=4`=g5UXt<5PK# zXG%UZHsF>JL;0e6Hout`=ND;7rapOL0^l`4LR+}uY)rs-27oOOaagY}A%+ViUb>Ak zFTAzQprwiFC@|23YBhlb>2#y1SFoblI*V8#^$F6C15$Lz#d+CD4aL50SOs6qT6L_N zEv;BJ$Fcn}0lRYB4pNG8HAMtV^0A8A&5sBf0eqE2xD^J=My*vt`6L5=vgGY0E0I-1 z3!3^BQb9=|EBWHoGoFIUg=u*w4YCnYZK=*KVe=?FJjcze!&6>oF=@LoJLRpRP;q0u z7QG57W^}$DOxsJG-$~P2U*$EcsC4qPuYfO#9tUUvWbAn1M~(%8cfm(HaILycb| z2$dbKk*{hbDAObf5Y?-l6i_4|27?9_co-bvnVO{EY5sTqq5SZn{0lkN0xliu zPfL^sp4P%Nx_O0|7Akh|`is*O8&5Vt=&2LzDySS-tT7_T|wBGgy zr%~;9GG2Lsl$=5`0VAC3;hu1bDG_DgtGkQbm6Sn&J#5?(*Q2mMI(H3^AR4bX!fOW5 zDl~HHDF@INHNxF;=WQBOXc1aO6o_i2sNj%Vrv~Ar23XU06E!_~MDT)($SGd{-l1in z3i{sefu&2z;+KF3Q@>69bD9Jw^Q3=o{WgFu$TU-CDM2ugoLSPUhVUl50y%-wB{7hw zU7R|yR?IEWWasYvhuZ5%d?y&+bj>i_KQE*YDXiqYF^nnsD%u0 zT>2!2nonM!8BcGAkIjph+2pA;V$v#>mO%>``PK7O2c zMeIcgN*FpbwUap>*ItB8KStGr>2>~&hR{dTdaGK#j6%xw+%@Y-DOwJ7kBR$)xE#}THnFIL;52AMehsgH6Q7ruA=AgwL|#es;N@C+j1V+{ zOlgtA_yqDHr!``AXhJ8S1DLwUu0?szmlcP1Dacb)r(=26;L{*e93+h9O6r)zG|{iX z4lic@A~WSc7Sy$pS_<*u$%H|I$>jU4bzNfotQRBnK*hqH_AY>gs2^Dl4%{<9> zSV#97D#>0(a?lQ%=H+w1EQd#^kAEtC4Eon{^$kkDiemo_KfirrBspE5mxKA8Ey2jp zp39xmtTZzEJ&|D)%A|A`5^H4T4(z7|SxtB0x;Om6zt2lD@oj(EtIx|{j$W0E<%%XG zsuL5kmkW#JVsXFx3_9k~#VEPAhz;m0MjJ>7au7E>?k)e1XSDO(cav-P$_y_@-DmtC zi<=`|LD~)g#K*!+6iNSdkop`=OcXsw)KJ!$iVNqY@EkS>GvbRH4UQ&Y;}$M*^CWzUxEE6t_ma5WdW1}J^9a%H`o8ZzP~B2ZhuZ)ZMJLvGV|$mqScM4 d@vDPMzw}wBgQGj Date: Wed, 22 Apr 2020 14:50:15 +0800 Subject: [PATCH 004/175] ! bash strict mode - use `set -eEuo pipefail` - use $* instead of $@ in string - use if-else instead &&/|| - use $() instead of `` --- bin/a2l | 6 +- bin/ap | 3 +- bin/c | 53 ++++++----- bin/coat | 11 +-- bin/echo-args | 8 +- bin/find-in-jars | 130 ++++++++++++++------------ bin/rp | 9 +- bin/show-busy-java-threads | 154 +++++++++++++++++-------------- bin/tcp-connection-state-counter | 3 +- bin/xpf | 1 + bin/xpl | 33 ++++--- docs/shell.md | 2 +- lib/console-text-color-themes.sh | 10 +- test-cases/parseOpts-test.sh | 4 +- 14 files changed, 238 insertions(+), 189 deletions(-) diff --git a/bin/a2l b/bin/a2l index caca085b..7c082b94 100755 --- a/bin/a2l +++ b/bin/a2l @@ -8,10 +8,10 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-a2l # @author Jerry Lee (oldratlee at gmail dot com) - +set -eEuo pipefail # NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char +readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end colorEcho() { @@ -19,7 +19,7 @@ colorEcho() { shift # check isatty in bash https://stackoverflow.com/questions/10022323 # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } readonly -a ECHO_COLORS=(33 35 36 31 32 37 34) diff --git a/bin/ap b/bin/ap index 925e4d52..bceae03c 100755 --- a/bin/ap +++ b/bin/ap @@ -10,10 +10,11 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-ap-and-rp # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail [ $# -eq 0 ] && files=(.) || files=("$@") -for f in "${files[@]}" ; do +for f in "${files[@]}"; do ! [ -e "$f" ] && { echo "$f does not exists!" continue diff --git a/bin/c b/bin/c index e18e11e0..9178992f 100755 --- a/bin/c +++ b/bin/c @@ -3,25 +3,26 @@ # Run command and put output to system clipper. # # @Usage -# $ c echo "hello world!" -# $ echo "hello world!" | c +# $ c echo 'hello world!' +# $ echo 'hello world!' | c # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-c # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -set -e -set -o pipefail +readonly PROG="$(basename "$0")" -readonly PROG="`basename "$0"`" +readonly nl=$'\n' # new line usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + 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 )) && { echo "$@"; echo; } > $out + (($# > 0)) && redEcho "$*$nl" >$out - > $out cat <$out <( content="$(cat)" + # shellcheck disable=SC2086 echo $eol "$content" | copy - ) > $out + ) >$out } if [ ${#args[@]} -eq 0 ]; then diff --git a/bin/coat b/bin/coat index 6ab5e1a7..a82e1b4e 100755 --- a/bin/coat +++ b/bin/coat @@ -9,26 +9,25 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-coat # @author Jerry Lee (oldratlee at gmail dot com) - -set -e -set -o pipefail +set -eEuo pipefail # if not in console, use cat directly # check isatty in bash https://stackoverflow.com/questions/10022323 [ ! -t 1 ] && exec cat "$@" # NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char +readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end readonly -a ECHO_COLORS=(33 35 36 31 32 37 34) COUNT=0 colorEcho() { local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" - echo "$ec[1;${color}m$@$eend" + echo "${ec}[1;${color}m$*$eend" } -# Bash read line does not read leading spaces https://stackoverflow.com/questions/29689172 +# Bash read line does not read leading spaces +# https://stackoverflow.com/questions/29689172 cat "$@" | while IFS= read -r line; do colorEcho "$line" done diff --git a/bin/echo-args b/bin/echo-args index 6bb2f661..5761c1bb 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -4,6 +4,7 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-echo-args # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end @@ -13,13 +14,12 @@ echoArg() { # if stdout is console, turn on color output. [ -t 1 ] && - echo "$index/$count: $ec[1;31m[$eend$ec[0;34;42m$value$eend$ec[1;31m]$eend" || - echo "$index/$count: [$value]" + echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[0;34;42m$value$eend${ec}[1;31m]$eend" || + echo "${index}/${count}: [${value}]" } - echoArg 0 $# "$0" idx=1 -for a ; do +for a; do echoArg $((idx++)) $# "$a" done diff --git a/bin/find-in-jars b/bin/find-in-jars index 83e6ebe1..102f658d 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -13,26 +13,29 @@ # # @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) +set -eEuo pipefail -readonly PROG="`basename "$0"`" +readonly PROG="$(basename "$0")" ################################################################################ # util functions ################################################################################ +# shellcheck disable=SC2015 [ -t 1 ] && readonly is_console=true || readonly is_console=false # NOTE: $'foo' is the escape sequence syntax of bash readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end readonly cr=$'\r' # carriage return +readonly nl=$'\n' # new line redEcho() { - $is_console && echo "$ec[1;31m$@$eend" || echo "$@" + $is_console && echo "${ec}[1;31m$*$eend" || echo "$*" } die() { - redEcho "Error: $@" 1>&2 + redEcho "Error: $*" 1>&2 exit 1 } @@ -41,7 +44,7 @@ die() { $is_console && readonly columns=$(stty size | awk '{print $2}') printResponsiveMessage() { - $is_console || return + $is_console || return 0 local message="$*" # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html @@ -49,7 +52,7 @@ printResponsiveMessage() { } clearResponsiveMessage() { - $is_console || return + $is_console || return 0 # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 @@ -59,17 +62,18 @@ clearResponsiveMessage() { # echo -e "\033[1K" # Or everything on the line, regardless of cursor position: # echo -e "\033[2K" - echo -n "$ec[2K$cr" + echo -n "${ec}[2K$cr" } usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + 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 )) && { echo "$@"; echo; } > $out + (($# > 0)) && redEcho "$*$nl" >$out - > $out cat <$out < 0 )); do +declare -a extensions=() +declare -a args=() + +separator='!' +regex_mode=-E +use_absolute_path=false + +while (($# > 0)); do case "$1" in - -d|--dir) - dirs=("${dirs[@]}" "$2") + -d | --dir) + dirs=(${dirs[@]:+"${dirs[@]}"} "$2") shift 2 ;; - -e|--extension) - extension=("${extension[@]}" "$2") + -e | --extension) + extensions=(${extensions[@]:+"${extensions[@]}"} "$2") shift 2 ;; # support the typo option --seperator for compatibility - -s|--separator|--seperator) + -s | --separator | --seperator) separator="$2" shift 2 ;; - -E|--extended-regexp) + -E | --extended-regexp) regex_mode=-E shift ;; - -F|--fixed-strings) + -F | --fixed-strings) regex_mode=-F shift ;; - -G|--basic-regexp) + -G | --basic-regexp) regex_mode=-G shift ;; - -P|--perl-regexp) + -P | --perl-regexp) regex_mode=-P shift ;; - -i|--ignore-case) + -i | --ignore-case) ignore_case_option=-i shift ;; - -a|--absolute-path) + -a | --absolute-path) use_absolute_path=true shift ;; - -h|--help) + -h | --help) usage ;; --) shift - args=("${args[@]}" "$@") + args=(${args[@]:+"${args[@]}"} "$@") break ;; -*) usage 2 "${PROG}: unrecognized option '$1'" ;; *) - args=("${args[@]}" "$1") + args=(${args[@]:+"${args[@]}"} "$1") shift ;; esac done +# shellcheck disable=SC2178 dirs=${dirs:-.} -extension=${extension:-jar} -regex_mode=${regex_mode:--E} +# shellcheck disable=SC2178 +extensions=${extensions:-jar} -use_absolute_path=${use_absolute_path:-false} -separator="${separator:-!}" - -(( "${#args[@]}" == 0 )) && usage 1 "No find file pattern!" -(( "${#args[@]}" > 1 )) && usage 1 "More than 1 file pattern: ${args[@]}" +(("${#args[@]}" == 0)) && usage 1 "No find file pattern!" +(("${#args[@]}" > 1)) && usage 1 "More than 1 file pattern: ${args[*]}" readonly pattern="${args[0]}" declare -a tmp_dirs=() @@ -187,17 +195,19 @@ 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 - $use_absolute_path && tmp_dirs=( "${tmp_dirs[@]}" "$(cd "$d" && pwd)" ) + + # 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[@]}" ) +$use_absolute_path && dirs=("${tmp_dirs[@]}") # convert extensions to find -iname options -for e in "${extension[@]}"; do - (( "${#find_iname_options[@]}" == 0 )) && - find_iname_options=( -iname "*.$e" ) || - find_iname_options=( "${find_iname_options[@]}" -o -iname "*.$e" ) +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 ################################################################################ @@ -208,12 +218,12 @@ done # # How to list files in a zip without extra information in command line # https://unix.stackexchange.com/a/128304/136953 -if which zipinfo &> /dev/null; then +if which zipinfo &>/dev/null; then readonly command_for_list_zip='zipinfo -1' -elif which unzip &> /dev/null; then +elif which unzip &>/dev/null; then readonly command_for_list_zip='unzip -Z1' else - if ! which jar &> /dev/null; then + if ! which jar &>/dev/null; then [ -n "$JAVA_HOME" ] || die "jar not found on PATH and JAVA_HOME env var is blank!" [ -f "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) file does NOT exists!" [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executalbe!" @@ -226,26 +236,30 @@ fi # find logic ################################################################################ -readonly jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" -readonly total_count="$(echo $(echo "$jar_files" | wc -l))" -[ -n "$jar_files" ] || die "No ${extension[@]} file found!" +jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" +[ -n "$jar_files" ] || die "No ${extensions[*]} file found!" + +total_count="$(echo "$jar_files" | wc -l)" +total_count="${total_count//[[:space:]]/}" # delete white space findInJarFiles() { $is_console && local -r grep_color_option='--color=always' local counter=1 jar_file - while read jar_file; do + while read -r jar_file; do printResponsiveMessage "finding in jar($((counter++))/$total_count): $jar_file" - $command_for_list_zip "${jar_file}" | - grep $regex_mode $ignore_case_option $grep_color_option -- "$pattern" | - while read file; do - clearResponsiveMessage - - $is_console && - echo "$ec[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" || - echo "${jar_file}${separator}${file}" - done + $command_for_list_zip "${jar_file}" | { + # Prevent grep from exiting in case of nomatch + # https://unix.stackexchange.com/questions/330660 + grep $regex_mode ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern" || true + } | while read -r file; do + clearResponsiveMessage + + $is_console && + echo "${ec}[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" || + echo "${jar_file}${separator}${file}" + done clearResponsiveMessage done diff --git a/bin/rp b/bin/rp index efb69ee6..82394030 100755 --- a/bin/rp +++ b/bin/rp @@ -10,28 +10,29 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-ap-and-rp # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail [ $# -eq 0 ] && { echo "ERROR: NO argument!" exit 1 } -[ $# -eq 1 ] && { +if [ $# -eq 1 ]; then relativeTo=. files=("$@") -} || { +else argv=("$@") argc=$# # Get last argument relativeTo="${argv[argc - 1]}" files=( "${argv[@]:0:argc - 1}" ) -} +fi [ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" for f in "${files[@]}" ; do - ! [ -e $f ] && { + ! [ -e "$f" ] && { echo "$f does not exists!" continue } diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 6b204989..2c93be91 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -9,27 +9,28 @@ # @author Jerry Lee (oldratlee at gmail dot com) # @author superhj1987 (superhj1987 at 126 dot com) -readonly PROG="`basename $0`" +readonly PROG="$(basename "$0")" readonly -a COMMAND_LINE=("$0" "$@") # Get current user name via whoami command # See https://www.lifewire.com/current-linux-user-whoami-command-3867579 # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! -readonly USER="`whoami`" +readonly USER="$(whoami)" ################################################################################ # util functions ################################################################################ # NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char +readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end +readonly nl=$'\n' # new line colorEcho() { local color=$1 shift # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" + [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" } colorPrint() { @@ -37,14 +38,15 @@ colorPrint() { shift colorEcho "$color" "$@" - [ -n "$append_file" -a -w "$append_file" ] && echo "$@" >> "$append_file" - [ -n "$store_dir" -a -w "$store_dir" ] && echo "$@" >> "${store_file_prefix}$PROG" + [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" } +# shellcheck disable=SC2120 normalPrint() { echo "$@" - [ -n "$append_file" -a -w "$append_file" ] && echo "$@" >> "$append_file" - [ -n "$store_dir" -a -w "$store_dir" ] && echo "$@" >> "${store_file_prefix}$PROG" + [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" } redPrint() { @@ -64,7 +66,7 @@ bluePrint() { } die() { - redPrint "Error: $@" 1>&2 + redPrint "Error: $*" 1>&2 exit 1 } @@ -81,13 +83,14 @@ logAndCat() { } usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + 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 )) && { echo "$@"; echo; } > $out + (($# > 0)) && colorEcho 31 "$*$nl" >$out - > $out cat <$out < /dev/null; then - jstack_path="`which jstack`" + [ -x "$jstack_path" ] || die "$jstack_path is NOT executable!" +elif which jstack &>/dev/null; then + jstack_path="$(which jstack)" else [ -n "$JAVA_HOME" ] || die "jstack not found on PATH and No JAVA_HOME setting! Use -s option set jstack path manually." [ -f "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) file does NOT exists! Use -s option set jstack path manually." - [ -x "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executalbe! Use -s option set jstack path manually." + [ -x "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executable! Use -s option set jstack path manually." jstack_path="$JAVA_HOME/bin/jstack" fi @@ -266,7 +275,7 @@ fi # biz logic ################################################################################ -readonly run_timestamp="`date "+%Y-%m-%d_%H:%M:%S.%N"`" +readonly run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" readonly uuid="${PROG}_${run_timestamp}_${RANDOM}_$$" readonly tmp_store_dir="/tmp/${uuid}" @@ -278,13 +287,13 @@ fi mkdir -p "$tmp_store_dir" cleanupWhenExit() { - rm -rf "$tmp_store_dir" &> /dev/null + rm -rf "$tmp_store_dir" &>/dev/null } trap "cleanupWhenExit" EXIT headInfo() { colorEcho "0;34;42" ================================================================================ - echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$(( i + 1 ))/$update_count]: ${COMMAND_LINE[@]}" + echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((i + 1))/$update_count]: ${COMMAND_LINE[*]}" colorEcho "0;34;42" ================================================================================ echo } @@ -301,13 +310,15 @@ findBusyJavaThreadsByPs() { # 1. sort by %cpu by ps option `--sort -pcpu` # 2. use wide output(unlimited width) by ps option `-ww` # avoid trunk user column to username_fo+ or $uid alike - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo pid,lwp,pcpu,user --sort -pcpu --no-headers) + + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --sort -pcpu --no-headers) local -r ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" fi - if (( count > 0 )); then + if ((count > 0)); then echo "$ps_out" | head -n "${count}" else echo "$ps_out" @@ -330,10 +341,10 @@ __top_threadId_cpu() { # and use second time update data to get cpu percentage of thread in 0.5 second interval # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! - local -a top_cmd_line=(top -H -b -d $top_delay -n 2) + local -a top_cmd_line=(top -H -b -d "$top_delay" -n 2) local -r top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then - echo "$top_out" | logAndCat "${top_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_top" + echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_top" fi echo "$top_out" | @@ -352,23 +363,24 @@ __top_threadId_cpu() { __complete_pid_user_by_ps() { # ps output field: pid, thread id(lwp), user - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo pid,lwp,user --no-headers) + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) local -r ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" fi - local idx=0 threadId pcpu - while read threadId pcpu ; do - (( count <= 0 || idx < count )) || break + local idx=0 threadId pcpu output_fields + while read -r threadId pcpu; do + ((count <= 0 || idx < count)) || break # output field: pid, threadId, pcpu, user - local output_fields="$( echo "$ps_out" | + output_fields="$(echo "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { printf "%s %s %s %s\n", $1, threadId, pcpu, $3; exit - }' )" + }')" if [ -n "$output_fields" ]; then - (( idx++ )) + ((idx++)) echo "$output_fields" fi done @@ -380,31 +392,32 @@ findBusyJavaThreadsByTop() { } printStackOfThreads() { - local idx=0 pid threadId pcpu user - while read pid threadId pcpu user ; do - local threadId0x="0x`printf %x ${threadId}`" + local idx=0 pid threadId pcpu user threadId0x + while read -r pid threadId pcpu user; do + threadId0x="0x$(printf %x "${threadId}")" - (( idx++ )) - local jstackFile="${store_file_prefix}$(( i + 1 ))_jstack_${pid}" + ((idx++)) + local jstackFile="${store_file_prefix}$((i + 1))_jstack_${pid}" [ -f "${jstackFile}" ] || { - local -a jstack_cmd_line=( "$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid} ) + # shellcheck disable=SC2206 + local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid}) if [ "${user}" == "${USER}" ]; then # run without sudo, when java process user is current user - logAndRun "${jstack_cmd_line[@]}" > ${jstackFile} + logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}" elif [ $UID == 0 ]; then # if java process user is not current user, must run jstack with sudo - logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" > ${jstackFile} + logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}" else # current user is not root user, so can not run with sudo; print error message and rerun suggestion redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" - yellowPrint " sudo ${COMMAND_LINE[@]}" + yellowPrint " sudo ${COMMAND_LINE[*]}" normalPrint continue fi || { redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." normalPrint - rm "${jstackFile}" &> /dev/null + rm "${jstackFile}" &>/dev/null continue } } @@ -427,7 +440,7 @@ printStackOfThreads() { }" fi { - sed "$sed_script" -n ${jstackFile} + sed "$sed_script" -n "${jstackFile}" echo } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} done @@ -440,11 +453,12 @@ printStackOfThreads() { main() { local i # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) - for (( i = 0; update_count <= 0 || i < update_count; ++i )); do - (( i > 0 )) && sleep "$update_delay" + for ((i = 0; update_count <= 0 || i < update_count; ++i)); do + ((i > 0)) && sleep "$update_delay" - [ -n "$append_file" -o -n "$store_dir" ] && headInfo | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} > /dev/null - (( update_count != 1 )) && headInfo + [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | + tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null + ((update_count != 1)) && headInfo if $use_ps; then findBusyJavaThreadsByPs diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 86b60351..d5f0a620 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -8,11 +8,12 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-tcp-connection-state-counter # @author Jerry Lee (oldratlee at gmail dot com) # @author @sunuslee (sunuslee at gmail dot com) +set -eEuo pipefail # On MacOS, netstat need to using -p tcp to get only tcp output. uname | grep Darwin -q && option_for_mac="-ptcp" -netstat -tna $option_for_mac | awk 'NR > 2 { +netstat -tna ${option_for_mac:-} | awk 'NR > 2 { s[$NF]++ } diff --git a/bin/xpf b/bin/xpf index 2e4ba51a..8e9a75e8 100755 --- a/bin/xpf +++ b/bin/xpf @@ -8,6 +8,7 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail BASE="$(dirname "$0")" source "$BASE/xpl" "$@" diff --git a/bin/xpl b/bin/xpl index b79bd271..41495571 100755 --- a/bin/xpl +++ b/bin/xpl @@ -7,13 +7,19 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -PROG=`basename $0` +readonly PROG="$(basename "$0")" + +readonly nl=$'\n' # new line usage() { - [ -n "$1" -a "$1" != 0 ] && local out=/dev/stderr || local out=/dev/stdout + local -r exit_code="${1:-0}" + (($# > 0)) && shift + # shellcheck disable=SC2015 + [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - [ $# -gt 1 ] && { echo "$2"; echo; } > $out + (($# > 0)) && echo "$*$nl" >$out cat < Date: Sun, 14 Feb 2021 16:24:20 +0800 Subject: [PATCH 005/175] ! rename file: uniq-lines -> uq --- bin/{uniq-lines => uq} | 2 ++ 1 file changed, 2 insertions(+) rename bin/{uniq-lines => uq} (83%) diff --git a/bin/uniq-lines b/bin/uq similarity index 83% rename from bin/uniq-lines rename to bin/uq index 6368665e..dc0a108d 100755 --- a/bin/uniq-lines +++ b/bin/uq @@ -1,6 +1,8 @@ #!/bin/bash # @Function # print uniq line keep order, no sorting required +# +# @author Zava Xu (zava.kid at gmail dot com) outputUniqLines() { awk '{ From 513a869e7b5b1333f0d5bf3b7c1ca2f795f7cc97 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 14 Feb 2021 22:02:09 +0800 Subject: [PATCH 006/175] ! improve uq and add its documents --- README.md | 2 + bin/helper/uq.awk | 74 +++++++++++++++++ bin/uq | 207 ++++++++++++++++++++++++++++++++++++++++++---- docs/shell.md | 131 +++++++++++++++++++++++++++-- 4 files changed, 394 insertions(+), 20 deletions(-) create mode 100644 bin/helper/uq.awk diff --git a/README.md b/README.md index fdde70ef..9cc4278b 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r 彩色`cat`出文件行,方便人眼区分不同的行。 1. [a2l](docs/shell.md#-a2l) 按行彩色输出参数,方便人眼查看。 +1. [uq](docs/shell.md#-uq) + 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 1. [ap and rp](docs/shell.md#-ap-and-rp) 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 1. [tcp-connection-state-counter](docs/shell.md#-tcp-connection-state-counter) diff --git a/bin/helper/uq.awk b/bin/helper/uq.awk new file mode 100644 index 00000000..e07ddcfd --- /dev/null +++ b/bin/helper/uq.awk @@ -0,0 +1,74 @@ +#!/usr/local/bin/awk -f + +function printResult(for_lines) { + for (idx = 0; idx < length(for_lines); idx++) { + line=for_lines[idx] + count=line_count_array[storeLine(line)] + + #printf "DEBUG 1: %7s %s, index: %s\n", count, line, idx + + if (uq_opt_only_unique) { + if (count == 1) printLine(count, line) + } else { + #printf "DEBUG 2: %7s %s uq_opt_only_repeated: %s\n", count, line, uq_opt_only_repeated + + if (uq_opt_only_repeated && count <= 1) { + continue + } + + if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && outputted) { + if (!compareLine(line, outputted)) print "" + } + + printLine(count, line) + outputted=line + } + } +} + +function printLine(count, line) { + if (uq_opt_count) { + printf "%7s %s%s", count, line, ORS + } else { + print line + } +} + +function storeLine(line) { + if (uq_opt_ignore_case) { + return tolower(line) + } else { + return line + } +} + +function compareLine(line1, line2) { + return storeLine(line1) == storeLine(line2) +} + + +BEGIN { + if (uq_opt_zero_terminated) { + RS = "\0" + ORS = "\0" + } +} + + +{ + # use index to keep lines order + lines[line_index++] = $0 + + store_line=storeLine($0) + # line_count_array: line content -> count + if (++line_count_array[store_line] == 1) { + # use index to keep lines order + deduplicated_lines[deduplicated_line_index++] = store_line + } +} + + +END { + if (uq_opt_all_repeated) printResult(lines) + else printResult(deduplicated_lines) +} diff --git a/bin/uq b/bin/uq index dc0a108d..6c0bae34 100755 --- a/bin/uq +++ b/bin/uq @@ -1,21 +1,200 @@ #!/bin/bash # @Function -# print uniq line keep order, no sorting required +# Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output). +# same as `uniq` command in core utils, +# but detect repeated lines that are not adjacent, no sorting required. # +# @Usage +# uq [OPTION]... [INPUT [OUTPUT]] +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq # @author Zava Xu (zava.kid at gmail dot com) +# @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail + +PROG="$(basename "$0")" +PROG_PATH="$(readlink -f "$0")" +PROG_DIR="$(dirname "$PROG_PATH")" + +################################################################################ +# 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 + +redEcho() { + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" +} + +yellowEcho() { + [ -t 1 ] && echo "${ec}[1;33m$*$eend" || echo "$*" +} + +die() { + redEcho "Error: $*" 1>&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 < 0)); do + case "$1" in + -c | --count) + uq_opt_count=1 + shift + ;; + -d | --repeated) + uq_opt_only_repeated=1 + shift + ;; + -D) + uq_opt_all_repeated=1 + shift + ;; + --all-repeated=*) + uq_opt_all_repeated=1 + uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}') + [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || + usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" + shift + ;; + -u | --unique) + uq_opt_only_unique=1 + shift + ;; + -i | --ignore-case) + uq_opt_ignore_case=1 + shift + ;; + -z | --zero-terminated) + uq_opt_zero_terminated=1 + shift + ;; + -h | --help) + usage + ;; + --) + shift + argv=("${argv[@]}" "$@") + break + ;; + -) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + -*) + usage 2 "${PROG}: unrecognized option '$1'" + ;; + *) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + esac +done + +[[ $uq_opt_only_repeated == 1 && $uq_opt_only_unique == 1 ]] && + usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" +[[ $uq_opt_all_repeated == 1 && $uq_opt_only_unique == 1 ]] && + usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" + +[[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ( $uq_opt_count == 0 && $uq_opt_only_repeated == 0 ) ]] && + yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 + +argc=${#argv[@]} + +if ((argc == 0)); then + input_files=() + output_file=/dev/stdout +elif ((argc == 1)); then + input_files=("${argv[0]}") + output_file=/dev/stdout +else + input_files=("${argv[@]:0:argc-1}") + output_file=${argv[argc - 1]} + if [ "$output_file" = - ]; then + output_file=/dev/stdout + fi +fi + +# Check input file +for f in ${input_files[@]:+"${input_files[@]}"}; do + # - is stdin, ok + [ "$f" = - ] && continue + + [ -e "$f" ] || die "input file $f does not exist!" + [ ! -d "$f" ] || die "input file $f exists, but is a directory!" + [ -f "$f" ] || die "input file $f exists, but is not a file!" + [ -r "$f" ] || die "input file $f exists, but is not readable!" +done + +################################################################################ +# biz logic +################################################################################ + +awk \ + -v "uq_opt_count=$uq_opt_count" \ + -v "uq_opt_only_repeated=$uq_opt_only_repeated" \ + -v "uq_opt_all_repeated=$uq_opt_all_repeated" \ + -v "uq_opt_repeated_method=$uq_opt_repeated_method" \ + -v "uq_opt_only_unique=$uq_opt_only_unique" \ + -v "uq_opt_ignore_case=$uq_opt_ignore_case" \ + -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ + -f "$PROG_DIR/helper/uq.awk" \ + -- ${input_files[@]:+"${input_files[@]}"} \ + >"$output_file" diff --git a/docs/shell.md b/docs/shell.md index 5f18bf43..5c18424a 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -13,29 +13,31 @@ - [示例](#%E7%A4%BA%E4%BE%8B) - [🍺 a2l](#-a2l) - [示例](#%E7%A4%BA%E4%BE%8B-1) - - [🍺 ap and rp](#-ap-and-rp) + - [🍺 uq](#-uq) - [示例](#%E7%A4%BA%E4%BE%8B-2) + - [🍺 ap and rp](#-ap-and-rp) + - [示例](#%E7%A4%BA%E4%BE%8B-3) - [🍺 tcp-connection-state-counter](#-tcp-connection-state-counter) - [用法](#%E7%94%A8%E6%B3%95) - - [示例](#%E7%A4%BA%E4%BE%8B-3) + - [示例](#%E7%A4%BA%E4%BE%8B-4) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85) - [🍺 xpl and xpf](#-xpl-and-xpf) - [用法](#%E7%94%A8%E6%B3%95-1) - - [示例](#%E7%A4%BA%E4%BE%8B-4) + - [示例](#%E7%A4%BA%E4%BE%8B-5) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-1) - [`Shell`开发/测试加强](#shell%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95%E5%8A%A0%E5%BC%BA) - [🍺 echo-args](#-echo-args) - - [示例](#%E7%A4%BA%E4%BE%8B-5) + - [示例](#%E7%A4%BA%E4%BE%8B-6) - [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) - [🍺 console-text-color-themes.sh](#-console-text-color-themessh) - [用法](#%E7%94%A8%E6%B3%95-2) - - [示例](#%E7%A4%BA%E4%BE%8B-6) + - [示例](#%E7%A4%BA%E4%BE%8B-7) - [运行效果](#%E8%BF%90%E8%A1%8C%E6%95%88%E6%9E%9C) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-2) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99-1) - [🍺 parseOpts.sh](#-parseoptssh) - [用法](#%E7%94%A8%E6%B3%95-3) - - [示例](#%E7%A4%BA%E4%BE%8B-7) + - [示例](#%E7%A4%BA%E4%BE%8B-8) - [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-3) @@ -202,6 +204,123 @@ test-cases/self-installer.sh 注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,和上面的`coat`命令一样。 +🍺 [uq](../bin/uq) +---------------------- + +不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 +使用方式与支持的选项 模仿系统的`uniq`命令。支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 + +> ‼️ **_注意_**: 去重过程会在内存持有整个输入(因为全局去重)! +> 对于输入大小较大的场景(如输入有几百M甚至几G),需谨慎使用;往往需要结合业务场景开发对应的优化实现。 +> +> 虽然平时的大部分场景输入量非常有限(如几M),一个简单没有充分优化的实现是快速够用的。 + +因为`uniq`命令完成是相邻行的去重,需要通过或是组合`sort`命令来完成整输入的去重,会有下面的问题: + +```bash +# 示例输入 +$ cat foo.txt +c +c +b +a +a +c +c + +$ uniq foo.txt +c +b +a +c +# c输出了2次,原因是第二个c与第一个c不是相邻的重复行 + +# 可以通过 sort -u 来完成整个输入去重,但这样操作,顺序与输入行不一致 +$ sort -u foo.txt +a +b +c +# 输入行重排序了! + +# 另外一个经典的用法 sort 与 uniq -c,输出重复次数 +$ sort foo.txt | uniq -c + 2 a + 1 b + 4 c +# 输入行重排序了! +``` + +### 示例 + +```bash +$ uq foo.txt # 输入是文件 +$ cat foo.txt | uq # 或是 标准输入/管道 +c +b +a +# 对整个输入行去重,且顺序与输入行一致(保留第一次出现的位置) + +# -c 选项:输出重复次数 +$ uq -c foo.txt + 4 c + 1 b + 2 a + +# -d, --repeated 选项:只输出 重复行 +$ uq -d foo.txt +c +a +# -u, --unique 选项:只输出 唯一行(即不重复的行) +$ uq -u foo.txt +b + +# -D 选项:重复行都输出,即重复了几次就输出几次 +$ uq -D -c foo.txt + 4 c + 4 c + 1 b + 2 a + 2 a + 4 c + 4 c + +# 有多个文件参数时,最后一个参数 是 输出文件 +$ uq in1.txt in2.txt out.txt +# 当有多个输入文件时,但要输出到控制台时,指定输出文件(最后一个文件参数)为 `-` 即可 +$ uq in1.txt in2.txt - + +# 帮助信息 +$ uq -h +Usage: uq [OPTION]... [INPUT [OUTPUT]] +Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output). +Same as `uniq` command in core utils, +but detect repeated lines that are not adjacent, no sorting required. + +Example: + # only one file, output to stdout + uq in.txt + # more than 1 file, last file argument is output file + uq in.txt out.txt + # when use - as output file, output to stdout + uq in1.txt in2.txt - + +Options: + -c, --count prefix lines by the number of occurrences + -d, --repeated only print duplicate lines, one for each group + -D print all duplicate lines + combined with -c/-d option usually + --all-repeated[=METHOD] like -D, but allow separating groups + with an empty line; + METHOD={none(default),prepend,separate} + -u, --unique Only output unique lines + that are not repeated in the input + -i, --ignore-case ignore differences in case when comparing + -z, --zero-terminated line delimiter is NUL, not newline + +Miscellaneous: + -h, --help display this help and exit +``` + 🍺 [ap](../bin/ap) and [rp](../bin/rp) ---------------------- From ccc256e5046231e42e313207d87ddd6311b7d353 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 20:13:03 +0800 Subject: [PATCH 007/175] ! improve/cleanup unit test of parseOpts --- docs/shell.md | 2 +- test-cases/my_unit_test_lib.sh | 100 +++++++++++++++ .../{parseOpts-test.sh => parseOpts_test.sh} | 120 +++++------------- 3 files changed, 134 insertions(+), 88 deletions(-) create mode 100644 test-cases/my_unit_test_lib.sh rename test-cases/{parseOpts-test.sh => parseOpts_test.sh} (62%) diff --git a/docs/shell.md b/docs/shell.md index 5c18424a..48673d8c 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -197,7 +197,7 @@ B.java # 把参数按行输出方便查看 或是 grep $ a2l **/*.sh lib/console-text-color-themes.sh -test-cases/parseOpts-test.sh +test-cases/parseOpts_test.sh test-cases/self-installer.sh ... ``` diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh new file mode 100644 index 00000000..63f5fb8d --- /dev/null +++ b/test-cases/my_unit_test_lib.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# unit test lib + +################################################# +# commons functions +################################################# + +# NOTE: $'foo' is the escape sequence syntax of bash +readonly __ut_ec=$'\033' # escape char +readonly __ut_eend=$'\033[0m' # escape end + +__ut_colorEcho() { + local color=$1 + shift + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${__ut_ec}[1;${color}m$*$__ut_eend" || echo "$*" +} + +redEcho() { + __ut_colorEcho 31 "$@" +} + +greenEcho() { + __ut_colorEcho 32 "$@" +} + +yellowEcho() { + __ut_colorEcho 33 "$@" +} + +blueEcho() { + __ut_colorEcho 34 "$@" +} + +fail() { + redEcho "TEST FAIL: $*" + exit 1 +} + +die() { + redEcho "Error: $*" 1>&2 + exit 1 +} + +################################################# +# assertion functions +################################################# + +assertArrayEquals() { + (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + + local a1PlaceHolder="$1[@]" + local a2PlaceHolder="$2[@]" + local a1=("${!a1PlaceHolder}") + local a2=("${!a2PlaceHolder}") + + [ ${#a1[@]} -eq ${#a2[@]} ] || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" + + local i + for ((i = 0; i < ${#a1[@]}; i++)); do + [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}" + done +} + +assertEquals() { + (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + [ "$1" = "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" +} + +assertAllVarsSame() { + local test_afterVars + test_afterVars=$(declare) + + diff \ + <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') \ + <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_') || + fail "assertAllVarsSame: Unexpected extra global vars!" +} + +assertAllVarsExcludeOptVarsSame() { + local test_afterVars + test_afterVars=$(declare) + + diff \ + <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') \ + <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_\|^_OPT_\|^_opts_index_name_') || + fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!" +} + +test_beforeVars=$(declare) diff --git a/test-cases/parseOpts-test.sh b/test-cases/parseOpts_test.sh similarity index 62% rename from test-cases/parseOpts-test.sh rename to test-cases/parseOpts_test.sh index aca6bdfb..d3aff579 100755 --- a/test-cases/parseOpts-test.sh +++ b/test-cases/parseOpts_test.sh @@ -1,76 +1,15 @@ #!/bin/bash -BASE=`dirname $0` +BASE="$(dirname "${BASH_SOURCE[0]}")" -. $BASE/../lib/parseOpts.sh +source "$BASE/../lib/parseOpts.sh" - -################################################# -# Util Functions -################################################# - -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end - -colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$*" -} - -redEcho() { - colorEcho 31 "$@" -} - -greenEcho() { - colorEcho 32 "$@" -} - -yellowEcho() { - colorEcho 33 "$@" -} - -blueEcho() { - colorEcho 34 "$@" -} - -arrayEquals() { - local a1PlaceHolder="$1[@]" - local a2PlaceHolder="$2[@]" - local a1=("${!a1PlaceHolder}") - local a2=("${!a2PlaceHolder}") - - [ ${#a1[@]} -eq ${#a2[@]} ] || return 1 - - local i - for((i=0; i<${#a1[@]}; i++)); do - [ "${a1[$i]}" = "${a2[$i]}" ] || return 1 - done -} - -compareAllVars() { - local test_afterVars=`declare` - diff <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_') -} - -compareAllVarsExcludeOptVars() { - local test_afterVars=`declare` - diff <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_\|^_OPT_\|^_opts_index_name_') -} - -fail() { - redEcho "TEST FAIL: $*" - exit 1 -} +source "$BASE/my_unit_test_lib.sh" ################################################# # Test ################################################# -test_beforeVars=`declare` - # ======================================== blueEcho "Test case: success parse" # ======================================== @@ -82,18 +21,25 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" + +[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" [ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" + test_cArray=(c.sh -p pv -q qv cc) -arrayEquals test_cArray _OPT_VALUE_c && arrayEquals test_cArray _OPT_VALUE_c_long || fail "Wrong option value of c!" -test_dArray=(d.sh -x xv d1 d2 d3 ) -arrayEquals test_dArray _OPT_VALUE_d && arrayEquals test_dArray _OPT_VALUE_d_long || fail "Wrong option value of d!" +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c_long + +test_dArray=(d.sh -x xv d1 d2 d3) +assertArrayEquals "Wrong option value of d!" test_dArray _OPT_VALUE_d +assertArrayEquals "Wrong option value of d!" test_dArray _OPT_VALUE_d_long + test_argArray=(aa bb cc dd ee) -arrayEquals test_argArray _OPT_ARGS || fail "Wrong args!" +assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS + +assertAllVarsExcludeOptVarsSame -compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!" _opts_cleanOptValueInfoList -compareAllVars || fail "Unpected extra glable vars!" +assertAllVarsSame # ======================================== blueEcho "Test case: success parse with -- " @@ -106,18 +52,23 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" + +[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" [ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" + test_cArray=(c.sh -p pv -q qv cc) -arrayEquals test_cArray _OPT_VALUE_c && arrayEquals test_cArray _OPT_VALUE_c_long || fail "Wrong option value of c!" +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c_long + [ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" + test_argArray=(aa bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee) -arrayEquals test_argArray _OPT_ARGS || fail "Wrong args!" +assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS -compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!" -_opts_cleanOptValueInfoList -compareAllVars || fail "Unpected extra glable vars!" +assertAllVarsExcludeOptVarsSame +_opts_cleanOptValueInfoList +assertAllVarsSame # ======================================== blueEcho "Test case: illegal option x" @@ -130,14 +81,12 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 232 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" +[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" [ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" [ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" [ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" [ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - - # ======================================== blueEcho "Test case: empty options" # ======================================== @@ -149,14 +98,12 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" +[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" [ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" [ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" [ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" [ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - - # ======================================== blueEcho "Test case: illegal option name" # ======================================== @@ -168,13 +115,12 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 221 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" +[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" [ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" [ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" [ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" [ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|z,z-#long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee test_exitCode=$? _opts_showOptDescInfoList @@ -182,12 +128,12 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 222 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" +[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" [ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" [ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" [ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" [ "$_OPT_ARGS" = "" ] || fail "Wrong args!" -compareAllVars || fail "Unpected extra glable vars!" +assertAllVarsSame greenEcho "TEST SUCCESS!!!" From 4bd0770580307ad2774d5bcb17f4362954d683c2 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 20:15:58 +0800 Subject: [PATCH 008/175] + add shUnit2 lib submodule #39 --- .gitmodules | 3 +++ test-cases/shunit2-lib | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 test-cases/shunit2-lib diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..94904715 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-cases/shunit2"] + path = test-cases/shunit2-lib + url = https://github.com/kward/shunit2.git diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib new file mode 160000 index 00000000..ebc4baa0 --- /dev/null +++ b/test-cases/shunit2-lib @@ -0,0 +1 @@ +Subproject commit ebc4baa08f045b7ef0f45c4b7d6f34f08d732f3d From 831391dec1c5b4970be38743559cce1c934e9aca Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 21:36:00 +0800 Subject: [PATCH 009/175] + add uq unit test: uq_test.sh --- test-cases/uq_test.sh | 110 +++++++++++++++++++++++++++++++ test-cases/uq_test_another_input | 4 ++ test-cases/uq_test_input | 8 +++ 3 files changed, 122 insertions(+) create mode 100755 test-cases/uq_test.sh create mode 100644 test-cases/uq_test_another_input create mode 100644 test-cases/uq_test_input diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh new file mode 100755 index 00000000..f3f944bc --- /dev/null +++ b/test-cases/uq_test.sh @@ -0,0 +1,110 @@ +#!/bin/bash +set -eEuo pipefail + +BASE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +cd "$BASE" + +################################################# +# commons and test data +################################################# + +readonly uq="../bin/uq" +readonly nl=$'\n' # new line + +test_input=$(cat uq_test_input) + +################################################# +# test cases +################################################# + +test_uq_simple() { + assertEquals "c${nl}v${nl}a${nl}u" \ + "$(echo "$test_input" | "$uq")" + assertEquals "c${nl}v${nl}a${nl}u" \ + "$("$uq" uq_test_input)" + + assertEquals "c${nl}a" \ + "$(echo "$test_input" | "$uq" -d)" + assertEquals "c${nl}a" \ + "$("$uq" -d uq_test_input)" + + assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)" + assertEquals "v${nl}u" "$("$uq" -u uq_test_input)" +} + +readonly test_output_uq_count=' 4 c + 1 v + 2 a + 1 u' + +readonly test_output_uq_D_count=' 4 c + 4 c + 1 v + 2 a + 2 a + 4 c + 4 c + 1 u' + +test_uq_count() { + assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)" + assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)" + + assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)" + assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)" +} + +test_uq_only_D_option__same_as_cat() { + assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)" + assertEquals "$test_input" "$("$uq" -D uq_test_input)" +} + +test_multi_input_files__output_file() { + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -d uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}a${nl}m" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -u uq_test_input uq_test_another_input "$output_file" + assertEquals "v${nl}u${nl}x" \ + "$(cat "$output_file")" +} + +test_multi_input_files__output_stdout() { + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)" + + assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)" + + assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)" +} + +test_ignore_case() { + local input="a${nl}b${nl}A" + + assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")" + assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)" +} + +test_ignore_case__count() { + local input="a${nl}b${nl}A" + + assertEquals " 1 a${nl} 1 b${nl} 1 A" \ + "$(echo "$input" | "$uq" -c)" + + assertEquals " 2 a${nl} 1 b" \ + "$(echo "$input" | "$uq" -i -c)" + + assertEquals " 2 a${nl} 1 b${nl} 2 A" \ + "$(echo "$input" | "$uq" -i -D -c)" +} + +################################################# +# Load and run shUnit2. +################################################# +source "$BASE/shunit2-lib/shunit2" diff --git a/test-cases/uq_test_another_input b/test-cases/uq_test_another_input new file mode 100644 index 00000000..0e8cf67f --- /dev/null +++ b/test-cases/uq_test_another_input @@ -0,0 +1,4 @@ +a +m +m +x diff --git a/test-cases/uq_test_input b/test-cases/uq_test_input new file mode 100644 index 00000000..6ad21c1b --- /dev/null +++ b/test-cases/uq_test_input @@ -0,0 +1,8 @@ +c +c +v +a +a +c +c +u From 95675ae79305ca570423618859792f6b919b22ff Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 22:08:09 +0800 Subject: [PATCH 010/175] + add travis-ci --- .travis.yml | 12 +++++++ README.md | 12 ++++--- test-cases/integration-test.sh | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 .travis.yml create mode 100755 test-cases/integration-test.sh diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..13702323 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: bash +# https://github.com/caarlos0-graveyard/shell-ci-build + +# The Ubuntu Linux Build Environments +# https://docs.travis-ci.com/user/reference/linux/ +dist: precise + +script: + - test-cases/integration-test.sh + +after_script: + - git status --ignored diff --git a/README.md b/README.md index 9cc4278b..78bdc993 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,14 @@ -[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![Join the chat at https://gitter.im/oldratlee/useful-scripts](https://badges.gitter.im/oldratlee/useful-scripts.svg)](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://img.shields.io/travis/oldratlee/useful-scripts/dev-2.x?logo=travis-ci&logoColor=white)](https://travis-ci.org/oldratlee/useful-scripts) [![GitHub release](https://img.shields.io/github/release/oldratlee/useful-scripts.svg)](https://github.com/oldratlee/useful-scripts/releases) -[![GitHub stars](https://img.shields.io/github/stars/oldratlee/useful-scripts.svg?style=social&label=Star&)](https://github.com/oldratlee/useful-scripts/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/oldratlee/useful-scripts.svg?style=social&label=Fork&)](https://github.com/oldratlee/useful-scripts/fork) - +[![License](https://img.shields.io/github/license/oldratlee/useful-scripts?color=4D7A97)](https://www.apache.org/licenses/LICENSE-2.0.html) +[![Chat at gitter.im](https://img.shields.io/gitter/room/oldratlee/useful-scripts?color=46BC99&logo=gitter&logoColor=white)](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![GitHub Stars](https://img.shields.io/github/stars/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/stargazers) +[![GitHub Forks](https://img.shields.io/github/forks/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/fork) +[![GitHub issues](https://img.shields.io/github/issues/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/issues) +[![GitHub Contributors](https://img.shields.io/github/contributors/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/graphs/contributors) 👉 把平时有用的手动操作做成脚本,这样可以便捷的使用。 ✨ diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh new file mode 100755 index 00000000..7d07e0df --- /dev/null +++ b/test-cases/integration-test.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -eEuo pipefail +cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +################################################################################ +# constants +################################################################################ + +# 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 + +################################################################################ +# common util functions +################################################################################ + +colorEcho() { + local color=$1 + shift + + # if stdout is the console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" +} + +redEcho() { + colorEcho 31 "$@" +} + +yellowEcho() { + colorEcho 33 "$@" +} + +blueEcho() { + colorEcho 36 "$@" +} + +logAndRun() { + local simple_mode=false + [ "$1" = "-s" ] && { + simple_mode=true + shift + } + + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + blueEcho "Run under work directory $PWD :$nl$*" + time "$@" + fi +} + +################################################################################ +# run *_test.sh unit test cases +################################################################################ + +for test_case in *_test.sh; do + logAndRun ./"$test_case" +done From 669647256946a359b4dd2f2a21e92e097867d62f Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 23:20:01 +0800 Subject: [PATCH 011/175] ! improve uq: use IGNORECASE built-in var --- bin/helper/uq.awk | 52 ++++++++++++++----------------------------- bin/uq | 2 +- test-cases/uq_test.sh | 1 + 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/bin/helper/uq.awk b/bin/helper/uq.awk index e07ddcfd..3d98db48 100644 --- a/bin/helper/uq.awk +++ b/bin/helper/uq.awk @@ -2,51 +2,35 @@ function printResult(for_lines) { for (idx = 0; idx < length(for_lines); idx++) { - line=for_lines[idx] - count=line_count_array[storeLine(line)] - - #printf "DEBUG 1: %7s %s, index: %s\n", count, line, idx + line = for_lines[idx] + count = line_count_array[caseAwareLine(line)] + #printf "DEBUG: %s %s, index: %s, uq_opt_only_repeated: %s\n", count, line, idx, uq_opt_only_repeated if (uq_opt_only_unique) { if (count == 1) printLine(count, line) } else { - #printf "DEBUG 2: %7s %s uq_opt_only_repeated: %s\n", count, line, uq_opt_only_repeated - - if (uq_opt_only_repeated && count <= 1) { - continue - } + if (uq_opt_only_repeated && count <= 1) continue - if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && outputted) { - if (!compareLine(line, outputted)) print "" + if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && previous_output) { + if (line != previous_output) print "" } printLine(count, line) - outputted=line + previous_output = line } } } function printLine(count, line) { - if (uq_opt_count) { - printf "%7s %s%s", count, line, ORS - } else { - print line - } -} - -function storeLine(line) { - if (uq_opt_ignore_case) { - return tolower(line) - } else { - return line - } + if (uq_opt_count) printf "%7s %s%s", count, line, ORS + else print line } -function compareLine(line1, line2) { - return storeLine(line1) == storeLine(line2) +function caseAwareLine(line) { + if (IGNORECASE) return tolower(line) + else return line } - BEGIN { if (uq_opt_zero_terminated) { RS = "\0" @@ -54,21 +38,19 @@ BEGIN { } } - { # use index to keep lines order - lines[line_index++] = $0 + original_lines[line_index++] = $0 - store_line=storeLine($0) + case_aware_line = caseAwareLine($0) # line_count_array: line content -> count - if (++line_count_array[store_line] == 1) { + if (++line_count_array[case_aware_line] == 1) { # use index to keep lines order - deduplicated_lines[deduplicated_line_index++] = store_line + deduplicated_lines[deduplicated_line_index++] = case_aware_line } } - END { - if (uq_opt_all_repeated) printResult(lines) + if (uq_opt_all_repeated) printResult(original_lines) else printResult(deduplicated_lines) } diff --git a/bin/uq b/bin/uq index 6c0bae34..56982535 100755 --- a/bin/uq +++ b/bin/uq @@ -193,7 +193,7 @@ awk \ -v "uq_opt_all_repeated=$uq_opt_all_repeated" \ -v "uq_opt_repeated_method=$uq_opt_repeated_method" \ -v "uq_opt_only_unique=$uq_opt_only_unique" \ - -v "uq_opt_ignore_case=$uq_opt_ignore_case" \ + -v "IGNORECASE=$uq_opt_ignore_case" \ -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ -f "$PROG_DIR/helper/uq.awk" \ -- ${input_files[@]:+"${input_files[@]}"} \ diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index f3f944bc..84b38948 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -107,4 +107,5 @@ test_ignore_case__count() { ################################################# # Load and run shUnit2. ################################################# + source "$BASE/shunit2-lib/shunit2" From 492327805ed5c78880aa3c5a1927958f4ba8ac6c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 18 Feb 2021 23:31:13 +0800 Subject: [PATCH 012/175] ! readlink "${BASH_SOURCE[0]}" instead of $0 readlink "${BASH_SOURCE[0]}" will success even run by `bash -x foo`, foo script is found from $PATH --- bin/uq | 2 +- bin/xpf | 1 + lib/console-text-color-themes.sh | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/uq b/bin/uq index 56982535..75d06f1e 100755 --- a/bin/uq +++ b/bin/uq @@ -13,7 +13,7 @@ set -eEuo pipefail PROG="$(basename "$0")" -PROG_PATH="$(readlink -f "$0")" +PROG_PATH="$(readlink -f "${BASH_SOURCE[0]}")" PROG_DIR="$(dirname "$PROG_PATH")" ################################################################################ diff --git a/bin/xpf b/bin/xpf index 8e9a75e8..2b9f5b5a 100755 --- a/bin/xpf +++ b/bin/xpf @@ -10,5 +10,6 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +# BASE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" BASE="$(dirname "$0")" source "$BASE/xpl" "$@" diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 3f33fce6..8b65f246 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -5,7 +5,7 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-console-text-color-themessh # @author Jerry Lee (oldratlee at gmail dot com) -readonly _ctct_PROG="$(basename "$(readlink -f "$0")")" +readonly _ctct_PROG="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" [ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true readonly _ctct_ec=$'\033' # escape char From f409325ec775022f34dbfe588f3dbcce5927ed06 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 19 Feb 2021 10:52:44 +0800 Subject: [PATCH 013/175] + add multiply linux distributions in travis-ci --- .travis.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13702323..6ca774b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,21 @@ -language: bash # https://github.com/caarlos0-graveyard/shell-ci-build +# https://github.com/kward/shunit2/blob/master/.travis.yml +language: bash # The Ubuntu Linux Build Environments # https://docs.travis-ci.com/user/reference/linux/ -dist: precise +matrix: + include: + - os: linux + dist: precise + - os: linux + dist: trusty + - os: linux + dist: xenial + - os: linux + dist: bionic + - os: linux + dist: focal script: - test-cases/integration-test.sh From d576849182a9f410684ae661712795c4d44ba926 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 19 Feb 2021 11:53:57 +0800 Subject: [PATCH 014/175] ! fix ci fail under distribution focal cause by COLUMNS, LINES --- test-cases/my_unit_test_lib.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index 63f5fb8d..ee26e10c 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -77,13 +77,16 @@ assertEquals() { [ "$1" = "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" } +readonly __ut_exclude_vars_builtin='^BASH_|^_=|^COLUMNS=|LINES=' +readonly __ut_exclude_vars_ut_functions='^FUNCNAME=|^test_' + assertAllVarsSame() { local test_afterVars test_afterVars=$(declare) diff \ - <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') \ - <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_') || + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") || fail "assertAllVarsSame: Unexpected extra global vars!" } @@ -92,8 +95,8 @@ assertAllVarsExcludeOptVarsSame() { test_afterVars=$(declare) diff \ - <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') \ - <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_\|^_OPT_\|^_opts_index_name_') || + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') || fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!" } From 3723073d4f3a959fcc8721eb5a4229a38d585508 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 19 Feb 2021 12:18:01 +0800 Subject: [PATCH 015/175] + add os osx in travis-ci --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6ca774b6..efc6822c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ language: bash # The Ubuntu Linux Build Environments # https://docs.travis-ci.com/user/reference/linux/ +# The macOS Build Environment +# https://docs.travis-ci.com/user/reference/osx/ matrix: include: - os: linux @@ -16,6 +18,7 @@ matrix: dist: bionic - os: linux dist: focal + - os: osx script: - test-cases/integration-test.sh From 71e763e1ca5235f107b9c4dc449098bd544ecff9 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 19 Feb 2021 13:38:59 +0800 Subject: [PATCH 016/175] ! fix osx ci fail - prefer greadlink than readlink - prefer gsed than sed --- .travis.yml | 6 ++++++ bin/ap | 7 ++++++- bin/uq | 7 ++++++- lib/console-text-color-themes.sh | 7 ++++++- lib/parseOpts.sh | 11 ++++++++--- test-cases/integration-test.sh | 8 +++++++- test-cases/uq_test.sh | 7 ++++++- 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index efc6822c..51f493a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ language: bash # https://docs.travis-ci.com/user/reference/linux/ # The macOS Build Environment # https://docs.travis-ci.com/user/reference/osx/ +# Installing Dependencies +# https://docs.travis-ci.com/user/installing-dependencies/#installing-packages-on-macos matrix: include: - os: linux @@ -19,6 +21,10 @@ matrix: - os: linux dist: focal - os: osx + osx_image: xcode11.3 + install: + - HOMEBREW_NO_AUTO_UPDATE=1 brew install coreutils + - HOMEBREW_NO_AUTO_UPDATE=1 brew install gnu-sed script: - test-cases/integration-test.sh diff --git a/bin/ap b/bin/ap index bceae03c..52b16376 100755 --- a/bin/ap +++ b/bin/ap @@ -12,6 +12,11 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +READLINK_CMD=readlink +if command -v greadlink > /dev/null; then + READLINK_CMD=greadlink +fi + [ $# -eq 0 ] && files=(.) || files=("$@") for f in "${files[@]}"; do @@ -19,5 +24,5 @@ for f in "${files[@]}"; do echo "$f does not exists!" continue } - readlink -f "$f" + $READLINK_CMD -f "$f" done diff --git a/bin/uq b/bin/uq index 75d06f1e..130950e8 100755 --- a/bin/uq +++ b/bin/uq @@ -12,8 +12,13 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +READLINK_CMD=readlink +if command -v greadlink > /dev/null; then + READLINK_CMD=greadlink +fi + PROG="$(basename "$0")" -PROG_PATH="$(readlink -f "${BASH_SOURCE[0]}")" +PROG_PATH="$($READLINK_CMD -f "${BASH_SOURCE[0]}")" PROG_DIR="$(dirname "$PROG_PATH")" ################################################################################ diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 8b65f246..9b9337f7 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -5,7 +5,12 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-console-text-color-themessh # @author Jerry Lee (oldratlee at gmail dot com) -readonly _ctct_PROG="$(basename "$(readlink -f "${BASH_SOURCE[0]}")")" +_ctct_READLINK_CMD=readlink +if command -v greadlink > /dev/null; then + _ctct_READLINK_CMD=greadlink +fi + +readonly _ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")" [ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true readonly _ctct_ec=$'\033' # escape char diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 6d67f736..197ec60f 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -26,6 +26,11 @@ readonly _opts_ec=$'\033' # escape char readonly _opts_eend=$'\033[0m' # escape end +_opts_SED_CMD=sed +if command -v gsed &> /dev/null; then + _opts_SED_CMD=gsed +fi + _opts_colorEcho() { local color=$1 shift @@ -42,7 +47,7 @@ _opts_convertToVarName() { _opts_redEcho "NOT 1 arguemnts when call _opts_convertToVarName: $@" return 1 } - echo "$1" | sed 's/-/_/g' + echo "$1" | $_opts_SED_CMD 's/-/_/g' } ##################################################################### @@ -177,7 +182,7 @@ parseOpts() { local optDescLines=`echo "$optsDescription" | # cut head and tail space - sed -r 's/^\s+//;s/\s+$//' | + $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` local optDesc @@ -258,7 +263,7 @@ parseOpts() { ;; -*) # short & long option(-a, -a-long), use same read-in logic. local opt="$1" - local optName=`echo "$1" | sed -r 's/^--?//'` + local optName=`echo "$1" | $_opts_SED_CMD -r 's/^--?//'` local mode=`_opts_findOptMode "$optName"` case "$mode" in -) diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index 7d07e0df..f52f379e 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -1,6 +1,12 @@ #!/bin/bash set -eEuo pipefail -cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" + +READLINK_CMD=readlink +if command -v greadlink &> /dev/null; then + READLINK_CMD=greadlink +fi + +cd "$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" ################################################################################ # constants diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 84b38948..5de8d056 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -1,7 +1,12 @@ #!/bin/bash set -eEuo pipefail -BASE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" +READLINK_CMD=readlink +if command -v greadlink &> /dev/null; then + READLINK_CMD=greadlink +fi + +BASE="$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" cd "$BASE" ################################################# From 5af9e3476fd21a5b3b60578adc0ab5e76e874905 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 20 Feb 2021 10:41:47 +0800 Subject: [PATCH 017/175] ! change output color of echo-args --- bin/echo-args | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/echo-args b/bin/echo-args index 5761c1bb..551f231f 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -14,7 +14,7 @@ echoArg() { # if stdout is console, turn on color output. [ -t 1 ] && - echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[0;34;42m$value$eend${ec}[1;31m]$eend" || + echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[1;36;40m$value$eend${ec}[1;31m]$eend" || echo "${index}/${count}: [${value}]" } From 874ccb05f97dda4f64c80a72d17b482347477a5c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 20 Feb 2021 10:35:33 +0800 Subject: [PATCH 018/175] ! improve uq, make it standalone aka. one file, no other script dependency(uq.awk) --- bin/helper/uq.awk | 56 ------------------------------------ bin/uq | 73 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 64 insertions(+), 65 deletions(-) delete mode 100644 bin/helper/uq.awk diff --git a/bin/helper/uq.awk b/bin/helper/uq.awk deleted file mode 100644 index 3d98db48..00000000 --- a/bin/helper/uq.awk +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/local/bin/awk -f - -function printResult(for_lines) { - for (idx = 0; idx < length(for_lines); idx++) { - line = for_lines[idx] - count = line_count_array[caseAwareLine(line)] - #printf "DEBUG: %s %s, index: %s, uq_opt_only_repeated: %s\n", count, line, idx, uq_opt_only_repeated - - if (uq_opt_only_unique) { - if (count == 1) printLine(count, line) - } else { - if (uq_opt_only_repeated && count <= 1) continue - - if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && previous_output) { - if (line != previous_output) print "" - } - - printLine(count, line) - previous_output = line - } - } -} - -function printLine(count, line) { - if (uq_opt_count) printf "%7s %s%s", count, line, ORS - else print line -} - -function caseAwareLine(line) { - if (IGNORECASE) return tolower(line) - else return line -} - -BEGIN { - if (uq_opt_zero_terminated) { - RS = "\0" - ORS = "\0" - } -} - -{ - # use index to keep lines order - original_lines[line_index++] = $0 - - case_aware_line = caseAwareLine($0) - # line_count_array: line content -> count - if (++line_count_array[case_aware_line] == 1) { - # use index to keep lines order - deduplicated_lines[deduplicated_line_index++] = case_aware_line - } -} - -END { - if (uq_opt_all_repeated) printResult(original_lines) - else printResult(deduplicated_lines) -} diff --git a/bin/uq b/bin/uq index 130950e8..5419ddc8 100755 --- a/bin/uq +++ b/bin/uq @@ -12,14 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -READLINK_CMD=readlink -if command -v greadlink > /dev/null; then - READLINK_CMD=greadlink -fi - PROG="$(basename "$0")" -PROG_PATH="$($READLINK_CMD -f "${BASH_SOURCE[0]}")" -PROG_DIR="$(dirname "$PROG_PATH")" ################################################################################ # util functions @@ -158,7 +151,7 @@ done [[ $uq_opt_all_repeated == 1 && $uq_opt_only_unique == 1 ]] && usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" -[[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ( $uq_opt_count == 0 && $uq_opt_only_repeated == 0 ) ]] && +[[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 argc=${#argv[@]} @@ -192,6 +185,68 @@ done # biz logic ################################################################################ +# uq awk script +# +# edit in a separated file(eg: uq.awk) then copy here, +# maybe more convenient(like good syntax highlight) + +# shellcheck disable=SC2016 +readonly uq_awk_script=' + +function printResult(for_lines) { + for (idx = 0; idx < length(for_lines); idx++) { + line = for_lines[idx] + count = line_count_array[caseAwareLine(line)] + #printf "DEBUG: %s %s, index: %s, uq_opt_only_repeated: %s\n", count, line, idx, uq_opt_only_repeated + + if (uq_opt_only_unique) { + if (count == 1) printLine(count, line) + } else { + if (uq_opt_only_repeated && count <= 1) continue + + if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && previous_output) { + if (line != previous_output) print "" + } + + printLine(count, line) + previous_output = line + } + } +} + +function printLine(count, line) { + if (uq_opt_count) printf "%7s %s%s", count, line, ORS + else print line +} + +function caseAwareLine(line) { + if (IGNORECASE) return tolower(line) + else return line +} + +BEGIN { + if (uq_opt_zero_terminated) ORS = RS = "\0" +} + +{ + # use index to keep lines order + original_lines[line_index++] = $0 + + case_aware_line = caseAwareLine($0) + # line_count_array: line content -> count + if (++line_count_array[case_aware_line] == 1) { + # use index to keep lines order + deduplicated_lines[deduplicated_line_index++] = case_aware_line + } +} + +END { + if (uq_opt_all_repeated) printResult(original_lines) + else printResult(deduplicated_lines) +} + +' + awk \ -v "uq_opt_count=$uq_opt_count" \ -v "uq_opt_only_repeated=$uq_opt_only_repeated" \ @@ -200,6 +255,6 @@ awk \ -v "uq_opt_only_unique=$uq_opt_only_unique" \ -v "IGNORECASE=$uq_opt_ignore_case" \ -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ - -f "$PROG_DIR/helper/uq.awk" \ + -f <(printf "%s" "$uq_awk_script") \ -- ${input_files[@]:+"${input_files[@]}"} \ >"$output_file" From 48c92656bef7f362a0245332f07fc46f918aa474 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 23 Feb 2021 11:26:40 +0800 Subject: [PATCH 019/175] ! improve .travis.yml --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 51f493a8..48979086 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,12 @@ # https://github.com/caarlos0-graveyard/shell-ci-build # https://github.com/kward/shunit2/blob/master/.travis.yml -language: bash +language: shell + +addons: + homebrew: + packages: + - coreutils + - gnu-sed # The Ubuntu Linux Build Environments # https://docs.travis-ci.com/user/reference/linux/ @@ -8,7 +14,7 @@ language: bash # https://docs.travis-ci.com/user/reference/osx/ # Installing Dependencies # https://docs.travis-ci.com/user/installing-dependencies/#installing-packages-on-macos -matrix: +jobs: include: - os: linux dist: precise @@ -22,9 +28,6 @@ matrix: dist: focal - os: osx osx_image: xcode11.3 - install: - - HOMEBREW_NO_AUTO_UPDATE=1 brew install coreutils - - HOMEBREW_NO_AUTO_UPDATE=1 brew install gnu-sed script: - test-cases/integration-test.sh From 719531bc263c5c4bde221d62ec196c0279c29573 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 8 Mar 2021 20:26:37 +0800 Subject: [PATCH 020/175] ! fix absence of redEcho function in script c --- bin/c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/bin/c b/bin/c index 9178992f..da714ab9 100755 --- a/bin/c +++ b/bin/c @@ -12,7 +12,17 @@ set -eEuo pipefail readonly PROG="$(basename "$0")" -readonly nl=$'\n' # new line +################################################################################ +# util functions +################################################################################ + +readonly ec=$'\033' # escape char +readonly eend=$'\033[0m' # escape end +readonly nl=$'\n' # new line + +redEcho() { + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" +} usage() { local -r exit_code="${1:-0}" From d81f27bd897496cf6327b64f38fb6f2c76f93561 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 8 Mar 2021 20:27:42 +0800 Subject: [PATCH 021/175] = improve find-in-jars - use `command -v` instead of `which` - fix typo: executalbe -> executable --- bin/find-in-jars | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index 102f658d..64e5dd37 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -218,15 +218,15 @@ done # # How to list files in a zip without extra information in command line # https://unix.stackexchange.com/a/128304/136953 -if which zipinfo &>/dev/null; then +if command -v zipinfo &>/dev/null; then readonly command_for_list_zip='zipinfo -1' -elif which unzip &>/dev/null; then +elif command -v unzip &>/dev/null; then readonly command_for_list_zip='unzip -Z1' else - if ! which jar &>/dev/null; then + if ! command -v jar &>/dev/null; then [ -n "$JAVA_HOME" ] || die "jar not found on PATH and JAVA_HOME env var is blank!" [ -f "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) file does NOT exists!" - [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executalbe!" + [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executable!" export PATH="$JAVA_HOME/bin:$PATH" fi readonly command_for_list_zip='jar tf' @@ -250,7 +250,7 @@ findInJarFiles() { printResponsiveMessage "finding in jar($((counter++))/$total_count): $jar_file" $command_for_list_zip "${jar_file}" | { - # Prevent grep from exiting in case of nomatch + # Prevent grep from exiting in case of no match # https://unix.stackexchange.com/questions/330660 grep $regex_mode ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern" || true } | while read -r file; do From ed8bdd61842a668525c76b0e9a9892bd106cfd42 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 14 Mar 2021 22:29:08 +0800 Subject: [PATCH 022/175] = fix typo: buildin -> builtin --- docs/shell.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/shell.md b/docs/shell.md index 48673d8c..ec8f1612 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -503,7 +503,7 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" 命令行选项解析库,加强支持选项有多个值(即数组)。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -自己写一个命令行选项解析函数,是因为[`bash`](http://linux.die.net/man/1/bash)的`buildin`命令[`getopts`](http://linux.die.net/man/1/getopts)和加强版本命令[`getopt`](http://linux.die.net/man/1/getopt)都不支持数组的值。 +自己写一个命令行选项解析函数,是因为[`bash`](http://linux.die.net/man/1/bash)的`builtin`命令[`getopts`](http://linux.die.net/man/1/getopts)和加强版本命令[`getopt`](http://linux.die.net/man/1/getopt)都不支持数组的值。 指定选项的多个值(即数组)的风格模仿[`find`](http://linux.die.net/man/1/find)命令的`-exec`选项: From 2816b4a3dc12224feb3b4759f344ec2e2a07bf25 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 14 Mar 2021 22:52:33 +0800 Subject: [PATCH 023/175] ! tcp-connection-state-counter: improve output format --- bin/tcp-connection-state-counter | 13 +++++++++++-- docs/shell.md | 8 +++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index d5f0a620..f16bc7b7 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -14,11 +14,20 @@ set -eEuo pipefail uname | grep Darwin -q && option_for_mac="-ptcp" netstat -tna ${option_for_mac:-} | awk 'NR > 2 { - s[$NF]++ + ++s[$NF] } END { + # get max length of stat and count for(v in s) { - printf "%-11s %s\n", v, s[v] + stat_len = length(v) + if(stat_len > max_stat_len) max_stat_len = stat_len + + count_len = length(s[v]) + if (count_len > max_count_len) max_count_len = count_len + } + + for(v in s) { + printf "%-" max_stat_len "s %" max_count_len "s\n", v, s[v] } }' | sort -nr -k2,2 diff --git a/docs/shell.md b/docs/shell.md index ec8f1612..e65671c2 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -378,9 +378,11 @@ tcp-connection-state-counter ```bash $ tcp-connection-state-counter -ESTABLISHED 290 -TIME_WAIT 212 -SYN_SENT 17 +CLOSE_WAIT 584 +ESTABLISHED 493 +TIME_WAIT 112 +LISTEN 27 +SYN_SENT 7 ``` ### 贡献者 From c58a91c614af9f73b1d73577aa05914ef9bd8d3c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 14 Mar 2021 23:51:58 +0800 Subject: [PATCH 024/175] ! improve show-duplicate-java-classes - move main logic to main function avoid shadows name warning - rename: optionParser -> option_parser - use print('') to print a new line avoid print '()' under python 2.7 - use single-quoted string instead of double-quoted --- bin/show-duplicate-java-classes | 46 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index e49c78aa..ae137a09 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -36,7 +36,7 @@ def list_class_under_jar_file(jar_file): def list_class_under_class_dir(class_dir): - return {relpath(dir_path + "/" + filename, class_dir) + return {relpath(dir_path + '/' + filename, class_dir) for dir_path, _, file_names in walk(class_dir) for filename in file_names if filename.lower().endswith('.class')} @@ -67,20 +67,20 @@ def find_duplicate_classes(java_class_2_class_paths): def print_class_paths(class_paths): - print() - print("=" * 80) - print("class paths to find:") - print("=" * 80) + print('') + print('=' * 80) + print('class paths to find:') + print('=' * 80) for idx, class_path in enumerate(class_paths): - print("%-3d: %s" % (idx + 1, class_path)) + print('%-3d: %s' % (idx + 1, class_path)) -if __name__ == '__main__': - optionParser = OptionParser('usage: %prog ' - '[-c class-dir1 [-c class-dir2] ...] ' - '[lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]') - optionParser.add_option("-c", "--class-dir", dest="class_dirs", default=[], action="append", help="add class dir") - options, libs = optionParser.parse_args() +def main(): + option_parser = OptionParser('usage: %prog ' + '[-c class-dir1 [-c class-dir2] ...] ' + '[lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]') + option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') + options, libs = option_parser.parse_args() if not options.class_dirs and not libs: libs = ['.'] @@ -91,22 +91,26 @@ if __name__ == '__main__': class_path_2_duplicate_classes = find_duplicate_classes(java_class_2_class_paths) if not class_path_2_duplicate_classes: - print("COOL! No duplicate classes found!") + print('COOL! No duplicate classes found!') print_class_paths(class_paths) exit() - print("Found duplicate classes in below class path:") + print('Found duplicate classes in below class path:') for idx, jars in enumerate(class_path_2_duplicate_classes): - print("%-3d(%d@%d): %s" % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), " ".join(jars))) + print('%-3d(%d@%d): %s' % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), ' '.join(jars))) - print() - print("=" * 80) - print("Duplicate classes detail info:") - print("=" * 80) + print('') + print('=' * 80) + print('Duplicate classes detail info:') + print('=' * 80) for idx, (jars, classes) in enumerate(class_path_2_duplicate_classes.items()): - print("%-3d(%d@%d): %s" % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), " ".join(jars))) + print('%-3d(%d@%d): %s' % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), ' '.join(jars))) for i, c in enumerate(classes): - print("\t%-3d %s" % (i + 1, c)) + print('\t%-3d %s' % (i + 1, c)) print_class_paths(class_paths) exit(1) + + +if __name__ == '__main__': + main() From 792faa5bb813147da7d2e2195a71cf518ea6c474 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 15 Mar 2021 00:02:10 +0800 Subject: [PATCH 025/175] ! show-duplicate-java-classes: skip java 9 module-info files --- bin/show-duplicate-java-classes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index ae137a09..5bef6c2f 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -59,6 +59,9 @@ def find_duplicate_classes(java_class_2_class_paths): class_path_2_duplicate_classes = {} for java_class, class_paths in list(java_class_2_class_paths.items()): + # skip java 9 module-info files + if java_class.endswith('/module-info.class'): + continue if len(class_paths) > 1: classes = class_path_2_duplicate_classes.setdefault(frozenset(class_paths), set()) classes.add(java_class) From d7d3d7b809e08f6dfe7fbee27123640f7a7d72b6 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 15 Mar 2021 00:24:36 +0800 Subject: [PATCH 026/175] ! show-duplicate-java-classes: add python version check #91 --- bin/show-duplicate-java-classes | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 5bef6c2f..a20ef65b 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -14,6 +14,7 @@ __author__ = 'tg123' +import sys from glob import glob from os import walk from zipfile import ZipFile @@ -115,5 +116,15 @@ def main(): exit(1) +def check_python_version(): + python_version = sys.version_info + if python_version.major > 2: + return + if python_version.minor < 7: + exit('Using python 2 and its version lower than 2.7, not support!\n' + 'Please use python 3 instead!\nPython version: ' + sys.version) + + if __name__ == '__main__': + check_python_version() main() From 8f369417b07f2b2591a6d95ca428fabcbddbfc42 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 15 Mar 2021 13:13:13 +0800 Subject: [PATCH 027/175] ! improve show-duplicate-java-classes - ignore BadZipFile for jar file - extract functions - biz function print_duplicate_classes_info - util function print_box_message - adjust function layout, move util functions to the front - from __future__ import print_function - use start=1 for enumerate - rename var name: java_class -> class/clazz --- bin/show-duplicate-java-classes | 140 ++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 61 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index a20ef65b..0e7885d9 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -12,28 +12,51 @@ # @author tg123 (farmer1992 at gmail dot com) # @author Jerry Lee (oldratlee at gmail dot com) +from __future__ import print_function + __author__ = 'tg123' import sys from glob import glob from os import walk -from zipfile import ZipFile +from zipfile import ZipFile, BadZipfile from os.path import relpath, isdir from optparse import OptionParser -def list_jar_file_under_lib_dirs(libs): +################################################################################ +# utils functions +################################################################################ + +def check_python_version(): + if sys.version_info < (2, 7): + exit('Using python 2 and its version lower than 2.7, not supported!\n' + 'Please use python 3 instead!\nPython version: ' + sys.version) + + +def print_box_message(msg): + print() + print('=' * 80) + print(msg) + print('=' * 80) + + +def list_jar_file_under_lib_dirs(lib_dirs): jar_files = set() - for lib in libs: - if isdir(lib): - jar_files |= {f for f in glob(lib + '/*.jar')} + for lib_dir in lib_dirs: + if isdir(lib_dir): + jar_files |= {f for f in glob(lib_dir + '/*.jar')} else: - jar_files.add(lib) + jar_files.add(lib_dir) return jar_files def list_class_under_jar_file(jar_file): - return {f for f in ZipFile(jar_file).namelist() if f.lower().endswith('.class')} + try: + return {f for f in ZipFile(jar_file).namelist() if f.lower().endswith('.class')} + except BadZipfile: + print('WARN: %s is bad zip file, ignored!' % jar_file, file=sys.stderr) + return set() def list_class_under_class_dir(class_dir): @@ -42,41 +65,56 @@ def list_class_under_class_dir(class_dir): for filename in file_names if filename.lower().endswith('.class')} -def expand_2_class_path(jar_files, class_dirs): - java_class_2_class_paths = {} +################################################################################ +# biz functions +################################################################################ + +def build_index__class_to_class_paths(jar_files, class_dirs): + class_to_class_paths = {} + # list all classes in jar files for jar_file in jar_files: for class_file in list_class_under_jar_file(jar_file): - java_class_2_class_paths.setdefault(class_file, set()).add(jar_file) - # list all classes in class dir + class_to_class_paths.setdefault(class_file, set()).add(jar_file) + + # list all classes in class dirs for class_dir in class_dirs: for class_file in list_class_under_class_dir(class_dir): - java_class_2_class_paths.setdefault(class_file, set()).add(class_dir) + class_to_class_paths.setdefault(class_file, set()).add(class_dir) - return java_class_2_class_paths, jar_files | set(class_dirs) + return class_to_class_paths, jar_files | set(class_dirs) -def find_duplicate_classes(java_class_2_class_paths): - class_path_2_duplicate_classes = {} +def find_duplicate_classes(class_to_class_paths): + class_paths_to_duplicate_classes = {} - for java_class, class_paths in list(java_class_2_class_paths.items()): + for clazz, class_paths in class_to_class_paths.items(): # skip java 9 module-info files - if java_class.endswith('/module-info.class'): + if clazz.endswith('/module-info.class'): continue if len(class_paths) > 1: - classes = class_path_2_duplicate_classes.setdefault(frozenset(class_paths), set()) - classes.add(java_class) + classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) + classes.add(clazz) - return class_path_2_duplicate_classes + return class_paths_to_duplicate_classes -def print_class_paths(class_paths): - print('') - print('=' * 80) - print('class paths to find:') - print('=' * 80) - for idx, class_path in enumerate(class_paths): - print('%-3d: %s' % (idx + 1, class_path)) +def print_duplicate_classes_info(class_paths_to_duplicate_classes): + print('Found duplicate classes in below class paths:') + for idx, jars in enumerate(class_paths_to_duplicate_classes, start=1): + print('%-3d(%d@%d): %s' % (idx, len(class_paths_to_duplicate_classes[jars]), len(jars), ' '.join(jars))) + + print_box_message('Duplicate classes detail info:') + for idx, (jars, classes) in enumerate(class_paths_to_duplicate_classes.items(), start=1): + print('%-3d(%d@%d): %s' % (idx, len(class_paths_to_duplicate_classes[jars]), len(jars), ' '.join(jars))) + for i, c in enumerate(classes, start=1): + print('%7d %s' % (i, c)) + + +def print_class_paths_info(class_paths): + print_box_message('class paths to find:') + for idx, class_path in enumerate(class_paths, start=1): + print('%-3d: %s' % (idx, class_path)) def main(): @@ -84,47 +122,27 @@ def main(): '[-c class-dir1 [-c class-dir2] ...] ' '[lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]') option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') - options, libs = option_parser.parse_args() + options, lib_dirs = option_parser.parse_args() + if not options.class_dirs and not lib_dirs: + lib_dirs = ['.'] - if not options.class_dirs and not libs: - libs = ['.'] + class_to_class_paths, class_paths = build_index__class_to_class_paths( + list_jar_file_under_lib_dirs(lib_dirs), options.class_dirs) - java_class_2_class_paths, class_paths = expand_2_class_path( - list_jar_file_under_lib_dirs(libs), options.class_dirs) + class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) - class_path_2_duplicate_classes = find_duplicate_classes(java_class_2_class_paths) - - if not class_path_2_duplicate_classes: + have_duplicate_classes = bool(class_paths_to_duplicate_classes) + if have_duplicate_classes: + print_duplicate_classes_info(class_paths_to_duplicate_classes) + else: print('COOL! No duplicate classes found!') - print_class_paths(class_paths) - exit() - print('Found duplicate classes in below class path:') - for idx, jars in enumerate(class_path_2_duplicate_classes): - print('%-3d(%d@%d): %s' % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), ' '.join(jars))) + print_class_paths_info(class_paths) - print('') - print('=' * 80) - print('Duplicate classes detail info:') - print('=' * 80) - for idx, (jars, classes) in enumerate(class_path_2_duplicate_classes.items()): - print('%-3d(%d@%d): %s' % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), ' '.join(jars))) - for i, c in enumerate(classes): - print('\t%-3d %s' % (i + 1, c)) - - print_class_paths(class_paths) - exit(1) - - -def check_python_version(): - python_version = sys.version_info - if python_version.major > 2: - return - if python_version.minor < 7: - exit('Using python 2 and its version lower than 2.7, not support!\n' - 'Please use python 3 instead!\nPython version: ' + sys.version) + return int(have_duplicate_classes) if __name__ == '__main__': check_python_version() - main() + + exit(main()) From 51f7247fe5f850a6a0317382ff441d3030ce4a52 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 16 Mar 2021 13:14:09 +0800 Subject: [PATCH 028/175] ! improve show-duplicate-java-classes output - improve format - sort items --- bin/show-duplicate-java-classes | 37 +++++++++++++---- docs/java.md | 74 ++++++++++++++++++--------------- 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 0e7885d9..3946187b 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -100,21 +100,42 @@ def find_duplicate_classes(class_to_class_paths): def print_duplicate_classes_info(class_paths_to_duplicate_classes): - print('Found duplicate classes in below class paths:') - for idx, jars in enumerate(class_paths_to_duplicate_classes, start=1): - print('%-3d(%d@%d): %s' % (idx, len(class_paths_to_duplicate_classes[jars]), len(jars), ' '.join(jars))) + # sort kv pairs + class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True, + key=lambda item: (len(item[0]), len(item[1]))) + # sort key(class_paths) and value(duplicate_classes) + class_paths_to_duplicate_classes = [(sorted(list(cps)), sorted(list(dcs))) + for cps, dcs in class_paths_to_duplicate_classes] + + print('Found duplicate classes in below %s class paths:' % len(class_paths_to_duplicate_classes)) + + idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) + for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): + print('[%*d] found %d duplicate classes in %d class paths:\n %s' % ( + idx_str_max_len, idx, + len(classes), len(class_paths), + '\n '.join(class_paths) + )) print_box_message('Duplicate classes detail info:') - for idx, (jars, classes) in enumerate(class_paths_to_duplicate_classes.items(), start=1): - print('%-3d(%d@%d): %s' % (idx, len(class_paths_to_duplicate_classes[jars]), len(jars), ' '.join(jars))) + for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): + print('[%*d] found %d duplicate classes in %d class paths %s :' % ( + idx_str_max_len, idx, + len(classes), len(class_paths), ' '.join(class_paths) + )) + class_idx_str_max_len = len(str(len(classes))) for i, c in enumerate(classes, start=1): - print('%7d %s' % (i, c)) + print(' %*d: %s' % (class_idx_str_max_len, i, c)) def print_class_paths_info(class_paths): - print_box_message('class paths to find:') + class_paths = sorted(list(class_paths)) + + print_box_message('Find in %s class paths:' % len(class_paths)) + + idx_str_max_len = len(str(len(class_paths))) for idx, class_path in enumerate(class_paths, start=1): - print('%-3d: %s' % (idx, class_path)) + print('%*d: %s' % (idx_str_max_len, idx, class_path)) def main(): diff --git a/docs/java.md b/docs/java.md index 0676f2bc..b50e6e28 100644 --- a/docs/java.md +++ b/docs/java.md @@ -227,7 +227,7 @@ $ show-busy-java-threads ---------------------- 找出`Java Lib`(`Java`库,即`Jar`文件)或`Class`目录(类目录)中的重复类。 -全系统支持(`Python`实现,安装`Python`即可),如`Linux`、`Mac`、`Windows`。 +全系统支持(`Python 3`实现,安装`Python 3`即可),如`Linux`、`Mac`、`Windows`。 `Java`开发的一个麻烦的问题是`Jar`冲突(即多个版本的`Jar`),或者说重复类。会出`NoSuchMethod`等的问题,还不见得当时出问题。找出有重复类的`Jar`,可以防患未然。 @@ -330,52 +330,58 @@ $ show-duplicate-java-classes WEB-INF/lib COOL! No duplicate classes found! ================================================================================ -class paths to find: +Find in 150 class paths: ================================================================================ -1 : WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar -2 : WEB-INF/lib/misc.htmlparser-0.0.0.jar -3 : WEB-INF/lib/normandy.client-1.0.2.jar + 1: WEB-INF/lib/aopalliance-1.0.jar + 2: WEB-INF/lib/asm-3.2.jar + 3: WEB-INF/lib/aspectjrt-1.6.1.jar + 4: WEB-INF/lib/aspectjweaver-1.6.6.jar ... $ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib -Found duplicate classes in below class path: -1 (293@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.orm-2.5.6.SEC02.jar -2 (2@3): WEB-INF/lib/servlet-api-3.0-alpha-1.jar WEB-INF/lib/jsp-api-2.1-rev-1.jar WEB-INF/lib/jstl-api-1.2-rev-1.jar -3 (104@2): WEB-INF/lib/commons-io-2.2.jar WEB-INF/lib/jakarta.commons.io-2.0.jar -4 (6@3): WEB-INF/lib/jakarta.commons.logging-1.1.jar WEB-INF/lib/commons-logging-1.1.1.jar WEB-INF/lib/org.slf4j.jcl104-over-slf4j-1.5.6.jar -5 (344@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar +Found duplicate classes in below 9 class paths: +[1] found 188 duplicate classes in 2 class paths: + WEB-INF/lib/jdom-2.0.2.jar + WEB-INF/lib/jdom2-2.0.6.jar +[2] found 150 duplicate classes in 2 class paths: + WEB-INF/lib/netty-all-4.0.35.Final.jar + WEB-INF/lib/netty-common-4.1.31.Final.jar +[3] found 148 duplicate classes in 2 class paths: + WEB-INF/lib/netty-all-4.0.35.Final.jar + WEB-INF/lib/netty-handler-4.1.31.Final.jar +[4] found 103 duplicate classes in 2 class paths: + WEB-INF/lib/hessian-3.0.14.bugfix-tae3.jar + WEB-INF/lib/hessian-4.0.38.jar ... ================================================================================ Duplicate classes detail info: ================================================================================ -1 (293@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.orm-2.5.6.SEC02.jar - 1 org/springframework/orm/toplink/TopLinkTemplate$13.class - 2 org/springframework/orm/hibernate3/HibernateTemplate$24.class - 3 org/springframework/orm/jpa/vendor/HibernateJpaDialect.class - 4 org/springframework/orm/hibernate3/TypeDefinitionBean.class - 5 org/springframework/orm/hibernate3/SessionHolder.class - ... -2 (2@3): WEB-INF/lib/servlet-api-3.0-alpha-1.jar WEB-INF/lib/jsp-api-2.1-rev-1.jar WEB-INF/lib/jstl-api-1.2-rev-1.jar - 1 javax/servlet/ServletException.class - 2 javax/servlet/ServletContext.class -3 (104@2): WEB-INF/lib/commons-io-2.2.jar WEB-INF/lib/jakarta.commons.io-2.0.jar - 1 org/apache/commons/io/input/ProxyReader.class - 2 org/apache/commons/io/output/FileWriterWithEncoding.class - 3 org/apache/commons/io/output/TaggedOutputStream.class - 4 org/apache/commons/io/filefilter/NotFileFilter.class - 5 org/apache/commons/io/filefilter/TrueFileFilter.class - ... +[1] found 188 duplicate classes in 2 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar : + 1: org/jdom2/Attribute.class + 2: org/jdom2/AttributeList$1.class + 3: org/jdom2/AttributeList$ALIterator.class + 4: org/jdom2/AttributeList.class + 5: org/jdom2/AttributeType.class + ... +[2] found 150 duplicate classes in 2 class paths WEB-INF/lib/netty-all-4.0.35.Final.jar WEB-INF/lib/netty-common-4.1.31.Final.jar : + 1: io/netty/util/AbstractReferenceCounted.class + 2: io/netty/util/Attribute.class + 3: io/netty/util/AttributeKey.class + 4: io/netty/util/AttributeMap.class + 5: io/netty/util/CharsetUtil.class + ... ... ================================================================================ -class paths to find: +Find in 232 class paths: ================================================================================ -1 : WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar -2 : WEB-INF/lib/misc.htmlparser-0.0.0.jar -3 : WEB-INF/lib/normandy.client-1.0.2.jar -4 : WEB-INF/lib/xml.xmlgraphics__batik-css-1.7.jar-1.7.jar -5 : WEB-INF/lib/jakarta.ecs-1.4.2.jar + 1: WEB-INF/classes + 2: WEB-INF/lib/HikariCP-2.7.8.jar + 3: WEB-INF/lib/accessors-smart-1.2.jar + 4: WEB-INF/lib/alimonitor-jmonitor-1.1.3.jar + 5: WEB-INF/lib/aopalliance-1.0.jar + 6: WEB-INF/lib/asm-5.0.4.jar ... ``` From 0c35709163ff0a43e82217828dad4e52b1139603 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 16 Mar 2021 13:32:28 +0800 Subject: [PATCH 029/175] ! improve show-duplicate-java-classes logic: skip java 9 module-info files --- bin/show-duplicate-java-classes | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 3946187b..2852f213 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -16,12 +16,13 @@ from __future__ import print_function __author__ = 'tg123' +import re import sys from glob import glob +from optparse import OptionParser from os import walk -from zipfile import ZipFile, BadZipfile from os.path import relpath, isdir -from optparse import OptionParser +from zipfile import ZipFile, BadZipfile ################################################################################ @@ -85,12 +86,15 @@ def build_index__class_to_class_paths(jar_files, class_dirs): return class_to_class_paths, jar_files | set(class_dirs) +_java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') + + def find_duplicate_classes(class_to_class_paths): class_paths_to_duplicate_classes = {} for clazz, class_paths in class_to_class_paths.items(): # skip java 9 module-info files - if clazz.endswith('/module-info.class'): + if _java9_module_file_pattern.match(clazz): continue if len(class_paths) > 1: classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) From 634bbd862c0e2d2852dfd72a39b2443725204ee4 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 17 Mar 2021 12:14:16 +0800 Subject: [PATCH 030/175] ! update show-duplicate-java-classes : specify python3 in shebang --- bin/show-duplicate-java-classes | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 2852f213..21b9713d 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Function # Find duplicate class among java libs. @@ -12,8 +12,6 @@ # @author tg123 (farmer1992 at gmail dot com) # @author Jerry Lee (oldratlee at gmail dot com) -from __future__ import print_function - __author__ = 'tg123' import re @@ -29,12 +27,6 @@ from zipfile import ZipFile, BadZipfile # utils functions ################################################################################ -def check_python_version(): - if sys.version_info < (2, 7): - exit('Using python 2 and its version lower than 2.7, not supported!\n' - 'Please use python 3 instead!\nPython version: ' + sys.version) - - def print_box_message(msg): print() print('=' * 80) @@ -168,6 +160,4 @@ def main(): if __name__ == '__main__': - check_python_version() - exit(main()) From 689658915027538c9e27903b1d5715aa499611cc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 22 Mar 2021 12:04:42 +0800 Subject: [PATCH 031/175] ! close zip file by with statement --- bin/show-duplicate-java-classes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 21b9713d..dcff1d81 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -46,7 +46,8 @@ def list_jar_file_under_lib_dirs(lib_dirs): def list_class_under_jar_file(jar_file): try: - return {f for f in ZipFile(jar_file).namelist() if f.lower().endswith('.class')} + with ZipFile(jar_file) as zf: + return {f for f in zf.namelist() if f.lower().endswith('.class')} except BadZipfile: print('WARN: %s is bad zip file, ignored!' % jar_file, file=sys.stderr) return set() From fcff5c5b2aaab2cc64695503744103ae7557f76a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 23 Mar 2021 22:14:05 +0800 Subject: [PATCH 032/175] + show-duplicate-java-classes: support find jar files in lib sub-dir and jar file recursively --- bin/show-duplicate-java-classes | 85 ++++++++++++++++++++++++++------- docs/java.md | 14 +++++- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index dcff1d81..06674b3f 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Function -# Find duplicate class among java libs. +# Find duplicate classes among java lib dirs and class dirs.. # # @Usage # $ show-duplicate-java-classes # find jars from current dir @@ -17,6 +17,7 @@ __author__ = 'tg123' import re import sys from glob import glob +from io import BytesIO from optparse import OptionParser from os import walk from os.path import relpath, isdir @@ -34,23 +35,52 @@ def print_box_message(msg): print('=' * 80) -def list_jar_file_under_lib_dirs(lib_dirs): +def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() for lib_dir in lib_dirs: if isdir(lib_dir): - jar_files |= {f for f in glob(lib_dir + '/*.jar')} + if recursive: + jar_files |= { + dir_path + '/' + filename + for dir_path, _, file_names in walk(lib_dir) + for filename in file_names if filename.lower().endswith('.jar') + } + else: + jar_files |= {f for f in glob(lib_dir + '/*.jar')} else: jar_files.add(lib_dir) return jar_files -def list_class_under_jar_file(jar_file): +def list_class_under_jar_file(jar_file, recursive): + """ + :return: map: jar_jar_path('a.jar!b.jar!c.jar') -> classes + """ + + def list_zip_in_zip(jar_paths, zf): + ret = {} + classes = {f for f in zf.namelist() if f.lower().endswith('.class')} + ret[jar_paths] = classes + if not recursive: + return ret + + jars_in_jar = {f for f in zf.namelist() if f.lower().endswith('.jar')} + for jar in jars_in_jar: + zip_paths = jar_paths + '!' + jar + try: + with ZipFile(BytesIO(zf.read(jar))) as f: + ret.update(list_zip_in_zip(zip_paths, f)) + except BadZipfile as e: + print('WARN: %s is bad zip file(%s), ignored!' % (zip_paths, e), file=sys.stderr) + + return ret + try: - with ZipFile(jar_file) as zf: - return {f for f in zf.namelist() if f.lower().endswith('.class')} - except BadZipfile: - print('WARN: %s is bad zip file, ignored!' % jar_file, file=sys.stderr) - return set() + with ZipFile(jar_file) as zip_file: + return list_zip_in_zip(jar_file, zip_file) + except BadZipfile as error: + print('WARN: %s is bad zip file(%s), ignored!' % (jar_file, error), file=sys.stderr) + return {} def list_class_under_class_dir(class_dir): @@ -63,20 +93,24 @@ def list_class_under_class_dir(class_dir): # biz functions ################################################################################ -def build_index__class_to_class_paths(jar_files, class_dirs): +def build_index__class_to_class_paths(jar_files, class_dirs, recursive_jar): class_to_class_paths = {} + class_paths = set() # list all classes in jar files for jar_file in jar_files: - for class_file in list_class_under_jar_file(jar_file): - class_to_class_paths.setdefault(class_file, set()).add(jar_file) + for jar_jar_path, classes in list_class_under_jar_file(jar_file, recursive=recursive_jar).items(): + class_paths.add(jar_jar_path) + for clazz in classes: + class_to_class_paths.setdefault(clazz, set()).add(jar_jar_path) # list all classes in class dirs for class_dir in class_dirs: + class_paths.add(class_dir) for class_file in list_class_under_class_dir(class_dir): class_to_class_paths.setdefault(class_file, set()).add(class_dir) - return class_to_class_paths, jar_files | set(class_dirs) + return class_to_class_paths, class_paths _java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') @@ -136,16 +170,31 @@ def print_class_paths_info(class_paths): def main(): - option_parser = OptionParser('usage: %prog ' - '[-c class-dir1 [-c class-dir2] ...] ' - '[lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]') - option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') + option_parser = OptionParser( + usage='%prog [OPTION]...' + ' [-c class-dir1 [-c class-dir2] ...]' + ' [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]' + '\nFind duplicate classes among java lib dirs and class dirs.' + '\n\nExamples:' + '\n %prog # find jars from current dir' + '\n %prog path/to/lib_dir1 /path/to/lib_dir2' + '\n %prog -c path/to/class_dir1 -c /path/to/class_dir2' + ) + option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False, + action='store_true', help='find jars in the sub-directories of lib dir') + option_parser.add_option('-J', '--recursive-jar', dest='recursive_jar', default=False, + action='store_true', help='find jars in the jar file') + option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], + action='append', help='add class dir') options, lib_dirs = option_parser.parse_args() + recursive_lib = options.recursive_lib + recursive_jar = options.recursive_jar if not options.class_dirs and not lib_dirs: lib_dirs = ['.'] class_to_class_paths, class_paths = build_index__class_to_class_paths( - list_jar_file_under_lib_dirs(lib_dirs), options.class_dirs) + list_jar_file_under_lib_dirs(lib_dirs, recursive=recursive_lib), + options.class_dirs, recursive_jar=recursive_jar) class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) diff --git a/docs/java.md b/docs/java.md index b50e6e28..74c1119a 100644 --- a/docs/java.md +++ b/docs/java.md @@ -243,6 +243,10 @@ show-duplicate-java-classes # 查找多个指定目录下所有Jar中的重复类 show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 +# 通过 -L 选项,查找子目录中的Jar文件 +show-duplicate-java-classes -L path/to/lib_dir1 +# 通过 -J 选项,查找Jar文件中的Jar文件(即查找FatJar中包含的Jar) +show-duplicate-java-classes -J path/to/lib_dir1 # 查找多个指定Class目录下的重复类。 Class目录 通过 -c 选项指定 show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 @@ -252,10 +256,18 @@ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 -c path/to/class_ # 帮助信息 $ show-duplicate-java-classes -h -Usage: show-duplicate-java-classes [-c class-dir1 [-c class-dir2] ...] [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...] +Usage: show-duplicate-java-classes [OPTION]... [-c class-dir1 [-c class-dir2] ...] [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...] +Find duplicate classes among java lib dirs and class dirs. + +Examples: + show-duplicate-java-classes # find jars from current dir + show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 + show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 Options: -h, --help show this help message and exit + -L, --recursive-lib find jars in the sub-directories of lib dir + -J, --recursive-jar find jars in the jar file -c CLASS_DIRS, --class-dir=CLASS_DIRS add class dir ``` From ad68fdf5b24768dd9d1db712895dc9e4959d7261 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Mar 2021 11:17:08 +0800 Subject: [PATCH 033/175] ! find-in-jars: improve output, display responsive message on stderr --- bin/find-in-jars | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index 64e5dd37..052dcdc0 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -21,9 +21,6 @@ readonly PROG="$(basename "$0")" # util functions ################################################################################ -# shellcheck disable=SC2015 -[ -t 1 ] && readonly is_console=true || readonly is_console=false - # NOTE: $'foo' is the escape sequence syntax of bash readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end @@ -31,7 +28,8 @@ readonly cr=$'\r' # carriage return readonly nl=$'\n' # new line redEcho() { - $is_console && echo "${ec}[1;31m$*$eend" || echo "$*" + # -t check: is a terminal device? + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } die() { @@ -41,18 +39,17 @@ die() { # Getting console width using a bash script # https://unix.stackexchange.com/questions/299067 -$is_console && readonly columns=$(stty size | awk '{print $2}') - +[ -t 2 ] && readonly columns=$(stty size | awk '{print $2}') printResponsiveMessage() { - $is_console || return 0 + [ -t 2 ] || return 0 local message="$*" # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - echo -n "${message:0:columns}" + echo -n "${message:0:columns}" >&2 } clearResponsiveMessage() { - $is_console || return 0 + [ -t 2 ] || return 0 # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 @@ -62,7 +59,7 @@ clearResponsiveMessage() { # echo -e "\033[1K" # Or everything on the line, regardless of cursor position: # echo -e "\033[2K" - echo -n "${ec}[2K$cr" + echo -n "${ec}[2K$cr" >&2 } usage() { @@ -75,6 +72,7 @@ usage() { cat >$out < Date: Thu, 25 Mar 2021 12:05:16 +0800 Subject: [PATCH 034/175] = find-in-jars: improve responsive message implementation --- bin/find-in-jars | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index 052dcdc0..b79b1c6b 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -24,8 +24,16 @@ readonly PROG="$(basename "$0")" # NOTE: $'foo' is the escape sequence syntax of bash readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end -readonly cr=$'\r' # carriage return 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? @@ -45,21 +53,13 @@ printResponsiveMessage() { local message="$*" # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - echo -n "${message:0:columns}" >&2 + echo -n "$clear_line${message:0:columns}" >&2 } clearResponsiveMessage() { [ -t 2 ] || return 0 - # 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" - echo -n "${ec}[2K$cr" >&2 + echo -n "$clear_line" >&2 } usage() { @@ -234,6 +234,8 @@ fi # find logic ################################################################################ +printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." + jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" [ -n "$jar_files" ] || die "No ${extensions[*]} file found!" @@ -258,9 +260,9 @@ findInJarFiles() { echo "${ec}[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" || echo "${jar_file}${separator}${file}" done - - clearResponsiveMessage done + + clearResponsiveMessage } echo "$jar_files" | findInJarFiles From cb8e96d774d459d7e5b19ceb7293f29e1bec610a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 27 Mar 2021 15:12:05 +0800 Subject: [PATCH 035/175] ! find-in-jars: add option -R, --no-find-progress, do not display responsive find progress --- bin/find-in-jars | 24 +++++++++++++++++------- docs/java.md | 2 ++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index b79b1c6b..6274c923 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -49,7 +49,9 @@ die() { # https://unix.stackexchange.com/questions/299067 [ -t 2 ] && readonly columns=$(stty size | awk '{print $2}') printResponsiveMessage() { - [ -t 2 ] || return 0 + 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 @@ -57,7 +59,9 @@ printResponsiveMessage() { } clearResponsiveMessage() { - [ -t 2 ] || return 0 + if ! $show_responsive || [ ! -t 2 ]; then + return + fi echo -n "$clear_line" >&2 } @@ -102,6 +106,7 @@ 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 \`!'. + -R, --no-find-progress do not display responsive find progress Miscellaneous: -h, --help display this help and exit @@ -121,6 +126,7 @@ declare -a args=() separator='!' regex_mode=-E use_absolute_path=false +show_responsive=true while (($# > 0)); do case "$1" in @@ -132,11 +138,6 @@ while (($# > 0)); do extensions=(${extensions[@]:+"${extensions[@]}"} "$2") shift 2 ;; - # support the typo option --seperator for compatibility - -s | --separator | --seperator) - separator="$2" - shift 2 - ;; -E | --extended-regexp) regex_mode=-E shift @@ -161,6 +162,15 @@ while (($# > 0)); do use_absolute_path=true shift ;; + # support the typo option name --seperator for compatibility + -s | --separator | --seperator) + separator="$2" + shift 2 + ;; + -R | --no-find-progress) + show_responsive=false + shift + ;; -h | --help) usage ;; diff --git a/docs/java.md b/docs/java.md index 74c1119a..c7d973ac 100644 --- a/docs/java.md +++ b/docs/java.md @@ -441,6 +441,7 @@ find-in-jars 'log4j\.properties' -s ' ' | awk '{print $2}' # 帮助信息 $ find-in-jars -h Usage: find-in-jars [OPTION]... PATTERN + Find files in the jar files under specified directory, search jar files recursively(include subdirectory). The pattern default is *extended* regex. @@ -470,6 +471,7 @@ 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 `!'. + -R, --no-find-progress do not display responsive find progress Miscellaneous: -h, --help display this help and exit From 96d0888304c9fc392228d75575956857b36fba10 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 27 Mar 2021 15:44:29 +0800 Subject: [PATCH 036/175] ! find-in-jars: extract searchJarFiles function --- bin/find-in-jars | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index 6274c923..e61f5b72 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -244,20 +244,32 @@ fi # find logic ################################################################################ -printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." +searchJarFiles() { + printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." -jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" -[ -n "$jar_files" ] || die "No ${extensions[*]} file found!" + local jar_files total_jar_count -total_count="$(echo "$jar_files" | wc -l)" -total_count="${total_count//[[:space:]]/}" # delete white space + 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" +} findInJarFiles() { [ -t 1 ] && local -r grep_color_option='--color=always' - local counter=1 jar_file + local counter=1 total_jar_count jar_file file + + read -r total_jar_count + while read -r jar_file; do - printResponsiveMessage "finding in jar($((counter++))/$total_count): $jar_file" + printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" $command_for_list_zip "${jar_file}" | { # Prevent grep from exiting in case of no match @@ -275,4 +287,4 @@ findInJarFiles() { clearResponsiveMessage } -echo "$jar_files" | findInJarFiles +searchJarFiles | findInJarFiles From bb4eaa2496753ef27c839ef1584b32c49da9c7a2 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 27 Mar 2021 16:31:51 +0800 Subject: [PATCH 037/175] ! improve docs of show-duplicate-java-classes --- bin/show-duplicate-java-classes | 2 +- docs/java.md | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 06674b3f..550a29bd 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -138,7 +138,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): class_paths_to_duplicate_classes = [(sorted(list(cps)), sorted(list(dcs))) for cps, dcs in class_paths_to_duplicate_classes] - print('Found duplicate classes in below %s class paths:' % len(class_paths_to_duplicate_classes)) + print('Found duplicate classes in below %s class path set:' % len(class_paths_to_duplicate_classes)) idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): diff --git a/docs/java.md b/docs/java.md index c7d973ac..24efec6c 100644 --- a/docs/java.md +++ b/docs/java.md @@ -233,9 +233,14 @@ $ show-busy-java-threads ### 用法 -- 通过脚本参数指定`Libs`目录,查找目录下`Jar`文件,收集`Jar`文件中`Class`文件以分析重复类。可以指定多个`Libs`目录。 - 注意,只会查找这个目录下`Jar`文件,不会查找子目录下`Jar`文件。因为`Libs`目录一般不会用子目录再放`Jar`,这样也避免把去查找不期望`Jar`。 -- 通过`-c`选项指定`Class`目录,直接收集这个目录下的`Class`文件以分析重复类。可以指定多个`Class`目录。 +- 通过脚本参数 指定 `Libs`目录,查找目录下`Jar`文件,收集`Jar`文件中`Class`文件以分析重复类。可以指定多个`Libs`目录。 + - 缺省只会查找指定`Lib`目录下`Jar`文件,不会收集`Lib`目录的子目录下`Jar`文件。 + - 因为`Libs`目录一般不会用子目录再放`Jar`,也避免把去查找不期望的`Jar`文件。 + - 可以通过 `-L`选项 设置 收集`Lib`子目录下的`Jar`文件;这样可以简化`Lib`目录的设置,不需要指定完整的`Lib`目录路径。 + - 对于找到的`Jar`文件,缺省不会进一步收集包含在`Jar`文件中的`Jar`。 + - 即`FatJar`/`UberJar`的场景,随着像`SpringBoot`的广泛使用,`FatJar`/`UberJar`也比较常见。 + - 可以通过 `-J`选项 设置 收集包含在`Jar`文件中的`Jar`。 +- 通过`-c`选项 指定 `Class`目录,直接收集这个目录下的`Class`文件以分析重复类。可以多次指定多个`Class`目录。 ```bash # 查找当前目录下所有Jar中的重复类 @@ -243,9 +248,9 @@ show-duplicate-java-classes # 查找多个指定目录下所有Jar中的重复类 show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 -# 通过 -L 选项,查找子目录中的Jar文件 +# 通过 -L 选项,收集子目录中的Jar文件 show-duplicate-java-classes -L path/to/lib_dir1 -# 通过 -J 选项,查找Jar文件中的Jar文件(即查找FatJar中包含的Jar) +# 通过 -J 选项,收集包含在Jar文件中的Jar文件(即 收集包含在FatJar/UberJar中的Jar) show-duplicate-java-classes -J path/to/lib_dir1 # 查找多个指定Class目录下的重复类。 Class目录 通过 -c 选项指定 @@ -351,7 +356,7 @@ Find in 150 class paths: ... $ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib -Found duplicate classes in below 9 class paths: +Found duplicate classes in below 9 class path set: [1] found 188 duplicate classes in 2 class paths: WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar From 5bfa3833bd5a6e24acc2d73fd193bd68d0e61a40 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 28 Mar 2021 19:12:30 +0800 Subject: [PATCH 038/175] + uq: add max input defensive check option --- bin/uq | 48 ++++++++++++++++++++++++++++++++++++++++--- docs/shell.md | 24 ++++++++++++++-------- test-cases/uq_test.sh | 37 ++++++++++++++++++++++++++++++++- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/bin/uq b/bin/uq index 5419ddc8..c3ae485e 100755 --- a/bin/uq +++ b/bin/uq @@ -12,7 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -PROG="$(basename "$0")" +readonly PROG="$(basename "$0")" ################################################################################ # util functions @@ -36,6 +36,27 @@ die() { exit 1 } +convertHumanReadableSizeToSize() { + local human_readable_size="$1" + + [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1 + + local size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" + case "$unit" in + g) + ((size *= 1024 * 1024 * 1024)) + ;; + m) + ((size *= 1024 * 1024)) + ;; + k) + ((size *= 1024)) + ;; + esac + + echo "$size" +} + usage() { local -r exit_code="${1:-0}" (($# > 0)) && shift @@ -70,8 +91,9 @@ Options: that are not repeated in the input -i, --ignore-case ignore differences in case when comparing -z, --zero-terminated line delimiter is NUL, not newline - -Miscellaneous: + -XM, --max-input max input size(count by char), support k,m,g postfix + default is 256m + avoid consuming large memory unexpectedly -h, --help display this help and exit EOF @@ -89,6 +111,7 @@ uq_opt_repeated_method=none uq_opt_only_unique=0 uq_opt_ignore_case=0 uq_opt_zero_terminated=0 +uq_max_input_human_readable_size=256m declare -a argv=() while (($# > 0)); do @@ -107,9 +130,11 @@ while (($# > 0)); do ;; --all-repeated=*) uq_opt_all_repeated=1 + uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}') [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" + shift ;; -u | --unique) @@ -124,6 +149,10 @@ while (($# > 0)); do uq_opt_zero_terminated=1 shift ;; + -XM | --max-input) + uq_max_input_human_readable_size="$2" + shift 2 + ;; -h | --help) usage ;; @@ -154,6 +183,9 @@ done [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 +uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || + usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" + argc=${#argv[@]} if ((argc == 0)); then @@ -229,6 +261,13 @@ BEGIN { } { + total_input_size += length + 1 + if (total_input_size > uq_max_input_size) { + printf "[%s] ERROR: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n", + uq_PROG, uq_max_input_human_readable_size > "/dev/stderr" + exit(1) + } + # use index to keep lines order original_lines[line_index++] = $0 @@ -255,6 +294,9 @@ awk \ -v "uq_opt_only_unique=$uq_opt_only_unique" \ -v "IGNORECASE=$uq_opt_ignore_case" \ -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ + -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \ + -v "uq_max_input_size=$uq_max_input_size" \ + -v "uq_PROG=$PROG" \ -f <(printf "%s" "$uq_awk_script") \ -- ${input_files[@]:+"${input_files[@]}"} \ >"$output_file" diff --git a/docs/shell.md b/docs/shell.md index e65671c2..d4f7e6ab 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -52,7 +52,7 @@ 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便键入。 +命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便快速键入。 更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip)。 @@ -121,9 +121,9 @@ Options: 彩色`cat`出文件行,方便人眼区分不同的行。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令支持选项、功能和使用方式与[`cat`命令](https://linux.die.net/man/1/cat)完全一样(实际上读流操作在实现上全部代理给`cat`命令)。 +命令支持选项、功能和使用方式与[`cat`命令](https://linux.die.net/man/1/cat)完全一样(实际上文件操作的实现全部代理给了`cat`命令)。 -命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色输入行就像件漂亮的外套~ 😆 +命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色的输出行就像件漂亮的外套~ 😆 ### 示例 @@ -210,12 +210,15 @@ test-cases/self-installer.sh 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 使用方式与支持的选项 模仿系统的`uniq`命令。支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -> ‼️ **_注意_**: 去重过程会在内存持有整个输入(因为全局去重)! -> 对于输入大小较大的场景(如输入有几百M甚至几G),需谨慎使用;往往需要结合业务场景开发对应的优化实现。 +> ‼️ **_注意_**: 去重过程会在内存持有整个输入(因为全局去重)! > +> 对于输入大小较大的场景(如输入量有几G),需谨慎使用以避免占用过多内存;往往需要结合业务场景开发对应的优化实现。 > 虽然平时的大部分场景输入量非常有限(如几M),一个简单没有充分优化的实现是快速够用的。 +> +> `uq`处理的最大输入量缺省是 256m(字符数),超过了最大输入量则出错退出,以避免意外消耗了过大的内存; +> 可以通过`-XM, --max-input`选项 设置 消耗更多内存可接受的合理最大输入量,如`uq --max-input 1g ...` -因为`uniq`命令完成是相邻行的去重,需要通过或是组合`sort`命令来完成整输入的去重,会有下面的问题: +因为系统的`uniq`命令去重相邻的行,需要组合`sort`命令以对整个输入去重,并且有下面的问题: ```bash # 示例输入 @@ -289,6 +292,10 @@ $ uq in1.txt in2.txt out.txt # 当有多个输入文件时,但要输出到控制台时,指定输出文件(最后一个文件参数)为 `-` 即可 $ uq in1.txt in2.txt - +# 如果消耗更多内存可接受的合理的,可以通过 -XM, --max-input 选项设置更大的最大输入量(缺省是256m) +$ uq -MI 768m large-file-input +$ uq --max-input 10g huge-file-input + # 帮助信息 $ uq -h Usage: uq [OPTION]... [INPUT [OUTPUT]] @@ -316,8 +323,9 @@ Options: that are not repeated in the input -i, --ignore-case ignore differences in case when comparing -z, --zero-terminated line delimiter is NUL, not newline - -Miscellaneous: + -XM, --max-input max input size(count by char), support k,m,g postfix + default is 256m + avoid consuming large memory unexpectedly -h, --help display this help and exit ``` diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 5de8d056..1cfb6d83 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -2,7 +2,7 @@ set -eEuo pipefail READLINK_CMD=readlink -if command -v greadlink &> /dev/null; then +if command -v greadlink &>/dev/null; then READLINK_CMD=greadlink fi @@ -109,6 +109,41 @@ test_ignore_case__count() { "$(echo "$input" | "$uq" -i -D -c)" } +test_max_input_check() { + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq"' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 4' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 1k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1042k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1m' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 10420g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input -XM 42m' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1024000g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1234567890g' + + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1' + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input' + + # shellcheck disable=SC2016 + assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g' +} + ################################################# # Load and run shUnit2. ################################################# From e7c8c095d6f4cacb8e546977869fcd361a17f90e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 28 Mar 2021 20:31:09 +0800 Subject: [PATCH 039/175] = improve/refactor show-duplicate-java-classes - reduce indentation by Guard Clauses - remove unnecessary list call in sorted - rename var --- bin/show-duplicate-java-classes | 37 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 550a29bd..1ebb4f70 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -38,17 +38,20 @@ def print_box_message(msg): def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() for lib_dir in lib_dirs: - if isdir(lib_dir): - if recursive: - jar_files |= { - dir_path + '/' + filename - for dir_path, _, file_names in walk(lib_dir) - for filename in file_names if filename.lower().endswith('.jar') - } - else: - jar_files |= {f for f in glob(lib_dir + '/*.jar')} - else: + if not isdir(lib_dir): jar_files.add(lib_dir) + continue + + if not recursive: + jar_files |= {p for p in glob(lib_dir + '/*.jar')} + continue + + jar_files |= { + dir_path + '/' + filename + for dir_path, _, file_names in walk(lib_dir) + for filename in file_names if filename.lower().endswith('.jar') + } + return jar_files @@ -57,21 +60,21 @@ def list_class_under_jar_file(jar_file, recursive): :return: map: jar_jar_path('a.jar!b.jar!c.jar') -> classes """ - def list_zip_in_zip(jar_paths, zf): + def list_zip_in_zip(jar_jar_path, zf): ret = {} classes = {f for f in zf.namelist() if f.lower().endswith('.class')} - ret[jar_paths] = classes + ret[jar_jar_path] = classes if not recursive: return ret jars_in_jar = {f for f in zf.namelist() if f.lower().endswith('.jar')} for jar in jars_in_jar: - zip_paths = jar_paths + '!' + jar + next_jar_jar_path = jar_jar_path + '!' + jar try: with ZipFile(BytesIO(zf.read(jar))) as f: - ret.update(list_zip_in_zip(zip_paths, f)) + ret.update(list_zip_in_zip(next_jar_jar_path, f)) except BadZipfile as e: - print('WARN: %s is bad zip file(%s), ignored!' % (zip_paths, e), file=sys.stderr) + print('WARN: %s is bad zip file(%s), ignored!' % (next_jar_jar_path, e), file=sys.stderr) return ret @@ -135,7 +138,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True, key=lambda item: (len(item[0]), len(item[1]))) # sort key(class_paths) and value(duplicate_classes) - class_paths_to_duplicate_classes = [(sorted(list(cps)), sorted(list(dcs))) + class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) for cps, dcs in class_paths_to_duplicate_classes] print('Found duplicate classes in below %s class path set:' % len(class_paths_to_duplicate_classes)) @@ -160,7 +163,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): def print_class_paths_info(class_paths): - class_paths = sorted(list(class_paths)) + class_paths = sorted(class_paths) print_box_message('Find in %s class paths:' % len(class_paths)) From 08e506b3e53c052b7b2bae35af2e4a29e042a960 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 28 Mar 2021 21:15:17 +0800 Subject: [PATCH 040/175] = refactor show-duplicate-java-classes - split function build_index__class_to_class_paths to 2 small functions: - collect_class_path_to_classes - invert_as_class_to_class_paths - use Guard Clauses --- bin/show-duplicate-java-classes | 53 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 1ebb4f70..f198172b 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -92,29 +92,28 @@ def list_class_under_class_dir(class_dir): for filename in file_names if filename.lower().endswith('.class')} -################################################################################ -# biz functions -################################################################################ - -def build_index__class_to_class_paths(jar_files, class_dirs, recursive_jar): - class_to_class_paths = {} - class_paths = set() - +def collect_class_path_to_classes(jar_files, class_dirs, recursive_jar): + class_path_to_classes = {} # list all classes in jar files for jar_file in jar_files: - for jar_jar_path, classes in list_class_under_jar_file(jar_file, recursive=recursive_jar).items(): - class_paths.add(jar_jar_path) - for clazz in classes: - class_to_class_paths.setdefault(clazz, set()).add(jar_jar_path) - + class_path_to_classes.update(list_class_under_jar_file(jar_file, recursive=recursive_jar)) # list all classes in class dirs for class_dir in class_dirs: - class_paths.add(class_dir) - for class_file in list_class_under_class_dir(class_dir): - class_to_class_paths.setdefault(class_file, set()).add(class_dir) + class_path_to_classes[class_dir] = list_class_under_class_dir(class_dir) + return class_path_to_classes - return class_to_class_paths, class_paths +def invert_as_class_to_class_paths(class_path_to_classes): + class_to_class_paths = {} + for class_path, classes in class_path_to_classes.items(): + for clazz in classes: + class_to_class_paths.setdefault(clazz, set()).add(class_path) + return class_to_class_paths + + +################################################################################ +# biz functions +################################################################################ _java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') @@ -124,11 +123,11 @@ def find_duplicate_classes(class_to_class_paths): for clazz, class_paths in class_to_class_paths.items(): # skip java 9 module-info files - if _java9_module_file_pattern.match(clazz): + if len(class_paths) == 1 or _java9_module_file_pattern.match(clazz): continue - if len(class_paths) > 1: - classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) - classes.add(clazz) + + classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) + classes.add(clazz) return class_paths_to_duplicate_classes @@ -190,24 +189,22 @@ def main(): option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') options, lib_dirs = option_parser.parse_args() - recursive_lib = options.recursive_lib - recursive_jar = options.recursive_jar if not options.class_dirs and not lib_dirs: lib_dirs = ['.'] - class_to_class_paths, class_paths = build_index__class_to_class_paths( - list_jar_file_under_lib_dirs(lib_dirs, recursive=recursive_lib), - options.class_dirs, recursive_jar=recursive_jar) + class_path_to_classes = collect_class_path_to_classes( + list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib), + options.class_dirs, recursive_jar=options.recursive_jar) + class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) - have_duplicate_classes = bool(class_paths_to_duplicate_classes) if have_duplicate_classes: print_duplicate_classes_info(class_paths_to_duplicate_classes) else: print('COOL! No duplicate classes found!') - print_class_paths_info(class_paths) + print_class_paths_info(class_path_to_classes.keys()) return int(have_duplicate_classes) From 480e905534dc84004c8b9dbd2d07dccc5c70e3e3 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 28 Mar 2021 22:32:38 +0800 Subject: [PATCH 041/175] + show-duplicate-java-classes: show duplicate classes total count --- bin/show-duplicate-java-classes | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index f198172b..94753b93 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -133,6 +133,7 @@ def find_duplicate_classes(class_to_class_paths): def print_duplicate_classes_info(class_paths_to_duplicate_classes): + duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) # sort kv pairs class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True, key=lambda item: (len(item[0]), len(item[1]))) @@ -140,7 +141,8 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) for cps, dcs in class_paths_to_duplicate_classes] - print('Found duplicate classes in below %s class path set:' % len(class_paths_to_duplicate_classes)) + print('Found %s duplicate classes in %s class path set:' % + (duplicate_classes_total_count, len(class_paths_to_duplicate_classes))) idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): From b107400c9022689a3726e95b1099ac45352efd42 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Mar 2021 10:58:07 +0800 Subject: [PATCH 042/175] = show-duplicate-java-classes: improve help and docs --- bin/show-duplicate-java-classes | 14 ++++++++++---- docs/java.md | 9 ++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 94753b93..1a995bb7 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -4,9 +4,12 @@ # Find duplicate classes among java lib dirs and class dirs.. # # @Usage -# $ show-duplicate-java-classes # find jars from current dir +# $ show-duplicate-java-classes # search jars from current dir # $ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 # $ show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 +# $ show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1 +# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the sub-directories of lib dir +# $ show-duplicate-java-classes -J path/to/lib_dir1 # search jars in the jar file # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-duplicate-java-classes # @author tg123 (farmer1992 at gmail dot com) @@ -180,14 +183,17 @@ def main(): ' [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]' '\nFind duplicate classes among java lib dirs and class dirs.' '\n\nExamples:' - '\n %prog # find jars from current dir' + '\n %prog # search jars from current dir' '\n %prog path/to/lib_dir1 /path/to/lib_dir2' '\n %prog -c path/to/class_dir1 -c /path/to/class_dir2' + '\n %prog -c path/to/class_dir1 path/to/lib_dir1' + '\n %prog -L path/to/lib_dir1' + '\n %prog -J path/to/lib_dir1' ) option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False, - action='store_true', help='find jars in the sub-directories of lib dir') + action='store_true', help='search jars in the sub-directories of lib dir') option_parser.add_option('-J', '--recursive-jar', dest='recursive_jar', default=False, - action='store_true', help='find jars in the jar file') + action='store_true', help='search jars in the jar file') option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') options, lib_dirs = option_parser.parse_args() diff --git a/docs/java.md b/docs/java.md index 24efec6c..a5857a9b 100644 --- a/docs/java.md +++ b/docs/java.md @@ -265,14 +265,17 @@ Usage: show-duplicate-java-classes [OPTION]... [-c class-dir1 [-c class-dir2] .. Find duplicate classes among java lib dirs and class dirs. Examples: - show-duplicate-java-classes # find jars from current dir + show-duplicate-java-classes # search jars from current dir show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 + show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1 + show-duplicate-java-classes -L path/to/lib_dir1 + show-duplicate-java-classes -J path/to/lib_dir1 Options: -h, --help show this help message and exit - -L, --recursive-lib find jars in the sub-directories of lib dir - -J, --recursive-jar find jars in the jar file + -L, --recursive-lib search jars in the sub-directories of lib dir + -J, --recursive-jar search jars in the jar file -c CLASS_DIRS, --class-dir=CLASS_DIRS add class dir ``` From 1fc77e3b21e553e12c98117340fe3ee1002d5399 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Mar 2021 15:43:48 +0800 Subject: [PATCH 043/175] + find-in-jars: new -L/-l options --- bin/find-in-jars | 77 +++++++++++++++++++++++++++++++++++++----------- docs/java.md | 16 ++++++++++ 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index e61f5b72..f44fac32 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -40,11 +40,6 @@ redEcho() { [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } -die() { - redEcho "Error: $*" 1>&2 - exit 1 -} - # Getting console width using a bash script # https://unix.stackexchange.com/questions/299067 [ -t 2 ] && readonly columns=$(stty size | awk '{print $2}') @@ -66,6 +61,12 @@ clearResponsiveMessage() { echo -n "$clear_line" >&2 } +die() { + clearResponsiveMessage + redEcho "Error: $*" 1>&2 + exit 1 +} + usage() { local -r exit_code="${1:-0}" (($# > 0)) && shift @@ -106,6 +107,10 @@ 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: @@ -127,6 +132,7 @@ separator='!' regex_mode=-E use_absolute_path=false show_responsive=true +only_print_file_name=false while (($# > 0)); do case "$1" in @@ -167,6 +173,16 @@ while (($# > 0)); do 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 @@ -261,27 +277,54 @@ searchJarFiles() { echo "$jar_files" } -findInJarFiles() { - [ -t 1 ] && local -r grep_color_option='--color=always' - - local counter=1 total_jar_count jar_file file - - read -r total_jar_count - - while read -r jar_file; do - printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" - - $command_for_list_zip "${jar_file}" | { +__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:-} -- "$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" + $command_for_list_zip "${jar_file}" | __outputResultOfJarFile "${jar_file}" done clearResponsiveMessage diff --git a/docs/java.md b/docs/java.md index a5857a9b..634f47e3 100644 --- a/docs/java.md +++ b/docs/java.md @@ -446,6 +446,10 @@ find-in-jars 'log4j\.properties' -a find-in-jars 'log4j\.properties' -s ' <-> ' find-in-jars 'log4j\.properties' -s ' ' | awk '{print $2}' +# -l选项 指定 只列出Jar文件,不显示Jar文件内匹配的文件列表 +# 列出 包含log4j.xml文件的Jar文件: +find-in-jars -l 'log4j\.xml$' + # 帮助信息 $ find-in-jars -h Usage: find-in-jars [OPTION]... PATTERN @@ -479,6 +483,10 @@ 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: @@ -493,6 +501,7 @@ Miscellaneous: # 在当前目录下的所有Jar文件中,查找出 log4j.properties文件 $ find-in-jars 'log4j\.properties$' ./hadoop-core-0.20.2-cdh3u3.jar!log4j.properties +...... # 查找出 以Service结尾的类,Jar文件路径输出成绝对路径 $ find-in-jars 'Service.class$' -a @@ -508,6 +517,13 @@ WEB-INF/lib/aspectjweaver-1.8.8.jar!org/aspectj/weaver/XlintDefault.properties ../deploy/lib/httpcore-4.3.3.jar!org/apache/http/version.properties ../deploy/lib/javax.servlet-api-3.0.1.jar!javax/servlet/http/LocalStrings_es.properties ...... + +# 列出 包含properties文件的Jar文件 +$ find-in-jars '\.properties$' -l -d WEB-INF/lib +WEB-INF/lib/aspectjtools-1.6.2.jar +WEB-INF/lib/aspectjweaver-1.8.8.jar +WEB-INF/lib/javax.servlet-api-3.0.1.jar +...... ``` ### 运行效果 From 4a563b7db13aa5fb8dec7c613384d745fe2832b2 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Mar 2021 16:37:39 +0800 Subject: [PATCH 044/175] ! fix Bash Traps and Pitfalls: DO NOT combine var declaration and assignment which value supplied by subshell! --- bin/c | 13 ++++++++++++- bin/find-in-jars | 18 ++++++++++++++++-- bin/show-busy-java-threads | 32 ++++++++++++++++++++++++++------ bin/uq | 18 +++++++++++++++--- bin/xpl | 13 ++++++++++++- legacy-bin/cp-svn-url | 16 ++++++++++++++-- lib/console-text-color-themes.sh | 13 ++++++++++++- 7 files changed, 107 insertions(+), 16 deletions(-) diff --git a/bin/c b/bin/c index da714ab9..3b372f36 100755 --- a/bin/c +++ b/bin/c @@ -8,9 +8,20 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-c # @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! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 set -eEuo pipefail -readonly PROG="$(basename "$0")" +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" ################################################################################ # util functions diff --git a/bin/find-in-jars b/bin/find-in-jars index f44fac32..1a62fe85 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -13,9 +13,20 @@ # # @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! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 set -eEuo pipefail -readonly PROG="$(basename "$0")" +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" ################################################################################ # util functions @@ -42,7 +53,10 @@ redEcho() { # Getting console width using a bash script # https://unix.stackexchange.com/questions/299067 -[ -t 2 ] && readonly columns=$(stty size | awk '{print $2}') +# +# NOTE: DO NOT declare columns var as readonly, because its value is supplied by subshell. +[ -t 2 ] && columns=$(stty size | awk '{print $2}') + printResponsiveMessage() { if ! $show_responsive || [ ! -t 2 ]; then return diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 2c93be91..33eff959 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -8,13 +8,26 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-busy-java-threads # @author Jerry Lee (oldratlee at gmail dot com) # @author superhj1987 (superhj1987 at 126 dot com) +# +# NOTE about Bash Traps and Pitfalls: +# +# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 -readonly PROG="$(basename "$0")" +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" readonly -a COMMAND_LINE=("$0" "$@") # Get current user name via whoami command # See https://www.lifewire.com/current-linux-user-whoami-command-3867579 # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! -readonly USER="$(whoami)" +# +# NOTE: DO NOT declare var USER as readonly, because its value is supplied by subshell. +USER="$(whoami)" ################################################################################ # util functions @@ -275,7 +288,8 @@ fi # biz logic ################################################################################ -readonly run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" +# NOTE: DO NOT declare var run_timestamp as readonly, because its value is supplied by subshell. +run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" readonly uuid="${PROG}_${run_timestamp}_${RANDOM}_$$" readonly tmp_store_dir="/tmp/${uuid}" @@ -313,7 +327,9 @@ findBusyJavaThreadsByPs() { # shellcheck disable=SC2206 local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --sort -pcpu --no-headers) - local -r ps_out="$("${ps_cmd_line[@]}")" + # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + local ps_out + ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" fi @@ -342,7 +358,9 @@ __top_threadId_cpu() { # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! local -a top_cmd_line=(top -H -b -d "$top_delay" -n 2) - local -r top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") + # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + local top_out + top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_top" fi @@ -365,7 +383,9 @@ __complete_pid_user_by_ps() { # ps output field: pid, thread id(lwp), user # shellcheck disable=SC2206 local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) - local -r ps_out="$("${ps_cmd_line[@]}")" + # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + local ps_out + ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" fi diff --git a/bin/uq b/bin/uq index c3ae485e..0a4ff950 100755 --- a/bin/uq +++ b/bin/uq @@ -9,10 +9,21 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq # @author Zava Xu (zava.kid at gmail dot com) -# @author Jerry Lee (oldratlee at gmail dot com) +# @author Jerry Lee (oldratlee at gmail dot com)# NOTE about Bash Traps and Pitfalls: +# +# NOTE about Bash Traps and Pitfalls: +# +# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 set -eEuo pipefail -readonly PROG="$(basename "$0")" +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" ################################################################################ # util functions @@ -183,10 +194,11 @@ done [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 +# NOTE: DO NOT declare var uq_max_input_size as readonly, because its value is supplied by subshell. uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" -argc=${#argv[@]} +readonly argc=${#argv[@]} if ((argc == 0)); then input_files=() diff --git a/bin/xpl b/bin/xpl index 41495571..6401cba5 100755 --- a/bin/xpl +++ b/bin/xpl @@ -7,9 +7,20 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @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! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 set -eEuo pipefail -readonly PROG="$(basename "$0")" +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" readonly nl=$'\n' # new line diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 34799b59..331b50ea 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -8,8 +8,19 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-cp-svn-url # @author ivanzhangwb (ivanzhangwb at gmail dot com) +# +# NOTE about Bash Traps and Pitfalls: +# +# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 -readonly PROG=`basename $0` +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG=`basename $0` usage() { cat <&2 exit 1 diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 9b9337f7..4e6cdc54 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -4,13 +4,24 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-console-text-color-themessh # @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! +# for example: readonly var1=$(echo value1) +# local var1=$(echo value1) +# +# declaration 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 _ctct_READLINK_CMD=readlink if command -v greadlink > /dev/null; then _ctct_READLINK_CMD=greadlink fi -readonly _ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")" +# NOTE: DO NOT declare var _ctct_PROG as readonly, because its value is supplied by subshell. +_ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")" [ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true readonly _ctct_ec=$'\033' # escape char From debab0973084d6cdc9159e40b5df17030a7ce6b7 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Mar 2021 19:14:50 +0800 Subject: [PATCH 045/175] = improvement: use `command -v` instead of `which` --- bin/show-busy-java-threads | 4 ++-- test-cases/self-installer.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 33eff959..0f231812 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -275,8 +275,8 @@ fi if [ -n "$jstack_path" ]; then [ -f "$jstack_path" ] || die "$jstack_path is NOT found!" [ -x "$jstack_path" ] || die "$jstack_path is NOT executable!" -elif which jstack &>/dev/null; then - jstack_path="$(which jstack)" +elif command -v jstack &>/dev/null; then + jstack_path="$(command -v jstack)" else [ -n "$JAVA_HOME" ] || die "jstack not found on PATH and No JAVA_HOME setting! Use -s option set jstack path manually." [ -f "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) file does NOT exists! Use -s option set jstack path manually." diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh index 0a16544e..60d26ac5 100644 --- a/test-cases/self-installer.sh +++ b/test-cases/self-installer.sh @@ -1,6 +1,6 @@ #!/bin/bash -if which svn &> /dev/null; then +if command -v svn &> /dev/null; then [ ! -d "/tmp/useful-scripts-$USER" ] && svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" fi From 1b99128af4e301adafcd9d115d25f235b831954e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Mar 2021 20:35:05 +0800 Subject: [PATCH 046/175] ! rp: output error message to stderr --- bin/rp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/rp b/bin/rp index 82394030..86d25d89 100755 --- a/bin/rp +++ b/bin/rp @@ -13,7 +13,7 @@ set -eEuo pipefail [ $# -eq 0 ] && { - echo "ERROR: NO argument!" + echo "ERROR: NO argument!" 1>&2 exit 1 } From 1ea4a3618475c6b0c24e3f1276613ab6f725481b Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 30 Mar 2021 09:50:17 +0800 Subject: [PATCH 047/175] = show-duplicate-java-classes: refactor print_duplicate_classes_info --- bin/show-duplicate-java-classes | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 1a995bb7..7dee2ce4 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -136,6 +136,10 @@ def find_duplicate_classes(class_to_class_paths): def print_duplicate_classes_info(class_paths_to_duplicate_classes): + if not class_paths_to_duplicate_classes: + print('COOL! No duplicate classes found!') + return + duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) # sort kv pairs class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True, @@ -206,15 +210,10 @@ def main(): class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) - have_duplicate_classes = bool(class_paths_to_duplicate_classes) - if have_duplicate_classes: - print_duplicate_classes_info(class_paths_to_duplicate_classes) - else: - print('COOL! No duplicate classes found!') - + print_duplicate_classes_info(class_paths_to_duplicate_classes) print_class_paths_info(class_path_to_classes.keys()) - return int(have_duplicate_classes) + return int(bool(class_paths_to_duplicate_classes)) if __name__ == '__main__': From 3d7509c5673d16112673860bd08b6e2719e6f4bc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 30 Mar 2021 15:26:03 +0800 Subject: [PATCH 048/175] + show-duplicate-java-classes: output responsive progress info at console --- bin/show-duplicate-java-classes | 66 ++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 7dee2ce4..118167fa 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -22,15 +22,38 @@ import sys from glob import glob from io import BytesIO from optparse import OptionParser -from os import walk +from os import walk, get_terminal_size from os.path import relpath, isdir from zipfile import ZipFile, BadZipfile - ################################################################################ # utils functions ################################################################################ +# 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" +__clear_line = '\033[2K\r' + + +def print_responsive_message(msg): + if not sys.stderr.isatty(): + return + columns, _ = get_terminal_size(sys.stderr.fileno()) + print(__clear_line + msg[:columns], end='', file=sys.stderr) + + +def clear_responsive_message(): + if not sys.stderr.isatty(): + return + print(__clear_line, end='', file=sys.stderr) + + def print_box_message(msg): print() print('=' * 80) @@ -40,7 +63,12 @@ def print_box_message(msg): def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() - for lib_dir in lib_dirs: + + idx_str_max_len = len(str(len(lib_dirs))) + for idx, lib_dir in enumerate(lib_dirs, start=1): + print_responsive_message('list jar file under lib dir(%*s/%s): %s' % + (idx_str_max_len, idx, len(lib_dirs), lib_dir)) + if not isdir(lib_dir): jar_files.add(lib_dir) continue @@ -58,12 +86,18 @@ def list_jar_file_under_lib_dirs(lib_dirs, recursive): return jar_files -def list_class_under_jar_file(jar_file, recursive): +def list_class_under_jar_file(jar_file, recursive, progress): """ :return: map: jar_jar_path('a.jar!b.jar!c.jar') -> classes """ + index = 0 def list_zip_in_zip(jar_jar_path, zf): + nonlocal index + index += 1 + print_responsive_message('list class under jar file(%*s/%s%s): %s' % ( + len(str(progress[1])), progress[0], progress[1], ' #' + str(index) if recursive else '', jar_jar_path)) + ret = {} classes = {f for f in zf.namelist() if f.lower().endswith('.class')} ret[jar_jar_path] = classes @@ -89,7 +123,10 @@ def list_class_under_jar_file(jar_file, recursive): return {} -def list_class_under_class_dir(class_dir): +def list_class_under_class_dir(class_dir, progress): + print_responsive_message('list class under class dir(%*s/%s): %s' % + (len(str(progress[1])), progress[0], progress[1], class_dir)) + return {relpath(dir_path + '/' + filename, class_dir) for dir_path, _, file_names in walk(class_dir) for filename in file_names if filename.lower().endswith('.class')} @@ -97,12 +134,18 @@ def list_class_under_class_dir(class_dir): def collect_class_path_to_classes(jar_files, class_dirs, recursive_jar): class_path_to_classes = {} + total_count = len(jar_files) + len(class_dirs) + index = 0 + # list all classes in jar files for jar_file in jar_files: - class_path_to_classes.update(list_class_under_jar_file(jar_file, recursive=recursive_jar)) + index += 1 + class_path_to_classes.update( + list_class_under_jar_file(jar_file, recursive=recursive_jar, progress=(index, total_count))) # list all classes in class dirs for class_dir in class_dirs: - class_path_to_classes[class_dir] = list_class_under_class_dir(class_dir) + index += 1 + class_path_to_classes[class_dir] = list_class_under_class_dir(class_dir, progress=(index, total_count)) return class_path_to_classes @@ -153,7 +196,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): - print('[%*d] found %d duplicate classes in %d class paths:\n %s' % ( + print('[%*s] found %s duplicate classes in %s class paths:\n %s' % ( idx_str_max_len, idx, len(classes), len(class_paths), '\n '.join(class_paths) @@ -161,7 +204,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): print_box_message('Duplicate classes detail info:') for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): - print('[%*d] found %d duplicate classes in %d class paths %s :' % ( + print('[%*s] found %s duplicate classes in %s class paths %s :' % ( idx_str_max_len, idx, len(classes), len(class_paths), ' '.join(class_paths) )) @@ -207,9 +250,12 @@ def main(): class_path_to_classes = collect_class_path_to_classes( list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib), options.class_dirs, recursive_jar=options.recursive_jar) - class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) + print_responsive_message('find duplicate classes...') + class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) + + clear_responsive_message() print_duplicate_classes_info(class_paths_to_duplicate_classes) print_class_paths_info(class_path_to_classes.keys()) From 0b14d4add4ad008324b458adfa3d157890c3b6ef Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 30 Mar 2021 16:09:38 +0800 Subject: [PATCH 049/175] ! show-duplicate-java-classes: validate lib/class dir --- bin/show-duplicate-java-classes | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 118167fa..72182bb6 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -23,7 +23,7 @@ from glob import glob from io import BytesIO from optparse import OptionParser from os import walk, get_terminal_size -from os.path import relpath, isdir +from os.path import relpath, isdir, exists from zipfile import ZipFile, BadZipfile ################################################################################ @@ -54,6 +54,10 @@ def clear_responsive_message(): print(__clear_line, end='', file=sys.stderr) +def print_error(msg): + print(__clear_line + msg, file=sys.stderr) + + def print_box_message(msg): print() print('=' * 80) @@ -69,6 +73,10 @@ def list_jar_file_under_lib_dirs(lib_dirs, recursive): print_responsive_message('list jar file under lib dir(%*s/%s): %s' % (idx_str_max_len, idx, len(lib_dirs), lib_dir)) + if not exists(lib_dir): + print_error('WARN: lib dir %s not exists, ignored!' % lib_dir) + continue + if not isdir(lib_dir): jar_files.add(lib_dir) continue @@ -111,7 +119,7 @@ def list_class_under_jar_file(jar_file, recursive, progress): with ZipFile(BytesIO(zf.read(jar))) as f: ret.update(list_zip_in_zip(next_jar_jar_path, f)) except BadZipfile as e: - print('WARN: %s is bad zip file(%s), ignored!' % (next_jar_jar_path, e), file=sys.stderr) + print_error('WARN: %s is bad zip file(%s), ignored!' % (next_jar_jar_path, e)) return ret @@ -119,7 +127,7 @@ def list_class_under_jar_file(jar_file, recursive, progress): with ZipFile(jar_file) as zip_file: return list_zip_in_zip(jar_file, zip_file) except BadZipfile as error: - print('WARN: %s is bad zip file(%s), ignored!' % (jar_file, error), file=sys.stderr) + print_error('WARN: %s is bad zip file(%s), ignored!' % (jar_file, error)) return {} @@ -127,6 +135,13 @@ def list_class_under_class_dir(class_dir, progress): print_responsive_message('list class under class dir(%*s/%s): %s' % (len(str(progress[1])), progress[0], progress[1], class_dir)) + if not exists(class_dir): + print_error('WARN: class dir %s not exists, ignored!' % class_dir) + return {} + if not isdir(class_dir): + print_error('WARN: class dir %s is not dir, ignored!' % class_dir) + return {} + return {relpath(dir_path + '/' + filename, class_dir) for dir_path, _, file_names in walk(class_dir) for filename in file_names if filename.lower().endswith('.class')} From 0f3d7106fdb4729af743e7a9e5733191938cd5fa Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 30 Mar 2021 16:21:57 +0800 Subject: [PATCH 050/175] + show-duplicate-java-classes: add option -R, --no-find-progress, do not display responsive find progress --- bin/show-duplicate-java-classes | 12 +++++++++--- docs/java.md | 2 ++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 72182bb6..2ddcb3f0 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -39,23 +39,25 @@ from zipfile import ZipFile, BadZipfile # Or everything on the line, regardless of cursor position: # echo -e "\033[2K" __clear_line = '\033[2K\r' +__show_responsive = True def print_responsive_message(msg): - if not sys.stderr.isatty(): + if not __show_responsive or not sys.stderr.isatty(): return columns, _ = get_terminal_size(sys.stderr.fileno()) print(__clear_line + msg[:columns], end='', file=sys.stderr) def clear_responsive_message(): - if not sys.stderr.isatty(): + if not __show_responsive or not sys.stderr.isatty(): return print(__clear_line, end='', file=sys.stderr) def print_error(msg): - print(__clear_line + msg, file=sys.stderr) + clear_responsive_message() + print(msg, file=sys.stderr) def print_box_message(msg): @@ -258,7 +260,11 @@ def main(): action='store_true', help='search jars in the jar file') option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], action='append', help='add class dir') + option_parser.add_option('-R', '--no-find-progress', dest='show_responsive', default=True, + action='store_false', help='do not display responsive find progress') options, lib_dirs = option_parser.parse_args() + global __show_responsive + __show_responsive = options.show_responsive if not options.class_dirs and not lib_dirs: lib_dirs = ['.'] diff --git a/docs/java.md b/docs/java.md index 634f47e3..eebc1706 100644 --- a/docs/java.md +++ b/docs/java.md @@ -278,6 +278,8 @@ Options: -J, --recursive-jar search jars in the jar file -c CLASS_DIRS, --class-dir=CLASS_DIRS add class dir + -R, --no-find-progress + do not display responsive find progress ``` #### `JDK`开发场景使用说明 From 2775555ec53e735d5673053f805895208153505a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 30 Mar 2021 19:01:09 +0800 Subject: [PATCH 051/175] ! show-duplicate-java-classes: improve get terminal columns logic --- bin/show-duplicate-java-classes | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 2ddcb3f0..439c9f4c 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -17,12 +17,13 @@ __author__ = 'tg123' +import os import re import sys from glob import glob from io import BytesIO from optparse import OptionParser -from os import walk, get_terminal_size +from os import walk from os.path import relpath, isdir, exists from zipfile import ZipFile, BadZipfile @@ -42,10 +43,25 @@ __clear_line = '\033[2K\r' __show_responsive = True +def __get_terminal_columns_of_stderr(): + """ + Rewritten for stderr from + """ + try: + columns, _ = os.get_terminal_size(sys.stderr.fileno()) + except (AttributeError, ValueError, OSError): + columns = 0 + + return columns + + def print_responsive_message(msg): if not __show_responsive or not sys.stderr.isatty(): return - columns, _ = get_terminal_size(sys.stderr.fileno()) + columns = __get_terminal_columns_of_stderr() + if columns <= 0: + return + print(__clear_line + msg[:columns], end='', file=sys.stderr) From bee5c9c336c84aff89d03bd567cd10b57f3145bf Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 31 Mar 2021 11:33:33 +0800 Subject: [PATCH 052/175] ! show-busy-java-threads: improve extract block logic of awk --- bin/show-busy-java-threads | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 0f231812..8489de8c 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -366,16 +366,13 @@ __top_threadId_cpu() { fi echo "$top_out" | - awk 'BEGIN { blockIndex = 0; currentLineHasText = 0; prevLineHasText = 0; } { - currentLineHasText = ($0 != "") - if (prevLineHasText && !currentLineHasText) - blockIndex++ # from text line to empty line, increase block index - - if (blockIndex == 3 && ($NF == "java" || $NF == "jsvc")) # $NF(last field) is command field - # only print 4th text block(blockIndex == 3), aka. process info of second top update - print $1 " " $9 # $1 is thread id field, $9 is %cpu field - - prevLineHasText = currentLineHasText # update prevLineHasText + awk '{ + # from text line to empty line, increase block index + if (previousLine && !$0) blockIndex++ + # only print 4th text block(blockIndex == 3), aka. process info of second top update + if (blockIndex == 3 && ($NF == "java" || $NF == "jsvc")) # $NF(last field) is command field + print $1, $9 # $1 is thread id field, $9 is %cpu field + previousLine = $0 }' | sort -k2,2nr } @@ -397,7 +394,7 @@ __complete_pid_user_by_ps() { # output field: pid, threadId, pcpu, user output_fields="$(echo "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { - printf "%s %s %s %s\n", $1, threadId, pcpu, $3; exit + print $1, threadId, pcpu, $3; exit }')" if [ -n "$output_fields" ]; then ((idx++)) From 74e6cb4a388d00c95c146c47e89ea833b3a01eba Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 31 Mar 2021 11:56:09 +0800 Subject: [PATCH 053/175] ! show-busy-java-threads: fix top extract error by process name COMMAND column of top -H output can be thread name not process name top output sample and env info: ``` $ uname -a Linux 33e449b39f66 4.19.121-linuxkit #1 SMP Thu Jan 21 15:36:34 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ java -version openjdk version "11.0.9.1" 2020-11-04 OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.18.04) OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.18.04, mixed mode, sharing) $ top -v procps-ng 3.3.12 Usage: top -hv | -bcHiOSs -d secs -n max -u|U user -p pid(s) -o field -w [cols] $ top -H PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 7012 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 java 7014 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.62 GC Thread#0 7015 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 G1 Main Marker 7017 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 G1 Refine#0 7018 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.06 G1 Young RemSet 7019 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.67 VM Thread 7020 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 Reference Handl 7021 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 Finalizer 7022 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 Signal Dispatch 7023 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.34 C2 CompilerThre 7024 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.03 C1 CompilerThre 7025 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 Sweeper thread 7026 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.00 Service Thread 7027 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.26 VM Periodic Tas 7032 jerry 20 0 3492604 79492 27096 S 0.0 3.9 0:00.52 GC Thread#1 ... ``` --- bin/show-busy-java-threads | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 8489de8c..928d6745 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -343,6 +343,14 @@ findBusyJavaThreadsByPs() { # top with output field: thread id, %cpu __top_threadId_cpu() { + local java_pid_list="${pid:-}" + if [ -z "$java_pid_list" ]; then + # shellcheck disable=SC2086 + java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" + # shellcheck disable=SC2086 + java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , + fi + # 1. sort by %cpu by top option `-o %CPU` # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), # use @@ -357,7 +365,7 @@ __top_threadId_cpu() { # and use second time update data to get cpu percentage of thread in 0.5 second interval # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! - local -a top_cmd_line=(top -H -b -d "$top_delay" -n 2) + local -a top_cmd_line=(top -H -b -d "$top_delay" -n 2 -p "$java_pid_list") # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. local top_out top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") @@ -370,7 +378,7 @@ __top_threadId_cpu() { # from text line to empty line, increase block index if (previousLine && !$0) blockIndex++ # only print 4th text block(blockIndex == 3), aka. process info of second top update - if (blockIndex == 3 && ($NF == "java" || $NF == "jsvc")) # $NF(last field) is command field + if (blockIndex == 3 && $1 ~ /^[0-9]+$/) print $1, $9 # $1 is thread id field, $9 is %cpu field previousLine = $0 }' | sort -k2,2nr From 1ad49b0e01b692ef1e0cbe4cd71bbad3a2104890 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 31 Mar 2021 13:44:01 +0800 Subject: [PATCH 054/175] ! show-busy-java-threads: add "No java process found" check --- bin/show-busy-java-threads | 42 ++++++++++++++++++++++++++------------ docs/java.md | 4 ++-- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 928d6745..2e67cf59 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -114,8 +114,8 @@ Example: ${PROG} 3 10 # update every 3 seconds, update 10 times Output control: - -p, --pid find out the highest cpu consumed threads from - the specified java process. + -p, --pid find out the highest cpu consumed threads from + the specified java process. support pid list(eg: 42,99). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. @@ -290,7 +290,7 @@ fi # NOTE: DO NOT declare var run_timestamp as readonly, because its value is supplied by subshell. run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" -readonly uuid="${PROG}_${run_timestamp}_${RANDOM}_$$" +readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" readonly tmp_store_dir="/tmp/${uuid}" if [ -n "$store_dir" ]; then @@ -318,6 +318,14 @@ else readonly ps_process_select_options="-C java -C jsvc" fi +__die_when_no_java_process_found() { + if [ -n "${pid}" ]; then + die "process($pid) is not running, or not java process!" + else + die 'No java process found!' + fi +} + # output field: pid, thread id(lwp), pcpu, user # order by pcpu(percentage of cpu usage) findBusyJavaThreadsByPs() { @@ -330,6 +338,8 @@ findBusyJavaThreadsByPs() { # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. local ps_out ps_out="$("${ps_cmd_line[@]}")" + [ -n "$ps_out" ] || __die_when_no_java_process_found + if [ -n "$store_dir" ]; then echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" fi @@ -343,13 +353,13 @@ findBusyJavaThreadsByPs() { # top with output field: thread id, %cpu __top_threadId_cpu() { - local java_pid_list="${pid:-}" - if [ -z "$java_pid_list" ]; then - # shellcheck disable=SC2086 - java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" - # shellcheck disable=SC2086 - java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , - fi + # DO NOT combine var java_pid_list declaration and assignment, because its value is supplied by subshell. + local java_pid_list + # shellcheck disable=SC2086 + java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" + [ -n "$java_pid_list" ] || __die_when_no_java_process_found + # shellcheck disable=SC2086 + java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , # 1. sort by %cpu by top option `-o %CPU` # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), @@ -373,15 +383,21 @@ __top_threadId_cpu() { echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_top" fi - echo "$top_out" | - awk '{ + # DO NOT combine var result_threads_top_info declaration and assignment, because its value is supplied by subshell. + local result_threads_top_info + result_threads_top_info=$( + echo "$top_out" | awk '{ # from text line to empty line, increase block index if (previousLine && !$0) blockIndex++ # only print 4th text block(blockIndex == 3), aka. process info of second top update if (blockIndex == 3 && $1 ~ /^[0-9]+$/) print $1, $9 # $1 is thread id field, $9 is %cpu field previousLine = $0 - }' | sort -k2,2nr + }' + ) + [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found + + echo "$result_threads_top_info" | sort -k2,2nr } __complete_pid_user_by_ps() { diff --git a/docs/java.md b/docs/java.md index eebc1706..981a79eb 100644 --- a/docs/java.md +++ b/docs/java.md @@ -120,8 +120,8 @@ Example: show-busy-java-threads 3 10 # update every 3 seconds, update 10 times Output control: - -p, --pid find out the highest cpu consumed threads from - the specified java process. + -p, --pid find out the highest cpu consumed threads from + the specified java process. support pid list(eg: 42,99). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. From 7942b5b594394a3d4eb0da86811f9cf6d2aab499 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 1 Apr 2021 13:48:44 +0800 Subject: [PATCH 055/175] ! show-busy-java-threads: rename global option value var pid -> pid_list --- bin/show-busy-java-threads | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 2e67cf59..0c4e4cc7 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -191,7 +191,7 @@ while true; do shift 2 ;; -p | --pid) - pid="$2" + pid_list="$2" shift 2 ;; -a | --append-file) @@ -312,15 +312,15 @@ headInfo() { echo } -if [ -n "${pid}" ]; then - readonly ps_process_select_options="-p $pid" +if [ -n "${pid_list}" ]; then + readonly ps_process_select_options="-p $pid_list" else readonly ps_process_select_options="-C java -C jsvc" fi __die_when_no_java_process_found() { - if [ -n "${pid}" ]; then - die "process($pid) is not running, or not java process!" + if [ -n "${pid_list}" ]; then + die "process($pid_list) is not running, or not java process!" else die 'No java process found!' fi From dce9ca88ef7063ad9b910c9da4459a5f1ae0b485 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 1 Apr 2021 16:28:35 +0800 Subject: [PATCH 056/175] ! refactor/improve bash script - use == instead of = in test - fix typo - use $() instead of `` - use [[ ]] instead of [] when compound condition - use if-else instead of &&-|| --- bin/uq | 4 +-- legacy-bin/cp-svn-url | 7 +++-- legacy-bin/svn-merge-stop-on-copy | 35 +++++++++++----------- legacy-bin/swtrunk | 21 ++++++------- lib/parseOpts.sh | 8 ++--- test-cases/integration-test.sh | 2 +- test-cases/my_unit_test_lib.sh | 2 +- test-cases/parseOpts_test.sh | 50 +++++++++++++++---------------- 8 files changed, 66 insertions(+), 63 deletions(-) diff --git a/bin/uq b/bin/uq index 0a4ff950..ad86a895 100755 --- a/bin/uq +++ b/bin/uq @@ -209,7 +209,7 @@ elif ((argc == 1)); then else input_files=("${argv[@]:0:argc-1}") output_file=${argv[argc - 1]} - if [ "$output_file" = - ]; then + if [ "$output_file" == - ]; then output_file=/dev/stdout fi fi @@ -217,7 +217,7 @@ fi # Check input file for f in ${input_files[@]:+"${input_files[@]}"}; do # - is stdin, ok - [ "$f" = - ] && continue + [ "$f" == - ] && continue [ -e "$f" ] || die "input file $f does not exist!" [ ! -d "$f" ] || die "input file $f exists, but is a directory!" diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 331b50ea..2c9a4d94 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -20,7 +20,7 @@ # tested on bash 3.2.57/4.2.46 # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. -PROG=`basename $0` +PROG="$(basename "$0")" usage() { cat < [target branch] -svn merge commit between verison when source branch copy(--stop-on-copy) +svn merge commit between version when source branch copy(--stop-on-copy) and head version of source branch. Source branch must be a remote branch. Example: ${PROG} http://www.foo.com/project1/branches/feature1 - # merge http://www.foo.com/project1/branches/feature1 to current svn direcotry + # merge http://www.foo.com/project1/branches/feature1 to current svn directory - ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/direcotry - # merge branch http://www.foo.com/project1/branches/feature1 to svn direcotry /path/to/svn/direcotry - # will prompt comfirm for committing to target branch. + ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/directory + # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory + # will prompt confirm for committing to target branch. ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 # because http://www.foo.com/project1/branches/feature2 is remote url, - # will check out target branch to tmp direcotry, and prompt comfirm for committing to target branch. + # will check out target branch to tmp directory, and prompt confirm for committing to target branch. EOF + # shellcheck disable=SC2086 exit $1 } @@ -68,21 +69,21 @@ cleanup() { } trap "cleanup" EXIT -svnstatusline=$(svn status --ignore-externals "$workDir" | grep -v ^X | wc -l) -[ "$svnstatusline" -ne 0 ] && { - echo "svn work direcotry is modified!" +svn_status_line=$(svn status --ignore-externals "$workDir" | grep -c -v ^X) +[ "$svn_status_line" -ne 0 ] && { + echo "svn work directory is modified!" exit 1 } cd "$workDir" && -from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1) && { +if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then echo "oldest version($from_version) of source branch $source_branch ." echo "starting merge to $workDir ." - svn merge -${from_version}:HEAD $source_branch -} || { + svn merge "-${from_version}:HEAD" "$source_branch" +else echo "Fail to merge to work dir $workDir ." exit 2 -} +fi -read -p "Check In? (Y/N)" ci -[ "$ci" = "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch" +read -r -p "Check In? (Y/N)" ci +[ "$ci" == "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch" diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index 8ab57641..902f3f6c 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -9,14 +9,14 @@ # @author Jerry Lee (oldratlee at gmail dot com) # NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char +readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end colorEcho() { local color=$1 shift # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" + [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" } redEcho() { @@ -29,18 +29,19 @@ greenEcho() { [ $# -eq 0 ] && dirs=(.) || dirs=("$@") -for d in "${dirs[@]}" ; do - [ ! -d ${d}/.svn ] && { +for d in "${dirs[@]}"; do + [ ! -d "${d}/.svn" ] && { redEcho "directory $d is not a svn work directory, ignore directory $d !" continue } ( cd "$d" && - branches=`svn info | grep '^URL' | awk '{print $2}'` && - trunk=`echo $branches | awk -F'/branches/' '{print $1}'`/trunk && - - svn sw "$trunk" && - greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." || - redEcho "fail to switch $d to trunk!" + branches=$(svn info | grep '^URL' | awk '{print $2}') && + trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk && + if svn sw "$trunk"; then + greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." + else + redEcho "fail to switch $d to trunk!" + fi ) done diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 197ec60f..34464458 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -76,7 +76,7 @@ _opts_findOptMode() { local optName for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "${optName}" ] && { + [ "$opt" == "${optName}" ] && { echo "$mode" return } @@ -111,7 +111,7 @@ _opts_setOptValue() { local optName for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "$optName" ] && { + [ "$opt" == "$optName" ] && { local optName2 for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "${optName2}"`" @@ -138,7 +138,7 @@ _opts_setOptArray() { local optName for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "$optName" ] && { + [ "$opt" == "$optName" ] && { # set _OPT_VALUE local optName2 for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do @@ -286,7 +286,7 @@ parseOpts() { local value for value in "$@" ; do - [ ";" = "$value" ] && { + [ ";" == "$value" ] && { foundComma=true break } || valueArray=("${valueArray[@]}" "$value") diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index f52f379e..efc1fd38 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -43,7 +43,7 @@ blueEcho() { logAndRun() { local simple_mode=false - [ "$1" = "-s" ] && { + [ "$1" == "-s" ] && { simple_mode=true shift } diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index ee26e10c..2c8bd79b 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -74,7 +74,7 @@ assertEquals() { failMsg=$1 shift } - [ "$1" = "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" + [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" } readonly __ut_exclude_vars_builtin='^BASH_|^_=|^COLUMNS=|LINES=' diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh index d3aff579..93e5704d 100755 --- a/test-cases/parseOpts_test.sh +++ b/test-cases/parseOpts_test.sh @@ -22,8 +22,8 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" -[ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" +[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" +[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" test_cArray=(c.sh -p pv -q qv cc) assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c @@ -53,14 +53,14 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" -[ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" +[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" +[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" test_cArray=(c.sh -p pv -q qv cc) assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c_long -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" test_argArray=(aa bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee) assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS @@ -81,11 +81,11 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 232 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" # ======================================== blueEcho "Test case: empty options" @@ -98,11 +98,11 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 0 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" # ======================================== blueEcho "Test case: illegal option name" @@ -115,11 +115,11 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 221 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|z,z-#long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee test_exitCode=$? @@ -128,11 +128,11 @@ _opts_showOptValueInfoList [ $test_exitCode -eq 222 ] || fail "Wrong exit code!" [ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" assertAllVarsSame From 91ca6915a5bf5acb75915ecd311484380e428efc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 1 Apr 2021 16:43:52 +0800 Subject: [PATCH 057/175] ! show-busy-java-threads: validate option value of update_delay/update_count/pid_list --- bin/show-busy-java-threads | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 0c4e4cc7..4cde6ddf 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -181,8 +181,9 @@ ARGS=$( } eval set -- "${ARGS}" -top_delay=0.5 count=5 +use_ps=false +top_delay=0.5 while true; do case "$1" in @@ -237,10 +238,17 @@ while true; do done update_delay=${1:-0} +# Bash RegEx to check floating point numbers from user input +# https://stackoverflow.com/questions/13790763 +[[ "$update_delay" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] || die "update delay($update_delay) is not a positive float number!" + [ -z "$1" ] && update_count=1 || update_count=${2:-0} -((update_count < 0)) && update_count=0 +[[ "$update_count" =~ ^[+]?[0-9]+$ ]] || die "update count($update_count) is not a natural number!" -use_ps=${use_ps:-false} +if [ -n "$pid_list" ]; then + pid_list="${pid_list//[[:space:]]/}" # delete white space + [[ "$pid_list" =~ ^([0-9]+)(,[0-9]+)*$ ]] || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" +fi # check the directory of append-file(-a) mode, create if not exist. if [ -n "$append_file" ]; then From 16a95a5672387766018e2884af17d512e1dd2a60 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 1 Apr 2021 21:00:16 +0800 Subject: [PATCH 058/175] ! show-busy-java-threads: improve self rerun command line in output --- bin/show-busy-java-threads | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 4cde6ddf..cf281890 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -21,7 +21,13 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly -a COMMAND_LINE=("$0" "$@") +# choosing between $0 and BASH_SOURCE +# https://stackoverflow.com/a/35006505/922688 +# How can I get the source directory of a Bash script from within the script itself? +# https://stackoverflow.com/questions/59895 +# Will $0 always include the path to the script? +# https://unix.stackexchange.com/questions/119929 +readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # Get current user name via whoami command # See https://www.lifewire.com/current-linux-user-whoami-command-3867579 # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! @@ -95,6 +101,23 @@ logAndCat() { cat } +# print calling(quoted) command line which is able to copy and paste to rerun safely +# +# How to get the complete calling command of a BASH script from inside the script (not just the arguments) +# https://stackoverflow.com/questions/36625593 +printCallingCommandLine() { + local arg isFirst=true + for arg in "${COMMAND_LINE[@]}"; do + if $isFirst; then + isFirst=false + else + printf ' ' + fi + printf '%q' "$arg" + done + echo +} + usage() { local -r exit_code="${1:-0}" (($# > 0)) && shift @@ -315,7 +338,7 @@ trap "cleanupWhenExit" EXIT headInfo() { colorEcho "0;34;42" ================================================================================ - echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((i + 1))/$update_count]: ${COMMAND_LINE[*]}" + echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((i + 1))/$update_count]: $(printCallingCommandLine)" colorEcho "0;34;42" ================================================================================ echo } @@ -460,7 +483,7 @@ printStackOfThreads() { # current user is not root user, so can not run with sudo; print error message and rerun suggestion redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" - yellowPrint " sudo ${COMMAND_LINE[*]}" + yellowPrint " sudo $(printCallingCommandLine)" normalPrint continue fi || { From d870ca35ef4bc45639ec43aee7b55a330ba49405 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 2 Apr 2021 13:51:48 +0800 Subject: [PATCH 059/175] ! show-duplicate-java-classes: output class path index --- bin/show-duplicate-java-classes | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 439c9f4c..24dd7b8d 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -229,11 +229,12 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): - print('[%*s] found %s duplicate classes in %s class paths:\n %s' % ( - idx_str_max_len, idx, - len(classes), len(class_paths), - '\n '.join(class_paths) - )) + print('[%*s] found %s duplicate classes in %s class paths:' % ( + idx_str_max_len, idx, len(classes), len(class_paths))) + + class_path_idx_str_max_len = len(str(len(class_paths))) + for i, cp in enumerate(class_paths, start=1): + print(' %*s: %s' % (class_path_idx_str_max_len, i, cp)) print_box_message('Duplicate classes detail info:') for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): @@ -241,9 +242,10 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): idx_str_max_len, idx, len(classes), len(class_paths), ' '.join(class_paths) )) + class_idx_str_max_len = len(str(len(classes))) for i, c in enumerate(classes, start=1): - print(' %*d: %s' % (class_idx_str_max_len, i, c)) + print(' %*s: %s' % (class_idx_str_max_len, i, c)) def print_class_paths_info(class_paths): @@ -253,7 +255,7 @@ def print_class_paths_info(class_paths): idx_str_max_len = len(str(len(class_paths))) for idx, class_path in enumerate(class_paths, start=1): - print('%*d: %s' % (idx_str_max_len, idx, class_path)) + print('%*s: %s' % (idx_str_max_len, idx, class_path)) def main(): From 68b50f574619eff0dc773b842ea1276d6144f0f8 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 2 Apr 2021 14:25:40 +0800 Subject: [PATCH 060/175] ! show-duplicate-java-classes: ensure output consistent for same input improve sort logic --- bin/show-duplicate-java-classes | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 24dd7b8d..2eef1376 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -217,12 +217,12 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): return duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) - # sort kv pairs - class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True, - key=lambda item: (len(item[0]), len(item[1]))) # sort key(class_paths) and value(duplicate_classes) class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) - for cps, dcs in class_paths_to_duplicate_classes] + for cps, dcs in class_paths_to_duplicate_classes.items()] + # sort kv pairs + class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes, reverse=True, + key=lambda item: (len(item[0]), len(item[1]), '|'.join(item[0]))) print('Found %s duplicate classes in %s class path set:' % (duplicate_classes_total_count, len(class_paths_to_duplicate_classes))) From f5b78ade87920278e04c6a03409c9aa7ee242c70 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 2 Apr 2021 11:10:19 +0800 Subject: [PATCH 061/175] ! show-busy-java-threads: rename i to update_round_num --- bin/show-busy-java-threads | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index cf281890..f66df870 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -338,7 +338,7 @@ trap "cleanupWhenExit" EXIT headInfo() { colorEcho "0;34;42" ================================================================================ - echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((i + 1))/$update_count]: $(printCallingCommandLine)" + echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" colorEcho "0;34;42" ================================================================================ echo } @@ -372,7 +372,7 @@ findBusyJavaThreadsByPs() { [ -n "$ps_out" ] || __die_when_no_java_process_found if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" fi if ((count > 0)); then @@ -411,7 +411,7 @@ __top_threadId_cpu() { local top_out top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then - echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_top" + echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" fi # DO NOT combine var result_threads_top_info declaration and assignment, because its value is supplied by subshell. @@ -439,7 +439,7 @@ __complete_pid_user_by_ps() { local ps_out ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" fi local idx=0 threadId pcpu output_fields @@ -469,7 +469,7 @@ printStackOfThreads() { threadId0x="0x$(printf %x "${threadId}")" ((idx++)) - local jstackFile="${store_file_prefix}$((i + 1))_jstack_${pid}" + local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" [ -f "${jstackFile}" ] || { # shellcheck disable=SC2206 local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid}) @@ -523,10 +523,10 @@ printStackOfThreads() { ################################################################################ main() { - local i + local update_round_num # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) - for ((i = 0; update_count <= 0 || i < update_count; ++i)); do - ((i > 0)) && sleep "$update_delay" + for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do + ((update_round_num > 0)) && sleep "$update_delay" [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null From 4c6b7f85a52ed6cf8d4d9a8eb7843b84f91021a7 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 2 Apr 2021 20:19:13 +0800 Subject: [PATCH 062/175] + add version option for scripts #73 --- bin/a2l | 72 +++++++++++++++++++++- bin/ap | 88 ++++++++++++++++++++++++++- bin/c | 10 ++++ bin/find-in-jars | 10 ++++ bin/rp | 100 ++++++++++++++++++++++++++++--- bin/show-busy-java-threads | 14 ++++- bin/show-duplicate-java-classes | 4 +- bin/tcp-connection-state-counter | 44 ++++++++++++++ bin/uq | 10 ++++ bin/xpl | 10 ++++ legacy-bin/cp-svn-url | 19 ++++++ 11 files changed, 366 insertions(+), 15 deletions(-) diff --git a/bin/a2l b/bin/a2l index 7c082b94..fc65b385 100755 --- a/bin/a2l +++ b/bin/a2l @@ -10,9 +10,18 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.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 colorEcho() { local color="$1" @@ -22,9 +31,70 @@ colorEcho() { [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } +redEcho() { + colorEcho 31 "$@" +} + +usage() { + cat < /dev/null; then +if command -v greadlink >/dev/null; then READLINK_CMD=greadlink fi -[ $# -eq 0 ] && files=(.) || files=("$@") +################################################################################ +# 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 + +colorEcho() { + local color="$1" + shift + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" +} + +redEcho() { + colorEcho 31 "$@" +} + +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 < 0)); do -h | --help) usage ;; + -V | --version) + progVersion + ;; --) shift args=(${args[@]:+"${args[@]}"} "$@") diff --git a/bin/rp b/bin/rp index 86d25d89..9ae755d0 100755 --- a/bin/rp +++ b/bin/rp @@ -12,26 +12,108 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -[ $# -eq 0 ] && { - echo "ERROR: NO argument!" 1>&2 +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.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 + +colorEcho() { + local color="$1" + shift + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" +} + +redEcho() { + colorEcho 31 "$@" +} + +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 <&2 exit 1 } -if [ $# -eq 1 ]; then +if [ "${#files[@]}" -eq 1 ]; then relativeTo=. - files=("$@") else - argv=("$@") - argc=$# + argc="${#files[@]}" # Get last argument - relativeTo="${argv[argc - 1]}" - files=( "${argv[@]:0:argc - 1}" ) + relativeTo="${files[argc - 1]}" + files=("${files[@]:0:argc-1}") fi [ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" -for f in "${files[@]}" ; do +for f in "${files[@]}"; do ! [ -e "$f" ] && { echo "$f does not exists!" continue diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index f66df870..0d20340c 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -21,6 +21,7 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.0-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 # How can I get the source directory of a Bash script from within the script itself? @@ -176,11 +177,17 @@ CPU usage calculation control: 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 +} + ################################################################################ # Check os support ################################################################################ @@ -195,8 +202,8 @@ uname | grep '^Linux' -q || die "$PROG only support Linux, not support $(uname) # readonly declaration make exit code of assignment to be always 0, aka. the exit code of `getopt` in subshell is discarded. # tested on bash 4.2.46 ARGS=$( - getopt -n "$PROG" -a -o p:c:a:s:S:Pd:Fmlh \ - -l count:,pid:,append-file:,jstack-path:,store-dir:,use-ps,top-delay:,force,mix-native-frames,lock-info,help \ + getopt -n "$PROG" -a -o c:p:a:s:S:Pd:FmlhV \ + -l count:,pid:,append-file:,jstack-path:,store-dir:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ -- "$@" ) || { echo @@ -253,6 +260,9 @@ while true; do -h | --help) usage ;; + -V | --version) + progVersion + ;; --) shift break diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 2eef1376..7b77bf2a 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -30,6 +30,7 @@ from zipfile import ZipFile, BadZipfile ################################################################################ # utils functions ################################################################################ +PROG_VERSION = '2.4.0-dev' # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 @@ -270,7 +271,8 @@ def main(): '\n %prog -c path/to/class_dir1 -c /path/to/class_dir2' '\n %prog -c path/to/class_dir1 path/to/lib_dir1' '\n %prog -L path/to/lib_dir1' - '\n %prog -J path/to/lib_dir1' + '\n %prog -J path/to/lib_dir1', + version='%prog ' + PROG_VERSION ) option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False, action='store_true', help='search jars in the sub-directories of lib dir') diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index f16bc7b7..f58991c8 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -10,6 +10,50 @@ # @author @sunuslee (sunuslee at gmail dot com) set -eEuo pipefail +# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.0-dev' + +################################################################################ +# util functions +################################################################################ + +usage() { + cat < 0)); do -h | --help) usage ;; + -V | --version) + progVersion + ;; --) shift argv=("${argv[@]}" "$@") diff --git a/bin/xpl b/bin/xpl index 6401cba5..6c8b87a9 100755 --- a/bin/xpl +++ b/bin/xpl @@ -21,6 +21,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.0-dev' readonly nl=$'\n' # new line @@ -40,12 +41,18 @@ Example: ${PROG} file.txt Options: -s, --selected select the file or dir -h, --help display this help and exit + -V, --version display version information and exit EOF # shellcheck disable=SC2086 exit "$exit_code" } +progVersion() { + echo "$PROG $PROG_VERSION" + exit +} + # if program name is xpf, set option selected! [ "xpf" == "${PROG}" ] && selected=true @@ -63,6 +70,9 @@ while [ $# -gt 0 ]; do -h | --help) usage ;; + -V | --version) + progVersion + ;; --) shift args=("${args[@]}" "$@") diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 2c9a4d94..aef54ee2 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -21,6 +21,7 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.0-dev' usage() { cat < Date: Fri, 2 Apr 2021 21:25:21 +0800 Subject: [PATCH 063/175] ! update docs: add version option for scripts --- docs/java.md | 7 +++++-- docs/shell.md | 4 +++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/java.md b/docs/java.md index 981a79eb..a6bd8bc4 100644 --- a/docs/java.md +++ b/docs/java.md @@ -159,6 +159,7 @@ CPU usage calculation control: Miscellaneous: -h, --help display this help and exit. + -V, --version display version information and exit. ``` ### 示例 @@ -210,7 +211,7 @@ $ show-busy-java-threads ### 贡献者 -- [silentforce](https://github.com/silentforce)改进此脚本,增加对环境变量`JAVA_HOME`的判断。 [#15](https://github.com/oldratlee/useful-scripts/pull/15) +- [silentforce](https://github.com/silentforce) 改进此脚本,增加对环境变量`JAVA_HOME`的判断。 [#15](https://github.com/oldratlee/useful-scripts/pull/15) - [liuyangc3](https://github.com/liuyangc3) - 发现并解决`jstack`非当前用户`Java`进程的问题。 [#50](https://github.com/oldratlee/useful-scripts/pull/50) - 优化性能,通过`read -a`简化反复的`awk`操作。 [#51](https://github.com/oldratlee/useful-scripts/pull/51) @@ -273,6 +274,7 @@ Examples: show-duplicate-java-classes -J path/to/lib_dir1 Options: + --version show program's version number and exit -h, --help show this help message and exit -L, --recursive-lib search jars in the sub-directories of lib dir -J, --recursive-jar search jars in the jar file @@ -409,7 +411,7 @@ Find in 232 class paths: ### 贡献者 -[tgic](https://github.com/tg123)提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673) +[tgic](https://github.com/tg123) 提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673) @@ -493,6 +495,7 @@ Output control: Miscellaneous: -h, --help display this help and exit + -V, --version display version information and exit ``` 注意,Pattern缺省是`grep`的 **扩展**正则表达式。 diff --git a/docs/shell.md b/docs/shell.md index d4f7e6ab..425eb990 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -99,7 +99,7 @@ Run command and put output to system clipper. If no command is specified, read from stdin(pipe). Example: - c echo 'hello world!' + c echo "hello world!" c grep -i 'hello world' menu.h main.c set | c c -q < ~/.ssh/id_rsa.pub @@ -108,6 +108,7 @@ Options: -k, --keep-eol do not trim new line at end of file -q, --quiet suppress all normal output, default is false -h, --help display this help and exit + -V, --version display version information and exit ``` ### 参考资料 @@ -327,6 +328,7 @@ Options: default is 256m avoid consuming large memory unexpectedly -h, --help display this help and exit + -V, --version display version information and exit ``` 🍺 [ap](../bin/ap) and [rp](../bin/rp) From 1c11fd293b40d7b5ea602a851a7614528f825bce Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 4 Apr 2021 20:12:39 +0800 Subject: [PATCH 064/175] ! find-in-jars: use `grep -c` option reduce output --- bin/find-in-jars | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index fdbc3db2..5c456303 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -314,7 +314,7 @@ __outputResultOfJarFile() { # - 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:-} -- "$pattern" &>/dev/null; then + if grep $regex_mode ${ignore_case_option:-} -c -- "$pattern" &>/dev/null; then matched=true fi From 3379bda9e3311b1494bb858a9a6118dd45d0043c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 5 Apr 2021 16:46:47 +0800 Subject: [PATCH 065/175] ! show-duplicate-java-classes: improve output sort --- bin/show-duplicate-java-classes | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 7b77bf2a..fddab7ae 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Function -# Find duplicate classes among java lib dirs and class dirs.. +# Find duplicate classes among java lib dirs and class dirs. # # @Usage # $ show-duplicate-java-classes # search jars from current dir @@ -218,15 +218,26 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): return duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) + print('Found %s duplicate classes in %s class path set:' % + (duplicate_classes_total_count, len(class_paths_to_duplicate_classes))) + # sort key(class_paths) and value(duplicate_classes) class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) for cps, dcs in class_paths_to_duplicate_classes.items()] # sort kv pairs - class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes, reverse=True, - key=lambda item: (len(item[0]), len(item[1]), '|'.join(item[0]))) - - print('Found %s duplicate classes in %s class path set:' % - (duplicate_classes_total_count, len(class_paths_to_duplicate_classes))) + # + # sort by multiple keys: + # 1. class paths count, *descending*; aka. sort by len(item[0]) *reverse=True* + # 2. duplicate classes count, *descending*; aka. sort by len(item[1]) *reverse=True* + # 3. class paths, ascending; aka. sort by item[0] + # sort also ensure output consistent for same input. + # + # How to sort objects by multiple keys in Python? + # https://stackoverflow.com/questions/1143671 + # Sort a list by multiple attributes? + # https://stackoverflow.com/questions/4233476 + class_paths_to_duplicate_classes.sort(key=lambda item: item[0]) + class_paths_to_duplicate_classes.sort(reverse=True, key=lambda item: (len(item[0]), len(item[1]))) idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): From 8c08e9daa2670de7f2a3956d226b6c62651d2058 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 5 Apr 2021 17:49:03 +0800 Subject: [PATCH 066/175] ! show-duplicate-java-classes: show duplicate class paths total count --- bin/show-duplicate-java-classes | 23 +++++++++++------------ docs/java.md | 23 ++++++++++++----------- docs/shell.md | 6 +++--- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index fddab7ae..d9a2d27a 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -89,8 +89,8 @@ def list_jar_file_under_lib_dirs(lib_dirs, recursive): idx_str_max_len = len(str(len(lib_dirs))) for idx, lib_dir in enumerate(lib_dirs, start=1): - print_responsive_message('list jar file under lib dir(%*s/%s): %s' % - (idx_str_max_len, idx, len(lib_dirs), lib_dir)) + print_responsive_message('list jar file under lib dir(%*s/%s): %s' % ( + idx_str_max_len, idx, len(lib_dirs), lib_dir)) if not exists(lib_dir): print_error('WARN: lib dir %s not exists, ignored!' % lib_dir) @@ -151,8 +151,8 @@ def list_class_under_jar_file(jar_file, recursive, progress): def list_class_under_class_dir(class_dir, progress): - print_responsive_message('list class under class dir(%*s/%s): %s' % - (len(str(progress[1])), progress[0], progress[1], class_dir)) + print_responsive_message('list class under class dir(%*s/%s): %s' % ( + len(str(progress[1])), progress[0], progress[1], class_dir)) if not exists(class_dir): print_error('WARN: class dir %s not exists, ignored!' % class_dir) @@ -195,7 +195,7 @@ def invert_as_class_to_class_paths(class_path_to_classes): # biz functions ################################################################################ -_java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') +__java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') def find_duplicate_classes(class_to_class_paths): @@ -203,7 +203,7 @@ def find_duplicate_classes(class_to_class_paths): for clazz, class_paths in class_to_class_paths.items(): # skip java 9 module-info files - if len(class_paths) == 1 or _java9_module_file_pattern.match(clazz): + if len(class_paths) == 1 or __java9_module_file_pattern.match(clazz): continue classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) @@ -218,8 +218,9 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): return duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) - print('Found %s duplicate classes in %s class path set:' % - (duplicate_classes_total_count, len(class_paths_to_duplicate_classes))) + class_paths_total_count = sum(len(cps) for cps in class_paths_to_duplicate_classes) + print('Found %s duplicate classes in %s class paths and %s class path sets:' % ( + duplicate_classes_total_count, class_paths_total_count, len(class_paths_to_duplicate_classes))) # sort key(class_paths) and value(duplicate_classes) class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) @@ -252,8 +253,7 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): print('[%*s] found %s duplicate classes in %s class paths %s :' % ( idx_str_max_len, idx, - len(classes), len(class_paths), ' '.join(class_paths) - )) + len(classes), len(class_paths), ' '.join(class_paths))) class_idx_str_max_len = len(str(len(classes))) for i, c in enumerate(classes, start=1): @@ -283,8 +283,7 @@ def main(): '\n %prog -c path/to/class_dir1 path/to/lib_dir1' '\n %prog -L path/to/lib_dir1' '\n %prog -J path/to/lib_dir1', - version='%prog ' + PROG_VERSION - ) + version='%prog ' + PROG_VERSION) option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False, action='store_true', help='search jars in the sub-directories of lib dir') option_parser.add_option('-J', '--recursive-jar', dest='recursive_jar', default=False, diff --git a/docs/java.md b/docs/java.md index a6bd8bc4..57a6acde 100644 --- a/docs/java.md +++ b/docs/java.md @@ -363,25 +363,26 @@ Find in 150 class paths: ... $ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib -Found duplicate classes in below 9 class path set: -[1] found 188 duplicate classes in 2 class paths: - WEB-INF/lib/jdom-2.0.2.jar - WEB-INF/lib/jdom2-2.0.6.jar +Found 1272 duplicate classes in 345 class paths and 9 class path sets: +[1] found 188 duplicate classes in 3 class paths: + 1: WEB-INF/lib/jdom-2.0.2.jar + 2: WEB-INF/lib/jdom2-2.0.6.jar + 3: WEB-INF/lib/jdom2-2.0.8.jar [2] found 150 duplicate classes in 2 class paths: - WEB-INF/lib/netty-all-4.0.35.Final.jar - WEB-INF/lib/netty-common-4.1.31.Final.jar + 1: WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: WEB-INF/lib/netty-common-4.1.31.Final.jar [3] found 148 duplicate classes in 2 class paths: - WEB-INF/lib/netty-all-4.0.35.Final.jar - WEB-INF/lib/netty-handler-4.1.31.Final.jar + 1: WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: WEB-INF/lib/netty-handler-4.1.31.Final.jar [4] found 103 duplicate classes in 2 class paths: - WEB-INF/lib/hessian-3.0.14.bugfix-tae3.jar - WEB-INF/lib/hessian-4.0.38.jar + 1: WEB-INF/lib/hessian-3.0.14.bugfix-tae3.jar + 2: WEB-INF/lib/hessian-4.0.38.jar ... ================================================================================ Duplicate classes detail info: ================================================================================ -[1] found 188 duplicate classes in 2 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar : +[1] found 188 duplicate classes in 3 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar WEB-INF/lib/jdom2-2.0.8.jar : 1: org/jdom2/Attribute.class 2: org/jdom2/AttributeList$1.class 3: org/jdom2/AttributeList$ALIterator.class diff --git a/docs/shell.md b/docs/shell.md index 425eb990..abaf74e1 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -436,7 +436,7 @@ xpf /path/to/dir1 /path/to/foo1.txt ### 贡献者 -[Linhua Tan](https://github.com/toolchainX)修复Linux的选定Bug。 +- [Linhua Tan](https://github.com/toolchainX) 修复Linux的选定Bug。 `Shell`开发/测试加强 ==================================== @@ -507,7 +507,7 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" ### 参考资料 -- [utensil](https://github.com/utensil)的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! +- [utensil](https://github.com/utensil) 的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! 🍺 [parseOpts.sh](../lib/parseOpts.sh) ---------------------- @@ -608,5 +608,5 @@ parseOpts "a,a-long|b,b-long:|c,c-long+" -a -b bv -- --c-long c.sh -p pv -q qv a ### 贡献者 -[Khotyn Huang](https://github.com/khotyn)指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。 +- [Khotyn Huang](https://github.com/khotyn) 指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。 From fd6bfed05127536dae30b3ddb9c5e6327e3cf16c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 5 Apr 2021 20:07:09 +0800 Subject: [PATCH 067/175] + add bump-scripts-version.sh --- test-cases/bump-scripts-version.sh | 71 ++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100755 test-cases/bump-scripts-version.sh diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh new file mode 100755 index 00000000..2a65493d --- /dev/null +++ b/test-cases/bump-scripts-version.sh @@ -0,0 +1,71 @@ +#!/bin/bash +set -eEuo pipefail + +################################################################################ +# 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 + +colorEcho() { + local color=$1 + shift + + # if stdout is the console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" +} + +redEcho() { + colorEcho 31 "$@" +} + +yellowEcho() { + colorEcho 33 "$@" +} + +blueEcho() { + colorEcho 36 "$@" +} + +logAndRun() { + local simple_mode=false + [ "$1" = "-s" ] && { + simple_mode=true + shift + } + + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + blueEcho "Run under work directory $PWD :$nl$*" + time "$@" + fi +} + +die() { + redEcho "Error: $*" 1>&2 + exit 1 +} + +################################################################################ +# biz logic +################################################################################ + +[ $# -ne 1 ] && die "need only 1 argument for version!$nl${nl}usage:$nl $0 2.x.y" +readonly bump_version="$1" + +# adjust current dir to project dir +cd "$(dirname "$(readlink -f "$0")")/.." + +script_files=$( + find bin legacy-bin -type f +) + +# shellcheck disable=SC2086 +logAndRun sed -ri \ + 's/^(.*PROG_VERSION\s*=\s*)'\''(.*)'\''(.*)$/\1'\'"$bump_version"\''\3/' \ + $script_files From 0e172ed714fd9cd925f8405a06c3e8a5a05be8f3 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 6 Apr 2021 13:37:35 +0800 Subject: [PATCH 068/175] ! show-duplicate-java-classes: improve jar in jar index marker --- bin/show-duplicate-java-classes | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index d9a2d27a..b282434e 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -122,8 +122,11 @@ def list_class_under_jar_file(jar_file, recursive, progress): def list_zip_in_zip(jar_jar_path, zf): nonlocal index index += 1 + index_marker = '' + if recursive: + index_marker = ' #%3s' % index print_responsive_message('list class under jar file(%*s/%s%s): %s' % ( - len(str(progress[1])), progress[0], progress[1], ' #' + str(index) if recursive else '', jar_jar_path)) + len(str(progress[1])), progress[0], progress[1], index_marker, jar_jar_path)) ret = {} classes = {f for f in zf.namelist() if f.lower().endswith('.class')} From f5d386225c463d08dfb05353420e7dd0446670a0 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 6 Apr 2021 13:39:40 +0800 Subject: [PATCH 069/175] ! show-duplicate-java-classes: improve sort in print_duplicate_classes_info sort multiple keys by one trip --- bin/show-duplicate-java-classes | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index b282434e..21ae69c8 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -240,8 +240,9 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): # https://stackoverflow.com/questions/1143671 # Sort a list by multiple attributes? # https://stackoverflow.com/questions/4233476 - class_paths_to_duplicate_classes.sort(key=lambda item: item[0]) - class_paths_to_duplicate_classes.sort(reverse=True, key=lambda item: (len(item[0]), len(item[1]))) + # + # use - operator of number key for reverse sort key + class_paths_to_duplicate_classes.sort(key=lambda item: (-len(item[0]), -len(item[1]), item[0])) idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): From aed64c6a50c01f1d1b2afd1eef0fd5462cf4b209 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 26 Apr 2021 13:02:16 +0800 Subject: [PATCH 070/175] + add cp-into-docker-run #83 --- bin/cp-into-docker-run | 184 +++++++++++++++++++++++++++++++++++++++++ docs/shell.md | 120 +++++++++++++++++---------- 2 files changed, 261 insertions(+), 43 deletions(-) create mode 100755 bin/cp-into-docker-run diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run new file mode 100755 index 00000000..b278e05c --- /dev/null +++ b/bin/cp-into-docker-run @@ -0,0 +1,184 @@ +#!/bin/bash +set -eEuo pipefail + +PROG="$(basename "$0")" +readonly PROG_VERSION='2.4.0-dev' + +READLINK_CMD=readlink +if command -v greadlink >/dev/null; then + READLINK_CMD=greadlink +fi + +################################################################################ +# 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 + +redEcho() { + # -t check: is a terminal device? + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" +} + +die() { + redEcho "Error: $*" 1>&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 < 0)); do + case "$1" in + -c | --container) + container_name="$2" + shift 2 + ;; + -u | --docker-user) + docker_user="$2" + shift 2 + ;; + -w | --workdir) + docker_workdir="$2" + shift 2 + ;; + -t | --tmpdir) + docker_tmpdir="$2" + shift 2 + ;; + -v | --verbose) + verbose=true + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + -*) + usage 2 "${PROG}: unrecognized option '$1'" + ;; + *) + # if not option, treat all follow args as command + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + esac +done + +################################################################################ +# biz logic +################################################################################ + +######################################## +# check docker command existence +######################################## + +command -v docker &>/dev/null || die 'docker command not found!' + +######################################## +# prepare vars for docker operation +######################################## + +readonly specified_run_command="${args[0]}" +run_command="$specified_run_command" +if [ ! -f "$specified_run_command" ]; then + which "$specified_run_command" &>/dev/null || + die "specified command not exists and not found in PATH: $specified_run_command" + + run_command="$(which "$specified_run_command")" +fi +run_command="$($READLINK_CMD -f "$run_command")" + +run_timestamp="$(date "+%Y%m%d_%H%M%S")" +readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" + +run_command_base_name="$(basename "$run_command")" +readonly run_command_dir_in_docker="$docker_tmpdir/$uuid" +readonly run_command_in_docker="$run_command_dir_in_docker/$run_command_base_name" + +cleanupWhenExit() { + # remove tmp dir in docker by root user + docker exec "${container_name}" rm -rf -- "$run_command_dir_in_docker" &>/dev/null +} +trap "cleanupWhenExit" EXIT + +######################################## +# docker operations +######################################## + +logAndRun() { + $verbose && echo "[$PROG] $*" 1>&2 + "$@" +} + +logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ + mkdir -p -- "$run_command_dir_in_docker" +logAndRun docker cp "$run_command" "$container_name:$run_command_in_docker" +logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ + chmod +x "$run_command_in_docker" + +logAndRun docker exec -i -t \ + ${docker_user:+"--user=$docker_user"} \ + ${docker_workdir:+"--workdir=$docker_workdir"} \ + "$container_name" \ + "$run_command_in_docker" "${args[@]:1:${#args[@]}}" diff --git a/docs/shell.md b/docs/shell.md index abaf74e1..d651ab13 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -10,34 +10,34 @@ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - [🍺 coat](#-coat) - - [示例](#%E7%A4%BA%E4%BE%8B) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-1) - [🍺 a2l](#-a2l) - - [示例](#%E7%A4%BA%E4%BE%8B-1) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-2) - [🍺 uq](#-uq) - - [示例](#%E7%A4%BA%E4%BE%8B-2) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-3) - [🍺 ap and rp](#-ap-and-rp) - - [示例](#%E7%A4%BA%E4%BE%8B-3) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-4) + - [🍺 cp-into-docker-run](#-cp-into-docker-run) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-5) - [🍺 tcp-connection-state-counter](#-tcp-connection-state-counter) - - [用法](#%E7%94%A8%E6%B3%95) - - [示例](#%E7%A4%BA%E4%BE%8B-4) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-6) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85) - [🍺 xpl and xpf](#-xpl-and-xpf) - - [用法](#%E7%94%A8%E6%B3%95-1) - - [示例](#%E7%A4%BA%E4%BE%8B-5) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-7) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-1) - [`Shell`开发/测试加强](#shell%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95%E5%8A%A0%E5%BC%BA) - [🍺 echo-args](#-echo-args) - - [示例](#%E7%A4%BA%E4%BE%8B-6) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-8) - [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) - [🍺 console-text-color-themes.sh](#-console-text-color-themessh) - - [用法](#%E7%94%A8%E6%B3%95-2) - - [示例](#%E7%A4%BA%E4%BE%8B-7) + - [用法](#%E7%94%A8%E6%B3%95) + - [示例](#%E7%A4%BA%E4%BE%8B) - [运行效果](#%E8%BF%90%E8%A1%8C%E6%95%88%E6%9E%9C) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-2) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99-1) - [🍺 parseOpts.sh](#-parseoptssh) - - [用法](#%E7%94%A8%E6%B3%95-3) - - [示例](#%E7%A4%BA%E4%BE%8B-8) + - [用法](#%E7%94%A8%E6%B3%95-1) + - [示例](#%E7%A4%BA%E4%BE%8B-1) - [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-3) @@ -126,7 +126,7 @@ Options: 命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色的输出行就像件漂亮的外套~ 😆 -### 示例 +### 用法/示例 ```bash $ echo Hello world | coat @@ -186,7 +186,7 @@ or available locally via: info '(coreutils) cat invocation' 命令名`a2l`意思是`Arguments to(2) Lines`。 -### 示例 +### 用法/示例 ```bash $ a2l *.java @@ -254,7 +254,7 @@ $ sort foo.txt | uniq -c # 输入行重排序了! ``` -### 示例 +### 用法/示例 ```bash $ uq foo.txt # 输入是文件 @@ -339,7 +339,7 @@ Options: 命令名`ap`意思是`Absolute Path`,`rp`是`Relative Path`。 -### 示例 +### 用法/示例 ```bash # ap缺省打印当前路径的绝对路径 @@ -363,6 +363,47 @@ $ rp /home /etc/../etc /home/admin ../../etc ``` +🍺 [cp-into-docker-run](../bin/cp-into-docker-run) +---------------------- + +一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 +支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 + +### 用法/示例 + +```bash +# 通过 -c 选项 指定 docker container +$ cp-into-docker-run -c container_foo /path/to/command command_args... +# 如果 指定的command 不是一个路径,会从 PATH 中查找 +$ cp-into-docker-run -c container_foo a2l command_arg1 command_arg2 + +# 帮助信息 +$ cp-into-docker-run -h +Usage: cp-into-docker-run [OPTION]... command [command-args]... + +Copy the command into docker container +and run the command in container. + +Example: + cp-into-docker-run -c container_foo command_copied_into_container command_arg1 + +docker options: + -c, --container destination docker container + -u, --docker-user docker username or UID to run command + optional, docker default is (maybe) root user + -w, --workdir working directory inside the container + optional, docker default is (maybe) root dir + -t, --tmpdir tmp dir in docker to copy command + optional, default is /tmp + +run options: + -v, --verbose show operation step infos + +miscellaneous: + -h, --help display this help and exit + -V, --version display version information and exit +``` + @@ -378,13 +419,7 @@ $ rp /home /etc/../etc /home/admin - 是否有攻击,查看`SYN_RECV`数(`SYN`攻击) - `TIME_WAIT`数,太多会导致`TCP: time wait bucket table overflow`。 -### 用法 - -```bash -tcp-connection-state-counter -``` - -### 示例 +### 用法/示例 ```bash $ tcp-connection-state-counter @@ -406,11 +441,11 @@ SYN_SENT 7 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 - `xpl`:在文件浏览器中打开指定的文件或文件夹。 - `xpl`是`explorer`的缩写。 + `xpl`是`explorer`的缩写。 - `xpf`: 在文件浏览器中打开指定的文件或文件夹,并选中。 - `xpf`是`explorer and select file`的缩写。 + `xpf`是`explorer and select file`的缩写。 -### 用法 +### 用法/示例 ```bash xpl @@ -422,11 +457,9 @@ xpf # 缺省打开当前目录 xpf <文件或是目录>... # 打开多个文件或目录 -``` -### 示例 -```bash +# 示例 xpl /path/to/dir xpl /path/to/foo.txt xpl /path/to/dir1 /path/to/foo1.txt @@ -452,7 +485,7 @@ xpf /path/to/dir1 /path/to/foo1.txt 这个脚本输出脚本收到的参数。在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。 -### 示例 +### 用法/示例 ```bash $ ./echo-args 1 " 2 foo " "3 3" @@ -507,7 +540,8 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" ### 参考资料 -- [utensil](https://github.com/utensil) 的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! +- [utensil](https://github.com/utensil) + 的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! 🍺 [parseOpts.sh](../lib/parseOpts.sh) ---------------------- @@ -537,7 +571,7 @@ find file: bar.txt - `-`: 无参数的选项。即有选项则把值设置成`true`。这是 ***缺省*** 的类型。 - `:`: 有参数的选项,值只有一个。 - `+`: 有多个参数值的选项。值列表要以`;`表示结束。 - 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。 + 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。 实际要解析的输入参数往往是你的脚本参数,这样`parseOpts`函数调用一般是: @@ -550,7 +584,7 @@ parseOpts "a,a-long|b,b-long:|c,c-long+" "$@" - 选项名为`a`,通过全局变量`_OPT_VALUE_a`来获取选项的值。 - 选项名为`a-long`,通过全局变量`_OPT_VALUE_a_long`来获取选项的值。 - 即,把选项名的`-`转`_`,再加上前缀`_OPT_VALUE_`对应的全局变量来获得选项值。 + 即,把选项名的`-`转`_`,再加上前缀`_OPT_VALUE_`对应的全局变量来获得选项值。 - 除了选项剩下的参数,通过全局变量`_OPT_ARGS`来获取。 按照惯例,输入参数中如果有`--`表示之后参数中不再有选项,即之后都是参数。 @@ -594,17 +628,17 @@ parseOpts "a,a-long|b,b-long:|c,c-long+" -a -b bv -- --c-long c.sh -p pv -q qv a 这个脚本比较复杂,测试过的环境有: 1. `bash --version` - `GNU bash, version 4.1.5(1)-release (x86_64-pc-linux-gnu)` - `uname -a` - `Linux foo-host 2.6.32-41-generic #94-Ubuntu SMP Fri Jul 6 18:00:34 UTC 2012 x86_64 GNU/Linux` + `GNU bash, version 4.1.5(1)-release (x86_64-pc-linux-gnu)` + `uname -a` + `Linux foo-host 2.6.32-41-generic #94-Ubuntu SMP Fri Jul 6 18:00:34 UTC 2012 x86_64 GNU/Linux` 1. `bash --version` - `GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)` - `uname -a` - `Darwin foo-host 14.0.0 Darwin Kernel Version 14.0.0: Fri Sep 19 00:26:44 PDT 2014; root:xnu-2782.1.97~2/RELEASE_X86_64 x86_64 i386 MacBookPro10,1 Darwin` + `GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)` + `uname -a` + `Darwin foo-host 14.0.0 Darwin Kernel Version 14.0.0: Fri Sep 19 00:26:44 PDT 2014; root:xnu-2782.1.97~2/RELEASE_X86_64 x86_64 i386 MacBookPro10,1 Darwin` 1. `bash --version` - `GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)` - `uname -a` - `Linux foo-host 2.6.9-103.ELxenU #1 SMP Wed Mar 14 16:31:15 CST 2012 i686 i686 i386 GNU/Linux` + `GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)` + `uname -a` + `Linux foo-host 2.6.9-103.ELxenU #1 SMP Wed Mar 14 16:31:15 CST 2012 i686 i686 i386 GNU/Linux` ### 贡献者 From 6db27f78d0e1686f96255d63910821e7626bfd3a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 26 Apr 2021 16:59:22 +0800 Subject: [PATCH 071/175] + cp-into-docker-run: add -p option --- README.md | 2 ++ bin/cp-into-docker-run | 35 ++++++++++++++++++++++++++++------- docs/shell.md | 9 ++++++--- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 78bdc993..64716414 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 1. [ap and rp](docs/shell.md#-ap-and-rp) 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 +1. [cp-into-docker-run](docs/shell.md#-cp-into-docker-run) + 一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 1. [tcp-connection-state-counter](docs/shell.md#-tcp-connection-state-counter) 统计各个`TCP`连接状态的个数。用于方便排查系统连接负荷问题。 1. [xpl and xpf](docs/shell.md#-xpl-and-xpf) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index b278e05c..1c799cd0 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -49,10 +49,13 @@ docker options: -c, --container destination docker container -u, --docker-user docker username or UID to run command optional, docker default is (maybe) root user - -w, --workdir working directory inside the container + -w, --workdir absolute working directory inside the container optional, docker default is (maybe) root dir -t, --tmpdir tmp dir in docker to copy command optional, default is /tmp + -p, --cp-path destination path in docker of the command(including file name) + if specified, command will be kept when run finished + optional, default is under tmp dir and deleted when run finished run options: -v, --verbose show operation step infos @@ -78,6 +81,7 @@ container_name= docker_user= docker_workdir= docker_tmpdir=/tmp +docker_command_cp_path= verbose=false declare -a args=() @@ -99,6 +103,10 @@ while (($# > 0)); do docker_tmpdir="$2" shift 2 ;; + -p | --cp-path) + docker_command_cp_path="$2" + shift 2 + ;; -v | --verbose) verbose=true shift @@ -125,6 +133,11 @@ while (($# > 0)); do esac done +if [ -n "${docker_command_cp_path:-}" ]; then + [[ "$docker_command_cp_path" =~ ^/ ]] || + die "the command path in docker to copy must be absolute path: $docker_command_cp_path" +fi + ################################################################################ # biz logic ################################################################################ @@ -148,17 +161,25 @@ if [ ! -f "$specified_run_command" ]; then run_command="$(which "$specified_run_command")" fi run_command="$($READLINK_CMD -f "$run_command")" +run_command_base_name="$(basename "$run_command")" -run_timestamp="$(date "+%Y%m%d_%H%M%S")" -readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" +if [ -n "${docker_command_cp_path:-}" ]; then + run_command_dir_in_docker="$(dirname "$docker_command_cp_path")" + readonly run_command_in_docker="$docker_command_cp_path" +else + run_timestamp="$(date "+%Y%m%d_%H%M%S")" + readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" + readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" -run_command_base_name="$(basename "$run_command")" -readonly run_command_dir_in_docker="$docker_tmpdir/$uuid" -readonly run_command_in_docker="$run_command_dir_in_docker/$run_command_base_name" + readonly run_command_dir_in_docker="$work_tmp_dir_in_docker" + readonly run_command_in_docker="$run_command_dir_in_docker/$run_command_base_name" +fi cleanupWhenExit() { + [ -n "${work_tmp_dir_in_docker:-}" ] || return 0 + # remove tmp dir in docker by root user - docker exec "${container_name}" rm -rf -- "$run_command_dir_in_docker" &>/dev/null + docker exec "${container_name}" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null } trap "cleanupWhenExit" EXIT diff --git a/docs/shell.md b/docs/shell.md index d651ab13..f3e5a49a 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -391,10 +391,13 @@ docker options: -c, --container destination docker container -u, --docker-user docker username or UID to run command optional, docker default is (maybe) root user - -w, --workdir working directory inside the container + -w, --workdir absolute working directory inside the container optional, docker default is (maybe) root dir -t, --tmpdir tmp dir in docker to copy command optional, default is /tmp + -p, --cp-path destination path in docker of the command(including file name) + if specified, command will be kept when run finished + optional, default is under tmp dir and deleted when run finished run options: -v, --verbose show operation step infos @@ -432,7 +435,7 @@ SYN_SENT 7 ### 贡献者 -[sunuslee](https://github.com/sunuslee)改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56) +[sunuslee](https://github.com/sunuslee) 改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56) 🍺 [xpl](../bin/xpl) and [xpf](../bin/xpf) ---------------------- @@ -536,7 +539,7 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" ### 贡献者 -[姜太公](https://github.com/jzwlqx)提供循环输出彩色组合的脚本。 +[姜太公](https://github.com/jzwlqx) 提供循环输出彩色组合的脚本。 ### 参考资料 From 1cc41aa6752481857df084c6709a20f52ce7b141 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 27 Apr 2021 16:29:31 +0800 Subject: [PATCH 072/175] ! cp-into-docker-run: check docker workdir option --- bin/cp-into-docker-run | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 1c799cd0..aaed11bd 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -1,4 +1,12 @@ #!/bin/bash +# @Function +# Copy the command into docker container and run the command in container. +# +# Example: +# ${PROG} -c container_foo command_copied_into_container command_arg1 +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-cp-into-docker-run +# @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail PROG="$(basename "$0")" @@ -23,6 +31,10 @@ redEcho() { [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } +isAbsolutePath() { + [[ "$1" =~ ^/ ]] +} + die() { redEcho "Error: $*" 1>&2 exit 1 @@ -133,9 +145,12 @@ while (($# > 0)); do esac done -if [ -n "${docker_command_cp_path:-}" ]; then - [[ "$docker_command_cp_path" =~ ^/ ]] || - die "the command path in docker to copy must be absolute path: $docker_command_cp_path" +if [ -n "${docker_workdir}" ]; then + isAbsolutePath "$docker_workdir" || + die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" +elif [ -n "${docker_command_cp_path}" ]; then + isAbsolutePath "$docker_command_cp_path" || + die "when no docker workdir(-w/--workdir) is specified, the command path in docker to copy(-p/--cp-path) must be absolute path: $docker_command_cp_path" fi ################################################################################ @@ -163,16 +178,20 @@ fi run_command="$($READLINK_CMD -f "$run_command")" run_command_base_name="$(basename "$run_command")" -if [ -n "${docker_command_cp_path:-}" ]; then - run_command_dir_in_docker="$(dirname "$docker_command_cp_path")" - readonly run_command_in_docker="$docker_command_cp_path" +if [ -n "${docker_command_cp_path}" ]; then + if isAbsolutePath "$docker_command_cp_path"; then + readonly run_command_in_docker="$docker_command_cp_path" + else + readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" + fi + run_command_dir_in_docker="$(dirname "$run_command_in_docker")" else run_timestamp="$(date "+%Y%m%d_%H%M%S")" readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" + readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" readonly run_command_dir_in_docker="$work_tmp_dir_in_docker" - readonly run_command_in_docker="$run_command_dir_in_docker/$run_command_base_name" fi cleanupWhenExit() { From 3c042ba605150e5af8aae3e29ae105ac9c9eb095 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 26 Apr 2021 15:24:12 +0800 Subject: [PATCH 073/175] ! update after release v2.4.0 --- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 2 +- bin/cp-into-docker-run | 2 +- bin/find-in-jars | 2 +- bin/rp | 2 +- bin/show-busy-java-threads | 2 +- bin/show-duplicate-java-classes | 2 +- bin/tcp-connection-state-counter | 2 +- bin/uq | 2 +- bin/xpl | 2 +- legacy-bin/cp-svn-url | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/a2l b/bin/a2l index fc65b385..5cc17528 100755 --- a/bin/a2l +++ b/bin/a2l @@ -12,7 +12,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/ap b/bin/ap index 1cd59f22..ef02f81a 100755 --- a/bin/ap +++ b/bin/ap @@ -14,7 +14,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' READLINK_CMD=readlink if command -v greadlink >/dev/null; then diff --git a/bin/c b/bin/c index 376158f4..21f76777 100755 --- a/bin/c +++ b/bin/c @@ -22,7 +22,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index aaed11bd..75151b73 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -10,7 +10,7 @@ set -eEuo pipefail PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' READLINK_CMD=readlink if command -v greadlink >/dev/null; then diff --git a/bin/find-in-jars b/bin/find-in-jars index 5c456303..63be54a3 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -27,7 +27,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/rp b/bin/rp index 9ae755d0..f0f70eee 100755 --- a/bin/rp +++ b/bin/rp @@ -14,7 +14,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 0d20340c..e437a2d2 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -21,7 +21,7 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 # How can I get the source directory of a Bash script from within the script itself? diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 21ae69c8..70ba316b 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -30,7 +30,7 @@ from zipfile import ZipFile, BadZipfile ################################################################################ # utils functions ################################################################################ -PROG_VERSION = '2.4.0-dev' +PROG_VERSION = '2.5.0-dev' # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index f58991c8..7231320b 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -12,7 +12,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/uq b/bin/uq index b8f771d0..c50ba1b9 100755 --- a/bin/uq +++ b/bin/uq @@ -24,7 +24,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' ################################################################################ # util functions diff --git a/bin/xpl b/bin/xpl index 6c8b87a9..465cadb1 100755 --- a/bin/xpl +++ b/bin/xpl @@ -21,7 +21,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' readonly nl=$'\n' # new line diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index aef54ee2..30bafcb0 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -21,7 +21,7 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" -readonly PROG_VERSION='2.4.0-dev' +readonly PROG_VERSION='2.5.0-dev' usage() { cat < Date: Thu, 23 May 2019 15:01:26 +0800 Subject: [PATCH 074/175] ! improve jstack search order --- bin/show-busy-java-threads | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index e437a2d2..edd31ca5 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -310,19 +310,33 @@ if [ -n "$store_dir" ]; then fi ################################################################################ -# check the existence of jstack command +# search/check the existence of jstack command +# +# search order/priority: +# 1. from -s option +# 2. from under env var JAVA_HOME +# 3. from under env var PATH ################################################################################ if [ -n "$jstack_path" ]; then - [ -f "$jstack_path" ] || die "$jstack_path is NOT found!" - [ -x "$jstack_path" ] || die "$jstack_path is NOT executable!" + # 1. check jstack_path set by -s option + [ -f "$jstack_path" ] || die "$jstack_path (set by -s option) is NOT found!" + [ -x "$jstack_path" ] || die "$jstack_path (set by -s option) is NOT executable!" +elif [ -n "$JAVA_HOME" ]; then + # 2. search jstack under JAVA_HOME + if [ -f "$JAVA_HOME/bin/jstack" ]; then + [ -x "$JAVA_HOME/bin/jstack" ] || die "found \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executable!${nl}Use -s option set jstack path manually." + jstack_path="$JAVA_HOME/bin/jstack" + elif [ -f "$JAVA_HOME/../bin/jstack" ]; then + [ -x "$JAVA_HOME/../bin/jstack" ] || die "found \$JAVA_HOME/../bin/jstack($JAVA_HOME/../bin/jstack) is NOT executable!${nl}Use -s option set jstack path manually." + jstack_path="$JAVA_HOME/../bin/jstack" + fi elif command -v jstack &>/dev/null; then + # 3. search jstack under PATH jstack_path="$(command -v jstack)" + [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${nl}Use -s option set jstack path manually." else - [ -n "$JAVA_HOME" ] || die "jstack not found on PATH and No JAVA_HOME setting! Use -s option set jstack path manually." - [ -f "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) file does NOT exists! Use -s option set jstack path manually." - [ -x "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executable! Use -s option set jstack path manually." - jstack_path="$JAVA_HOME/bin/jstack" + die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." fi ################################################################################ From d447718438c82a01d1d9ea7817efacbe1fb1de2b Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 30 Apr 2021 11:50:07 +0800 Subject: [PATCH 075/175] ! find-in-jars improvement - check list zip entries fail, especially common case "Empty zipfile" - improve command to list zip entries search --- bin/find-in-jars | 75 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index 63be54a3..e9c4af27 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -78,7 +78,7 @@ clearResponsiveMessage() { die() { clearResponsiveMessage - redEcho "Error: $*" 1>&2 + redEcho "Error: $*" >&2 exit 1 } @@ -262,23 +262,62 @@ done # Check the existence of command for listing zip entry! ################################################################################ -# `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 - readonly command_for_list_zip='zipinfo -1' -elif command -v unzip &>/dev/null; then - readonly command_for_list_zip='unzip -Z1' -else - if ! command -v jar &>/dev/null; then - [ -n "$JAVA_HOME" ] || die "jar not found on PATH and JAVA_HOME env var is blank!" - [ -f "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) file does NOT exists!" - [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executable!" - export PATH="$JAVA_HOME/bin:$PATH" +__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 +} +__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 - readonly command_for_list_zip='jar tf' -fi + + "${command_to_list_zip_entries[@]}" "$zip_file" || { + clearResponsiveMessage + redEcho "fail to list zip entries of $zip_file, ignored!" >&2 + return 0 + } +} ################################################################################ # find logic @@ -348,7 +387,7 @@ findInJarFiles() { while read -r jar_file; do printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" - $command_for_list_zip "${jar_file}" | __outputResultOfJarFile "${jar_file}" + listZipEntries "${jar_file}" | __outputResultOfJarFile "${jar_file}" done clearResponsiveMessage From a7c08465fd5d85afdc360a1710f315329df57199 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 30 Apr 2021 14:11:06 +0800 Subject: [PATCH 076/175] ! improve scripts: use portableReadLink --- bin/ap | 35 +++++++++++++++++++++++++++++------ bin/cp-into-docker-run | 36 +++++++++++++++++++++++++++--------- bin/xpf | 27 +++++++++++++++++++++++++-- 3 files changed, 81 insertions(+), 17 deletions(-) diff --git a/bin/ap b/bin/ap index ef02f81a..7db14ae8 100755 --- a/bin/ap +++ b/bin/ap @@ -16,11 +16,6 @@ set -eEuo pipefail PROG="$(basename "$0")" readonly PROG_VERSION='2.5.0-dev' -READLINK_CMD=readlink -if command -v greadlink >/dev/null; then - READLINK_CMD=greadlink -fi - ################################################################################ # util functions ################################################################################ @@ -42,6 +37,34 @@ redEcho() { colorEcho 31 "$@" } +die() { + redEcho "Error: $*" 1>&2 + exit 1 +} + +# How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +portableReadLink() { + local file="$1" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + die "NOT support uname($uname)!" + ;; + esac +} + usage() { local -r exit_code="${1:-0}" (($# > 0)) && shift @@ -108,5 +131,5 @@ for f in "${files[@]}"; do echo "$f does not exists!" continue } - $READLINK_CMD -f "$f" + portableReadLink "$f" done diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 75151b73..09143e4f 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -12,11 +12,6 @@ set -eEuo pipefail PROG="$(basename "$0")" readonly PROG_VERSION='2.5.0-dev' -READLINK_CMD=readlink -if command -v greadlink >/dev/null; then - READLINK_CMD=greadlink -fi - ################################################################################ # util functions ################################################################################ @@ -31,13 +26,36 @@ redEcho() { [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } +die() { + redEcho "Error: $*" 1>&2 + exit 1 +} + isAbsolutePath() { [[ "$1" =~ ^/ ]] } -die() { - redEcho "Error: $*" 1>&2 - exit 1 +# How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +portableReadLink() { + local file="$1" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + die "NOT support uname($uname)!" + ;; + esac } usage() { @@ -175,7 +193,7 @@ if [ ! -f "$specified_run_command" ]; then run_command="$(which "$specified_run_command")" fi -run_command="$($READLINK_CMD -f "$run_command")" +run_command="$(portableReadLink "$run_command")" run_command_base_name="$(basename "$run_command")" if [ -n "${docker_command_cp_path}" ]; then diff --git a/bin/xpf b/bin/xpf index 2b9f5b5a..a1e89c79 100755 --- a/bin/xpf +++ b/bin/xpf @@ -10,6 +10,29 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# BASE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" -BASE="$(dirname "$0")" +# How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +portableReadLink() { + local file="$1" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + echo "not support uname($uname)!" >&2 + exit 1 + ;; + esac +} + +BASE="$(dirname "$(portableReadLink "${BASH_SOURCE[0]}")")" source "$BASE/xpl" "$@" From 2a88bbda3280f6e95b065f11615ec34dd27ad3ca Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 30 Apr 2021 14:14:20 +0800 Subject: [PATCH 077/175] ! fix unbound variable: args[@] --- bin/xpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/xpl b/bin/xpl index 465cadb1..dbe1f13d 100755 --- a/bin/xpl +++ b/bin/xpl @@ -75,14 +75,14 @@ while [ $# -gt 0 ]; do ;; --) shift - args=("${args[@]}" "$@") + args=(${args[@]:+"${args[@]}"} "$@") break ;; -*) usage 2 "${PROG}: unrecognized option '$1'" ;; *) - args=("${args[@]}" "$1") + args=(${args[@]:+"${args[@]}"} "$1") shift ;; esac From 352a288f7a08a70b06e2006d255096b561df65bd Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 1 May 2021 15:23:59 +0800 Subject: [PATCH 078/175] = code cleanup --- bin/cp-into-docker-run | 2 +- bin/show-busy-java-threads | 2 +- legacy-bin/svn-merge-stop-on-copy | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 09143e4f..ac5e5697 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -218,7 +218,7 @@ cleanupWhenExit() { # remove tmp dir in docker by root user docker exec "${container_name}" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null } -trap "cleanupWhenExit" EXIT +trap cleanupWhenExit EXIT ######################################## # docker operations diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index edd31ca5..7ddc41f5 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -358,7 +358,7 @@ mkdir -p "$tmp_store_dir" cleanupWhenExit() { rm -rf "$tmp_store_dir" &>/dev/null } -trap "cleanupWhenExit" EXIT +trap cleanupWhenExit EXIT headInfo() { colorEcho "0;34;42" ================================================================================ diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index cf358383..e4d4cb22 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -61,13 +61,13 @@ target=${2:-.} } } || workDir="$target" -cleanup() { +cleanupWhenExit() { [ "$workDir" != "$target" ] && { echo "rm tmp dir $workDir ." rm -rf "$workDir" } } -trap "cleanup" EXIT +trap cleanupWhenExit EXIT svn_status_line=$(svn status --ignore-externals "$workDir" | grep -c -v ^X) [ "$svn_status_line" -ne 0 ] && { From 51e60b6a477d0b8008b38f0c5f03937811cfdc0e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 1 May 2021 21:14:07 +0800 Subject: [PATCH 079/175] ! improve show-duplicate-java-classes: output duplicate ratio and class number of class path #59 --- bin/show-duplicate-java-classes | 53 +++++++++++++++++++-------------- docs/java.md | 46 ++++++++++++++-------------- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 70ba316b..69e4d20b 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -84,13 +84,17 @@ def print_box_message(msg): print('=' * 80) +def str_len(x): + return len(str(x)) + + def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() - idx_str_max_len = len(str(len(lib_dirs))) + max_idx_str_len = str_len(len(lib_dirs)) for idx, lib_dir in enumerate(lib_dirs, start=1): print_responsive_message('list jar file under lib dir(%*s/%s): %s' % ( - idx_str_max_len, idx, len(lib_dirs), lib_dir)) + max_idx_str_len, idx, len(lib_dirs), lib_dir)) if not exists(lib_dir): print_error('WARN: lib dir %s not exists, ignored!' % lib_dir) @@ -126,7 +130,7 @@ def list_class_under_jar_file(jar_file, recursive, progress): if recursive: index_marker = ' #%3s' % index print_responsive_message('list class under jar file(%*s/%s%s): %s' % ( - len(str(progress[1])), progress[0], progress[1], index_marker, jar_jar_path)) + str_len(progress[1]), progress[0], progress[1], index_marker, jar_jar_path)) ret = {} classes = {f for f in zf.namelist() if f.lower().endswith('.class')} @@ -155,7 +159,7 @@ def list_class_under_jar_file(jar_file, recursive, progress): def list_class_under_class_dir(class_dir, progress): print_responsive_message('list class under class dir(%*s/%s): %s' % ( - len(str(progress[1])), progress[0], progress[1], class_dir)) + str_len(progress[1]), progress[0], progress[1], class_dir)) if not exists(class_dir): print_error('WARN: class dir %s not exists, ignored!' % class_dir) @@ -215,7 +219,7 @@ def find_duplicate_classes(class_to_class_paths): return class_paths_to_duplicate_classes -def print_duplicate_classes_info(class_paths_to_duplicate_classes): +def print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes): if not class_paths_to_duplicate_classes: print('COOL! No duplicate classes found!') return @@ -244,34 +248,37 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes): # use - operator of number key for reverse sort key class_paths_to_duplicate_classes.sort(key=lambda item: (-len(item[0]), -len(item[1]), item[0])) - idx_str_max_len = len(str(len(class_paths_to_duplicate_classes))) + max_idx_str_len = str_len(len(class_paths_to_duplicate_classes)) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): - print('[%*s] found %s duplicate classes in %s class paths:' % ( - idx_str_max_len, idx, len(classes), len(class_paths))) + duplicate_ratio = len(classes) / min((len(class_path_to_classes[cp]) for cp in class_paths)) + print('[%*s] found %s(%.3g%%) duplicate classes in %s class paths:' % ( + max_idx_str_len, idx, len(classes), duplicate_ratio * 100, len(class_paths))) - class_path_idx_str_max_len = len(str(len(class_paths))) + max_class_path_idx_str_len = str_len(len(class_paths)) + max_classes_count_str_len = str_len(max(len(class_path_to_classes[cp]) for cp in class_paths)) for i, cp in enumerate(class_paths, start=1): - print(' %*s: %s' % (class_path_idx_str_max_len, i, cp)) + print(' %*s: (contain %*s classes) %s' % ( + max_class_path_idx_str_len, i, max_classes_count_str_len, len(class_path_to_classes[cp]), cp)) print_box_message('Duplicate classes detail info:') for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): print('[%*s] found %s duplicate classes in %s class paths %s :' % ( - idx_str_max_len, idx, - len(classes), len(class_paths), ' '.join(class_paths))) + max_idx_str_len, idx, len(classes), len(class_paths), ' '.join(class_paths))) - class_idx_str_max_len = len(str(len(classes))) + max_class_idx_str_len = str_len(len(classes)) for i, c in enumerate(classes, start=1): - print(' %*s: %s' % (class_idx_str_max_len, i, c)) - + print(' %*s: %s' % (max_class_idx_str_len, i, c)) -def print_class_paths_info(class_paths): - class_paths = sorted(class_paths) - print_box_message('Find in %s class paths:' % len(class_paths)) +def print_class_paths_info(class_path_to_classes): + max_idx_str_len = str_len(len(class_path_to_classes)) + max_classes_count_str_len = str_len(max(len(classes) for classes in class_path_to_classes.values())) - idx_str_max_len = len(str(len(class_paths))) - for idx, class_path in enumerate(class_paths, start=1): - print('%*s: %s' % (idx_str_max_len, idx, class_path)) + class_path_to_classes = sorted(class_path_to_classes.items(), key=lambda item: item[0]) + print_box_message('Find in %s class paths:' % len(class_path_to_classes)) + for idx, (cp, classes) in enumerate(class_path_to_classes, start=1): + print('%*s: (contain %*s classes) %s' % ( + max_idx_str_len, idx, max_classes_count_str_len, len(classes), cp)) def main(): @@ -311,8 +318,8 @@ def main(): class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) clear_responsive_message() - print_duplicate_classes_info(class_paths_to_duplicate_classes) - print_class_paths_info(class_path_to_classes.keys()) + print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes) + print_class_paths_info(class_path_to_classes) return int(bool(class_paths_to_duplicate_classes)) diff --git a/docs/java.md b/docs/java.md index 57a6acde..22c205be 100644 --- a/docs/java.md +++ b/docs/java.md @@ -356,27 +356,27 @@ COOL! No duplicate classes found! ================================================================================ Find in 150 class paths: ================================================================================ - 1: WEB-INF/lib/aopalliance-1.0.jar - 2: WEB-INF/lib/asm-3.2.jar - 3: WEB-INF/lib/aspectjrt-1.6.1.jar - 4: WEB-INF/lib/aspectjweaver-1.6.6.jar + 1: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar + 2: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar + 3: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar + 4: (contain 687 classes) WEB-INF/lib/cassandra-0.6.1.jar ... $ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib Found 1272 duplicate classes in 345 class paths and 9 class path sets: -[1] found 188 duplicate classes in 3 class paths: - 1: WEB-INF/lib/jdom-2.0.2.jar - 2: WEB-INF/lib/jdom2-2.0.6.jar - 3: WEB-INF/lib/jdom2-2.0.8.jar -[2] found 150 duplicate classes in 2 class paths: - 1: WEB-INF/lib/netty-all-4.0.35.Final.jar - 2: WEB-INF/lib/netty-common-4.1.31.Final.jar -[3] found 148 duplicate classes in 2 class paths: - 1: WEB-INF/lib/netty-all-4.0.35.Final.jar - 2: WEB-INF/lib/netty-handler-4.1.31.Final.jar -[4] found 103 duplicate classes in 2 class paths: - 1: WEB-INF/lib/hessian-3.0.14.bugfix-tae3.jar - 2: WEB-INF/lib/hessian-4.0.38.jar +[1] found 188(100%) duplicate classes in 3 class paths: + 1: (contain 188 classes) WEB-INF/lib/jdom-2.0.2.jar + 2: (contain 195 classes) WEB-INF/lib/jdom2-2.0.6.jar + 3: (contain 195 classes) WEB-INF/lib/jdom2-2.0.8.jar +[2] found 150(33.8%) duplicate classes in 2 class paths: + 1: (contain 1385 classes) WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: (contain 444 classes) WEB-INF/lib/netty-common-4.1.31.Final.jar +[3] found 148(55.4%) duplicate classes in 2 class paths: + 1: (contain 1385 classes) WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: (contain 267 classes) WEB-INF/lib/netty-handler-4.1.31.Final.jar +[4] found 103(82.4%) duplicate classes in 2 class paths: + 1: (contain 125 classes) WEB-INF/lib/hessian-3.0.14.bugfix.jar + 2: (contain 275 classes) WEB-INF/lib/hessian-4.0.38.jar ... ================================================================================ @@ -401,12 +401,12 @@ Duplicate classes detail info: ================================================================================ Find in 232 class paths: ================================================================================ - 1: WEB-INF/classes - 2: WEB-INF/lib/HikariCP-2.7.8.jar - 3: WEB-INF/lib/accessors-smart-1.2.jar - 4: WEB-INF/lib/alimonitor-jmonitor-1.1.3.jar - 5: WEB-INF/lib/aopalliance-1.0.jar - 6: WEB-INF/lib/asm-5.0.4.jar + 1: (contain 42 classes) WEB-INF/classes + 2: (contain 70 classes) WEB-INF/lib/HikariCP-2.7.8.jar + 3: (contain 13 classes) WEB-INF/lib/accessors-smart-1.2.jar + 4: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar + 5: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar + 6: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar ... ``` From 67c977ffb931d7c8f1a9f990e80fff5a68ae87d8 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 3 May 2021 18:23:29 +0800 Subject: [PATCH 080/175] ! update README --- README.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 64716414..24501c37 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ 🐌 useful-scripts ==================================== - +repo-icon [![Build Status](https://img.shields.io/travis/oldratlee/useful-scripts/dev-2.x?logo=travis-ci&logoColor=white)](https://travis-ci.org/oldratlee/useful-scripts) [![GitHub release](https://img.shields.io/github/release/oldratlee/useful-scripts.svg)](https://github.com/oldratlee/useful-scripts/releases) @@ -14,12 +14,17 @@ 👉 把平时有用的手动操作做成脚本,这样可以便捷的使用。 ✨ -有自己用的好的脚本 或是 平时常用但没有写成脚本的功能,欢迎提供([提交Issue](https://github.com/oldratlee/useful-scripts/issues))和分享([Fork后提交代码](https://github.com/oldratlee/useful-scripts/fork))! 💖 -PS: +欢迎 👏💖 -本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 -如果你的公司有部署使用,欢迎使用通过[提交Issue](https://github.com/oldratlee/useful-scripts/issues)告知,方便互相交流反馈~ 💘 +- 建议和提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 贡献和改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork) +- 分享 平时常用但没有写成脚本的功能(即需求、想法),[提交Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 提供 自己的好用脚本,[Fork 后提通过 Pull Request 提供](https://github.com/oldratlee/useful-scripts/fork) + +本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 + +如果你的公司有部署使用,欢迎使用通过 [Issue:who's using | 用户反馈收集](https://github.com/oldratlee/useful-scripts/issues/96) 告知,方便互相交流反馈~ 💘 🔰 快速下载&使用 ---------------------- From ac6cf80627cec7db79ad96578f81cef65931d9a9 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 5 May 2021 20:37:17 +0800 Subject: [PATCH 081/175] ! improve cp-into-docker-run: check require option -c/--container --- bin/cp-into-docker-run | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index ac5e5697..99de0525 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -163,6 +163,9 @@ while (($# > 0)); do esac done +[ -n "$container_name" ] || + usage 1 "No destination docker container name, specified by option -c/--container!" + if [ -n "${docker_workdir}" ]; then isAbsolutePath "$docker_workdir" || die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" From de2e80b95ed93c02b82f0685a51ab1307ad45d4b Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 5 May 2021 21:09:50 +0800 Subject: [PATCH 082/175] ! show-duplicate-java-classes: improve percent output format --- bin/show-duplicate-java-classes | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 69e4d20b..f874864a 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -88,6 +88,32 @@ def str_len(x): return len(str(x)) +# issue 32790: Keep trailing zeros in precision for string format option g - Python tracker +# https://bugs.python.org/issue32790 +def percent_str(num): + """ + Input => Output + 1.4545 / 10 **-1 => 1455% + 1.4545 / 10 ** 0 => 145% + 1.4545 / 10 ** 1 => 14.5% + 1.4545 / 10 ** 2 => 1.45% + 1.4545 / 10 ** 3 => 0.145% + 1.4545 / 10 ** 4 => 0.015% + 1.4545 / 10 ** 5 => 0.001% + 1.4545 / 10 ** 6 => 0.000% + 1.4545 / 10 ** 7 => 0.000% + """ + num = num * 100 + if num >= 100: + return '%.0f%%' % num + elif num >= 10: + return '%.1f%%' % num + elif num >= 1: + return '%.2f%%' % num + else: + return '%.3f%%' % num + + def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() @@ -251,8 +277,8 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to max_idx_str_len = str_len(len(class_paths_to_duplicate_classes)) for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): duplicate_ratio = len(classes) / min((len(class_path_to_classes[cp]) for cp in class_paths)) - print('[%*s] found %s(%.3g%%) duplicate classes in %s class paths:' % ( - max_idx_str_len, idx, len(classes), duplicate_ratio * 100, len(class_paths))) + print('[%*s] found %s(%s) duplicate classes in %s class paths:' % ( + max_idx_str_len, idx, len(classes), percent_str(duplicate_ratio), len(class_paths))) max_class_path_idx_str_len = str_len(len(class_paths)) max_classes_count_str_len = str_len(max(len(class_path_to_classes[cp]) for cp in class_paths)) From d81e89dc1b2761ec54df2900bf523b71133d6bd1 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 7 May 2021 17:30:03 +0800 Subject: [PATCH 083/175] ! fix broken link #98 --- docs/java.md | 2 +- docs/shell.md | 4 ++-- docs/vcs.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/java.md b/docs/java.md index 22c205be..b059db7b 100644 --- a/docs/java.md +++ b/docs/java.md @@ -540,4 +540,4 @@ WEB-INF/lib/javax.servlet-api-3.0.1.jar ### 参考资料 -[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.com/458/tech/shell/find-file-in-jar-zip-files.html) +[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.github.io/458/tech/shell/find-file-in-jar-zip-files.html) diff --git a/docs/shell.md b/docs/shell.md index f3e5a49a..89b37a39 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -54,7 +54,7 @@ 命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便快速键入。 -更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip)。 +更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip)。 ### 用法/示例 @@ -113,7 +113,7 @@ Options: ### 参考资料 -- [拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 +- [拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 - 关于文本文件最后的换行,参见[Why should text files end with a newline?](https://stackoverflow.com/questions/729692) 🍺 [coat](../bin/coat) diff --git a/docs/vcs.md b/docs/vcs.md index 00ea9bf8..3105209b 100644 --- a/docs/vcs.md +++ b/docs/vcs.md @@ -111,4 +111,4 @@ http://www.foo.com/project1/branches/feature1 copied! ### 参考资料 -[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 +[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 From eee600ae1e834f5edc444de6dfe6c2a6ebc7c966 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 18 May 2021 10:11:24 +0800 Subject: [PATCH 084/175] = show-duplicate-java-classes refactor: reorder parameters of collect_class_path_to_classes --- .travis.yml | 4 ++-- bin/show-duplicate-java-classes | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48979086..e5486c34 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ addons: # https://docs.travis-ci.com/user/installing-dependencies/#installing-packages-on-macos jobs: include: + - os: osx + osx_image: xcode11.3 - os: linux dist: precise - os: linux @@ -26,8 +28,6 @@ jobs: dist: bionic - os: linux dist: focal - - os: osx - osx_image: xcode11.3 script: - test-cases/integration-test.sh diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index f874864a..1a83dbdd 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -199,7 +199,7 @@ def list_class_under_class_dir(class_dir, progress): for filename in file_names if filename.lower().endswith('.class')} -def collect_class_path_to_classes(jar_files, class_dirs, recursive_jar): +def collect_class_path_to_classes(class_dirs, jar_files, recursive_jar): class_path_to_classes = {} total_count = len(jar_files) + len(class_dirs) index = 0 @@ -335,9 +335,8 @@ def main(): if not options.class_dirs and not lib_dirs: lib_dirs = ['.'] - class_path_to_classes = collect_class_path_to_classes( - list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib), - options.class_dirs, recursive_jar=options.recursive_jar) + jar_files = list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib) + class_path_to_classes = collect_class_path_to_classes(options.class_dirs, jar_files, options.recursive_jar) print_responsive_message('find duplicate classes...') class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) From 715e6f2a765b16f08502d67fdfae707e0b6433fc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 2 Jul 2021 16:37:11 +0800 Subject: [PATCH 085/175] = remove unused var --- bin/a2l | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/a2l b/bin/a2l index 5cc17528..8d7b9345 100755 --- a/bin/a2l +++ b/bin/a2l @@ -21,7 +21,6 @@ readonly PROG_VERSION='2.5.0-dev' # 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 colorEcho() { local color="$1" From b417c8d46bb110910d3c8ce770d0943cc204b75d Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 4 Jul 2021 17:58:02 +0800 Subject: [PATCH 086/175] ! update travis badge to travis.com --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 24501c37..ed53898b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ repo-icon -[![Build Status](https://img.shields.io/travis/oldratlee/useful-scripts/dev-2.x?logo=travis-ci&logoColor=white)](https://travis-ci.org/oldratlee/useful-scripts) +[![Build Status](https://img.shields.io/travis/com/oldratlee/useful-scripts/dev-2.x?logo=travis-ci&logoColor=white)](https://travis-ci.com/github/oldratlee/useful-scripts) [![GitHub release](https://img.shields.io/github/release/oldratlee/useful-scripts.svg)](https://github.com/oldratlee/useful-scripts/releases) [![License](https://img.shields.io/github/license/oldratlee/useful-scripts?color=4D7A97)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Chat at gitter.im](https://img.shields.io/gitter/room/oldratlee/useful-scripts?color=46BC99&logo=gitter&logoColor=white)](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) From 7f8d4bc3ab2487430785df5c196fcdf8adffe29e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 21 Jul 2021 11:06:01 +0800 Subject: [PATCH 087/175] = improve java.md --- bin/show-busy-java-threads | 2 +- docs/java.md | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 7ddc41f5..5ae1517e 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -139,7 +139,7 @@ Example: Output control: -p, --pid find out the highest cpu consumed threads from - the specified java process. support pid list(eg: 42,99). + the specified java process. support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. diff --git a/docs/java.md b/docs/java.md index b059db7b..d347c8fe 100644 --- a/docs/java.md +++ b/docs/java.md @@ -27,10 +27,10 @@ ------------------------------- -关于`Java`排错与诊断,力荐️`Arthas` ❤️ +关于`Java`排错与诊断,力荐️`Arthas`: ❤️ -- [alibaba/arthas: Alibaba Java诊断利器 - github.com](https://github.com/alibaba/arthas) -- `Arthas`用户文档 https://alibaba.github.io/arthas/ +- `Arthas`用户文档: https://arthas.aliyun.com/doc/quick-start.html +- github repo: [alibaba/arthas: Alibaba Java诊断利器](https://github.com/alibaba/arthas) `Arthas`功能异常(😜)强劲,且在阿里巴巴线上支持使用多年。我自己也常用,一定要看看用用! @@ -51,9 +51,9 @@ ---------------------- 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 -目前只支持`Linux`。原因是`Mac`、`Windows`的`ps`命令不支持列出进程的线程`id`,更多信息参见[#33](https://github.com/oldratlee/useful-scripts/issues/33),欢迎提供解法。 +目前只支持`Linux`。原因是`Mac`、`Windows`的`ps`命令不支持列出进程的线程`id`,更多信息参见 [#33](https://github.com/oldratlee/useful-scripts/issues/33),欢迎提供解法。 -PS,如何操作可以参见[@bluedavy](http://weibo.com/bluedavy)的[《分布式Java应用》](https://book.douban.com/subject/4848587/)的【5.1.1 `CPU`消耗分析】一节,说得很详细: +PS,如何操作可以参见[`@bluedavy`](http://weibo.com/bluedavy)的[《分布式Java应用》](https://book.douban.com/subject/4848587/)的【5.1.1 `CPU`消耗分析】一节,说得很详细: 1. `top`命令找出消耗`CPU`高的`Java`进程及其线程`id`: 1. 开启线程显示模式(`top -H`,或是打开`top`后按`H`) @@ -65,7 +65,8 @@ PS,如何操作可以参见[@bluedavy](http://weibo.com/bluedavy)的[《分布 1. 在`jstack`输出中查找十六进制的线程`id`(可以用`vim`的查找功能`/0x1234`,或是`grep 0x1234 -A 20`) 1. 查看对应的线程栈,分析问题 -查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。 +查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。 +期望整合上面的过程成一个脚本,这样一行命令就可以自动化地搞定。 ### 用法 @@ -74,10 +75,12 @@ show-busy-java-threads # 从所有运行的Java进程中找出最消耗CPU的线程(缺省5个),打印出其线程栈 # 缺省会自动从所有的Java进程中找出最消耗CPU的线程,这样用更方便 -# 当然你可以手动指定要分析的Java进程Id,以保证只会显示你关心的那个Java进程的信息 +# 当然你可以通过 -p 选项 手动指定要分析的Java进程Id,以保证只会显示你关心的那个Java进程的信息 show-busy-java-threads -p <指定的Java进程Id> +show-busy-java-threads -p 42 +show-busy-java-threads -p 42,47 -show-busy-java-threads -c <要显示的线程栈数> +show-busy-java-threads -c <要展示示的线程栈个数> show-busy-java-threads <重复执行的间隔秒数> [<重复执行的次数>] # 多次执行;这2个参数的使用方式类似vmstat命令 @@ -97,14 +100,14 @@ sudo show-busy-java-threads show-busy-java-threads -s <指定jstack命令的全路径> # 对于sudo方式的运行,JAVA_HOME环境变量不能传递给root, -# 而root用户往往没有配置JAVA_HOME且不方便配置, -# 显式指定jstack命令的路径就反而显得更方便了 +# 而root用户往往没有配置JAVA_HOME且不方便配置,不能找到jstack命令。 +# 这时显式指定jstack命令的路径就反而显得更方便了 -# -m选项:执行jstack命令时加上-m选项,显示上Native的栈帧,一般应用排查不需要使用 +# -m 选项:执行jstack命令时加上 -m 选项,显示上Native的栈帧,一般应用排查不需要使用 show-busy-java-threads -m -# -F选项:执行jstack命令时加上 -F 选项(如果直接jstack无响应时,用于强制jstack),一般情况不需要使用 +# -F 选项:执行jstack命令时加上 -F 选项(如果直接jstack无响应时,用于强制jstack),一般情况不需要使用 show-busy-java-threads -F -# -l选项:执行jstack命令时加上 -l 选项,显示上更多相关锁的信息,一般情况不需要使用 +# -l 选项:执行jstack命令时加上 -l 选项,显示上更多相关锁的信息,一般情况不需要使用 # 注意:和 -m -F 选项一起使用时,可能会大大增加jstack操作的耗时 show-busy-java-threads -l @@ -121,7 +124,7 @@ Example: Output control: -p, --pid find out the highest cpu consumed threads from - the specified java process. support pid list(eg: 42,99). + the specified java process. support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. From af6c6a3778db72923b8aba2791286ae2d8ef40a8 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 21 Jul 2021 13:40:06 +0800 Subject: [PATCH 088/175] ! use one option cpu-sample-interval instead of top-delay/use-ps --- bin/show-busy-java-threads | 75 +++++++++++++++++++++++--------------- docs/java.md | 26 ++++++------- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 5ae1517e..532d214a 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -102,6 +102,20 @@ logAndCat() { cat } +# Bash RegEx to check floating point numbers from user input +# https://stackoverflow.com/questions/13790763 +isNonNegativeFloatNumber() { + [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] +} + +isNaturalNumber() { + [[ "$1" =~ ^[+]?[0-9]+$ ]] +} + +isNaturalNumberList() { + [[ "$1" =~ ^([0-9]+)(,[0-9]+)*$ ]] +} + # print calling(quoted) command line which is able to copy and paste to rerun safely # # How to get the complete calling command of a BASH script from inside the script (not just the arguments) @@ -139,7 +153,8 @@ Example: Output control: -p, --pid find out the highest cpu consumed threads from - the specified java process. support pid list(eg: 42,47). + the specified java process. + support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. @@ -156,24 +171,19 @@ Output control: jstack control: -s, --jstack-path specifies the path of jstack command. - -F, --force set jstack to force a thread dump. use when jstack - does not respond (process is hung). - -m, --mix-native-frames set jstack to print both java and native frames - (mixed mode). + -F, --force set jstack to force a thread dump. + use when jstack does not respond (process is hung). + -m, --mix-native-frames set jstack to print both java and + native frames (mixed mode). -l, --lock-info set jstack with long listing. prints additional information about locks. CPU usage calculation control: - -d, --top-delay specifies the delay between top samples. - default is 0.5 (second). get thread cpu percentage - during this delay interval. - more info see top -d option. eg: -d 1 (1 second). - -P, --use-ps use ps command to find busy thread(cpu usage) - instead of top command. - default use top command, because cpu usage of - ps command is expressed as the percentage of - time spent running during the *entire lifetime* - of a process, this is not ideal in general. + -i, --cpu-sample-interval specifies the delay between cpu samples to get + thread cpu usage percentage during this interval. + default is 0.5 (second). + set interval 0 to get the percentage of time spent + running during the *entire lifetime* of a process. Miscellaneous: -h, --help display this help and exit. @@ -202,8 +212,8 @@ uname | grep '^Linux' -q || die "$PROG only support Linux, not support $(uname) # readonly declaration make exit code of assignment to be always 0, aka. the exit code of `getopt` in subshell is discarded. # tested on bash 4.2.46 ARGS=$( - getopt -n "$PROG" -a -o c:p:a:s:S:Pd:FmlhV \ - -l count:,pid:,append-file:,jstack-path:,store-dir:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ + getopt -n "$PROG" -a -o c:p:a:s:S:i:Pd:FmlhV \ + -l count:,pid:,append-file:,jstack-path:,store-dir:,cpu-sample-interval:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ -- "$@" ) || { echo @@ -212,8 +222,7 @@ ARGS=$( eval set -- "${ARGS}" count=5 -use_ps=false -top_delay=0.5 +cpu_sample_interval=0.5 while true; do case "$1" in @@ -237,12 +246,14 @@ while true; do store_dir="$2" shift 2 ;; + # support the option name -P,--use-ps for compatibility -P | --use-ps) - use_ps=true + cpu_sample_interval=0 shift ;; - -d | --top-delay) - top_delay="$2" + # support the option name -d,--top-delay for compatibility + -i | --cpu-sample-interval | -d | --top-delay) + cpu_sample_interval="$2" shift 2 ;; -F | --force) @@ -271,16 +282,14 @@ while true; do done update_delay=${1:-0} -# Bash RegEx to check floating point numbers from user input -# https://stackoverflow.com/questions/13790763 -[[ "$update_delay" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] || die "update delay($update_delay) is not a positive float number!" +isNonNegativeFloatNumber "$update_delay" || die "update delay($update_delay) is not a non-negative float number!" [ -z "$1" ] && update_count=1 || update_count=${2:-0} -[[ "$update_count" =~ ^[+]?[0-9]+$ ]] || die "update count($update_count) is not a natural number!" +isNaturalNumber "$update_count" || die "update count($update_count) is not a natural number!" if [ -n "$pid_list" ]; then pid_list="${pid_list//[[:space:]]/}" # delete white space - [[ "$pid_list" =~ ^([0-9]+)(,[0-9]+)*$ ]] || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" + isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" fi # check the directory of append-file(-a) mode, create if not exist. @@ -309,6 +318,8 @@ if [ -n "$store_dir" ]; then fi fi +isNonNegativeFloatNumber "$cpu_sample_interval" || die "cpu sample interval($cpu_sample_interval) is not a non-negative float number!" + ################################################################################ # search/check the existence of jstack command # @@ -383,6 +394,12 @@ __die_when_no_java_process_found() { # output field: pid, thread id(lwp), pcpu, user # order by pcpu(percentage of cpu usage) +# +# NOTE: +# use ps command to find busy thread(cpu usage) +# cpu usage of ps command is expressed as +# the percentage of time spent running during the *entire lifetime* of a process, +# this is not ideal in general. findBusyJavaThreadsByPs() { # 1. sort by %cpu by ps option `--sort -pcpu` # 2. use wide output(unlimited width) by ps option `-ww` @@ -430,7 +447,7 @@ __top_threadId_cpu() { # and use second time update data to get cpu percentage of thread in 0.5 second interval # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! - local -a top_cmd_line=(top -H -b -d "$top_delay" -n 2 -p "$java_pid_list") + local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. local top_out top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") @@ -556,7 +573,7 @@ main() { tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null ((update_count != 1)) && headInfo - if $use_ps; then + if [ "$cpu_sample_interval" == 0 ]; then findBusyJavaThreadsByPs else findBusyJavaThreadsByTop diff --git a/docs/java.md b/docs/java.md index d347c8fe..96ab98c1 100644 --- a/docs/java.md +++ b/docs/java.md @@ -124,7 +124,8 @@ Example: Output control: -p, --pid find out the highest cpu consumed threads from - the specified java process. support pid list(eg: 42,47). + the specified java process. + support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. @@ -141,24 +142,19 @@ Output control: jstack control: -s, --jstack-path specifies the path of jstack command. - -F, --force set jstack to force a thread dump. use when jstack - does not respond (process is hung). - -m, --mix-native-frames set jstack to print both java and native frames - (mixed mode). + -F, --force set jstack to force a thread dump. + use when jstack does not respond (process is hung). + -m, --mix-native-frames set jstack to print both java and + native frames (mixed mode). -l, --lock-info set jstack with long listing. prints additional information about locks. CPU usage calculation control: - -d, --top-delay specifies the delay between top samples. - default is 0.5 (second). get thread cpu percentage - during this delay interval. - more info see top -d option. eg: -d 1 (1 second). - -P, --use-ps use ps command to find busy thread(cpu usage) - instead of top command. - default use top command, because cpu usage of - ps command is expressed as the percentage of - time spent running during the *entire lifetime* - of a process, this is not ideal in general. + -i, --cpu-sample-interval specifies the delay between cpu samples to get + thread cpu usage percentage during this interval. + default is 0.5 (second). + set interval 0 to get the percentage of time spent + running during the *entire lifetime* of a process. Miscellaneous: -h, --help display this help and exit. From c116327092ebeaf11255997c7e724d658b5fef8e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 21 Jul 2021 14:23:59 +0800 Subject: [PATCH 089/175] ! show-busy-java-threads: fix wrong CPU percentage order by ps from procps-ng 3.3.12 --- bin/show-busy-java-threads | 13 ++++++++++--- docs/java.md | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 532d214a..c1fdcf24 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -402,18 +402,25 @@ __die_when_no_java_process_found() { # this is not ideal in general. findBusyJavaThreadsByPs() { # 1. sort by %cpu by ps option `--sort -pcpu` + # unfortunately, ps from `procps-ng 3.3.12`, `--sort` does not work properly with other options, + # use + # ps + # combined + # sort -k3,3nr + # instead of + # ps --sort -pcpu # 2. use wide output(unlimited width) by ps option `-ww` # avoid trunk user column to username_fo+ or $uid alike # shellcheck disable=SC2206 - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --sort -pcpu --no-headers) + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. local ps_out - ps_out="$("${ps_cmd_line[@]}")" + ps_out="$("${ps_cmd_line[@]}" | sort -k3,3nr)" [ -n "$ps_out" ] || __die_when_no_java_process_found if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[@]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" fi if ((count > 0)); then diff --git a/docs/java.md b/docs/java.md index 96ab98c1..2c3ad0b8 100644 --- a/docs/java.md +++ b/docs/java.md @@ -30,7 +30,7 @@ 关于`Java`排错与诊断,力荐️`Arthas`: ❤️ - `Arthas`用户文档: https://arthas.aliyun.com/doc/quick-start.html -- github repo: [alibaba/arthas: Alibaba Java诊断利器](https://github.com/alibaba/arthas) +- GitHub Repo: [alibaba/arthas: Alibaba Java诊断利器](https://github.com/alibaba/arthas) `Arthas`功能异常(😜)强劲,且在阿里巴巴线上支持使用多年。我自己也常用,一定要看看用用! @@ -324,7 +324,7 @@ $ show-duplicate-java-classes -c target/war/WEB-INF/classes target/war/WEB-INF/l 在`App`的`build.gradle`中添加拷贝库到目录`build/dependencies`下。 -```java +```groovy task copyDependencies(type: Copy) { def dest = new File(buildDir, "dependencies") From f411b6519816717981510ca697ab27911ee60b00 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 17 Aug 2021 14:11:16 +0800 Subject: [PATCH 090/175] = improve Markdown docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix typo - 即有 -> 既有 - 便捷的使用 -> 便捷地使用 - http -> https --- README.md | 2 +- docs/shell.md | 6 +++--- docs/vcs.md | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ed53898b..a9647737 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![GitHub issues](https://img.shields.io/github/issues/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/issues) [![GitHub Contributors](https://img.shields.io/github/contributors/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/graphs/contributors) -👉 把平时有用的手动操作做成脚本,这样可以便捷的使用。 ✨ +👉 把平时有用的手动操作做成脚本,以便捷地使用。 ✨ 欢迎 👏💖 diff --git a/docs/shell.md b/docs/shell.md index 89b37a39..36bdb400 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -170,8 +170,8 @@ Examples: cat f - g Output f's contents, then standard input, then g's contents. cat Copy standard input to standard output. -GNU coreutils online help: -Full documentation at: +GNU coreutils online help: +Full documentation at: or available locally via: info '(coreutils) cat invocation' ``` @@ -571,7 +571,7 @@ find file: bar.txt 选项说明最后可以有选项类型说明: -- `-`: 无参数的选项。即有选项则把值设置成`true`。这是 ***缺省*** 的类型。 +- `-`: 无参数的选项。既有选项则把值设置成`true`。这是 ***缺省*** 的类型。 - `:`: 有参数的选项,值只有一个。 - `+`: 有多个参数值的选项。值列表要以`;`表示结束。 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。 diff --git a/docs/vcs.md b/docs/vcs.md index 3105209b..0ca26976 100644 --- a/docs/vcs.md +++ b/docs/vcs.md @@ -46,17 +46,17 @@ swtrunk path/to/svn/work/directory1 /path/to/svn/work/directory2 # svn工作目 ```bash $ swtrunk # -svn work dir . switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir . switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! $ swtrunk /path/to/svn/work/dir # -svn work dir /path/to/svn/work/dir switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir /path/to/svn/work/dir switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! $ swtrunk /path/to/svn/work/dir1 /path/to/svn/work/dir2 # -svn work dir /path/to/svn/work/dir1 switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir /path/to/svn/work/dir1 switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! # -svn work dir /path/to/svn/work/dir2 switch from http://www.foo.com/project2/branches/feature1 to http://www.foo.com/project2/trunk ! +svn work dir /path/to/svn/work/dir2 switch from https://www.foo.com/project2/branches/feature1 to https://www.foo.com/project2/trunk ! ``` 🍺 [svn-merge-stop-on-copy](../legacy-bin/svn-merge-stop-on-copy) @@ -76,9 +76,9 @@ svn-merge-stop-on-copy <来源的远程分支> <目标远程分支> ### 示例 ```bash -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 # 缺省使用当前目录作为svn工作目录 -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 /path/to/svn/work/directory -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 # 缺省使用当前目录作为svn工作目录 +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 /path/to/svn/work/directory +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 https://www.foo.com/project1/branches/feature2 ``` ### 贡献者 @@ -102,7 +102,7 @@ cp-svn-url /path/to/svn/work/directory ```bash $ cp-svn-url -http://www.foo.com/project1/branches/feature1 copied! +https://www.foo.com/project1/branches/feature1 copied! ``` ### 贡献者 From 1b72352b1d49570274c845045e5a25eef8141c0d Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 17 Aug 2021 14:45:17 +0800 Subject: [PATCH 091/175] = improve scripts - use `$*` in string instead of `$@` - improve comments --- bin/find-in-jars | 2 +- bin/show-busy-java-threads | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index e9c4af27..786ef478 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -189,7 +189,7 @@ while (($# > 0)); do use_absolute_path=true shift ;; - # support the typo option name --seperator for compatibility + # support the legacy typo option name --seperator for compatibility -s | --separator | --seperator) separator="$2" shift 2 diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index c1fdcf24..dc02237d 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -246,12 +246,12 @@ while true; do store_dir="$2" shift 2 ;; - # support the option name -P,--use-ps for compatibility + # support the legacy option name -P,--use-ps for compatibility -P | --use-ps) cpu_sample_interval=0 shift ;; - # support the option name -d,--top-delay for compatibility + # support the legacy option name -d,--top-delay for compatibility -i | --cpu-sample-interval | -d | --top-delay) cpu_sample_interval="$2" shift 2 @@ -420,7 +420,7 @@ findBusyJavaThreadsByPs() { [ -n "$ps_out" ] || __die_when_no_java_process_found if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" + echo "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" fi if ((count > 0)); then From cc0d87e84a8efbe46898f0284d4b416b3239f580 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 17 Aug 2021 14:57:42 +0800 Subject: [PATCH 092/175] + coat/a2l: skip color for white space lines --- bin/a2l | 13 ++++++++++++- bin/coat | 15 +++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/bin/a2l b/bin/a2l index 8d7b9345..7202ed7e 100755 --- a/bin/a2l +++ b/bin/a2l @@ -93,7 +93,18 @@ done readonly -a ECHO_COLORS=(33 35 36 31 32 37 34) COUNT=0 +rotateColorEcho() { + local message="$*" + + # skip color for white space + if [[ "$message" =~ ^[[:space:]]*$ ]]; then + echo "$message" + else + local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" + colorEcho "$color" "$*" + fi +} for a in ${args[@]:+"${args[@]}"}; do - colorEcho "${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" "$a" + rotateColorEcho "$a" done diff --git a/bin/coat b/bin/coat index a82e1b4e..2fcf2fa3 100755 --- a/bin/coat +++ b/bin/coat @@ -21,13 +21,20 @@ readonly eend=$'\033[0m' # escape end readonly -a ECHO_COLORS=(33 35 36 31 32 37 34) COUNT=0 -colorEcho() { - local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" - echo "${ec}[1;${color}m$*$eend" +rotateColorEcho() { + local message="$*" + + # skip color for white space + if [[ "$message" =~ ^[[:space:]]*$ ]]; then + echo "$message" + else + local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" + echo "${ec}[1;${color}m$message$eend" + fi } # Bash read line does not read leading spaces # https://stackoverflow.com/questions/29689172 cat "$@" | while IFS= read -r line; do - colorEcho "$line" + rotateColorEcho "$line" done From e500efb0872e99c9daeec32567c2be44828c255f Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 28 Aug 2021 23:42:07 +0800 Subject: [PATCH 093/175] + add dev guide #64 --- README.md | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/README.md b/README.md index a9647737..0b08a13b 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,111 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r 目前`VCS`的脚本都是`svn`分支相关的操作。使用更现代的`Git`吧! 💥 因为不推荐使用`svn`,这里不再列出有哪些脚本了,如果你有兴趣可以点上面链接去看。 + +## 🎓 Developer Guide + +为用户提供有用的功能当然是这个库的首要的价值体现和存在理由。 + +但作为一个**开源**项目,每个人都可以看到源码实现的,这个库或许能做得更多。 + +### 🎯 面向开发者的目标 + +- 期望体现`Shell/Bash`脚本生产环境级的严谨开发方式与最佳实践,进而有可能改善大家在生产环境中`Shell`脚本的质量状况。 +- 将`Shell/Bash`看作线上生产环境可使用的专业语言。 + +PS: + +- 虽然上面是自己期望的目标,但自己在`Shell`语言上一定会有很多理解和使用上的问题,这些实现脚本中也会很多需要的改进,可以一起学习、讨论与实践~ 💕 +- 这个库脚本的实现 也使用了`Python`。 + +#### 关于`Shell`脚本 + +命令行(`CLI`)几乎是每个程序员几乎每天都要使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。 + +命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常常会看到`Shell`脚本(虽然不如主流语言不那么常见)。 + +可能正因为上面所说的`Shell`脚本的便利性和大众性,`Shell`脚本的实现常常质量不高;包含生产环境的`Shell`脚本,不少是顺手实现的。 + +### 🚦 开发约定 + +这个库里,`Shell`脚本都使用`Bash 3+`。 + +原因是: + +- 个人系统学习过的是`Bash`,比较理解熟悉。 +- `Bash`目前还是`Shell`编程主流。 + PS: 虽然交互`Shell`个人已经使用`Zsh`,但严谨的`Shell`脚本开发时还是使用`Bash`。 + +### 📚 `Shell`学习与开发的相关资料 + +- 👷 **`Bash/Shell`最佳实践与安全编程** + - **_`Google Shell Style Guide`_** + - https://google.github.io/styleguide/shell.xml + - 中文版 https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/ + - 编写可靠shell脚本的八个建议 + https://www.xshell.net/shell/1577.html + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝 + - http://kodango.com/bash-pitfalls-part-1 + - http://kodango.com/bash-pitfalls-part-2 + - http://kodango.com/bash-pitfalls-part-3 + - http://kodango.com/bash-pitfalls-part-4 + - Shell 编码风格 - 团子的小窝 + http://kodango.com/shell-script-style + - Bash 优良编程实践 + http://blog.jobbole.com/93447/ + - 不要自己去指定sh的方式去执行脚本 + https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965 + - Use the Unofficial Bash Strict Mode (Unless You Looove Debugging) + http://redsymbol.net/articles/unofficial-bash-strict-mode/ + - koalaman/shellcheck: ShellCheck, a static analysis tool for shell scripts + https://github.com/koalaman/shellcheck +- 🎶 **Tips** + - 让你提升命令行效率的 Bash 快捷键 【完整版】 + https://linuxtoy.org/archives/bash-shortcuts.html + 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 + - 应该知道的Linux技巧 | | 酷 壳 - CoolShell + https://coolshell.cn/articles/8883.html + - 简洁的 Bash Programming 技巧 - 团子的小窝 + - http://kodango.com/simple-bash-programming-skills + - http://kodango.com/simple-bash-programming-skills-2 + - http://kodango.com/simple-bash-programming-skills-3 + - Bash 测试和比较函数 — `test`、`[`、`[[`、`((`、和 `if-then-else` 解密 + https://www.ibm.com/developerworks/cn/linux/l-bash-test.html + - Filenames and Pathnames in Shell (bash, dash, ash, ksh, and so on): How to do it Correctly + https://dwheeler.com/essays/filenames-in-shell.html + - 理解 IFS - 团子的小窝 + http://kodango.com/understand-ifs + - shell中的IFS详解 – 笑遍世界 + http://smilejay.com/2011/12/bash_ifs/ + - Bash脚本:怎样一行行地读文件(最好和最坏的方法) + http://blog.jobbole.com/72185/ + - Shell 脚本避免多次重复 source - 团子的小窝 + http://kodango.com/avoid-repeated-source-in-shell + - 一个奇怪的 echo 结果 - 团子的小窝 + http://kodango.com/a-strange-echo-result + - 浅谈 Shell 脚本配置文件格式 - 团子的小窝 + http://kodango.com/config-file-format-in-shell + - Bash function 还能这么玩 - 团子的小窝 + http://kodango.com/bash-functions + - Bash 获取当前函数名 - 团子的小窝 + http://kodango.com/get-function-name-in-bash + - Zsh和Bash,究竟有何不同 坑很深 + https://www.xshell.net/shell/bash_zsh.html +- 💎 **系统学习** + 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! + - [Bash Pocket Reference](https://book.douban.com/subject/26738258/) + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且更新到了新版的`Bash 4` + - [学习bash](https://book.douban.com/subject/1241361/) + 上面那本的展开版 + - Bash的官方手册(`man`的中文译文) + http://ahei.info/chinese-bash-man.htm + - Bash Reference Manual http://www.gnu.org/software/bash/manual/ + 官方文档,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [命令行的艺术- github.com/jlevy/the-art-of-command-line](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - wzb56/13_questions_of_shell: shell十三问--shell教程 https://github.com/wzb56/13_questions_of_shell + - 信息来自 https://github.com/KeKe-Li/book#linux + - alebcay/awesome-shell: A curated list of awesome command-line frameworks, toolkits, guides and gizmos. Inspired by awesome-php. + https://github.com/alebcay/awesome-shell + - 实用 Shell 文档 - 团子的小窝 + http://kodango.com/useful-documents-about-shell + - 更多参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) From 330c1873f83acce70795c8b8a0d5de67b469f8ec Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 29 Aug 2021 00:10:26 +0800 Subject: [PATCH 094/175] ! split developer-guide.md --- README.md | 106 ++++++++++++---------------------------- docs/developer-guide.md | 41 ++++++++++++++++ 2 files changed, 71 insertions(+), 76 deletions(-) create mode 100644 docs/developer-guide.md diff --git a/README.md b/README.md index 0b08a13b..1df867d9 100644 --- a/README.md +++ b/README.md @@ -87,106 +87,60 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r 为用户提供有用的功能当然是这个库的首要的价值体现和存在理由。 -但作为一个**开源**项目,每个人都可以看到源码实现的,这个库或许能做得更多。 +但作为一个**开源**项目,每个人都可以看到源码实现,这个库或许能做得更多。 ### 🎯 面向开发者的目标 -- 期望体现`Shell/Bash`脚本生产环境级的严谨开发方式与最佳实践,进而有可能改善大家在生产环境中`Shell`脚本的质量状况。 +- 期望体现`Shell/Bash`脚本生产环境级的严谨开发方式与最佳实践,进而有可能示例改善在生产环境中`Shell`脚本的质量状况。 - 将`Shell/Bash`看作线上生产环境可使用的专业语言。 PS: -- 虽然上面是自己期望的目标,但自己在`Shell`语言上一定会有很多理解和使用上的问题,这些实现脚本中也会很多需要的改进,可以一起学习、讨论与实践~ 💕 -- 这个库脚本的实现 也使用了`Python`。 +- 虽然上面是自己期望的目标,但自己在`Shell`语言上一定会有很多理解和使用上的问题、在这些实现脚本中也会很多需要的改进,可以一起学习、讨论与实践~ 💕 +- 这个库中脚本的实现也有使用`Python`。 #### 关于`Shell`脚本 命令行(`CLI`)几乎是每个程序员几乎每天都要使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。 -命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常常会看到`Shell`脚本(虽然不如主流语言不那么常见)。 +命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常会看到`Shell`脚本(虽然不如主流语言那么常见)。 -可能正因为上面所说的`Shell`脚本的便利性和大众性,`Shell`脚本的实现常常质量不高;包含生产环境的`Shell`脚本,不少是顺手实现的。 +可能正因为上面所说的`Shell`脚本的便利性和大众性,`Shell`脚本有不少是顺手实现的(包括生产环境用的`Shell`脚本),`Shell`脚本的实现常常可能质量不高。 ### 🚦 开发约定 -这个库里,`Shell`脚本都使用`Bash 3+`。 +这个库里,`Shell`脚本统一使用`Bash 3+`。 原因是: - 个人系统学习过的是`Bash`,比较理解熟悉。 -- `Bash`目前还是`Shell`编程主流。 - PS: 虽然交互`Shell`个人已经使用`Zsh`,但严谨的`Shell`脚本开发时还是使用`Bash`。 +- `Bash`目前还是`Shell`编程主流,并且基本上缺省部署了。 + PS: 虽然交互`Shell`个人已经使用`Zsh`,但在严谨的`Shell`脚本开发时还是使用`Bash`。 -### 📚 `Shell`学习与开发的相关资料 +### 📚 `Shell`学习与开发的资料 + +> 更多资料放在[子文档](docs/developer-guide.md)。 - 👷 **`Bash/Shell`最佳实践与安全编程** - - **_`Google Shell Style Guide`_** - - https://google.github.io/styleguide/shell.xml - - 中文版 https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/ - - 编写可靠shell脚本的八个建议 - https://www.xshell.net/shell/1577.html - - Bash Pitfalls: 编程易犯的错误 - 团子的小窝 - - http://kodango.com/bash-pitfalls-part-1 - - http://kodango.com/bash-pitfalls-part-2 - - http://kodango.com/bash-pitfalls-part-3 - - http://kodango.com/bash-pitfalls-part-4 - - Shell 编码风格 - 团子的小窝 - http://kodango.com/shell-script-style - - Bash 优良编程实践 - http://blog.jobbole.com/93447/ - - 不要自己去指定sh的方式去执行脚本 - https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965 - - Use the Unofficial Bash Strict Mode (Unless You Looove Debugging) - http://redsymbol.net/articles/unofficial-bash-strict-mode/ - - koalaman/shellcheck: ShellCheck, a static analysis tool for shell scripts - https://github.com/koalaman/shellcheck + - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): ShellCheck, a static analysis tool for shell scripts + - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) + - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) - 🎶 **Tips** - - 让你提升命令行效率的 Bash 快捷键 【完整版】 - https://linuxtoy.org/archives/bash-shortcuts.html + - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 - - 应该知道的Linux技巧 | | 酷 壳 - CoolShell - https://coolshell.cn/articles/8883.html - - 简洁的 Bash Programming 技巧 - 团子的小窝 - - http://kodango.com/simple-bash-programming-skills - - http://kodango.com/simple-bash-programming-skills-2 - - http://kodango.com/simple-bash-programming-skills-3 - - Bash 测试和比较函数 — `test`、`[`、`[[`、`((`、和 `if-then-else` 解密 - https://www.ibm.com/developerworks/cn/linux/l-bash-test.html - - Filenames and Pathnames in Shell (bash, dash, ash, ksh, and so on): How to do it Correctly - https://dwheeler.com/essays/filenames-in-shell.html - - 理解 IFS - 团子的小窝 - http://kodango.com/understand-ifs - - shell中的IFS详解 – 笑遍世界 - http://smilejay.com/2011/12/bash_ifs/ - - Bash脚本:怎样一行行地读文件(最好和最坏的方法) - http://blog.jobbole.com/72185/ - - Shell 脚本避免多次重复 source - 团子的小窝 - http://kodango.com/avoid-repeated-source-in-shell - - 一个奇怪的 echo 结果 - 团子的小窝 - http://kodango.com/a-strange-echo-result - - 浅谈 Shell 脚本配置文件格式 - 团子的小窝 - http://kodango.com/config-file-format-in-shell - - Bash function 还能这么玩 - 团子的小窝 - http://kodango.com/bash-functions - - Bash 获取当前函数名 - 团子的小窝 - http://kodango.com/get-function-name-in-bash - - Zsh和Bash,究竟有何不同 坑很深 - https://www.xshell.net/shell/bash_zsh.html + - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) + - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) - 💎 **系统学习** 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! - - [Bash Pocket Reference](https://book.douban.com/subject/26738258/) - 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且更新到了新版的`Bash 4` - - [学习bash](https://book.douban.com/subject/1241361/) - 上面那本的展开版 - - Bash的官方手册(`man`的中文译文) - http://ahei.info/chinese-bash-man.htm - - Bash Reference Manual http://www.gnu.org/software/bash/manual/ - 官方文档,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 - - [命令行的艺术- github.com/jlevy/the-art-of-command-line](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) - - wzb56/13_questions_of_shell: shell十三问--shell教程 https://github.com/wzb56/13_questions_of_shell - - 信息来自 https://github.com/KeKe-Li/book#linux - - alebcay/awesome-shell: A curated list of awesome command-line frameworks, toolkits, guides and gizmos. Inspired by awesome-php. - https://github.com/alebcay/awesome-shell - - 实用 Shell 文档 - 团子的小窝 - http://kodango.com/useful-documents-about-shell - - 更多参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) + - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` + - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 + - 官方资料 + - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) + Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. + - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) diff --git a/docs/developer-guide.md b/docs/developer-guide.md new file mode 100644 index 00000000..d796a035 --- /dev/null +++ b/docs/developer-guide.md @@ -0,0 +1,41 @@ +### 📚 `Shell`学习与开发的资料 + +- 👷 **`Bash/Shell`最佳实践与安全编程** + - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): ShellCheck, a static analysis tool for shell scripts + - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) + - [编写可靠shell脚本的八个建议 - xshell.net](https://www.xshell.net/shell/1577.html) + - [Shell 编码风格 - 团子的小窝](http://kodango.com/shell-script-style) + - [Bash 优良编程实践](https://www.techug.com/post/bash-practice.html) + - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) +- 🎶 **Tips** + - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) + 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 + - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) + - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) + - [Bash 测试和比较函数 — `test`、`[`、`[[`、`((`、和 `if-then-else` 解密](https://www.ibm.com/developerworks/cn/linux/l-bash-test.html) + - [Filenames and Pathnames in Shell (bash, dash, ash, ksh, and so on): How to do it Correctly](https://dwheeler.com/essays/filenames-in-shell.html) + - [理解 IFS - 团子的小窝](http://kodango.com/understand-ifs) + - [shell中的IFS详解 – 笑遍世界](http://smilejay.com/2011/12/bash_ifs/) + - [Bash脚本:怎样一行行地读文件(最好和最坏的方法)](http://blog.jobbole.com/72185/) + - [Shell 脚本避免多次重复 source - 团子的小窝](http://kodango.com/avoid-repeated-source-in-shell) + - [一个奇怪的 echo 结果 - 团子的小窝](http://kodango.com/a-strange-echo-result) + - [浅谈 Shell 脚本配置文件格式 - 团子的小窝](http://kodango.com/config-file-format-in-shell) + - [Bash function 还能这么玩 - 团子的小窝](http://kodango.com/bash-functions) + - [Bash 获取当前函数名 - 团子的小窝](http://kodango.com/get-function-name-in-bash) + - [Zsh和Bash,究竟有何不同 坑很深](https://www.xshell.net/shell/bash_zsh.html) +- 💎 **系统学习** + 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! + - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` + - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 + - 官方资料 + - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) + Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. + - [wzb56/13_questions_of_shell: shell十三问 - shell教程](https://github.com/wzb56/13_questions_of_shell) + - [实用 Shell 文档 - 团子的小窝](http://kodango.com/useful-documents-about-shell) + - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) From 0ab2b393d40a3562468a2df09d7bd202265fb8eb Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 29 Aug 2021 19:28:20 +0800 Subject: [PATCH 095/175] + add toc for README --- README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1df867d9..af21acfa 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ 👉 把平时有用的手动操作做成脚本,以便捷地使用。 ✨ -欢迎 👏💖 +欢迎 👏 💖 - 建议和提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new) - 贡献和改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork) @@ -26,6 +26,26 @@ 如果你的公司有部署使用,欢迎使用通过 [Issue:who's using | 用户反馈收集](https://github.com/oldratlee/useful-scripts/issues/96) 告知,方便互相交流反馈~ 💘 +---------------------- + + + + +- [🔰 快速下载&使用](#-%E5%BF%AB%E9%80%9F%E4%B8%8B%E8%BD%BD%E4%BD%BF%E7%94%A8) +- [📚 使用文档](#-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3) + - [☕ `Java`相关脚本](#-java%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) + - [🐚 `Shell`相关脚本](#-shell%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) + - [⌚ `VCS`相关脚本](#-vcs%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) +- [🎓 Developer Guide](#-developer-guide) + - [🎯 面向开发者的目标](#-%E9%9D%A2%E5%90%91%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E7%9B%AE%E6%A0%87) + - [关于`Shell`脚本](#%E5%85%B3%E4%BA%8Eshell%E8%84%9A%E6%9C%AC) + - [🚦 开发约定](#-%E5%BC%80%E5%8F%91%E7%BA%A6%E5%AE%9A) + - [📚 `Shell`学习与开发的资料](#-shell%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%BC%80%E5%8F%91%E7%9A%84%E8%B5%84%E6%96%99) + + + +---------------------- + 🔰 快速下载&使用 ---------------------- @@ -119,7 +139,7 @@ PS: ### 📚 `Shell`学习与开发的资料 -> 更多资料放在[子文档](docs/developer-guide.md)。 +> 更多资料参见 [子文档](docs/developer-guide.md)。 - 👷 **`Bash/Shell`最佳实践与安全编程** - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) From 856ad2e3d98164487f047c8b62b6e1b21aebf353 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 13 Nov 2021 21:20:06 +0800 Subject: [PATCH 096/175] ! adjust travis ci images - upgrade osx_image to xcode12.5 - reduce linux image count --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index e5486c34..02c84fc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,15 +17,11 @@ addons: jobs: include: - os: osx - osx_image: xcode11.3 + osx_image: xcode12.5 - os: linux dist: precise - - os: linux - dist: trusty - os: linux dist: xenial - - os: linux - dist: bionic - os: linux dist: focal From 433521542375df7b896bc82642ebcbe295df2c02 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 13 Nov 2021 21:41:10 +0800 Subject: [PATCH 097/175] ! disable home-brew auto updating --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 02c84fc9..20fcbd24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,12 @@ jobs: - os: linux dist: focal +env: + # Is it possible to `brew install` without updating? · Issue #1670 · Homebrew/brew + # https://github.com/Homebrew/brew/issues/1670 + # python - How to not update homebrew automatically when brew install some packages? - Stack Overflow + # https://stackoverflow.com/questions/41530314 + - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_AUTO_UPDATING=0 HOMEBREW_UPDATE_PREINSTALL=0 HOMEBREW_NO_ANALYTICS=1 script: - test-cases/integration-test.sh From dc3814687bb147ecc1a604d62f09e08de3092969 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 27 Dec 2021 20:02:28 +0800 Subject: [PATCH 098/175] docs: add logo logo is created by https://www.logoly.pro --- README.md | 22 ++++++++++++---------- docs/logo-social-original.png | Bin 0 -> 28127 bytes docs/logo-social.png | Bin 0 -> 41280 bytes docs/logo.meta.txt | 6 ++++++ docs/logo.png | Bin 0 -> 10954 bytes 5 files changed, 18 insertions(+), 10 deletions(-) create mode 100644 docs/logo-social-original.png create mode 100644 docs/logo-social.png create mode 100644 docs/logo.meta.txt create mode 100644 docs/logo.png diff --git a/README.md b/README.md index af21acfa..cce7afb7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -🐌 useful-scripts -==================================== +#
🐌 useful-scripts
repo-icon -[![Build Status](https://img.shields.io/travis/com/oldratlee/useful-scripts/dev-2.x?logo=travis-ci&logoColor=white)](https://travis-ci.com/github/oldratlee/useful-scripts) -[![GitHub release](https://img.shields.io/github/release/oldratlee/useful-scripts.svg)](https://github.com/oldratlee/useful-scripts/releases) -[![License](https://img.shields.io/github/license/oldratlee/useful-scripts?color=4D7A97)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![Chat at gitter.im](https://img.shields.io/gitter/room/oldratlee/useful-scripts?color=46BC99&logo=gitter&logoColor=white)](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![GitHub Stars](https://img.shields.io/github/stars/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/stargazers) -[![GitHub Forks](https://img.shields.io/github/forks/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/fork) -[![GitHub issues](https://img.shields.io/github/issues/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/issues) -[![GitHub Contributors](https://img.shields.io/github/contributors/oldratlee/useful-scripts)](https://github.com/oldratlee/useful-scripts/graphs/contributors) +

+Build Status +GitHub release +License +Chat at gitter.im +GitHub Stars +GitHub Forks +GitHub issues +GitHub Contributors +GitHub repo size +

👉 把平时有用的手动操作做成脚本,以便捷地使用。 ✨ diff --git a/docs/logo-social-original.png b/docs/logo-social-original.png new file mode 100644 index 0000000000000000000000000000000000000000..633f18aa05af3fe792e9174e4457a68d5c2da64f GIT binary patch literal 28127 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 Q`*Af{Nu}py;xGOG3sl5HqW}N^ literal 0 HcmV?d00001 diff --git a/docs/logo-social.png b/docs/logo-social.png new file mode 100644 index 0000000000000000000000000000000000000000..cc4a4715c7063343d96858fe6b6b20e9baf418df GIT binary patch literal 41280 zcmeGEby!qe{|1Z?9ZE{~(A^y>s7OgSD&5^MqewF}3=Pr^(#^Za zbIx zh|D3S{)I4b;kDUIS*W5Sh#5G>0->Yb0^IO`r0r!1F; zwoxGR?8*OUCeccHEZfas2Y_dF1o0G9~l;^DAH z@tf9wzz|R!=Kr<>sL|nN04Ss7l|1sdSLr|-s6YQ*b-%GH z{~FBybqN3Zy5IQLe|_C=JpI36%5TD=f5VjD1Zn?aL<0WJ>;5+o z`8Os1O(61bO8%Qbbc^2FkAA-#4JFVi;06}hm^-dLzv;QUdA8LrKc;hdTCa<>j8Z>%p-A6=v= z0KrDvz%AJSM0^<{00t3ZDmn4BtGZ z`p-)BAmbeJi>GnNOZ1;vDh+&&m7oA8Fi?^Z{USsAC;CI@0?t_rrX~4v`pej%RRofu zqmco4Z$0Px1GGQE*(VPS-OjWAoEG9vxGriJ)` zWHSCy<1GgG^$K>-N_jJYfghyC;N1Mpf+NC;0IJ#jfX*iI8^bLQcL#u}=4h3>f4$&; zx(C&uA2;bhchbr-?*BDT|K$)**a5iMU4{AS?<%Gf28OAHo3s-0SM&d?akBtIeWoP$ z=C>8UmIj8(qb(@zH){m|#Tx)nd{#D(=yw$d?qU&v@cLf%-};Bq`5TS%Ap>xDA*S2+ z{_8dWYy=Sv&=)_*akYc~>oxyuM8FR4LcR~Op8U4rZ}j3IPGJ)X`M;m_kE2~ZU}o%O zs=fVP#o`{oga|fgwZQ)E82mf9|LO352lpRq=--w5_Zt3xUAZH^B~kxv9s}j>J+E{x zDP)?s&w>-mkdrF|$B;MUzX-C(xAZQMCoA-#8K-JaH@Qfr^SIa)G*d44}j?si>? zGv=v!vPSIF_jbRm2!je>tnMAw@&0r5zSc1O5uUo&v(CfC8&Yov- zZGVc5%Vd&3E5|xvYpUTvIaDi3tHNg1Z#+k7q~Psvp|-k}W&z{cnR<C`SXCOw; ztG~DDoT0{}?`{aJ!7+NO(KJzQNV>vmG}7`lK+Atg^=I z>8;!b7(Jz51@Yu-L=Ev^olI62WtBj&RsMD+L1di!+((%HTE%Yz{J~XLBOl2Od|qG> z(y%UvrJQn&i!FCUKs}nbQ{S65&oePTPe@OvRlfZqJ^ACudx?pOQGB1Nh1_CzZ6{>9 z`TkbUDhQ{j1gj2wLR%#Vl(UfV&Z7~ci(wlQgIpv9BfT$*1K&{cq;Og;*CW-GKThYrSG<2I>^VOwT<1B!MV=nNVqTTD(N!2lfei__8hN5( zj5p}r{O|!CQ3!m$0NG{cyyc5^P*J$Nr`u=eSQ)8$Q=ASfkeSw>=DS%f23P~V160{1 zza)W`z-Mos?(0SrU+xwL%!E$vK4Id-Vv8Uz%%HeYc9~WzhB~-qPAX%c=(!EU7rwW} z2~6L=NJqr6lr(bP;>4Lx(xvx8<3>zEj?WG^9!xj5N0>e_H#2)y-i!!&nuUJyxkRKc z;=*A8TIV=mtW{gVNkXkvuz{mrsZS(8fqUrdqeU#&C+%Y2At0t$ow@JC$}wdo?Na6E#;My|<%UW|QM!-Zn|+cR zcw(k?i&S4Lr|_!!^=C8-k9-NOMBe9n_;=`Z1bG=Xp2o&QkFtm-4-LzAo6pu+g8W63 z7@gIjsj3x6^j~3;(vA^!i;mZO>VrlX-}^DH<*H9Y*>tkHk+WS3_3U@i{}WEZ(?H|_ zM>cuns59^fXcb3GCqEbHH;m8J?vK#TrNGbh8Wu?q`N`F`(kh3xeOH%GTS2W{_U%fZ zg*xVWq$^2HvLl`pp??`R2!l9=p?JUUuRxZL7cKh7;a)cf@kMA@L{)E@VP7o-OG*K9X}pFU35;pHRV4~qNS=9bKv{wKXM201V{pyDtjPf1GJRZG$=U_yxaAMVa4xjOw!ImM)Pm}>w`l+)m_7MJC7f7t?kL?F!oI)o^96MWk0 zZMs}-%k{`Uxo4tTuV-V5Vj9n~0V-URt0gH)*{`WRny)pUxGDz5dI<cmYPv4>J(U4Ebol7K#PXTyNpHbWF^qh;N<}xRA>+%;yJxo`xv#? zsBW9f5l`l|UHpd2aZn+*{sc}Q_)cekag)5lES+0Ku$+F!GGIThwR>^9mpa_%qckvG z10mQ76G?z zLNJ8dS+%tj>lF+D+mQ(t5L}8!#rX^cKzd|KEie%V{K3!KUuCT+D=X_*8v=-DcEA-b zO?k5$NglCfjkRt~3jMBxOsEFjLw9mn(MP4Ik%g#i)OFE<57pzP;>X+5)Z5g0sAv17 zo_Qy3pER_e=k=yIhAE-X=wOeExIp2nSYQGiQ?e+u)w$Ba`w-?ZR8J&*%Kn0?dJiiB z4^G#upK4rdF>vqnU`W|)Z?Uu7xD`jM!L5jdgoJAF!?$nWB>k2#Awjo1+!q)Vn|-C9 z$Kx)!Landcp(SzF+BJA)=N2SjtXhspNl^c#M0Uu3NqfZ0aVo7wm`k($uTEE-PM6}l z^VJK$4UA+@;GqGXSLX}!OZrMb566a?)ftMkdB#hswucQp9l;g5G%pj`zGzoP=1(x9 zUY@8@oYxwJq1bx}^u08YJc$~s+y>4E1c-qFS97JNCcy?6+Cp8&*4wwKT8FS0H0Fx% zDb0F&Qv-RwK`x|fx<0aKo66=?PK4+-$h-=eU6W#PXVYOyU$lD%!5|cX&l9p7B95h# zot9!4!&vp-P^rl$C6c;xyxHs3WAZfT*Lw#bj_`aCs z=)qj;3Ht%K5be&{-OqtobkD#cU+D}x(2thWP{orh&G=lvMKrY+=RcoyA=mtl{GPs zPq#MD(y9KmnPF2smmJ?3)@Pqa&rsHBEjns~ZUwyg3EeMy*lNfRP4eYj^aimd#|O2U z0b1dA8v?2VA= z>e7`DU|$;UsiG_q9eTnQWhO5Y5E4>?-DZu(Egrf45e~COMnxGl`2#UZ^`W%`8f5-< zLY-{`D}hD*cy63lvqD}|wkz&hY8t6MvRhc}*^OAuRf;zT)YBzBCLSo~QF=F0PtEj; z_Bsl-023SH!{NDgzpxzf$KbWKwE-$pV_O(dQB}O(&PMEp>KIVC!2K zGk$vTZd=c^W6`pC*gfN8eSO`xFk78&4xO5q4_v$t1RPkeQ4QsVIN@mVjRms+%@%h6 zjKr&oWw~F2{Q^ska1L>qt*Aiz!T2FR!&fl(;~SiBom{5t2l;tMYUHU^VQIQ4;|f%I0VD|T z`p&>NLr4$~$gNPX`c7oD93jj5GvRLebNa4Mx5i)kw>UN zB^-OK7c>R)huO?Djz8k5Ur2(dYzdkAi>I!?oV*5QSf1U{tJv|x2WbpPBneg>=f@>F zIXCbQ7D|N$cp=PC(Um=Tybx^_|HBu+qPqZHEXl|47z^zfJ%9DyPd1(E7$Dx(2qvIr z7V$cWWK>L&vGpa^R;`dyxvn7{*#^?Q_HgQQ+ezN>a)PP6i7IO{N1KVtyvG*(bjCI2eKeij-O*u`d~!-k;j9MDx}>Zx z=_<_o;_l1GbsTMdEo5?gIV5svu}zrVdS`QFlNkqWMEbIUrM7i>BWLW?xwK*%!zUPe z&{R=&z{5vrRlS4Q((dllsg8U+z1WgfE_;1_7JqbJz~)(AZxL@Zn!bN<=!wck3u&LI zjl|b!Q=BLiXeh4d`Fmep+h;1dimObuARrR9LiQyGaoH6Fqo*eWx0V#AUuC^n?<98SXuaPpUAN; zL$23%G*Vjrjlv4ko`gqH8D^gGWg_VO^z@Q|g;Lcg?(*qPIo@p{l-{o6BHa6dZPMT9 z>AW^o=ghEb-PZgplr*ZHg2#$svZ4-CF&SY3 z%!)X@v$i!LMvcLw@qnk}3hIYqt3lMA9*dh5Obs?8ZUXjic`lWDvU?nnWI@+s2cOk| z5tO*cH-Ei!Q7yjk-4Y}(k$@>uIOONr5bsO8rqO)JrFTQ!zHs)AK&$Cfg8cZqe3f>$ zv9UCe>Ct7I{Q)aD_*kr?d7It1IHs|FFew8}UW+;N0zX%!5;j7Y?T6I!DM9+VXu*Vh zwsAjxXxDcJU2)?q#7nhJwE`vuE2G^+OVkBb`LycwOUfKpdiRo!E}fUd^B*Rk zp1(3VS&p%+$kb0s&fr19iu?!a-{?-<;|?bE5HnMJ5!G04C%I@{4K3Okh4rzpo|V_! z-@Bglzn&$BsW;DZb{)WK*smSZFjo7JVVUdS*IpRu@Ph*79*ToXZxIOUNzpw-i$=K5 zc&>CR@=cRq9&LrM)+JFmiWgW&-xo|H|{GXiu%?Y#LK=EadnmM#bvJ* zmx`ca0}u>iG46SV=gU{;JMrE-O`6#XiT4OaPckkx46biQ+gdgNiEPbqM&{N=oQ6LX zYU59pykFe7C)bB2&K=AsUt!W79#wKyxaic%*bh3Zm8%m1vz16K7TxCSV1)d zb>2znnQoc1;ZjvhF0hLH{MmcNwD=b2gWd2RkH;;wM`#o}SunfhpTzhFz7;$M6;oEv zc#k+6APp)CZrui&OC71*8dki~Vu`JcI?xo1n~3<%TaC|Wv64YDP{enoh?$}-F8dR* z9oCpEihi9uw6u}43hdQHy!N8AY`+Gm%WbP&gsFfA3C_!0SG+t;Wxp?48T#_Y`uph= zHoi))NaQf3Sp@hh^6f z|IT@pXl`8yY$|^sCo<;Ap~2WqT3eB#%Q-lw`S)p+O}E-Sm(zp zxe81KvI>{9+cJ!@v*_%uiovBnDtNd8fck^>C~YF~jMw5`UiBAi%Kjf;e36nZ(!SbZ z*P2&}vRAB*>15|N0C0-vWcy~V8IOgq7DC+1K1TUL)0ImgVF+giktl~KltKaI=VlK0 zlK$jV2+hYydzjw)FCE(N4(pej9X=&6tqqWH>z%(M6n$&jlDJv>KFDNX*z!C9Z~@&w zS?W$Y#W>zcURkwE2@KGRbRrsYL94Z|SDTlWMIU#f3Smi4oVR9SKIbHBno>dre5e2^ zHLB8>t**g9^iuh{4>vzyZh1>y^J#g-rr#owUW5W@k5E3Yd)C#qGb6~)h&xw?`NbAu zAYeDkdAXB}qV}jE!Eg`8XCI}^ryM8U;2Jkjr2X9&n^@PSY`7|h$6SBEXJ;l_puknm z{@IVq6SL#h5G?x*E}Z3pR3+l(9A`{{9Ic@fmTU|NbDg+TFH`Q~E2-m~SQYTj2kBJU z-xBlQNkFa%YB(k0kp})EAE!qv%=a4C9+UfrlCpgqQW8RV?d~{mN4p+1%w8&GE2UC& zDhXY9!u*PD(8Qz3&ADb20w$z(`xM?BHw@>gKKNKr*lviI3ZXCv^(w4A(nRUOB?*!#I>Gx1XqdY`}5=b znIpCK_k`pz$?LI0B`QtGUAz(3&mycdYQJ15jbUBIZq|?=TQ75bOz%k{*3v0nbk>Iv z?n#oC*oLlnaNADo>_sV+*&mPa?UjZ*6McoXOu}?ub&DkmXksX&+=ISi1(K5Qs2Iad zsP`!-@D>;vR#G$LB`PXwo zON42i*EYDbg5M;oaAThz%nqy`7bB9j8(?hG+LQTE`UXT2Q>_s*G-+c*(}23OkQ?o5 zY3z`)m>wyIK5I@IFqe~{qfXw-iROy{uo+q?zhd5^iw%+zLLXBWTF>_ZALe87A%<43 z6p>*QaDsTV8H>}i<>TQz?jpsm zR`b>Gotcg5Z-JPt^_9yKdy35hEmwnx#KmUI&_VfQ0G3!g$ds zFlu{~*FwfU=2Z}^la$g@VTut?M{U9A?aGOav^KqprrhDRN6e{dLN0r1i5qh<{1tT_ zhKR~W18#PvAH*kI?x9rNmiaac4y2<7E^_P66nO8m7RfLLqJ-l}+uW2I$pa=25AD9k ztVJm-WW@2m&B{r3q0hK`M)9-Ujz|(f`c?seLdw213P?bozzc64^$P8DyuC>;CrmM8 zuc`{n9vj4V4I?*aRBK^`M+-h5Lpk;fy~)iZt8ABK@e%PI|D!?)(WE|zA9q8>KrBifz%$>B~69xVPFyP$bu!P_Si zqc=V{Y!33ZF_2tV|#&>`3JS_#Fuy zC_%=zI1GUDr`%_KKP4$`#QtpD?~NAPX&ll1`uL1AlSl(2hSN~R}wqxvg4b8Bz+ z5C#^OxbtO#Y4y|XB^xQ?P(3rT_Ch18QHBE*UA-Hnc5*i33nmAy*SGa_9&2?MZ18my zJ%R*9SHUVN178A@rH#4W6H84c(BN`i(`g}$kI{F>zWEedw^`l|-jF^&+FAlUS!5En zk1DOQIU?bD(_Abx=l$~%;6wDs6x!d#KF`Q!ui~urYkGa; zV=3|JU^o?8g3K5ske+3Y#}L-bxwvwBvDpwBr(ym)B5Xt<*WY%GWO&J^uGd4)zDGeM zla43?zLC^aJ<^iRFnLs)_+yjj#x76lpsgF9dzLPbeV|Wq^6+yY#&7}PsjPm*%+gisQFRWLnN2v__3IU?PR789-Fi-6Iv~Tc}B&NYcN{3ue8;7#UVT@XV2Ev(J zVcCRr#k2W2=m94>Kgg8*=1u-={pr{839(}|qp7?);pbBN+70YKw#udhQYlFI-Iu9k zm!~oe*mPN;m)}euQo8TN4Y>>jnjR@(taSBDRYaePjXu4mSXw7BIdo17LREycb`BbJ*)hKnyqfhx(lzsK70h!|}^`Ope2I*LfAyJCT0T&C&8M zUCU74=p~iJPu8m<6@vAkCu8TAD`qS}Hmhf3&-fMAAMxCY$Kk3swcN#A;!W7hwnM)F zeHTn1XB|`Ey1$A~k8N2p^8Sk{+~T8jzq`-l0OYUS6orsClhs}xO}1v`!Dihz{bvE5 zu8TfG*y*Ij@!DpzTWH|}_k3~AXPl$;oG}*7;uvMIOSR_HWyS}z zb6=#b=c?v}=Pa~%?JD^?8#OXvLGC_RM4~k|nrRTl@A=^Gx0S^z;a-I{cTQ}`V00!h z=pC0-eq|YJP^RhA*PBok4h|RsU%xZH<$b14rdi@*^M5&ilr7YHc0E|~(M6RZ z$6sPi_Zwgl7=5rTP#|GPPftt~y)?#P$8?fnx#NNqieObp6;zMoFko*0cNy{n@P*m# z+}Gx#eITbfu4*1+P8Rkoue2FgL1n~ZX9E%Ul3gBK4a@Wdvu%t%gOMs(y2c-m71hiz z>j!xin}R-GYR>3=b>@Yz_jQr%XS09EdXAhri)E*ob-Z{=9&dg83Ul)E+QqrpCvMS~ ze`?jf{;o`UA-adhW}vm&nNbu)xSLY_6GOMHaF(Ixe)Q>i7Hdm zFBpDmW<>-Q%TqsR72{R25iy$#x?+8ej32Na$a)NC&feQh=&?dWlyShk=n8Z8gBe#` z{$qx9dA^haQb_i#*eG_*YMZ;!7=)462V!zb7NCc@d#!P7x1slU24+v8^^HN}gRB!A zlUscDU0-bFE!z__v^DX|pQ-i)D^=HEug9h_?$C_Lzn7xnpp>R!*$>A5nAc9l)*V54 zZ&LXVd!O45Gn-YW)X-FIxTjwjWw+q>H}FoZ7$OI2qx@Uc3pxOFD@_-NPy4xPZyAwH zuVp=yqs*~S5`FswMcM-J3ahg4&)2$sr%Prulsm+X*4yt_<=EaC!=zeR=!lAH-deVkUYay~=5m~WYB|&B`7GpF8eiZL zz`#FqwPD3fWNr33TyJ$^UZIO53KXlfFBqq|eVukc)A2t0*^g}9wT$rq00L*LskEl& z`I7Qp_{3MqJti$;MY+xIodK2Npu$IP@NZI)GlTaf)qh z6ebt-sf{7KVDU5}#%i2@&$5VwUAcCK_+HMKQl@z>CpscMFMhU5my<0N=YGIJ2@Xu! zJ}@neOVaZ80_?Tufke6=muf(tsGFr`@n8Q7;M^wN>I(&g=%GB^bXYfLys$7w@~~FR zy>qj9Y`wsKF%_Rua-_PP!X&$Ef|ak*GHd+BtHnjeu={#st0l$ueh3VXh4AeM543u> zQwnh}lh^!6AQz;y+3InP<2mMPfw9Gml!f*j*y~{{n_H8Y+r`|6iHN{utuuJbruao{ zb8#&2M%b3%z`mnRMnd%gq{F(y=xGdp&#;73KYmd!a3w%8w+4`kIF~W zk!J`fHGr!edT9^08+ff{y`a?@4P=b&*LY-oFAV^DjI+N;>4=_ha(nJ0_`Zlt4LM#P z`(LlDc@O|~jX}1zh9w3M5Gv&)+?NN1qq5H#&qc$GhXP)fxK`QC z?*%m1@z}c%ax9m*pB=@0T{7*W@>60J^RKmZX3cRV0Gr`Z70}9EWLcXlQ+~h9FnR?q zf3adylRTiqsBla8XR6;r!vdc{ScFSS^mg5u!CDA|SWWGZ5nNJkJQ4ReVj`JRc$9w_ zL#>1^4I1PdeLu|KpCwGI$|^Pi!D7q?O|lbSiFehj*w$&JG|yZW)SgCxOEy24eXp0$6AeV=@cms?ucL2vFb3LA-dutc%iH-*LLeRa^!@Gt&g0Rl zef^Cg$sYD4{qCar$KpixqUQFqKdLNamrwFe-OA~8kcCV)Hb%n62NZ@xv~!^6diiU% zR{Eg;2MUZIo&Kvb)cDR+L zQJ!63S54tM{LZ5%6pu?Q3}KyD+#7UlDm}$rxTLIamxi7C@`{H&2rD@Wd3` zp;*12Y1vU#IZ4Hag(|*EBDHkiD9?KFNM64G_Lx%I#(+AKaf4poMet}#_KpH!_sM0w zD+{23l2SmdWl#p|-hn$|0MuTbfas75wJD{U%;nfC|7;OPdauH(@Us^^f(2s3$PuLX zapaa56}vyumHChEcz?|-0Z@y~MdDCsc5HKrev{y~mw8XZ-JPameTA<%MLIQI?X>=^ z%A%*435J>DJjfDb=d+!Hl4j;)-qz;u&QOl#hwjs^@dkRA%`_DRa{w8*d)epn3Iuk8 z^h`L~Q;n#e`EYCTu98fevOw9gS$K#Y3Ng?O2Kb<^tr;>9B%iM$A{ z{xA@x+7n|*nf3fSBDrL!)LW;cxNJb5!!?PgU2s)t{Kkgas@Hpbvs@*`BC@&g3jK-o zuCfu6jtiD?cVbXw9rl~L(dBvL@eJA|-mpi{waS}{eeJCk`$?0~?$`^+8GKQ)AC#Gy zT=g9f@;cLYFkT4elFPZ3j10_ooXPol^k)Ai7K&Rh4aeJhLI(j?yU77)Y6(YE0|4oN z#sMYS5EHxjY!uZjuH~f1o$jxkJ6*Q0$SNwva`*Bt?1uQBpAJaVh7}YOie2=}fp%XL zE4?*UPMUTFy4z3_ZaUNGtHvSVw)c44`dDSuDm`1sCCT!u|9ZbhTcs`aI7!PyYggXb z^6*$RCC^6^{4psVCA-4*jjz?;>Y;L(xM*-GiYdw?n4+VU6`VuyMp4Y-El%>1ssfFu zAYV7`KuuE8n;_Pu(T<@dJDZp_(M$Xjq3J`>36LS~<@N3+G;E0(|IAhXxcMjpA9MXF z-6nY8+U(X*TE8gZO!eRzAcvFPAM@=P^ib`M`PNYRt(wmDWFTCdav4$_{QL~pawK2f z6w^rOtJu(6Af|iI#$B;&R4K$5kMo%@sp^I;WX)=o)-Wfcq3grzIiyYLl3*zJW-9lbXCn% zN*dA8epI03Hq`SI71Z&xgvj;&lw7(w{ULbHz*1W{SbPnL{N>$$ynrw_iQiUCEz#j? zJ{0bH@_3^yQP1)cD=x9H`9^CYzD4#yQmPN4jRYX#h$>E7C#sG}J;fYs_e->-0mCkI z+NPuMe7J3yDO-=1PxM{QY)V8xC>SL)5CROODF?1QsSHe3bJoy)c5Ga_{W@~k_9&Njh(ANP$vC2;JInMAzn zgaGJa{Lh7U`7%O(kM9!+fj2M#Ncqi*@8u!#HVrWXO*LELCbt=KXI@$=XV&(!C`6a6 z_F0ZxqA}On$GXjt!~o5;iGb++noi=RaSPH{HF~1u371H<_*c2KErKpk>IN8ea;+|r zAQ7+C^dx1rF>h+N?n^>y36mWTUOy|xOqa@@tw8B-gXmq8D7cYU7tk!VALyAy7tUz$L6yPCsl1AXY6QN9)F0&=B3^ zEbrrOEx>4;um?7zmiwi?O54+slc3lbeg?AnayzrS-FahzN9 z2r9x{f4F5p+|SeS5McJ{UZN30T<#M0=ZZ9V6;bVPsg+g9_;yB>k%Jy5t9)0f*U@dU zs>nhp8LydZtt7Cf*GXb_$=6HZeT9|BOnEfQgD62aLBf0bdD-JGRjmGSF8>v!wL?VB z-c0ZqUlBL#+MLh+p4hOfDE1Y#K)7&_IBFll!oY$qgo>=ROn>T!>DpQF%C!4znCZ^p z+R(~wPV}-laZhFJBg>%7N#CUA*i>7lrl-tO{uCdVG>ww(+Un$DQPY)E)_b8Qk0w){PU}^oR6-nuz_PD53BOfP(sQ z6D+GFVvSmYpYhh6tbU33$Q3zUVXkbg=4W=Dh*7Lv)uk=@9|5a*5&_F~yaL+w*W=06cZqm`T+E2_=EN&t zzfMpGh;>|Vzx`VFK`P03(dg!=_g8c>nf&6c7(5h*JDRk%!pK;lSyDZ!1P&Or$h#Am z3khD|hC2rneDotO^$EgQx7F#9}eOR>s zL)hQ?B1;+TgrO7=PNiGz=_{hp5dh`?2t*2dl*UsO;rSTiKF<= z4;-{7jtQM>Ya0DbBYhY5<`#xrkTg2O$0`pWNB1nPrD2 z?KVgHdRaK*lwt>+X(kIxd}@Mn>yL z*2vV{p?0VhsGv-l{6{r6Na%YlMh5v8AWHtYHDS(ud#o^8L;Z$72}GB~VsU84S&J7+ zEm$#=Gky}}^gJW}>hd_)M)*V=+|1Gs`;p$GMSQxR{zCey|5MS$OELJ|Av@ZuulHF} z0R>B}UB?79t@q13)}sm(e0DZPY<^Hs zlGMb+^!SGmMNDhB+J}HIV3Cc?Zan-Hk^HalB-sl_kFDZqr|q7)D879^yb;!PUUlXo zHboc329NBqMCS?@?#TM2rS{|>gReb%ZU+W?MNG1vwkq5ZovL;3hs$T! z!l!Yn-Xqaw80ig=N@K)yH~LWm+sFg35e_Ksd!YfWdCF^}$Z1HCuM^Nue>X)Brxj~n zyc_6e&wbwKvKS>z69oVW=c5mjB?K#oOUa3WYF$FDyGe z?lEC$o+?+j({EJU-k2I#NDQHAwmi0blqgXxcMef1sTg^%{tO;0PLCfq$v*iwdDNy@ zQs7~KgHq}%vDd3Y(ObUAy;!FRKYZkU{Ht88!1i?*U=(&W0jVY|Z8$hU6+AvZj?&{> z0*m0`xw>MGOxT@A_x7snrUgA$KZX1X!06mLcsUi5d8^hB*DNVT@yUV+M!3f%&_ zZeq#63(YJWN(ttoz7@G_wYr^EP3>9#h|&z-zPbOihwSWnm`t@=u3}iNPG1aOFNfqQ zN`5kSPf#NKNrn$vHYk3lr207o{>Ydudq2NQ|CmXr7H z4nOOyPrW=h-|8DCK-E}9-AaW&4+SBzZ*^)&S(1LyqURX>sA|dwO30>Fcma4e-M?_Ob;mlBaf(iZH?cs_r;Xj45 z1)`|bJi(aH=?@J&mtsFgN7ElqIY*e9n!dF6CqkCaV5bAbWB&u0dyTnH%=`N|Yx6 z*p8C#M*-Qygw;7Vh-UFV$fG}KhX|`dJeneJuTNCO3_zU*>)f_V`Hs(zF3r@#S>HOE zFW7XG2UK-}F8oqXFaq}=J{;*BHJCAmc|aG?y)7XD{VlBYnDX|X(A}Ug(qV1)g6l= z6rSPbYIapBvFCwQQkmSNbP`ig;O%h5C2dz5IYaRQkak(DmqapUjY4&vMHw;doY}S; zgXuCs(g-Ofv2>VBu&NQI>_^sOBj0_*wd z1GoXxfPf%SKhoYJwgzc0WjVD&RPrbE&?E8S<@O2d6h4=#Ic{dO zkqmtaVfySy{3lWform6?CklXCBJpo(>lSt+K(ulBU7RpjIgTq*^H|0B91KX$YnHyp zgTET}OQb1`+5slfP4J~lST5jxy11tsWa=Ym6HdnC)2MXjxA`K*;`Xsfd!@@ax? zU$GcIiCf)nuz!XPit|?m0?b6bq5E{Y4^5m$J7iVO+p6RnaS0C40^dV6o7Xj5T+^gM zftfeQa?(pv0cCMHFd~_j&Yt+x0#2CR_l^G@6UnjXsR_)Tdvscr3jYy#PK1m+_ z$T`ATfl{*5h)jfw2SI>sP7-whN_)%2^_s(buk%5-B%uh2bE}Zs=G#+Uq|)P|d{iT< z;cpTVuG+iL>lECh6kHymy>`^8mYT+!v<}~@6mI2T4|k#i%hDMN8R|!a42Rvn;sa7w zgL;?uXD6k!Cp(Bbr^|2m0Y+v^lFk7z2Z7Pf5|Ak(4`AN4+GPKbYeQBc^;sl?@!nZF zRNK$e40#r(nq~*XjwY;&T|=(`xjPfs^|AHYc-r)0B8?uyKwUukk`yhdQskLCCK_C2 zo0R5BhD)GvH+dZ@(d5_|FsQaxOJ1oSF4^iRA|8NQoaQsUk?um$0Ca`uW5rH1si^DP}o^aBWX zsqNYY0%fvlTO06)P%@6AVMYM)L<9B!@MrspBtG^CIJktCZz{l(*(I>lo2>FdBARm(kb8vT}v?lilnzSlrt zD>-;CDNx)PJpr8Z(!Y57RRD|XbKi`IRXCGhnVeq`X9(^z?wgL48gcoa9jHu3u@WZb z!I|n8vf_<}Zj=IB@${2g%1}EQ=D?|gR^7A2mC>f3FmCd0zh`%Da8qv}j_U;BU5qXS z$+8-H^nOi^-c%u0ymJS4 zrMu&AoXPD7S~?=*-{SCYv&RAd!$9eKRvjbw6EV=1`n#Vb^wE3TZk(Q;>MQU2_*hxx zuQ-1e`3jw`ex;gq=r~pD=rsI7S~h}4WN|6pAP(4q80+@Zg0=y$(jrx1cz^EwM8hgX zBBx=+T~YC)#kg};m>iDwIRt}x6=<*63qI@aROhu`cPpj?ede`Tr_Y58(9fPy_8u)~ zT+SVIi3J;Lsk4Q7nc52arJwc=)_HjIvy>noQQIR2@yYSo73nzot_YNY2%2?kuvr*D zIq?nFnTl>j-%ZTrvY!#I+x>>yYFZjbBciQQtecpd?NPto5k=o>3YOBb!PEkz4{i+n z1Za@B+aw*HfPj58o_LWA(DFU2Siz)#6c&^7A!}@@Ps9`OA}r(zw2!Vh_>kuKa0{tG z6Qcel6PF>nNfzj6W#h8B8CCmsy+lX1uUE$?EzN@n_YH<3>j`hdg`AxS$u%9p4DvF& z71zUW;Ansy!DKk$%R>*s0!`ozju%e4IWQ+t=7`TmiUR!RV70Bz8bC8Qo^Lm(=u%B5 zS6QqdpC{y>VYmS*z!#eG7>2`YRDg&*4g$5AWT%HEvs1S#e?)CL^}S#0-16;y6yM~1 zn)0))Q$(_yq@EaahYCwqo?j8GUxN8rgKx=Jq*P=+u%E#jT=xQFYrzuD8r1eh^QsOi zeFzBky1ITyn&sPpU8SO>RHkR%OD*v+zBfPKcr?3g8#*sTAOW`pUcfHGK5I>n2(ZTp z5S}EcbTN$IcH*(Xeh+sRAaO*gI=TQsxx6TK(herx9U@m9n^}KTkdz1s)m!nbU#cU# z1pa4d>qvj6n)ja3&K{4(9sPC7FLpzUE_GFB?_4#i<60laYm{k~iY5(CIAP3nJ^LUc|rv9I&L5R^CUR5``D$vnE-$_6=$y7&qffDDB@%4 zW_#yl>V&*=w>;bTiB~&&*q}D6cF%3Br^m>TLYG+&!g0n3;CFbF(ZrLjLBTw5X!$Z~ z6Kw~w|C8hrN{8?Pfb{d%Xl;xh>VpFSEWdQZ)D5i~*p7+CkO~*o18CVp%BAJ|-KLfj zMrvFBYRDTGTA|B>EE&^O933`{?(7ES>XR4zxppyV;*+WY&hOo11EFY1n+YCW_M~i4 zbwX}5DsNvns^-g^_`XV2r&pgC$!AM-MYPTcvXzAR@yHxjhhE+#o5O^FNlH`E z67pcQF^1?pn2-3Xt%*;QGH7+DifN5~ifI*Mdt?Z!pM~g$f#yEZT&M{E{JKW3qkAz@ z0s^u|R@da#A?nH4b?==0PmZ;GB&1@`|LYVfpK*|enz0*YtVx;4J~eN=55 zU`ro-46+79>@tv^pi_=Q{RjTTeCiu0ufn{KVKdzLjLXPStgLSTB2cQTKyL$H6L^vQ zLZm(*89QZUlukQPdxBZsjZ_AR1sv63zI{=u?-bo~EZx5XvVA2O)SfSX%&EgwA+j|4 zsIT#+ec@L)t6qA6xmSJ4NaZ`cVuRyIlT)<7r#Oq+y`Y|Q;c2lbw@wG!3h&*Vdgoo} zC2aktdcjiJCRHcfOmu%XF-YN|7#ZO^kZOP=7tv#S(Lg$I0N@pW@|34kB=cBn^j4IK zsjI2QJf^fy(6^gu)UtzJx>*ipzDsfVhP?++bbj3x8`Y45$?n%cJSVL+ONDu_t603szIy`v(i2&nXqB0)+B zNGFhpfQU33y-V*Py#}R84ZRbZl+XeM2oOlV+v~aao^zki^Zo;`{FH~Cz4t0}t~J+~ zW6W$XFD$Meo>ihD(XTx0?uua>p)3V@Us*)C{W0rIfY`s&cZVV5G??0abvU1GkIxpB%(Yi)!5}#2ReP;wKLs&+q7!#K`Od>O6+p8$b(@zPOlHIVDXw zH`6`)dJWr1a1z|L^WS?{B!(nN!X^MdtIK5f7@|C|=!~3I!z8{#3z*F6<|9{ahWF zUiHiI%>(I3$!~Ye6*{PU&eZ2Z)_#1Yx>a!%s^jt~W?r5)@K+6m>%l?l-mJ0m?s>5~ z-H;2PLcsZBon+;5BE!*ljrW$8m%Bu&rmpErlw}6mCf=8u#08pX-}sITJna6C2ui&Y`@SuQ+fvwyXj7Zr(6dWnPXv$E2^#* z=Ez&<0WCGhm_l+JlimI5()*;^?Lw5{hskH=r^J%K3U`K^*PEp)kgI~sI5}Q5eV0Y( z+^~O^MICp2?t&P{rRnLK0O3p0MzyP8^;;}A$pRiNU3z+9*4!diBe?Reh}8(0B!~Ez zF#p!`^o)#*dy|zAh@Ob(b24uFPf1B48a#gGzCdn2SY?rbviLghXiFBaOsKf?HP;rP z83lXAwMVEBy?&{Zo&$jU&cQ-ts4?t4vxUDrcL4X?#%U1+-YvaelS3=lqOlX%HKbvk z5Bj#aZWtt*mE6V=WXWk#j30;?VxRxw4oHB+}!>oOi0 zymELST`xKVV-u?@L&zMwIDDK=jn>)sQiq4F<@?m13%<%|0@z?0*QHBK`c|Ev1ub;8 z-FyK!DbtUzpWGX=G<*5Zn|rmJeYWY>uAJ(79!bZeY=-t(6B$n%7oMzw#sHf&YIJjB zszw*!zbcMb_d8LC#uN+yRT0#`uJ~@e208##8s_~#?`biPrAJdt)z=4m-#p4Z9hch#jxUWFtr=JqToXS}*(Erk zKQh>;jGkP`;?|eqmA=RJrAoj1Hg22J(AFu`5xZuJlgF5Z2_7LNsQgQ4V@GLV1ZPrJ zOdP;tYJ)Iri|YHaiPVJeuY4=Ll0JNUWe8A!6@s8Q3r$3g6Y#@-u)aBu*;Z3S%$e++& zyY`+eEx0e}70WGYNg=IUs*GyBhW_S#euY04-rZyMVzn_8=6#};+1yE?`GH^a6a8sE zM&8eT%=g~okK4jIo^>X{mQ_5Lx|bDEK?yKhqHFJRq5{6xQ@iG5|Ba<9`Ye3inkNCT zsYLW#Z#KAFvlQ$;ixsYLA=bd=@mI*G|}k6M9#Q_tED1E zJMyQBTHXz&n)j2_Op$&UsnXBgov>!5Us$cX1j@8-X)@?&D_9|#e z6F31fQX=H^wSynCUWRfS+cV37QCJ$))SiHs4bzKNc8qEpzxfZYAJ-oTbm;VA=PnT| zVy7#?GW%7N?nV{lv(V%<^tnCRv_mvCPeh5+-TZTx&~c(;;uJ9k$JF0^4&}ZTj2;E+ z2Eq-O`=FyL)B0ymJbu6iED{O!ZN9`2xFl$f-JKeUl^hl`f79E1qu#WgUy7s`-@*`V~PKTbN9X+<)uv?^v2%D8n%7Y&Z`I=g7? zZ9ICFZt6WL(Kba>N7C*P>(pFVbq?-`J`XKCgKw={KeV%3QrJsbgxyk3&Fs%yy`yu< z+;UXNbasQ2-#iYXoF^=vP}^@)s&46=yryKhom_xZoSGrBL&wZM+-Xe09$bfyYFnePL780 zUEakF`wvVZ6~nLdCO3)=YdEFon{%=OvZmBDuv_m*2aUT>Lw|+vs&cW3I;a_?OZ^M)>tIo~{rEJOSd3Zsw zw3AlqAq7R#QUa{y3{@}Ci!;Th*Iw4=lQ$T{@Y!!Jb{nAl~in{O(Oh5*`8+g@MPXzflU4yA$= zl3Ck_%&SIhMuQNCU37aXx{yRIGDuO0S(i%LWjNRTnHidfDu{}#aK>ZB3Ymw(n2&oZbD^v5iIk(VB^szI*1Cdzg-|?X<)Q z<)f9(yz#f=2ytCu@X?;+ZbID$fMH|tuH^bYgQMDEr(xqji9?TR3Xoj0MuXfjGi>H= zk2AYoS)cas+((Cw2WW7*sKrb?LWOT4Nklh*b3W`E6Hx<+f;snwUnIOG67fjv&ix*y zOWZGiT&`Her5twKa<1N#0f+iFs$0Y#VVHUpn7p(B;r7N}d4Eq;W^ipOjd>!!?^Xjt zM!t{pL2IIq+a3vq?^`()K$BU@Dk=rL2?|i2_b*ENEax|}9&c$li5{Nxd$IDq2tBkx z>t;Nw=^YLA)Mi9(L}4)Boi-!KhdW!zc^6{1$A%L;4?XS((UWo1&^5olwAW>uwG;;j z?aS^a5RUMRGi)%lJM`VjDUQbxT{OD-6?J%iR$)4*T7r3isst6~tDd2Kqo}hj$<6>r z-DxrMJSg?*^H9F}z4bU&O@8Yo%Ro)@ahc*eIy*J8i<*A<*Gm%*x+PHSF3$LI6kJ;B znMxg1MQI(m)i5xE>+ZJHt3af!+Yw6`E{0d|evcseG-&>SQS5(+^v$T`Suk-Zl$z;k zan<3xa-Q5b(aEok-PUMF!7_K`a2`KmCbDN)ziOUMY;4foVMf!E>mcTHSzveEGg~Y? zX?|{Dvr&qZQ3*0XrB86Kb5GM@(x(*RqV(o9wxdRR)oKk!6RZFXlLa1#!G)`HxI?nI9GrTAb=oRrBjpm6vLH})sn1O1~r zxRv6Rq=^-KM&lKfE{x+P8PTIF({z=`Ua!_l+8 zI4-jezHRTZMypmm6vhnVRL0k!G6809=CsEomz6zKp&!oJYEDjTz;lASzSWF|J1|e#gJnGvWXqQ zFg7yUo53}lVo($d@@BnDR&6yLW8q`>ZKhyWA-}NF#Anu@=1D!%uk&1{Y@9-}>oYl+ zuZZ%DD@ut9!8&H>((%DNWw^Mg_5o{*#ZM_~QY~nST{*oNDR{DrvAs|mwigM~(OH}F z(me7$-;8|GbXh3;VHbYm5MH0cE_Y?G2Z_oT4qnb|I`33-RY^L94_>8%kb>;APYy`2 zb|-Si+D<~jZJJT$ll`N_iK~&>=LMAdZ33?1P~Q-HRv2ePo}70lr1{cdpr)2#ejh-b zT{vNoc@7$((&t4pt{C>?rcqgn3DWhL5jO;!>j^Yu5T)Wbn&SHQz^cKsaBV-qN4cP@ zE4+&(M>OdApl1GrDlgcn^<#@{Vk$e|et#{1=;;dML)ldTdeQwz#hrT_l;gA~Z4q1v zZy0zi68u=xo(?Yp0g5)XcloY*LnO!OTShVP1^vfukvu7zz$P@2b_1ZDb>5Eo(3U2| zL);2>5I{KrB8VvJoc<9&Vn6)-<7TBo+a1b=J?_s>Pz!ha{D3tkOCZJfWtwMbs+f~$ z>sTlN>^s{KDO|)JA9K=O6tg1AZ#p5 zxm=@^(k%cYDT2cDzfQ#$wh>2p24|0{XLI0wgReMymE!QVK9w{uQPI3cp6+18Zmt#o z&=>Aeyn@E;55;usqH)>4sG+HiKP){|4H?QryC&t~$--!0 z;-+vSiXqE&lqV)SXVz9G*^~Hmtk2EmZnjuEch&;5UTULke#@;THfoUitXvqMc0l7n z4cmaAcjcjox1HC52a~T()yW1=X&|iY+i+MoQt7P@ZMpv|vJyJ^`>bnh2UQ3{mD>>} zdlb>mJ#`hlN5>&uu-{*>5qgz-(~`L5iVR zIaYy>fGoY!uH`M0AU)wlOGlUa-M7Y&vK^kM(;_GplCL}fh3l@f8taFg_q(b(4OB=Q z03d0iC&T#}QkzWUPp6I%Y_A?U;MZ`2 zNkKs=S;i(Z(%?GHqV7k-?#m|Woum%UkbHWw<)OVrud=PZaaW=-hIDmIk5*4)tJoV= zX;gXG!$|=bMRjw@j^2n_+vy%bkq-TL`x1r=uE$kDwxwbE!Gti#K!P{2BW)~a&FP^) zQH{@p0*zg$r^}A&`c-))6{K*&37f~WZH*Wn17os_Crr1{pdF4I+v3+pZF=gnH-7lO zwW=TA@j-9S7DIjOqmp}J8NK#>jsDx!p*IKx)wlW5R)5X=>GF7B%!hr{{4QN#ESqh_ zC1Epe1yD=<^x1g%0nnJsO#|x$UH&G3Aefm_n=QhFTyH3NZOyWj5lqsKkC#=jE>ey7 z2JCS3TSGbOg$b7@H~P#b-aUaM0unj7$PlcFL3#C&LCJHLVl4Dmc7*(5KP2?iPC%Kc95^u}Y z&T-cDdpUHGcYh^FQA1tzQr9b6>C3(_zEL5tO4aRgL@QI_BRY59zBM40v%pfY=G^9$ zu8ABs{FN`utM=`(DEm-G`tqTaMY@QcCdTbS$l?N%(_VQ?SQ{1?nq2m>pI5d*Bf-%? zIaB#YlSX$=GG6yE<$%qNTvso!?4-+52 zV>vt;3~KsPh`ZTt9egq658O>^xs!G|hX%O%m!d)!i`XF57Cx27Z8mu{#aStm_)8|Yo<>P~*QMRL(Eu+KB}VDDWsrv&B0=6|6JxH(?A!JiOeo812=f)ukFuVs*=JG0OX7x~ijffk{-A@Kutk+o{>yCY#_))pO>OZ*1YoPP5{p_ zuU5Qd?pOY{op~T{UjB8h>U$9KXt+2lmQvT!3FtG-dNQksgp+y-BpF{bQTW}jFTS5T z@>Z&aKc8z~+49@feNsZ5vde_aX+HFp>{XJoU=3$oo4sJA=ObFi150x9S(3>?!6Sd8 z3$U$kG}+@?vK9F~Je)1Jw@ z8re?J;m${<=XTw z5b{n*V8zevynP;k3s$b(WZf)M?cSLHO2>qrA-A4EmZ6mE>aMxZfavwc0Zp8)@M+Ps zhsla^8|8!AS_+G@`xonvce*AcHi4Rq55pC?YBtC&J(`ncIs*CvZHmgPQS06cGtNT- z+Ub06GDAqUYM>rs%&l3n^u4a&GWEMd2XS1n41OV$ccWl?EyE*DQ|5VyGR6}3QYJ_> z`!r|O(wL-}3XgQzx};-G`NsQOJ-$CWxg9#{8D^oTQX%0~U^d2f>!)OZf(tAp=rS|m1X9|10R!w~_9ha~ zc$#$JJD(u#*Y9y2JT`Ggm(!=L`9I4gUJmBGIcjLKJUGUG`Sl5{VAMYIN=nQr%dgu< zc6in|6^5;BPbM2fLDBF8%dq{nZlrhq9)?vT0|EV{^i|sw)*~sK^0|O9$0RlC_O@1x zx%@^;)t#gn9lregSBJ_~`o@=3^ey(zSzA`VeFB%QMw)0I_-dPA$!uPt}vaD5!lwAtT%J9&c4>6t7l*W;xj*Is~&NW0a|fabMvX^ zBeW@2K#tO}RV5x76LZ2Hjenp=E2(LPtH-b0lDmlQVO9xMF@+|vxMO;lFVE6>tBgFk z-L!~1*OVYVBYAQs0V1*;DHQpjYKc9pYw?7am4fnuXx}?TmvDVsqh{(Y3N|z6TPD`w zQUk1pX8ox`28cM>-2HT(lNW>g1*UqTzsdrc++%@F_9V)=K9<1_V|7xL8w^IrmW87g z_dUFUJ$lf<6cRPh7=w=QkW&dT%HkxPYyc~^mVy}=zIT1kVOydMP=#mH@c5RD#$SZ` zwxGTNqPuUqz+r#aL`l(kfvRnhoe}?&acgCG!d80r!6DTyq5+ZaW&jrl>esaw>34mT zqpTN4X>+1)gTx|x%aoB{S7QzMbcoU8V0_P@$*(HkEtN-`p(!qN4st= zoleL|is6_zGmLTJdWNm<0%8||r3O|)!JZ|Em^uk}=cbd+zt9Jkv;6%qA1VD=AaSAvAlN<-gKl7(7$0i4 z7z=21OlxLPQ7i-!0)qS24vWmeoa+Eht-=@rC@!JJhElIP##|v$`UdxZfnh$2QHDMZ z+Gq1r*D)70#jU+xLV=0Cx;~_2dJ6G8_#bA1>gp{vMWzi<*$Lfb!m6AR0GuniW|TXK_>|~v-;wDkz|zg z+M*5etVHCXzW}7{?$JbEx^8_(|E5ilBOI>$8PnDyt-WAk1#MzH8G6e)@YB)v0XJ;h zw}6rNQHrWWa`9UJ2h;N_eEzgyS6;~hq-KY-NdN^%_yx6EjGM`7=O_U=atC+2pvAmB zxBK1p@5F>|chRDs6}Q?%8)qeJ?ygiasU-t)d`y@`@5->BT6+RaMZC5=CWA%dnstbqLQHJWg*m(9dbPJa8{U}A82jMDmhLv$jYELS zRUBklC^mVsARV>fy`-UX^jIDs&kX*;!TUtL00q_HT{d&Qgf!Ddf2ED*JnrFqcyaCT z0&Vv>1DR1A;t~?zpmy01zKKr)I!-STyCbdA<{oiwtyvsdxivlQDnzxTZC%{~4z1+- z+~0;xyrJLIcfvmU;hwUJYnD!T`OCcxtxS|!9N+EH;MPPnDCH0EYyCL!0@HJiLgtK< zTu%vqXikIIq|hw!`2@UtBrl14#FLC?;~1~9yrh(BX(Ey`P&3lxZ|C+c+uh^cE#X#k z36d{jvyob+#`K=^90weE_kKCXvZ z!tWrHx}pJ`6sA(*?Q5K0U<)q04M! zrOV?_gSmh*XpUu)ZYzCn-sEk+R<@{ae|nAS%P*k=E??agE)s38{9n*;7(Mq}?1uj=N zyK>L2)#csOz(q^K#ORZ96}VHaF1RN?s;F+WmW2>R1ftg*m^$X2P?1prmAvo477I$V z(2+;rLpyg7uva@4Vy)ZU0wEx}baK zW^7P8nzkwmQqwz~ySwYAP{W;ItFYN^xq>4{`c7r3XI_`$Et{fDh#%XBsB}{_RlMeN zce63YnN=KOZS!i&?zpf7C7fdnt+F~EJ0s+qy(oP$6U?n8fvXPLvdQIi68?m@=Zsg0 zMiDL-ja5NSX9TpnqJL=7ZYQeBLZBj_rP#%N16bT%I-aR_`s*A@EGi} z^IDE#GX%caiz&{6s>lnP?|&~J`s8)pB6ah|7d5}h3dbc-X?ozGZ;L90l5s?_jm!yGbm2C)AHO{r z8CTzZwOe}X69wbu4Gu9-nVTwA56ztJ9hspK75ZR5QIAC_^CBb%^M6*&ibT~w1hDf% zM9zg-fq%TeT=593O0gw!lX1C{wb@`CQ9tA{@Tica{xdnMdvS$#Kd;ey26720>k+DwV9VOg?DM?m~s z;D?^}skU?l%0zP~dd&FY%SlASDE*ChTQYFvrCsRIl_ObjNvrGqz%(W&b3n3Dp(=b^ zAybOdM4T+q5Dcc5a`HNia)Z;CIC){Bb{)~oR!a(pa@UeQmm9Vzh+Gm$25;HYPSIqX z_Vz|ccuO3IM;ptxK8)2C+OAtIGPKl0`hx9`nnOXEg5pAQF!e2YEPdm4g6R6uveF4P zIREk?G(`4?4U+Q!64M-+XW8OuGUN6D29qF5v_5{4%mmXWvRBKKBI1F>laDB}UrdQa z$A%Jejq!T(_L28Jcz~4}oSt1G6)_=CW6aY<=xuHE$&9uIH%1l3N*C@Y65NQ#QJkq- z2wM*y>wSfaPbFRtKv^$GP2Lj99Zd3AB+{ZeuDJ9VpMMw*>uAqwOyn8UdST2&~4r!-@_s> z@ae@g4a#&Tx0cWw02<7s8f^TT_TeJ6>jAX7FrDqkbi9oc-#~$6o)#pvkGZ-S0)Cn3;qmrK`?KNpx2^H(Fkm54NjH zOw61CqT>nn1yXC3ZMNqQO11k;#ChAOUdFfYoz*u z7^MO~t@CdeZ2?^oVk6y-!DQvs-RjG;D?ZEWvjV$owT5-*m+Ej0C1nDKRY`Tv})RGr`oL)U-IV=BErADgK#q@;VmZowv|Xu-ni!LKJ3UgITm z%tSw8`};iXeo+nd6WYVZNSpXzBwfbdOS;sg+36wS4MtZPw0_+d&ImwOxBI33W?H;` zV2baJ(raSFfZ3@s0Mn?{`@Y6ZStmu{D7tW+XhqzDGiKX zc1Ujdg=SZgc8YB3wRG{SfX%&N@Na!7H3Nyv%j5g&%c*{p=xG<#i-4}=6JXhlp3WNhJ#wn~XBGFC!VHSP95MY~ zJVP11hOoH1w1*ybw6tWCnb|{sS?miT|In0_5raK_`-O}{ zZv4v+DQU}Vo9uKHqbw@URS(zQKlztKHc?Q`Qc>@&&Y0F-9H9Ya$utS>b>h#(-ciF2?kw%UsnEFAg*b?qNDiz z=>M)5K(DCI(vqFsG}F5Jw}s>{m*yptr2api#3BrgNwup~_E`QsyheB`eY__L4v(-bIa|Lp94LX^LURFpte{U_G?=ZO6Akv~52PmB2DBY*ah z|BS$&qwb$G`#({RKPbvSmrDS<{{J-`MB<4VsAl99blV^U{_fw^Rx7$~_4@w-YU6_$ literal 0 HcmV?d00001 diff --git a/docs/logo.meta.txt b/docs/logo.meta.txt new file mode 100644 index 00000000..13daba05 --- /dev/null +++ b/docs/logo.meta.txt @@ -0,0 +1,6 @@ +logo is created by https://www.logoly.pro + +font: Francois One + +logo.fond-size: 65 +logo-social.fond-size: 160 diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..371650a6c39755ba63a388daed8765c048102287 GIT binary patch literal 10954 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 literal 0 HcmV?d00001 From f8ace085dfe90007a0fa2c652192512ae36c3ab9 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 23 Jan 2022 13:41:39 +0800 Subject: [PATCH 099/175] docs: improve README.md --- README.md | 41 ++++++++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index cce7afb7..e87a0ccf 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,15 @@ GitHub repo size

-👉 把平时有用的手动操作做成脚本,以便捷地使用。 ✨ - +🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc. +👉 平时有用的手动操作做成脚本,以便捷地使用,让开发的日常生活更轻松些。 💕 欢迎 👏 💖 -- 建议和提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new) -- 贡献和改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork) -- 分享 平时常用但没有写成脚本的功能(即需求、想法),[提交Issue](https://github.com/oldratlee/useful-scripts/issues/new) -- 提供 自己的好用脚本,[Fork 后提通过 Pull Request 提供](https://github.com/oldratlee/useful-scripts/fork) +- 提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 分享平时自己常用但没有写成脚本的功能(即需求、想法),[提交Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 优化改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork) +- 提供的自己好用脚本实现,[Fork 后提通过 Pull Request 提供](https://github.com/oldratlee/useful-scripts/fork) 本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 @@ -107,14 +107,14 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r ## 🎓 Developer Guide -为用户提供有用的功能当然是这个库的首要的价值体现和存在理由。 +为用户提供有用的功能,当然是这个库的首要的价值体现和存在理由。 但作为一个**开源**项目,每个人都可以看到源码实现,这个库或许能做得更多。 ### 🎯 面向开发者的目标 -- 期望体现`Shell/Bash`脚本生产环境级的严谨开发方式与最佳实践,进而有可能示例改善在生产环境中`Shell`脚本的质量状况。 -- 将`Shell/Bash`看作线上生产环境可使用的专业语言。 +- 将`Shell/Bash`作为线上生产环境使用的专业编程语言。 +- 期望体现`Shell/Bash`脚本 生产环境级的严谨开发方式与最佳实践,进而有可能示例与改善在生产环境中`Shell`脚本的质量状况。 PS: @@ -123,21 +123,32 @@ PS: #### 关于`Shell`脚本 -命令行(`CLI`)几乎是每个程序员几乎每天都要使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。 +命令行(`CLI`)几乎是每个程序员每天都在使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。 命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常会看到`Shell`脚本(虽然不如主流语言那么常见)。 -可能正因为上面所说的`Shell`脚本的便利性和大众性,`Shell`脚本有不少是顺手实现的(包括生产环境用的`Shell`脚本),`Shell`脚本的实现常常可能质量不高。 +可能正因为上面所说的`Shell`脚本的便利性和大众性: + +- `Shell`脚本有不少是顺手实现的(包括生产环境用的`Shell`脚本); +- `Shell`脚本的实现常常可能质量不高,会引发线上严重的故障。 ### 🚦 开发约定 -这个库里,`Shell`脚本统一使用`Bash 3+`。 +在这个库中的`Shell`脚本: -原因是: +- 统一使用`Bash 3+`; +- 面向生产环境,尽可能使用严谨安全的开发方式。 +`Shell`用`Bash`的原因是: + +- 目前仍然是主流的`Shell`,并且在不同环境基本上都缺省部署了。 +- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/)中,明确说明了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 + - 有大量的`Shell`实现,`sh`、`bash`、`zsh`、`fish`、`csh`、`tcsh`、`ksh`、`ash`、`dash`…… + - 不同的`Shell`有各种差异,深坑勿入。 + - 统一用`Bash`,可以避免差异带来的风险与没有收益的复杂性。 - 个人系统学习过的是`Bash`,比较理解熟悉。 -- `Bash`目前还是`Shell`编程主流,并且基本上缺省部署了。 - PS: 虽然交互`Shell`个人已经使用`Zsh`,但在严谨的`Shell`脚本开发时还是使用`Bash`。 + +PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/),但在严谨的`Shell`脚本开发时还是使用`Bash`。 ### 📚 `Shell`学习与开发的资料 From 9fd82c16ebfcbf881220ba11c6ac6d3a93d1cc3c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 23 Jan 2022 17:32:12 +0800 Subject: [PATCH 100/175] refactor: declare var as `readonly` if possible, more info in Google's Shell Style Guide: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 常量和环境变量名 https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/naming_conventions/#id4 - 只读变量 https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/naming_conventions/#id6 --- bin/a2l | 3 +++ bin/ap | 2 ++ bin/c | 3 +++ bin/cp-into-docker-run | 11 +++++++++-- bin/find-in-jars | 11 +++++++++-- bin/rp | 2 ++ bin/show-busy-java-threads | 16 +++++++++++++--- bin/tcp-connection-state-counter | 1 + bin/uq | 1 + bin/xpl | 16 ++++++++++------ 10 files changed, 53 insertions(+), 13 deletions(-) diff --git a/bin/a2l b/bin/a2l index 7202ed7e..31e5f82e 100755 --- a/bin/a2l +++ b/bin/a2l @@ -12,6 +12,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -87,6 +88,8 @@ while [ $# -gt 0 ]; do esac done +readonly args + ################################################################################ # biz logic ################################################################################ diff --git a/bin/ap b/bin/ap index 7db14ae8..126b3a52 100755 --- a/bin/ap +++ b/bin/ap @@ -14,6 +14,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -125,6 +126,7 @@ while [ $# -gt 0 ]; do done [ ${#files[@]} -eq 0 ] && files=(.) +readonly files for f in "${files[@]}"; do ! [ -e "$f" ] && { diff --git a/bin/c b/bin/c index 21f76777..d94dac18 100755 --- a/bin/c +++ b/bin/c @@ -22,6 +22,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -109,6 +110,8 @@ while [ $# -gt 0 ]; do esac done +readonly eol quiet args + ################################################################################ # biz logic ################################################################################ diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 99de0525..15849c0e 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -10,6 +10,7 @@ set -eEuo pipefail PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -163,6 +164,8 @@ while (($# > 0)); do esac done +readonly container_name docker_user docker_workdir docker_tmpdir docker_command_cp_path verbose args + [ -n "$container_name" ] || usage 1 "No destination docker container name, specified by option -c/--container!" @@ -198,6 +201,11 @@ if [ ! -f "$specified_run_command" ]; then fi run_command="$(portableReadLink "$run_command")" run_command_base_name="$(basename "$run_command")" +readonly run_command run_command_base_name + +run_timestamp="$(date "+%Y%m%d_%H%M%S")" +readonly run_timestamp +readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" if [ -n "${docker_command_cp_path}" ]; then if isAbsolutePath "$docker_command_cp_path"; then @@ -206,9 +214,8 @@ if [ -n "${docker_command_cp_path}" ]; then readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" fi run_command_dir_in_docker="$(dirname "$run_command_in_docker")" + readonly run_command_dir_in_docker else - run_timestamp="$(date "+%Y%m%d_%H%M%S")" - readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" diff --git a/bin/find-in-jars b/bin/find-in-jars index 786ef478..5bd7e802 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -27,6 +27,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -229,12 +230,14 @@ while (($# > 0)); do 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 -extensions=${extensions:-jar} +readonly extensions=${extensions:-jar} -(("${#args[@]}" == 0)) && usage 1 "No find file pattern!" +(("${#args[@]}" == 0)) && usage 1 "Missing file pattern!" (("${#args[@]}" > 1)) && usage 1 "More than 1 file pattern: ${args[*]}" readonly pattern="${args[0]}" @@ -249,6 +252,7 @@ for d in "${dirs[@]}"; do done # set dirs to Absolute Path $use_absolute_path && dirs=("${tmp_dirs[@]}") +readonly dirs # convert extensions to find -iname options find_iname_options=() @@ -257,6 +261,7 @@ for e in "${extensions[@]}"; do 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! @@ -291,6 +296,8 @@ __prepareCommandToListZipEntries() { 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 diff --git a/bin/rp b/bin/rp index f0f70eee..ec919f46 100755 --- a/bin/rp +++ b/bin/rp @@ -14,6 +14,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ @@ -112,6 +113,7 @@ else fi [ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" +readonly files relativeTo for f in "${files[@]}"; do ! [ -e "$f" ] && { diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index dc02237d..a1cfe81e 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -21,6 +21,7 @@ # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 @@ -35,6 +36,7 @@ readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # # NOTE: DO NOT declare var USER as readonly, because its value is supplied by subshell. USER="$(whoami)" +readonly USER ################################################################################ # util functions @@ -219,6 +221,7 @@ ARGS=$( echo usage 1 } +readonly ARGS eval set -- "${ARGS}" count=5 @@ -265,7 +268,7 @@ while true; do shift ;; -l | --lock-info) - more_lock_info=-l + lock_info=-l shift ;; -h | --help) @@ -281,16 +284,19 @@ while true; do esac done -update_delay=${1:-0} +readonly count cpu_sample_interval force mix_native_frames lock_info +readonly update_delay=${1:-0} isNonNegativeFloatNumber "$update_delay" || die "update delay($update_delay) is not a non-negative float number!" [ -z "$1" ] && update_count=1 || update_count=${2:-0} isNaturalNumber "$update_count" || die "update count($update_count) is not a natural number!" +readonly update_count if [ -n "$pid_list" ]; then pid_list="${pid_list//[[:space:]]/}" # delete white space isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" fi +readonly pid_list # check the directory of append-file(-a) mode, create if not exist. if [ -n "$append_file" ]; then @@ -307,6 +313,7 @@ if [ -n "$append_file" ]; then fi fi fi +readonly append_file # check store directory(-S) mode, create directory if not exist. if [ -n "$store_dir" ]; then @@ -317,6 +324,7 @@ if [ -n "$store_dir" ]; then mkdir -p "$store_dir" || die "fail to create directory $store_dir(specified by option -S, for storing output files)!" fi fi +readonly store_dir isNonNegativeFloatNumber "$cpu_sample_interval" || die "cpu sample interval($cpu_sample_interval) is not a non-negative float number!" @@ -349,6 +357,7 @@ elif command -v jstack &>/dev/null; then else die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." fi +readonly jstack_path ################################################################################ # biz logic @@ -356,6 +365,7 @@ fi # NOTE: DO NOT declare var run_timestamp as readonly, because its value is supplied by subshell. run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" +readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" readonly tmp_store_dir="/tmp/${uuid}" @@ -520,7 +530,7 @@ printStackOfThreads() { local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" [ -f "${jstackFile}" ] || { # shellcheck disable=SC2206 - local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid}) + local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $lock_info ${pid}) if [ "${user}" == "${USER}" ]; then # run without sudo, when java process user is current user logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}" diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 7231320b..b7cc6533 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -12,6 +12,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ diff --git a/bin/uq b/bin/uq index c50ba1b9..0cd36a82 100755 --- a/bin/uq +++ b/bin/uq @@ -24,6 +24,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' ################################################################################ diff --git a/bin/xpl b/bin/xpl index dbe1f13d..7be8ec1f 100755 --- a/bin/xpl +++ b/bin/xpl @@ -21,6 +21,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. PROG="$(basename "$0")" +readonly PROG readonly PROG_VERSION='2.5.0-dev' readonly nl=$'\n' # new line @@ -61,6 +62,7 @@ progVersion() { ################################################################################ declare -a args=() +selected=false while [ $# -gt 0 ]; do case "$1" in -s | --selected) @@ -88,28 +90,30 @@ while [ $# -gt 0 ]; do esac done +readonly args selected + ################################################################################ # biz options ################################################################################ # open one file openOneFile() { - local file="$1" + local file="$1" slt="${selected}" case "$(uname)" in Darwin*) - [ -f "${file}" ] && selected=true - open ${selected:+-R} "$file" + [ -f "${file}" ] && slt=true + open ${slt:+-R} "$file" ;; CYGWIN*) - [ -f "${file}" ] && selected=true - explorer ${selected:+/select,} "$(cygpath -w "${file}")" + [ -f "${file}" ] && slt=true + explorer ${slt:+/select,} "$(cygpath -w "${file}")" ;; *) if [ -d "${file}" ]; then nautilus "$(dirname "${file}")" else - if [ -z "${selected}" ]; then + if [ -z "${slt}" ]; then nautilus "$(dirname "${file}")" else nautilus "${file}" From 9902012e4e1959d83fe497fce02a9880dea42e55 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 23 Jan 2022 17:44:45 +0800 Subject: [PATCH 101/175] docs: update NOTE comments --- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 14 ++++++++++---- bin/find-in-jars | 16 +++++++++++----- bin/rp | 2 +- bin/show-busy-java-threads | 32 ++++++++++++++++++-------------- bin/tcp-connection-state-counter | 2 +- bin/uq | 16 +++++++++++----- bin/xpl | 14 ++++++++++---- legacy-bin/cp-svn-url | 16 +++++++++++----- lib/console-text-color-themes.sh | 14 ++++++++++---- 11 files changed, 85 insertions(+), 45 deletions(-) diff --git a/bin/a2l b/bin/a2l index 31e5f82e..5d84fcde 100755 --- a/bin/a2l +++ b/bin/a2l @@ -10,7 +10,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/bin/ap b/bin/ap index 126b3a52..6bf557c5 100755 --- a/bin/ap +++ b/bin/ap @@ -12,7 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/bin/c b/bin/c index d94dac18..b19b608e 100755 --- a/bin/c +++ b/bin/c @@ -11,16 +11,22 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/bin/find-in-jars b/bin/find-in-jars index 5bd7e802..19560d11 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -16,16 +16,22 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' @@ -56,7 +62,7 @@ redEcho() { # Getting console width using a bash script # https://unix.stackexchange.com/questions/299067 # -# NOTE: DO NOT declare columns var as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare columns var as readonly in ONE line! [ -t 2 ] && columns=$(stty size | awk '{print $2}') printResponsiveMessage() { diff --git a/bin/rp b/bin/rp index ec919f46..17f22d12 100755 --- a/bin/rp +++ b/bin/rp @@ -12,7 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index a1cfe81e..814a0163 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -11,15 +11,21 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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) -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' @@ -34,7 +40,7 @@ readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # See https://www.lifewire.com/current-linux-user-whoami-command-3867579 # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! # -# NOTE: DO NOT declare var USER as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var USER as readonly in ONE line! USER="$(whoami)" readonly USER @@ -210,9 +216,7 @@ uname | grep '^Linux' -q || die "$PROG only support Linux, not support $(uname) # parse options ################################################################################ -# NOTE: ARGS can not be declared as readonly!! -# readonly declaration make exit code of assignment to be always 0, aka. the exit code of `getopt` in subshell is discarded. -# tested on bash 4.2.46 +# DO NOT declare var ARGS as readonly in ONE line! ARGS=$( getopt -n "$PROG" -a -o c:p:a:s:S:i:Pd:FmlhV \ -l count:,pid:,append-file:,jstack-path:,store-dir:,cpu-sample-interval:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ @@ -363,7 +367,7 @@ readonly jstack_path # biz logic ################################################################################ -# NOTE: DO NOT declare var run_timestamp as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var run_timestamp as readonly in ONE line! run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" @@ -424,7 +428,7 @@ findBusyJavaThreadsByPs() { # shellcheck disable=SC2206 local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) - # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + # DO NOT combine var ps_out declaration and assignment in ONE line! local ps_out ps_out="$("${ps_cmd_line[@]}" | sort -k3,3nr)" [ -n "$ps_out" ] || __die_when_no_java_process_found @@ -442,7 +446,7 @@ findBusyJavaThreadsByPs() { # top with output field: thread id, %cpu __top_threadId_cpu() { - # DO NOT combine var java_pid_list declaration and assignment, because its value is supplied by subshell. + # DO NOT combine var java_pid_list declaration and assignment in ONE line! local java_pid_list # shellcheck disable=SC2086 java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" @@ -465,14 +469,14 @@ __top_threadId_cpu() { # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") - # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + # DO NOT combine var ps_out declaration and assignment in ONE line! local top_out top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" fi - # DO NOT combine var result_threads_top_info declaration and assignment, because its value is supplied by subshell. + # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! local result_threads_top_info result_threads_top_info=$( echo "$top_out" | awk '{ @@ -493,7 +497,7 @@ __complete_pid_user_by_ps() { # ps output field: pid, thread id(lwp), user # shellcheck disable=SC2206 local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) - # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell. + # DO NOT combine var ps_out declaration and assignment in ONE line! local ps_out ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index b7cc6533..abaf97ca 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -10,7 +10,7 @@ # @author @sunuslee (sunuslee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/bin/uq b/bin/uq index 0cd36a82..ee7e4527 100755 --- a/bin/uq +++ b/bin/uq @@ -13,16 +13,22 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' @@ -205,7 +211,7 @@ done [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 -# NOTE: DO NOT declare var uq_max_input_size as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var uq_max_input_size as readonly in ONE line! uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" diff --git a/bin/xpl b/bin/xpl index 7be8ec1f..caa6c36f 100755 --- a/bin/xpl +++ b/bin/xpl @@ -10,16 +10,22 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 30bafcb0..8e055e8e 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -11,15 +11,21 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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) -# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG_VERSION='2.5.0-dev' @@ -66,7 +72,7 @@ done readonly dir="${1:-.}" -# NOTE: DO NOT declare var url as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var url as readonly in ONE line! url="$(svn info "${dir}" | awk '/^URL: /{print $2}')" if [ -z "${url}" ]; then echo "Fail to get svn url!" 1>&2 diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 4e6cdc54..dc9c9e12 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -7,20 +7,26 @@ # # NOTE about Bash Traps and Pitfalls: # -# 1. DO NOT combine var declaration and assignment which value supplied by subshell! +# 1. DO NOT combine var declaration and assignment which value supplied by subshell in ONE line! # for example: readonly var1=$(echo value1) -# local var1=$(echo value1) +# local var2=$(echo value1) # -# declaration make exit code of assignment to be always 0, +# 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) _ctct_READLINK_CMD=readlink if command -v greadlink > /dev/null; then _ctct_READLINK_CMD=greadlink fi -# NOTE: DO NOT declare var _ctct_PROG as readonly, because its value is supplied by subshell. +# NOTE: DO NOT declare var _ctct_PROG as readonly in ONE line! _ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")" [ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true From 44f8091386e6e7e8f2a795157e9ce24a6923ffe1 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 28 Nov 2022 23:32:34 +0800 Subject: [PATCH 102/175] chore(ci): add github action `ci.yaml` --- .github/workflows/ci.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/ci.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..a854fb64 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,18 @@ +# Quickstart for GitHub Actions +# https://docs.github.com/en/actions/quickstart + +name: CI +on: [ push, pull_request, workflow_dispatch ] +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + fail-fast: false + max-parallel: 32 + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - run: test-cases/integration-test.sh From 1090d3949072a0ad60a0b18c385ae4c9b090c722 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 28 Nov 2022 23:47:17 +0800 Subject: [PATCH 103/175] docs: use GitHub actions instead of `travis` --- .travis.yml | 38 -------------------------------------- README.md | 3 +-- 2 files changed, 1 insertion(+), 40 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 20fcbd24..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -# https://github.com/caarlos0-graveyard/shell-ci-build -# https://github.com/kward/shunit2/blob/master/.travis.yml -language: shell - -addons: - homebrew: - packages: - - coreutils - - gnu-sed - -# The Ubuntu Linux Build Environments -# https://docs.travis-ci.com/user/reference/linux/ -# The macOS Build Environment -# https://docs.travis-ci.com/user/reference/osx/ -# Installing Dependencies -# https://docs.travis-ci.com/user/installing-dependencies/#installing-packages-on-macos -jobs: - include: - - os: osx - osx_image: xcode12.5 - - os: linux - dist: precise - - os: linux - dist: xenial - - os: linux - dist: focal - -env: - # Is it possible to `brew install` without updating? · Issue #1670 · Homebrew/brew - # https://github.com/Homebrew/brew/issues/1670 - # python - How to not update homebrew automatically when brew install some packages? - Stack Overflow - # https://stackoverflow.com/questions/41530314 - - HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_AUTO_UPDATING=0 HOMEBREW_UPDATE_PREINSTALL=0 HOMEBREW_NO_ANALYTICS=1 -script: - - test-cases/integration-test.sh - -after_script: - - git status --ignored diff --git a/README.md b/README.md index e87a0ccf..e100393b 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,9 @@ repo-icon

-Build Status +Github Workflow Build Status GitHub release License -Chat at gitter.im GitHub Stars GitHub Forks GitHub issues From 98282f7aeef0036ae522879b1e9b3c8b5ba58b45 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 19:19:48 +0800 Subject: [PATCH 104/175] docs: fix github action badges https://github.com/badges/shields/issues/8671 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e100393b..a87326a0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ repo-icon

-Github Workflow Build Status +Github Workflow Build Status GitHub release License GitHub Stars From 144feb926be1875195418220fd99e21406474a10 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 19:44:56 +0800 Subject: [PATCH 105/175] feat: print info message and exit when search no jar files or find no class files --- bin/show-duplicate-java-classes | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 1a83dbdd..08689ab1 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -8,7 +8,7 @@ # $ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 # $ show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 # $ show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1 -# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the sub-directories of lib dir +# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the subdirectories of lib dir # $ show-duplicate-java-classes -J path/to/lib_dir1 # search jars in the jar file # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-duplicate-java-classes @@ -297,6 +297,9 @@ def print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to def print_class_paths_info(class_path_to_classes): + if not class_path_to_classes: + return + max_idx_str_len = str_len(len(class_path_to_classes)) max_classes_count_str_len = str_len(max(len(classes) for classes in class_path_to_classes.values())) @@ -329,14 +332,24 @@ def main(): action='append', help='add class dir') option_parser.add_option('-R', '--no-find-progress', dest='show_responsive', default=True, action='store_false', help='do not display responsive find progress') + options, lib_dirs = option_parser.parse_args() + class_dirs = options.class_dirs + if not lib_dirs and not class_dirs: + lib_dirs = ['.'] global __show_responsive __show_responsive = options.show_responsive - if not options.class_dirs and not lib_dirs: - lib_dirs = ['.'] jar_files = list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib) - class_path_to_classes = collect_class_path_to_classes(options.class_dirs, jar_files, options.recursive_jar) + if not jar_files and not class_dirs: + clear_responsive_message() + print('search no jar files under lib dirs, and class dirs is absent.') + return 0 + class_path_to_classes = collect_class_path_to_classes(class_dirs, jar_files, options.recursive_jar) + if all(not classes for classes in class_path_to_classes.values()): + clear_responsive_message() + print('find no class files in jar files or class dirs.') + return 0 print_responsive_message('find duplicate classes...') class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) From cee95d00183a71144d8d4111a2da1c572eaf104a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 20:20:55 +0800 Subject: [PATCH 106/175] chore(ci): add ci on `macos` --- .github/workflows/ci.yaml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a854fb64..d5964f54 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -4,15 +4,30 @@ name: CI on: [ push, pull_request, workflow_dispatch ] jobs: - test: + + test-on-ubuntu: runs-on: ubuntu-latest timeout-minutes: 5 + name: Test on ubuntu + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - run: test-cases/integration-test.sh + + test-on-macos: + runs-on: ${{ matrix.os }} + timeout-minutes: 5 strategy: + matrix: + os: [ macos-11, macos-12 ] fail-fast: false - max-parallel: 32 + max-parallel: 64 + name: Test on ${{ matrix.os }} steps: - uses: actions/checkout@v3 with: submodules: recursive + - run: brew install coreutils gnu-sed - run: test-cases/integration-test.sh From 5c7ebf4f1df4ea30546d3f3d323cf9c9034bc346 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 20:33:50 +0800 Subject: [PATCH 107/175] chore(ci): improve `ci.yaml`, use `expression` --- .github/workflows/ci.yaml | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d5964f54..99e91726 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,24 +3,15 @@ name: CI on: [ push, pull_request, workflow_dispatch ] -jobs: - test-on-ubuntu: - runs-on: ubuntu-latest - timeout-minutes: 5 - name: Test on ubuntu - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - run: test-cases/integration-test.sh +jobs: - test-on-macos: + test: runs-on: ${{ matrix.os }} timeout-minutes: 5 strategy: matrix: - os: [ macos-11, macos-12 ] + os: [ ubuntu-latest, macos-11, macos-12 ] fail-fast: false max-parallel: 64 name: Test on ${{ matrix.os }} @@ -30,4 +21,6 @@ jobs: with: submodules: recursive - run: brew install coreutils gnu-sed + # https://docs.github.com/en/actions/learn-github-actions/expressions + if: ${{ startsWith(matrix.os, 'macos') }} - run: test-cases/integration-test.sh From dff7a5568cdb0a19116984c73b080ccf8d3ecac4 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 20:54:38 +0800 Subject: [PATCH 108/175] style: adjust file format - update `.editorconfig` - shell files - use 2 spaces indentation - use `$()` instead of `\`\`` --- .editorconfig | 10 +- README.md | 36 +- bin/.editorconfig | 2 + bin/a2l | 86 ++--- bin/ap | 124 +++--- bin/c | 118 +++--- bin/coat | 18 +- bin/cp-into-docker-run | 212 +++++----- bin/echo-args | 12 +- bin/find-in-jars | 406 ++++++++++---------- bin/rp | 98 ++--- bin/show-busy-java-threads | 596 ++++++++++++++--------------- bin/tcp-connection-state-counter | 12 +- bin/uq | 248 ++++++------ bin/xpf | 36 +- bin/xpl | 126 +++--- docs/developer-guide.md | 8 +- docs/java.md | 1 - docs/shell.md | 1 - docs/vcs.md | 12 +- legacy-bin/cp-svn-url | 42 +- legacy-bin/svn-merge-stop-on-copy | 74 ++-- legacy-bin/swtrunk | 40 +- lib/console-text-color-themes.sh | 82 ++-- lib/parseOpts.sh | 536 +++++++++++++------------- test-cases/bump-scripts-version.sh | 48 +-- test-cases/integration-test.sh | 44 +-- test-cases/my_unit_test_lib.sh | 98 ++--- test-cases/self-installer.sh | 6 +- test-cases/uq_test.sh | 156 ++++---- 30 files changed, 1649 insertions(+), 1639 deletions(-) create mode 100644 bin/.editorconfig diff --git a/.editorconfig b/.editorconfig index 40d2df19..7c0b7d4c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,12 +4,16 @@ root = true charset = utf-8 end_of_line = lf insert_final_newline = true +tab_width = 4 indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.{xml,py,md,mkd,markdown}] indent_size = 4 -tab_width = 4 -[*.yml] -indent_size = 2 +[*.xml] +indent_style = tab [*.{md,mkd,markdown}] trim_trailing_whitespace = false diff --git a/README.md b/README.md index a87326a0..72090567 100644 --- a/README.md +++ b/README.md @@ -62,41 +62,41 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r ### ☕ [`Java`相关脚本](docs/java.md) 1. [show-busy-java-threads](docs/java.md#-show-busy-java-threads) - 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 + 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 1. [show-duplicate-java-classes](docs/java.md#-show-duplicate-java-classes) - 找出`jar`文件和`class`目录中的重复类。用于排查`Java`类冲突问题。 + 找出`jar`文件和`class`目录中的重复类。用于排查`Java`类冲突问题。 1. [find-in-jars](docs/java.md#-find-in-jars) - 在目录下所有`jar`文件里,查找类或资源文件。 + 在目录下所有`jar`文件里,查找类或资源文件。 ### 🐚 [`Shell`相关脚本](docs/shell.md) `Shell`使用加强: 1. [c](docs/shell.md#-c) - 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 + 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 1. [coat](docs/shell.md#-coat) - 彩色`cat`出文件行,方便人眼区分不同的行。 + 彩色`cat`出文件行,方便人眼区分不同的行。 1. [a2l](docs/shell.md#-a2l) - 按行彩色输出参数,方便人眼查看。 + 按行彩色输出参数,方便人眼查看。 1. [uq](docs/shell.md#-uq) - 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 + 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 1. [ap and rp](docs/shell.md#-ap-and-rp) - 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 + 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 1. [cp-into-docker-run](docs/shell.md#-cp-into-docker-run) - 一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 + 一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 1. [tcp-connection-state-counter](docs/shell.md#-tcp-connection-state-counter) - 统计各个`TCP`连接状态的个数。用于方便排查系统连接负荷问题。 + 统计各个`TCP`连接状态的个数。用于方便排查系统连接负荷问题。 1. [xpl and xpf](docs/shell.md#-xpl-and-xpf) - 在命令行中快速完成 在文件浏览器中 打开/选中 指定的文件或文件夹的操作,优化命令行与其它应用之间的操作流。 + 在命令行中快速完成 在文件浏览器中 打开/选中 指定的文件或文件夹的操作,优化命令行与其它应用之间的操作流。 `Shell`开发/测试加强: 1. [echo-args](docs/shell.md#-echo-args) - 输出脚本收到的参数,在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。用于调试脚本参数输入。 + 输出脚本收到的参数,在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。用于调试脚本参数输入。 1. [console-text-color-themes.sh](docs/shell.md#-console-text-color-themessh) - 显示`Terminator`的全部文字彩色组合的效果及其打印方式,用于开发`Shell`的彩色输出。 + 显示`Terminator`的全部文字彩色组合的效果及其打印方式,用于开发`Shell`的彩色输出。 1. [parseOpts.sh](docs/shell.md#-parseoptssh) - 命令行选项解析库,加强支持选项有多个值(即数组)。 + 命令行选项解析库,加强支持选项有多个值(即数组)。 ### ⌚ [`VCS`相关脚本](docs/vcs.md) @@ -161,18 +161,18 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) - 🎶 **Tips** - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) - 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 + 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) - 💎 **系统学习** - 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! + 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) - 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 - 官方资料 - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) - Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) diff --git a/bin/.editorconfig b/bin/.editorconfig new file mode 100644 index 00000000..5a38bfbe --- /dev/null +++ b/bin/.editorconfig @@ -0,0 +1,2 @@ +[show-duplicate-java-classes] +indent_size = 4 diff --git a/bin/a2l b/bin/a2l index 5d84fcde..f940fa51 100755 --- a/bin/a2l +++ b/bin/a2l @@ -24,19 +24,19 @@ readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end colorEcho() { - local color="$1" - shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + local color="$1" + shift + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } usage() { - cat <&2 - exit 1 + redEcho "Error: $*" 1>&2 + exit 1 } # How can I get the behavior of GNU's readlink -f on a Mac? # https://stackoverflow.com/questions/1055671 portableReadLink() { - local file="$1" uname - - uname="$(uname)" - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f "$file" - ;; - Darwin*) - if command -v greadlink >/dev/null; then - greadlink -f "$file" - else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" - fi - ;; - *) - die "NOT support uname($uname)!" - ;; - esac + local file="$1" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + die "NOT support uname($uname)!" + ;; + esac } 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 + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out <( - content="$(cat)" - # shellcheck disable=SC2086 - echo $eol "$content" | copy - ) >$out + # shellcheck disable=SC2015 + $quiet && local -r out=/dev/null || local -r out=/dev/stdout + tee >( + content="$(cat)" + # shellcheck disable=SC2086 + echo $eol "$content" | copy + ) >$out } if [ ${#args[@]} -eq 0 ]; then - teeAndCopy + teeAndCopy else - "${args[@]}" | teeAndCopy + "${args[@]}" | teeAndCopy fi diff --git a/bin/coat b/bin/coat index 2fcf2fa3..a86c2c0f 100755 --- a/bin/coat +++ b/bin/coat @@ -22,19 +22,19 @@ readonly eend=$'\033[0m' # escape end readonly -a ECHO_COLORS=(33 35 36 31 32 37 34) COUNT=0 rotateColorEcho() { - local message="$*" + local message="$*" - # skip color for white space - if [[ "$message" =~ ^[[:space:]]*$ ]]; then - echo "$message" - else - local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" - echo "${ec}[1;${color}m$message$eend" - fi + # skip color for white space + if [[ "$message" =~ ^[[:space:]]*$ ]]; then + echo "$message" + else + local color="${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" + echo "${ec}[1;${color}m$message$eend" + fi } # Bash read line does not read leading spaces # https://stackoverflow.com/questions/29689172 cat "$@" | while IFS= read -r line; do - rotateColorEcho "$line" + rotateColorEcho "$line" done diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 15849c0e..66648c0b 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -23,51 +23,51 @@ readonly eend=$'\033[0m' # escape end readonly nl=$'\n' # new line redEcho() { - # -t check: is a terminal device? - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" + # -t check: is a terminal device? + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } die() { - redEcho "Error: $*" 1>&2 - exit 1 + redEcho "Error: $*" 1>&2 + exit 1 } isAbsolutePath() { - [[ "$1" =~ ^/ ]] + [[ "$1" =~ ^/ ]] } # How can I get the behavior of GNU's readlink -f on a Mac? # https://stackoverflow.com/questions/1055671 portableReadLink() { - local file="$1" uname - - uname="$(uname)" - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f "$file" - ;; - Darwin*) - if command -v greadlink >/dev/null; then - greadlink -f "$file" - else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" - fi - ;; - *) - die "NOT support uname($uname)!" - ;; - esac + local file="$1" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + die "NOT support uname($uname)!" + ;; + esac } 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 + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out < 0)); do - case "$1" in - -c | --container) - container_name="$2" - shift 2 - ;; - -u | --docker-user) - docker_user="$2" - shift 2 - ;; - -w | --workdir) - docker_workdir="$2" - shift 2 - ;; - -t | --tmpdir) - docker_tmpdir="$2" - shift 2 - ;; - -p | --cp-path) - docker_command_cp_path="$2" - shift 2 - ;; - -v | --verbose) - verbose=true - shift - ;; - -h | --help) - usage - ;; - -V | --version) - progVersion - ;; - --) - shift - args=(${args[@]:+"${args[@]}"} "$@") - break - ;; - -*) - usage 2 "${PROG}: unrecognized option '$1'" - ;; - *) - # if not option, treat all follow args as command - args=(${args[@]:+"${args[@]}"} "$@") - break - ;; - esac + case "$1" in + -c | --container) + container_name="$2" + shift 2 + ;; + -u | --docker-user) + docker_user="$2" + shift 2 + ;; + -w | --workdir) + docker_workdir="$2" + shift 2 + ;; + -t | --tmpdir) + docker_tmpdir="$2" + shift 2 + ;; + -p | --cp-path) + docker_command_cp_path="$2" + shift 2 + ;; + -v | --verbose) + verbose=true + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + -*) + usage 2 "${PROG}: unrecognized option '$1'" + ;; + *) + # if not option, treat all follow args as command + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + esac done readonly container_name docker_user docker_workdir docker_tmpdir docker_command_cp_path verbose args [ -n "$container_name" ] || - usage 1 "No destination docker container name, specified by option -c/--container!" + usage 1 "No destination docker container name, specified by option -c/--container!" if [ -n "${docker_workdir}" ]; then - isAbsolutePath "$docker_workdir" || - die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" + isAbsolutePath "$docker_workdir" || + die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" elif [ -n "${docker_command_cp_path}" ]; then - isAbsolutePath "$docker_command_cp_path" || - die "when no docker workdir(-w/--workdir) is specified, the command path in docker to copy(-p/--cp-path) must be absolute path: $docker_command_cp_path" + isAbsolutePath "$docker_command_cp_path" || + die "when no docker workdir(-w/--workdir) is specified, the command path in docker to copy(-p/--cp-path) must be absolute path: $docker_command_cp_path" fi ################################################################################ @@ -194,10 +194,10 @@ command -v docker &>/dev/null || die 'docker command not found!' readonly specified_run_command="${args[0]}" run_command="$specified_run_command" if [ ! -f "$specified_run_command" ]; then - which "$specified_run_command" &>/dev/null || - die "specified command not exists and not found in PATH: $specified_run_command" + which "$specified_run_command" &>/dev/null || + die "specified command not exists and not found in PATH: $specified_run_command" - run_command="$(which "$specified_run_command")" + run_command="$(which "$specified_run_command")" fi run_command="$(portableReadLink "$run_command")" run_command_base_name="$(basename "$run_command")" @@ -208,25 +208,25 @@ readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" if [ -n "${docker_command_cp_path}" ]; then - if isAbsolutePath "$docker_command_cp_path"; then - readonly run_command_in_docker="$docker_command_cp_path" - else - readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" - fi - run_command_dir_in_docker="$(dirname "$run_command_in_docker")" - readonly run_command_dir_in_docker + if isAbsolutePath "$docker_command_cp_path"; then + readonly run_command_in_docker="$docker_command_cp_path" + else + readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" + fi + run_command_dir_in_docker="$(dirname "$run_command_in_docker")" + readonly run_command_dir_in_docker else - readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" + readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" - readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" - readonly run_command_dir_in_docker="$work_tmp_dir_in_docker" + readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" + readonly run_command_dir_in_docker="$work_tmp_dir_in_docker" fi cleanupWhenExit() { - [ -n "${work_tmp_dir_in_docker:-}" ] || return 0 + [ -n "${work_tmp_dir_in_docker:-}" ] || return 0 - # remove tmp dir in docker by root user - docker exec "${container_name}" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null + # remove tmp dir in docker by root user + docker exec "${container_name}" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null } trap cleanupWhenExit EXIT @@ -235,18 +235,18 @@ trap cleanupWhenExit EXIT ######################################## logAndRun() { - $verbose && echo "[$PROG] $*" 1>&2 - "$@" + $verbose && echo "[$PROG] $*" 1>&2 + "$@" } logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ - mkdir -p -- "$run_command_dir_in_docker" + mkdir -p -- "$run_command_dir_in_docker" logAndRun docker cp "$run_command" "$container_name:$run_command_in_docker" logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ - chmod +x "$run_command_in_docker" + chmod +x "$run_command_in_docker" logAndRun docker exec -i -t \ - ${docker_user:+"--user=$docker_user"} \ - ${docker_workdir:+"--workdir=$docker_workdir"} \ - "$container_name" \ - "$run_command_in_docker" "${args[@]:1:${#args[@]}}" + ${docker_user:+"--user=$docker_user"} \ + ${docker_workdir:+"--workdir=$docker_workdir"} \ + "$container_name" \ + "$run_command_in_docker" "${args[@]:1:${#args[@]}}" diff --git a/bin/echo-args b/bin/echo-args index 551f231f..82b76859 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -10,16 +10,16 @@ readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end echoArg() { - local index="$1" count="$2" value="$3" + local index="$1" count="$2" value="$3" - # if stdout is console, turn on color output. - [ -t 1 ] && - echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[1;36;40m$value$eend${ec}[1;31m]$eend" || - echo "${index}/${count}: [${value}]" + # if stdout is console, turn on color output. + [ -t 1 ] && + echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[1;36;40m$value$eend${ec}[1;31m]$eend" || + echo "${index}/${count}: [${value}]" } echoArg 0 $# "$0" idx=1 for a; do - echoArg $((idx++)) $# "$a" + echoArg $((idx++)) $# "$a" done diff --git a/bin/find-in-jars b/bin/find-in-jars index 19560d11..efcd9e24 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -55,8 +55,8 @@ readonly nl=$'\n' # new line readonly clear_line=$'\033[2K\r' redEcho() { - # -t check: is a terminal device? - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" + # -t check: is a terminal device? + [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" } # Getting console width using a bash script @@ -66,38 +66,38 @@ redEcho() { [ -t 2 ] && columns=$(stty size | awk '{print $2}') printResponsiveMessage() { - if ! $show_responsive || [ ! -t 2 ]; then - return - fi + 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 + 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 + if ! $show_responsive || [ ! -t 2 ]; then + return + fi - echo -n "$clear_line" >&2 + echo -n "$clear_line" >&2 } die() { - clearResponsiveMessage - redEcho "Error: $*" >&2 - exit 1 + 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 + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out < 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 + 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 @@ -249,12 +249,12 @@ 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!" + [ -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)") + # 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[@]}") @@ -263,9 +263,9 @@ 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") + (("${#find_iname_options[@]}" == 0)) && + find_iname_options=(-iname "*.$e") || + find_iname_options=("${find_iname_options[@]}" -o -iname "*.$e") done readonly find_iname_options @@ -274,62 +274,62 @@ readonly find_iname_options ################################################################################ __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!" + # `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 - - readonly command_to_list_zip_entries is_use_zip_cmd_to_list_zip_entries + 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" || { + 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!" >&2 - return 0 + 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 + } } ################################################################################ @@ -337,73 +337,73 @@ listZipEntries() { ################################################################################ searchJarFiles() { - printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." + printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." - local jar_files total_jar_count + local jar_files total_jar_count - jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" - [ -n "$jar_files" ] || die "No ${extensions[*]} file found!" + 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:]]/}" + 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" + 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 + 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 - 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 + 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' + [ -t 1 ] && local -r grep_color_option='--color=always' - local counter=1 total_jar_count jar_file + local counter=1 total_jar_count jar_file - read -r total_jar_count + 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 + while read -r jar_file; do + printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" + listZipEntries "${jar_file}" | __outputResultOfJarFile "${jar_file}" + done - clearResponsiveMessage + clearResponsiveMessage } searchJarFiles | findInJarFiles diff --git a/bin/rp b/bin/rp index 17f22d12..01ad6c47 100755 --- a/bin/rp +++ b/bin/rp @@ -27,26 +27,26 @@ readonly eend=$'\033[0m' # escape end readonly nl=$'\n' # new line colorEcho() { - local color="$1" - shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + local color="$1" + shift + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } 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 + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out <&2 - exit 1 + redEcho "Error: NO argument!" 1>&2 + exit 1 } if [ "${#files[@]}" -eq 1 ]; then - relativeTo=. + relativeTo=. else - argc="${#files[@]}" + argc="${#files[@]}" - # Get last argument - relativeTo="${files[argc - 1]}" - files=("${files[@]:0:argc-1}") + # Get last argument + relativeTo="${files[argc - 1]}" + files=("${files[@]:0:argc-1}") fi [ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" readonly files relativeTo for f in "${files[@]}"; do - ! [ -e "$f" ] && { - echo "$f does not exists!" - continue - } - realpath "$f" --relative-to="$relativeTo" + ! [ -e "$f" ] && { + echo "$f does not exists!" + continue + } + realpath "$f" --relative-to="$relativeTo" done diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 814a0163..4abd1778 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -54,74 +54,74 @@ readonly eend=$'\033[0m' # escape end readonly nl=$'\n' # new line colorEcho() { - local color=$1 - shift + local color=$1 + shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" } colorPrint() { - local color=$1 - shift + local color=$1 + shift - colorEcho "$color" "$@" - [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" - [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" + colorEcho "$color" "$@" + [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" } # shellcheck disable=SC2120 normalPrint() { - echo "$@" - [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" - [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" + echo "$@" + [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" } redPrint() { - colorPrint 31 "$@" + colorPrint 31 "$@" } greenPrint() { - colorPrint 32 "$@" + colorPrint 32 "$@" } yellowPrint() { - colorPrint 33 "$@" + colorPrint 33 "$@" } bluePrint() { - colorPrint 36 "$@" + colorPrint 36 "$@" } die() { - redPrint "Error: $*" 1>&2 - exit 1 + redPrint "Error: $*" 1>&2 + exit 1 } logAndRun() { - echo "$@" - echo - "$@" + echo "$@" + echo + "$@" } logAndCat() { - echo "$@" - echo - cat + echo "$@" + echo + cat } # Bash RegEx to check floating point numbers from user input # https://stackoverflow.com/questions/13790763 isNonNegativeFloatNumber() { - [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] + [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] } isNaturalNumber() { - [[ "$1" =~ ^[+]?[0-9]+$ ]] + [[ "$1" =~ ^[+]?[0-9]+$ ]] } isNaturalNumberList() { - [[ "$1" =~ ^([0-9]+)(,[0-9]+)*$ ]] + [[ "$1" =~ ^([0-9]+)(,[0-9]+)*$ ]] } # print calling(quoted) command line which is able to copy and paste to rerun safely @@ -129,27 +129,27 @@ isNaturalNumberList() { # How to get the complete calling command of a BASH script from inside the script (not just the arguments) # https://stackoverflow.com/questions/36625593 printCallingCommandLine() { - local arg isFirst=true - for arg in "${COMMAND_LINE[@]}"; do - if $isFirst; then - isFirst=false - else - printf ' ' - fi - printf '%q' "$arg" - done - echo + local arg isFirst=true + for arg in "${COMMAND_LINE[@]}"; do + if $isFirst; then + isFirst=false + else + printf ' ' + fi + printf '%q' "$arg" + done + echo } 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 + 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)) && colorEcho 31 "$*$nl" >$out + (($# > 0)) && colorEcho 31 "$*$nl" >$out - cat >$out <$out </dev/null; then - # 3. search jstack under PATH - jstack_path="$(command -v jstack)" - [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${nl}Use -s option set jstack path manually." + # 3. search jstack under PATH + jstack_path="$(command -v jstack)" + [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${nl}Use -s option set jstack path manually." else - die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." + die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." fi readonly jstack_path @@ -374,36 +374,36 @@ readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" readonly tmp_store_dir="/tmp/${uuid}" if [ -n "$store_dir" ]; then - readonly store_file_prefix="$store_dir/${run_timestamp}_" + readonly store_file_prefix="$store_dir/${run_timestamp}_" else - readonly store_file_prefix="$tmp_store_dir/${run_timestamp}_" + readonly store_file_prefix="$tmp_store_dir/${run_timestamp}_" fi mkdir -p "$tmp_store_dir" cleanupWhenExit() { - rm -rf "$tmp_store_dir" &>/dev/null + rm -rf "$tmp_store_dir" &>/dev/null } trap cleanupWhenExit EXIT headInfo() { - colorEcho "0;34;42" ================================================================================ - echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" - colorEcho "0;34;42" ================================================================================ - echo + colorEcho "0;34;42" ================================================================================ + echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" + colorEcho "0;34;42" ================================================================================ + echo } if [ -n "${pid_list}" ]; then - readonly ps_process_select_options="-p $pid_list" + readonly ps_process_select_options="-p $pid_list" else - readonly ps_process_select_options="-C java -C jsvc" + readonly ps_process_select_options="-C java -C jsvc" fi __die_when_no_java_process_found() { - if [ -n "${pid_list}" ]; then - die "process($pid_list) is not running, or not java process!" - else - die 'No java process found!' - fi + if [ -n "${pid_list}" ]; then + die "process($pid_list) is not running, or not java process!" + else + die 'No java process found!' + fi } # output field: pid, thread id(lwp), pcpu, user @@ -415,71 +415,71 @@ __die_when_no_java_process_found() { # the percentage of time spent running during the *entire lifetime* of a process, # this is not ideal in general. findBusyJavaThreadsByPs() { - # 1. sort by %cpu by ps option `--sort -pcpu` - # unfortunately, ps from `procps-ng 3.3.12`, `--sort` does not work properly with other options, - # use - # ps - # combined - # sort -k3,3nr - # instead of - # ps --sort -pcpu - # 2. use wide output(unlimited width) by ps option `-ww` - # avoid trunk user column to username_fo+ or $uid alike - - # shellcheck disable=SC2206 - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) - # DO NOT combine var ps_out declaration and assignment in ONE line! - local ps_out - ps_out="$("${ps_cmd_line[@]}" | sort -k3,3nr)" - [ -n "$ps_out" ] || __die_when_no_java_process_found - - if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" - fi - - if ((count > 0)); then - echo "$ps_out" | head -n "${count}" - else - echo "$ps_out" - fi + # 1. sort by %cpu by ps option `--sort -pcpu` + # unfortunately, ps from `procps-ng 3.3.12`, `--sort` does not work properly with other options, + # use + # ps + # combined + # sort -k3,3nr + # instead of + # ps --sort -pcpu + # 2. use wide output(unlimited width) by ps option `-ww` + # avoid trunk user column to username_fo+ or $uid alike + + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) + # DO NOT combine var ps_out declaration and assignment in ONE line! + local ps_out + ps_out="$("${ps_cmd_line[@]}" | sort -k3,3nr)" + [ -n "$ps_out" ] || __die_when_no_java_process_found + + if [ -n "$store_dir" ]; then + echo "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" + fi + + if ((count > 0)); then + echo "$ps_out" | head -n "${count}" + else + echo "$ps_out" + fi } # top with output field: thread id, %cpu __top_threadId_cpu() { - # DO NOT combine var java_pid_list declaration and assignment in ONE line! - local java_pid_list - # shellcheck disable=SC2086 - java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" - [ -n "$java_pid_list" ] || __die_when_no_java_process_found - # shellcheck disable=SC2086 - java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , - - # 1. sort by %cpu by top option `-o %CPU` - # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), - # use - # HOME="$tmp_store_dir" top -H -b -n 1 - # combined - # sort - # instead of - # HOME="$tmp_store_dir" top -H -b -n 1 -o '%CPU' - # 2. change HOME env var when run top, - # so as to prevent top command output format being change by .toprc user config file unexpectedly - # 3. use option `-d 0.5`(update interval 0.5 second) and `-n 2`(update 2 times), - # and use second time update data to get cpu percentage of thread in 0.5 second interval - # 4. top v3.3, there is 1 black line between 2 update; - # but top v3.2, there is 2 blank lines between 2 update! - local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") - # DO NOT combine var ps_out declaration and assignment in ONE line! - local top_out - top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") - if [ -n "$store_dir" ]; then - echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" - fi - - # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! - local result_threads_top_info - result_threads_top_info=$( - echo "$top_out" | awk '{ + # DO NOT combine var java_pid_list declaration and assignment in ONE line! + local java_pid_list + # shellcheck disable=SC2086 + java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" + [ -n "$java_pid_list" ] || __die_when_no_java_process_found + # shellcheck disable=SC2086 + java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , + + # 1. sort by %cpu by top option `-o %CPU` + # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), + # use + # HOME="$tmp_store_dir" top -H -b -n 1 + # combined + # sort + # instead of + # HOME="$tmp_store_dir" top -H -b -n 1 -o '%CPU' + # 2. change HOME env var when run top, + # so as to prevent top command output format being change by .toprc user config file unexpectedly + # 3. use option `-d 0.5`(update interval 0.5 second) and `-n 2`(update 2 times), + # and use second time update data to get cpu percentage of thread in 0.5 second interval + # 4. top v3.3, there is 1 black line between 2 update; + # but top v3.2, there is 2 blank lines between 2 update! + local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") + # DO NOT combine var ps_out declaration and assignment in ONE line! + local top_out + top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") + if [ -n "$store_dir" ]; then + echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" + fi + + # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! + local result_threads_top_info + result_threads_top_info=$( + echo "$top_out" | awk '{ # from text line to empty line, increase block index if (previousLine && !$0) blockIndex++ # only print 4th text block(blockIndex == 3), aka. process info of second top update @@ -487,97 +487,97 @@ __top_threadId_cpu() { print $1, $9 # $1 is thread id field, $9 is %cpu field previousLine = $0 }' - ) - [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found + ) + [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found - echo "$result_threads_top_info" | sort -k2,2nr + echo "$result_threads_top_info" | sort -k2,2nr } __complete_pid_user_by_ps() { - # ps output field: pid, thread id(lwp), user - # shellcheck disable=SC2206 - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) - # DO NOT combine var ps_out declaration and assignment in ONE line! - local ps_out - ps_out="$("${ps_cmd_line[@]}")" - if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" - fi - - local idx=0 threadId pcpu output_fields - while read -r threadId pcpu; do - ((count <= 0 || idx < count)) || break - - # output field: pid, threadId, pcpu, user - output_fields="$(echo "$ps_out" | - awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { + # ps output field: pid, thread id(lwp), user + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) + # DO NOT combine var ps_out declaration and assignment in ONE line! + local ps_out + ps_out="$("${ps_cmd_line[@]}")" + if [ -n "$store_dir" ]; then + echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" + fi + + local idx=0 threadId pcpu output_fields + while read -r threadId pcpu; do + ((count <= 0 || idx < count)) || break + + # output field: pid, threadId, pcpu, user + output_fields="$(echo "$ps_out" | + awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { print $1, threadId, pcpu, $3; exit }')" - if [ -n "$output_fields" ]; then - ((idx++)) - echo "$output_fields" - fi - done + if [ -n "$output_fields" ]; then + ((idx++)) + echo "$output_fields" + fi + done } # output format is same as function findBusyJavaThreadsByPs findBusyJavaThreadsByTop() { - __top_threadId_cpu | __complete_pid_user_by_ps + __top_threadId_cpu | __complete_pid_user_by_ps } printStackOfThreads() { - local idx=0 pid threadId pcpu user threadId0x - while read -r pid threadId pcpu user; do - threadId0x="0x$(printf %x "${threadId}")" - - ((idx++)) - local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" - [ -f "${jstackFile}" ] || { - # shellcheck disable=SC2206 - local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $lock_info ${pid}) - if [ "${user}" == "${USER}" ]; then - # run without sudo, when java process user is current user - logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}" - elif [ $UID == 0 ]; then - # if java process user is not current user, must run jstack with sudo - logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}" - else - # current user is not root user, so can not run with sudo; print error message and rerun suggestion - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" - yellowPrint " sudo $(printCallingCommandLine)" - normalPrint - continue - fi || { - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - normalPrint - rm "${jstackFile}" &>/dev/null - continue - } - } - - bluePrint "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" - - if [ -n "$mix_native_frames" ]; then - local sed_script="/--------------- $threadId ---------------/,/^---------------/ { + local idx=0 pid threadId pcpu user threadId0x + while read -r pid threadId pcpu user; do + threadId0x="0x$(printf %x "${threadId}")" + + ((idx++)) + local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" + [ -f "${jstackFile}" ] || { + # shellcheck disable=SC2206 + local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $lock_info ${pid}) + if [ "${user}" == "${USER}" ]; then + # run without sudo, when java process user is current user + logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}" + elif [ $UID == 0 ]; then + # if java process user is not current user, must run jstack with sudo + logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}" + else + # current user is not root user, so can not run with sudo; print error message and rerun suggestion + redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" + yellowPrint " sudo $(printCallingCommandLine)" + normalPrint + continue + fi || { + redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + normalPrint + rm "${jstackFile}" &>/dev/null + continue + } + } + + bluePrint "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" + + if [ -n "$mix_native_frames" ]; then + local sed_script="/--------------- $threadId ---------------/,/^---------------/ { /--------------- $threadId ---------------/b # skip first separator line /^---------------/d # delete second separator line p }" - elif [ -n "$force" ]; then - local sed_script="/^Thread ${threadId}:/,/^$/ { + elif [ -n "$force" ]; then + local sed_script="/^Thread ${threadId}:/,/^$/ { /^$/d; p # delete end separator line }" - else - local sed_script="/ nid=${threadId0x} /,/^$/ { + else + local sed_script="/ nid=${threadId0x} /,/^$/ { /^$/d; p # delete end separator line }" - fi - { - sed "$sed_script" -n "${jstackFile}" - echo - } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} - done + fi + { + sed "$sed_script" -n "${jstackFile}" + echo + } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} + done } ################################################################################ @@ -585,21 +585,21 @@ printStackOfThreads() { ################################################################################ main() { - local update_round_num - # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) - for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do - ((update_round_num > 0)) && sleep "$update_delay" - - [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | - tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null - ((update_count != 1)) && headInfo - - if [ "$cpu_sample_interval" == 0 ]; then - findBusyJavaThreadsByPs - else - findBusyJavaThreadsByTop - fi | printStackOfThreads - done + local update_round_num + # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) + for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do + ((update_round_num > 0)) && sleep "$update_delay" + + [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | + tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null + ((update_count != 1)) && headInfo + + if [ "$cpu_sample_interval" == 0 ]; then + findBusyJavaThreadsByPs + else + findBusyJavaThreadsByTop + fi | printStackOfThreads + done } main diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index abaf97ca..6629e8fb 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -20,7 +20,7 @@ readonly PROG_VERSION='2.5.0-dev' ################################################################################ usage() { - cat <&2 - exit 1 + redEcho "Error: $*" 1>&2 + exit 1 } convertHumanReadableSizeToSize() { - local human_readable_size="$1" - - [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1 - - local size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" - case "$unit" in - g) - ((size *= 1024 * 1024 * 1024)) - ;; - m) - ((size *= 1024 * 1024)) - ;; - k) - ((size *= 1024)) - ;; - esac - - echo "$size" + local human_readable_size="$1" + + [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1 + + local size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" + case "$unit" in + g) + ((size *= 1024 * 1024 * 1024)) + ;; + m) + ((size *= 1024 * 1024)) + ;; + k) + ((size *= 1024)) + ;; + esac + + echo "$size" } 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 + 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 + (($# > 0)) && redEcho "$*$nl" >$out - cat >$out <$out < 0)); do - case "$1" in - -c | --count) - uq_opt_count=1 - shift - ;; - -d | --repeated) - uq_opt_only_repeated=1 - shift - ;; - -D) - uq_opt_all_repeated=1 - shift - ;; - --all-repeated=*) - uq_opt_all_repeated=1 - - uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}') - [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || - usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" - - shift - ;; - -u | --unique) - uq_opt_only_unique=1 - shift - ;; - -i | --ignore-case) - uq_opt_ignore_case=1 - shift - ;; - -z | --zero-terminated) - uq_opt_zero_terminated=1 - shift - ;; - -XM | --max-input) - uq_max_input_human_readable_size="$2" - shift 2 - ;; - -h | --help) - usage - ;; - -V | --version) - progVersion - ;; - --) - shift - argv=("${argv[@]}" "$@") - break - ;; - -) - argv=(${argv[@]:+"${argv[@]}"} "$1") - shift - ;; - -*) - usage 2 "${PROG}: unrecognized option '$1'" - ;; - *) - argv=(${argv[@]:+"${argv[@]}"} "$1") - shift - ;; - esac + case "$1" in + -c | --count) + uq_opt_count=1 + shift + ;; + -d | --repeated) + uq_opt_only_repeated=1 + shift + ;; + -D) + uq_opt_all_repeated=1 + shift + ;; + --all-repeated=*) + uq_opt_all_repeated=1 + + uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}') + [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || + usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" + + shift + ;; + -u | --unique) + uq_opt_only_unique=1 + shift + ;; + -i | --ignore-case) + uq_opt_ignore_case=1 + shift + ;; + -z | --zero-terminated) + uq_opt_zero_terminated=1 + shift + ;; + -XM | --max-input) + uq_max_input_human_readable_size="$2" + shift 2 + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + argv=("${argv[@]}" "$@") + break + ;; + -) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + -*) + usage 2 "${PROG}: unrecognized option '$1'" + ;; + *) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + esac done [[ $uq_opt_only_repeated == 1 && $uq_opt_only_unique == 1 ]] && - usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" + usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" [[ $uq_opt_all_repeated == 1 && $uq_opt_only_unique == 1 ]] && - usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" + usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && - yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 + yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 # NOTE: DO NOT declare var uq_max_input_size as readonly in ONE line! uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || - usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" + usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" readonly argc=${#argv[@]} if ((argc == 0)); then - input_files=() - output_file=/dev/stdout + input_files=() + output_file=/dev/stdout elif ((argc == 1)); then - input_files=("${argv[0]}") - output_file=/dev/stdout + input_files=("${argv[0]}") + output_file=/dev/stdout else - input_files=("${argv[@]:0:argc-1}") - output_file=${argv[argc - 1]} - if [ "$output_file" == - ]; then - output_file=/dev/stdout - fi + input_files=("${argv[@]:0:argc-1}") + output_file=${argv[argc - 1]} + if [ "$output_file" == - ]; then + output_file=/dev/stdout + fi fi # Check input file for f in ${input_files[@]:+"${input_files[@]}"}; do - # - is stdin, ok - [ "$f" == - ] && continue + # - is stdin, ok + [ "$f" == - ] && continue - [ -e "$f" ] || die "input file $f does not exist!" - [ ! -d "$f" ] || die "input file $f exists, but is a directory!" - [ -f "$f" ] || die "input file $f exists, but is not a file!" - [ -r "$f" ] || die "input file $f exists, but is not readable!" + [ -e "$f" ] || die "input file $f does not exist!" + [ ! -d "$f" ] || die "input file $f exists, but is a directory!" + [ -f "$f" ] || die "input file $f exists, but is not a file!" + [ -r "$f" ] || die "input file $f exists, but is not readable!" done ################################################################################ @@ -316,16 +316,16 @@ END { ' awk \ - -v "uq_opt_count=$uq_opt_count" \ - -v "uq_opt_only_repeated=$uq_opt_only_repeated" \ - -v "uq_opt_all_repeated=$uq_opt_all_repeated" \ - -v "uq_opt_repeated_method=$uq_opt_repeated_method" \ - -v "uq_opt_only_unique=$uq_opt_only_unique" \ - -v "IGNORECASE=$uq_opt_ignore_case" \ - -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ - -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \ - -v "uq_max_input_size=$uq_max_input_size" \ - -v "uq_PROG=$PROG" \ - -f <(printf "%s" "$uq_awk_script") \ - -- ${input_files[@]:+"${input_files[@]}"} \ - >"$output_file" + -v "uq_opt_count=$uq_opt_count" \ + -v "uq_opt_only_repeated=$uq_opt_only_repeated" \ + -v "uq_opt_all_repeated=$uq_opt_all_repeated" \ + -v "uq_opt_repeated_method=$uq_opt_repeated_method" \ + -v "uq_opt_only_unique=$uq_opt_only_unique" \ + -v "IGNORECASE=$uq_opt_ignore_case" \ + -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ + -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \ + -v "uq_max_input_size=$uq_max_input_size" \ + -v "uq_PROG=$PROG" \ + -f <(printf "%s" "$uq_awk_script") \ + -- ${input_files[@]:+"${input_files[@]}"} \ + >"$output_file" diff --git a/bin/xpf b/bin/xpf index a1e89c79..3abad3b2 100755 --- a/bin/xpf +++ b/bin/xpf @@ -13,25 +13,25 @@ set -eEuo pipefail # How can I get the behavior of GNU's readlink -f on a Mac? # https://stackoverflow.com/questions/1055671 portableReadLink() { - local file="$1" uname + local file="$1" uname - uname="$(uname)" - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f "$file" - ;; - Darwin*) - if command -v greadlink >/dev/null; then - greadlink -f "$file" - else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" - fi - ;; - *) - echo "not support uname($uname)!" >&2 - exit 1 - ;; - esac + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + readlink -f "$file" + ;; + Darwin*) + if command -v greadlink >/dev/null; then + greadlink -f "$file" + else + python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + fi + ;; + *) + echo "not support uname($uname)!" >&2 + exit 1 + ;; + esac } BASE="$(dirname "$(portableReadLink "${BASH_SOURCE[0]}")")" diff --git a/bin/xpl b/bin/xpl index caa6c36f..b197836e 100755 --- a/bin/xpl +++ b/bin/xpl @@ -33,14 +33,14 @@ readonly PROG_VERSION='2.5.0-dev' readonly nl=$'\n' # new line 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 + 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)) && echo "$*$nl" >$out + (($# > 0)) && echo "$*$nl" >$out - cat < - - [🍺 show-busy-java-threads](#-show-busy-java-threads) - [用法](#%E7%94%A8%E6%B3%95) - [示例](#%E7%A4%BA%E4%BE%8B) diff --git a/docs/shell.md b/docs/shell.md index 36bdb400..5d451f19 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -4,7 +4,6 @@ - - [`Shell`使用加强](#shell%E4%BD%BF%E7%94%A8%E5%8A%A0%E5%BC%BA) - [🍺 c](#-c) - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B) diff --git a/docs/vcs.md b/docs/vcs.md index 0ca26976..aa42a1e5 100644 --- a/docs/vcs.md +++ b/docs/vcs.md @@ -11,14 +11,14 @@ > 使用更现代的`Git`吧! 💥 1. [swtrunk](#-swtrunk) - 自动`svn`工作目录从分支(`branches`)切换到主干(`trunk`)。 - PS: `Git`对应的是`git checkout master`,如果你使用了`oh-my-zsh`,已经有对应的别名加速了:`gcm`。 + 自动`svn`工作目录从分支(`branches`)切换到主干(`trunk`)。 + PS: `Git`对应的是`git checkout master`,如果你使用了`oh-my-zsh`,已经有对应的别名加速了:`gcm`。 1. [svn-merge-stop-on-copy](#-svn-merge-stop-on-copy) - 把指定的远程分支从刚新建分支以来的修改合并到本地`svn`目录或是另一个远程分支。 - PS:`Git`的合并很直接简单,`git merge branch-foo`,也更智能(没有树冲突一说)。 + 把指定的远程分支从刚新建分支以来的修改合并到本地`svn`目录或是另一个远程分支。 + PS:`Git`的合并很直接简单,`git merge branch-foo`,也更智能(没有树冲突一说)。 1. [cp-svn-url](#-cp-svn-url) - 拷贝当前`svn`目录对应的远程分支到系统的粘贴板,省去`CTRL+C`操作。 - PS:`Git`分支不需要`URL`来引用,没有这个脚本的需求,直接给个分支名就好了。 + 拷贝当前`svn`目录对应的远程分支到系统的粘贴板,省去`CTRL+C`操作。 + PS:`Git`分支不需要`URL`来引用,没有这个脚本的需求,直接给个分支名就好了。 🍺 [swtrunk](../legacy-bin/swtrunk) ---------------------- diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 8e055e8e..eb02988a 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -30,7 +30,7 @@ PROG="$(basename "$0")" readonly PROG_VERSION='2.5.0-dev' usage() { - cat <&2 - exit 1 + echo "Fail to get svn url!" 1>&2 + exit 1 fi copy() { - case "$(uname)" in - Darwin*) - pbcopy ;; - CYGWIN*|MINGW*) - clip ;; - *) - xsel -b ;; - esac + case "$(uname)" in + Darwin*) + pbcopy + ;; + CYGWIN* | MINGW*) + clip + ;; + *) + xsel -b + ;; + esac } echo -n "${url}" | copy && echo "${url} copied!" diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index e4d4cb22..8821bcc4 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -14,76 +14,76 @@ PROG="$(basename "$0")" usage() { - cat < [target branch] svn merge commit between version when source branch copy(--stop-on-copy) and head version of source branch. Source branch must be a remote branch. -Example: - ${PROG} http://www.foo.com/project1/branches/feature1 - # merge http://www.foo.com/project1/branches/feature1 to current svn directory +Example: + ${PROG} http://www.foo.com/project1/branches/feature1 + # merge http://www.foo.com/project1/branches/feature1 to current svn directory - ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/directory - # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory - # will prompt confirm for committing to target branch. + ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/directory + # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory + # will prompt confirm for committing to target branch. - ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 - # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 - # because http://www.foo.com/project1/branches/feature2 is remote url, - # will check out target branch to tmp directory, and prompt confirm for committing to target branch. + ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 + # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 + # because http://www.foo.com/project1/branches/feature2 is remote url, + # will check out target branch to tmp directory, and prompt confirm for committing to target branch. EOF - # shellcheck disable=SC2086 - exit $1 + # shellcheck disable=SC2086 + exit $1 } [ $# -gt 2 ] && { - echo "too many arguments!" - usage 1 + echo "too many arguments!" + usage 1 } source_branch=$1 target=${2:-.} [ -z "$source_branch" ] && { - echo "missing source branch argument!" - usage 1 + echo "missing source branch argument!" + usage 1 } [ -e "$source_branch" ] && { - echo "source branch must be a remote branch!" - usage 1 + echo "source branch must be a remote branch!" + usage 1 } [ ! -d "$target" ] && { - workDir=$(mktemp -d) && svn co "$target" "$workDir" || { - echo "Fail to checkout target remote branch $target !" - exit 1 - } + workDir=$(mktemp -d) && svn co "$target" "$workDir" || { + echo "Fail to checkout target remote branch $target !" + exit 1 + } } || workDir="$target" cleanupWhenExit() { - [ "$workDir" != "$target" ] && { - echo "rm tmp dir $workDir ." - rm -rf "$workDir" - } + [ "$workDir" != "$target" ] && { + echo "rm tmp dir $workDir ." + rm -rf "$workDir" + } } trap cleanupWhenExit EXIT svn_status_line=$(svn status --ignore-externals "$workDir" | grep -c -v ^X) [ "$svn_status_line" -ne 0 ] && { - echo "svn work directory is modified!" - exit 1 + echo "svn work directory is modified!" + exit 1 } cd "$workDir" && -if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then - echo "oldest version($from_version) of source branch $source_branch ." - echo "starting merge to $workDir ." - svn merge "-${from_version}:HEAD" "$source_branch" -else - echo "Fail to merge to work dir $workDir ." - exit 2 -fi + if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then + echo "oldest version($from_version) of source branch $source_branch ." + echo "starting merge to $workDir ." + svn merge "-${from_version}:HEAD" "$source_branch" + else + echo "Fail to merge to work dir $workDir ." + exit 2 + fi read -r -p "Check In? (Y/N)" ci [ "$ci" == "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch" diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index 902f3f6c..cde45c6e 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -13,35 +13,35 @@ readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" + local color=$1 + shift + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } greenEcho() { - colorEcho 32 "$@" + colorEcho 32 "$@" } [ $# -eq 0 ] && dirs=(.) || dirs=("$@") for d in "${dirs[@]}"; do - [ ! -d "${d}/.svn" ] && { - redEcho "directory $d is not a svn work directory, ignore directory $d !" - continue - } - ( - cd "$d" && - branches=$(svn info | grep '^URL' | awk '{print $2}') && - trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk && - if svn sw "$trunk"; then - greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." - else - redEcho "fail to switch $d to trunk!" - fi - ) + [ ! -d "${d}/.svn" ] && { + redEcho "directory $d is not a svn work directory, ignore directory $d !" + continue + } + ( + cd "$d" && + branches=$(svn info | grep '^URL' | awk '{print $2}') && + trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk && + if svn sw "$trunk"; then + greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." + else + redEcho "fail to switch $d to trunk!" + fi + ) done diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index dc9c9e12..dcff9380 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -22,8 +22,8 @@ # var2=$(echo value1) _ctct_READLINK_CMD=readlink -if command -v greadlink > /dev/null; then - _ctct_READLINK_CMD=greadlink +if command -v greadlink >/dev/null; then + _ctct_READLINK_CMD=greadlink fi # NOTE: DO NOT declare var _ctct_PROG as readonly in ONE line! @@ -34,58 +34,58 @@ readonly _ctct_ec=$'\033' # escape char readonly _ctct_eend=$'\033[0m' # escape end colorEcho() { - local combination="$1" - shift 1 + local combination="$1" + shift 1 - [ -t 1 ] && echo "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo "$*" + [ -t 1 ] && echo "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo "$*" } colorEchoWithoutNewLine() { - local combination="$1" - shift 1 + local combination="$1" + shift 1 - [ -t 1 ] && echo -n "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo -n "$*" + [ -t 1 ] && echo -n "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo -n "$*" } # if not directly run this script(use as lib), just export 2 helper functions, # and do NOT print anything. [ "$_ctct_is_direct_run" == "true" ] && { - for style in 0 1 2 3 4 5 6 7; do - for fg in 30 31 32 33 34 35 36 37; do - for bg in 40 41 42 43 44 45 46 47; do - combination="${style};${fg};${bg}" - colorEchoWithoutNewLine "$combination" "$combination" - echo -n " " - done - echo - done - echo + for style in 0 1 2 3 4 5 6 7; do + for fg in 30 31 32 33 34 35 36 37; do + for bg in 40 41 42 43 44 45 46 47; do + combination="${style};${fg};${bg}" + colorEchoWithoutNewLine "$combination" "$combination" + echo -n " " + done + echo done + echo + done - echo "Code sample to print color text:" + echo "Code sample to print color text:" - echo -n ' echo -e "\033[' - colorEchoWithoutNewLine "3;35;40" "1;36;41" - echo -n "m" - colorEchoWithoutNewLine "0;32;40" "Sample Text" - echo "\033[0m\"" + echo -n ' echo -e "\033[' + colorEchoWithoutNewLine "3;35;40" "1;36;41" + echo -n "m" + colorEchoWithoutNewLine "0;32;40" "Sample Text" + echo "\033[0m\"" - echo -n " echo \$'\033[" - colorEchoWithoutNewLine "3;35;40" "1;36;41" - echo -n "m'\"" - colorEchoWithoutNewLine "0;32;40" "Sample Text" - echo "\"$'\033[0m'" - echo " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" + echo -n " echo \$'\033[" + colorEchoWithoutNewLine "3;35;40" "1;36;41" + echo -n "m'\"" + colorEchoWithoutNewLine "0;32;40" "Sample Text" + echo "\"$'\033[0m'" + echo " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" - echo "Output of above code:" - echo " ${_ctct_ec}[1;36;41mSample Text${_ctct_eend}" - echo - echo "If you are going crazy to write text in escapes string like me," - echo "you can use colorEcho and colorEchoWithoutNewLine function in this script." - echo - echo "Code sample to print color text:" - echo ' colorEcho "1;36;41" "Sample Text"' - echo "Output of above code:" - echo -n " " - colorEcho "1;36;41" "Sample Text" + echo "Output of above code:" + echo " ${_ctct_ec}[1;36;41mSample Text${_ctct_eend}" + echo + echo "If you are going crazy to write text in escapes string like me," + echo "you can use colorEcho and colorEchoWithoutNewLine function in this script." + echo + echo "Code sample to print color text:" + echo ' colorEcho "1;36;41" "Sample Text"' + echo "Output of above code:" + echo -n " " + colorEcho "1;36;41" "Sample Text" } diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 34464458..7a1a5408 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -23,37 +23,37 @@ ##################################################################### # NOTE: $'foo' is the escape sequence syntax of bash -readonly _opts_ec=$'\033' # escape char +readonly _opts_ec=$'\033' # escape char readonly _opts_eend=$'\033[0m' # escape end _opts_SED_CMD=sed -if command -v gsed &> /dev/null; then - _opts_SED_CMD=gsed +if command -v gsed &>/dev/null; then + _opts_SED_CMD=gsed fi _opts_colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$_opts_ec[1;${color}m$@$_opts_eend" || echo "$@" + local color=$1 + shift + # if stdout is console, turn on color output. + [ -t 1 ] && echo "$_opts_ec[1;${color}m$@$_opts_eend" || echo "$@" } _opts_redEcho() { - _opts_colorEcho 31 "$@" + _opts_colorEcho 31 "$@" } _opts_convertToVarName() { - [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_convertToVarName: $@" - return 1 - } - echo "$1" | $_opts_SED_CMD 's/-/_/g' + [ $# -ne 1 ] && { + _opts_redEcho "NOT 1 arguemnts when call _opts_convertToVarName: $@" + return 1 + } + echo "$1" | $_opts_SED_CMD 's/-/_/g' } ##################################################################### # Parse Functions # -# Use Globle Variable: +# Use Globle Variable: # * _OPT_INFO_LIST_INDEX : Option info, data structure. # _OPT_INFO_LIST_INDEX ->* _a_a_long -> option value. # * _OPT_VALUE_* : value of option. is Array type for + mode option @@ -61,258 +61,258 @@ _opts_convertToVarName() { ##################################################################### _opts_findOptMode() { - [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_findOptMode: $@" - return 1 - } - - local opt="$1" # like a, a-long - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local mode="${idxNameArray[0]}" - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" == "${optName}" ] && { - echo "$mode" - return - } - done + [ $# -ne 1 ] && { + _opts_redEcho "NOT 1 arguemnts when call _opts_findOptMode: $@" + return 1 + } + + local opt="$1" # like a, a-long + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local mode="${idxNameArray[0]}" + + local optName + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + [ "$opt" == "${optName}" ] && { + echo "$mode" + return + } done + done - echo "" + echo "" } _opts_setOptBool() { - [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptBool: $@" - return 1 - } + [ $# -ne 2 ] && { + _opts_redEcho "NOT 2 arguemnts when call _opts_setOptBool: $@" + return 1 + } - _opts_setOptValue "$@" + _opts_setOptValue "$@" } _opts_setOptValue() { - [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptValue: $@" - return 1 - } - - local opt="$1" # like a, a-long - local value="$2" - - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" == "$optName" ] && { - local optName2 - for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "${optName2}"`" - local from='"$value"' - eval "$optValueVarName=$from" # set global var! - done - return - } + [ $# -ne 2 ] && { + _opts_redEcho "NOT 2 arguemnts when call _opts_setOptValue: $@" + return 1 + } + + local opt="$1" # like a, a-long + local value="$2" + + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local optName + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + [ "$opt" == "$optName" ] && { + local optName2 + for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + local from='"$value"' + eval "$optValueVarName=$from" # set global var! done + return + } done + done - _opts_redEcho "NOT Found option $opt!" - return 1 + _opts_redEcho "NOT Found option $opt!" + return 1 } _opts_setOptArray() { - local opt="$1" # like a, a-long - shift - - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" == "$optName" ] && { - # set _OPT_VALUE - local optName2 - for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "${optName2}"`" - local from='"$@"' - eval "$optValueVarName=($from)" # set global var! - done - return - } + local opt="$1" # like a, a-long + shift + + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local optName + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + [ "$opt" == "$optName" ] && { + # set _OPT_VALUE + local optName2 + for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + local from='"$@"' + eval "$optValueVarName=($from)" # set global var! done + return + } done + done - _opts_redEcho "NOT Found option $opt!" - return 1 + _opts_redEcho "NOT Found option $opt!" + return 1 } _opts_cleanOptValueInfoList() { - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - eval "unset $idxName" + eval "unset $idxName" - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "$optName"`" - eval "unset $optValueVarName" - done + local optName + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + eval "unset $optValueVarName" done + done - unset _OPT_INFO_LIST_INDEX - unset _OPT_ARGS + unset _OPT_INFO_LIST_INDEX + unset _OPT_ARGS } parseOpts() { - local optsDescription="$1" # optsDescription LIKE a,a-long|b,b-long:|c,c-long+ - shift - - _OPT_INFO_LIST_INDEX=() # set global var! - - local optDescLines=`echo "$optsDescription" | - # cut head and tail space - $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | - awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` - - local optDesc - while read optDesc ; do # optDesc LIKE b,b-long: - [ -z "$optDesc" ] && continue - - local mode="${optDesc:(-1)}" # LIKE : or + - case "$mode" in - +|:|-) - optDesc="${optDesc:0:(${#optDesc}-1)}" # LIKE b,b-long - ;; - *) - mode="-" - ;; - esac - - local optLines=`echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` # LIKE "a\na-long" - - [ $(echo "$optLines" | wc -l) -gt 2 ] && { - _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2 - _opts_cleanOptValueInfoList - return 220 - } + local optsDescription="$1" # optsDescription LIKE a,a-long|b,b-long:|c,c-long+ + shift + + _OPT_INFO_LIST_INDEX=() # set global var! + + local optDescLines=$(echo "$optsDescription" | + # cut head and tail space + $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | + awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}') + + local optDesc + while read optDesc; do # optDesc LIKE b,b-long: + [ -z "$optDesc" ] && continue + + local mode="${optDesc:(-1)}" # LIKE : or + + case "$mode" in + + | : | -) + optDesc="${optDesc:0:(${#optDesc} - 1)}" # LIKE b,b-long + ;; + *) + mode="-" + ;; + esac + + local optLines=$(echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}') # LIKE "a\na-long" + + [ $(echo "$optLines" | wc -l) -gt 2 ] && { + _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2 + _opts_cleanOptValueInfoList + return 220 + } + + local -a optTuple=() + local opt + while read opt; do # opt LIKE a , a-long + [ -z "$opt" ] && continue - local -a optTuple=() - local opt - while read opt ; do # opt LIKE a , a-long - [ -z "$opt" ] && continue - - [ ${#opt} -eq 1 ] && { - echo "$opt" | grep -E '^[a-zA-Z0-9]$' -q || { - _opts_redEcho "Illegal short option name($opt in $optDesc) in option description!" 1>&2 - _opts_cleanOptValueInfoList - return 221 - } - } || { - echo "$opt" | grep -E '^[-a-zA-Z0-9]+$' -q || { - _opts_redEcho "Illegal long option name($opt in $optDesc) in option description!" 1>&2 - _opts_cleanOptValueInfoList - return 222 - } - } - optTuple=("${optTuple[@]}" "$opt") - done < <(echo "$optLines") - - [ ${#optTuple[@]} -gt 2 ] && { - _opts_redEcho "more than 2 opt(${optTuple[@]}) in option description($optDesc)!" 1>&2 - _opts_cleanOptValueInfoList - return 223 + [ ${#opt} -eq 1 ] && { + echo "$opt" | grep -E '^[a-zA-Z0-9]$' -q || { + _opts_redEcho "Illegal short option name($opt in $optDesc) in option description!" 1>&2 + _opts_cleanOptValueInfoList + return 221 } + } || { + echo "$opt" | grep -E '^[-a-zA-Z0-9]+$' -q || { + _opts_redEcho "Illegal long option name($opt in $optDesc) in option description!" 1>&2 + _opts_cleanOptValueInfoList + return 222 + } + } + optTuple=("${optTuple[@]}" "$opt") + done < <(echo "$optLines") + + [ ${#optTuple[@]} -gt 2 ] && { + _opts_redEcho "more than 2 opt(${optTuple[@]}) in option description($optDesc)!" 1>&2 + _opts_cleanOptValueInfoList + return 223 + } - local idxName= - local evalOpts= - local o - for o in "${optTuple[@]}"; do - idxName="${idxName}_opts_index_name_`_opts_convertToVarName "$o"`" - evalOpts="${evalOpts} $o" - done + local idxName= + local evalOpts= + local o + for o in "${optTuple[@]}"; do + idxName="${idxName}_opts_index_name_$(_opts_convertToVarName "$o")" + evalOpts="${evalOpts} $o" + done - eval "$idxName=($mode $evalOpts)" - _OPT_INFO_LIST_INDEX=("${_OPT_INFO_LIST_INDEX[@]}" "$idxName") - done < <(echo "$optDescLines") - - local -a args=() - while true; do - [ $# -eq 0 ] && break - - case "$1" in - ---*) - _opts_redEcho "Illegal option($1), more than 2 prefix '-'!" 1>&2 - _opts_cleanOptValueInfoList - return 230 - ;; - --) - shift - args=("${args[@]}" "$@") + eval "$idxName=($mode $evalOpts)" + _OPT_INFO_LIST_INDEX=("${_OPT_INFO_LIST_INDEX[@]}" "$idxName") + done < <(echo "$optDescLines") + + local -a args=() + while true; do + [ $# -eq 0 ] && break + + case "$1" in + ---*) + _opts_redEcho "Illegal option($1), more than 2 prefix '-'!" 1>&2 + _opts_cleanOptValueInfoList + return 230 + ;; + --) + shift + args=("${args[@]}" "$@") + break + ;; + -*) # short & long option(-a, -a-long), use same read-in logic. + local opt="$1" + local optName=$(echo "$1" | $_opts_SED_CMD -r 's/^--?//') + local mode=$(_opts_findOptMode "$optName") + case "$mode" in + -) + _opts_setOptBool "$optName" "true" + shift + ;; + :) + [ $# -lt 2 ] && { + _opts_redEcho "Option $opt has NO value!" 1>&2 + _opts_cleanOptValueInfoList + return 231 + } + _opts_setOptValue "$optName" "$2" + shift 2 + ;; + +) + shift + local -a valueArray=() + local foundComma="" + + local value + for value in "$@"; do + [ ";" == "$value" ] && { + foundComma=true break - ;; - -*) # short & long option(-a, -a-long), use same read-in logic. - local opt="$1" - local optName=`echo "$1" | $_opts_SED_CMD -r 's/^--?//'` - local mode=`_opts_findOptMode "$optName"` - case "$mode" in - -) - _opts_setOptBool "$optName" "true" - shift - ;; - :) - [ $# -lt 2 ] && { - _opts_redEcho "Option $opt has NO value!" 1>&2 - _opts_cleanOptValueInfoList - return 231 - } - _opts_setOptValue "$optName" "$2" - shift 2 - ;; - +) - shift - local -a valueArray=() - local foundComma="" - - local value - for value in "$@" ; do - [ ";" == "$value" ] && { - foundComma=true - break - } || valueArray=("${valueArray[@]}" "$value") - done - [ "$foundComma" ] || { - _opts_redEcho "value of option $opt no end comma, value = ${valueArray[@]}" 1>&2 - _opts_cleanOptValueInfoList - return 231 - } - shift "$((${#valueArray[@]} + 1))" - _opts_setOptArray "$optName" "${valueArray[@]}" - ;; - *) - _opts_redEcho "Undefined option $opt!" 1>&2 - _opts_cleanOptValueInfoList - return 232 - ;; - esac - ;; - *) - args=("${args[@]}" "$1") - shift - ;; - esac - done - _OPT_ARGS=("${args[@]}") # set global var! + } || valueArray=("${valueArray[@]}" "$value") + done + [ "$foundComma" ] || { + _opts_redEcho "value of option $opt no end comma, value = ${valueArray[@]}" 1>&2 + _opts_cleanOptValueInfoList + return 231 + } + shift "$((${#valueArray[@]} + 1))" + _opts_setOptArray "$optName" "${valueArray[@]}" + ;; + *) + _opts_redEcho "Undefined option $opt!" 1>&2 + _opts_cleanOptValueInfoList + return 232 + ;; + esac + ;; + *) + args=("${args[@]}" "$1") + shift + ;; + esac + done + _OPT_ARGS=("${args[@]}") # set global var! } ##################################################################### @@ -320,43 +320,43 @@ parseOpts() { ##################################################################### _opts_showOptDescInfoList() { - echo "===============================================================================" - echo "show option desc info list:" - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - echo "$idxName = (${!idxNameArrayPlaceHolder})" - done - echo "===============================================================================" + echo "===============================================================================" + echo "show option desc info list:" + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + echo "$idxName = (${!idxNameArrayPlaceHolder})" + done + echo "===============================================================================" } _opts_showOptValueInfoList() { - echo "===============================================================================" - echo "show option value info list:" - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local mode=${idxNameArray[0]} - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "$optName"`" - case "$mode" in - -) - echo "$optValueVarName=${!optValueVarName}" - ;; - :) - echo "$optValueVarName=${!optValueVarName}" - ;; - +) - local optArrayValueArrayPlaceHolder="$optValueVarName[@]" - echo "$optValueVarName=(${!optArrayValueArrayPlaceHolder})" - ;; - esac - done + echo "===============================================================================" + echo "show option value info list:" + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="$idxName[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local mode=${idxNameArray[0]} + + local optName + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + case "$mode" in + -) + echo "$optValueVarName=${!optValueVarName}" + ;; + :) + echo "$optValueVarName=${!optValueVarName}" + ;; + +) + local optArrayValueArrayPlaceHolder="$optValueVarName[@]" + echo "$optValueVarName=(${!optArrayValueArrayPlaceHolder})" + ;; + esac done - echo "_OPT_ARGS=(${_OPT_ARGS[@]})" - echo "===============================================================================" + done + echo "_OPT_ARGS=(${_OPT_ARGS[@]})" + echo "===============================================================================" } diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 2a65493d..f639fade 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -11,44 +11,44 @@ readonly eend=$'\033[0m' # escape end readonly nl=$'\n' # new line colorEcho() { - local color=$1 - shift + local color=$1 + shift - # if stdout is the console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + # if stdout is the console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } yellowEcho() { - colorEcho 33 "$@" + colorEcho 33 "$@" } blueEcho() { - colorEcho 36 "$@" + colorEcho 36 "$@" } logAndRun() { - local simple_mode=false - [ "$1" = "-s" ] && { - simple_mode=true - shift - } + local simple_mode=false + [ "$1" = "-s" ] && { + simple_mode=true + shift + } - if $simple_mode; then - echo "Run under work directory $PWD : $*" - "$@" - else - blueEcho "Run under work directory $PWD :$nl$*" - time "$@" - fi + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + blueEcho "Run under work directory $PWD :$nl$*" + time "$@" + fi } die() { - redEcho "Error: $*" 1>&2 - exit 1 + redEcho "Error: $*" 1>&2 + exit 1 } ################################################################################ @@ -62,10 +62,10 @@ readonly bump_version="$1" cd "$(dirname "$(readlink -f "$0")")/.." script_files=$( - find bin legacy-bin -type f + find bin legacy-bin -type f ) # shellcheck disable=SC2086 logAndRun sed -ri \ - 's/^(.*PROG_VERSION\s*=\s*)'\''(.*)'\''(.*)$/\1'\'"$bump_version"\''\3/' \ - $script_files + 's/^(.*PROG_VERSION\s*=\s*)'\''(.*)'\''(.*)$/\1'\'"$bump_version"\''\3/' \ + $script_files diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index efc1fd38..ae909c84 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -2,8 +2,8 @@ set -eEuo pipefail READLINK_CMD=readlink -if command -v greadlink &> /dev/null; then - READLINK_CMD=greadlink +if command -v greadlink &>/dev/null; then + READLINK_CMD=greadlink fi cd "$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" @@ -22,39 +22,39 @@ readonly nl=$'\n' # new line ################################################################################ colorEcho() { - local color=$1 - shift + local color=$1 + shift - # if stdout is the console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + # if stdout is the console, turn on color output. + [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } yellowEcho() { - colorEcho 33 "$@" + colorEcho 33 "$@" } blueEcho() { - colorEcho 36 "$@" + colorEcho 36 "$@" } logAndRun() { - local simple_mode=false - [ "$1" == "-s" ] && { - simple_mode=true - shift - } + local simple_mode=false + [ "$1" == "-s" ] && { + simple_mode=true + shift + } - if $simple_mode; then - echo "Run under work directory $PWD : $*" - "$@" - else - blueEcho "Run under work directory $PWD :$nl$*" - time "$@" - fi + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + blueEcho "Run under work directory $PWD :$nl$*" + time "$@" + fi } ################################################################################ @@ -62,5 +62,5 @@ logAndRun() { ################################################################################ for test_case in *_test.sh; do - logAndRun ./"$test_case" + logAndRun ./"$test_case" done diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index 2c8bd79b..ab349d06 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -10,36 +10,36 @@ readonly __ut_ec=$'\033' # escape char readonly __ut_eend=$'\033[0m' # escape end __ut_colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${__ut_ec}[1;${color}m$*$__ut_eend" || echo "$*" + local color=$1 + shift + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${__ut_ec}[1;${color}m$*$__ut_eend" || echo "$*" } redEcho() { - __ut_colorEcho 31 "$@" + __ut_colorEcho 31 "$@" } greenEcho() { - __ut_colorEcho 32 "$@" + __ut_colorEcho 32 "$@" } yellowEcho() { - __ut_colorEcho 33 "$@" + __ut_colorEcho 33 "$@" } blueEcho() { - __ut_colorEcho 34 "$@" + __ut_colorEcho 34 "$@" } fail() { - redEcho "TEST FAIL: $*" - exit 1 + redEcho "TEST FAIL: $*" + exit 1 } die() { - redEcho "Error: $*" 1>&2 - exit 1 + redEcho "Error: $*" 1>&2 + exit 1 } ################################################# @@ -47,57 +47,57 @@ die() { ################################################# assertArrayEquals() { - (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!" - local failMsg="" - (($# == 3)) && { - failMsg=$1 - shift - } - - local a1PlaceHolder="$1[@]" - local a2PlaceHolder="$2[@]" - local a1=("${!a1PlaceHolder}") - local a2=("${!a2PlaceHolder}") - - [ ${#a1[@]} -eq ${#a2[@]} ] || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" - - local i - for ((i = 0; i < ${#a1[@]}; i++)); do - [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}" - done + (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + + local a1PlaceHolder="$1[@]" + local a2PlaceHolder="$2[@]" + local a1=("${!a1PlaceHolder}") + local a2=("${!a2PlaceHolder}") + + [ ${#a1[@]} -eq ${#a2[@]} ] || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" + + local i + for ((i = 0; i < ${#a1[@]}; i++)); do + [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}" + done } assertEquals() { - (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!" - local failMsg="" - (($# == 3)) && { - failMsg=$1 - shift - } - [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" + (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" } readonly __ut_exclude_vars_builtin='^BASH_|^_=|^COLUMNS=|LINES=' readonly __ut_exclude_vars_ut_functions='^FUNCNAME=|^test_' assertAllVarsSame() { - local test_afterVars - test_afterVars=$(declare) + local test_afterVars + test_afterVars=$(declare) - diff \ - <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ - <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") || - fail "assertAllVarsSame: Unexpected extra global vars!" + diff \ + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") || + fail "assertAllVarsSame: Unexpected extra global vars!" } assertAllVarsExcludeOptVarsSame() { - local test_afterVars - test_afterVars=$(declare) + local test_afterVars + test_afterVars=$(declare) - diff \ - <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ - <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') || - fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!" + diff \ + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') || + fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!" } test_beforeVars=$(declare) diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh index 60d26ac5..90ab614e 100644 --- a/test-cases/self-installer.sh +++ b/test-cases/self-installer.sh @@ -1,8 +1,8 @@ #!/bin/bash -if command -v svn &> /dev/null; then - [ ! -d "/tmp/useful-scripts-$USER" ] && - svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" +if command -v svn &>/dev/null; then + [ ! -d "/tmp/useful-scripts-$USER" ] && + svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" fi export PATH="$PATH:/tmp/useful-scripts-$USER/bin:/tmp/useful-scripts-$USER/legacy-bin" diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 1cfb6d83..242f79b8 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -3,7 +3,7 @@ set -eEuo pipefail READLINK_CMD=readlink if command -v greadlink &>/dev/null; then - READLINK_CMD=greadlink + READLINK_CMD=greadlink fi BASE="$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" @@ -23,18 +23,18 @@ test_input=$(cat uq_test_input) ################################################# test_uq_simple() { - assertEquals "c${nl}v${nl}a${nl}u" \ - "$(echo "$test_input" | "$uq")" - assertEquals "c${nl}v${nl}a${nl}u" \ - "$("$uq" uq_test_input)" - - assertEquals "c${nl}a" \ - "$(echo "$test_input" | "$uq" -d)" - assertEquals "c${nl}a" \ - "$("$uq" -d uq_test_input)" - - assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)" - assertEquals "v${nl}u" "$("$uq" -u uq_test_input)" + assertEquals "c${nl}v${nl}a${nl}u" \ + "$(echo "$test_input" | "$uq")" + assertEquals "c${nl}v${nl}a${nl}u" \ + "$("$uq" uq_test_input)" + + assertEquals "c${nl}a" \ + "$(echo "$test_input" | "$uq" -d)" + assertEquals "c${nl}a" \ + "$("$uq" -d uq_test_input)" + + assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)" + assertEquals "v${nl}u" "$("$uq" -u uq_test_input)" } readonly test_output_uq_count=' 4 c @@ -52,96 +52,96 @@ readonly test_output_uq_D_count=' 4 c 1 u' test_uq_count() { - assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)" - assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)" + assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)" + assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)" - assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)" - assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)" + assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)" + assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)" } test_uq_only_D_option__same_as_cat() { - assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)" - assertEquals "$test_input" "$("$uq" -D uq_test_input)" + assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)" + assertEquals "$test_input" "$("$uq" -D uq_test_input)" } test_multi_input_files__output_file() { - local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" - "$uq" uq_test_input uq_test_another_input "$output_file" - assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \ - "$(cat "$output_file")" - - local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" - "$uq" -d uq_test_input uq_test_another_input "$output_file" - assertEquals "c${nl}a${nl}m" \ - "$(cat "$output_file")" - - local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" - "$uq" -u uq_test_input uq_test_another_input "$output_file" - assertEquals "v${nl}u${nl}x" \ - "$(cat "$output_file")" + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -d uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}a${nl}m" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -u uq_test_input uq_test_another_input "$output_file" + assertEquals "v${nl}u${nl}x" \ + "$(cat "$output_file")" } test_multi_input_files__output_stdout() { - assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)" + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)" - assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)" + assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)" - assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)" + assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)" } test_ignore_case() { - local input="a${nl}b${nl}A" + local input="a${nl}b${nl}A" - assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")" - assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)" + assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")" + assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)" } test_ignore_case__count() { - local input="a${nl}b${nl}A" + local input="a${nl}b${nl}A" - assertEquals " 1 a${nl} 1 b${nl} 1 A" \ - "$(echo "$input" | "$uq" -c)" + assertEquals " 1 a${nl} 1 b${nl} 1 A" \ + "$(echo "$input" | "$uq" -c)" - assertEquals " 2 a${nl} 1 b" \ - "$(echo "$input" | "$uq" -i -c)" + assertEquals " 2 a${nl} 1 b" \ + "$(echo "$input" | "$uq" -i -c)" - assertEquals " 2 a${nl} 1 b${nl} 2 A" \ - "$(echo "$input" | "$uq" -i -D -c)" + assertEquals " 2 a${nl} 1 b${nl} 2 A" \ + "$(echo "$input" | "$uq" -i -D -c)" } test_max_input_check() { - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq"' - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq" -XM 4' - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq" -XM 1k' - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq" --max-input 1042k' - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq" --max-input 1m' - # shellcheck disable=SC2016 - assertTrue 'echo 123 | "$uq" --max-input 10420g' - # shellcheck disable=SC2016 - assertTrue '"$uq" uq_test_input' - # shellcheck disable=SC2016 - assertTrue '"$uq" uq_test_input -XM 42m' - # shellcheck disable=SC2016 - assertTrue '"$uq" uq_test_input --max-input 1024000g' - # shellcheck disable=SC2016 - assertTrue '"$uq" uq_test_input --max-input 1234567890g' - - # shellcheck disable=SC2016 - assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1' - # shellcheck disable=SC2016 - assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3' - # shellcheck disable=SC2016 - assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2' - # shellcheck disable=SC2016 - assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input' - - # shellcheck disable=SC2016 - assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq"' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 4' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 1k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1042k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1m' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 10420g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input -XM 42m' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1024000g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1234567890g' + + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1' + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input' + + # shellcheck disable=SC2016 + assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g' } ################################################# From 9eede160555f81fcc0d65fd34c777ca6a65266a0 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 21:55:06 +0800 Subject: [PATCH 109/175] chore(ci): fix ci failure of `parseOpts.sh` --- lib/parseOpts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 7a1a5408..b9d38ea5 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -183,7 +183,7 @@ parseOpts() { local optDescLines=$(echo "$optsDescription" | # cut head and tail space $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | - awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}') + awk -F '[\t ]*\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}') local optDesc while read optDesc; do # optDesc LIKE b,b-long: From aeb58b14c7ef3f471fae5a5ede40818b1f571923 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 22:03:50 +0800 Subject: [PATCH 110/175] refactor: fix `parseOpts.sh` warning from `shellcheck`; fix typos --- lib/parseOpts.sh | 90 ++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index b9d38ea5..571045d3 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -4,9 +4,9 @@ # # @Usage # source this script to your script file, then use func parseOpts. -# parseOpts func useage sample: +# parseOpts func usage sample: # $ parseOpts "a,a-long|b,b-long:|c,c-long+" -a -b bv -c c.sh -p pv -q qv arg1 \; aa bb cc -# then below globle var is set: +# then below global var is set: # _OPT_VALUE_a = true # _OPT_VALUE_a_long = true # _OPT_VALUE_b = bv @@ -19,13 +19,14 @@ # @author Jerry Lee (oldratlee at gmail dot com) ##################################################################### -# Util Funtions +# Util Functions ##################################################################### # NOTE: $'foo' is the escape sequence syntax of bash readonly _opts_ec=$'\033' # escape char readonly _opts_eend=$'\033[0m' # escape end +# shellcheck disable=SC2209 _opts_SED_CMD=sed if command -v gsed &>/dev/null; then _opts_SED_CMD=gsed @@ -35,7 +36,7 @@ _opts_colorEcho() { local color=$1 shift # if stdout is console, turn on color output. - [ -t 1 ] && echo "$_opts_ec[1;${color}m$@$_opts_eend" || echo "$@" + [ -t 1 ] && echo "${_opts_ec}[1;${color}m$*${_opts_eend}" || echo "$*" } _opts_redEcho() { @@ -44,7 +45,7 @@ _opts_redEcho() { _opts_convertToVarName() { [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_convertToVarName: $@" + _opts_redEcho "NOT 1 arguments when call _opts_convertToVarName: $*" return 1 } echo "$1" | $_opts_SED_CMD 's/-/_/g' @@ -53,7 +54,7 @@ _opts_convertToVarName() { ##################################################################### # Parse Functions # -# Use Globle Variable: +# Use Globe Variable: # * _OPT_INFO_LIST_INDEX : Option info, data structure. # _OPT_INFO_LIST_INDEX ->* _a_a_long -> option value. # * _OPT_VALUE_* : value of option. is Array type for + mode option @@ -62,14 +63,14 @@ _opts_convertToVarName() { _opts_findOptMode() { [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_findOptMode: $@" + _opts_redEcho "NOT 1 arguments when call _opts_findOptMode: $*" return 1 } local opt="$1" # like a, a-long local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local mode="${idxNameArray[0]}" @@ -88,7 +89,7 @@ _opts_findOptMode() { _opts_setOptBool() { [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptBool: $@" + _opts_redEcho "NOT 2 arguments when call _opts_setOptBool: $*" return 1 } @@ -97,7 +98,7 @@ _opts_setOptBool() { _opts_setOptValue() { [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptValue: $@" + _opts_redEcho "NOT 2 arguments when call _opts_setOptValue: $*" return 1 } @@ -106,7 +107,7 @@ _opts_setOptValue() { local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local optName @@ -114,7 +115,9 @@ _opts_setOptValue() { [ "$opt" == "$optName" ] && { local optName2 for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + # shellcheck disable=SC2016 local from='"$value"' eval "$optValueVarName=$from" # set global var! done @@ -133,7 +136,7 @@ _opts_setOptArray() { local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local optName @@ -142,7 +145,8 @@ _opts_setOptArray() { # set _OPT_VALUE local optName2 for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" local from='"$@"' eval "$optValueVarName=($from)" # set global var! done @@ -158,14 +162,15 @@ _opts_setOptArray() { _opts_cleanOptValueInfoList() { local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" local -a idxNameArray=("${!idxNameArrayPlaceHolder}") eval "unset $idxName" local optName for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" eval "unset $optValueVarName" done done @@ -180,28 +185,34 @@ parseOpts() { _OPT_INFO_LIST_INDEX=() # set global var! - local optDescLines=$(echo "$optsDescription" | - # cut head and tail space - $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | - awk -F '[\t ]*\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}') + local optDescLines + optDescLines=$( + echo "$optsDescription" | + $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | # cut head and tail space + awk -F '[\t ]*\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}' + ) local optDesc - while read optDesc; do # optDesc LIKE b,b-long: + while read -r optDesc; do # optDesc LIKE b,b-long: [ -z "$optDesc" ] && continue - local mode="${optDesc:(-1)}" # LIKE : or + + # LIKE : or + + local mode="${optDesc:(-1)}" case "$mode" in + | : | -) - optDesc="${optDesc:0:(${#optDesc} - 1)}" # LIKE b,b-long + # LIKE b,b-long + optDesc="${optDesc:0:(${#optDesc} - 1)}" ;; *) mode="-" ;; esac - local optLines=$(echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}') # LIKE "a\na-long" + # LIKE "a\na-long" + local optLines + optLines="$(echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}')" - [ $(echo "$optLines" | wc -l) -gt 2 ] && { + [ "$(echo "$optLines" | wc -l)" -gt 2 ] && { _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2 _opts_cleanOptValueInfoList return 220 @@ -209,27 +220,27 @@ parseOpts() { local -a optTuple=() local opt - while read opt; do # opt LIKE a , a-long + while read -r opt; do # opt LIKE a , a-long [ -z "$opt" ] && continue - [ ${#opt} -eq 1 ] && { + if [ ${#opt} -eq 1 ]; then echo "$opt" | grep -E '^[a-zA-Z0-9]$' -q || { _opts_redEcho "Illegal short option name($opt in $optDesc) in option description!" 1>&2 _opts_cleanOptValueInfoList return 221 } - } || { + else echo "$opt" | grep -E '^[-a-zA-Z0-9]+$' -q || { _opts_redEcho "Illegal long option name($opt in $optDesc) in option description!" 1>&2 _opts_cleanOptValueInfoList return 222 } - } + fi optTuple=("${optTuple[@]}" "$opt") done < <(echo "$optLines") [ ${#optTuple[@]} -gt 2 ] && { - _opts_redEcho "more than 2 opt(${optTuple[@]}) in option description($optDesc)!" 1>&2 + _opts_redEcho "more than 2 opt(${optTuple[*]}) in option description($optDesc)!" 1>&2 _opts_cleanOptValueInfoList return 223 } @@ -263,8 +274,10 @@ parseOpts() { ;; -*) # short & long option(-a, -a-long), use same read-in logic. local opt="$1" - local optName=$(echo "$1" | $_opts_SED_CMD -r 's/^--?//') - local mode=$(_opts_findOptMode "$optName") + local optName + optName=$(echo "$1" | $_opts_SED_CMD -r 's/^--?//') + local mode + mode=$(_opts_findOptMode "$optName") case "$mode" in -) _opts_setOptBool "$optName" "true" @@ -292,7 +305,7 @@ parseOpts() { } || valueArray=("${valueArray[@]}" "$value") done [ "$foundComma" ] || { - _opts_redEcho "value of option $opt no end comma, value = ${valueArray[@]}" 1>&2 + _opts_redEcho "value of option $opt no end comma, value = ${valueArray[*]}" 1>&2 _opts_cleanOptValueInfoList return 231 } @@ -324,7 +337,7 @@ _opts_showOptDescInfoList() { echo "show option desc info list:" local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" echo "$idxName = (${!idxNameArrayPlaceHolder})" done echo "===============================================================================" @@ -335,14 +348,15 @@ _opts_showOptValueInfoList() { echo "show option value info list:" local idxName for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" + local idxNameArrayPlaceHolder="${idxName}[@]" local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local mode=${idxNameArray[0]} local optName for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" case "$mode" in -) echo "$optValueVarName=${!optValueVarName}" @@ -351,12 +365,12 @@ _opts_showOptValueInfoList() { echo "$optValueVarName=${!optValueVarName}" ;; +) - local optArrayValueArrayPlaceHolder="$optValueVarName[@]" + local optArrayValueArrayPlaceHolder="${optValueVarName}[@]" echo "$optValueVarName=(${!optArrayValueArrayPlaceHolder})" ;; esac done done - echo "_OPT_ARGS=(${_OPT_ARGS[@]})" + echo "_OPT_ARGS=(${_OPT_ARGS[*]})" echo "===============================================================================" } From fd5e18e84034a6198723f51156cd798dd62cf6bc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Dec 2022 22:11:02 +0800 Subject: [PATCH 111/175] refactor: set `_opts_SED_CMD` if absent; adjust comments format --- lib/parseOpts.sh | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 571045d3..341d7e4e 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -27,9 +27,13 @@ readonly _opts_ec=$'\033' # escape char readonly _opts_eend=$'\033[0m' # escape end # shellcheck disable=SC2209 -_opts_SED_CMD=sed -if command -v gsed &>/dev/null; then - _opts_SED_CMD=gsed + +if [ -z "${_opts_SED_CMD:-}" ]; then + _opts_SED_CMD=sed + if command -v gsed &>/dev/null; then + _opts_SED_CMD=gsed + fi + readonly _opts_SED_CMD fi _opts_colorEcho() { @@ -76,7 +80,8 @@ _opts_findOptMode() { local mode="${idxNameArray[0]}" local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do [ "$opt" == "${optName}" ] && { echo "$mode" return @@ -111,7 +116,8 @@ _opts_setOptValue() { local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do [ "$opt" == "$optName" ] && { local optName2 for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do @@ -119,7 +125,8 @@ _opts_setOptValue() { optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" # shellcheck disable=SC2016 local from='"$value"' - eval "$optValueVarName=$from" # set global var! + # set global var! + eval "$optValueVarName=$from" done return } @@ -140,7 +147,8 @@ _opts_setOptArray() { local -a idxNameArray=("${!idxNameArrayPlaceHolder}") local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do [ "$opt" == "$optName" ] && { # set _OPT_VALUE local optName2 @@ -168,7 +176,8 @@ _opts_cleanOptValueInfoList() { eval "unset $idxName" local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do local optValueVarName optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" eval "unset $optValueVarName" @@ -192,8 +201,8 @@ parseOpts() { awk -F '[\t ]*\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}' ) - local optDesc - while read -r optDesc; do # optDesc LIKE b,b-long: + local optDesc # optDesc LIKE b,b-long: + while read -r optDesc; do [ -z "$optDesc" ] && continue # LIKE : or + @@ -208,8 +217,7 @@ parseOpts() { ;; esac - # LIKE "a\na-long" - local optLines + local optLines # LIKE "a\na-long" optLines="$(echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}')" [ "$(echo "$optLines" | wc -l)" -gt 2 ] && { @@ -219,8 +227,8 @@ parseOpts() { } local -a optTuple=() - local opt - while read -r opt; do # opt LIKE a , a-long + local opt # opt LIKE a , a-long + while read -r opt; do [ -z "$opt" ] && continue if [ ${#opt} -eq 1 ]; then @@ -325,7 +333,8 @@ parseOpts() { ;; esac done - _OPT_ARGS=("${args[@]}") # set global var! + # set global var! + _OPT_ARGS=("${args[@]}") } ##################################################################### @@ -354,7 +363,8 @@ _opts_showOptValueInfoList() { local mode=${idxNameArray[0]} local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do local optValueVarName optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" case "$mode" in From d3ec7425fab4b91d61369a310681667589eb8734 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 6 Jan 2023 00:29:04 +0800 Subject: [PATCH 112/175] disable default link of github images --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 72090567..e910e2e5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #

🐌 useful-scripts
-repo-icon +repo-icon

Github Workflow Build Status From 9d8379c6af92c3d21d1ed1a3bc94e8a2394d20e8 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 3 Mar 2023 09:10:56 +0800 Subject: [PATCH 113/175] chore(ci): update `ci.yaml` --- .github/workflows/ci.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 99e91726..d867be6e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - os: [ ubuntu-latest, macos-11, macos-12 ] + os: [ ubuntu-latest, macos-11, macos-latest ] fail-fast: false max-parallel: 64 name: Test on ${{ matrix.os }} @@ -21,6 +21,7 @@ jobs: with: submodules: recursive - run: brew install coreutils gnu-sed + # https://docs.github.com/en/actions/learn-github-actions/variables#detecting-the-operating-system # https://docs.github.com/en/actions/learn-github-actions/expressions - if: ${{ startsWith(matrix.os, 'macos') }} + if: runner.os == 'macOS' - run: test-cases/integration-test.sh From b94f8ac6d70867f3231b24b13274c3df6530ea93 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 9 Aug 2023 00:01:14 +0800 Subject: [PATCH 114/175] docs: update logo and image links in docs --- README.md | 9 +++++---- docs/logo-social-original.png | Bin 28127 -> 29770 bytes docs/logo-social.png | Bin 41280 -> 42461 bytes docs/logo.meta.txt | 4 ++-- docs/logo.png | Bin 10954 -> 11392 bytes 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e910e2e5..9f01e635 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -#

🐌 useful-scripts
- -repo-icon +#
🐌 useful-scripts

Github Workflow Build Status @@ -14,6 +12,7 @@

🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc. + 👉 平时有用的手动操作做成脚本,以便捷地使用,让开发的日常生活更轻松些。 💕 欢迎 👏 💖 @@ -25,7 +24,9 @@ 本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 -如果你的公司有部署使用,欢迎使用通过 [Issue:who's using | 用户反馈收集](https://github.com/oldratlee/useful-scripts/issues/96) 告知,方便互相交流反馈~ 💘 +如果你的公司有部署使用,欢迎使用通过 [Issue:who's using | 用户反馈收集](https://github.com/oldratlee/useful-scripts/issues/96) 告知,方便互相交流反馈~ 💗 + +repo-icon ---------------------- diff --git a/docs/logo-social-original.png b/docs/logo-social-original.png index 633f18aa05af3fe792e9174e4457a68d5c2da64f..9820b4ca3100ed65220f408cda62827fbdabd543 100644 GIT binary patch literal 29770 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 jPhgykZcF&^IsE3fL}?T7cbES&s!3H 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 Q`*Af{Nu}py;xGOG3sl5HqW}N^ diff --git a/docs/logo-social.png b/docs/logo-social.png index cc4a4715c7063343d96858fe6b6b20e9baf418df..70a623e46f82bee90b0d3a6066d46b03352a59bd 100644 GIT binary patch literal 42461 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 diff --git a/docs/logo.meta.txt b/docs/logo.meta.txt index 13daba05..ffa36200 100644 --- a/docs/logo.meta.txt +++ b/docs/logo.meta.txt @@ -1,6 +1,6 @@ logo is created by https://www.logoly.pro -font: Francois One +font: Zilla Slab -logo.fond-size: 65 +logo.fond-size: 60 logo-social.fond-size: 160 diff --git a/docs/logo.png b/docs/logo.png index 371650a6c39755ba63a388daed8765c048102287..ede9eeccb2e6e67822197cb19628dea6cd40bb89 100644 GIT binary patch literal 11392 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 literal 10954 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 From fa92c9fd2b53a2358ae8b8245d9460276257abed Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 9 Aug 2023 00:03:12 +0800 Subject: [PATCH 115/175] build: upgrade `shunit2` lib --- test-cases/shunit2-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib index ebc4baa0..47be8b23 160000 --- a/test-cases/shunit2-lib +++ b/test-cases/shunit2-lib @@ -1 +1 @@ -Subproject commit ebc4baa08f045b7ef0f45c4b7d6f34f08d732f3d +Subproject commit 47be8b23a46a7897e849f1841f0fb704d34d0f6e From e1febba9b29a9f0252baa0c5b9fd53b844f7aa7e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 11 Aug 2023 12:42:49 +0800 Subject: [PATCH 116/175] =?UTF-8?q?refactor:=20simplify=20`teeAndCopy`=20f?= =?UTF-8?q?unction=20in=20`c`=20=F0=9F=90=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bin/c b/bin/c index 442ac2bb..414f8bce 100755 --- a/bin/c +++ b/bin/c @@ -136,14 +136,19 @@ copy() { esac } +catThenCopy() { + local content + content="$(cat)" + # shellcheck disable=SC2086 + echo $eol "$content" | copy +} + teeAndCopy() { - # shellcheck disable=SC2015 - $quiet && local -r out=/dev/null || local -r out=/dev/stdout - tee >( - content="$(cat)" - # shellcheck disable=SC2086 - echo $eol "$content" | copy - ) >$out + if $quiet; then + catThenCopy + else + tee >(catThenCopy) + fi } if [ ${#args[@]} -eq 0 ]; then From 84b6218da62e8bc9669b210ee846bf688ebab87c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 11 Aug 2023 13:01:04 +0800 Subject: [PATCH 117/175] refactor: use boolean option parse value holder in `c`; reanme var. more readable --- bin/c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/bin/c b/bin/c index 414f8bce..7c023d63 100755 --- a/bin/c +++ b/bin/c @@ -82,12 +82,12 @@ progVersion() { ################################################################################ quiet=false -eol=-n -declare -a args=() +keep_eol=false +declare -a target_command=() while [ $# -gt 0 ]; do case "$1" in -k | --keep-eol) - eol= + keep_eol=true shift ;; -q | --quiet) @@ -102,7 +102,7 @@ while [ $# -gt 0 ]; do ;; --) shift - args=(${args[@]:+"${args[@]}"} "$@") + target_command=(${target_command[@]:+"${target_command[@]}"} "$@") break ;; -*) @@ -110,13 +110,13 @@ while [ $# -gt 0 ]; do ;; *) # if not option, treat all follow args as command - args=(${args[@]:+"${args[@]}"} "$@") + target_command=(${target_command[@]:+"${target_command[@]}"} "$@") break ;; esac done -readonly eol quiet args +readonly keep_eol quiet target_command ################################################################################ # biz logic @@ -139,8 +139,10 @@ copy() { catThenCopy() { local content content="$(cat)" - # shellcheck disable=SC2086 - echo $eol "$content" | copy + { + printf %s "$content" + $keep_eol && echo + } | copy } teeAndCopy() { @@ -151,8 +153,8 @@ teeAndCopy() { fi } -if [ ${#args[@]} -eq 0 ]; then +if [ ${#target_command[@]} -eq 0 ]; then teeAndCopy else - "${args[@]}" | teeAndCopy + "${target_command[@]}" | teeAndCopy fi From 59044e2d2ad4b3e4653ad95810eec5a9ff998e29 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 28 Aug 2023 12:35:45 +0800 Subject: [PATCH 118/175] =?UTF-8?q?refactor/robust(`c`):=20use=20`printf`?= =?UTF-8?q?=20=F0=9F=92=AA=20instead=20of=20`echo`;=20use=20`if-else`=20in?= =?UTF-8?q?stead=20of=20`&&-||`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/c | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/bin/c b/bin/c index 7c023d63..9f65eee4 100755 --- a/bin/c +++ b/bin/c @@ -35,12 +35,14 @@ readonly PROG_VERSION='2.5.0-dev' # util functions ################################################################################ -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end -readonly nl=$'\n' # new line - -redEcho() { - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" +printErrorMsg() { + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, print with red color. + if [ -t 1 ]; then + printf "\033[1;31m%s\033[0m\n\n" "Error: $*" + else + printf '%s\n\n' "Error: $*" + fi } usage() { @@ -49,7 +51,7 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + (($# > 0)) && printErrorMsg "$*" >$out cat >$out < Date: Mon, 28 Aug 2023 12:39:53 +0800 Subject: [PATCH 119/175] =?UTF-8?q?refactor/robust(`coat/a2l`):=20use=20`p?= =?UTF-8?q?rintf`=20=F0=9F=92=AA=20instead=20of=20`echo`;=20the=20`echo`?= =?UTF-8?q?=20option(e.g.=20-e=20-n)=20may=20effect=20correctness=20?= =?UTF-8?q?=F0=9F=90=9E=20,=20`printf`=20is=20more=20robust=20=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/a2l | 34 +++++++++++++++------------------- bin/coat | 16 ++++++---------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/bin/a2l b/bin/a2l index f940fa51..6ab30820 100755 --- a/bin/a2l +++ b/bin/a2l @@ -1,6 +1,6 @@ #!/bin/bash # @Function -# echo each arguments on one line colorfully. +# print each arguments on one line colorfully. # # @Usage # $ ./a2l arg1 arg2 @@ -19,26 +19,22 @@ 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 - -colorEcho() { +colorPrint() { local color="$1" shift # check isatty in bash https://stackoverflow.com/questions/10022323 # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" -} - -redEcho() { - colorEcho 31 "$@" + if [ -t 1 ]; then + printf "\033[1;${color}m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } usage() { cat < Date: Mon, 28 Aug 2023 12:43:33 +0800 Subject: [PATCH 120/175] =?UTF-8?q?refactor/robust(`echo-args`):=20use=20`?= =?UTF-8?q?printf`=20=F0=9F=92=AA=20instead=20of=20`echo`;=20align=20the?= =?UTF-8?q?=20index=20number?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/echo-args | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/bin/echo-args b/bin/echo-args index 82b76859..a2dd472a 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -6,20 +6,37 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end +digitCount() { + # argument 1(num) is always a non-negative integer in this script usage, + # so NO argument validation logic. + local num="$1" count=0 + while ((num != 0)); do + ((++count)) + ((num = num / 10)) + done + echo "$count" +} + +digit_count=$(digitCount $#) +readonly arg_count=$# digit_count + +readonly red='\033[1;31m' +readonly blue='\033[1;36m' +readonly normal='\033[0m' -echoArg() { - local index="$1" count="$2" value="$3" +printArg() { + local idx="$1" value="$2" # if stdout is console, turn on color output. - [ -t 1 ] && - echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[1;36;40m$value$eend${ec}[1;31m]$eend" || - echo "${index}/${count}: [${value}]" + if [ -t 1 ]; then + printf "%${digit_count}s/%s: ${red}[${blue}%s${red}]${normal}\n" "$idx" "$arg_count" "$value" + else + printf "%${digit_count}s/%s: [%s]\n" "$idx" "$arg_count" "$value" + fi } -echoArg 0 $# "$0" +printArg 0 "$0" idx=1 for a; do - echoArg $((idx++)) $# "$a" + printArg $((idx++)) "$a" done From e9f69f7f0a008ec66139fc1c41e0b56afa1aa48c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 28 Aug 2023 13:25:05 +0800 Subject: [PATCH 121/175] =?UTF-8?q?refactor/robust(`find-in-jars`):=20use?= =?UTF-8?q?=20`printf`=20=F0=9F=92=AA=20instead=20of=20`echo`;=20use=20`if?= =?UTF-8?q?-else`=20instead=20of=20`&&-||`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/find-in-jars | 63 ++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index efcd9e24..5f393589 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -40,10 +40,9 @@ 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 +readonly red='\033[1;31m' normal='\033[0m' +readonly jar_color='\033[1;35m' sep_color='\033[1;32m' + # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 # @@ -52,11 +51,15 @@ readonly nl=$'\n' # new line # echo -e "\033[1K" # Or everything on the line, regardless of cursor position: # echo -e "\033[2K" -readonly clear_line=$'\033[2K\r' +readonly clear_line='\033[2K\r' -redEcho() { +redPrint() { # -t check: is a terminal device? - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" + if [ -t 1 ]; then + printf "${red}%s${normal}\n" "$*" + else + printf '%s\n' "$*" + fi } # Getting console width using a bash script @@ -72,7 +75,7 @@ printResponsiveMessage() { 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 + printf "${clear_line}%s" "${message:0:columns}" >&2 } clearResponsiveMessage() { @@ -80,12 +83,12 @@ clearResponsiveMessage() { return fi - echo -n "$clear_line" >&2 + printf "%b" "$clear_line" >&2 } die() { clearResponsiveMessage - redEcho "Error: $*" >&2 + redPrint "Error: $*" >&2 exit 1 } @@ -95,7 +98,9 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + # NOTE: $'foo' is the escape sequence syntax of bash + local -r nl=$'\n' # new line + (($# > 0)) && redPrint "$*$nl" >$out cat >$out < 0)); do break ;; -*) - usage 2 "${PROG}: unrecognized option '$1'" + usage 2 "Error: unrecognized option '$1'" ;; *) args=(${args[@]:+"${args[@]}"} "$1") @@ -319,7 +324,7 @@ listZipEntries() { # 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 + redPrint "fail to list zip entries of $zip_file, ignored: $msg" >&2 fi return 0 } @@ -327,7 +332,7 @@ listZipEntries() { "${command_to_list_zip_entries[@]}" "$zip_file" || { clearResponsiveMessage - redEcho "fail to list zip entries of $zip_file, ignored!" >&2 + redPrint "fail to list zip entries of $zip_file, ignored!" >&2 return 0 } } @@ -344,17 +349,18 @@ searchJarFiles() { 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="$(printf '%s\n' "$jar_files" | wc -l)" + # remove white space, because the `wc -l` output on mac contains white space! total_jar_count="${total_jar_count//[[:space:]]/}" echo "$total_jar_count" - echo "$jar_files" + printf '%s\n' "$jar_files" } __outputResultOfJarFile() { local jar_file="$1" file + # shellcheck disable=SC2206 + local grep_opt_args=($regex_mode ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern") if $only_print_file_name; then local matched=false @@ -366,7 +372,7 @@ __outputResultOfJarFile() { # - 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 + if grep -c "${grep_opt_args[@]}" &>/dev/null; then matched=true fi @@ -375,18 +381,23 @@ __outputResultOfJarFile() { fi clearResponsiveMessage - [ -t 1 ] && echo "${ec}[1;35m${jar_file}${eend}" || echo "${jar_file}" + if [ -t 1 ]; then + printf "${jar_color}%s${normal}\n" "${jar_file}" + else + printf '%s\n' "${jar_file}" + fi 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 + grep "${grep_opt_args[@]}" || 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}" + if [ -t 1 ]; then + printf "${jar_color}%s${sep_color}%s${normal}%s\n" "$jar_file" "$separator" "$file" + else + printf '%s\n' "${jar_file}${separator}${file}" + fi done fi } From 15047b039e151ced56be35a1fa52e1448300d942 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 28 Aug 2023 20:09:48 +0800 Subject: [PATCH 122/175] =?UTF-8?q?refactor/robust(`ap/rp`):=20improve=20r?= =?UTF-8?q?obustness=20=F0=9F=92=AA=20;=20check=20failure=20and=20reflect?= =?UTF-8?q?=20as=20exit=20code;=20improve=20portability(`portableRelPath/p?= =?UTF-8?q?ortableReadLink`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - use `printf` 💪 instead of `echo`; use `if-else` instead of `&&-||` - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/ap | 55 +++++++++++++++++++++++------------- bin/rp | 89 ++++++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 100 insertions(+), 44 deletions(-) diff --git a/bin/ap b/bin/ap index 901d8055..ea6c984c 100755 --- a/bin/ap +++ b/bin/ap @@ -21,25 +21,24 @@ 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 - -colorEcho() { +colorPrint() { local color="$1" shift # check isatty in bash https://stackoverflow.com/questions/10022323 # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + if [ -t 1 ]; then + printf "\033[1;${color}m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } -redEcho() { - colorEcho 31 "$@" +redPrint() { + colorPrint 31 "$*" } die() { - redEcho "Error: $*" 1>&2 + redPrint "Error: $*" >&2 exit 1 } @@ -54,10 +53,15 @@ portableReadLink() { readlink -f "$file" ;; Darwin*) + local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then greadlink -f "$file" + elif command -v python3 >/dev/null; then + python3 "${py_args[@]}" + elif command -v python >/dev/null; then + python "${py_args[@]}" else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + die "fail to find command(greadlink/python3/python) to get absolute path!" fi ;; *) @@ -72,10 +76,12 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + # NOTE: $'foo' is the escape sequence syntax of bash + local nl=$'\n' # new line + (($# > 0)) && redPrint "$*$nl" >$out cat >$out <&2 + has_error=true + fi done + +# set exit status +! $has_error diff --git a/bin/rp b/bin/rp index 01ad6c47..b11c5bd8 100755 --- a/bin/rp +++ b/bin/rp @@ -21,21 +21,51 @@ 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 - -colorEcho() { +colorPrint() { local color="$1" shift # check isatty in bash https://stackoverflow.com/questions/10022323 # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + if [ -t 1 ]; then + printf "\033[1;${color}m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi +} + +redPrint() { + colorPrint 31 "$@" } -redEcho() { - colorEcho 31 "$@" +die() { + redPrint "Error: $*" >&2 + exit 1 +} + +portableRelPath() { + local file="$1" relTo="$2" uname + + uname="$(uname)" + case "$uname" in + Linux* | CYGWIN* | MINGW*) + realpath "$f" --relative-to="$relTo" + ;; + Darwin*) + local py_args=(-c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$file" "$relTo") + if command -v grealpath >/dev/null; then + grealpath "$f" --relative-to="$relTo" + elif command -v python3 >/dev/null; then + python3 "${py_args[@]}" + elif command -v python >/dev/null; then + python "${py_args[@]}" + else + die "fail to find command(grealpath/python3/python) to get relative path!" + fi + ;; + *) + die "NOT support uname($uname)!" + ;; + esac } usage() { @@ -44,15 +74,18 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + # NOTE: $'foo' is the escape sequence syntax of bash + local nl=$'\n' # new line + (($# > 0)) && redPrint "$*$nl" >$out cat >$out <&2 - exit 1 -} +[ "${#files[@]}" -eq 0 ] && die "NO argument!" if [ "${#files[@]}" -eq 1 ]; then relativeTo=. @@ -113,12 +142,24 @@ else fi [ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" +[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo) does NOT exists!" + readonly files relativeTo +################################################################################ +# biz logic +################################################################################ + +has_error=false + for f in "${files[@]}"; do - ! [ -e "$f" ] && { - echo "$f does not exists!" - continue - } - realpath "$f" --relative-to="$relativeTo" + if [ -e "$f" ]; then + portableRelPath "$f" "$relativeTo" + else + redPrint "error: $f does not exists!" >&2 + has_error=true + fi done + +# set exit status +! $has_error From 7d166fe34e5f5cd3e010ef0961b11ca79d84a017 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 29 Aug 2023 12:40:58 +0800 Subject: [PATCH 123/175] =?UTF-8?q?refactor/robust(`xpl/xpf`):=20improve?= =?UTF-8?q?=20robustness=20=F0=9F=92=AA=20;=20check=20failure=20and=20refl?= =?UTF-8?q?ect=20as=20exit=20code;=20improve=20portability(`portableRelPat?= =?UTF-8?q?h/portableReadLink`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - use `printf` 💪 instead of `echo`; use `if-else` instead of `&&-||` - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/xpf | 26 ++++++++++++++++++++++---- bin/xpl | 46 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/bin/xpf b/bin/xpf index 3abad3b2..4e6c9dae 100755 --- a/bin/xpf +++ b/bin/xpf @@ -1,7 +1,7 @@ #!/bin/bash # @Function # Open file in file explorer, file is selected. -# same as xpl --selected [file [file ...] ] +# same as xpl --selected [file]... # # @Usage # $ ./xpf file @@ -10,6 +10,10 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +################################################################################ +# util functions +################################################################################ + # How can I get the behavior of GNU's readlink -f on a Mac? # https://stackoverflow.com/questions/1055671 portableReadLink() { @@ -21,10 +25,16 @@ portableReadLink() { readlink -f "$file" ;; Darwin*) + local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then greadlink -f "$file" + elif command -v python3 >/dev/null; then + python3 "${py_args[@]}" + elif command -v python >/dev/null; then + python "${py_args[@]}" else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + echo "fail to find command(greadlink/python3/python) for readlink!" >&2 + exit 1 fi ;; *) @@ -34,5 +44,13 @@ portableReadLink() { esac } -BASE="$(dirname "$(portableReadLink "${BASH_SOURCE[0]}")")" -source "$BASE/xpl" "$@" +################################################################################ +# biz logic +################################################################################ + +# DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell: +# BASE_DIR="$(dirname "$(portableReadLink "${BASH_SOURCE[0]}")")" +THIS_SCRIPT="$(portableReadLink "${BASH_SOURCE[0]}")" +BASE_DIR="$(dirname "$THIS_SCRIPT")" + +source "$BASE_DIR/xpl" "$@" diff --git a/bin/xpl b/bin/xpl index b197836e..fbad3a37 100755 --- a/bin/xpl +++ b/bin/xpl @@ -30,7 +30,9 @@ PROG="$(basename "$0")" readonly PROG readonly PROG_VERSION='2.5.0-dev' -readonly nl=$'\n' # new line +################################################################################ +# util functions +################################################################################ usage() { local -r exit_code="${1:-0}" @@ -38,7 +40,7 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && echo "$*$nl" >$out + (($# > 0)) && printf '%s\n\n' "$*" >$out cat < Date: Wed, 30 Aug 2023 13:08:59 +0800 Subject: [PATCH 124/175] =?UTF-8?q?refactor/robust(`cp-into-docker-run`):?= =?UTF-8?q?=20use=20`printf`=20=F0=9F=92=AA=20instead=20of=20`echo`;=20use?= =?UTF-8?q?=20`if-else`=20instead=20of=20`&&-||`;=20improve=20portability(?= =?UTF-8?q?`portableReadLink`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/cp-into-docker-run | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 66648c0b..ffac978b 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -17,18 +17,17 @@ 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 - -redEcho() { +redPrint() { # -t check: is a terminal device? - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" + if [ -t 1 ]; then + printf "\033[1;31m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } die() { - redEcho "Error: $*" 1>&2 + redPrint "Error: $*" 1>&2 exit 1 } @@ -47,10 +46,15 @@ portableReadLink() { readlink -f "$file" ;; Darwin*) + local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then greadlink -f "$file" + elif command -v python3 >/dev/null; then + python3 "${py_args[@]}" + elif command -v python >/dev/null; then + python "${py_args[@]}" else - python -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file" + die "fail to find command(greadlink/python3/python) for readlink!" fi ;; *) @@ -65,7 +69,9 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + # NOTE: $'foo' is the escape sequence syntax of bash + local nl=$'\n' # new line + (($# > 0)) && redPrint "$*$nl" >$out cat >$out <&2 + $verbose && printf '%s\n' "[$PROG] $*" 1>&2 "$@" } From e5f5fac46cf69663bde478095fc70b5bef20f71f Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 30 Aug 2023 13:21:00 +0800 Subject: [PATCH 125/175] =?UTF-8?q?refactor/robust(`tcp-connection-state-c?= =?UTF-8?q?ounter/console-text-color-themes.sh`):=20use=20`printf`=20?= =?UTF-8?q?=F0=9F=92=AA=20instead=20of=20`echo`;=20use=20`if-else`=20inste?= =?UTF-8?q?ad=20of=20`&&-||`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/tcp-connection-state-counter | 3 +- lib/console-text-color-themes.sh | 47 +++++++++++++++++++------------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 6629e8fb..cbebc013 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -35,7 +35,7 @@ EOF } progVersion() { - echo "$PROG $PROG_VERSION" + printf '%s\n' "$PROG $PROG_VERSION" exit } @@ -58,6 +58,7 @@ done # On MacOS, netstat need to using -p tcp to get only tcp output. uname | grep Darwin -q && option_for_mac="-ptcp" +# shellcheck disable=SC2086 netstat -tna ${option_for_mac:-} | awk 'NR > 2 { ++s[$NF] } diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index dcff9380..63de93ff 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -37,55 +37,64 @@ colorEcho() { local combination="$1" shift 1 - [ -t 1 ] && echo "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo "$*" + if [ -t 1 ]; then + echo "${_ctct_ec}[${combination}m$*$_ctct_eend" + else + echo "$*" + fi } colorEchoWithoutNewLine() { local combination="$1" shift 1 - [ -t 1 ] && echo -n "${_ctct_ec}[${combination}m$*$_ctct_eend" || echo -n "$*" + if [ -t 1 ]; then + echo -n "${_ctct_ec}[${combination}m$*$_ctct_eend" + else + echo -n "$*" + fi } # if not directly run this script(use as lib), just export 2 helper functions, # and do NOT print anything. -[ "$_ctct_is_direct_run" == "true" ] && { +[ "$_ctct_is_direct_run" == true ] && { for style in 0 1 2 3 4 5 6 7; do for fg in 30 31 32 33 34 35 36 37; do for bg in 40 41 42 43 44 45 46 47; do combination="${style};${fg};${bg}" colorEchoWithoutNewLine "$combination" "$combination" - echo -n " " + printf ' ' done echo done echo done - echo "Code sample to print color text:" + echo 'Code sample to print color text:' echo -n ' echo -e "\033[' - colorEchoWithoutNewLine "3;35;40" "1;36;41" - echo -n "m" - colorEchoWithoutNewLine "0;32;40" "Sample Text" - echo "\033[0m\"" + colorEchoWithoutNewLine '3;35;40' '1;36;41' + echo -n m + colorEchoWithoutNewLine '0;32;40' 'Sample Text' + echo '\033[0m"' echo -n " echo \$'\033[" - colorEchoWithoutNewLine "3;35;40" "1;36;41" + colorEchoWithoutNewLine '3;35;40' '1;36;41' echo -n "m'\"" - colorEchoWithoutNewLine "0;32;40" "Sample Text" + colorEchoWithoutNewLine '0;32;40' 'Sample Text' echo "\"$'\033[0m'" echo " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" - echo "Output of above code:" - echo " ${_ctct_ec}[1;36;41mSample Text${_ctct_eend}" + echo 'Output of above code:' + echo -n ' ' + colorEcho '1;36;41' 'Sample Text' echo - echo "If you are going crazy to write text in escapes string like me," - echo "you can use colorEcho and colorEchoWithoutNewLine function in this script." + echo 'If you are going crazy to write text in escapes string like me,' + echo 'you can use colorEcho and colorEchoWithoutNewLine function in this script.' echo - echo "Code sample to print color text:" + echo 'Code sample to print color text:' echo ' colorEcho "1;36;41" "Sample Text"' - echo "Output of above code:" - echo -n " " - colorEcho "1;36;41" "Sample Text" + echo 'Output of above code:' + echo -n ' ' + colorEcho '1;36;41' 'Sample Text' } From b2a6bb6e457d39a93a928c15a55f8f6931d7298c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 30 Aug 2023 13:58:55 +0800 Subject: [PATCH 126/175] =?UTF-8?q?refactor/robust(`uq`):=20use=20`printf`?= =?UTF-8?q?=20=F0=9F=92=AA=20instead=20of=20`echo`;=20use=20`if-else`=20in?= =?UTF-8?q?stead=20of=20`&&-||`;=20use=20`${var#}`=20instead=20of=20`awk`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/uq | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/bin/uq b/bin/uq index f7b2ebee..bdb59914 100755 --- a/bin/uq +++ b/bin/uq @@ -38,20 +38,28 @@ readonly PROG_VERSION='2.5.0-dev' ################################################################################ # 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 - -redEcho() { - [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*" +readonly nl=$'\n' # new line + +redPrint() { + # -t check: is a terminal device? + if [ -t 1 ]; then + printf "\033[1;31m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } -yellowEcho() { - [ -t 1 ] && echo "${ec}[1;33m$*$eend" || echo "$*" +yellowPrint() { + # -t check: is a terminal device? + if [ -t 1 ]; then + printf "\033[1;33m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } die() { - redEcho "Error: $*" 1>&2 + redPrint "Error: $*" 1>&2 exit 1 } @@ -82,7 +90,7 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && redEcho "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >$out cat >$out < 0)); do --all-repeated=*) uq_opt_all_repeated=1 - uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}') + uq_opt_repeated_method=${1#--all-repeated=} [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" @@ -209,7 +217,7 @@ done usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && - yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 + yellowPrint "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 # NOTE: DO NOT declare var uq_max_input_size as readonly in ONE line! uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || From 0942d598eeee3619540834f4191460283dfda507 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 1 Sep 2023 00:41:37 +0800 Subject: [PATCH 127/175] =?UTF-8?q?refactor/robust(`show-busy-java-threads?= =?UTF-8?q?`):=20use=20`printf`=20=F0=9F=92=AA=20instead=20of=20`echo`;=20?= =?UTF-8?q?use=20`if-else`=20instead=20of=20`&&-||`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTE: - the `echo` option(e.g. -e -n) may effect correctness, `printf` is more robust 💪 - about `&&-||` see shell check: https://www.shellcheck.net/wiki/SC2015 --- bin/show-busy-java-threads | 91 ++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 4abd1778..2e761a96 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -53,59 +53,66 @@ readonly ec=$'\033' # escape char readonly eend=$'\033[0m' # escape end readonly nl=$'\n' # new line -colorEcho() { +colorPrint() { local color=$1 shift # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" + if [ -t 1 ]; then + printf "\033[1;${color}m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } -colorPrint() { +__appendToFile() { + [[ -n "$append_file" && -w "$append_file" ]] && printf '%s\n' "$*" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && printf '%s\n' "$*" >>"${store_file_prefix}$PROG" +} + +colorOutput() { local color=$1 shift - colorEcho "$color" "$@" - [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" - [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" + colorPrint "$color" "$*" + __appendToFile "$*" } # shellcheck disable=SC2120 -normalPrint() { - echo "$@" - [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file" - [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG" +normalOutput() { + printf '%s\n' "$*" + __appendToFile "$*" } -redPrint() { - colorPrint 31 "$@" +redOutput() { + colorOutput 31 "$*" } -greenPrint() { - colorPrint 32 "$@" +greenOutput() { + colorOutput 32 "$*" } -yellowPrint() { - colorPrint 33 "$@" +yellowOutput() { + colorOutput 33 "$*" } -bluePrint() { - colorPrint 36 "$@" +blueOutput() { + colorOutput 36 "$*" } die() { - redPrint "Error: $*" 1>&2 + redOutput "Error: $*" 1>&2 exit 1 } logAndRun() { - echo "$@" + printf '%s\n' "$*" echo "$@" } logAndCat() { - echo "$@" + printf '%s\n' "$*" echo cat } @@ -147,7 +154,7 @@ usage() { # shellcheck disable=SC2015 [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout - (($# > 0)) && colorEcho 31 "$*$nl" >$out + (($# > 0)) && colorPrint 31 "$*$nl" >$out cat >$out <"${store_file_prefix}$((update_round_num + 1))_ps" + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" fi if ((count > 0)); then - echo "$ps_out" | head -n "${count}" + printf '%s\n' "$ps_out" | head -n "${count}" else - echo "$ps_out" + printf '%s\n' "$ps_out" fi } @@ -473,13 +480,13 @@ __top_threadId_cpu() { local top_out top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then - echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" + printf '%s\n' "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" fi # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! local result_threads_top_info result_threads_top_info=$( - echo "$top_out" | awk '{ + printf '%s\n' "$top_out" | awk '{ # from text line to empty line, increase block index if (previousLine && !$0) blockIndex++ # only print 4th text block(blockIndex == 3), aka. process info of second top update @@ -490,7 +497,7 @@ __top_threadId_cpu() { ) [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found - echo "$result_threads_top_info" | sort -k2,2nr + printf '%s\n' "$result_threads_top_info" | sort -k2,2nr } __complete_pid_user_by_ps() { @@ -501,7 +508,7 @@ __complete_pid_user_by_ps() { local ps_out ps_out="$("${ps_cmd_line[@]}")" if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" fi local idx=0 threadId pcpu output_fields @@ -509,13 +516,13 @@ __complete_pid_user_by_ps() { ((count <= 0 || idx < count)) || break # output field: pid, threadId, pcpu, user - output_fields="$(echo "$ps_out" | + output_fields="$(printf '%s\n' "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { print $1, threadId, pcpu, $3; exit }')" if [ -n "$output_fields" ]; then ((idx++)) - echo "$output_fields" + printf '%s\n' "$output_fields" fi done } @@ -543,20 +550,20 @@ printStackOfThreads() { logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}" else # current user is not root user, so can not run with sudo; print error message and rerun suggestion - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" - yellowPrint " sudo $(printCallingCommandLine)" - normalPrint + redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + redOutput "User of java process($user) is not current user($USER), need sudo to rerun:" + yellowOutput " sudo $(printCallingCommandLine)" + normalOutput continue fi || { - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - normalPrint + redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + normalOutput rm "${jstackFile}" &>/dev/null continue } } - bluePrint "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" + blueOutput "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" if [ -n "$mix_native_frames" ]; then local sed_script="/--------------- $threadId ---------------/,/^---------------/ { From b82d5325b93e1340643f329575be63d0997dbced Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 3 Sep 2023 13:25:13 +0800 Subject: [PATCH 128/175] refactor: fix `shellcheck` issues --- bin/ap | 4 ++-- bin/c | 4 ++-- bin/cp-into-docker-run | 4 ++-- bin/find-in-jars | 6 +++--- bin/rp | 4 ++-- bin/show-busy-java-threads | 8 +++----- bin/uq | 4 ++-- bin/xpl | 2 +- lib/console-text-color-themes.sh | 18 +++++++++--------- 9 files changed, 26 insertions(+), 28 deletions(-) diff --git a/bin/ap b/bin/ap index ea6c984c..7ab6f4ef 100755 --- a/bin/ap +++ b/bin/ap @@ -78,9 +78,9 @@ usage() { # NOTE: $'foo' is the escape sequence syntax of bash local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >"$out" - cat >$out <"$out" < 0)) && printErrorMsg "$*" >$out + (($# > 0)) && printErrorMsg "$*" >"$out" - cat >$out <"$out" < 0)) && redPrint "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >"$out" - cat >$out <"$out" < 0)) && redPrint "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >"$out" - cat >$out <"$out" < 0)) && redPrint "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >"$out" - cat >$out <"$out" < 0)) && colorPrint 31 "$*$nl" >$out + (($# > 0)) && colorPrint 31 "$*$nl" >"$out" - cat >$out <"$out" < 0)) && redPrint "$*$nl" >$out + (($# > 0)) && redPrint "$*$nl" >"$out" - cat >$out <"$out" < 0)) && printf '%s\n\n' "$*" >$out + (($# > 0)) && printf '%s\n\n' "$*" >"$out" cat < Date: Tue, 5 Sep 2023 10:16:16 +0800 Subject: [PATCH 129/175] =?UTF-8?q?feat:=20add=20`taoc`=20=F0=9F=AB=B4=20?= =?UTF-8?q?=F0=9F=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- bin/taoc | 36 ++++++++++++++++++++++++++++++++++++ docs/shell.md | 44 ++++++++++++++++++++++++++++++++++---------- 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100755 bin/taoc diff --git a/README.md b/README.md index 9f01e635..5c6fb9cd 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r 1. [c](docs/shell.md#-c) 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 -1. [coat](docs/shell.md#-coat) - 彩色`cat`出文件行,方便人眼区分不同的行。 +1. [coat and taoc](docs/shell.md#-coat) + 彩色`cat`/`tac`出文件行,方便人眼区分不同的行。 1. [a2l](docs/shell.md#-a2l) 按行彩色输出参数,方便人眼查看。 1. [uq](docs/shell.md#-uq) diff --git a/bin/taoc b/bin/taoc new file mode 100755 index 00000000..de5f600f --- /dev/null +++ b/bin/taoc @@ -0,0 +1,36 @@ +#!/bin/bash +# @Function +# tac lines colorfully. taoc means *CO*lorful c*AT* in reverse(last line first). +# +# @Usage +# $ echo -e 'Hello\nWorld' | taoc +# $ taoc /path/to/file1 +# $ taoc /path/to/file1 /path/to/file2 +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-coat +# @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail + +# if not in console, use tac directly +# check isatty in bash https://stackoverflow.com/questions/10022323 +[ ! -t 1 ] && exec tac "$@" + +readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) +COUNT=0 +rotateColorPrint() { + local message="$*" + + # skip color for white space + if [[ "$message" =~ ^[[:space:]]*$ ]]; then + printf '%s\n' "$message" + else + local color="${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]}" + printf "\033[1;${color}m%s\033[0m\n" "$message" + fi +} + +# Bash read line does not read leading spaces +# https://stackoverflow.com/questions/29689172 +tac "$@" | while IFS= read -r line; do + rotateColorPrint "$line" +done diff --git a/docs/shell.md b/docs/shell.md index 5d451f19..ff81c3ae 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -8,7 +8,7 @@ - [🍺 c](#-c) - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - [🍺 coat](#-coat) + - [🍺 coat and taoc](#-coat-and-taoc) - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-1) - [🍺 a2l](#-a2l) - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-2) @@ -51,7 +51,7 @@ 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便快速键入。 +命令名`c`的意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便快速键入。 更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip)。 @@ -115,15 +115,19 @@ Options: - [拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 - 关于文本文件最后的换行,参见[Why should text files end with a newline?](https://stackoverflow.com/questions/729692) -🍺 [coat](../bin/coat) + + +🍺 [coat](../bin/coat) and [taoc](../bin/taoc) ---------------------- -彩色`cat`出文件行,方便人眼区分不同的行。 +彩色`cat`/`tac`出文件行,方便人眼区分不同的行。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令支持选项、功能和使用方式与[`cat`命令](https://linux.die.net/man/1/cat)完全一样(实际上文件操作的实现全部代理给了`cat`命令)。 +命令支持选项、功能和使用方式与[`cat`](https://linux.die.net/man/1/cat)/[`tac`](https://linux.die.net/man/1/cat)命令完全一样。 +文件操作在实现上完全代理给了`cat`/`tac`命令。 -命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色的输出行就像件漂亮的外套~ 😆 +- 命令名`coat`的意思是`COlorful cAT`;同时单词`coat`是外套,而彩色的输出行就像件漂亮的外套~ 🌈 😆 +- 命令名`taoc`是`coat`倒序拼写;命名方式就像`tac`之于`cat`。 🐈 ### 用法/示例 @@ -133,6 +137,9 @@ Hello world $ echo -e 'Hello\nWorld' | coat Hello World +$ echo -e 'Hello\nWorld' | taoc +World +Hello $ echo -e 'Hello\nWorld' | nl | coat 1 Hello 2 World @@ -145,7 +152,7 @@ line2 of file2 ... # 帮助信息 -# 可以看到本人机器上实现代理的`cat`命令是GNU的实现。 +# 可以看到本人机器上实现代理的`cat`/`tac`命令是GNU的实现。 $ coat --help Usage: cat [OPTION]... [FILE]... Concatenate FILE(s) to standard output. @@ -172,6 +179,23 @@ Examples: GNU coreutils online help: Full documentation at: or available locally via: info '(coreutils) cat invocation' + +$ taoc --help +Usage: tac [OPTION]... [FILE]... +Write each FILE to standard output, last line first. + +With no FILE, or when FILE is -, read standard input. + +Mandatory arguments to long options are mandatory for short options too. + -b, --before attach the separator before instead of after + -r, --regex interpret the separator as a regular expression + -s, --separator=STRING use STRING as the separator instead of newline + --help display this help and exit + --version output version information and exit + +GNU coreutils online help: +Full documentation +or available locally via: info '(coreutils) tac invocation' ``` 注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,如下: @@ -183,7 +207,7 @@ or available locally via: info '(coreutils) cat invocation' 按行彩色输出参数,方便人眼查看。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`a2l`意思是`Arguments to(2) Lines`。 +命令名`a2l`的意思是`Arguments to(2) Lines`。 ### 用法/示例 @@ -336,7 +360,7 @@ Options: 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`ap`意思是`Absolute Path`,`rp`是`Relative Path`。 +命令名`ap`的意思是`Absolute Path`,`rp`是`Relative Path`。 ### 用法/示例 @@ -445,7 +469,7 @@ SYN_SENT 7 - `xpl`:在文件浏览器中打开指定的文件或文件夹。 `xpl`是`explorer`的缩写。 - `xpf`: 在文件浏览器中打开指定的文件或文件夹,并选中。 - `xpf`是`explorer and select file`的缩写。 + `xpf`是`EXplorer and select File`的缩写。 ### 用法/示例 From caab849227c5c81c0208c31ff03337987b034be0 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 6 Sep 2023 11:22:14 +0800 Subject: [PATCH 130/175] ! update after release `v2.5.0` --- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 2 +- bin/cp-into-docker-run | 2 +- bin/find-in-jars | 2 +- bin/rp | 2 +- bin/show-busy-java-threads | 2 +- bin/show-duplicate-java-classes | 2 +- bin/tcp-connection-state-counter | 2 +- bin/uq | 2 +- bin/xpl | 2 +- legacy-bin/cp-svn-url | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/bin/a2l b/bin/a2l index 6ab30820..9f8faf44 100755 --- a/bin/a2l +++ b/bin/a2l @@ -13,7 +13,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/ap b/bin/ap index 7ab6f4ef..35d0dce2 100755 --- a/bin/ap +++ b/bin/ap @@ -15,7 +15,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/c b/bin/c index 6b8d1d6a..fede96a2 100755 --- a/bin/c +++ b/bin/c @@ -29,7 +29,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 94b7a554..552b23f1 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -11,7 +11,7 @@ set -eEuo pipefail PROG="$(basename "$0")" readonly PROG -readonly PROG_VERSION='2.5.0-dev' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/find-in-jars b/bin/find-in-jars index 52b1bdca..c69cdbc8 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -34,7 +34,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/rp b/bin/rp index 89a323e4..da277761 100755 --- a/bin/rp +++ b/bin/rp @@ -15,7 +15,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 7d45ecf2..1fa90961 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -28,7 +28,7 @@ # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" readonly PROG -readonly PROG_VERSION='2.5.0-dev' +readonly PROG_VERSION='2.6.0-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 # How can I get the source directory of a Bash script from within the script itself? diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 08689ab1..70737791 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -30,7 +30,7 @@ from zipfile import ZipFile, BadZipfile ################################################################################ # utils functions ################################################################################ -PROG_VERSION = '2.5.0-dev' +PROG_VERSION = '2.6.0-dev' # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index cbebc013..64d78601 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -13,7 +13,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/uq b/bin/uq index b33e9a32..c4fce26d 100755 --- a/bin/uq +++ b/bin/uq @@ -31,7 +31,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/bin/xpl b/bin/xpl index fab8ad64..0ee0269d 100755 --- a/bin/xpl +++ b/bin/xpl @@ -28,7 +28,7 @@ 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' +readonly PROG_VERSION='2.6.0-dev' ################################################################################ # util functions diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index eb02988a..064428b0 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -27,7 +27,7 @@ # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename "$0")" -readonly PROG_VERSION='2.5.0-dev' +readonly PROG_VERSION='2.6.0-dev' usage() { cat < Date: Wed, 6 Sep 2023 22:51:30 +0800 Subject: [PATCH 131/175] build(CI): add `lint.yaml` --- .github/workflows/lint.yaml | 18 ++++++++++++++++++ test-cases/lint.sh | 10 ++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .github/workflows/lint.yaml create mode 100755 test-cases/lint.sh diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..60468c95 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,18 @@ +# Quickstart for GitHub Actions +# https://docs.github.com/en/actions/quickstart + +name: Lint +on: [ push, pull_request, workflow_dispatch ] + +jobs: + + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + name: Lint + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - run: test-cases/lint.sh diff --git a/test-cases/lint.sh b/test-cases/lint.sh new file mode 100755 index 00000000..ab2691aa --- /dev/null +++ b/test-cases/lint.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -eEuo pipefail + +# cd to the root of the project +cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/.. + +find bin lib legacy-bin -type f | + grep -Pv '/show-duplicate-java-classes$' | + grep -Pv '/\.editorconfig$' | + xargs --verbose shellcheck --shell=bash From 8d2910298fd87f857436341140895cf15b99af72 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 6 Sep 2023 23:11:19 +0800 Subject: [PATCH 132/175] build(CI): fix `shellcheck` failure --- bin/xpf | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/xpf b/bin/xpf index 4e6c9dae..8baf4d02 100755 --- a/bin/xpf +++ b/bin/xpf @@ -53,4 +53,5 @@ portableReadLink() { THIS_SCRIPT="$(portableReadLink "${BASH_SOURCE[0]}")" BASE_DIR="$(dirname "$THIS_SCRIPT")" +# shellcheck disable=SC1091 source "$BASE_DIR/xpl" "$@" From 51ef409d276e4e0b4a36901497c8bea0314a5e22 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 8 Sep 2023 01:45:27 +0800 Subject: [PATCH 133/175] =?UTF-8?q?refactor:=20use=20`${array[@]:-}`/`${ar?= =?UTF-8?q?ray[@]:+}`=20simplify=20codes=20=F0=9F=9A=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/ap | 3 ++- bin/find-in-jars | 4 +--- bin/uq | 2 +- bin/xpl | 12 +++++++----- legacy-bin/swtrunk | 3 ++- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/bin/ap b/bin/ap index 35d0dce2..1c598985 100755 --- a/bin/ap +++ b/bin/ap @@ -130,7 +130,8 @@ while [ $# -gt 0 ]; do esac done -[ ${#files[@]} -eq 0 ] && files=(.) +# if files is empty, use "." +files=("${files[@]:-.}") readonly files ################################################################################ diff --git a/bin/find-in-jars b/bin/find-in-jars index c69cdbc8..5ff3e892 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -268,9 +268,7 @@ 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") + find_iname_options=(${find_iname_options[@]:+"${find_iname_options[@]}" -o} -iname "*.$e") done readonly find_iname_options diff --git a/bin/uq b/bin/uq index c4fce26d..8f833f6b 100755 --- a/bin/uq +++ b/bin/uq @@ -194,7 +194,7 @@ while (($# > 0)); do ;; --) shift - argv=("${argv[@]}" "$@") + argv=(${argv[@]:+"${argv[@]}"} "$@") break ;; -) diff --git a/bin/xpl b/bin/xpl index 0ee0269d..38b92461 100755 --- a/bin/xpl +++ b/bin/xpl @@ -66,7 +66,7 @@ progVersion() { # parse options ################################################################################ -declare -a args=() +declare -a files=() selected=false while [ $# -gt 0 ]; do case "$1" in @@ -82,23 +82,26 @@ while [ $# -gt 0 ]; do ;; --) shift - args=(${args[@]:+"${args[@]}"} "$@") + files=(${files[@]:+"${files[@]}"} "$@") break ;; -*) usage 2 "${PROG}: unrecognized option '$1'" ;; *) - args=(${args[@]:+"${args[@]}"} "$1") + files=(${files[@]:+"${files[@]}"} "$1") shift ;; esac done +# if files is empty, use one element "." +files=("${files[@]:-.}") + # if program name is xpf, set option selected! [ "xpf" == "${PROG}" ] && selected=true -readonly args selected +readonly files selected ################################################################################ # biz logic @@ -143,7 +146,6 @@ openOneFile() { printf 'open %14s: %s\n' "$selected_msg" "$file" } -[ "${#args[@]}" == 0 ] && files=(.) || files=("${args[@]}") has_error=false for file in "${files[@]}"; do diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index cde45c6e..fc14abfd 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -27,7 +27,8 @@ greenEcho() { colorEcho 32 "$@" } -[ $# -eq 0 ] && dirs=(.) || dirs=("$@") +# if dirs is empty, use "." +dirs=("${dirs[@]:-.}") for d in "${dirs[@]}"; do [ ! -d "${d}/.svn" ] && { From 1e5c5c4c2f40fb969608c98e4cbf60f48c4fe875 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 8 Sep 2023 02:09:15 +0800 Subject: [PATCH 134/175] =?UTF-8?q?chore:=20update=20`.editorconfig`=20?= =?UTF-8?q?=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 6 +++++- bin/.editorconfig | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) delete mode 100644 bin/.editorconfig diff --git a/.editorconfig b/.editorconfig index 7c0b7d4c..4b2606b8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ indent_style = space indent_size = 2 trim_trailing_whitespace = true -[*.{xml,py,md,mkd,markdown}] +[*.{py,md,mkd,markdown}] indent_size = 4 [*.xml] @@ -17,3 +17,7 @@ indent_style = tab [*.{md,mkd,markdown}] trim_trailing_whitespace = false + +# python files without extension +[show-duplicate-java-classes] +indent_size = 4 diff --git a/bin/.editorconfig b/bin/.editorconfig deleted file mode 100644 index 5a38bfbe..00000000 --- a/bin/.editorconfig +++ /dev/null @@ -1,2 +0,0 @@ -[show-duplicate-java-classes] -indent_size = 4 From 02588c9e1f6c17a57228c11764481b82e4db8cf0 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 20 Sep 2023 01:18:44 +0800 Subject: [PATCH 135/175] =?UTF-8?q?fix(`c`):=20wrong=20exit=20code=20with?= =?UTF-8?q?=20`-q`=20option=20=F0=9F=90=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bin/c b/bin/c index fede96a2..7faf5487 100755 --- a/bin/c +++ b/bin/c @@ -141,10 +141,11 @@ copy() { catThenCopy() { local content content="$(cat)" - { + if $keep_eol; then + printf '%s\n' "$content" + else printf %s "$content" - $keep_eol && echo - } | copy + fi | copy } teeAndCopy() { From 592719ed20297800f7640e925c78f59bf8f3fdb1 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 20 Nov 2023 19:40:33 +0800 Subject: [PATCH 136/175] =?UTF-8?q?refactor/robust(`bump-scripts-version.s?= =?UTF-8?q?h`)=20=F0=9F=92=AA=20:=20fix=20Bash=20Pitfalls=20No.1/No.5;=20u?= =?UTF-8?q?se=20`printf`=20instead=20of=20`echo`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test-cases/bump-scripts-version.sh | 53 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index f639fade..159cb64f 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -5,29 +5,30 @@ set -eEuo pipefail # 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 +readonly nl=$'\n' # new line -colorEcho() { - local color=$1 +colorPrint() { + local color="$1" shift - - # if stdout is the console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + # check isatty in bash https://stackoverflow.com/questions/10022323 + # if stdout is console, turn on color output. + if [ -t 1 ]; then + printf "\033[1;${color}m%s\033[0m\n" "$*" + else + printf '%s\n' "$*" + fi } -redEcho() { - colorEcho 31 "$@" +redPrint() { + colorPrint 31 "$@" } -yellowEcho() { - colorEcho 33 "$@" +yellowPrint() { + colorPrint 33 "$@" } -blueEcho() { - colorEcho 36 "$@" +bluePrint() { + colorPrint 36 "$@" } logAndRun() { @@ -41,13 +42,13 @@ logAndRun() { echo "Run under work directory $PWD : $*" "$@" else - blueEcho "Run under work directory $PWD :$nl$*" + bluePrint "Run under work directory $PWD :$nl$*" time "$@" fi } die() { - redEcho "Error: $*" 1>&2 + redPrint "Error: $*" 1>&2 exit 1 } @@ -59,13 +60,13 @@ die() { readonly bump_version="$1" # adjust current dir to project dir -cd "$(dirname "$(readlink -f "$0")")/.." - -script_files=$( - find bin legacy-bin -type f -) +# +# Bash Pitfalls#5 +# http://mywiki.wooledge.org/BashPitfalls#cd_.24.28dirname_.22.24f.22.29 +cd -P -- "$(dirname -- "$0")"/.. -# shellcheck disable=SC2086 -logAndRun sed -ri \ - 's/^(.*PROG_VERSION\s*=\s*)'\''(.*)'\''(.*)$/\1'\'"$bump_version"\''\3/' \ - $script_files +# Bash Pitfalls#1 +# http://mywiki.wooledge.org/BashPitfalls#for_f_in_.24.28ls_.2A.mp3.29 +logAndRun find -D exec bin legacy-bin lib -type f -exec \ + sed -ri "s/^(.*\bPROG_VERSION\s*=\s*')\S*('.*)$/\1$bump_version\2/" -- \ + {} + From dc5b239b24472fdafe556eac265874955e3ff79e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 16 Nov 2023 01:10:26 +0800 Subject: [PATCH 137/175] refactor: use file descriptor number instead of `/dev/std*`; move var location --- bin/ap | 7 +++---- bin/c | 7 +++---- bin/cp-into-docker-run | 7 +++---- bin/find-in-jars | 12 ++++++------ bin/rp | 7 +++---- bin/show-busy-java-threads | 7 +++---- bin/uq | 7 +++---- bin/xpl | 7 +++---- 8 files changed, 27 insertions(+), 34 deletions(-) diff --git a/bin/ap b/bin/ap index 1c598985..355163dc 100755 --- a/bin/ap +++ b/bin/ap @@ -73,14 +73,13 @@ portableReadLink() { 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 + local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >"$out" + (($# > 0)) && redPrint "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && printErrorMsg "$*" >"$out" + (($# > 0)) && printErrorMsg "$*" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >"$out" + (($# > 0)) && redPrint "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash local -r nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >"$out" + (($# > 0)) && redPrint "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >"$out" + (($# > 0)) && redPrint "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && colorPrint 31 "$*$nl" >"$out" + (($# > 0)) && colorPrint 31 "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && redPrint "$*$nl" >"$out" + (($# > 0)) && redPrint "$*$nl" >&"$out" - cat >"$out" <&"$out" < 0)) && shift - # shellcheck disable=SC2015 - [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout + local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && printf '%s\n\n' "$*" >"$out" + (($# > 0)) && printf '%s\n\n' "$*" >&"$out" - cat <&"$out" < Date: Sun, 19 Nov 2023 22:29:59 +0800 Subject: [PATCH 138/175] =?UTF-8?q?docs:=20add=20more=20resources=20of=20`?= =?UTF-8?q?bash`=20=F0=9F=93=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++++----- docs/developer-guide.md | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5c6fb9cd..e83f1335 100644 --- a/README.md +++ b/README.md @@ -154,19 +154,20 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ > 更多资料参见 [子文档](docs/developer-guide.md)。 -- 👷 **`Bash/Shell`最佳实践与安全编程** +- 开发规范与工具 - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) - - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): ShellCheck, a static analysis tool for shell scripts + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts + - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs +- 👷 **`Bash/Shell`最佳实践与安全编程**文章 - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) - - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) | [英文原文:Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls) - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) - 🎶 **Tips** - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) -- 💎 **系统学习** - 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! +- 💎 **系统学习** — 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 @@ -174,6 +175,8 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`awesome-lists/awesome-bash`](https://github.com/awesome-lists/awesome-bash): A curated list of delightful Bash scripts and resources. - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 0a1c80da..dfb8d625 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -1,10 +1,12 @@ -### 📚 `Shell`学习与开发的资料 +# 📚 `Shell`学习与开发的资料 -- 👷 **`Bash/Shell`最佳实践与安全编程** +- 开发规范与工具 - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) - - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): ShellCheck, a static analysis tool for shell scripts + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts + - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs +- 👷 **`Bash/Shell`最佳实践与安全编程**文章 - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) - - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) | [英文原文:Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls) - [编写可靠shell脚本的八个建议 - xshell.net](https://www.xshell.net/shell/1577.html) - [Shell 编码风格 - 团子的小窝](http://kodango.com/shell-script-style) - [Bash 优良编程实践](https://www.techug.com/post/bash-practice.html) @@ -25,8 +27,7 @@ - [Bash function 还能这么玩 - 团子的小窝](http://kodango.com/bash-functions) - [Bash 获取当前函数名 - 团子的小窝](http://kodango.com/get-function-name-in-bash) - [Zsh和Bash,究竟有何不同 坑很深](https://www.xshell.net/shell/bash_zsh.html) -- 💎 **系统学习** - 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! +- 💎 **系统学习** — 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 @@ -34,7 +35,9 @@ - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`awesome-lists/awesome-bash`](https://github.com/awesome-lists/awesome-bash): A curated list of delightful Bash scripts and resources. - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. - [wzb56/13_questions_of_shell: shell十三问 - shell教程](https://github.com/wzb56/13_questions_of_shell) - [实用 Shell 文档 - 团子的小窝](http://kodango.com/useful-documents-about-shell) From 3b92d9d11e79c9b5e36360680701e9bf055cdf4a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Tue, 21 Nov 2023 14:11:27 +0800 Subject: [PATCH 139/175] =?UTF-8?q?refactor/robust=20=F0=9F=92=AA=20:=20fi?= =?UTF-8?q?x=20Bash=20Pitfalls=20No.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash Pitfalls No.5 http://mywiki.wooledge.org/BashPitfalls#cd_.24.28dirname_.22.24f.22.29 --- bin/a2l | 2 +- bin/ap | 6 +++--- bin/c | 2 +- bin/cp-into-docker-run | 10 +++++----- bin/find-in-jars | 2 +- bin/rp | 4 ++-- bin/show-busy-java-threads | 4 ++-- bin/tcp-connection-state-counter | 2 +- bin/uq | 2 +- bin/xpf | 8 ++++---- bin/xpl | 6 +++--- legacy-bin/cp-svn-url | 2 +- legacy-bin/svn-merge-stop-on-copy | 2 +- lib/console-text-color-themes.sh | 2 +- test-cases/integration-test.sh | 2 +- test-cases/lint.sh | 2 +- test-cases/parseOpts_test.sh | 2 +- test-cases/uq_test.sh | 2 +- 18 files changed, 31 insertions(+), 31 deletions(-) diff --git a/bin/a2l b/bin/a2l index 9f8faf44..d751aeda 100755 --- a/bin/a2l +++ b/bin/a2l @@ -11,7 +11,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/ap b/bin/ap index 355163dc..51c587a5 100755 --- a/bin/ap +++ b/bin/ap @@ -13,7 +13,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -50,12 +50,12 @@ portableReadLink() { uname="$(uname)" case "$uname" in Linux* | CYGWIN* | MINGW*) - readlink -f "$file" + readlink -f -- "$file" ;; Darwin*) local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then - greadlink -f "$file" + greadlink -f -- "$file" elif command -v python3 >/dev/null; then python3 "${py_args[@]}" elif command -v python >/dev/null; then diff --git a/bin/c b/bin/c index aaaf4d89..9ebd0e75 100755 --- a/bin/c +++ b/bin/c @@ -27,7 +27,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index fc4af8f8..bf1dd578 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -9,7 +9,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -43,12 +43,12 @@ portableReadLink() { uname="$(uname)" case "$uname" in Linux* | CYGWIN* | MINGW*) - readlink -f "$file" + readlink -f -- "$file" ;; Darwin*) local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then - greadlink -f "$file" + greadlink -f -- "$file" elif command -v python3 >/dev/null; then python3 "${py_args[@]}" elif command -v python >/dev/null; then @@ -205,7 +205,7 @@ if [ ! -f "$specified_run_command" ]; then run_command="$(which "$specified_run_command")" fi run_command="$(portableReadLink "$run_command")" -run_command_base_name="$(basename "$run_command")" +run_command_base_name="$(basename -- "$run_command")" readonly run_command run_command_base_name run_timestamp="$(date "+%Y%m%d_%H%M%S")" @@ -218,7 +218,7 @@ if [ -n "${docker_command_cp_path}" ]; then else readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" fi - run_command_dir_in_docker="$(dirname "$run_command_in_docker")" + run_command_dir_in_docker="$(dirname -- "$run_command_in_docker")" readonly run_command_dir_in_docker else readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" diff --git a/bin/find-in-jars b/bin/find-in-jars index cd69bee5..9805e714 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -32,7 +32,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/rp b/bin/rp index a9f7e859..960f2bda 100755 --- a/bin/rp +++ b/bin/rp @@ -13,7 +13,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -140,7 +140,7 @@ else files=("${files[@]:0:argc-1}") fi -[ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")" +[ -f "$relativeTo" ] && relativeTo="$(dirname -- "$relativeTo")" [ -e "$relativeTo" ] || die "relativeTo dir($relativeTo) does NOT exists!" readonly files relativeTo diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 777660ae..a7f2e30c 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -26,7 +26,7 @@ # var2=$(echo value1) # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' # choosing between $0 and BASH_SOURCE @@ -312,7 +312,7 @@ if [ -n "$append_file" ]; then [ -f "$append_file" ] || die "$append_file(specified by option -a, for storing run output files) exists but is not a file!" [ -w "$append_file" ] || die "file $append_file(specified by option -a, for storing run output files) exists but is not writable!" else - append_file_dir="$(dirname "$append_file")" + append_file_dir="$(dirname -- "$append_file")" if [ -e "$append_file_dir" ]; then [ -d "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not a directory!" [ -w "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not writable!" diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 64d78601..ec0440b7 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -11,7 +11,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/uq b/bin/uq index 712bbc46..51447e1f 100755 --- a/bin/uq +++ b/bin/uq @@ -29,7 +29,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/xpf b/bin/xpf index 8baf4d02..a0c52cd3 100755 --- a/bin/xpf +++ b/bin/xpf @@ -22,12 +22,12 @@ portableReadLink() { uname="$(uname)" case "$uname" in Linux* | CYGWIN* | MINGW*) - readlink -f "$file" + readlink -f -- "$file" ;; Darwin*) local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") if command -v greadlink >/dev/null; then - greadlink -f "$file" + greadlink -f -- "$file" elif command -v python3 >/dev/null; then python3 "${py_args[@]}" elif command -v python >/dev/null; then @@ -49,9 +49,9 @@ portableReadLink() { ################################################################################ # DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell: -# BASE_DIR="$(dirname "$(portableReadLink "${BASH_SOURCE[0]}")")" +# BASE_DIR="$(dirname -- "$(portableReadLink "${BASH_SOURCE[0]}")")" THIS_SCRIPT="$(portableReadLink "${BASH_SOURCE[0]}")" -BASE_DIR="$(dirname "$THIS_SCRIPT")" +BASE_DIR="$(dirname -- "$THIS_SCRIPT")" # shellcheck disable=SC1091 source "$BASE_DIR/xpl" "$@" diff --git a/bin/xpl b/bin/xpl index 4c1b7a28..7ae388ba 100755 --- a/bin/xpl +++ b/bin/xpl @@ -26,7 +26,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -129,12 +129,12 @@ openOneFile() { ;; *) if [ -d "${file}" ]; then - nautilus "$(dirname "${file}")" + nautilus "$(dirname -- "${file}")" else if $slt; then nautilus "${file}" else - nautilus "$(dirname "${file}")" + nautilus "$(dirname -- "${file}")" fi fi ;; diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 064428b0..d0534517 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -26,7 +26,7 @@ # var2=$(echo value1) # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" readonly PROG_VERSION='2.6.0-dev' usage() { diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index 8821bcc4..28c04fa1 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -11,7 +11,7 @@ # @author jiangjizhong(@jzwlqx) # @author Jerry Lee (oldratlee at gmail dot com) -PROG="$(basename "$0")" +PROG="$(basename -- "$0")" usage() { cat </dev/null; then fi # NOTE: DO NOT declare var _ctct_PROG as readonly in ONE line! -_ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")" +_ctct_PROG="$(basename -- "$($_ctct_READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" [ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true readonly _ctct_ec=$'\033' # escape char diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index ae909c84..e7e5e8e7 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -6,7 +6,7 @@ if command -v greadlink &>/dev/null; then READLINK_CMD=greadlink fi -cd "$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" +cd "$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" ################################################################################ # constants diff --git a/test-cases/lint.sh b/test-cases/lint.sh index ab2691aa..9cef7d3a 100755 --- a/test-cases/lint.sh +++ b/test-cases/lint.sh @@ -2,7 +2,7 @@ set -eEuo pipefail # cd to the root of the project -cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"/.. +cd "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"/.. find bin lib legacy-bin -type f | grep -Pv '/show-duplicate-java-classes$' | diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh index 93e5704d..03a2acac 100755 --- a/test-cases/parseOpts_test.sh +++ b/test-cases/parseOpts_test.sh @@ -1,6 +1,6 @@ #!/bin/bash -BASE="$(dirname "${BASH_SOURCE[0]}")" +BASE="$(dirname -- "${BASH_SOURCE[0]}")" source "$BASE/../lib/parseOpts.sh" diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 242f79b8..93aa69cf 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -6,7 +6,7 @@ if command -v greadlink &>/dev/null; then READLINK_CMD=greadlink fi -BASE="$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")" +BASE="$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" cd "$BASE" ################################################# From 90eb4418dc1f85a3e1302367b68c94ab130d01c6 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Wed, 22 Nov 2023 22:16:10 +0800 Subject: [PATCH 140/175] =?UTF-8?q?refactor/robust(`console-text-color-the?= =?UTF-8?q?mes.sh`):=20script=20source=20check,=20etc=20=F0=9F=9B=A0?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use `BASH_SOURCE` array to detect whether this script is sourced or not 🕵️ - use `printf` 💪 instead of `echo` - use `if` instead of `&&` - remove global var declare --- lib/console-text-color-themes.sh | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index c7b3d287..3944458a 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -21,26 +21,14 @@ # local var2 # var2=$(echo value1) -_ctct_READLINK_CMD=readlink -if command -v greadlink >/dev/null; then - _ctct_READLINK_CMD=greadlink -fi - -# NOTE: DO NOT declare var _ctct_PROG as readonly in ONE line! -_ctct_PROG="$(basename -- "$($_ctct_READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" -[ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true - -readonly _ctct_ec=$'\033' # escape char -readonly _ctct_eend=$'\033[0m' # escape end - colorEcho() { local combination="$1" shift 1 if [ -t 1 ]; then - echo "${_ctct_ec}[${combination}m$*$_ctct_eend" + printf "\e[${combination}m%s\e[0m\n" "$*" else - echo "$*" + print '%s\n' "$*" fi } @@ -49,15 +37,16 @@ colorEchoWithoutNewLine() { shift 1 if [ -t 1 ]; then - echo -n "${_ctct_ec}[${combination}m$*$_ctct_eend" + printf "\e[${combination}m%s\e[0m" "$*" else - echo -n "$*" + printf %s "$*" fi } -# if not directly run this script(use as lib), just export 2 helper functions, -# and do NOT print anything. -[ "$_ctct_is_direct_run" == true ] && { +# if not directly run this script(use as lib), just export 2 helper functions, and do NOT print anything. +# +# if directly run this script, the length of array BASH_SOURCE is 1. +if ((${#BASH_SOURCE[@]} == 1)); then for style in 0 1 2 3 4 5 6 7; do for fg in 30 31 32 33 34 35 36 37; do for bg in 40 41 42 43 44 45 46 47; do @@ -97,4 +86,4 @@ colorEchoWithoutNewLine() { echo 'Output of above code:' echo -n ' ' colorEcho '1;36;41' 'Sample Text' -} +fi From 1dae473c0daa1b5a2c479cf9cae405e4fa170e72 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 15 Dec 2023 13:31:33 +0800 Subject: [PATCH 141/175] =?UTF-8?q?refactor(`console-text-color-themes.sh`?= =?UTF-8?q?):=20use=20`Guard=20Clauses`=20instead=20of=20nested=20`if`=20?= =?UTF-8?q?=F0=9F=92=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/console-text-color-themes.sh | 73 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 3944458a..94614c14 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -43,47 +43,48 @@ colorEchoWithoutNewLine() { fi } -# if not directly run this script(use as lib), just export 2 helper functions, and do NOT print anything. +# if source this script(use as lib), just export 2 helper functions, and do NOT print anything. # -# if directly run this script, the length of array BASH_SOURCE is 1. -if ((${#BASH_SOURCE[@]} == 1)); then - for style in 0 1 2 3 4 5 6 7; do - for fg in 30 31 32 33 34 35 36 37; do - for bg in 40 41 42 43 44 45 46 47; do - combination="${style};${fg};${bg}" - colorEchoWithoutNewLine "$combination" "$combination" - printf ' ' - done - echo +# if directly run this script, the length of array BASH_SOURCE is 1; +# if source this script, the length of array BASH_SOURCE is grater than 1. +((${#BASH_SOURCE[@]} == 1)) || return 0 + +for style in 0 1 2 3 4 5 6 7; do + for fg in 30 31 32 33 34 35 36 37; do + for bg in 40 41 42 43 44 45 46 47; do + combination="${style};${fg};${bg}" + colorEchoWithoutNewLine "$combination" "$combination" + printf ' ' done echo done + echo +done - echo 'Code sample to print color text:' +echo 'Code sample to print color text:' - printf %s ' echo -e "\033[' - colorEchoWithoutNewLine '3;35;40' '1;36;41' - printf %s m - colorEchoWithoutNewLine '0;32;40' 'Sample Text' - printf '%s\n' '\033[0m"' +printf %s ' echo -e "\033[' +colorEchoWithoutNewLine '3;35;40' '1;36;41' +printf %s m +colorEchoWithoutNewLine '0;32;40' 'Sample Text' +printf '%s\n' '\033[0m"' - printf %s " echo \$'\033[" - colorEchoWithoutNewLine '3;35;40' '1;36;41' - printf %s "m'\"" - colorEchoWithoutNewLine '0;32;40' 'Sample Text' - printf '%s\n' "\"$'\033[0m'" - printf '%s\n' " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" +printf %s " echo \$'\033[" +colorEchoWithoutNewLine '3;35;40' '1;36;41' +printf %s "m'\"" +colorEchoWithoutNewLine '0;32;40' 'Sample Text' +printf '%s\n' "\"$'\033[0m'" +printf '%s\n' " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" - printf '%s\n' 'Output of above code:' - printf %s ' ' - colorEcho '1;36;41' 'Sample Text' - echo - echo 'If you are going crazy to write text in escapes string like me,' - echo 'you can use colorEcho and colorEchoWithoutNewLine function in this script.' - echo - echo 'Code sample to print color text:' - echo ' colorEcho "1;36;41" "Sample Text"' - echo 'Output of above code:' - echo -n ' ' - colorEcho '1;36;41' 'Sample Text' -fi +printf '%s\n' 'Output of above code:' +printf %s ' ' +colorEcho '1;36;41' 'Sample Text' +echo +echo 'If you are going crazy to write text in escapes string like me,' +echo 'you can use colorEcho and colorEchoWithoutNewLine function in this script.' +echo +echo 'Code sample to print color text:' +echo ' colorEcho "1;36;41" "Sample Text"' +echo 'Output of above code:' +echo -n ' ' +colorEcho '1;36;41' 'Sample Text' From 9f24158b7d7a7f77d716c60e14e87cbbbdae68e3 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 7 Jan 2024 18:20:45 +0800 Subject: [PATCH 142/175] build: upgrade `shunit2` lib --- test-cases/shunit2-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib index 47be8b23..3334e530 160000 --- a/test-cases/shunit2-lib +++ b/test-cases/shunit2-lib @@ -1 +1 @@ -Subproject commit 47be8b23a46a7897e849f1841f0fb704d34d0f6e +Subproject commit 3334e53047ad143669870a9c223b70a81156533a From 7a779ef45e99894c9b5625ac95cf29d24f600a4b Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 7 Jan 2024 18:08:59 +0800 Subject: [PATCH 143/175] =?UTF-8?q?refactor:=20improve=20readability=20?= =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - remove var interpolation in `printf format` if possible - rename vars, e.g. `message` -> `content`, `normal` -> `color_reset` - use `\e` instead of `\033` - improve comments for `-t` check --- bin/a2l | 15 ++++++++------- bin/ap | 7 ++++--- bin/c | 7 ++++--- bin/coat | 11 ++++++----- bin/cp-into-docker-run | 6 ++++-- bin/echo-args | 10 +++++----- bin/find-in-jars | 30 +++++++++++++++--------------- bin/rp | 7 ++++--- bin/show-busy-java-threads | 10 ++++++---- bin/taoc | 13 +++++++------ bin/uq | 9 +++++---- lib/console-text-color-themes.sh | 16 +++++++++------- lib/parseOpts.sh | 4 ++-- test-cases/bump-scripts-version.sh | 7 ++++--- test-cases/integration-test.sh | 15 +++++++++------ test-cases/my_unit_test_lib.sh | 14 ++++++++------ 16 files changed, 100 insertions(+), 81 deletions(-) diff --git a/bin/a2l b/bin/a2l index d751aeda..c3f55c14 100755 --- a/bin/a2l +++ b/bin/a2l @@ -22,10 +22,11 @@ readonly PROG_VERSION='2.6.0-dev' colorPrint() { local color="$1" shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;${color}m%s\033[0m\n" "$*" + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" else printf '%s\n' "$*" fi @@ -93,14 +94,14 @@ readonly args readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 rotateColorPrint() { - local message="$*" + local content="$*" # skip color for white space - if [[ "$message" =~ ^[[:space:]]*$ ]]; then - printf '%s\n' "$message" + if [[ "$content" =~ ^[[:space:]]*$ ]]; then + printf '%s\n' "$content" else local color="${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]}" - colorPrint "$color" "$*" + colorPrint "$color" "$content" fi } diff --git a/bin/ap b/bin/ap index 51c587a5..3b270014 100755 --- a/bin/ap +++ b/bin/ap @@ -24,10 +24,11 @@ readonly PROG_VERSION='2.6.0-dev' colorPrint() { local color="$1" shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;${color}m%s\033[0m\n" "$*" + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" else printf '%s\n' "$*" fi diff --git a/bin/c b/bin/c index 9ebd0e75..f7348ada 100755 --- a/bin/c +++ b/bin/c @@ -36,10 +36,11 @@ readonly PROG_VERSION='2.6.0-dev' ################################################################################ printErrorMsg() { - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, print with red color. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;31m%s\033[0m\n\n" "Error: $*" + printf '\e[1;31m%s\e[0m\n\n' "Error: $*" else printf '%s\n\n' "Error: $*" fi diff --git a/bin/coat b/bin/coat index 496bf7ef..4ad9c400 100755 --- a/bin/coat +++ b/bin/coat @@ -11,21 +11,22 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# if not in console, use cat directly +# if stdout is a terminal, use `cat` directly. +# '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 [ ! -t 1 ] && exec cat "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 rotateColorPrint() { - local message="$*" + local content="$*" # skip color for white space - if [[ "$message" =~ ^[[:space:]]*$ ]]; then - printf '%s\n' "$message" + if [[ "$content" =~ ^[[:space:]]*$ ]]; then + printf '%s\n' "$content" else local color="${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]}" - printf "\033[1;${color}m%s\033[0m\n" "$message" + printf '\e[1;%sm%s\e[0m\n' "$color" "$content" fi } diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index bf1dd578..00b0d941 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -18,9 +18,11 @@ readonly PROG_VERSION='2.6.0-dev' ################################################################################ redPrint() { - # -t check: is a terminal device? + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;31m%s\033[0m\n" "$*" + printf '\e[1;31m%s\e[0m\n' "$*" else printf '%s\n' "$*" fi diff --git a/bin/echo-args b/bin/echo-args index a2dd472a..b7f2ada9 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -20,16 +20,16 @@ digitCount() { digit_count=$(digitCount $#) readonly arg_count=$# digit_count -readonly red='\033[1;31m' -readonly blue='\033[1;36m' -readonly normal='\033[0m' +readonly red='\e[1;31m' blue='\e[1;36m' color_reset='\e[0m' printArg() { local idx="$1" value="$2" - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "%${digit_count}s/%s: ${red}[${blue}%s${red}]${normal}\n" "$idx" "$arg_count" "$value" + printf "%${digit_count}s/%s: ${red}[${blue}%s${red}]${color_reset}\n" "$idx" "$arg_count" "$value" else printf "%${digit_count}s/%s: [%s]\n" "$idx" "$arg_count" "$value" fi diff --git a/bin/find-in-jars b/bin/find-in-jars index 9805e714..52d892db 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -40,7 +40,7 @@ readonly PROG_VERSION='2.6.0-dev' # util functions ################################################################################ -readonly red='\033[1;31m' normal='\033[0m' +readonly color_reset='\e[0m' # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 @@ -50,12 +50,14 @@ readonly red='\033[1;31m' normal='\033[0m' # echo -e "\033[1K" # Or everything on the line, regardless of cursor position: # echo -e "\033[2K" -readonly clear_line='\033[2K\r' +readonly clear_line='\e[2K\r' redPrint() { - # -t check: is a terminal device? + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "${red}%s${normal}\n" "$*" + printf "\e[1;31m%s${color_reset}\n" "$*" else printf '%s\n' "$*" fi @@ -72,9 +74,9 @@ printResponsiveMessage() { return fi - local message="$*" + local content="$*" # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - printf "${clear_line}%s" "${message:0:columns}" >&2 + printf %b%s "${clear_line}" "${content:0:columns}" >&2 } clearResponsiveMessage() { @@ -82,7 +84,7 @@ clearResponsiveMessage() { return fi - printf "%b" "$clear_line" >&2 + printf %b "$clear_line" >&2 } die() { @@ -322,14 +324,14 @@ listZipEntries() { clearResponsiveMessage redPrint "fail to list zip entries of $zip_file, ignored: $msg" >&2 fi - return 0 + return } fi "${command_to_list_zip_entries[@]}" "$zip_file" || { clearResponsiveMessage redPrint "fail to list zip entries of $zip_file, ignored!" >&2 - return 0 + return } } @@ -347,16 +349,14 @@ searchJarFiles() { total_jar_count="$(printf '%s\n' "$jar_files" | wc -l)" # remove white space, because the `wc -l` output on mac contains white space! - total_jar_count="${total_jar_count//[[:space:]]/}" + total_jar_count=${total_jar_count//[[:space:]]} echo "$total_jar_count" printf '%s\n' "$jar_files" } -readonly jar_color='\033[1;35m' sep_color='\033[1;32m' - __outputResultOfJarFile() { - local jar_file="$1" file + local jar_file="$1" file jar_color='\e[1;35m' sep_color='\e[1;32m' # shellcheck disable=SC2206 local grep_opt_args=("$regex_mode" ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern") @@ -380,7 +380,7 @@ __outputResultOfJarFile() { clearResponsiveMessage if [ -t 1 ]; then - printf "${jar_color}%s${normal}\n" "${jar_file}" + printf "${jar_color}%s${color_reset}\n" "${jar_file}" else printf '%s\n' "${jar_file}" fi @@ -392,7 +392,7 @@ __outputResultOfJarFile() { } | while read -r file; do clearResponsiveMessage if [ -t 1 ]; then - printf "${jar_color}%s${sep_color}%s${normal}%s\n" "$jar_file" "$separator" "$file" + printf "${jar_color}%s${sep_color}%s${color_reset}%s\n" "$jar_file" "$separator" "$file" else printf '%s\n' "${jar_file}${separator}${file}" fi diff --git a/bin/rp b/bin/rp index 960f2bda..5e5d578e 100755 --- a/bin/rp +++ b/bin/rp @@ -24,10 +24,11 @@ readonly PROG_VERSION='2.6.0-dev' colorPrint() { local color="$1" shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;${color}m%s\033[0m\n" "$*" + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" else printf '%s\n' "$*" fi diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index a7f2e30c..41f1ee56 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -55,9 +55,11 @@ colorPrint() { local color=$1 shift - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;${color}m%s\033[0m\n" "$*" + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" else printf '%s\n' "$*" fi @@ -301,7 +303,7 @@ isNaturalNumber "$update_count" || die "update count($update_count) is not a nat readonly update_count if [ -n "$pid_list" ]; then - pid_list="${pid_list//[[:space:]]/}" # delete white space + pid_list=${pid_list//[[:space:]]} # delete white space isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" fi readonly pid_list @@ -532,7 +534,7 @@ findBusyJavaThreadsByTop() { printStackOfThreads() { local idx=0 pid threadId pcpu user threadId0x while read -r pid threadId pcpu user; do - threadId0x="0x$(printf %x "${threadId}")" + printf -v threadId0x '%#x' "$threadId" ((idx++)) local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" diff --git a/bin/taoc b/bin/taoc index de5f600f..17419a41 100755 --- a/bin/taoc +++ b/bin/taoc @@ -1,6 +1,6 @@ #!/bin/bash # @Function -# tac lines colorfully. taoc means *CO*lorful c*AT* in reverse(last line first). +# tac lines colorfully. taoc means coat(*CO*lorful c*AT*) in reverse(last line first). # # @Usage # $ echo -e 'Hello\nWorld' | taoc @@ -11,21 +11,22 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# if not in console, use tac directly +# if stdout is a terminal, use `tac` directly. +# '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 [ ! -t 1 ] && exec tac "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 rotateColorPrint() { - local message="$*" + local content="$*" # skip color for white space - if [[ "$message" =~ ^[[:space:]]*$ ]]; then - printf '%s\n' "$message" + if [[ "$content" =~ ^[[:space:]]*$ ]]; then + printf '%s\n' "$content" else local color="${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]}" - printf "\033[1;${color}m%s\033[0m\n" "$message" + printf '\e[1;%sm%s\e[0m\n' "$color" "$content" fi } diff --git a/bin/uq b/bin/uq index 51447e1f..2d958fdf 100755 --- a/bin/uq +++ b/bin/uq @@ -41,18 +41,19 @@ readonly PROG_VERSION='2.6.0-dev' readonly nl=$'\n' # new line redPrint() { - # -t check: is a terminal device? + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;31m%s\033[0m\n" "$*" + printf '\e[1;31m%s\e[0m\n' "$*" else printf '%s\n' "$*" fi } yellowPrint() { - # -t check: is a terminal device? if [ -t 1 ]; then - printf "\033[1;33m%s\033[0m\n" "$*" + printf '\e[1;33m%s\e[0m\n' "$*" else printf '%s\n' "$*" fi diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 94614c14..e416388c 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -24,9 +24,11 @@ colorEcho() { local combination="$1" shift 1 - + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\e[${combination}m%s\e[0m\n" "$*" + printf '\e[%sm%s\e[0m\n' "$combination" "$*" else print '%s\n' "$*" fi @@ -37,7 +39,7 @@ colorEchoWithoutNewLine() { shift 1 if [ -t 1 ]; then - printf "\e[${combination}m%s\e[0m" "$*" + printf '\e[%sm%s\e[0m' "$combination" "$*" else printf %s "$*" fi @@ -63,17 +65,17 @@ done echo 'Code sample to print color text:' -printf %s ' echo -e "\033[' +printf %s ' echo -e "\e[' colorEchoWithoutNewLine '3;35;40' '1;36;41' printf %s m colorEchoWithoutNewLine '0;32;40' 'Sample Text' -printf '%s\n' '\033[0m"' +printf '%s\n' '\e[0m"' -printf %s " echo \$'\033[" +printf %s " echo \$'\e[" colorEchoWithoutNewLine '3;35;40' '1;36;41' printf %s "m'\"" colorEchoWithoutNewLine '0;32;40' 'Sample Text' -printf '%s\n' "\"$'\033[0m'" +printf '%s\n' "\"$'\e[0m'" printf '%s\n' " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" printf '%s\n' 'Output of above code:' diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 341d7e4e..31c506bc 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -23,8 +23,8 @@ ##################################################################### # NOTE: $'foo' is the escape sequence syntax of bash -readonly _opts_ec=$'\033' # escape char -readonly _opts_eend=$'\033[0m' # escape end +readonly _opts_ec=$'\e' # escape char +readonly _opts_eend=$'\e[0m' # escape end # shellcheck disable=SC2209 diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 159cb64f..7cd2f70d 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -10,10 +10,11 @@ readonly nl=$'\n' # new line colorPrint() { local color="$1" shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\033[1;${color}m%s\033[0m\n" "$*" + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" else printf '%s\n' "$*" fi diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index e7e5e8e7..0242edf7 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -13,9 +13,7 @@ cd "$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" ################################################################################ # 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 +readonly nl=$'\n' # new line ################################################################################ # common util functions @@ -24,9 +22,14 @@ readonly nl=$'\n' # new line colorEcho() { local color=$1 shift - - # if stdout is the console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*" + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [[ -t 1 || "${GITHUB_ACTIONS:-}" = true ]]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi } redEcho() { diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index ab349d06..ebd779f9 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -5,15 +5,17 @@ # commons functions ################################################# -# NOTE: $'foo' is the escape sequence syntax of bash -readonly __ut_ec=$'\033' # escape char -readonly __ut_eend=$'\033[0m' # escape end - __ut_colorEcho() { local color=$1 shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${__ut_ec}[1;${color}m$*$__ut_eend" || echo "$*" + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [[ -t 1 || "${GITHUB_ACTIONS:-}" = true ]]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi } redEcho() { From a38e59fe7d64258d1db0b6b2cad2b6186f262118 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 6 Jan 2024 14:43:01 +0800 Subject: [PATCH 144/175] =?UTF-8?q?refactor:=20simplify=20var=20usage,=20i?= =?UTF-8?q?mprove=20readability=20=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - use unnecessary `{}` when use var - remove quote of values in simple assignment more info about assignment see https://www.gnu.org/software/bash/manual/html_node/Shell-Parameters.html All values undergo tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal. Word splitting and filename expansion are not performed. --- bin/a2l | 14 ++--- bin/ap | 18 +++---- bin/c | 16 +++--- bin/coat | 4 +- bin/cp-into-docker-run | 54 +++++++++---------- bin/echo-args | 4 +- bin/find-in-jars | 52 +++++++++--------- bin/rp | 22 ++++---- bin/show-busy-java-threads | 86 +++++++++++++++--------------- bin/taoc | 4 +- bin/tcp-connection-state-counter | 2 +- bin/uq | 14 ++--- bin/xpf | 10 ++-- bin/xpl | 26 ++++----- lib/console-text-color-themes.sh | 4 +- test-cases/bump-scripts-version.sh | 4 +- test-cases/uq_test.sh | 2 +- 17 files changed, 168 insertions(+), 168 deletions(-) diff --git a/bin/a2l b/bin/a2l index c3f55c14..340879aa 100755 --- a/bin/a2l +++ b/bin/a2l @@ -11,7 +11,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -20,7 +20,7 @@ readonly PROG_VERSION='2.6.0-dev' ################################################################################ colorPrint() { - local color="$1" + local color=$1 shift # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? @@ -34,12 +34,12 @@ colorPrint() { usage() { cat < 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -81,12 +81,12 @@ usage() { (($# > 0)) && redPrint "$*$nl" >&"$out" cat >&"$out" < 0)) && shift local -r out=$(((exit_code != 0) + 1)) (($# > 0)) && printErrorMsg "$*" >&"$out" cat >&"$out" < 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -75,13 +75,13 @@ usage() { (($# > 0)) && redPrint "$*$nl" >&"$out" cat >&"$out" < 0)); do case "$1" in -c | --container) - container_name="$2" + container_name=$2 shift 2 ;; -u | --docker-user) - docker_user="$2" + docker_user=$2 shift 2 ;; -w | --workdir) - docker_workdir="$2" + docker_workdir=$2 shift 2 ;; -t | --tmpdir) - docker_tmpdir="$2" + docker_tmpdir=$2 shift 2 ;; -p | --cp-path) - docker_command_cp_path="$2" + docker_command_cp_path=$2 shift 2 ;; -v | --verbose) @@ -161,7 +161,7 @@ while (($# > 0)); do break ;; -*) - usage 2 "${PROG}: unrecognized option '$1'" + usage 2 "$PROG: unrecognized option '$1'" ;; *) # if not option, treat all follow args as command @@ -176,10 +176,10 @@ readonly container_name docker_user docker_workdir docker_tmpdir docker_command_ [ -n "$container_name" ] || usage 1 "No destination docker container name, specified by option -c/--container!" -if [ -n "${docker_workdir}" ]; then +if [ -n "$docker_workdir" ]; then isAbsolutePath "$docker_workdir" || die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" -elif [ -n "${docker_command_cp_path}" ]; then +elif [ -n "$docker_command_cp_path" ]; then isAbsolutePath "$docker_command_cp_path" || die "when no docker workdir(-w/--workdir) is specified, the command path in docker to copy(-p/--cp-path) must be absolute path: $docker_command_cp_path" fi @@ -198,42 +198,42 @@ command -v docker &>/dev/null || die 'docker command not found!' # prepare vars for docker operation ######################################## -readonly specified_run_command="${args[0]}" -run_command="$specified_run_command" +readonly specified_run_command=${args[0]} +run_command=$specified_run_command if [ ! -f "$specified_run_command" ]; then which "$specified_run_command" &>/dev/null || die "specified command not exists and not found in PATH: $specified_run_command" - run_command="$(which "$specified_run_command")" + run_command=$(which "$specified_run_command") fi -run_command="$(portableReadLink "$run_command")" -run_command_base_name="$(basename -- "$run_command")" +run_command=$(portableReadLink "$run_command") +run_command_base_name=$(basename -- "$run_command") readonly run_command run_command_base_name -run_timestamp="$(date "+%Y%m%d_%H%M%S")" +run_timestamp=$(date "+%Y%m%d_%H%M%S") readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" -if [ -n "${docker_command_cp_path}" ]; then +if [ -n "$docker_command_cp_path" ]; then if isAbsolutePath "$docker_command_cp_path"; then - readonly run_command_in_docker="$docker_command_cp_path" + readonly run_command_in_docker=$docker_command_cp_path else readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" fi - run_command_dir_in_docker="$(dirname -- "$run_command_in_docker")" + run_command_dir_in_docker=$(dirname -- "$run_command_in_docker") readonly run_command_dir_in_docker else - readonly work_tmp_dir_in_docker="$docker_tmpdir/$uuid" + readonly work_tmp_dir_in_docker=$docker_tmpdir/$uuid readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" - readonly run_command_dir_in_docker="$work_tmp_dir_in_docker" + readonly run_command_dir_in_docker=$work_tmp_dir_in_docker fi cleanupWhenExit() { [ -n "${work_tmp_dir_in_docker:-}" ] || return 0 # remove tmp dir in docker by root user - docker exec "${container_name}" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null + docker exec "$container_name" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null } trap cleanupWhenExit EXIT diff --git a/bin/echo-args b/bin/echo-args index b7f2ada9..61dacf2d 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -9,7 +9,7 @@ set -eEuo pipefail digitCount() { # argument 1(num) is always a non-negative integer in this script usage, # so NO argument validation logic. - local num="$1" count=0 + local num=$1 count=0 while ((num != 0)); do ((++count)) ((num = num / 10)) @@ -23,7 +23,7 @@ readonly arg_count=$# digit_count readonly red='\e[1;31m' blue='\e[1;36m' color_reset='\e[0m' printArg() { - local idx="$1" value="$2" + local idx=$1 value=$2 # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? diff --git a/bin/find-in-jars b/bin/find-in-jars index 52d892db..5d7f7db1 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -32,7 +32,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -57,7 +57,7 @@ redPrint() { # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\e[1;31m%s${color_reset}\n" "$*" + printf "\e[1;31m%s$color_reset\n" "$*" else printf '%s\n' "$*" fi @@ -74,9 +74,9 @@ printResponsiveMessage() { return fi - local content="$*" + local content=$* # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - printf %b%s "${clear_line}" "${content:0:columns}" >&2 + printf %b%s "$clear_line" "${content:0:columns}" >&2 } clearResponsiveMessage() { @@ -94,7 +94,7 @@ die() { } usage() { - local -r exit_code="${1:-0}" + local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -103,20 +103,20 @@ usage() { (($# > 0)) && redPrint "$*$nl" >&"$out" cat >&"$out" < ' + $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. @@ -203,7 +203,7 @@ while (($# > 0)); do ;; # support the legacy typo option name --seperator for compatibility -s | --separator | --seperator) - separator="$2" + separator=$2 shift 2 ;; -L | --files-not-contained-found) @@ -248,9 +248,9 @@ 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]}" +((${#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 @@ -311,12 +311,12 @@ __prepareCommandToListZipEntries() { __prepareCommandToListZipEntries listZipEntries() { - local zip_file="$1" msg + 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)" || { + 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.' @@ -344,10 +344,10 @@ searchJarFiles() { local jar_files total_jar_count - jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" + jar_files=$(find "${dirs[@]}" "${find_iname_options[@]}" -type f) [ -n "$jar_files" ] || die "No ${extensions[*]} file found!" - total_jar_count="$(printf '%s\n' "$jar_files" | wc -l)" + total_jar_count=$(printf '%s\n' "$jar_files" | wc -l) # remove white space, because the `wc -l` output on mac contains white space! total_jar_count=${total_jar_count//[[:space:]]} @@ -356,7 +356,7 @@ searchJarFiles() { } __outputResultOfJarFile() { - local jar_file="$1" file jar_color='\e[1;35m' sep_color='\e[1;32m' + local jar_file=$1 file jar_color='\e[1;35m' sep_color='\e[1;32m' # shellcheck disable=SC2206 local grep_opt_args=("$regex_mode" ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern") @@ -380,9 +380,9 @@ __outputResultOfJarFile() { clearResponsiveMessage if [ -t 1 ]; then - printf "${jar_color}%s${color_reset}\n" "${jar_file}" + printf "$jar_color%s$color_reset\n" "$jar_file" else - printf '%s\n' "${jar_file}" + printf '%s\n' "$jar_file" fi else { @@ -392,9 +392,9 @@ __outputResultOfJarFile() { } | while read -r file; do clearResponsiveMessage if [ -t 1 ]; then - printf "${jar_color}%s${sep_color}%s${color_reset}%s\n" "$jar_file" "$separator" "$file" + printf "$jar_color%s$sep_color%s$color_reset%s\n" "$jar_file" "$separator" "$file" else - printf '%s\n' "${jar_file}${separator}${file}" + printf '%s\n' "$jar_file$separator$file" fi done fi @@ -409,7 +409,7 @@ findInJarFiles() { while read -r jar_file; do printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" - listZipEntries "${jar_file}" | __outputResultOfJarFile "${jar_file}" + listZipEntries "$jar_file" | __outputResultOfJarFile "$jar_file" done clearResponsiveMessage diff --git a/bin/rp b/bin/rp index 5e5d578e..852ceae0 100755 --- a/bin/rp +++ b/bin/rp @@ -13,7 +13,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -22,7 +22,7 @@ readonly PROG_VERSION='2.6.0-dev' ################################################################################ colorPrint() { - local color="$1" + local color=$1 shift # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? @@ -44,9 +44,9 @@ die() { } portableRelPath() { - local file="$1" relTo="$2" uname + local file=$1 relTo=$2 uname - uname="$(uname)" + uname=$(uname) case "$uname" in Linux* | CYGWIN* | MINGW*) realpath "$f" --relative-to="$relTo" @@ -70,7 +70,7 @@ portableRelPath() { } usage() { - local -r exit_code="${1:-0}" + local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -119,7 +119,7 @@ while [ $# -gt 0 ]; do break ;; -*) - usage 2 "${PROG}: unrecognized option '$1'" + usage 2 "$PROG: unrecognized option '$1'" ;; *) # if not option, treat all follow files as args @@ -129,19 +129,19 @@ while [ $# -gt 0 ]; do esac done -[ "${#files[@]}" -eq 0 ] && die "NO argument!" +[ ${#files[@]} -eq 0 ] && die "NO argument!" -if [ "${#files[@]}" -eq 1 ]; then +if [ ${#files[@]} -eq 1 ]; then relativeTo=. else - argc="${#files[@]}" + argc=${#files[@]} # Get last argument - relativeTo="${files[argc - 1]}" + relativeTo=${files[argc - 1]} files=("${files[@]:0:argc-1}") fi -[ -f "$relativeTo" ] && relativeTo="$(dirname -- "$relativeTo")" +[ -f "$relativeTo" ] && relativeTo=$(dirname -- "$relativeTo") [ -e "$relativeTo" ] || die "relativeTo dir($relativeTo) does NOT exists!" readonly files relativeTo diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 41f1ee56..f3e06d91 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -26,7 +26,7 @@ # var2=$(echo value1) # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' # choosing between $0 and BASH_SOURCE @@ -41,7 +41,7 @@ readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! # # NOTE: DO NOT declare var USER as readonly in ONE line! -USER="$(whoami)" +USER=$(whoami) readonly USER ################################################################################ @@ -67,7 +67,7 @@ colorPrint() { __appendToFile() { [[ -n "$append_file" && -w "$append_file" ]] && printf '%s\n' "$*" >>"$append_file" - [[ -n "$store_dir" && -w "$store_dir" ]] && printf '%s\n' "$*" >>"${store_file_prefix}$PROG" + [[ -n "$store_dir" && -w "$store_dir" ]] && printf '%s\n' "$*" >>"$store_file_prefix$PROG" } colorOutput() { @@ -149,7 +149,7 @@ printCallingCommandLine() { } usage() { - local -r exit_code="${1:-0}" + local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -240,23 +240,23 @@ cpu_sample_interval=0.5 while true; do case "$1" in -c | --count) - count="$2" + count=$2 shift 2 ;; -p | --pid) - pid_list="$2" + pid_list=$2 shift 2 ;; -a | --append-file) - append_file="$2" + append_file=$2 shift 2 ;; -s | --jstack-path) - jstack_path="$2" + jstack_path=$2 shift 2 ;; -S | --store-dir) - store_dir="$2" + store_dir=$2 shift 2 ;; # support the legacy option name -P,--use-ps for compatibility @@ -266,7 +266,7 @@ while true; do ;; # support the legacy option name -d,--top-delay for compatibility -i | --cpu-sample-interval | -d | --top-delay) - cpu_sample_interval="$2" + cpu_sample_interval=$2 shift 2 ;; -F | --force) @@ -314,7 +314,7 @@ if [ -n "$append_file" ]; then [ -f "$append_file" ] || die "$append_file(specified by option -a, for storing run output files) exists but is not a file!" [ -w "$append_file" ] || die "file $append_file(specified by option -a, for storing run output files) exists but is not writable!" else - append_file_dir="$(dirname -- "$append_file")" + append_file_dir=$(dirname -- "$append_file") if [ -e "$append_file_dir" ]; then [ -d "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not a directory!" [ -w "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not writable!" @@ -362,7 +362,7 @@ elif [ -n "$JAVA_HOME" ]; then fi elif command -v jstack &>/dev/null; then # 3. search jstack under PATH - jstack_path="$(command -v jstack)" + jstack_path=$(command -v jstack) [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${nl}Use -s option set jstack path manually." else die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." @@ -374,11 +374,11 @@ readonly jstack_path ################################################################################ # NOTE: DO NOT declare var run_timestamp as readonly in ONE line! -run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")" +run_timestamp=$(date "+%Y-%m-%d_%H:%M:%S.%N") readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" -readonly tmp_store_dir="/tmp/${uuid}" +readonly tmp_store_dir="/tmp/$uuid" if [ -n "$store_dir" ]; then readonly store_file_prefix="$store_dir/${run_timestamp}_" else @@ -398,14 +398,14 @@ headInfo() { echo } -if [ -n "${pid_list}" ]; then +if [ -n "$pid_list" ]; then readonly ps_process_select_options="-p $pid_list" else readonly ps_process_select_options="-C java -C jsvc" fi __die_when_no_java_process_found() { - if [ -n "${pid_list}" ]; then + if [ -n "$pid_list" ]; then die "process($pid_list) is not running, or not java process!" else die 'No java process found!' @@ -436,15 +436,15 @@ findBusyJavaThreadsByPs() { local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) # DO NOT combine var ps_out declaration and assignment in ONE line! local ps_out - ps_out="$("${ps_cmd_line[@]}" | sort -k3,3nr)" + ps_out=$("${ps_cmd_line[@]}" | sort -k3,3nr) [ -n "$ps_out" ] || __die_when_no_java_process_found if [ -n "$store_dir" ]; then - printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"${store_file_prefix}$((update_round_num + 1))_ps" + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"$store_file_prefix$((update_round_num + 1))_ps" fi if ((count > 0)); then - printf '%s\n' "$ps_out" | head -n "${count}" + printf '%s\n' "$ps_out" | head -n "$count" else printf '%s\n' "$ps_out" fi @@ -455,19 +455,19 @@ __top_threadId_cpu() { # DO NOT combine var java_pid_list declaration and assignment in ONE line! local java_pid_list # shellcheck disable=SC2086 - java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)" + java_pid_list=$(ps $ps_process_select_options -o pid --no-headers) [ -n "$java_pid_list" ] || __die_when_no_java_process_found # shellcheck disable=SC2086 - java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with , + java_pid_list=$(echo $java_pid_list | tr ' ' ,) # join with , # 1. sort by %cpu by top option `-o %CPU` # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), # use - # HOME="$tmp_store_dir" top -H -b -n 1 + # HOME=$tmp_store_dir top -H -b -n 1 # combined # sort # instead of - # HOME="$tmp_store_dir" top -H -b -n 1 -o '%CPU' + # HOME=$tmp_store_dir top -H -b -n 1 -o %CPU # 2. change HOME env var when run top, # so as to prevent top command output format being change by .toprc user config file unexpectedly # 3. use option `-d 0.5`(update interval 0.5 second) and `-n 2`(update 2 times), @@ -477,9 +477,9 @@ __top_threadId_cpu() { local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") # DO NOT combine var ps_out declaration and assignment in ONE line! local top_out - top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") + top_out=$(HOME=$tmp_store_dir "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then - printf '%s\n' "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_top" + printf '%s\n' "$top_out" | logAndCat "${top_cmd_line[@]}" >"$store_file_prefix$((update_round_num + 1))_top" fi # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! @@ -505,9 +505,9 @@ __complete_pid_user_by_ps() { local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) # DO NOT combine var ps_out declaration and assignment in ONE line! local ps_out - ps_out="$("${ps_cmd_line[@]}")" + ps_out=$("${ps_cmd_line[@]}") if [ -n "$store_dir" ]; then - printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((update_round_num + 1))_ps" + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"$store_file_prefix$((update_round_num + 1))_ps" fi local idx=0 threadId pcpu output_fields @@ -515,10 +515,10 @@ __complete_pid_user_by_ps() { ((count <= 0 || idx < count)) || break # output field: pid, threadId, pcpu, user - output_fields="$(printf '%s\n' "$ps_out" | + output_fields=$(printf '%s\n' "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { print $1, threadId, pcpu, $3; exit - }')" + }') if [ -n "$output_fields" ]; then ((idx++)) printf '%s\n' "$output_fields" @@ -537,19 +537,19 @@ printStackOfThreads() { printf -v threadId0x '%#x' "$threadId" ((idx++)) - local jstackFile="${store_file_prefix}$((update_round_num + 1))_jstack_${pid}" - [ -f "${jstackFile}" ] || { + local jstackFile="$store_file_prefix$((update_round_num + 1))_jstack_$pid" + [ -f "$jstackFile" ] || { # shellcheck disable=SC2206 - local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $lock_info ${pid}) - if [ "${user}" == "${USER}" ]; then + local -a jstack_cmd_line=("$jstack_path" $force $mix_native_frames $lock_info $pid) + if [ "$user" == "$USER" ]; then # run without sudo, when java process user is current user - logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}" + logAndRun "${jstack_cmd_line[@]}" >"$jstackFile" elif [ $UID == 0 ]; then # if java process user is not current user, must run jstack with sudo - logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}" + logAndRun sudo -u "$user" "${jstack_cmd_line[@]}" >"$jstackFile" else # current user is not root user, so can not run with sudo; print error message and rerun suggestion - redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." redOutput "User of java process($user) is not current user($USER), need sudo to rerun:" yellowOutput " sudo $(printCallingCommandLine)" normalOutput @@ -557,12 +557,12 @@ printStackOfThreads() { fi || { redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." normalOutput - rm "${jstackFile}" &>/dev/null + rm "$jstackFile" &>/dev/null continue } } - blueOutput "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" + blueOutput "[$idx] Busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user):" if [ -n "$mix_native_frames" ]; then local sed_script="/--------------- $threadId ---------------/,/^---------------/ { @@ -571,18 +571,18 @@ printStackOfThreads() { p }" elif [ -n "$force" ]; then - local sed_script="/^Thread ${threadId}:/,/^$/ { + local sed_script="/^Thread $threadId:/,/^$/ { /^$/d; p # delete end separator line }" else - local sed_script="/ nid=${threadId0x} /,/^$/ { + local sed_script="/ nid=$threadId0x /,/^$/ { /^$/d; p # delete end separator line }" fi { - sed "$sed_script" -n "${jstackFile}" + sed "$sed_script" -n "$jstackFile" echo - } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} + } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} done } @@ -597,7 +597,7 @@ main() { ((update_round_num > 0)) && sleep "$update_delay" [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | - tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} >/dev/null + tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} >/dev/null ((update_count != 1)) && headInfo if [ "$cpu_sample_interval" == 0 ]; then diff --git a/bin/taoc b/bin/taoc index 17419a41..a51a93ee 100755 --- a/bin/taoc +++ b/bin/taoc @@ -19,13 +19,13 @@ set -eEuo pipefail readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 rotateColorPrint() { - local content="$*" + local content=$* # skip color for white space if [[ "$content" =~ ^[[:space:]]*$ ]]; then printf '%s\n' "$content" else - local color="${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]}" + local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} printf '\e[1;%sm%s\e[0m\n' "$color" "$content" fi } diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index ec0440b7..0ae8746a 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -11,7 +11,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' diff --git a/bin/uq b/bin/uq index 2d958fdf..c26565ad 100755 --- a/bin/uq +++ b/bin/uq @@ -29,7 +29,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -65,11 +65,11 @@ die() { } convertHumanReadableSizeToSize() { - local human_readable_size="$1" + local human_readable_size=$1 [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1 - local size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}" + local size=${BASH_REMATCH[1]} unit=${BASH_REMATCH[2]} case "$unit" in g) ((size *= 1024 * 1024 * 1024)) @@ -86,7 +86,7 @@ convertHumanReadableSizeToSize() { } usage() { - local -r exit_code="${1:-0}" + local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -183,7 +183,7 @@ while (($# > 0)); do shift ;; -XM | --max-input) - uq_max_input_human_readable_size="$2" + uq_max_input_human_readable_size=$2 shift 2 ;; -h | --help) @@ -202,7 +202,7 @@ while (($# > 0)); do shift ;; -*) - usage 2 "${PROG}: unrecognized option '$1'" + usage 2 "$PROG: unrecognized option '$1'" ;; *) argv=(${argv[@]:+"${argv[@]}"} "$1") @@ -220,7 +220,7 @@ done yellowPrint "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 # NOTE: DO NOT declare var uq_max_input_size as readonly in ONE line! -uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" || +uq_max_input_size=$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size") || usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" readonly argc=${#argv[@]} diff --git a/bin/xpf b/bin/xpf index a0c52cd3..fe8b5b1a 100755 --- a/bin/xpf +++ b/bin/xpf @@ -17,9 +17,9 @@ set -eEuo pipefail # How can I get the behavior of GNU's readlink -f on a Mac? # https://stackoverflow.com/questions/1055671 portableReadLink() { - local file="$1" uname + local file=$1 uname - uname="$(uname)" + uname=$(uname) case "$uname" in Linux* | CYGWIN* | MINGW*) readlink -f -- "$file" @@ -49,9 +49,9 @@ portableReadLink() { ################################################################################ # DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell: -# BASE_DIR="$(dirname -- "$(portableReadLink "${BASH_SOURCE[0]}")")" -THIS_SCRIPT="$(portableReadLink "${BASH_SOURCE[0]}")" -BASE_DIR="$(dirname -- "$THIS_SCRIPT")" +# BASE_DIR=$(dirname -- "$(portableReadLink "${BASH_SOURCE[0]}")") +THIS_SCRIPT=$(portableReadLink "${BASH_SOURCE[0]}") +BASE_DIR=$(dirname -- "$THIS_SCRIPT") # shellcheck disable=SC1091 source "$BASE_DIR/xpl" "$@" diff --git a/bin/xpl b/bin/xpl index 7ae388ba..dc5c42ab 100755 --- a/bin/xpl +++ b/bin/xpl @@ -26,7 +26,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +PROG=$(basename -- "$0") readonly PROG readonly PROG_VERSION='2.6.0-dev' @@ -35,7 +35,7 @@ readonly PROG_VERSION='2.6.0-dev' ################################################################################ usage() { - local -r exit_code="${1:-0}" + local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) @@ -85,7 +85,7 @@ while [ $# -gt 0 ]; do break ;; -*) - usage 2 "${PROG}: unrecognized option '$1'" + usage 2 "$PROG: unrecognized option '$1'" ;; *) files=(${files[@]:+"${files[@]}"} "$1") @@ -98,7 +98,7 @@ done files=("${files[@]:-.}") # if program name is xpf, set option selected! -[ "xpf" == "${PROG}" ] && selected=true +[ "xpf" == "$PROG" ] && selected=true readonly files selected @@ -108,11 +108,11 @@ readonly files selected # open one file openOneFile() { - local file="$1" slt="${selected}" + local file=$1 slt=${selected} case "$(uname)" in Darwin*) - [ -f "${file}" ] && slt=true + [ -f "$file" ] && slt=true if $slt; then open -R "$file" else @@ -120,21 +120,21 @@ openOneFile() { fi ;; CYGWIN*) - [ -f "${file}" ] && slt=true + [ -f "$file" ] && slt=true if $slt; then - explorer /select, "$(cygpath -w "${file}")" + explorer /select, "$(cygpath -w "$file")" else - explorer "$(cygpath -w "${file}")" + explorer "$(cygpath -w "$file")" fi ;; *) - if [ -d "${file}" ]; then - nautilus "$(dirname -- "${file}")" + if [ -d "$file" ]; then + nautilus "$(dirname -- "$file")" else if $slt; then - nautilus "${file}" + nautilus "$file" else - nautilus "$(dirname -- "${file}")" + nautilus "$(dirname -- "$file")" fi fi ;; diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index e416388c..b536aa46 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -22,7 +22,7 @@ # var2=$(echo value1) colorEcho() { - local combination="$1" + local combination=$1 shift 1 # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? @@ -35,7 +35,7 @@ colorEcho() { } colorEchoWithoutNewLine() { - local combination="$1" + local combination=$1 shift 1 if [ -t 1 ]; then diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 7cd2f70d..4ae45e20 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -8,7 +8,7 @@ set -eEuo pipefail readonly nl=$'\n' # new line colorPrint() { - local color="$1" + local color=$1 shift # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? @@ -58,7 +58,7 @@ die() { ################################################################################ [ $# -ne 1 ] && die "need only 1 argument for version!$nl${nl}usage:$nl $0 2.x.y" -readonly bump_version="$1" +readonly bump_version=$1 # adjust current dir to project dir # diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 93aa69cf..dba6139d 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -6,7 +6,7 @@ if command -v greadlink &>/dev/null; then READLINK_CMD=greadlink fi -BASE="$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" +BASE=$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")") cd "$BASE" ################################################# From adefc1a3e8c5702ba59959603a7ae18171036094 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 6 Jan 2024 15:15:19 +0800 Subject: [PATCH 145/175] =?UTF-8?q?refactor(`coat`/`taoc`):=20extract=20`c?= =?UTF-8?q?olorLines`=20function;=20remove=20pipe=20when=20naked=20`cat`?= =?UTF-8?q?=20=F0=9F=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/coat | 18 +++++++++++++----- bin/taoc | 14 +++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/bin/coat b/bin/coat index 301a1d95..73fa06aa 100755 --- a/bin/coat +++ b/bin/coat @@ -30,8 +30,16 @@ rotateColorPrint() { fi } -# Bash read line does not read leading spaces -# https://stackoverflow.com/questions/29689172 -cat "$@" | while IFS= read -r line; do - rotateColorPrint "$line" -done +colorLines() { + # Bash read line does not read leading spaces + # https://stackoverflow.com/questions/29689172 + while IFS= read -r line; do + rotateColorPrint "$line" + done +} + +if [ $# == 0 ]; then + colorLines +else + cat "$@" | colorLines +fi diff --git a/bin/taoc b/bin/taoc index a51a93ee..db20368d 100755 --- a/bin/taoc +++ b/bin/taoc @@ -30,8 +30,12 @@ rotateColorPrint() { fi } -# Bash read line does not read leading spaces -# https://stackoverflow.com/questions/29689172 -tac "$@" | while IFS= read -r line; do - rotateColorPrint "$line" -done +colorLines() { + # Bash read line does not read leading spaces + # https://stackoverflow.com/questions/29689172 + while IFS= read -r line; do + rotateColorPrint "$line" + done +} + +tac "$@" | colorLines From eb47955ab1685f1a92a251406107295b305b9495 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 6 Jan 2024 15:35:24 +0800 Subject: [PATCH 146/175] =?UTF-8?q?fix:=20missing=20error=20exit=20code=20?= =?UTF-8?q?when=20file=20not=20existed=20or=20open=20file=20failure=20?= =?UTF-8?q?=E2=9D=97=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/xpl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/bin/xpl b/bin/xpl index dc5c42ab..d595eed8 100755 --- a/bin/xpl +++ b/bin/xpl @@ -34,12 +34,23 @@ readonly PROG_VERSION='2.6.0-dev' # util functions ################################################################################ +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} + usage() { local -r exit_code=${1:-0} (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && printf '%s\n\n' "$*" >&"$out" + (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" <&2 continue } - openOneFile "$file" + openOneFile "$file" || has_error=true done # set exit status From 68b38e74b6277d7ce60b5438db54abf03a506562 Mon Sep 17 00:00:00 2001 From: Edwin Kofler Date: Sat, 13 Jan 2024 13:32:33 -0800 Subject: [PATCH 147/175] fix: find bash with `/usr/bin/env` in shebang (#119) --- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 2 +- bin/coat | 2 +- bin/cp-into-docker-run | 2 +- bin/echo-args | 2 +- bin/find-in-jars | 2 +- bin/rp | 2 +- bin/show-busy-java-threads | 2 +- bin/taoc | 2 +- bin/tcp-connection-state-counter | 2 +- bin/uq | 2 +- bin/xpf | 2 +- bin/xpl | 2 +- legacy-bin/cp-svn-url | 2 +- legacy-bin/svn-merge-stop-on-copy | 2 +- legacy-bin/swtrunk | 2 +- lib/console-text-color-themes.sh | 2 +- lib/parseOpts.sh | 2 +- test-cases/bump-scripts-version.sh | 2 +- test-cases/integration-test.sh | 2 +- test-cases/lint.sh | 2 +- test-cases/my_unit_test_lib.sh | 2 +- test-cases/parseOpts_test.sh | 2 +- test-cases/self-installer.sh | 2 +- test-cases/uq_test.sh | 2 +- 26 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bin/a2l b/bin/a2l index 340879aa..468c4890 100755 --- a/bin/a2l +++ b/bin/a2l @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # print each arguments on one line colorfully. # diff --git a/bin/ap b/bin/ap index 250a0a76..3ae5d56e 100755 --- a/bin/ap +++ b/bin/ap @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # convert to Absolute Path. # diff --git a/bin/c b/bin/c index cdf1ee8a..fde157c1 100755 --- a/bin/c +++ b/bin/c @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Run command and put output to system clipper. # diff --git a/bin/coat b/bin/coat index 73fa06aa..ce6dc159 100755 --- a/bin/coat +++ b/bin/coat @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # cat lines colorfully. coat means *CO*lorful c*AT*. # diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index a6334f44..8fff22c3 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Copy the command into docker container and run the command in container. # diff --git a/bin/echo-args b/bin/echo-args index 61dacf2d..3739021f 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # print arguments in human and debugging friendly style. # diff --git a/bin/find-in-jars b/bin/find-in-jars index 5d7f7db1..44e53d31 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Find files in the jar files under specified directory, search jar files recursively(include subdirectory). # diff --git a/bin/rp b/bin/rp index 852ceae0..868a8cfb 100755 --- a/bin/rp +++ b/bin/rp @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # convert to Relative Path. # diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index f3e06d91..7888a7b9 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Find out the highest cpu consumed threads of java processes, and print the stack of these threads. # diff --git a/bin/taoc b/bin/taoc index db20368d..f30f0082 100755 --- a/bin/taoc +++ b/bin/taoc @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # tac lines colorfully. taoc means coat(*CO*lorful c*AT*) in reverse(last line first). # diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 0ae8746a..217b55eb 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # show count of tcp connection stat. # diff --git a/bin/uq b/bin/uq index c26565ad..4687c5f7 100755 --- a/bin/uq +++ b/bin/uq @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output). # same as `uniq` command in core utils, diff --git a/bin/xpf b/bin/xpf index fe8b5b1a..4d1fa279 100755 --- a/bin/xpf +++ b/bin/xpf @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Open file in file explorer, file is selected. # same as xpl --selected [file]... diff --git a/bin/xpl b/bin/xpl index d595eed8..4a130a39 100755 --- a/bin/xpl +++ b/bin/xpl @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Open file in file explorer. # diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index d0534517..16b20cce 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # copy the svn remote url of current svn directory. # diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index 28c04fa1..f65e533c 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # svn merge commit between version when source branch copy(--stop-on-copy) # and head version of source branch. diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index fc14abfd..8fb31274 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # switch svn work directory to trunk. # diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index b536aa46..07775cf6 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # show all console text color themes. # diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 31c506bc..86dd0842 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # parse options lib, support multiple values for one option. # diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 4ae45e20..118d1f1f 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eEuo pipefail ################################################################################ diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index 0242edf7..efe67421 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eEuo pipefail READLINK_CMD=readlink diff --git a/test-cases/lint.sh b/test-cases/lint.sh index 9cef7d3a..bc211274 100755 --- a/test-cases/lint.sh +++ b/test-cases/lint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eEuo pipefail # cd to the root of the project diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index ebd779f9..fe0c6c78 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # unit test lib ################################################# diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh index 03a2acac..f67d6883 100755 --- a/test-cases/parseOpts_test.sh +++ b/test-cases/parseOpts_test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash BASE="$(dirname -- "${BASH_SOURCE[0]}")" diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh index 90ab614e..ee0f8f57 100644 --- a/test-cases/self-installer.sh +++ b/test-cases/self-installer.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash if command -v svn &>/dev/null; then [ ! -d "/tmp/useful-scripts-$USER" ] && diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index dba6139d..d0fb8fcf 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eEuo pipefail READLINK_CMD=readlink From 5b25e6778cd0d0589b6d55c871bc2e9c1f91b234 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Jan 2024 01:37:11 +0800 Subject: [PATCH 148/175] =?UTF-8?q?fix(`coat/taoc`)=20=F0=9F=90=9E:=20miss?= =?UTF-8?q?ing=20last=20line=20if=20there=E2=80=99s=20no=20newline=20at=20?= =?UTF-8?q?the=20end=20of=20the=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/coat | 23 +++++++++++++++++------ bin/taoc | 23 +++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/bin/coat b/bin/coat index ce6dc159..c1fba865 100755 --- a/bin/coat +++ b/bin/coat @@ -11,31 +11,42 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# if stdout is a terminal, use `cat` directly. +# if stdout is not a terminal, use `cat` directly. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 -[ ! -t 1 ] && exec cat "$@" +[ -t 1 ] || exec cat "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 +# CAUTION: print content WITHOUT new line rotateColorPrint() { local content=$* - # skip color for white space if [[ "$content" =~ ^[[:space:]]*$ ]]; then - printf '%s\n' "$content" + printf %s "$content" else local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} - printf '\e[1;%sm%s\e[0m\n' "$color" "$content" + printf '\e[1;%sm%s\e[0m' "$color" "$content" fi } +rotateColorPrintln() { + rotateColorPrint "$*"$'\n' +} + colorLines() { + local line # Bash read line does not read leading spaces # https://stackoverflow.com/questions/29689172 while IFS= read -r line; do - rotateColorPrint "$line" + rotateColorPrintln "$line" done + # How to use `while read` (Bash) to read the last line in a file + # if there’s no newline at the end of the file? + # https://stackoverflow.com/questions/4165135 + if [ -n "$line" ]; then + rotateColorPrint "$line" + fi } if [ $# == 0 ]; then diff --git a/bin/taoc b/bin/taoc index f30f0082..4b2c4f76 100755 --- a/bin/taoc +++ b/bin/taoc @@ -11,31 +11,42 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# if stdout is a terminal, use `tac` directly. +# if stdout is not a terminal, use `tac` directly. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 -[ ! -t 1 ] && exec tac "$@" +[ -t 1 ] || exec tac "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) COUNT=0 +# CAUTION: print content WITHOUT new line rotateColorPrint() { local content=$* - # skip color for white space if [[ "$content" =~ ^[[:space:]]*$ ]]; then - printf '%s\n' "$content" + printf %s "$content" else local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} - printf '\e[1;%sm%s\e[0m\n' "$color" "$content" + printf '\e[1;%sm%s\e[0m' "$color" "$content" fi } +rotateColorPrintln() { + rotateColorPrint "$*"$'\n' +} + colorLines() { + local line # Bash read line does not read leading spaces # https://stackoverflow.com/questions/29689172 while IFS= read -r line; do - rotateColorPrint "$line" + rotateColorPrintln "$line" done + # How to use `while read` (Bash) to read the last line in a file + # if there’s no newline at the end of the file? + # https://stackoverflow.com/questions/4165135 + if [ -n "$line" ]; then + rotateColorPrint "$line" + fi } tac "$@" | colorLines From 176443e75de5451f4160540d65ff94ea1564a2a7 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Jan 2024 01:49:01 +0800 Subject: [PATCH 149/175] =?UTF-8?q?refactor(`awl`):=20merge=20function=20`?= =?UTF-8?q?colorPrint`=20into=20`rotateColorPrint`=20=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/a2l | 23 ++++++----------------- bin/coat | 2 +- bin/taoc | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/bin/a2l b/bin/a2l index 468c4890..b18c5f7b 100755 --- a/bin/a2l +++ b/bin/a2l @@ -19,19 +19,6 @@ readonly PROG_VERSION='2.6.0-dev' # util functions ################################################################################ -colorPrint() { - local color=$1 - shift - # if stdout is a terminal, turn on color output. - # '-t' check: is a terminal? - # check isatty in bash https://stackoverflow.com/questions/10022323 - if [ -t 1 ]; then - printf '\e[1;%sm%s\e[0m\n' "$color" "$*" - else - printf '%s\n' "$*" - fi -} - usage() { cat < Date: Fri, 16 Feb 2024 21:57:05 +0800 Subject: [PATCH 150/175] fix: missing stack on Java21 Java21 changed the format of stack title line #120 --- bin/show-busy-java-threads | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 7888a7b9..3c94da73 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -575,12 +575,12 @@ printStackOfThreads() { /^$/d; p # delete end separator line }" else - local sed_script="/ nid=$threadId0x /,/^$/ { + local sed_script="/ nid=($threadId0x|$threadId) /,/^$/ { /^$/d; p # delete end separator line }" fi { - sed "$sed_script" -n "$jstackFile" + sed "$sed_script" -n -r "$jstackFile" echo } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} done From 1aa76a6e47ff6c66d6fe076847644b313cbbf65e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 22:57:48 +0800 Subject: [PATCH 151/175] =?UTF-8?q?feat:=20improve=20Separator=20blank=20l?= =?UTF-8?q?ine=20output=20=F0=9F=92=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/show-busy-java-threads | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 3c94da73..6992d5c7 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -303,7 +303,7 @@ isNaturalNumber "$update_count" || die "update count($update_count) is not a nat readonly update_count if [ -n "$pid_list" ]; then - pid_list=${pid_list//[[:space:]]} # delete white space + pid_list=${pid_list//[[:space:]]/} # delete white space isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" fi readonly pid_list @@ -562,6 +562,7 @@ printStackOfThreads() { } } + ((idx > 1)) && normalOutput blueOutput "[$idx] Busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user):" if [ -n "$mix_native_frames" ]; then @@ -579,10 +580,7 @@ printStackOfThreads() { /^$/d; p # delete end separator line }" fi - { - sed "$sed_script" -n -r "$jstackFile" - echo - } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} + sed "$sed_script" -n -r "$jstackFile" | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} done } @@ -594,7 +592,10 @@ main() { local update_round_num # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do - ((update_round_num > 0)) && sleep "$update_delay" + ((update_round_num > 0)) && { + sleep "$update_delay" + normalOutput + } [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} >/dev/null From c678e28fccba1238ed2bb4030629a35106c2574c Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Jan 2024 01:56:43 +0800 Subject: [PATCH 152/175] ! update after release v2.5.2/v2.5.3 --- README.md | 8 ++++---- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 2 +- bin/cp-into-docker-run | 2 +- bin/find-in-jars | 2 +- bin/rp | 2 +- bin/show-busy-java-threads | 2 +- bin/show-duplicate-java-classes | 2 +- bin/tcp-connection-state-counter | 2 +- bin/uq | 2 +- bin/xpl | 2 +- legacy-bin/cp-svn-url | 2 +- 13 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e83f1335..3f32367e 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Github Workflow Build Status GitHub release License -GitHub Stars -GitHub Forks +GitHub Stars +GitHub Forks GitHub issues GitHub Contributors GitHub repo size @@ -142,10 +142,10 @@ PS: `Shell`用`Bash`的原因是: - 目前仍然是主流的`Shell`,并且在不同环境基本上都缺省部署了。 -- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/)中,明确说明了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 +- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/)中,明确说到了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 +- 统一用`Bash`,可以避免差异带来的风险与没有收益的复杂性。 - 有大量的`Shell`实现,`sh`、`bash`、`zsh`、`fish`、`csh`、`tcsh`、`ksh`、`ash`、`dash`…… - 不同的`Shell`有各种差异,深坑勿入。 - - 统一用`Bash`,可以避免差异带来的风险与没有收益的复杂性。 - 个人系统学习过的是`Bash`,比较理解熟悉。 PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/),但在严谨的`Shell`脚本开发时还是使用`Bash`。 diff --git a/bin/a2l b/bin/a2l index b18c5f7b..01612197 100755 --- a/bin/a2l +++ b/bin/a2l @@ -13,7 +13,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/ap b/bin/ap index 3ae5d56e..5d2ffd8d 100755 --- a/bin/ap +++ b/bin/ap @@ -15,7 +15,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/c b/bin/c index fde157c1..d844196a 100755 --- a/bin/c +++ b/bin/c @@ -29,7 +29,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 8fff22c3..d4841e67 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -11,7 +11,7 @@ set -eEuo pipefail PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/find-in-jars b/bin/find-in-jars index 44e53d31..fc5e198c 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -34,7 +34,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/rp b/bin/rp index 868a8cfb..d4cce78c 100755 --- a/bin/rp +++ b/bin/rp @@ -15,7 +15,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 6992d5c7..b9ed6a91 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -28,7 +28,7 @@ # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 # How can I get the source directory of a Bash script from within the script itself? diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 70737791..08365b69 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -30,7 +30,7 @@ from zipfile import ZipFile, BadZipfile ################################################################################ # utils functions ################################################################################ -PROG_VERSION = '2.6.0-dev' +PROG_VERSION = '2.x-dev' # How to delete line with echo? # https://unix.stackexchange.com/questions/26576 diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 217b55eb..726b7229 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -13,7 +13,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/uq b/bin/uq index 4687c5f7..d05d62b2 100755 --- a/bin/uq +++ b/bin/uq @@ -31,7 +31,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/bin/xpl b/bin/xpl index 4a130a39..160fbfb6 100755 --- a/bin/xpl +++ b/bin/xpl @@ -28,7 +28,7 @@ set -eEuo pipefail # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG=$(basename -- "$0") readonly PROG -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 16b20cce..970147b5 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -27,7 +27,7 @@ # NOTE: DO NOT declare var PROG as readonly in ONE line! PROG="$(basename -- "$0")" -readonly PROG_VERSION='2.6.0-dev' +readonly PROG_VERSION='2.x-dev' usage() { cat < Date: Fri, 16 Feb 2024 23:12:34 +0800 Subject: [PATCH 153/175] =?UTF-8?q?fix:=20improve=20Separator=20blank=20li?= =?UTF-8?q?ne=20output=20again=20=F0=9F=92=85=20fix=20wrong=20blank=20line?= =?UTF-8?q?=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/show-busy-java-threads | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index b9ed6a91..affb4e12 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -536,7 +536,7 @@ printStackOfThreads() { while read -r pid threadId pcpu user; do printf -v threadId0x '%#x' "$threadId" - ((idx++)) + ((idx++ > 0)) && normalOutput local jstackFile="$store_file_prefix$((update_round_num + 1))_jstack_$pid" [ -f "$jstackFile" ] || { # shellcheck disable=SC2206 @@ -552,17 +552,14 @@ printStackOfThreads() { redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." redOutput "User of java process($user) is not current user($USER), need sudo to rerun:" yellowOutput " sudo $(printCallingCommandLine)" - normalOutput continue fi || { redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - normalOutput rm "$jstackFile" &>/dev/null continue } } - ((idx > 1)) && normalOutput blueOutput "[$idx] Busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user):" if [ -n "$mix_native_frames" ]; then From 7f83a7ebb939d31b2bb8ac93812c7b4d1cbb1a50 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Jan 2024 13:38:12 +0800 Subject: [PATCH 154/175] =?UTF-8?q?refactor:=20improve=20comments;=20use?= =?UTF-8?q?=20`||/&&`=20instead=20of=20one=20branch/one=20line=20`if`=20?= =?UTF-8?q?=F0=9F=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/a2l | 4 ++-- bin/c | 2 +- bin/coat | 4 +--- bin/cp-into-docker-run | 2 +- bin/find-in-jars | 10 +++------- bin/taoc | 4 +--- 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/bin/a2l b/bin/a2l index 01612197..81cfcc21 100755 --- a/bin/a2l +++ b/bin/a2l @@ -60,12 +60,12 @@ while [ $# -gt 0 ]; do break ;; -*) - # if unrecognized option, treat it and all follow args as args + # if unrecognized option, treat it and all follow arguments as args args=(${args[@]:+"${args[@]}"} "$@") break ;; *) - # if not option, treat all follow args as args + # if not option, treat all follow arguments as args args=(${args[@]:+"${args[@]}"} "$@") break ;; diff --git a/bin/c b/bin/c index d844196a..3aba6a03 100755 --- a/bin/c +++ b/bin/c @@ -111,7 +111,7 @@ while [ $# -gt 0 ]; do usage 2 "unrecognized option '$1'" ;; *) - # if not option, treat all follow args as command + # if not option, treat all follow arguments as command target_command=(${target_command[@]:+"${target_command[@]}"} "$@") break ;; diff --git a/bin/coat b/bin/coat index 9eff9922..d98d50e0 100755 --- a/bin/coat +++ b/bin/coat @@ -44,9 +44,7 @@ colorLines() { # How to use `while read` (Bash) to read the last line in a file # if there’s no newline at the end of the file? # https://stackoverflow.com/questions/4165135 - if [ -n "$line" ]; then - rotateColorPrint "$line" - fi + [ -z "$line" ] || rotateColorPrint "$line" } if [ $# == 0 ]; then diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index d4841e67..c5a0d4e2 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -164,7 +164,7 @@ while (($# > 0)); do usage 2 "$PROG: unrecognized option '$1'" ;; *) - # if not option, treat all follow args as command + # if not option, treat all follow arguments as command args=(${args[@]:+"${args[@]}"} "$@") break ;; diff --git a/bin/find-in-jars b/bin/find-in-jars index fc5e198c..05d23aec 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -349,7 +349,7 @@ searchJarFiles() { total_jar_count=$(printf '%s\n' "$jar_files" | wc -l) # remove white space, because the `wc -l` output on mac contains white space! - total_jar_count=${total_jar_count//[[:space:]]} + total_jar_count=${total_jar_count//[[:space:]]/} echo "$total_jar_count" printf '%s\n' "$jar_files" @@ -370,13 +370,9 @@ __outputResultOfJarFile() { # - 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 -c "${grep_opt_args[@]}" &>/dev/null; then - matched=true - fi + grep -c "${grep_opt_args[@]}" &>/dev/null && matched=true - if [ "$print_matched_files" != "$matched" ]; then - return - fi + [ "$print_matched_files" != "$matched" ] && return clearResponsiveMessage if [ -t 1 ]; then diff --git a/bin/taoc b/bin/taoc index 7a0ebe23..adf7e663 100755 --- a/bin/taoc +++ b/bin/taoc @@ -44,9 +44,7 @@ colorLines() { # How to use `while read` (Bash) to read the last line in a file # if there’s no newline at the end of the file? # https://stackoverflow.com/questions/4165135 - if [ -n "$line" ]; then - rotateColorPrint "$line" - fi + [ -z "$line" ] || rotateColorPrint "$line" } tac "$@" | colorLines From 470ef867714718ba682d4516088d1099755fca66 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 26 Jan 2024 13:15:09 +0800 Subject: [PATCH 155/175] =?UTF-8?q?refactor(`uq`):=20use=20exponential=20o?= =?UTF-8?q?peration=20instead=20of=20multiplication=20to=20calculate=20siz?= =?UTF-8?q?e=20=F0=9F=A7=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/uq | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/uq b/bin/uq index d05d62b2..edf3180a 100755 --- a/bin/uq +++ b/bin/uq @@ -9,7 +9,7 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq # @author Zava Xu (zava.kid at gmail dot com) -# @author Jerry Lee (oldratlee at gmail dot com)# NOTE about Bash Traps and Pitfalls: +# @author Jerry Lee (oldratlee at gmail dot com) # # NOTE about Bash Traps and Pitfalls: # @@ -71,14 +71,14 @@ convertHumanReadableSizeToSize() { local size=${BASH_REMATCH[1]} unit=${BASH_REMATCH[2]} case "$unit" in - g) - ((size *= 1024 * 1024 * 1024)) + k) + ((size *= 1024)) ;; m) - ((size *= 1024 * 1024)) + ((size *= 1024 ** 2)) ;; - k) - ((size *= 1024)) + g) + ((size *= 1024 ** 3)) ;; esac From 59eca5d2f3a14b3c266cbfabf1b4804125f79ebf Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Thu, 25 Jan 2024 18:22:25 +0800 Subject: [PATCH 156/175] =?UTF-8?q?refactor:=20remove=20unnecessary=20file?= =?UTF-8?q?=20descriptor=20number=20`1`=20in=20redirections=20=E2=9E=A1?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/cp-into-docker-run | 4 ++-- bin/show-busy-java-threads | 2 +- bin/uq | 2 +- legacy-bin/cp-svn-url | 2 +- test-cases/bump-scripts-version.sh | 2 +- test-cases/my_unit_test_lib.sh | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index c5a0d4e2..6d1aff02 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -29,7 +29,7 @@ redPrint() { } die() { - redPrint "Error: $*" 1>&2 + redPrint "Error: $*" >&2 exit 1 } @@ -242,7 +242,7 @@ trap cleanupWhenExit EXIT ######################################## logAndRun() { - $verbose && printf '%s\n' "[$PROG] $*" 1>&2 + $verbose && printf '%s\n' "[$PROG] $*" >&2 "$@" } diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index affb4e12..70384464 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -101,7 +101,7 @@ blueOutput() { } die() { - redOutput "Error: $*" 1>&2 + redOutput "Error: $*" >&2 exit 1 } diff --git a/bin/uq b/bin/uq index edf3180a..e4133c57 100755 --- a/bin/uq +++ b/bin/uq @@ -60,7 +60,7 @@ yellowPrint() { } die() { - redPrint "Error: $*" 1>&2 + redPrint "Error: $*" >&2 exit 1 } diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 970147b5..0866c4ed 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -78,7 +78,7 @@ readonly dir="${1:-.}" # NOTE: DO NOT declare var url as readonly in ONE line! url="$(svn info "${dir}" | awk '/^URL: /{print $2}')" if [ -z "${url}" ]; then - echo "Fail to get svn url!" 1>&2 + echo "Fail to get svn url!" >&2 exit 1 fi diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 118d1f1f..32ab7568 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -49,7 +49,7 @@ logAndRun() { } die() { - redPrint "Error: $*" 1>&2 + redPrint "Error: $*" >&2 exit 1 } diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index fe0c6c78..907c1e20 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -40,7 +40,7 @@ fail() { } die() { - redEcho "Error: $*" 1>&2 + redEcho "Error: $*" >&2 exit 1 } From 5a1a8e3baa3fa0d4e10d547cf88d7f6fbde74af2 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 26 Jan 2024 13:06:50 +0800 Subject: [PATCH 157/175] =?UTF-8?q?refactor:=20use=20`Shell=20Arithmetic`?= =?UTF-8?q?=20comparison=20instead=20of=20`Conditional=20Expressions`=20?= =?UTF-8?q?=F0=9F=94=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/a2l | 2 +- bin/ap | 2 +- bin/c | 4 ++-- bin/coat | 2 +- bin/rp | 6 +++--- bin/xpl | 2 +- legacy-bin/cp-svn-url | 2 +- legacy-bin/svn-merge-stop-on-copy | 2 +- test-cases/bump-scripts-version.sh | 2 +- test-cases/my_unit_test_lib.sh | 2 +- test-cases/parseOpts_test.sh | 24 ++++++++++++------------ 11 files changed, 25 insertions(+), 25 deletions(-) diff --git a/bin/a2l b/bin/a2l index 81cfcc21..75ba380c 100755 --- a/bin/a2l +++ b/bin/a2l @@ -46,7 +46,7 @@ progVersion() { ################################################################################ declare -a args=() -while [ $# -gt 0 ]; do +while (($# > 0)); do case "$1" in -h | --help) usage diff --git a/bin/ap b/bin/ap index 5d2ffd8d..bcadda3f 100755 --- a/bin/ap +++ b/bin/ap @@ -106,7 +106,7 @@ progVersion() { ################################################################################ declare -a files=() -while [ $# -gt 0 ]; do +while (($# > 0)); do case "$1" in -h | --help) usage diff --git a/bin/c b/bin/c index 3aba6a03..ea9d4984 100755 --- a/bin/c +++ b/bin/c @@ -86,7 +86,7 @@ progVersion() { quiet=false keep_eol=false declare -a target_command=() -while [ $# -gt 0 ]; do +while (($# > 0)); do case "$1" in -k | --keep-eol) keep_eol=true @@ -156,7 +156,7 @@ teeAndCopy() { fi } -if [ ${#target_command[@]} -eq 0 ]; then +if ((${#target_command[@]} == 0)); then teeAndCopy else "${target_command[@]}" | teeAndCopy diff --git a/bin/coat b/bin/coat index d98d50e0..e38b558b 100755 --- a/bin/coat +++ b/bin/coat @@ -47,7 +47,7 @@ colorLines() { [ -z "$line" ] || rotateColorPrint "$line" } -if [ $# == 0 ]; then +if (($# == 0)); then colorLines else cat "$@" | colorLines diff --git a/bin/rp b/bin/rp index d4cce78c..469be555 100755 --- a/bin/rp +++ b/bin/rp @@ -105,7 +105,7 @@ progVersion() { ################################################################################ declare -a files=() -while [ $# -gt 0 ]; do +while (($# > 0)); do case "$1" in -h | --help) usage @@ -129,9 +129,9 @@ while [ $# -gt 0 ]; do esac done -[ ${#files[@]} -eq 0 ] && die "NO argument!" +((${#files[@]} == 0)) && die "NO argument!" -if [ ${#files[@]} -eq 1 ]; then +if ((${#files[@]} == 1)); then relativeTo=. else argc=${#files[@]} diff --git a/bin/xpl b/bin/xpl index 160fbfb6..74494a38 100755 --- a/bin/xpl +++ b/bin/xpl @@ -78,7 +78,7 @@ progVersion() { declare -a files=() selected=false -while [ $# -gt 0 ]; do +while (($# > 0)); do case "$1" in -s | --selected) selected=true diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 0866c4ed..e6704ea6 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -68,7 +68,7 @@ done # biz logic ################################################################################ -[ $# -gt 1 ] && { +(($# > 1)) && { echo At most 1 local directory is need! usage 1 } diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index f65e533c..4a969b2a 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -37,7 +37,7 @@ EOF exit $1 } -[ $# -gt 2 ] && { +(($# > 2)) && { echo "too many arguments!" usage 1 } diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 32ab7568..8e838110 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -57,7 +57,7 @@ die() { # biz logic ################################################################################ -[ $# -ne 1 ] && die "need only 1 argument for version!$nl${nl}usage:$nl $0 2.x.y" +(($# != 1)) && die "need only 1 argument for version!$nl${nl}usage:$nl $0 2.x.y" readonly bump_version=$1 # adjust current dir to project dir diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh index 907c1e20..df2cc668 100644 --- a/test-cases/my_unit_test_lib.sh +++ b/test-cases/my_unit_test_lib.sh @@ -61,7 +61,7 @@ assertArrayEquals() { local a1=("${!a1PlaceHolder}") local a2=("${!a2PlaceHolder}") - [ ${#a1[@]} -eq ${#a2[@]} ] || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" + ((${#a1[@]} == ${#a2[@]})) || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" local i for ((i = 0; i < ${#a1[@]}; i++)); do diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh index f67d6883..35ad0018 100755 --- a/test-cases/parseOpts_test.sh +++ b/test-cases/parseOpts_test.sh @@ -19,8 +19,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" [[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" @@ -50,8 +50,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" [[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" @@ -79,8 +79,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 232 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 232)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" [[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" [[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" @@ -96,8 +96,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" [[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" [[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" @@ -113,8 +113,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 221 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 221)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" [[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" [[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" @@ -126,8 +126,8 @@ test_exitCode=$? _opts_showOptDescInfoList _opts_showOptValueInfoList -[ $test_exitCode -eq 222 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" +((test_exitCode == 222)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" [[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" [[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" [[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" From c547856bedeb930a5540a0a22ef940bd77755672 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Jan 2024 13:35:19 +0800 Subject: [PATCH 158/175] =?UTF-8?q?feat(`coat/taoc`):=20add=20`--help/--ve?= =?UTF-8?q?rsion`=20options=20=F0=9F=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/coat | 43 ++++++++++++++++++++++++++++++++++++++ bin/taoc | 43 ++++++++++++++++++++++++++++++++++++++ docs/shell.md | 58 ++++++++++++++++----------------------------------- 3 files changed, 104 insertions(+), 40 deletions(-) diff --git a/bin/coat b/bin/coat index e38b558b..2b116a9b 100755 --- a/bin/coat +++ b/bin/coat @@ -11,6 +11,49 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +# NOTE: DO NOT declare var PROG as readonly in ONE line! +PROG=$(basename -- "$0") +readonly PROG +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# parse options +################################################################################ + +progVersion() { + printf '%s version: %s\n' "$PROG" "$PROG_VERSION" + printf 'cat executable: %s\n' "$(command -v cat)" + exit +} + +usage() { + cat <= 0; --idx)); do + [ "${args[idx]}" = --help ] && usage + [ "${args[idx]}" = --version ] && progVersion +done +unset args idx + +################################################################################ +# biz logic +################################################################################ + # if stdout is not a terminal, use `cat` directly. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 diff --git a/bin/taoc b/bin/taoc index adf7e663..4534365e 100755 --- a/bin/taoc +++ b/bin/taoc @@ -11,6 +11,49 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail +# NOTE: DO NOT declare var PROG as readonly in ONE line! +PROG=$(basename -- "$0") +readonly PROG +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# parse options +################################################################################ + +progVersion() { + printf '%s version: %s\n' "$PROG" "$PROG_VERSION" + printf 'tac executable: %s\n' "$(command -v tac)" + exit +} + +usage() { + cat <= 0; --idx)); do + [ "${args[idx]}" = --help ] && usage + [ "${args[idx]}" = --version ] && progVersion +done +unset args idx + +################################################################################ +# biz logic +################################################################################ + # if stdout is not a terminal, use `tac` directly. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 diff --git a/docs/shell.md b/docs/shell.md index ff81c3ae..b64e217e 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -154,48 +154,26 @@ line2 of file2 # 帮助信息 # 可以看到本人机器上实现代理的`cat`/`tac`命令是GNU的实现。 $ coat --help -Usage: cat [OPTION]... [FILE]... -Concatenate FILE(s) to standard output. - -With no FILE, or when FILE is -, read standard input. - - -A, --show-all equivalent to -vET - -b, --number-nonblank number nonempty output lines, overrides -n - -e equivalent to -vE - -E, --show-ends display $ at end of each line - -n, --number number all output lines - -s, --squeeze-blank suppress repeated empty output lines - -t equivalent to -vT - -T, --show-tabs display TAB characters as ^I - -u (ignored) - -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB - --help display this help and exit - --version output version information and exit - -Examples: - cat f - g Output f's contents, then standard input, then g's contents. - cat Copy standard input to standard output. - -GNU coreutils online help: -Full documentation at: -or available locally via: info '(coreutils) cat invocation' +Usage: coat [OPTION]... [FILE]... +cat lines colorfully. -$ taoc --help -Usage: tac [OPTION]... [FILE]... -Write each FILE to standard output, last line first. - -With no FILE, or when FILE is -, read standard input. +Support options: + --help display this help and exit + --version output version information and exit +All other options and arguments are delegated to command cat, +more info see the help/man of command cat(e.g. cat --help). +cat executable: /usr/local/opt/coreutils/libexec/gnubin/cat -Mandatory arguments to long options are mandatory for short options too. - -b, --before attach the separator before instead of after - -r, --regex interpret the separator as a regular expression - -s, --separator=STRING use STRING as the separator instead of newline - --help display this help and exit - --version output version information and exit - -GNU coreutils online help: -Full documentation -or available locally via: info '(coreutils) tac invocation' +$ taoc --help +Usage: taoc [OPTION]... [FILE]... +tac lines colorfully. + +Support options: + --help display this help and exit + --version output version information and exit +All other options and arguments are delegated to command tac, +more info see the help/man of command tac(e.g. tac --help). +tac executable: /usr/local/opt/coreutils/libexec/gnubin/tac ``` 注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,如下: From f5219c52efccffe6ba1c10c6e487da182116f72b Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Mon, 29 Jan 2024 18:08:29 +0800 Subject: [PATCH 159/175] =?UTF-8?q?perf:=20use=20`${var##}`=20instead=20of?= =?UTF-8?q?=20`basename`=20=F0=9F=8E=9B=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no subprocess fork, faster --- bin/a2l | 4 +--- bin/ap | 4 +--- bin/c | 4 +--- bin/coat | 4 +--- bin/cp-into-docker-run | 6 ++---- bin/find-in-jars | 4 +--- bin/rp | 4 +--- bin/show-busy-java-threads | 4 +--- bin/taoc | 4 +--- bin/tcp-connection-state-counter | 4 +--- bin/uq | 4 +--- bin/xpl | 4 +--- legacy-bin/cp-svn-url | 3 +-- legacy-bin/svn-merge-stop-on-copy | 2 +- 14 files changed, 15 insertions(+), 40 deletions(-) diff --git a/bin/a2l b/bin/a2l index 75ba380c..fc09bb39 100755 --- a/bin/a2l +++ b/bin/a2l @@ -10,9 +10,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/ap b/bin/ap index bcadda3f..11505e85 100755 --- a/bin/ap +++ b/bin/ap @@ -12,9 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/c b/bin/c index ea9d4984..a17149ec 100755 --- a/bin/c +++ b/bin/c @@ -26,9 +26,7 @@ # var2=$(echo value1) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/coat b/bin/coat index 2b116a9b..e3d7ca95 100755 --- a/bin/coat +++ b/bin/coat @@ -11,9 +11,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 6d1aff02..e7370381 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -9,8 +9,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ @@ -207,8 +206,7 @@ if [ ! -f "$specified_run_command" ]; then run_command=$(which "$specified_run_command") fi run_command=$(portableReadLink "$run_command") -run_command_base_name=$(basename -- "$run_command") -readonly run_command run_command_base_name +readonly run_command run_command_base_name=${run_command##*/} run_timestamp=$(date "+%Y%m%d_%H%M%S") readonly run_timestamp diff --git a/bin/find-in-jars b/bin/find-in-jars index 05d23aec..3a93f12b 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -31,9 +31,7 @@ # var2=$(echo value1) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/rp b/bin/rp index 469be555..2f54ac39 100755 --- a/bin/rp +++ b/bin/rp @@ -12,9 +12,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 70384464..0f00a6f1 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -25,9 +25,7 @@ # local var2 # var2=$(echo value1) -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' # choosing between $0 and BASH_SOURCE # https://stackoverflow.com/a/35006505/922688 diff --git a/bin/taoc b/bin/taoc index 4534365e..fa9df313 100755 --- a/bin/taoc +++ b/bin/taoc @@ -11,9 +11,7 @@ # @author Jerry Lee (oldratlee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 726b7229..1ef92360 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -10,9 +10,7 @@ # @author @sunuslee (sunuslee at gmail dot com) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/uq b/bin/uq index e4133c57..9d0cec67 100755 --- a/bin/uq +++ b/bin/uq @@ -28,9 +28,7 @@ # var2=$(echo value1) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/bin/xpl b/bin/xpl index 74494a38..30281993 100755 --- a/bin/xpl +++ b/bin/xpl @@ -25,9 +25,7 @@ # var2=$(echo value1) set -eEuo pipefail -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG=$(basename -- "$0") -readonly PROG +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index e6704ea6..2bfaea12 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -25,8 +25,7 @@ # local var2 # var2=$(echo value1) -# NOTE: DO NOT declare var PROG as readonly in ONE line! -PROG="$(basename -- "$0")" +readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' usage() { diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index 4a969b2a..1cf033e5 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -11,7 +11,7 @@ # @author jiangjizhong(@jzwlqx) # @author Jerry Lee (oldratlee at gmail dot com) -PROG="$(basename -- "$0")" +readonly PROG=${0##*/} usage() { cat < Date: Mon, 29 Jan 2024 18:26:53 +0800 Subject: [PATCH 160/175] =?UTF-8?q?refactor(`c`):=20rename=20functions=20?= =?UTF-8?q?=F0=9F=94=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/c b/bin/c index a17149ec..c26c4447 100755 --- a/bin/c +++ b/bin/c @@ -122,7 +122,7 @@ readonly keep_eol quiet target_command # biz logic ################################################################################ -copy() { +systemClip() { case "$(uname)" in Darwin*) pbcopy @@ -136,21 +136,21 @@ copy() { esac } -catThenCopy() { +bufferCopy() { local content content=$(cat) if $keep_eol; then printf '%s\n' "$content" else printf %s "$content" - fi | copy + fi | systemClip } teeAndCopy() { if $quiet; then - catThenCopy + bufferCopy else - tee >(catThenCopy) + tee >(bufferCopy) fi } From 7e03b4ffa0cec3b9bfdac34d28ef73ff6db0012a Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 3 Feb 2024 00:44:06 +0800 Subject: [PATCH 161/175] refactor: simplify and uniform the codes - remove declare for global vars, more consistent - use upper-case var name for global readonly vars - unset temp global vars after use --- bin/a2l | 2 +- bin/ap | 5 ++-- bin/c | 2 +- bin/coat | 2 +- bin/cp-into-docker-run | 2 +- bin/echo-args | 5 ++-- bin/find-in-jars | 47 ++++++++++++++++++++------------------ bin/rp | 2 +- bin/show-busy-java-threads | 12 +++++----- bin/taoc | 2 +- bin/uq | 6 +++-- bin/xpl | 2 +- legacy-bin/swtrunk | 14 +++++++----- 13 files changed, 54 insertions(+), 49 deletions(-) diff --git a/bin/a2l b/bin/a2l index fc09bb39..697c59c0 100755 --- a/bin/a2l +++ b/bin/a2l @@ -43,7 +43,7 @@ progVersion() { # parse options ################################################################################ -declare -a args=() +args=() while (($# > 0)); do case "$1" in -h | --help) diff --git a/bin/ap b/bin/ap index 11505e85..4a38e027 100755 --- a/bin/ap +++ b/bin/ap @@ -103,7 +103,7 @@ progVersion() { # parse options ################################################################################ -declare -a files=() +files=() while (($# > 0)); do case "$1" in -h | --help) @@ -129,8 +129,7 @@ while (($# > 0)); do done # if files is empty, use "." -files=("${files[@]:-.}") -readonly files +readonly files=("${files[@]:-.}") ################################################################################ # biz logic diff --git a/bin/c b/bin/c index c26c4447..b1d1146e 100755 --- a/bin/c +++ b/bin/c @@ -83,7 +83,7 @@ progVersion() { quiet=false keep_eol=false -declare -a target_command=() +target_command=() while (($# > 0)); do case "$1" in -k | --keep-eol) diff --git a/bin/coat b/bin/coat index e3d7ca95..1658408e 100755 --- a/bin/coat +++ b/bin/coat @@ -40,7 +40,7 @@ EOF exit } -declare -a args=("$@") +args=("$@") # check arguments in reverse, so last option wins. for ((idx = $# - 1; idx >= 0; --idx)); do [ "${args[idx]}" = --help ] && usage diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index e7370381..90b72bab 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -120,7 +120,7 @@ docker_workdir= docker_tmpdir=/tmp docker_command_cp_path= verbose=false -declare -a args=() +args=() while (($# > 0)); do case "$1" in diff --git a/bin/echo-args b/bin/echo-args index 3739021f..9186a6e3 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -20,8 +20,7 @@ digitCount() { digit_count=$(digitCount $#) readonly arg_count=$# digit_count -readonly red='\e[1;31m' blue='\e[1;36m' color_reset='\e[0m' - +readonly RED='\e[1;31m' BLUE='\e[1;36m' COLOR_RESET='\e[0m' printArg() { local idx=$1 value=$2 @@ -29,7 +28,7 @@ printArg() { # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "%${digit_count}s/%s: ${red}[${blue}%s${red}]${color_reset}\n" "$idx" "$arg_count" "$value" + printf "%${digit_count}s/%s: ${RED}[${BLUE}%s${RED}]${COLOR_RESET}\n" "$idx" "$arg_count" "$value" else printf "%${digit_count}s/%s: [%s]\n" "$idx" "$arg_count" "$value" fi diff --git a/bin/find-in-jars b/bin/find-in-jars index 3a93f12b..1f4c9d18 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -38,34 +38,34 @@ readonly PROG_VERSION='2.x-dev' # util functions ################################################################################ -readonly color_reset='\e[0m' - -# 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='\e[2K\r' +readonly COLOR_RESET='\e[0m' redPrint() { # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf "\e[1;31m%s$color_reset\n" "$*" + printf "\e[1;31m%s$COLOR_RESET\n" "$*" else printf '%s\n' "$*" fi } +# 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 LINE_CLEAR='\e[2K\r' + # 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}') +[ -t 2 ] && COLUMNS=$(stty size | awk '{print $2}') printResponsiveMessage() { if ! $show_responsive || [ ! -t 2 ]; then @@ -74,7 +74,7 @@ printResponsiveMessage() { local content=$* # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - printf %b%s "$clear_line" "${content:0:columns}" >&2 + printf %b%s "$LINE_CLEAR" "${content:0:COLUMNS}" >&2 } clearResponsiveMessage() { @@ -82,7 +82,7 @@ clearResponsiveMessage() { return fi - printf %b "$clear_line" >&2 + printf %b "$LINE_CLEAR" >&2 } die() { @@ -155,9 +155,9 @@ progVersion() { # parse options ################################################################################ -declare -a dirs=() -declare -a extensions=() -declare -a args=() +dirs=() +extensions=() +args=() separator='!' regex_mode=-E @@ -250,7 +250,7 @@ readonly extensions=${extensions:-jar} ((${#args[@]} > 1)) && usage 1 "More than 1 file pattern: ${args[*]}" readonly pattern=${args[0]} -declare -a tmp_dirs=() +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!" @@ -262,6 +262,7 @@ done # set dirs to Absolute Path $use_absolute_path && dirs=("${tmp_dirs[@]}") readonly dirs +unset d tmp_dirs # convert extensions to find -iname options find_iname_options=() @@ -269,6 +270,7 @@ for e in "${extensions[@]}"; do find_iname_options=(${find_iname_options[@]:+"${find_iname_options[@]}" -o} -iname "*.$e") done readonly find_iname_options +unset e ################################################################################ # Check the existence of command for listing zip entry! @@ -353,8 +355,9 @@ searchJarFiles() { printf '%s\n' "$jar_files" } +readonly JAR_COLOR='\e[1;35m' SEP_COLOR='\e[1;32m' __outputResultOfJarFile() { - local jar_file=$1 file jar_color='\e[1;35m' sep_color='\e[1;32m' + local jar_file=$1 file # shellcheck disable=SC2206 local grep_opt_args=("$regex_mode" ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern") @@ -374,7 +377,7 @@ __outputResultOfJarFile() { clearResponsiveMessage if [ -t 1 ]; then - printf "$jar_color%s$color_reset\n" "$jar_file" + printf "$JAR_COLOR%s$COLOR_RESET\n" "$jar_file" else printf '%s\n' "$jar_file" fi @@ -386,7 +389,7 @@ __outputResultOfJarFile() { } | while read -r file; do clearResponsiveMessage if [ -t 1 ]; then - printf "$jar_color%s$sep_color%s$color_reset%s\n" "$jar_file" "$separator" "$file" + printf "$JAR_COLOR%s$SEP_COLOR%s$COLOR_RESET%s\n" "$jar_file" "$separator" "$file" else printf '%s\n' "$jar_file$separator$file" fi diff --git a/bin/rp b/bin/rp index 2f54ac39..54a26586 100755 --- a/bin/rp +++ b/bin/rp @@ -102,7 +102,7 @@ progVersion() { # parse options ################################################################################ -declare -a files=() +files=() while (($# > 0)); do case "$1" in -h | --help) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 0f00a6f1..aaf2f419 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -47,7 +47,7 @@ readonly USER ################################################################################ # NOTE: $'foo' is the escape sequence syntax of bash -readonly nl=$'\n' # new line +readonly NL=$'\n' # new line colorPrint() { local color=$1 @@ -151,7 +151,7 @@ usage() { (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && colorPrint 31 "$*$nl" >&"$out" + (($# > 0)) && colorPrint 31 "$*$NL" >&"$out" cat >&"$out" </dev/null; then # 3. search jstack under PATH jstack_path=$(command -v jstack) - [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${nl}Use -s option set jstack path manually." + [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${NL}Use -s option set jstack path manually." else - die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${nl}Use -s option set jstack path manually." + die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${NL}Use -s option set jstack path manually." fi readonly jstack_path diff --git a/bin/taoc b/bin/taoc index fa9df313..a54f0d78 100755 --- a/bin/taoc +++ b/bin/taoc @@ -40,7 +40,7 @@ EOF exit } -declare -a args=("$@") +args=("$@") # check arguments in reverse, so last option wins. for ((idx = $# - 1; idx >= 0; --idx)); do [ "${args[idx]}" = --help ] && usage diff --git a/bin/uq b/bin/uq index 9d0cec67..6bcc7137 100755 --- a/bin/uq +++ b/bin/uq @@ -143,7 +143,7 @@ uq_opt_only_unique=0 uq_opt_ignore_case=0 uq_opt_zero_terminated=0 uq_max_input_human_readable_size=256m -declare -a argv=() +argv=() while (($# > 0)); do case "$1" in @@ -221,7 +221,7 @@ done uq_max_input_size=$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size") || usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" -readonly argc=${#argv[@]} +readonly argc=${#argv[@]} argv if ((argc == 0)); then input_files=() @@ -236,6 +236,7 @@ else output_file=/dev/stdout fi fi +readonly output_file # Check input file for f in ${input_files[@]:+"${input_files[@]}"}; do @@ -247,6 +248,7 @@ for f in ${input_files[@]:+"${input_files[@]}"}; do [ -f "$f" ] || die "input file $f exists, but is not a file!" [ -r "$f" ] || die "input file $f exists, but is not readable!" done +unset f ################################################################################ # biz logic diff --git a/bin/xpl b/bin/xpl index 30281993..8dfb7d7a 100755 --- a/bin/xpl +++ b/bin/xpl @@ -74,7 +74,7 @@ progVersion() { # parse options ################################################################################ -declare -a files=() +files=() selected=false while (($# > 0)); do case "$1" in diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index 8fb31274..bf8bedef 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -8,15 +8,17 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-swtrunk # @author Jerry Lee (oldratlee at gmail dot com) -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end - colorEcho() { local color=$1 shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "${ec}[1;${color}m$*$eend" || echo "$@" + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi } redEcho() { From ebb62cd8f3e3f05d0bfe0ff0b44a84200c02a77e Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 3 Feb 2024 01:18:34 +0800 Subject: [PATCH 162/175] =?UTF-8?q?refactor:=20use=20`shellcheck`=20item?= =?UTF-8?q?=20doc=20link=20instead=20of=20prolixity=20comments=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 16 ---------------- bin/find-in-jars | 18 ------------------ bin/show-busy-java-threads | 26 ++++++-------------------- bin/uq | 21 +++------------------ bin/xpl | 16 ---------------- legacy-bin/cp-svn-url | 19 ++----------------- lib/console-text-color-themes.sh | 16 ---------------- 7 files changed, 11 insertions(+), 121 deletions(-) diff --git a/bin/c b/bin/c index b1d1146e..5d4912a7 100755 --- a/bin/c +++ b/bin/c @@ -8,22 +8,6 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-c # @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 readonly PROG=${0##*/} diff --git a/bin/find-in-jars b/bin/find-in-jars index 1f4c9d18..c4b970f2 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -13,22 +13,6 @@ # # @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 readonly PROG=${0##*/} @@ -63,8 +47,6 @@ readonly LINE_CLEAR='\e[2K\r' # 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() { diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index aaf2f419..45ae7734 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -8,22 +8,6 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-busy-java-threads # @author Jerry Lee (oldratlee at gmail dot com) # @author superhj1987 (superhj1987 at 126 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) readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' @@ -38,7 +22,8 @@ readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # See https://www.lifewire.com/current-linux-user-whoami-command-3867579 # Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! # -# NOTE: DO NOT declare var USER as readonly in ONE line! +# DO NOT declare and assign var USER(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 USER=$(whoami) readonly USER @@ -220,7 +205,7 @@ uname | grep '^Linux' -q || die "$PROG only support Linux, not support $(uname) # parse options ################################################################################ -# DO NOT declare var ARGS as readonly in ONE line! +# DO NOT declare and assign var ARGS(as readonly) in ONE line! ARGS=$( getopt -n "$PROG" -a -o c:p:a:s:S:i:Pd:FmlhV \ -l count:,pid:,append-file:,jstack-path:,store-dir:,cpu-sample-interval:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ @@ -371,7 +356,7 @@ readonly jstack_path # biz logic ################################################################################ -# NOTE: DO NOT declare var run_timestamp as readonly in ONE line! +# DO NOT declare and assign var run_timestamp(as readonly) in ONE line! run_timestamp=$(date "+%Y-%m-%d_%H:%M:%S.%N") readonly run_timestamp readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" @@ -433,6 +418,7 @@ findBusyJavaThreadsByPs() { # shellcheck disable=SC2206 local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) # DO NOT combine var ps_out declaration and assignment in ONE line! + # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 local ps_out ps_out=$("${ps_cmd_line[@]}" | sort -k3,3nr) [ -n "$ps_out" ] || __die_when_no_java_process_found @@ -473,7 +459,7 @@ __top_threadId_cpu() { # 4. top v3.3, there is 1 black line between 2 update; # but top v3.2, there is 2 blank lines between 2 update! local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") - # DO NOT combine var ps_out declaration and assignment in ONE line! + # DO NOT combine var top_out declaration and assignment in ONE line! local top_out top_out=$(HOME=$tmp_store_dir "${top_cmd_line[@]}") if [ -n "$store_dir" ]; then diff --git a/bin/uq b/bin/uq index 6bcc7137..e963e391 100755 --- a/bin/uq +++ b/bin/uq @@ -10,22 +10,6 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq # @author Zava Xu (zava.kid at gmail dot com) # @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 readonly PROG=${0##*/} @@ -217,11 +201,12 @@ done [[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && yellowPrint "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 -# NOTE: DO NOT declare var uq_max_input_size as readonly in ONE line! +# DO NOT declare and assign var uq_max_input_size(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 uq_max_input_size=$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size") || usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" -readonly argc=${#argv[@]} argv +readonly argc=${#argv[@]} argv uq_max_input_size if ((argc == 0)); then input_files=() diff --git a/bin/xpl b/bin/xpl index 8dfb7d7a..c0501f03 100755 --- a/bin/xpl +++ b/bin/xpl @@ -7,22 +7,6 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @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 readonly PROG=${0##*/} diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 2bfaea12..0b13038b 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -8,22 +8,6 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-cp-svn-url # @author ivanzhangwb (ivanzhangwb 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) readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' @@ -74,7 +58,8 @@ done readonly dir="${1:-.}" -# NOTE: DO NOT declare var url as readonly in ONE line! +# DO NOT declare and assign var url(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 url="$(svn info "${dir}" | awk '/^URL: /{print $2}')" if [ -z "${url}" ]; then echo "Fail to get svn url!" >&2 diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index 07775cf6..70833615 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -4,22 +4,6 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-console-text-color-themessh # @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) colorEcho() { local combination=$1 From 4ad6b5b88d20e45c72a7643f6178c694f6b1e058 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 18:08:58 +0800 Subject: [PATCH 163/175] refactor: small code cleanup - merge `colorPrint` function if only one caller - remove `local nl=$'\n'` declaration if only one usage - remove unnecessary `{}` when use var - use upper-case var name for global readonly vars - use `=` instead of `==` in `Conditional Expressions` - improve/fix/add code comments - remove section comments for simple section --- README.md | 10 +++++----- bin/a2l | 9 ++------- bin/ap | 13 +++--------- bin/coat | 1 + bin/cp-into-docker-run | 3 +-- bin/find-in-jars | 3 +-- bin/rp | 21 +++++++------------- bin/show-busy-java-threads | 20 +++++++++---------- bin/show-duplicate-java-classes | 4 ++-- bin/taoc | 1 + bin/tcp-connection-state-counter | 24 ++++++++++------------ bin/uq | 20 +++++++++---------- bin/xpl | 9 +++++---- legacy-bin/cp-svn-url | 32 +++++++++++++++--------------- legacy-bin/svn-merge-stop-on-copy | 12 +++++------ legacy-bin/swtrunk | 4 ++-- test-cases/bump-scripts-version.sh | 7 ++++--- test-cases/integration-test.sh | 9 ++------- test-cases/uq_test.sh | 1 + 19 files changed, 89 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index 3f32367e..c6bac05b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ GitHub repo size

-🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc. +🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc. 👉 平时有用的手动操作做成脚本,以便捷地使用,让开发的日常生活更轻松些。 💕 @@ -143,7 +143,7 @@ PS: - 目前仍然是主流的`Shell`,并且在不同环境基本上都缺省部署了。 - 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/)中,明确说到了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 -- 统一用`Bash`,可以避免差异带来的风险与没有收益的复杂性。 +- 统一用`Bash`,可以避免不同`Shell`之间差异所带来的风险与没有收益的复杂性。 - 有大量的`Shell`实现,`sh`、`bash`、`zsh`、`fish`、`csh`、`tcsh`、`ksh`、`ash`、`dash`…… - 不同的`Shell`有各种差异,深坑勿入。 - 个人系统学习过的是`Bash`,比较理解熟悉。 @@ -154,14 +154,14 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ > 更多资料参见 [子文档](docs/developer-guide.md)。 -- 开发规范与工具 - - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) +- 🛠️ 开发规范与工具 + - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs - 👷 **`Bash/Shell`最佳实践与安全编程**文章 - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) | [英文原文:Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls) - - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) + - [不要自己去指定`sh`的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) - 🎶 **Tips** - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 diff --git a/bin/a2l b/bin/a2l index 697c59c0..1eda0b17 100755 --- a/bin/a2l +++ b/bin/a2l @@ -14,7 +14,7 @@ readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ -# util functions +# parse options ################################################################################ usage() { @@ -39,10 +39,6 @@ progVersion() { exit } -################################################################################ -# parse options -################################################################################ - args=() while (($# > 0)); do case "$1" in @@ -63,13 +59,12 @@ while (($# > 0)); do break ;; *) - # if not option, treat all follow arguments as args + # if not option, treat it and all follow arguments as args args=(${args[@]:+"${args[@]}"} "$@") break ;; esac done - readonly args ################################################################################ diff --git a/bin/ap b/bin/ap index 4a38e027..0b7e2026 100755 --- a/bin/ap +++ b/bin/ap @@ -19,23 +19,17 @@ readonly PROG_VERSION='2.x-dev' # util functions ################################################################################ -colorPrint() { - local color=$1 - shift +redPrint() { # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + printf '\e[1;31m%s\e[0m\n' "$*" else printf '%s\n' "$*" fi } -redPrint() { - colorPrint 31 "$*" -} - die() { redPrint "Error: $*" >&2 exit 1 @@ -75,8 +69,7 @@ usage() { local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash - local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >&"$out" + (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" < 0)) && redPrint "$*$nl" >&"$out" + (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" < 0)) && redPrint "$*$nl" >&"$out" + (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" <&2 exit 1 @@ -73,17 +67,16 @@ usage() { local -r out=$(((exit_code != 0) + 1)) # NOTE: $'foo' is the escape sequence syntax of bash - local nl=$'\n' # new line - (($# > 0)) && redPrint "$*$nl" >&"$out" + (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" < 0)) && colorPrint 31 "$*$NL" >&"$out" cat >&"$out" < find out the highest cpu consumed threads from @@ -215,7 +215,7 @@ ARGS=$( usage 1 } readonly ARGS -eval set -- "${ARGS}" +eval set -- "$ARGS" count=5 cpu_sample_interval=0.5 @@ -525,20 +525,20 @@ printStackOfThreads() { [ -f "$jstackFile" ] || { # shellcheck disable=SC2206 local -a jstack_cmd_line=("$jstack_path" $force $mix_native_frames $lock_info $pid) - if [ "$user" == "$USER" ]; then + if [ "$user" = "$USER" ]; then # run without sudo, when java process user is current user logAndRun "${jstack_cmd_line[@]}" >"$jstackFile" - elif [ $UID == 0 ]; then + elif ((UID == 0)); then # if java process user is not current user, must run jstack with sudo logAndRun sudo -u "$user" "${jstack_cmd_line[@]}" >"$jstackFile" else # current user is not root user, so can not run with sudo; print error message and rerun suggestion - redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." + redOutput "[$idx] Fail to jstack busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." redOutput "User of java process($user) is not current user($USER), need sudo to rerun:" yellowOutput " sudo $(printCallingCommandLine)" continue fi || { - redOutput "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." + redOutput "[$idx] Fail to jstack busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." rm "$jstackFile" &>/dev/null continue } @@ -582,7 +582,7 @@ main() { tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} >/dev/null ((update_count != 1)) && headInfo - if [ "$cpu_sample_interval" == 0 ]; then + if [ "$cpu_sample_interval" = 0 ]; then findBusyJavaThreadsByPs else findBusyJavaThreadsByTop diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index 08365b69..942ad2e9 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -24,8 +24,8 @@ from glob import glob from io import BytesIO from optparse import OptionParser from os import walk -from os.path import relpath, isdir, exists -from zipfile import ZipFile, BadZipfile +from os.path import exists, isdir, relpath +from zipfile import BadZipfile, ZipFile ################################################################################ # utils functions diff --git a/bin/taoc b/bin/taoc index a54f0d78..e51de9ce 100755 --- a/bin/taoc +++ b/bin/taoc @@ -72,6 +72,7 @@ rotateColorPrint() { } rotateColorPrintln() { + # NOTE: $'foo' is the escape sequence syntax of bash rotateColorPrint "$*"$'\n' } diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 1ef92360..899bc3fc 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -14,16 +14,16 @@ readonly PROG=${0##*/} readonly PROG_VERSION='2.x-dev' ################################################################################ -# util functions +# parse options ################################################################################ usage() { cat <= 0; --idx)); do + [[ "${args[idx]}" = -h || "${args[idx]}" = --help ]] && usage + [[ "${args[idx]}" = -V || "${args[idx]}" = --version ]] && progVersion done +unset args idx ################################################################################ # biz logic ################################################################################ # On MacOS, netstat need to using -p tcp to get only tcp output. -uname | grep Darwin -q && option_for_mac="-ptcp" +uname | grep Darwin -q && option_for_mac=-ptcp # shellcheck disable=SC2086 netstat -tna ${option_for_mac:-} | awk 'NR > 2 { diff --git a/bin/uq b/bin/uq index e963e391..840ea1d3 100755 --- a/bin/uq +++ b/bin/uq @@ -20,7 +20,7 @@ readonly PROG_VERSION='2.x-dev' ################################################################################ # NOTE: $'foo' is the escape sequence syntax of bash -readonly nl=$'\n' # new line +readonly NL=$'\n' # new line redPrint() { # if stdout is a terminal, turn on color output. @@ -72,10 +72,10 @@ usage() { (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) - (($# > 0)) && redPrint "$*$nl" >&"$out" + (($# > 0)) && redPrint "$*$NL" >&"$out" cat >&"$out" < 0)); do uq_opt_all_repeated=1 uq_opt_repeated_method=${1#--all-repeated=} - [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] || - usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’" + [[ $uq_opt_repeated_method = 'none' || $uq_opt_repeated_method = 'prepend' || $uq_opt_repeated_method = 'separate' ]] || + usage 1 "$PROG: invalid argument ‘$uq_opt_repeated_method’ for ‘--all-repeated’${NL}Valid arguments are:$NL - ‘none’$NL - ‘prepend’$NL - ‘separate’" shift ;; @@ -193,12 +193,12 @@ while (($# > 0)); do esac done -[[ $uq_opt_only_repeated == 1 && $uq_opt_only_unique == 1 ]] && +[[ $uq_opt_only_repeated = 1 && $uq_opt_only_unique = 1 ]] && usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" -[[ $uq_opt_all_repeated == 1 && $uq_opt_only_unique == 1 ]] && +[[ $uq_opt_all_repeated = 1 && $uq_opt_only_unique = 1 ]] && usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" -[[ $uq_opt_all_repeated == 1 && $uq_opt_repeated_method == none && ($uq_opt_count == 0 && $uq_opt_only_repeated == 0) ]] && +[[ $uq_opt_all_repeated = 1 && $uq_opt_repeated_method = none && ($uq_opt_count = 0 && $uq_opt_only_repeated = 0) ]] && yellowPrint "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 # DO NOT declare and assign var uq_max_input_size(as readonly) in ONE line! @@ -217,7 +217,7 @@ elif ((argc == 1)); then else input_files=("${argv[@]:0:argc-1}") output_file=${argv[argc - 1]} - if [ "$output_file" == - ]; then + if [ "$output_file" = - ]; then output_file=/dev/stdout fi fi @@ -226,7 +226,7 @@ readonly output_file # Check input file for f in ${input_files[@]:+"${input_files[@]}"}; do # - is stdin, ok - [ "$f" == - ] && continue + [ "$f" = - ] && continue [ -e "$f" ] || die "input file $f does not exist!" [ ! -d "$f" ] || die "input file $f exists, but is a directory!" diff --git a/bin/xpl b/bin/xpl index c0501f03..2bac215c 100755 --- a/bin/xpl +++ b/bin/xpl @@ -32,12 +32,13 @@ usage() { (($# > 0)) && shift local -r out=$(((exit_code != 0) + 1)) + # NOTE: $'foo' is the escape sequence syntax of bash (($# > 0)) && redPrint "$*"$'\n' >&"$out" cat >&"$out" <= 0; --idx)); do + [[ "${args[idx]}" = -h || "${args[idx]}" = --help ]] && usage + [[ "${args[idx]}" = -V || "${args[idx]}" = --version ]] && progVersion done +unset args idx ################################################################################ # biz logic @@ -60,8 +60,8 @@ readonly dir="${1:-.}" # DO NOT declare and assign var url(as readonly) in ONE line! # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 -url="$(svn info "${dir}" | awk '/^URL: /{print $2}')" -if [ -z "${url}" ]; then +url="$(svn info "$dir" | awk '/^URL: /{print $2}')" +if [ -z "$url" ]; then echo "Fail to get svn url!" >&2 exit 1 fi @@ -80,4 +80,4 @@ copy() { esac } -echo -n "${url}" | copy && echo "${url} copied!" +echo -n "$url" | copy && echo "$url copied!" diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index 1cf033e5..635b2abc 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -15,20 +15,20 @@ readonly PROG=${0##*/} usage() { cat < [target branch] +Usage: $PROG [target branch] svn merge commit between version when source branch copy(--stop-on-copy) and head version of source branch. Source branch must be a remote branch. Example: - ${PROG} http://www.foo.com/project1/branches/feature1 + $PROG http://www.foo.com/project1/branches/feature1 # merge http://www.foo.com/project1/branches/feature1 to current svn directory - ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/directory + $PROG http://www.foo.com/project1/branches/feature1 /path/to/svn/directory # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory # will prompt confirm for committing to target branch. - ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 + $PROG http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 # because http://www.foo.com/project1/branches/feature2 is remote url, # will check out target branch to tmp directory, and prompt confirm for committing to target branch. @@ -79,11 +79,11 @@ cd "$workDir" && if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then echo "oldest version($from_version) of source branch $source_branch ." echo "starting merge to $workDir ." - svn merge "-${from_version}:HEAD" "$source_branch" + svn merge "-$from_version:HEAD" "$source_branch" else echo "Fail to merge to work dir $workDir ." exit 2 fi read -r -p "Check In? (Y/N)" ci -[ "$ci" == "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch" +[ "$ci" = "Y" ] && svn ci -m "svn merge -$from_version:HEAD $source_branch" diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index bf8bedef..6578fd3b 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -33,7 +33,7 @@ greenEcho() { dirs=("${dirs[@]:-.}") for d in "${dirs[@]}"; do - [ ! -d "${d}/.svn" ] && { + [ ! -d "$d/.svn" ] && { redEcho "directory $d is not a svn work directory, ignore directory $d !" continue } @@ -42,7 +42,7 @@ for d in "${dirs[@]}"; do branches=$(svn info | grep '^URL' | awk '{print $2}') && trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk && if svn sw "$trunk"; then - greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." + greenEcho "svn work directory $d switch from $branches to $trunk ." else redEcho "fail to switch $d to trunk!" fi diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh index 8e838110..d71213c4 100755 --- a/test-cases/bump-scripts-version.sh +++ b/test-cases/bump-scripts-version.sh @@ -5,7 +5,8 @@ set -eEuo pipefail # util functions ################################################################################ -readonly nl=$'\n' # new line +# NOTE: $'foo' is the escape sequence syntax of bash +readonly NL=$'\n' # new line colorPrint() { local color=$1 @@ -43,7 +44,7 @@ logAndRun() { echo "Run under work directory $PWD : $*" "$@" else - bluePrint "Run under work directory $PWD :$nl$*" + bluePrint "Run under work directory $PWD :$NL$*" time "$@" fi } @@ -57,7 +58,7 @@ die() { # biz logic ################################################################################ -(($# != 1)) && die "need only 1 argument for version!$nl${nl}usage:$nl $0 2.x.y" +(($# != 1)) && die "need only 1 argument for version!$NL${NL}usage:$NL $0 2.x.y" readonly bump_version=$1 # adjust current dir to project dir diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index efe67421..1bc1d233 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -8,13 +8,6 @@ fi cd "$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" -################################################################################ -# constants -################################################################################ - -# NOTE: $'foo' is the escape sequence syntax of bash -readonly nl=$'\n' # new line - ################################################################################ # common util functions ################################################################################ @@ -55,6 +48,8 @@ logAndRun() { echo "Run under work directory $PWD : $*" "$@" else + # NOTE: $'foo' is the escape sequence syntax of bash + local nl=$'\n' # new line blueEcho "Run under work directory $PWD :$nl$*" time "$@" fi diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index d0fb8fcf..0634a109 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -14,6 +14,7 @@ cd "$BASE" ################################################# readonly uq="../bin/uq" +# NOTE: $'foo' is the escape sequence syntax of bash readonly nl=$'\n' # new line test_input=$(cat uq_test_input) From 55d7c15de49de2b47f1b04cbd2facb9ab22d0414 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 18:39:17 +0800 Subject: [PATCH 164/175] =?UTF-8?q?refactor:=20use=20command=20`realpath`?= =?UTF-8?q?=20instead=20of=20function=20`portableReadLink`=20without=20los?= =?UTF-8?q?ing=20portability=20=F0=9F=94=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/ap | 39 ++++++++-------------------------- bin/cp-into-docker-run | 36 ++++++++----------------------- bin/xpf | 39 ++++++++-------------------------- test-cases/integration-test.sh | 9 ++++---- test-cases/lint.sh | 6 +++++- test-cases/uq_test.sh | 9 ++++---- 6 files changed, 40 insertions(+), 98 deletions(-) diff --git a/bin/ap b/bin/ap index 0b7e2026..921f8afd 100755 --- a/bin/ap +++ b/bin/ap @@ -35,32 +35,13 @@ die() { exit 1 } -# How can I get the behavior of GNU's readlink -f on a Mac? -# https://stackoverflow.com/questions/1055671 -portableReadLink() { - local file=$1 uname - - uname=$(uname) - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f -- "$file" - ;; - Darwin*) - local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") - if command -v greadlink >/dev/null; then - greadlink -f -- "$file" - elif command -v python3 >/dev/null; then - python3 "${py_args[@]}" - elif command -v python >/dev/null; then - python "${py_args[@]}" - else - die "fail to find command(greadlink/python3/python) to get absolute path!" - fi - ;; - *) - die "NOT support uname($uname)!" - ;; - esac +# `realpath` command existed on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" } usage() { @@ -131,12 +112,10 @@ readonly files=("${files[@]:-.}") has_error=false for f in "${files[@]}"; do - if [ -e "$f" ]; then - portableReadLink "$f" - else + realpath "$f" || { redPrint "error: $f does not exists!" >&2 has_error=true - fi + } done # set exit status diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 9c563d33..4d69a502 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -36,32 +36,13 @@ isAbsolutePath() { [[ "$1" =~ ^/ ]] } -# How can I get the behavior of GNU's readlink -f on a Mac? -# https://stackoverflow.com/questions/1055671 -portableReadLink() { - local file=$1 uname - - uname=$(uname) - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f -- "$file" - ;; - Darwin*) - local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") - if command -v greadlink >/dev/null; then - greadlink -f -- "$file" - elif command -v python3 >/dev/null; then - python3 "${py_args[@]}" - elif command -v python >/dev/null; then - python "${py_args[@]}" - else - die "fail to find command(greadlink/python3/python) for readlink!" - fi - ;; - *) - die "NOT support uname($uname)!" - ;; - esac +# `realpath` command existed on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" } usage() { @@ -204,7 +185,8 @@ if [ ! -f "$specified_run_command" ]; then run_command=$(which "$specified_run_command") fi -run_command=$(portableReadLink "$run_command") + +run_command=$(realpath "$run_command") readonly run_command run_command_base_name=${run_command##*/} run_timestamp=$(date "+%Y%m%d_%H%M%S") diff --git a/bin/xpf b/bin/xpf index 4d1fa279..b407f0fb 100755 --- a/bin/xpf +++ b/bin/xpf @@ -14,34 +14,13 @@ set -eEuo pipefail # util functions ################################################################################ -# How can I get the behavior of GNU's readlink -f on a Mac? -# https://stackoverflow.com/questions/1055671 -portableReadLink() { - local file=$1 uname - - uname=$(uname) - case "$uname" in - Linux* | CYGWIN* | MINGW*) - readlink -f -- "$file" - ;; - Darwin*) - local py_args=(-c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$file") - if command -v greadlink >/dev/null; then - greadlink -f -- "$file" - elif command -v python3 >/dev/null; then - python3 "${py_args[@]}" - elif command -v python >/dev/null; then - python "${py_args[@]}" - else - echo "fail to find command(greadlink/python3/python) for readlink!" >&2 - exit 1 - fi - ;; - *) - echo "not support uname($uname)!" >&2 - exit 1 - ;; - esac +# `realpath` command existed on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" } ################################################################################ @@ -49,8 +28,8 @@ portableReadLink() { ################################################################################ # DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell: -# BASE_DIR=$(dirname -- "$(portableReadLink "${BASH_SOURCE[0]}")") -THIS_SCRIPT=$(portableReadLink "${BASH_SOURCE[0]}") +# BASE_DIR=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")") +THIS_SCRIPT=$(realpath "${BASH_SOURCE[0]}") BASE_DIR=$(dirname -- "$THIS_SCRIPT") # shellcheck disable=SC1091 diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh index 1bc1d233..cbcdbe5c 100755 --- a/test-cases/integration-test.sh +++ b/test-cases/integration-test.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash set -eEuo pipefail -READLINK_CMD=readlink -if command -v greadlink &>/dev/null; then - READLINK_CMD=greadlink -fi +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} -cd "$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")")" +cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")" ################################################################################ # common util functions diff --git a/test-cases/lint.sh b/test-cases/lint.sh index bc211274..76859457 100755 --- a/test-cases/lint.sh +++ b/test-cases/lint.sh @@ -1,8 +1,12 @@ #!/usr/bin/env bash set -eEuo pipefail +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + # cd to the root of the project -cd "$(dirname -- "$(readlink -f -- "${BASH_SOURCE[0]}")")"/.. +cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")"/.. find bin lib legacy-bin -type f | grep -Pv '/show-duplicate-java-classes$' | diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh index 0634a109..c3bfdfe0 100755 --- a/test-cases/uq_test.sh +++ b/test-cases/uq_test.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash set -eEuo pipefail -READLINK_CMD=readlink -if command -v greadlink &>/dev/null; then - READLINK_CMD=greadlink -fi +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} -BASE=$(dirname -- "$($READLINK_CMD -f -- "${BASH_SOURCE[0]}")") +BASE=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")") cd "$BASE" ################################################# From 96bb69ce0147623d348b63bc3fb5df879cfa7ed6 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sun, 18 Feb 2024 20:06:13 +0800 Subject: [PATCH 165/175] =?UTF-8?q?build/ci:=20upgrade=20GitHub=20actions;?= =?UTF-8?q?=20add=20`dependabot.yml`=20=F0=9F=A4=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/ci.yaml | 7 ++++++- .github/workflows/lint.yaml | 7 ++++++- 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..35615a0e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d867be6e..2c4b2298 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: name: Test on ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - run: brew install coreutils gnu-sed @@ -25,3 +25,8 @@ jobs: # https://docs.github.com/en/actions/learn-github-actions/expressions if: runner.os == 'macOS' - run: test-cases/integration-test.sh + # https://remarkablemark.org/blog/2017/10/12/check-git-dirty/ + - name: Check git dirty + run: | + git status --short + [ -z "$(git status --short)" ] diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 60468c95..26788dce 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -12,7 +12,12 @@ jobs: name: Lint steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: submodules: recursive - run: test-cases/lint.sh + # https://remarkablemark.org/blog/2017/10/12/check-git-dirty/ + - name: Check git dirty + run: | + git status --short + [ -z "$(git status --short)" ] From af3bcb59fc996e41b6bd0763323e310dcdd302bb Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 22:43:35 +0800 Subject: [PATCH 166/175] =?UTF-8?q?feat(`c`):=20ensure=20use=20command=20o?= =?UTF-8?q?n=20path=20=F0=9F=91=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 8 ++++---- docs/shell.md | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/bin/c b/bin/c index 5d4912a7..68a23f07 100755 --- a/bin/c +++ b/bin/c @@ -3,8 +3,9 @@ # Run command and put output to system clipper. # # @Usage -# $ c echo 'hello world!' -# $ echo 'hello world!' | c +# $ c ls -l +# $ ls -l | c +# $ c -q < ~/.ssh/id_rsa.pub # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-c # @author Jerry Lee (oldratlee at gmail dot com) @@ -41,7 +42,6 @@ Run command and put output to system clipper. If no command is specified, read from stdin(pipe). Example: - $PROG echo "hello world!" $PROG grep -i 'hello world' menu.h main.c set | $PROG $PROG -q < ~/.ssh/id_rsa.pub @@ -141,5 +141,5 @@ teeAndCopy() { if ((${#target_command[@]} == 0)); then teeAndCopy else - "${target_command[@]}" | teeAndCopy + command "${target_command[@]}" | teeAndCopy fi diff --git a/docs/shell.md b/docs/shell.md index b64e217e..425a116f 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -98,7 +98,6 @@ Run command and put output to system clipper. If no command is specified, read from stdin(pipe). Example: - c echo "hello world!" c grep -i 'hello world' menu.h main.c set | c c -q < ~/.ssh/id_rsa.pub From 906ec30f68ebf3cede3139b0ba2c16f6fdea6368 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 22:52:46 +0800 Subject: [PATCH 167/175] feat(`c`): check the command existence on PATH --- bin/c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/c b/bin/c index 68a23f07..96d4d6b3 100755 --- a/bin/c +++ b/bin/c @@ -102,6 +102,10 @@ done readonly keep_eol quiet target_command +if ((${#target_command[@]} > 0)) && ! which "${target_command[0]}" &>/dev/null; then + die "command '${target_command[0]}' not found on PATH" +fi + ################################################################################ # biz logic ################################################################################ From 8f2487e41a365a0ce541b003c1b28b83fddcd2b8 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 22:37:01 +0800 Subject: [PATCH 168/175] =?UTF-8?q?refactor(`show-busy-java-threads`):=20r?= =?UTF-8?q?ename=20global=20var=20`USER`=20->=20`WHOAMI`=20=F0=9F=91=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/show-busy-java-threads | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 3a62b373..3ef0db54 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -18,14 +18,14 @@ readonly PROG_VERSION='2.x-dev' # Will $0 always include the path to the script? # https://unix.stackexchange.com/questions/119929 readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") -# Get current user name via whoami command -# See https://www.lifewire.com/current-linux-user-whoami-command-3867579 -# Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! +# CAUTION: env var $USER is not reliable! +# $USER may be overwritten; if run command by `sudo -u`, may is not `root`. +# more info see https://www.baeldung.com/linux/get-current-user # -# DO NOT declare and assign var USER(as readonly) in ONE line! +# DO NOT declare and assign var WHOAMI(as readonly) in ONE line! # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 -USER=$(whoami) -readonly USER +WHOAMI=$(whoami) +readonly WHOAMI ################################################################################ # util functions @@ -525,7 +525,7 @@ printStackOfThreads() { [ -f "$jstackFile" ] || { # shellcheck disable=SC2206 local -a jstack_cmd_line=("$jstack_path" $force $mix_native_frames $lock_info $pid) - if [ "$user" = "$USER" ]; then + if [ "$user" = "$WHOAMI" ]; then # run without sudo, when java process user is current user logAndRun "${jstack_cmd_line[@]}" >"$jstackFile" elif ((UID == 0)); then @@ -534,7 +534,7 @@ printStackOfThreads() { else # current user is not root user, so can not run with sudo; print error message and rerun suggestion redOutput "[$idx] Fail to jstack busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." - redOutput "User of java process($user) is not current user($USER), need sudo to rerun:" + redOutput "User of java process($user) is not current user($WHOAMI), need sudo to rerun:" yellowOutput " sudo $(printCallingCommandLine)" continue fi || { From e3533a063f87253d0dcab03fcdb41d08f2e62d19 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 21:19:59 +0800 Subject: [PATCH 169/175] =?UTF-8?q?docs:=20update=20manual=20pages=20links?= =?UTF-8?q?=20to=20manned.org=20=F0=9F=93=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- docs/developer-guide.md | 8 ++++---- docs/shell.md | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c6bac05b..32a6764d 100644 --- a/README.md +++ b/README.md @@ -136,13 +136,13 @@ PS: 在这个库中的`Shell`脚本: -- 统一使用`Bash 3+`; +- 统一使用`Bash 3.2+`; - 面向生产环境,尽可能使用严谨安全的开发方式。 `Shell`用`Bash`的原因是: - 目前仍然是主流的`Shell`,并且在不同环境基本上都缺省部署了。 -- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/)中,明确说到了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 +- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background.html#shell)中,明确说到了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 - 统一用`Bash`,可以避免不同`Shell`之间差异所带来的风险与没有收益的复杂性。 - 有大量的`Shell`实现,`sh`、`bash`、`zsh`、`fish`、`csh`、`tcsh`、`ksh`、`ash`、`dash`…… - 不同的`Shell`有各种差异,深坑勿入。 @@ -155,7 +155,7 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ > 更多资料参见 [子文档](docs/developer-guide.md)。 - 🛠️ 开发规范与工具 - - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) + - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents.html) - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs - 👷 **`Bash/Shell`最佳实践与安全编程**文章 @@ -172,7 +172,7 @@ PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/ 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 - 官方资料 - - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [`bash man`](https://manned.org/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. diff --git a/docs/developer-guide.md b/docs/developer-guide.md index dfb8d625..af731386 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -1,7 +1,7 @@ # 📚 `Shell`学习与开发的资料 -- 开发规范与工具 - - [**_`Google Shell Style Guide`_**](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background/) +- 🛠️ 开发规范与工具 + - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents.html) - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs - 👷 **`Bash/Shell`最佳实践与安全编程**文章 @@ -10,7 +10,7 @@ - [编写可靠shell脚本的八个建议 - xshell.net](https://www.xshell.net/shell/1577.html) - [Shell 编码风格 - 团子的小窝](http://kodango.com/shell-script-style) - [Bash 优良编程实践](https://www.techug.com/post/bash-practice.html) - - [不要自己去指定sh的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) + - [不要自己去指定`sh`的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) - 🎶 **Tips** - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 @@ -32,7 +32,7 @@ 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 - 官方资料 - - [`bash man`](https://linux.die.net/man/1/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [`bash man`](https://manned.org/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. diff --git a/docs/shell.md b/docs/shell.md index 425a116f..4ff27aac 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -122,7 +122,7 @@ Options: 彩色`cat`/`tac`出文件行,方便人眼区分不同的行。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令支持选项、功能和使用方式与[`cat`](https://linux.die.net/man/1/cat)/[`tac`](https://linux.die.net/man/1/cat)命令完全一样。 +命令支持选项、功能和使用方式与[`cat`](https://manned.org/cat)/[`tac`](https://manned.org/tac)命令完全一样。 文件操作在实现上完全代理给了`cat`/`tac`命令。 - 命令名`coat`的意思是`COlorful cAT`;同时单词`coat`是外套,而彩色的输出行就像件漂亮的外套~ 🌈 😆 @@ -552,9 +552,9 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" 命令行选项解析库,加强支持选项有多个值(即数组)。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -自己写一个命令行选项解析函数,是因为[`bash`](http://linux.die.net/man/1/bash)的`builtin`命令[`getopts`](http://linux.die.net/man/1/getopts)和加强版本命令[`getopt`](http://linux.die.net/man/1/getopt)都不支持数组的值。 +自己写一个命令行选项解析函数,是因为[`bash`](https://manned.org/bash)的`builtin`命令[`getopts`](https://manned.org/man/getopts.1)和加强版本命令[`getopt`](https://manned.org/getopt)都不支持数组的值。 -指定选项的多个值(即数组)的风格模仿[`find`](http://linux.die.net/man/1/find)命令的`-exec`选项: +指定选项的多个值(即数组)的风格模仿[`find`](https://manned.org/find)命令的`-exec`选项: ```bash $ find . -name \*.txt -exec echo "find file: " {} \; From a9822022a6b6b800ba1664132a66032c3cc394be Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Feb 2024 16:45:55 +0800 Subject: [PATCH 170/175] =?UTF-8?q?refactor:=20extract=20`UNAME`=20var=20?= =?UTF-8?q?=F0=9F=94=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/show-busy-java-threads | 13 +++++-------- bin/tcp-connection-state-counter | 3 ++- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 3ef0db54..2c988438 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -22,10 +22,11 @@ readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") # $USER may be overwritten; if run command by `sudo -u`, may is not `root`. # more info see https://www.baeldung.com/linux/get-current-user # -# DO NOT declare and assign var WHOAMI(as readonly) in ONE line! +# DO NOT declare and assign var(as readonly) in ONE line! # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 WHOAMI=$(whoami) -readonly WHOAMI +UNAME=$(uname) +readonly WHOAMI UNAME ################################################################################ # util functions @@ -196,10 +197,10 @@ progVersion() { } ################################################################################ -# Check os support +# check os support ################################################################################ -uname | grep '^Linux' -q || die "$PROG only support Linux, not support $(uname) yet!" +[[ $UNAME = Linux* ]] || die "$PROG only support Linux, not support $UNAME yet!" ################################################################################ # parse options @@ -565,10 +566,6 @@ printStackOfThreads() { done } -################################################################################ -# Main -################################################################################ - main() { local update_round_num # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 899bc3fc..104ea6de 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -50,7 +50,8 @@ unset args idx ################################################################################ # On MacOS, netstat need to using -p tcp to get only tcp output. -uname | grep Darwin -q && option_for_mac=-ptcp +UNAME=$(uname) +[[ $UNAME = Darwin* ]] && option_for_mac=-ptcp # shellcheck disable=SC2086 netstat -tna ${option_for_mac:-} | awk 'NR > 2 { From 31e1087c178b42cd5191c7a4429ff5b55540d6dc Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Feb 2024 17:09:56 +0800 Subject: [PATCH 171/175] =?UTF-8?q?refactor:=20use=20bash=20builtin=20`typ?= =?UTF-8?q?e=20-P`=20instead=20of=20`which`=20command=20=F0=9F=90=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/c | 2 +- bin/cp-into-docker-run | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/c b/bin/c index 96d4d6b3..b36fd495 100755 --- a/bin/c +++ b/bin/c @@ -102,7 +102,7 @@ done readonly keep_eol quiet target_command -if ((${#target_command[@]} > 0)) && ! which "${target_command[0]}" &>/dev/null; then +if ((${#target_command[@]} > 0)) && ! type -P "${target_command[0]}" &>/dev/null; then die "command '${target_command[0]}' not found on PATH" fi diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index 4d69a502..cddc245b 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -180,10 +180,10 @@ command -v docker &>/dev/null || die 'docker command not found!' readonly specified_run_command=${args[0]} run_command=$specified_run_command if [ ! -f "$specified_run_command" ]; then - which "$specified_run_command" &>/dev/null || + type -P "$specified_run_command" &>/dev/null || die "specified command not exists and not found in PATH: $specified_run_command" - run_command=$(which "$specified_run_command") + run_command=$(type -P "$specified_run_command") fi run_command=$(realpath "$run_command") From 7894528ab73b9293e5d99c5d787b41c742ada298 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 24 Feb 2024 18:27:36 +0800 Subject: [PATCH 172/175] =?UTF-8?q?refactor(`show-busy-java-threads`):=20e?= =?UTF-8?q?xtract=20`timestamp`=20var=20to=20avoid=20inconsistency=20?= =?UTF-8?q?=E2=8C=9A=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/show-busy-java-threads | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 2c988438..73e78194 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -376,8 +376,9 @@ cleanupWhenExit() { trap cleanupWhenExit EXIT headInfo() { + local timestamp=$1 colorPrint "0;34;42" ================================================================================ - printf '%s\n' "$(date "+%Y-%m-%d %H:%M:%S.%N") [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" + printf '%s\n' "$timestamp [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" colorPrint "0;34;42" ================================================================================ echo } @@ -567,7 +568,7 @@ printStackOfThreads() { } main() { - local update_round_num + local update_round_num timestamp # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do ((update_round_num > 0)) && { @@ -575,9 +576,10 @@ main() { normalOutput } - [[ -n "$append_file" || -n "$store_dir" ]] && headInfo | + timestamp=$(date "+%Y-%m-%d %H:%M:%S.%N") + [[ -n "$append_file" || -n "$store_dir" ]] && headInfo "$timestamp" | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} >/dev/null - ((update_count != 1)) && headInfo + ((update_count != 1)) && headInfo "$timestamp" if [ "$cpu_sample_interval" = 0 ]; then findBusyJavaThreadsByPs From 52f6a07d29adc9f8fca90471e391e14bf9572ed9 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 9 Mar 2024 17:52:10 +0800 Subject: [PATCH 173/175] build: upgrade `shunit2` lib --- test-cases/shunit2-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib index 3334e530..da1e19de 160000 --- a/test-cases/shunit2-lib +++ b/test-cases/shunit2-lib @@ -1 +1 @@ -Subproject commit 3334e53047ad143669870a9c223b70a81156533a +Subproject commit da1e19de845a77628d9684e609cc0f8160782c68 From 0b4180c81160af750d34469fa50591392b4eb207 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Fri, 16 Feb 2024 20:17:15 +0800 Subject: [PATCH 174/175] =?UTF-8?q?refactor:=20unify=20error=20message=20f?= =?UTF-8?q?ormat=20and=20refactor=20related=20functions=20=E2=84=B9?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - keep `usage` function simple - use `-s`/`-h` option for optional argument of `die` function - use bash builtin `type -P` instead of `which` command 🐚 - rename var, use `COLOR_INDEX` instead of `COUNT` --- bin/a2l | 4 +- bin/ap | 53 ++++++++++------ bin/c | 42 +++++++++---- bin/coat | 18 +++--- bin/cp-into-docker-run | 44 ++++++++----- bin/find-in-jars | 57 ++++++++++------- bin/rp | 52 +++++++++------ bin/show-busy-java-threads | 101 ++++++++++++++++-------------- bin/taoc | 18 +++--- bin/tcp-connection-state-counter | 1 + bin/uq | 51 +++++++++------ bin/xpf | 2 +- bin/xpl | 40 ++++++++---- legacy-bin/cp-svn-url | 4 +- legacy-bin/svn-merge-stop-on-copy | 4 +- test-cases/self-installer.sh | 2 +- 16 files changed, 297 insertions(+), 196 deletions(-) diff --git a/bin/a2l b/bin/a2l index 1eda0b17..203513c9 100755 --- a/bin/a2l +++ b/bin/a2l @@ -72,7 +72,7 @@ readonly args ################################################################################ readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) -COUNT=0 +COLOR_INDEX=0 rotateColorPrint() { local content=$* # - if stdout is a terminal, turn on color output. @@ -82,7 +82,7 @@ rotateColorPrint() { if [[ ! -t 1 || $content =~ ^[[:space:]]*$ ]]; then printf '%s\n' "$content" else - local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} printf '\e[1;%sm%s\e[0m\n' "$color" "$content" fi } diff --git a/bin/ap b/bin/ap index 921f8afd..051f66d9 100755 --- a/bin/ap +++ b/bin/ap @@ -19,23 +19,43 @@ readonly PROG_VERSION='2.x-dev' # util functions ################################################################################ -redPrint() { +errorMsgPrint() { + local errorMsg="$PROG: $*" # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf '\e[1;31m%s\e[0m\n' "$*" + printf '\e[1;31m%s\e[0m\n' "$errorMsg" else - printf '%s\n' "$*" + printf '%s\n' "$errorMsg" fi -} +} >&2 die() { - redPrint "Error: $*" >&2 - exit 1 -} - -# `realpath` command existed on Linux and macOS, return resolved physical path + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && errorMsgPrint "$*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 + +# `realpath` command exists on Linux and macOS, return resolved physical path # - realpath command on macOS do NOT support option `-e`; # combined `[ -e $file ]` to check file existence first. # - How can I get the behavior of GNU's readlink -f on a Mac? @@ -45,14 +65,7 @@ realpath() { } usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - # NOTE: $'foo' is the escape sequence syntax of bash - (($# > 0)) && redPrint "$*"$'\n' >&"$out" - - cat >&"$out" < 0)); do break ;; -*) - usage 2 "$PROG: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) # if not option, treat all follow files as args @@ -113,8 +126,8 @@ has_error=false for f in "${files[@]}"; do realpath "$f" || { - redPrint "error: $f does not exists!" >&2 has_error=true + errorMsgPrint "$f: No such file or directory!" } done diff --git a/bin/c b/bin/c index b36fd495..c0bbbd5a 100755 --- a/bin/c +++ b/bin/c @@ -18,25 +18,43 @@ readonly PROG_VERSION='2.x-dev' # util functions ################################################################################ -printErrorMsg() { +redPrint() { # if stdout is a terminal, turn on color output. # '-t' check: is a terminal? # check isatty in bash https://stackoverflow.com/questions/10022323 if [ -t 1 ]; then - printf '\e[1;31m%s\e[0m\n\n' "Error: $*" + printf '\e[1;31m%s\e[0m\n' "$*" else - printf '%s\n\n' "Error: $*" + printf '%s\n' "$*" fi } -usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - (($# > 0)) && printErrorMsg "$*" >&"$out" +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 - cat >&"$out" < 0)); do break ;; -*) - usage 2 "unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) # if not option, treat all follow arguments as command diff --git a/bin/coat b/bin/coat index 37d20757..b1280722 100755 --- a/bin/coat +++ b/bin/coat @@ -18,12 +18,6 @@ readonly PROG_VERSION='2.x-dev' # parse options ################################################################################ -progVersion() { - printf '%s version: %s\n' "$PROG" "$PROG_VERSION" - printf 'cat executable: %s\n' "$(command -v cat)" - exit -} - usage() { cat <= 0; --idx)); do @@ -58,7 +58,7 @@ unset args idx [ -t 1 ] || exec cat "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) -COUNT=0 +COLOR_INDEX=0 # CAUTION: print content WITHOUT new line rotateColorPrint() { local content=$* @@ -66,7 +66,7 @@ rotateColorPrint() { if [[ $content =~ ^[[:space:]]*$ ]]; then printf %s "$content" else - local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} printf '\e[1;%sm%s\e[0m' "$color" "$content" fi } diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run index cddc245b..7ad6b164 100755 --- a/bin/cp-into-docker-run +++ b/bin/cp-into-docker-run @@ -28,15 +28,34 @@ redPrint() { } die() { - redPrint "Error: $*" >&2 - exit 1 -} + local prompt_help=false exit_staus=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_staus=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_staus" +} >&2 isAbsolutePath() { [[ "$1" =~ ^/ ]] } -# `realpath` command existed on Linux and macOS, return resolved physical path +# `realpath` command exists on Linux and macOS, return resolved physical path # - realpath command on macOS do NOT support option `-e`; # combined `[ -e $file ]` to check file existence first. # - How can I get the behavior of GNU's readlink -f on a Mac? @@ -46,14 +65,7 @@ realpath() { } usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - # NOTE: $'foo' is the escape sequence syntax of bash - (($# > 0)) && redPrint "$*"$'\n' >&"$out" - - cat >&"$out" < 0)); do break ;; -*) - usage 2 "$PROG: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) # if not option, treat all follow arguments as command @@ -153,7 +165,7 @@ done readonly container_name docker_user docker_workdir docker_tmpdir docker_command_cp_path verbose args [ -n "$container_name" ] || - usage 1 "No destination docker container name, specified by option -c/--container!" + die -h "requires destination docker container name, specified by option -c/--container!" if [ -n "$docker_workdir" ]; then isAbsolutePath "$docker_workdir" || @@ -171,7 +183,7 @@ fi # check docker command existence ######################################## -command -v docker &>/dev/null || die 'docker command not found!' +type -P docker &>/dev/null || die 'docker command not found!' ######################################## # prepare vars for docker operation diff --git a/bin/find-in-jars b/bin/find-in-jars index 9a2ef20c..fd00d3fd 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -68,20 +68,32 @@ clearResponsiveMessage() { } die() { - clearResponsiveMessage - redPrint "Error: $*" >&2 - exit 1 -} + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done -usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) + clearResponsiveMessage + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." - # NOTE: $'foo' is the escape sequence syntax of bash - (($# > 0)) && redPrint "$*"$'\n' >&"$out" + exit "$exit_status" +} >&2 - cat >&"$out" < 0)); do break ;; -*) - usage 2 "Error: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) args=(${args[@]:+"${args[@]}"} "$1") @@ -227,13 +239,13 @@ 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[*]}" +((${#args[@]} == 0)) && die -h "requires file pattern!" +((${#args[@]} > 1)) && die -h "more than 1 file pattern: ${args[*]}" readonly pattern=${args[0]} tmp_dirs=() for d in "${dirs[@]}"; do - [ -e "$d" ] || die "file $d(specified by option -d) does not exist!" + [ -e "$d" ] || die "file $d(specified by option -d): No such file or directory!" [ -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!" @@ -263,10 +275,10 @@ __prepareCommandToListZipEntries() { # 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 + if type -P 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 + elif type -P 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 @@ -279,12 +291,12 @@ __prepareCommandToListZipEntries() { 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 + elif type -P 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!" + die "command to list zip entries NOT found : zipinfo, unzip or jar!" fi readonly command_to_list_zip_entries is_use_zip_cmd_to_list_zip_entries @@ -312,7 +324,6 @@ listZipEntries() { "${command_to_list_zip_entries[@]}" "$zip_file" || { clearResponsiveMessage redPrint "fail to list zip entries of $zip_file, ignored!" >&2 - return } } @@ -326,7 +337,7 @@ searchJarFiles() { local jar_files total_jar_count jar_files=$(find "${dirs[@]}" "${find_iname_options[@]}" -type f) - [ -n "$jar_files" ] || die "No ${extensions[*]} file found!" + [ -n "$jar_files" ] || die "${extensions[*]} file NOT found!" total_jar_count=$(printf '%s\n' "$jar_files" | wc -l) # remove white space, because the `wc -l` output on mac contains white space! @@ -380,11 +391,9 @@ __outputResultOfJarFile() { 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" diff --git a/bin/rp b/bin/rp index f44ab089..d4d85210 100755 --- a/bin/rp +++ b/bin/rp @@ -31,9 +31,28 @@ redPrint() { } die() { - redPrint "Error: $*" >&2 - exit 1 -} + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 portableRelPath() { local file=$1 relTo=$2 uname @@ -45,31 +64,24 @@ portableRelPath() { ;; Darwin*) local py_args=(-c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$file" "$relTo") - if command -v grealpath >/dev/null; then + if type -P grealpath >/dev/null; then grealpath "$f" --relative-to="$relTo" - elif command -v python3 >/dev/null; then + elif type -P python3 >/dev/null; then python3 "${py_args[@]}" - elif command -v python >/dev/null; then + elif type -P python >/dev/null; then python "${py_args[@]}" else die "fail to find command(grealpath/python3/python) to get relative path!" fi ;; *) - die "NOT support uname($uname)!" + die "uname($uname) NOT support!" ;; esac } usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - # NOTE: $'foo' is the escape sequence syntax of bash - (($# > 0)) && redPrint "$*"$'\n' >&"$out" - - cat >&"$out" < 0)); do break ;; -*) - usage 2 "$PROG: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) # if not option, treat all follow files as args @@ -120,7 +132,7 @@ while (($# > 0)); do esac done -((${#files[@]} == 0)) && die "NO argument!" +((${#files[@]} == 0)) && die -h "requires at least one argument!" if ((${#files[@]} == 1)); then relativeTo=. @@ -133,7 +145,7 @@ else fi [ -f "$relativeTo" ] && relativeTo=$(dirname -- "$relativeTo") -[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo) does NOT exists!" +[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo): No such file or directory!" readonly files relativeTo @@ -147,7 +159,7 @@ for f in "${files[@]}"; do if [ -e "$f" ]; then portableRelPath "$f" "$relativeTo" else - redPrint "error: $f does not exists!" >&2 + redPrint "$PROG: $f: No such file or directory!" >&2 has_error=true fi done diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 73e78194..c141ad8d 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -85,9 +85,28 @@ blueOutput() { } die() { - redOutput "Error: $*" >&2 - exit 1 -} + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && colorPrint "1;31" "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 logAndRun() { printf '%s\n' "$*" @@ -133,13 +152,7 @@ printCallingCommandLine() { } usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - (($# > 0)) && colorPrint 31 "$*$NL" >&"$out" - - cat >&"$out" </dev/null; then +elif type -P jstack &>/dev/null; then # 3. search jstack under PATH - jstack_path=$(command -v jstack) - [ -x "$jstack_path" ] || die "found $jstack_path from PATH is NOT executable!${NL}Use -s option set jstack path manually." + jstack_path=$(type -P jstack) + [ -x "$jstack_path" ] || die -h "found $jstack_path from PATH is NOT executable!${NL}Use -s option set jstack path manually." else - die "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${NL}Use -s option set jstack path manually." + die -h "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${NL}Use -s option set jstack path manually." fi readonly jstack_path @@ -470,16 +480,14 @@ __top_threadId_cpu() { # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! local result_threads_top_info - result_threads_top_info=$( - printf '%s\n' "$top_out" | awk '{ - # from text line to empty line, increase block index - if (previousLine && !$0) blockIndex++ - # only print 4th text block(blockIndex == 3), aka. process info of second top update - if (blockIndex == 3 && $1 ~ /^[0-9]+$/) - print $1, $9 # $1 is thread id field, $9 is %cpu field - previousLine = $0 - }' - ) + result_threads_top_info=$(printf '%s\n' "$top_out" | awk '{ + # from text line to empty line, increase block index + if (previousLine && !$0) blockIndex++ + # only print 4th text block(blockIndex == 3), aka. process info of second top update + if (blockIndex == 3 && $1 ~ /^[0-9]+$/) + print $1, $9 # $1 is thread id field, $9 is %cpu field + previousLine = $0 + }') [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found printf '%s\n' "$result_threads_top_info" | sort -k2,2nr @@ -501,10 +509,9 @@ __complete_pid_user_by_ps() { ((count <= 0 || idx < count)) || break # output field: pid, threadId, pcpu, user - output_fields=$(printf '%s\n' "$ps_out" | - awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { - print $1, threadId, pcpu, $3; exit - }') + output_fields=$(printf '%s\n' "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { + print $1, threadId, pcpu, $3; exit + }') if [ -n "$output_fields" ]; then ((idx++)) printf '%s\n' "$output_fields" @@ -550,18 +557,18 @@ printStackOfThreads() { if [ -n "$mix_native_frames" ]; then local sed_script="/--------------- $threadId ---------------/,/^---------------/ { - /--------------- $threadId ---------------/b # skip first separator line - /^---------------/d # delete second separator line - p - }" + /--------------- $threadId ---------------/b # skip first separator line + /^---------------/d # delete second separator line + p + }" elif [ -n "$force" ]; then local sed_script="/^Thread $threadId:/,/^$/ { - /^$/d; p # delete end separator line - }" + /^$/d; p # delete end separator line + }" else local sed_script="/ nid=($threadId0x|$threadId) /,/^$/ { - /^$/d; p # delete end separator line - }" + /^$/d; p # delete end separator line + }" fi sed "$sed_script" -n -r "$jstackFile" | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} done diff --git a/bin/taoc b/bin/taoc index e51de9ce..5e5fcfc0 100755 --- a/bin/taoc +++ b/bin/taoc @@ -18,12 +18,6 @@ readonly PROG_VERSION='2.x-dev' # parse options ################################################################################ -progVersion() { - printf '%s version: %s\n' "$PROG" "$PROG_VERSION" - printf 'tac executable: %s\n' "$(command -v tac)" - exit -} - usage() { cat <= 0; --idx)); do @@ -58,7 +58,7 @@ unset args idx [ -t 1 ] || exec tac "$@" readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) -COUNT=0 +COLOR_INDEX=0 # CAUTION: print content WITHOUT new line rotateColorPrint() { local content=$* @@ -66,7 +66,7 @@ rotateColorPrint() { if [[ $content =~ ^[[:space:]]*$ ]]; then printf %s "$content" else - local color=${ROTATE_COLORS[COUNT++ % ${#ROTATE_COLORS[@]}]} + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} printf '\e[1;%sm%s\e[0m' "$color" "$content" fi } diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 104ea6de..dc7dae87 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -29,6 +29,7 @@ Options: -h, --help display this help and exit -V, --version display version information and exit EOF + exit } diff --git a/bin/uq b/bin/uq index 840ea1d3..535f9a85 100755 --- a/bin/uq +++ b/bin/uq @@ -42,9 +42,28 @@ yellowPrint() { } die() { - redPrint "Error: $*" >&2 - exit 1 -} + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 convertHumanReadableSizeToSize() { local human_readable_size=$1 @@ -68,13 +87,7 @@ convertHumanReadableSizeToSize() { } usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - (($# > 0)) && redPrint "$*$NL" >&"$out" - - cat >&"$out" < 0)); do uq_opt_repeated_method=${1#--all-repeated=} [[ $uq_opt_repeated_method = 'none' || $uq_opt_repeated_method = 'prepend' || $uq_opt_repeated_method = 'separate' ]] || - usage 1 "$PROG: invalid argument ‘$uq_opt_repeated_method’ for ‘--all-repeated’${NL}Valid arguments are:$NL - ‘none’$NL - ‘prepend’$NL - ‘separate’" + die -h "invalid argument ‘$uq_opt_repeated_method’ for ‘--all-repeated’${NL}Valid arguments are:$NL - ‘none’$NL - ‘prepend’$NL - ‘separate’" shift ;; @@ -184,7 +197,7 @@ while (($# > 0)); do shift ;; -*) - usage 2 "$PROG: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) argv=(${argv[@]:+"${argv[@]}"} "$1") @@ -194,17 +207,17 @@ while (($# > 0)); do done [[ $uq_opt_only_repeated = 1 && $uq_opt_only_unique = 1 ]] && - usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" + die -h "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" [[ $uq_opt_all_repeated = 1 && $uq_opt_only_unique = 1 ]] && - usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" + die -h "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" [[ $uq_opt_all_repeated = 1 && $uq_opt_repeated_method = none && ($uq_opt_count = 0 && $uq_opt_only_repeated = 0) ]] && - yellowPrint "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 + yellowPrint "WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 # DO NOT declare and assign var uq_max_input_size(as readonly) in ONE line! # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 uq_max_input_size=$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size") || - usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" + die -h "illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" readonly argc=${#argv[@]} argv uq_max_input_size @@ -228,7 +241,7 @@ for f in ${input_files[@]:+"${input_files[@]}"}; do # - is stdin, ok [ "$f" = - ] && continue - [ -e "$f" ] || die "input file $f does not exist!" + [ -e "$f" ] || die "input file $f: No such file or directory!" [ ! -d "$f" ] || die "input file $f exists, but is a directory!" [ -f "$f" ] || die "input file $f exists, but is not a file!" [ -r "$f" ] || die "input file $f exists, but is not readable!" @@ -285,7 +298,7 @@ BEGIN { { total_input_size += length + 1 if (total_input_size > uq_max_input_size) { - printf "[%s] ERROR: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n", + printf "%s: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n", uq_PROG, uq_max_input_human_readable_size > "/dev/stderr" exit(1) } diff --git a/bin/xpf b/bin/xpf index b407f0fb..34af6d91 100755 --- a/bin/xpf +++ b/bin/xpf @@ -14,7 +14,7 @@ set -eEuo pipefail # util functions ################################################################################ -# `realpath` command existed on Linux and macOS, return resolved physical path +# `realpath` command exists on Linux and macOS, return resolved physical path # - realpath command on macOS do NOT support option `-e`; # combined `[ -e $file ]` to check file existence first. # - How can I get the behavior of GNU's readlink -f on a Mac? diff --git a/bin/xpl b/bin/xpl index 2bac215c..392415aa 100755 --- a/bin/xpl +++ b/bin/xpl @@ -27,15 +27,32 @@ redPrint() { fi } -usage() { - local -r exit_code=${1:-0} - (($# > 0)) && shift - local -r out=$(((exit_code != 0) + 1)) - - # NOTE: $'foo' is the escape sequence syntax of bash - (($# > 0)) && redPrint "$*"$'\n' >&"$out" +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 - cat >&"$out" < 0)); do break ;; -*) - usage 2 "$PROG: unrecognized option '$1'" + die -h "unrecognized option '$1'" ;; *) files=(${files[@]:+"${files[@]}"} "$1") @@ -144,7 +160,7 @@ has_error=false for file in "${files[@]}"; do [ -e "$file" ] || { has_error=true - redPrint "$file not existed!" >&2 + redPrint "$PROG: $file: No such file or directory!" >&2 continue } diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 97437e17..d5d15095 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -30,8 +30,8 @@ Options: -h, --help display this help and exit -V, --version display version information and exit EOF - # shellcheck disable=SC2086 - exit $1 + + exit "$1" } progVersion() { diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index 635b2abc..249fa7d2 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -33,8 +33,8 @@ Example: # because http://www.foo.com/project1/branches/feature2 is remote url, # will check out target branch to tmp directory, and prompt confirm for committing to target branch. EOF - # shellcheck disable=SC2086 - exit $1 + + exit "$1" } (($# > 2)) && { diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh index ee0f8f57..801e7a79 100644 --- a/test-cases/self-installer.sh +++ b/test-cases/self-installer.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -if command -v svn &>/dev/null; then +if type -P svn &>/dev/null; then [ ! -d "/tmp/useful-scripts-$USER" ] && svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" fi From 0ca9fed6a6b8f2a21f9b83b565fc191d7e943d74 Mon Sep 17 00:00:00 2001 From: Jerry Lee Date: Sat, 16 Mar 2024 13:13:18 +0800 Subject: [PATCH 175/175] =?UTF-8?q?refactor(`find-in-jars`):=20add=20`IFS?= =?UTF-8?q?=3D`=20for=20`read`,=20more=20robust=20=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit file paths may contain leading spaces --- bin/find-in-jars | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/find-in-jars b/bin/find-in-jars index fd00d3fd..eda51301 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -378,7 +378,7 @@ __outputResultOfJarFile() { # Prevent grep from exiting in case of no match # https://unix.stackexchange.com/questions/330660 grep "${grep_opt_args[@]}" || true - } | while read -r file; do + } | while IFS= read -r file; do clearResponsiveMessage if [ -t 1 ]; then printf "$JAR_COLOR%s$SEP_COLOR%s$COLOR_RESET%s\n" "$jar_file" "$separator" "$file" @@ -394,7 +394,7 @@ findInJarFiles() { local counter=1 total_jar_count jar_file read -r total_jar_count - while read -r jar_file; do + while IFS= read -r jar_file; do printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" listZipEntries "$jar_file" | __outputResultOfJarFile "$jar_file" done