diff --git a/plugins/getopts/getopts.fish b/plugins/getopts/getopts.fish new file mode 100644 index 0000000..4d02111 --- /dev/null +++ b/plugins/getopts/getopts.fish @@ -0,0 +1,409 @@ +# NAME +# getopts -- getopts for fish +# +# SYNOPSIS +# getopts [ARGV...] +# +# OPTIONS +# [:][][[:][:[^]]] +# +# A string containing the option characters recognized by the utility +# calling getopts. If a or ends in `:`, the option is +# expected to have an argument, which may be supplied separately or +# next to the option without spaces in the same string. +# +# To indicate long options: : and are both valid +# option strings that will attempt to match - and --. +# +# For only short options, do not specify a : after . +# For example, `a b` will match `-a` and/or `-b`. +# +# To indicate optional arguments, use a `^` character after a `:` at +# the end of the option in the option string. For example :^ +# and ::^ are both valid. Optional arguments should be +# supplied in the same string as the option and without spaces, e.g, +# -value will correctly assign `value` as the argument to the +# the option , but - value, will parse `value` as the +# next argument in . +# +# To specify optional arguments using the option's long form, use a +# `=` character after the option: --=value. +# +# Use a `:` at the beginning of the option string to enable strict +# mode. If enabled, getopts will exit with a status > 0 if or when +# an unknown option is found. See DIAGNOSTICS. +# +# +# +# List of options and operands to parse. getopts prints any matched +# options as well as available argument separated by a \n to stdout +# and returns with a status of 0 if there are still arguments; else +# returns with a status > 0 if the end of the options is reached or +# an error occurs. See DIAGNOSTICS. +# +# DESCRIPTION +# getopts obtains options and their arguments from a list of parameters +# that, as indicated by , are single letters preceded by +# a - or words preceded by -- and possibly followed by an argument value. +# +# fish getopts follows the specifications described in the Utility Syntax +# Guidelines (see LINKS); the following is a summary of the features: +# +# + Short options; single letters preceded by -, and long options; +# words preceded by --, are both supported. +# +# + Single letters may be grouped. -abc → -a -b -c +# +# + Options required to take an argument can specify the argument +# either in the same string as the option or separated from the +# by a space. (1) -a argument, (2) -aargument +# +# + Options that can take an argument optionally shall specify the +# argument in the same string as the option argument if in short +# option style: -aargument, or separated by a = if in long form: +# --long-form=argument. If a blank space is used, the following +# argument will be treated independently. +# +# + Options can appear multiple times in the same argument list. +# getopts will print every match sequentally on each call, and +# should default to the short form of the option if available. +# +# + The option delimiter `:` and optional argument character `^` +# shall not be used as an option. +# +# + getopts will return the remaining operands when the end of the +# options is reached, i.e, a `--` argument that is not an option +# is found, or an argument that does not begin with `-` is found. +# +# ENVIRONMENT VARIABLES +# The following environment variables are used internally by getopts. +# These variables are erased from memory when the functions returns 1. +# +# + __getopts_optstr Whitespace trimmed option string. +# + __getopts_argv Preprocessed copy of arguments. +# + __getopts_index Index of the next argument to handle. +# + __getopts_required List of options with required arguments. +# + __getopts_optional List of options with optional arguments. +# +# DIAGNOSTICS +# Possible exit status values are: +# +# 0 An argument formed like an option was found. This causes getopts +# to print the option short-style and its argument if avaiable. If +# strict-mode is enabled setting the first character of the option +# to `:`, an unknown option will cause getopts to fail. See below. +# +# 1 The end of the options was reached. Remaining operands are also +# sent to stdout. +# +# 2 An option argument was missing. +# +# 3 An unknown option was found. Only if strict-mode is enabled. +# +# EXAMPLES +# function my_utility +# while set optarg (getopts "l:long x: o:optional:^" $argv) +# switch $optarg[1] +# case l +# echo handle `-l --long` +# case x +# echo handle `-x` w/ argument `$optarg[2]` +# case o +# echo handle `-o --optional` w/ optional argument `$optarg[2]` +# case \* +# echo unknown option `$optarg[1]` +# end +# end +# echo -n operands: "`$optarg`" +# end +# +# LINKS +# UNIX Utility Conventions +# → http://pubs.opengroup.org/onlinepubs/7908799/xbd/utilconv.html +# +# AUTHORS +# Jorge Bucaran <@bucaran> +#/ +function getopts + # Currently supported return success/error conditions. + set -l __CONTINUE 0 + set -l __END_OF_OPTIONS 1 + set -l __OPTION_ARGUMENT_EXPECTED 2 + set -l __UNKNOWN_OPTION_FOUND 3 + + # Self-destroying global variable cleanup utility. + # Should be called before returning 1. + function __getopts_cleanup + set -e __getopts_optstr + set -e __getopts_argv + set -e __getopts_index + set -e __getopts_required + set -e __getopts_optional + set -e __getopts_strict_mode + functions -e __getopts_increase_index + end + + function __getopts_increase_index + set __getopts_index (math $__getopts_index + 1) + end + + # Options string pre-processing. + if not set -q __getopts_optstr + set -g __getopts_optstr $argv[1] + set -e argv[1] + + # Trim option string and collect required / optional options. + if [ -n "$__getopts_optstr" ] + set __getopts_optstr (printf $__getopts_optstr | tr '[:space:]' \n) + + # Setting the first token of the option string to `:` enables + # strict mode. This causes getopts to abort the process if an + # unknown option is found. + set -l first_token (printf $__getopts_optstr | head -c1) + + if [ : = "$first_token" ] + set -g __getopts_strict_mode --true + + # We can safely remove the `:` character now. + set __getopts_optstr[1] (printf $__getopts_optstr[1] | cut -c2-) + end + + # Collect options with optional and required option-arguments. + function __getopts_collect_optargs + for string in (printf "%s\n" $__getopts_optstr) + set -l token (printf $string | tail -c1) + set -l split_string (printf "%s\n" $string | tr : \n) + + # Erase last to make sure not to append `^` token. + switch $token + case : + set -g __getopts_required $__getopts_required $split_string + set -e __getopts_required[-1] + case \^ + set -g __getopts_optional $__getopts_optional $split_string + set -e __getopts_optional[-1] + end + end + functions -e __getopts_collect_optargs + end + + __getopts_collect_optargs + end + end + + # Sanitize arguments. Break up flags: -abc → -a -b -c, but skip optional + # arguments. If -w is a flag that can take an optional argument, -abw123 + # should be parsed to → -a -b -w 123. + if not set -q __getopts_argv + function __getopts_sanitize_argv + for token in $argv + switch $token + case --\* # Skip! + case -\* + # Split each token into single characters with `.` + for char in (printf $token | cut -c2- | grep --only-matching .) + # Do not split short option characters if this option can + # take optional required arguments. + if [ -z "$suspend_break" ] + printf "-%s\n" $char + else + printf "%s" $char + end + + # Suspend option break-up if the current option is either + # a required or an optional option type. + contains -- $char $__getopts_required $__getopts_optional + and set suspend_break --true + + set last_char $char + end + + # Break options if we were in suspend mode and reset flag. + # This makes sure to add a blank space if no argument was + # specified for the optional option-argument, but not for + # the required option-argument. + if [ -n "$suspend_break" ] + not contains -- $last_char $__getopts_required + and printf "\n" + set suspend_break "" + end + continue + end + + printf "%s\n" $token + end + functions -e __getopts_sanitize_argv + end + set -g __getopts_argv (__getopts_sanitize_argv $argv) + end + + # Always use our preprocessed argument list. + set argv $__getopts_argv + + if not set -q __getopts_index + set -g __getopts_index 1 + end + + # Handle next argument as indicated by the current index. + if set -q argv[$__getopts_index] + set -l option $argv[$__getopts_index] + + # Potential matches for $option are: (1) end of arguments `--`, + # (2) long/short options --* or -* or (3) begin of operands, if + # (1) and (2) fail. + switch $option + # End of arguments. Assume everything from this point are operands. + case -- + set -e argv[1..$__getopts_index] + printf "%s\n" $argv + __getopts_cleanup + return $__END_OF_OPTIONS + + # Looks like we have a well-formed long/short option. Parse. + case --\* -\* + # It will be useful later to know if the current option is in long + # style. Notice we add a leading blank, because cut fails when the + # first character is a dash `-`. + set -e is_long_option + test (printf " "$option | cut -c2-3) = "--" + and set -l is_long_option + + # Trim leading dashes and prepare to match with valid options. + set option (printf $option | sed 's/^-*//g') + + for substring in $__getopts_optstr + # Split up by token separator `:`. The resulting list contains + # all valid options, both in short and long style. + set -l tokens (printf $substring | tr : \n) + + # Start last to first to avoid mistaking long w/ short options. + for index in (seq (count $tokens) 1) + set -l last_token (printf $substring | tail -c1) + + # Find options with optional argument in long-style and + # try to split by required delimiter `=` + + # Note: Optional values shall be separated from options in + # long style by an equals sign. See documentation. + + if [ $last_token = \^ ] + if set -q is_long_option + set -l option_value (printf $option | tr = \n) + + if set -q option_value[2] + # Check if it is a valid option match. + if [ $option_value[1] = $tokens[$index] ] + printf "%s\n" $tokens[1] + printf $option_value[2] + + __getopts_increase_index + return $__CONTINUE + end + end + end + end + + # We are already inside a possible short/long option match. + # Option is the argument in __getopts_argv[__getopts_index] + # The following compares option against each valid argument + # in the option string __getopts_optstr. If no match can be + # found, print the argument after the loop. + + switch $option + case $tokens[$index] + # Success. Send first token to stdout to make sure short + # options, if available, always get the highest priority. + printf "%s\n" $tokens[1] + + switch $last_token + # At this point, both required and optional arguments + # look the same to the parser, so we match last_token + # to either `:` required or `^` optional tokens. This + # is the result of preprocessing __getopts_argv. + + case : \^ + # Find options with optional argument in long-style. + if [ $last_token = \^ ] + if set -q is_long_option + + # An optional argument in long-style here means it + # didn't split by = in previous checks, so we can + # handle it as a missing argument option and exit. + + # Print any non-empty list character. + printf "\n" + __getopts_increase_index + return $__CONTINUE + end + end + + # If we reach this, we are either handling a required + # option or an optional in short-style form. Both are + # seen by the parser the same after preprocessing. + + # For example: + # Given the option string `a b:^`, -abX → -a -b X + + # We need to peek at the next option + __getopts_increase_index + + # Check if next argument exists. + if set -q argv[$__getopts_index] + # Sanitize the option-argument. + set -l value (printf $argv[$__getopts_index]) + if [ -z "$value" ] + # Print any non-empty list character. + printf "\n" + else + printf $value + end + __getopts_increase_index + return $__CONTINUE + else + __getopts_cleanup + return $__OPTION_ARGUMENT_EXPECTED + end + + # Option was a flag that takes no arguments. + case \* + __getopts_increase_index + return $__CONTINUE + end + end + end + end + + # No match found. Print unknown option to stdout first, and + # if strict mode is enabled, abort, else continue. + printf $option + __getopts_increase_index + + # If the first character of the option string is a `:`, this + # enables strict mode and any unknown options will cause the + # process to return > 0. + + if set -q __getopts_strict_mode + __getopts_cleanup + return $__UNKNOWN_OPTION_FOUND + else + return $__CONTINUE + end + + # Looks like we run out of options. Print operands return. + case \* + if [ $__getopts_index -gt 1 ] + + # Sans the current index, get rid of everything up to here. + set -e argv[1..(math $__getopts_index-1)] + end + printf "%s\n" $argv + + __getopts_cleanup + return $__END_OF_OPTIONS + end + else + # End of arguments, cleanup and exit. + __getopts_cleanup + return $__END_OF_OPTIONS + end +end diff --git a/plugins/getopts/getopts.spec.fish b/plugins/getopts/getopts.spec.fish new file mode 100644 index 0000000..da6b822 --- /dev/null +++ b/plugins/getopts/getopts.spec.fish @@ -0,0 +1,142 @@ +import plugins/fish-spec +import plugins/getopts + +function describe_getotps -d "fish getopts" + function __getopts + set -l options $argv[1] + set -e argv[1] + while set option (getopts $options $argv) + if [ (count $option) -gt 1 ] # w/ argument + set result "$result$option[1]($option[2])" + else + set result "$result$option" + end + end + printf "$result$option" + end + + function it_returns_1_if_empty + getopts + expect $status --to-equal 1 + end + + function it_handles_a_single_short_option + expect (__getopts "a" -a) --to-equal a + end + + function it_handles_a_single_long_option + expect (__getopts "aa" --aa) --to-equal aa + end + + function it_handles_short_options + set -l options "a b c x y z" + set -l expected abcxyz + expect (__getopts $options -abc -xyz) --to-equal $expected + and expect (__getopts $options -a -b -c -x -y -z) --to-equal $expected + end + + function it_handles_long_options + expect (__getopts "x-rays ye zombie" --ye --zombie --x-rays) \ + --to-equal yezombiex-rays + end + + function it_handles_long_options_only + expect (__getopts "all blink crunch" --all --blink --crunch) \ + --to-equal allblinkcrunch + end + + function it_handles_all_options + expect (__getopts "g:get p:put u:update d:delete t:post" -tud --put --get) \ + --to-equal tudpg + end + + function it_handles_operands + expect (__getopts "" 1 2 3 4 5) --to-equal "1 2 3 4 5" + end + + function it_handles_options_and_operands + expect (__getopts "g:get p:put u:update d:delete t:post" \ + -ud --put --get --post url1 url2 user:password) \ + --to-equal "udpgturl1 url2 user:password" + end + + function it_handles_end_of_args_separator + expect (__getopts "a b c d" -abcd -- -x -y -z) --to-equal "abcd-x -y -z" + end + + function it_detects_end_of_args + expect (__getopts "a b c" -abc 1 2 3 -x) --to-equal "abc1 2 3 -x" + end + + function it_returns_unknown_option_error_on_strict_mode + while set option (getopts ":a b c d e f" -abcXdef 1 2 3) + set result "$result$option" + end + + expect $status --to-equal 3 + end + + function it_handles_valid_options_while_on_strict_mode + while set option (getopts ":a b c d e f" -fed -cba 1 2 3) + set result "$result$option" + end + + expect "$result" --to-equal fedcba + and expect "$option" --to-equal "1 2 3" + end + + function it_returns_argument_missing_error_if_argument_is_missing + while set option (getopts "x y z:" -xy -z) + end + + expect $status --to-equal 2 + end + + function it_handles_options_normally_until_an_error_is_found + while set option (getopts ":a b c d e f" -abcXdef 1 2 3) + set result "$result$option" + end + + expect "$result" --to-equal abc + expect "$option" --to-equal X + end + + function it_handles_short_options_separated_by_blanks + expect (__getopts "a: b: c:" -a '1st arg' -b '2nd arg' -c '3rd arg') \ + --to-equal "a(1st arg)b(2nd arg)c(3rd arg)" + end + + function it_handles_a_short_option_w_required_argument_without_space_separation + expect (__getopts "a: b:" -adarwin -blinux) --to-equal "a(darwin)b(linux)" + end + + function it_handles_a_short_option_w_optional_argument_with_space_separation + expect (__getopts "a: b:^" -a darwin -b linux) --to-equal "a(darwin)b()linux" + end + + function it_handles_options_w_argument + set -l url "http://github.com" + set -l expected "pg($url)" + set -l options "g:get: p:put" + expect (__getopts $options -pg$url) --to-equal $expected + and expect (__getopts $options -p --get $url) --to-equal $expected + end + + function it_handles_options_w_optional_argument + set -l url "http://github.com" + set -l expected "pg($url)" + set -l options "g:get:^ p:put" + expect (__getopts $options -pg$url) --to-equal $expected + and expect (__getopts $options -p --get=$url) --to-equal $expected + and expect (__getopts $options -pg) --to-equal "pg()" + end + + function it_handles_a_contrived_example + set -l options "A b c: x y z: long long-req: long-opt:^" + set -l args -xyz777 -Abc100 --long --long-req 32 --long-opt=!!! D O N E + expect (__getopts $options $args) --to-equal \ + "xyz(777)Abc(100)longlong-req(32)long-opt(!!!)D O N E" + end +end + +spec.run $argv