View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0001974 | 1003.1(2024)/Issue8 | Shell and Utilities | public | 2026-03-11 15:00 | 2026-03-22 00:13 |
| Reporter | mkoskar | Assigned To | |||
| Priority | normal | Severity | Objection | Type | Error |
| Status | New | Resolution | Open | ||
| Name | Miroslav Koškár | ||||
| Organization | |||||
| User Reference | |||||
| Section | 3 Utilities / tabs | ||||
| Page Number | 3415 | ||||
| Line Number | 116444-116446 | ||||
| Interp Status | |||||
| Final Accepted Text | |||||
| Summary | 0001974: tabs: wrong description of semantics of arbitrary tab stops | ||||
| Description | To get tab stops with a standard interval of 8: $ tabs -8 Analogous to that when arbitrary tab stops are used: $ tabs 1,9,17,25 Tab stop value: * is > 0 * represents a column (left-most being 1) where cursor moves and output occurs after preceding \t character * as a special case, the 1st tab stop value is always 1 (whether explicitly specified or not) to demonstrate that without preceding \t character output occurs in the left-most column The above is true for most common implementation of tabs from ncurses. I believe that indicated sentence: > The phrase "tab-stop position N" shall be taken to mean that, from the start of a line of output, tabbing to position N shall cause the next character output to be in the (N+1)th column position on that line. is wrong with regards to what actual semantics of operand(s) is. For additional context, this came up in following: * https://github.com/gwsw/less/issues/737 I've tried to summarize in following comment: * https://github.com/gwsw/less/issues/737#issuecomment-4033517061 Further of note are comments by @avih who were instrumental in tracking down historical sources of tabs and its documentation. | ||||
| Desired Action | Remove following sentence from the DESCRIPTION section: > The phrase "tab-stop position N" shall be taken to mean that, from the start of a line of output, tabbing to position N shall cause the next character output to be in the (N+1)th column position on that line. In OPERANDS section add following lines (demarcated with +++): n[[sep[+]n]...] A single command line argument that consists of one or more tab-stop values (n) separated by a separator character (sep) which is either a <comma> or a <blank> character. The application shall ensure that the tab-stop values are positive decimal integers in strictly ascending order. +++ A tab-stop value shall be taken to mean a column (left-most being 1) where cursor moves and output occurs after preceding \t character. The 1st tab-stop value is always 1 (whether explicitly specified or not) to demonstrate that without preceding \t character output occurs in the left-most column. +++ If any tab-stop value (except the first one) is preceded by a <plus-sign>, it is taken as an increment to be added to the previous value. For example, the tab lists 1,10,20,30 and "1 10 +10 +10" are considered to be identical. | ||||
| Tags | No tags attached. | ||||
|
|
Correction: I meant to reference following comment https://github.com/gwsw/less/issues/737#issuecomment-4030159904 instead. |
|
|
I think the terms left and right should be avoided, since such directions do not account for right-to-left scripts (in tbe Unicode sense of the term). "Beginning of line" and "end of line" might be better? |
|
|
Column-1 is the first available position for a tab stop on a line. An (ECMA-48 compatible) terminal can be set up so that the first tab stop on a line is not on the first column. The tabs program is documented to take this into account. Given that (and spelling), the sentence "The 1st tab-stop value is always 1 (whether explicitly specified or not) to demonstrate that without preceding \t character output occurs in the left-most column." needs some revision. Rather than removing the obscurely-worded sentence with "tabbing to position N", it should be revised to explain that tab-stops are 1-based, and that while the interval between stops may be 8, the 1-based detail makes the stops one column past the simple multiple of the tab interval. (making that both concise and clear doesn't look simple -- but the existing description is concise) |
|
|
My main objection to the sentence > The phrase "tab-stop position N" shall be taken to mean that, from the start > of a line of output, tabbing to position N shall cause the next character output > to be in the (N+1)th column position on that line. is that it implies that doing e.g., $ N=9 ; tabs 1,$N ; printf '\tX\n' causes the letter "X" to be printed in the column (N+1)=10, instead of 9 (which is in fact what happens). So in keeping changes as minimal as possible, an alternative fix is amending the above sentence and replacing "(N+1)th column" with "Nth column". Note that the reason why the variation of above sentence works for expand/unexpand is that "-t tablist" option in those utilities takes 0-based tab stops but here tab stops are 1-based as @dickey also pointed out. |
|
|
Regarding "expand" versus 1-based or 0-based, it states in https://pubs.opengroup.org/onlinepubs/9799919799/utilities/expand.html Each tab stop N shall be an integer value greater than zero, and the list is in strictly ascending order. which is 1-based. |
|
|
Regarding the tabbing to position N, I noticed an analogous issue in ECMA-48 section 8.3.61 (HTJ - character tabulation with justification) which describes behavior before the tab stop. The original authors of the document may have had something like that in mind, distinguish the tab movement (all whitespace) from the position where the next character would be printed. Since (without being familiar with terminals at that level), readers can get confused, the goal here seems to be to rephrase things in clearer language without changing the underlying technical content. |
|
|
> which is 1-based. Ah right, granted, I should have said as-if-0-based, at least that's how I think about it. Alternatively one can look at it such that "tab stop N" is either where: 1) N refers to column on which the next character should be printed. or 2) N refers to column after which the next character should printed. With regards to difference of e.g., `tabs 1,9,17` and `tabs 9,17` I stand corrected. I assumed 1 is always implied. Investigating its output e.g., via `xxd` or `od` reveals that latter does not put a tab stop on 1. |
|
|
ECMA-48 https://ecma-international.org/publications-and-standards/standards/ecma-48/ standard for terminal controls (including cursor movement and tab stops) numbers columns starting at 1. The (presumably) AT&T people who documented tabs had that in mind. The tab stops are the columns on which the first character is printed after tabbing to that column. There's some possible confusion over what happens with the column just before the tab stop (because it may appear that the the tab "must" go there. But it doesn't happen that way. The terminal simply skips over the intervening columns (without modifying them) to prepare to show a printable character at the tab stop. (If something other than a printable character follows, that's outside the scope of this document, and is "unspecified"). |
|
|
Rather than removing the paragraph with the problematic N+1, rewriting it to something like this: Tab stops are a list of one or more decimal integers, each greater than zero, in ascending order. The tabs utility allows the user to specify tab stops either by a uniform interval via a single option, or by an arbitrary list with one or more option values. Tab stop 1 corresponds to the start of a line of output. If tab stops are specified by a uniform interval N, a <tab> character output at the start of a line of output will cause the next character output to be in position N+1. The maximum number of tab stops allowed is terminal-dependent. |
|
|
The suggested addtion "A tab-stop value shall be taken to mean a column (left-most" does not appear to provide further clarification than the suggested rewrite of the paragraph. |
|
|
Hi, and thanks @mkoskar for opening this issue. Here's a recap of my current conclusions. The full research can be examined at the "less" issue 737 on github, linked at the first post here. - "tabs" first appeared in unix v7, but the "n1,n2,..." argument support was added in 1982 for unix v8 (2.9BSD and later had a copy from unix-v7 without n1,n2..., which was removed with AT&T/BSD settlement. I could not find evidence for different versions of "tabs"). - The behavior of "tabs n1,n2,..." did not change since it first appeared, and the current implementations (FreeBSD, ncurses) still behave the same. The issue is only how this behavior is currently documented. - The first official documentation appeared at AT&T SVID 1 (1983), with the only meaningful change after that is an addition of few (important) examples at SVID 2 around 1985. It was not changed meaningfully after that, and SVR4 and Solaris include the same docs with the same examples. - The examples added in 1985 do match the historic versions behavior (I compiled and tested the unix v8 and SVR4.2 "tabs.c"). - "tabs" was first documented in XPG at issue 2 (1986). I didn't find a copy of that, but at issue 4 (SUSv1 - 1994) it already appears like it is today (I didn't compare char by char, but the issue discussed here already existed then). There were no examples in 1994, and there are still no examples today. The important examples which were added to SVID 2 around 1985 are: The next command is an example of using -n (repetitive specification), where n is 8, causes tabs to be set every eighth position: 1+(1*8), 1+(2*8), ... which evaluate to columns 9, 17, ...: example: tabs -8 This command uses n1,n2,... (arbitrary specification) to set tabs at columns 1, 8, and 36: example: tabs 1,8,36 Summary: "tabs -n" sets the tab stops at columns 1+n 1+2*n 1+3*n ... "tabs" is the same as "tabs -8" "tabs n1,n2,n3..." sets the tabs stops at exactly columns n1 n2 n3 ... "tabs 9,17,25,33,..." is the same as "tabs -8".(historically - see note below) Where "column" always counts from 1 - which is always the leftmost terminal display column. So it appears that the current "(N+1)" docs comment roughly sort of applies only to the "repetitive specifications" (-n), and it should not apply to the "n1,n2,..." form, but it's hard to understand that currently. As for the actual change which we want, I'd think examples could be very useful, possibly with wording that the "n1,n2,..." form sets the explicit stops at the list values, while the "-n" form sets the stops according to the formula 1+1*n 1+2*n 1+3*n ... Note: There's one difference between the historic and current implementations of tabs, where "tabs" and "tabs -8" (and by extension any -n) historically set the 1st tabstop at column 9 (n+1), while current implementations also add an initial stop at column 1 (and then 9... - same as the historic). This was observed using using "TERM=... tabs -8 | od -c" . I don't have an opinion on whether or how this should be clarified in the docs. There's no difference between the historic and current implementations with the form "n1,n2,n3..." - which always set the stops at exactly the listed columns. |
|
|
Putting a tab stop at the first column is not necessary for a standard terminal's functionality. The current versions of tabs do this, but the reason for it may have just been for simplicity of a for-loop, or it may have been influenced by the predefined lists which begin with column 1. In any case, FreeBSD (2002), Solaris 10 (2005), ncurses (2008) do set a tab stop on column 1. The MacOS tabs has some differences in the unspecified "tabs 9 16 23" vs "tabs '9 16 23'" which differs from current FreeBSD, but does set a tab stop on column 1. Given that, mentioning that an implementation *might* set a tab stop on column 1 would be appropriate for the discussion text, but perhaps not in the initial sections where it might be construed as a requirement. |
|
|
> In any case, FreeBSD (2002), Solaris 10 (2005), ncurses (2008) do set a tab stop on column 1. I don't have access to Solaris 10, but in OmniOS r151046 (2023, illumos-based), "man tabs" show the same two examples I quoted above - where "tabs -8" sets the first stop at column 9, and the behavior does match the documentation as far as I can tell: <code> ~$ uname -a SunOS omniosce 5.11 omnios-r151046-8ab991ea831 i86pc i386 i86pc ~$ TERM=vt100 tabs -8 | xxd | head -4 00000000: 0d1b 5b33 6720 2020 2020 2020 201b 4820 ..[3g .H 00000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 00000020: 201b 4820 2020 2020 2020 201b 4820 2020 .H .H 00000030: 2020 2020 201b 4820 2020 2020 2020 201b .H . </code> It starts the same as "tabs 9,17..." (1st stop at column 9): <code> ~$ TERM=vt100 tabs 9,17,25,33 | xxd | head -4 00000000: 0d1b 5b33 6720 2020 2020 2020 201b 4820 ..[3g .H 00000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 00000020: 201b 4820 2020 2020 2020 201b 480d .H .H. </code> This is evident compared to "tabs 1,9,17,25,33" - which does set the first stop at column 1: <code> ~$ TERM=vt100 tabs 1,9,17,25,33 | xxd | head -4 00000000: 0d1b 5b33 671b 4820 2020 2020 2020 201b ..[3g.H . 00000010: 4820 2020 2020 2020 201b 4820 2020 2020 H .H 00000020: 2020 201b 4820 2020 2020 2020 201b 480d .H .H. </code> |
|
|
I was misreading a different type of output, but making a script to follow your example, here are results from my Solaris 10 machine: + uname -a SunOS vbx-solaris10 5.10 Generic_142910-17 i86pc i386 i86pc + /usr/bin/tabs -8 TERM=vt100 + xxd + head -4 0000000: 0d1b 5b33 6720 2020 2020 2020 201b 4820 ..[3g .H 0000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 0000020: 201b 4820 2020 2020 2020 201b 4820 2020 .H .H 0000030: 2020 2020 201b 4820 2020 2020 2020 201b .H . + /usr/bin/tabs 9,17,25,33,41,49 TERM=vt100 + xxd + head -4 0000000: 0d1b 5b33 6720 2020 2020 2020 201b 4820 ..[3g .H 0000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 0000020: 201b 4820 2020 2020 2020 201b 4820 2020 .H .H 0000030: 2020 2020 201b 4820 2020 2020 2020 201b .H . and from MacOS + uname -a Darwin muskrat.jexium-island.net 24.6.0 Darwin Kernel Version 24.6.0: Mon Jan 19 22:00:10 PST 2026; root:xnu-11417.140.69.708.3~1/RELEASE_X86_64 x86_64 + TERM=vt100 + /usr/bin/tabs -8 + xxd + head -4 00000000: 0d1b 5b33 671b 4820 2020 2020 2020 201b ..[3g.H . 00000010: 4820 2020 2020 2020 201b 4820 2020 2020 H .H 00000020: 2020 201b 4820 2020 2020 2020 201b 4820 .H .H 00000030: 2020 2020 2020 201b 4820 2020 2020 2020 .H + TERM=vt100 + /usr/bin/tabs 9,17,25,33,41,49 + xxd + head -4 00000000: 0d1b 5b33 6720 2020 2020 2020 201b 4820 ..[3g .H 00000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 00000020: 201b 4820 2020 2020 2020 201b 4820 2020 .H .H 00000030: 2020 2020 201b 4820 2020 2020 2020 201b .H . as well as ncurses: + uname -a Linux prl-debianold-64.jexium-island.net 6.12.74+deb13+1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.74-2 (2026-03-08) x86_64 GNU/Linux + TERM=vt100 /usr/bin/tabs -8 + xxd + head -4 00000000: 1b5b 3367 0d1b 4820 2020 2020 2020 201b .[3g..H . 00000010: 4820 2020 2020 2020 201b 4820 2020 2020 H .H 00000020: 2020 201b 4820 2020 2020 2020 201b 4820 .H .H 00000030: 2020 2020 2020 201b 4820 2020 2020 2020 .H + TERM=vt100 /usr/bin/tabs 9,17,25,33,41,49 + xxd + head -4 00000000: 1b5b 3367 0d20 2020 2020 2020 201b 4820 .[3g. .H 00000010: 2020 2020 2020 201b 4820 2020 2020 2020 .H 00000020: 201b 4820 2020 2020 2020 201b 4820 2020 .H .H 00000030: 2020 2020 201b 4820 2020 2020 2020 201b .H . |
|
|
Thanks. So if I read it correctly, "tabs -8" sets 1st stop at column 9 in unix v8, SVR4.2, Solaris 10, OmniOS 5.11 . While the same command sets 1st stop at column 1 in ncurses, FreBSD, and macOS (uses the FreeBSD "tabs"?) Again, no opinion on what to document here, but it's clear that FreeBSD/ncurses diverged from AT&T/Solaris. I also agree it's confusing that all the presets set 1st stop at column 1 (both POSIX and AT&T docs), and some AT&T docs examples also show 1st stop at column 1. I don't know what it could mean in practice that column 1 has a tabstop. I don't have access to real hardware, and I didn't experimen with it in emulators. My guess is that emulators ignore tabstop at column 1, possibly implicitly (i.e. in the same way that if the cursor is at column 9, then tab would jump to column 17, "ignoring" the stop at column 9). |
|
|
I'm assuming that since the misfeature is roughly 20 years old, that real and emulated terminals don't do something bizarre like hang on that condition. Ignoring it seems consistent. |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-03-11 15:00 | mkoskar | New Issue | |
| 2026-03-11 15:10 | mkoskar | Note Added: 0007395 | |
| 2026-03-11 17:02 | wpollock | Note Added: 0007396 | |
| 2026-03-11 19:39 | dickey | Note Added: 0007397 | |
| 2026-03-11 21:56 | mkoskar | Note Added: 0007398 | |
| 2026-03-11 22:29 | dickey | Note Added: 0007399 | |
| 2026-03-11 22:40 | dickey | Note Added: 0007400 | |
| 2026-03-13 09:49 | mkoskar | Note Added: 0007403 | |
| 2026-03-14 01:15 | dickey | Note Added: 0007406 | |
| 2026-03-16 00:29 | dickey | Note Added: 0007407 | |
| 2026-03-16 08:05 | dickey | Note Added: 0007408 | |
| 2026-03-21 08:03 | avih | Note Added: 0007409 | |
| 2026-03-21 08:10 | avih | Note Edited: 0007409 | |
| 2026-03-21 10:46 | dickey | Note Added: 0007410 | |
| 2026-03-21 15:52 | avih | Note Added: 0007411 | |
| 2026-03-21 15:57 | avih | Note Edited: 0007411 | |
| 2026-03-21 16:31 | dickey | Note Added: 0007412 | |
| 2026-03-21 16:55 | avih | Note Edited: 0007411 | |
| 2026-03-21 18:24 | avih | Note Added: 0007413 | |
| 2026-03-21 18:32 | avih | Note Edited: 0007413 | |
| 2026-03-22 00:13 | dickey | Note Added: 0007414 |