Subject: Re: bug#25540: notice issue in expand -- doesn't allow for expressing tabsize value in tabstop(s)

From: Pádraig Brady <[email-masked]>
Date: Sun, 26 Feb 2017 15:44:18 -0800

On 26/01/17 03:27, Reuti wrote:
> 
>> Am 26.01.2017 um 12:04 schrieb Pádraig Brady <[email-masked]>:
>>
>> On 26/01/17 10:26, Reuti wrote:
>>>
>>>> Am 26.01.2017 um 05:29 schrieb L A Walsh <[email-masked]>:
>>>>
>>>>
>>>> In programs that take tabstops, as an alternative to a tabsize, I've always
>>>> seen tabs beyond the end of the list taken as equal to the highest tab-stop
>>>> difference.  So for a tabsize=8, a tabset of 1,9 would be equivalent -- with
>>>> tabs above "9" being "9-1" or every 8th column above 9.
>>>>
>>>> Otherwise you have no way of expression all tabs on a line that stretches out to
>>>> "???" 160? 240? what? other than to enumerate tabstops to infinity.
>>>>
>>>> If they want to limit tabstops above the last to size "1", they can use something like 1,9,10.  How else can one specify tabs beyond the last
>>>> for a size other than "1"?
>>>>
>>>> Could this be changed/fixed?
>>>
>>> For now the behavior is like specified on the info page: "[…] and replace any tabs beyond the last tab stop given with single spaces." To avoid that this gets broken, I would suggest to use a modified syntax like 1,9,30,34,/4 for using a width of 4 beyond 34.
>>>
>>
>> I like that. Explicit and extensible.
>> An alternative could be: --tabs=1,9,30,34,+4 ?
> 
> Sure, this works too. In fact: I had this also in mind first, but with the extension to specify a multiplicator for the width I found "2+4" misleading and a slash more appropriate.
> 

Even without the multiplier a '+' would be misleading
since we wouldn't be necessarily adding that to the last value.

The attached uses your suggestion of '/' but only allowed in the last position.

thanks,
Pádraig.

>From cb158f2a7458ddc793cb81fec429b571e33f302f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?P=C3=A1draig=20Brady?= <[email-masked]>
Date: Sun, 26 Feb 2017 14:40:19 -0800
Subject: [PATCH] expand,unexpand: support specifying a trailing tab size

* doc/coreutils.texi (expand invocation): Document the feature.
(unexpand invocation): Likewise.
* src/expand-common.c (extend_size): A new global to use
when the last tab stop is prefixed by '/'.
(set_extend_size): A new function to validate and set
the new extend_size global.
(parse_tab_stops): Call set_extend_size() for '/' prefixes.
(finalize_tab_stops): Ensure a single specified '/' is
treated like a standard tabsize, but also ensure that
when '/' is specified with a single other entry that
we process as a list rather than a tab size.
(get_next_tab_stop): Use the tab size if set,
for items after the user specified tab position list.
* tests/misc/expand.pl: Add test cases
* NEWS: Mention the new feature.
Fixes http://bugs.gnu.org/25540
---
 NEWS                 |  6 +++++
 doc/coreutils.texi   |  8 +++++--
 src/expand-common.c  | 68 +++++++++++++++++++++++++++++++++++++++++++++-------
 tests/misc/expand.pl | 20 +++++++++++++++-
 4 files changed, 91 insertions(+), 11 deletions(-)

diff --git a/NEWS b/NEWS
index 11931a4..1fc92c8 100644
--- a/NEWS
+++ b/NEWS
@@ -58,6 +58,12 @@ GNU coreutils NEWS                                    -*- outline -*-
   'cp -fl A B' no longer remove B before creating the new link.
   That is, there is no longer a brief moment when B does not exist.
 
+** New features
+
+  expand and unexpand now support specifying a tab size to use
+  after explicitly specified tab stops, by prefixing the last
+  specified number like --tabs=2,4,/8.
+
 
 * Noteworthy changes in release 8.26 (2016-11-30) [stable]
 
diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index d72760b..ed644c7 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -6696,7 +6696,9 @@ If only one tab stop is given, set the tabs @var{tab1} spaces apart
 (default is 8).  Otherwise, set the tabs at columns @var{tab1},
 @var{tab2}, @dots{} (numbered from 0), and replace any tabs beyond the
 last tab stop given with single spaces.  Tab stops can be separated by
-blanks as well as by commas.
+blanks as well as by commas.  As a GNU extension the last @var{tab} specified
+can be prefixed with a @samp{/} to indicate a tab size to use for
+remaining positions.
 
 For compatibility, GNU @command{expand} also accepts the obsolete
 option syntax, @option{-@var{t1}[,@var{t2}]@dots{}}.  New scripts
@@ -6749,7 +6751,9 @@ If only one tab stop is given, set the tabs @var{tab1} columns apart
 instead of the default 8.  Otherwise, set the tabs at columns
 @var{tab1}, @var{tab2}, @dots{} (numbered from 0), and leave blanks
 beyond the tab stops given unchanged.  Tab stops can be separated by
-blanks as well as by commas.  This option implies the @option{-a} option.
+blanks as well as by commas.  As a GNU extension the last @var{tab} specified
+can be prefixed with a @samp{/} to indicate a tab size to use for
+remaining positions.  This option implies the @option{-a} option.
 
 For compatibility, GNU @command{unexpand} supports the obsolete option syntax,
 @option{-@var{tab1}[,@var{tab2}]@dots{}}, where tab stops must be
diff --git a/src/expand-common.c b/src/expand-common.c
index ae2b979..9b6cdb9 100644
--- a/src/expand-common.c
+++ b/src/expand-common.c
@@ -34,6 +34,9 @@ bool convert_entire_line = false;
 /* If nonzero, the size of all tab stops.  If zero, use 'tab_list' instead.  */
 static uintmax_t tab_size = 0;
 
+/* If nonzero, the size of all tab stops after the last specifed.  */
+static uintmax_t extend_size = 0;
+
 /* The maximum distance between tab stops.  */
 size_t max_column_width;
 
@@ -85,14 +88,32 @@ add_tab_stop (uintmax_t tabval)
     }
 }
 
+static bool
+set_extend_size (uintmax_t tabval)
+{
+  bool ok = true;
+
+  if (extend_size)
+    {
+      error (0, 0,
+             _("'/' specifier only allowed"
+               " with the last value"));
+      ok = false;
+    }
+  extend_size = tabval;
+
+  return ok;
+}
+
 /* Add the comma or blank separated list of tab stops STOPS
    to the list of tab stops.  */
 extern void
 parse_tab_stops (char const *stops)
 {
   bool have_tabval = false;
-  uintmax_t tabval IF_LINT ( = 0);
-  char const *num_start IF_LINT ( = NULL);
+  uintmax_t tabval = 0;
+  bool extend_tabval = false;
+  char const *num_start = NULL;
   bool ok = true;
 
   for (; *stops; stops++)
@@ -100,9 +121,31 @@ parse_tab_stops (char const *stops)
       if (*stops == ',' || isblank (to_uchar (*stops)))
         {
           if (have_tabval)
-            add_tab_stop (tabval);
+            {
+              if (extend_tabval)
+                {
+                  if (! set_extend_size (tabval))
+                    {
+                      extend_tabval = false;
+                      ok = false;
+                      break;
+                    }
+                }
+              else
+                add_tab_stop (tabval);
+            }
           have_tabval = false;
         }
+      else if (*stops == '/')
+        {
+          if (have_tabval)
+            {
+              error (0, 0, _("'/' specifier not at start of number: %s"),
+                     quote (stops));
+              ok = false;
+            }
+          extend_tabval = true;
+        }
       else if (ISDIGIT (*stops))
         {
           if (!have_tabval)
@@ -132,11 +175,16 @@ parse_tab_stops (char const *stops)
         }
     }
 
+  if (have_tabval)
+    {
+      if (extend_tabval)
+        ok &= set_extend_size (tabval);
+      else
+        add_tab_stop (tabval);
+    }
+
   if (!ok)
     exit (EXIT_FAILURE);
-
-  if (have_tabval)
-    add_tab_stop (tabval);
 }
 
 /* Check that the list of tab stops TABS, with ENTRIES entries,
@@ -172,8 +220,8 @@ finalize_tab_stops (void)
   validate_tab_stops (tab_list, first_free_tab);
 
   if (first_free_tab == 0)
-    tab_size = max_column_width = 8;
-  else if (first_free_tab == 1)
+    tab_size = max_column_width = extend_size ? extend_size : 8;
+  else if (first_free_tab == 1 && ! extend_size)
     tab_size = tab_list[0];
   else
     tab_size = 0;
@@ -199,6 +247,10 @@ get_next_tab_column (const uintmax_t column, size_t* tab_index,
             return tab;
     }
 
+  /* relative last tab - return multiples of it */
+  if (extend_size)
+    return column + (extend_size - column % extend_size);
+
   *last_tab = true;
   return 0;
 }
diff --git a/tests/misc/expand.pl b/tests/misc/expand.pl
index aa8341c..b04d2e7 100755
--- a/tests/misc/expand.pl
+++ b/tests/misc/expand.pl
@@ -39,9 +39,10 @@ my @Tests =
    ['t5', '--tabs=""',        {IN=>"a\tb\tc"}, {OUT=>"a       b       c"}],
    ['t6', '--tabs=","',       {IN=>"a\tb\tc"}, {OUT=>"a       b       c"}],
    ['t7', '--tabs=" "',       {IN=>"a\tb\tc"}, {OUT=>"a       b       c"}],
+   ['t8', '--tabs="/"',       {IN=>"a\tb\tc"}, {OUT=>"a       b       c"}],
 
    # Input field wider than the specified tab list
-   ['t8', '--tabs=6,9', {IN=>"a\tbbbbbbbbbbbbb\tc"},
+   ['if', '--tabs=6,9', {IN=>"a\tbbbbbbbbbbbbb\tc"},
     {OUT=>"a     bbbbbbbbbbbbb c"}],
 
    ['i1', '--tabs=3 -i', {IN=>"\ta\tb"}, {OUT=>"   a\tb"}],
@@ -138,6 +139,17 @@ my @Tests =
     {OUT=>"1 2 3 4   5\n" .
           "a bHELLO\b\b\b c   d e\n"}],
 
+   # Test the trailing '/' feature which specifies the
+   # tab size to use after the last specified stop
+   ['trail1', '--tabs=1,/5',   {IN=>"\ta\tb\tc"}, {OUT=>" a   b    c"}],
+   ['trail2', '--tabs=2,/5',   {IN=>"\ta\tb\tc"}, {OUT=>"  a  b    c"}],
+   ['trail3', '--tabs=1,2,/5', {IN=>"\ta\tb\tc"}, {OUT=>" a   b    c"}],
+   ['trail4', '--tabs=/5',     {IN=>"\ta\tb"},    {OUT=>"     a    b"}],
+   ['trail5', '--tabs=//5',    {IN=>"\ta\tb"},    {OUT=>"     a    b"}],
+   ['trail6', '--tabs=/,/5',   {IN=>"\ta\tb"},    {OUT=>"     a    b"}],
+   ['trail7', '--tabs=,/5',    {IN=>"\ta\tb"},    {OUT=>"     a    b"}],
+   ['trail8', '--tabs=1 -t/5', {IN=>"\ta\tb\tc"}, {OUT=>" a   b    c"}],
+   ['trail9', '--tab=1,2 -t/5',{IN=>"\ta\tb\tc"}, {OUT=>" a   b    c"}],
 
    # Test errors
    ['e1', '--tabs="a"', {IN=>''}, {OUT=>''}, {EXIT=>1},
@@ -148,6 +160,12 @@ my @Tests =
     {ERR => "$prog: tab size cannot be 0\n"}],
    ['e4', '--tabs=3,3', {IN=>''}, {OUT=>''}, {EXIT=>1},
     {ERR => "$prog: tab sizes must be ascending\n"}],
+   ['e5', '--tabs=/3,6,8', {IN=>''}, {OUT=>''}, {EXIT=>1},
+    {ERR => "$prog: '/' specifier only allowed with the last value\n"}],
+   ['e6', '-t/3 -t/6', {IN=>''}, {OUT=>''}, {EXIT=>1},
+    {ERR => "$prog: '/' specifier only allowed with the last value\n"}],
+   ['e7', '--tabs=3/', {IN=>''}, {OUT=>''}, {EXIT=>1},
+    {ERR => "$prog: '/' specifier not at start of number: '/'\n"}],
   );
 
 my $save_temps = $ENV{DEBUG};
-- 
2.5.5