From bd1d554a35b543afa5a79dd483583bf2aad9cf47 Mon Sep 17 00:00:00 2001
From: Luke Shumaker <lukeshu@sbcglobal.net>
Date: Fri, 15 Apr 2016 17:06:52 -0400
Subject: libremessages:flag: Fix several things (also librexgettext)

 - Fix a bug where it panicked if given an odd number of sub-headings.
 - Document the the ability to include sub-headings.
 - Fix librexgettext's handling of it.  It only worked correctly for times
   when it was only called once in a program, or when it was only ever
   called with exactly one flag/description pair (and no headings).
---
 src/lib/libremessages.1.ronn |   8 +++-
 src/lib/librexgettext        | 101 +++++++++++++++++++++++++++++++++----------
 src/lib/messages.sh          |  12 +++--
 3 files changed, 92 insertions(+), 29 deletions(-)

(limited to 'src')

diff --git a/src/lib/libremessages.1.ronn b/src/lib/libremessages.1.ronn
index 9a976a8..d4fac85 100644
--- a/src/lib/libremessages.1.ronn
+++ b/src/lib/libremessages.1.ronn
@@ -101,7 +101,7 @@ routines by default.
      Similar to `prose`, but prints a bullet point before the first
      line, and indents the remaining lines.
 
-   * `flag` <FLAG> <DESCRIPTION> [<FLAG2> <DESCRIPTION2>...]:
+   * `flag` [<FLAG> <DESCRIPTION>|<HEADING>:]...:
      Print a flag and description formatted for `--help` text.  For
      example:<br>
      `flag '-N' 'Disable networking in the chroot'`<br>
@@ -113,7 +113,11 @@ routines by default.
      whitespace-collapsed (so newlines are stripped), then it is
      re-word-wrapped, in the same way as `prose` and `bullet`.  If you
      pass in multiple flag/description pairs to the same invocation,
-     the descriptions are all aligned together.
+     the descriptions are all aligned together.  The ability to do
+     insert headings without resetting the alignment is the motivation
+     for also allowing headings to be in the list.  In order to tell
+     the difference between a flag and a heading, a heading must end
+     with a colon (':'), and a flag must not.
 
 ### NOTIFICATION ROUTINES
 
diff --git a/src/lib/librexgettext b/src/lib/librexgettext
index 22a6df2..c82b9ae 100755
--- a/src/lib/librexgettext
+++ b/src/lib/librexgettext
@@ -65,49 +65,104 @@ xgettext-sh() {
 
 xgettext-flag() {
 	{
-		declare -i x=0
+		# Stage 1: Generate
+		#
+		# Get all of the arguments to `flag`.  Because `flag`
+		# takes an arbitrary number of arguments, just iterate
+		# through arg1, arg2, ... argN; until we've come up
+		# empty 3 times.  Why 3?  Because each flag takes 2
+		# arguments, and because we don't keep track of which
+		# one of those we're on, waiting for 3 empties ensures
+		# us that we've had a complete "round" with nothing.
+		#
+		# Why can't I just do i+=2, and not have to keep track
+		# of empties?  Because, we also allow for arguments
+		# ending in a colon to be headings, which changes the
+		# offsets.
+		declare -i empties=0
 		declare -i i
-		for (( i=1; x < 3; i++ )); do
+		for (( i=1; empties < 3; i++ )); do
 			local out
 			out="$(xgettext-sh --keyword="flag:$i,\"$i\"" "$@")"
 			if [[ -n $out ]]; then
 				printf -- '%s\n' "$out"
 			else
-				x+=1
+				empties+=1
 			fi
 		done
 	} | whitespace-collapse | sed '/^\#, sh-format/d' | {
-		declare -i i
-
-		IFS=''
-		local segments=()
+		# Stage 2: Parse
+		#
+		# Read in the lines, and group them into an array of
+		# (multi-line) msgs.  This just makes working with
+		# them easier.
+		local msgs=()
+		declare -i i=-1
 		local re='^#\. ([0-9]+)$'
+		IFS=''
 		local line
 		while read -r line; do
 			if [[ $line =~ $re ]]; then
-				i=${BASH_REMATCH[1]}
-			else
-				segments[$i]+="$line"$'\n'
+				i+=1
 			fi
+			msgs[$i]+="$line"$'\n'
 		done
-
-		declare -i last=$(printf '%s\n' "${!segments[@]}"|sed -n '$p')
-
+		# Stage 3: Sort
+		#
+		# Now, we have the `msgs` array, and it is
+		# sorted such that it is all of the arg1's to `flag`,
+		# then all of the arg2's, then all of the arg3's, and
+		# so on.  We want to re-order them such that it's all
+		# of the args for the first invocation then all of the
+		# args for the second; and so on.
+		#
+		# We do this by simply sorting them by the location
+		# that they appear in the file.  Then, when we see the
+		# argument number go back down to 1, we know that a
+		# new invocation has started!
 		IFS=$'\n'
-		local flag=true
-		for (( i=1; i <= last; i++ )); do
-			if $flag; then
-				local lines=(${segments[$i]})
+		local locations=($(
+			local i
+			for i in "${!msgs[@]}"; do
+				declare -i arg row
+				local lines=(${msgs[$i]})
+				arg=${lines[0]#'#. '}
+				row=${lines[1]##*:}
+				printf '%d.%d %d\n' "$row" "$arg" "$i"
+			done | sort -n
+		))
+		# Stage 4: Output
+		#
+		# Now, we prune out the arguments that aren't
+		# localizable.  Also, remove the "#." comment lines.
+		# As explained above (in stage 3), when we see $arg go
+		# to 1, that's the beginning of a new invocation.
+		local expectflag=true
+		local location
+		for location in "${locations[@]}"; do
+			IFS=' .'
+			local row arg i
+			read -r row arg i <<<"$location"
+			local msg="${msgs[$i]#*$'\n'}"
+			# Now we operate based on $row, $arg, and $msg
+			if [[ $arg == 1 ]]; then
+				expectflag=true
+			fi
+			if $expectflag; then
+				IFS=$'\n'
+				local lines=(${msg})
 				if [[ ${lines[1]} == *':"' ]]; then
-					printf -- '%s\n' "${segments[$i]}"
+					# We expected a flag, but got
+					# a heading
+					printf -- '%s\n' "$msg"
 				else
-					flag=false
+					# We expected a flag, and got
+					# one!
+					expectflag=false
 				fi
 			else
-				if [[ -n ${segments[$i]} ]]; then
-					printf -- '%s\n' "${segments[$i]}"
-				fi
-				flag=true
+				printf -- '%s\n' "$msg"
+				expectflag=true
 			fi
 		done
 	}
diff --git a/src/lib/messages.sh b/src/lib/messages.sh
index 4b4897e..0125003 100644
--- a/src/lib/messages.sh
+++ b/src/lib/messages.sh
@@ -92,14 +92,15 @@ bullet() {
 	printf -- "$mesg" "$@" | fmt -u -w 71 | sed -e '1s/^/  - /' -e '2,$s/^/    /'
 }
 
-# Usage: flag FLAG DESCRIPTION [FLAG2 DESCRIPTION2...]
+# Usage: flag [FLAG DESCRIPTION|HEADING:]...
 #
 # Print a flag and description formatted for --help text.
 #
 #   ex: flag '-C <FILE>' 'Use this file instead of pacman.conf'
 #
-# The description is fed through gettext, the flag is not, so if part
-# of the flag needs to be translated, you must do that yourself:
+# The descriptions and headings are fed through gettext, the flags ar
+# not, so if part of a flag needs to be translated, you must do that
+# yourself:
 #
 #   ex: flag "-C <$(_ FILE)>" 'Use this file instead of pacman.conf'
 #
@@ -107,8 +108,10 @@ bullet() {
 # crazy-long, feel free, it is reflowed/wrapped the same way as prose
 # and bullet.  If you pass in multiple flag/description pairs at once,
 # the descriptions are all alligned together.
+#
+# A heading MUST end with a colon (':'), this is how it knows that it
+# is a heading.  Similarly, a flag MUST NOT end with a colon.
 flag() {
-	[[ $# == $(($#/2*2)) ]] || panic
 	local args=("$@")
 
 	declare -i flaglen=0
@@ -147,6 +150,7 @@ flag() {
 			printf -- ' %s\n' "$(_ "$1")"
 			shift
 		else
+			[[ $# -gt 1 ]] || panic
 			local flag=$1
 			local desc="$(_ "$(whitespace_collapse <<<"$2")")"
 			shift 2
-- 
cgit v1.2.3-2-g168b