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