diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..eaa0a14 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,83 @@ +# Contributor / development notes + +## Known issues + +- ~~Breaks with nounset enabled~~ +- ~~Default values containing spaces breaks script~~ + +## Unit tests + +Tests are written using BATS (Bash Automated Testing System). On my Ubuntu +box, bats was available in APT, so just an *apt-get install bats* set it up for +me. YYMV, but check your package manager before installing from source. + +Bash Automated Testing System: +https://github.com/sstephenson/bats + +### Running test + +Tests must be run from the project root. All paths used in testing must also be +from the project root. + +```bash +bats tests/ +``` + +Should probably create a Makefile to do this someday...but it's pretty easy to +remember as is... + +### Debugging + +Test assertion failures should be printed right to the screen when running bats. +However, if bats encounters a syntax error when processing a sourced / included +file (which is pretty frequent), then you won't get any output + +#### If your test case fails, but you don't get a message + +Check for /tmp/bats.* files. If bats gets an error when sourcing / including other +files, it chokes & saves output under /tmp in a new file like bats..out + + +## Execution Workflow + +Just a description of how this script runs. It seemed a bit cryptic at first, but once you get +the idea, it's really straight forward. + +- Script is source and included as normal +- Calls to optparse.define are made: + - Each call runs a parser loop to turn key=val assignments into local variables + - local variables are short, shortname, long, longname, variable, default, val + - Next we build multi line strings. Which are actually bash code. They will later be written to a file and executed to do the parsing. + - **optparse_usage** + - Generates each help line for each define method + - **optparse_contrations** + - This builds the lookup section of a CASE statement below + - Used for converting longopts into shortopts + - Triggers the call to **usage** when --help is found + - Catch all / default will detect unrecognized options & throw an error + - In future versions, this should handle variable assignment, instead of relying on getopts. Which will also add longname only support without a shortname. + - **optparse_defaults** + - Sets up our local variable defaults + - In original optparse, only args with default values are specified here (No initializations) + - In new version, all variables defined are initialized here, with an empty string if no default has been specified. This fixes support for bash's nounset option + - **optparse_arguments_string** + - The getops shortname only argument string (like: "io:uay:p") + - This should be removed in upcoming versions... + - **optparse_process** + - This is the assignment done inside the getopts CASE statement. + - Handles assigning the user specified value to the local variable. + - In future versions, the logic here should be moved to the **optparse_contractions** segment. +- After all arguments are defined, we call .build or .run to do argument to variable assignments + - ```source $(optparse.build)``` + - In older versions, the .build method is used, to create a local temp file of valid bash code, which is then executed. + - In future versions, we will just return the code to be executed and run it using process substitution. ```source <(optparse.run)```. This saves us from creating a temp file on every run. + - For backwards compatibility, the build method will still be available. + - Currently, creates a temp file like /tmp/optparse..tmp: + - usage() definition + - optparse_contraction - assigns shortop codes when longopts are used + - ```eval set -- params``` # This sets our input parameters. Since our nested script isn't passed the shell arguments + - optparse_defaults - Default assignments & local variable initilization + - getopts processing (legacy) + - Assigns local variables to user specified args via getops CASE statement + - Removes the local optparse.tmp script, since it's not longer needed at this point. + diff --git a/tests/interface.bash b/tests/interface.bash new file mode 100755 index 0000000..320c97a --- /dev/null +++ b/tests/interface.bash @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# This is a command line interface to optparse for testing. +# It just prints the input that was parsed after calling optparse.build +# 3 arguments are currently available. INPUT, OUTPUT and ATTRIB +# +# MODES +# Different modes can be activated using the environment variable +# OPTPARSE_TEST_MODE. Use export OPTPARSE_TEST_MODE="mode" to change. +# Possible values are: +# print - Just print input to output, with no requirements (Default) +# nounset - Set the nounset option (fail when accessing undefined variables), +# then call *print* like default +# require - Use some required options (not implemented yet...) + +# Path variables +SCRIPT_FILE=${0}; +TESTS_DIR=$(dirname $(realpath "${SCRIPT_FILE}")); +SRC_FILE=$(realpath "${TESTS_DIR}/../optparse.bash"); +source "${SRC_FILE}" || { echo "ERROR: Could not load script source." && exit 1; } + +# Check environment variable OPTPARSE_TEST_MODE for requested mode, +# and call mode_{requested_mode} function +main() { + + case "${OPTPARSE_TEST_MODE:=print}" in + require) + mode_require + ;; + nounset) + set -o nounset + mode_print "$@" + ;; + *) + mode_print "$@" + esac +} + +mode_require() { + echo "Mode require" +} + +mode_print() { + optparse.define short=i long=input desc="The input file. No default" required="true" variable=INPUT value="" + optparse.define short=o long=output desc="Output file. Default is default_value" variable=OUTPUT default="default_value" + optparse.define short=a long=attrib desc="Boolean style attribute." variable="ATTRIB" value="true" default="false" +# optparse.define short=d long=default-value-with-spaces desc="An argument which has spaces in it's default value" variable=DEFAULT_WITH_SPACES default="default value with spaces" +# optparse.define short=s long=default-value-with-specials desc="An argument with a few special characters in it. A single quote should be handled ok" variable=DEFAULT_WITH_SPECIALS default="this is ' the !@#$%^&*( \${P\} special values" + source $(optparse.build); + + echo "INPUT=${INPUT}"; + echo "OUTPUT=${OUTPUT}"; + echo "ATTRIB=${ATTRIB}"; + echo "DEFAULT_WITH_SPACES=${DEFAULT_WITH_SPACES}"; + echo "DEFAULT_WITH_SPECIALS=${DEFAULT_WITH_SPECIALS}" +} + +main "$@" + + + + + + diff --git a/tests/optparse.load.bats b/tests/optparse.load.bats new file mode 100644 index 0000000..6c4e798 --- /dev/null +++ b/tests/optparse.load.bats @@ -0,0 +1,42 @@ +#!/usr/bin/env bats + +############################################################################### +# These tests use the LOAD method bats to include optparse here and we add +# options in each test case. +# Command line arguments are specified by using ```eval set -- ``` +# Example: +# +# eval set -- -i "input" --output "whatever" -v +# +# **NOTE**: If your testcase fails without any output from bats, then there's +# likely syntax errors on an included file. Check under /tmp for bats.* files. +# The error output will be there. (Also might check for optparse.* files too) +############################################################################### + +@test "include optparse from test script" { + load ../optparse + #optparse.define short=t long=testing desc="Test attribute" variable="TESTATTRIB" value="true" default="false" + #eval set -- # Our command line arguments go here + file=$(optparse.build) + echo "${file}" # This echo will show filename if the tests fail + [ -e "${file}" -a -r "${file}" ] + + # Optparse file was created ok. Run bash -n to lint the file & verify we don't have any errors + run bash -n "${file}" # Lint check + [ "$status" -eq 0 ] + + # Looks good. Now we could just rm it, but let's just source it, and let it remove itself, eh? + source "${file}" + + # Make sure it removed itself + [ ! -e "${file}" ] +} + +@test "verify basic usage output with --help" { + load ../optparse + optparse.define short=t long=testing desc="Test_attribute_description" variable="TESTATTRIB" value="true" default="false" + eval set -- --help + file=$(optparse.build) + output=`source <(cat "${file}")`; + [[ "$output" == usage:* && "$output" == *--testing* && "$output" == *Test_attribute_description* ]] +} diff --git a/tests/optparse.run.bats b/tests/optparse.run.bats new file mode 100644 index 0000000..a9a2a26 --- /dev/null +++ b/tests/optparse.run.bats @@ -0,0 +1,104 @@ +#!/usr/bin/env bats + +############################################################################### +# These test cases use the RUN bats keyword. This is equivalent to running +# the 'run' script right from the shell. The return status is saved as $status, +# while output is under $output and each line is saved in the $lines array. +# +# For optparse, this requires a wrapper script, which just prints our input +# back to us, so we can verify optparse did everything correctly. This method +# is for light / simple tests. For more advanced testing, see the 'load' test +# suite. +# +# See *tests/interface.bash* for more documentation. Currently supported +# arguments (from tests/interface.bash --help) are: +# -i --input: The input file. No default +# -o --output: Output file. Default is default_value +# [default:default_value] +# -a --attrib: Boolean style attribute. [default:false] +# -d --default-value-with-spaces: An argument which has spaces in it's +# default value [default:default value with spaces] +# -s --default-value-with-specials: An argument with a few special +# characters in it. A single quote should be handled ok +# [default:this is ' the !@#$%^&*( ${P\} special values] + +############################################################################### + +# This sets our global mode' nounset should be used instead of print. +#export OPTPARSE_TEST_MODE="nounset" + +@test "run with no arguments" { + run ./tests/interface.bash + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "specify short input argument" { + run ./tests/interface.bash -i DEADBEEF + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "specify long input argument" { + run ./tests/interface.bash --input DEADBEEF + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "override default argument with shortopt" { + run ./tests/interface.bash --input DEADBEEF -o OVERRIDDEN + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=OVERRIDDEN" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "override default argument with longopt" { + run ./tests/interface.bash --input DEADBEEF --output OVERRIDDEN + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=DEADBEEF" ] + [ "${lines[1]}" = "OUTPUT=OVERRIDDEN" ] + [ "${lines[2]}" = "ATTRIB=false" ] +} + +@test "test boolean value with shortname" { + run ./tests/interface.bash -a + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +} + +@test "test boolean value with longname" { + run ./tests/interface.bash --attrib + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +} + +@test "use an invalid argument" { + run ./tests/interface.bash --unspecified_argument + [ "$status" -eq 1 ] + [ "${lines[0]}" = "Unrecognized long option: --unspecified_argument" ] +} + +@test "test if -- stops argument processing" { + run ./tests/interface.bash -o one -- -o two + [ "$status" -eq 0 ] +} + +@test "test bash set -o nounset - fail when accessing undefined variables" { + export OPTPARSE_TEST_MODE="nounset" + run ./tests/interface.bash --input afile -a + [ "$status" -eq 0 ] + [ "${lines[0]}" = "INPUT=afile" ] + [ "${lines[1]}" = "OUTPUT=default_value" ] + [ "${lines[2]}" = "ATTRIB=true" ] +}