#!/bin/bash
# -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*-
# vim:set ft=sh et sw=2 ts=2:
#
# nmcli-mock - nmcli command wrapper/replacement for testing
# Author: Scott Shambarger <devel@shambarger.net>
#
# Copyright (C) 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/>.
#
# USAGE
#
# Values returned are from the datafile defined in NMCLI_MOCK_VALUES
# (default "conf/nmcli-mock-values").  See below for format of file.
#
# Individual values may be overridden by setting:
#   NMCLI_MOCK_FORCE_<name>=[ <value> ] (<name> should have '.','-' as '_')
# Primary keys not force-able (GENERAL.DEVICE, connection.uuid, connection.id)
#
# All output formats of original nmcli command are supported, including
# terse, tabular, and multiline
#
# Filtering fields is fully supported.
#
# Currently supports the following modes:
#
#   nmcli
#   nmcli general
#   nmcli dev status
#   nmcli dev show [ <DEVICE> ]
#   nmcli con show [ <CONNECTION> ]
#
VERSION=0.3.1

# app in not case sensitive
shopt -s nocasematch

# MOCK_VALUES file has the following format:
# -CONN-STATUS
# `nmcli -t -f all -m m con show`
# -CONN-ITEMS
# `nmcli -t -m m con show $(nmcli -t -f name con show)`
# -DEV-STATUS
# `nmcli -t -f all -m m dev status`
# -DEV-ITEMS
# `nmcli -t -f all -m m dev show`
# -GEN-STATUS
# `nmcli -t -f all -m m general`
NMCLI_MOCK_VALUES=${NMCLI_MOCK_VALUES:-conf/nmcli-mock-values}

# only show these fields
FIELDS=''
# set for terse
TERSE=''
# may be tabular or multiline
MODE=''
# empty to non-escape
ESCAPE=1

ORIG_ARGS=("$@")

err() {
  local IFS; unset IFS
  printf >&2 "%s\n" "$*"
}

fail() {
  local -i rc=0
  [[ $1 != 0 ]] && { printf 2>/dev/null -v rc "%d" "$1" || rc=1; }
  shift
  [[ $1 ]] && { if [[ $rc == 0 ]]; then echo "$@"; else err "$@"; fi; }
  exit "$rc"
}

usage40() {
  printf >&2 "  %-40s %s\n" "$1" "$2"
}

usage() {
  err "Usage: nmcli [OPTIONS] OBJECT { COMMAND | help }"
  err
  err "OPTIONS"
  usage40 "-e, --escape yes|no" "escape columns separators in values"
  usage40 "-f, --fields <field,....>|all|common" "specify fields to output"
  usage40 "-g, --get-values <field,....>|all|common" "shoftcut for -m tabular -t -f"
  usage40 "-h, --help" "print this help"
  usage40 "-m, --mode tabular|multiline" "output mode"
  usage40 "-t, --terse" "terse output"
  usage40 "-v, --version" "show program version"
  err
  err "OBJECT"
  err "g[eneral]       NetworkManager's general status and operations"
  err "c[connection]   NetworkManager's connections"
  err "d[evice]        devices managed by NetworkManager"
  err
  exit 0
}

fail_arg() {
  # arg
  fail 2 "Error: argument '$1' not understood. Try passing --help instead."
}

pre_match() {
  # <arg> <full-word>
  local i
  [[ $1 ]] || return 1
  (( ${#1} > ${#2} )) && return 1
  for (( i=0; i<${#1}; i++)); do
    [[ ${1:$i:1} == "${2:$i:1}" ]] || return 1
  done
  return 0
}

mock_cmd() {
  # echo commands if requested
  args='' arg=''
  for arg in "${ORIG_ARGS[@]}"; do args+=" '${arg}'"; done
  [[ ${MOCK_ECHO} ]] && echo "nmcli${args}"
}

CONN_STATUS=()
CONNS=()
DEV_STATUS=()
DEVS=()
GENERAL=()

mode_assign() {
  # <mode> <details>
  [[ $1 && $2 ]] || return 0
  case $1 in
    CONN-STATUS) CONN_STATUS+=("$2") ;;
    CONN-ITEMS) CONNS+=("$2") ;;
    DEV-STATUS) DEV_STATUS+=("$2") ;;
    DEV-ITEMS) DEVS+=("$2") ;;
    GEN-STATUS) GENERAL+=("$2") ;;
  esac
  return 0
}

load_values() {
  # <filename>
  local file=$1 mode='' sep=''
  [[ -f ${file} ]] || { fail 1 "$1 file $file not found"; }
  local line details=''
  while read -r line || [[ ${line} ]]; do
    [[ -z ${line} || ${line} =~ ^# ]] && continue
    if [[ ${line} =~ ^- ]]; then
      mode_assign "${mode}" "${details}"
      details=''
      mode=${line#-}
      case ${mode} in
        CONN-STATUS) sep="NAME" ;;
        CONN-ITEMS) sep="connection.id" ;;
        DEV-STATUS) sep="DEVICE" ;;
        DEV-ITEMS) sep="GENERAL.DEVICE" ;;
        GEN-STATUS) sep="RUNNING" ;;
        *) mode='' sep='' ;;
      esac
    else
      if [[ ${line%%:*} == "${sep}" ]]; then
        mode_assign "${mode}" "${details}"
        details=''
      fi
      details+="${line}"$'\n'
    fi
  done < "${file}"
  mode_assign "${mode}" "${details}"
  return 0
}

check_force_value() { # sets "v" if override
  # <name>
  # don't allow force on primary keys
  case $1 in
    GENERAL.DEVICE|connection.uuid|connection.id) return 0 ;;
  esac
  # map ./- to _
  local name="NMCLI_MOCK_FORCE_${1//./_}"
  name=${name//-/_}
  [[ ${!name+set} ]] && printf -v v '%s' "${!name}"
  return 0
}

TABLE_BREAK=''
print_values() {
  # <names> <values>...
  local names=$1; shift
  [[ ${names} ]] || return 0
  local i j v anames=() avals=() asize=() fmt='' name
  { read -r -a anames -d '' || :; } <<< "${names}"
  if [[ ${MODE} != tabular ]]; then
    fmt="%-40s%s"
    [[ ${TERSE} ]] && fmt="%s%s"
    [[ ${TABLE_BREAK} ]] && echo; TABLE_BREAK=''
    for values in "$@"; do
      i=0
      while read -r v; do
        check_force_value "${anames[$i]}"
        [[ $v || ${TERSE} ]] || v="--"
        # shellcheck disable=SC2059
        printf "${fmt}\n" "${anames[$i]}:" "$v"
        ((i++)) || :
      done <<< "${values}"
    done
    return 0
  fi
  if [[ ${TERSE} ]]; then
    for values in "$@"; do
      i=0
      while read -r v; do
        check_force_value "${anames[$i]}"
        [[ ${ESCAPE} ]] && v=${v//:/\\:}
        fmt+="${fmt:+:}%s" avals+=("$v")
        ((i++)) || :
      done <<< "${values}"
      # shellcheck disable=SC2059
      printf "${fmt}\n" "${avals[@]}"
    done
    return 0
  fi
  [[ ${TABLE_BREAK} ]] && echo; TABLE_BREAK=1
  for ((i=0; i<${#anames[*]}; i++)); do
    name=${anames[$i]#*.}
    anames[$i]=${name} asize[$i]=${#name}
  done
  for values in "$@"; do
    avals=() i=0
    while read -r v; do
      check_force_value "${anames[$i]}"
      v=${v:---}
      avals+=("$v")
      if (( ${#asize[*]} > i )); then
        (( ${#v} > ${asize[$i]} )) && asize[$i]=${#v}
      else
        asize[$i]=${#v}
      fi
      ((i++)) || :
    done <<< "${values}"
  done
  for ((j=0; j<i; j++)); do fmt+="${fmt:+  }%-${asize[$j]}s"; done
  # shellcheck disable=SC2059
  [[ ${#anames[*]} != 0 ]] && printf "${fmt}\n" "${anames[@]}"
  for values in "$@"; do
    avals=() i=0
    while read -r v; do
      check_force_value "${anames[$i]}"
      avals+=("${v:---}")
      ((i++)) || :
    done <<< "${values}"
    # shellcheck disable=SC2059
    printf "${fmt}\n" "${avals[@]}"
  done
}

print_section_values() {
  # <match> <array-name>...
  local match=$1 src="$2[@]" entry label value field output=''
  local rc=1 IFS=$'\n'

  shift 2
  for entry in "${!src}"; do
    local table='' names='' values='' cur_label='' found=''
    # shellcheck disable=SC2086
    set -- ${entry}
    while [[ ${1-} ]]; do
      label=${1%%:*}
      value=${1:${#label}:${#1}}; value=${value#:}
      if [[ ${label%%.*} != "${table}" ]]; then
        print_values "${names}" "${values}"
        table='' names='' values=''
      fi
      # shellcheck disable=SC2053
      if [[ ${label} == ${match%%:*} ]]; then
        # shellcheck disable=SC2053
        if [[ -z ${match#*:} || $1 == ${match} ]]; then
          (( rc == 0 )) && TABLE_BREAK=1
          found=1; rc=0
        fi
      fi
      if [[ ${found} ]]; then
        if [[ ${FIELDS} ]]; then
          for field in ${FIELDS//,/$'\n'}; do
            # special internal negative-match
            if [[ ${field} =~ ^- ]]; then
              [[ ${field} == "-${label%.*}" ]] && { output=''; break; }
              output=1; continue
            elif [[ ${field} == "${label}" ||
                    ${match%%.*}.${field} == "${label}" ]]; then
              table=invalid output=1
            elif [[ ${field%.*} == "${field}" ]] &&
                   pre_match "${field}." "${label}"; then
              output=1
            fi
            [[ ${output} ]] && break
          done
        else
          output=1
        fi
        if [[ ${output} ]]; then
          output=''
          if [[ -z ${table} ]]; then
            table=${label%%.*}
            [[ ${MODE} == tabular ]] && {
              # add group names
              names="name"
              [[ GENERAL:IP4:IP6:DHCP4:DHCP6 =~ ${table} ]] && names="GROUP"
              [[ ${match%%:*} == "GENERAL.DEVICE" &&
                   GENERAL:CAPABILITIES:INTERFACE-FLAGS:WIRED-PROPERTIES:CONNECTIONS =~ ${table} ]] &&
                names="NAME"
              values="${table}"
            }
          fi
          [[ ${MODE} == tabular ]] && label=${label%%\[*}
          if [[ ${label} != "${cur_label}" ]]; then
            [[ ${names} ]] && { names+=$'\n' values+=$'\n'; }
            names+="${label}" cur_label=${label}
          else
            values+=" | "
          fi
          values+=${value}
        fi
      fi
      shift
    done
    print_values "${names}" "${values}"
  done
  return ${rc}
}

print_field_values() {
  # <array-name> <fields>
  local src="$1[@]"
  local i field output match label value entry items anames avals

  shift
  local afields=("$@") names='' avalues=() IFS=$'\n'

  load_values "${NMCLI_MOCK_VALUES}"

  for entry in "${!src}"; do
    # shellcheck disable=SC2086
    set -- ${entry}
    anames=() avals=()
    while [[ ${1-} ]]; do
      match='' label=${1%%:*}
      value=${1:${#label}:${#1}}; value=${value#:}
      for field in "${afields[@]-}"; do
        if [[ -z ${field} || ${label} == "${field}" ]]; then
          if [[ -z ${TERSE} ]]; then
            # friendly-names
            if [[ ${label} == TYPE ]]; then
              [[ ${value} == 802-3-ethernet ]] && value=ethernet
            fi
          fi
          anames+=("${label}")
          avals+=("${value}")
          break
        fi
      done
      shift
    done
    names='' items=''
    if [[ ${afields[*]-} ]]; then
      for field in "${afields[@]-}"; do
        for ((i=0; i<${#anames[*]}; i++)); do
          [[ ${field} == "${anames[$i]}" ]] && break
        done
        (( i < ${#anames[*]} )) && value=${avals[$i]} || value=''
        names+="${names:+$'\n'}${field}"
        items+="${items:+$'\n'}${value}"
      done
    else
      for ((i=0; i<${#anames[*]}; i++)); do
        names+="${names:+$'\n'}${anames[$i]}"
        items+="${items:+$'\n'}${avals[$i]}"
      done
    fi
    if [[ ${MODE} == tabular && -z ${TERSE} ]]; then
      avalues+=("${items}")
    else
      print_values "${names}" "${items}"
      names=''
    fi
  done
  print_values "${names}" "${avalues[@]-}"
}

set_fields() { # sets fields
  # <allowed-,> [ <prefix> ]
  local field match found
  for field in ${FIELDS//,/ }; do
    [[ ${field} == all || ${field} == common ]] &&
      fail 2 "Error: ${2-}field '${field}' has to be alone"
    found=''
    for match in ${1//,/ }; do
      [[ ${field} == "${match}" || ${field%.*} == "${match}" ]] && {
        found=1; break
      }
    done
    [[ ${found} ]] ||
      fail 2 "Error: ${2-}invalid field '${field}'; allowed fields: $1."
    fields+=("${match}")
  done
  return 0
}

GENERAL_FIELDS="RUNNING,VERSION,STATE,STARTUP,CONNECTIVITY,NETWORKING,WIFI-HW,WIFI,WWAN-HW,WWAN"

do_gen_status() {
  MODE=${MODE:-tabular}
  FIELDS=${FIELDS:-STATE,CONNECTIVITY,WIFI-HW,WIFI,WWAN-HW,WWAN}
  [[ ${FIELDS} == all ]] && FIELDS=''
  set_fields "${GENERAL_FIELDS}"
  print_field_values "GENERAL" "${fields[@]}"
  return 0
}

do_general() {
  local c cmd req=${1:-status}

  if pre_match "${req}" "status"; then
    do_gen_status
    return 0
  fi

  for cmd in hostname permissions logging reload; do
    if pre_match "${req}" "${cmd}"; then
      mock_cmd
      return 0
    fi
  done

  fail_arg "${req}"
}

CONN_STATUS_FIELDS="NAME,UUID,TYPE,TIMESTAMP,TIMESTAMP-REAL,AUTOCONNECT,AUTOCONNECT-PRIORITY,READONLY,DBUS-PATH,ACTIVE,DEVICE,STATE,ACTIVE-PATH,SLAVE,FILENAME"

do_conn_status() {
  local fields=()
  MODE=${MODE:-tabular}
  [[ ${FIELDS} == common || -z ${FIELDS} ]] &&
    FIELDS="NAME,UUID,TYPE,DEVICE"
  [[ ${FIELDS} == all ]] && FIELDS=''
  set_fields "${CONN_STATUS_FIELDS}"
  print_field_values "CONN_STATUS" "${fields[@]}"
}

CONN_SHOW_FIELDS="6lowpan,802-11-olpc-mesh,802-11-wireless,802-11-wireless-security,802-1x,802-3-ethernet,adsl,bluetooth,bond,bridge,bridge-port,cdma,connection,dcb,dummy,ethtool,generic,gsm,hostname,infiniband,ip-tunnel,ipv4,ipv6,macsec,macvlan,match,ovs-bridge,ovs-dpdk,ovs-external-ids,ovs-interface,ovs-patch,ovs-port,ppp,pppoe,proxy,serial,sriov,tc,team,team-port,tun,user,veth,vlan,vpn,vrf,vxlan,wifi-p2p,wimax,wireguard,wpan,GENERAL,IP4,DHCP4,IP6,DHCP6,VPN"

# sets <retvar> to conn.id for <uuid>
find_conn_id() {
  # <retvar> <uuid>
  local retvar=$1 uuid=$2 entry label value id

  for entry in "${CONNS[@]}"; do
    id=''
    # shellcheck disable=SC2086
    set -- ${entry}
    while [[ ${1-} ]]; do
      label=${1%%:*}
      value=${1:${#label}:${#1}}; value=${value#:}
      case ${label} in
        connection.id) id=${value} ;;
        connection.uuid)
          [[ ${value} == "${uuid}" ]] && {
            printf -v "${retvar}" '%s' "${id:--}"
            return 0
          } ;;
      esac
      shift
    done
  done
  printf -v "${retvar}" '-'
}

do_conn_show() {
  # <conn_id>
  local fields=() conn=$1
  MODE=${MODE:-multiline}
  if [[ ${FIELDS} == all || ${FIELDS} == common ]]; then
    FIELDS="-GENERAL,-IP4,-DHCP4,-IP6,-DHCP6,-VPN"
  else
    set_fields "${CONN_SHOW_FIELDS}"
  fi
  [[ ${#conn} == 36 ]] && find_conn_id conn "$conn"
  print_section_values "connection.id:${conn}" "CONNS" ||
    fail 10 "Error: $1 - no such conection profile."
}

do_connection() {
  local c cmd req=${1:-show}

  if pre_match "${req}" "show"; then
    if [[ ${2-} ]]; then
      shift
      load_values "${NMCLI_MOCK_VALUES}"
      for c in "$@"; do do_conn_show "$c"; TABLE_BREAK=1; done
    else
      do_conn_status
    fi
    return 0
  fi

  for cmd in up down modify add edit clone delete monitor reload \
                load import export; do
    if pre_match "${req}" "${cmd}"; then
      mock_cmd
      return 0
    fi
  done

  fail_arg "${req}"
}

DEV_STATUS_FIELDS="DEVICE,TYPE,STATE,IP4-CONNECTIVITY,IP6-CONNECTIVITY,DBUS-PATH,CONNECTION,CON-UUID,CON-PATH"

do_dev_status() {
  local fields=()
  [[ $1 ]] && fail 2 "Error: invalid extra argument '$1'."
  MODE=${MODE:-tabular}
  [[ ${FIELDS} == common || -z ${FIELDS} ]] &&
    FIELDS="DEVICE,TYPE,STATE,CONNECTION"
  [[ ${FIELDS} == all ]] && FIELDS=''
  set_fields "${DEV_STATUS_FIELDS}" "'device status': "
  print_field_values "DEV_STATUS" "${fields[@]}"
}

DEV_SHOW_FIELDS="GENERAL,CAPABILITIES,INTERFACE-FLAGS,WIFI-PROPERTIES,AP,WIRED-PROPERTIES,WIMAX-PROPERTIES,NSP,IP4,DHCP4,IP6,DHCP6,BOND,TEAM,BRIDGE,VLAN,BLUETOOTH,CONNECTIONS"

do_dev_show() {
  local fields=()
  MODE=${MODE:-multiline}
  [[ ${FIELDS} == common || -z ${FIELDS} ]] &&
    FIELDS="GENERAL.DEVICE,GENERAL.TYPE,GENERAL.HWADDR,GENERAL.MTU,GENERAL.STATE,GENERAL.CONNECTION,GENERAL.CON-PATH,WIRED-PROPERTIES,IP4,IP6"
  [[ ${FIELDS} == all ]] && FIELDS=''

  set_fields "${DEV_SHOW_FIELDS}" "'device show': "
  print_section_values "GENERAL.DEVICE:$1" "DEVS" ||
    fail 10 "Error: Device '$1' not found."
}

do_device() {
  local d cmd req=${1:-status}

  shift || :
  if pre_match "${req}" "status"; then
    do_dev_status "$@"
  elif pre_match "${req}" "show"; then
    load_values "${NMCLI_MOCK_VALUES}"
    if [[ ${1-} ]]; then
      for d in "$@"; do do_dev_show "$d"; TABLE_BREAK=1; done
    else
      do_dev_show
    fi
    return 0
  fi

  for cmd in status show set connect reapply modify disconnect delete \
                monitor wifi lldp; do
    if pre_match "${req}" "${cmd}"; then
      mock_cmd
      return 0
    fi
  done

  fail_arg "${req}"
}

do_summary() {
  local entry label v field version dns='' IFS=$'\n'

  load_values "${NMCLI_MOCK_VALUES}"

  for entry in "${DEVS[@]}"; do
    # shellcheck disable=SC2086
    set -- ${entry}
    local dev='' conn='' vendor='' prod='' inet4=() route4=()
    local inet6=() route6=() type='' driver='unknown' mac=''
    local soft='hw' mtu='1500' def4='' def6=''

    while [[ ${1-} ]]; do
      label=${1%%:*}
      v=${1:${#label}:${#1}}; v=${v#:}
      check_force_value "${label}"
      case ${label} in
        GENERAL.DEVICE) dev=$v ;;
        GENERAL.CONNECTION) conn=$v ;;
        GENERAL.VENDOR) vendor=${v%% *} ;;
        GENERAL.PRODUCT) prod=${v%% *} ;;
        GENERAL.TYPE) type=$v ;;
        GENERAL.DRIVER) driver=$v ;;
        GENERAL.HWADDR) [[ ${v} ]] && mac=", $v" ;;
        GENERAL.IS-SOFTWARE) [[ $v == yes ]] && soft='sw' ;;
        GENERAL.MTU) mtu=$v ;;
        GENERAL.DEFAULT) [[ $v == yes ]] && def4="ip4 default" ;;
        GENERAL.DEFAULT6) [[ $v == yes ]] && def6="ip6 default" ;;
        IP4.ADDRESS*) [[ $v ]] && inet4+=("$v") ;;
        IP4.ROUTE*) [[ $v =~ "dst = " ]] && {
                      v=${v#*dst = }; route4+=("${v%%,*}"); } ;;
        IP6.ADDRESS*) [[ $v ]] && inet6+=("$v") ;;
        IP6.ROUTE*) [[ $v =~ "dst = " ]] && {
                      v=${v#*dst = }; route6+=("${v%%,*}"); } ;;
      esac
      shift
    done

    # now display the device summary
    [[ ${dev} ]] || continue
    printf '%s' "${dev}: "
    if [[ ${conn} ]]; then
      printf 'connected to %s\n' "${conn}"
    else
      printf 'unmanaged\n'
    fi
    if [[ ${vendor} || ${prod} ]]; then
      printf '\t"%s%s"\n' "${vendor}" "${vendor:+ }${prod}"
    else
      printf '\t"%s"\n' "${dev}"
    fi
    if [[ ${type} == "${driver}" ]]; then
      printf "\t%s" "${type}"
    else
      printf "\t%s (%s)" "${type}" "${driver}"
    fi
    printf "%s, %s, mtu %s\n" "${mac}" "${soft}" "${mtu}"
    if [[ ${conn} ]]; then
      [[ ${def4} || ${def6} ]] && printf '\t%s\n' "${def4}${def4:+, }${def6}"
      for v in ${inet4[*]+"${inet4[@]}"}; do
        printf "\tinet4 %s\n" "$v"
      done
      for v in ${route4[*]+"${route4[@]}"}; do
        printf "\troute4 %s\n" "$v"
      done
      for v in ${inet6[*]+"${inet6[@]}"}; do
        printf "\tinet6 %s\n" "$v"
      done
      for v in ${route6[*]+"${route6[@]}"}; do
        printf "\troute6 %s\n" "$v"
      done
    fi
    printf '\n'
  done

  for version in 4 6; do
    for entry in "${CONNS[@]}"; do
      # shellcheck disable=SC2086
      set -- ${entry}
      local dev='' servers='' dom=''
      while [[ ${1-} ]]; do
        label=${1%%:*}
        v=${1:${#label}:${#1}}; v=${v#:}
        check_force_value "${label}"
        case ${label} in
          connection.interface-name) dev=$v ;;
          "ipv${version}.dns") [[ $v ]] && servers+=" $v" ;;
          "ipv${version}.dns-search") [[ $v ]] && dom+=" $v" ;;
        esac
        shift
      done
      [[ ${dev} && ${servers} ]] || continue
      [[ ${dns} ]] || { dns=1; printf 'DNS configuration\n'; }
      printf '\tservers:%s\n' "${servers}"
      [[ ${dom} ]] && printf '\tdomains:%s\n' "${dom}"
      printf '\tinterface: %s\n' "${dev}"
      printf '\n'
    done
  done

  printf '%s\n' "Use \"nmcli device show\" to get complete information about known devices and"
  printf '%s\n' "\"nmcli connection show\" to get an overview on active connection profiles."
  printf '\n%s\n' "Consult nmcli(1) and nmcli-examples(7) manual pages for complete usage details."
}

[[ ${NMCLI_MOCK_FAIL-} || ${NMCLI_MOCK_OUTPUT-} ]] && {
  args='' arg=''; for arg in "$@"; do args+="${args:+ }'${arg}'"; done
  fail "${NMCLI_MOCK_FAIL:-0}" \
       "${NMCLI_MOCK_OUTPUT+${NMCLI_MOCK_OUTPUT/@ARGS@/${args}}}"
}

while [[ ${1-} =~ ^- ]]; do
  OPTERR='' OPTARG=''
  OPT=${1#-}; OPT=${OPT#-}
  if pre_match "$OPT" "escape"; then
    case ${2-} in
      yes) ESCAPE=1 ;;
      no) ESCAPE='' ;;
      *) OPTERR=$1 OPTARG=${2-} ;;
    esac
    shift
  elif pre_match "$OPT" "fields"; then
    [[ ${2-} ]] && FIELDS=$2 || OPTERR=$1
    shift
  elif pre_match "$OPT" "get-values"; then
    [[ ${2-} ]] && FIELDS=$2 || OPTERR=$1
    MODE=tabular TERSE=1
    shift
  elif pre_match "$OPT" "help"; then
    usage
  elif pre_match "$OPT" "mode"; then
    if pre_match "${2-}" "tabular"; then
      MODE=tabular
    elif pre_match "${2-}" "multiline"; then
      MODE=multiline
    else
      OPTERR=$1 OPTARG=${2-}
    fi
    shift
  elif pre_match "$OPT" "terse"; then
    TERSE=1
  elif pre_match "$OPT" "version"; then
    echo "nmcli-mock tool, version $VERSION"
    exit 0
  else
    fail 2 "Error: Option '$1' is unknown, try 'nmcli -help'."
  fi
  [[ ${OPTARG} ]] &&
    fail 2 "Error: '$OPTARG' is not a valid argument for '$OPTERR' option."
  [[ ${OPTERR} ]] && fail 2 "Error: missing argument for '$OPTERR' option."
  shift
done

OBJECT=${1-}
shift || :

if pre_match "$OBJECT" "help"; then
  usage
elif pre_match "$OBJECT" "general"; then
  do_general "$@"
elif pre_match "$OBJECT" "connection"; then
  do_connection "$@"
elif pre_match "$OBJECT" "device"; then
  do_device "$@"
elif [[ $OBJECT ]]; then
  fail_arg "${OBJECT}"
else
  do_summary
fi

exit 0
