Testing if stdout is empty in bash

This blog post is more than 5 years old, so the content may be out of date.

So, you want to check if something's in LDAP…ldapsearch, right?

Well, that works fine if you're a human…but if you're trying to script it - say, because you're trying to puppetise something - it's a little harder. Because ldapsearch provides an exit code of 0, irrespective of whether it matches any entries. It only returns a failure exit code if the query syntax is invalid.

I boiled the problem down to:

  • Make ldapsearch only provide output if it matches something
  • Have bash test for an empty string in stdout

Tweaking ldapsearch to only provide results if it matches

Here's a very basic search, to list the defined LDAP schemas in the config:

ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(objectclass=olcSchemaConfig)'

Here's the start of the results:

SASL/EXTERNAL authentication started
SASL username: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
SASL SSF: 0
# extended LDIF
#
# LDAPv3
# base <cn=schema,cn=config> with scope subtree
# filter: (objectclass=olcSchemaConfig)
# requesting: ALL
#
 
# schema, config
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
olcObjectIdentifier: OLcfg 1.3.6.1.4.1.4203.1.12.2
olcObjectIdentifier: OLcfgAt OLcfg:3
olcObjectIdentifier: OLcfgGlAt OLcfgAt:0
olcObjectIdentifier: OLcfgBkAt OLcfgAt:1
olcObjectIdentifier: OLcfgDbAt OLcfgAt:2
olcObjectIdentifier: OLcfgOvAt OLcfgAt:3
olcObjectIdentifier: OLcfgCtAt OLcfgAt:4
olcObjectIdentifier: OLcfgOc OLcfg:4

First things first: that SASL authentication message (at the start) has to go. Firstly, it's spat out to STDERR, not STDOUT (so we could simply use output redirection: 2>/dev/null).

There is a simpler way though: -Q (SASL quiet mode).

ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(objectclass=olcSchemaConfig)' -Q
# extended LDIF
#
# LDAPv3
# base <cn=schema,cn=config> with scope subtree
# filter: (objectclass=olcSchemaConfig)
# requesting: ALL
#
 
# schema, config
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
olcObjectIdentifier: OLcfg 1.3.6.1.4.1.4203.1.12.2
olcObjectIdentifier: OLcfgAt OLcfg:3
olcObjectIdentifier: OLcfgGlAt OLcfgAt:0
olcObjectIdentifier: OLcfgBkAt OLcfgAt:1
olcObjectIdentifier: OLcfgDbAt OLcfgAt:2
olcObjectIdentifier: OLcfgOvAt OLcfgAt:3
olcObjectIdentifier: OLcfgCtAt OLcfgAt:4
olcObjectIdentifier: OLcfgOc OLcfg:4

Next, let's remove the LDAP version comments. Here are some controlling options - we want -LLL.

  -L         print responses in LDIFv1 format
  -LL        print responses in LDIF format without comments
  -LLL       print responses in LDIF format without comments
             and version
ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(objectclass=olcSchemaConfig)' -Q -LLL 
dn: cn=schema,cn=config
objectClass: olcSchemaConfig
cn: schema
olcObjectIdentifier: OLcfg 1.3.6.1.4.1.4203.1.12.2
olcObjectIdentifier: OLcfgAt OLcfg:3
olcObjectIdentifier: OLcfgGlAt OLcfgAt:0
olcObjectIdentifier: OLcfgBkAt OLcfgAt:1
olcObjectIdentifier: OLcfgDbAt OLcfgAt:2
olcObjectIdentifier: OLcfgOvAt OLcfgAt:3
olcObjectIdentifier: OLcfgCtAt OLcfgAt:4
olcObjectIdentifier: OLcfgOc OLcfg:4

OK, that's much better - now we're getting just the data.

Say we're looking for something more precise though - for example, we want puppet to check if the nis schema is installed.

Firstly, lets change the query to check for the NIS schema:

ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))'
# extended LDIF
#
# LDAPv3
# base <cn=schema,cn=config> with scope subtree
# filter: (&(objectclass=olcSchemaConfig)(cn={*}nis))
# requesting: ALL
#
 
# {2}nis, schema, config
dn: cn={2}nis,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: {2}nis
olcAttributeTypes: {0}( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; th
 e common name' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatc
 h SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolut
 e path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1
 466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to th
 e login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.2
 6 SINGLE-VALUE )

And with our quiet options:

ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))' -Q -LLL
dn: cn={2}nis,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: {2}nis
olcAttributeTypes: {0}( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; th
 e common name' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatc
 h SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolut
 e path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1
 466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to th
 e login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.2
 6 SINGLE-VALUE )

OK, better again. Still a lot of output though - we only want to know if it's there or not. Thankfully there's an option for that.

usage: ldapsearch [options] [filter [attributes...]]
where:
  filter	RFC 4515 compliant LDAP search filter
  attributes	whitespace-separated list of attribute descriptions
    which may include:
      1.1   no attributes
      *     all user attributes
      +     all operational attributes
ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))' -Q -LLL 1,1 

Gives us:

dn: cn={2}nis,cn=schema,cn=config

Great - now we have an ldapsearch command that gives us a 1-liner output.

Testing for a zero-length string in stdout in bash

Typically, you'd use the test command:

# -n test passes if the string length is not 0.
test -n "my string here" && echo "String is not empty."
 
# With a variable:
foo="some string here."
test -n $foo && echo "String is not empty."

Logically therefore, you simply pipe the ldapsearch command to test:

ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))' -Q -LLL 1,1 | test -n

BZZZT…Wrong! test doesn't accept input from pipe. Backticks to exec, maybe?

test -n `ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))' -Q -LLL 1,1` && echo "NIS schema is present"
bash: test: dn:: unary operator expected

OK,so doesn't like that either. How about the [[ ]] syntax, with backticks?

[[ -n `ldapsearch -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config'  '(&(objectclass=olcSchemaConfig)(cn={*}nis))' -Q -LLL 1,1` ]] && echo "NIS schema is present."

Success! Now to go puppetise that :-)

Post-script

Turns out puppet uses sh, not bash - and of course, I should have known that ;-)

Here's what I ended up with:

  # Example: touch /etc/ldap_matched if a schema with name $schema_cn is found.
  exec { 'test':
    path => "/usr/bin:/usr/sbin:/bin",
    command => "touch /etc/ldap_matched",
    onlyif => "search=`ldapsearch -LLL -Q -Y EXTERNAL -H ldapi:/// -b 'cn=schema,cn=config' '(&(objectclass=olcSchemaConfig)(cn={*}${schema_cn}))' 1,1` && test -n \"\$search\"",
  }

Add new comment

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <apache>, <bash>, <c>, <cpp>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>. The supported tag styles are: <foo>, [foo]. PHP source code can also be enclosed in <?php ... ?> or <% ... %>.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.