diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..4b2606b8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,23 @@
+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
+
+[*.{py,md,mkd,markdown}]
+indent_size = 4
+
+[*.xml]
+indent_style = tab
+
+[*.{md,mkd,markdown}]
+trim_trailing_whitespace = false
+
+# python files without extension
+[show-duplicate-java-classes]
+indent_size = 4
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
new file mode 100644
index 00000000..2c4b2298
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,32 @@
+# Quickstart for GitHub Actions
+# https://docs.github.com/en/actions/quickstart
+
+name: CI
+on: [ push, pull_request, workflow_dispatch ]
+
+jobs:
+
+ test:
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 5
+ strategy:
+ matrix:
+ os: [ ubuntu-latest, macos-11, macos-latest ]
+ fail-fast: false
+ max-parallel: 64
+ name: Test on ${{ matrix.os }}
+
+ steps:
+ - uses: actions/checkout@v4
+ 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: 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
new file mode 100644
index 00000000..26788dce
--- /dev/null
+++ b/.github/workflows/lint.yaml
@@ -0,0 +1,23 @@
+# 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@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)" ]
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a406f269
Binary files /dev/null and b/.gitignore differ
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/README.md b/README.md
index fdde70ef..32a6764d 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,52 @@
-🐌 useful-scripts
-====================================
+#

-
+
+
+
+
+
+
+
+
+
+
-[](https://www.apache.org/licenses/LICENSE-2.0.html)
-[](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-[](https://github.com/oldratlee/useful-scripts/releases)
-[](https://github.com/oldratlee/useful-scripts/stargazers)
-[](https://github.com/oldratlee/useful-scripts/fork)
+🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc.
+👉 平时有用的手动操作做成脚本,以便捷地使用,让开发的日常生活更轻松些。 💕
-👉 把平时有用的手动操作做成脚本,这样可以便捷的使用。 ✨
+欢迎 👏 💖
-有自己用的好的脚本 或是 平时常用但没有写成脚本的功能,欢迎提供([提交Issue](https://github.com/oldratlee/useful-scripts/issues))和分享([Fork后提交代码](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)
-PS:
+本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。
+
+如果你的公司有部署使用,欢迎使用通过 [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)
+
+
-本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。
-如果你的公司有部署使用,欢迎使用通过[提交Issue](https://github.com/oldratlee/useful-scripts/issues)告知,方便互相交流反馈~ 💘
+----------------------
🔰 快速下载&使用
----------------------
@@ -34,40 +63,120 @@ 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`操作,优化命令行与其它应用之间的操作流。
-1. [coat](docs/shell.md#-coat)
- 彩色`cat`出文件行,方便人眼区分不同的行。
+ 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。
+1. [coat and taoc](docs/shell.md#-coat)
+ 彩色`cat`/`tac`出文件行,方便人眼区分不同的行。
1. [a2l](docs/shell.md#-a2l)
- 按行彩色输出参数,方便人眼查看。
+ 按行彩色输出参数,方便人眼查看。
+1. [uq](docs/shell.md#-uq)
+ 不重排序输入完成整个输入行的去重。相比系统的`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`连接状态的个数。用于方便排查系统连接负荷问题。
+ 统计各个`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)
目前`VCS`的脚本都是`svn`分支相关的操作。使用更现代的`Git`吧! 💥
因为不推荐使用`svn`,这里不再列出有哪些脚本了,如果你有兴趣可以点上面链接去看。
+
+## 🎓 Developer Guide
+
+为用户提供有用的功能,当然是这个库的首要的价值体现和存在理由。
+
+但作为一个**开源**项目,每个人都可以看到源码实现,这个库或许能做得更多。
+
+### 🎯 面向开发者的目标
+
+- 将`Shell/Bash`作为线上生产环境使用的专业编程语言。
+- 期望体现`Shell/Bash`脚本 生产环境级的严谨开发方式与最佳实践,进而有可能示例与改善在生产环境中`Shell`脚本的质量状况。
+
+PS:
+
+- 虽然上面是自己期望的目标,但自己在`Shell`语言上一定会有很多理解和使用上的问题、在这些实现脚本中也会很多需要的改进,可以一起学习、讨论与实践~ 💕
+- 这个库中脚本的实现也有使用`Python`。
+
+#### 关于`Shell`脚本
+
+命令行(`CLI`)几乎是每个程序员每天都在使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。
+
+命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常会看到`Shell`脚本(虽然不如主流语言那么常见)。
+
+可能正因为上面所说的`Shell`脚本的便利性和大众性:
+
+- `Shell`脚本有不少是顺手实现的(包括生产环境用的`Shell`脚本);
+- `Shell`脚本的实现常常可能质量不高,会引发线上严重的故障。
+
+### 🚦 开发约定
+
+在这个库中的`Shell`脚本:
+
+- 统一使用`Bash 3.2+`;
+- 面向生产环境,尽可能使用严谨安全的开发方式。
+
+`Shell`用`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`有各种差异,深坑勿入。
+- 个人系统学习过的是`Bash`,比较理解熟悉。
+
+PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/),但在严谨的`Shell`脚本开发时还是使用`Bash`。
+
+### 📚 `Shell`学习与开发的资料
+
+> 更多资料参见 [子文档](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/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`最佳实践与安全编程**文章
+ - [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)
+- 🎶 **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完全不能替代系统学习才能真正理解并专业开发!
+ - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/)
+ 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4`
+ - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版
+ - 官方资料
+ - [`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.
+ - [命令行的艺术 - `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/bin/a2l b/bin/a2l
index caca085b..203513c9 100755
--- a/bin/a2l
+++ b/bin/a2l
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
-# echo each arguments on one line colorfully.
+# print each arguments on one line colorfully.
#
# @Usage
# $ ./a2l arg1 arg2
@@ -8,23 +8,85 @@
#
# @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
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-# NOTE: $'foo' is the escape sequence syntax of bash
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
+################################################################################
+# parse options
+################################################################################
-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 "$@"
+usage() {
+ cat < 0)); do
+ case "$1" in
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ args=(${args[@]:+"${args[@]}"} "$@")
+ break
+ ;;
+ -*)
+ # if unrecognized option, treat it and all follow arguments as args
+ args=(${args[@]:+"${args[@]}"} "$@")
+ break
+ ;;
+ *)
+ # if not option, treat it and all follow arguments as args
+ args=(${args[@]:+"${args[@]}"} "$@")
+ break
+ ;;
+ esac
+done
+readonly args
+
+################################################################################
+# biz logic
+################################################################################
+
+readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34)
+COLOR_INDEX=0
+rotateColorPrint() {
+ local content=$*
+ # - if stdout is a terminal, turn on color output.
+ # '-t' check: is a terminal?
+ # check isatty in bash https://stackoverflow.com/questions/10022323
+ # - skip color for white space
+ if [[ ! -t 1 || $content =~ ^[[:space:]]*$ ]]; then
+ printf '%s\n' "$content"
+ else
+ local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]}
+ printf '\e[1;%sm%s\e[0m\n' "$color" "$content"
+ fi
+}
-for a; do
- colorEcho "${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" "$a"
+for a in ${args[@]:+"${args[@]}"}; do
+ rotateColorPrint "$a"
done
diff --git a/bin/ap b/bin/ap
index 925e4d52..051f66d9 100755
--- a/bin/ap
+++ b/bin/ap
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# convert to Absolute Path.
#
@@ -10,13 +10,126 @@
#
# @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=("$@")
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-for f in "${files[@]}" ; do
- ! [ -e "$f" ] && {
- echo "$f does not exists!"
- continue
- }
- readlink -f "$f"
+################################################################################
+# util functions
+################################################################################
+
+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' "$errorMsg"
+ else
+ printf '%s\n' "$errorMsg"
+ fi
+} >&2
+
+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)) && 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?
+# https://stackoverflow.com/questions/1055671
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+usage() {
+ cat < 0)); do
+ case "$1" in
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ files=(${files[@]:+"${files[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ # if not option, treat all follow files as args
+ files=(${files[@]:+"${files[@]}"} "$@")
+ break
+ ;;
+ esac
done
+
+# if files is empty, use "."
+readonly files=("${files[@]:-.}")
+
+################################################################################
+# biz logic
+################################################################################
+
+has_error=false
+
+for f in "${files[@]}"; do
+ realpath "$f" || {
+ has_error=true
+ errorMsgPrint "$f: No such file or directory!"
+ }
+done
+
+# set exit status
+! $has_error
diff --git a/bin/c b/bin/c
index e18e11e0..c0bbbd5a 100755
--- a/bin/c
+++ b/bin/c
@@ -1,44 +1,82 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# 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)
+set -eEuo pipefail
-set -e
-set -o pipefail
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-readonly PROG="`basename "$0"`"
+################################################################################
+# util functions
+################################################################################
-usage() {
- local -r exit_code="$1"
- shift
- [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+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
+}
- (( $# > 0 )) && { echo "$@"; echo; } > $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
- > $out cat < 0)); do
+ case "$1" in
+ -k | --keep-eol)
+ keep_eol=true
+ shift
+ ;;
+ -q | --quiet)
+ quiet=true
+ shift
+ ;;
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ target_command=(${target_command[@]:+"${target_command[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ # if not option, treat all follow arguments as command
+ target_command=(${target_command[@]:+"${target_command[@]}"} "$@")
+ break
+ ;;
+ esac
done
+readonly keep_eol quiet target_command
+
+if ((${#target_command[@]} > 0)) && ! type -P "${target_command[0]}" &>/dev/null; then
+ die "command '${target_command[0]}' not found on PATH"
+fi
+
################################################################################
# biz logic
################################################################################
-copy() {
- case "`uname`" in
- Darwin*)
- pbcopy ;;
- CYGWIN*|MINGW*)
- clip ;;
- *)
- xsel -b ;;
- esac
+systemClip() {
+ case "$(uname)" in
+ Darwin*)
+ pbcopy
+ ;;
+ CYGWIN* | MINGW*)
+ clip
+ ;;
+ *)
+ xsel -b
+ ;;
+ esac
+}
+
+bufferCopy() {
+ local content
+ content=$(cat)
+ if $keep_eol; then
+ printf '%s\n' "$content"
+ else
+ printf %s "$content"
+ fi | systemClip
}
teeAndCopy() {
- $quiet && local out=/dev/null || local out=/dev/stdout
- tee >(
- content="$(cat)"
- echo $eol "$content" | copy
- ) > $out
+ if $quiet; then
+ bufferCopy
+ else
+ tee >(bufferCopy)
+ fi
}
-if [ ${#args[@]} -eq 0 ]; then
- teeAndCopy
+if ((${#target_command[@]} == 0)); then
+ teeAndCopy
else
- "${args[@]}" | teeAndCopy
+ command "${target_command[@]}" | teeAndCopy
fi
diff --git a/bin/coat b/bin/coat
index 6ab5e1a7..b1280722 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*.
#
@@ -9,26 +9,88 @@
#
# @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
-set -e
-set -o pipefail
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-# if not in console, use cat directly
-# check isatty in bash https://stackoverflow.com/questions/10022323
-[ ! -t 1 ] && exec cat "$@"
+################################################################################
+# parse options
+################################################################################
+
+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
+[ -t 1 ] || exec cat "$@"
+
+readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34)
+COLOR_INDEX=0
+# CAUTION: print content WITHOUT new line
+rotateColorPrint() {
+ local content=$*
+ # skip color for white space
+ if [[ $content =~ ^[[:space:]]*$ ]]; then
+ printf %s "$content"
+ else
+ local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]}
+ printf '\e[1;%sm%s\e[0m' "$color" "$content"
+ fi
+}
+
+rotateColorPrintln() {
+ # NOTE: $'foo' is the escape sequence syntax of bash
+ rotateColorPrint "$*"$'\n'
+}
+
+colorLines() {
+ local line
+ # Bash read line does not read leading spaces
+ # https://stackoverflow.com/questions/29689172
+ while IFS= read -r line; do
+ 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
+ [ -z "$line" ] || rotateColorPrint "$line"
+}
+
+if (($# == 0)); then
+ colorLines
+else
+ cat "$@" | colorLines
+fi
diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run
new file mode 100755
index 00000000..7ad6b164
--- /dev/null
+++ b/bin/cp-into-docker-run
@@ -0,0 +1,250 @@
+#!/usr/bin/env bash
+# @Function
+# 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
+#
+# @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
+
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-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
+}
+
+die() {
+ 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 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?
+# https://stackoverflow.com/questions/1055671
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+usage() {
+ cat < 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
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ # if not option, treat all follow arguments 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" ] ||
+ die -h "requires 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"
+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
+
+################################################################################
+# biz logic
+################################################################################
+
+########################################
+# check docker command existence
+########################################
+
+type -P 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
+ type -P "$specified_run_command" &>/dev/null ||
+ die "specified command not exists and not found in PATH: $specified_run_command"
+
+ run_command=$(type -P "$specified_run_command")
+fi
+
+run_command=$(realpath "$run_command")
+readonly run_command run_command_base_name=${run_command##*/}
+
+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
+ 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 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
+
+ # remove tmp dir in docker by root user
+ docker exec "$container_name" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null
+}
+trap cleanupWhenExit EXIT
+
+########################################
+# docker operations
+########################################
+
+logAndRun() {
+ $verbose && printf '%s\n' "[$PROG] $*" >&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/bin/echo-args b/bin/echo-args
index 6bb2f661..9186a6e3 100755
--- a/bin/echo-args
+++ b/bin/echo-args
@@ -1,25 +1,41 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# print arguments in human and debugging friendly style.
#
# @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
+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"
+}
-echoArg() {
- local index="$1" count="$2" value="$3"
+digit_count=$(digitCount $#)
+readonly arg_count=$# digit_count
- # 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]"
-}
+readonly RED='\e[1;31m' BLUE='\e[1;36m' COLOR_RESET='\e[0m'
+printArg() {
+ local idx=$1 value=$2
+ # 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}]${COLOR_RESET}\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"
+for a; do
+ printArg $((idx++)) "$a"
done
diff --git a/bin/find-in-jars b/bin/find-in-jars
index 83e6ebe1..eda51301 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).
#
@@ -13,76 +13,101 @@
#
# @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=${0##*/}
+readonly PROG_VERSION='2.x-dev'
################################################################################
# util functions
################################################################################
-[ -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
-
-redEcho() {
- $is_console && echo "$ec[1;31m$@$eend" || echo "$@"
+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" "$*"
+ else
+ printf '%s\n' "$*"
+ fi
}
-die() {
- redEcho "Error: $@" 1>&2
- exit 1
-}
+# 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
-$is_console && readonly columns=$(stty size | awk '{print $2}')
+[ -t 2 ] && COLUMNS=$(stty size | awk '{print $2}')
printResponsiveMessage() {
- $is_console || return
+ 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 "${message:0:columns}"
+ local content=$*
+ # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html
+ printf %b%s "$LINE_CLEAR" "${content:0:COLUMNS}" >&2
}
clearResponsiveMessage() {
- $is_console || return
-
- # 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"
+ if ! $show_responsive || [ ! -t 2 ]; then
+ return
+ fi
+
+ printf %b "$LINE_CLEAR" >&2
}
-usage() {
- local -r exit_code="$1"
- shift
- [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+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 )) && { echo "$@"; echo; } > $out
+ clearResponsiveMessage
+ (($# > 0)) && redPrint "$PROG: $*"
+ $prompt_help && echo "Try '$PROG --help' for more information."
+
+ exit "$exit_status"
+} >&2
+
+usage() {
+ cat < $out cat < '
+ $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.
@@ -100,155 +125,281 @@ Output control:
-a, --absolute-path always print absolute path of jar file
-s, --separator specify the separator between jar file and zip entry.
default is \`!'.
+ -L, --files-not-contained-found
+ print only names of JAR FILEs NOT contained found
+ -l, --files-contained-found
+ print only names of JAR FILEs contained found
+ -R, --no-find-progress do not display responsive find progress
Miscellaneous:
-h, --help display this help and exit
+ -V, --version display version information and exit
EOF
- exit $1
+ exit
+}
+
+progVersion() {
+ printf '%s\n' "$PROG $PROG_VERSION"
+ exit
}
################################################################################
# parse options
################################################################################
-declare -a args=()
-declare -a dirs=()
-while (( $# > 0 )); do
- case "$1" in
- -d|--dir)
- dirs=("${dirs[@]}" "$2")
- shift 2
- ;;
- -e|--extension)
- extension=("${extension[@]}" "$2")
- shift 2
- ;;
- # support the typo option --seperator for compatibility
- -s|--separator|--seperator)
- separator="$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
- ;;
- -h|--help)
- usage
- ;;
- --)
- shift
- args=("${args[@]}" "$@")
- break
- ;;
- -*)
- usage 2 "${PROG}: unrecognized option '$1'"
- ;;
- *)
- args=("${args[@]}" "$1")
- shift
- ;;
- esac
+dirs=()
+extensions=()
+args=()
+
+separator='!'
+regex_mode=-E
+use_absolute_path=false
+show_responsive=true
+only_print_file_name=false
+
+while (($# > 0)); do
+ case "$1" in
+ -d | --dir)
+ dirs=(${dirs[@]:+"${dirs[@]}"} "$2")
+ shift 2
+ ;;
+ -e | --extension)
+ extensions=(${extensions[@]:+"${extensions[@]}"} "$2")
+ shift 2
+ ;;
+ -E | --extended-regexp)
+ regex_mode=-E
+ shift
+ ;;
+ -F | --fixed-strings)
+ regex_mode=-F
+ shift
+ ;;
+ -G | --basic-regexp)
+ regex_mode=-G
+ shift
+ ;;
+ -P | --perl-regexp)
+ regex_mode=-P
+ shift
+ ;;
+ -i | --ignore-case)
+ ignore_case_option=-i
+ shift
+ ;;
+ -a | --absolute-path)
+ use_absolute_path=true
+ shift
+ ;;
+ # support the legacy typo option name --seperator for compatibility
+ -s | --separator | --seperator)
+ separator=$2
+ shift 2
+ ;;
+ -L | --files-not-contained-found)
+ only_print_file_name=true
+ print_matched_files=false
+ shift
+ ;;
+ -l | --files-contained-found)
+ only_print_file_name=true
+ print_matched_files=true
+ shift
+ ;;
+ -R | --no-find-progress)
+ show_responsive=false
+ shift
+ ;;
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ args=(${args[@]:+"${args[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ args=(${args[@]:+"${args[@]}"} "$1")
+ shift
+ ;;
+ esac
done
-dirs=${dirs:-.}
-extension=${extension:-jar}
-regex_mode=${regex_mode:--E}
+readonly separator regex_mode ignore_case_option use_absolute_path only_print_file_name print_matched_files show_responsive args
-use_absolute_path=${use_absolute_path:-false}
-separator="${separator:-!}"
+# shellcheck disable=SC2178
+dirs=${dirs:-.}
+# shellcheck disable=SC2178
+readonly extensions=${extensions:-jar}
-(( "${#args[@]}" == 0 )) && usage 1 "No find file pattern!"
-(( "${#args[@]}" > 1 )) && usage 1 "More than 1 file pattern: ${args[@]}"
-readonly pattern="${args[0]}"
+((${#args[@]} == 0)) && die -h "requires file pattern!"
+((${#args[@]} > 1)) && die -h "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!"
- [ -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)" )
+ [ -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!"
+
+ # 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[@]}")
+readonly dirs
+unset d 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=(${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!
################################################################################
-# `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 which zipinfo &> /dev/null; then
- readonly command_for_list_zip='zipinfo -1'
-elif which unzip &> /dev/null; then
- readonly command_for_list_zip='unzip -Z1'
-else
- 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!"
- 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 type -P zipinfo &>/dev/null; then
+ command_to_list_zip_entries=(zipinfo -1)
+ is_use_zip_cmd_to_list_zip_entries=true
+ 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
+ # 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_for_list_zip='jar tf'
-fi
+ is_use_zip_cmd_to_list_zip_entries=false
+ 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 "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
+}
+__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
+ redPrint "fail to list zip entries of $zip_file, ignored: $msg" >&2
+ fi
+ return
+ }
+ fi
+
+ "${command_to_list_zip_entries[@]}" "$zip_file" || {
+ clearResponsiveMessage
+ redPrint "fail to list zip entries of $zip_file, ignored!" >&2
+ }
+}
################################################################################
# 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!"
+searchJarFiles() {
+ printResponsiveMessage "searching jars under dir ${dirs[*]} , ..."
-findInJarFiles() {
- $is_console && local -r grep_color_option='--color=always'
+ local jar_files total_jar_count
- local counter=1 jar_file
- while read jar_file; do
- printResponsiveMessage "finding in jar($((counter++))/$total_count): $jar_file"
+ jar_files=$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)
+ [ -n "$jar_files" ] || die "${extensions[*]} file NOT found!"
- $command_for_list_zip "${jar_file}" |
- grep $regex_mode $ignore_case_option $grep_color_option -- "$pattern" |
- while read file; do
- clearResponsiveMessage
+ 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:]]/}
- $is_console &&
- echo "$ec[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" ||
- echo "${jar_file}${separator}${file}"
- done
+ echo "$total_jar_count"
+ printf '%s\n' "$jar_files"
+}
- clearResponsiveMessage
+readonly JAR_COLOR='\e[1;35m' SEP_COLOR='\e[1;32m'
+__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
+ # 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
+ grep -c "${grep_opt_args[@]}" &>/dev/null && matched=true
+
+ [ "$print_matched_files" != "$matched" ] && return
+
+ clearResponsiveMessage
+ if [ -t 1 ]; then
+ printf "$JAR_COLOR%s$COLOR_RESET\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
+ grep "${grep_opt_args[@]}" || true
+ } | 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"
+ else
+ printf '%s\n' "$jar_file$separator$file"
+ fi
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 IFS= read -r jar_file; do
+ printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file"
+ listZipEntries "$jar_file" | __outputResultOfJarFile "$jar_file"
+ done
+
+ clearResponsiveMessage
}
-echo "$jar_files" | findInJarFiles
+searchJarFiles | findInJarFiles
diff --git a/bin/rp b/bin/rp
index efb69ee6..d4d85210 100755
--- a/bin/rp
+++ b/bin/rp
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# convert to Relative Path.
#
@@ -10,30 +10,159 @@
#
# @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
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-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
}
-[ $# -eq 1 ] && {
- relativeTo=.
- files=("$@")
-} || {
- argv=("$@")
- argc=$#
+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
+
+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 type -P grealpath >/dev/null; then
+ grealpath "$f" --relative-to="$relTo"
+ elif type -P python3 >/dev/null; then
+ python3 "${py_args[@]}"
+ 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 "uname($uname) NOT support!"
+ ;;
+ esac
+}
+
+usage() {
+ cat < 0)); do
+ case "$1" in
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ files=(${files[@]:+"${files[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ # if not option, treat all follow files as args
+ files=(${files[@]:+"${files[@]}"} "$@")
+ break
+ ;;
+ esac
done
+
+((${#files[@]} == 0)) && die -h "requires at least one argument!"
+
+if ((${#files[@]} == 1)); then
+ relativeTo=.
+else
+ argc=${#files[@]}
+
+ # Get last argument
+ relativeTo=${files[argc - 1]}
+ files=("${files[@]:0:argc-1}")
+fi
+
+[ -f "$relativeTo" ] && relativeTo=$(dirname -- "$relativeTo")
+[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo): No such file or directory!"
+
+readonly files relativeTo
+
+################################################################################
+# biz logic
+################################################################################
+
+has_error=false
+
+for f in "${files[@]}"; do
+ if [ -e "$f" ]; then
+ portableRelPath "$f" "$relativeTo"
+ else
+ redPrint "$PROG: $f: No such file or directory!" >&2
+ has_error=true
+ fi
+done
+
+# set exit status
+! $has_error
diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads
index 6b204989..c141ad8d 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.
#
@@ -9,97 +9,163 @@
# @author Jerry Lee (oldratlee at gmail dot com)
# @author superhj1987 (superhj1987 at 126 dot com)
-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 PROG=${0##*/}
+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?
+# 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]}" "$@")
+# 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(as readonly) in ONE line!
+# more info see https://github.com/koalaman/shellcheck/wiki/SC2155
+WHOAMI=$(whoami)
+UNAME=$(uname)
+readonly WHOAMI UNAME
################################################################################
# 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
+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
+}
- # if stdout is console, turn on color output.
- [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@"
+__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"
}
-colorPrint() {
- local color=$1
- shift
+colorOutput() {
+ local color=$1
+ 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"
+ colorPrint "$color" "$*"
+ __appendToFile "$*"
}
-normalPrint() {
- echo "$@"
- [ -n "$append_file" -a -w "$append_file" ] && echo "$@" >> "$append_file"
- [ -n "$store_dir" -a -w "$store_dir" ] && echo "$@" >> "${store_file_prefix}$PROG"
+# shellcheck disable=SC2120
+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
- 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() {
- echo "$@"
- echo
- "$@"
+ printf '%s\n' "$*"
+ echo
+ "$@"
}
logAndCat() {
- echo "$@"
- echo
- cat
+ printf '%s\n' "$*"
+ echo
+ cat
}
-usage() {
- local -r exit_code="$1"
- shift
- [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+# Bash RegEx to check floating point numbers from user input
+# https://stackoverflow.com/questions/13790763
+isNonNegativeFloatNumber() {
+ [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]]
+}
- (( $# > 0 )) && { echo "$@"; echo; } > $out
+isNaturalNumber() {
+ [[ "$1" =~ ^[+]?[0-9]+$ ]]
+}
- > $out cat < find out the highest cpu consumed threads from
+ -p, --pid find out the highest cpu consumed threads from
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.
@@ -116,342 +182,418 @@ 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.
+ -V, --version display version information and exit.
EOF
- exit $exit_code
+ exit
+}
+
+progVersion() {
+ printf '%s\n' "$PROG $PROG_VERSION"
+ exit
}
################################################################################
-# Check os support
+# check os support
################################################################################
-uname | grep '^Linux' -q || die "$PROG only support Linux, not support `uname` yet!"
+[[ $UNAME = Linux* ]] || die "only support Linux, not support $UNAME yet!"
################################################################################
# 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
-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 -- "$@"`
-[ $? -ne 0 ] && { echo; usage 1; }
-eval set -- "${ARGS}"
+# 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 \
+ -- "$@"
+) || die -h
+eval set -- "$ARGS"
+unset ARGS
+
+count=5
+cpu_sample_interval=0.5
while true; do
- case "$1" in
- -c|--count)
- count="$2"
- shift 2
- ;;
- -p|--pid)
- pid="$2"
- shift 2
- ;;
- -a|--append-file)
- append_file="$2"
- shift 2
- ;;
- -s|--jstack-path)
- jstack_path="$2"
- shift 2
- ;;
- -S|--store-dir)
- store_dir="$2"
- shift 2
- ;;
- -P|--use-ps)
- use_ps=true
- shift
- ;;
- -d|--top-delay)
- top_delay="$2"
- shift 2
- ;;
- -F|--force)
- force=-F
- shift
- ;;
- -m|--mix-native-frames)
- mix_native_frames=-m
- shift
- ;;
- -l|--lock-info)
- more_lock_info=-l
- shift
- ;;
- -h|--help)
- usage
- ;;
- --)
- shift
- break
- ;;
- esac
+ case "$1" in
+ -c | --count)
+ count=$2
+ shift 2
+ ;;
+ -p | --pid)
+ pid_list=$2
+ shift 2
+ ;;
+ -a | --append-file)
+ append_file=$2
+ shift 2
+ ;;
+ -s | --jstack-path)
+ jstack_path=$2
+ shift 2
+ ;;
+ -S | --store-dir)
+ store_dir=$2
+ shift 2
+ ;;
+ # support the legacy option name -P,--use-ps for compatibility
+ -P | --use-ps)
+ cpu_sample_interval=0
+ shift
+ ;;
+ # support the legacy option name -d,--top-delay for compatibility
+ -i | --cpu-sample-interval | -d | --top-delay)
+ cpu_sample_interval=$2
+ shift 2
+ ;;
+ -F | --force)
+ force=-F
+ shift
+ ;;
+ -m | --mix-native-frames)
+ mix_native_frames=-m
+ shift
+ ;;
+ -l | --lock-info)
+ lock_info=-l
+ shift
+ ;;
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ break
+ ;;
+ esac
done
-count=${count:-5}
+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!"
-update_delay=${1:-0}
[ -z "$1" ] && update_count=1 || update_count=${2:-0}
-(( update_count < 0 )) && update_count=0
+isNaturalNumber "$update_count" || die "update count($update_count) is not a natural number!"
+readonly update_count
-top_delay=${top_delay:-0.5}
-use_ps=${use_ps:-false}
+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 exsit.
+# check the directory of append-file(-a) mode, create if not existed.
if [ -n "$append_file" ]; then
- if [ -e "$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!"
+ if [ -e "$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")
+ 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!"
else
- 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!"
- else
- mkdir -p "$append_file_dir" || die "fail to create directory $append_file_dir(specified by option -a, for storing run output files)!"
- fi
+ mkdir -p "$append_file_dir" || die "fail to create directory $append_file_dir(specified by option -a, for storing run output files)!"
fi
+ fi
fi
+readonly append_file
-# check store directory(-S) mode, create directory if not exsit.
+# check store directory(-S) mode, create directory if not existed.
if [ -n "$store_dir" ]; then
- if [ -e "$store_dir" ]; then
- [ -d "$store_dir" ] || die "$store_dir(specified by option -S, for storing output files) exists but is not a directory!"
- [ -w "$store_dir" ] || die "directory $store_dir(specified by option -S, for storing output files) exists but is not writable!"
- else
- mkdir -p "$store_dir" || die "fail to create directory $store_dir(specified by option -S, for storing output files)!"
- fi
+ if [ -e "$store_dir" ]; then
+ [ -d "$store_dir" ] || die "$store_dir(specified by option -S, for storing output files) exists but is not a directory!"
+ [ -w "$store_dir" ] || die "directory $store_dir(specified by option -S, for storing output files) exists but is not writable!"
+ else
+ 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!"
################################################################################
-# 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 executalbe!"
-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."
+ # 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 -h "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 -h "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 type -P jstack &>/dev/null; then
+ # 3. search jstack under PATH
+ 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 -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
################################################################################
# biz logic
################################################################################
-readonly run_timestamp="`date "+%Y-%m-%d_%H:%M:%S.%N"`"
-readonly uuid="${PROG}_${run_timestamp}_${RANDOM}_$$"
+# 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}"
-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}_"
+ 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
+trap cleanupWhenExit EXIT
headInfo() {
- colorEcho "0;34;42" ================================================================================
- echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$(( i + 1 ))/$update_count]: ${COMMAND_LINE[@]}"
- colorEcho "0;34;42" ================================================================================
- echo
+ local timestamp=$1
+ colorPrint "0;34;42" ================================================================================
+ printf '%s\n' "$timestamp [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)"
+ colorPrint "0;34;42" ================================================================================
+ 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"
+ 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
+}
+
# 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`
- # 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)
- 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"
- 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!
+ # 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
+
+ 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"
+ fi
+
+ if ((count > 0)); then
+ printf '%s\n' "$ps_out" | head -n "$count"
+ else
+ printf '%s\n' "$ps_out"
+ fi
}
# top with output field: thread id, %cpu
__top_threadId_cpu() {
- # 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 $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"
- 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
- }' | sort -k2,2nr
+ # 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 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
+ 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=$(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
}
__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)
- 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"
+ # 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
+ 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
+ while read -r threadId pcpu; do
+ ((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
+ }')
+ if [ -n "$output_fields" ]; then
+ ((idx++))
+ printf '%s\n' "$output_fields"
fi
-
- local idx=0 threadId pcpu
- while read threadId pcpu ; do
- (( count <= 0 || idx < count )) || break
-
- # output field: pid, threadId, pcpu, user
- local 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++ ))
- echo "$output_fields"
- fi
- done
+ 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
- while read pid threadId pcpu user ; do
- local threadId0x="0x`printf %x ${threadId}`"
-
- (( 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} )
- 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 ${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
- 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}:/,/^$/ {
- /^$/d; p # delete end separator line
- }"
- 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
+ local idx=0 pid threadId pcpu user threadId0x
+ while read -r pid threadId pcpu user; do
+ printf -v threadId0x '%#x' "$threadId"
+
+ ((idx++ > 0)) && normalOutput
+ 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" = "$WHOAMI" ]; 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
+ 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($WHOAMI), 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)."
+ rm "$jstackFile" &>/dev/null
+ continue
+ }
+ }
+
+ 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 ---------------/,/^---------------/ {
+ /--------------- $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
+ }"
+ else
+ local sed_script="/ nid=($threadId0x|$threadId) /,/^$/ {
+ /^$/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
}
-################################################################################
-# Main
-################################################################################
-
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"
-
- [ -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
-
- if $use_ps; then
- findBusyJavaThreadsByPs
- else
- findBusyJavaThreadsByTop
- fi | printStackOfThreads
- done
+ 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)) && {
+ sleep "$update_delay"
+ normalOutput
+ }
+
+ 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 "$timestamp"
+
+ if [ "$cpu_sample_interval" = 0 ]; then
+ findBusyJavaThreadsByPs
+ else
+ findBusyJavaThreadsByTop
+ fi | printStackOfThreads
+ done
}
main
diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes
index e49c78aa..942ad2e9 100755
--- a/bin/show-duplicate-java-classes
+++ b/bin/show-duplicate-java-classes
@@ -1,12 +1,15 @@
-#!/usr/bin/env python
+#!/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
+# $ 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 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
# @author tg123 (farmer1992 at gmail dot com)
@@ -14,99 +17,350 @@
__author__ = 'tg123'
+import os
+import re
+import sys
from glob import glob
-from os import walk
-from zipfile import ZipFile
-from os.path import relpath, isdir
+from io import BytesIO
from optparse import OptionParser
+from os import walk
+from os.path import exists, isdir, relpath
+from zipfile import BadZipfile, ZipFile
+
+################################################################################
+# utils functions
+################################################################################
+PROG_VERSION = '2.x-dev'
+
+# 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'
+__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_columns_of_stderr()
+ if columns <= 0:
+ return
+
+ print(__clear_line + msg[:columns], end='', file=sys.stderr)
+
+
+def clear_responsive_message():
+ if not __show_responsive or not sys.stderr.isatty():
+ return
+ print(__clear_line, end='', file=sys.stderr)
+
+def print_error(msg):
+ clear_responsive_message()
+ print(msg, file=sys.stderr)
-def list_jar_file_under_lib_dirs(libs):
+
+def print_box_message(msg):
+ print()
+ print('=' * 80)
+ print(msg)
+ print('=' * 80)
+
+
+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()
- for lib in libs:
- if isdir(lib):
- jar_files |= {f for f in glob(lib + '/*.jar')}
- else:
- jar_files.add(lib)
+
+ 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' % (
+ 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)
+ continue
+
+ 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
-def list_class_under_jar_file(jar_file):
- return {f for f in ZipFile(jar_file).namelist() if f.lower().endswith('.class')}
+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
+ index_marker = ''
+ if recursive:
+ index_marker = ' #%3s' % index
+ print_responsive_message('list class under jar file(%*s/%s%s): %s' % (
+ str_len(progress[1]), progress[0], progress[1], index_marker, jar_jar_path))
-def list_class_under_class_dir(class_dir):
- return {relpath(dir_path + "/" + filename, class_dir)
+ ret = {}
+ classes = {f for f in zf.namelist() if f.lower().endswith('.class')}
+ 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:
+ next_jar_jar_path = jar_jar_path + '!' + jar
+ try:
+ with ZipFile(BytesIO(zf.read(jar))) as f:
+ ret.update(list_zip_in_zip(next_jar_jar_path, f))
+ except BadZipfile as e:
+ print_error('WARN: %s is bad zip file(%s), ignored!' % (next_jar_jar_path, e))
+
+ return ret
+
+ try:
+ with ZipFile(jar_file) as zip_file:
+ return list_zip_in_zip(jar_file, zip_file)
+ except BadZipfile as error:
+ print_error('WARN: %s is bad zip file(%s), ignored!' % (jar_file, error))
+ return {}
+
+
+def list_class_under_class_dir(class_dir, progress):
+ print_responsive_message('list class under class dir(%*s/%s): %s' % (
+ 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)
+ 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')}
-def expand_2_class_path(jar_files, class_dirs):
- java_class_2_class_paths = {}
+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
+
# 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
+ 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:
- for class_file in list_class_under_class_dir(class_dir):
- java_class_2_class_paths.setdefault(class_file, set()).add(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
- return java_class_2_class_paths, jar_files | set(class_dirs)
+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
-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()):
- if len(class_paths) > 1:
- classes = class_path_2_duplicate_classes.setdefault(frozenset(class_paths), set())
- classes.add(java_class)
+################################################################################
+# biz functions
+################################################################################
- return class_path_2_duplicate_classes
+__java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$')
-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 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 len(class_paths) == 1 or __java9_module_file_pattern.match(clazz):
+ continue
-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()
+ classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set())
+ classes.add(clazz)
- if not options.class_dirs and not libs:
- libs = ['.']
+ return class_paths_to_duplicate_classes
- java_class_2_class_paths, class_paths = expand_2_class_path(
- list_jar_file_under_lib_dirs(libs), options.class_dirs)
- class_path_2_duplicate_classes = find_duplicate_classes(java_class_2_class_paths)
+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
- if not class_path_2_duplicate_classes:
- print("COOL! No duplicate classes found!")
- print_class_paths(class_paths)
- exit()
+ duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values())
+ 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)))
- 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)))
+ # 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
+ #
+ # 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
+ #
+ # 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]))
- 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)
+ 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(%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))
+ for i, cp in enumerate(class_paths, start=1):
+ 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 :' % (
+ max_idx_str_len, idx, len(classes), len(class_paths), ' '.join(class_paths)))
+
+ max_class_idx_str_len = str_len(len(classes))
+ for i, c in enumerate(classes, start=1):
+ print(' %*s: %s' % (max_class_idx_str_len, i, c))
+
+
+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()))
+
+ 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():
+ 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 # 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',
+ 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,
+ 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()
+ class_dirs = options.class_dirs
+ if not lib_dirs and not class_dirs:
+ lib_dirs = ['.']
+ global __show_responsive
+ __show_responsive = options.show_responsive
+
+ jar_files = list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib)
+ 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)
+ class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths)
+
+ clear_responsive_message()
+ 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))
+
+
+if __name__ == '__main__':
+ exit(main())
diff --git a/bin/taoc b/bin/taoc
new file mode 100755
index 00000000..5e5fcfc0
--- /dev/null
+++ b/bin/taoc
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# @Function
+# tac lines colorfully. taoc means coat(*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
+
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
+
+################################################################################
+# parse options
+################################################################################
+
+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
+[ -t 1 ] || exec tac "$@"
+
+readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34)
+COLOR_INDEX=0
+# CAUTION: print content WITHOUT new line
+rotateColorPrint() {
+ local content=$*
+ # skip color for white space
+ if [[ $content =~ ^[[:space:]]*$ ]]; then
+ printf %s "$content"
+ else
+ local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]}
+ printf '\e[1;%sm%s\e[0m' "$color" "$content"
+ fi
+}
+
+rotateColorPrintln() {
+ # NOTE: $'foo' is the escape sequence syntax of bash
+ rotateColorPrint "$*"$'\n'
+}
+
+colorLines() {
+ local line
+ # Bash read line does not read leading spaces
+ # https://stackoverflow.com/questions/29689172
+ while IFS= read -r line; do
+ 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
+ [ -z "$line" ] || rotateColorPrint "$line"
+}
+
+tac "$@" | colorLines
diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter
index 86b60351..dc7dae87 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.
#
@@ -8,16 +8,68 @@
# @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
+
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
+
+################################################################################
+# 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=$(uname)
+[[ $UNAME = Darwin* ]] && option_for_mac=-ptcp
-netstat -tna $option_for_mac | awk 'NR > 2 {
- s[$NF]++
+# shellcheck disable=SC2086
+netstat -tna ${option_for_mac:-} | awk 'NR > 2 {
+ ++s[$NF]
}
END {
+ # get max length of stat and count
+ for(v in s) {
+ 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 "%-11s %s\n", v, s[v]
+ printf "%-" max_stat_len "s %" max_count_len "s\n", v, s[v]
}
}' | sort -nr -k2,2
diff --git a/bin/uq b/bin/uq
new file mode 100755
index 00000000..535f9a85
--- /dev/null
+++ b/bin/uq
@@ -0,0 +1,337 @@
+#!/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,
+# 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
+
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
+
+################################################################################
+# util functions
+################################################################################
+
+# NOTE: $'foo' is the escape sequence syntax of bash
+readonly NL=$'\n' # new line
+
+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
+}
+
+yellowPrint() {
+ if [ -t 1 ]; then
+ printf '\e[1;33m%s\e[0m\n' "$*"
+ else
+ printf '%s\n' "$*"
+ fi
+}
+
+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
+
+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
+ k)
+ ((size *= 1024))
+ ;;
+ m)
+ ((size *= 1024 ** 2))
+ ;;
+ g)
+ ((size *= 1024 ** 3))
+ ;;
+ esac
+
+ echo "$size"
+}
+
+usage() {
+ cat < 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=${1#--all-repeated=}
+ [[ $uq_opt_repeated_method = 'none' || $uq_opt_repeated_method = 'prepend' || $uq_opt_repeated_method = 'separate' ]] ||
+ die -h "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[@]:+"${argv[@]}"} "$@")
+ break
+ ;;
+ -)
+ argv=(${argv[@]:+"${argv[@]}"} "$1")
+ shift
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ argv=(${argv[@]:+"${argv[@]}"} "$1")
+ shift
+ ;;
+ esac
+done
+
+[[ $uq_opt_only_repeated = 1 && $uq_opt_only_unique = 1 ]] &&
+ die -h "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless"
+[[ $uq_opt_all_repeated = 1 && $uq_opt_only_unique = 1 ]] &&
+ 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 "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") ||
+ die -h "illegal value of option -XM/--max-input: $uq_max_input_human_readable_size"
+
+readonly argc=${#argv[@]} argv uq_max_input_size
+
+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
+readonly output_file
+
+# Check input file
+for f in ${input_files[@]:+"${input_files[@]}"}; do
+ # - is stdin, ok
+ [ "$f" = - ] && continue
+
+ [ -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!"
+done
+unset f
+
+################################################################################
+# 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"
+}
+
+{
+ total_input_size += length + 1
+ if (total_input_size > uq_max_input_size) {
+ 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)
+ }
+
+ # 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" \
+ -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 2e4ba51a..34af6d91 100755
--- a/bin/xpf
+++ b/bin/xpf
@@ -1,13 +1,36 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# Open file in file explorer, file is selected.
-# same as xpl --selected [file [file ...] ]
+# same as xpl --selected [file]...
#
# @Usage
# $ ./xpf file
#
# @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" "$@"
+################################################################################
+# util functions
+################################################################################
+
+# `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?
+# https://stackoverflow.com/questions/1055671
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+################################################################################
+# biz logic
+################################################################################
+
+# DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell:
+# BASE_DIR=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")
+THIS_SCRIPT=$(realpath "${BASH_SOURCE[0]}")
+BASE_DIR=$(dirname -- "$THIS_SCRIPT")
+
+# shellcheck disable=SC1091
+source "$BASE_DIR/xpl" "$@"
diff --git a/bin/xpl b/bin/xpl
index b79bd271..392415aa 100755
--- a/bin/xpl
+++ b/bin/xpl
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# Open file in file explorer.
#
@@ -7,93 +7,165 @@
#
# @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=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-usage() {
- [ -n "$1" -a "$1" != 0 ] && local out=/dev/stderr || local out=/dev/stdout
+################################################################################
+# 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
+}
+
+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
- [ $# -gt 1 ] && { echo "$2"; echo; } > $out
+ (($# > 0)) && redPrint "$PROG: $*"
+ $prompt_help && echo "Try '$PROG --help' for more information."
- cat <&2
+
+usage() {
+ cat < 0)); do
+ case "$1" in
+ -s | --selected)
+ selected=true
+ shift
+ ;;
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ files=(${files[@]:+"${files[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$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 files selected
+
################################################################################
-# biz options
+# biz logic
################################################################################
# open one file
openOneFile() {
- local file="$1"
-
- case "$(uname)" in
- Darwin*)
- [ -f "${file}" ] && selected=true
- open ${selected:+-R} "$file"
- ;;
- CYGWIN*)
- [ -f "${file}" ] && selected=true
- explorer ${selected:+/select,} "$(cygpath -w "${file}")"
- ;;
- *)
- if [ -d "${file}" ] ; then
- nautilus "$(dirname "${file}")"
- else
- if [ -z "${selected}" ] ; then
- nautilus "$(dirname "${file}")"
- else
- nautilus "${file}"
- fi
- fi
- ;;
- esac
+ local file=$1 slt=$selected
+
+ case "$(uname)" in
+ Darwin*)
+ [ -f "$file" ] && slt=true
+ if $slt; then
+ open -R "$file"
+ else
+ open "$file"
+ fi
+ ;;
+ CYGWIN*)
+ [ -f "$file" ] && slt=true
+ if $slt; then
+ explorer /select, "$(cygpath -w "$file")"
+ else
+ explorer "$(cygpath -w "$file")"
+ fi
+ ;;
+ *)
+ if [ -d "$file" ]; then
+ nautilus "$(dirname -- "$file")"
+ else
+ if $slt; then
+ nautilus "$file"
+ else
+ nautilus "$(dirname -- "$file")"
+ fi
+ fi
+ ;;
+ esac
+
+ local selected_msg
+ $slt && selected_msg='with selection'
+ printf 'open %14s: %s\n' "$selected_msg" "$file"
}
-[ "${#args[@]}" == 0 ] && files=( . ) || files=( "${args[@]}" )
-for file in "${files[@]}" ; do
- [ ! -e "$file" ] && { echo "$file not exsited!"; continue; }
+has_error=false
+
+for file in "${files[@]}"; do
+ [ -e "$file" ] || {
+ has_error=true
+ redPrint "$PROG: $file: No such file or directory!" >&2
+ continue
+ }
- openOneFile "$file"
- echo "$file opened${selected:+ with selection}!"
+ openOneFile "$file" || has_error=true
done
+
+# set exit status
+! $has_error
diff --git a/docs/developer-guide.md b/docs/developer-guide.md
new file mode 100644
index 00000000..af731386
--- /dev/null
+++ b/docs/developer-guide.md
@@ -0,0 +1,44 @@
+# 📚 `Shell`学习与开发的资料
+
+- 🛠️ 开发规范与工具
+ - [`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`最佳实践与安全编程**文章
+ - [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)
+ - [编写可靠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://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.
+ - [命令行的艺术 - `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)
+ - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/)
diff --git a/docs/java.md b/docs/java.md
index 0676f2bc..8715329e 100644
--- a/docs/java.md
+++ b/docs/java.md
@@ -4,7 +4,6 @@
-
- [🍺 show-busy-java-threads](#-show-busy-java-threads)
- [用法](#%E7%94%A8%E6%B3%95)
- [示例](#%E7%A4%BA%E4%BE%8B)
@@ -27,10 +26,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 +50,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 +64,8 @@ PS,如何操作可以参见[@bluedavy](http://weibo.com/bluedavy)的[《分布
1. 在`jstack`输出中查找十六进制的线程`id`(可以用`vim`的查找功能`/0x1234`,或是`grep 0x1234 -A 20`)
1. 查看对应的线程栈,分析问题
-查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。
+查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。
+期望整合上面的过程成一个脚本,这样一行命令就可以自动化地搞定。
### 用法
@@ -74,10 +74,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 +99,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
@@ -120,8 +122,9 @@ 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
+ -p, --pid find out the highest cpu consumed threads from
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.
@@ -138,27 +141,23 @@ 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.
+ -V, --version display version information and exit.
```
### 示例
@@ -210,7 +209,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)
@@ -227,15 +226,20 @@ $ 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`,可以防患未然。
### 用法
-- 通过脚本参数指定`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,6 +247,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/UberJar中的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,12 +260,26 @@ 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 # 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:
+ --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
-c CLASS_DIRS, --class-dir=CLASS_DIRS
add class dir
+ -R, --no-find-progress
+ do not display responsive find progress
```
#### `JDK`开发场景使用说明
@@ -301,7 +323,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")
@@ -330,58 +352,65 @@ $ 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: (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 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 1272 duplicate classes in 345 class paths and 9 class path sets:
+[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
...
================================================================================
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 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
+ 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: (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
...
```
### 贡献者
-[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)
@@ -420,9 +449,14 @@ 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
+
Find files in the jar files under specified directory,
search jar files recursively(include subdirectory).
The pattern default is *extended* regex.
@@ -452,9 +486,15 @@ Output control:
-a, --absolute-path always print absolute path of jar file
-s, --separator specify the separator between jar file and zip entry.
default is `!'.
+ -L, --files-not-contained-found
+ print only names of JAR FILEs NOT contained found
+ -l, --files-contained-found
+ print only names of JAR FILEs contained found
+ -R, --no-find-progress do not display responsive find progress
Miscellaneous:
-h, --help display this help and exit
+ -V, --version display version information and exit
```
注意,Pattern缺省是`grep`的 **扩展**正则表达式。
@@ -465,6 +505,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
@@ -480,6 +521,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
+......
```
### 运行效果
@@ -490,4 +538,4 @@ WEB-INF/lib/aspectjweaver-1.8.8.jar!org/aspectj/weaver/XlintDefault.properties
### 参考资料
-[在多个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/logo-social-original.png b/docs/logo-social-original.png
new file mode 100644
index 00000000..9820b4ca
Binary files /dev/null and b/docs/logo-social-original.png differ
diff --git a/docs/logo-social.png b/docs/logo-social.png
new file mode 100644
index 00000000..70a623e4
Binary files /dev/null and b/docs/logo-social.png differ
diff --git a/docs/logo.meta.txt b/docs/logo.meta.txt
new file mode 100644
index 00000000..ffa36200
--- /dev/null
+++ b/docs/logo.meta.txt
@@ -0,0 +1,6 @@
+logo is created by https://www.logoly.pro
+
+font: Zilla Slab
+
+logo.fond-size: 60
+logo-social.fond-size: 160
diff --git a/docs/logo.png b/docs/logo.png
new file mode 100644
index 00000000..ede9eecc
Binary files /dev/null and b/docs/logo.png differ
diff --git a/docs/shell.md b/docs/shell.md
index 21a4bdd5..4ff27aac 100644
--- a/docs/shell.md
+++ b/docs/shell.md
@@ -4,38 +4,39 @@
-
- [`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)
- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99)
- - [🍺 coat](#-coat)
- - [示例](#%E7%A4%BA%E4%BE%8B)
+ - [🍺 coat and taoc](#-coat-and-taoc)
+ - [用法/示例](#%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%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-3)
- [🍺 ap and rp](#-ap-and-rp)
- - [示例](#%E7%A4%BA%E4%BE%8B-2)
+ - [用法/示例](#%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-3)
+ - [用法/示例](#%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-4)
+ - [用法/示例](#%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-5)
+ - [用法/示例](#%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-6)
+ - [用法](#%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-7)
+ - [用法](#%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)
@@ -50,9 +51,9 @@
原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
-命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便键入。
+命令名`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)。
### 用法/示例
@@ -97,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
@@ -106,24 +106,29 @@ 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
```
### 参考资料
-- [拷贝复制命令行输出放在系统剪贴板上](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)
+
+
+🍺 [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://manned.org/cat)/[`tac`](https://manned.org/tac)命令完全一样。
+文件操作在实现上完全代理给了`cat`/`tac`命令。
-命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色输入行就像件漂亮的外套~ 😆
+- 命令名`coat`的意思是`COlorful cAT`;同时单词`coat`是外套,而彩色的输出行就像件漂亮的外套~ 🌈 😆
+- 命令名`taoc`是`coat`倒序拼写;命名方式就像`tac`之于`cat`。 🐈
-### 示例
+### 用法/示例
```bash
$ echo Hello world | coat
@@ -131,6 +136,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
@@ -143,33 +151,28 @@ line2 of file2
...
# 帮助信息
-# 可以看到本人机器上实现代理的`cat`命令是GNU的实现。
+# 可以看到本人机器上实现代理的`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.
+
+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
+
+$ 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
```
注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,如下:
@@ -181,9 +184,9 @@ or available locally via: info '(coreutils) cat invocation'
按行彩色输出参数,方便人眼查看。
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
-命令名`a2l`意思是`Arguments to(2) Lines`。
+命令名`a2l`的意思是`Arguments to(2) Lines`。
-### 示例
+### 用法/示例
```bash
$ a2l *.java
@@ -195,22 +198,148 @@ 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
...
```
注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,和上面的`coat`命令一样。
+🍺 [uq](../bin/uq)
+----------------------
+
+不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。
+使用方式与支持的选项 模仿系统的`uniq`命令。支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
+
+> ‼️ **_注意_**: 去重过程会在内存持有整个输入(因为全局去重)!
+>
+> 对于输入大小较大的场景(如输入量有几G),需谨慎使用以避免占用过多内存;往往需要结合业务场景开发对应的优化实现。
+> 虽然平时的大部分场景输入量非常有限(如几M),一个简单没有充分优化的实现是快速够用的。
+>
+> `uq`处理的最大输入量缺省是 256m(字符数),超过了最大输入量则出错退出,以避免意外消耗了过大的内存;
+> 可以通过`-XM, --max-input`选项 设置 消耗更多内存可接受的合理最大输入量,如`uq --max-input 1g ...`
+
+因为系统的`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 -
+
+# 如果消耗更多内存可接受的合理的,可以通过 -XM, --max-input 选项设置更大的最大输入量(缺省是256m)
+$ uq -MI 768m large-file-input
+$ uq --max-input 10g huge-file-input
+
+# 帮助信息
+$ 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
+ -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
+ -V, --version display version information and exit
+```
+
🍺 [ap](../bin/ap) and [rp](../bin/rp)
----------------------
批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
-命令名`ap`意思是`Absolute Path`,`rp`是`Relative Path`。
+命令名`ap`的意思是`Absolute Path`,`rp`是`Relative Path`。
-### 示例
+### 用法/示例
```bash
# ap缺省打印当前路径的绝对路径
@@ -234,6 +363,50 @@ $ 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 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
+
+miscellaneous:
+ -h, --help display this help and exit
+ -V, --version display version information and exit
+```
+
@@ -249,24 +422,20 @@ $ 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
-ESTABLISHED 290
-TIME_WAIT 212
-SYN_SENT 17
+CLOSE_WAIT 584
+ESTABLISHED 493
+TIME_WAIT 112
+LISTEN 27
+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)
----------------------
@@ -275,11 +444,11 @@ SYN_SENT 17
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
- `xpl`:在文件浏览器中打开指定的文件或文件夹。
- `xpl`是`explorer`的缩写。
+ `xpl`是`explorer`的缩写。
- `xpf`: 在文件浏览器中打开指定的文件或文件夹,并选中。
- `xpf`是`explorer and select file`的缩写。
+ `xpf`是`EXplorer and select File`的缩写。
-### 用法
+### 用法/示例
```bash
xpl
@@ -291,11 +460,9 @@ xpf
# 缺省打开当前目录
xpf <文件或是目录>...
# 打开多个文件或目录
-```
-### 示例
-```bash
+# 示例
xpl /path/to/dir
xpl /path/to/foo.txt
xpl /path/to/dir1 /path/to/foo1.txt
@@ -305,7 +472,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`开发/测试加强
====================================
@@ -321,7 +488,7 @@ xpf /path/to/dir1 /path/to/foo1.txt
这个脚本输出脚本收到的参数。在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。
-### 示例
+### 用法/示例
```bash
$ ./echo-args 1 " 2 foo " "3 3"
@@ -372,11 +539,12 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!"
### 贡献者
-[姜太公](https://github.com/jzwlqx)提供循环输出彩色组合的脚本。
+[姜太公](https://github.com/jzwlqx) 提供循环输出彩色组合的脚本。
### 参考资料
-- [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)
----------------------
@@ -384,9 +552,9 @@ 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`](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: " {} \;
@@ -403,10 +571,10 @@ find file: bar.txt
选项说明最后可以有选项类型说明:
-- `-`: 无参数的选项。即有选项则把值设置成`true`。这是 ***缺省*** 的类型。
+- `-`: 无参数的选项。既有选项则把值设置成`true`。这是 ***缺省*** 的类型。
- `:`: 有参数的选项,值只有一个。
- `+`: 有多个参数值的选项。值列表要以`;`表示结束。
- 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。
+ 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。
实际要解析的输入参数往往是你的脚本参数,这样`parseOpts`函数调用一般是:
@@ -419,7 +587,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`来获取。
按照惯例,输入参数中如果有`--`表示之后参数中不再有选项,即之后都是参数。
@@ -463,19 +631,19 @@ 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`
### 贡献者
-[Khotyn Huang](https://github.com/khotyn)指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。
+- [Khotyn Huang](https://github.com/khotyn) 指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。
diff --git a/docs/vcs.md b/docs/vcs.md
index 00ea9bf8..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)
----------------------
@@ -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!
```
### 贡献者
@@ -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),给出了不同系统可用命令。
diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url
index 34799b59..d5d15095 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.
#
@@ -9,47 +9,75 @@
# @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)
-readonly PROG=`basename $0`
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
+
+################################################################################
+# 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
+################################################################################
-[ $# -gt 1 ] && { echo At most 1 local directory is need! ; usage 1; }
+(($# > 1)) && {
+ echo At most 1 local directory is need!
+ usage 1
+}
readonly dir="${1:-.}"
-readonly url="$(svn info "${dir}" | awk '/^URL: /{print $2}')"
-if [ -z "${url}" ]; then
- echo "Fail to get svn url!" 1>&2
- exit 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
+ echo "Fail to get svn url!" >&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!"
+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 dd8c90bf..249fa7d2 100755
--- a/legacy-bin/svn-merge-stop-on-copy
+++ b/legacy-bin/svn-merge-stop-on-copy
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
-# 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.
#
# @Usage
@@ -11,78 +11,79 @@
# @author jiangjizhong(@jzwlqx)
# @author Jerry Lee (oldratlee at gmail dot com)
-PROG=`basename $0`
+readonly PROG=${0##*/}
usage() {
- cat < [target branch]
-svn merge commit between verison when source branch copy(--stop-on-copy)
+ 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 direcotry
+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/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.
+ $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
- exit $1
+
+ exit "$1"
}
-[ $# -gt 2 ] && {
- echo "too many arguments!"
- usage 1
+(($# > 2)) && {
+ 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"
-cleanup() {
- [ "$workDir" != "$target" ] && {
- echo "rm tmp dir $workDir ."
- rm -rf "$workDir"
- }
+cleanupWhenExit() {
+ [ "$workDir" != "$target" ] && {
+ echo "rm tmp dir $workDir ."
+ rm -rf "$workDir"
+ }
}
-trap "cleanup" EXIT
+trap cleanupWhenExit EXIT
-svnstatusline=$(svn status --ignore-externals "$workDir" | grep -v ^X | wc -l)
-[ "$svnstatusline" -ne 0 ] && {
- echo "svn work direcotry is modified!"
- exit 1
+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) && {
- echo "oldest version($from_version) of source branch $source_branch ."
- echo "starting merge to $workDir ."
- svn merge -${from_version}:HEAD $source_branch
-} || {
- echo "Fail to merge to work dir $workDir ."
- exit 2
-}
+ 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 -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..6578fd3b 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.
#
@@ -8,39 +8,43 @@
# @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 "$@"
+ 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
}
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 dirs is empty, use "."
+dirs=("${dirs[@]:-.}")
- svn sw "$trunk" &&
- greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." ||
+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
+ )
done
diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh
index d95dceac..70833615 100755
--- a/lib/console-text-color-themes.sh
+++ b/lib/console-text-color-themes.sh
@@ -1,69 +1,76 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# show all console text color themes.
#
# @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")")"
-[ "$_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
-
- [ -t 1 ] && echo "$_ctct_ec[${combination}m$@$_ctct_eend" || echo "$@"
+ 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[%sm%s\e[0m\n' "$combination" "$*"
+ else
+ print '%s\n' "$*"
+ fi
}
colorEchoWithoutNewLine() {
- local combination="$1"
- shift 1
+ local combination=$1
+ shift 1
- [ -t 1 ] && echo -n "$_ctct_ec[${combination}m$@$_ctct_eend" || echo -n "$@"
+ if [ -t 1 ]; then
+ printf '\e[%sm%s\e[0m' "$combination" "$*"
+ else
+ 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" ] && {
- 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
+# 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 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:'
- echo -n ' echo -e "\033['
- colorEchoWithoutNewLine "3;35;40" "1;36;41"
- echo -n "m"
- colorEchoWithoutNewLine "0;32;40" "Sample Text"
- echo "\033[0m\""
+printf %s ' echo -e "\e['
+colorEchoWithoutNewLine '3;35;40' '1;36;41'
+printf %s m
+colorEchoWithoutNewLine '0;32;40' 'Sample Text'
+printf '%s\n' '\e[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"
+printf %s " echo \$'\e["
+colorEchoWithoutNewLine '3;35;40' '1;36;41'
+printf %s "m'\""
+colorEchoWithoutNewLine '0;32;40' 'Sample Text'
+printf '%s\n' "\"$'\e[0m'"
+printf '%s\n' " # 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"
-}
+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'
diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh
index 6d67f736..86dd0842 100755
--- a/lib/parseOpts.sh
+++ b/lib/parseOpts.sh
@@ -1,12 +1,12 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# parse options lib, support multiple values for one option.
#
# @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,36 +19,46 @@
# @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
+readonly _opts_ec=$'\e' # escape char
+readonly _opts_eend=$'\e[0m' # escape end
+
+# shellcheck disable=SC2209
+
+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() {
- 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" | sed 's/-/_/g'
+ [ $# -ne 1 ] && {
+ _opts_redEcho "NOT 1 arguments when call _opts_convertToVarName: $*"
+ return 1
+ }
+ echo "$1" | $_opts_SED_CMD 's/-/_/g'
}
#####################################################################
# 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
@@ -56,258 +66,275 @@ _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 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 -a idxNameArray=("${!idxNameArrayPlaceHolder}")
+
+ local mode="${idxNameArray[0]}"
+
+ local optName
+ # index from 1, skip mode
+ for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ [ "$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 arguments 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 arguments 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
+ # index from 1, skip mode
+ for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ [ "$opt" == "$optName" ] && {
+ local optName2
+ for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ local optValueVarName
+ optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")"
+ # shellcheck disable=SC2016
+ local from='"$value"'
+ # set global var!
+ eval "$optValueVarName=$from"
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
+ # index from 1, skip mode
+ for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ [ "$opt" == "$optName" ] && {
+ # set _OPT_VALUE
+ local optName2
+ for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ local optValueVarName
+ 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}")
-
- 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 idxName
+ for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do
+ local idxNameArrayPlaceHolder="${idxName}[@]"
+ local -a idxNameArray=("${!idxNameArrayPlaceHolder}")
+
+ eval "unset $idxName"
+
+ local optName
+ # index from 1, skip mode
+ for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ local optValueVarName
+ 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
- sed -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
+ 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 # optDesc LIKE b,b-long:
+ while read -r optDesc; do
+ [ -z "$optDesc" ] && continue
+
+ # LIKE : or +
+ local mode="${optDesc:(-1)}"
+ case "$mode" in
+ + | : | -)
+ # LIKE b,b-long
+ optDesc="${optDesc:0:(${#optDesc} - 1)}"
+ ;;
+ *)
+ mode="-"
+ ;;
+ esac
+
+ 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 ] && {
+ _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2
+ _opts_cleanOptValueInfoList
+ return 220
+ }
+
+ local -a optTuple=()
+ local opt # opt LIKE a , a-long
+ while read -r opt; do
+ [ -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
+ 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_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
+ optName=$(echo "$1" | $_opts_SED_CMD -r 's/^--?//')
+ local mode
+ 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" | sed -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
+ # set global var!
+ _OPT_ARGS=("${args[@]}")
}
#####################################################################
@@ -315,43 +342,45 @@ 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
+ # index from 1, skip mode
+ for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do
+ local optValueVarName
+ 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
new file mode 100755
index 00000000..d71213c4
--- /dev/null
+++ b/test-cases/bump-scripts-version.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+set -eEuo pipefail
+
+################################################################################
+# util functions
+################################################################################
+
+# NOTE: $'foo' is the escape sequence syntax of bash
+readonly NL=$'\n' # new line
+
+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
+}
+
+redPrint() {
+ colorPrint 31 "$@"
+}
+
+yellowPrint() {
+ colorPrint 33 "$@"
+}
+
+bluePrint() {
+ colorPrint 36 "$@"
+}
+
+logAndRun() {
+ local simple_mode=false
+ [ "$1" = "-s" ] && {
+ simple_mode=true
+ shift
+ }
+
+ if $simple_mode; then
+ echo "Run under work directory $PWD : $*"
+ "$@"
+ else
+ bluePrint "Run under work directory $PWD :$NL$*"
+ time "$@"
+ fi
+}
+
+die() {
+ redPrint "Error: $*" >&2
+ exit 1
+}
+
+################################################################################
+# biz logic
+################################################################################
+
+(($# != 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
+#
+# Bash Pitfalls#5
+# http://mywiki.wooledge.org/BashPitfalls#cd_.24.28dirname_.22.24f.22.29
+cd -P -- "$(dirname -- "$0")"/..
+
+# 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/" -- \
+ {} +
diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh
new file mode 100755
index 00000000..cbcdbe5c
--- /dev/null
+++ b/test-cases/integration-test.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+set -eEuo pipefail
+
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")"
+
+################################################################################
+# common util functions
+################################################################################
+
+colorEcho() {
+ 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 || "${GITHUB_ACTIONS:-}" = true ]]; then
+ printf '\e[1;%sm%s\e[0m\n' "$color" "$*"
+ else
+ printf '%s\n' "$*"
+ fi
+}
+
+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
+ # NOTE: $'foo' is the escape sequence syntax of bash
+ local nl=$'\n' # new line
+ 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
diff --git a/test-cases/lint.sh b/test-cases/lint.sh
new file mode 100755
index 00000000..76859457
--- /dev/null
+++ b/test-cases/lint.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+set -eEuo pipefail
+
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+# cd to the root of the project
+cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")"/..
+
+find bin lib legacy-bin -type f |
+ grep -Pv '/show-duplicate-java-classes$' |
+ grep -Pv '/\.editorconfig$' |
+ xargs --verbose shellcheck --shell=bash
diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh
new file mode 100644
index 00000000..df2cc668
--- /dev/null
+++ b/test-cases/my_unit_test_lib.sh
@@ -0,0 +1,105 @@
+#!/usr/bin/env bash
+# unit test lib
+
+#################################################
+# commons functions
+#################################################
+
+__ut_colorEcho() {
+ 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 || "${GITHUB_ACTIONS:-}" = true ]]; then
+ printf '\e[1;%sm%s\e[0m\n' "$color" "$*"
+ else
+ printf '%s\n' "$*"
+ fi
+}
+
+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: $*" >&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[@]} == ${#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}"
+}
+
+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 -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)
+
+ 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/parseOpts-test.sh b/test-cases/parseOpts-test.sh
deleted file mode 100755
index d87943d3..00000000
--- a/test-cases/parseOpts-test.sh
+++ /dev/null
@@ -1,193 +0,0 @@
-#!/bin/bash
-
-BASE=`dirname $0`
-
-. $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
-}
-
-#################################################
-# Test
-#################################################
-
-test_beforeVars=`declare`
-
-# ========================================
-blueEcho "Test case: success parse"
-# ========================================
-
-parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee
-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!"
-[ $_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!"
-test_argArray=(aa bb cc dd ee)
-arrayEquals test_argArray _OPT_ARGS || fail "Wrong args!"
-
-compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!"
-_opts_cleanOptValueInfoList
-compareAllVars || fail "Unpected extra glable vars!"
-
-# ========================================
-blueEcho "Test case: success parse with -- "
-# ========================================
-
-parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb -- --d-long d.sh -x xv d1 d2 d3 \; cc dd ee
-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!"
-[ $_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!"
-[ "$_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!"
-
-compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!"
-_opts_cleanOptValueInfoList
-compareAllVars || fail "Unpected extra glable vars!"
-
-
-# ========================================
-blueEcho "Test case: illegal option x"
-# ========================================
-
-parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc -- dd ee
-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!"
-[ "$_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"
-# ========================================
-
-parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+"
-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!"
-[ "$_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"
-# ========================================
-
-parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|#,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
-_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!"
-
-
-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
-_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!"
-
-compareAllVars || fail "Unpected extra glable vars!"
-
-greenEcho "TEST SUCCESS!!!"
diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh
new file mode 100755
index 00000000..35ad0018
--- /dev/null
+++ b/test-cases/parseOpts_test.sh
@@ -0,0 +1,139 @@
+#!/usr/bin/env bash
+
+BASE="$(dirname -- "${BASH_SOURCE[0]}")"
+
+source "$BASE/../lib/parseOpts.sh"
+
+source "$BASE/my_unit_test_lib.sh"
+
+#################################################
+# Test
+#################################################
+
+# ========================================
+blueEcho "Test case: success parse"
+# ========================================
+
+parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee
+test_exitCode=$?
+_opts_showOptDescInfoList
+_opts_showOptValueInfoList
+
+((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!"
+
+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
+
+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)
+assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS
+
+assertAllVarsExcludeOptVarsSame
+
+_opts_cleanOptValueInfoList
+assertAllVarsSame
+
+# ========================================
+blueEcho "Test case: success parse with -- "
+# ========================================
+
+parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb -- --d-long d.sh -x xv d1 d2 d3 \; cc dd ee
+test_exitCode=$?
+_opts_showOptDescInfoList
+_opts_showOptValueInfoList
+
+((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!"
+
+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!"
+
+test_argArray=(aa bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee)
+assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS
+
+assertAllVarsExcludeOptVarsSame
+
+_opts_cleanOptValueInfoList
+assertAllVarsSame
+
+# ========================================
+blueEcho "Test case: illegal option x"
+# ========================================
+
+parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc -- dd ee
+test_exitCode=$?
+_opts_showOptDescInfoList
+_opts_showOptValueInfoList
+
+((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!"
+[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!"
+[ "$_OPT_ARGS" == "" ] || fail "Wrong args!"
+
+# ========================================
+blueEcho "Test case: empty options"
+# ========================================
+
+parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+"
+test_exitCode=$?
+_opts_showOptDescInfoList
+_opts_showOptValueInfoList
+
+((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!"
+[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!"
+[ "$_OPT_ARGS" == "" ] || fail "Wrong args!"
+
+# ========================================
+blueEcho "Test case: illegal option name"
+# ========================================
+
+parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|#,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
+_opts_showOptValueInfoList
+
+((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!"
+[[ "$_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
+_opts_showOptValueInfoList
+
+((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!"
+[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!"
+[ "$_OPT_ARGS" == "" ] || fail "Wrong args!"
+
+assertAllVarsSame
+
+greenEcho "TEST SUCCESS!!!"
diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh
index 0a16544e..801e7a79 100644
--- a/test-cases/self-installer.sh
+++ b/test-cases/self-installer.sh
@@ -1,8 +1,8 @@
-#!/bin/bash
+#!/usr/bin/env bash
-if which 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 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
export PATH="$PATH:/tmp/useful-scripts-$USER/bin:/tmp/useful-scripts-$USER/legacy-bin"
diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib
new file mode 160000
index 00000000..da1e19de
--- /dev/null
+++ b/test-cases/shunit2-lib
@@ -0,0 +1 @@
+Subproject commit da1e19de845a77628d9684e609cc0f8160782c68
diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh
new file mode 100755
index 00000000..c3bfdfe0
--- /dev/null
+++ b/test-cases/uq_test.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env bash
+set -eEuo pipefail
+
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
+
+BASE=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")
+cd "$BASE"
+
+#################################################
+# commons and test data
+#################################################
+
+readonly uq="../bin/uq"
+# NOTE: $'foo' is the escape sequence syntax of bash
+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)"
+}
+
+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.
+#################################################
+
+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