├── .npmignore ├── test ├── Bash must be installed ├── invocation │ ├── reads from stdin with -s │ ├── reads from stdin if there is no operand │ ├── failure still reported when using -q or -Q │ ├── executes a command string with -c │ ├── operand is interpreted as script file │ └── pass shell options through with -p ├── output │ ├── suppresses stdout with -q │ └── suppresses both stdout and stderr with -Q ├── target shells │ ├── uses only specified shells with -l │ └── uses only specified shells with SHELLs env variable └── standard CLI options │ ├── Option --version prints version │ └── Options -h and --help print CLI help ├── doc ├── images │ └── example-output-hello.png └── shall.md ├── LICENSE.md ├── .gitignore ├── package.json ├── CHANGELOG.md ├── man └── shall.1 ├── README.md ├── Makefile └── bin └── shall /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # Do not publish tests to the npm registry. 3 | test/ 4 | -------------------------------------------------------------------------------- /test/Bash must be installed: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | bash --version >/dev/null 4 | 5 | 6 | -------------------------------------------------------------------------------- /doc/images/example-output-hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mklement0/shall/HEAD/doc/images/example-output-hello.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2015 Michael Klement, released under the [MIT license](https://spdx.org/licenses/MIT#licenseText). 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /test/invocation/reads from stdin with -s: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | echo 'echo $1' | shall -s 'foo' | fgrep 'foo' >/dev/null || exit 6 | 7 | (( ${PIPESTATUS[1]} == 0 )) 8 | -------------------------------------------------------------------------------- /test/invocation/reads from stdin if there is no operand: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | echo 'echo foo' | shall | fgrep 'foo' >/dev/null || exit 6 | 7 | (( ${PIPESTATUS[1]} == 0 )) 8 | -------------------------------------------------------------------------------- /test/output/suppresses stdout with -q: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | trap 'rm -f tmpfile' EXIT 6 | 7 | echo 'echo foo; echo bar >&2' | shall -l sh -q >tmpfile || exit 8 | fgrep -q 'foo' tmpfile && exit 1 9 | fgrep -q 'bar' tmpfile || exit 10 | exit 0 -------------------------------------------------------------------------------- /test/output/suppresses both stdout and stderr with -Q: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | trap 'rm -f tmpfile' EXIT 6 | 7 | echo 'echo foo; echo bar >&2' | shall -l sh -Q >tmpfile || exit 8 | fgrep -q 'foo' tmpfile && exit 1 9 | fgrep -q 'bar' tmpfile && exit 1 10 | exit 0 11 | -------------------------------------------------------------------------------- /test/invocation/failure still reported when using -q or -Q: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | trap 'rm -f tmpfile' EXIT 4 | 5 | # The following commands *should* fail (cause shall to report a nonzero exit code) -- the presence of -q and -Q should not change that. 6 | echo '<' | shall -l sh -q >/dev/null && exit 1 7 | echo '<' | shall -l sh -Q >/dev/null && exit 1 8 | exit 0 -------------------------------------------------------------------------------- /test/invocation/executes a command string with -c: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | # -c with command string - no further args 6 | shall -c 'echo foo' | fgrep 'foo' >/dev/null || exit 7 | (( ${PIPESTATUS[0]} == 0 )) || exit 8 | 9 | # -c with command string - plus further args 10 | shall -c 'echo foo $1' unused bar | fgrep 'foo bar' >/dev/null || exit 11 | (( ${PIPESTATUS[0]} == 0 )) || exit 12 | -------------------------------------------------------------------------------- /test/invocation/operand is interpreted as script file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | trap 'rm -f tmpfileIn tmpfileOut' EXIT # Set up exit trap to automatically clean up the temp file. 6 | 7 | # operand only 8 | echo 'echo foo' >tmpfileIn 9 | shall tmpfileIn >/dev/null || exit 10 | 11 | # operand + arguments 12 | echo 'echo foo: $*' >tmpfileIn 13 | shall tmpfileIn bar baz >tmpfileOut || exit 14 | fgrep -q 'foo: bar baz' tmpfileOut || exit 15 | -------------------------------------------------------------------------------- /test/invocation/pass shell options through with -p: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | # -p with pass-through option -e with intentionally broken command to ensure that 6 | # the failing first command prevents the 2nd one from being executed. 7 | shall -p '-e' -c 'ls nosuchthing; echo DONOTPRINTME' | fgrep 'DONOTPRINTME' >/dev/null && exit 8 | (( ${PIPESTATUS[0]} > 0 )) || exit 9 | 10 | 11 | # -p with 2 pass-through options: -e as a dummy, and -o noglob to test disabling of globbing. 12 | shall -p '-e -o noglob' -c 'echo *' | fgrep '*' >/dev/null || exit 13 | (( ${PIPESTATUS[0]} == 0 )) || exit 14 | -------------------------------------------------------------------------------- /test/target shells/uses only specified shells with -l: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | trap 'rm -f tmpfile' EXIT # Set up exit trap to automatically clean up the temp file. 6 | 7 | shells='sh,bash' 8 | shellCount=$(awk -F, '{ print NF }' <<<"$shells") 9 | 10 | # make sure that ALL SPECIFIED shells are executed 11 | shall -l "$shells" -c 'echo FOO$0' >tmpfile || exit 12 | 13 | fgrep -qw 'FOOsh' tmpfile && fgrep -qw 'FOObash' tmpfile || exit 14 | 15 | # also make sure that ONLY THE SPECIFIED shells are executed 16 | shall -l "$shells" -c '<' >/dev/null # use deliberately broken command 17 | (( $? == $shellCount )) || exit # exit code should reflect the number of shells that failed, i.e., all invoked, in this case. 18 | -------------------------------------------------------------------------------- /test/target shells/uses only specified shells with SHELLs env variable: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PATH="../../bin:$PATH" 4 | 5 | trap 'rm -f tmpfile' EXIT # Set up exit trap to automatically clean up the temp file. 6 | 7 | shells='sh,bash' 8 | shellCount=$(awk -F, '{ print NF }' <<<"$shells") 9 | 10 | # make sure that ALL SPECIFIED shells are executed 11 | SHELLS="$shells" shall -c 'echo FOO$0' >tmpfile || exit 12 | 13 | fgrep -qw 'FOOsh' tmpfile && fgrep -qw 'FOObash' tmpfile || exit 14 | 15 | # also make sure that ONLY THE SPECIFIED shells are executed 16 | SHELLS="$shells" shall -c '<' >/dev/null # use deliberately broken command 17 | (( $? == $shellCount )) || exit # exit code should reflect the number of shells that failed, i.e., all invoked, in this case. 18 | -------------------------------------------------------------------------------- /test/standard CLI options/Option --version prints version: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # --- 4 | # IMPORTANT: Use the following statement at the TOP OF EVERY TEST SCRIPT 5 | # to ensure that this package's 'bin/' subfolder is added to the path so that 6 | # this package's CLIs can be invoked by their mere filename in the rest 7 | # of the script. 8 | # --- 9 | PATH=${PWD%%/test*}/bin:$PATH 10 | 11 | # Helper function for error reporting. 12 | die() { (( $# > 0 )) && echo "ERROR: $*" >&2; exit 1; } 13 | 14 | # Execute your tests as shell commands below. 15 | # An exit code of zero signals success, any other code signals an error 16 | # and causes this test to fail. 17 | # See https://github.com/tlevine/urchin. 18 | 19 | perli --version | grep -Ew 'v?[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+' || die "No major.minor.patch version number detected in the output." 20 | 21 | exit 0 22 | 23 | -------------------------------------------------------------------------------- /test/standard CLI options/Options -h and --help print CLI help: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # --- 4 | # IMPORTANT: Use the following statement at the TOP OF EVERY TEST SCRIPT 5 | # to ensure that this package's 'bin/' subfolder is added to the path and this 6 | # package's CLIs can therefore be invoked by their mere filename in the rest 7 | # of the script. 8 | # --- 9 | PATH=${PWD%%/test*}/bin:$PATH 10 | 11 | # Helper function for error reporting. 12 | die() { (( $# > 0 )) && echo "ERROR: $*" >&2; exit 1; } 13 | 14 | # Execute your tests as shell commands below. 15 | # An exit code of zero signals success, any other code signals an error 16 | # and causes this test to fail. 17 | # See https://github.com/tlevine/urchin. 18 | 19 | cli='perli' 20 | errMsg="CLI name '$cli' not found in stdout output." 21 | 22 | for opt in '-h' '--help'; do 23 | perli -h | fgrep -qw "$cli" || die "Option $opt: $errMsg" 24 | done 25 | 26 | exit 0 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shall", 3 | "description": "Unix CLI and REPL for invoking shell scripts or commands with multiple POSIX-like shells for portability testing.", 4 | "private": false, 5 | "version": "0.2.8", 6 | "os": [ 7 | "!win32" 8 | ], 9 | "preferGlobal": true, 10 | "bin": { 11 | "shall": "bin/shall" 12 | }, 13 | "homepage": "https://github.com/mklement0/shall", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/mklement0/shall" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/mklement0/shall/issues" 20 | }, 21 | "scripts": { 22 | "test": "make test" 23 | }, 24 | "keywords": [ 25 | "shell", 26 | "cross-shell", 27 | "test", 28 | "compatibility", 29 | "POSIX", 30 | "portable", 31 | "REPL", 32 | "CLI" 33 | ], 34 | "author": "Michael Klement (http://same2u.net)", 35 | "license": "MIT", 36 | "net_same2u": { 37 | "make_pkg": { 38 | "tocOn": true, 39 | "tocTitle": "**Contents**", 40 | "manOn": true 41 | } 42 | }, 43 | "devDependencies": { 44 | "doctoc": "^0.14.2", 45 | "json": "^9.0.3", 46 | "marked-man": "^0.1.5", 47 | "replace": "^0.3.0", 48 | "semver": "^4.2.0", 49 | "urchin": "^0.0.5" 50 | }, 51 | "man": "./man/shall.1" 52 | } 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /doc/shall.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # shall(1) - cross-shell compatibility testing 4 | 5 | ## SYNOPSIS 6 | 7 | Cross-POSIX-compatible-shell testing: 8 | 9 | Run a script file: 10 | 11 | shall [-w ,...] [-q|-Q] [-p ]