Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -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.<random number>.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.<randomnumber>.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.

64 changes: 64 additions & 0 deletions tests/interface.bash
Original file line number Diff line number Diff line change
@@ -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 "$@"






42 changes: 42 additions & 0 deletions tests/optparse.load.bats
Original file line number Diff line number Diff line change
@@ -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 -- <arguments>```
# 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* ]]
}
104 changes: 104 additions & 0 deletions tests/optparse.run.bats
Original file line number Diff line number Diff line change
@@ -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" ]
}