# -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*-
# vim:set ft=sh et sw=2 ts=2:
#
# NMG - Some useful functions NetworkManager scripts can include and use
# Author: Scott Shambarger <devel@shambarger.net>
#
# Copyright (C) 2014-2021 Scott Shambarger
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Instructions for use:
#
#   Setup a few constants in your NetworkManager dispatcher script, and
#   include this file, here's an example:
#
#       # optional, for logging
#       NMG_TAG="nmg"
#
#       # set NMUTILS/NMCONF early, and allow environment to override
#       NMUTILS=${NMUTILS:-/etc/nmutils}
#       NMCONF=${NMCONF:-${NMUTILS}/conf}
#
#       # optional min-version requirement
#       NMG_REQUIRED="1.4.1"
#
#       NMG=${NMG:-${NMUTILS}/general-functions}
#       { [[ -r ${NMG} ]] && . "${NMG}"; } || NMG=''
#
#   Use of NM* variables above is optional (NMG here indicates nmg_* functions
#   were loaded), but the above allows easy overrides from the environment
#   (and easy testing).  You may also want to customize some settings in
#   NMCONF/general.conf (see "Global overrides" below).
#
# Requires:
#
#   pgrep - to find running processes
#   rm - to remove stale files
#   ip - to query/manipulate interface addresses
#
# Supported, but optional:
#
#   logger - to log errors to syslog
#   radvd - triggered when ipv6 LAN addresses change
#
# Logging Functions (all clear error codes)
#
#   <err-modes> values (default: err):
#       err - log errors
#       info - log errors to info
#       debug - log errors to debug
#       nolog - don't log anything
#       ignore - don't log and return 0
#       retvar - <retvar>="<err-text>"
#
#   NOTE: <retvar> may be empty for any function that accepts <err-modes>,
#       except when <err-modes> includes "retvar".
#   NOTE: parameter errors (eg invalid <retvar> name) return 3.
#
#   nmg_log <log-level> <msg>
#
#     Log to syslog level <log-level>.
#
#   nmg_debug <msg> => nmg_log debug <msg>
#   nmg_info <msg> => nmg_log info <msg>
#   nmg_err <msg> => nmg_log err <msg>
#
#   nmg_read_config <filename> [ <no-err-log> ]
#
#     Read <filename> or returns 1 if not found, 2 on parse error.
#     If <no-err-log> set, don't log parse errors.
#
#   nmg_required_config <filename>
#
#     Read <filename> or exit 0 if not found, or exit with error
#     if any other error.
#
#   nmg_need_progs <exe>...
#
#     Tests if can run each <exe>, and if can't log error and return 2.
#
#   nmg_write <filename> [ <value>... ]
#
#     Write <value> to <filename> and log any errors.
#
#   nmg::read <retvar> <err-modes> <filename>
#
#     Reads <filename>, sets <retvar> to content.  Return 2 if not found
#     (logs debug), logs other errors. See <err-modes> above for options.
#
#   nmg_remove <filename>
#
#     Removes <filename>, ignores errors.
#
#   nmg::foreach_filematch <pattern> <wild> <callback> [ <args>... ]
#
#     Replaces <wild> in <pattern> with '*', and for each matching file calls
#       <callback> <filename> <match> [ <args>... ]
#     where <match> is part of <filename> that matched '*'.
#     If <callback> fails, callbacks stop and filematch returns value.
#
#   nmg::realpath <retvar> [ <path> ]
#
#     Sets <retvar> to full path of <path> (default `pwd`)
#
# Execution Functions (all functions return any error codes):
#
#   nmg_daemon <cmd> [ <args>... ]
#
#     Fork <cmd> into the background, and adds to NMG_DAEMON_CGROUP
#     control group (if set).  Returns 2 if <cmd> not found.
#
#   nmg::run <retvar> <err-modes> <cmd> [ <args>... ]
#
#     Run <cmd> with <args>, sets <retvar> to any output.  If <retvar>
#     empty, any output is logged info.  See <err-modes> above for options.
#
#   nmg::saferun => nmg::run, ignoring nmg_dryrun
#
#   nmg_cmd <cmd> [ <args> ]
#
#     Runs <cmd> with <args>, output is logged info.
#
#   nmg_qcmd <cmd> [ <args> ]
#
#     Runs <cmd> with <args>, output is logged debug.
#
#   nmg_is_running <cmd> [ <pidfile> [ <no-remove-pid> ] ]
#
#     Checks if program <cmd> is running. If <pidfile> supplied, checks
#     only pid in file. Removes <pidfile> unless <no-remove-pid> set.
#
# Number Functions
#
#   nmg::2dec <retvar> <value>
#
#     Sets <retvar> to decimal of <value>.
#
#   nmg::2hex <retvar> <value>
#
#     Sets <retvar> to hexidecimal of <value>.
#
#   nmg_hex_to_dec <hex> => nmg::2dec, echos result
#   nmg_dec_to_hex <num> => nmg::2hex, echos result
#
#   nmg::require_version <version> <min-required>
#
#     Returns 0 if <version> >= <min-required> ('.' format), 1 otherwise.
#
# String Functions
#
#   nmg::transpose <retvar> <string> <from> <to>
#
#     Transpose characters <from> to <to> in <string>, assign to <retvar>.
#
#   nmg::lowercase <retvar> <string>
#
#     Assign <retvar> <string> lowercased.
#
#   nmg::uppercase <retvar> <string>
#
#     Assign <retvar> <string> uppercased.
#
#   nmg::list_match_values <sep> <alist> <blist>
#
#     Compare all items in <alist> with <blist> (items separated by <sep>).
#     Returns 0 if match, 1 otherwise.  Duplicates are ignored.
#
# Array Functions
#
#   nmg::array <retarr> <sep> <sep-string>
#
#     Assign array <retarr> values from <sep-string> separated by <sep>.
#
#   nmg::array_join <retvar> <sep> <vals>...
#
#     Set <retvar> to string of <vars> joined by <sep>.
#
#   nmg::array_copy <retarr> <srcarr>
#
#     Copy values from <srcarr> to <retarr> (names must differ)
#
#   nmg::array_unique <retarr> <name>
#
#     Set <retarr> array to unique values of indexed array <name>.
#
#   nmg::array_match_values <name1> <name2>
#
#     Compare values of arrays named <name1> and <name2>, and returns 0
#     if all values match (any order), else return 1.  Duplicates are
#     ignored.
#
# IP Functions
#
#   nmg::query_ips <retvar> <err-modes> <[4|6][<flag>]>
#                                  [ <intf> [ <match> [ <ip-args>... ] ] ]
#
#     Sets <retvar> to array of addresses on <intf> (all if unset) matching
#     pattern <match> (all if unset) optionally limited to ipv4 (4)
#     or ipv6 (6). By default, <retvar> items are each
#         "<addr/prefix> <valid-life> <pref-life>".
#     <ip-args>... replace default "'scope' 'global'" ip args.
#     See <err-modes> above for options.  Returns 2 if interface not found
#     or <match> set and no matches found.
#     Optional <flag> can be:
#        'a' - return only address
#        'p' - replace lifetimes with property-list, eg format:
#           "<addr/prefix> temporary scope:global valid_lft:forever..."
#
#   nmg::wait_dad6 <intf> <addr> [ <timeout> ]
#
#     Wait for <addr> on interface <intf> to complete duplicate address
#     detection.  Returns 0 when non-tentative, 1 if DAD fails,
#     2 after <timeout> seconds (default 5) or address removed.
#
#   nmg::mod_ip <retvar> <err-modes> <cmd> <intf> <addr/plen> [ <ip-args>... ]
#
#     <cmd> one of add4 | del4 | change4 | add6 | del6 | change6
#     Performs <cmd> on <addr/plen> on <intf>.
#     See <err-modes> above for options.
#
# IPV4 Functions
#
#   nmg_check_ip4_addr <ip4-addr> [ <private-ok> ]
#
#     Returns 1 if <ip4-addr> has a invalid format or missing.
#     Returns 11 if <ip4-addr> is host-only.
#     Returns 12 if <ip4-addr> is link-local.
#     Returns 13 if <private-ok> not set and <ip4-addr> for a private network.
#     Returns 0 otherwise.
#
#   nmg_find_ip4_addrs [ <intf> [ <pattern> [ <ip-args> ] ] ]
#
#     Echos space seperated <addr/plen> addresses on <intf> (default all)
#     matching <pattern> (default all). <ip-args> replace default
#     "scope global".  Returns 0.
#
#   nmg_add_ip4_addr <intf> <addr/plen> [ <ip-args>... ]
#
#     Add <addr/plen> to <intf>, and updating with <ip-args> if already
#     present. <ip-args> optional ip command args (eg lifetimes)
#
#   nmg_change_ip4_addr <intf> <addr/plen> <ip-arg>...
#
#     Change <ip-args> for <addr/plen> on <intf>.
#
#   nmg_del_ip4_addr <intf> <addr/plen>
#
#     Remove <addr/plen> from <intf>. Return 2 if not present
#     (or other error).
#
# IPV6 Functions
#
#   nmg::is_ip6_prefix <prefix>
#
#    Returns 0 if <prefix> is valid, otherwise 1
#
#   nmg::create_ip6_prefix <retvar> <ip6-prefix/plen> <site> <site-len>
#
#     Assigns <retvar> a prefix of <site-len> combining <ip6-prefix/plen>
#     and <site>, <site> limited by bits <site-len> exceeds <plen>.
#     eg: <ip6-prefix>::<site>/<site-len> but handles overlap, etc
#
#   nmg::expand_ip6 <retvar> <err-modes> <ip6-prefix[/plen]> [ <fmt> ]
#
#     Assigns <retvar> to the fully expanded format of <ip6-prefix>
#     (/plen is preserved).  <fmt> is printf pattern for quads ("%x")
#
#   nmg_create_ip6_prefix <ip6-prefix/plen> <site> <site-len>
#
#     Echos result of nmg::create_ip6_prefix
#
#   nmg_check_ip6_addr <ip6-addr> [ <private-ok> ]
#
#     Returns 1 if <ip6-addr> has a invalid format or missing.
#     Returns 11 if <ip6-addr> is host-only.
#     Returns 12 if <ip6-addr> is link-local.
#     Returns 13 if <private-ok> not set and <ip6-addr> for a ULA network.
#     Returns 0 otherwise.
#
#   nmg_find_ip6_addrs [ <intf> [ <pattern> [ <ip-args> ] ] ]
#
#     Echos space seperated <addr/plen> addresses on <intf> (default all)
#     matching <pattern> (default all). <ip-args> replace default
#     "scope global".  Returns 0.
#
#   nmg_add_ip6_addr <intf> <addr/plen> [ <ip-args>... ]
#
#     Add <addr/plen> to <intf>, and updating with <ip-args> if already
#     present. <ip-args> optional ip command args (eg lifetimes)
#
#   nmg_change_ip6_addr <intf> <addr/plen> <ip-arg>...
#
#     Change <ip-args> for <addr/plen> on <intf>.
#
#   nmg_del_ip6_addr <intf> <addr/plen>
#
#     Remove <addr/plen> from <intf>. Return 2 if not present
#     (or other error).
#
#   nmg::create_ip6_host <retvar> <intf> [ <node> ]
#
#     Assigns <retvar> appropriate host-part for <intf> if <node>=auto/<unset>,
#     or <node> otherwise.  Returns 1 if none can be determined;
#     if <node>=auto, no error logged (retry later)
#
#   nmg_create_ip6_host <intf> [ <node> ]
#
#     Echos result of nmg::create_ip6_host
#
#   nmg::create_ip6_addr <retvar> <prefix/plen> <host-part>
#
#     Assigns <retvar> combination of <prefix/plen> and <host-part>
#
#   nmg_create_ip6_addr <prefix/plen> <host-part>
#
#     Echos result of nmg::create_ip6_addr (without /plen)
#
# Miscellaneous Functions
#
#   nmg::args_contains <item> <arg1>...
#
#     Returns 0 if <item> found in arguments <arg1>...
#
#   nmg_radvd_trigger
#
#     Signal radvd if it's running (see config below)
#
# NetworkManager Property Functions
#
#   nmg::prop_get_value <retvar> <props> <name>
#
#     Assign <retvar> (optional) the value of property <name> from <props>.
#     <props> format is "<name>:<value>[\n<name>:<value>]..."
#     Returns 0 if found, 1 if not (<retvar> cleared)
#
#   nmg::prop_set_value <retvar> <props> <name> [ <value> ]
#
#     Search <props>, and replace/add <name> with <value>, or
#     remove <name> if <value> empty.  Assign <retvar> with new <props>
#
#   nmg::prop_has_value <props> <name> <value> [ <sep> ]
#
#     Returns 0 if property <name> in <props> has <value>.
#     If <sep> not empty, property is multi-valued with separator <sep>.
#
#   nmg::prop_has_ivalue <props> <name> <value> [ <sep> ]
#
#     Same as nmc::prop_has_value, but comparison is case-insensitive
#
#   nmg::prop_match_values <props> <name> <sep> <list>
#
#     Compares multi-value property <name> (using <sep>) against
#     <list> ignoring duplicates and ordering.  Return 0 if values
#     match, 1 otherwise
#
# Configuration Settings (set before including this file)
#
#   NMG_TAG (optional) - tag to use on syslog messages
#
#   NMUTILS (default: /etc/nmutils) - location of this file
#
#   NMCONF (default: NMUTILS/conf) - location of general.conf
#
#   NMG_REQUIRED (optional) - minimum required NMG_VERSION
#
# Global Overrides (put in NMCONF/general.conf):
#
#   NMG_DAEMON_CGROUP (default NetworkManager's) - control group
#      to add commands passed to nmg_daemon.  Unset to not set cgroup.
#
#   NMG_RADVD_TRIGGER (optional) - executable name to use in place
#      of signalling radvd (for example to create a dynamic radvd.conf)
#
#   NMG_RADVD_TRIGGER_ARGS (optional) - args for NMG_RADVD_TRIGGER
#
#   NMG_RADVD (default: radvd) - radvd executable name seen by pgrep.
#      Unset to disable radvd signalling
#
#   NMG_RADVD_PID (default: /run/radvd/radvd.pid) - pid of running radvd
#
#   NMG_LOGGER_USEID (optional) - log script pid (requires appropriate
#      SELinux permisions and script run as root)
#
# NOTE: executable paths (see below) may be overriden if needed
#
# Globals
#
#   NMG_VERSION - current file version
#
# shellcheck shell=bash disable=SC1090,SC1091

[[ ${NMG_VERSION-} ]] || declare -r NMG_VERSION="1.6.1"

# set default paths if missing
NMUTILS=${NMUTILS:-/etc/nmutils}
NMCONF=${NMCONF:-$NMUTILS/conf}

########## Defaults (customize in $NMCONF/general.conf)

# A few settings for debugging
#   nmg_dryrun - if set, action commands are not run but return setting
#       as error code (may be 0)
nmg_dryrun=${nmg_dryrun-}
#   nmg_show_debug - if set logs debug level messages (normally not logged)
nmg_show_debug=${nmg_show_debug-}
#   nmg_log_stderr=<fileno> - log to <fileno> (>=2), or stderr (other)
#       default (empty): use default logger
nmg_log_stderr=${nmg_log_stderr-}

########## Default Paths

# by default, add daemons to NetworkManager
NMG_DAEMON_CGROUP="/sys/fs/cgroup/system.slice/NetworkManager.service/cgroup.procs"

# ip in /sbin, so add to PATH (and export it for daemons)
export PATH="${PATH}:/usr/local/sbin:/usr/sbin:/sbin"

########## Support Programs

NMG_LOGGER=${NMG_LOGGER:-logger}
NMG_PGREP=${NMG_PGREP:-pgrep}
NMG_RM=${NMG_RM:-rm}
NMG_IP=${NMG_IP:-ip}

# SELINUX: you may need to add rules for radvd signal to work
NMG_RADVD=${NMG_RADVD:-radvd}
NMG_RADVD_PID=${NMG_RADVD_PID:-/run/radvd/radvd.pid}
# SELINUX: you may need to add rules to allow logger to set pid
NMG_LOGGER_USEID=

########## SCRIPT START

# optional shared config defaults
[[ -r "${NMCONF}/general.conf" ]] && . "${NMCONF}/general.conf"

# if no logger, use stderr
command &>/dev/null -v "${NMG_LOGGER}" || nmg_log_stderr=1

nmg::_logfd() { # returns log <fileno>
  local -i fd
  printf 2>/dev/null -v fd %d "${nmg_log_stderr-}" || fd=2
  [[ ${fd} -lt 2 ]] && fd=2
  return ${fd}
}

nmg_log() {
  # <log-level> <msg>
  local prio=${1-} pfx='' IFS; unset IFS

  [[ -z ${prio} ]] && return 0
  [[ ${prio} != debug || ${nmg_show_debug-} ]] || return 0
  shift

  if [[ ${nmg_log_stderr-} ]]; then
    [[ ${prio} == debug ]] && pfx="DBG: "
    [[ ${prio} == err ]] && pfx="ERR: "
    [[ ${NMG_TAG-} ]] && pfx="${NMG_TAG}: ${pfx}"
    local -i fd=2; nmg::_logfd || fd=$?
    if [[ ${fd} == 2 ]]; then
      printf >&${fd} '%s\n' "${pfx}$*" || :
    else
      # shellcheck disable=SC2261
      printf 2>/dev/null >&${fd} '%s\n' "${pfx}$*" || :
    fi
  else
    local args=("-p" "daemon.${prio}")
    [[ ${NMG_TAG-} ]] && args+=("-t" "${NMG_TAG}")
    # if root and default logger, log with shell pid
    [[ ${EUID} == 0 && ${NMG_LOGGER_USEID-} ]] && args+=("--id=$$")
    "$NMG_LOGGER" >&2 "${args[@]}" "$*" || :
  fi

  return 0
}

nmg_debug() { nmg_log debug "$@"; }
nmg_info() { nmg_log info "$@"; }
nmg_err() { nmg_log err "$@"; }
nmg::_perr() { nmg_err "${FUNCNAME[1]}:" "$@"; }
nmg::_p2err() { nmg_err "${FUNCNAME[2]}:" "$@"; }
nmg::_p3err() { nmg_err "${FUNCNAME[3]}:" "$@"; }

# uses <err-modes> to determine correct log level (or not to log)
nmg::_logmsg() { # returns 0
  # <err-modes> <msg>
  local msg=${2-} mode err=1 info='' debug='' IFS; unset IFS
  for mode in ${1-}; do
    case "${mode}" in
      err) err=1 ;;
      info) info=1 ;;
      debug) debug=1 ;;
      nolog|ignore) return 0 ;;
    esac
  done
  # select debug > info > err
  if [[ ${debug} ]]; then
    nmg_debug "${msg}"
  elif [[ ${info} ]]; then
    nmg_info "${msg}"
  elif [[ ${err} ]]; then
    nmg_err "${msg}"
  fi
  return 0
}

# may use _nmgfunc, may set _nmgrc=0 (if ignore)
# may set _nmgvar="<msg>" (if retvar)
nmg::_errmode() { # returns 0
  # <err-modes> <msg>
  local msg=${2-} mode IFS; unset IFS
  for mode in ${1-}; do
    case ${mode} in
      nmg*) ;; # internal modes
      err|info|debug|nolog) ;; # handled in _logmsg
      ignore) [[ ${_nmgrc+x} ]] && _nmgrc=0 ;;
      retvar) _nmgvar="${msg}" ;;
      *) nmg_err "${_nmgfunc:+${_nmgfunc}: }unknown error mode '${mode}'" ;;
    esac
  done
  return 0
}

# calls nmg::_errmode, then nmg::_logerr "<_nmgfunc>: <msg>"
nmg::_err() { # returns 0
  # <msg>
  local msg=${1-}
  [[ ${msg} ]] || msg="empty <msg> to nmg::_perr"
  nmg::_errmode "${_nmglog-}" "${msg}"
  [[ ${_nmgfunc-} ]] && msg="${_nmgfunc}: ${msg}"
  nmg::_logmsg "${_nmglog-}" "${msg}"
}

nmg::_reqarg() {
  # <value> <arg-name>
  [[ $1 ]] && return 0
  nmg::_p2err "missing <$2>"
  return 3
}

nmg::_varset() {
  # <var-name>
  declare &>/dev/null -p "$1" && eval "(( \${#$1[*]} > 0 ))" && return 0
  return 1
}

nmg::require_version() { # <version> >= <min-required>
  # <version> <min-required>
  nmg::_reqarg "${1-}" "version" || return
  nmg::_reqarg "${2-}" "min-required" || return
  local IFS=. va=() ra=() i
  read -r -a va <<< "$1"; read -r -a ra <<< "$2"
  for (( i=0; i<${#ra[*]}; i++ )); do
    (( i > ${#va[*]} || ra[i] > va[i] )) && return 1
    (( ra[i] < va[i] )) && return 0
  done
  return 0
}

nmg_read_config() {
  # <filename> [ <no-err-log> ]
  local file=${1-} nolog=${2-} msg
  [[ ${file} ]] || return 1
  [[ -f ${file} ]] || return 1
  [[ -r ${file} ]] || {
    [[ ${nolog} ]] || nmg_err "nmg_read_config(${file}): access denied"
    return 2; }

  nmg_debug "Reading config file '${file}'"
  shopt -u sourcepath
  if [[ -x ${BASH} ]]; then
    msg=$("${BASH}" 2>&1 -n "${file}" || return) || {
      [[ ${nolog} ]] ||
        nmg_err "Failed to parse config file '${file}': ${msg//$'\n'/ }"
      return 2
    }
  fi
  . "${file}" &>/dev/null && return 0

  # capture error for logging
  [[ ${nolog} ]] || {
    msg=$(. "${file}" 2>&1 >/dev/null || :)
    nmg_err "Failed to load config file '${file}': ${msg//$'\n'/ }"
  }
  return 2
}

nmg_required_config() {
  # <filename>
  local rc=0
  nmg_read_config "$@" || rc=$?

  # 1 means no file, just exit 0
  [[ ${rc} == 1 ]] && exit 0
  # any other error, exit with it
  [[ ${rc} != 0 ]] && exit ${rc}
  # no errors, continue...
  return 0
}

nmg_need_progs() {
  # <exe>...
  local exe file

  for exe in "$@"; do
    [[ $exe ]] || {
      nmg::_perr "<exe> cannot be empty."; return 3; }
    read -r file <<< "$(command -v "${exe}" || :)"
    [[ $file ]] || {
      nmg_err "'${exe}' not found (locate in ${NMCONF}/general.conf)"
      return 2
    }
    [[ -x $file ]] || {
      nmg_err "${file} is required, but not executable!"
      return 2
    }
  done
  return 0
}

nmg_daemon() {
  # <cmd> [<cmd-args>...]
  nmg::_reqarg "${1-}" "cmd" || return

  nmg_debug "nmg_daemon:" "$@"

  nmg_need_progs "$1" || return

  # fork, add to daemon cgroup (if any) and re-exec command
  (
    [[ ${NMG_DAEMON_CGROUP} ]] && [[ -w ${NMG_DAEMON_CGROUP} ]] && {
      nmg_write "${NMG_DAEMON_CGROUP}" "${BASHPID}" || :; }
    # export environment
    export NMUTILS NMCONF
    if [[ ${nmg_log_stderr-} ]]; then
      local -i fd=2; nmg::_logfd || fd=$?
      exec </dev/null >&${fd} "$@"
    else
      exec </dev/null &>/dev/null "$@"
    fi
  ) &

  return 0
}

# see nmg::_err for variable use
nmg::_clearvar() {
  # <retvar> <err-modes> [ <call-depth>(1) ]
  local _nmglog=$2 _nmgfunc=${_nmgfunc:-${FUNCNAME[${3:-1}]}}
  if [[ $1 ]]; then
    printf 2>/dev/null -v "$1" %s "" ||
      { nmg::_err "invalid <retvar> '$1'"; return 3; }
    return 0
  fi
  # if nmgret/retvar mode, <retvar> required
  [[ ${2/ret} == "$2" ]] && return 0
  # "nmgret" mode used internally for return vals
  if [[ ${2/nmgret} != "$2" ]]; then
    nmg::_err "missing <retvar>"
  else
    nmg::_err "<err-mode> retvar requires one"
  fi
  return 3
}

# generic wrapper, calls "_{CALLER}", sets <retvar>
# wrapped func: use _nmglog, may set _nmgrc/_nmgvar
nmg::_wrap() { # returns 0
  # <retvar> <err-modes> <func-args>...
  local _nmgvar='' _nmgret=${1-} _nmglog=${2-}
  local _nmgfunc=${_nmgfunc:-${FUNCNAME[1]}} IFS
  unset IFS; shift 2 || set --
  nmg::_clearvar "${_nmgret}" "${_nmglog}" || { _nmgrc=$?; return 0; }
  # call _${CALLER} to avoid retvar name masking
  "nmg::_${FUNCNAME[1]##*::}" "$@"
  [[ ${_nmgret} ]] && printf -v "${_nmgret}" %s "${_nmgvar}"
  return 0
}

# calls nmg::<func> <func-args> isolating _nmg* variables from caller
nmg::_mock() { # internal callers for _wrap funcs
  # <retvar> <err-modes> <func> <func-args>...
  local _nmgrc=1 _nmgvar='' _nmgret=$1 _nmglog=$2 _nmgwhat=$3
  local _nmgfunc=${_nmgfunc:-${FUNCNAME[1]}}
  shift 3
  "nmg::_${_nmgwhat}" "$@"
  [[ ${_nmgret} ]] && printf -v "${_nmgret}" %s "${_nmgvar}"
  return "${_nmgrc}"
}

nmg::_cleararr() {
  # <retvar> <err-modes>
  nmg::_clearvar "$1" "$2" 2 || return
  # clear array, but retain type; $1 cleared as varname in _clearvar
  [[ $1 ]] && eval "$1=()"
  return 0
}

# generic array wrapper, calls "_{CALLER}", sets <retvar> as array
# wrapped func: use _nmglog, may set _nmgrc/_nmgarr (_nmgvar on error)
nmg::_awrap() { # returns 0
  # <retvar> <err-modes> <func-args>...
  local _nmgarr=() _nmgret=${1-} _nmglog=${2-} _nmgvar=''
  local _nmgfunc=${_nmgfunc:-${FUNCNAME[1]}} IFS
  unset IFS; shift 2 || set --
  if nmg::_cleararr "${_nmgret}" "${_nmglog}"; then
    # call _${CALLER} to avoid retvar name masking
    "nmg::_${FUNCNAME[1]##*::}" "$@"
  else
    _nmgrc=$?
    # if <retvar> invalid, clear it
    (( _nmgrc == 3 )) && _nmgret=''
  fi
  [[ ${_nmgret} ]] && {
    # return any error in _nmgvar if requested
    [[ ${_nmgvar} && ${_nmglog} =~ retvar ]] && _nmgarr=("${_nmgvar}")
    nmg::array_copy "${_nmgret}" _nmgarr || :
  }
  return 0
}

# called from nmg::_wrap
nmg::_run() { # returns 0
  # <cmd> [<cmd-args>...]
  local cmd=${1-} out=''

  [[ ${cmd} ]] || { nmg::_err "missing <cmd>"; _nmgrc=3; return 0; }
  shift

  if [[ ${nmg_dryrun-} ]]; then
    cmd="DRY-RUN: ${cmd}" _nmgrc=${nmg_dryrun}
  fi

  nmg_debug "${cmd}" "$@"

  if [[ -z ${nmg_dryrun-} ]]; then
    _nmgrc=0; out=$("${cmd}" 2>&1 "$@" || return) || _nmgrc=$?
  fi

  if [[ ${_nmgrc} != 0 ]]; then
    # clean up error messages for command not found etc...
    if [[ ${_nmgrc} == 127 ]] || [[ ${_nmgrc} == 126 ]]; then
      out=${out##*"${cmd}: "}
    fi
    _nmgfunc=''
    nmg::_err "FAIL(${_nmgrc}) ${cmd}${*:+ $*}${out:+ => ${out}}"
  elif [[ ${out} ]]; then
    # if no _nmgret, then log output (as would be lost)
    [[ ${_nmgret} ]] || nmg::_logmsg "${_nmglog} info" "${cmd} => ${out}"
    _nmgvar=${out}
  fi

  return 0
}

nmg::run() {
  local _nmgrc=1; nmg::_wrap "$@"; return ${_nmgrc}
}

nmg::saferun() {
  local nmg_dryrun=''
  nmg::run "$@" || return
}

nmg_cmd() {
  nmg::run "" "" "$@" || return
}

nmg_qcmd() {
  nmg::run "" "debug" "$@" || return
}

nmg_write() {
  # <filename> [<value>...]
  nmg::_reqarg "${1-}" "filename" || return
  local file=${1-} rc=0 res='' IFS; unset IFS
  shift

  nmg_debug "nmg_write($file)"

  if [[ ${nmg_dryrun-} ]]; then
    rc=${nmg_dryrun}
  else
    res=$(printf 2>&1 %s "$*" > "$file" || return) || rc=$?
  fi

  if [[ ${nmg_dryrun-} ]]; then
    local pfx='' log=info; [[ ${rc} != 0 ]] && pfx="FAIL(${rc}): " log=err
    nmg_log "${log}" "${pfx}DRY-RUN: nmg_write(${file})"
  elif [[ ${rc} != 0 ]]; then
    nmg_err "FAIL(${rc}) nmg_write(${file}) => ${res##*: }"
  fi

  return ${rc}
}

# called from nmg::_wrap
nmg::_read() { # returns 0
  # <filename>
  local file=${1-} out

  [[ ${file} ]] || { nmg::_err "empty <filename>"; return 0; }
  shift

  [[ -f ${file} ]] || {
    _nmglog+=" debug"; _nmgrc=2
    nmg::_err "'${file}' not found"; return 0; }
  [[ -r ${file} ]] || {
    nmg::_err "'${file}' Permission denied"; return 0; }

  nmg_debug "read file ${file}"
  unset out # out will not be set if read fails
  local IFS='' # keep trailing newlines
  { read -r -d '' out || :; } 2>/dev/null < "${file}"
  [[ ${out+x} ]] && { _nmgvar=${out}; _nmgrc=0; return 0; }

  # use subshell to collect stderr
  out=$(: 2>&1 >/dev/null < "${file}" || :)
  [[ ${out} ]] || out="read failed"
  nmg::_err "'${file}'${out##*:}"
}

nmg::read() {
  local _nmgrc=1; nmg::_wrap "$@"; return ${_nmgrc}
}

nmg_remove() {
  # <filename>
  nmg::_reqarg "${1-}" "filename" || return

  nmg_qcmd "$NMG_RM" -f "$1" || :
}

nmg::foreach_filematch() {
  # <pattern> <wild> <callback> [ <args>... ]
  nmg::_reqarg "${1-}" "pattern" || return
  nmg::_reqarg "${2-}" "wild" || return
  nmg::_reqarg "${3-}" "callback" || return
  [[ $1 == *"$2"* ]] || return 0
  local _pre=${1%"$2"*} _post=${1##*"$2"} _func=$3 _file _match
  shift 3

  for _file in "${_pre}"*"${_post}"; do

    # any matches? (handle unset nullglob)
    [[ ${_file} == "${_pre}*${_post}" ]] && break

    # get match
    _match=${_file#"${_pre}"}; _match=${_match%"${_post}"}
    "${_func}" "${_file}" "${_match}" "$@" || return
  done

  return 0
}

nmg::realpath() {
  # <retvar> <dir>
  nmg::_clearvar "${1-}" nmgret || return
  local __a=${2:-.} __b __p=''
  command &>/dev/null -v realpath && {
    read -r __p < <(realpath 2>/dev/null "${__a}") || __p=''; }
  [[ ${__p} ]] || {
    __b=${__a##*/} __p=${__a%"${__b}"}
    __p=$(cd "${__p}" &>/dev/null && pwd -P) || :
    [[ ${__p} ]] && __p="${__p}/${__b}"
  }
  [[ ${__p} ]] || { nmg::_perr "failed to locate ${2-}"; return 2; }
  printf -v "$1" %s "${__p}"
}

nmg_is_running() {
  # <name> <pidfile> <no-remove-pid>
  local prog=${1-} pidfile=${2-} no_remove=${3-}

  [[ ${prog} ]] || return 1

  if ! [[ ${pidfile} ]]; then
    [[ $("${NMG_PGREP}" -x "${prog##*/}" || :) ]] && return 0
  elif [[ -f ${pidfile} ]] && [[ -r ${pidfile} ]]; then
    [[ $("${NMG_PGREP}" -F "${pidfile}" -x "${prog##*/}" || :) ]] && return 0
    [[ ${no_remove} ]] || nmg_remove "${pidfile}"
  fi

  return 1
}

# called from nmg::_query_ips
nmg::_query_ips_append() {
  [[ ${addr} && ${valid} && ${pref} ]] || return 0
  local val=${addr}
  if [[ ${flag} == p ]]; then
    [[ ${#flags[*]} != 0 ]] && val+=" ${flags[*]}"
  elif [[ ${flag} != a ]]; then
    val+=" ${valid} ${pref}"
  fi
  _nmgarr+=("${val}")
}

# called from nmg::_awrap
nmg::_query_ips() { # returns 0
  # [4|6][<flag>] [ <intf> [ <match> [ <ip-args>... ] ] ]
  local el eel pat epat='' state=text out addr='' valid='' pref=''
  local args=() flags=() flag=${1-} pat=${3-} vers=''

  [[ ${flag} =~ ^(4|6)?(a|p)?$ ]] || {
    nmg::_err "invalid <version><flag> '${flag}'"; _nmgrc=3; return 0; }
  [[ ${2-} ]] && args+=("dev" "$2")
  shift 3 || set --
  if [[ ${1+x} ]]; then
    [[ $1 ]] && args+=("$@")
  else
    args+=("scope" "global")
  fi
  [[ ${flag} =~ ^(4|6) ]] && { vers=${flag:0:1}; flag=${flag#?}; }
  if [[ ${pat} && ${pat} =~ : &&
          ${pat} =~ ^(\^?)([^\*\[\^\$\?]+)(\$?)$ ]]; then
    local pre=${BASH_REMATCH[1]} post=${BASH_REMATCH[3]}
    nmg::_mock epat ignore expand_ip6 "${BASH_REMATCH[2]}" &&
      epat="${pre}${epat}${post}"
  fi

  # ensure we have consistent order/error lang
  local -x LC_LANG=C
  # convert ignore->nolog, so we can catch failure
  nmg::saferun out "${_nmglog//ignore/nolog} retvar" \
               "$NMG_IP" ${vers:+"-${vers}"} addr show "${args[@]-}" || {
    _nmgrc=$?
    [[ ${out} =~ "does not exist" ]] && _nmgrc=2
    nmg::_errmode "${_nmglog}" "${out}"
    return 0
  }

  _nmgrc=0
  for el in $out; do
    case ${state} in
      text)
        case ${el} in
          inet|inet6)
            nmg::_query_ips_append
            addr='' valid='' pref='' flags=() state=${el}
            ;;
          valid_lft|preferred_lft)
            state=${el}
            ;;
          scope|brd|any|metric)
            state="kw:${el}"
            ;;
          permanent|dynamic|secondary|primary|tentative|deprecated|dadfailed|\
            temporary|home|mngtmpaddr|nodad|optimstic|noprefixroute|autojoin|\
            stable-privacy)
            flags+=("${el}")
            ;;
        esac
        ;;
      inet|inet6)
        if [[ ${pat} ]]; then
          if [[ ${el} =~ ${pat} ]]; then
            addr=${el}
          elif [[ ${state} == inet6 ]] &&
                 nmg::_mock eel nolog expand_ip6 "${el}"; then
            [[ ${eel} =~ ${pat} || ( ${epat} && ${eel} =~ ${epat} ) ]] &&
              addr=${el}
          fi
        elif [[ ${vers} == 4 ]]; then
          [[ ${state} == inet ]] && addr=${el}
        elif [[ ${vers} == 6 ]]; then
          [[ ${state} == inet6 ]] && addr=${el}
        else
          addr=${el}
        fi
        state=text
        ;;
      valid_lft|preferred_lft)
        flags+=("${state}:${el}")
        if [[ ${el} =~ ^([0-9]+sec|forever)$ ]]; then
          [[ ${state} == valid_lft ]] && valid=${el} || pref=${el}
        fi
        state=text
        ;;
      kw:*)
        flags+=("${state#kw:}:${el}")
        state=text
        ;;
    esac
  done

  nmg::_query_ips_append
  [[ ${#_nmgarr[*]} != 0 || -z ${pat} ]] || {
    _nmgrc=2; _nmglog+=" debug"; nmg::_err "none found"; }
  return 0
}

nmg::query_ips() {
  local _nmgrc=1; nmg::_awrap "$@"; return ${_nmgrc}
}

nmg::wait_dad6() {
  # <intf> <addr> [ <timeout> ]
  local -i i IFS; unset IFS
  nmg::_reqarg "${1-}" "intf" || return
  nmg::_reqarg "${2-}" "addr" || return
  local timeout addrs=() sleep="0.5"
  printf 2>/dev/null -v timeout %d "${3:-5}" || timeout=-1
  (( timeout >= 0 )) || { nmg::_perr "invalid <timeout> '$3'"; return 3; }
  (( timeout=timeout*2 )) || :

  nmg::query_ips addrs "" 6p "$1" "^$2" "" || :

  if [[ ${addrs[*]-} =~ (^| )tentative($| ) ]]; then
    for ((i=0; i<timeout; i++)); do
      [[ ${addrs[*]} =~ (^| )dadfailed($| ) ]] && break
      [[ ${i} == 0 ]] && nmg_debug "nmg::wait_dad6: waiting on DAD for $2"
      sleep 2>/dev/null "${sleep}" || {
        (( timeout=timeout/2 )) || :; sleep=1; sleep 1; }
      nmg::query_ips addrs "" 6p "$1" "^$2" "" || :
      [[ ${addrs[*]-} =~ (^| )tentative($| ) ]] || break
    done
  fi

  if [[ -z ${addrs[*]-} ]]; then
    nmg_debug "nmg::wait_dad6: address $2 removed"
    return 2
  elif [[ ${addrs[*]} =~ (^| )tentative($| ) ]]; then
    if [[ ${addrs[*]} =~ (^| )dadfailed($| ) ]]; then
      nmg_debug "nmg::wait_dad6: address $2 failed DAD"
      return 1
    else
      nmg_debug "nmg::wait_dad6: timeout waiting on DAD for $2"
      return 2
    fi
  fi
  return 0
}

# called from nmg::_awrap
nmg::__mod_ip() {
  # <add|del|change><4|6> <intf> <addr/plen> [ <ip-args>... ]
  local cmd=${1-} v intf=${2-} addr=${3-} cur_addr=() rc=0 arg
  shift 3 || set --

  [[ ${cmd} =~ ^(add|del|change)(4|6)$ ]] || {
    nmg::_err "unknown <cmd> '${cmd}'"; return 0; }

  [[ $intf ]] || { nmg::_err "missing <intf>"; return 0; }

  [[ $addr ]] || { nmg::_err "missing <addr/plen>"; return 0; }

  v=${cmd#"${cmd%?}"}; cmd=${cmd%?}

  # check if address looks legit
  "nmg_check_ip${v}_addr" "${addr%%/*}" || rc=$?
  [[ ${rc} == 1 ]] && { # rc 2/3 indicate link/private addrs, allowed
    nmg::_err "invalid address '${addr}'"; _nmgrc=1; return 0; }

  # add any missing plen
  if [[ ${addr} == "${addr%%/*}" ]]; then
    if [[ $v == 4 ]]; then addr+="/32"; else addr+="/128"; fi
  fi

  # check if addr already on interface
  nmg::query_ips cur_addr "" "${v}a" "${intf}" "^${addr}$" "" || :

  if [[ ${cmd} == add ]]; then
    if [[ ${cur_addr-} ]]; then
      cmd=replace
      nmg_info "Replacing ${addr} on ${intf}"
    else
      nmg_info "Adding ${addr} to ${intf}"
    fi
  elif [[ ${cmd} == change ]]; then
    if [[ ${cur_addr-} ]]; then
      # any ip-args to change?
      [[ $# == 0 ]] && { _nmgrc=0; return 0; }
      nmg_info "Changing ${addr} on ${intf}"
    else
      cmd=replace
      nmg_info "Adding ${addr} to ${intf}"
    fi
  else
    # if not present, quietly return 2
    [[ ${cur_addr-} ]] || { _nmgrc=2; return 0; }
    nmg_info "Removing ${addr} from ${intf}"
  fi

  _nmgarr+=("-${v}" "addr" "${cmd}" "${addr}")
  _nmgarr+=("dev" "${intf}")
  # handle both multi-word or multi-arg ip-args
  # shellcheck disable=SC2048
  for arg in $*; do _nmgarr+=("${arg}"); done
  _nmgrc=0
}

nmg::_mod_ip() {
  local _nmgargs=() _nmgret=${1-} _nmglog=${2-} _nmgvar=''
  local _nmgfunc=${_nmgfunc:-${FUNCNAME[1]}}
  shift 2 || set --
  nmg::_clearvar "${_nmgret}" "${_nmglog}" || { _nmgrc=$?; return 0; }
  # get ip args, convert ignore->nolog to catch errors
  nmg::_awrap _nmgargs "${_nmglog//ignore/nolog} retvar" "$@"
  if [[ ${_nmgrc} != 0 ]]; then
    nmg::_errmode "${_nmglog}" "${_nmgargs-}"
    [[ ${_nmgret} ]] && printf -v "${_nmgret}" %s "${_nmgvar}"
  else
    # now actually run the command
    nmg::run "${_nmgret}" "${_nmglog}" "$NMG_IP" "${_nmgargs[@]}" || _nmgrc=$?
  fi
  return 0
}

nmg::mod_ip() {
  local _nmgrc=3; nmg::_mod_ip "$@"; return ${_nmgrc}
}

nmg_add_ip4_addr() {
  # <intf> <addr/plen> [ <ip-args>... ]
  local _nmgrc=3; nmg::_mod_ip "" "" add4 "$@"; return ${_nmgrc}
}

nmg_change_ip4_addr() {
  # <intf> <addr/plen> <ip-args>...
  local _nmgrc=3; nmg::_mod_ip "" "" change4 "$@"; return ${_nmgrc}
}

nmg_del_ip4_addr() {
  # <intf> <addr/plen>
  local _nmgrc=3; nmg::_mod_ip "" "" del4 "$@"; return ${_nmgrc}
}

nmg_check_ip4_addr() {
  # <ip4-addr> [ <private-ok> ]
  local ip4=${1-} priv_ok=${2-}

  # check if have address
  [[ ${ip4} ]] || return 1

  nmg_debug "Checking IP4 address ${ip4}${priv_ok:+ (private ok)}"

  # check format
  [[ ${ip4} =~ ^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}$ ]] || return 1

  [[ ${ip4} =~ ^127\. ]] && return 11
  [[ ${ip4} =~ ^169.254\. ]] && return 12

  # if we allow private networks, allow all
  [[ ${priv_ok} ]] && {
    nmg_debug "IP4 private address ${ip4} allowed"; return 0; }

  # check if a private network address
  [[ ${ip4} =~ ^192\.168\. ]] && return 13
  [[ ${ip4} =~ ^10\. ]] && return 13
  if [[ ${ip4} =~ ^172\. ]]; then
    # isolate second byte to check 16-31 range
    local n=${ip4#172.}
    n=${n%%.*}
    (( n >= 16 && n <= 31 )) && return 13
  fi

  nmg_debug "IP4 address ${ip4} allowed"

  return 0
}

nmg_find_ip4_addrs() {
  # [ <intf> [ <match> [ <ip-args> ] ] ]
  local ips=() IFS; unset IFS
  # shellcheck disable=SC2086
  nmg::query_ips ips "" 4a "${1-}" "${2-}" ${3-} || return 0
  printf %s "${ips[*]-}" || :
  return 0
}

nmg::2dec() {
  # <retvar> <value>
  nmg::_clearvar "${1-}" nmgret || return
  printf 2>/dev/null -v "$1" %d "${2:-0}" || {
    nmg::_perr "invalid <value> '${2-}'"
    nmg::_clearvar "$1" ""; return 1; }
}

nmg_hex_to_dec() {
  # <hex number>
  local dec
  nmg::2dec dec "0x${1:-0}" && { printf %s "${dec}" || :; }
}

nmg::2hex() {
  # <retvar> <value>
  nmg::_clearvar "${1-}" nmgret || return
  printf 2>/dev/null -v "$1" %x "${2:-0}" || {
    nmg::_perr "invalid <value> '${2-}'"
    nmg::_clearvar "$1" ""; return 1; }
}

nmg_dec_to_hex() {
  # <decimal number>
  local hex
  nmg::2hex hex "${1-}" && { printf %s "${hex}" || :; }
}

nmg::transpose() {
  # <retvar> <string> <from> <to>
  nmg::_clearvar "${1-}" nmgret || return
  local __u=''
  shopt -q nocasematch && { shopt -u nocasematch; __u=1; }
  local __r=${2-} __m __p
  if [[ ${3-} && ${4-} && ${#3} == "${#4}" ]]; then
    while [[ ${__r} =~ ([${3-}]) ]]; do
      __m=${BASH_REMATCH[1]}; __p=${3%"${__m}"*}
      __r=${__r//"${__m}"/${4:${#__p}:1}}
    done
  fi
  [[ ${__u} ]] && shopt -s nocasematch
  printf -v "$1" %s "${__r}"
}

nmg::lowercase() {
  # <retvar> <string>
  if (( BASH_VERSINFO[0] >= 4 )); then
    nmg::_clearvar "${1-}" nmgret || return
    local __r=${2-}; printf -v "$1" %s "${__r,,}"
  else
    nmg::transpose "$1" "${2-}" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \
                   "abcdefghijklmnopqrstuvwxyz" || return
  fi
}

nmg::uppercase() {
  # <retvar> <string>
  if (( BASH_VERSINFO[0] >= 4 )); then
    nmg::_clearvar "${1-}" nmgret || return
    local __r=${2-}; printf -v "$1" %s "${__r^^}"
  else
    nmg::transpose "$1" "${2-}" "abcdefghijklmnopqrstuvwxyz" \
                   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" || return
  fi
}

nmg::array_join() {
  # <var> <sep> <vals>...
  nmg::_clearvar "${1-}" nmgret || return
  local _nmgret=$1 IFS=${2-}
  # handles ^? on empty items on some bash versions...
  shift 2 || return 0
  printf -v "${_nmgret}" %s "$*" || return
}

nmg::array_copy() {
  # <retarr> <srcarr>
  # shellcheck disable=SC2034
  local _i
  nmg::_cleararr "${1-}" nmgret || return
  nmg::_reqarg "${2-}" "srcarr" || return
  [[ $1 == "$2" ]] && {
    nmg::_perr "<retarr> and <srcarr> must differ"; return 3; }
  declare &>/dev/null -p "$2" || {
    nmg::_perr "<srcarr> '$2' is unset"; return 3; }
  # this copies both indexed and assoc arrays
  eval "for _i in \${$2[*]+\"\${!$2[@]}\"}; do
    $1[\"\${_i}\"]=\"\${$2[\"\${_i}\"]}\"
    done"
  return 0
}

nmg::array() {
  # <retarr> <sep> <sep-string>
  nmg::_cleararr "${1-}" nmgret || return
  [[ -z ${3-} ]] && return 0
  local IFS=${2-}
  if [[ ${2-} == $'\n' ]]; then
    { read -r -a "$1" -d '' || :; } <<< "$3"
  else
    { read -r -a "$1" || :; } <<< "$3"
  fi
  return 0
}

nmg::args_contains() {
  # <item> <arg1>...
  local item=${1-} var
  shift || return 0
  [[ -z ${item} && -z $* ]] && return 0
  for var in "$@"; do [[ ${item} == "${var}" ]] && return 0; done
  return 1
}

nmg::array_unique() {
  # <retarr> <name>
  local _nmgb=()
  nmg::_reqarg "${1-}" "retarr" || return
  nmg::_reqarg "${2-}" "name" || return
  if nmg::_varset "$2"; then
    local _nmgv="$2[@]"
    local _nmga=("${!_nmgv-}")
    for _nmgv in "${_nmga[@]}"; do
      nmg::args_contains "${_nmgv}" ${_nmgb[@]+"${_nmgb[@]}"} ||
        _nmgb+=("${_nmgv}")
    done
  fi
  nmg::_cleararr "$1" nmgret || return
  nmg::array_copy "$1" _nmgb || return
  return 0
}

nmg::array_match_values() {
  # <name_a> <name_b>
  nmg::_reqarg "${1-}" "name1" || return
  nmg::_reqarg "${2-}" "name2" || return
  nmg::_varset "$1" || {
    nmg::_varset "$2" && return 1
    # both unset
    return 0
  }
  nmg::_varset "$2" || return 1
  local _nmgx="$1[@]" _nmgy="$2[@]"
  # shellcheck disable=SC2034
  local aa=("${!_nmgx-}") ba=("${!_nmgy-}") av
  nmg::array_unique aa aa
  nmg::array_unique ba ba
  (( ${#aa[*]} == ${#ba[*]} )) || return
  (( ${#aa[*]} == 0 )) && return 0
  for av in "${aa[@]}"; do
    nmg::args_contains "${av}" "${ba[@]}" || return
  done
  return 0
}

nmg::list_match_values() {
  # <sep> <alist> <blist>
  # shellcheck disable=SC2034
  local a=() b=()
  nmg::_reqarg "${1-}" "sep" || return
  nmg::array a "$1" "${2-}"
  nmg::array b "$1" "${3-}"

  nmg::array_match_values a b || return
}

# called from nmg::_wrap
nmg::_expand_ip6() { # returns 0
  # <ip6/plen> [ <fmt> ]
  local ip6=${1-} plen='' fmt=${2:-%x}
  [[ ${ip6} =~ / ]] && {
    plen=${ip6#*/} ip6=${ip6%/*}
    { [[ ${plen} =~ ^[1-9][0-9]{0,2}$ ]] && (( plen > 0 && plen <= 128 )); } ||
      { nmg::_err "invalid ip6 format '${1-}'"; return 0; }
  }
  [[ ${ip6} =~ :: ]] && {
    [[ ${ip6} =~ ^:: ]] && ip6="0${ip6}"
    [[ ${ip6} =~ ::$ ]] && ip6="${ip6}0"
    local sub='::::::::' colons=${ip6//[^:]}
    sub=${sub/"${colons}"}
    [[ ${sub} ]] && ip6=${ip6/::/${sub//:/:0}:0}
  }
  [[ ${ip6} =~ ^(0?[0-9a-fA-F]{1,4}:){7}0?[0-9a-fA-F]{1,4}$ ]] ||
    { nmg::_err "invalid ip6 format '${1-}'"; return 0; }
  local IFS=' '
  # shellcheck disable=SC2059,SC2086
  printf -v ip6 "${fmt}:" 0x${ip6//:/ 0x}
  _nmgvar=${ip6%:}${plen:+/}${plen} _nmgrc=0
}

nmg::expand_ip6() {
  local _nmgrc=1; nmg::_wrap "$@"; return ${_nmgrc}
}

nmg::is_ip6_prefix() {
  local pfx
  [[ ${1-} && $1 =~ / ]] || return 1
  nmg::expand_ip6 pfx nolog "$1" || return
  [[ ${pfx} =~ 0/ ]] || return 1
}

# called from nmg::_wrap
nmg::_create_ip6_prefix() { # returns 0
  # <ip6-prefix/plen> <site> [ <site-len> ]
  local pfx=${1-} asite=${2-} aslen=${3-}
  local -i slen plen site

  # clear return value
  nmg::is_ip6_prefix "${pfx}" || {
    nmg::_err "invalid ip6-prefix '${pfx}'"; return 0; }
  plen=${pfx#*/}; pfx=${pfx%/*}

  [[ $asite ]] || { nmg::_err "missing <site>"; _nmgrc=3; return 0; }

  # hex2dec
  printf 2>/dev/null -v site %d "0x${asite//:/}" || {
    nmg::_err "invalid <site> '${asite}'"; return 0; }

  printf 2>/dev/null -v slen %d "${aslen:-64}" || {
    nmg::_err "invalid <site-len> '${aslen}'"; return 0; }
  (( slen == 0 )) && slen=64

  # use aslen on result
  aslen=${slen}
  (( slen > 64 )) && slen=64
  # make site_len >= prefix_len
  (( slen < plen )) && slen=${plen}

  local n quad pquad=() squad=() bits=64

  # init arrays
  for n in {0..3}; do pquad[${n}]=0; squad[${n}]=0; done

  # parse address into pquad array
  local IFS=':'; n=0
  for quad in ${pfx}; do
    printf 2>/dev/null -v quad %d "0x${quad:-0}" || {
      nmg::_err "invalid ip6-prefix '${pfx}'"; return 0; }
    (( pquad[n] = quad, pquad[n] &= 0xffff, n++ )) || :
    (( n > 3 )) && break
  done
  unset IFS

  while (( bits > slen )); do
    # clear network/site bits > slen
    (( n = (bits-1) / 16, pquad[n] &= ~(1 << (15 - (bits-1) % 16)),
       bits-- )) || :
  done
  while (( bits > plen )); do
    (( n = (bits-1) / 16 )) || :
    if (( site != 0 )); then
      # set site bits <= slen
      (( squad[n] |= (site % 2) << (15 - (bits-1) % 16), site /= 2 )) || :
    fi
    # clear network bits > plen
    (( pquad[n] &= ~(1 << (15 - (bits-1) % 16)), bits-- )) || :
  done

  for n in {0..3}; do (( pquad[n] |= squad[n] )) || :; done
  printf 2>/dev/null -v _nmgvar "%x:%x:%x:%x::/${aslen}" "${pquad[@]:0:4}" && {
    _nmgrc=0; return 0; }
  nmg::_err "failed to build prefix"
}

nmg::create_ip6_prefix() {
  local _nmgrc=1 _nmgret=${1-}
  shift || set --; nmg::_wrap "${_nmgret}" nmgret "$@"; return ${_nmgrc}
}

nmg_create_ip6_prefix() {
  local pfx
  nmg::create_ip6_prefix pfx "$@" && { printf %s "${pfx}" || :; }
}

nmg_check_ip6_addr() {
  # <ip6-addr> [ <private-ok> ]
  local ip6=${1-} priv_ok=${2-}

  # check if have address
  [[ ${ip6} ]] || return 1

  nmg_debug "Checking IP6 address $1${priv_ok:+ (private ok)}"

  [[ ${ip6} =~ ^::1$ ]] && return 11

  # check format
  nmg::_mock ip6 debug expand_ip6 "${ip6}" || return

  [[ ${ip6} =~ ^fe80 ]] && return 12

  # check if a rfc4193 local address
  if [[ ${ip6} =~ ^f(c|d) ]]; then
    [[ ${priv_ok} ]] || return 13
    nmg_debug "IP6 private address $1 allowed"
    return 0
  fi

  nmg_debug "IP6 address $1 allowed"

  return 0
}

nmg_find_ip6_addrs() {
  # [ <intf> [ <pattern> [ <ip-args> ] ] ]
  local ips=() IFS; unset IFS
  # shellcheck disable=SC2086
  nmg::query_ips ips "" 6a "${1-}" "${2-}" ${3-} || return 0
  printf %s "${ips[*]-}" || :
  return 0
}

nmg_add_ip6_addr() {
  # <intf> <addr/plen> [<ip-arg>...]
  local _nmgrc=3; nmg::_mod_ip "" "" add6 "$@"; return ${_nmgrc}
}

nmg_change_ip6_addr() {
  # <intf> <addr/plen> <ip-arg>...
  local _nmgrc=3; nmg::_mod_ip "" "" change6 "$@"; return ${_nmgrc}
}

nmg_del_ip6_addr() {
  # <intf> <addr/plen>
  local _nmgrc=3; nmg::_mod_ip "" "" del6 "$@"; return ${_nmgrc}
}

# called from nmg::_wrap
nmg::_create_ip6_host() { # returns 0
  # <intf> [ <node> ]
  local intf=${1-} node=${2-} addr link_addrs

  [[ ${intf} ]] || { nmg::_err "missing <intf>"; return 0; }

  if [[ -z ${node} || ${node} == auto ]]; then

    if nmg::query_ips link_addrs "nolog" 6a "${intf}" "" "scope" "link"; then

      for addr in ${link_addrs[*]+"${link_addrs[@]}"}; do
        addr=${addr#*::}; addr=${addr%/*}
        [[ ${addr} ]] && { node=${addr}; break; }
      done
    fi
  fi

  # if query failed in auto (interface down?), don't log
  [[ ${node} == auto ]] && { _nmgrc=1; return 0; }

  [[ ${node} ]] || {
    nmg::_err "Unable to determine an auto host-part for interface ${intf}"
    _nmgrc=1; return 0; }

  _nmgrc=0
  _nmgvar=${node}
}

nmg::create_ip6_host() {
  local _nmgrc=3 _nmgret=${1-}
  shift || set --; nmg::_wrap "${_nmgret}" nmgret "$@"; return ${_nmgrc}
}

nmg_create_ip6_host() {
  local host
  nmg::create_ip6_host host "$@" && { printf %s "${host}" || :; }
}

# called from nmg::_wrap
nmg::_create_ip6_addr() {
  # <prefix/plen> <host-part>
  local pfx=${1-} node=${2-} plen

  nmg::is_ip6_prefix "${pfx}" || {
    nmg::_err "missing <prefix/plen>"; return 0; }
  [[ ${node} ]] || { nmg::_err "missing <host-part>"; return 0; }

  # strip subnet from prefix
  plen=${pfx#*/}
  nmg::lowercase pfx "${pfx%/*}"
  nmg::lowercase node "${node}"

  if [[ ${pfx} =~ ^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+::$ ]] \
       && [[ ${node} =~ ^[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+$ ]]; then
    # both prefix and node are 4-quads, remove the ::
    _nmgvar="${pfx%::}:${node}/${plen}"
  else
    _nmgvar="${pfx}${node}/${plen}"
  fi
  _nmgrc=0
}

nmg::create_ip6_addr() {
  local _nmgrc=3 _nmgret=${1-}
  shift || set --; nmg::_wrap "${_nmgret}" nmgret "$@"; return ${_nmgrc}
}

nmg_create_ip6_addr() {
  local addr
  nmg::create_ip6_addr addr "$@" && { printf %s "${addr%/*}" || :; }
}

nmg_radvd_trigger() {

  if [[ ${NMG_RADVD_TRIGGER-} ]] && nmg_need_progs "${NMG_RADVD_TRIGGER}"; then
    local IFS; unset IFS
    # shellcheck disable=SC2086
    nmg_cmd "${NMG_RADVD_TRIGGER}" ${NMG_RADVD_TRIGGER_ARGS-}
    return 0
  fi

  # signal radvd to advertise new prefix
  if nmg_is_running "${NMG_RADVD}" "${NMG_RADVD_PID}" 1; then
    nmg_debug "signaling ${NMG_RADVD}"
    local pid
    nmg::read pid "ignore" "${NMG_RADVD_PID}"
    pid=${pid%$'\n'} # strip newline
    [[ ${pid} ]] && { nmg_cmd kill -HUP "${pid}" || :; }
  fi
  return 0
}

nmg::prop_get_value() {
  # <retvar> <props> <name>
  local _nmgprop
  nmg::_clearvar "${1-}" "" || return
  [[ ${2-} && ${3-} ]] || return 1
  local IFS=$'\n'
  while read -r _nmgprop; do
    [[ ${_nmgprop} =~ ^${3}: ]] || continue
    [[ $1 ]] && printf -v "$1" %s "${_nmgprop#*:}"
    return 0
  done <<< "$2"
  return 1
}

nmg::prop_set_value() {
  # <retvar> <props> <name> [ <value> ]
  local _nmgprop _nmgnew=() _nmgfound=''
  nmg::_clearvar "${1-}" "" || return
  [[ ${3-} ]] || return 1
  local IFS=$'\n'
  while read -r _nmgprop; do
    [[ ${_nmgprop} ]] || continue
    if [[ ${_nmgprop} =~ ^${3}: ]]; then
      _nmgfound=1
      [[ ${4+x} ]] && _nmgnew+=("${3}:$4")
    else
      _nmgnew+=("${_nmgprop}")
    fi
  done <<< "$2"
  [[ -z ${_nmgfound} && ${4+x} ]] && _nmgnew+=("${3}:$4")
  printf -v "$1" '%s\n' "${_nmgnew[*]-}"
}

nmg::prop_has_value() {
  # <props> <name> <value> [ <sep> ]
  local fval word IFS; unset IFS
  nmg::prop_get_value fval "${1-}" "${2-}" || return 1
  if [[ ${4-} ]]; then
    for word in ${fval//"$4"/ }; do
      [[ ${word} == "${3-}" ]] && return 0
    done
  else
    [[ ${fval} == "${3-}" ]] && return 0
  fi
  return 1
}

nmg::prop_has_ivalue() {
  # <props> <name> <value> [ <sep> ]
  local u='' rc=0
  shopt -q nocasematch || { shopt -s nocasematch; u=1; }
  nmg::prop_has_value "$@" || rc=$?
  [[ ${u} ]] && shopt -u nocasematch
  return ${rc}
}

nmg::prop_match_values() {
  # <props> <name> <sep> <list>
  # shellcheck disable=SC2034
  local val varr=() larr=() new=()

  nmg::_reqarg "${3-}" "sep" || return
  nmg::prop_get_value val "$1" "$2" || :

  nmg::array varr "$3" "${val}"
  nmg::array larr "$3" "${4-}"

  nmg::array_match_values varr larr || return
}

# private
nmg::_loaded() {

  if [[ ${NMG_REQUIRED-} ]] &&
       ! nmg::require_version "${NMG_VERSION}" "${NMG_REQUIRED}"; then
    nmg_err "${BASH_SOURCE[0]}: NMG_VERSION=${NMG_VERSION} < NMG_REQUIRED=${NMG_REQUIRED}"
    return 1
  fi
  # test required programs
  nmg_need_progs "${NMG_PGREP}" "${NMG_RM}" "${NMG_IP}" || return
}

# last, so load fails if any missing components
nmg::_loaded
