docs: def build_rule(table='filter', chain=None, command=None, position='', full=None, family='ipv4',
                 **kwargs):
      '''
      Build a well-formatted iptables rule based on kwargs. A `table` and `chain`
      are not required, unless `full` is True.
  
      If `full` is `True`, then `table`, `chain` and `command` are required.
      `command` may be specified as either a short option ('I') or a long option
      (`--insert`). This will return the iptables command, exactly as it would
      be used from the command line.
  
      If a position is required (as with `-I` or `-D`), it may be specified as
      `position`. This will only be useful if `full` is True.
  
      If `state` is passed, it will be ignored, use `connstate`.
      If `connstate` is passed in, it will automatically be changed to `state`.
  
      To pass in jump options that doesn't take arguments, pass in an empty
      string.
  
      .. note::
  
          Whereas iptables will accept ``-p``, ``--proto[c[o[l]]]`` as synonyms
          of ``--protocol``, if ``--proto`` appears in an iptables command after
          the appearance of ``-m policy``, it is interpreted as the ``--proto``
          option of the policy extension (see the iptables-extensions(8) man
          page).
  
      CLI Examples:
  
      .. code-block:: bash
  
          salt '*' iptables.build_rule match=state \\
              connstate=RELATED,ESTABLISHED jump=ACCEPT
  
          salt '*' iptables.build_rule filter INPUT command=I position=3 \\
              full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT
  
          salt '*' iptables.build_rule filter INPUT command=A \\
              full=True match=state connstate=RELATED,ESTABLISHED \\
              source='127.0.0.1' jump=ACCEPT
  
          .. Invert Rules
          salt '*' iptables.build_rule filter INPUT command=A \\
              full=True match=state connstate=RELATED,ESTABLISHED \\
              source='!127.0.0.1' jump=ACCEPT
  
          salt '*' iptables.build_rule filter INPUT command=A \\
              full=True match=state connstate=RELATED,ESTABLISHED \\
              destination='not 127.0.0.1' jump=ACCEPT
  
          IPv6:
          salt '*' iptables.build_rule match=state \\
              connstate=RELATED,ESTABLISHED jump=ACCEPT \\
              family=ipv6
          salt '*' iptables.build_rule filter INPUT command=I position=3 \\
              full=True match=state connstate=RELATED,ESTABLISHED jump=ACCEPT \\
              family=ipv6
      '''
      if 'target' in kwargs:
          kwargs['jump'] = kwargs.pop('target')
  
      # Ignore name and state for this function
      kwargs.pop('name', None)
      kwargs.pop('state', None)
  
      for ignore in list(_STATE_INTERNAL_KEYWORDS) + ['chain', 'save', 'table']:
          if ignore in kwargs:
              del kwargs[ignore]
  
      rule = []
      protocol = False
      bang_not_pat = re.compile(r'(!|not)\s?')
  
      def maybe_add_negation(arg):
          '''
          Will check if the defined argument is intended to be negated,
          (i.e. prefixed with '!' or 'not'), and add a '! ' to the rule.
  
          The prefix will be removed from the value in the kwargs dict.
          '''
          value = str(kwargs[arg])
          if value.startswith('!') or value.startswith('not'):
              kwargs[arg] = re.sub(bang_not_pat, '', value)
              return '! '
          return ''
  
      if 'if' in kwargs:
          rule.append('{0}-i {1}'.format(maybe_add_negation('if'), kwargs['if']))
          del kwargs['if']
  
      if 'of' in kwargs:
          rule.append('{0}-o {1}'.format(maybe_add_negation('of'), kwargs['of']))
          del kwargs['of']
  
      if 'proto' in kwargs and kwargs.get('match') != 'policy':
          kwargs['protocol'] = kwargs['proto']
          del kwargs['proto']
          # Handle the case 'proto' in kwargs and kwargs.get('match') == 'policy' below
      if 'protocol' in kwargs:
          if not protocol:
              rule.append('{0}-p {1}'.format(maybe_add_negation('protocol'), kwargs['protocol']))
              protocol = True
          del kwargs['protocol']
  
      if 'match' in kwargs:
          match_value = kwargs['match']
          if not isinstance(match_value, list):
              match_value = match_value.split(',')
          for match in match_value:
              rule.append('-m {0}'.format(match))
              if 'name_' in kwargs and match.strip() in ('pknock', 'quota2', 'recent'):
                  rule.append('--name {0}'.format(kwargs['name_']))
                  del kwargs['name_']
          if 'proto' in kwargs and kwargs.get('match') == 'policy':
              rule.append('{0}--proto {1}'.format(maybe_add_negation('proto'), kwargs['proto']))
              del kwargs['proto']
          del kwargs['match']
  
      if 'match-set' in kwargs:
          if isinstance(kwargs['match-set'], six.string_types):
              kwargs['match-set'] = [kwargs['match-set']]
          for match_set in kwargs['match-set']:
              negative_match_set = ''
              if match_set.startswith('!') or match_set.startswith('not'):
                  negative_match_set = '! '
                  match_set = re.sub(bang_not_pat, '', match_set)
              rule.append('-m set {0}--match-set {1}'.format(negative_match_set, match_set))
          del kwargs['match-set']
  
      if 'connstate' in kwargs:
          if '-m state' not in rule:
              rule.append('-m state')
  
          rule.append('{0}--state {1}'.format(maybe_add_negation('connstate'), kwargs['connstate']))
  
          del kwargs['connstate']
  
      if 'dport' in kwargs:
          rule.append('{0}--dport {1}'.format(maybe_add_negation('dport'), kwargs['dport']))
          del kwargs['dport']
  
      if 'sport' in kwargs:
          rule.append('{0}--sport {1}'.format(maybe_add_negation('sport'), kwargs['sport']))
          del kwargs['sport']
  
      for multiport_arg in ('dports', 'sports'):
          if multiport_arg in kwargs:
              if '-m multiport' not in rule:
                  rule.append('-m multiport')
                  if not protocol:
                      return 'Error: protocol must be specified'
  
              mp_value = kwargs[multiport_arg]
              if isinstance(mp_value, list):
                  if any(i for i in mp_value if str(i).startswith('!') or str(i).startswith('not')):
                      mp_value = [re.sub(bang_not_pat, '', str(item)) for item in mp_value]
                      rule.append('!')
                  dports = ','.join(str(i) for i in mp_value)
              else:
                  if str(mp_value).startswith('!') or str(mp_value).startswith('not'):
                      dports = re.sub(bang_not_pat, '', mp_value)
                      rule.append('!')
                  else:
                      dports = mp_value
  
              rule.append('--{0} {1}'.format(multiport_arg, dports))
              del kwargs[multiport_arg]
  
      if 'comment' in kwargs:
          if '-m comment' not in rule:
              rule.append('-m comment')
  
          rule.append('--comment "{0}"'.format(kwargs['comment']))
          del kwargs['comment']
  
      # --set in ipset is deprecated, works but returns error.
      # rewrite to --match-set if not empty, otherwise treat as recent option
      if 'set' in kwargs and kwargs['set']:
          rule.append('{0}--match-set {1}'.format(maybe_add_negation('set'), kwargs['set']))
          del kwargs['set']
  
      # Jumps should appear last, except for any arguments that are passed to
      # jumps, which of course need to follow.
      after_jump = []
      # All jump arguments as extracted from man iptables-extensions, man iptables,
      # man xtables-addons and http://www.iptables.info/en/iptables-targets-and-jumps.html
      after_jump_arguments = (
          'j',  # j and jump needs to be first
          'jump',
  
          # IPTABLES
          'add-set',
          'and-mark',
          'and-tos',
          'checksum-fill',
          'clamp-mss-to-pmtu',
          'clustermac',
          'ctevents',
          'ctmask',
          'del-set',
          'ecn-tcp-remove',
          'exist',
          'expevents',
          'gateway',
          'hash-init',
          'hashmode',
          'helper',
          'label',
          'local-node',
          'log-ip-options',
          'log-level',
          'log-prefix',
          'log-tcp-options',
          'log-tcp-sequence',
          'log-uid',
          'mask',
          'new',
          'nfmask',
          'nflog-group',
          'nflog-prefix',
          'nflog-range',
          'nflog-threshold',
          'nodst',
          'notrack',
          'on-ip',
          'on-port',
          'or-mark',
          'or-tos',
          'persistent',
          'queue-balance',
          'queue-bypass',
          'queue-num',
          'random',
          'rateest-ewmalog',
          'rateest-interval',
          'rateest-name',
          'reject-with',
          'restore',
          'restore-mark',
          #'save',  # no arg, problematic name: How do we avoid collision with this?
          'save-mark',
          'selctx',
          'set-class',
          'set-dscp',
          'set-dscp-class',
          'set-mark',
          'set-mss',
          'set-tos',
          'set-xmark',
          'strip-options',
          'timeout',
          'to',
          'to-destination',
          'to-ports',
          'to-source',
          'total-nodes',
          'tproxy-mark',
          'ttl-dec',
          'ttl-inc',
          'ttl-set',
          'type',
          'ulog-cprange',
          'ulog-nlgroup',
          'ulog-prefix',
          'ulog-qthreshold',
          'xor-mark',
          'xor-tos',
          'zone',
  
          # IPTABLES-EXTENSIONS
          'dst-pfx',
          'hl-dec',
          'hl-inc',
          'hl-set',
          'hmark-dport-mask',
          'hmark-dst-prefix',
          'hmark-mod',
          'hmark-offset',
          'hmark-proto-mask',
          'hmark-rnd',
          'hmark-spi-mask',
          'hmark-sport-mask',
          'hmark-src-prefix',
          'hmark-tuple',
          'led-always-blink',
          'led-delay',
          'led-trigger-id',
          'queue-cpu-fanout',
          'src-pfx',
  
          # WEB
          'to-port',
  
          # XTABLES
          'addr',
          'and-mask',
          'delude',
          'honeypot',
          'or-mask',
          'prefix',
          'reset',
          'reuse',
          'set-mac',
          'shift',
          'static',
          'tarpit',
          'tname',
          'ttl',
      )
      for after_jump_argument in after_jump_arguments:
          if after_jump_argument in kwargs:
              value = kwargs[after_jump_argument]
              if value in (None, ''):  # options without arguments
                  after_jump.append('--{0}'.format(after_jump_argument))
              elif any(ws_char in str(value) for ws_char in string.whitespace):
                  after_jump.append('--{0} "{1}"'.format(after_jump_argument, value))
              else:
                  after_jump.append('--{0} {1}'.format(after_jump_argument, value))
              del kwargs[after_jump_argument]
  
      for key in kwargs:
          negation = maybe_add_negation(key)
          # don't use .items() since maybe_add_negation removes the prefix from
          # the value in the kwargs, thus we need to fetch it after that has run
          value = kwargs[key]
          flag = '-' if len(key) == 1 else '--'
          value = '' if value in (None, '') else ' {0}'.format(value)
          rule.append('{0}{1}{2}{3}'.format(negation, flag, key, value))
  
      rule += after_jump
  
      if full:
          if not table:
              return 'Error: Table needs to be specified'
          if not chain:
              return 'Error: Chain needs to be specified'
          if not command:
              return 'Error: Command needs to be specified'
  
          if command in 'ACDIRLSFZNXPE':
              flag = '-'
          else:
              flag = '--'
  
          wait = '--wait' if _has_option('--wait', family) else ''
  
          return '{0} {1} -t {2} {3}{4} {5} {6} {7}'.format(_iptables_cmd(family),
                 wait, table, flag, command, chain, position, ' '.join(rule))
  
      return ' '.join(rule)