A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(expand): single-space past last tab stop and wide-rune width

Implements GNU coreutils compatibility for expand:

- Past the last specified -t stop, each tab becomes a
single space rather than extrapolating with the last
interval
- East Asian Wide and Fullwidth runes count as two
columns via golang.org/x/text/width

The -i/--initial GNU extension is retained.

Refs: docs/posix2018/CONFORMANCE.md
Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso d7c2346a 22ca091d

+53 -10
+9 -8
command/internal/expand/expand.go
··· 10 10 "strings" 11 11 12 12 "github.com/pborman/getopt/v2" 13 + "golang.org/x/text/width" 13 14 "mvdan.cc/sh/v3/interp" 14 15 "tangled.org/xeiaso.net/kefka/command" 15 16 ) ··· 118 119 return stop - column 119 120 } 120 121 } 121 - if len(tabStops) >= 2 { 122 - last := tabStops[len(tabStops)-1] 123 - prev := tabStops[len(tabStops)-2] 124 - interval := last - prev 125 - steps := (column-last)/interval + 1 126 - next := last + steps*interval 127 - return next - column 122 + return 1 123 + } 124 + 125 + func runeWidth(r rune) int { 126 + switch width.LookupRune(r).Kind() { 127 + case width.EastAsianWide, width.EastAsianFullwidth: 128 + return 2 128 129 } 129 130 return 1 130 131 } ··· 151 152 inLeading = false 152 153 } 153 154 result.WriteRune(r) 154 - column++ 155 + column += runeWidth(r) 155 156 } 156 157 return result.String() 157 158 }
+44 -2
command/internal/expand/expand_test.go
··· 89 89 wantStdout: " end\n", 90 90 }, 91 91 { 92 - name: "explicit tab stops past last uses last interval", 92 + name: "explicit tab stops past last become single space", 93 93 args: []string{"-t", "4,8"}, 94 94 stdin: "a\t\t\tx\n", 95 - wantStdout: "a" + strings.Repeat(" ", 11) + "x\n", 95 + wantStdout: "a" + strings.Repeat(" ", 8) + "x\n", 96 + }, 97 + { 98 + name: "multi-stop several tabs past last each one space", 99 + args: []string{"-t", "4,8"}, 100 + stdin: "12345678\t\t\tx\n", 101 + wantStdout: "12345678 x\n", 102 + }, 103 + { 104 + name: "narrow rune counts as one column", 105 + args: nil, 106 + stdin: "λ\tx\n", 107 + wantStdout: "λ" + strings.Repeat(" ", 7) + "x\n", 108 + }, 109 + { 110 + name: "wide rune counts as two columns", 111 + args: nil, 112 + stdin: "中\tx\n", 113 + wantStdout: "中" + strings.Repeat(" ", 6) + "x\n", 114 + }, 115 + { 116 + name: "wide rune 日 counts as two columns", 117 + args: nil, 118 + stdin: "日\tx\n", 119 + wantStdout: "日" + strings.Repeat(" ", 6) + "x\n", 120 + }, 121 + { 122 + name: "mixed narrow and wide runes", 123 + args: nil, 124 + stdin: "a中\tb\n", 125 + wantStdout: "a中" + strings.Repeat(" ", 5) + "b\n", 126 + }, 127 + { 128 + name: "leading-only kefka extension preserves interior tabs after text", 129 + args: []string{"-i"}, 130 + stdin: "leading\tword\ttrailing\n", 131 + wantStdout: "leading\tword\ttrailing\n", 96 132 }, 97 133 { 98 134 name: "leading-only short flag preserves interior tabs", ··· 111 147 args: []string{"-i", "-t", "4"}, 112 148 stdin: " \tfoo\tbar\n", 113 149 wantStdout: " foo\tbar\n", 150 + }, 151 + { 152 + name: "leading-only with space-then-tab leading and interior tab", 153 + args: []string{"-i"}, 154 + stdin: " \tword\ttrailing\n", 155 + wantStdout: " word\ttrailing\n", 114 156 }, 115 157 { 116 158 name: "expand from file",