├── .editorconfig ├── .travis.yml ├── LICENSE ├── README.md ├── getopt.bash ├── test.bash └── travis.bash /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | tab_width = 8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_size = 8 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: focal 3 | language: bash 4 | env: 5 | - BASHES="bash2.05b bash3.0.16 bash3.2.48 bash4.2.45" 6 | before_install: 7 | - wget https://launchpad.net/~agriffis/+archive/ubuntu/bashes/+files/bash2.05b_2.05b-2_amd64.deb 8 | - wget https://launchpad.net/~agriffis/+archive/ubuntu/bashes/+files/bash3.0.16_3.0.16-2_amd64.deb 9 | - wget https://launchpad.net/~agriffis/+archive/ubuntu/bashes/+files/bash3.2.48_3.2.48-2_amd64.deb 10 | - wget https://launchpad.net/~agriffis/+archive/ubuntu/bashes/+files/bash4.2.45_4.2.45-2_amd64.deb 11 | - sudo dpkg -i bash*deb 12 | script: bash travis.bash 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pure-getopt 2 | 3 | [![Build Status](https://travis-ci.com/agriffis/pure-getopt.svg?branch=master)](https://travis-ci.com/github/agriffis/pure-getopt) 4 | 5 | pure-getopt is a drop-in replacement for GNU getopt, implemented in pure 6 | Bash compatible back to 2.05b. It makes no external calls and faithfully 7 | reimplements GNU getopt features, including: 8 | 9 | * all three calling forms in the synopsis 10 | * all getopt options 11 | * matching error messages 12 | * matching return codes 13 | * proper handling of abbreviated long options 14 | * alternative parsing mode (long options with single dash) 15 | * GETOPT_COMPATIBLE flag 16 | * POSIXLY_CORRECT flag 17 | * leading + or - on options string 18 | 19 | # How to use it 20 | 21 | pure-getopt provides a single bash function `getopt` that you can insert 22 | directly into your script. It should be defined before calling `getopt` from 23 | your code, so either place the definition above your code, or use this 24 | pattern (recommended): 25 | 26 | ```bash 27 | #!/bin/bash 28 | 29 | main() { 30 | declare argv 31 | argv=$(getopt -o fb: --long foo,bar: -- "$@") || return 32 | eval "set -- $argv" 33 | 34 | declare foo=false bar= 35 | 36 | while true; do 37 | case $1 in 38 | f|foo) foo=true ; shift ;; 39 | b|bar) bar=$2 ; shift 2 ;; 40 | --) break ;; 41 | esac 42 | done 43 | 44 | # etc 45 | } 46 | 47 | # INSERT getopt function here 48 | getopt() { 49 | ... 50 | } 51 | 52 | # CALL main at very bottom, passing script args. 53 | # The test here distinguishes script execution from "source myscript.bash" which 54 | # will define the functions without calling main, for calling functions from 55 | # another script or testing at the command-line. 56 | [[ $BASH_SOURCE != "$0" ]] || main "$@" 57 | ``` 58 | 59 | # Differences between pure-getopt and GNU getopt 60 | 61 | The only intentional divergences between pure-getopt and GNU getopt are 62 | either inconsequential or due to bugs in GNU getopt: 63 | 64 | 1. GNU getopt mishandles ambiguities in abbreviated long options, for 65 | example this doesn't produce an error message: 66 | 67 | getopt -o '' --long xy,xz -- --x 68 | 69 | but this does produce an error message: 70 | 71 | getopt -o '' --long xy,xz: -- --x 72 | 73 | Pure-getopt generates an error message in both cases, diverging from 74 | GNU getopt to fix this bug. 75 | 76 | 2. In the case of an ambiguous long option with an argument, GNU getopt 77 | generates an error message that includes the argument: 78 | 79 | getopt: option '--x=foo' is ambiguous; possibilities: '--xy' '--xz' 80 | 81 | We consider this a bug in GNU getopt, since the value might be very 82 | long and inappropriate for printing to the screen, and since GNU getopt 83 | ordinarily omits the value in its error messages. Pure-getopt's error 84 | message in this case is: 85 | 86 | getopt: option '--x' is ambiguous; possibilities: '--xy' '--xz' 87 | 88 | 3. Pure-getopt uses a different method of quoting the output. The result 89 | is the same as GNU getopt when eval'd by the shell. 90 | 91 | # References 92 | 93 | * [getopt in util-linux](http://software.frodo.looijaard.name/getopt/) 94 | -------------------------------------------------------------------------------- /getopt.bash: -------------------------------------------------------------------------------- 1 | getopt() { 2 | # pure-getopt, a drop-in replacement for GNU getopt in pure Bash. 3 | # version 1.4.5 4 | # 5 | # Copyright 2012-2021 Aron Griffis 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining 8 | # a copy of this software and associated documentation files (the 9 | # "Software"), to deal in the Software without restriction, including 10 | # without limitation the rights to use, copy, modify, merge, publish, 11 | # distribute, sublicense, and/or sell copies of the Software, and to 12 | # permit persons to whom the Software is furnished to do so, subject to 13 | # the following conditions: 14 | # 15 | # The above copyright notice and this permission notice shall be included 16 | # in all copies or substantial portions of the Software. 17 | # 18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | _getopt_main() { 27 | # Returns one of the following statuses: 28 | # 0 success 29 | # 1 error parsing parameters 30 | # 2 error in getopt invocation 31 | # 3 internal error 32 | # 4 reserved for -T 33 | # 34 | # For statuses 0 and 1, generates normalized and shell-quoted 35 | # "options -- parameters" on stdout. 36 | 37 | declare parsed status 38 | declare short long='' name flags='' 39 | declare have_short=false 40 | 41 | # Synopsis from getopt man-page: 42 | # 43 | # getopt optstring parameters 44 | # getopt [options] [--] optstring parameters 45 | # getopt [options] -o|--options optstring [options] [--] parameters 46 | # 47 | # The first form can be normalized to the third form which 48 | # _getopt_parse() understands. The second form can be recognized after 49 | # first parse when $short hasn't been set. 50 | 51 | if [[ -n ${GETOPT_COMPATIBLE+isset} || $1 == [^-]* ]]; then 52 | # Enable compatibility mode 53 | flags=c$flags 54 | # Normalize first to third synopsis form 55 | set -- -o "$1" -- "${@:2}" 56 | fi 57 | 58 | # First parse always uses flags=p since getopt always parses its own 59 | # arguments effectively in this mode. 60 | parsed=$(_getopt_parse getopt ahl:n:o:qQs:TuV \ 61 | alternative,help,longoptions:,name:,options:,quiet,quiet-output,shell:,test,version \ 62 | p "$@") 63 | status=$? 64 | if [[ $status != 0 ]]; then 65 | if [[ $status == 1 ]]; then 66 | echo "Try 'getopt --help' for more information." >&2 67 | # Since this is the first parse, convert status 1 to 2 68 | status=2 69 | fi 70 | return $status 71 | fi 72 | eval "set -- $parsed" 73 | 74 | while [[ $# -gt 0 ]]; do 75 | case $1 in 76 | (-a|--alternative) 77 | flags=a$flags ;; 78 | 79 | (-h|--help) 80 | _getopt_help 81 | return 0 82 | ;; 83 | 84 | (-l|--longoptions) 85 | long="$long${long:+,}$2" 86 | shift ;; 87 | 88 | (-n|--name) 89 | name=$2 90 | shift ;; 91 | 92 | (-o|--options) 93 | short=$2 94 | have_short=true 95 | shift ;; 96 | 97 | (-q|--quiet) 98 | flags=q$flags ;; 99 | 100 | (-Q|--quiet-output) 101 | flags=Q$flags ;; 102 | 103 | (-s|--shell) 104 | case $2 in 105 | (sh|bash) 106 | flags=${flags//t/} ;; 107 | (csh|tcsh) 108 | flags=t$flags ;; 109 | (*) 110 | echo 'getopt: unknown shell after -s or --shell argument' >&2 111 | echo "Try 'getopt --help' for more information." >&2 112 | return 2 ;; 113 | esac 114 | shift ;; 115 | 116 | (-u|--unquoted) 117 | flags=u$flags ;; 118 | 119 | (-T|--test) 120 | return 4 ;; 121 | 122 | (-V|--version) 123 | echo "pure-getopt 1.4.4" 124 | return 0 ;; 125 | 126 | (--) 127 | shift 128 | break ;; 129 | esac 130 | 131 | shift 132 | done 133 | 134 | if ! $have_short; then 135 | # $short was declared but never set, not even to an empty string. 136 | # This implies the second form in the synopsis. 137 | if [[ $# == 0 ]]; then 138 | echo 'getopt: missing optstring argument' >&2 139 | echo "Try 'getopt --help' for more information." >&2 140 | return 2 141 | fi 142 | short=$1 143 | have_short=true 144 | shift 145 | fi 146 | 147 | if [[ $short == -* ]]; then 148 | # Leading dash means generate output in place rather than reordering, 149 | # unless we're already in compatibility mode. 150 | [[ $flags == *c* ]] || flags=i$flags 151 | short=${short#?} 152 | elif [[ $short == +* ]]; then 153 | # Leading plus means POSIXLY_CORRECT, unless we're already in 154 | # compatibility mode. 155 | [[ $flags == *c* ]] || flags=p$flags 156 | short=${short#?} 157 | fi 158 | 159 | # This should fire if POSIXLY_CORRECT is in the environment, even if 160 | # it's an empty string. That's the difference between :+ and + 161 | flags=${POSIXLY_CORRECT+p}$flags 162 | 163 | _getopt_parse "${name:-getopt}" "$short" "$long" "$flags" "$@" 164 | } 165 | 166 | _getopt_parse() { 167 | # Inner getopt parser, used for both first parse and second parse. 168 | # Returns 0 for success, 1 for error parsing, 3 for internal error. 169 | # In the case of status 1, still generates stdout with whatever could 170 | # be parsed. 171 | # 172 | # $flags is a string of characters with the following meanings: 173 | # a - alternative parsing mode 174 | # c - GETOPT_COMPATIBLE 175 | # i - generate output in place rather than reordering 176 | # p - POSIXLY_CORRECT 177 | # q - disable error reporting 178 | # Q - disable normal output 179 | # t - quote for csh/tcsh 180 | # u - unquoted output 181 | 182 | declare name="$1" short="$2" long="$3" flags="$4" 183 | shift 4 184 | 185 | # Split $long on commas, prepend double-dashes, strip colons; 186 | # for use with _getopt_resolve_abbrev 187 | declare -a longarr 188 | _getopt_split longarr "$long" 189 | longarr=( "${longarr[@]/#/--}" ) 190 | longarr=( "${longarr[@]%:}" ) 191 | longarr=( "${longarr[@]%:}" ) 192 | 193 | # Parse and collect options and parameters 194 | declare -a opts params 195 | declare o alt_recycled=false error=0 196 | 197 | while [[ $# -gt 0 ]]; do 198 | case $1 in 199 | (--) 200 | params=( "${params[@]}" "${@:2}" ) 201 | break ;; 202 | 203 | (--*=*) 204 | o=${1%%=*} 205 | if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then 206 | error=1 207 | elif [[ ,"$long", == *,"${o#--}"::,* ]]; then 208 | opts=( "${opts[@]}" "$o" "${1#*=}" ) 209 | elif [[ ,"$long", == *,"${o#--}":,* ]]; then 210 | opts=( "${opts[@]}" "$o" "${1#*=}" ) 211 | elif [[ ,"$long", == *,"${o#--}",* ]]; then 212 | if $alt_recycled; then o=${o#-}; fi 213 | _getopt_err "$name: option '$o' doesn't allow an argument" 214 | error=1 215 | else 216 | echo "getopt: assertion failed (1)" >&2 217 | return 3 218 | fi 219 | alt_recycled=false 220 | ;; 221 | 222 | (--?*) 223 | o=$1 224 | if ! o=$(_getopt_resolve_abbrev "$o" "${longarr[@]}"); then 225 | error=1 226 | elif [[ ,"$long", == *,"${o#--}",* ]]; then 227 | opts=( "${opts[@]}" "$o" ) 228 | elif [[ ,"$long", == *,"${o#--}::",* ]]; then 229 | opts=( "${opts[@]}" "$o" '' ) 230 | elif [[ ,"$long", == *,"${o#--}:",* ]]; then 231 | if [[ $# -ge 2 ]]; then 232 | shift 233 | opts=( "${opts[@]}" "$o" "$1" ) 234 | else 235 | if $alt_recycled; then o=${o#-}; fi 236 | _getopt_err "$name: option '$o' requires an argument" 237 | error=1 238 | fi 239 | else 240 | echo "getopt: assertion failed (2)" >&2 241 | return 3 242 | fi 243 | alt_recycled=false 244 | ;; 245 | 246 | (-*) 247 | if [[ $flags == *a* ]]; then 248 | # Alternative parsing mode! 249 | # Try to handle as a long option if any of the following apply: 250 | # 1. There's an equals sign in the mix -x=3 or -xy=3 251 | # 2. There's 2+ letters and an abbreviated long match -xy 252 | # 3. There's a single letter and an exact long match 253 | # 4. There's a single letter and no short match 254 | o=${1::2} # temp for testing #4 255 | if [[ $1 == *=* || $1 == -?? || \ 256 | ,$long, == *,"${1#-}"[:,]* || \ 257 | ,$short, != *,"${o#-}"[:,]* ]]; then 258 | o=$(_getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" 2>/dev/null) 259 | case $? in 260 | (0) 261 | # Unambiguous match. Let the long options parser handle 262 | # it, with a flag to get the right error message. 263 | set -- "-$1" "${@:2}" 264 | alt_recycled=true 265 | continue ;; 266 | (1) 267 | # Ambiguous match, generate error and continue. 268 | _getopt_resolve_abbrev "${1%%=*}" "${longarr[@]}" >/dev/null 269 | error=1 270 | shift 271 | continue ;; 272 | (2) 273 | # No match, fall through to single-character check. 274 | true ;; 275 | (*) 276 | echo "getopt: assertion failed (3)" >&2 277 | return 3 ;; 278 | esac 279 | fi 280 | fi 281 | 282 | o=${1::2} 283 | if [[ "$short" == *"${o#-}"::* ]]; then 284 | if [[ ${#1} -gt 2 ]]; then 285 | opts=( "${opts[@]}" "$o" "${1:2}" ) 286 | else 287 | opts=( "${opts[@]}" "$o" '' ) 288 | fi 289 | elif [[ "$short" == *"${o#-}":* ]]; then 290 | if [[ ${#1} -gt 2 ]]; then 291 | opts=( "${opts[@]}" "$o" "${1:2}" ) 292 | elif [[ $# -ge 2 ]]; then 293 | shift 294 | opts=( "${opts[@]}" "$o" "$1" ) 295 | else 296 | _getopt_err "$name: option requires an argument -- '${o#-}'" 297 | error=1 298 | fi 299 | elif [[ "$short" == *"${o#-}"* ]]; then 300 | opts=( "${opts[@]}" "$o" ) 301 | if [[ ${#1} -gt 2 ]]; then 302 | set -- "$o" "-${1:2}" "${@:2}" 303 | fi 304 | else 305 | if [[ $flags == *a* ]]; then 306 | # Alternative parsing mode! Report on the entire failed 307 | # option. GNU includes =value but we omit it for sanity with 308 | # very long values. 309 | _getopt_err "$name: unrecognized option '${1%%=*}'" 310 | else 311 | _getopt_err "$name: invalid option -- '${o#-}'" 312 | if [[ ${#1} -gt 2 ]]; then 313 | set -- "$o" "-${1:2}" "${@:2}" 314 | fi 315 | fi 316 | error=1 317 | fi ;; 318 | 319 | (*) 320 | # GNU getopt in-place mode (leading dash on short options) 321 | # overrides POSIXLY_CORRECT 322 | if [[ $flags == *i* ]]; then 323 | opts=( "${opts[@]}" "$1" ) 324 | elif [[ $flags == *p* ]]; then 325 | params=( "${params[@]}" "$@" ) 326 | break 327 | else 328 | params=( "${params[@]}" "$1" ) 329 | fi 330 | esac 331 | 332 | shift 333 | done 334 | 335 | if [[ $flags == *Q* ]]; then 336 | true # generate no output 337 | else 338 | echo -n ' ' 339 | if [[ $flags == *[cu]* ]]; then 340 | printf '%s -- %s' "${opts[*]}" "${params[*]}" 341 | else 342 | if [[ $flags == *t* ]]; then 343 | _getopt_quote_csh "${opts[@]}" -- "${params[@]}" 344 | else 345 | _getopt_quote "${opts[@]}" -- "${params[@]}" 346 | fi 347 | fi 348 | echo 349 | fi 350 | 351 | return $error 352 | } 353 | 354 | _getopt_err() { 355 | if [[ $flags != *q* ]]; then 356 | printf '%s\n' "$1" >&2 357 | fi 358 | } 359 | 360 | _getopt_resolve_abbrev() { 361 | # Resolves an abbrevation from a list of possibilities. 362 | # If the abbreviation is unambiguous, echoes the expansion on stdout 363 | # and returns 0. If the abbreviation is ambiguous, prints a message on 364 | # stderr and returns 1. (For first parse this should convert to exit 365 | # status 2.) If there is no match at all, prints a message on stderr 366 | # and returns 2. 367 | declare a q="$1" 368 | declare -a matches=() 369 | shift 370 | for a; do 371 | if [[ $q == "$a" ]]; then 372 | # Exact match. Squash any other partial matches. 373 | matches=( "$a" ) 374 | break 375 | elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q" ]]; then 376 | # Exact alternative match. Squash any other partial matches. 377 | matches=( "$a" ) 378 | break 379 | elif [[ $a == "$q"* ]]; then 380 | # Abbreviated match. 381 | matches=( "${matches[@]}" "$a" ) 382 | elif [[ $flags == *a* && $q == -[^-]* && $a == -"$q"* ]]; then 383 | # Abbreviated alternative match. 384 | matches=( "${matches[@]}" "${a#-}" ) 385 | fi 386 | done 387 | case ${#matches[@]} in 388 | (0) 389 | [[ $flags == *q* ]] || \ 390 | printf "$name: unrecognized option %s\\n" >&2 \ 391 | "$(_getopt_quote "$q")" 392 | return 2 ;; 393 | (1) 394 | printf '%s' "${matches[0]}"; return 0 ;; 395 | (*) 396 | [[ $flags == *q* ]] || \ 397 | printf "$name: option %s is ambiguous; possibilities: %s\\n" >&2 \ 398 | "$(_getopt_quote "$q")" "$(_getopt_quote "${matches[@]}")" 399 | return 1 ;; 400 | esac 401 | } 402 | 403 | _getopt_split() { 404 | # Splits $2 at commas to build array specified by $1 405 | declare IFS=, 406 | eval "$1=( \$2 )" 407 | } 408 | 409 | _getopt_quote() { 410 | # Quotes arguments with single quotes, escaping inner single quotes 411 | declare s space='' q=\' 412 | for s; do 413 | printf "$space'%s'" "${s//$q/$q\\$q$q}" 414 | space=' ' 415 | done 416 | } 417 | 418 | _getopt_quote_csh() { 419 | # Quotes arguments with single quotes, escaping inner single quotes, 420 | # bangs, backslashes and newlines 421 | declare s i c space 422 | for s; do 423 | echo -n "$space'" 424 | for ((i=0; i<${#s}; i++)); do 425 | c=${s:i:1} 426 | case $c in 427 | (\\|\'|!) 428 | echo -n "'\\$c'" ;; 429 | ($'\n') 430 | echo -n "\\$c" ;; 431 | (*) 432 | echo -n "$c" ;; 433 | esac 434 | done 435 | echo -n \' 436 | space=' ' 437 | done 438 | } 439 | 440 | _getopt_help() { 441 | cat <<-EOT 442 | 443 | Usage: 444 | getopt 445 | getopt [options] [--] 446 | getopt [options] -o|--options [options] [--] 447 | 448 | Parse command options. 449 | 450 | Options: 451 | -a, --alternative allow long options starting with single - 452 | -l, --longoptions the long options to be recognized 453 | -n, --name the name under which errors are reported 454 | -o, --options the short options to be recognized 455 | -q, --quiet disable error reporting by getopt(3) 456 | -Q, --quiet-output no normal output 457 | -s, --shell set quoting conventions to those of 458 | -T, --test test for getopt(1) version 459 | -u, --unquoted do not quote the output 460 | 461 | -h, --help display this help 462 | -V, --version display version 463 | 464 | For more details see getopt(1). 465 | EOT 466 | } 467 | 468 | _getopt_version_check() { 469 | if [[ -z $BASH_VERSION ]]; then 470 | echo "getopt: unknown version of bash might not be compatible" >&2 471 | return 1 472 | fi 473 | 474 | # This is a lexical comparison that should be sufficient forever. 475 | if [[ $BASH_VERSION < 2.05b ]]; then 476 | echo "getopt: bash $BASH_VERSION might not be compatible" >&2 477 | return 1 478 | fi 479 | 480 | return 0 481 | } 482 | 483 | _getopt_version_check 484 | _getopt_main "$@" 485 | declare status=$? 486 | unset -f _getopt_main _getopt_err _getopt_parse _getopt_quote \ 487 | _getopt_quote_csh _getopt_resolve_abbrev _getopt_split _getopt_help \ 488 | _getopt_version_check 489 | return $status 490 | } 491 | -------------------------------------------------------------------------------- /test.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source getopt.bash 4 | 5 | evalbash() { 6 | eval "set -- $1" 7 | if [[ $# -gt 0 ]]; then 8 | printf '%q ' % "$@" 9 | echo 10 | fi 11 | } 12 | 13 | evalcsh() { 14 | # shellcheck disable=SC2016 15 | ARGS=$1 tcsh -c ' 16 | eval set argv = \( $ARGS:q \) 17 | echo $1:q 18 | echo $2:q 19 | echo $3:q 20 | echo $4:q 21 | ' 22 | } 23 | 24 | status=0 25 | want=$1 26 | num=1 27 | 28 | test() { 29 | declare myout myerr mynorm mystatus 30 | declare refout referr refnorm refstatus 31 | declare q=\' nl=$'\n' evalnorm=evalbash 32 | declare tmp 33 | 34 | if [[ -z $want || $want -eq $num ]]; then 35 | declare t="$*" 36 | t=${t%%$nl*} 37 | [[ ${#t} -gt 68 ]] && t="${t::65}..." 38 | printf "%3s. Testing %s%s " "$((num++))" \ 39 | "${GETOPT_COMPATIBLE+GETOPT_COMPATIBLE }${POSIXLY_CORRECT+POSIXLY_CORRECT }" \ 40 | "$t" 41 | else 42 | (( num++ )) 43 | return $status 44 | fi 45 | 46 | if [[ $1 == -s ]]; then 47 | if [[ $2 == csh || $2 == tcsh ]]; then 48 | evalnorm=evalcsh 49 | elif [[ $2 == none ]]; then 50 | evalnorm='printf %s' 51 | shift 2 52 | fi 53 | fi 54 | 55 | if [[ "$1 $2" == '-s csh' || "$1 $2" == '-s tcsh' ]]; then 56 | evalnorm=evalcsh 57 | fi 58 | 59 | refout=$(command getopt "$@" 2>/dev/null) 60 | refstatus=$? 61 | referr=$(command getopt "$@" 2>&1 >/dev/null) 62 | refnorm=$($evalnorm "$refout") 63 | 64 | myout=$(getopt "$@" 2>/dev/null) 65 | mystatus=$? 66 | myerr=$(getopt "$@" 2>&1 >/dev/null) 67 | mynorm=$($evalnorm "$myout") 68 | 69 | if [[ "$mystatus" == "$refstatus" && \ 70 | "$mynorm" == "$refnorm" && \ 71 | "$myerr" == "$referr" ]] 72 | then 73 | echo PASS 74 | elif [[ "$mystatus" == "$refstatus" && \ 75 | "$mynorm" == "$refnorm" && \ 76 | "$myerr" == *"Try 'getopt --help"* && \ 77 | "$myerr" == "${referr/\`/\'}" ]]; then 78 | # Older GNU getopt used a backquote in error message. 79 | echo PASS 80 | elif [[ "$mystatus" == "$refstatus" && \ 81 | "$mynorm" == "$refnorm" && \ 82 | "$myerr" == *ambiguous* && \ 83 | "$myerr" == "${referr/=* is /$q is }" ]]; then 84 | # Intentional difference between GNU getopt and pure-getopt: 85 | # gnu: getopt: option '--x=foo' is ambiguous; possibilities: '--xy' '--xz' 86 | # pure: getopt: option '--x' is ambiguous; possibilities: '--xy' '--xz' 87 | echo PASS 88 | elif [[ "$1" == -a && \ 89 | "$mystatus" == "$refstatus" && \ 90 | "$mynorm" == "$refnorm" && \ 91 | ( "$myerr" == *ambiguous* || "$myerr" == *requires* ) ]] && \ 92 | tmp=${referr/=* is /$q is } && tmp=${tmp//--/-} && \ 93 | [[ "$myerr" == "$tmp" ]]; then 94 | # GNU getopt reports errors inconsistently for alternative mode, 95 | # sometimes with one dash, sometimes with two. It seems to depend on 96 | # system libraries rather than the version of util-linux. We report 97 | # with a single dash (since it's alternative mode), for example: 98 | # gnu: getopt: option '-de' is ambiguous; possibilities: '--def' '--dez' 99 | # pure: getopt: option '-de' is ambiguous; possibilities: '-def' '-dez' 100 | echo PASS 101 | else 102 | echo FAIL 103 | diff -u \ 104 | --label reference \ 105 | <(printf 'EXIT: %s\nOUT: %s\nERR: %s\n' "$refstatus" "$refout" "$referr") \ 106 | --label mine \ 107 | <(printf 'EXIT: %s\nOUT: %s\nERR: %s\n' "$mystatus" "$myout" "$myerr") 108 | status=1 109 | fi 110 | 111 | # These get stuck 112 | unset GETOPT_COMPATIBLE POSIXLY_CORRECT 113 | } 114 | 115 | test_no_eval() { 116 | test -s none "$@" 117 | } 118 | 119 | title() { 120 | if [[ -z $want ]]; then 121 | echo 122 | echo "$*" 123 | echo 124 | fi 125 | } 126 | 127 | title "Simple short options" 128 | 129 | test -o xy:z:: --long=abc,def:,dez:: -- -x 130 | test -o xy:z:: --long=abc,def:,dez:: -- -yfoo 131 | test -o xy:z:: --long=abc,def:,dez:: -- -y foo 132 | test -o xy:z:: --long=abc,def:,dez:: -- -z foo 133 | test -o xy:z:: --long=abc,def:,dez:: -- -zfoo 134 | test -o xy:z:: --long=abc,def:,dez:: -- -K 135 | 136 | title "Simple long options" 137 | 138 | test -o xy:z:: --long=abc,def:,dez:: -- --abc 139 | test -o xy:z:: --long=abc,def:,dez:: -- --abc=foo 140 | test -o xy:z:: --long=abc,def:,dez:: -- --abc foo 141 | test -o xy:z:: --long=abc,def:,dez:: -- --def 142 | test -o xy:z:: --long=abc,def:,dez:: -- --def=foo 143 | test -o xy:z:: --long=abc,def:,dez:: -- --def foo 144 | test -o xy:z:: --long=abc,def:,dez:: -- --dez 145 | test -o xy:z:: --long=abc,def:,dez:: -- --dez=foo 146 | test -o xy:z:: --long=abc,def:,dez:: -- --dez foo 147 | test -o xy:z:: --long=abc,def:,dez:: -- --KK 148 | 149 | title "Abbreviated long options" 150 | 151 | test -o xy:z:: --long=abc,def:,dez:: -- --ab 152 | test -o xy:z:: --long=abc,def:,dez:: -- --a 153 | test -o xy:z:: --long=abc,def:,dez:: -- --ab foo 154 | test -o xy:z:: --long=abc,def:,dez:: -- --de 155 | test -o xy:z:: --long=abc,def:,dez:: -- --de=foo 156 | test -o xy:z:: --long=abc,def:,dez:: -- --de foo 157 | # Test exact match against partial match 158 | test -o '' --long=abc,abcd -- --abc 159 | 160 | title "Empty command lines" 161 | 162 | test -o xy:z:: --long=abc,def:,dez:: 163 | test -o xy:z:: --long=abc,def:,dez:: -- 164 | test -o xy:z:: --long=abc,def:,dez:: -- foo 165 | test -o xy:z:: --long=abc,def:,dez:: -- foo bar 166 | 167 | title "Alternative parsing" 168 | 169 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -abc 170 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -abc=foo 171 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -abc foo 172 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -def 173 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -def=foo 174 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -def foo 175 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -dez 176 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -dez=foo 177 | test -a -o xy:z:: --long=abc,def:,dez:: -- -xyz -dez foo 178 | # Test exact match against partial match 179 | test -a -o '' --long=abc,abcd -- -abc 180 | 181 | title "Alternative parsing abbreviated long options" 182 | 183 | test -a -o xy:z:: --long=abc,def:,dez:: -- -ab 184 | test -a -o xy:z:: --long=abc,def:,dez:: -- -a 185 | test -a -o xy:z:: --long=abc,def:,dez:: -- -ab foo 186 | test -a -o xy:z:: --long=abc,def:,dez:: -- -de 187 | test -a -o xy:z:: --long=abc,def:,dez:: -- -de=foo 188 | test -a -o xy:z:: --long=abc,def:,dez:: -- -de foo 189 | 190 | title "Quoting long arguments" 191 | 192 | test -o xy:z:: --long=abc,def:,dez:: -- -y "$(head -n 200 getopt.bash)" 193 | 194 | title "Passing custom program name with -n or --name" 195 | 196 | test -o xy:z:: --long=abc,def:,dez:: -n custom -- -y 197 | test -o xy:z:: --long=abc,def:,dez:: --name custom -- -y 198 | 199 | title "GETOPT_COMPATIBLE and POSIXLY_CORRECT" 200 | 201 | # Baseline reorders options before non-option params: -x -y -- foo 202 | test -o xy -- -x foo -y 203 | # Leading dash doesn't reorder: -x foo -y 204 | test -o -xy -- -x foo -y 205 | # ..except in compatibility mode: -x -y -- foo 206 | GETOPT_COMPATIBLE='' test -xy -x foo -y 207 | # Leading plus does POSIXLY_CORRECT: -x -- foo -y 208 | test -o +xy -- -x foo -y 209 | POSIXLY_CORRECT='' test -o xy -- -x foo -y 210 | POSIXLY_CORRECT='' test -o +xy -- -x foo -y 211 | # ..except in compatibility mode: -x -y -- foo 212 | GETOPT_COMPATIBLE='' test +xy -x foo -y 213 | # and POSIXLY_CORRECT overrides GETOPT_COMPATIBLE: -x -- foo -y 214 | GETOPT_COMPATIBLE='' POSIXLY_CORRECT='' test xy -x foo -y 215 | 216 | title "Error getopt invocations" 217 | 218 | test 219 | test -o 220 | test -- 221 | test --long=foo 222 | 223 | # Only check --help against latest version 224 | getopt_version=$(command getopt --version 2>&1) 225 | getopt_version=${getopt_version##* } 226 | if [[ $getopt_version != *.* ]]; then 227 | echo "can't figure out getopt version" >&2 228 | status=1 229 | else 230 | reference_version=2.36.1 231 | if [[ $(printf '%s\n%s\n' "$getopt_version" "$reference_version" | \ 232 | sort --version-sort | head -n1) == "$reference_version" ]]; then 233 | title "Getopt help" 234 | 235 | test_no_eval -h 236 | test_no_eval --help 237 | fi 238 | fi 239 | 240 | title "Getopt version with -T" 241 | 242 | test -T 243 | GETOPT_COMPATIBLE=1 test -T 244 | # GETOPT_COMPATIBLE empty string should work too 245 | GETOPT_COMPATIBLE='' test -T 246 | 247 | title "Setting shell with -s" 248 | 249 | test -s sh -o xy:z:: --long=abc,def:,dez:: -- -x -y a\\b\ c 250 | test -s bash -o xy:z:: --long=abc,def:,dez:: -- -x -y a\\b\ c 251 | test -s foo -o xy:z:: --long=abc,def:,dez:: -- -x -y a\\b\ c 252 | 253 | if type tcsh &>/dev/null; then 254 | test -s csh -o xy:z:: --long=abc,def:,dez:: -- -x -y a\\b\ c 255 | test -s tcsh -o xy:z:: --long=abc,def:,dez:: -- -x -y a\\b\ c 256 | fi 257 | 258 | title "Regression tests" 259 | 260 | # Spelling error $flgas. 261 | # The bug causes -a (and any other flags) to be dropped. 262 | # https://github.com/agriffis/pure-getopt/issues/2 263 | test -a -o -xy:z:: --long=abc,def:,dez:: -- -ab 264 | 265 | exit $status 266 | 267 | # vim:sw=2 268 | -------------------------------------------------------------------------------- /travis.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | status=0 4 | 5 | for b in $BASHES; do 6 | echo 7 | echo ================== 8 | echo $b 9 | echo ================== 10 | $b test.bash || status=$? 11 | done 12 | 13 | exit $status 14 | --------------------------------------------------------------------------------