diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4b2606b8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +tab_width = 4 +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.{py,md,mkd,markdown}] +indent_size = 4 + +[*.xml] +indent_style = tab + +[*.{md,mkd,markdown}] +trim_trailing_whitespace = false + +# python files without extension +[show-duplicate-java-classes] +indent_size = 4 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..35615a0e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..2c4b2298 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,32 @@ +# Quickstart for GitHub Actions +# https://docs.github.com/en/actions/quickstart + +name: CI +on: [ push, pull_request, workflow_dispatch ] + +jobs: + + test: + runs-on: ${{ matrix.os }} + timeout-minutes: 5 + strategy: + matrix: + os: [ ubuntu-latest, macos-11, macos-latest ] + fail-fast: false + max-parallel: 64 + name: Test on ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - run: brew install coreutils gnu-sed + # https://docs.github.com/en/actions/learn-github-actions/variables#detecting-the-operating-system + # https://docs.github.com/en/actions/learn-github-actions/expressions + if: runner.os == 'macOS' + - run: test-cases/integration-test.sh + # https://remarkablemark.org/blog/2017/10/12/check-git-dirty/ + - name: Check git dirty + run: | + git status --short + [ -z "$(git status --short)" ] diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..26788dce --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,23 @@ +# Quickstart for GitHub Actions +# https://docs.github.com/en/actions/quickstart + +name: Lint +on: [ push, pull_request, workflow_dispatch ] + +jobs: + + test: + runs-on: ubuntu-latest + timeout-minutes: 5 + name: Lint + + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - run: test-cases/lint.sh + # https://remarkablemark.org/blog/2017/10/12/check-git-dirty/ + - name: Check git dirty + run: | + git status --short + [ -z "$(git status --short)" ] diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a406f269 Binary files /dev/null and b/.gitignore differ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..94904715 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test-cases/shunit2"] + path = test-cases/shunit2-lib + url = https://github.com/kward/shunit2.git diff --git a/README.md b/README.md index fdde70ef..32a6764d 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,52 @@ -🐌 useful-scripts -==================================== +#
🐌 useful-scripts
- +

+Github Workflow Build Status +GitHub release +License +GitHub Stars +GitHub Forks +GitHub issues +GitHub Contributors +GitHub repo size +

-[![License](https://img.shields.io/badge/license-Apache%202-4EB1BA.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) -[![Join the chat at https://gitter.im/oldratlee/useful-scripts](https://badges.gitter.im/oldratlee/useful-scripts.svg)](https://gitter.im/oldratlee/useful-scripts?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![GitHub release](https://img.shields.io/github/release/oldratlee/useful-scripts.svg)](https://github.com/oldratlee/useful-scripts/releases) -[![GitHub stars](https://img.shields.io/github/stars/oldratlee/useful-scripts.svg?style=social&label=Star&)](https://github.com/oldratlee/useful-scripts/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/oldratlee/useful-scripts.svg?style=social&label=Fork&)](https://github.com/oldratlee/useful-scripts/fork) +🐌 useful scripts for making developer's everyday life easier and happier, involved java, shell etc. +👉 平时有用的手动操作做成脚本,以便捷地使用,让开发的日常生活更轻松些。 💕 -👉 把平时有用的手动操作做成脚本,这样可以便捷的使用。 ✨ +欢迎 👏 💖 -有自己用的好的脚本 或是 平时常用但没有写成脚本的功能,欢迎提供([提交Issue](https://github.com/oldratlee/useful-scripts/issues))和分享([Fork后提交代码](https://github.com/oldratlee/useful-scripts/fork))! 💖 +- 提问,[提交 Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 分享平时自己常用但没有写成脚本的功能(即需求、想法),[提交Issue](https://github.com/oldratlee/useful-scripts/issues/new) +- 优化改进,[Fork 后提通过 Pull Request 贡献代码](https://github.com/oldratlee/useful-scripts/fork) +- 提供的自己好用脚本实现,[Fork 后提通过 Pull Request 提供](https://github.com/oldratlee/useful-scripts/fork) -PS: +本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 + +如果你的公司有部署使用,欢迎使用通过 [Issue:who's using | 用户反馈收集](https://github.com/oldratlee/useful-scripts/issues/96) 告知,方便互相交流反馈~ 💗 + +repo-icon + +---------------------- + + + + +- [🔰 快速下载&使用](#-%E5%BF%AB%E9%80%9F%E4%B8%8B%E8%BD%BD%E4%BD%BF%E7%94%A8) +- [📚 使用文档](#-%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3) + - [☕ `Java`相关脚本](#-java%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) + - [🐚 `Shell`相关脚本](#-shell%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) + - [⌚ `VCS`相关脚本](#-vcs%E7%9B%B8%E5%85%B3%E8%84%9A%E6%9C%AC) +- [🎓 Developer Guide](#-developer-guide) + - [🎯 面向开发者的目标](#-%E9%9D%A2%E5%90%91%E5%BC%80%E5%8F%91%E8%80%85%E7%9A%84%E7%9B%AE%E6%A0%87) + - [关于`Shell`脚本](#%E5%85%B3%E4%BA%8Eshell%E8%84%9A%E6%9C%AC) + - [🚦 开发约定](#-%E5%BC%80%E5%8F%91%E7%BA%A6%E5%AE%9A) + - [📚 `Shell`学习与开发的资料](#-shell%E5%AD%A6%E4%B9%A0%E4%B8%8E%E5%BC%80%E5%8F%91%E7%9A%84%E8%B5%84%E6%96%99) + + -本仓库的脚本(如`Java`相关脚本)在阿里等公司(如随身云,见[`awesome-scripts`仓库](https://github.com/Suishenyun/awesome-scripts)说明)的线上生产环境部署使用。 -如果你的公司有部署使用,欢迎使用通过[提交Issue](https://github.com/oldratlee/useful-scripts/issues)告知,方便互相交流反馈~ 💘 +---------------------- 🔰 快速下载&使用 ---------------------- @@ -34,40 +63,120 @@ source <(curl -fsSL https://raw.githubusercontent.com/oldratlee/useful-scripts/r ### ☕ [`Java`相关脚本](docs/java.md) 1. [show-busy-java-threads](docs/java.md#-show-busy-java-threads) - 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 + 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 1. [show-duplicate-java-classes](docs/java.md#-show-duplicate-java-classes) - 找出`jar`文件和`class`目录中的重复类。用于排查`Java`类冲突问题。 + 找出`jar`文件和`class`目录中的重复类。用于排查`Java`类冲突问题。 1. [find-in-jars](docs/java.md#-find-in-jars) - 在目录下所有`jar`文件里,查找类或资源文件。 + 在目录下所有`jar`文件里,查找类或资源文件。 ### 🐚 [`Shell`相关脚本](docs/shell.md) `Shell`使用加强: 1. [c](docs/shell.md#-c) - 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 -1. [coat](docs/shell.md#-coat) - 彩色`cat`出文件行,方便人眼区分不同的行。 + 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 +1. [coat and taoc](docs/shell.md#-coat) + 彩色`cat`/`tac`出文件行,方便人眼区分不同的行。 1. [a2l](docs/shell.md#-a2l) - 按行彩色输出参数,方便人眼查看。 + 按行彩色输出参数,方便人眼查看。 +1. [uq](docs/shell.md#-uq) + 不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 1. [ap and rp](docs/shell.md#-ap-and-rp) - 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 + 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 +1. [cp-into-docker-run](docs/shell.md#-cp-into-docker-run) + 一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 1. [tcp-connection-state-counter](docs/shell.md#-tcp-connection-state-counter) - 统计各个`TCP`连接状态的个数。用于方便排查系统连接负荷问题。 + 统计各个`TCP`连接状态的个数。用于方便排查系统连接负荷问题。 1. [xpl and xpf](docs/shell.md#-xpl-and-xpf) - 在命令行中快速完成 在文件浏览器中 打开/选中 指定的文件或文件夹的操作,优化命令行与其它应用之间的操作流。 + 在命令行中快速完成 在文件浏览器中 打开/选中 指定的文件或文件夹的操作,优化命令行与其它应用之间的操作流。 `Shell`开发/测试加强: 1. [echo-args](docs/shell.md#-echo-args) - 输出脚本收到的参数,在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。用于调试脚本参数输入。 + 输出脚本收到的参数,在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。用于调试脚本参数输入。 1. [console-text-color-themes.sh](docs/shell.md#-console-text-color-themessh) - 显示`Terminator`的全部文字彩色组合的效果及其打印方式,用于开发`Shell`的彩色输出。 + 显示`Terminator`的全部文字彩色组合的效果及其打印方式,用于开发`Shell`的彩色输出。 1. [parseOpts.sh](docs/shell.md#-parseoptssh) - 命令行选项解析库,加强支持选项有多个值(即数组)。 + 命令行选项解析库,加强支持选项有多个值(即数组)。 ### ⌚ [`VCS`相关脚本](docs/vcs.md) 目前`VCS`的脚本都是`svn`分支相关的操作。使用更现代的`Git`吧! 💥 因为不推荐使用`svn`,这里不再列出有哪些脚本了,如果你有兴趣可以点上面链接去看。 + +## 🎓 Developer Guide + +为用户提供有用的功能,当然是这个库的首要的价值体现和存在理由。 + +但作为一个**开源**项目,每个人都可以看到源码实现,这个库或许能做得更多。 + +### 🎯 面向开发者的目标 + +- 将`Shell/Bash`作为线上生产环境使用的专业编程语言。 +- 期望体现`Shell/Bash`脚本 生产环境级的严谨开发方式与最佳实践,进而有可能示例与改善在生产环境中`Shell`脚本的质量状况。 + +PS: + +- 虽然上面是自己期望的目标,但自己在`Shell`语言上一定会有很多理解和使用上的问题、在这些实现脚本中也会很多需要的改进,可以一起学习、讨论与实践~ 💕 +- 这个库中脚本的实现也有使用`Python`。 + +#### 关于`Shell`脚本 + +命令行(`CLI`)几乎是每个程序员每天都在使用的工具。相比图形界面工具(`GUI`),命令行有着自己不可替代的便利性和优越性。 + +命令行里写出来其实就是`Shell`脚本,可以说每个开发者会写`Shell`脚本(或多或少)。在生产环境的功能实现中,也常会看到`Shell`脚本(虽然不如主流语言那么常见)。 + +可能正因为上面所说的`Shell`脚本的便利性和大众性: + +- `Shell`脚本有不少是顺手实现的(包括生产环境用的`Shell`脚本); +- `Shell`脚本的实现常常可能质量不高,会引发线上严重的故障。 + +### 🚦 开发约定 + +在这个库中的`Shell`脚本: + +- 统一使用`Bash 3.2+`; +- 面向生产环境,尽可能使用严谨安全的开发方式。 + +`Shell`用`Bash`的原因是: + +- 目前仍然是主流的`Shell`,并且在不同环境基本上都缺省部署了。 +- 在[`Google`的`Shell`风格指南](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/background.html#shell)中,明确说到了:`Bash`是**唯一**被允许执行的`shell`脚本语言。 +- 统一用`Bash`,可以避免不同`Shell`之间差异所带来的风险与没有收益的复杂性。 + - 有大量的`Shell`实现,`sh`、`bash`、`zsh`、`fish`、`csh`、`tcsh`、`ksh`、`ash`、`dash`…… + - 不同的`Shell`有各种差异,深坑勿入。 +- 个人系统学习过的是`Bash`,比较理解熟悉。 + +PS: 虽然交互`Shell`个人已经使用`Zsh` + [`oh-my-zsh`](https://ohmyz.sh/),但在严谨的`Shell`脚本开发时还是使用`Bash`。 + +### 📚 `Shell`学习与开发的资料 + +> 更多资料参见 [子文档](docs/developer-guide.md)。 + +- 🛠️ 开发规范与工具 + - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents.html) + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts + - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs +- 👷 **`Bash/Shell`最佳实践与安全编程**文章 + - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) | [英文原文:Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls) + - [不要自己去指定`sh`的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) +- 🎶 **Tips** + - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) + 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 + - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) + - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) +- 💎 **系统学习** — 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! + - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` + - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 + - 官方资料 + - [`bash man`](https://manned.org/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) + Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. + - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`awesome-lists/awesome-bash`](https://github.com/awesome-lists/awesome-bash): A curated list of delightful Bash scripts and resources. + - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. + - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) diff --git a/bin/a2l b/bin/a2l index caca085b..203513c9 100755 --- a/bin/a2l +++ b/bin/a2l @@ -1,6 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function -# echo each arguments on one line colorfully. +# print each arguments on one line colorfully. # # @Usage # $ ./a2l arg1 arg2 @@ -8,23 +8,85 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-a2l # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end +################################################################################ +# parse options +################################################################################ -colorEcho() { - local color="$1" - shift - # check isatty in bash https://stackoverflow.com/questions/10022323 - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" +usage() { + cat < 0)); do + case "$1" in + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + -*) + # if unrecognized option, treat it and all follow arguments as args + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + *) + # if not option, treat it and all follow arguments as args + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + esac +done +readonly args + +################################################################################ +# biz logic +################################################################################ + +readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) +COLOR_INDEX=0 +rotateColorPrint() { + local content=$* + # - if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + # - skip color for white space + if [[ ! -t 1 || $content =~ ^[[:space:]]*$ ]]; then + printf '%s\n' "$content" + else + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} + printf '\e[1;%sm%s\e[0m\n' "$color" "$content" + fi +} -for a; do - colorEcho "${ECHO_COLORS[COUNT++ % ${#ECHO_COLORS[@]}]}" "$a" +for a in ${args[@]:+"${args[@]}"}; do + rotateColorPrint "$a" done diff --git a/bin/ap b/bin/ap index 925e4d52..051f66d9 100755 --- a/bin/ap +++ b/bin/ap @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # convert to Absolute Path. # @@ -10,13 +10,126 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-ap-and-rp # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -[ $# -eq 0 ] && files=(.) || files=("$@") +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' -for f in "${files[@]}" ; do - ! [ -e "$f" ] && { - echo "$f does not exists!" - continue - } - readlink -f "$f" +################################################################################ +# util functions +################################################################################ + +errorMsgPrint() { + local errorMsg="$PROG: $*" + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$errorMsg" + else + printf '%s\n' "$errorMsg" + fi +} >&2 + +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && errorMsgPrint "$*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 + +# `realpath` command exists on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +usage() { + cat < 0)); do + case "$1" in + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + files=(${files[@]:+"${files[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + # if not option, treat all follow files as args + files=(${files[@]:+"${files[@]}"} "$@") + break + ;; + esac done + +# if files is empty, use "." +readonly files=("${files[@]:-.}") + +################################################################################ +# biz logic +################################################################################ + +has_error=false + +for f in "${files[@]}"; do + realpath "$f" || { + has_error=true + errorMsgPrint "$f: No such file or directory!" + } +done + +# set exit status +! $has_error diff --git a/bin/c b/bin/c index e18e11e0..c0bbbd5a 100755 --- a/bin/c +++ b/bin/c @@ -1,44 +1,82 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Run command and put output to system clipper. # # @Usage -# $ c echo "hello world!" -# $ echo "hello world!" | c +# $ c ls -l +# $ ls -l | c +# $ c -q < ~/.ssh/id_rsa.pub # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-c # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -set -e -set -o pipefail +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' -readonly PROG="`basename "$0"`" +################################################################################ +# util functions +################################################################################ -usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} - (( $# > 0 )) && { echo "$@"; echo; } > $out +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 - > $out cat < 0)); do + case "$1" in + -k | --keep-eol) + keep_eol=true + shift + ;; + -q | --quiet) + quiet=true + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + target_command=(${target_command[@]:+"${target_command[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + # if not option, treat all follow arguments as command + target_command=(${target_command[@]:+"${target_command[@]}"} "$@") + break + ;; + esac done +readonly keep_eol quiet target_command + +if ((${#target_command[@]} > 0)) && ! type -P "${target_command[0]}" &>/dev/null; then + die "command '${target_command[0]}' not found on PATH" +fi + ################################################################################ # biz logic ################################################################################ -copy() { - case "`uname`" in - Darwin*) - pbcopy ;; - CYGWIN*|MINGW*) - clip ;; - *) - xsel -b ;; - esac +systemClip() { + case "$(uname)" in + Darwin*) + pbcopy + ;; + CYGWIN* | MINGW*) + clip + ;; + *) + xsel -b + ;; + esac +} + +bufferCopy() { + local content + content=$(cat) + if $keep_eol; then + printf '%s\n' "$content" + else + printf %s "$content" + fi | systemClip } teeAndCopy() { - $quiet && local out=/dev/null || local out=/dev/stdout - tee >( - content="$(cat)" - echo $eol "$content" | copy - ) > $out + if $quiet; then + bufferCopy + else + tee >(bufferCopy) + fi } -if [ ${#args[@]} -eq 0 ]; then - teeAndCopy +if ((${#target_command[@]} == 0)); then + teeAndCopy else - "${args[@]}" | teeAndCopy + command "${target_command[@]}" | teeAndCopy fi diff --git a/bin/coat b/bin/coat index 6ab5e1a7..b1280722 100755 --- a/bin/coat +++ b/bin/coat @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # cat lines colorfully. coat means *CO*lorful c*AT*. # @@ -9,26 +9,88 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-coat # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -set -e -set -o pipefail +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' -# if not in console, use cat directly -# check isatty in bash https://stackoverflow.com/questions/10022323 -[ ! -t 1 ] && exec cat "$@" +################################################################################ +# parse options +################################################################################ + +usage() { + cat <= 0; --idx)); do + [ "${args[idx]}" = --help ] && usage + [ "${args[idx]}" = --version ] && progVersion done +unset args idx + +################################################################################ +# biz logic +################################################################################ + +# if stdout is not a terminal, use `cat` directly. +# '-t' check: is a terminal? +# check isatty in bash https://stackoverflow.com/questions/10022323 +[ -t 1 ] || exec cat "$@" + +readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) +COLOR_INDEX=0 +# CAUTION: print content WITHOUT new line +rotateColorPrint() { + local content=$* + # skip color for white space + if [[ $content =~ ^[[:space:]]*$ ]]; then + printf %s "$content" + else + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} + printf '\e[1;%sm%s\e[0m' "$color" "$content" + fi +} + +rotateColorPrintln() { + # NOTE: $'foo' is the escape sequence syntax of bash + rotateColorPrint "$*"$'\n' +} + +colorLines() { + local line + # Bash read line does not read leading spaces + # https://stackoverflow.com/questions/29689172 + while IFS= read -r line; do + rotateColorPrintln "$line" + done + # How to use `while read` (Bash) to read the last line in a file + # if there’s no newline at the end of the file? + # https://stackoverflow.com/questions/4165135 + [ -z "$line" ] || rotateColorPrint "$line" +} + +if (($# == 0)); then + colorLines +else + cat "$@" | colorLines +fi diff --git a/bin/cp-into-docker-run b/bin/cp-into-docker-run new file mode 100755 index 00000000..7ad6b164 --- /dev/null +++ b/bin/cp-into-docker-run @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# @Function +# Copy the command into docker container and run the command in container. +# +# Example: +# cp-into-docker-run -c container_foo command_copied_into_container command_arg1 +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-cp-into-docker-run +# @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail + +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# util functions +################################################################################ + +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} + +die() { + local prompt_help=false exit_staus=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_staus=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_staus" +} >&2 + +isAbsolutePath() { + [[ "$1" =~ ^/ ]] +} + +# `realpath` command exists on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +usage() { + cat < 0)); do + case "$1" in + -c | --container) + container_name=$2 + shift 2 + ;; + -u | --docker-user) + docker_user=$2 + shift 2 + ;; + -w | --workdir) + docker_workdir=$2 + shift 2 + ;; + -t | --tmpdir) + docker_tmpdir=$2 + shift 2 + ;; + -p | --cp-path) + docker_command_cp_path=$2 + shift 2 + ;; + -v | --verbose) + verbose=true + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + # if not option, treat all follow arguments as command + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + esac +done + +readonly container_name docker_user docker_workdir docker_tmpdir docker_command_cp_path verbose args + +[ -n "$container_name" ] || + die -h "requires destination docker container name, specified by option -c/--container!" + +if [ -n "$docker_workdir" ]; then + isAbsolutePath "$docker_workdir" || + die "docker workdir(-w/--workdir) must be absolute path: $docker_workdir" +elif [ -n "$docker_command_cp_path" ]; then + isAbsolutePath "$docker_command_cp_path" || + die "when no docker workdir(-w/--workdir) is specified, the command path in docker to copy(-p/--cp-path) must be absolute path: $docker_command_cp_path" +fi + +################################################################################ +# biz logic +################################################################################ + +######################################## +# check docker command existence +######################################## + +type -P docker &>/dev/null || die 'docker command not found!' + +######################################## +# prepare vars for docker operation +######################################## + +readonly specified_run_command=${args[0]} +run_command=$specified_run_command +if [ ! -f "$specified_run_command" ]; then + type -P "$specified_run_command" &>/dev/null || + die "specified command not exists and not found in PATH: $specified_run_command" + + run_command=$(type -P "$specified_run_command") +fi + +run_command=$(realpath "$run_command") +readonly run_command run_command_base_name=${run_command##*/} + +run_timestamp=$(date "+%Y%m%d_%H%M%S") +readonly run_timestamp +readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" + +if [ -n "$docker_command_cp_path" ]; then + if isAbsolutePath "$docker_command_cp_path"; then + readonly run_command_in_docker=$docker_command_cp_path + else + readonly run_command_in_docker="${docker_workdir:+"$docker_workdir/"}$docker_command_cp_path" + fi + run_command_dir_in_docker=$(dirname -- "$run_command_in_docker") + readonly run_command_dir_in_docker +else + readonly work_tmp_dir_in_docker=$docker_tmpdir/$uuid + + readonly run_command_in_docker="$work_tmp_dir_in_docker/$run_command_base_name" + readonly run_command_dir_in_docker=$work_tmp_dir_in_docker +fi + +cleanupWhenExit() { + [ -n "${work_tmp_dir_in_docker:-}" ] || return 0 + + # remove tmp dir in docker by root user + docker exec "$container_name" rm -rf -- "$work_tmp_dir_in_docker" &>/dev/null +} +trap cleanupWhenExit EXIT + +######################################## +# docker operations +######################################## + +logAndRun() { + $verbose && printf '%s\n' "[$PROG] $*" >&2 + "$@" +} + +logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ + mkdir -p -- "$run_command_dir_in_docker" +logAndRun docker cp "$run_command" "$container_name:$run_command_in_docker" +logAndRun docker exec ${docker_user:+"--user=$docker_user"} "$container_name" \ + chmod +x "$run_command_in_docker" + +logAndRun docker exec -i -t \ + ${docker_user:+"--user=$docker_user"} \ + ${docker_workdir:+"--workdir=$docker_workdir"} \ + "$container_name" \ + "$run_command_in_docker" "${args[@]:1:${#args[@]}}" diff --git a/bin/echo-args b/bin/echo-args index 6bb2f661..9186a6e3 100755 --- a/bin/echo-args +++ b/bin/echo-args @@ -1,25 +1,41 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # print arguments in human and debugging friendly style. # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-echo-args # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end +digitCount() { + # argument 1(num) is always a non-negative integer in this script usage, + # so NO argument validation logic. + local num=$1 count=0 + while ((num != 0)); do + ((++count)) + ((num = num / 10)) + done + echo "$count" +} -echoArg() { - local index="$1" count="$2" value="$3" +digit_count=$(digitCount $#) +readonly arg_count=$# digit_count - # if stdout is console, turn on color output. - [ -t 1 ] && - echo "$index/$count: $ec[1;31m[$eend$ec[0;34;42m$value$eend$ec[1;31m]$eend" || - echo "$index/$count: [$value]" -} +readonly RED='\e[1;31m' BLUE='\e[1;36m' COLOR_RESET='\e[0m' +printArg() { + local idx=$1 value=$2 + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf "%${digit_count}s/%s: ${RED}[${BLUE}%s${RED}]${COLOR_RESET}\n" "$idx" "$arg_count" "$value" + else + printf "%${digit_count}s/%s: [%s]\n" "$idx" "$arg_count" "$value" + fi +} -echoArg 0 $# "$0" +printArg 0 "$0" idx=1 -for a ; do - echoArg $((idx++)) $# "$a" +for a; do + printArg $((idx++)) "$a" done diff --git a/bin/find-in-jars b/bin/find-in-jars index 83e6ebe1..eda51301 100755 --- a/bin/find-in-jars +++ b/bin/find-in-jars @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Find files in the jar files under specified directory, search jar files recursively(include subdirectory). # @@ -13,76 +13,101 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-find-in-jars # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -readonly PROG="`basename "$0"`" +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' ################################################################################ # util functions ################################################################################ -[ -t 1 ] && readonly is_console=true || readonly is_console=false - -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end -readonly cr=$'\r' # carriage return - -redEcho() { - $is_console && echo "$ec[1;31m$@$eend" || echo "$@" +readonly COLOR_RESET='\e[0m' + +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf "\e[1;31m%s$COLOR_RESET\n" "$*" + else + printf '%s\n' "$*" + fi } -die() { - redEcho "Error: $@" 1>&2 - exit 1 -} +# How to delete line with echo? +# https://unix.stackexchange.com/questions/26576 +# +# terminal escapes: http://ascii-table.com/ansi-escape-sequences.php +# In particular, to clear from the cursor position to the beginning of the line: +# echo -e "\033[1K" +# Or everything on the line, regardless of cursor position: +# echo -e "\033[2K" +readonly LINE_CLEAR='\e[2K\r' # Getting console width using a bash script # https://unix.stackexchange.com/questions/299067 -$is_console && readonly columns=$(stty size | awk '{print $2}') +[ -t 2 ] && COLUMNS=$(stty size | awk '{print $2}') printResponsiveMessage() { - $is_console || return + if ! $show_responsive || [ ! -t 2 ]; then + return + fi - local message="$*" - # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html - echo -n "${message:0:columns}" + local content=$* + # http://www.linuxforums.org/forum/red-hat-fedora-linux/142825-how-truncate-string-bash-script.html + printf %b%s "$LINE_CLEAR" "${content:0:COLUMNS}" >&2 } clearResponsiveMessage() { - $is_console || return - - # How to delete line with echo? - # https://unix.stackexchange.com/questions/26576 - # - # terminal escapes: http://ascii-table.com/ansi-escape-sequences.php - # In particular, to clear from the cursor position to the beginning of the line: - # echo -e "\033[1K" - # Or everything on the line, regardless of cursor position: - # echo -e "\033[2K" - echo -n "$ec[2K$cr" + if ! $show_responsive || [ ! -t 2 ]; then + return + fi + + printf %b "$LINE_CLEAR" >&2 } -usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done - (( $# > 0 )) && { echo "$@"; echo; } > $out + clearResponsiveMessage + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 + +usage() { + cat < $out cat < ' + $PROG '^log4j\.(properties|xml)$' + $PROG 'log4j\.properties$' -d /path/to/find/directory + $PROG '\.properties$' -d /path/to/find/dir1 -d path/to/find/dir2 + $PROG 'Service\.class$' -e jar -e zip + $PROG 'Mon[^$/]*Service\.class$' -s ' <-> ' Find control: -d, --dir the directory that find jar files. @@ -100,155 +125,281 @@ Output control: -a, --absolute-path always print absolute path of jar file -s, --separator specify the separator between jar file and zip entry. default is \`!'. + -L, --files-not-contained-found + print only names of JAR FILEs NOT contained found + -l, --files-contained-found + print only names of JAR FILEs contained found + -R, --no-find-progress do not display responsive find progress Miscellaneous: -h, --help display this help and exit + -V, --version display version information and exit EOF - exit $1 + exit +} + +progVersion() { + printf '%s\n' "$PROG $PROG_VERSION" + exit } ################################################################################ # parse options ################################################################################ -declare -a args=() -declare -a dirs=() -while (( $# > 0 )); do - case "$1" in - -d|--dir) - dirs=("${dirs[@]}" "$2") - shift 2 - ;; - -e|--extension) - extension=("${extension[@]}" "$2") - shift 2 - ;; - # support the typo option --seperator for compatibility - -s|--separator|--seperator) - separator="$2" - shift 2 - ;; - -E|--extended-regexp) - regex_mode=-E - shift - ;; - -F|--fixed-strings) - regex_mode=-F - shift - ;; - -G|--basic-regexp) - regex_mode=-G - shift - ;; - -P|--perl-regexp) - regex_mode=-P - shift - ;; - -i|--ignore-case) - ignore_case_option=-i - shift - ;; - -a|--absolute-path) - use_absolute_path=true - shift - ;; - -h|--help) - usage - ;; - --) - shift - args=("${args[@]}" "$@") - break - ;; - -*) - usage 2 "${PROG}: unrecognized option '$1'" - ;; - *) - args=("${args[@]}" "$1") - shift - ;; - esac +dirs=() +extensions=() +args=() + +separator='!' +regex_mode=-E +use_absolute_path=false +show_responsive=true +only_print_file_name=false + +while (($# > 0)); do + case "$1" in + -d | --dir) + dirs=(${dirs[@]:+"${dirs[@]}"} "$2") + shift 2 + ;; + -e | --extension) + extensions=(${extensions[@]:+"${extensions[@]}"} "$2") + shift 2 + ;; + -E | --extended-regexp) + regex_mode=-E + shift + ;; + -F | --fixed-strings) + regex_mode=-F + shift + ;; + -G | --basic-regexp) + regex_mode=-G + shift + ;; + -P | --perl-regexp) + regex_mode=-P + shift + ;; + -i | --ignore-case) + ignore_case_option=-i + shift + ;; + -a | --absolute-path) + use_absolute_path=true + shift + ;; + # support the legacy typo option name --seperator for compatibility + -s | --separator | --seperator) + separator=$2 + shift 2 + ;; + -L | --files-not-contained-found) + only_print_file_name=true + print_matched_files=false + shift + ;; + -l | --files-contained-found) + only_print_file_name=true + print_matched_files=true + shift + ;; + -R | --no-find-progress) + show_responsive=false + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + args=(${args[@]:+"${args[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + args=(${args[@]:+"${args[@]}"} "$1") + shift + ;; + esac done -dirs=${dirs:-.} -extension=${extension:-jar} -regex_mode=${regex_mode:--E} +readonly separator regex_mode ignore_case_option use_absolute_path only_print_file_name print_matched_files show_responsive args -use_absolute_path=${use_absolute_path:-false} -separator="${separator:-!}" +# shellcheck disable=SC2178 +dirs=${dirs:-.} +# shellcheck disable=SC2178 +readonly extensions=${extensions:-jar} -(( "${#args[@]}" == 0 )) && usage 1 "No find file pattern!" -(( "${#args[@]}" > 1 )) && usage 1 "More than 1 file pattern: ${args[@]}" -readonly pattern="${args[0]}" +((${#args[@]} == 0)) && die -h "requires file pattern!" +((${#args[@]} > 1)) && die -h "more than 1 file pattern: ${args[*]}" +readonly pattern=${args[0]} -declare -a tmp_dirs=() +tmp_dirs=() for d in "${dirs[@]}"; do - [ -e "$d" ] || die "file $d(specified by option -d) does not exist!" - [ -d "$d" ] || die "file $d(specified by option -d) exists but is not a directory!" - [ -r "$d" ] || die "directory $d(specified by option -d) exists but is not readable!" - # convert dirs to Absolute Path - $use_absolute_path && tmp_dirs=( "${tmp_dirs[@]}" "$(cd "$d" && pwd)" ) + [ -e "$d" ] || die "file $d(specified by option -d): No such file or directory!" + [ -d "$d" ] || die "file $d(specified by option -d) exists but is not a directory!" + [ -r "$d" ] || die "directory $d(specified by option -d) exists but is not readable!" + + # convert dirs to Absolute Path if has option -a, --absolute-path + $use_absolute_path && tmp_dirs=(${tmp_dirs[@]:+"${tmp_dirs[@]}"} "$(cd "$d" && pwd)") done # set dirs to Absolute Path -$use_absolute_path && dirs=( "${tmp_dirs[@]}" ) +$use_absolute_path && dirs=("${tmp_dirs[@]}") +readonly dirs +unset d tmp_dirs # convert extensions to find -iname options -for e in "${extension[@]}"; do - (( "${#find_iname_options[@]}" == 0 )) && - find_iname_options=( -iname "*.$e" ) || - find_iname_options=( "${find_iname_options[@]}" -o -iname "*.$e" ) +find_iname_options=() +for e in "${extensions[@]}"; do + find_iname_options=(${find_iname_options[@]:+"${find_iname_options[@]}" -o} -iname "*.$e") done +readonly find_iname_options +unset e ################################################################################ # Check the existence of command for listing zip entry! ################################################################################ -# `zipinfo -1`/`unzip -Z1` is ~25 times faster than `jar tf`, find zipinfo/unzip command first. -# -# How to list files in a zip without extra information in command line -# https://unix.stackexchange.com/a/128304/136953 -if which zipinfo &> /dev/null; then - readonly command_for_list_zip='zipinfo -1' -elif which unzip &> /dev/null; then - readonly command_for_list_zip='unzip -Z1' -else - if ! which jar &> /dev/null; then - [ -n "$JAVA_HOME" ] || die "jar not found on PATH and JAVA_HOME env var is blank!" - [ -f "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) file does NOT exists!" - [ -x "$JAVA_HOME/bin/jar" ] || die "jar not found on PATH and \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executalbe!" - export PATH="$JAVA_HOME/bin:$PATH" +__prepareCommandToListZipEntries() { + # `zipinfo -1`/`unzip -Z1` is ~25 times faster than `jar tf`, find zipinfo/unzip command first. + # + # How to list files in a zip without extra information in command line + # https://unix.stackexchange.com/a/128304/136953 + + if type -P zipinfo &>/dev/null; then + command_to_list_zip_entries=(zipinfo -1) + is_use_zip_cmd_to_list_zip_entries=true + elif type -P unzip &>/dev/null; then + command_to_list_zip_entries=(unzip -Z1) + is_use_zip_cmd_to_list_zip_entries=true + elif [ -n "$JAVA_HOME" ]; then + # search jar command under JAVA_HOME + if [ -f "$JAVA_HOME/bin/jar" ]; then + [ -x "$JAVA_HOME/bin/jar" ] || die "found \$JAVA_HOME/bin/jar($JAVA_HOME/bin/jar) is NOT executable!" + command_to_list_zip_entries=("$JAVA_HOME/bin/jar" tf) + elif [ -f "$JAVA_HOME/../bin/jar" ]; then + [ -x "$JAVA_HOME/../bin/jar" ] || die "found \$JAVA_HOME/../bin/jar($JAVA_HOME/../bin/jar) is NOT executable!" + command_to_list_zip_entries=("$JAVA_HOME/../bin/jar" tf) fi - readonly command_for_list_zip='jar tf' -fi + is_use_zip_cmd_to_list_zip_entries=false + elif type -P jar &>/dev/null; then + # search jar command under PATH + command_to_list_zip_entries=(jar tf) + is_use_zip_cmd_to_list_zip_entries=false + else + die "command to list zip entries NOT found : zipinfo, unzip or jar!" + fi + + readonly command_to_list_zip_entries is_use_zip_cmd_to_list_zip_entries +} +__prepareCommandToListZipEntries + +listZipEntries() { + local zip_file=$1 msg + + if $is_use_zip_cmd_to_list_zip_entries; then + # How to check if zip file is empty in bash + # https://superuser.com/questions/438878 + msg=$("${command_to_list_zip_entries[@]}" -t "$zip_file" 2>&1) || { + # NOTE: + # if list emtpy zip file by zipinfo/unzip command, + # exit code is 1, and print 'Empty zipfile.' + if [ "$msg" != 'Empty zipfile.' ]; then + clearResponsiveMessage + redPrint "fail to list zip entries of $zip_file, ignored: $msg" >&2 + fi + return + } + fi + + "${command_to_list_zip_entries[@]}" "$zip_file" || { + clearResponsiveMessage + redPrint "fail to list zip entries of $zip_file, ignored!" >&2 + } +} ################################################################################ # find logic ################################################################################ -readonly jar_files="$(find "${dirs[@]}" "${find_iname_options[@]}" -type f)" -readonly total_count="$(echo $(echo "$jar_files" | wc -l))" -[ -n "$jar_files" ] || die "No ${extension[@]} file found!" +searchJarFiles() { + printResponsiveMessage "searching jars under dir ${dirs[*]} , ..." -findInJarFiles() { - $is_console && local -r grep_color_option='--color=always' + local jar_files total_jar_count - local counter=1 jar_file - while read jar_file; do - printResponsiveMessage "finding in jar($((counter++))/$total_count): $jar_file" + jar_files=$(find "${dirs[@]}" "${find_iname_options[@]}" -type f) + [ -n "$jar_files" ] || die "${extensions[*]} file NOT found!" - $command_for_list_zip "${jar_file}" | - grep $regex_mode $ignore_case_option $grep_color_option -- "$pattern" | - while read file; do - clearResponsiveMessage + total_jar_count=$(printf '%s\n' "$jar_files" | wc -l) + # remove white space, because the `wc -l` output on mac contains white space! + total_jar_count=${total_jar_count//[[:space:]]/} - $is_console && - echo "$ec[1;35m${jar_file}${eend}${ec}[1;32m${separator}${eend}${file}" || - echo "${jar_file}${separator}${file}" - done + echo "$total_jar_count" + printf '%s\n' "$jar_files" +} - clearResponsiveMessage +readonly JAR_COLOR='\e[1;35m' SEP_COLOR='\e[1;32m' +__outputResultOfJarFile() { + local jar_file=$1 file + # shellcheck disable=SC2206 + local grep_opt_args=("$regex_mode" ${ignore_case_option:-} ${grep_color_option:-} -- "$pattern") + + if $only_print_file_name; then + local matched=false + # NOTE: Do NOT use -q flag with grep: + # With the -q flag the grep program will stop immediately when the first line of data matches. + # Normally you shouldn't use -q in a pipeline like this + # unless you are sure the program at the other end can handle SIGPIPE. + # more info see: + # - https://stackoverflow.com/questions/19120263/why-exit-code-141-with-grep-q + # - https://unix.stackexchange.com/questions/305547/broken-pipe-when-grepping-output-but-only-with-i-flag + # - http://www.pixelbeat.org/programming/sigpipe_handling.html + grep -c "${grep_opt_args[@]}" &>/dev/null && matched=true + + [ "$print_matched_files" != "$matched" ] && return + + clearResponsiveMessage + if [ -t 1 ]; then + printf "$JAR_COLOR%s$COLOR_RESET\n" "$jar_file" + else + printf '%s\n' "$jar_file" + fi + else + { + # Prevent grep from exiting in case of no match + # https://unix.stackexchange.com/questions/330660 + grep "${grep_opt_args[@]}" || true + } | while IFS= read -r file; do + clearResponsiveMessage + if [ -t 1 ]; then + printf "$JAR_COLOR%s$SEP_COLOR%s$COLOR_RESET%s\n" "$jar_file" "$separator" "$file" + else + printf '%s\n' "$jar_file$separator$file" + fi done + fi +} + +findInJarFiles() { + [ -t 1 ] && local -r grep_color_option='--color=always' + local counter=1 total_jar_count jar_file + + read -r total_jar_count + while IFS= read -r jar_file; do + printResponsiveMessage "finding in jar($((counter++))/$total_jar_count): $jar_file" + listZipEntries "$jar_file" | __outputResultOfJarFile "$jar_file" + done + + clearResponsiveMessage } -echo "$jar_files" | findInJarFiles +searchJarFiles | findInJarFiles diff --git a/bin/rp b/bin/rp index efb69ee6..d4d85210 100755 --- a/bin/rp +++ b/bin/rp @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # convert to Relative Path. # @@ -10,30 +10,159 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-ap-and-rp # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -[ $# -eq 0 ] && { - echo "ERROR: NO argument!" - exit 1 +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# util functions +################################################################################ + +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi } -[ $# -eq 1 ] && { - relativeTo=. - files=("$@") -} || { - argv=("$@") - argc=$# +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 + +portableRelPath() { + local file=$1 relTo=$2 uname + + uname=$(uname) + case "$uname" in + Linux* | CYGWIN* | MINGW*) + realpath "$f" --relative-to="$relTo" + ;; + Darwin*) + local py_args=(-c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$file" "$relTo") + if type -P grealpath >/dev/null; then + grealpath "$f" --relative-to="$relTo" + elif type -P python3 >/dev/null; then + python3 "${py_args[@]}" + elif type -P python >/dev/null; then + python "${py_args[@]}" + else + die "fail to find command(grealpath/python3/python) to get relative path!" + fi + ;; + *) + die "uname($uname) NOT support!" + ;; + esac +} + +usage() { + cat < 0)); do + case "$1" in + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + files=(${files[@]:+"${files[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + # if not option, treat all follow files as args + files=(${files[@]:+"${files[@]}"} "$@") + break + ;; + esac done + +((${#files[@]} == 0)) && die -h "requires at least one argument!" + +if ((${#files[@]} == 1)); then + relativeTo=. +else + argc=${#files[@]} + + # Get last argument + relativeTo=${files[argc - 1]} + files=("${files[@]:0:argc-1}") +fi + +[ -f "$relativeTo" ] && relativeTo=$(dirname -- "$relativeTo") +[ -e "$relativeTo" ] || die "relativeTo dir($relativeTo): No such file or directory!" + +readonly files relativeTo + +################################################################################ +# biz logic +################################################################################ + +has_error=false + +for f in "${files[@]}"; do + if [ -e "$f" ]; then + portableRelPath "$f" "$relativeTo" + else + redPrint "$PROG: $f: No such file or directory!" >&2 + has_error=true + fi +done + +# set exit status +! $has_error diff --git a/bin/show-busy-java-threads b/bin/show-busy-java-threads index 6b204989..c141ad8d 100755 --- a/bin/show-busy-java-threads +++ b/bin/show-busy-java-threads @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Find out the highest cpu consumed threads of java processes, and print the stack of these threads. # @@ -9,97 +9,163 @@ # @author Jerry Lee (oldratlee at gmail dot com) # @author superhj1987 (superhj1987 at 126 dot com) -readonly PROG="`basename $0`" -readonly -a COMMAND_LINE=("$0" "$@") -# Get current user name via whoami command -# See https://www.lifewire.com/current-linux-user-whoami-command-3867579 -# Because if run command by `sudo -u`, env var $USER is not rewritten/correct, just inherited from outside! -readonly USER="`whoami`" +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' +# choosing between $0 and BASH_SOURCE +# https://stackoverflow.com/a/35006505/922688 +# How can I get the source directory of a Bash script from within the script itself? +# https://stackoverflow.com/questions/59895 +# Will $0 always include the path to the script? +# https://unix.stackexchange.com/questions/119929 +readonly -a COMMAND_LINE=("${BASH_SOURCE[0]}" "$@") +# CAUTION: env var $USER is not reliable! +# $USER may be overwritten; if run command by `sudo -u`, may is not `root`. +# more info see https://www.baeldung.com/linux/get-current-user +# +# DO NOT declare and assign var(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 +WHOAMI=$(whoami) +UNAME=$(uname) +readonly WHOAMI UNAME ################################################################################ # util functions ################################################################################ # NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end +readonly NL=$'\n' # new line -colorEcho() { - local color=$1 - shift +colorPrint() { + local color=$1 + shift + + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi +} - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" +__appendToFile() { + [[ -n "$append_file" && -w "$append_file" ]] && printf '%s\n' "$*" >>"$append_file" + [[ -n "$store_dir" && -w "$store_dir" ]] && printf '%s\n' "$*" >>"$store_file_prefix$PROG" } -colorPrint() { - local color=$1 - shift +colorOutput() { + local color=$1 + shift - colorEcho "$color" "$@" - [ -n "$append_file" -a -w "$append_file" ] && echo "$@" >> "$append_file" - [ -n "$store_dir" -a -w "$store_dir" ] && echo "$@" >> "${store_file_prefix}$PROG" + colorPrint "$color" "$*" + __appendToFile "$*" } -normalPrint() { - echo "$@" - [ -n "$append_file" -a -w "$append_file" ] && echo "$@" >> "$append_file" - [ -n "$store_dir" -a -w "$store_dir" ] && echo "$@" >> "${store_file_prefix}$PROG" +# shellcheck disable=SC2120 +normalOutput() { + printf '%s\n' "$*" + __appendToFile "$*" } -redPrint() { - colorPrint 31 "$@" +redOutput() { + colorOutput 31 "$*" } -greenPrint() { - colorPrint 32 "$@" +greenOutput() { + colorOutput 32 "$*" } -yellowPrint() { - colorPrint 33 "$@" +yellowOutput() { + colorOutput 33 "$*" } -bluePrint() { - colorPrint 36 "$@" +blueOutput() { + colorOutput 36 "$*" } die() { - redPrint "Error: $@" 1>&2 - exit 1 -} + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && colorPrint "1;31" "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 logAndRun() { - echo "$@" - echo - "$@" + printf '%s\n' "$*" + echo + "$@" } logAndCat() { - echo "$@" - echo - cat + printf '%s\n' "$*" + echo + cat } -usage() { - local -r exit_code="$1" - shift - [ -n "$exit_code" -a "$exit_code" != 0 ] && local -r out=/dev/stderr || local -r out=/dev/stdout +# Bash RegEx to check floating point numbers from user input +# https://stackoverflow.com/questions/13790763 +isNonNegativeFloatNumber() { + [[ "$1" =~ ^[+]?[0-9]+\.?[0-9]*$ ]] +} - (( $# > 0 )) && { echo "$@"; echo; } > $out +isNaturalNumber() { + [[ "$1" =~ ^[+]?[0-9]+$ ]] +} - > $out cat < find out the highest cpu consumed threads from + -p, --pid find out the highest cpu consumed threads from the specified java process. + support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. @@ -116,342 +182,418 @@ Output control: jstack control: -s, --jstack-path specifies the path of jstack command. - -F, --force set jstack to force a thread dump. use when jstack - does not respond (process is hung). - -m, --mix-native-frames set jstack to print both java and native frames - (mixed mode). + -F, --force set jstack to force a thread dump. + use when jstack does not respond (process is hung). + -m, --mix-native-frames set jstack to print both java and + native frames (mixed mode). -l, --lock-info set jstack with long listing. prints additional information about locks. CPU usage calculation control: - -d, --top-delay specifies the delay between top samples. - default is 0.5 (second). get thread cpu percentage - during this delay interval. - more info see top -d option. eg: -d 1 (1 second). - -P, --use-ps use ps command to find busy thread(cpu usage) - instead of top command. - default use top command, because cpu usage of - ps command is expressed as the percentage of - time spent running during the *entire lifetime* - of a process, this is not ideal in general. + -i, --cpu-sample-interval specifies the delay between cpu samples to get + thread cpu usage percentage during this interval. + default is 0.5 (second). + set interval 0 to get the percentage of time spent + running during the *entire lifetime* of a process. Miscellaneous: -h, --help display this help and exit. + -V, --version display version information and exit. EOF - exit $exit_code + exit +} + +progVersion() { + printf '%s\n' "$PROG $PROG_VERSION" + exit } ################################################################################ -# Check os support +# check os support ################################################################################ -uname | grep '^Linux' -q || die "$PROG only support Linux, not support `uname` yet!" +[[ $UNAME = Linux* ]] || die "only support Linux, not support $UNAME yet!" ################################################################################ # parse options ################################################################################ -# NOTE: ARGS can not be declared as readonly!! -# readonly declaration make exit code of assignment to be always 0, aka. the exit code of `getopt` in subshell is discarded. -# tested on bash 4.2.46 -ARGS=`getopt -n "$PROG" -a -o p:c:a:s:S:Pd:Fmlh -l count:,pid:,append-file:,jstack-path:,store-dir:,use-ps,top-delay:,force,mix-native-frames,lock-info,help -- "$@"` -[ $? -ne 0 ] && { echo; usage 1; } -eval set -- "${ARGS}" +# DO NOT declare and assign var ARGS(as readonly) in ONE line! +ARGS=$( + getopt -n "$PROG" -a -o c:p:a:s:S:i:Pd:FmlhV \ + -l count:,pid:,append-file:,jstack-path:,store-dir:,cpu-sample-interval:,use-ps,top-delay:,force,mix-native-frames,lock-info,help,version \ + -- "$@" +) || die -h +eval set -- "$ARGS" +unset ARGS + +count=5 +cpu_sample_interval=0.5 while true; do - case "$1" in - -c|--count) - count="$2" - shift 2 - ;; - -p|--pid) - pid="$2" - shift 2 - ;; - -a|--append-file) - append_file="$2" - shift 2 - ;; - -s|--jstack-path) - jstack_path="$2" - shift 2 - ;; - -S|--store-dir) - store_dir="$2" - shift 2 - ;; - -P|--use-ps) - use_ps=true - shift - ;; - -d|--top-delay) - top_delay="$2" - shift 2 - ;; - -F|--force) - force=-F - shift - ;; - -m|--mix-native-frames) - mix_native_frames=-m - shift - ;; - -l|--lock-info) - more_lock_info=-l - shift - ;; - -h|--help) - usage - ;; - --) - shift - break - ;; - esac + case "$1" in + -c | --count) + count=$2 + shift 2 + ;; + -p | --pid) + pid_list=$2 + shift 2 + ;; + -a | --append-file) + append_file=$2 + shift 2 + ;; + -s | --jstack-path) + jstack_path=$2 + shift 2 + ;; + -S | --store-dir) + store_dir=$2 + shift 2 + ;; + # support the legacy option name -P,--use-ps for compatibility + -P | --use-ps) + cpu_sample_interval=0 + shift + ;; + # support the legacy option name -d,--top-delay for compatibility + -i | --cpu-sample-interval | -d | --top-delay) + cpu_sample_interval=$2 + shift 2 + ;; + -F | --force) + force=-F + shift + ;; + -m | --mix-native-frames) + mix_native_frames=-m + shift + ;; + -l | --lock-info) + lock_info=-l + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + break + ;; + esac done -count=${count:-5} +readonly count cpu_sample_interval force mix_native_frames lock_info +readonly update_delay=${1:-0} +isNonNegativeFloatNumber "$update_delay" || die "update delay($update_delay) is not a non-negative float number!" -update_delay=${1:-0} [ -z "$1" ] && update_count=1 || update_count=${2:-0} -(( update_count < 0 )) && update_count=0 +isNaturalNumber "$update_count" || die "update count($update_count) is not a natural number!" +readonly update_count -top_delay=${top_delay:-0.5} -use_ps=${use_ps:-false} +if [ -n "$pid_list" ]; then + pid_list=${pid_list//[[:space:]]/} # delete white space + isNaturalNumberList "$pid_list" || die "pid(s)($pid_list) is illegal! example: 42 or 42,99,67" +fi +readonly pid_list -# check the directory of append-file(-a) mode, create if not exsit. +# check the directory of append-file(-a) mode, create if not existed. if [ -n "$append_file" ]; then - if [ -e "$append_file" ]; then - [ -f "$append_file" ] || die "$append_file(specified by option -a, for storing run output files) exists but is not a file!" - [ -w "$append_file" ] || die "file $append_file(specified by option -a, for storing run output files) exists but is not writable!" + if [ -e "$append_file" ]; then + [ -f "$append_file" ] || die "$append_file(specified by option -a, for storing run output files) exists but is not a file!" + [ -w "$append_file" ] || die "file $append_file(specified by option -a, for storing run output files) exists but is not writable!" + else + append_file_dir=$(dirname -- "$append_file") + if [ -e "$append_file_dir" ]; then + [ -d "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not a directory!" + [ -w "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not writable!" else - append_file_dir="$(dirname "$append_file")" - if [ -e "$append_file_dir" ]; then - [ -d "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not a directory!" - [ -w "$append_file_dir" ] || die "directory $append_file_dir(specified by option -a, for storing run output files) exists but is not writable!" - else - mkdir -p "$append_file_dir" || die "fail to create directory $append_file_dir(specified by option -a, for storing run output files)!" - fi + mkdir -p "$append_file_dir" || die "fail to create directory $append_file_dir(specified by option -a, for storing run output files)!" fi + fi fi +readonly append_file -# check store directory(-S) mode, create directory if not exsit. +# check store directory(-S) mode, create directory if not existed. if [ -n "$store_dir" ]; then - if [ -e "$store_dir" ]; then - [ -d "$store_dir" ] || die "$store_dir(specified by option -S, for storing output files) exists but is not a directory!" - [ -w "$store_dir" ] || die "directory $store_dir(specified by option -S, for storing output files) exists but is not writable!" - else - mkdir -p "$store_dir" || die "fail to create directory $store_dir(specified by option -S, for storing output files)!" - fi + if [ -e "$store_dir" ]; then + [ -d "$store_dir" ] || die "$store_dir(specified by option -S, for storing output files) exists but is not a directory!" + [ -w "$store_dir" ] || die "directory $store_dir(specified by option -S, for storing output files) exists but is not writable!" + else + mkdir -p "$store_dir" || die "fail to create directory $store_dir(specified by option -S, for storing output files)!" + fi fi +readonly store_dir + +isNonNegativeFloatNumber "$cpu_sample_interval" || die "cpu sample interval($cpu_sample_interval) is not a non-negative float number!" ################################################################################ -# check the existence of jstack command +# search/check the existence of jstack command +# +# search order/priority: +# 1. from -s option +# 2. from under env var JAVA_HOME +# 3. from under env var PATH ################################################################################ if [ -n "$jstack_path" ]; then - [ -f "$jstack_path" ] || die "$jstack_path is NOT found!" - [ -x "$jstack_path" ] || die "$jstack_path is NOT executalbe!" -elif which jstack &> /dev/null; then - jstack_path="`which jstack`" -else - [ -n "$JAVA_HOME" ] || die "jstack not found on PATH and No JAVA_HOME setting! Use -s option set jstack path manually." - [ -f "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) file does NOT exists! Use -s option set jstack path manually." - [ -x "$JAVA_HOME/bin/jstack" ] || die "jstack not found on PATH and \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executalbe! Use -s option set jstack path manually." + # 1. check jstack_path set by -s option + [ -f "$jstack_path" ] || die "$jstack_path (set by -s option) is NOT found!" + [ -x "$jstack_path" ] || die "$jstack_path (set by -s option) is NOT executable!" +elif [ -n "$JAVA_HOME" ]; then + # 2. search jstack under JAVA_HOME + if [ -f "$JAVA_HOME/bin/jstack" ]; then + [ -x "$JAVA_HOME/bin/jstack" ] || die -h "found \$JAVA_HOME/bin/jstack($JAVA_HOME/bin/jstack) is NOT executable!${NL}Use -s option set jstack path manually." jstack_path="$JAVA_HOME/bin/jstack" + elif [ -f "$JAVA_HOME/../bin/jstack" ]; then + [ -x "$JAVA_HOME/../bin/jstack" ] || die -h "found \$JAVA_HOME/../bin/jstack($JAVA_HOME/../bin/jstack) is NOT executable!${NL}Use -s option set jstack path manually." + jstack_path="$JAVA_HOME/../bin/jstack" + fi +elif type -P jstack &>/dev/null; then + # 3. search jstack under PATH + jstack_path=$(type -P jstack) + [ -x "$jstack_path" ] || die -h "found $jstack_path from PATH is NOT executable!${NL}Use -s option set jstack path manually." +else + die -h "jstack NOT found by JAVA_HOME(${JAVA_HOME:-not set}) setting and PATH!${NL}Use -s option set jstack path manually." fi +readonly jstack_path ################################################################################ # biz logic ################################################################################ -readonly run_timestamp="`date "+%Y-%m-%d_%H:%M:%S.%N"`" -readonly uuid="${PROG}_${run_timestamp}_${RANDOM}_$$" +# DO NOT declare and assign var run_timestamp(as readonly) in ONE line! +run_timestamp=$(date "+%Y-%m-%d_%H:%M:%S.%N") +readonly run_timestamp +readonly uuid="${PROG}_${run_timestamp}_${$}_${RANDOM}" -readonly tmp_store_dir="/tmp/${uuid}" +readonly tmp_store_dir="/tmp/$uuid" if [ -n "$store_dir" ]; then - readonly store_file_prefix="$store_dir/${run_timestamp}_" + readonly store_file_prefix="$store_dir/${run_timestamp}_" else - readonly store_file_prefix="$tmp_store_dir/${run_timestamp}_" + readonly store_file_prefix="$tmp_store_dir/${run_timestamp}_" fi mkdir -p "$tmp_store_dir" cleanupWhenExit() { - rm -rf "$tmp_store_dir" &> /dev/null + rm -rf "$tmp_store_dir" &>/dev/null } -trap "cleanupWhenExit" EXIT +trap cleanupWhenExit EXIT headInfo() { - colorEcho "0;34;42" ================================================================================ - echo "$(date "+%Y-%m-%d %H:%M:%S.%N") [$(( i + 1 ))/$update_count]: ${COMMAND_LINE[@]}" - colorEcho "0;34;42" ================================================================================ - echo + local timestamp=$1 + colorPrint "0;34;42" ================================================================================ + printf '%s\n' "$timestamp [$((update_round_num + 1))/$update_count]: $(printCallingCommandLine)" + colorPrint "0;34;42" ================================================================================ + echo } -if [ -n "${pid}" ]; then - readonly ps_process_select_options="-p $pid" +if [ -n "$pid_list" ]; then + readonly ps_process_select_options="-p $pid_list" else - readonly ps_process_select_options="-C java -C jsvc" + readonly ps_process_select_options="-C java -C jsvc" fi +__die_when_no_java_process_found() { + if [ -n "$pid_list" ]; then + die "process($pid_list) is not running, or not java process!" + else + die 'No java process found!' + fi +} + # output field: pid, thread id(lwp), pcpu, user # order by pcpu(percentage of cpu usage) +# +# NOTE: +# use ps command to find busy thread(cpu usage) +# cpu usage of ps command is expressed as +# the percentage of time spent running during the *entire lifetime* of a process, +# this is not ideal in general. findBusyJavaThreadsByPs() { - # 1. sort by %cpu by ps option `--sort -pcpu` - # 2. use wide output(unlimited width) by ps option `-ww` - # avoid trunk user column to username_fo+ or $uid alike - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo pid,lwp,pcpu,user --sort -pcpu --no-headers) - local -r ps_out="$("${ps_cmd_line[@]}")" - if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_ps" - fi - - if (( count > 0 )); then - echo "$ps_out" | head -n "${count}" - else - echo "$ps_out" - fi + # 1. sort by %cpu by ps option `--sort -pcpu` + # unfortunately, ps from `procps-ng 3.3.12`, `--sort` does not work properly with other options, + # use + # ps + # combined + # sort -k3,3nr + # instead of + # ps --sort -pcpu + # 2. use wide output(unlimited width) by ps option `-ww` + # avoid trunk user column to username_fo+ or $uid alike + + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,pcpu,user' --no-headers) + # DO NOT combine var ps_out declaration and assignment in ONE line! + # more info see https://github.com/koalaman/shellcheck/wiki/SC2155 + local ps_out + ps_out=$("${ps_cmd_line[@]}" | sort -k3,3nr) + [ -n "$ps_out" ] || __die_when_no_java_process_found + + if [ -n "$store_dir" ]; then + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[*]} | sort -k3,3nr" >"$store_file_prefix$((update_round_num + 1))_ps" + fi + + if ((count > 0)); then + printf '%s\n' "$ps_out" | head -n "$count" + else + printf '%s\n' "$ps_out" + fi } # top with output field: thread id, %cpu __top_threadId_cpu() { - # 1. sort by %cpu by top option `-o %CPU` - # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), - # use - # HOME="$tmp_store_dir" top -H -b -n 1 - # combined - # sort - # instead of - # HOME="$tmp_store_dir" top -H -b -n 1 -o '%CPU' - # 2. change HOME env var when run top, - # so as to prevent top command output format being change by .toprc user config file unexpectedly - # 3. use option `-d 0.5`(update interval 0.5 second) and `-n 2`(update 2 times), - # and use second time update data to get cpu percentage of thread in 0.5 second interval - # 4. top v3.3, there is 1 black line between 2 update; - # but top v3.2, there is 2 blank lines between 2 update! - local -a top_cmd_line=(top -H -b -d $top_delay -n 2) - local -r top_out=$(HOME="$tmp_store_dir" "${top_cmd_line[@]}") - if [ -n "$store_dir" ]; then - echo "$top_out" | logAndCat "${top_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_top" - fi - - echo "$top_out" | - awk 'BEGIN { blockIndex = 0; currentLineHasText = 0; prevLineHasText = 0; } { - currentLineHasText = ($0 != "") - if (prevLineHasText && !currentLineHasText) - blockIndex++ # from text line to empty line, increase block index - - if (blockIndex == 3 && ($NF == "java" || $NF == "jsvc")) # $NF(last field) is command field - # only print 4th text block(blockIndex == 3), aka. process info of second top update - print $1 " " $9 # $1 is thread id field, $9 is %cpu field - - prevLineHasText = currentLineHasText # update prevLineHasText - }' | sort -k2,2nr + # DO NOT combine var java_pid_list declaration and assignment in ONE line! + local java_pid_list + # shellcheck disable=SC2086 + java_pid_list=$(ps $ps_process_select_options -o pid --no-headers) + [ -n "$java_pid_list" ] || __die_when_no_java_process_found + # shellcheck disable=SC2086 + java_pid_list=$(echo $java_pid_list | tr ' ' ,) # join with , + + # 1. sort by %cpu by top option `-o %CPU` + # unfortunately, top version 3.2 does not support -o option(supports from top version 3.3+), + # use + # HOME=$tmp_store_dir top -H -b -n 1 + # combined + # sort + # instead of + # HOME=$tmp_store_dir top -H -b -n 1 -o %CPU + # 2. change HOME env var when run top, + # so as to prevent top command output format being change by .toprc user config file unexpectedly + # 3. use option `-d 0.5`(update interval 0.5 second) and `-n 2`(update 2 times), + # and use second time update data to get cpu percentage of thread in 0.5 second interval + # 4. top v3.3, there is 1 black line between 2 update; + # but top v3.2, there is 2 blank lines between 2 update! + local -a top_cmd_line=(top -H -b -d "$cpu_sample_interval" -n 2 -p "$java_pid_list") + # DO NOT combine var top_out declaration and assignment in ONE line! + local top_out + top_out=$(HOME=$tmp_store_dir "${top_cmd_line[@]}") + if [ -n "$store_dir" ]; then + printf '%s\n' "$top_out" | logAndCat "${top_cmd_line[@]}" >"$store_file_prefix$((update_round_num + 1))_top" + fi + + # DO NOT combine var result_threads_top_info declaration and assignment in ONE line! + local result_threads_top_info + result_threads_top_info=$(printf '%s\n' "$top_out" | awk '{ + # from text line to empty line, increase block index + if (previousLine && !$0) blockIndex++ + # only print 4th text block(blockIndex == 3), aka. process info of second top update + if (blockIndex == 3 && $1 ~ /^[0-9]+$/) + print $1, $9 # $1 is thread id field, $9 is %cpu field + previousLine = $0 + }') + [ -n "$result_threads_top_info" ] || __die_when_no_java_process_found + + printf '%s\n' "$result_threads_top_info" | sort -k2,2nr } __complete_pid_user_by_ps() { - # ps output field: pid, thread id(lwp), user - local -a ps_cmd_line=(ps $ps_process_select_options -wwLo pid,lwp,user --no-headers) - local -r ps_out="$("${ps_cmd_line[@]}")" - if [ -n "$store_dir" ]; then - echo "$ps_out" | logAndCat "${ps_cmd_line[@]}" > "${store_file_prefix}$(( i + 1 ))_ps" + # ps output field: pid, thread id(lwp), user + # shellcheck disable=SC2206 + local -a ps_cmd_line=(ps $ps_process_select_options -wwLo 'pid,lwp,user' --no-headers) + # DO NOT combine var ps_out declaration and assignment in ONE line! + local ps_out + ps_out=$("${ps_cmd_line[@]}") + if [ -n "$store_dir" ]; then + printf '%s\n' "$ps_out" | logAndCat "${ps_cmd_line[@]}" >"$store_file_prefix$((update_round_num + 1))_ps" + fi + + local idx=0 threadId pcpu output_fields + while read -r threadId pcpu; do + ((count <= 0 || idx < count)) || break + + # output field: pid, threadId, pcpu, user + output_fields=$(printf '%s\n' "$ps_out" | awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { + print $1, threadId, pcpu, $3; exit + }') + if [ -n "$output_fields" ]; then + ((idx++)) + printf '%s\n' "$output_fields" fi - - local idx=0 threadId pcpu - while read threadId pcpu ; do - (( count <= 0 || idx < count )) || break - - # output field: pid, threadId, pcpu, user - local output_fields="$( echo "$ps_out" | - awk -v "threadId=$threadId" -v "pcpu=$pcpu" '$2==threadId { - printf "%s %s %s %s\n", $1, threadId, pcpu, $3; exit - }' )" - if [ -n "$output_fields" ]; then - (( idx++ )) - echo "$output_fields" - fi - done + done } # output format is same as function findBusyJavaThreadsByPs findBusyJavaThreadsByTop() { - __top_threadId_cpu | __complete_pid_user_by_ps + __top_threadId_cpu | __complete_pid_user_by_ps } printStackOfThreads() { - local idx=0 pid threadId pcpu user - while read pid threadId pcpu user ; do - local threadId0x="0x`printf %x ${threadId}`" - - (( idx++ )) - local jstackFile="${store_file_prefix}$(( i + 1 ))_jstack_${pid}" - [ -f "${jstackFile}" ] || { - local -a jstack_cmd_line=( "$jstack_path" ${force} $mix_native_frames $more_lock_info ${pid} ) - if [ "${user}" == "${USER}" ]; then - # run without sudo, when java process user is current user - logAndRun "${jstack_cmd_line[@]}" > ${jstackFile} - elif [ $UID == 0 ]; then - # if java process user is not current user, must run jstack with sudo - logAndRun sudo -u "${user}" "${jstack_cmd_line[@]}" > ${jstackFile} - else - # current user is not root user, so can not run with sudo; print error message and rerun suggestion - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - redPrint "User of java process($user) is not current user($USER), need sudo to rerun:" - yellowPrint " sudo ${COMMAND_LINE[@]}" - normalPrint - continue - fi || { - redPrint "[$idx] Fail to jstack busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user})." - normalPrint - rm "${jstackFile}" &> /dev/null - continue - } - } - - bluePrint "[$idx] Busy(${pcpu}%) thread(${threadId}/${threadId0x}) stack of java process(${pid}) under user(${user}):" - - if [ -n "$mix_native_frames" ]; then - local sed_script="/--------------- $threadId ---------------/,/^---------------/ { - /--------------- $threadId ---------------/b # skip first separator line - /^---------------/d # delete second separator line - p - }" - elif [ -n "$force" ]; then - local sed_script="/^Thread ${threadId}:/,/^$/ { - /^$/d; p # delete end separator line - }" - else - local sed_script="/ nid=${threadId0x} /,/^$/ { - /^$/d; p # delete end separator line - }" - fi - { - sed "$sed_script" -n ${jstackFile} - echo - } | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} - done + local idx=0 pid threadId pcpu user threadId0x + while read -r pid threadId pcpu user; do + printf -v threadId0x '%#x' "$threadId" + + ((idx++ > 0)) && normalOutput + local jstackFile="$store_file_prefix$((update_round_num + 1))_jstack_$pid" + [ -f "$jstackFile" ] || { + # shellcheck disable=SC2206 + local -a jstack_cmd_line=("$jstack_path" $force $mix_native_frames $lock_info $pid) + if [ "$user" = "$WHOAMI" ]; then + # run without sudo, when java process user is current user + logAndRun "${jstack_cmd_line[@]}" >"$jstackFile" + elif ((UID == 0)); then + # if java process user is not current user, must run jstack with sudo + logAndRun sudo -u "$user" "${jstack_cmd_line[@]}" >"$jstackFile" + else + # current user is not root user, so can not run with sudo; print error message and rerun suggestion + redOutput "[$idx] Fail to jstack busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." + redOutput "User of java process($user) is not current user($WHOAMI), need sudo to rerun:" + yellowOutput " sudo $(printCallingCommandLine)" + continue + fi || { + redOutput "[$idx] Fail to jstack busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user)." + rm "$jstackFile" &>/dev/null + continue + } + } + + blueOutput "[$idx] Busy($pcpu%) thread($threadId/$threadId0x) stack of java process($pid) under user($user):" + + if [ -n "$mix_native_frames" ]; then + local sed_script="/--------------- $threadId ---------------/,/^---------------/ { + /--------------- $threadId ---------------/b # skip first separator line + /^---------------/d # delete second separator line + p + }" + elif [ -n "$force" ]; then + local sed_script="/^Thread $threadId:/,/^$/ { + /^$/d; p # delete end separator line + }" + else + local sed_script="/ nid=($threadId0x|$threadId) /,/^$/ { + /^$/d; p # delete end separator line + }" + fi + sed "$sed_script" -n -r "$jstackFile" | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} + done } -################################################################################ -# Main -################################################################################ - main() { - local i - # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) - for (( i = 0; update_count <= 0 || i < update_count; ++i )); do - (( i > 0 )) && sleep "$update_delay" - - [ -n "$append_file" -o -n "$store_dir" ] && headInfo | tee ${append_file:+-a "$append_file"} ${store_dir:+-a "${store_file_prefix}$PROG"} > /dev/null - (( update_count != 1 )) && headInfo - - if $use_ps; then - findBusyJavaThreadsByPs - else - findBusyJavaThreadsByTop - fi | printStackOfThreads - done + local update_round_num timestamp + # if update_count <= 0, infinite loop till user interrupted (eg: CTRL+C) + for ((update_round_num = 0; update_count <= 0 || update_round_num < update_count; ++update_round_num)); do + ((update_round_num > 0)) && { + sleep "$update_delay" + normalOutput + } + + timestamp=$(date "+%Y-%m-%d %H:%M:%S.%N") + [[ -n "$append_file" || -n "$store_dir" ]] && headInfo "$timestamp" | + tee ${append_file:+-a "$append_file"} ${store_dir:+-a "$store_file_prefix$PROG"} >/dev/null + ((update_count != 1)) && headInfo "$timestamp" + + if [ "$cpu_sample_interval" = 0 ]; then + findBusyJavaThreadsByPs + else + findBusyJavaThreadsByTop + fi | printStackOfThreads + done } main diff --git a/bin/show-duplicate-java-classes b/bin/show-duplicate-java-classes index e49c78aa..942ad2e9 100755 --- a/bin/show-duplicate-java-classes +++ b/bin/show-duplicate-java-classes @@ -1,12 +1,15 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding: utf-8 -*- # @Function -# Find duplicate class among java libs. +# Find duplicate classes among java lib dirs and class dirs. # # @Usage -# $ show-duplicate-java-classes # find jars from current dir +# $ show-duplicate-java-classes # search jars from current dir # $ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 # $ show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 +# $ show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1 +# $ show-duplicate-java-classes -L path/to/lib_dir1 # search jars in the subdirectories of lib dir +# $ show-duplicate-java-classes -J path/to/lib_dir1 # search jars in the jar file # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/java.md#-show-duplicate-java-classes # @author tg123 (farmer1992 at gmail dot com) @@ -14,99 +17,350 @@ __author__ = 'tg123' +import os +import re +import sys from glob import glob -from os import walk -from zipfile import ZipFile -from os.path import relpath, isdir +from io import BytesIO from optparse import OptionParser +from os import walk +from os.path import exists, isdir, relpath +from zipfile import BadZipfile, ZipFile + +################################################################################ +# utils functions +################################################################################ +PROG_VERSION = '2.x-dev' + +# How to delete line with echo? +# https://unix.stackexchange.com/questions/26576 +# +# terminal escapes: http://ascii-table.com/ansi-escape-sequences.php +# In particular, to clear from the cursor position to the beginning of the line: +# echo -e "\033[1K" +# Or everything on the line, regardless of cursor position: +# echo -e "\033[2K" +__clear_line = '\033[2K\r' +__show_responsive = True + + +def __get_terminal_columns_of_stderr(): + """ + Rewritten for stderr from + """ + try: + columns, _ = os.get_terminal_size(sys.stderr.fileno()) + except (AttributeError, ValueError, OSError): + columns = 0 + + return columns + + +def print_responsive_message(msg): + if not __show_responsive or not sys.stderr.isatty(): + return + columns = __get_terminal_columns_of_stderr() + if columns <= 0: + return + + print(__clear_line + msg[:columns], end='', file=sys.stderr) + + +def clear_responsive_message(): + if not __show_responsive or not sys.stderr.isatty(): + return + print(__clear_line, end='', file=sys.stderr) + +def print_error(msg): + clear_responsive_message() + print(msg, file=sys.stderr) -def list_jar_file_under_lib_dirs(libs): + +def print_box_message(msg): + print() + print('=' * 80) + print(msg) + print('=' * 80) + + +def str_len(x): + return len(str(x)) + + +# issue 32790: Keep trailing zeros in precision for string format option g - Python tracker +# https://bugs.python.org/issue32790 +def percent_str(num): + """ + Input => Output + 1.4545 / 10 **-1 => 1455% + 1.4545 / 10 ** 0 => 145% + 1.4545 / 10 ** 1 => 14.5% + 1.4545 / 10 ** 2 => 1.45% + 1.4545 / 10 ** 3 => 0.145% + 1.4545 / 10 ** 4 => 0.015% + 1.4545 / 10 ** 5 => 0.001% + 1.4545 / 10 ** 6 => 0.000% + 1.4545 / 10 ** 7 => 0.000% + """ + num = num * 100 + if num >= 100: + return '%.0f%%' % num + elif num >= 10: + return '%.1f%%' % num + elif num >= 1: + return '%.2f%%' % num + else: + return '%.3f%%' % num + + +def list_jar_file_under_lib_dirs(lib_dirs, recursive): jar_files = set() - for lib in libs: - if isdir(lib): - jar_files |= {f for f in glob(lib + '/*.jar')} - else: - jar_files.add(lib) + + max_idx_str_len = str_len(len(lib_dirs)) + for idx, lib_dir in enumerate(lib_dirs, start=1): + print_responsive_message('list jar file under lib dir(%*s/%s): %s' % ( + max_idx_str_len, idx, len(lib_dirs), lib_dir)) + + if not exists(lib_dir): + print_error('WARN: lib dir %s not exists, ignored!' % lib_dir) + continue + + if not isdir(lib_dir): + jar_files.add(lib_dir) + continue + + if not recursive: + jar_files |= {p for p in glob(lib_dir + '/*.jar')} + continue + + jar_files |= { + dir_path + '/' + filename + for dir_path, _, file_names in walk(lib_dir) + for filename in file_names if filename.lower().endswith('.jar') + } + return jar_files -def list_class_under_jar_file(jar_file): - return {f for f in ZipFile(jar_file).namelist() if f.lower().endswith('.class')} +def list_class_under_jar_file(jar_file, recursive, progress): + """ + :return: map: jar_jar_path('a.jar!b.jar!c.jar') -> classes + """ + index = 0 + def list_zip_in_zip(jar_jar_path, zf): + nonlocal index + index += 1 + index_marker = '' + if recursive: + index_marker = ' #%3s' % index + print_responsive_message('list class under jar file(%*s/%s%s): %s' % ( + str_len(progress[1]), progress[0], progress[1], index_marker, jar_jar_path)) -def list_class_under_class_dir(class_dir): - return {relpath(dir_path + "/" + filename, class_dir) + ret = {} + classes = {f for f in zf.namelist() if f.lower().endswith('.class')} + ret[jar_jar_path] = classes + if not recursive: + return ret + + jars_in_jar = {f for f in zf.namelist() if f.lower().endswith('.jar')} + for jar in jars_in_jar: + next_jar_jar_path = jar_jar_path + '!' + jar + try: + with ZipFile(BytesIO(zf.read(jar))) as f: + ret.update(list_zip_in_zip(next_jar_jar_path, f)) + except BadZipfile as e: + print_error('WARN: %s is bad zip file(%s), ignored!' % (next_jar_jar_path, e)) + + return ret + + try: + with ZipFile(jar_file) as zip_file: + return list_zip_in_zip(jar_file, zip_file) + except BadZipfile as error: + print_error('WARN: %s is bad zip file(%s), ignored!' % (jar_file, error)) + return {} + + +def list_class_under_class_dir(class_dir, progress): + print_responsive_message('list class under class dir(%*s/%s): %s' % ( + str_len(progress[1]), progress[0], progress[1], class_dir)) + + if not exists(class_dir): + print_error('WARN: class dir %s not exists, ignored!' % class_dir) + return {} + if not isdir(class_dir): + print_error('WARN: class dir %s is not dir, ignored!' % class_dir) + return {} + + return {relpath(dir_path + '/' + filename, class_dir) for dir_path, _, file_names in walk(class_dir) for filename in file_names if filename.lower().endswith('.class')} -def expand_2_class_path(jar_files, class_dirs): - java_class_2_class_paths = {} +def collect_class_path_to_classes(class_dirs, jar_files, recursive_jar): + class_path_to_classes = {} + total_count = len(jar_files) + len(class_dirs) + index = 0 + # list all classes in jar files for jar_file in jar_files: - for class_file in list_class_under_jar_file(jar_file): - java_class_2_class_paths.setdefault(class_file, set()).add(jar_file) - # list all classes in class dir + index += 1 + class_path_to_classes.update( + list_class_under_jar_file(jar_file, recursive=recursive_jar, progress=(index, total_count))) + # list all classes in class dirs for class_dir in class_dirs: - for class_file in list_class_under_class_dir(class_dir): - java_class_2_class_paths.setdefault(class_file, set()).add(class_dir) + index += 1 + class_path_to_classes[class_dir] = list_class_under_class_dir(class_dir, progress=(index, total_count)) + return class_path_to_classes - return java_class_2_class_paths, jar_files | set(class_dirs) +def invert_as_class_to_class_paths(class_path_to_classes): + class_to_class_paths = {} + for class_path, classes in class_path_to_classes.items(): + for clazz in classes: + class_to_class_paths.setdefault(clazz, set()).add(class_path) + return class_to_class_paths -def find_duplicate_classes(java_class_2_class_paths): - class_path_2_duplicate_classes = {} - for java_class, class_paths in list(java_class_2_class_paths.items()): - if len(class_paths) > 1: - classes = class_path_2_duplicate_classes.setdefault(frozenset(class_paths), set()) - classes.add(java_class) +################################################################################ +# biz functions +################################################################################ - return class_path_2_duplicate_classes +__java9_module_file_pattern = re.compile(r'(^|.*/)module-info\.class$') -def print_class_paths(class_paths): - print() - print("=" * 80) - print("class paths to find:") - print("=" * 80) - for idx, class_path in enumerate(class_paths): - print("%-3d: %s" % (idx + 1, class_path)) +def find_duplicate_classes(class_to_class_paths): + class_paths_to_duplicate_classes = {} + for clazz, class_paths in class_to_class_paths.items(): + # skip java 9 module-info files + if len(class_paths) == 1 or __java9_module_file_pattern.match(clazz): + continue -if __name__ == '__main__': - optionParser = OptionParser('usage: %prog ' - '[-c class-dir1 [-c class-dir2] ...] ' - '[lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]') - optionParser.add_option("-c", "--class-dir", dest="class_dirs", default=[], action="append", help="add class dir") - options, libs = optionParser.parse_args() + classes = class_paths_to_duplicate_classes.setdefault(frozenset(class_paths), set()) + classes.add(clazz) - if not options.class_dirs and not libs: - libs = ['.'] + return class_paths_to_duplicate_classes - java_class_2_class_paths, class_paths = expand_2_class_path( - list_jar_file_under_lib_dirs(libs), options.class_dirs) - class_path_2_duplicate_classes = find_duplicate_classes(java_class_2_class_paths) +def print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes): + if not class_paths_to_duplicate_classes: + print('COOL! No duplicate classes found!') + return - if not class_path_2_duplicate_classes: - print("COOL! No duplicate classes found!") - print_class_paths(class_paths) - exit() + duplicate_classes_total_count = sum(len(dcs) for dcs in class_paths_to_duplicate_classes.values()) + class_paths_total_count = sum(len(cps) for cps in class_paths_to_duplicate_classes) + print('Found %s duplicate classes in %s class paths and %s class path sets:' % ( + duplicate_classes_total_count, class_paths_total_count, len(class_paths_to_duplicate_classes))) - print("Found duplicate classes in below class path:") - for idx, jars in enumerate(class_path_2_duplicate_classes): - print("%-3d(%d@%d): %s" % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), " ".join(jars))) + # sort key(class_paths) and value(duplicate_classes) + class_paths_to_duplicate_classes = [(sorted(cps), sorted(dcs)) + for cps, dcs in class_paths_to_duplicate_classes.items()] + # sort kv pairs + # + # sort by multiple keys: + # 1. class paths count, *descending*; aka. sort by len(item[0]) *reverse=True* + # 2. duplicate classes count, *descending*; aka. sort by len(item[1]) *reverse=True* + # 3. class paths, ascending; aka. sort by item[0] + # sort also ensure output consistent for same input. + # + # How to sort objects by multiple keys in Python? + # https://stackoverflow.com/questions/1143671 + # Sort a list by multiple attributes? + # https://stackoverflow.com/questions/4233476 + # + # use - operator of number key for reverse sort key + class_paths_to_duplicate_classes.sort(key=lambda item: (-len(item[0]), -len(item[1]), item[0])) - print() - print("=" * 80) - print("Duplicate classes detail info:") - print("=" * 80) - for idx, (jars, classes) in enumerate(class_path_2_duplicate_classes.items()): - print("%-3d(%d@%d): %s" % (idx + 1, len(class_path_2_duplicate_classes[jars]), len(jars), " ".join(jars))) - for i, c in enumerate(classes): - print("\t%-3d %s" % (i + 1, c)) - - print_class_paths(class_paths) - exit(1) + max_idx_str_len = str_len(len(class_paths_to_duplicate_classes)) + for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): + duplicate_ratio = len(classes) / min((len(class_path_to_classes[cp]) for cp in class_paths)) + print('[%*s] found %s(%s) duplicate classes in %s class paths:' % ( + max_idx_str_len, idx, len(classes), percent_str(duplicate_ratio), len(class_paths))) + + max_class_path_idx_str_len = str_len(len(class_paths)) + max_classes_count_str_len = str_len(max(len(class_path_to_classes[cp]) for cp in class_paths)) + for i, cp in enumerate(class_paths, start=1): + print(' %*s: (contain %*s classes) %s' % ( + max_class_path_idx_str_len, i, max_classes_count_str_len, len(class_path_to_classes[cp]), cp)) + + print_box_message('Duplicate classes detail info:') + for idx, (class_paths, classes) in enumerate(class_paths_to_duplicate_classes, start=1): + print('[%*s] found %s duplicate classes in %s class paths %s :' % ( + max_idx_str_len, idx, len(classes), len(class_paths), ' '.join(class_paths))) + + max_class_idx_str_len = str_len(len(classes)) + for i, c in enumerate(classes, start=1): + print(' %*s: %s' % (max_class_idx_str_len, i, c)) + + +def print_class_paths_info(class_path_to_classes): + if not class_path_to_classes: + return + + max_idx_str_len = str_len(len(class_path_to_classes)) + max_classes_count_str_len = str_len(max(len(classes) for classes in class_path_to_classes.values())) + + class_path_to_classes = sorted(class_path_to_classes.items(), key=lambda item: item[0]) + print_box_message('Find in %s class paths:' % len(class_path_to_classes)) + for idx, (cp, classes) in enumerate(class_path_to_classes, start=1): + print('%*s: (contain %*s classes) %s' % ( + max_idx_str_len, idx, max_classes_count_str_len, len(classes), cp)) + + +def main(): + option_parser = OptionParser( + usage='%prog [OPTION]...' + ' [-c class-dir1 [-c class-dir2] ...]' + ' [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...]' + '\nFind duplicate classes among java lib dirs and class dirs.' + '\n\nExamples:' + '\n %prog # search jars from current dir' + '\n %prog path/to/lib_dir1 /path/to/lib_dir2' + '\n %prog -c path/to/class_dir1 -c /path/to/class_dir2' + '\n %prog -c path/to/class_dir1 path/to/lib_dir1' + '\n %prog -L path/to/lib_dir1' + '\n %prog -J path/to/lib_dir1', + version='%prog ' + PROG_VERSION) + option_parser.add_option('-L', '--recursive-lib', dest='recursive_lib', default=False, + action='store_true', help='search jars in the sub-directories of lib dir') + option_parser.add_option('-J', '--recursive-jar', dest='recursive_jar', default=False, + action='store_true', help='search jars in the jar file') + option_parser.add_option('-c', '--class-dir', dest='class_dirs', default=[], + action='append', help='add class dir') + option_parser.add_option('-R', '--no-find-progress', dest='show_responsive', default=True, + action='store_false', help='do not display responsive find progress') + + options, lib_dirs = option_parser.parse_args() + class_dirs = options.class_dirs + if not lib_dirs and not class_dirs: + lib_dirs = ['.'] + global __show_responsive + __show_responsive = options.show_responsive + + jar_files = list_jar_file_under_lib_dirs(lib_dirs, recursive=options.recursive_lib) + if not jar_files and not class_dirs: + clear_responsive_message() + print('search no jar files under lib dirs, and class dirs is absent.') + return 0 + class_path_to_classes = collect_class_path_to_classes(class_dirs, jar_files, options.recursive_jar) + if all(not classes for classes in class_path_to_classes.values()): + clear_responsive_message() + print('find no class files in jar files or class dirs.') + return 0 + + print_responsive_message('find duplicate classes...') + class_to_class_paths = invert_as_class_to_class_paths(class_path_to_classes) + class_paths_to_duplicate_classes = find_duplicate_classes(class_to_class_paths) + + clear_responsive_message() + print_duplicate_classes_info(class_paths_to_duplicate_classes, class_path_to_classes) + print_class_paths_info(class_path_to_classes) + + return int(bool(class_paths_to_duplicate_classes)) + + +if __name__ == '__main__': + exit(main()) diff --git a/bin/taoc b/bin/taoc new file mode 100755 index 00000000..5e5fcfc0 --- /dev/null +++ b/bin/taoc @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# @Function +# tac lines colorfully. taoc means coat(*CO*lorful c*AT*) in reverse(last line first). +# +# @Usage +# $ echo -e 'Hello\nWorld' | taoc +# $ taoc /path/to/file1 +# $ taoc /path/to/file1 /path/to/file2 +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-coat +# @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail + +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# parse options +################################################################################ + +usage() { + cat <= 0; --idx)); do + [ "${args[idx]}" = --help ] && usage + [ "${args[idx]}" = --version ] && progVersion +done +unset args idx + +################################################################################ +# biz logic +################################################################################ + +# if stdout is not a terminal, use `tac` directly. +# '-t' check: is a terminal? +# check isatty in bash https://stackoverflow.com/questions/10022323 +[ -t 1 ] || exec tac "$@" + +readonly -a ROTATE_COLORS=(33 35 36 31 32 37 34) +COLOR_INDEX=0 +# CAUTION: print content WITHOUT new line +rotateColorPrint() { + local content=$* + # skip color for white space + if [[ $content =~ ^[[:space:]]*$ ]]; then + printf %s "$content" + else + local color=${ROTATE_COLORS[COLOR_INDEX++ % ${#ROTATE_COLORS[@]}]} + printf '\e[1;%sm%s\e[0m' "$color" "$content" + fi +} + +rotateColorPrintln() { + # NOTE: $'foo' is the escape sequence syntax of bash + rotateColorPrint "$*"$'\n' +} + +colorLines() { + local line + # Bash read line does not read leading spaces + # https://stackoverflow.com/questions/29689172 + while IFS= read -r line; do + rotateColorPrintln "$line" + done + # How to use `while read` (Bash) to read the last line in a file + # if there’s no newline at the end of the file? + # https://stackoverflow.com/questions/4165135 + [ -z "$line" ] || rotateColorPrint "$line" +} + +tac "$@" | colorLines diff --git a/bin/tcp-connection-state-counter b/bin/tcp-connection-state-counter index 86b60351..dc7dae87 100755 --- a/bin/tcp-connection-state-counter +++ b/bin/tcp-connection-state-counter @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # show count of tcp connection stat. # @@ -8,16 +8,68 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-tcp-connection-state-counter # @author Jerry Lee (oldratlee at gmail dot com) # @author @sunuslee (sunuslee at gmail dot com) +set -eEuo pipefail + +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# parse options +################################################################################ + +usage() { + cat <= 0; --idx)); do + [[ "${args[idx]}" = -h || "${args[idx]}" = --help ]] && usage + [[ "${args[idx]}" = -V || "${args[idx]}" = --version ]] && progVersion +done +unset args idx + +################################################################################ +# biz logic +################################################################################ # On MacOS, netstat need to using -p tcp to get only tcp output. -uname | grep Darwin -q && option_for_mac="-ptcp" +UNAME=$(uname) +[[ $UNAME = Darwin* ]] && option_for_mac=-ptcp -netstat -tna $option_for_mac | awk 'NR > 2 { - s[$NF]++ +# shellcheck disable=SC2086 +netstat -tna ${option_for_mac:-} | awk 'NR > 2 { + ++s[$NF] } END { + # get max length of stat and count + for(v in s) { + stat_len = length(v) + if(stat_len > max_stat_len) max_stat_len = stat_len + + count_len = length(s[v]) + if (count_len > max_count_len) max_count_len = count_len + } + for(v in s) { - printf "%-11s %s\n", v, s[v] + printf "%-" max_stat_len "s %" max_count_len "s\n", v, s[v] } }' | sort -nr -k2,2 diff --git a/bin/uq b/bin/uq new file mode 100755 index 00000000..535f9a85 --- /dev/null +++ b/bin/uq @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# @Function +# Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output). +# same as `uniq` command in core utils, +# but detect repeated lines that are not adjacent, no sorting required. +# +# @Usage +# uq [OPTION]... [INPUT [OUTPUT]] +# +# @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-uq +# @author Zava Xu (zava.kid at gmail dot com) +# @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail + +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# util functions +################################################################################ + +# NOTE: $'foo' is the escape sequence syntax of bash +readonly NL=$'\n' # new line + +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} + +yellowPrint() { + if [ -t 1 ]; then + printf '\e[1;33m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} + +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done + + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." + + exit "$exit_status" +} >&2 + +convertHumanReadableSizeToSize() { + local human_readable_size=$1 + + [[ "$human_readable_size" =~ ^([0-9][0-9]*)([kmg]?)$ ]] || return 1 + + local size=${BASH_REMATCH[1]} unit=${BASH_REMATCH[2]} + case "$unit" in + k) + ((size *= 1024)) + ;; + m) + ((size *= 1024 ** 2)) + ;; + g) + ((size *= 1024 ** 3)) + ;; + esac + + echo "$size" +} + +usage() { + cat < 0)); do + case "$1" in + -c | --count) + uq_opt_count=1 + shift + ;; + -d | --repeated) + uq_opt_only_repeated=1 + shift + ;; + -D) + uq_opt_all_repeated=1 + shift + ;; + --all-repeated=*) + uq_opt_all_repeated=1 + + uq_opt_repeated_method=${1#--all-repeated=} + [[ $uq_opt_repeated_method = 'none' || $uq_opt_repeated_method = 'prepend' || $uq_opt_repeated_method = 'separate' ]] || + die -h "invalid argument ‘$uq_opt_repeated_method’ for ‘--all-repeated’${NL}Valid arguments are:$NL - ‘none’$NL - ‘prepend’$NL - ‘separate’" + + shift + ;; + -u | --unique) + uq_opt_only_unique=1 + shift + ;; + -i | --ignore-case) + uq_opt_ignore_case=1 + shift + ;; + -z | --zero-terminated) + uq_opt_zero_terminated=1 + shift + ;; + -XM | --max-input) + uq_max_input_human_readable_size=$2 + shift 2 + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + argv=(${argv[@]:+"${argv[@]}"} "$@") + break + ;; + -) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + argv=(${argv[@]:+"${argv[@]}"} "$1") + shift + ;; + esac +done + +[[ $uq_opt_only_repeated = 1 && $uq_opt_only_unique = 1 ]] && + die -h "printing duplicated lines(-d, --repeated) and unique lines(-u, --unique) is meaningless" +[[ $uq_opt_all_repeated = 1 && $uq_opt_only_unique = 1 ]] && + die -h "printing all duplicate lines(-D, --all-repeated) and unique lines(-u, --unique) is meaningless" + +[[ $uq_opt_all_repeated = 1 && $uq_opt_repeated_method = none && ($uq_opt_count = 0 && $uq_opt_only_repeated = 0) ]] && + yellowPrint "WARN: -D/--all-repeated=none option without -c/-d option, just cat input simply!" >&2 + +# DO NOT declare and assign var uq_max_input_size(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 +uq_max_input_size=$(convertHumanReadableSizeToSize "$uq_max_input_human_readable_size") || + die -h "illegal value of option -XM/--max-input: $uq_max_input_human_readable_size" + +readonly argc=${#argv[@]} argv uq_max_input_size + +if ((argc == 0)); then + input_files=() + output_file=/dev/stdout +elif ((argc == 1)); then + input_files=("${argv[0]}") + output_file=/dev/stdout +else + input_files=("${argv[@]:0:argc-1}") + output_file=${argv[argc - 1]} + if [ "$output_file" = - ]; then + output_file=/dev/stdout + fi +fi +readonly output_file + +# Check input file +for f in ${input_files[@]:+"${input_files[@]}"}; do + # - is stdin, ok + [ "$f" = - ] && continue + + [ -e "$f" ] || die "input file $f: No such file or directory!" + [ ! -d "$f" ] || die "input file $f exists, but is a directory!" + [ -f "$f" ] || die "input file $f exists, but is not a file!" + [ -r "$f" ] || die "input file $f exists, but is not readable!" +done +unset f + +################################################################################ +# biz logic +################################################################################ + +# uq awk script +# +# edit in a separated file(eg: uq.awk) then copy here, +# maybe more convenient(like good syntax highlight) + +# shellcheck disable=SC2016 +readonly uq_awk_script=' + +function printResult(for_lines) { + for (idx = 0; idx < length(for_lines); idx++) { + line = for_lines[idx] + count = line_count_array[caseAwareLine(line)] + #printf "DEBUG: %s %s, index: %s, uq_opt_only_repeated: %s\n", count, line, idx, uq_opt_only_repeated + + if (uq_opt_only_unique) { + if (count == 1) printLine(count, line) + } else { + if (uq_opt_only_repeated && count <= 1) continue + + if (uq_opt_repeated_method == "prepend" || uq_opt_repeated_method == "separate" && previous_output) { + if (line != previous_output) print "" + } + + printLine(count, line) + previous_output = line + } + } +} + +function printLine(count, line) { + if (uq_opt_count) printf "%7s %s%s", count, line, ORS + else print line +} + +function caseAwareLine(line) { + if (IGNORECASE) return tolower(line) + else return line +} + +BEGIN { + if (uq_opt_zero_terminated) ORS = RS = "\0" +} + +{ + total_input_size += length + 1 + if (total_input_size > uq_max_input_size) { + printf "%s: input size exceed max input size %s!\nuse option -XM/--max-input specify a REASONABLE larger value.\n", + uq_PROG, uq_max_input_human_readable_size > "/dev/stderr" + exit(1) + } + + # use index to keep lines order + original_lines[line_index++] = $0 + + case_aware_line = caseAwareLine($0) + # line_count_array: line content -> count + if (++line_count_array[case_aware_line] == 1) { + # use index to keep lines order + deduplicated_lines[deduplicated_line_index++] = case_aware_line + } +} + +END { + if (uq_opt_all_repeated) printResult(original_lines) + else printResult(deduplicated_lines) +} + +' + +awk \ + -v "uq_opt_count=$uq_opt_count" \ + -v "uq_opt_only_repeated=$uq_opt_only_repeated" \ + -v "uq_opt_all_repeated=$uq_opt_all_repeated" \ + -v "uq_opt_repeated_method=$uq_opt_repeated_method" \ + -v "uq_opt_only_unique=$uq_opt_only_unique" \ + -v "IGNORECASE=$uq_opt_ignore_case" \ + -v "uq_opt_zero_terminated=$uq_opt_zero_terminated" \ + -v "uq_max_input_human_readable_size=$uq_max_input_human_readable_size" \ + -v "uq_max_input_size=$uq_max_input_size" \ + -v "uq_PROG=$PROG" \ + -f <(printf "%s" "$uq_awk_script") \ + -- ${input_files[@]:+"${input_files[@]}"} \ + >"$output_file" diff --git a/bin/xpf b/bin/xpf index 2e4ba51a..34af6d91 100755 --- a/bin/xpf +++ b/bin/xpf @@ -1,13 +1,36 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Open file in file explorer, file is selected. -# same as xpl --selected [file [file ...] ] +# same as xpl --selected [file]... # # @Usage # $ ./xpf file # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -BASE="$(dirname "$0")" -source "$BASE/xpl" "$@" +################################################################################ +# util functions +################################################################################ + +# `realpath` command exists on Linux and macOS, return resolved physical path +# - realpath command on macOS do NOT support option `-e`; +# combined `[ -e $file ]` to check file existence first. +# - How can I get the behavior of GNU's readlink -f on a Mac? +# https://stackoverflow.com/questions/1055671 +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +################################################################################ +# biz logic +################################################################################ + +# DO NOT inline THIS_SCRIPT into BASE_DIR, because sub-shell: +# BASE_DIR=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")") +THIS_SCRIPT=$(realpath "${BASH_SOURCE[0]}") +BASE_DIR=$(dirname -- "$THIS_SCRIPT") + +# shellcheck disable=SC1091 +source "$BASE_DIR/xpl" "$@" diff --git a/bin/xpl b/bin/xpl index b79bd271..392415aa 100755 --- a/bin/xpl +++ b/bin/xpl @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # Open file in file explorer. # @@ -7,93 +7,165 @@ # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-xpl-and-xpf # @author Jerry Lee (oldratlee at gmail dot com) +set -eEuo pipefail -PROG=`basename $0` +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' -usage() { - [ -n "$1" -a "$1" != 0 ] && local out=/dev/stderr || local out=/dev/stdout +################################################################################ +# util functions +################################################################################ + +redPrint() { + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;31m%s\e[0m\n' "$*" + else + printf '%s\n' "$*" + fi +} + +die() { + local prompt_help=false exit_status=2 + while (($# > 0)); do + case "$1" in + -h) + prompt_help=true + shift + ;; + -s) + exit_status=$2 + shift 2 + ;; + *) + break + ;; + esac + done - [ $# -gt 1 ] && { echo "$2"; echo; } > $out + (($# > 0)) && redPrint "$PROG: $*" + $prompt_help && echo "Try '$PROG --help' for more information." - cat <&2 + +usage() { + cat < 0)); do + case "$1" in + -s | --selected) + selected=true + shift + ;; + -h | --help) + usage + ;; + -V | --version) + progVersion + ;; + --) + shift + files=(${files[@]:+"${files[@]}"} "$@") + break + ;; + -*) + die -h "unrecognized option '$1'" + ;; + *) + files=(${files[@]:+"${files[@]}"} "$1") + shift + ;; + esac done +# if files is empty, use one element "." +files=("${files[@]:-.}") + +# if program name is xpf, set option selected! +[ "xpf" = "$PROG" ] && selected=true + +readonly files selected + ################################################################################ -# biz options +# biz logic ################################################################################ # open one file openOneFile() { - local file="$1" - - case "$(uname)" in - Darwin*) - [ -f "${file}" ] && selected=true - open ${selected:+-R} "$file" - ;; - CYGWIN*) - [ -f "${file}" ] && selected=true - explorer ${selected:+/select,} "$(cygpath -w "${file}")" - ;; - *) - if [ -d "${file}" ] ; then - nautilus "$(dirname "${file}")" - else - if [ -z "${selected}" ] ; then - nautilus "$(dirname "${file}")" - else - nautilus "${file}" - fi - fi - ;; - esac + local file=$1 slt=$selected + + case "$(uname)" in + Darwin*) + [ -f "$file" ] && slt=true + if $slt; then + open -R "$file" + else + open "$file" + fi + ;; + CYGWIN*) + [ -f "$file" ] && slt=true + if $slt; then + explorer /select, "$(cygpath -w "$file")" + else + explorer "$(cygpath -w "$file")" + fi + ;; + *) + if [ -d "$file" ]; then + nautilus "$(dirname -- "$file")" + else + if $slt; then + nautilus "$file" + else + nautilus "$(dirname -- "$file")" + fi + fi + ;; + esac + + local selected_msg + $slt && selected_msg='with selection' + printf 'open %14s: %s\n' "$selected_msg" "$file" } -[ "${#args[@]}" == 0 ] && files=( . ) || files=( "${args[@]}" ) -for file in "${files[@]}" ; do - [ ! -e "$file" ] && { echo "$file not exsited!"; continue; } +has_error=false + +for file in "${files[@]}"; do + [ -e "$file" ] || { + has_error=true + redPrint "$PROG: $file: No such file or directory!" >&2 + continue + } - openOneFile "$file" - echo "$file opened${selected:+ with selection}!" + openOneFile "$file" || has_error=true done + +# set exit status +! $has_error diff --git a/docs/developer-guide.md b/docs/developer-guide.md new file mode 100644 index 00000000..af731386 --- /dev/null +++ b/docs/developer-guide.md @@ -0,0 +1,44 @@ +# 📚 `Shell`学习与开发的资料 + +- 🛠️ 开发规范与工具 + - [`Google Shell Style Guide`](https://google.github.io/styleguide/shell.xml) | [中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-shell-styleguide/contents.html) + - [`koalaman/shellcheck`](https://github.com/koalaman/shellcheck): `ShellCheck`, a static analysis tool for shell scripts + - [`mvdan/sh(shfmt)`](https://github.com/mvdan/sh): `shfmt` formats shell programs +- 👷 **`Bash/Shell`最佳实践与安全编程**文章 + - [Use the Unofficial Bash Strict Mode (Unless You Looove Debugging)](http://redsymbol.net/articles/unofficial-bash-strict-mode/) + - Bash Pitfalls: 编程易犯的错误 - 团子的小窝:[Part 1](http://kodango.com/bash-pitfalls-part-1) | [Part 2](http://kodango.com/bash-pitfalls-part-2) | [Part 3](http://kodango.com/bash-pitfalls-part-3) | [Part 4](http://kodango.com/bash-pitfalls-part-4) | [英文原文:Bash Pitfalls](http://mywiki.wooledge.org/BashPitfalls) + - [编写可靠shell脚本的八个建议 - xshell.net](https://www.xshell.net/shell/1577.html) + - [Shell 编码风格 - 团子的小窝](http://kodango.com/shell-script-style) + - [Bash 优良编程实践](https://www.techug.com/post/bash-practice.html) + - [不要自己去指定`sh`的方式去执行脚本](https://github.com/oldratlee/useful-scripts/issues/57#issuecomment-326485965) +- 🎶 **Tips** + - [让你提升命令行效率的 Bash 快捷键 【完整版】](https://linuxtoy.org/archives/bash-shortcuts.html) + 补充:`ctrl + x, ctrl + e` 就地打开文本编辑器来编辑当前命令行,对于复杂命令行特别有用 + - [应该知道的Linux技巧 | 酷 壳 - CoolShell](https://coolshell.cn/articles/8883.html) + - 简洁的 Bash Programming 技巧 - 团子的小窝:[Part 1](http://kodango.com/simple-bash-programming-skills) | [Part 2](http://kodango.com/simple-bash-programming-skills-2) | [Part 3](http://kodango.com/simple-bash-programming-skills-3) + - [Bash 测试和比较函数 — `test`、`[`、`[[`、`((`、和 `if-then-else` 解密](https://www.ibm.com/developerworks/cn/linux/l-bash-test.html) + - [Filenames and Pathnames in Shell (bash, dash, ash, ksh, and so on): How to do it Correctly](https://dwheeler.com/essays/filenames-in-shell.html) + - [理解 IFS - 团子的小窝](http://kodango.com/understand-ifs) + - [shell中的IFS详解 – 笑遍世界](http://smilejay.com/2011/12/bash_ifs/) + - [Bash脚本:怎样一行行地读文件(最好和最坏的方法)](http://blog.jobbole.com/72185/) + - [Shell 脚本避免多次重复 source - 团子的小窝](http://kodango.com/avoid-repeated-source-in-shell) + - [一个奇怪的 echo 结果 - 团子的小窝](http://kodango.com/a-strange-echo-result) + - [浅谈 Shell 脚本配置文件格式 - 团子的小窝](http://kodango.com/config-file-format-in-shell) + - [Bash function 还能这么玩 - 团子的小窝](http://kodango.com/bash-functions) + - [Bash 获取当前函数名 - 团子的小窝](http://kodango.com/get-function-name-in-bash) + - [Zsh和Bash,究竟有何不同 坑很深](https://www.xshell.net/shell/bash_zsh.html) +- 💎 **系统学习** — 看文章、了解Tips完全不能替代系统学习才能真正理解并专业开发! + - [《Bash Pocket Reference》](https://book.douban.com/subject/26738258/) + 力荐!说明简单直接结构体系的佳作,专业`Bash`编程必备!且16年的第二版更新到了新版的`Bash 4` + - [《学习bash》](https://book.douban.com/subject/1241361/) 上面那本的展开版 + - 官方资料 + - [`bash man`](https://manned.org/bash) | [中文版](http://ahei.info/chinese-bash-man.htm) + - [Bash Reference Manual - gnu.org](http://www.gnu.org/software/bash/manual/) | [中文版](https://yiyibooks.cn/Phiix/bash_reference_manual/bash%E5%8F%82%E8%80%83%E6%96%87%E6%A1%A3.html) + Bash参考手册,讲得全面且有深度,比如会全面地讲解不同转义的区别、命令的解析过程,这有助统一深入的方式认识Bash整个执行方式和过程。这些内容在其它书中往往不会讲(因为复杂难于深入浅出的讲解),但却一通百通的关键。 + - [Advanced Bash-Scripting Guide](https://hangar118.sdf.org/p/bash-scripting-guide/index.html): An in-depth exploration of the art of shell scripting. + - [命令行的艺术 - `jlevy/the-art-of-command-line`](https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md) + - [`awesome-lists/awesome-bash`](https://github.com/awesome-lists/awesome-bash): A curated list of delightful Bash scripts and resources. + - [`alebcay/awesome-shell`](https://github.com/alebcay/awesome-shell): A curated list of awesome command-line frameworks, toolkits, guides and gizmos. + - [wzb56/13_questions_of_shell: shell十三问 - shell教程](https://github.com/wzb56/13_questions_of_shell) + - [实用 Shell 文档 - 团子的小窝](http://kodango.com/useful-documents-about-shell) + - 更多书籍参见个人整理的[书籍豆列 **_`Bash/Shell`_**](https://www.douban.com/doulist/1779379/) diff --git a/docs/java.md b/docs/java.md index 0676f2bc..8715329e 100644 --- a/docs/java.md +++ b/docs/java.md @@ -4,7 +4,6 @@ - - [🍺 show-busy-java-threads](#-show-busy-java-threads) - [用法](#%E7%94%A8%E6%B3%95) - [示例](#%E7%A4%BA%E4%BE%8B) @@ -27,10 +26,10 @@ ------------------------------- -关于`Java`排错与诊断,力荐️`Arthas` ❤️ +关于`Java`排错与诊断,力荐️`Arthas`: ❤️ -- [alibaba/arthas: Alibaba Java诊断利器 - github.com](https://github.com/alibaba/arthas) -- `Arthas`用户文档 https://alibaba.github.io/arthas/ +- `Arthas`用户文档: https://arthas.aliyun.com/doc/quick-start.html +- GitHub Repo: [alibaba/arthas: Alibaba Java诊断利器](https://github.com/alibaba/arthas) `Arthas`功能异常(😜)强劲,且在阿里巴巴线上支持使用多年。我自己也常用,一定要看看用用! @@ -51,9 +50,9 @@ ---------------------- 用于快速排查`Java`的`CPU`性能问题(`top us`值过高),自动查出运行的`Java`进程中消耗`CPU`多的线程,并打印出其线程栈,从而确定导致性能问题的方法调用。 -目前只支持`Linux`。原因是`Mac`、`Windows`的`ps`命令不支持列出进程的线程`id`,更多信息参见[#33](https://github.com/oldratlee/useful-scripts/issues/33),欢迎提供解法。 +目前只支持`Linux`。原因是`Mac`、`Windows`的`ps`命令不支持列出进程的线程`id`,更多信息参见 [#33](https://github.com/oldratlee/useful-scripts/issues/33),欢迎提供解法。 -PS,如何操作可以参见[@bluedavy](http://weibo.com/bluedavy)的[《分布式Java应用》](https://book.douban.com/subject/4848587/)的【5.1.1 `CPU`消耗分析】一节,说得很详细: +PS,如何操作可以参见[`@bluedavy`](http://weibo.com/bluedavy)的[《分布式Java应用》](https://book.douban.com/subject/4848587/)的【5.1.1 `CPU`消耗分析】一节,说得很详细: 1. `top`命令找出消耗`CPU`高的`Java`进程及其线程`id`: 1. 开启线程显示模式(`top -H`,或是打开`top`后按`H`) @@ -65,7 +64,8 @@ PS,如何操作可以参见[@bluedavy](http://weibo.com/bluedavy)的[《分布 1. 在`jstack`输出中查找十六进制的线程`id`(可以用`vim`的查找功能`/0x1234`,或是`grep 0x1234 -A 20`) 1. 查看对应的线程栈,分析问题 -查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。 +查问题时,会要多次上面的操作以分析确定问题,这个过程**太繁琐太慢了**。 +期望整合上面的过程成一个脚本,这样一行命令就可以自动化地搞定。 ### 用法 @@ -74,10 +74,12 @@ show-busy-java-threads # 从所有运行的Java进程中找出最消耗CPU的线程(缺省5个),打印出其线程栈 # 缺省会自动从所有的Java进程中找出最消耗CPU的线程,这样用更方便 -# 当然你可以手动指定要分析的Java进程Id,以保证只会显示你关心的那个Java进程的信息 +# 当然你可以通过 -p 选项 手动指定要分析的Java进程Id,以保证只会显示你关心的那个Java进程的信息 show-busy-java-threads -p <指定的Java进程Id> +show-busy-java-threads -p 42 +show-busy-java-threads -p 42,47 -show-busy-java-threads -c <要显示的线程栈数> +show-busy-java-threads -c <要展示示的线程栈个数> show-busy-java-threads <重复执行的间隔秒数> [<重复执行的次数>] # 多次执行;这2个参数的使用方式类似vmstat命令 @@ -97,14 +99,14 @@ sudo show-busy-java-threads show-busy-java-threads -s <指定jstack命令的全路径> # 对于sudo方式的运行,JAVA_HOME环境变量不能传递给root, -# 而root用户往往没有配置JAVA_HOME且不方便配置, -# 显式指定jstack命令的路径就反而显得更方便了 +# 而root用户往往没有配置JAVA_HOME且不方便配置,不能找到jstack命令。 +# 这时显式指定jstack命令的路径就反而显得更方便了 -# -m选项:执行jstack命令时加上-m选项,显示上Native的栈帧,一般应用排查不需要使用 +# -m 选项:执行jstack命令时加上 -m 选项,显示上Native的栈帧,一般应用排查不需要使用 show-busy-java-threads -m -# -F选项:执行jstack命令时加上 -F 选项(如果直接jstack无响应时,用于强制jstack),一般情况不需要使用 +# -F 选项:执行jstack命令时加上 -F 选项(如果直接jstack无响应时,用于强制jstack),一般情况不需要使用 show-busy-java-threads -F -# -l选项:执行jstack命令时加上 -l 选项,显示上更多相关锁的信息,一般情况不需要使用 +# -l 选项:执行jstack命令时加上 -l 选项,显示上更多相关锁的信息,一般情况不需要使用 # 注意:和 -m -F 选项一起使用时,可能会大大增加jstack操作的耗时 show-busy-java-threads -l @@ -120,8 +122,9 @@ Example: show-busy-java-threads 3 10 # update every 3 seconds, update 10 times Output control: - -p, --pid find out the highest cpu consumed threads from + -p, --pid find out the highest cpu consumed threads from the specified java process. + support pid list(eg: 42,47). default from all java process. -c, --count set the thread count to show, default is 5. set count 0 to show all threads. @@ -138,27 +141,23 @@ Output control: jstack control: -s, --jstack-path specifies the path of jstack command. - -F, --force set jstack to force a thread dump. use when jstack - does not respond (process is hung). - -m, --mix-native-frames set jstack to print both java and native frames - (mixed mode). + -F, --force set jstack to force a thread dump. + use when jstack does not respond (process is hung). + -m, --mix-native-frames set jstack to print both java and + native frames (mixed mode). -l, --lock-info set jstack with long listing. prints additional information about locks. CPU usage calculation control: - -d, --top-delay specifies the delay between top samples. - default is 0.5 (second). get thread cpu percentage - during this delay interval. - more info see top -d option. eg: -d 1 (1 second). - -P, --use-ps use ps command to find busy thread(cpu usage) - instead of top command. - default use top command, because cpu usage of - ps command is expressed as the percentage of - time spent running during the *entire lifetime* - of a process, this is not ideal in general. + -i, --cpu-sample-interval specifies the delay between cpu samples to get + thread cpu usage percentage during this interval. + default is 0.5 (second). + set interval 0 to get the percentage of time spent + running during the *entire lifetime* of a process. Miscellaneous: -h, --help display this help and exit. + -V, --version display version information and exit. ``` ### 示例 @@ -210,7 +209,7 @@ $ show-busy-java-threads ### 贡献者 -- [silentforce](https://github.com/silentforce)改进此脚本,增加对环境变量`JAVA_HOME`的判断。 [#15](https://github.com/oldratlee/useful-scripts/pull/15) +- [silentforce](https://github.com/silentforce) 改进此脚本,增加对环境变量`JAVA_HOME`的判断。 [#15](https://github.com/oldratlee/useful-scripts/pull/15) - [liuyangc3](https://github.com/liuyangc3) - 发现并解决`jstack`非当前用户`Java`进程的问题。 [#50](https://github.com/oldratlee/useful-scripts/pull/50) - 优化性能,通过`read -a`简化反复的`awk`操作。 [#51](https://github.com/oldratlee/useful-scripts/pull/51) @@ -227,15 +226,20 @@ $ show-busy-java-threads ---------------------- 找出`Java Lib`(`Java`库,即`Jar`文件)或`Class`目录(类目录)中的重复类。 -全系统支持(`Python`实现,安装`Python`即可),如`Linux`、`Mac`、`Windows`。 +全系统支持(`Python 3`实现,安装`Python 3`即可),如`Linux`、`Mac`、`Windows`。 `Java`开发的一个麻烦的问题是`Jar`冲突(即多个版本的`Jar`),或者说重复类。会出`NoSuchMethod`等的问题,还不见得当时出问题。找出有重复类的`Jar`,可以防患未然。 ### 用法 -- 通过脚本参数指定`Libs`目录,查找目录下`Jar`文件,收集`Jar`文件中`Class`文件以分析重复类。可以指定多个`Libs`目录。 - 注意,只会查找这个目录下`Jar`文件,不会查找子目录下`Jar`文件。因为`Libs`目录一般不会用子目录再放`Jar`,这样也避免把去查找不期望`Jar`。 -- 通过`-c`选项指定`Class`目录,直接收集这个目录下的`Class`文件以分析重复类。可以指定多个`Class`目录。 +- 通过脚本参数 指定 `Libs`目录,查找目录下`Jar`文件,收集`Jar`文件中`Class`文件以分析重复类。可以指定多个`Libs`目录。 + - 缺省只会查找指定`Lib`目录下`Jar`文件,不会收集`Lib`目录的子目录下`Jar`文件。 + - 因为`Libs`目录一般不会用子目录再放`Jar`,也避免把去查找不期望的`Jar`文件。 + - 可以通过 `-L`选项 设置 收集`Lib`子目录下的`Jar`文件;这样可以简化`Lib`目录的设置,不需要指定完整的`Lib`目录路径。 + - 对于找到的`Jar`文件,缺省不会进一步收集包含在`Jar`文件中的`Jar`。 + - 即`FatJar`/`UberJar`的场景,随着像`SpringBoot`的广泛使用,`FatJar`/`UberJar`也比较常见。 + - 可以通过 `-J`选项 设置 收集包含在`Jar`文件中的`Jar`。 +- 通过`-c`选项 指定 `Class`目录,直接收集这个目录下的`Class`文件以分析重复类。可以多次指定多个`Class`目录。 ```bash # 查找当前目录下所有Jar中的重复类 @@ -243,6 +247,10 @@ show-duplicate-java-classes # 查找多个指定目录下所有Jar中的重复类 show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 +# 通过 -L 选项,收集子目录中的Jar文件 +show-duplicate-java-classes -L path/to/lib_dir1 +# 通过 -J 选项,收集包含在Jar文件中的Jar文件(即 收集包含在FatJar/UberJar中的Jar) +show-duplicate-java-classes -J path/to/lib_dir1 # 查找多个指定Class目录下的重复类。 Class目录 通过 -c 选项指定 show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 @@ -252,12 +260,26 @@ show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 -c path/to/class_ # 帮助信息 $ show-duplicate-java-classes -h -Usage: show-duplicate-java-classes [-c class-dir1 [-c class-dir2] ...] [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...] +Usage: show-duplicate-java-classes [OPTION]... [-c class-dir1 [-c class-dir2] ...] [lib-dir1|jar-file1 [lib-dir2|jar-file2] ...] +Find duplicate classes among java lib dirs and class dirs. + +Examples: + show-duplicate-java-classes # search jars from current dir + show-duplicate-java-classes path/to/lib_dir1 /path/to/lib_dir2 + show-duplicate-java-classes -c path/to/class_dir1 -c /path/to/class_dir2 + show-duplicate-java-classes -c path/to/class_dir1 path/to/lib_dir1 + show-duplicate-java-classes -L path/to/lib_dir1 + show-duplicate-java-classes -J path/to/lib_dir1 Options: + --version show program's version number and exit -h, --help show this help message and exit + -L, --recursive-lib search jars in the sub-directories of lib dir + -J, --recursive-jar search jars in the jar file -c CLASS_DIRS, --class-dir=CLASS_DIRS add class dir + -R, --no-find-progress + do not display responsive find progress ``` #### `JDK`开发场景使用说明 @@ -301,7 +323,7 @@ $ show-duplicate-java-classes -c target/war/WEB-INF/classes target/war/WEB-INF/l 在`App`的`build.gradle`中添加拷贝库到目录`build/dependencies`下。 -```java +```groovy task copyDependencies(type: Copy) { def dest = new File(buildDir, "dependencies") @@ -330,58 +352,65 @@ $ show-duplicate-java-classes WEB-INF/lib COOL! No duplicate classes found! ================================================================================ -class paths to find: +Find in 150 class paths: ================================================================================ -1 : WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar -2 : WEB-INF/lib/misc.htmlparser-0.0.0.jar -3 : WEB-INF/lib/normandy.client-1.0.2.jar + 1: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar + 2: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar + 3: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar + 4: (contain 687 classes) WEB-INF/lib/cassandra-0.6.1.jar ... $ show-duplicate-java-classes -c WEB-INF/classes WEB-INF/lib -Found duplicate classes in below class path: -1 (293@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.orm-2.5.6.SEC02.jar -2 (2@3): WEB-INF/lib/servlet-api-3.0-alpha-1.jar WEB-INF/lib/jsp-api-2.1-rev-1.jar WEB-INF/lib/jstl-api-1.2-rev-1.jar -3 (104@2): WEB-INF/lib/commons-io-2.2.jar WEB-INF/lib/jakarta.commons.io-2.0.jar -4 (6@3): WEB-INF/lib/jakarta.commons.logging-1.1.jar WEB-INF/lib/commons-logging-1.1.1.jar WEB-INF/lib/org.slf4j.jcl104-over-slf4j-1.5.6.jar -5 (344@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar +Found 1272 duplicate classes in 345 class paths and 9 class path sets: +[1] found 188(100%) duplicate classes in 3 class paths: + 1: (contain 188 classes) WEB-INF/lib/jdom-2.0.2.jar + 2: (contain 195 classes) WEB-INF/lib/jdom2-2.0.6.jar + 3: (contain 195 classes) WEB-INF/lib/jdom2-2.0.8.jar +[2] found 150(33.8%) duplicate classes in 2 class paths: + 1: (contain 1385 classes) WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: (contain 444 classes) WEB-INF/lib/netty-common-4.1.31.Final.jar +[3] found 148(55.4%) duplicate classes in 2 class paths: + 1: (contain 1385 classes) WEB-INF/lib/netty-all-4.0.35.Final.jar + 2: (contain 267 classes) WEB-INF/lib/netty-handler-4.1.31.Final.jar +[4] found 103(82.4%) duplicate classes in 2 class paths: + 1: (contain 125 classes) WEB-INF/lib/hessian-3.0.14.bugfix.jar + 2: (contain 275 classes) WEB-INF/lib/hessian-4.0.38.jar ... ================================================================================ Duplicate classes detail info: ================================================================================ -1 (293@2): WEB-INF/lib/sourceforge.spring-2.5.6.SEC02.jar WEB-INF/lib/sourceforge.spring.modules.orm-2.5.6.SEC02.jar - 1 org/springframework/orm/toplink/TopLinkTemplate$13.class - 2 org/springframework/orm/hibernate3/HibernateTemplate$24.class - 3 org/springframework/orm/jpa/vendor/HibernateJpaDialect.class - 4 org/springframework/orm/hibernate3/TypeDefinitionBean.class - 5 org/springframework/orm/hibernate3/SessionHolder.class - ... -2 (2@3): WEB-INF/lib/servlet-api-3.0-alpha-1.jar WEB-INF/lib/jsp-api-2.1-rev-1.jar WEB-INF/lib/jstl-api-1.2-rev-1.jar - 1 javax/servlet/ServletException.class - 2 javax/servlet/ServletContext.class -3 (104@2): WEB-INF/lib/commons-io-2.2.jar WEB-INF/lib/jakarta.commons.io-2.0.jar - 1 org/apache/commons/io/input/ProxyReader.class - 2 org/apache/commons/io/output/FileWriterWithEncoding.class - 3 org/apache/commons/io/output/TaggedOutputStream.class - 4 org/apache/commons/io/filefilter/NotFileFilter.class - 5 org/apache/commons/io/filefilter/TrueFileFilter.class - ... +[1] found 188 duplicate classes in 3 class paths WEB-INF/lib/jdom-2.0.2.jar WEB-INF/lib/jdom2-2.0.6.jar WEB-INF/lib/jdom2-2.0.8.jar : + 1: org/jdom2/Attribute.class + 2: org/jdom2/AttributeList$1.class + 3: org/jdom2/AttributeList$ALIterator.class + 4: org/jdom2/AttributeList.class + 5: org/jdom2/AttributeType.class + ... +[2] found 150 duplicate classes in 2 class paths WEB-INF/lib/netty-all-4.0.35.Final.jar WEB-INF/lib/netty-common-4.1.31.Final.jar : + 1: io/netty/util/AbstractReferenceCounted.class + 2: io/netty/util/Attribute.class + 3: io/netty/util/AttributeKey.class + 4: io/netty/util/AttributeMap.class + 5: io/netty/util/CharsetUtil.class + ... ... ================================================================================ -class paths to find: +Find in 232 class paths: ================================================================================ -1 : WEB-INF/lib/sourceforge.spring.modules.context-2.5.6.SEC02.jar -2 : WEB-INF/lib/misc.htmlparser-0.0.0.jar -3 : WEB-INF/lib/normandy.client-1.0.2.jar -4 : WEB-INF/lib/xml.xmlgraphics__batik-css-1.7.jar-1.7.jar -5 : WEB-INF/lib/jakarta.ecs-1.4.2.jar + 1: (contain 42 classes) WEB-INF/classes + 2: (contain 70 classes) WEB-INF/lib/HikariCP-2.7.8.jar + 3: (contain 13 classes) WEB-INF/lib/accessors-smart-1.2.jar + 4: (contain 9 classes) WEB-INF/lib/aopalliance-1.0.jar + 5: (contain 25 classes) WEB-INF/lib/asm-5.0.4.jar + 6: (contain 313 classes) WEB-INF/lib/aviator-5.0.0.jar ... ``` ### 贡献者 -[tgic](https://github.com/tg123)提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673) +[tgic](https://github.com/tg123) 提供此脚本。友情贡献者的链接 [commandlinefu.cn](http://commandlinefu.cn/) | [微博linux命令行精选](http://weibo.com/u/2674868673) @@ -420,9 +449,14 @@ find-in-jars 'log4j\.properties' -a find-in-jars 'log4j\.properties' -s ' <-> ' find-in-jars 'log4j\.properties' -s ' ' | awk '{print $2}' +# -l选项 指定 只列出Jar文件,不显示Jar文件内匹配的文件列表 +# 列出 包含log4j.xml文件的Jar文件: +find-in-jars -l 'log4j\.xml$' + # 帮助信息 $ find-in-jars -h Usage: find-in-jars [OPTION]... PATTERN + Find files in the jar files under specified directory, search jar files recursively(include subdirectory). The pattern default is *extended* regex. @@ -452,9 +486,15 @@ Output control: -a, --absolute-path always print absolute path of jar file -s, --separator specify the separator between jar file and zip entry. default is `!'. + -L, --files-not-contained-found + print only names of JAR FILEs NOT contained found + -l, --files-contained-found + print only names of JAR FILEs contained found + -R, --no-find-progress do not display responsive find progress Miscellaneous: -h, --help display this help and exit + -V, --version display version information and exit ``` 注意,Pattern缺省是`grep`的 **扩展**正则表达式。 @@ -465,6 +505,7 @@ Miscellaneous: # 在当前目录下的所有Jar文件中,查找出 log4j.properties文件 $ find-in-jars 'log4j\.properties$' ./hadoop-core-0.20.2-cdh3u3.jar!log4j.properties +...... # 查找出 以Service结尾的类,Jar文件路径输出成绝对路径 $ find-in-jars 'Service.class$' -a @@ -480,6 +521,13 @@ WEB-INF/lib/aspectjweaver-1.8.8.jar!org/aspectj/weaver/XlintDefault.properties ../deploy/lib/httpcore-4.3.3.jar!org/apache/http/version.properties ../deploy/lib/javax.servlet-api-3.0.1.jar!javax/servlet/http/LocalStrings_es.properties ...... + +# 列出 包含properties文件的Jar文件 +$ find-in-jars '\.properties$' -l -d WEB-INF/lib +WEB-INF/lib/aspectjtools-1.6.2.jar +WEB-INF/lib/aspectjweaver-1.8.8.jar +WEB-INF/lib/javax.servlet-api-3.0.1.jar +...... ``` ### 运行效果 @@ -490,4 +538,4 @@ WEB-INF/lib/aspectjweaver-1.8.8.jar!org/aspectj/weaver/XlintDefault.properties ### 参考资料 -[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.com/458/tech/shell/find-file-in-jar-zip-files.html) +[在多个Jar(Zip)文件查找Log4J配置文件的Shell命令行](http://oldratlee.github.io/458/tech/shell/find-file-in-jar-zip-files.html) diff --git a/docs/logo-social-original.png b/docs/logo-social-original.png new file mode 100644 index 00000000..9820b4ca Binary files /dev/null and b/docs/logo-social-original.png differ diff --git a/docs/logo-social.png b/docs/logo-social.png new file mode 100644 index 00000000..70a623e4 Binary files /dev/null and b/docs/logo-social.png differ diff --git a/docs/logo.meta.txt b/docs/logo.meta.txt new file mode 100644 index 00000000..ffa36200 --- /dev/null +++ b/docs/logo.meta.txt @@ -0,0 +1,6 @@ +logo is created by https://www.logoly.pro + +font: Zilla Slab + +logo.fond-size: 60 +logo-social.fond-size: 160 diff --git a/docs/logo.png b/docs/logo.png new file mode 100644 index 00000000..ede9eecc Binary files /dev/null and b/docs/logo.png differ diff --git a/docs/shell.md b/docs/shell.md index 21a4bdd5..4ff27aac 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -4,38 +4,39 @@ - - [`Shell`使用加强](#shell%E4%BD%BF%E7%94%A8%E5%8A%A0%E5%BC%BA) - [🍺 c](#-c) - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99) - - [🍺 coat](#-coat) - - [示例](#%E7%A4%BA%E4%BE%8B) + - [🍺 coat and taoc](#-coat-and-taoc) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-1) - [🍺 a2l](#-a2l) - - [示例](#%E7%A4%BA%E4%BE%8B-1) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-2) + - [🍺 uq](#-uq) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-3) - [🍺 ap and rp](#-ap-and-rp) - - [示例](#%E7%A4%BA%E4%BE%8B-2) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-4) + - [🍺 cp-into-docker-run](#-cp-into-docker-run) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-5) - [🍺 tcp-connection-state-counter](#-tcp-connection-state-counter) - - [用法](#%E7%94%A8%E6%B3%95) - - [示例](#%E7%A4%BA%E4%BE%8B-3) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-6) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85) - [🍺 xpl and xpf](#-xpl-and-xpf) - - [用法](#%E7%94%A8%E6%B3%95-1) - - [示例](#%E7%A4%BA%E4%BE%8B-4) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-7) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-1) - [`Shell`开发/测试加强](#shell%E5%BC%80%E5%8F%91%E6%B5%8B%E8%AF%95%E5%8A%A0%E5%BC%BA) - [🍺 echo-args](#-echo-args) - - [示例](#%E7%A4%BA%E4%BE%8B-5) + - [用法/示例](#%E7%94%A8%E6%B3%95%E7%A4%BA%E4%BE%8B-8) - [使用方式](#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F) - [🍺 console-text-color-themes.sh](#-console-text-color-themessh) - - [用法](#%E7%94%A8%E6%B3%95-2) - - [示例](#%E7%A4%BA%E4%BE%8B-6) + - [用法](#%E7%94%A8%E6%B3%95) + - [示例](#%E7%A4%BA%E4%BE%8B) - [运行效果](#%E8%BF%90%E8%A1%8C%E6%95%88%E6%9E%9C) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-2) - [参考资料](#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99-1) - [🍺 parseOpts.sh](#-parseoptssh) - - [用法](#%E7%94%A8%E6%B3%95-3) - - [示例](#%E7%A4%BA%E4%BE%8B-7) + - [用法](#%E7%94%A8%E6%B3%95-1) + - [示例](#%E7%A4%BA%E4%BE%8B-1) - [兼容性](#%E5%85%BC%E5%AE%B9%E6%80%A7) - [贡献者](#%E8%B4%A1%E7%8C%AE%E8%80%85-3) @@ -50,9 +51,9 @@ 原样命令行输出,并拷贝标准输出到系统剪贴板,省去`CTRL+C`操作,优化命令行与其它应用之间的操作流。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`c`意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便键入。 +命令名`c`的意思是`Copy`,因为这个命令我平时非常常用,所以使用一个字符的命令名,方便快速键入。 -更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip)。 +更多说明参见[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip)。 ### 用法/示例 @@ -97,7 +98,6 @@ Run command and put output to system clipper. If no command is specified, read from stdin(pipe). Example: - c echo "hello world!" c grep -i 'hello world' menu.h main.c set | c c -q < ~/.ssh/id_rsa.pub @@ -106,24 +106,29 @@ Options: -k, --keep-eol do not trim new line at end of file -q, --quiet suppress all normal output, default is false -h, --help display this help and exit + -V, --version display version information and exit ``` ### 参考资料 -- [拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 +- [拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 - 关于文本文件最后的换行,参见[Why should text files end with a newline?](https://stackoverflow.com/questions/729692) -🍺 [coat](../bin/coat) + + +🍺 [coat](../bin/coat) and [taoc](../bin/taoc) ---------------------- -彩色`cat`出文件行,方便人眼区分不同的行。 +彩色`cat`/`tac`出文件行,方便人眼区分不同的行。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令支持选项、功能和使用方式与[`cat`命令](https://linux.die.net/man/1/cat)完全一样(实际上读流操作在实现上全部代理给`cat`命令)。 +命令支持选项、功能和使用方式与[`cat`](https://manned.org/cat)/[`tac`](https://manned.org/tac)命令完全一样。 +文件操作在实现上完全代理给了`cat`/`tac`命令。 -命令名`coat`意思是`COlorful cAT`;当然单词`coat`的意思是外套,彩色输入行就像件漂亮的外套~ 😆 +- 命令名`coat`的意思是`COlorful cAT`;同时单词`coat`是外套,而彩色的输出行就像件漂亮的外套~ 🌈 😆 +- 命令名`taoc`是`coat`倒序拼写;命名方式就像`tac`之于`cat`。 🐈 -### 示例 +### 用法/示例 ```bash $ echo Hello world | coat @@ -131,6 +136,9 @@ Hello world $ echo -e 'Hello\nWorld' | coat Hello World +$ echo -e 'Hello\nWorld' | taoc +World +Hello $ echo -e 'Hello\nWorld' | nl | coat 1 Hello 2 World @@ -143,33 +151,28 @@ line2 of file2 ... # 帮助信息 -# 可以看到本人机器上实现代理的`cat`命令是GNU的实现。 +# 可以看到本人机器上实现代理的`cat`/`tac`命令是GNU的实现。 $ coat --help -Usage: cat [OPTION]... [FILE]... -Concatenate FILE(s) to standard output. - -With no FILE, or when FILE is -, read standard input. - - -A, --show-all equivalent to -vET - -b, --number-nonblank number nonempty output lines, overrides -n - -e equivalent to -vE - -E, --show-ends display $ at end of each line - -n, --number number all output lines - -s, --squeeze-blank suppress repeated empty output lines - -t equivalent to -vT - -T, --show-tabs display TAB characters as ^I - -u (ignored) - -v, --show-nonprinting use ^ and M- notation, except for LFD and TAB - --help display this help and exit - --version output version information and exit - -Examples: - cat f - g Output f's contents, then standard input, then g's contents. - cat Copy standard input to standard output. - -GNU coreutils online help: -Full documentation at: -or available locally via: info '(coreutils) cat invocation' +Usage: coat [OPTION]... [FILE]... +cat lines colorfully. + +Support options: + --help display this help and exit + --version output version information and exit +All other options and arguments are delegated to command cat, +more info see the help/man of command cat(e.g. cat --help). +cat executable: /usr/local/opt/coreutils/libexec/gnubin/cat + +$ taoc --help +Usage: taoc [OPTION]... [FILE]... +tac lines colorfully. + +Support options: + --help display this help and exit + --version output version information and exit +All other options and arguments are delegated to command tac, +more info see the help/man of command tac(e.g. tac --help). +tac executable: /usr/local/opt/coreutils/libexec/gnubin/tac ``` 注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,如下: @@ -181,9 +184,9 @@ or available locally via: info '(coreutils) cat invocation' 按行彩色输出参数,方便人眼查看。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`a2l`意思是`Arguments to(2) Lines`。 +命令名`a2l`的意思是`Arguments to(2) Lines`。 -### 示例 +### 用法/示例 ```bash $ a2l *.java @@ -195,22 +198,148 @@ B.java # 把参数按行输出方便查看 或是 grep $ a2l **/*.sh lib/console-text-color-themes.sh -test-cases/parseOpts-test.sh +test-cases/parseOpts_test.sh test-cases/self-installer.sh ... ``` 注:上面示例中,没有彩色;在控制台上运行可以看出彩色效果,和上面的`coat`命令一样。 +🍺 [uq](../bin/uq) +---------------------- + +不重排序输入完成整个输入行的去重。相比系统的`uniq`命令加强的是可以跨行去重,不需要排序输入。 +使用方式与支持的选项 模仿系统的`uniq`命令。支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 + +> ‼️ **_注意_**: 去重过程会在内存持有整个输入(因为全局去重)! +> +> 对于输入大小较大的场景(如输入量有几G),需谨慎使用以避免占用过多内存;往往需要结合业务场景开发对应的优化实现。 +> 虽然平时的大部分场景输入量非常有限(如几M),一个简单没有充分优化的实现是快速够用的。 +> +> `uq`处理的最大输入量缺省是 256m(字符数),超过了最大输入量则出错退出,以避免意外消耗了过大的内存; +> 可以通过`-XM, --max-input`选项 设置 消耗更多内存可接受的合理最大输入量,如`uq --max-input 1g ...` + +因为系统的`uniq`命令去重相邻的行,需要组合`sort`命令以对整个输入去重,并且有下面的问题: + +```bash +# 示例输入 +$ cat foo.txt +c +c +b +a +a +c +c + +$ uniq foo.txt +c +b +a +c +# c输出了2次,原因是第二个c与第一个c不是相邻的重复行 + +# 可以通过 sort -u 来完成整个输入去重,但这样操作,顺序与输入行不一致 +$ sort -u foo.txt +a +b +c +# 输入行重排序了! + +# 另外一个经典的用法 sort 与 uniq -c,输出重复次数 +$ sort foo.txt | uniq -c + 2 a + 1 b + 4 c +# 输入行重排序了! +``` + +### 用法/示例 + +```bash +$ uq foo.txt # 输入是文件 +$ cat foo.txt | uq # 或是 标准输入/管道 +c +b +a +# 对整个输入行去重,且顺序与输入行一致(保留第一次出现的位置) + +# -c 选项:输出重复次数 +$ uq -c foo.txt + 4 c + 1 b + 2 a + +# -d, --repeated 选项:只输出 重复行 +$ uq -d foo.txt +c +a +# -u, --unique 选项:只输出 唯一行(即不重复的行) +$ uq -u foo.txt +b + +# -D 选项:重复行都输出,即重复了几次就输出几次 +$ uq -D -c foo.txt + 4 c + 4 c + 1 b + 2 a + 2 a + 4 c + 4 c + +# 有多个文件参数时,最后一个参数 是 输出文件 +$ uq in1.txt in2.txt out.txt +# 当有多个输入文件时,但要输出到控制台时,指定输出文件(最后一个文件参数)为 `-` 即可 +$ uq in1.txt in2.txt - + +# 如果消耗更多内存可接受的合理的,可以通过 -XM, --max-input 选项设置更大的最大输入量(缺省是256m) +$ uq -MI 768m large-file-input +$ uq --max-input 10g huge-file-input + +# 帮助信息 +$ uq -h +Usage: uq [OPTION]... [INPUT [OUTPUT]] +Filter lines from INPUT (or standard input), writing to OUTPUT (or standard output). +Same as `uniq` command in core utils, +but detect repeated lines that are not adjacent, no sorting required. + +Example: + # only one file, output to stdout + uq in.txt + # more than 1 file, last file argument is output file + uq in.txt out.txt + # when use - as output file, output to stdout + uq in1.txt in2.txt - + +Options: + -c, --count prefix lines by the number of occurrences + -d, --repeated only print duplicate lines, one for each group + -D print all duplicate lines + combined with -c/-d option usually + --all-repeated[=METHOD] like -D, but allow separating groups + with an empty line; + METHOD={none(default),prepend,separate} + -u, --unique Only output unique lines + that are not repeated in the input + -i, --ignore-case ignore differences in case when comparing + -z, --zero-terminated line delimiter is NUL, not newline + -XM, --max-input max input size(count by char), support k,m,g postfix + default is 256m + avoid consuming large memory unexpectedly + -h, --help display this help and exit + -V, --version display version information and exit +``` + 🍺 [ap](../bin/ap) and [rp](../bin/rp) ---------------------- 批量转换文件路径为绝对路径/相对路径,会自动跟踪链接并规范化路径。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -命令名`ap`意思是`Absolute Path`,`rp`是`Relative Path`。 +命令名`ap`的意思是`Absolute Path`,`rp`是`Relative Path`。 -### 示例 +### 用法/示例 ```bash # ap缺省打印当前路径的绝对路径 @@ -234,6 +363,50 @@ $ rp /home /etc/../etc /home/admin ../../etc ``` +🍺 [cp-into-docker-run](../bin/cp-into-docker-run) +---------------------- + +一个`Docker`使用的便利脚本。拷贝本机的执行文件到指定的`docker container`中并在`docker container`中执行。 +支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 + +### 用法/示例 + +```bash +# 通过 -c 选项 指定 docker container +$ cp-into-docker-run -c container_foo /path/to/command command_args... +# 如果 指定的command 不是一个路径,会从 PATH 中查找 +$ cp-into-docker-run -c container_foo a2l command_arg1 command_arg2 + +# 帮助信息 +$ cp-into-docker-run -h +Usage: cp-into-docker-run [OPTION]... command [command-args]... + +Copy the command into docker container +and run the command in container. + +Example: + cp-into-docker-run -c container_foo command_copied_into_container command_arg1 + +docker options: + -c, --container destination docker container + -u, --docker-user docker username or UID to run command + optional, docker default is (maybe) root user + -w, --workdir absolute working directory inside the container + optional, docker default is (maybe) root dir + -t, --tmpdir tmp dir in docker to copy command + optional, default is /tmp + -p, --cp-path destination path in docker of the command(including file name) + if specified, command will be kept when run finished + optional, default is under tmp dir and deleted when run finished + +run options: + -v, --verbose show operation step infos + +miscellaneous: + -h, --help display this help and exit + -V, --version display version information and exit +``` + @@ -249,24 +422,20 @@ $ rp /home /etc/../etc /home/admin - 是否有攻击,查看`SYN_RECV`数(`SYN`攻击) - `TIME_WAIT`数,太多会导致`TCP: time wait bucket table overflow`。 -### 用法 - -```bash -tcp-connection-state-counter -``` - -### 示例 +### 用法/示例 ```bash $ tcp-connection-state-counter -ESTABLISHED 290 -TIME_WAIT 212 -SYN_SENT 17 +CLOSE_WAIT 584 +ESTABLISHED 493 +TIME_WAIT 112 +LISTEN 27 +SYN_SENT 7 ``` ### 贡献者 -[sunuslee](https://github.com/sunuslee)改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56) +[sunuslee](https://github.com/sunuslee) 改进此脚本,增加对`MacOS`的支持。 [#56](https://github.com/oldratlee/useful-scripts/pull/56) 🍺 [xpl](../bin/xpl) and [xpf](../bin/xpf) ---------------------- @@ -275,11 +444,11 @@ SYN_SENT 17 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 - `xpl`:在文件浏览器中打开指定的文件或文件夹。 - `xpl`是`explorer`的缩写。 + `xpl`是`explorer`的缩写。 - `xpf`: 在文件浏览器中打开指定的文件或文件夹,并选中。 - `xpf`是`explorer and select file`的缩写。 + `xpf`是`EXplorer and select File`的缩写。 -### 用法 +### 用法/示例 ```bash xpl @@ -291,11 +460,9 @@ xpf # 缺省打开当前目录 xpf <文件或是目录>... # 打开多个文件或目录 -``` -### 示例 -```bash +# 示例 xpl /path/to/dir xpl /path/to/foo.txt xpl /path/to/dir1 /path/to/foo1.txt @@ -305,7 +472,7 @@ xpf /path/to/dir1 /path/to/foo1.txt ### 贡献者 -[Linhua Tan](https://github.com/toolchainX)修复Linux的选定Bug。 +- [Linhua Tan](https://github.com/toolchainX) 修复Linux的选定Bug。 `Shell`开发/测试加强 ==================================== @@ -321,7 +488,7 @@ xpf /path/to/dir1 /path/to/foo1.txt 这个脚本输出脚本收到的参数。在控制台运行时,把参数值括起的括号显示成 **红色**,方便人眼查看。 -### 示例 +### 用法/示例 ```bash $ ./echo-args 1 " 2 foo " "3 3" @@ -372,11 +539,12 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" ### 贡献者 -[姜太公](https://github.com/jzwlqx)提供循环输出彩色组合的脚本。 +[姜太公](https://github.com/jzwlqx) 提供循环输出彩色组合的脚本。 ### 参考资料 -- [utensil](https://github.com/utensil)的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! +- [utensil](https://github.com/utensil) + 的[在Bash下输出彩色的文本](http://utensil.github.io/tech/2007/09/10/colorful-bash.html),这是篇很有信息量很钻研的文章! 🍺 [parseOpts.sh](../lib/parseOpts.sh) ---------------------- @@ -384,9 +552,9 @@ colorEchoWithoutNewLine "4;33;40" "Hello world!" "Hello Hell!" 命令行选项解析库,加强支持选项有多个值(即数组)。 支持`Linux`、`Mac`、`Windows`(`cygwin`、`MSSYS`)。 -自己写一个命令行选项解析函数,是因为[`bash`](http://linux.die.net/man/1/bash)的`buildin`命令[`getopts`](http://linux.die.net/man/1/getopts)和加强版本命令[`getopt`](http://linux.die.net/man/1/getopt)都不支持数组的值。 +自己写一个命令行选项解析函数,是因为[`bash`](https://manned.org/bash)的`builtin`命令[`getopts`](https://manned.org/man/getopts.1)和加强版本命令[`getopt`](https://manned.org/getopt)都不支持数组的值。 -指定选项的多个值(即数组)的风格模仿[`find`](http://linux.die.net/man/1/find)命令的`-exec`选项: +指定选项的多个值(即数组)的风格模仿[`find`](https://manned.org/find)命令的`-exec`选项: ```bash $ find . -name \*.txt -exec echo "find file: " {} \; @@ -403,10 +571,10 @@ find file: bar.txt 选项说明最后可以有选项类型说明: -- `-`: 无参数的选项。即有选项则把值设置成`true`。这是 ***缺省*** 的类型。 +- `-`: 无参数的选项。既有选项则把值设置成`true`。这是 ***缺省*** 的类型。 - `:`: 有参数的选项,值只有一个。 - `+`: 有多个参数值的选项。值列表要以`;`表示结束。 - 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。 + 注意,`;`是`Bash`的元字符(用于一行中多个命令分隔),所以加上转义写成`\;`(当然也可以按你的喜好写成`";"`或`';'`)。 实际要解析的输入参数往往是你的脚本参数,这样`parseOpts`函数调用一般是: @@ -419,7 +587,7 @@ parseOpts "a,a-long|b,b-long:|c,c-long+" "$@" - 选项名为`a`,通过全局变量`_OPT_VALUE_a`来获取选项的值。 - 选项名为`a-long`,通过全局变量`_OPT_VALUE_a_long`来获取选项的值。 - 即,把选项名的`-`转`_`,再加上前缀`_OPT_VALUE_`对应的全局变量来获得选项值。 + 即,把选项名的`-`转`_`,再加上前缀`_OPT_VALUE_`对应的全局变量来获得选项值。 - 除了选项剩下的参数,通过全局变量`_OPT_ARGS`来获取。 按照惯例,输入参数中如果有`--`表示之后参数中不再有选项,即之后都是参数。 @@ -463,19 +631,19 @@ parseOpts "a,a-long|b,b-long:|c,c-long+" -a -b bv -- --c-long c.sh -p pv -q qv a 这个脚本比较复杂,测试过的环境有: 1. `bash --version` - `GNU bash, version 4.1.5(1)-release (x86_64-pc-linux-gnu)` - `uname -a` - `Linux foo-host 2.6.32-41-generic #94-Ubuntu SMP Fri Jul 6 18:00:34 UTC 2012 x86_64 GNU/Linux` + `GNU bash, version 4.1.5(1)-release (x86_64-pc-linux-gnu)` + `uname -a` + `Linux foo-host 2.6.32-41-generic #94-Ubuntu SMP Fri Jul 6 18:00:34 UTC 2012 x86_64 GNU/Linux` 1. `bash --version` - `GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)` - `uname -a` - `Darwin foo-host 14.0.0 Darwin Kernel Version 14.0.0: Fri Sep 19 00:26:44 PDT 2014; root:xnu-2782.1.97~2/RELEASE_X86_64 x86_64 i386 MacBookPro10,1 Darwin` + `GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin14)` + `uname -a` + `Darwin foo-host 14.0.0 Darwin Kernel Version 14.0.0: Fri Sep 19 00:26:44 PDT 2014; root:xnu-2782.1.97~2/RELEASE_X86_64 x86_64 i386 MacBookPro10,1 Darwin` 1. `bash --version` - `GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)` - `uname -a` - `Linux foo-host 2.6.9-103.ELxenU #1 SMP Wed Mar 14 16:31:15 CST 2012 i686 i686 i386 GNU/Linux` + `GNU bash, version 3.00.15(1)-release (i386-redhat-linux-gnu)` + `uname -a` + `Linux foo-host 2.6.9-103.ELxenU #1 SMP Wed Mar 14 16:31:15 CST 2012 i686 i686 i386 GNU/Linux` ### 贡献者 -[Khotyn Huang](https://github.com/khotyn)指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。 +- [Khotyn Huang](https://github.com/khotyn) 指出`bash` `3.0`下使用有问题,并提供`bash` `3.0`的测试机器。 diff --git a/docs/vcs.md b/docs/vcs.md index 00ea9bf8..aa42a1e5 100644 --- a/docs/vcs.md +++ b/docs/vcs.md @@ -11,14 +11,14 @@ > 使用更现代的`Git`吧! 💥 1. [swtrunk](#-swtrunk) - 自动`svn`工作目录从分支(`branches`)切换到主干(`trunk`)。 - PS: `Git`对应的是`git checkout master`,如果你使用了`oh-my-zsh`,已经有对应的别名加速了:`gcm`。 + 自动`svn`工作目录从分支(`branches`)切换到主干(`trunk`)。 + PS: `Git`对应的是`git checkout master`,如果你使用了`oh-my-zsh`,已经有对应的别名加速了:`gcm`。 1. [svn-merge-stop-on-copy](#-svn-merge-stop-on-copy) - 把指定的远程分支从刚新建分支以来的修改合并到本地`svn`目录或是另一个远程分支。 - PS:`Git`的合并很直接简单,`git merge branch-foo`,也更智能(没有树冲突一说)。 + 把指定的远程分支从刚新建分支以来的修改合并到本地`svn`目录或是另一个远程分支。 + PS:`Git`的合并很直接简单,`git merge branch-foo`,也更智能(没有树冲突一说)。 1. [cp-svn-url](#-cp-svn-url) - 拷贝当前`svn`目录对应的远程分支到系统的粘贴板,省去`CTRL+C`操作。 - PS:`Git`分支不需要`URL`来引用,没有这个脚本的需求,直接给个分支名就好了。 + 拷贝当前`svn`目录对应的远程分支到系统的粘贴板,省去`CTRL+C`操作。 + PS:`Git`分支不需要`URL`来引用,没有这个脚本的需求,直接给个分支名就好了。 🍺 [swtrunk](../legacy-bin/swtrunk) ---------------------- @@ -46,17 +46,17 @@ swtrunk path/to/svn/work/directory1 /path/to/svn/work/directory2 # svn工作目 ```bash $ swtrunk # -svn work dir . switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir . switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! $ swtrunk /path/to/svn/work/dir # -svn work dir /path/to/svn/work/dir switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir /path/to/svn/work/dir switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! $ swtrunk /path/to/svn/work/dir1 /path/to/svn/work/dir2 # -svn work dir /path/to/svn/work/dir1 switch from http://www.foo.com/project1/branches/feature1 to http://www.foo.com/project1/trunk ! +svn work dir /path/to/svn/work/dir1 switch from https://www.foo.com/project1/branches/feature1 to https://www.foo.com/project1/trunk ! # -svn work dir /path/to/svn/work/dir2 switch from http://www.foo.com/project2/branches/feature1 to http://www.foo.com/project2/trunk ! +svn work dir /path/to/svn/work/dir2 switch from https://www.foo.com/project2/branches/feature1 to https://www.foo.com/project2/trunk ! ``` 🍺 [svn-merge-stop-on-copy](../legacy-bin/svn-merge-stop-on-copy) @@ -76,9 +76,9 @@ svn-merge-stop-on-copy <来源的远程分支> <目标远程分支> ### 示例 ```bash -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 # 缺省使用当前目录作为svn工作目录 -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 /path/to/svn/work/directory -svn-merge-stop-on-copy http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 # 缺省使用当前目录作为svn工作目录 +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 /path/to/svn/work/directory +svn-merge-stop-on-copy https://www.foo.com/project1/branches/feature1 https://www.foo.com/project1/branches/feature2 ``` ### 贡献者 @@ -102,7 +102,7 @@ cp-svn-url /path/to/svn/work/directory ```bash $ cp-svn-url -http://www.foo.com/project1/branches/feature1 copied! +https://www.foo.com/project1/branches/feature1 copied! ``` ### 贡献者 @@ -111,4 +111,4 @@ http://www.foo.com/project1/branches/feature1 copied! ### 参考资料 -[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.com/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 +[拷贝复制命令行输出放在系统剪贴板上](http://oldratlee.github.io/post/2012-12-23/command-output-to-clip),给出了不同系统可用命令。 diff --git a/legacy-bin/cp-svn-url b/legacy-bin/cp-svn-url index 34799b59..d5d15095 100755 --- a/legacy-bin/cp-svn-url +++ b/legacy-bin/cp-svn-url @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # copy the svn remote url of current svn directory. # @@ -9,47 +9,75 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-cp-svn-url # @author ivanzhangwb (ivanzhangwb at gmail dot com) -readonly PROG=`basename $0` +readonly PROG=${0##*/} +readonly PROG_VERSION='2.x-dev' + +################################################################################ +# parse options +################################################################################ usage() { - cat <= 0; --idx)); do + [[ "${args[idx]}" = -h || "${args[idx]}" = --help ]] && usage + [[ "${args[idx]}" = -V || "${args[idx]}" = --version ]] && progVersion done +unset args idx + +################################################################################ +# biz logic +################################################################################ -[ $# -gt 1 ] && { echo At most 1 local directory is need! ; usage 1; } +(($# > 1)) && { + echo At most 1 local directory is need! + usage 1 +} readonly dir="${1:-.}" -readonly url="$(svn info "${dir}" | awk '/^URL: /{print $2}')" -if [ -z "${url}" ]; then - echo "Fail to get svn url!" 1>&2 - exit 1 +# DO NOT declare and assign var url(as readonly) in ONE line! +# more info see https://github.com/koalaman/shellcheck/wiki/SC2155 +url="$(svn info "$dir" | awk '/^URL: /{print $2}')" +if [ -z "$url" ]; then + echo "Fail to get svn url!" >&2 + exit 1 fi copy() { - case "`uname`" in - Darwin*) - pbcopy ;; - CYGWIN*|MINGW*) - clip ;; - *) - xsel -b ;; - esac + case "$(uname)" in + Darwin*) + pbcopy + ;; + CYGWIN* | MINGW*) + clip + ;; + *) + xsel -b + ;; + esac } -echo -n "${url}" | copy && echo "${url} copied!" +echo -n "$url" | copy && echo "$url copied!" diff --git a/legacy-bin/svn-merge-stop-on-copy b/legacy-bin/svn-merge-stop-on-copy index dd8c90bf..249fa7d2 100755 --- a/legacy-bin/svn-merge-stop-on-copy +++ b/legacy-bin/svn-merge-stop-on-copy @@ -1,6 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function -# svn merge commit between verison when source branch copy(--stop-on-copy) +# svn merge commit between version when source branch copy(--stop-on-copy) # and head version of source branch. # # @Usage @@ -11,78 +11,79 @@ # @author jiangjizhong(@jzwlqx) # @author Jerry Lee (oldratlee at gmail dot com) -PROG=`basename $0` +readonly PROG=${0##*/} usage() { - cat < [target branch] -svn merge commit between verison when source branch copy(--stop-on-copy) + cat < [target branch] +svn merge commit between version when source branch copy(--stop-on-copy) and head version of source branch. Source branch must be a remote branch. -Example: - ${PROG} http://www.foo.com/project1/branches/feature1 - # merge http://www.foo.com/project1/branches/feature1 to current svn direcotry +Example: + $PROG http://www.foo.com/project1/branches/feature1 + # merge http://www.foo.com/project1/branches/feature1 to current svn directory - ${PROG} http://www.foo.com/project1/branches/feature1 /path/to/svn/direcotry - # merge branch http://www.foo.com/project1/branches/feature1 to svn direcotry /path/to/svn/direcotry - # will prompt comfirm for committing to target branch. + $PROG http://www.foo.com/project1/branches/feature1 /path/to/svn/directory + # merge branch http://www.foo.com/project1/branches/feature1 to svn directory /path/to/svn/directory + # will prompt confirm for committing to target branch. - ${PROG} http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 - # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 - # because http://www.foo.com/project1/branches/feature2 is remote url, - # will check out target branch to tmp direcotry, and prompt comfirm for committing to target branch. + $PROG http://www.foo.com/project1/branches/feature1 http://www.foo.com/project1/branches/feature2 + # merge http://www.foo.com/project1/branches/feature1 to branch http://www.foo.com/project1/branches/feature2 + # because http://www.foo.com/project1/branches/feature2 is remote url, + # will check out target branch to tmp directory, and prompt confirm for committing to target branch. EOF - exit $1 + + exit "$1" } -[ $# -gt 2 ] && { - echo "too many arguments!" - usage 1 +(($# > 2)) && { + echo "too many arguments!" + usage 1 } source_branch=$1 target=${2:-.} [ -z "$source_branch" ] && { - echo "missing source branch argument!" - usage 1 + echo "missing source branch argument!" + usage 1 } [ -e "$source_branch" ] && { - echo "source branch must be a remote branch!" - usage 1 + echo "source branch must be a remote branch!" + usage 1 } [ ! -d "$target" ] && { - workDir=$(mktemp -d) && svn co "$target" "$workDir" || { - echo "Fail to checkout target remote branch $target !" - exit 1 - } + workDir=$(mktemp -d) && svn co "$target" "$workDir" || { + echo "Fail to checkout target remote branch $target !" + exit 1 + } } || workDir="$target" -cleanup() { - [ "$workDir" != "$target" ] && { - echo "rm tmp dir $workDir ." - rm -rf "$workDir" - } +cleanupWhenExit() { + [ "$workDir" != "$target" ] && { + echo "rm tmp dir $workDir ." + rm -rf "$workDir" + } } -trap "cleanup" EXIT +trap cleanupWhenExit EXIT -svnstatusline=$(svn status --ignore-externals "$workDir" | grep -v ^X | wc -l) -[ "$svnstatusline" -ne 0 ] && { - echo "svn work direcotry is modified!" - exit 1 +svn_status_line=$(svn status --ignore-externals "$workDir" | grep -c -v ^X) +[ "$svn_status_line" -ne 0 ] && { + echo "svn work directory is modified!" + exit 1 } cd "$workDir" && -from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1) && { - echo "oldest version($from_version) of source branch $source_branch ." - echo "starting merge to $workDir ." - svn merge -${from_version}:HEAD $source_branch -} || { - echo "Fail to merge to work dir $workDir ." - exit 2 -} + if from_version=$(svn log --stop-on-copy --quiet "$source_branch" | awk '$1~/^r[0-9]+/{print $1}' | tail -n1); then + echo "oldest version($from_version) of source branch $source_branch ." + echo "starting merge to $workDir ." + svn merge "-$from_version:HEAD" "$source_branch" + else + echo "Fail to merge to work dir $workDir ." + exit 2 + fi -read -p "Check In? (Y/N)" ci -[ "$ci" = "Y" ] && svn ci -m "svn merge -${from_version}:HEAD $source_branch" +read -r -p "Check In? (Y/N)" ci +[ "$ci" = "Y" ] && svn ci -m "svn merge -$from_version:HEAD $source_branch" diff --git a/legacy-bin/swtrunk b/legacy-bin/swtrunk index 8ab57641..6578fd3b 100755 --- a/legacy-bin/swtrunk +++ b/legacy-bin/swtrunk @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # switch svn work directory to trunk. # @@ -8,39 +8,43 @@ # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/vcs.md#-swtrunk # @author Jerry Lee (oldratlee at gmail dot com) -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end - colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" + local color=$1 + shift + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi } redEcho() { - colorEcho 31 "$@" + colorEcho 31 "$@" } greenEcho() { - colorEcho 32 "$@" + colorEcho 32 "$@" } -[ $# -eq 0 ] && dirs=(.) || dirs=("$@") - -for d in "${dirs[@]}" ; do - [ ! -d ${d}/.svn ] && { - redEcho "directory $d is not a svn work directory, ignore directory $d !" - continue - } - ( - cd "$d" && - branches=`svn info | grep '^URL' | awk '{print $2}'` && - trunk=`echo $branches | awk -F'/branches/' '{print $1}'`/trunk && +# if dirs is empty, use "." +dirs=("${dirs[@]:-.}") - svn sw "$trunk" && - greenEcho "svn work directory $d switch from ${branches} to ${trunk} ." || +for d in "${dirs[@]}"; do + [ ! -d "$d/.svn" ] && { + redEcho "directory $d is not a svn work directory, ignore directory $d !" + continue + } + ( + cd "$d" && + branches=$(svn info | grep '^URL' | awk '{print $2}') && + trunk=$(echo "$branches" | awk -F'/branches/' '{print $1}')/trunk && + if svn sw "$trunk"; then + greenEcho "svn work directory $d switch from $branches to $trunk ." + else redEcho "fail to switch $d to trunk!" - ) + fi + ) done diff --git a/lib/console-text-color-themes.sh b/lib/console-text-color-themes.sh index d95dceac..70833615 100755 --- a/lib/console-text-color-themes.sh +++ b/lib/console-text-color-themes.sh @@ -1,69 +1,76 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # show all console text color themes. # # @online-doc https://github.com/oldratlee/useful-scripts/blob/dev-2.x/docs/shell.md#-console-text-color-themessh # @author Jerry Lee (oldratlee at gmail dot com) -readonly _ctct_PROG="$(basename "$(readlink -f "$0")")" -[ "$_ctct_PROG" == 'console-text-color-themes.sh' ] && readonly _ctct_is_direct_run=true - -readonly _ctct_ec=$'\033' # escape char -readonly _ctct_eend=$'\033[0m' # escape end - colorEcho() { - local combination="$1" - shift 1 - - [ -t 1 ] && echo "$_ctct_ec[${combination}m$@$_ctct_eend" || echo "$@" + local combination=$1 + shift 1 + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[%sm%s\e[0m\n' "$combination" "$*" + else + print '%s\n' "$*" + fi } colorEchoWithoutNewLine() { - local combination="$1" - shift 1 + local combination=$1 + shift 1 - [ -t 1 ] && echo -n "$_ctct_ec[${combination}m$@$_ctct_eend" || echo -n "$@" + if [ -t 1 ]; then + printf '\e[%sm%s\e[0m' "$combination" "$*" + else + printf %s "$*" + fi } -# if not directly run this script(use as lib), just export 2 helper functions, -# and do NOT print anything. -[ "$_ctct_is_direct_run" == "true" ] && { - for style in 0 1 2 3 4 5 6 7; do - for fg in 30 31 32 33 34 35 36 37; do - for bg in 40 41 42 43 44 45 46 47; do - combination="${style};${fg};${bg}" - colorEchoWithoutNewLine "$combination" "$combination" - echo -n " " - done - echo - done - echo +# if source this script(use as lib), just export 2 helper functions, and do NOT print anything. +# +# if directly run this script, the length of array BASH_SOURCE is 1; +# if source this script, the length of array BASH_SOURCE is grater than 1. +((${#BASH_SOURCE[@]} == 1)) || return 0 + +for style in 0 1 2 3 4 5 6 7; do + for fg in 30 31 32 33 34 35 36 37; do + for bg in 40 41 42 43 44 45 46 47; do + combination="${style};${fg};${bg}" + colorEchoWithoutNewLine "$combination" "$combination" + printf ' ' done + echo + done + echo +done - echo "Code sample to print color text:" +echo 'Code sample to print color text:' - echo -n ' echo -e "\033[' - colorEchoWithoutNewLine "3;35;40" "1;36;41" - echo -n "m" - colorEchoWithoutNewLine "0;32;40" "Sample Text" - echo "\033[0m\"" +printf %s ' echo -e "\e[' +colorEchoWithoutNewLine '3;35;40' '1;36;41' +printf %s m +colorEchoWithoutNewLine '0;32;40' 'Sample Text' +printf '%s\n' '\e[0m"' - echo -n " echo \$'\033[" - colorEchoWithoutNewLine "3;35;40" "1;36;41" - echo -n "m'\"" - colorEchoWithoutNewLine "0;32;40" "Sample Text" - echo "\"$'\033[0m'" - echo " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" +printf %s " echo \$'\e[" +colorEchoWithoutNewLine '3;35;40' '1;36;41' +printf %s "m'\"" +colorEchoWithoutNewLine '0;32;40' 'Sample Text' +printf '%s\n' "\"$'\e[0m'" +printf '%s\n' " # NOTE: $'foo' is the escape sequence syntax of bash, safer escape" - echo "Output of above code:" - echo " $_ctct_ec[1;36;41mSample Text$_ctct_eend" - echo - echo "If you are going crazy to write text in escapes string like me," - echo "you can use colorEcho and colorEchoWithoutNewLine function in this script." - echo - echo "Code sample to print color text:" - echo ' colorEcho "1;36;41" "Sample Text"' - echo "Output of above code:" - echo -n " " - colorEcho "1;36;41" "Sample Text" -} +printf '%s\n' 'Output of above code:' +printf %s ' ' +colorEcho '1;36;41' 'Sample Text' +echo +echo 'If you are going crazy to write text in escapes string like me,' +echo 'you can use colorEcho and colorEchoWithoutNewLine function in this script.' +echo +echo 'Code sample to print color text:' +echo ' colorEcho "1;36;41" "Sample Text"' +echo 'Output of above code:' +echo -n ' ' +colorEcho '1;36;41' 'Sample Text' diff --git a/lib/parseOpts.sh b/lib/parseOpts.sh index 6d67f736..86dd0842 100755 --- a/lib/parseOpts.sh +++ b/lib/parseOpts.sh @@ -1,12 +1,12 @@ -#!/bin/bash +#!/usr/bin/env bash # @Function # parse options lib, support multiple values for one option. # # @Usage # source this script to your script file, then use func parseOpts. -# parseOpts func useage sample: +# parseOpts func usage sample: # $ parseOpts "a,a-long|b,b-long:|c,c-long+" -a -b bv -c c.sh -p pv -q qv arg1 \; aa bb cc -# then below globle var is set: +# then below global var is set: # _OPT_VALUE_a = true # _OPT_VALUE_a_long = true # _OPT_VALUE_b = bv @@ -19,36 +19,46 @@ # @author Jerry Lee (oldratlee at gmail dot com) ##################################################################### -# Util Funtions +# Util Functions ##################################################################### # NOTE: $'foo' is the escape sequence syntax of bash -readonly _opts_ec=$'\033' # escape char -readonly _opts_eend=$'\033[0m' # escape end +readonly _opts_ec=$'\e' # escape char +readonly _opts_eend=$'\e[0m' # escape end + +# shellcheck disable=SC2209 + +if [ -z "${_opts_SED_CMD:-}" ]; then + _opts_SED_CMD=sed + if command -v gsed &>/dev/null; then + _opts_SED_CMD=gsed + fi + readonly _opts_SED_CMD +fi _opts_colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$_opts_ec[1;${color}m$@$_opts_eend" || echo "$@" + local color=$1 + shift + # if stdout is console, turn on color output. + [ -t 1 ] && echo "${_opts_ec}[1;${color}m$*${_opts_eend}" || echo "$*" } _opts_redEcho() { - _opts_colorEcho 31 "$@" + _opts_colorEcho 31 "$@" } _opts_convertToVarName() { - [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_convertToVarName: $@" - return 1 - } - echo "$1" | sed 's/-/_/g' + [ $# -ne 1 ] && { + _opts_redEcho "NOT 1 arguments when call _opts_convertToVarName: $*" + return 1 + } + echo "$1" | $_opts_SED_CMD 's/-/_/g' } ##################################################################### # Parse Functions # -# Use Globle Variable: +# Use Globe Variable: # * _OPT_INFO_LIST_INDEX : Option info, data structure. # _OPT_INFO_LIST_INDEX ->* _a_a_long -> option value. # * _OPT_VALUE_* : value of option. is Array type for + mode option @@ -56,258 +66,275 @@ _opts_convertToVarName() { ##################################################################### _opts_findOptMode() { - [ $# -ne 1 ] && { - _opts_redEcho "NOT 1 arguemnts when call _opts_findOptMode: $@" - return 1 - } - - local opt="$1" # like a, a-long - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local mode="${idxNameArray[0]}" - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "${optName}" ] && { - echo "$mode" - return - } - done + [ $# -ne 1 ] && { + _opts_redEcho "NOT 1 arguments when call _opts_findOptMode: $*" + return 1 + } + + local opt="$1" # like a, a-long + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local mode="${idxNameArray[0]}" + + local optName + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + [ "$opt" == "${optName}" ] && { + echo "$mode" + return + } done + done - echo "" + echo "" } _opts_setOptBool() { - [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptBool: $@" - return 1 - } + [ $# -ne 2 ] && { + _opts_redEcho "NOT 2 arguments when call _opts_setOptBool: $*" + return 1 + } - _opts_setOptValue "$@" + _opts_setOptValue "$@" } _opts_setOptValue() { - [ $# -ne 2 ] && { - _opts_redEcho "NOT 2 arguemnts when call _opts_setOptValue: $@" - return 1 - } - - local opt="$1" # like a, a-long - local value="$2" - - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "$optName" ] && { - local optName2 - for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "${optName2}"`" - local from='"$value"' - eval "$optValueVarName=$from" # set global var! - done - return - } + [ $# -ne 2 ] && { + _opts_redEcho "NOT 2 arguments when call _opts_setOptValue: $*" + return 1 + } + + local opt="$1" # like a, a-long + local value="$2" + + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local optName + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + [ "$opt" == "$optName" ] && { + local optName2 + for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + # shellcheck disable=SC2016 + local from='"$value"' + # set global var! + eval "$optValueVarName=$from" done + return + } done + done - _opts_redEcho "NOT Found option $opt!" - return 1 + _opts_redEcho "NOT Found option $opt!" + return 1 } _opts_setOptArray() { - local opt="$1" # like a, a-long - shift - - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}" ; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - [ "$opt" = "$optName" ] && { - # set _OPT_VALUE - local optName2 - for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "${optName2}"`" - local from='"$@"' - eval "$optValueVarName=($from)" # set global var! - done - return - } + local opt="$1" # like a, a-long + shift + + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local optName + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + [ "$opt" == "$optName" ] && { + # set _OPT_VALUE + local optName2 + for optName2 in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "${optName2}")" + local from='"$@"' + eval "$optValueVarName=($from)" # set global var! done + return + } done + done - _opts_redEcho "NOT Found option $opt!" - return 1 + _opts_redEcho "NOT Found option $opt!" + return 1 } _opts_cleanOptValueInfoList() { - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - eval "unset $idxName" - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "$optName"`" - eval "unset $optValueVarName" - done + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + eval "unset $idxName" + + local optName + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + eval "unset $optValueVarName" done + done - unset _OPT_INFO_LIST_INDEX - unset _OPT_ARGS + unset _OPT_INFO_LIST_INDEX + unset _OPT_ARGS } parseOpts() { - local optsDescription="$1" # optsDescription LIKE a,a-long|b,b-long:|c,c-long+ - shift - - _OPT_INFO_LIST_INDEX=() # set global var! - - local optDescLines=`echo "$optsDescription" | - # cut head and tail space - sed -r 's/^\s+//;s/\s+$//' | - awk -F '[\t ]*\\\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` - - local optDesc - while read optDesc ; do # optDesc LIKE b,b-long: - [ -z "$optDesc" ] && continue - - local mode="${optDesc:(-1)}" # LIKE : or + - case "$mode" in - +|:|-) - optDesc="${optDesc:0:(${#optDesc}-1)}" # LIKE b,b-long - ;; - *) - mode="-" - ;; - esac - - local optLines=`echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}'` # LIKE "a\na-long" - - [ $(echo "$optLines" | wc -l) -gt 2 ] && { - _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2 - _opts_cleanOptValueInfoList - return 220 - } + local optsDescription="$1" # optsDescription LIKE a,a-long|b,b-long:|c,c-long+ + shift + + _OPT_INFO_LIST_INDEX=() # set global var! + + local optDescLines + optDescLines=$( + echo "$optsDescription" | + $_opts_SED_CMD -r 's/^\s+//;s/\s+$//' | # cut head and tail space + awk -F '[\t ]*\\|[\t ]*' '{for(i=1; i<=NF; i++) print $i}' + ) + + local optDesc # optDesc LIKE b,b-long: + while read -r optDesc; do + [ -z "$optDesc" ] && continue + + # LIKE : or + + local mode="${optDesc:(-1)}" + case "$mode" in + + | : | -) + # LIKE b,b-long + optDesc="${optDesc:0:(${#optDesc} - 1)}" + ;; + *) + mode="-" + ;; + esac + + local optLines # LIKE "a\na-long" + optLines="$(echo "$optDesc" | awk -F '[\t ]*,[\t ]*' '{for(i=1; i<=NF; i++) print $i}')" + + [ "$(echo "$optLines" | wc -l)" -gt 2 ] && { + _opts_redEcho "Illegal option description($optDesc), more than 2 opt name!" 1>&2 + _opts_cleanOptValueInfoList + return 220 + } + + local -a optTuple=() + local opt # opt LIKE a , a-long + while read -r opt; do + [ -z "$opt" ] && continue - local -a optTuple=() - local opt - while read opt ; do # opt LIKE a , a-long - [ -z "$opt" ] && continue - - [ ${#opt} -eq 1 ] && { - echo "$opt" | grep -E '^[a-zA-Z0-9]$' -q || { - _opts_redEcho "Illegal short option name($opt in $optDesc) in option description!" 1>&2 - _opts_cleanOptValueInfoList - return 221 - } - } || { - echo "$opt" | grep -E '^[-a-zA-Z0-9]+$' -q || { - _opts_redEcho "Illegal long option name($opt in $optDesc) in option description!" 1>&2 - _opts_cleanOptValueInfoList - return 222 - } - } - optTuple=("${optTuple[@]}" "$opt") - done < <(echo "$optLines") - - [ ${#optTuple[@]} -gt 2 ] && { - _opts_redEcho "more than 2 opt(${optTuple[@]}) in option description($optDesc)!" 1>&2 - _opts_cleanOptValueInfoList - return 223 + if [ ${#opt} -eq 1 ]; then + echo "$opt" | grep -E '^[a-zA-Z0-9]$' -q || { + _opts_redEcho "Illegal short option name($opt in $optDesc) in option description!" 1>&2 + _opts_cleanOptValueInfoList + return 221 } + else + echo "$opt" | grep -E '^[-a-zA-Z0-9]+$' -q || { + _opts_redEcho "Illegal long option name($opt in $optDesc) in option description!" 1>&2 + _opts_cleanOptValueInfoList + return 222 + } + fi + optTuple=("${optTuple[@]}" "$opt") + done < <(echo "$optLines") + + [ ${#optTuple[@]} -gt 2 ] && { + _opts_redEcho "more than 2 opt(${optTuple[*]}) in option description($optDesc)!" 1>&2 + _opts_cleanOptValueInfoList + return 223 + } - local idxName= - local evalOpts= - local o - for o in "${optTuple[@]}"; do - idxName="${idxName}_opts_index_name_`_opts_convertToVarName "$o"`" - evalOpts="${evalOpts} $o" - done + local idxName= + local evalOpts= + local o + for o in "${optTuple[@]}"; do + idxName="${idxName}_opts_index_name_$(_opts_convertToVarName "$o")" + evalOpts="${evalOpts} $o" + done - eval "$idxName=($mode $evalOpts)" - _OPT_INFO_LIST_INDEX=("${_OPT_INFO_LIST_INDEX[@]}" "$idxName") - done < <(echo "$optDescLines") - - local -a args=() - while true; do - [ $# -eq 0 ] && break - - case "$1" in - ---*) - _opts_redEcho "Illegal option($1), more than 2 prefix '-'!" 1>&2 - _opts_cleanOptValueInfoList - return 230 - ;; - --) - shift - args=("${args[@]}" "$@") + eval "$idxName=($mode $evalOpts)" + _OPT_INFO_LIST_INDEX=("${_OPT_INFO_LIST_INDEX[@]}" "$idxName") + done < <(echo "$optDescLines") + + local -a args=() + while true; do + [ $# -eq 0 ] && break + + case "$1" in + ---*) + _opts_redEcho "Illegal option($1), more than 2 prefix '-'!" 1>&2 + _opts_cleanOptValueInfoList + return 230 + ;; + --) + shift + args=("${args[@]}" "$@") + break + ;; + -*) # short & long option(-a, -a-long), use same read-in logic. + local opt="$1" + local optName + optName=$(echo "$1" | $_opts_SED_CMD -r 's/^--?//') + local mode + mode=$(_opts_findOptMode "$optName") + case "$mode" in + -) + _opts_setOptBool "$optName" "true" + shift + ;; + :) + [ $# -lt 2 ] && { + _opts_redEcho "Option $opt has NO value!" 1>&2 + _opts_cleanOptValueInfoList + return 231 + } + _opts_setOptValue "$optName" "$2" + shift 2 + ;; + +) + shift + local -a valueArray=() + local foundComma="" + + local value + for value in "$@"; do + [ ";" == "$value" ] && { + foundComma=true break - ;; - -*) # short & long option(-a, -a-long), use same read-in logic. - local opt="$1" - local optName=`echo "$1" | sed -r 's/^--?//'` - local mode=`_opts_findOptMode "$optName"` - case "$mode" in - -) - _opts_setOptBool "$optName" "true" - shift - ;; - :) - [ $# -lt 2 ] && { - _opts_redEcho "Option $opt has NO value!" 1>&2 - _opts_cleanOptValueInfoList - return 231 - } - _opts_setOptValue "$optName" "$2" - shift 2 - ;; - +) - shift - local -a valueArray=() - local foundComma="" - - local value - for value in "$@" ; do - [ ";" = "$value" ] && { - foundComma=true - break - } || valueArray=("${valueArray[@]}" "$value") - done - [ "$foundComma" ] || { - _opts_redEcho "value of option $opt no end comma, value = ${valueArray[@]}" 1>&2 - _opts_cleanOptValueInfoList - return 231 - } - shift "$((${#valueArray[@]} + 1))" - _opts_setOptArray "$optName" "${valueArray[@]}" - ;; - *) - _opts_redEcho "Undefined option $opt!" 1>&2 - _opts_cleanOptValueInfoList - return 232 - ;; - esac - ;; - *) - args=("${args[@]}" "$1") - shift - ;; - esac - done - _OPT_ARGS=("${args[@]}") # set global var! + } || valueArray=("${valueArray[@]}" "$value") + done + [ "$foundComma" ] || { + _opts_redEcho "value of option $opt no end comma, value = ${valueArray[*]}" 1>&2 + _opts_cleanOptValueInfoList + return 231 + } + shift "$((${#valueArray[@]} + 1))" + _opts_setOptArray "$optName" "${valueArray[@]}" + ;; + *) + _opts_redEcho "Undefined option $opt!" 1>&2 + _opts_cleanOptValueInfoList + return 232 + ;; + esac + ;; + *) + args=("${args[@]}" "$1") + shift + ;; + esac + done + # set global var! + _OPT_ARGS=("${args[@]}") } ##################################################################### @@ -315,43 +342,45 @@ parseOpts() { ##################################################################### _opts_showOptDescInfoList() { - echo "===============================================================================" - echo "show option desc info list:" - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - echo "$idxName = (${!idxNameArrayPlaceHolder})" - done - echo "===============================================================================" + echo "===============================================================================" + echo "show option desc info list:" + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + echo "$idxName = (${!idxNameArrayPlaceHolder})" + done + echo "===============================================================================" } _opts_showOptValueInfoList() { - echo "===============================================================================" - echo "show option value info list:" - local idxName - for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do - local idxNameArrayPlaceHolder="$idxName[@]" - local -a idxNameArray=("${!idxNameArrayPlaceHolder}") - - local mode=${idxNameArray[0]} - - local optName - for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do # index from 1, skip mode - local optValueVarName="_OPT_VALUE_`_opts_convertToVarName "$optName"`" - case "$mode" in - -) - echo "$optValueVarName=${!optValueVarName}" - ;; - :) - echo "$optValueVarName=${!optValueVarName}" - ;; - +) - local optArrayValueArrayPlaceHolder="$optValueVarName[@]" - echo "$optValueVarName=(${!optArrayValueArrayPlaceHolder})" - ;; - esac - done + echo "===============================================================================" + echo "show option value info list:" + local idxName + for idxName in "${_OPT_INFO_LIST_INDEX[@]}"; do + local idxNameArrayPlaceHolder="${idxName}[@]" + local -a idxNameArray=("${!idxNameArrayPlaceHolder}") + + local mode=${idxNameArray[0]} + + local optName + # index from 1, skip mode + for optName in "${idxNameArray[@]:1:${#idxNameArray[@]}}"; do + local optValueVarName + optValueVarName="_OPT_VALUE_$(_opts_convertToVarName "$optName")" + case "$mode" in + -) + echo "$optValueVarName=${!optValueVarName}" + ;; + :) + echo "$optValueVarName=${!optValueVarName}" + ;; + +) + local optArrayValueArrayPlaceHolder="${optValueVarName}[@]" + echo "$optValueVarName=(${!optArrayValueArrayPlaceHolder})" + ;; + esac done - echo "_OPT_ARGS=(${_OPT_ARGS[@]})" - echo "===============================================================================" + done + echo "_OPT_ARGS=(${_OPT_ARGS[*]})" + echo "===============================================================================" } diff --git a/test-cases/bump-scripts-version.sh b/test-cases/bump-scripts-version.sh new file mode 100755 index 00000000..d71213c4 --- /dev/null +++ b/test-cases/bump-scripts-version.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +################################################################################ +# util functions +################################################################################ + +# NOTE: $'foo' is the escape sequence syntax of bash +readonly NL=$'\n' # new line + +colorPrint() { + local color=$1 + shift + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [ -t 1 ]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi +} + +redPrint() { + colorPrint 31 "$@" +} + +yellowPrint() { + colorPrint 33 "$@" +} + +bluePrint() { + colorPrint 36 "$@" +} + +logAndRun() { + local simple_mode=false + [ "$1" = "-s" ] && { + simple_mode=true + shift + } + + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + bluePrint "Run under work directory $PWD :$NL$*" + time "$@" + fi +} + +die() { + redPrint "Error: $*" >&2 + exit 1 +} + +################################################################################ +# biz logic +################################################################################ + +(($# != 1)) && die "need only 1 argument for version!$NL${NL}usage:$NL $0 2.x.y" +readonly bump_version=$1 + +# adjust current dir to project dir +# +# Bash Pitfalls#5 +# http://mywiki.wooledge.org/BashPitfalls#cd_.24.28dirname_.22.24f.22.29 +cd -P -- "$(dirname -- "$0")"/.. + +# Bash Pitfalls#1 +# http://mywiki.wooledge.org/BashPitfalls#for_f_in_.24.28ls_.2A.mp3.29 +logAndRun find -D exec bin legacy-bin lib -type f -exec \ + sed -ri "s/^(.*\bPROG_VERSION\s*=\s*')\S*('.*)$/\1$bump_version\2/" -- \ + {} + diff --git a/test-cases/integration-test.sh b/test-cases/integration-test.sh new file mode 100755 index 00000000..cbcdbe5c --- /dev/null +++ b/test-cases/integration-test.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")" + +################################################################################ +# common util functions +################################################################################ + +colorEcho() { + local color=$1 + shift + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [[ -t 1 || "${GITHUB_ACTIONS:-}" = true ]]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi +} + +redEcho() { + colorEcho 31 "$@" +} + +yellowEcho() { + colorEcho 33 "$@" +} + +blueEcho() { + colorEcho 36 "$@" +} + +logAndRun() { + local simple_mode=false + [ "$1" == "-s" ] && { + simple_mode=true + shift + } + + if $simple_mode; then + echo "Run under work directory $PWD : $*" + "$@" + else + # NOTE: $'foo' is the escape sequence syntax of bash + local nl=$'\n' # new line + blueEcho "Run under work directory $PWD :$nl$*" + time "$@" + fi +} + +################################################################################ +# run *_test.sh unit test cases +################################################################################ + +for test_case in *_test.sh; do + logAndRun ./"$test_case" +done diff --git a/test-cases/lint.sh b/test-cases/lint.sh new file mode 100755 index 00000000..76859457 --- /dev/null +++ b/test-cases/lint.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +# cd to the root of the project +cd "$(dirname -- "$(realpath "${BASH_SOURCE[0]}")")"/.. + +find bin lib legacy-bin -type f | + grep -Pv '/show-duplicate-java-classes$' | + grep -Pv '/\.editorconfig$' | + xargs --verbose shellcheck --shell=bash diff --git a/test-cases/my_unit_test_lib.sh b/test-cases/my_unit_test_lib.sh new file mode 100644 index 00000000..df2cc668 --- /dev/null +++ b/test-cases/my_unit_test_lib.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# unit test lib + +################################################# +# commons functions +################################################# + +__ut_colorEcho() { + local color=$1 + shift + # if stdout is a terminal, turn on color output. + # '-t' check: is a terminal? + # check isatty in bash https://stackoverflow.com/questions/10022323 + if [[ -t 1 || "${GITHUB_ACTIONS:-}" = true ]]; then + printf '\e[1;%sm%s\e[0m\n' "$color" "$*" + else + printf '%s\n' "$*" + fi +} + +redEcho() { + __ut_colorEcho 31 "$@" +} + +greenEcho() { + __ut_colorEcho 32 "$@" +} + +yellowEcho() { + __ut_colorEcho 33 "$@" +} + +blueEcho() { + __ut_colorEcho 34 "$@" +} + +fail() { + redEcho "TEST FAIL: $*" + exit 1 +} + +die() { + redEcho "Error: $*" >&2 + exit 1 +} + +################################################# +# assertion functions +################################################# + +assertArrayEquals() { + (($# == 2 || $# == 3)) || die "assertArrayEquals must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + + local a1PlaceHolder="$1[@]" + local a2PlaceHolder="$2[@]" + local a1=("${!a1PlaceHolder}") + local a2=("${!a2PlaceHolder}") + + ((${#a1[@]} == ${#a2[@]})) || fail "assertArrayEquals array length [${#a1[@]}] != [${#a2[@]}]${failMsg:+: $failMsg}" + + local i + for ((i = 0; i < ${#a1[@]}; i++)); do + [ "${a1[$i]}" = "${a2[$i]}" ] || fail "assertArrayEquals fail element $i: [${a1[$i]}] != [${a2[$i]}]${failMsg:+: $failMsg}" + done +} + +assertEquals() { + (($# == 2 || $# == 3)) || die "assertEqual must 2 or 3 arguments!" + local failMsg="" + (($# == 3)) && { + failMsg=$1 + shift + } + [ "$1" == "$2" ] || fail "assertEqual fail [$1] != [$2]${failMsg:+: $failMsg}" +} + +readonly __ut_exclude_vars_builtin='^BASH_|^_=|^COLUMNS=|LINES=' +readonly __ut_exclude_vars_ut_functions='^FUNCNAME=|^test_' + +assertAllVarsSame() { + local test_afterVars + test_afterVars=$(declare) + + diff \ + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions") || + fail "assertAllVarsSame: Unexpected extra global vars!" +} + +assertAllVarsExcludeOptVarsSame() { + local test_afterVars + test_afterVars=$(declare) + + diff \ + <(echo "$test_beforeVars" | grep -Ev "$__ut_exclude_vars_builtin") \ + <(echo "$test_afterVars" | grep -Ev "$__ut_exclude_vars_builtin|$__ut_exclude_vars_ut_functions"'|^_OPT_|^_opts_index_name_') || + fail "assertAllVarsExcludeOptVarsSame: Unexpected extra global vars!" +} + +test_beforeVars=$(declare) diff --git a/test-cases/parseOpts-test.sh b/test-cases/parseOpts-test.sh deleted file mode 100755 index d87943d3..00000000 --- a/test-cases/parseOpts-test.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/bash - -BASE=`dirname $0` - -. $BASE/../lib/parseOpts.sh - - -################################################# -# Util Functions -################################################# - -# NOTE: $'foo' is the escape sequence syntax of bash -readonly ec=$'\033' # escape char -readonly eend=$'\033[0m' # escape end - -colorEcho() { - local color=$1 - shift - # if stdout is console, turn on color output. - [ -t 1 ] && echo "$ec[1;${color}m$@$eend" || echo "$@" -} - -redEcho() { - colorEcho 31 "$@" -} - -greenEcho() { - colorEcho 32 "$@" -} - -yellowEcho() { - colorEcho 33 "$@" -} - -blueEcho() { - colorEcho 34 "$@" -} - -arrayEquals() { - local a1PlaceHolder="$1[@]" - local a2PlaceHolder="$2[@]" - local a1=("${!a1PlaceHolder}") - local a2=("${!a2PlaceHolder}") - - [ ${#a1[@]} -eq ${#a2[@]} ] || return 1 - - local i - for((i=0; i<${#a1[@]}; i++)); do - [ "${a1[$i]}" = "${a2[$i]}" ] || return 1 - done -} - -compareAllVars() { - local test_afterVars=`declare` - diff <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_') -} - -compareAllVarsExcludeOptVars() { - local test_afterVars=`declare` - diff <(echo "$test_beforeVars" | grep -v '^BASH_\|^_=') <(echo "$test_afterVars" | grep -v '^BASH_\|^_=\|^FUNCNAME=\|^test_\|^_OPT_\|^_opts_index_name_') -} - -fail() { - redEcho "TEST FAIL: $@" - exit 1 -} - -################################################# -# Test -################################################# - -test_beforeVars=`declare` - -# ======================================== -blueEcho "Test case: success parse" -# ======================================== - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" -[ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" -test_cArray=(c.sh -p pv -q qv cc) -arrayEquals test_cArray _OPT_VALUE_c && arrayEquals test_cArray _OPT_VALUE_c_long || fail "Wrong option value of c!" -test_dArray=(d.sh -x xv d1 d2 d3 ) -arrayEquals test_dArray _OPT_VALUE_d && arrayEquals test_dArray _OPT_VALUE_d_long || fail "Wrong option value of d!" -test_argArray=(aa bb cc dd ee) -arrayEquals test_argArray _OPT_ARGS || fail "Wrong args!" - -compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!" -_opts_cleanOptValueInfoList -compareAllVars || fail "Unpected extra glable vars!" - -# ======================================== -blueEcho "Test case: success parse with -- " -# ======================================== - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb -- --d-long d.sh -x xv d1 d2 d3 \; cc dd ee -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ $_OPT_VALUE_a = "true" ] && [ $_OPT_VALUE_a_long = "true" ] || fail "Wrong option value of a!" -[ $_OPT_VALUE_b = "bb" ] && [ $_OPT_VALUE_b_long = "bb" ] || fail "Wrong option value of b!" -test_cArray=(c.sh -p pv -q qv cc) -arrayEquals test_cArray _OPT_VALUE_c && arrayEquals test_cArray _OPT_VALUE_c_long || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -test_argArray=(aa bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee) -arrayEquals test_argArray _OPT_ARGS || fail "Wrong args!" - -compareAllVarsExcludeOptVars || fail "Unpected extra glable vars!" -_opts_cleanOptValueInfoList -compareAllVars || fail "Unpected extra glable vars!" - - -# ======================================== -blueEcho "Test case: illegal option x" -# ======================================== - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc -- dd ee -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 232 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - - - -# ======================================== -blueEcho "Test case: empty options" -# ======================================== - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 0 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 4 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - - - -# ======================================== -blueEcho "Test case: illegal option name" -# ======================================== - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|#,z-long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 221 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - - -parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|z,z-#long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee -test_exitCode=$? -_opts_showOptDescInfoList -_opts_showOptValueInfoList - -[ $test_exitCode -eq 222 ] || fail "Wrong exit code!" -[ ${#_OPT_INFO_LIST_INDEX[@]} -eq 0 ] || fail "Wrong _OPT_INFO_LIST_INDEX!" -[ "$_OPT_VALUE_a" = "" ] && [ "$_OPT_VALUE_a_long" = "" ] || fail "Wrong option value of a!" -[ "$_OPT_VALUE_b" = "" ] && [ "$_OPT_VALUE_b_long" = "" ] || fail "Wrong option value of b!" -[ "$_OPT_VALUE_c" = "" ] && [ "$_OPT_VALUE_c_long" = "" ] || fail "Wrong option value of c!" -[ "$_OPT_VALUE_d" = "" ] && [ "$_OPT_VALUE_d_long" = "" ] || fail "Wrong option value of d!" -[ "$_OPT_ARGS" = "" ] || fail "Wrong args!" - -compareAllVars || fail "Unpected extra glable vars!" - -greenEcho "TEST SUCCESS!!!" diff --git a/test-cases/parseOpts_test.sh b/test-cases/parseOpts_test.sh new file mode 100755 index 00000000..35ad0018 --- /dev/null +++ b/test-cases/parseOpts_test.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash + +BASE="$(dirname -- "${BASH_SOURCE[0]}")" + +source "$BASE/../lib/parseOpts.sh" + +source "$BASE/my_unit_test_lib.sh" + +################################################# +# Test +################################################# + +# ======================================== +blueEcho "Test case: success parse" +# ======================================== + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" + +[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" +[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" + +test_cArray=(c.sh -p pv -q qv cc) +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c_long + +test_dArray=(d.sh -x xv d1 d2 d3) +assertArrayEquals "Wrong option value of d!" test_dArray _OPT_VALUE_d +assertArrayEquals "Wrong option value of d!" test_dArray _OPT_VALUE_d_long + +test_argArray=(aa bb cc dd ee) +assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS + +assertAllVarsExcludeOptVarsSame + +_opts_cleanOptValueInfoList +assertAllVarsSame + +# ======================================== +blueEcho "Test case: success parse with -- " +# ======================================== + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -c c.sh -p pv -q qv cc \; bb -- --d-long d.sh -x xv d1 d2 d3 \; cc dd ee +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" + +[[ $_OPT_VALUE_a == "true" && $_OPT_VALUE_a_long == "true" ]] || fail "Wrong option value of a!" +[[ $_OPT_VALUE_b == "bb" && $_OPT_VALUE_b_long == "bb" ]] || fail "Wrong option value of b!" + +test_cArray=(c.sh -p pv -q qv cc) +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c +assertArrayEquals "Wrong option value of c!" test_cArray _OPT_VALUE_c_long + +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" + +test_argArray=(aa bb --d-long d.sh -x xv d1 d2 d3 \; cc dd ee) +assertArrayEquals "Wrong args!" test_argArray _OPT_ARGS + +assertAllVarsExcludeOptVarsSame + +_opts_cleanOptValueInfoList +assertAllVarsSame + +# ======================================== +blueEcho "Test case: illegal option x" +# ======================================== + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb --d-long d.sh -x xv d1 d2 d3 \; cc -- dd ee +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 232)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" + +# ======================================== +blueEcho "Test case: empty options" +# ======================================== + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+" +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 0)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 4)) || fail "Wrong _OPT_INFO_LIST_INDEX!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" + +# ======================================== +blueEcho "Test case: illegal option name" +# ======================================== + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|#,z-long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 221)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" + +parseOpts "a,a-long|b,b-long:|c,c-long+|d,d-long+|z,z-#long" aa -a -b bb -x -c c.sh -p pv -q qv cc \; bb -d d.sh -x xv d1 d2 d3 \; cc -- dd ee +test_exitCode=$? +_opts_showOptDescInfoList +_opts_showOptValueInfoList + +((test_exitCode == 222)) || fail "Wrong exit code!" +((${#_OPT_INFO_LIST_INDEX[@]} == 0)) || fail "Wrong _OPT_INFO_LIST_INDEX!" +[[ "$_OPT_VALUE_a" == "" && "$_OPT_VALUE_a_long" == "" ]] || fail "Wrong option value of a!" +[[ "$_OPT_VALUE_b" == "" && "$_OPT_VALUE_b_long" == "" ]] || fail "Wrong option value of b!" +[[ "$_OPT_VALUE_c" == "" && "$_OPT_VALUE_c_long" == "" ]] || fail "Wrong option value of c!" +[[ "$_OPT_VALUE_d" == "" && "$_OPT_VALUE_d_long" == "" ]] || fail "Wrong option value of d!" +[ "$_OPT_ARGS" == "" ] || fail "Wrong args!" + +assertAllVarsSame + +greenEcho "TEST SUCCESS!!!" diff --git a/test-cases/self-installer.sh b/test-cases/self-installer.sh index 0a16544e..801e7a79 100644 --- a/test-cases/self-installer.sh +++ b/test-cases/self-installer.sh @@ -1,8 +1,8 @@ -#!/bin/bash +#!/usr/bin/env bash -if which svn &> /dev/null; then - [ ! -d "/tmp/useful-scripts-$USER" ] && - svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" +if type -P svn &>/dev/null; then + [ ! -d "/tmp/useful-scripts-$USER" ] && + svn checkout https://github.com/oldratlee/useful-scripts/branches/release-2.x "/tmp/useful-scripts-$USER" fi export PATH="$PATH:/tmp/useful-scripts-$USER/bin:/tmp/useful-scripts-$USER/legacy-bin" diff --git a/test-cases/shunit2-lib b/test-cases/shunit2-lib new file mode 160000 index 00000000..da1e19de --- /dev/null +++ b/test-cases/shunit2-lib @@ -0,0 +1 @@ +Subproject commit da1e19de845a77628d9684e609cc0f8160782c68 diff --git a/test-cases/uq_test.sh b/test-cases/uq_test.sh new file mode 100755 index 00000000..c3bfdfe0 --- /dev/null +++ b/test-cases/uq_test.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash +set -eEuo pipefail + +realpath() { + [ -e "$1" ] && command realpath -- "$1" +} + +BASE=$(dirname -- "$(realpath "${BASH_SOURCE[0]}")") +cd "$BASE" + +################################################# +# commons and test data +################################################# + +readonly uq="../bin/uq" +# NOTE: $'foo' is the escape sequence syntax of bash +readonly nl=$'\n' # new line + +test_input=$(cat uq_test_input) + +################################################# +# test cases +################################################# + +test_uq_simple() { + assertEquals "c${nl}v${nl}a${nl}u" \ + "$(echo "$test_input" | "$uq")" + assertEquals "c${nl}v${nl}a${nl}u" \ + "$("$uq" uq_test_input)" + + assertEquals "c${nl}a" \ + "$(echo "$test_input" | "$uq" -d)" + assertEquals "c${nl}a" \ + "$("$uq" -d uq_test_input)" + + assertEquals "v${nl}u" "$(echo "$test_input" | "$uq" -u)" + assertEquals "v${nl}u" "$("$uq" -u uq_test_input)" +} + +readonly test_output_uq_count=' 4 c + 1 v + 2 a + 1 u' + +readonly test_output_uq_D_count=' 4 c + 4 c + 1 v + 2 a + 2 a + 4 c + 4 c + 1 u' + +test_uq_count() { + assertEquals "$test_output_uq_count" "$(echo "$test_input" | "$uq" -c)" + assertEquals "$test_output_uq_count" "$("$uq" -c uq_test_input)" + + assertEquals "$test_output_uq_D_count" "$(echo "$test_input" | "$uq" -D -c)" + assertEquals "$test_output_uq_D_count" "$("$uq" -D -c uq_test_input)" +} + +test_uq_only_D_option__same_as_cat() { + assertEquals "$test_input" "$(echo "$test_input" | "$uq" -D)" + assertEquals "$test_input" "$("$uq" -D uq_test_input)" +} + +test_multi_input_files__output_file() { + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -d uq_test_input uq_test_another_input "$output_file" + assertEquals "c${nl}a${nl}m" \ + "$(cat "$output_file")" + + local output_file="$SHUNIT_TMPDIR/uq_output_file_${$}_${RANDOM}_${RANDOM}" + "$uq" -u uq_test_input uq_test_another_input "$output_file" + assertEquals "v${nl}u${nl}x" \ + "$(cat "$output_file")" +} + +test_multi_input_files__output_stdout() { + assertEquals "c${nl}v${nl}a${nl}u${nl}m${nl}x" "$("$uq" uq_test_input uq_test_another_input -)" + + assertEquals "c${nl}a${nl}m" "$("$uq" -d uq_test_input uq_test_another_input -)" + + assertEquals "v${nl}u${nl}x" "$("$uq" -u uq_test_input uq_test_another_input -)" +} + +test_ignore_case() { + local input="a${nl}b${nl}A" + + assertEquals "a${nl}b${nl}A" "$(echo "$input" | "$uq")" + assertEquals "a${nl}b" "$(echo "$input" | "$uq" -i)" +} + +test_ignore_case__count() { + local input="a${nl}b${nl}A" + + assertEquals " 1 a${nl} 1 b${nl} 1 A" \ + "$(echo "$input" | "$uq" -c)" + + assertEquals " 2 a${nl} 1 b" \ + "$(echo "$input" | "$uq" -i -c)" + + assertEquals " 2 a${nl} 1 b${nl} 2 A" \ + "$(echo "$input" | "$uq" -i -D -c)" +} + +test_max_input_check() { + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq"' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 4' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" -XM 1k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1042k' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 1m' + # shellcheck disable=SC2016 + assertTrue 'echo 123 | "$uq" --max-input 10420g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input -XM 42m' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1024000g' + # shellcheck disable=SC2016 + assertTrue '"$uq" uq_test_input --max-input 1234567890g' + + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 1' + # shellcheck disable=SC2016 + assertFalse 'should fail by -XM' 'echo -e 123 | "$uq" -XM 3' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' 'echo -e 123 | "$uq" --max-input 2' + # shellcheck disable=SC2016 + assertFalse 'should fail by --max-input' '"$uq" --max-input 2 uq_test_input' + + # shellcheck disable=SC2016 + assertFalse 'should fail, number overflow!' '"$uq" uq_test_input --max-input 12345678901g' +} + +################################################# +# Load and run shUnit2. +################################################# + +source "$BASE/shunit2-lib/shunit2" diff --git a/test-cases/uq_test_another_input b/test-cases/uq_test_another_input new file mode 100644 index 00000000..0e8cf67f --- /dev/null +++ b/test-cases/uq_test_another_input @@ -0,0 +1,4 @@ +a +m +m +x diff --git a/test-cases/uq_test_input b/test-cases/uq_test_input new file mode 100644 index 00000000..6ad21c1b --- /dev/null +++ b/test-cases/uq_test_input @@ -0,0 +1,8 @@ +c +c +v +a +a +c +c +u