zsh-workers
 help / color / mirror / code / Atom feed
* PATCH: complete ldap search filters
@ 2023-02-17 23:47 Oliver Kiddle
  2023-02-19 16:12 ` Oliver Kiddle
  0 siblings, 1 reply; 2+ messages in thread
From: Oliver Kiddle @ 2023-02-17 23:47 UTC (permalink / raw)
  To: Zsh workers

This adds a helper function for completing LDAP search filters
conforming to RFC4515. Also included is completion for the OpenLDAP
client utilities (ldapsearch etc). There are alternate implementations
of some of these (such as in OpenDS) but I've only ever used those from
OpenLDAP. Finally, there is a helper for LDAP attributes. These can in
theory be anything - they are just database keys. But in practice there
probably isn't so much variation. I've taken a lazy approach of dumping
all keys from both a FreeIPA and OpenLDAP installation - both for the
attributes and classes.

It still needs some adapting to work properly with backslash quoting. I've
made it force double-quoting by default which is more useful for the
filters anyway. For some reason making the filter argument in _arguments
optional causes it to add an extra layer of quoting to & and |; for now,
I've not marked them as optional. I've also used an obscure workaround
of completing an empty string with a prefix to avoid quotes being
removed:
  _foo() {
    compset -P \*
    compadd -P '' \)
    compadd -P \( one two
  }
  foo '(&(a=b)<tab> → removes the quote
  The workaround is to use compadd -P '\)' ''

Oliver

diff --git a/Completion/BSD/Command/_ldap b/Completion/BSD/Command/_ldap
index 8fa17e2f8..181e6b0d0 100644
--- a/Completion/BSD/Command/_ldap
+++ b/Completion/BSD/Command/_ldap
@@ -80,8 +80,8 @@ else
         '-x[use simple authentication]' \
         '-Z[use StartTLS]' \
         '-z+[specify maximum number of results or 0 for no limit]:size limit [0]:' \
-        '::filter:' \
-        '*:attribute:'
+        '1: :_ldap_filters' \
+        '*: :_ldap_attributes'
       ;;
   esac
 fi
diff --git a/Completion/Unix/Command/_openldap b/Completion/Unix/Command/_openldap
new file mode 100644
index 000000000..233d0950e
--- /dev/null
+++ b/Completion/Unix/Command/_openldap
@@ -0,0 +1,222 @@
+#compdef ldapadd ldapcompare ldapdelete ldapexop ldapmodify ldapmodrdn ldappasswd ldapsearch ldapurl ldapwhoami
+
+local curcontext="$curcontext" nm="$compstate[nmatches]"
+local -a args auth state line expl
+
+args=( '*-e[general extensions]:extension:->general-extensions' )
+
+case $service in
+  ldapadd|ldapcompare|ldapdelete|ldapexop|ldapmodify|ldapmodrdn|ldappasswd|ldapsearch|ldapwhoami)
+    if (( $words[(I)-[^Z]#Z[^Z]#] )); then
+      args+=( '*-Z[require success for start TLS request]' )
+    elif (( ! $words[(I)-[^Z]#Z] )); then
+      args+=( '-Z[start TLS request]' )
+    fi
+    args+=(
+      '!(-)-VV' '-V[display version information]'
+      '*-d+[set LDAP debugging level]:level:((1\:trace 2\:packets 4\:args 8\:conns 10\:ber 2048\:parse -1\:all))'
+      "-n[show what would be done but don't actually do it]"
+      '-v[verbose output]'
+      "-N[don't use reverse DNS to canonicalize SASL host name]"
+      '*-o+[specify any ldap.conf options]: : _values option
+        "ldif_wrap[specify width]\:width"
+        "nettimeout[specify timeout]\:timeout (seconds)"'
+    )
+    auth=(
+      '-D[specify bind DN]:binddn'
+      '-H[specify LDAP URIs]:uri'
+      '-P[specify protocol version]:version [3]:(2 3)'
+      + simple
+      '(sasl)-x[use simple authentication]'
+      '(sasl -W -y)-w+[specify bind password]:bind password'
+      '(sasl -w -y)-W[prompt for bind password]'
+      '(sasl -w -W)-y+[read password from file]:file:_files'
+      + sasl
+      '(simple)-O+[specify SASL security properties]: : _values -s , property
+          none noplain noactive nodict noanonymous forwardsec passcred
+          minssf\:factor maxssf\:factor maxbufsize\:factor'
+      '(simple)-X+[specify SASL authorization identity]:authzid:->authzids'
+      '(simple)-Y+[specify SASL mechanism]:mechanism:compadd -M "m:{a-zA-Z}={A-Za-z}" EXTERNAL GSSAPI' # iana has a full list but cyrus support seems limited
+      '(simple)-R+[specify SASL realm]:realm'
+      '(simple)-U+[specify SASL authentication identity]:authcid'
+      '(simple)-I[use SASL Interactive mode]'
+      '(simple)-Q[use SASL Quiet mode]'
+    )
+  ;|
+  ldapadd|ldapcompare|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    if (( $words[(I)-[^M]#M[^M]#] )); then
+      args+=( '*-M[enable Manage DSA IT control critical]' )
+    elif (( ! $words[(I)-[^M]#M] )); then
+      args+=( '-M[enable Manage DSA IT control]' )
+    fi
+  ;|
+  ldapadd|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    # ldapexop documents but doesn't implement this
+    args+=( '(1 2 *)-f+[read operations from file]:file:_files' )
+  ;|
+  ldapadd|ldapdelete|ldapmodify|ldapmodrdn|ldapsearch)
+    args+=( "-c[continuous operation mode (don't stop on errors)]" )
+  ;|
+  ldapdelete|ldapsearch)
+    args+=( '-z+[specify size limit]:size limit (entries)' )
+  ;|
+  ldapadd|ldapmodify)
+    args+=(
+      '-S+[write records that are skipped due to an error to file]:file:_files'
+      '*-E+[modify extensions]:extension:->modify-extensions'
+    )
+  ;|
+  ldapurl|ldapsearch)
+    args+=(
+      '(decompose)-s+[specify search scope]:search scope [sub]:(base one sub children)'
+    )
+  ;|
+  ldapdelete|ldapmodrdn|ldapurl|ldapwhoami) args+=( '!*-E+:extension' ) ;|
+
+  ldapadd) args+=( '!-a' ) ;;
+  ldapmodify) args+=( '-a[add new entries]' ) ;;
+  ldapcompare)
+    args+=(
+      '-z[quiet mode - no output aside return status]'
+      '*-E+[compare extensions]:extension:->compare-extensions'
+    )
+  ;;
+  ldapdelete)
+    args+=(
+      '-r[do a recursive delete]'
+      '*: :_guard "^-*" "distinguished name"'
+    )
+  ;;
+  ldapexop) args+=( '*:: :->extended-operations' ) ;;
+  ldapmodrdn)
+    args+=(
+      '-r[remove old RDN values from the entry]'
+      '-s[specify new superior entry to move target to]:entry'
+      '1:distinguished name'
+      '2:relative distinguished name'
+    )
+  ;;
+  ldappasswd)
+    args+=(
+      '(-a -t)-A[prompt for old password]'
+      '(-A -t)-a+[specify old password]:password'
+      '(-A -a)-t+[read old password from file]:file:_files'
+      '(-s -T)-S[prompt for new password]'
+      '(-S -T)-s+[specify new password]:password'
+      '(-S -s)-T+[read new password from file]:file:_files'
+    )
+  ;;
+  ldapsearch)
+    if (( $words[(I)-[^L]#L[^L]#L[^L]#] )); then
+      args+=( '*-L[LDIF format without comments and version]' )
+    elif (( $words[(I)-[^L]#L[^L]#] )); then
+      args+=( '*-L[LDIF format without comments]' )
+    elif ! (( $words[(I)-[^L]#L[^L]#L[^L]#L] )); then
+      args+=( '-L[LDIFv1 format]' )
+    else
+      args+=( '!*-L' )
+    fi
+    if (( $words[(I)-[^t]#t[^t]#] )); then
+      args+=( '*-t[write all retrieved values to files in temporary directory]' )
+    elif (( ! $words[(I)-[^t]#t] )); then
+      args+=( '-t[write binary values to files in temporary directory]' )
+    fi
+
+    args+=(
+      '-a+[specify how aliases dereferencing is done]:deref [never]:(never always search find)'
+      '-A[retrieve attributes only (no values)]'
+      '-b+[specify base dn for search]:basedn'
+      '*-E+[search extensions]:extension:->search-extensions'
+      '-F+[specify URL prefix for temporary files]:prefix [file:///tmp//]'
+      '-l+[specify time limit for search]:time limit (seconds)'
+      '-S+[sort results by specified attribute]:attribute:_ldap_attributes'
+      '-T[write files to specified directory]:path [/tmp]:_directories'
+      '-u[include User Friendly entry names in the output]'
+      '1: :_ldap_filters'
+      '2: : _alternative
+        "attributes:attribute:_ldap_attributes"
+        "attributes:attribute:((1.1\:no\ attributes \*\:all\ user\ attributes \+\:all\ operational\ attributes))"'
+      '*:attribute:_ldap_attributes -F line'
+    )
+  ;;
+  ldapurl)
+    args+=(
+      - compose
+      '-a+[set a list of attribute selectors]:attribute selectors (comma separated)'
+      '-b+[set the searchbase]:search base'
+      '-f+[set the URL filter]:filter:_ldap_filters'
+      '-h+[set the host]:host:_hosts'
+      '-p+[set the tcp port]:port:(389 636)'
+      '-S+[set the URL scheme]:scheme:(ldap ldaps)'
+      - decompose
+      '(-s)-H+[specify URI to be exploded]:uri'
+    )
+  ;;
+esac
+
+_arguments -C -S -s $args $auth
+
+case $state in
+  extended-operations)
+    case $CURRENT:$words[1] in
+      1:*)
+        if compset -P '*::'; then
+          _message -e data 'base64 data'
+        elif compset -P '*:'; then
+          _message -e data data
+        else
+          _alternative \
+            'oids::_guard "(<->(|.))#" oid' \
+            'operations:operation:(whoami cancel refresh)'
+        fi
+      ;;
+      2:cancel) _message -e ids 'cancel id' ;;
+      2:refresh) _message -e names 'distinguished name' ;;
+      3:refresh) _message -e times 'ttl' ;;
+      *) _message 'no more arguments' ;;
+    esac
+  ;;
+  *-extensions)
+    if ! compset -P \!; then
+      _description criticality expl critical
+      compadd -S "" "$expl[@]" \!
+    fi
+  ;|
+  modify-extensions) _values extension 'txn:txn:(abort commit)' ;;
+  compare-extensions) _values extension dontUseCopy ;;
+  search-extensions)
+    _values extension \
+      'mv[matched values filter]:filter:_ldap_filters' \
+      'pr[paged results/prompt]:size[/prompt|noprompt]' \
+      'sss[server side sorting]: :_sequence -s / _ldap_attributes' \
+      'subentries: :(true false)' \
+      'sync:sync[/cookie][/slimit]:((ro\:refreshOnly rp\:refreshAndPersist))' \
+      'vlv[virtual list view]:before/after(/offset/count|\:value' \
+      'deref:derefAttr:_sequence _ldap_attributes' \
+      dontUseCopy domainScope
+  ;;
+  general-extensions)
+    _values extension \
+      'assert:filter:_ldap_filters' \
+      'authzid:authzid:->authzids' \
+      {post,pre}'read: :_sequence _ldap_attributes' \
+      'sessiontracking:username:_users' \
+      'chaining:behavior:(chainingPreferred chainingRequired referralsPreferred referralsRequired)' \
+      bauthzid manageDSAit noop ppolicy relax abandon cancel ignore
+  ;&
+  authzids)
+    if [[ $state != authzids ]]; then
+      : # fall-through from above without the authzids state
+    elif compset -P 'u:'; then
+      _description users expl authzid
+      _users "$expl[@]"
+    elif compset -P 'dn:'; then
+      _message -e ids 'distinguished name'
+    else
+      _description prefixes expl prefix
+      compadd -S: "$expl[@]" u dn
+    fi
+  ;;
+esac
+
+[[ nm -ne "$compstate[nmatches]" ]]
diff --git a/Completion/Unix/Type/_ldap_attributes b/Completion/Unix/Type/_ldap_attributes
new file mode 100644
index 000000000..0711cfbf1
--- /dev/null
+++ b/Completion/Unix/Type/_ldap_attributes
@@ -0,0 +1,27 @@
+#autoload
+
+local -a expl attrs
+
+# These come from dumping attributes from basic installations of both openldap
+# and FreeIPA and combining results. It is possible to have custom additions so
+# a definitive list is not possible Hence the use of -x with compadd.
+#
+attrs=(
+  associatedDomain authenticationMethod automountInformation automountKey
+  automountMapName bindTimeLimit cACertificate;binary cn dc defaultSearchBase
+  defaultServerList description displayName dn followReferrals gecos gidNumber
+  givenName homeDirectory info initials ipaCertIssuerSerial ipaCertSubject
+  ipaConfigString ipaKeyExtUsage ipaKeyTrust ipaNTSecurityIdentifier
+  ipaPublicKey ipaUniqueID ipHostNumber loginShell mail member memberUid
+  mepManagedBy nisDomain nisNetgroupTriple o objectClass objectClassMap ou
+  pwdAllowUserChange pwdAttribute pwdCheckQuality pwdExpireWarning
+  pwdFailureCountInterval pwdGraceAuthNLimit pwdInHistory pwdLockout
+  pwdLockoutDuration pwdMaxAge pwdMaxFailure pwdMinAge pwdMinLength
+  pwdMustChange pwdSafeModify searchTimeLimit serviceSearchDescriptor sn
+  telephoneNumber uid uidNumber userCertificate;binary userPKCS12
+  userSMIMECertificate
+)
+
+_description ldap-attributes expl "ldap attribute"
+compadd "${@:/-X/-x}" "${expl[@]:/-X/-x}" \
+    -M 'm:{a-zA-Z}={A-Za-z} r:[^A-Z]||[A-Z]=* r:|=*' -a attrs
diff --git a/Completion/Unix/Type/_ldap_filters b/Completion/Unix/Type/_ldap_filters
new file mode 100644
index 000000000..919f9e266
--- /dev/null
+++ b/Completion/Unix/Type/_ldap_filters
@@ -0,0 +1,91 @@
+#autoload
+
+# LDAP search filters conforming to RFC4515
+
+local -a expl excl optype disp end pre
+local -i nest=0
+local open='(' close=')'
+
+[[ -prefix - ]] && return 1
+
+local -a matchingrules=( # From RFC4517
+  bitStringMatch booleanMatch caseExactIA5Match
+  caseExactMatch caseExactOrderingMatch caseExactSubstringsMatch
+  caseIgnoreIA5Match caseIgnoreIA5SubstringsMatch caseIgnoreListMatch
+  caseIgnoreListSubstringsMatch caseIgnoreMatch caseIgnoreOrderingMatch
+  caseIgnoreSubstringsMatch directoryStringFirstComponentMatch
+  distinguishedNameMatch generalizedTimeMatch generalizedTimeOrderingMatch
+  integerFirstComponentMatch integerMatch integerOrderingMatch keywordMatch
+  numericStringMatch numericStringOrderingMatch numericStringSubstringsMatch
+  objectIdentifierFirstComponentMatch objectIdentifierMatch octetStringMatch
+  octetStringOrderingMatch telephoneNumberMatch telephoneNumberSubstringsMatch
+  uniqueMemberMatch wordMatch
+)
+local -a classes=( # Sampled from real servers, arbitrary other values allowed
+  automount automountMap cosTemplate dcObject device dnaSharedConfig domain
+  domainRelatedObject DUAConfigProfile extensibleObject groupOfNames
+  groupOfPrincipals ieee802device inetOrgPerson inetuser ipaassociation ipaca
+  ipacaacl ipaCertificate ipaCertMapConfigObject ipacertprofile ipaConfigObject
+  ipaDomainIDRange ipaDomainLevelConfig ipaGuiConfig ipahbacrule ipahbacservice
+  ipahbacservicegroup ipahost ipahostgroup ipaIDrange ipaKeyPolicy
+  ipakrbprincipal ipaNameResolutionData ipaNTDomainAttrs ipaNTGroupAttrs
+  ipaNTUserAttrs ipaobject ipaPublicKeyObject ipaReplTopoManagedServer
+  ipaservice ipaSshGroupOfPubKeys ipasshhost ipasshuser ipasudorule
+  ipaSupportedDomainLevelConfig ipaTrustedADDomainRange ipaUserAuthTypeClass
+  ipausergroup ipHost krbContainer krbprincipal krbprincipalaux
+  krbrealmcontainer krbTicketPolicyAux mepManagedEntry mepOriginEntry
+  nestedGroup nisDomainObject nisNetgroup nsContainer nsDS5Replica nshost
+  organization organizationalPerson organizationalRole organizationalUnit
+  person pilotObject pkiCA pkiuser posixAccount posixGroup pwdPolicy
+  shadowAccount simpleSecurityObject top
+)
+
+compquote open close
+open=${(q)open} close=${(q)close}
+# default to double rather than backslash quoting
+[[ -z $compstate[quote] || -z $PREFIX ]] && pre='"('
+
+zstyle -s ":completion:${curcontext}:operators" list-separator sep || sep=--
+print -v disp -f "%s $sep %s" \| or \& and \! not
+end=( ") $sep end" )
+excl=( \\\| \& ) # compadd -F uses globs: only | needs quoting
+
+local -a query=(
+  \( /$'*\0[ \t\n]#'/ \) # strip off any preceding arguments
+  \(
+    \( "/${open}!/" -'optype[++nest]=0;pre=""'
+    \| "/${open}\|/" -'optype[++nest]=1;pre=""'
+    \| "/${open}&/" -'optype[++nest]=2;pre=""'
+    \| '/[]/' ':operators:operator:compadd -F "( ${(q)excl[optype[nest]]} )" -d disp -P ${pre:-${(Q)open}} -S \( \| \& \!' \)
+  \|
+    \( '/[^)]##/' '%\)%' # pass over whole var=value, needed due to lack of backtracking after the following
+    \| "/${open}(#i)homeDirectory=/" '/[]/' ':directories:directory:_directories -P / -W / -r ") \t\n\-"'
+    \| "/${open}(#i)loginShell=/" '/[]/' ':shells:shell:compadd -S ${(Q)close} ${(f)^"$(</etc/shells)"}(N)'
+    \| "/${open}(#i)mail=/" '/[]/' ':email-addresses:mail:_email_addresses -S ${(Q)close}'
+    \| "/${open}(#i)objectClass=/" '/[]/' ':object-classes:class:compadd -S ${(Q)close} -M "m:{a-zA-Z}={A-Za-z} r:[^A-Z]||[A-Z]=* r:|=*" -a classes'
+    \| "/${open}(#i)(automountKey|(member|)uid)=/" '/[]/' ':users:username:_users -S ${(Q)close}'
+    \| "/${open}(#i)cn=/" '/[]/' ':cn:cn: _alternative "users:user:_users -S ${close}" "groups:group:_groups -S ${close}" "hosts:host:_hosts -S ${close}"'
+    \|
+      '/[^:=<>~]##/' '%[=:<>~]%' -'pre=""'
+      ':object-types:object type:_ldap_attributes -P ${pre:-${(Q)open}}  -S = -r ":=~<> \t\n\-"'
+      \(
+        '/:/'
+        '/[^:]##:=/' ':matching-rules:matching rule:compadd -S ":=" -a matchingrules'
+      \|
+        '/([~<>]|)=/' ':operators:operator:compadd -S "" "<=" \>= \~='
+      \)
+      '/[^)]##/' '%\)%' ': _message -e object-values "object value (* for presence check)"'
+    \)
+    "/$close/" -'(( nest ))' ':brackets:bracket:compadd ${=query[nest]:+-S ""} \)'
+    \(
+      # This use of -P/-d and an empty match works around a limitation/bug where
+      # mixed use of -P removes any quoting
+      "/$close/" ':operators:operator:compadd ${=query[nest-1]:+-S ""} -d end -P ${(Q)close} ""'
+      \( // -'(( --nest ))' \| '//' -'((!nest))' '/[]/' ': compadd ""' \)
+    \) \#
+    // -'(( nest && optype[nest] ))'
+  \) \#
+)
+
+_regex_arguments _ldap_search_filters "$query[@]"
+_ldap_search_filters


^ permalink raw reply	[flat|nested] 2+ messages in thread

* Re: PATCH: complete ldap search filters
  2023-02-17 23:47 PATCH: complete ldap search filters Oliver Kiddle
@ 2023-02-19 16:12 ` Oliver Kiddle
  0 siblings, 0 replies; 2+ messages in thread
From: Oliver Kiddle @ 2023-02-19 16:12 UTC (permalink / raw)
  To: Zsh workers

I wrote:
> This adds a helper function for completing LDAP search filters

> It still needs some adapting to work properly with backslash quoting.

This further patch gets the quoting right. The completion system will
sometimes remove backslash quoting on ! which isn't helpful with
bang-history enabled. Ideally, _regex_arguments would let you do
matching against the unquoted form by supporting some sort of compset
-q.

The function ignores & (AND) and | (OR) operators where you would be
nesting them directly. It occurred to me that it should also do the same
for ! (NOT) - it's even more obvious why that's pointless.

Oliver

diff --git a/Completion/Unix/Type/_ldap_filters b/Completion/Unix/Type/_ldap_filters
index 919f9e266..5e0e30f01 100644
--- a/Completion/Unix/Type/_ldap_filters
+++ b/Completion/Unix/Type/_ldap_filters
@@ -4,7 +4,7 @@
 
 local -a expl excl optype disp end pre
 local -i nest=0
-local open='(' close=')'
+local open='(' close=')' andop='&' orop='|'
 
 [[ -prefix - ]] && return 1
 
@@ -40,25 +40,25 @@ local -a classes=( # Sampled from real servers, arbitrary other values allowed
   shadowAccount simpleSecurityObject top
 )
 
-compquote open close
+compquote open close andop orop
 open=${(q)open} close=${(q)close}
 # default to double rather than backslash quoting
-[[ -z $compstate[quote] || -z $PREFIX ]] && pre='"('
+[[ -z $compstate[quote] && -z $PREFIX ]] && pre='"('
 
 zstyle -s ":completion:${curcontext}:operators" list-separator sep || sep=--
 print -v disp -f "%s $sep %s" \| or \& and \! not
 end=( ") $sep end" )
-excl=( \\\| \& ) # compadd -F uses globs: only | needs quoting
+excl=( ! \\\| \& ) # compadd -F uses globs: only | needs quoting
 
 local -a query=(
   \( /$'*\0[ \t\n]#'/ \) # strip off any preceding arguments
   \(
-    \( "/${open}!/" -'optype[++nest]=0;pre=""'
-    \| "/${open}\|/" -'optype[++nest]=1;pre=""'
-    \| "/${open}&/" -'optype[++nest]=2;pre=""'
-    \| '/[]/' ':operators:operator:compadd -F "( ${(q)excl[optype[nest]]} )" -d disp -P ${pre:-${(Q)open}} -S \( \| \& \!' \)
+    \( "/${open}!/" -'optype[++nest]=1;pre=""'
+    \| "/${open}${(q)orop}/" -'optype[++nest]=2;pre=""'
+    \| "/${open}${andop}/" -'optype[++nest]=3;pre=""'
+    \| '/[]/' ':operators:operator:compadd -F "( ${(q)excl[optype[nest]]} )" -d disp -P ${pre:-${(Q)open}} -S ${(Q)open} \| \& \!' \)
   \|
-    \( '/[^)]##/' '%\)%' # pass over whole var=value, needed due to lack of backtracking after the following
+    \( "/${open}[^\\)]##/" "%$close%" # pass over whole var=value, needed due to lack of backtracking after the following
     \| "/${open}(#i)homeDirectory=/" '/[]/' ':directories:directory:_directories -P / -W / -r ") \t\n\-"'
     \| "/${open}(#i)loginShell=/" '/[]/' ':shells:shell:compadd -S ${(Q)close} ${(f)^"$(</etc/shells)"}(N)'
     \| "/${open}(#i)mail=/" '/[]/' ':email-addresses:mail:_email_addresses -S ${(Q)close}'
@@ -74,7 +74,7 @@ local -a query=(
       \|
         '/([~<>]|)=/' ':operators:operator:compadd -S "" "<=" \>= \~='
       \)
-      '/[^)]##/' '%\)%' ': _message -e object-values "object value (* for presence check)"'
+      '/[^\\)]##/' "%$close%" ': _message -e object-values "object value (* for presence check)"'
     \)
     "/$close/" -'(( nest ))' ':brackets:bracket:compadd ${=query[nest]:+-S ""} \)'
     \(
@@ -83,7 +83,7 @@ local -a query=(
       "/$close/" ':operators:operator:compadd ${=query[nest-1]:+-S ""} -d end -P ${(Q)close} ""'
       \( // -'(( --nest ))' \| '//' -'((!nest))' '/[]/' ': compadd ""' \)
     \) \#
-    // -'(( nest && optype[nest] ))'
+    // -'(( nest && optype[nest] > 1 ))'
   \) \#
 )
 


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2023-02-19 16:13 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-02-17 23:47 PATCH: complete ldap search filters Oliver Kiddle
2023-02-19 16:12 ` Oliver Kiddle

Code repositories for project(s) associated with this public inbox

	https://git.vuxu.org/mirror/zsh/

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).