github.com/AbhinandanKurakure/podman/v3@v3.4.10/test/system/helpers.bash (about)

     1  # -*- bash -*-
     2  
     3  # Podman command to run; may be podman-remote
     4  PODMAN=${PODMAN:-podman}
     5  
     6  # Standard image to use for most tests
     7  PODMAN_TEST_IMAGE_REGISTRY=${PODMAN_TEST_IMAGE_REGISTRY:-"quay.io"}
     8  PODMAN_TEST_IMAGE_USER=${PODMAN_TEST_IMAGE_USER:-"libpod"}
     9  PODMAN_TEST_IMAGE_NAME=${PODMAN_TEST_IMAGE_NAME:-"testimage"}
    10  PODMAN_TEST_IMAGE_TAG=${PODMAN_TEST_IMAGE_TAG:-"20210610"}
    11  PODMAN_TEST_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG"
    12  PODMAN_TEST_IMAGE_ID=
    13  
    14  # Remote image that we *DO NOT* fetch or keep by default; used for testing pull
    15  # This has changed in 2021, from 0 through 3, various iterations of getting
    16  # multiarch to work. It should change only very rarely.
    17  PODMAN_NONLOCAL_IMAGE_TAG=${PODMAN_NONLOCAL_IMAGE_TAG:-"00000003"}
    18  PODMAN_NONLOCAL_IMAGE_FQN="$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME:$PODMAN_NONLOCAL_IMAGE_TAG"
    19  
    20  # Because who wants to spell that out each time?
    21  IMAGE=$PODMAN_TEST_IMAGE_FQN
    22  
    23  # Default timeout for a podman command.
    24  PODMAN_TIMEOUT=${PODMAN_TIMEOUT:-120}
    25  
    26  # Prompt to display when logging podman commands; distinguish root/rootless
    27  _LOG_PROMPT='$'
    28  if [ $(id -u) -eq 0 ]; then
    29      _LOG_PROMPT='#'
    30  fi
    31  
    32  ###############################################################################
    33  # BEGIN setup/teardown tools
    34  
    35  # Provide common setup and teardown functions, but do not name them such!
    36  # That way individual tests can override with their own setup/teardown,
    37  # while retaining the ability to include these if they so desire.
    38  
    39  # Some CI systems set this to runc, overriding the default crun.
    40  # Although it would be more elegant to override options in run_podman(),
    41  # we instead override $PODMAN itself because some tests (170-run-userns)
    42  # have to invoke $PODMAN directly.
    43  if [[ -n $OCI_RUNTIME ]]; then
    44      if [[ -z $CONTAINERS_CONF ]]; then
    45          # FIXME: BATS provides no mechanism for end-of-run cleanup[1]; how
    46          # can we avoid leaving this file behind when we finish?
    47          #   [1] https://github.com/bats-core/bats-core/issues/39
    48          export CONTAINERS_CONF=$(mktemp --tmpdir=${BATS_TMPDIR:-/tmp} podman-bats-XXXXXXX.containers.conf)
    49          cat >$CONTAINERS_CONF <<EOF
    50  [engine]
    51  runtime="$OCI_RUNTIME"
    52  EOF
    53      fi
    54  fi
    55  
    56  # Setup helper: establish a test environment with exactly the images needed
    57  function basic_setup() {
    58      # Clean up all containers
    59      run_podman rm --all --force
    60  
    61      # ...including external (buildah) ones
    62      run_podman ps --all --external --format '{{.ID}} {{.Names}}'
    63      for line in "${lines[@]}"; do
    64          set $line
    65          echo "# setup(): removing stray external container $1 ($2)" >&3
    66          run_podman rm $1
    67      done
    68  
    69      # Clean up all images except those desired
    70      found_needed_image=
    71      run_podman images --all --format '{{.Repository}}:{{.Tag}} {{.ID}}'
    72      for line in "${lines[@]}"; do
    73          set $line
    74          if [ "$1" == "$PODMAN_TEST_IMAGE_FQN" ]; then
    75              if [[ -z "$PODMAN_TEST_IMAGE_ID" ]]; then
    76                  # This will probably only trigger the 2nd time through setup
    77                  PODMAN_TEST_IMAGE_ID=$2
    78              fi
    79              found_needed_image=1
    80          else
    81              # Always remove image that doesn't match by name
    82              echo "# setup(): removing stray image $1" >&3
    83              run_podman rmi --force "$1" >/dev/null 2>&1 || true
    84  
    85              # Tagged image will have same IID as our test image; don't rmi it.
    86              if [[ $2 != "$PODMAN_TEST_IMAGE_ID" ]]; then
    87                  echo "# setup(): removing stray image $2" >&3
    88                  run_podman rmi --force "$2" >/dev/null 2>&1 || true
    89              fi
    90          fi
    91      done
    92  
    93      # Make sure desired images are present
    94      if [ -z "$found_needed_image" ]; then
    95          run_podman pull "$PODMAN_TEST_IMAGE_FQN"
    96      fi
    97  
    98      # Argh. Although BATS provides $BATS_TMPDIR, it's just /tmp!
    99      # That's bloody worthless. Let's make our own, in which subtests
   100      # can write whatever they like and trust that it'll be deleted
   101      # on cleanup.
   102      # TODO: do this outside of setup, so it carries across tests?
   103      PODMAN_TMPDIR=$(mktemp -d --tmpdir=${BATS_TMPDIR:-/tmp} podman_bats.XXXXXX)
   104  
   105      # In the unlikely event that a test runs is() before a run_podman()
   106      MOST_RECENT_PODMAN_COMMAND=
   107  }
   108  
   109  # Basic teardown: remove all pods and containers
   110  function basic_teardown() {
   111      echo "# [teardown]" >&2
   112      run_podman '?' pod rm --all --force
   113      run_podman '?'     rm --all --force
   114  
   115      command rm -rf $PODMAN_TMPDIR
   116  }
   117  
   118  
   119  # Provide the above as default methods.
   120  function setup() {
   121      basic_setup
   122  }
   123  
   124  function teardown() {
   125      basic_teardown
   126  }
   127  
   128  
   129  # Helpers useful for tests running rmi
   130  function archive_image() {
   131      local image=$1
   132  
   133      # FIXME: refactor?
   134      archive_basename=$(echo $1 | tr -c a-zA-Z0-9._- _)
   135      archive=$BATS_TMPDIR/$archive_basename.tar
   136  
   137      run_podman save -o $archive $image
   138  }
   139  
   140  function restore_image() {
   141      local image=$1
   142  
   143      archive_basename=$(echo $1 | tr -c a-zA-Z0-9._- _)
   144      archive=$BATS_TMPDIR/$archive_basename.tar
   145  
   146      run_podman restore $archive
   147  }
   148  
   149  # END   setup/teardown tools
   150  ###############################################################################
   151  # BEGIN podman helpers
   152  
   153  ################
   154  #  run_podman  #  Invoke $PODMAN, with timeout, using BATS 'run'
   155  ################
   156  #
   157  # This is the preferred mechanism for invoking podman: first, it
   158  # invokes $PODMAN, which may be 'podman-remote' or '/some/path/podman'.
   159  #
   160  # Second, we use 'timeout' to abort (with a diagnostic) if something
   161  # takes too long; this is preferable to a CI hang.
   162  #
   163  # Third, we log the command run and its output. This doesn't normally
   164  # appear in BATS output, but it will if there's an error.
   165  #
   166  # Next, we check exit status. Since the normal desired code is 0,
   167  # that's the default; but the first argument can override:
   168  #
   169  #     run_podman 125  nonexistent-subcommand
   170  #     run_podman '?'  some-other-command       # let our caller check status
   171  #
   172  # Since we use the BATS 'run' mechanism, $output and $status will be
   173  # defined for our caller.
   174  #
   175  function run_podman() {
   176      # Number as first argument = expected exit code; default 0
   177      expected_rc=0
   178      case "$1" in
   179          [0-9])           expected_rc=$1; shift;;
   180          [1-9][0-9])      expected_rc=$1; shift;;
   181          [12][0-9][0-9])  expected_rc=$1; shift;;
   182          '?')             expected_rc=  ; shift;;  # ignore exit code
   183      esac
   184  
   185      # Remember command args, for possible use in later diagnostic messages
   186      MOST_RECENT_PODMAN_COMMAND="podman $*"
   187  
   188      # stdout is only emitted upon error; this echo is to help a debugger
   189      echo "$_LOG_PROMPT $PODMAN $*"
   190      # BATS hangs if a subprocess remains and keeps FD 3 open; this happens
   191      # if podman crashes unexpectedly without cleaning up subprocesses.
   192      run timeout --foreground -v --kill=10 $PODMAN_TIMEOUT $PODMAN $_PODMAN_TEST_OPTS "$@" 3>/dev/null
   193      # without "quotes", multiple lines are glommed together into one
   194      if [ -n "$output" ]; then
   195          echo "$output"
   196      fi
   197      if [ "$status" -ne 0 ]; then
   198          echo -n "[ rc=$status ";
   199          if [ -n "$expected_rc" ]; then
   200              if [ "$status" -eq "$expected_rc" ]; then
   201                  echo -n "(expected) ";
   202              else
   203                  echo -n "(** EXPECTED $expected_rc **) ";
   204              fi
   205          fi
   206          echo "]"
   207      fi
   208  
   209      if [ "$status" -eq 124 ]; then
   210          if expr "$output" : ".*timeout: sending" >/dev/null; then
   211              # It's possible for a subtest to _want_ a timeout
   212              if [[ "$expected_rc" != "124" ]]; then
   213                  echo "*** TIMED OUT ***"
   214                  false
   215              fi
   216          fi
   217      fi
   218  
   219      if [ -n "$expected_rc" ]; then
   220          if [ "$status" -ne "$expected_rc" ]; then
   221              die "exit code is $status; expected $expected_rc"
   222          fi
   223      fi
   224  }
   225  
   226  
   227  # Wait for certain output from a container, indicating that it's ready.
   228  function wait_for_output {
   229      local sleep_delay=5
   230      local how_long=$PODMAN_TIMEOUT
   231      local expect=
   232      local cid=
   233  
   234      # Arg processing. A single-digit number is how long to sleep between
   235      # iterations; a 2- or 3-digit number is the total time to wait; all
   236      # else are, in order, the string to expect and the container name/ID.
   237      local i
   238      for i in "$@"; do
   239          if expr "$i" : '[0-9]\+$' >/dev/null; then
   240              if [ $i -le 9 ]; then
   241                  sleep_delay=$i
   242              else
   243                  how_long=$i
   244              fi
   245          elif [ -z "$expect" ]; then
   246              expect=$i
   247          else
   248              cid=$i
   249          fi
   250      done
   251  
   252      [ -n "$cid" ] || die "FATAL: wait_for_output: no container name/ID in '$*'"
   253  
   254      t1=$(expr $SECONDS + $how_long)
   255      while [ $SECONDS -lt $t1 ]; do
   256          run_podman logs $cid
   257          logs=$output
   258          if expr "$logs" : ".*$expect" >/dev/null; then
   259              return
   260          fi
   261  
   262          # Barf if container is not running
   263          run_podman inspect --format '{{.State.Running}}' $cid
   264          if [ $output != "true" ]; then
   265              run_podman inspect --format '{{.State.ExitCode}}' $cid
   266              exitcode=$output
   267              die "Container exited (status: $exitcode) before we saw '$expect': $logs"
   268          fi
   269  
   270          sleep $sleep_delay
   271      done
   272  
   273      die "timed out waiting for '$expect' from $cid"
   274  }
   275  
   276  # Shortcut for the lazy
   277  function wait_for_ready {
   278      wait_for_output 'READY' "$@"
   279  }
   280  
   281  ######################
   282  #  random_free_port  #  Pick an available port within a specified range
   283  ######################
   284  function random_free_port() {
   285      local range=${1:-5000-5999}
   286  
   287      local port
   288      for port in $(shuf -i ${range}); do
   289          if ! { exec {unused_fd}<> /dev/tcp/127.0.0.1/$port; } &>/dev/null; then
   290              echo $port
   291              return
   292          fi
   293      done
   294  
   295      die "Could not find open port in range $range"
   296  }
   297  
   298  ###################
   299  #  wait_for_port  #  Returns once port is available on host
   300  ###################
   301  function wait_for_port() {
   302      local host=$1                      # Probably "localhost"
   303      local port=$2                      # Numeric port
   304      local _timeout=${3:-5}              # Optional; default to 5 seconds
   305  
   306      # Wait
   307      while [ $_timeout -gt 0 ]; do
   308          { exec {unused_fd}<> /dev/tcp/$host/$port; } &>/dev/null && return
   309          sleep 1
   310          _timeout=$(( $_timeout - 1 ))
   311      done
   312  
   313      die "Timed out waiting for $host:$port"
   314  }
   315  
   316  # END   podman helpers
   317  ###############################################################################
   318  # BEGIN miscellaneous tools
   319  
   320  # Shortcuts for common needs:
   321  function is_rootless() {
   322      [ "$(id -u)" -ne 0 ]
   323  }
   324  
   325  function is_remote() {
   326      [[ "$PODMAN" =~ -remote ]]
   327  }
   328  
   329  function is_cgroupsv1() {
   330      # WARNING: This will break if there's ever a cgroups v3
   331      ! is_cgroupsv2
   332  }
   333  
   334  # True if cgroups v2 are enabled
   335  function is_cgroupsv2() {
   336      cgroup_type=$(stat -f -c %T /sys/fs/cgroup)
   337      test "$cgroup_type" = "cgroup2fs"
   338  }
   339  
   340  # Returns the OCI runtime *basename* (typically crun or runc). Much as we'd
   341  # love to cache this result, we probably shouldn't.
   342  function podman_runtime() {
   343      # This function is intended to be used as '$(podman_runtime)', i.e.
   344      # our caller wants our output. run_podman() messes with output because
   345      # it emits the command invocation to stdout, hence the redirection.
   346      run_podman info --format '{{ .Host.OCIRuntime.Name }}' >/dev/null
   347      basename "${output:-[null]}"
   348  }
   349  
   350  # rhbz#1895105: rootless journald is unavailable except to users in
   351  # certain magic groups; which our testuser account does not belong to
   352  # (intentional: that is the RHEL default, so that's the setup we test).
   353  function journald_unavailable() {
   354      if ! is_rootless; then
   355          # root must always have access to journal
   356          return 1
   357      fi
   358  
   359      run journalctl -n 1
   360      if [[ $status -eq 0 ]]; then
   361          return 1
   362      fi
   363  
   364      if [[ $output =~ permission ]]; then
   365          return 0
   366      fi
   367  
   368      # This should never happen; if it does, it's likely that a subsequent
   369      # test will fail. This output may help track that down.
   370      echo "WEIRD: 'journalctl -n 1' failed with a non-permission error:"
   371      echo "$output"
   372      return 1
   373  }
   374  
   375  ###########################
   376  #  _add_label_if_missing  #  make sure skip messages include rootless/remote
   377  ###########################
   378  function _add_label_if_missing() {
   379      local msg="$1"
   380      local want="$2"
   381  
   382      if [ -z "$msg" ]; then
   383          echo
   384      elif expr "$msg" : ".*$want" &>/dev/null; then
   385          echo "$msg"
   386      else
   387          echo "[$want] $msg"
   388      fi
   389  }
   390  
   391  ######################
   392  #  skip_if_rootless  #  ...with an optional message
   393  ######################
   394  function skip_if_rootless() {
   395      if is_rootless; then
   396          local msg=$(_add_label_if_missing "$1" "rootless")
   397          skip "${msg:-not applicable under rootless podman}"
   398      fi
   399  }
   400  
   401  ####################
   402  #  skip_if_remote  #  ...with an optional message
   403  ####################
   404  function skip_if_remote() {
   405      if is_remote; then
   406          local msg=$(_add_label_if_missing "$1" "remote")
   407          skip "${msg:-test does not work with podman-remote}"
   408      fi
   409  }
   410  
   411  ########################
   412  #  skip_if_no_selinux  #
   413  ########################
   414  function skip_if_no_selinux() {
   415      if [ ! -e /usr/sbin/selinuxenabled ]; then
   416          skip "selinux not available"
   417      elif ! /usr/sbin/selinuxenabled; then
   418          skip "selinux disabled"
   419      fi
   420  }
   421  
   422  #######################
   423  #  skip_if_cgroupsv1  #  ...with an optional message
   424  #######################
   425  function skip_if_cgroupsv1() {
   426      if ! is_cgroupsv2; then
   427          skip "${1:-test requires cgroupsv2}"
   428      fi
   429  }
   430  
   431  ##################################
   432  #  skip_if_journald_unavailable  #  rhbz#1895105: rootless journald permissions
   433  ##################################
   434  function skip_if_journald_unavailable {
   435      if journald_unavailable; then
   436          skip "Cannot use rootless journald on this system"
   437      fi
   438  }
   439  
   440  #########
   441  #  die  #  Abort with helpful message
   442  #########
   443  function die() {
   444      # FIXME: handle multi-line output
   445      echo "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"  >&2
   446      echo "#| FAIL: $*"                                           >&2
   447      echo "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" >&2
   448      false
   449  }
   450  
   451  
   452  ########
   453  #  is  #  Compare actual vs expected string; fail w/diagnostic if mismatch
   454  ########
   455  #
   456  # Compares given string against expectations, using 'expr' to allow patterns.
   457  #
   458  # Examples:
   459  #
   460  #   is "$actual" "$expected" "descriptive test name"
   461  #   is "apple" "orange"  "name of a test that will fail in most universes"
   462  #   is "apple" "[a-z]\+" "this time it should pass"
   463  #
   464  function is() {
   465      local actual="$1"
   466      local expect="$2"
   467      local testname="${3:-${MOST_RECENT_PODMAN_COMMAND:-[no test name given]}}"
   468  
   469      if [ -z "$expect" ]; then
   470          if [ -z "$actual" ]; then
   471              return
   472          fi
   473          expect='[no output]'
   474      elif expr "$actual" : "$expect" >/dev/null; then
   475          return
   476      fi
   477  
   478      # This is a multi-line message, which may in turn contain multi-line
   479      # output, so let's format it ourself, readably
   480      local -a actual_split
   481      readarray -t actual_split <<<"$actual"
   482      printf "#/vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv\n" >&2
   483      printf "#|     FAIL: $testname\n"                          >&2
   484      printf "#| expected: '%s'\n" "$expect"                     >&2
   485      printf "#|   actual: '%s'\n" "${actual_split[0]}"          >&2
   486      local line
   487      for line in "${actual_split[@]:1}"; do
   488          printf "#|         > '%s'\n" "$line"                   >&2
   489      done
   490      printf "#\\^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n" >&2
   491      false
   492  }
   493  
   494  
   495  ############
   496  #  dprint  #  conditional debug message
   497  ############
   498  #
   499  # Set PODMAN_TEST_DEBUG to the name of one or more functions you want to debug
   500  #
   501  # Examples:
   502  #
   503  #    $ PODMAN_TEST_DEBUG=parse_table bats .
   504  #    $ PODMAN_TEST_DEBUG="test_podman_images test_podman_run" bats .
   505  #
   506  function dprint() {
   507      test -z "$PODMAN_TEST_DEBUG" && return
   508  
   509      caller="${FUNCNAME[1]}"
   510  
   511      # PODMAN_TEST_DEBUG is a space-separated list of desired functions
   512      # e.g. "parse_table test_podman_images" (or even just "table")
   513      for want in $PODMAN_TEST_DEBUG; do
   514          # Check if our calling function matches any of the desired strings
   515          if expr "$caller" : ".*$want" >/dev/null; then
   516              echo "# ${FUNCNAME[1]}() : $*" >&3
   517              return
   518          fi
   519      done
   520  }
   521  
   522  
   523  #################
   524  #  parse_table  #  Split a table on '|' delimiters; return space-separated
   525  #################
   526  #
   527  # See sample .bats scripts for examples. The idea is to list a set of
   528  # tests in a table, then use simple logic to iterate over each test.
   529  # Columns are separated using '|' (pipe character) because sometimes
   530  # we need spaces in our fields.
   531  #
   532  function parse_table() {
   533      while read line; do
   534          test -z "$line" && continue
   535  
   536          declare -a row=()
   537          while read col; do
   538              dprint "col=<<$col>>"
   539              row+=("$col")
   540          done <  <(echo "$line" | sed -E -e 's/(^|\s)\|(\s|$)/\n /g' | sed -e 's/^ *//' -e 's/\\/\\\\/g')
   541          # the above seds:
   542          #   1) Convert '|' to newline, but only if bracketed by spaces or
   543          #      at beginning/end of line (this allows 'foo|bar' in tests);
   544          #   2) then remove leading whitespace;
   545          #   3) then double-escape all backslashes
   546  
   547          printf "%q " "${row[@]}"
   548          printf "\n"
   549      done <<<"$1"
   550  }
   551  
   552  
   553  ###################
   554  #  random_string  #  Returns a pseudorandom human-readable string
   555  ###################
   556  #
   557  # Numeric argument, if present, is desired length of string
   558  #
   559  function random_string() {
   560      local length=${1:-10}
   561  
   562      head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length
   563  }
   564  
   565  
   566  ###########################
   567  #  random_rfc1918_subnet  #
   568  ###########################
   569  #
   570  # Use the class B set, because much of our CI environment (Google, RH)
   571  # already uses up much of the class A, and it's really hard to test
   572  # if a block is in use.
   573  #
   574  # This returns THREE OCTETS! It is up to our caller to append .0/24, .255, &c.
   575  #
   576  function random_rfc1918_subnet() {
   577      local retries=1024
   578  
   579      while [ "$retries" -gt 0 ];do
   580          local cidr=172.$(( 16 + $RANDOM % 16 )).$(( $RANDOM & 255 ))
   581  
   582          in_use=$(ip route list | fgrep $cidr)
   583          if [ -z "$in_use" ]; then
   584              echo "$cidr"
   585              return
   586          fi
   587  
   588          retries=$(( retries - 1 ))
   589      done
   590  
   591      die "Could not find a random not-in-use rfc1918 subnet"
   592  }
   593  
   594  
   595  #########################
   596  #  find_exec_pid_files  #  Returns nothing or exec_pid hash files
   597  #########################
   598  #
   599  # Return exec_pid hash files if exists, otherwise, return nothing
   600  #
   601  function find_exec_pid_files() {
   602      run_podman info --format '{{.Store.RunRoot}}'
   603      local storage_path="$output"
   604      if [ -d $storage_path ]; then
   605          find $storage_path -type f -iname 'exec_pid_*'
   606      fi
   607  }
   608  
   609  
   610  #############################
   611  #  remove_same_dev_warning  #  Filter out useless warning from output
   612  #############################
   613  #
   614  # On some CI systems, 'podman run --privileged' emits a useless warning:
   615  #
   616  #    WARNING: The same type, major and minor should not be used for multiple devices.
   617  #
   618  # This obviously screws us up when we look at output results.
   619  #
   620  # This function removes the warning from $output and $lines. We don't
   621  # do a full string match because there's another variant of that message:
   622  #
   623  #    WARNING: Creating device "/dev/null" with same type, major and minor as existing "/dev/foodevdir/null".
   624  #
   625  # (We should never again see that precise error ever again, but we could
   626  # see variants of it).
   627  #
   628  function remove_same_dev_warning() {
   629      # No input arguments. We operate in-place on $output and $lines
   630  
   631      local i=0
   632      local -a new_lines=()
   633      while [[ $i -lt ${#lines[@]} ]]; do
   634          if expr "${lines[$i]}" : 'WARNING: .* same type, major' >/dev/null; then
   635              :
   636          else
   637              new_lines+=("${lines[$i]}")
   638          fi
   639          i=$(( i + 1 ))
   640      done
   641  
   642      lines=("${new_lines[@]}")
   643      output=$(printf '%s\n' "${lines[@]}")
   644  }
   645  
   646  # run 'podman help', parse the output looking for 'Available Commands';
   647  # return that list.
   648  function _podman_commands() {
   649      dprint "$@"
   650      run_podman help "$@" |
   651          awk '/^Available Commands:/{ok=1;next}/^Options:/{ok=0}ok { print $1 }' |
   652          grep .
   653      "$output"
   654  }
   655  
   656  # END   miscellaneous tools
   657  ###############################################################################