m4: Improved capitalize

1 
1 17.7 Solution for 'capitalize'
1 ==============================
1 
1 The 'capitalize' macro (⇒Patsubst) as presented earlier does not
1 allow clients to follow the quoting rule of thumb.  Consider the three
1 macros 'active', 'Active', and 'ACTIVE', and the difference between
1 calling 'capitalize' with the expansion of a macro, expanding the result
1 of a case change, and changing the case of a double-quoted string:
1 
1      $ m4 -I examples
1      include(`capitalize.m4')dnl
1      define(`active', `act1, ive')dnl
1      define(`Active', `Act2, Ive')dnl
1      define(`ACTIVE', `ACT3, IVE')dnl
1      upcase(active)
1      =>ACT1,IVE
1      upcase(`active')
1      =>ACT3, IVE
1      upcase(``active'')
1      =>ACTIVE
1      downcase(ACTIVE)
1      =>act3,ive
1      downcase(`ACTIVE')
1      =>act1, ive
1      downcase(``ACTIVE'')
1      =>active
1      capitalize(active)
1      =>Act1
1      capitalize(`active')
1      =>Active
1      capitalize(``active'')
1      =>_capitalize(`active')
1      define(`A', `OOPS')
1      =>
1      capitalize(active)
1      =>OOPSct1
1      capitalize(`active')
1      =>OOPSctive
1 
1    First, when 'capitalize' is called with more than one argument, it
1 was throwing away later arguments, whereas 'upcase' and 'downcase' used
1 '$*' to collect them all.  The fix is simple: use '$*' consistently.
1 
1    Next, with single-quoting, 'capitalize' outputs a single character, a
1 set of quotes, then the rest of the characters, making it impossible to
1 invoke 'Active' after the fact, and allowing the alternate macro 'A' to
1 interfere.  Here, the solution is to use additional quoting in the
1 helper macros, then pass the final over-quoted output string through
1 '_arg1' to remove the extra quoting and finally invoke the concatenated
1 portions as a single string.
1 
1    Finally, when passed a double-quoted string, the nested macro
1 '_capitalize' is never invoked because it ended up nested inside quotes.
1 This one is the toughest to fix.  In short, we have no idea how many
1 levels of quotes are in effect on the substring being altered by
1 'patsubst'.  If the replacement string cannot be expressed entirely in
1 terms of literal text and backslash substitutions, then we need a
1 mechanism to guarantee that the helper macros are invoked outside of
11 quotes.  In other words, this sounds like a job for 'changequote' (⇒
 Changequote).  By changing the active quoting characters, we can
1 guarantee that replacement text injected by 'patsubst' always occurs in
1 the middle of a string that has exactly one level of over-quoting using
1 alternate quotes; so the replacement text closes the quoted string,
1 invokes the helper macros, then reopens the quoted string.  In turn,
1 that means the replacement text has unbalanced quotes, necessitating
1 another round of 'changequote'.
1 
1    In the fixed version below, (also shipped as
1 'm4-1.4.18/examples/capitalize2.m4'), 'capitalize' uses the alternate
1 quotes of '<<[' and ']>>' (the longer strings are chosen so as to be
1 less likely to appear in the text being converted).  The helpers
1 '_to_alt' and '_from_alt' merely reduce the number of characters
1 required to perform a 'changequote', since the definition changes twice.
1 The outermost pair means that 'patsubst' and '_capitalize_alt' are
1 invoked with alternate quoting; the innermost pair is used so that the
1 third argument to 'patsubst' can contain an unbalanced ']>>'/'<<[' pair.
1 Note that 'upcase' and 'downcase' must be redefined as '_upcase_alt' and
1 '_downcase_alt', since they contain nested quotes but are invoked with
1 the alternate quoting scheme in effect.
1 
1      $ m4 -I examples
1      include(`capitalize2.m4')dnl
1      define(`active', `act1, ive')dnl
1      define(`Active', `Act2, Ive')dnl
1      define(`ACTIVE', `ACT3, IVE')dnl
1      define(`A', `OOPS')dnl
1      capitalize(active; `active'; ``active''; ```actIVE''')
1      =>Act1,Ive; Act2, Ive; Active; `Active'
1      undivert(`capitalize2.m4')dnl
1      =>divert(`-1')
1      =># upcase(text)
1      =># downcase(text)
1      =># capitalize(text)
1      =>#   change case of text, improved version
1      =>define(`upcase', `translit(`$*', `a-z', `A-Z')')
1      =>define(`downcase', `translit(`$*', `A-Z', `a-z')')
1      =>define(`_arg1', `$1')
1      =>define(`_to_alt', `changequote(`<<[', `]>>')')
1      =>define(`_from_alt', `changequote(<<[`]>>, <<[']>>)')
1      =>define(`_upcase_alt', `translit(<<[$*]>>, <<[a-z]>>, <<[A-Z]>>)')
1      =>define(`_downcase_alt', `translit(<<[$*]>>, <<[A-Z]>>, <<[a-z]>>)')
1      =>define(`_capitalize_alt',
1      =>  `regexp(<<[$1]>>, <<[^\(\w\)\(\w*\)]>>,
1      =>    <<[_upcase_alt(<<[<<[\1]>>]>>)_downcase_alt(<<[<<[\2]>>]>>)]>>)')
1      =>define(`capitalize',
1      =>  `_arg1(_to_alt()patsubst(<<[<<[$*]>>]>>, <<[\w+]>>,
1      =>    _from_alt()`]>>_$0_alt(<<[\&]>>)<<['_to_alt())_from_alt())')
1      =>divert`'dnl
1