nl.ha (8279B)
1 use ascii; 2 use bufio; 3 use encoding::utf8; 4 use fmt; 5 use fs; 6 use getopt; 7 use io; 8 use main; 9 use os; 10 use regex; 11 use strconv; 12 use strings; 13 use types; 14 15 type style = (all | nonempty | none | regex::regex); 16 17 // Number all lines. 18 type all = void; 19 20 // Number lines which are non-empty. 21 type nonempty = void; 22 23 // Number no lines. 24 type none = void; 25 26 type section = enum { 27 HEAD, 28 BODY, 29 FOOT, 30 }; 31 32 type context = struct { 33 linenum: int, 34 conblanks: uint, // counter of consecutive blank lines 35 maxblanks: uint, // 1 + maximum run of unnumbered blank lines 36 sep: str, 37 sepblank: str, 38 incr: int, 39 mod: *fmt::mods, 40 }; 41 42 export fn utilmain() (void | main::error) = { 43 const help: [_]getopt::help = [ 44 "line numbering filter", 45 ('p', "do not restart numbering at logical page delimiters"), 46 ('b', "type", "set body line numbering strategy"), 47 ('d', "delim", "set delimiter for new pages"), 48 ('f', "type", "set footer line numbering strategy"), 49 ('h', "type", "set header line numbering strategy"), 50 ('i', "incr", "amount to increment each page"), 51 ('l', "num", "number of consecutive blank lines considered one"), 52 ('n', "format", "set line number format"), 53 ('s', "sep", "set separator to place between line number and text"), 54 ('v', "startnum", "initial value for each page"), 55 ('w', "width", "number of columns for numbers"), 56 "[file]", 57 ]; 58 const cmd = getopt::parse(os::args, help...); 59 defer getopt::finish(&cmd); 60 61 if (len(cmd.args) > 1) { 62 main::usage(help); 63 }; 64 65 static const delim_buf: [3 * 2 * 4]u8 = [0...]; // 3 pairs of runes 66 let delim = "\\:"; 67 68 let head_style: style = none; 69 let body_style: style = nonempty; 70 let foot_style: style = none; 71 72 let paged_numbering = true; 73 let startnum = 1; 74 75 const ctx = context { 76 linenum = 0, 77 conblanks = 0, 78 maxblanks = 1, 79 sep = "\t", 80 sepblank = "\t", 81 incr = 1, 82 mod = &fmt::mods { 83 width = 6, 84 padding = fmt::padding::ALIGN_RIGHT, 85 ... 86 }, 87 }; 88 89 for (let i = 0z; i < len(cmd.opts); i += 1) { 90 const opt = cmd.opts[i]; 91 switch (opt.0) { 92 case 'p' => 93 paged_numbering = false; 94 case 'b' => 95 body_style = if (strings::hasprefix(opt.1, "p")) { 96 const pat = strings::trimprefix(opt.1, "p"); 97 yield match(regex::compile(pat)) { 98 case let re: regex::regex => 99 yield re; 100 case let err: regex::error => 101 fmt::fatalf("Error: -bp<string>: {}", err); 102 }; 103 } else { 104 yield switch (opt.1) { 105 case "a" => 106 yield all; 107 case "t" => 108 yield nonempty; 109 case "n" => 110 yield none; 111 case => 112 main::usage(help, 'b'); 113 }; 114 }; 115 case 'd' => 116 const rs = strings::torunes(opt.1); 117 defer free(rs); 118 delim = switch (len(rs)) { 119 case 1 => 120 yield fmt::bsprintf(delim_buf, "{}:", rs[0]); 121 case 2 => 122 yield opt.1; 123 case => 124 main::usage(help, 'd'); 125 }; 126 case 'f' => 127 foot_style = if (strings::hasprefix(opt.1, "p")) { 128 const pat = strings::trimprefix(opt.1, "p"); 129 yield match(regex::compile(pat)) { 130 case let re: regex::regex => 131 yield re; 132 case let err: regex::error => 133 fmt::fatalf("Error: -fp<string>: {}", err); 134 }; 135 } else { 136 yield switch (opt.1) { 137 case "a" => 138 yield all; 139 case "t" => 140 yield nonempty; 141 case "n" => 142 yield none; 143 case => 144 main::usage(help, 'f'); 145 }; 146 }; 147 case 'h' => 148 head_style = if (strings::hasprefix(opt.1, "p")) { 149 const pat = strings::trimprefix(opt.1, "p"); 150 yield match(regex::compile(pat)) { 151 case let re: regex::regex => 152 yield re; 153 case let err: regex::error => 154 fmt::fatalf("Error: -hp<string>: {}", err); 155 }; 156 } else { 157 yield switch (opt.1) { 158 case "a" => 159 yield all; 160 case "t" => 161 yield nonempty; 162 case "n" => 163 yield none; 164 case => 165 main::usage(help, 'h'); 166 }; 167 }; 168 case 'i' => 169 ctx.incr = match (strconv::stoi(opt.1)) { 170 case (strconv::invalid | strconv::overflow) => 171 main::usage(help, 'i'); 172 case let incr: int => 173 yield incr; 174 }; 175 case 'l' => 176 ctx.maxblanks = match (strconv::stou(opt.1)) { 177 case (strconv::invalid | strconv::overflow) => 178 main::usage(help, 'l'); 179 case let maxblanks: uint => 180 yield if (maxblanks > 0) 181 maxblanks 182 else 183 main::usage(help, 'l'); 184 }; 185 case 'n' => 186 ctx.mod.padding = switch (opt.1) { 187 case "ln" => 188 yield fmt::padding::ALIGN_LEFT; 189 case "rn" => 190 yield fmt::padding::ALIGN_RIGHT; 191 case "rz" => 192 yield fmt::padding::ZEROES; 193 case => 194 main::usage(help, 'n'); 195 }; 196 case 's' => 197 ctx.sep = opt.1; 198 case 'w' => 199 ctx.mod.width = match (strconv::stou(opt.1)) { 200 case (strconv::invalid | strconv::overflow) => 201 main::usage(help, 'w'); 202 case let width: uint => 203 yield if (width > 0) 204 width 205 else 206 main::usage(help, 'w'); 207 }; 208 case 'v' => 209 startnum = match (strconv::stoi(opt.1)) { 210 case (strconv::invalid | strconv::overflow) => 211 main::usage(help, 'v'); 212 case let startnum: int => 213 yield startnum; 214 }; 215 case => 216 main::usage(help); 217 }; 218 }; 219 220 defer { 221 if (head_style is regex::regex) { 222 regex::finish(&(head_style as regex::regex)); 223 }; 224 if (body_style is regex::regex) { 225 regex::finish(&(body_style as regex::regex)); 226 }; 227 if (foot_style is regex::regex) { 228 regex::finish(&(foot_style as regex::regex)); 229 }; 230 }; 231 232 const use_file = len(cmd.args) == 1 && cmd.args[0] != "-"; 233 const input: io::handle = if (use_file) 234 match (os::open(cmd.args[0])) { 235 case let err: fs::error => 236 fmt::fatalf("Error opening '{}': {}", 237 cmd.args[0], fs::strerror(err)); 238 case let file: io::file => 239 static const rbuf: [os::BUFSZ]u8 = [0...]; 240 static const wbuf: [os::BUFSZ]u8 = [0...]; 241 yield &bufio::init(file, rbuf, wbuf); 242 } 243 else 244 os::stdin; 245 246 defer io::close(input)!; 247 248 const delim_head = fmt::bsprintf(delim_buf, "{0}{0}{0}", delim); 249 const delim_body = fmt::bsprintf(delim_buf, "{0}{0}", delim); 250 const delim_foot = delim; 251 252 if (ctx.sep != "\t") { 253 ctx.sepblank = strings::padend("", ' ', len(ctx.sep)); 254 }; 255 defer if (ctx.sep != "\t") { 256 free(ctx.sepblank); 257 }; 258 259 let section = section::BODY; 260 ctx.linenum = startnum; 261 262 for (true) { 263 const rawline = match (bufio::read_line(input)) { 264 case let err: io::error => 265 return err; 266 case io::EOF => 267 break; 268 case let rawline: []u8 => 269 yield rawline; 270 }; 271 defer free(rawline); 272 273 const line = match (strings::fromutf8(rawline)) { 274 case let line: str => 275 yield line; 276 case encoding::utf8::invalid => 277 fmt::fatal("Error: Invalid UTF-8 input"); 278 }; 279 280 if (line == delim_head) { 281 section = section::HEAD; 282 fmt::println()?; 283 if (paged_numbering == true) 284 ctx.linenum = startnum; 285 continue; 286 }; 287 if (line == delim_body) { 288 section = section::BODY; 289 fmt::println()?; 290 continue; 291 }; 292 if (line == delim_foot) { 293 section = section::FOOT; 294 fmt::println()?; 295 continue; 296 }; 297 298 switch (section) { 299 case section::HEAD => 300 println(line, head_style, &ctx)?; 301 case section::BODY => 302 println(line, body_style, &ctx)?; 303 case section::FOOT => 304 println(line, foot_style, &ctx)?; 305 }; 306 }; 307 }; 308 309 fn println(line: str, s: style, ctx: *context) (void | io::error) = { 310 match (s) { 311 case all => 312 if (!isblank(line)) { 313 fmt::printf("{%}{}", ctx.linenum, ctx.mod, ctx.sep)?; 314 ctx.linenum += ctx.incr; 315 ctx.conblanks = 0; 316 } else { 317 ctx.conblanks += 1; 318 if (ctx.conblanks == ctx.maxblanks) { 319 fmt::printf("{%}{}", ctx.linenum, ctx.mod, ctx.sep)?; 320 ctx.linenum += ctx.incr; 321 ctx.conblanks = 0; 322 } else { 323 fmt::printf("{%}{}", " ", ctx.mod, ctx.sepblank)?; 324 }; 325 }; 326 case nonempty => 327 if (!isblank(line)) { 328 fmt::printf("{%}{}", ctx.linenum, ctx.mod, ctx.sep)?; 329 ctx.linenum += ctx.incr; 330 } else { 331 fmt::printf("{%}{}", " ", ctx.mod, ctx.sepblank)?; 332 }; 333 case none => 334 fmt::printf("{%}{}", " ", ctx.mod, ctx.sepblank)?; 335 case let re: regex::regex => 336 if (len(regex::find(&re, line)) > 0) { 337 fmt::printf("{%}{}", ctx.linenum, ctx.mod, ctx.sep)?; 338 ctx.linenum += ctx.incr; 339 } else { 340 fmt::printf("{%}{}", " ", ctx.mod, ctx.sepblank)?; 341 }; 342 }; 343 fmt::println(line)?; 344 }; 345 346 fn isblank(line: str) bool = { 347 const iter = strings::iter(line); 348 for (true) { 349 const r = match (strings::next(&iter)) { 350 case let r: rune => 351 yield r; 352 case void => 353 break; 354 }; 355 if (!ascii::isspace(r)) { 356 return false; 357 }; 358 }; 359 return true; 360 };