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.

at main 262 lines 6.3 kB view raw
1package paste 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "path" 9 "strings" 10 11 "github.com/pborman/getopt/v2" 12 "mvdan.cc/sh/v3/interp" 13 "tangled.org/xeiaso.net/kefka/command" 14) 15 16type Impl struct{} 17 18func (Impl) Exec(_ context.Context, ec *command.ExecContext, args []string) error { 19 if ec == nil { 20 return errors.New("paste: nil ExecContext") 21 } 22 23 stdout := ec.Stdout 24 if stdout == nil { 25 stdout = io.Discard 26 } 27 stderr := ec.Stderr 28 if stderr == nil { 29 stderr = io.Discard 30 } 31 32 set := getopt.New() 33 set.SetProgram("paste") 34 set.SetParameters("[FILE]...") 35 36 usage := func() { 37 fmt.Fprint(stderr, "Usage: paste [OPTION]... [FILE]...\n") 38 fmt.Fprint(stderr, "Write lines consisting of the sequentially corresponding lines from\n") 39 fmt.Fprint(stderr, "each FILE, separated by TABs, to standard output.\n") 40 fmt.Fprint(stderr, "With no FILE, or when FILE is -, read standard input.\n\n") 41 fmt.Fprint(stderr, " -d, --delimiters=LIST reuse characters from LIST instead of TABs;\n") 42 fmt.Fprint(stderr, " recognized backslash escapes are \\n, \\t, \\\\,\n") 43 fmt.Fprint(stderr, " and \\0 (which represents the empty/no-delimiter\n") 44 fmt.Fprint(stderr, " slot in the cycle). An empty LIST is accepted\n") 45 fmt.Fprint(stderr, " and behaves like \\0 (no separator emitted).\n") 46 fmt.Fprint(stderr, " -s, --serial paste one file at a time instead of in parallel\n") 47 fmt.Fprint(stderr, " --help display this help and exit\n") 48 } 49 set.SetUsage(usage) 50 51 delimiters := set.StringLong("delimiters", 'd', "\t", "reuse characters from LIST instead of TABs") 52 serial := set.BoolLong("serial", 's', "paste one file at a time instead of in parallel") 53 help := set.BoolLong("help", 0, "display this help and exit") 54 55 if err := set.Getopt(append([]string{"paste"}, args...), nil); err != nil { 56 fmt.Fprintf(stderr, "paste: %s\n", err) 57 usage() 58 return interp.ExitStatus(1) 59 } 60 if *help { 61 usage() 62 return nil 63 } 64 65 files := set.Args() 66 if len(files) == 0 { 67 fmt.Fprint(stderr, "usage: paste [-s] [-d delimiters] file ...\n") 68 return interp.ExitStatus(1) 69 } 70 71 delims, err := parseDelimiters(*delimiters) 72 if err != nil { 73 fmt.Fprintf(stderr, "paste: %s\n", err) 74 return interp.ExitStatus(1) 75 } 76 77 stdinCount := 0 78 for _, f := range files { 79 if f == "-" { 80 stdinCount++ 81 } 82 } 83 84 var stdinLines []string 85 if stdinCount > 0 { 86 var err error 87 stdinLines, err = readStdinLines(ec) 88 if err != nil { 89 return err 90 } 91 } 92 93 fileContents := make([][]string, 0, len(files)) 94 stdinIndex := 0 95 for _, file := range files { 96 if file == "-" { 97 var slice []string 98 for i := stdinIndex; i < len(stdinLines); i += stdinCount { 99 slice = append(slice, stdinLines[i]) 100 } 101 fileContents = append(fileContents, slice) 102 stdinIndex++ 103 continue 104 } 105 lines, err := readFileLines(ec, file, stderr) 106 if err != nil { 107 return err 108 } 109 fileContents = append(fileContents, lines) 110 } 111 112 var output strings.Builder 113 114 if *serial { 115 for _, lines := range fileContents { 116 output.WriteString(joinWithDelimiters(lines, delims)) 117 output.WriteByte('\n') 118 } 119 } else { 120 maxLines := 0 121 for _, lines := range fileContents { 122 if len(lines) > maxLines { 123 maxLines = len(lines) 124 } 125 } 126 for lineIdx := 0; lineIdx < maxLines; lineIdx++ { 127 parts := make([]string, len(fileContents)) 128 for i, lines := range fileContents { 129 if lineIdx < len(lines) { 130 parts[i] = lines[lineIdx] 131 } 132 } 133 output.WriteString(joinWithDelimiters(parts, delims)) 134 output.WriteByte('\n') 135 } 136 } 137 138 io.WriteString(stdout, output.String()) 139 return nil 140} 141 142// delim is one element of the parsed delimiter cycle. empty=true represents the 143// '\0' POSIX sentinel: at this slot in the cycle, no character is emitted. 144type delim struct { 145 r rune 146 empty bool 147} 148 149func parseDelimiters(list string) ([]delim, error) { 150 if list == "" { 151 return nil, nil 152 } 153 var out []delim 154 runes := []rune(list) 155 for i := 0; i < len(runes); i++ { 156 c := runes[i] 157 if c != '\\' { 158 out = append(out, delim{r: c}) 159 continue 160 } 161 if i+1 >= len(runes) { 162 return nil, errors.New("delimiter list ends with an unescaped backslash") 163 } 164 i++ 165 switch runes[i] { 166 case 'n': 167 out = append(out, delim{r: '\n'}) 168 case 't': 169 out = append(out, delim{r: '\t'}) 170 case '\\': 171 out = append(out, delim{r: '\\'}) 172 case '0': 173 out = append(out, delim{empty: true}) 174 default: 175 return nil, fmt.Errorf("'\\%c' is not a valid delimiter", runes[i]) 176 } 177 } 178 return out, nil 179} 180 181func joinWithDelimiters(parts []string, delims []delim) string { 182 if len(parts) == 0 { 183 return "" 184 } 185 if len(parts) == 1 { 186 return parts[0] 187 } 188 if len(delims) == 0 { 189 return strings.Join(parts, "") 190 } 191 var b strings.Builder 192 b.WriteString(parts[0]) 193 for i := 1; i < len(parts); i++ { 194 d := delims[(i-1)%len(delims)] 195 if !d.empty { 196 b.WriteRune(d.r) 197 } 198 b.WriteString(parts[i]) 199 } 200 return b.String() 201} 202 203func splitLines(content string) []string { 204 if content == "" { 205 return nil 206 } 207 lines := strings.Split(content, "\n") 208 if len(lines) > 0 && lines[len(lines)-1] == "" { 209 lines = lines[:len(lines)-1] 210 } 211 return lines 212} 213 214func readStdinLines(ec *command.ExecContext) ([]string, error) { 215 if ec.Stdin == nil { 216 return nil, nil 217 } 218 data, err := io.ReadAll(ec.Stdin) 219 if err != nil { 220 return nil, interp.ExitStatus(1) 221 } 222 return splitLines(string(data)), nil 223} 224 225func readFileLines(ec *command.ExecContext, file string, stderr io.Writer) ([]string, error) { 226 if ec.FS == nil { 227 fmt.Fprintf(stderr, "paste: %s: No such file or directory\n", file) 228 return nil, interp.ExitStatus(1) 229 } 230 full := resolvePath(ec, file) 231 f, err := ec.FS.Open(full) 232 if err != nil { 233 fmt.Fprintf(stderr, "paste: %s: No such file or directory\n", file) 234 return nil, interp.ExitStatus(1) 235 } 236 data, err := io.ReadAll(f) 237 f.Close() 238 if err != nil { 239 fmt.Fprintf(stderr, "paste: %s: %v\n", file, err) 240 return nil, interp.ExitStatus(1) 241 } 242 return splitLines(string(data)), nil 243} 244 245func resolvePath(ec *command.ExecContext, p string) string { 246 dir := ec.Dir 247 if dir == "" { 248 dir = "." 249 } 250 if path.IsAbs(p) { 251 p = strings.TrimPrefix(p, "/") 252 if p == "" { 253 return "." 254 } 255 return path.Clean(p) 256 } 257 joined := path.Join(dir, p) 258 if joined == "" { 259 return "." 260 } 261 return joined 262}