Next: , Previous: Improved cleardivert, Up: Answers


17.7 Solution for capitalize

The capitalize macro (see Patsubst) as presented earlier does not allow clients to follow the quoting rule of thumb. Consider the three macros active, Active, and ACTIVE, and the difference between calling capitalize with the expansion of a macro, expanding the result of a case change, and changing the case of a double-quoted string:

     $ m4 -I examples
     include(`capitalize.m4')dnl
     define(`active', `act1, ive')dnl
     define(`Active', `Act2, Ive')dnl
     define(`ACTIVE', `ACT3, IVE')dnl
     upcase(active)
     ⇒ACT1,IVE
     upcase(`active')
     ⇒ACT3, IVE
     upcase(``active'')
     ⇒ACTIVE
     downcase(ACTIVE)
     ⇒act3,ive
     downcase(`ACTIVE')
     ⇒act1, ive
     downcase(``ACTIVE'')
     ⇒active
     capitalize(active)
     ⇒Act1
     capitalize(`active')
     ⇒Active
     capitalize(``active'')
     ⇒_capitalize(`active')
     define(`A', `OOPS')
     ⇒
     capitalize(active)
     ⇒OOPSct1
     capitalize(`active')
     ⇒OOPSctive

First, when capitalize is called with more than one argument, it was throwing away later arguments, whereas upcase and downcase used ‘$*’ to collect them all. The fix is simple: use ‘$*’ consistently.

Next, with single-quoting, capitalize outputs a single character, a set of quotes, then the rest of the characters, making it impossible to invoke Active after the fact, and allowing the alternate macro A to interfere. Here, the solution is to use additional quoting in the helper macros, then pass the final over-quoted output string through _arg1 to remove the extra quoting and finally invoke the concatenated portions as a single string.

Finally, when passed a double-quoted string, the nested macro _capitalize is never invoked because it ended up nested inside quotes. This one is the toughest to fix. In short, we have no idea how many levels of quotes are in effect on the substring being altered by patsubst. If the replacement string cannot be expressed entirely in terms of literal text and backslash substitutions, then we need a mechanism to guarantee that the helper macros are invoked outside of quotes. In other words, this sounds like a job for changequote (see Changequote). By changing the active quoting characters, we can guarantee that replacement text injected by patsubst always occurs in the middle of a string that has exactly one level of over-quoting using alternate quotes; so the replacement text closes the quoted string, invokes the helper macros, then reopens the quoted string. In turn, that means the replacement text has unbalanced quotes, necessitating another round of changequote.

In the fixed version below, (also shipped as m4-1.4.16/examples/capitalize2.m4), capitalize uses the alternate quotes of ‘<<[’ and ‘]>>’ (the longer strings are chosen so as to be less likely to appear in the text being converted). The helpers _to_alt and _from_alt merely reduce the number of characters required to perform a changequote, since the definition changes twice. The outermost pair means that patsubst and _capitalize_alt are invoked with alternate quoting; the innermost pair is used so that the third argument to patsubst can contain an unbalanced ‘]>>’/‘<<[’ pair. Note that upcase and downcase must be redefined as _upcase_alt and _downcase_alt, since they contain nested quotes but are invoked with the alternate quoting scheme in effect.

     $ m4 -I examples
     include(`capitalize2.m4')dnl
     define(`active', `act1, ive')dnl
     define(`Active', `Act2, Ive')dnl
     define(`ACTIVE', `ACT3, IVE')dnl
     define(`A', `OOPS')dnl
     capitalize(active; `active'; ``active''; ```actIVE''')
     ⇒Act1,Ive; Act2, Ive; Active; `Active'
     undivert(`capitalize2.m4')dnl
     ⇒divert(`-1')
     ⇒# upcase(text)
     ⇒# downcase(text)
     ⇒# capitalize(text)
     ⇒#   change case of text, improved version
     ⇒define(`upcase', `translit(`$*', `a-z', `A-Z')')
     ⇒define(`downcase', `translit(`$*', `A-Z', `a-z')')
     ⇒define(`_arg1', `$1')
     ⇒define(`_to_alt', `changequote(`<<[', `]>>')')
     ⇒define(`_from_alt', `changequote(<<[`]>>, <<[']>>)')
     ⇒define(`_upcase_alt', `translit(<<[$*]>>, <<[a-z]>>, <<[A-Z]>>)')
     ⇒define(`_downcase_alt', `translit(<<[$*]>>, <<[A-Z]>>, <<[a-z]>>)')
     ⇒define(`_capitalize_alt',
     ⇒  `regexp(<<[$1]>>, <<[^\(\w\)\(\w*\)]>>,
     ⇒    <<[_upcase_alt(<<[<<[\1]>>]>>)_downcase_alt(<<[<<[\2]>>]>>)]>>)')
     ⇒define(`capitalize',
     ⇒  `_arg1(_to_alt()patsubst(<<[<<[$*]>>]>>, <<[\w+]>>,
     ⇒    _from_alt()`]>>_$0_alt(<<[\&]>>)<<['_to_alt())_from_alt())')
     ⇒divert`'dnl