hautils

[hare] Set of POSIX utilities
Log | Files | Refs | README | LICENSE

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 };