├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE.md ├── Makefile ├── README.md ├── bin └── shall ├── doc ├── images │ └── example-output-hello.png └── shall.md ├── man └── shall.1 ├── package.json └── test ├── Bash must be installed ├── invocation ├── executes a command string with -c ├── failure still reported when using -q or -Q ├── operand is interpreted as script file ├── pass shell options through with -p ├── reads from stdin if there is no operand └── reads from stdin with -s ├── output ├── suppresses both stdout and stderr with -Q └── suppresses stdout with -q ├── standard CLI options ├── Option --version prints version └── Options -h and --help print CLI help └── target shells ├── uses only specified shells with -l └── uses only specified shells with SHELLs env variable /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # npm's debug files, created when something goes wrong. 3 | npm-debug.log 4 | 5 | # npm modules 6 | node_modules/ 7 | 8 | # urchin log files 9 | .urchin.log 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # Do not publish tests to the npm registry. 3 | test/ 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | Versioning complies with [semantic versioning (semver)](http://semver.org/). 4 | 5 | 6 | 7 | * **[v0.2.8](https://github.com/mklement0/shall/compare/v0.2.7...v0.2.8)** (2015-10-23): 8 | * [doc] `README.md` examples still contained obsolete `-l` switch. 9 | * [dev] Improved robustness of internal `rreadlink()` function. 10 | 11 | * **[v0.2.7](https://github.com/mklement0/shall/compare/v0.2.6...v0.2.7)** (2015-09-20): 12 | * [dev] Confusing changelog typos fixed. 13 | * [dev] Removed post-install command that verifies presence of Bash, because 14 | `npm` always _prints_ the command during installation, which can be confusing. 15 | 16 | * **[v0.2.6](https://github.com/mklement0/shall/compare/v0.2.5...v0.2.6)** (2015-09-19): 17 | * [doc] `shall` now has a man page (if manually installed, use `shall --man`); 18 | `shall -h` now just prints concise usage information. 19 | 20 | * **[v0.2.5](https://github.com/mklement0/shall/compare/v0.2.4...v0.2.5)** (2015-09-15): 21 | * [dev] Makefile improvements; various other behind-the-scenes tweaks. 22 | 23 | * **[v0.2.4](https://github.com/mklement0/shall/compare/v0.2.3...v0.2.4)** (2015-07-08): 24 | * [fix] Pass-through option-arguments with embedded spaces are now handled correctly; process substitution replaced with alternative so as to improve FreeBSD compatibility. 25 | * [doc] Read-me improved, notably: manual-installation instructions added, TOC added. 26 | 27 | * **[v0.2.3](https://github.com/mklement0/shall/compare/v0.2.2...v0.2.3)** (2015-06-26): 28 | * [doc] Read-me: npm badge changed to [shields.io](http://shields.io); license badge added; typo fixed. 29 | * [dev] To-do added; Makefile updated. 30 | 31 | * **v0.2.2** (2015-05-31): 32 | * [doc] [npm registry badge](https://badge.fury.io) added 33 | 34 | * **v0.2.1** (2015-05-27): 35 | * [fix] Options passed through with -p are no longer ignored on Linux. 36 | * [fix] Removed extraneous status output. 37 | 38 | * **v0.2.0** (2015-05-24): 39 | * [new] New -p option allows passing additional options through to the shells invoked; e.g.: -p '-e' 40 | * [deprecated] -l option for specifying shells to target renamed to -w to avoid confusion with shells' native -l version (login shells); -l will continue to work. 41 | * [robustness] Exit codes relating to shall's *own* failures changed to: 126 (incorrect arguments) and 127 (unexpected failure), chosen so as to avoid clashes with exit codes produced during normal operation and termination by signal. 42 | 43 | * **v0.1.7** (2015-02-11): 44 | * [doc] improved description in package.json 45 | 46 | * **v0.1.6** (2015-02-11): 47 | * [fix] When using the default target shells, only those actually installed should be targeted. 48 | 49 | * **v0.1.5** (2015-02-11): 50 | * [install] warning added, if bash not found 51 | * [dev] bash-presence test improved 52 | * [dev] Makefile improvements 53 | 54 | * **v0.1.4** (2015-02-11): 55 | * [dev] testing no longer requires the CLI to be in the path 56 | * [dev] bash-presence test added 57 | * [dev] Makefile improvements 58 | * [doc] read-me improvements (examples) 59 | 60 | * **v0.1.3** (2015-01-28): 61 | * [doc] read-me typo corrected 62 | * [dev] Makefile improvements 63 | 64 | * **v0.1.2** (2015-01-27): 65 | * [fix] -q option no longer masks failures 66 | * [doc] CLI help and read-me updates 67 | * [dev] Urchin-based tests added 68 | 69 | * **v0.1.1** (2014-12-23): 70 | * [doc] read-me and CLI help fixes 71 | 72 | * **v0.1.0** (2014-12-23): 73 | * Initial release 74 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Michael Klement, released under the [MIT license](https://spdx.org/licenses/MIT#licenseText). 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Since we rely on paths relative to the Makefile location, abort if make isn't being run from there. 2 | $(if $(findstring /,$(MAKEFILE_LIST)),$(error Please only invoke this makefile from the directory it resides in)) 3 | # Run all shell commands with bash. 4 | SHELL := bash 5 | # Add the local npm packages' bin folder to the PATH, so that `make` can find them even when invoked directly (not via npm). 6 | # !! Note that this extended path only takes effect in (a) recipe commands that are (b) true shell commands (not optimized away) - when in doubt, simply append ';' 7 | # !! To also use the extended path in $(shell ...) function calls, use $(shell PATH="$(PATH)" ...), 8 | # !! ALSO: WE use "./" rather than "$(PWD)/" to add the path, because if you use PowerShell Core as your shell, $PWD will not be defined as an *environment* variable. 9 | # !! "./" (appending a *relative* path) is safe in this case, because we've ensured above that we're running from the project directory. 10 | export PATH := ./node_modules/.bin:$(PATH) 11 | # Sanity check: git repo must exist. 12 | $(if $(shell [[ -d .git ]] && echo ok),,$(error No git repo found in current dir. Please at least initialize one with 'git init')) 13 | # Sanity check: make sure dev dependencies (and npm) are installed - skip this check only for certain generic targets (':' is the pseudo target used by the `list` target's recipe.) 14 | $(if $(or $(shell [[ '$(MAKECMDGOALS)' =~ list|: ]] && echo ok), $(shell [[ -d ./node_modules/semver ]] && echo 'ok')),,$(error Did you forget to run `npm install` after cloning the repo (Node.js must be installed)? At least one of the required dev dependencies not found)) 15 | # Determine the editor to use for modal editing. Use the same as for git, if configured; otherwise $EDITOR, then fall back to vi (which may be vim). 16 | EDITOR := $(shell git config --global --get core.editor || echo "$${EDITOR:-vi}") 17 | 18 | # Default target (by virtue of being the first non '.'-prefixed target in the file). 19 | .PHONY: _no-target-specified 20 | _no-target-specified: 21 | $(error Please specify the target to make - `make list` shows targets. Alternatively, use `npm test` to run the default tests; `npm run` shows all commands) 22 | 23 | # Lists all targets defined in this makefile. 24 | .PHONY: list 25 | list: 26 | @$(MAKE) -pRrn -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | grep -Ev -e '^[^[:alnum:]]' -e '^$@$$' | sort 27 | 28 | # Open this package's online repository URL (typically, on GitHub) in the default browser. 29 | # Note: Supported on OSX and Freedesktop-compliant systems, which includes many Linux and BSD variants. 30 | .PHONY: browse 31 | browse: 32 | @exe=; url=`json -f package.json repository.url` || exit; \ 33 | [[ `uname` == 'Darwin' ]] && exe='open'; \ 34 | [[ -n `command -v xdg-open` ]] && exe='xdg-open'; \ 35 | [[ -n $$exe ]] || { echo "Don't know how to open $$url in the default browser on this platform." >&2; exit 1; }; \ 36 | "$$exe" "$$url" 37 | 38 | # Open this package's page in the npm registry. 39 | # Note: Supported on OSX and Freedesktop-compliant systems, which includes many Linux and BSD variants. 40 | .PHONY: browse-npm 41 | browse-npm: 42 | @exe=; [[ `json -f package.json private` == 'true' ]] && { echo "This package is marked private (not for publication in the npm registry)." >&2; exit 1; }; \ 43 | url="https://www.npmjs.com/package/`json -f package.json name`" || exit; \ 44 | [[ `uname` == 'Darwin' ]] && exe='open'; \ 45 | [[ -n `command -v xdg-open` ]] && exe='xdg-open'; \ 46 | [[ -n $$exe ]] || { echo "Don't know how to open $$url in the default browser on this platform." >&2; exit 1; }; \ 47 | "$$exe" "$$url" 48 | 49 | .PHONY: test 50 | # To optionally skip tests in the context of target 'release', for instance, invoke with NOTEST=1; e.g.: make release NOTEST=1 51 | test: 52 | ifeq ($(NOTEST),1) 53 | @echo Note: Skipping tests, as requested. >&2 54 | else 55 | @exists() { [ -e "$$1" ]; }; exists ./test/* || { echo "(No tests defined.)" >&2; exit 0; }; \ 56 | if [[ -n $$(json -f package.json main) ]]; then tap ./test; else urchin ./test; fi 57 | endif 58 | 59 | # Commits (with prompt for message) and pushes to the branch of the same name in remote repo 'origin', 60 | # but *without* tags, so as to allow quick pushing of changes without running into problems with tag redefinitions. 61 | # (Tags are only pushed - forcefully - with `make release`.) 62 | .PHONY: push 63 | push: _need-clean-ws-or-no-untracked-files 64 | @[[ -z $$(git status --porcelain || echo no) ]] && echo "-- (Nothing to commit.)" || { git commit || exit; echo "-- Committed."; }; \ 65 | targetBranch=`git symbolic-ref --short HEAD` || exit; \ 66 | git push origin "$$targetBranch" || exit; \ 67 | echo "-- Pushed." 68 | 69 | 70 | # Reports the current version number - both from package.json and as defined by the latest git tag 71 | # Implementation note: simply uses 'version' as a prerequisite, which queries $(MAKECMDGOALS) to adjust its behavior based on the caller. 72 | .PHONY: verinfo 73 | verinfo: version 74 | 75 | # Increments the package's version number: 76 | # Unless called via 'make verinfo', the workspace must be clean or at least have no untracked files. 77 | # If VER is *not* specified in the environment: 78 | # Reports the current version number - both from package.json and as defined by the latest git tag. 79 | # If 'make version' was called directly, then prompts to change the version number. 80 | # If called via 'make release', only prompts to change the version number if the git tag version number is the same as the package's. 81 | # VER is set to the value entered and processing continues below. 82 | # If VER *is* specified or continuing from above: 83 | # Validates the new version number: 84 | # If an increment specifier was given, increments from the latest package.json version number (as the version numbers stored in source files are assumed to be in sync with package.json). 85 | # Implementation note: semver, as of v4.3.6, does not validate increment specifiers and simply defaults to 'patch' in case of an valid specifier; thus, we roll our own validation here. 86 | # An increment specifier starting with 'pre' increments [to] a prerelease version number. By default, this simply appends or increments '-', whereas '--preid ' can be used 87 | # to append '-' instead; however, we don't expose that, at least for now, though the user may specify an explicit, full pre-release version number. 88 | # We use tag 'pre' with npm publish --tag, so as to have the latest prerelease be installable with @pre, analogous to the (implicit) 'latest' tag that tracks production releases. 89 | # An explicitly specified version number must be *higher* than the current one; pass variable FORCE=1 to override this in exceptional situations. 90 | # Updates the version number in package.json and in source files in ./bin and ./lib. 91 | .PHONY: version 92 | version: 93 | @[[ '$(MAKECMDGOALS)' == *verinfo* ]] && infoOnly=1 || infoOnly=0; \ 94 | gitTagVer=`git describe --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' 2>/dev/null || echo '(none)'` || exit; gitTagVer=$${gitTagVer#v}; \ 95 | pkgVer=`json -f package.json version` || exit; \ 96 | if [[ -z $$VER ]]; then \ 97 | printf 'CURRENT version:\n\t%s (package.json)\n\t%s (git tag)\n' "$$pkgVer" "$$gitTagVer"; \ 98 | (( infoOnly )) && exit; \ 99 | [[ $$pkgVer != "$$gitTagVer" && $$pkgVer != '0.0.0' ]] && { alreadyBumped=1 || alreadyBumped=0; }; \ 100 | if [[ '$(MAKECMDGOALS)' == 'release' && $$alreadyBumped -eq 1 ]]; then \ 101 | printf "=== `[[ $$pkgVer == *-* ]] && printf 'PRE-'`RELEASING:\n\t%s -> **%s** \n===\n" "$$gitTagVer" "$$pkgVer"; \ 102 | read -p '(Y)es or (c)hange (y/c/N)?: ' -re response && [[ "$$response" == [yYcC] ]] || { echo 'Aborted.' >&2; exit 2; }; \ 103 | [[ $$response =~ [yY] ]] && exit 0; \ 104 | alreadyBumped=0; \ 105 | fi; \ 106 | if [[ '$(MAKECMDGOALS)' == 'version' || $$alreadyBumped -eq 0 ]]; then \ 107 | echo "==="; \ 108 | echo "Enter new version number in full or as one of: 'patch', 'minor', 'major', optionally prefixed with 'pre', or 'prerelease'."; \ 109 | echo "(Alternatively, pass a value from the command line with 'VER='.)"; \ 110 | read -p "NEW VERSION number (just Enter to abort)?: " -re VER && { [[ -z $$VER ]] && echo 'Aborted.' >&2 && exit 2; }; \ 111 | fi; \ 112 | fi; \ 113 | oldVer=$$pkgVer; \ 114 | newVer=$${VER#v}; \ 115 | if printf "$$newVer" | grep -q '^[0-9]'; then \ 116 | semver "$$newVer" >/dev/null || { echo "Invalid semver version number specified: $$VER" >&2; exit 2; }; \ 117 | [[ "$(FORCE)" != '1' ]] && { semver -r "> $$oldVer" "$$newVer" >/dev/null || { echo "Invalid version number specified: $$VER - must be HIGHER than $$oldVer. To force this change, use FORCE=1 on the command line." >&2; exit 2; }; } \ 118 | else \ 119 | [[ $$newVer =~ ^(patch|minor|major|prepatch|preminor|premajor|prerelease)$$ ]] && newVer=`semver -i "$$newVer" "$$oldVer"` || { echo "Invalid version-increment specifier: $$VER" >&2; exit 2; } \ 120 | fi; \ 121 | printf "=== About to BUMP VERSION:\n\t$$oldVer -> **$$newVer**\n===\nProceed (y/N)?: " && read -re response && [[ "$$response" = [yY] ]] || { echo 'Aborted.' >&2; exit 2; }; \ 122 | for dir in ./bin ./lib; do [[ -d $$dir ]] && { replace --quiet --recursive "v$${oldVer//./\\.}" "v$${newVer}" "$$dir" || exit; }; done; \ 123 | [[ `json -f package.json version` == "$$newVer" ]] || { npm version $$newVer --no-git-tag-version >/dev/null && printf $$'\e[0;33m%s\e[0m\n' 'package.json' || exit; }; \ 124 | [[ $$gitTagVer == '(none)' ]] && newVerMdSnippet="**v$$newVer**" || newVerMdSnippet="**[v$$newVer](`json -f package.json repository.url | sed 's/.git$$//'`/compare/v$$gitTagVer...v$$newVer)**"; \ 125 | grep -Eq "\bv$${newVer//./\.}[^[:digit:]-]" CHANGELOG.md || { { sed -n '1,/^\n\n' > doc/"$$cliName".md; \ 201 | "$$cliPath" --man-source >> doc/"$$cliName".md || { printf "ERROR: Failed to extract man-page source.\nPlease ensure that '$$cliName --man-source' outputs the Markdown-formatted man-page source.\n" | fold -s >&2; exit 1; }; \ 202 | "$$cliPath" --man-source | marked-man --version "$$ver" > man/"$$cliName".1 || { echo "Do you need to install marked-man (npm install marked-man --save-dev)?" | fold -s >&2; exit 1; }; \ 203 | [[ '$(MAKECMDGOALS)' == 'update-man' ]] && echo "-- 'doc/$$cliName.md' and 'man/$$cliName.1' updated."$$'\n'"To view the latter as a man page, run: man man/$$cliName.1"$$'\n'"To update and view in one step, run: make view-man" || : 204 | 205 | # If man-page creation is turned on: recreate the man page and view it with `man`. 206 | .PHONY: view-man 207 | view-man: update-man 208 | @manfile=`json -f package.json man`; [[ -n $$manfile ]] || { echo "ERROR: No 'man' property found in 'package.json'." >&2; exit 2; }; \ 209 | man "$$manfile" 210 | 211 | # Toggles inclusion of an auto-updating TOC in README.md via doctoc. 212 | .PHONY: toggle-toc 213 | toggle-toc: 214 | @isOn=$$([[ `json -f package.json net_same2u.make_pkg.tocOn` == 'true' ]] && printf 1 || printf 0); \ 215 | nowState=`(( isOn )) && printf 'ON' || printf 'OFF'`; otherState=`(( isOn )) && printf 'OFF' || printf 'ON'`; \ 216 | echo "Inclusion of an auto-updating TOC for README.md is currently $$nowState."; \ 217 | read -re -p "Turn it $$otherState (y/N)?: " response && [[ "$$response" =~ [yY] ]] || { exit 0; }; \ 218 | json -I -f package.json -e 'this.net_same2u || (this.net_same2u = {}); this.net_same2u.make_pkg || (this.net_same2u.make_pkg = {}); this.net_same2u.make_pkg.tocOn = '`(( isOn )) && printf 'false' || printf 'true'`'; this.net_same2u.make_pkg.tocTitle || (this.net_same2u.make_pkg.tocTitle = "**Contents**")' || exit; \ 219 | if (( isOn )); then \ 220 | echo "NOTE: To be safe, no attempt was made to remove any existing TOC from README.md, if present." | fold -s >&2; \ 221 | else \ 222 | echo "-- Automatic TOC generation for README.md activated."; \ 223 | printf "Run 'make update-toc' to insert a TOC now.\n'make update-readme' and 'make release' will now update it automatically.\n" | fold -s; \ 224 | fi 225 | 226 | # Toggles generation of a man page via marked-man, based on a Markdown-formatted document 227 | # that the package's CLI must output with --man-source. 228 | .PHONY: toggle-man 229 | toggle-man: 230 | @isOn=$$([[ `json -f package.json net_same2u.make_pkg.manOn` == 'true' ]] && printf 1 || printf 0); \ 231 | nowState=`(( isOn )) && printf 'ON' || printf 'OFF'`; otherState=`(( isOn )) && printf 'OFF' || printf 'ON'`; \ 232 | echo "Generating a man page for this package's CLI is currently $$nowState."; \ 233 | read -re -p "Turn it $$otherState (y/N)?: " response && [[ "$$response" =~ [yY] ]] || { exit 0; }; \ 234 | if (( ! isOn )); then \ 235 | read -r cliName cliPath < <(json -f package.json bin | json -Ma key value | head -n 1); \ 236 | [[ -n $$cliName ]] || { echo "ERROR: No CLI declared in 'package.json'; please declare a CLI via the 'bin' property and try again." | fold -s >&2; exit 1; }; \ 237 | fi; \ 238 | json -I -f package.json -e 'this.net_same2u || (this.net_same2u = {}); this.net_same2u.make_pkg || (this.net_same2u.make_pkg = {}); this.net_same2u.make_pkg.manOn = '`(( isOn )) && printf 'false' || printf 'true'` || exit; \ 239 | if (( isOn )); then \ 240 | echo "-- Man-page creation is now OFF."; \ 241 | echo "NOTE: To be safe, a 'man' property, if present, was not removed from 'package.json', and no attempt was made to uninstall the 'marked-man' package, if present. Please make required changes manually." | fold -s >&2; \ 242 | else \ 243 | [[ -n `json -f package.json devDependencies.marked-man` ]] || { echo "-- Installing marked-man as a dev. dependency..."; npm install --save-dev marked-man || exit; }; \ 244 | [[ -n `json -f package.json man` ]] && { echo "NOTE: Retaining existing 'man' property in 'package.json'." >&2; } || \ 245 | { json -I -f package.json -e "this.man = \"./man/$$cliName.1\"" || exit; }; \ 246 | echo "-- Man-page creation is now ON."; echo "Run 'make update-man' to generate the man page now."$$'\n'"Note that '$$cliName --man-source' must output the man-page source in Markdown format for this to work." | fold -s; \ 247 | fi 248 | 249 | 250 | # Updates LICENSE.md if the stated calendar year (e.g., '2015') / the end point in a calendar-year range (e.g., '2014-2015') 251 | # lies in the past; E.g., if the current calendary year is 2016, the first example is updated to '2015-2016', and the second 252 | # one to '2014-2016'. 253 | .PHONY: update-license-year 254 | update-license-year: 255 | @f='LICENSE.md'; thisYear=`date +%Y`; yearRange=`sed -n 's/.*(c) \([0-9]\{4\}\)\(-[0-9]\{4\}\)\{0,1\}.*/\1\2/p' "$$f"`; \ 256 | [[ -n $$yearRange ]] || { echo "Failed to extract calendar year(s) from '$$f'." >&2; exit 1; }; laterYear=$${yearRange#*-}; \ 257 | if (( laterYear < thisYear )); then \ 258 | replace -s '(\(c\) )([0-9]{4})(-[0-9]{4})?' '$$1$$2-'"$$thisYear" "$$f" || exit; \ 259 | echo "NOTE: '$$f' updated to reflect current calendar year, $$thisYear."; \ 260 | elif [[ '$(MAKECMDGOALS)' == 'update-license-year' ]]; then \ 261 | echo "('$$f' calendar year(s) are up-to-date: $$yearRange)"; \ 262 | fi 263 | 264 | # --------- Aux. targets 265 | 266 | # If applicable, replaces the usage read-me chapter with the current CLI help output, 267 | # enclosed in a fenced codeblock and preceded by '$ --help'. 268 | # Replacement is attempted if the project at hand has a (at least one) CLI, as defined in the 'bin' key in package.json. 269 | # is an *object* that has (at least 1) property (rather than containing a string-scalar value that implies the package name as the CLI name). 270 | # - If 'bin' has *multiple* properties, the *1st* is the one whose usage info is to be used. 271 | # To change this, modify CLI_HELP_CMD in the shell command below. 272 | .PHONY: _update-readme-usage 273 | # The arguments to pass to the CLI to have it output its help. 274 | CLI_HELP_ARGS:= --help 275 | # Note that the recipe exits right away if no CLIs are found in 'package.json'. 276 | # TO DISABLE THIS RULE, REMOVE ALL OF ITS RECIPE LINES. 277 | _update-readme-usage: 278 | @read -r cliName cliPath < <(json -f package.json bin | json -Ma key value | head -n 1) || exit 0; \ 279 | CLI_HELP_CMD=( "$$cliPath" $(CLI_HELP_ARGS) ); \ 280 | CLI_HELP_CMD_DISPLAY=( "$${CLI_HELP_CMD[@]}" ); CLI_HELP_CMD_DISPLAY[0]="$$cliName"; \ 281 | newText="$${CLI_HELP_CMD_DISPLAY[@]}"$$'\n\n'"$$( "$${CLI_HELP_CMD[@]}" )" || { echo "Failed to update read-me chapter: usage: invoking CLI help failed: $${CLI_HELP_CMD[@]}" >&2; exit 1; }; \ 282 | newText="$${newText//\$$/$$\$$}"; \ 283 | newText="$${newText//~/\~}"; \ 284 | replace --count --quiet --multiline=false '(\n)(\n\s*?\n```nohighlight\n\$$ )[\s\S]*?(\n```\n|$$)' '$$1$$2'"$$newText"'$$3' README.md | grep -Fq ' (1)' || { echo "Failed to update read-me chapter: usage." >&2; exit 1; } 285 | # !! REGRETTABLY, the ``` sequences in the line above break syntax coloring for the rest of the file in Sublime Text 3 - ?? unclear, how to work around that. 286 | 287 | # - Replaces the '## License' chapter with the contents of LICENSE.md 288 | .PHONY: _update-readme-license 289 | # TO DISABLE THIS RULE, REMOVE ALL OF ITS RECIPE LINES. 290 | _update-readme-license: 291 | @newText=$$'\n'"$$(< LICENSE.md)"$$'\n'; \ 292 | newText="$${newText//\$$/$$\$$}"; \ 293 | replace --count --quiet --multiline=false '(^|\n)(#+ License\n)[\s\S]*?(\n([ \t]*\s*?\n)?#|$$)' '$$1$$2'"$$newText"'$$3' README.md | grep -Fq ' (1)' || { echo "Failed to update read-me chapter: license." >&2; exit 1; } 294 | 295 | # - Replaces the dependencies chapter with the current list of dependencies. 296 | .PHONY: _update-readme-dependencies 297 | # A regex that matches the chapter heading to replace in README.md; watch for unintentional trailing whitespace. '#' must be represented as '\#'. 298 | README_HEADING_DEPENDENCIES := \#+ npm dependencies 299 | # TO DISABLE THIS RULE, REMOVE ALL OF ITS RECIPE LINES. 300 | _update-readme-dependencies: 301 | @newText=$$'\n'$$( \ 302 | keys=( dependencies peerDependencies devDependencies optionalDependencies ); \ 303 | qualifiers=( '' '(P)' '(D)' '(O)'); \ 304 | i=0; \ 305 | for key in "$${keys[@]}"; do \ 306 | json -f ./package.json $$key | json -ka | { \ 307 | while read -r pn; do \ 308 | hp=$$(json -f "./node_modules/$$pn/package.json" homepage); \ 309 | echo "* [$$pn$${qualifiers[i]:+ $${qualifiers[i]}}]($$hp)"; \ 310 | done \ 311 | }; \ 312 | (( ++i )); \ 313 | done)$$'\n'; \ 314 | [[ -n $$newText ]] || { echo "Failed to determine npm dependencies." >&2; exit 1; }; \ 315 | newText="$${newText//\$$/$$\$$}"; \ 316 | replace --count --quiet --multiline=false '(^|\n)($(README_HEADING_DEPENDENCIES)\n)[\s\S]*?(\n([ \t]*\s*?\n)?#|$$)' '$$1$$2'"$$newText"'$$3' README.md | grep -Fq ' (1)' || { echo "Failed to update read-me chapter: npm dependencies." >&2; exit 1; } 317 | 318 | # - Replaces the changelog chapter with the contents of CHANGELOG.md 319 | .PHONY: _update-readme-changelog 320 | # A regex that matches the chapter heading to replace in README.md; watch for unintentional trailing whitespace. '#' must be represented as '\#'. 321 | README_HEADING_CHANGELOG := \#+ Changelog 322 | # TO DISABLE THIS RULE, REMOVE ALL OF ITS RECIPE LINES. 323 | _update-readme-changelog: 324 | @newText=$$'\n'"$$(tail -n +3 CHANGELOG.md)"$$'\n'; \ 325 | newText="$${newText//\$$/$$\$$}"; \ 326 | replace --count --quiet --multiline=false '(^|\n)($(README_HEADING_CHANGELOG)\n)[\s\S]*?(\n([ \t]*\s*?\n)?#|$$)' '$$1$$2'"$$newText"'$$3' README.md | grep -Fq ' (1)' || { echo "Failed to update read-me chapter: changelog." >&2; exit 1; } 327 | 328 | .PHONY: _need-master-branch 329 | _need-master-branch: 330 | @[[ `git symbolic-ref --short HEAD` == 'master' ]] || { echo 'Please release from the master branch only.' >&2; exit 2; } 331 | 332 | # Ensures that the git workspace is clean or contains no untracked files - any tracked files are implicitly added to the index. 333 | .PHONY: _need-clean-ws-or-no-untracked-files 334 | _need-clean-ws-or-no-untracked-files: 335 | @git add --update . || exit 336 | @[[ -z $$(git status --porcelain | awk -F'\0' '$$2 != " " { print $$2 }') ]] || { echo "Workspace must either be clean or contain no untracked files; please add untracked files to the index first (e.g., \`git add .\`) or delete them." >&2; exit 2; } 337 | 338 | # Ensure that a remote git repo named 'origin' is defined. 339 | .PHONY: _need-origin 340 | _need-origin: 341 | @git remote | grep -Fqx 'origin' || { echo "ERROR: Remote git repo 'origin' must be defined." >&2; exit 2; } 342 | 343 | # Unless the package is marked private, ensure that npm credentials have been saved. 344 | .PHONY: _need-npm-credentials 345 | _need-npm-credentials: 346 | @[[ `json -f package.json private` == 'true' ]] && exit 0; \ 347 | grep -Eq '^//registry.npmjs.org/:(_password|_authToken)=' ~/.npmrc || { echo "ERROR: npm-registry credentials not found. Please log in with 'npm login' in order to enable publishing." >&2; exit 2; }; \ 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://img.shields.io/npm/v/shall.svg)](https://npmjs.com/package/shall) [![license](https://img.shields.io/npm/l/shall.svg)](https://github.com/mklement0/shall/blob/master/LICENSE.md) 2 | 3 | 4 | 5 | 6 | **Contents** 7 | 8 | - [shall — introduction](#shall-&mdash-introduction) 9 | - [Examples](#examples) 10 | - [Installation](#installation) 11 | - [Installation from the npm registry](#installation-from-the-npm-registry) 12 | - [Manual installation](#manual-installation) 13 | - [Usage](#usage) 14 | - [License](#license) 15 | - [Acknowledgements](#acknowledgements) 16 | - [npm dependencies](#npm-dependencies) 17 | - [Changelog](#changelog) 18 | 19 | 20 | 21 | # shall — introduction 22 | 23 | `shall` is a Unix CLI and REPL for invoking shell scripts or commands with 24 | multiple POSIX-like shells for portability testing. 25 | 26 | **`shall`** (for ***sh***ell with ***all*** (POSIX-like) shells) offers a 27 | convenient way of running a given shell script or shell command with a default 28 | set or specifiable set of POSIX-like shells, so as to facilitate testing of 29 | portable (POSIX-compliant, cross-shell) shell code. 30 | 31 | By default, the following shells are targeted, if installed: **sh, dash, bash, zsh, ksh** 32 | 33 | Additionally, you can use `shall`: 34 | 35 | * as a REPL, with `-i`. 36 | * in a script's shebang line. 37 | 38 | Each shell's execution is automatically timed to allow performance comparisons. 39 | 40 | The syntax is modeled on that of the underlying shells. 41 | 42 | See the examples below, concise [usage information](#usage) further below, 43 | or read the [manual](doc/shall.md). 44 | 45 | # Examples 46 | 47 | ```sh 48 | 49 | # Echo the name of each executing shell; sample output included. 50 | $ shall -c 'echo "Hello from $0."' 51 | ``` 52 | ![Hello example - sample output](doc/images/example-output-hello.png) 53 | 54 | 55 | ```sh 56 | 57 | # Pass a script to all shells via stdin, plus an argument on the command line. 58 | echo 'echo "Passed to $0: $1"' | shall -s one 59 | 60 | # Execute script 'foo-script' with argument 'bar' in all shells. 61 | shall foo-script bar 62 | 63 | # Print the type of the 'which' command in Bash and Zsh. 64 | shall -w bash,zsh -c 'type which' 65 | 66 | # Enter a REPL that evaluates commands in both Bash and Dash. 67 | SHELLS=bash,dash shall -i 68 | 69 | ``` 70 | 71 | # Installation 72 | 73 | **Supported platforms** 74 | 75 | * When installing from the [**npm registry**](https://www.npmjs.com): all **Unix-like** platforms supported by [Node.js](http://nodejs.org/) with [**Bash**](http://www.gnu.org/software/bash/) installed. 76 | * When installing **manually**: any **Unix-like** platform with **Bash** installed. 77 | 78 | ## Installation from the npm registry 79 | 80 | Note: Even if you don't use Node.js, its package manager, `npm`, works across platforms and is easy to install; try [`curl -L http://git.io/n-install | bash`](https://github.com/mklement0/n-install) 81 | 82 | With [Node.js](http://nodejs.org/) or [io.js](https://iojs.org/) installed, install [the package](https://www.npmjs.com/package/shall) as follows: 83 | 84 | [sudo] npm install shall -g 85 | 86 | **Note**: 87 | 88 | * Whether you need `sudo` depends on how you installed Node.js / io.js and whether you've [changed permissions later](https://docs.npmjs.com/getting-started/fixing-npm-permissions); if you get an `EACCES` error, try again with `sudo`. 89 | * The `-g` ensures [_global_ installation](https://docs.npmjs.com/getting-started/installing-npm-packages-globally) and is needed to put `shall` in your system's `$PATH`. 90 | 91 | ## Manual installation 92 | 93 | * Download [the CLI](https://raw.githubusercontent.com/mklement0/shall/stable/bin/shall) as `shall`. 94 | * Make it executable with `chmod +x shall`. 95 | * Move it or symlink it to a folder in your `$PATH`, such as `/usr/local/bin` (OSX) or `/usr/bin` (Linux). 96 | 97 | # Usage 98 | 99 | Find concise usage information below; for complete documentation, read the [manual online](doc/shall.md), or, once installed, run `man shall` (`shall --man` if installed manually). 100 | 101 | 102 | 103 | ```nohighlight 104 | $ shall --help 105 | 106 | 107 | Cross-POSIX-compatible-shell testing: 108 | 109 | Run a script file: 110 | 111 | shall [-w ,...] [-q|-Q] [-p ]