diff --git a/.editorconfig b/.editorconfig
index 40d2df19..4b2606b8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,12 +4,20 @@ 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
-tab_width = 4
-[*.yml]
-indent_size = 2
+[*.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/.travis.yml b/.travis.yml
deleted file mode 100644
index 48979086..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# https://github.com/caarlos0-graveyard/shell-ci-build
-# https://github.com/kward/shunit2/blob/master/.travis.yml
-language: shell
-
-addons:
- homebrew:
- packages:
- - coreutils
- - gnu-sed
-
-# The Ubuntu Linux Build Environments
-# https://docs.travis-ci.com/user/reference/linux/
-# The macOS Build Environment
-# https://docs.travis-ci.com/user/reference/osx/
-# Installing Dependencies
-# https://docs.travis-ci.com/user/installing-dependencies/#installing-packages-on-macos
-jobs:
- include:
- - os: linux
- dist: precise
- - os: linux
- dist: trusty
- - os: linux
- dist: xenial
- - os: linux
- dist: bionic
- - os: linux
- dist: focal
- - os: osx
- osx_image: xcode11.3
-
-script:
- - test-cases/integration-test.sh
-
-after_script:
- - git status --ignored
diff --git a/README.md b/README.md
index 78bdc993..32a6764d 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,52 @@
-🐌 useful-scripts
-====================================
+#

-
+
+
+
+
+
+
+
+
+
+
-[](https://travis-ci.org/oldratlee/useful-scripts)
-[](https://github.com/oldratlee/useful-scripts/releases)
-[](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/stargazers)
-[](https://github.com/oldratlee/useful-scripts/fork)
-[](https://github.com/oldratlee/useful-scripts/issues)
-[](https://github.com/oldratlee/useful-scripts/graphs/contributors)
+🐌 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))! 💖
+欢迎 👏 💖
-PS:
+- 提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new)
+- 分享平时自己常用但没有写成脚本的功能(即需求、想法),[提交Issue](https://github.com/oldratlee/useful-scripts/issues/new)
+- 优化改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork)
+- 提供的自己好用脚本实现,[Fork 后提通过 Pull Request 提供](https://github.com/oldratlee/useful-scripts/fork)
+
+本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。
+
+如果你的公司有部署使用,欢迎使用通过 [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)告知,方便互相交流反馈~ 💘
+
+
+----------------------
🔰 快速下载&使用
----------------------
@@ -36,42 +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`命令加强的是可以跨行去重,不需要排序输入。
+ 不重排序输入完成整个输入行的去重。相比系统的`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 7c082b94..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
@@ -10,21 +10,83 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-# NOTE: $'foo' is the escape sequence syntax of bash
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-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 "$*"
+################################################################################
+# parse options
+################################################################################
+
+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 52b16376..051f66d9 100755
--- a/bin/ap
+++ b/bin/ap
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# convert to Absolute Path.
#
@@ -12,17 +12,124 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-READLINK_CMD=readlink
-if command -v greadlink > /dev/null; then
- READLINK_CMD=greadlink
-fi
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-[ $# -eq 0 ] && files=(.) || files=("$@")
+################################################################################
+# 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
- ! [ -e "$f" ] && {
- echo "$f does not exists!"
- continue
- }
- $READLINK_CMD -f "$f"
+ 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 3b372f36..c0bbbd5a 100755
--- a/bin/c
+++ b/bin/c
@@ -1,66 +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)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
set -eEuo pipefail
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-PROG="$(basename "$0")"
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
################################################################################
# util functions
################################################################################
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
-readonly nl=$'\n' # new line
-
-redEcho() {
- [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*"
+redPrint() {
+ # if stdout is a terminal, turn on color output.
+ # '-t' check: is a terminal?
+ # check isatty in bash https://stackoverflow.com/questions/10022323
+ if [ -t 1 ]; then
+ printf '\e[1;31m%s\e[0m\n' "$*"
+ else
+ printf '%s\n' "$*"
+ fi
}
-usage() {
- local -r exit_code="${1:-0}"
- (($# > 0)) && shift
- # shellcheck disable=SC2015
- [ "$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)) && redPrint "$PROG: $*"
+ $prompt_help && echo "Try '$PROG --help' for more information."
- (($# > 0)) && redEcho "$*$nl" >$out
+ exit "$exit_status"
+} >&2
- cat >$out < 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() {
- # shellcheck disable=SC2015
- $quiet && local -r out=/dev/null || local -r out=/dev/stdout
- tee >(
- content="$(cat)"
- # shellcheck disable=SC2086
- echo $eol "$content" | copy
- ) >$out
+ if $quiet; then
+ 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 a82e1b4e..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*.
#
@@ -11,23 +11,86 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-# if not in console, use cat directly
-# check isatty in bash https://stackoverflow.com/questions/10022323
-[ ! -t 1 ] && exec cat "$@"
+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 `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 551f231f..9186a6e3 100755
--- a/bin/echo-args
+++ b/bin/echo-args
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# print arguments in human and debugging friendly style.
#
@@ -6,20 +6,36 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
+digitCount() {
+ # argument 1(num) is always a non-negative integer in this script usage,
+ # so NO argument validation logic.
+ local num=$1 count=0
+ while ((num != 0)); do
+ ((++count))
+ ((num = num / 10))
+ done
+ echo "$count"
+}
+
+digit_count=$(digitCount $#)
+readonly arg_count=$# digit_count
-echoArg() {
- local index="$1" count="$2" value="$3"
+readonly RED='\e[1;31m' BLUE='\e[1;36m' COLOR_RESET='\e[0m'
+printArg() {
+ local idx=$1 value=$2
- # if stdout is console, turn on color output.
- [ -t 1 ] &&
- echo "${index}/${count}: ${ec}[1;31m[$eend${ec}[1;36;40m$value$eend${ec}[1;31m]$eend" ||
- echo "${index}/${count}: [${value}]"
+ # if stdout is 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"
+ printArg $((idx++)) "$a"
done
diff --git a/bin/find-in-jars b/bin/find-in-jars
index 1a62fe85..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,29 +13,28 @@
#
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-find-in-jars
# @author Jerry Lee (oldratlee at gmail dot com)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
set -eEuo pipefail
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-PROG="$(basename "$0")"
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
################################################################################
# util functions
################################################################################
-# NOTE: $'foo' is the escape sequence syntax of bash
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
-readonly nl=$'\n' # new line
+readonly 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
+}
+
# How to delete line with echo?
# https://unix.stackexchange.com/questions/26576
#
@@ -44,66 +43,71 @@ readonly nl=$'\n' # new line
# echo -e "\033[1K"
# Or everything on the line, regardless of cursor position:
# echo -e "\033[2K"
-readonly clear_line=$'\033[2K\r'
-
-redEcho() {
- # -t check: is a terminal device?
- [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*"
-}
+readonly LINE_CLEAR='\e[2K\r'
# Getting console width using a bash script
# https://unix.stackexchange.com/questions/299067
-#
-# NOTE: DO NOT declare columns var as readonly, because its value is supplied by subshell.
-[ -t 2 ] && columns=$(stty size | awk '{print $2}')
+[ -t 2 ] && COLUMNS=$(stty size | awk '{print $2}')
printResponsiveMessage() {
- if ! $show_responsive || [ ! -t 2 ]; then
- return
- fi
+ if ! $show_responsive || [ ! -t 2 ]; then
+ return
+ fi
- local message="$*"
- # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html
- echo -n "$clear_line${message:0:columns}" >&2
+ local 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() {
- if ! $show_responsive || [ ! -t 2 ]; then
- return
- fi
+ if ! $show_responsive || [ ! -t 2 ]; then
+ return
+ fi
- echo -n "$clear_line" >&2
+ printf %b "$LINE_CLEAR" >&2
}
die() {
- clearResponsiveMessage
- redEcho "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
-usage() {
- local -r exit_code="${1:-0}"
- (($# > 0)) && shift
- # shellcheck disable=SC2015
- [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+ clearResponsiveMessage
+ (($# > 0)) && redPrint "$PROG: $*"
+ $prompt_help && echo "Try '$PROG --help' for more information."
- (($# > 0)) && redEcho "$*$nl" >$out
+ exit "$exit_status"
+} >&2
- cat >$out < '
+ $PROG '^log4j\.(properties|xml)$'
+ $PROG 'log4j\.properties$' -d /path/to/find/directory
+ $PROG '\.properties$' -d /path/to/find/dir1 -d path/to/find/dir2
+ $PROG 'Service\.class$' -e jar -e zip
+ $PROG 'Mon[^$/]*Service\.class$' -s ' <-> '
Find control:
-d, --dir the directory that find jar files.
@@ -129,18 +133,24 @@ Output control:
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
}
################################################################################
# parse options
################################################################################
-declare -a dirs=()
-declare -a extensions=()
-declare -a args=()
+dirs=()
+extensions=()
+args=()
separator='!'
regex_mode=-E
@@ -149,199 +159,247 @@ 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 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
- ;;
- --)
- shift
- args=(${args[@]:+"${args[@]}"} "$@")
- break
- ;;
- -*)
- usage 2 "${PROG}: unrecognized option '$1'"
- ;;
- *)
- args=(${args[@]:+"${args[@]}"} "$1")
- shift
- ;;
- esac
+ case "$1" in
+ -d | --dir)
+ dirs=(${dirs[@]:+"${dirs[@]}"} "$2")
+ shift 2
+ ;;
+ -e | --extension)
+ extensions=(${extensions[@]:+"${extensions[@]}"} "$2")
+ shift 2
+ ;;
+ -E | --extended-regexp)
+ regex_mode=-E
+ shift
+ ;;
+ -F | --fixed-strings)
+ regex_mode=-F
+ shift
+ ;;
+ -G | --basic-regexp)
+ regex_mode=-G
+ shift
+ ;;
+ -P | --perl-regexp)
+ regex_mode=-P
+ shift
+ ;;
+ -i | --ignore-case)
+ ignore_case_option=-i
+ shift
+ ;;
+ -a | --absolute-path)
+ use_absolute_path=true
+ shift
+ ;;
+ # support the legacy typo option name --seperator for compatibility
+ -s | --separator | --seperator)
+ separator=$2
+ shift 2
+ ;;
+ -L | --files-not-contained-found)
+ only_print_file_name=true
+ print_matched_files=false
+ shift
+ ;;
+ -l | --files-contained-found)
+ only_print_file_name=true
+ print_matched_files=true
+ shift
+ ;;
+ -R | --no-find-progress)
+ show_responsive=false
+ shift
+ ;;
+ -h | --help)
+ usage
+ ;;
+ -V | --version)
+ progVersion
+ ;;
+ --)
+ shift
+ args=(${args[@]:+"${args[@]}"} "$@")
+ break
+ ;;
+ -*)
+ die -h "unrecognized option '$1'"
+ ;;
+ *)
+ args=(${args[@]:+"${args[@]}"} "$1")
+ shift
+ ;;
+ esac
done
+readonly separator regex_mode ignore_case_option use_absolute_path only_print_file_name print_matched_files show_responsive args
+
# shellcheck disable=SC2178
dirs=${dirs:-.}
# shellcheck disable=SC2178
-extensions=${extensions:-jar}
+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!"
+ [ -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)")
+ # convert dirs to Absolute Path if has option -a, --absolute-path
+ $use_absolute_path && tmp_dirs=(${tmp_dirs[@]:+"${tmp_dirs[@]}"} "$(cd "$d" && pwd)")
done
# set dirs to Absolute Path
$use_absolute_path && dirs=("${tmp_dirs[@]}")
+readonly dirs
+unset d tmp_dirs
# convert extensions to find -iname options
find_iname_options=()
for e in "${extensions[@]}"; do
- (("${#find_iname_options[@]}" == 0)) &&
- find_iname_options=(-iname "*.$e") ||
- find_iname_options=("${find_iname_options[@]}" -o -iname "*.$e")
+ find_iname_options=(${find_iname_options[@]:+"${find_iname_options[@]}" -o} -iname "*.$e")
done
+readonly find_iname_options
+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 command -v zipinfo &>/dev/null; then
- readonly command_for_list_zip='zipinfo -1'
-elif command -v unzip &>/dev/null; then
- readonly command_for_list_zip='unzip -Z1'
-else
- if ! command -v jar &>/dev/null; then
- [ -n "$JAVA_HOME" ] || die "jar not found on PATH and JAVA_HOME env var is blank!"
- [ -f "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) file does NOT exists!"
- [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executable!"
- export PATH="$JAVA_HOME/bin:$PATH"
+__prepareCommandToListZipEntries() {
+ # `zipinfo -1`/`unzip -Z1` is ~25 times faster than `jar tf`, find zipinfo/unzip command first.
+ #
+ # How to list files in a zip without extra information in command line
+ # https://unix.stackexchange.com/a/128304/136953
+
+ if 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
################################################################################
searchJarFiles() {
- printResponsiveMessage "searching jars under dir ${dirs[*]} , ..."
+ printResponsiveMessage "searching jars under dir ${dirs[*]} , ..."
- local jar_files total_jar_count
+ local jar_files total_jar_count
- jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)"
- [ -n "$jar_files" ] || die "No ${extensions[*]} file found!"
+ jar_files=$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)
+ [ -n "$jar_files" ] || die "${extensions[*]} file NOT found!"
- total_jar_count="$(echo "$jar_files" | wc -l)"
- # delete white space
- # because the output of mac system command `wc -l` contains white space!
- total_jar_count="${total_jar_count//[[:space:]]/}"
+ total_jar_count=$(printf '%s\n' "$jar_files" | wc -l)
+ # remove white space, because the `wc -l` output on mac contains white space!
+ total_jar_count=${total_jar_count//[[:space:]]/}
- echo "$total_jar_count"
- echo "$jar_files"
+ echo "$total_jar_count"
+ printf '%s\n' "$jar_files"
}
+readonly JAR_COLOR='\e[1;35m' SEP_COLOR='\e[1;32m'
__outputResultOfJarFile() {
- local jar_file="$1" file
-
- if $only_print_file_name; then
- local matched=false
- # NOTE: Do NOT use -q flag with grep:
- # With the -q flag the grep program will stop immediately when the first line of data matches.
- # Normally you shouldn't use -q in a pipeline like this
- # unless you are sure the program at the other end can handle SIGPIPE.
- # more info see:
- # - https://stackoverflow.com/questions/19120263/why-exit-code-141-with-grep-q
- # - https://unix.stackexchange.com/questions/305547/broken-pipe-when-grepping-output-but-only-with-i-flag
- # - http://www.pixelbeat.org/programming/sigpipe_handling.html
- if grep $regex_mode ${ignore_case_option:-} -- "$pattern" &>/dev/null; then
- matched=true
- fi
-
- if [ $print_matched_files != $matched ]; then
- return
- fi
+ 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
- [ -t 1 ] && echo "${ec}[1;35m${jar_file}${eend}" || echo "${jar_file}"
+ clearResponsiveMessage
+ if [ -t 1 ]; then
+ printf "$JAR_COLOR%s$COLOR_RESET\n" "$jar_file"
else
- {
- # Prevent grep from exiting in case of no match
- # https://unix.stackexchange.com/questions/330660
- # shellcheck disable=SC2086
- grep $regex_mode ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern" || true
- } | while read -r file; do
- clearResponsiveMessage
- [ -t 1 ] &&
- echo "${ec}[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" ||
- echo "${jar_file}${separator}${file}"
- done
+ 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'
+ [ -t 1 ] && local -r grep_color_option='--color=always'
+ local counter=1 total_jar_count jar_file
- local counter=1 total_jar_count jar_file
+ read -r total_jar_count
+ while IFS= read -r jar_file; do
+ printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file"
+ listZipEntries "$jar_file" | __outputResultOfJarFile "$jar_file"
+ done
- read -r total_jar_count
-
- while read -r jar_file; do
- printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file"
- $command_for_list_zip "${jar_file}" | __outputResultOfJarFile "${jar_file}"
- done
-
- clearResponsiveMessage
+ clearResponsiveMessage
}
searchJarFiles | findInJarFiles
diff --git a/bin/rp b/bin/rp
index 86d25d89..d4d85210 100755
--- a/bin/rp
+++ b/bin/rp
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# convert to Relative Path.
#
@@ -12,29 +12,157 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-[ $# -eq 0 ] && {
- echo "ERROR: NO argument!" 1>&2
- 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
+}
+
+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
}
-if [ $# -eq 1 ]; then
- relativeTo=.
- files=("$@")
+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
- argv=("$@")
- argc=$#
+ argc=${#files[@]}
- # Get last argument
- relativeTo="${argv[argc - 1]}"
- files=( "${argv[@]:0:argc - 1}" )
+ # Get last argument
+ relativeTo=${files[argc - 1]}
+ files=("${files[@]:0:argc-1}")
fi
-[ -f "$relativeTo" ] && relativeTo="$(dirname "$relativeTo")"
+[ -f "$relativeTo" ] && relativeTo=$(dirname -- "$relativeTo")
+[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo): No such file or directory!"
+
+readonly files relativeTo
+
+################################################################################
+# biz logic
+################################################################################
-for f in "${files[@]}" ; do
- ! [ -e "$f" ] && {
- echo "$f does not exists!"
- continue
- }
- realpath "$f" --relative-to="$relativeTo"
+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 cf281890..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.
#
@@ -8,19 +8,9 @@
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-busy-java-threads
# @author Jerry Lee (oldratlee at gmail dot com)
# @author superhj1987 (superhj1987 at 126 dot com)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-PROG="$(basename "$0")"
+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?
@@ -28,77 +18,120 @@ PROG="$(basename "$0")"
# Will $0 always include the path to the script?
# https://unix.stackexchange.com/questions/119929
readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@")
-# Get current user name via whoami command
-# See https://www.lifewire.com/current-linux-user-whoami-command-3867579
-# Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside!
+# CAUTION: env var $USER is not reliable!
+# $USER may be overwritten; if run command by `sudo -u`, may is not `root`.
+# more info see https://www.baeldung.com/linux/get-current-user
#
-# NOTE: DO NOT declare var USER as readonly, because its value is supplied by subshell.
-USER="$(whoami)"
+# 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
+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" && -w "$append_file" ]] && echo "$@" >>"$append_file"
- [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG"
+ colorPrint "$color" "$*"
+ __appendToFile "$*"
}
# shellcheck disable=SC2120
-normalPrint() {
- echo "$@"
- [[ -n "$append_file" && -w "$append_file" ]] && echo "$@" >>"$append_file"
- [[ -n "$store_dir" && -w "$store_dir" ]] && echo "$@" >>"${store_file_prefix}$PROG"
+normalOutput() {
+ printf '%s\n' "$*"
+ __appendToFile "$*"
}
-redPrint() {
- colorPrint 31 "$@"
+redOutput() {
+ colorOutput 31 "$*"
}
-greenPrint() {
- colorPrint 32 "$@"
+greenOutput() {
+ colorOutput 32 "$*"
}
-yellowPrint() {
- colorPrint 33 "$@"
+yellowOutput() {
+ colorOutput 33 "$*"
}
-bluePrint() {
- colorPrint 36 "$@"
+blueOutput() {
+ colorOutput 36 "$*"
}
die() {
- redPrint "Error: $*" 1>&2
- 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
+}
+
+# Bash RegEx to check floating point numbers from user input
+# https://stackoverflow.com/questions/13790763
+isNonNegativeFloatNumber() {
+ [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]]
+}
+
+isNaturalNumber() {
+ [[ "$1" =~ ^[+]?[0-9]+$ ]]
+}
+
+isNaturalNumberList() {
+ [[ "$1" =~ ^([0-9]+)(,[0-9]+)*$ ]]
}
# print calling(quoted) command line which is able to copy and paste to rerun safely
@@ -106,39 +139,33 @@ logAndCat() {
# How to get the complete calling command of a BASH script from inside the script (not just the arguments)
# https://stackoverflow.com/questions/36625593
printCallingCommandLine() {
- local arg isFirst=true
- for arg in "${COMMAND_LINE[@]}"; do
- if $isFirst; then
- isFirst=false
- else
- printf ' '
- fi
- printf '%q' "$arg"
- done
- echo
+ local arg isFirst=true
+ for arg in "${COMMAND_LINE[@]}"; do
+ if $isFirst; then
+ isFirst=false
+ else
+ printf ' '
+ fi
+ printf '%q' "$arg"
+ done
+ echo
}
usage() {
- local -r exit_code="${1:-0}"
- (($# > 0)) && shift
- # shellcheck disable=SC2015
- [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
-
- (($# > 0)) && colorEcho 31 "$*$nl" >$out
-
- cat >$out < find out the highest cpu consumed threads from
- the specified java process. support pid list(eg: 42,99).
+ the specified java process.
+ support pid list(eg: 42,47).
default from all java process.
-c, --count set the thread count to show, default is 5.
set count 0 to show all threads.
@@ -155,389 +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
+# DO NOT declare and assign var ARGS(as readonly) in ONE line!
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 \
- -- "$@"
-) || {
- echo
- usage 1
-}
-eval set -- "${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
-use_ps=false
-top_delay=0.5
+cpu_sample_interval=0.5
while true; do
- 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
- ;;
- -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
-update_delay=${1:-0}
-# Bash RegEx to check floating point numbers from user input
-# https://stackoverflow.com/questions/13790763
-[[ "$update_delay" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] || die "update delay($update_delay) is not a positive float number!"
+readonly count cpu_sample_interval force mix_native_frames lock_info
+readonly update_delay=${1:-0}
+isNonNegativeFloatNumber "$update_delay" || die "update delay($update_delay) is not a non-negative float number!"
[ -z "$1" ] && update_count=1 || update_count=${2:-0}
-[[ "$update_count" =~ ^[+]?[0-9]+$ ]] || die "update count($update_count) is not a natural number!"
+isNaturalNumber "$update_count" || die "update count($update_count) is not a natural number!"
+readonly update_count
if [ -n "$pid_list" ]; then
- pid_list="${pid_list//[[:space:]]/}" # delete white space
- [[ "$pid_list" =~ ^([0-9]+)(,[0-9]+)*$ ]] || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67"
+ pid_list=${pid_list//[[:space:]]/} # delete white space
+ isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67"
fi
+readonly pid_list
-# check the directory of append-file(-a) mode, create if not exist.
+# 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 exist.
+# 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 executable!"
-elif command -v jstack &>/dev/null; then
- jstack_path="$(command -v jstack)"
-else
- [ -n "$JAVA_HOME" ] || die "jstack not found on PATH and No JAVA_HOME setting! Use -s option set jstack path manually."
- [ -f "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) file does NOT exists! Use -s option set jstack path manually."
- [ -x "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executable! Use -s option set jstack path manually."
+ # 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
################################################################################
-# NOTE: DO NOT declare var run_timestamp as readonly, because its value is supplied by subshell.
-run_timestamp="$(date "+%Y-%m-%d_%H:%M:%S.%N")"
+# 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]: $(printCallingCommandLine)"
- 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_list}" ]; then
- readonly ps_process_select_options="-p $pid_list"
+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
+ 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
-
- # shellcheck disable=SC2206
- local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --sort -pcpu --no-headers)
- # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell.
- local ps_out
- ps_out="$("${ps_cmd_line[@]}")"
- [ -n "$ps_out" ] || __die_when_no_java_process_found
-
- if [ -n "$store_dir" ]; then
- echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps"
- fi
-
- 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() {
- # DO NOT combine var java_pid_list declaration and assignment, because its value is supplied by subshell.
- local java_pid_list
- # shellcheck disable=SC2086
- java_pid_list="$(ps $ps_process_select_options -o pid --no-headers)"
- [ -n "$java_pid_list" ] || __die_when_no_java_process_found
- # shellcheck disable=SC2086
- java_pid_list="$(echo $java_pid_list | tr ' ' ,)" # join with ,
-
- # 1. sort by %cpu by top option `-o %CPU`
- # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+),
- # 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 -p "$java_pid_list")
- # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell.
- local top_out
- top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}")
- if [ -n "$store_dir" ]; then
- echo "$top_out" | logAndCat "${top_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_top"
- fi
-
- # DO NOT combine var result_threads_top_info declaration and assignment, because its value is supplied by subshell.
- local result_threads_top_info
- result_threads_top_info=$(
- echo "$top_out" | awk '{
- # from text line to empty line, increase block index
- if (previousLine && !$0) blockIndex++
- # only print 4th text block(blockIndex == 3), aka. process info of second top update
- if (blockIndex == 3 && $1 ~ /^[0-9]+$/)
- print $1, $9 # $1 is thread id field, $9 is %cpu field
- previousLine = $0
- }'
- )
- [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found
-
- echo "$result_threads_top_info" | 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
- # shellcheck disable=SC2206
- local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers)
- # DO NOT combine var ps_out declaration and assignment, because its value is supplied by subshell.
- local ps_out
- ps_out="$("${ps_cmd_line[@]}")"
- if [ -n "$store_dir" ]; then
- echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"${store_file_prefix}$((i + 1))_ps"
+ # 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 output_fields
- while read -r threadId pcpu; do
- ((count <= 0 || idx < count)) || break
-
- # output field: pid, threadId, pcpu, user
- output_fields="$(echo "$ps_out" |
- awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId {
- print $1, threadId, pcpu, $3; exit
- }')"
- if [ -n "$output_fields" ]; then
- ((idx++))
- echo "$output_fields"
- fi
- done
+ done
}
# output format is same as function findBusyJavaThreadsByPs
findBusyJavaThreadsByTop() {
- __top_threadId_cpu | __complete_pid_user_by_ps
+ __top_threadId_cpu | __complete_pid_user_by_ps
}
printStackOfThreads() {
- local idx=0 pid threadId pcpu user threadId0x
- while read -r pid threadId pcpu user; do
- threadId0x="0x$(printf %x "${threadId}")"
-
- ((idx++))
- local jstackFile="${store_file_prefix}$((i + 1))_jstack_${pid}"
- [ -f "${jstackFile}" ] || {
- # shellcheck disable=SC2206
- local -a jstack_cmd_line=("$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid})
- if [ "${user}" == "${USER}" ]; then
- # run without sudo, when java process user is current user
- logAndRun "${jstack_cmd_line[@]}" >"${jstackFile}"
- elif [ $UID == 0 ]; then
- # if java process user is not current user, must run jstack with sudo
- logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" >"${jstackFile}"
- else
- # current user is not root user, so can not run with sudo; print error message and rerun suggestion
- redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})."
- redPrint "User of java process($user) is not current user($USER), need sudo to rerun:"
- yellowPrint " sudo $(printCallingCommandLine)"
- normalPrint
- continue
- fi || {
- redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})."
- normalPrint
- rm "${jstackFile}" &>/dev/null
- continue
- }
- }
-
- bluePrint "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):"
-
- if [ -n "$mix_native_frames" ]; then
- local sed_script="/--------------- $threadId ---------------/,/^---------------/ {
- /--------------- $threadId ---------------/b # skip first separator line
- /^---------------/d # delete second separator line
- p
- }"
- elif [ -n "$force" ]; then
- local sed_script="/^Thread ${threadId}:/,/^$/ {
- /^$/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" || -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 439c9f4c..942ad2e9 100755
--- a/bin/show-duplicate-java-classes
+++ b/bin/show-duplicate-java-classes
@@ -1,14 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Function
-# Find duplicate classes among java lib dirs and class dirs..
+# Find duplicate classes among java lib dirs and class dirs.
#
# @Usage
# $ show-duplicate-java-classes # search jars from current dir
# $ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2
# $ show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2
# $ show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1
-# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the sub-directories of lib dir
+# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the subdirectories of lib dir
# $ show-duplicate-java-classes -J path/to/lib_dir1 # search jars in the jar file
#
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-duplicate-java-classes
@@ -24,12 +24,13 @@ from glob import glob
from io import BytesIO
from optparse import OptionParser
from os import walk
-from os.path import relpath, isdir, exists
-from zipfile import ZipFile, BadZipfile
+from os.path import exists, isdir, relpath
+from zipfile import BadZipfile, ZipFile
################################################################################
# utils functions
################################################################################
+PROG_VERSION = '2.x-dev'
# How to delete line with echo?
# https://unix.stackexchange.com/questions/26576
@@ -83,13 +84,43 @@ def print_box_message(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()
- idx_str_max_len = len(str(len(lib_dirs)))
+ max_idx_str_len = str_len(len(lib_dirs))
for idx, lib_dir in enumerate(lib_dirs, start=1):
- print_responsive_message('list jar file under lib dir(%*s/%s): %s' %
- (idx_str_max_len, idx, len(lib_dirs), lib_dir))
+ 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)
@@ -121,8 +152,11 @@ def list_class_under_jar_file(jar_file, recursive, progress):
def list_zip_in_zip(jar_jar_path, zf):
nonlocal index
index += 1
+ index_marker = ''
+ if recursive:
+ index_marker = ' #%3s' % index
print_responsive_message('list class under jar file(%*s/%s%s): %s' % (
- len(str(progress[1])), progress[0], progress[1], ' #' + str(index) if recursive else '', jar_jar_path))
+ str_len(progress[1]), progress[0], progress[1], index_marker, jar_jar_path))
ret = {}
classes = {f for f in zf.namelist() if f.lower().endswith('.class')}
@@ -150,8 +184,8 @@ def list_class_under_jar_file(jar_file, recursive, progress):
def list_class_under_class_dir(class_dir, progress):
- print_responsive_message('list class under class dir(%*s/%s): %s' %
- (len(str(progress[1])), progress[0], progress[1], class_dir))
+ print_responsive_message('list class under class dir(%*s/%s): %s' % (
+ 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)
@@ -165,7 +199,7 @@ def list_class_under_class_dir(class_dir, progress):
for filename in file_names if filename.lower().endswith('.class')}
-def collect_class_path_to_classes(jar_files, class_dirs, recursive_jar):
+def collect_class_path_to_classes(class_dirs, jar_files, recursive_jar):
class_path_to_classes = {}
total_count = len(jar_files) + len(class_dirs)
index = 0
@@ -194,7 +228,7 @@ def invert_as_class_to_class_paths(class_path_to_classes):
# biz functions
################################################################################
-_java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$')
+__java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$')
def find_duplicate_classes(class_to_class_paths):
@@ -202,7 +236,7 @@ def find_duplicate_classes(class_to_class_paths):
for clazz, class_paths in class_to_class_paths.items():
# skip java 9 module-info files
- if len(class_paths) == 1 or _java9_module_file_pattern.match(clazz):
+ if len(class_paths) == 1 or __java9_module_file_pattern.match(clazz):
continue
classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set())
@@ -211,49 +245,69 @@ def find_duplicate_classes(class_to_class_paths):
return class_paths_to_duplicate_classes
-def print_duplicate_classes_info(class_paths_to_duplicate_classes):
+def print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes):
if not class_paths_to_duplicate_classes:
print('COOL! No duplicate classes found!')
return
duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values())
- # sort kv pairs
- class_paths_to_duplicate_classes = sorted(class_paths_to_duplicate_classes.items(), reverse=True,
- key=lambda item: (len(item[0]), len(item[1])))
+ class_paths_total_count = sum(len(cps) for cps in class_paths_to_duplicate_classes)
+ print('Found %s duplicate classes in %s class paths and %s class path sets:' % (
+ duplicate_classes_total_count, class_paths_total_count, len(class_paths_to_duplicate_classes)))
+
# sort key(class_paths) and value(duplicate_classes)
class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs))
- for cps, dcs in class_paths_to_duplicate_classes]
-
- print('Found %s duplicate classes in %s class path set:' %
- (duplicate_classes_total_count, len(class_paths_to_duplicate_classes)))
-
- idx_str_max_len = len(str(len(class_paths_to_duplicate_classes)))
+ for 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]))
+
+ max_idx_str_len = str_len(len(class_paths_to_duplicate_classes))
for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1):
- print('[%*s] found %s duplicate classes in %s class paths:\n %s' % (
- idx_str_max_len, idx,
- len(classes), len(class_paths),
- '\n '.join(class_paths)
- ))
+ 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 :' % (
- idx_str_max_len, idx,
- len(classes), len(class_paths), ' '.join(class_paths)
- ))
- class_idx_str_max_len = len(str(len(classes)))
+ 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(' %*d: %s' % (class_idx_str_max_len, i, c))
+ print(' %*s: %s' % (max_class_idx_str_len, i, c))
-def print_class_paths_info(class_paths):
- class_paths = sorted(class_paths)
+def print_class_paths_info(class_path_to_classes):
+ if not class_path_to_classes:
+ return
- print_box_message('Find in %s class paths:' % len(class_paths))
+ max_idx_str_len = str_len(len(class_path_to_classes))
+ max_classes_count_str_len = str_len(max(len(classes) for classes in class_path_to_classes.values()))
- idx_str_max_len = len(str(len(class_paths)))
- for idx, class_path in enumerate(class_paths, start=1):
- print('%*d: %s' % (idx_str_max_len, idx, class_path))
+ class_path_to_classes = sorted(class_path_to_classes.items(), key=lambda item: item[0])
+ print_box_message('Find in %s class paths:' % len(class_path_to_classes))
+ for idx, (cp, classes) in enumerate(class_path_to_classes, start=1):
+ print('%*s: (contain %*s classes) %s' % (
+ max_idx_str_len, idx, max_classes_count_str_len, len(classes), cp))
def main():
@@ -268,8 +322,8 @@ def main():
'\n %prog -c path/to/class_dir1 -c /path/to/class_dir2'
'\n %prog -c path/to/class_dir1 path/to/lib_dir1'
'\n %prog -L path/to/lib_dir1'
- '\n %prog -J path/to/lib_dir1'
- )
+ '\n %prog -J path/to/lib_dir1',
+ version='%prog ' + PROG_VERSION)
option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False,
action='store_true', help='search jars in the sub-directories of lib dir')
option_parser.add_option('-J', '--recursive-jar', dest='recursive_jar', default=False,
@@ -278,23 +332,32 @@ def main():
action='append', help='add class dir')
option_parser.add_option('-R', '--no-find-progress', dest='show_responsive', default=True,
action='store_false', help='do not display responsive find progress')
+
options, lib_dirs = option_parser.parse_args()
+ class_dirs = options.class_dirs
+ if not lib_dirs and not class_dirs:
+ lib_dirs = ['.']
global __show_responsive
__show_responsive = options.show_responsive
- if not options.class_dirs and not lib_dirs:
- lib_dirs = ['.']
- class_path_to_classes = collect_class_path_to_classes(
- list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib),
- options.class_dirs, recursive_jar=options.recursive_jar)
+ jar_files = list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib)
+ 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)
- print_class_paths_info(class_path_to_classes.keys())
+ print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes)
+ print_class_paths_info(class_path_to_classes)
return int(bool(class_paths_to_duplicate_classes))
diff --git a/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 f16bc7b7..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.
#
@@ -10,9 +10,51 @@
# @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
+# shellcheck disable=SC2086
netstat -tna ${option_for_mac:-} | awk 'NR > 2 {
++s[$NF]
}
diff --git a/bin/uq b/bin/uq
index ad86a895..535f9a85 100755
--- a/bin/uq
+++ b/bin/uq
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output).
# same as `uniq` command in core utils,
@@ -9,75 +9,86 @@
#
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq
# @author Zava Xu (zava.kid at gmail dot com)
-# @author Jerry Lee (oldratlee at gmail dot com)# NOTE about Bash Traps and Pitfalls:
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
+# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-PROG="$(basename "$0")"
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
################################################################################
# util functions
################################################################################
# NOTE: $'foo' is the escape sequence syntax of bash
-readonly ec=$'\033' # escape char
-readonly eend=$'\033[0m' # escape end
-readonly nl=$'\n' # new line
-
-redEcho() {
- [ -t 1 ] && echo "${ec}[1;31m$*$eend" || echo "$*"
+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
}
-yellowEcho() {
- [ -t 1 ] && echo "${ec}[1;33m$*$eend" || echo "$*"
+yellowPrint() {
+ if [ -t 1 ]; then
+ printf '\e[1;33m%s\e[0m\n' "$*"
+ else
+ printf '%s\n' "$*"
+ fi
}
die() {
- redEcho "Error: $*" 1>&2
- exit 1
-}
-
-convertHumanReadableSizeToSize() {
- local human_readable_size="$1"
-
- [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1
-
- local size="${BASH_REMATCH[1]}" unit="${BASH_REMATCH[2]}"
- case "$unit" in
- g)
- ((size *= 1024 * 1024 * 1024))
- ;;
- m)
- ((size *= 1024 * 1024))
- ;;
- k)
- ((size *= 1024))
- ;;
+ 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
- echo "$size"
-}
+ (($# > 0)) && redPrint "$PROG: $*"
+ $prompt_help && echo "Try '$PROG --help' for more information."
-usage() {
- local -r exit_code="${1:-0}"
- (($# > 0)) && shift
- # shellcheck disable=SC2015
- [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+ exit "$exit_status"
+} >&2
- (($# > 0)) && redEcho "$*$nl" >$out
+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"
+}
- cat >$out < 0)); do
- case "$1" in
- -c | --count)
- uq_opt_count=1
- shift
- ;;
- -d | --repeated)
- uq_opt_only_repeated=1
- shift
- ;;
- -D)
- uq_opt_all_repeated=1
- shift
- ;;
- --all-repeated=*)
- uq_opt_all_repeated=1
-
- uq_opt_repeated_method=$(echo "$1" | awk -F= '{print $2}')
- [[ $uq_opt_repeated_method == 'none' || $uq_opt_repeated_method == 'prepend' || $uq_opt_repeated_method == 'separate' ]] ||
- usage 1 "$PROG: invalid argument ‘${uq_opt_repeated_method}’ for ‘--all-repeated’${nl}Valid arguments are:$nl - ‘none’$nl - ‘prepend’$nl - ‘separate’"
-
- shift
- ;;
- -u | --unique)
- uq_opt_only_unique=1
- shift
- ;;
- -i | --ignore-case)
- uq_opt_ignore_case=1
- shift
- ;;
- -z | --zero-terminated)
- uq_opt_zero_terminated=1
- shift
- ;;
- -XM | --max-input)
- uq_max_input_human_readable_size="$2"
- shift 2
- ;;
- -h | --help)
- usage
- ;;
- --)
- shift
- argv=("${argv[@]}" "$@")
- break
- ;;
- -)
- argv=(${argv[@]:+"${argv[@]}"} "$1")
- shift
- ;;
- -*)
- usage 2 "${PROG}: unrecognized option '$1'"
- ;;
- *)
- argv=(${argv[@]:+"${argv[@]}"} "$1")
- shift
- ;;
- esac
+ case "$1" in
+ -c | --count)
+ uq_opt_count=1
+ shift
+ ;;
+ -d | --repeated)
+ uq_opt_only_repeated=1
+ shift
+ ;;
+ -D)
+ uq_opt_all_repeated=1
+ shift
+ ;;
+ --all-repeated=*)
+ uq_opt_all_repeated=1
+
+ uq_opt_repeated_method=${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 ]] &&
- usage 2 "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless"
-[[ $uq_opt_all_repeated == 1 && $uq_opt_only_unique == 1 ]] &&
- usage 2 "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless"
+[[ $uq_opt_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) ]] &&
- yellowEcho "[$PROG] WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2
+[[ $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
-# NOTE: DO NOT declare var uq_max_input_size as readonly, because its value is supplied by subshell.
-uq_max_input_size="$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size")" ||
- usage 2 "[$PROG] ERROR: illegal value of option -XM/--max-input: $uq_max_input_human_readable_size"
+# 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[@]}
+readonly argc=${#argv[@]} argv uq_max_input_size
if ((argc == 0)); then
- input_files=()
- output_file=/dev/stdout
+ input_files=()
+ output_file=/dev/stdout
elif ((argc == 1)); then
- input_files=("${argv[0]}")
- output_file=/dev/stdout
+ input_files=("${argv[0]}")
+ output_file=/dev/stdout
else
- input_files=("${argv[@]:0:argc-1}")
- output_file=${argv[argc - 1]}
- if [ "$output_file" == - ]; then
- output_file=/dev/stdout
- fi
+ input_files=("${argv[@]:0:argc-1}")
+ output_file=${argv[argc - 1]}
+ if [ "$output_file" = - ]; then
+ output_file=/dev/stdout
+ fi
fi
+readonly output_file
# Check input file
for f in ${input_files[@]:+"${input_files[@]}"}; do
- # - is stdin, ok
- [ "$f" == - ] && continue
+ # - is stdin, ok
+ [ "$f" = - ] && continue
- [ -e "$f" ] || die "input file $f does not exist!"
- [ ! -d "$f" ] || die "input file $f exists, but is a directory!"
- [ -f "$f" ] || die "input file $f exists, but is not a file!"
- [ -r "$f" ] || die "input file $f exists, but is not readable!"
+ [ -e "$f" ] || die "input file $f: 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
@@ -275,7 +298,7 @@ BEGIN {
{
total_input_size += length + 1
if (total_input_size > uq_max_input_size) {
- printf "[%s] ERROR: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n",
+ printf "%s: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n",
uq_PROG, uq_max_input_human_readable_size > "/dev/stderr"
exit(1)
}
@@ -299,16 +322,16 @@ END {
'
awk \
- -v "uq_opt_count=$uq_opt_count" \
- -v "uq_opt_only_repeated=$uq_opt_only_repeated" \
- -v "uq_opt_all_repeated=$uq_opt_all_repeated" \
- -v "uq_opt_repeated_method=$uq_opt_repeated_method" \
- -v "uq_opt_only_unique=$uq_opt_only_unique" \
- -v "IGNORECASE=$uq_opt_ignore_case" \
- -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \
- -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \
- -v "uq_max_input_size=$uq_max_input_size" \
- -v "uq_PROG=$PROG" \
- -f <(printf "%s" "$uq_awk_script") \
- -- ${input_files[@]:+"${input_files[@]}"} \
- >"$output_file"
+ -v "uq_opt_count=$uq_opt_count" \
+ -v "uq_opt_only_repeated=$uq_opt_only_repeated" \
+ -v "uq_opt_all_repeated=$uq_opt_all_repeated" \
+ -v "uq_opt_repeated_method=$uq_opt_repeated_method" \
+ -v "uq_opt_only_unique=$uq_opt_only_unique" \
+ -v "IGNORECASE=$uq_opt_ignore_case" \
+ -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \
+ -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \
+ -v "uq_max_input_size=$uq_max_input_size" \
+ -v "uq_PROG=$PROG" \
+ -f <(printf "%s" "$uq_awk_script") \
+ -- ${input_files[@]:+"${input_files[@]}"} \
+ >"$output_file"
diff --git a/bin/xpf b/bin/xpf
index 2b9f5b5a..34af6d91 100755
--- a/bin/xpf
+++ b/bin/xpf
@@ -1,7 +1,7 @@
-#!/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
@@ -10,6 +10,27 @@
# @author Jerry Lee (oldratlee at gmail dot com)
set -eEuo pipefail
-# BASE="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
-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 6401cba5..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,115 +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)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
set -eEuo pipefail
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-PROG="$(basename "$0")"
+readonly PROG=${0##*/}
+readonly PROG_VERSION='2.x-dev'
-readonly nl=$'\n' # new line
+################################################################################
+# util functions
+################################################################################
-usage() {
- local -r exit_code="${1:-0}"
- (($# > 0)) && shift
- # shellcheck disable=SC2015
- [ "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout
+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 "$*$nl" >$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."
- 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[@]}")
+has_error=false
+
for file in "${files[@]}"; do
- [ ! -e "$file" ] && {
- echo "$file not existed!"
- continue
- }
+ [ -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 981a79eb..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
@@ -121,7 +123,8 @@ Example:
Output control:
-p, --pid find out the highest cpu consumed threads from
- the specified java process. support pid list(eg: 42,99).
+ the specified java process.
+ support pid list(eg: 42,47).
default from all java process.
-c, --count set the thread count to show, default is 5.
set count 0 to show all threads.
@@ -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)
@@ -273,6 +272,7 @@ Examples:
show-duplicate-java-classes -J path/to/lib_dir1
Options:
+ --version show program's version number and exit
-h, --help show this help message and exit
-L, --recursive-lib search jars in the sub-directories of lib dir
-J, --recursive-jar search jars in the jar file
@@ -323,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")
@@ -354,32 +354,33 @@ COOL! No duplicate classes found!
================================================================================
Find in 150 class paths:
================================================================================
- 1: WEB-INF/lib/aopalliance-1.0.jar
- 2: WEB-INF/lib/asm-3.2.jar
- 3: WEB-INF/lib/aspectjrt-1.6.1.jar
- 4: WEB-INF/lib/aspectjweaver-1.6.6.jar
+ 1: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar
+ 2: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar
+ 3: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar
+ 4: (contain 687 classes) WEB-INF/lib/cassandra-0.6.1.jar
...
$ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib
-Found duplicate classes in below 9 class path set:
-[1] found 188 duplicate classes in 2 class paths:
- WEB-INF/lib/jdom-2.0.2.jar
- WEB-INF/lib/jdom2-2.0.6.jar
-[2] found 150 duplicate classes in 2 class paths:
- WEB-INF/lib/netty-all-4.0.35.Final.jar
- WEB-INF/lib/netty-common-4.1.31.Final.jar
-[3] found 148 duplicate classes in 2 class paths:
- WEB-INF/lib/netty-all-4.0.35.Final.jar
- WEB-INF/lib/netty-handler-4.1.31.Final.jar
-[4] found 103 duplicate classes in 2 class paths:
- WEB-INF/lib/hessian-3.0.14.bugfix-tae3.jar
- WEB-INF/lib/hessian-4.0.38.jar
+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] found 188 duplicate classes in 2 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar :
+[1] found 188 duplicate classes in 3 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar WEB-INF/lib/jdom2-2.0.8.jar :
1: org/jdom2/Attribute.class
2: org/jdom2/AttributeList$1.class
3: org/jdom2/AttributeList$ALIterator.class
@@ -398,18 +399,18 @@ Duplicate classes detail info:
================================================================================
Find in 232 class paths:
================================================================================
- 1: WEB-INF/classes
- 2: WEB-INF/lib/HikariCP-2.7.8.jar
- 3: WEB-INF/lib/accessors-smart-1.2.jar
- 4: WEB-INF/lib/alimonitor-jmonitor-1.1.3.jar
- 5: WEB-INF/lib/aopalliance-1.0.jar
- 6: WEB-INF/lib/asm-5.0.4.jar
+ 1: (contain 42 classes) WEB-INF/classes
+ 2: (contain 70 classes) WEB-INF/lib/HikariCP-2.7.8.jar
+ 3: (contain 13 classes) WEB-INF/lib/accessors-smart-1.2.jar
+ 4: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar
+ 5: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar
+ 6: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar
...
```
### 贡献者
-[tgic](https://github.com/tg123)提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673)
+[tgic](https://github.com/tg123) 提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673)
@@ -493,6 +494,7 @@ Output control:
Miscellaneous:
-h, --help display this help and exit
+ -V, --version display version information and exit
```
注意,Pattern缺省是`grep`的 **扩展**正则表达式。
@@ -536,4 +538,4 @@ WEB-INF/lib/javax.servlet-api-3.0.1.jar
### 参考资料
-[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.com/458/tech/shell/find-file-in-jar-zip-files.html)
+[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.github.io/458/tech/shell/find-file-in-jar-zip-files.html)
diff --git a/docs/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 d4f7e6ab..4ff27aac 100644
--- a/docs/shell.md
+++ b/docs/shell.md
@@ -4,40 +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%A4%BA%E4%BE%8B-2)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-3)
- [🍺 ap and rp](#-ap-and-rp)
- - [示例](#%E7%A4%BA%E4%BE%8B-3)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-4)
+ - [🍺 cp-into-docker-run](#-cp-into-docker-run)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-5)
- [🍺 tcp-connection-state-counter](#-tcp-connection-state-counter)
- - [用法](#%E7%94%A8%E6%B3%95)
- - [示例](#%E7%A4%BA%E4%BE%8B-4)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-6)
- [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85)
- [🍺 xpl and xpf](#-xpl-and-xpf)
- - [用法](#%E7%94%A8%E6%B3%95-1)
- - [示例](#%E7%A4%BA%E4%BE%8B-5)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-7)
- [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-1)
- [`Shell`开发/测试加强](#shell%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95%E5%8A%A0%E5%BC%BA)
- [🍺 echo-args](#-echo-args)
- - [示例](#%E7%A4%BA%E4%BE%8B-6)
+ - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-8)
- [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F)
- [🍺 console-text-color-themes.sh](#-console-text-color-themessh)
- - [用法](#%E7%94%A8%E6%B3%95-2)
- - [示例](#%E7%A4%BA%E4%BE%8B-7)
+ - [用法](#%E7%94%A8%E6%B3%95)
+ - [示例](#%E7%A4%BA%E4%BE%8B)
- [运行效果](#%E8%BF%90%E8%A1%8C%E6%95%88%E6%9E%9C)
- [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-2)
- [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99-1)
- [🍺 parseOpts.sh](#-parseoptssh)
- - [用法](#%E7%94%A8%E6%B3%95-3)
- - [示例](#%E7%A4%BA%E4%BE%8B-8)
+ - [用法](#%E7%94%A8%E6%B3%95-1)
+ - [示例](#%E7%A4%BA%E4%BE%8B-1)
- [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7)
- [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-3)
@@ -52,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)。
### 用法/示例
@@ -99,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
@@ -108,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
@@ -133,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
@@ -145,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
```
注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,如下:
@@ -183,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
@@ -253,7 +254,7 @@ $ sort foo.txt | uniq -c
# 输入行重排序了!
```
-### 示例
+### 用法/示例
```bash
$ uq foo.txt # 输入是文件
@@ -327,6 +328,7 @@ Options:
default is 256m
avoid consuming large memory unexpectedly
-h, --help display this help and exit
+ -V, --version display version information and exit
```
🍺 [ap](../bin/ap) and [rp](../bin/rp)
@@ -335,9 +337,9 @@ Options:
批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
-命令名`ap`意思是`Absolute Path`,`rp`是`Relative Path`。
+命令名`ap`的意思是`Absolute Path`,`rp`是`Relative Path`。
-### 示例
+### 用法/示例
```bash
# ap缺省打印当前路径的绝对路径
@@ -361,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
+```
+
@@ -376,13 +422,7 @@ $ rp /home /etc/../etc /home/admin
- 是否有攻击,查看`SYN_RECV`数(`SYN`攻击)
- `TIME_WAIT`数,太多会导致`TCP: time wait bucket table overflow`。
-### 用法
-
-```bash
-tcp-connection-state-counter
-```
-
-### 示例
+### 用法/示例
```bash
$ tcp-connection-state-counter
@@ -395,7 +435,7 @@ SYN_SENT 7
### 贡献者
-[sunuslee](https://github.com/sunuslee)改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56)
+[sunuslee](https://github.com/sunuslee) 改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56)
🍺 [xpl](../bin/xpl) and [xpf](../bin/xpf)
----------------------
@@ -404,11 +444,11 @@ SYN_SENT 7
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
- `xpl`:在文件浏览器中打开指定的文件或文件夹。
- `xpl`是`explorer`的缩写。
+ `xpl`是`explorer`的缩写。
- `xpf`: 在文件浏览器中打开指定的文件或文件夹,并选中。
- `xpf`是`explorer and select file`的缩写。
+ `xpf`是`EXplorer and select File`的缩写。
-### 用法
+### 用法/示例
```bash
xpl
@@ -420,11 +460,9 @@ xpf
# 缺省打开当前目录
xpf <文件或是目录>...
# 打开多个文件或目录
-```
-### 示例
-```bash
+# 示例
xpl /path/to/dir
xpl /path/to/foo.txt
xpl /path/to/dir1 /path/to/foo1.txt
@@ -434,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`开发/测试加强
====================================
@@ -450,7 +488,7 @@ xpf /path/to/dir1 /path/to/foo1.txt
这个脚本输出脚本收到的参数。在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。
-### 示例
+### 用法/示例
```bash
$ ./echo-args 1 " 2 foo " "3 3"
@@ -501,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)
----------------------
@@ -513,9 +552,9 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!"
命令行选项解析库,加强支持选项有多个值(即数组)。
支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。
-自己写一个命令行选项解析函数,是因为[`bash`](http://linux.die.net/man/1/bash)的`builtin`命令[`getopts`](http://linux.die.net/man/1/getopts)和加强版本命令[`getopt`](http://linux.die.net/man/1/getopt)都不支持数组的值。
+自己写一个命令行选项解析函数,是因为[`bash`](https://manned.org/bash)的`builtin`命令[`getopts`](https://manned.org/man/getopts.1)和加强版本命令[`getopt`](https://manned.org/getopt)都不支持数组的值。
-指定选项的多个值(即数组)的风格模仿[`find`](http://linux.die.net/man/1/find)命令的`-exec`选项:
+指定选项的多个值(即数组)的风格模仿[`find`](https://manned.org/find)命令的`-exec`选项:
```bash
$ find . -name \*.txt -exec echo "find file: " {} \;
@@ -532,10 +571,10 @@ find file: bar.txt
选项说明最后可以有选项类型说明:
-- `-`: 无参数的选项。即有选项则把值设置成`true`。这是 ***缺省*** 的类型。
+- `-`: 无参数的选项。既有选项则把值设置成`true`。这是 ***缺省*** 的类型。
- `:`: 有参数的选项,值只有一个。
- `+`: 有多个参数值的选项。值列表要以`;`表示结束。
- 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。
+ 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。
实际要解析的输入参数往往是你的脚本参数,这样`parseOpts`函数调用一般是:
@@ -548,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`来获取。
按照惯例,输入参数中如果有`--`表示之后参数中不再有选项,即之后都是参数。
@@ -592,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 2c9a4d94..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.
#
@@ -8,61 +8,76 @@
#
# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-cp-svn-url
# @author ivanzhangwb (ivanzhangwb at gmail dot com)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
-# NOTE: DO NOT declare var PROG as readonly, because its value is supplied by subshell.
-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
-[ $# -gt 1 ] && { echo At most 1 local directory is need! ; usage 1; }
+################################################################################
+# biz logic
+################################################################################
+
+(($# > 1)) && {
+ echo At most 1 local directory is need!
+ usage 1
+}
readonly dir="${1:-.}"
-# NOTE: DO NOT declare var url as readonly, because its value is supplied by subshell.
-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 cf358383..249fa7d2 100755
--- a/legacy-bin/svn-merge-stop-on-copy
+++ b/legacy-bin/svn-merge-stop-on-copy
@@ -1,4 +1,4 @@
-#!/bin/bash
+#!/usr/bin/env bash
# @Function
# svn merge commit between version when source branch copy(--stop-on-copy)
# and head version of source branch.
@@ -11,79 +11,79 @@
# @author jiangjizhong(@jzwlqx)
# @author Jerry Lee (oldratlee at gmail dot com)
-PROG="$(basename "$0")"
+readonly PROG=${0##*/}
usage() {
- cat < [target branch]
+ cat < [target branch]
svn merge commit between version when source branch copy(--stop-on-copy)
and head version of source branch.
Source branch must be a remote branch.
-Example:
- ${PROG} http://www.foo.com/project1/branches/feature1
- # merge http://www.foo.com/project1/branches/feature1 to current svn directory
+Example:
+ $PROG http://www.foo.com/project1/branches/feature1
+ # merge http://www.foo.com/project1/branches/feature1 to current svn directory
- ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/directory
- # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory
- # will prompt confirm for committing to target branch.
+ $PROG http://www.foo.com/project1/branches/feature1 /path/to/svn/directory
+ # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory
+ # will prompt confirm for committing to target branch.
- ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2
- # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2
- # because http://www.foo.com/project1/branches/feature2 is remote url,
- # will check out target branch to tmp directory, and prompt confirm for committing to target branch.
+ $PROG http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2
+ # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2
+ # because http://www.foo.com/project1/branches/feature2 is remote url,
+ # will check out target branch to tmp directory, and prompt confirm for committing to target branch.
EOF
- # shellcheck disable=SC2086
- exit $1
+
+ 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
svn_status_line=$(svn status --ignore-externals "$workDir" | grep -c -v ^X)
[ "$svn_status_line" -ne 0 ] && {
- echo "svn work directory is modified!"
- exit 1
+ echo "svn work directory is modified!"
+ exit 1
}
cd "$workDir" &&
-if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then
- echo "oldest version($from_version) of source branch $source_branch ."
- echo "starting merge to $workDir ."
- svn merge "-${from_version}:HEAD" "$source_branch"
-else
- echo "Fail to merge to work dir $workDir ."
- exit 2
-fi
+ if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then
+ echo "oldest version($from_version) of source branch $source_branch ."
+ echo "starting merge to $workDir ."
+ svn merge "-$from_version:HEAD" "$source_branch"
+ else
+ echo "Fail to merge to work dir $workDir ."
+ exit 2
+ fi
read -r -p "Check In? (Y/N)" ci
-[ "$ci" == "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch"
+[ "$ci" = "Y" ] && svn ci -m "svn merge -$from_version:HEAD $source_branch"
diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk
index 902f3f6c..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,40 +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=("$@")
+# if dirs is empty, use "."
+dirs=("${dirs[@]:-.}")
for d in "${dirs[@]}"; do
- [ ! -d "${d}/.svn" ] && {
- redEcho "directory $d is not a svn work directory, ignore directory $d !"
- continue
- }
- (
- cd "$d" &&
- branches=$(svn info | grep '^URL' | awk '{print $2}') &&
- trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk &&
- if svn sw "$trunk"; then
- greenEcho "svn work directory $d switch from ${branches} to ${trunk} ."
- else
- redEcho "fail to switch $d to trunk!"
- fi
- )
+ [ ! -d "$d/.svn" ] && {
+ redEcho "directory $d is not a svn work directory, ignore directory $d !"
+ continue
+ }
+ (
+ cd "$d" &&
+ branches=$(svn info | grep '^URL' | awk '{print $2}') &&
+ trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk &&
+ if svn sw "$trunk"; then
+ greenEcho "svn work directory $d switch from $branches to $trunk ."
+ else
+ redEcho "fail to switch $d to trunk!"
+ fi
+ )
done
diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh
index 4e6cdc54..70833615 100755
--- a/lib/console-text-color-themes.sh
+++ b/lib/console-text-color-themes.sh
@@ -1,85 +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)
-#
-# NOTE about Bash Traps and Pitfalls:
-#
-# 1. DO NOT combine var declaration and assignment which value supplied by subshell!
-# for example: readonly var1=$(echo value1)
-# local var1=$(echo value1)
-#
-# declaration make exit code of assignment to be always 0,
-# aka. the exit code of command in subshell is discarded.
-# tested on bash 3.2.57/4.2.46
-
-_ctct_READLINK_CMD=readlink
-if command -v greadlink > /dev/null; then
- _ctct_READLINK_CMD=greadlink
-fi
-
-# NOTE: DO NOT declare var _ctct_PROG as readonly, because its value is supplied by subshell.
-_ctct_PROG="$(basename "$($_ctct_READLINK_CMD -f "${BASH_SOURCE[0]}")")"
-[ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true
-
-readonly _ctct_ec=$'\033' # escape char
-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 34464458..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,41 +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
-_opts_SED_CMD=sed
-if command -v gsed &> /dev/null; then
+# 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" | $_opts_SED_CMD '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
@@ -61,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
- $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' |
- awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}'`
-
- local optDesc
- while read optDesc ; do # optDesc LIKE b,b-long:
- [ -z "$optDesc" ] && continue
-
- local mode="${optDesc:(-1)}" # LIKE : or +
- case "$mode" in
- +|:|-)
- optDesc="${optDesc:0:(${#optDesc}-1)}" # LIKE b,b-long
- ;;
- *)
- mode="-"
- ;;
- esac
-
- local optLines=`echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` # LIKE "a\na-long"
-
- [ $(echo "$optLines" | wc -l) -gt 2 ] && {
- _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2
- _opts_cleanOptValueInfoList
- return 220
- }
+ local optsDescription="$1" # optsDescription LIKE a,a-long|b,b-long:|c,c-long+
+ shift
+
+ _OPT_INFO_LIST_INDEX=() # set global var!
+
+ local optDescLines
+ 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" | $_opts_SED_CMD -r 's/^--?//'`
- local mode=`_opts_findOptMode "$optName"`
- case "$mode" in
- -)
- _opts_setOptBool "$optName" "true"
- shift
- ;;
- :)
- [ $# -lt 2 ] && {
- _opts_redEcho "Option $opt has NO value!" 1>&2
- _opts_cleanOptValueInfoList
- return 231
- }
- _opts_setOptValue "$optName" "$2"
- shift 2
- ;;
- +)
- shift
- local -a valueArray=()
- local foundComma=""
-
- local value
- for value in "$@" ; do
- [ ";" == "$value" ] && {
- foundComma=true
- break
- } || valueArray=("${valueArray[@]}" "$value")
- done
- [ "$foundComma" ] || {
- _opts_redEcho "value of option $opt no end comma, value = ${valueArray[@]}" 1>&2
- _opts_cleanOptValueInfoList
- return 231
- }
- shift "$((${#valueArray[@]} + 1))"
- _opts_setOptArray "$optName" "${valueArray[@]}"
- ;;
- *)
- _opts_redEcho "Undefined option $opt!" 1>&2
- _opts_cleanOptValueInfoList
- return 232
- ;;
- esac
- ;;
- *)
- args=("${args[@]}" "$1")
- shift
- ;;
- esac
- done
- _OPT_ARGS=("${args[@]}") # set global var!
+ } || valueArray=("${valueArray[@]}" "$value")
+ done
+ [ "$foundComma" ] || {
+ _opts_redEcho "value of option $opt no end comma, value = ${valueArray[*]}" 1>&2
+ _opts_cleanOptValueInfoList
+ return 231
+ }
+ shift "$((${#valueArray[@]} + 1))"
+ _opts_setOptArray "$optName" "${valueArray[@]}"
+ ;;
+ *)
+ _opts_redEcho "Undefined option $opt!" 1>&2
+ _opts_cleanOptValueInfoList
+ return 232
+ ;;
+ esac
+ ;;
+ *)
+ args=("${args[@]}" "$1")
+ shift
+ ;;
+ esac
+ done
+ # set global var!
+ _OPT_ARGS=("${args[@]}")
}
#####################################################################
@@ -320,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
index efc1fd38..cbcdbe5c 100755
--- a/test-cases/integration-test.sh
+++ b/test-cases/integration-test.sh
@@ -1,60 +1,57 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -eEuo pipefail
-READLINK_CMD=readlink
-if command -v greadlink &> /dev/null; then
- READLINK_CMD=greadlink
-fi
-
-cd "$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")"
-
-################################################################################
-# constants
-################################################################################
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
-# 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
+cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")"
################################################################################
# common util functions
################################################################################
colorEcho() {
- local color=$1
- shift
-
- # if stdout is the console, turn on color output.
- [ -t 1 ] && echo "${ec}[1;${color}m$*${eend}" || echo "$*"
+ 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 "$@"
+ colorEcho 31 "$@"
}
yellowEcho() {
- colorEcho 33 "$@"
+ colorEcho 33 "$@"
}
blueEcho() {
- colorEcho 36 "$@"
+ colorEcho 36 "$@"
}
logAndRun() {
- local simple_mode=false
- [ "$1" == "-s" ] && {
- simple_mode=true
- shift
- }
-
- if $simple_mode; then
- echo "Run under work directory $PWD : $*"
- "$@"
- else
- blueEcho "Run under work directory $PWD :$nl$*"
- time "$@"
- fi
+ 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
}
################################################################################
@@ -62,5 +59,5 @@ logAndRun() {
################################################################################
for test_case in *_test.sh; do
- logAndRun ./"$test_case"
+ 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
index 2c8bd79b..df2cc668 100644
--- a/test-cases/my_unit_test_lib.sh
+++ b/test-cases/my_unit_test_lib.sh
@@ -1,45 +1,47 @@
-#!/bin/bash
+#!/usr/bin/env bash
# unit test lib
#################################################
# commons functions
#################################################
-# NOTE: $'foo' is the escape sequence syntax of bash
-readonly __ut_ec=$'\033' # escape char
-readonly __ut_eend=$'\033[0m' # escape end
-
__ut_colorEcho() {
- local color=$1
- shift
- # if stdout is console, turn on color output.
- [ -t 1 ] && echo "${__ut_ec}[1;${color}m$*$__ut_eend" || echo "$*"
+ 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 "$@"
+ __ut_colorEcho 31 "$@"
}
greenEcho() {
- __ut_colorEcho 32 "$@"
+ __ut_colorEcho 32 "$@"
}
yellowEcho() {
- __ut_colorEcho 33 "$@"
+ __ut_colorEcho 33 "$@"
}
blueEcho() {
- __ut_colorEcho 34 "$@"
+ __ut_colorEcho 34 "$@"
}
fail() {
- redEcho "TEST FAIL: $*"
- exit 1
+ redEcho "TEST FAIL: $*"
+ exit 1
}
die() {
- redEcho "Error: $*" 1>&2
- exit 1
+ redEcho "Error: $*" >&2
+ exit 1
}
#################################################
@@ -47,57 +49,57 @@ die() {
#################################################
assertArrayEquals() {
- (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!"
- local failMsg=""
- (($# == 3)) && {
- failMsg=$1
- shift
- }
-
- local a1PlaceHolder="$1[@]"
- local a2PlaceHolder="$2[@]"
- local a1=("${!a1PlaceHolder}")
- local a2=("${!a2PlaceHolder}")
-
- [ ${#a1[@]} -eq ${#a2[@]} ] || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}"
-
- local i
- for ((i = 0; i < ${#a1[@]}; i++)); do
- [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}"
- done
+ (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!"
+ local failMsg=""
+ (($# == 3)) && {
+ failMsg=$1
+ shift
+ }
+
+ local a1PlaceHolder="$1[@]"
+ local a2PlaceHolder="$2[@]"
+ local a1=("${!a1PlaceHolder}")
+ local a2=("${!a2PlaceHolder}")
+
+ ((${#a1[@]} == ${#a2[@]})) || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}"
+
+ local i
+ for ((i = 0; i < ${#a1[@]}; i++)); do
+ [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}"
+ done
}
assertEquals() {
- (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!"
- local failMsg=""
- (($# == 3)) && {
- failMsg=$1
- shift
- }
- [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}"
+ (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!"
+ local failMsg=""
+ (($# == 3)) && {
+ failMsg=$1
+ shift
+ }
+ [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}"
}
readonly __ut_exclude_vars_builtin='^BASH_|^_=|^COLUMNS=|LINES='
readonly __ut_exclude_vars_ut_functions='^FUNCNAME=|^test_'
assertAllVarsSame() {
- local test_afterVars
- test_afterVars=$(declare)
+ local test_afterVars
+ test_afterVars=$(declare)
- diff \
- <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \
- <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") ||
- fail "assertAllVarsSame: Unexpected extra global vars!"
+ diff \
+ <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \
+ <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") ||
+ fail "assertAllVarsSame: Unexpected extra global vars!"
}
assertAllVarsExcludeOptVarsSame() {
- local test_afterVars
- test_afterVars=$(declare)
+ local test_afterVars
+ test_afterVars=$(declare)
- diff \
- <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \
- <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') ||
- fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!"
+ diff \
+ <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \
+ <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') ||
+ fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!"
}
test_beforeVars=$(declare)
diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh
index 93e5704d..35ad0018 100755
--- a/test-cases/parseOpts_test.sh
+++ b/test-cases/parseOpts_test.sh
@@ -1,6 +1,6 @@
-#!/bin/bash
+#!/usr/bin/env bash
-BASE="$(dirname "${BASH_SOURCE[0]}")"
+BASE="$(dirname -- "${BASH_SOURCE[0]}")"
source "$BASE/../lib/parseOpts.sh"
@@ -19,8 +19,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 0 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 0)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!"
[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!"
@@ -50,8 +50,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 0 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 0)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!"
[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!"
@@ -79,8 +79,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 232 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 232)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!"
[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!"
[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!"
@@ -96,8 +96,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 0 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 0)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!"
[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!"
[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!"
@@ -113,8 +113,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 221 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 221)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!"
[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!"
[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!"
@@ -126,8 +126,8 @@ test_exitCode=$?
_opts_showOptDescInfoList
_opts_showOptValueInfoList
-[ $test_exitCode -eq 222 ] || fail "Wrong exit code!"
-[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!"
+((test_exitCode == 222)) || fail "Wrong exit code!"
+((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!"
[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!"
[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!"
[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!"
diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh
index 60d26ac5..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 command -v svn &> /dev/null; then
- [ ! -d "/tmp/useful-scripts-$USER" ] &&
- svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER"
+if 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
index ebc4baa0..da1e19de 160000
--- a/test-cases/shunit2-lib
+++ b/test-cases/shunit2-lib
@@ -1 +1 @@
-Subproject commit ebc4baa08f045b7ef0f45c4b7d6f34f08d732f3d
+Subproject commit da1e19de845a77628d9684e609cc0f8160782c68
diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh
index 1cfb6d83..c3bfdfe0 100755
--- a/test-cases/uq_test.sh
+++ b/test-cases/uq_test.sh
@@ -1,12 +1,11 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -eEuo pipefail
-READLINK_CMD=readlink
-if command -v greadlink &>/dev/null; then
- READLINK_CMD=greadlink
-fi
+realpath() {
+ [ -e "$1" ] && command realpath -- "$1"
+}
-BASE="$(dirname "$($READLINK_CMD -f "${BASH_SOURCE[0]}")")"
+BASE=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")
cd "$BASE"
#################################################
@@ -14,6 +13,7 @@ cd "$BASE"
#################################################
readonly uq="../bin/uq"
+# NOTE: $'foo' is the escape sequence syntax of bash
readonly nl=$'\n' # new line
test_input=$(cat uq_test_input)
@@ -23,18 +23,18 @@ test_input=$(cat uq_test_input)
#################################################
test_uq_simple() {
- assertEquals "c${nl}v${nl}a${nl}u" \
- "$(echo "$test_input" | "$uq")"
- assertEquals "c${nl}v${nl}a${nl}u" \
- "$("$uq" uq_test_input)"
-
- assertEquals "c${nl}a" \
- "$(echo "$test_input" | "$uq" -d)"
- assertEquals "c${nl}a" \
- "$("$uq" -d uq_test_input)"
-
- assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)"
- assertEquals "v${nl}u" "$("$uq" -u uq_test_input)"
+ assertEquals "c${nl}v${nl}a${nl}u" \
+ "$(echo "$test_input" | "$uq")"
+ assertEquals "c${nl}v${nl}a${nl}u" \
+ "$("$uq" uq_test_input)"
+
+ assertEquals "c${nl}a" \
+ "$(echo "$test_input" | "$uq" -d)"
+ assertEquals "c${nl}a" \
+ "$("$uq" -d uq_test_input)"
+
+ assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)"
+ assertEquals "v${nl}u" "$("$uq" -u uq_test_input)"
}
readonly test_output_uq_count=' 4 c
@@ -52,96 +52,96 @@ readonly test_output_uq_D_count=' 4 c
1 u'
test_uq_count() {
- assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)"
- assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)"
+ assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)"
+ assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)"
- assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)"
- assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)"
+ assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)"
+ assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)"
}
test_uq_only_D_option__same_as_cat() {
- assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)"
- assertEquals "$test_input" "$("$uq" -D uq_test_input)"
+ assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)"
+ assertEquals "$test_input" "$("$uq" -D uq_test_input)"
}
test_multi_input_files__output_file() {
- local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
- "$uq" uq_test_input uq_test_another_input "$output_file"
- assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \
- "$(cat "$output_file")"
-
- local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
- "$uq" -d uq_test_input uq_test_another_input "$output_file"
- assertEquals "c${nl}a${nl}m" \
- "$(cat "$output_file")"
-
- local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
- "$uq" -u uq_test_input uq_test_another_input "$output_file"
- assertEquals "v${nl}u${nl}x" \
- "$(cat "$output_file")"
+ local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
+ "$uq" uq_test_input uq_test_another_input "$output_file"
+ assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \
+ "$(cat "$output_file")"
+
+ local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
+ "$uq" -d uq_test_input uq_test_another_input "$output_file"
+ assertEquals "c${nl}a${nl}m" \
+ "$(cat "$output_file")"
+
+ local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}"
+ "$uq" -u uq_test_input uq_test_another_input "$output_file"
+ assertEquals "v${nl}u${nl}x" \
+ "$(cat "$output_file")"
}
test_multi_input_files__output_stdout() {
- assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)"
+ assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)"
- assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)"
+ assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)"
- assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)"
+ assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)"
}
test_ignore_case() {
- local input="a${nl}b${nl}A"
+ local input="a${nl}b${nl}A"
- assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")"
- assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)"
+ assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")"
+ assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)"
}
test_ignore_case__count() {
- local input="a${nl}b${nl}A"
+ local input="a${nl}b${nl}A"
- assertEquals " 1 a${nl} 1 b${nl} 1 A" \
- "$(echo "$input" | "$uq" -c)"
+ assertEquals " 1 a${nl} 1 b${nl} 1 A" \
+ "$(echo "$input" | "$uq" -c)"
- assertEquals " 2 a${nl} 1 b" \
- "$(echo "$input" | "$uq" -i -c)"
+ assertEquals " 2 a${nl} 1 b" \
+ "$(echo "$input" | "$uq" -i -c)"
- assertEquals " 2 a${nl} 1 b${nl} 2 A" \
- "$(echo "$input" | "$uq" -i -D -c)"
+ assertEquals " 2 a${nl} 1 b${nl} 2 A" \
+ "$(echo "$input" | "$uq" -i -D -c)"
}
test_max_input_check() {
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq"'
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq" -XM 4'
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq" -XM 1k'
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq" --max-input 1042k'
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq" --max-input 1m'
- # shellcheck disable=SC2016
- assertTrue 'echo 123 | "$uq" --max-input 10420g'
- # shellcheck disable=SC2016
- assertTrue '"$uq" uq_test_input'
- # shellcheck disable=SC2016
- assertTrue '"$uq" uq_test_input -XM 42m'
- # shellcheck disable=SC2016
- assertTrue '"$uq" uq_test_input --max-input 1024000g'
- # shellcheck disable=SC2016
- assertTrue '"$uq" uq_test_input --max-input 1234567890g'
-
- # shellcheck disable=SC2016
- assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1'
- # shellcheck disable=SC2016
- assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3'
- # shellcheck disable=SC2016
- assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2'
- # shellcheck disable=SC2016
- assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input'
-
- # shellcheck disable=SC2016
- assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq"'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq" -XM 4'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq" -XM 1k'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq" --max-input 1042k'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq" --max-input 1m'
+ # shellcheck disable=SC2016
+ assertTrue 'echo 123 | "$uq" --max-input 10420g'
+ # shellcheck disable=SC2016
+ assertTrue '"$uq" uq_test_input'
+ # shellcheck disable=SC2016
+ assertTrue '"$uq" uq_test_input -XM 42m'
+ # shellcheck disable=SC2016
+ assertTrue '"$uq" uq_test_input --max-input 1024000g'
+ # shellcheck disable=SC2016
+ assertTrue '"$uq" uq_test_input --max-input 1234567890g'
+
+ # shellcheck disable=SC2016
+ assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1'
+ # shellcheck disable=SC2016
+ assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3'
+ # shellcheck disable=SC2016
+ assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2'
+ # shellcheck disable=SC2016
+ assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input'
+
+ # shellcheck disable=SC2016
+ assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g'
}
#################################################