github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/builtins/core/tabulate/tabulate.go (about) 1 package tabulate 2 3 import ( 4 "bufio" 5 "encoding/csv" 6 "fmt" 7 "regexp" 8 "strings" 9 10 "github.com/lmorg/murex/config/defaults" 11 "github.com/lmorg/murex/lang" 12 "github.com/lmorg/murex/lang/parameters" 13 "github.com/lmorg/murex/lang/types" 14 "github.com/lmorg/murex/utils/json" 15 ) 16 17 func init() { 18 lang.DefineMethod("tabulate", cmdTabulate, types.Generic, types.Any) 19 20 defaults.AppendProfile(` 21 autocomplete set tabulate { [{ 22 "DynamicDesc": ({ tabulate --help }), 23 "AllowMultiple": true 24 }] } 25 `) 26 } 27 28 const ( 29 constSeparator = `(\t|\s[\s]+)+` 30 ) 31 32 var ( 33 rxSplitComma = regexp.MustCompile(`[\s\t]*,[\s\t]*`) 34 rxSplitSpace = regexp.MustCompile(`[\s\t]+-`) 35 ) 36 37 // flags 38 39 const ( 40 fSeparator = "--separator" 41 fSplitComma = "--split-comma" 42 fSplitSpace = "--split-space" 43 fKeyIncHint = "--key-inc-hint" 44 fKeyVal = "--key-value" 45 fMap = "--map" 46 fJoiner = "--joiner" 47 fColumnWraps = "--column-wraps" 48 fHelp = "--help" 49 ) 50 51 var flags = map[string]string{ 52 fSeparator: types.String, 53 fSplitComma: types.Boolean, 54 fSplitSpace: types.Boolean, 55 fKeyIncHint: types.Boolean, 56 fKeyVal: types.Boolean, 57 fMap: types.Boolean, 58 fJoiner: types.String, 59 fColumnWraps: types.Boolean, 60 fHelp: types.Boolean, 61 } 62 63 var desc = map[string]string{ 64 fSeparator: "String, custom regex pattern for splitting fields (default: `" + constSeparator + "`)", 65 fSplitComma: "Boolean, split first field and duplicate the line if comma found in first field (eg parsing flags in help pages)", 66 fSplitSpace: "Boolean, split first field and duplicate the line if white space found in first field (eg parsing flags in help pages)", 67 fKeyIncHint: "Boolean, used with " + fMap + " to split any space or equal delimited hints/examples (eg parsing flags)", 68 fKeyVal: "Boolean, discard any records that don't appear key value pairs (auto-enabled when " + fMap + " used)", 69 fMap: "Boolean, return JSON map instead of table", 70 fJoiner: "String, used with " + fMap + " to concatenate any trailing records in a given field", 71 fColumnWraps: "Boolean, used with " + fMap + " or " + fKeyVal + " to merge trailing lines if the text wraps within the same column", 72 fHelp: "Boolean, displays this help message", 73 } 74 75 func cmdTabulate(p *lang.Process) error { 76 f, _, err := p.Parameters.ParseFlags( 77 ¶meters.Arguments{ 78 Flags: flags, 79 AllowAdditional: false, 80 }, 81 ) 82 83 if err != nil { 84 return err 85 } 86 87 var ( 88 separator = constSeparator 89 splitComma bool 90 splitSpace bool 91 keyIncHint bool 92 keyVal = false 93 joiner = " " 94 columnWraps = false 95 colWrapsBuf string // buffer for wrapped columns 96 keys []string 97 w writer 98 last string 99 split []string 100 //iKeyStart int // where the key starts when column wraps and keyVal used 101 processKey bool 102 //iValStart int // where the value starts when column wraps and keyVal used 103 ) 104 105 for flag, value := range f { 106 switch flag { 107 case fSeparator: 108 separator = value 109 case fSplitComma: 110 splitComma = true 111 case fSplitSpace: 112 splitSpace = true 113 case fKeyIncHint: 114 keyIncHint = true 115 case fKeyVal: 116 keyVal = true 117 case fJoiner: 118 joiner = value 119 case fMap: 120 keyVal = true 121 case fColumnWraps: 122 columnWraps = true 123 case fHelp: 124 return help(p) 125 } 126 } 127 128 if splitSpace && splitComma { 129 return fmt.Errorf("cannot have %s and %s both enabled. Please pick one or the other", fSplitComma, fSplitSpace) 130 } 131 132 if !keyVal && keyIncHint { 133 return fmt.Errorf("cannot use %s without %s or %s being set", fKeyIncHint, fKeyVal, fMap) 134 } 135 136 if !keyVal && columnWraps { 137 return fmt.Errorf("cannot use %s without %s or %s being set", fColumnWraps, fKeyVal, fMap) 138 } 139 140 if err := p.ErrIfNotAMethod(); err != nil { 141 p.Stdout.SetDataType(types.Null) 142 return err 143 } 144 145 dt := p.Stdin.GetDataType() 146 if dt != types.Generic && dt != types.String { 147 p.Stdout.SetDataType(types.Null) 148 return fmt.Errorf("`%s` is designed to only take string (%s) or generic (%s) data-types from STDIN. Instead it received '%s'", 149 p.Name.String(), types.String, types.Generic, dt) 150 } 151 152 if f[fMap] == "" { 153 p.Stdout.SetDataType("csv") 154 w = csv.NewWriter(p.Stdout) 155 } else { 156 p.Stdout.SetDataType(types.Json) 157 w = newMapWriter(p.Stdout, joiner) 158 } 159 160 rxTableSplit, err := regexp.Compile(separator) 161 if err != nil { 162 return err 163 } 164 165 if err := p.ErrIfNotAMethod(); err != nil { 166 return err 167 } 168 169 scanner := bufio.NewScanner(p.Stdin) 170 for scanner.Scan() { 171 s := scanner.Text() 172 173 // not a table row 174 if !rxTableSplit.MatchString(s) { 175 continue 176 } 177 178 // still not a table row 179 split = rxTableSplit.Split(s, -1) 180 if len(split) == 0 { 181 continue 182 } 183 184 // table has indentation, lets remove that 185 if len(split) > 1 && split[0] == "" { 186 split = split[1:] 187 } 188 189 if keyVal && (len(split) < 2 || split[0] == "") { 190 // is this a wrapped column? 191 if columnWraps && last != "" { 192 // is it a wrapped key? Check if indented flag 193 for i := 0; i < len(s); i++ { 194 if s[i] == '-' && i > 0 { 195 // it's a key 196 processKey = true 197 if len(split) == 1 { 198 split = []string{split[0], ""} 199 } 200 201 break 202 } 203 if s[i] != ' ' && s[i] != '\t' { 204 break 205 } 206 } 207 208 // look like it's just a wrapped column 209 if !processKey { 210 if len(colWrapsBuf) == 0 || colWrapsBuf[len(colWrapsBuf)-1] == ' ' { 211 colWrapsBuf += strings.Join(split, joiner) 212 } else { 213 colWrapsBuf += joiner + strings.Join(split, joiner) 214 } 215 } 216 } 217 218 // else silently ignore heading 219 if !processKey { 220 continue 221 } 222 } 223 224 if len(split) > 1 || processKey { // recheck because we've redefined the length of split 225 processKey = false 226 227 // looks like there's a new key, so lets write the colWrapsBuf 228 if columnWraps && last != "" { 229 if len(keys) == 0 { 230 231 err = w.Write([]string{last, colWrapsBuf}) 232 if err != nil { 233 return err 234 } 235 236 } else { 237 238 for i := range keys { 239 err = w.Write([]string{keys[i], colWrapsBuf}) 240 if err != nil { 241 return err 242 } 243 } 244 } 245 246 colWrapsBuf = "" 247 248 /*for i, r := range s { 249 if r != ' ' && r != '\t' { 250 iKeyStart = i 251 break 252 } 253 }*/ 254 } 255 256 // split keys by comma 257 if splitComma { 258 keys = rxSplitComma.Split(split[0], -1) 259 } 260 261 // split keys by space 262 if splitSpace { 263 keys = rxSplitSpace.Split(split[0], 2) 264 if len(keys) == 2 { 265 keys[1] = "-" + keys[1] 266 } 267 } 268 269 // remove the hint stuff 270 if keyIncHint { 271 var hint string 272 if len(keys) != 0 { 273 _, hint = stripKeyHint(keys) 274 } else { 275 split[0], hint = stripKeyHint([]string{split[0]}) 276 } 277 if len(hint) != 0 { 278 split[1] = "(args: " + hint + ") " + split[1] 279 } 280 } 281 282 } 283 284 if keyVal { 285 last = split[0] 286 } 287 288 // only write if columns not wrapped, otherwise loop round to check for 289 // any wrapped columns 290 if !columnWraps { 291 if len(keys) == 0 { 292 293 err = w.Write(split) 294 if err != nil { 295 return err 296 } 297 298 } else { 299 300 for i := range keys { 301 split[0] = keys[i] 302 err = w.Write(split) 303 if err != nil { 304 return err 305 } 306 } 307 } 308 309 keys = nil 310 311 } else { 312 colWrapsBuf = strings.Join(split[1:], joiner) 313 } 314 } 315 316 // clean up any trailing wrapped columns 317 if columnWraps && len(colWrapsBuf) != 0 { 318 if len(keys) == 0 { 319 320 err = w.Write([]string{last, colWrapsBuf}) 321 if err != nil { 322 return err 323 } 324 325 } else { 326 327 for i := range keys { 328 err = w.Write([]string{keys[i], colWrapsBuf}) 329 if err != nil { 330 return err 331 } 332 } 333 } 334 } 335 336 if err := scanner.Err(); err != nil { 337 return err 338 } 339 340 w.Flush() 341 return w.Error() 342 } 343 344 var rxSquareHints = regexp.MustCompile(`\[.*\]$`) 345 346 func stripKeyHint(keys []string) (string, string) { 347 var ( 348 space, equ, square []string 349 hint string 350 ) 351 352 for i := range keys { 353 square = rxSquareHints.FindStringSubmatch(keys[i]) 354 if len(square) != 0 { 355 keys[i] = strings.Replace(keys[i], square[0], "", 1) 356 } 357 358 space = strings.SplitN(keys[i], " ", 2) 359 keys[i] = space[0] 360 if strings.Contains(space[0], "=") { 361 equ = strings.SplitN(keys[i], "=", 2) 362 keys[i] = equ[0] + "=" 363 } 364 } 365 366 switch { 367 case len(square) != 0: 368 hint = square[0] 369 370 case len(space) == 2: 371 hint = space[1] 372 373 case len(equ) == 2: 374 hint = equ[1] 375 } 376 377 return keys[0], hint 378 } 379 380 func help(p *lang.Process) error { 381 p.Stdout.SetDataType(types.Json) 382 b, err := json.Marshal(desc, p.Stdout.IsTTY()) 383 if err != nil { 384 return err 385 } 386 387 _, err = p.Stdout.Write(b) 388 return err 389 }