aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRob Landley <rob@landley.net>2021-07-20 16:02:31 +0200
committerDenys Vlasenko <vda.linux@googlemail.com>2021-07-20 16:29:22 +0200
commit0068ce2fa0e3dbc618b8ca87ab56e144731da784 (patch)
tree6f4d2d6cd45089b7fdb67fcdfcb4f85baba6976e
parentdabbeeb79356eef78528acd55e1f143ae80372f7 (diff)
downloadbusybox-w32-0068ce2fa0e3dbc618b8ca87ab56e144731da784.tar.gz
busybox-w32-0068ce2fa0e3dbc618b8ca87ab56e144731da784.tar.bz2
busybox-w32-0068ce2fa0e3dbc618b8ca87ab56e144731da784.zip
cut: add toybox-compatible options -O OUTSEP, -D, -F LIST
function old new delta cut_main 884 1201 +317 packed_usage 33823 33885 +62 .rodata 104186 104179 -7 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 2/1 up/down: 379/-7) Total: 372 bytes Signed-off-by: Rob Landley <rob@landley.net> Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
-rw-r--r--coreutils/cut.c207
-rwxr-xr-xtestsuite/cut.tests64
2 files changed, 174 insertions, 97 deletions
diff --git a/coreutils/cut.c b/coreutils/cut.c
index cc3c32576..7009e74cf 100644
--- a/coreutils/cut.c
+++ b/coreutils/cut.c
@@ -14,6 +14,13 @@
14//config: help 14//config: help
15//config: cut is used to print selected parts of lines from 15//config: cut is used to print selected parts of lines from
16//config: each file to stdout. 16//config: each file to stdout.
17//config:
18//config:config FEATURE_CUT_REGEX
19//config: bool "cut -F"
20//config: default y
21//config: depends on CUT
22//config: help
23//config: Allow regex based delimiters.
17 24
18//applet:IF_CUT(APPLET_NOEXEC(cut, cut, BB_DIR_USR_BIN, BB_SUID_DROP, cut)) 25//applet:IF_CUT(APPLET_NOEXEC(cut, cut, BB_DIR_USR_BIN, BB_SUID_DROP, cut))
19 26
@@ -25,9 +32,14 @@
25//usage: "Print selected fields from FILEs to stdout\n" 32//usage: "Print selected fields from FILEs to stdout\n"
26//usage: "\n -b LIST Output only bytes from LIST" 33//usage: "\n -b LIST Output only bytes from LIST"
27//usage: "\n -c LIST Output only characters from LIST" 34//usage: "\n -c LIST Output only characters from LIST"
28//usage: "\n -d CHAR Use CHAR instead of tab as field delimiter" 35//usage: "\n -d SEP Field delimiter for input (default -f TAB, -F run of whitespace)"
36//usage: "\n -O SEP Field delimeter for output (default = -d for -f, one space for -F)"
37//usage: "\n -D Don't sort/collate sections or match -fF lines without delimeter"
38//usage: "\n -f LIST Print only these fields (-d is single char)"
39//usage: IF_FEATURE_CUT_REGEX(
40//usage: "\n -F LIST Print only these fields (-d is regex)"
41//usage: )
29//usage: "\n -s Output only lines containing delimiter" 42//usage: "\n -s Output only lines containing delimiter"
30//usage: "\n -f LIST Print only these fields"
31//usage: "\n -n Ignored" 43//usage: "\n -n Ignored"
32//(manpage:-n with -b: don't split multibyte characters) 44//(manpage:-n with -b: don't split multibyte characters)
33//usage: 45//usage:
@@ -39,38 +51,49 @@
39 51
40#include "libbb.h" 52#include "libbb.h"
41 53
54#if ENABLE_FEATURE_CUT_REGEX
55#include "xregex.h"
56#else
57#define regex_t int
58typedef struct { int rm_eo, rm_so; } regmatch_t;
59#define xregcomp(x, ...) *(x) = 0
60#define regexec(...) 0
61#endif
62
42/* This is a NOEXEC applet. Be very careful! */ 63/* This is a NOEXEC applet. Be very careful! */
43 64
44 65
45/* option vars */ 66/* option vars */
46#define OPT_STR "b:c:f:d:sn" 67#define OPT_STR "b:c:f:d:O:sD"IF_FEATURE_CUT_REGEX("F:")"n"
47#define CUT_OPT_BYTE_FLGS (1 << 0) 68#define CUT_OPT_BYTE_FLGS (1 << 0)
48#define CUT_OPT_CHAR_FLGS (1 << 1) 69#define CUT_OPT_CHAR_FLGS (1 << 1)
49#define CUT_OPT_FIELDS_FLGS (1 << 2) 70#define CUT_OPT_FIELDS_FLGS (1 << 2)
50#define CUT_OPT_DELIM_FLGS (1 << 3) 71#define CUT_OPT_DELIM_FLGS (1 << 3)
51#define CUT_OPT_SUPPRESS_FLGS (1 << 4) 72#define CUT_OPT_ODELIM_FLGS (1 << 4)
73#define CUT_OPT_SUPPRESS_FLGS (1 << 5)
74#define CUT_OPT_NOSORT_FLGS (1 << 6)
75#define CUT_OPT_REGEX_FLGS ((1 << 7) * ENABLE_FEATURE_CUT_REGEX)
52 76
53struct cut_list { 77struct cut_list {
54 int startpos; 78 int startpos;
55 int endpos; 79 int endpos;
56}; 80};
57 81
58enum {
59 BOL = 0,
60 EOL = INT_MAX,
61 NON_RANGE = -1
62};
63
64static int cmpfunc(const void *a, const void *b) 82static int cmpfunc(const void *a, const void *b)
65{ 83{
66 return (((struct cut_list *) a)->startpos - 84 return (((struct cut_list *) a)->startpos -
67 ((struct cut_list *) b)->startpos); 85 ((struct cut_list *) b)->startpos);
68} 86}
69 87
70static void cut_file(FILE *file, char delim, const struct cut_list *cut_lists, unsigned nlists) 88static void cut_file(FILE *file, const char *delim, const char *odelim,
89 const struct cut_list *cut_lists, unsigned nlists)
71{ 90{
72 char *line; 91 char *line;
73 unsigned linenum = 0; /* keep these zero-based to be consistent */ 92 unsigned linenum = 0; /* keep these zero-based to be consistent */
93 regex_t reg;
94 int spos, shoe = option_mask32 & CUT_OPT_REGEX_FLGS;
95
96 if (shoe) xregcomp(&reg, delim, REG_EXTENDED);
74 97
75 /* go through every line in the file */ 98 /* go through every line in the file */
76 while ((line = xmalloc_fgetline(file)) != NULL) { 99 while ((line = xmalloc_fgetline(file)) != NULL) {
@@ -80,29 +103,22 @@ static void cut_file(FILE *file, char delim, const struct cut_list *cut_lists, u
80 char *printed = xzalloc(linelen + 1); 103 char *printed = xzalloc(linelen + 1);
81 char *orig_line = line; 104 char *orig_line = line;
82 unsigned cl_pos = 0; 105 unsigned cl_pos = 0;
83 int spos;
84 106
85 /* cut based on chars/bytes XXX: only works when sizeof(char) == byte */ 107 /* cut based on chars/bytes XXX: only works when sizeof(char) == byte */
86 if (option_mask32 & (CUT_OPT_CHAR_FLGS | CUT_OPT_BYTE_FLGS)) { 108 if (option_mask32 & (CUT_OPT_CHAR_FLGS | CUT_OPT_BYTE_FLGS)) {
87 /* print the chars specified in each cut list */ 109 /* print the chars specified in each cut list */
88 for (; cl_pos < nlists; cl_pos++) { 110 for (; cl_pos < nlists; cl_pos++) {
89 spos = cut_lists[cl_pos].startpos; 111 for (spos = cut_lists[cl_pos].startpos; spos < linelen;) {
90 while (spos < linelen) {
91 if (!printed[spos]) { 112 if (!printed[spos]) {
92 printed[spos] = 'X'; 113 printed[spos] = 'X';
93 putchar(line[spos]); 114 putchar(line[spos]);
94 } 115 }
95 spos++; 116 if (++spos > cut_lists[cl_pos].endpos) {
96 if (spos > cut_lists[cl_pos].endpos
97 /* NON_RANGE is -1, so if below is true,
98 * the above was true too (spos is >= 0) */
99 /* || cut_lists[cl_pos].endpos == NON_RANGE */
100 ) {
101 break; 117 break;
102 } 118 }
103 } 119 }
104 } 120 }
105 } else if (delim == '\n') { /* cut by lines */ 121 } else if (*delim == '\n') { /* cut by lines */
106 spos = cut_lists[cl_pos].startpos; 122 spos = cut_lists[cl_pos].startpos;
107 123
108 /* get out if we have no more lists to process or if the lines 124 /* get out if we have no more lists to process or if the lines
@@ -115,9 +131,7 @@ static void cut_file(FILE *file, char delim, const struct cut_list *cut_lists, u
115 while (spos < (int)linenum) { 131 while (spos < (int)linenum) {
116 spos++; 132 spos++;
117 /* go to the next list if we're at the end of this one */ 133 /* go to the next list if we're at the end of this one */
118 if (spos > cut_lists[cl_pos].endpos 134 if (spos > cut_lists[cl_pos].endpos) {
119 || cut_lists[cl_pos].endpos == NON_RANGE
120 ) {
121 cl_pos++; 135 cl_pos++;
122 /* get out if there's no more lists to process */ 136 /* get out if there's no more lists to process */
123 if (cl_pos >= nlists) 137 if (cl_pos >= nlists)
@@ -135,55 +149,56 @@ static void cut_file(FILE *file, char delim, const struct cut_list *cut_lists, u
135 puts(line); 149 puts(line);
136 goto next_line; 150 goto next_line;
137 } else { /* cut by fields */ 151 } else { /* cut by fields */
138 int ndelim = -1; /* zero-based / one-based problem */ 152 unsigned uu = 0, start = 0, end = 0, out = 0;
139 int nfields_printed = 0; 153 int dcount = 0;
140 char *field = NULL; 154
141 char delimiter[2]; 155 /* Loop through bytes, finding next delimiter */
142 156 for (;;) {
143 delimiter[0] = delim; 157 /* End of current range? */
144 delimiter[1] = 0; 158 if (end == linelen || dcount > cut_lists[cl_pos].endpos) {
145 159 if (++cl_pos >= nlists) break;
146 /* does this line contain any delimiters? */ 160 if (option_mask32 & CUT_OPT_NOSORT_FLGS)
147 if (strchr(line, delim) == NULL) { 161 start = dcount = uu = 0;
148 if (!(option_mask32 & CUT_OPT_SUPPRESS_FLGS)) 162 end = 0;
149 puts(line); 163 }
150 goto next_line; 164 /* End of current line? */
151 } 165 if (uu == linelen) {
152 166 /* If we've seen no delimiters, check -s */
153 /* process each list on this line, for as long as we've got 167 if (!cl_pos && !dcount && !shoe) {
154 * a line to process */ 168 if (option_mask32 & CUT_OPT_SUPPRESS_FLGS)
155 for (; cl_pos < nlists && line; cl_pos++) { 169 goto next_line;
156 spos = cut_lists[cl_pos].startpos; 170 } else if (dcount<cut_lists[cl_pos].startpos)
157 do { 171 start = linelen;
158 /* find the field we're looking for */ 172 end = linelen;
159 while (line && ndelim < spos) { 173 } else {
160 field = strsep(&line, delimiter); 174 /* Find next delimiter */
161 ndelim++; 175 if (shoe) {
162 } 176 regmatch_t rr = {-1, -1};
163 177
164 /* we found it, and it hasn't been printed yet */ 178 if (!regexec(&reg, line+uu, 1, &rr, REG_NOTBOL|REG_NOTEOL)) {
165 if (field && ndelim == spos && !printed[ndelim]) { 179 end = uu + rr.rm_so;
166 /* if this isn't our first time through, we need to 180 uu += rr.rm_eo;
167 * print the delimiter after the last field that was 181 } else {
168 * printed */ 182 uu = linelen;
169 if (nfields_printed > 0) 183 continue;
170 putchar(delim); 184 }
171 fputs_stdout(field); 185 } else if (line[end = uu++] != *delim)
172 printed[ndelim] = 'X'; 186 continue;
173 nfields_printed++; /* shouldn't overflow.. */ 187
188 /* Got delimiter. Loop if not yet within range. */
189 if (dcount++ < cut_lists[cl_pos].startpos) {
190 start = uu;
191 continue;
174 } 192 }
175 193 }
176 spos++; 194 if (end != start || !shoe)
177 195 printf("%s%.*s", out++ ? odelim : "", end-start, line + start);
178 /* keep going as long as we have a line to work with, 196 start = uu;
179 * this is a list, and we're not at the end of that 197 if (!dcount)
180 * list */ 198 break;
181 } while (spos <= cut_lists[cl_pos].endpos && line
182 && cut_lists[cl_pos].endpos != NON_RANGE);
183 } 199 }
184 } 200 }
185 /* if we printed anything at all, we need to finish it with a 201 /* if we printed anything, finish with newline */
186 * newline cuz we were handed a chomped line */
187 putchar('\n'); 202 putchar('\n');
188 next_line: 203 next_line:
189 linenum++; 204 linenum++;
@@ -198,37 +213,35 @@ int cut_main(int argc UNUSED_PARAM, char **argv)
198 /* growable array holding a series of lists */ 213 /* growable array holding a series of lists */
199 struct cut_list *cut_lists = NULL; 214 struct cut_list *cut_lists = NULL;
200 unsigned nlists = 0; /* number of elements in above list */ 215 unsigned nlists = 0; /* number of elements in above list */
201 char delim = '\t'; /* delimiter, default is tab */
202 char *sopt, *ltok; 216 char *sopt, *ltok;
217 const char *delim = NULL;
218 const char *odelim = NULL;
203 unsigned opt; 219 unsigned opt;
204 220
221#define ARG "bcf"IF_FEATURE_CUT_REGEX("F")
205 opt = getopt32(argv, "^" 222 opt = getopt32(argv, "^"
206 OPT_STR 223 OPT_STR // = "b:c:f:d:O:sD"IF_FEATURE_CUT_REGEX("F:")"n"
207 "\0" "b--bcf:c--bcf:f--bcf", 224 "\0" "b--"ARG":c--"ARG":f--"ARG IF_FEATURE_CUT_REGEX("F--"ARG),
208 &sopt, &sopt, &sopt, &ltok 225 &sopt, &sopt, &sopt, &delim, &odelim IF_FEATURE_CUT_REGEX(, &sopt)
209 ); 226 );
227 if (!delim || !*delim)
228 delim = (opt & CUT_OPT_REGEX_FLGS) ? "[[:space:]]+" : "\t";
229 if (!odelim) odelim = (opt & CUT_OPT_REGEX_FLGS) ? " " : delim;
230
210// argc -= optind; 231// argc -= optind;
211 argv += optind; 232 argv += optind;
212 if (!(opt & (CUT_OPT_BYTE_FLGS | CUT_OPT_CHAR_FLGS | CUT_OPT_FIELDS_FLGS))) 233 if (!(opt & (CUT_OPT_BYTE_FLGS | CUT_OPT_CHAR_FLGS | CUT_OPT_FIELDS_FLGS | CUT_OPT_REGEX_FLGS)))
213 bb_simple_error_msg_and_die("expected a list of bytes, characters, or fields"); 234 bb_simple_error_msg_and_die("expected a list of bytes, characters, or fields");
214 235
215 if (opt & CUT_OPT_DELIM_FLGS) {
216 if (ltok[0] && ltok[1]) { /* more than 1 char? */
217 bb_simple_error_msg_and_die("the delimiter must be a single character");
218 }
219 delim = ltok[0];
220 }
221
222 /* non-field (char or byte) cutting has some special handling */ 236 /* non-field (char or byte) cutting has some special handling */
223 if (!(opt & CUT_OPT_FIELDS_FLGS)) { 237 if (!(opt & (CUT_OPT_FIELDS_FLGS|CUT_OPT_REGEX_FLGS))) {
224 static const char _op_on_field[] ALIGN1 = " only when operating on fields"; 238 static const char _op_on_field[] ALIGN1 = " only when operating on fields";
225 239
226 if (opt & CUT_OPT_SUPPRESS_FLGS) { 240 if (opt & CUT_OPT_SUPPRESS_FLGS) {
227 bb_error_msg_and_die 241 bb_error_msg_and_die
228 ("suppressing non-delimited lines makes sense%s", 242 ("suppressing non-delimited lines makes sense%s", _op_on_field);
229 _op_on_field);
230 } 243 }
231 if (delim != '\t') { 244 if (opt & CUT_OPT_DELIM_FLGS) {
232 bb_error_msg_and_die 245 bb_error_msg_and_die
233 ("a delimiter may be specified%s", _op_on_field); 246 ("a delimiter may be specified%s", _op_on_field);
234 } 247 }
@@ -253,7 +266,7 @@ int cut_main(int argc UNUSED_PARAM, char **argv)
253 /* get the start pos */ 266 /* get the start pos */
254 ntok = strsep(&ltok, "-"); 267 ntok = strsep(&ltok, "-");
255 if (!ntok[0]) { 268 if (!ntok[0]) {
256 s = BOL; 269 s = 0;
257 } else { 270 } else {
258 s = xatoi_positive(ntok); 271 s = xatoi_positive(ntok);
259 /* account for the fact that arrays are zero based, while 272 /* account for the fact that arrays are zero based, while
@@ -264,24 +277,23 @@ int cut_main(int argc UNUSED_PARAM, char **argv)
264 277
265 /* get the end pos */ 278 /* get the end pos */
266 if (ltok == NULL) { 279 if (ltok == NULL) {
267 e = NON_RANGE; 280 e = s;
268 } else if (!ltok[0]) { 281 } else if (!ltok[0]) {
269 e = EOL; 282 e = INT_MAX;
270 } else { 283 } else {
271 e = xatoi_positive(ltok); 284 e = xatoi_positive(ltok);
272 /* if the user specified and end position of 0, 285 /* if the user specified and end position of 0,
273 * that means "til the end of the line" */ 286 * that means "til the end of the line" */
274 if (e == 0) 287 if (!*ltok)
275 e = EOL; 288 e = INT_MAX;
289 else if (e < s)
290 bb_error_msg_and_die("%d<%d", e, s);
276 e--; /* again, arrays are zero based, lines are 1 based */ 291 e--; /* again, arrays are zero based, lines are 1 based */
277 if (e == s)
278 e = NON_RANGE;
279 } 292 }
280 293
281 /* add the new list */ 294 /* add the new list */
282 cut_lists = xrealloc_vector(cut_lists, 4, nlists); 295 cut_lists = xrealloc_vector(cut_lists, 4, nlists);
283 /* NB: startpos is always >= 0, 296 /* NB: startpos is always >= 0 */
284 * while endpos may be = NON_RANGE (-1) */
285 cut_lists[nlists].startpos = s; 297 cut_lists[nlists].startpos = s;
286 cut_lists[nlists].endpos = e; 298 cut_lists[nlists].endpos = e;
287 nlists++; 299 nlists++;
@@ -294,7 +306,8 @@ int cut_main(int argc UNUSED_PARAM, char **argv)
294 /* now that the lists are parsed, we need to sort them to make life 306 /* now that the lists are parsed, we need to sort them to make life
295 * easier on us when it comes time to print the chars / fields / lines 307 * easier on us when it comes time to print the chars / fields / lines
296 */ 308 */
297 qsort(cut_lists, nlists, sizeof(cut_lists[0]), cmpfunc); 309 if (!(opt & CUT_OPT_NOSORT_FLGS))
310 qsort(cut_lists, nlists, sizeof(cut_lists[0]), cmpfunc);
298 } 311 }
299 312
300 { 313 {
@@ -309,7 +322,7 @@ int cut_main(int argc UNUSED_PARAM, char **argv)
309 retval = EXIT_FAILURE; 322 retval = EXIT_FAILURE;
310 continue; 323 continue;
311 } 324 }
312 cut_file(file, delim, cut_lists, nlists); 325 cut_file(file, delim, odelim, cut_lists, nlists);
313 fclose_if_not_stdin(file); 326 fclose_if_not_stdin(file);
314 } while (*++argv); 327 } while (*++argv);
315 328
diff --git a/testsuite/cut.tests b/testsuite/cut.tests
index 110340277..d705b91c3 100755
--- a/testsuite/cut.tests
+++ b/testsuite/cut.tests
@@ -15,4 +15,68 @@ testing "cut '-' (stdin) and multi file handling" \
15 "the quick brown fox\n" \ 15 "the quick brown fox\n" \
16 "jumps over the lazy dog\n" \ 16 "jumps over the lazy dog\n" \
17 17
18abc="\
19one:two:three:four:five:six:seven
20alpha:beta:gamma:delta:epsilon:zeta:eta:theta:iota:kappa:lambda:mu
21the quick brown fox jumps over the lazy dog
22"
23
24testing "cut -b a,a,a" "cut -b 3,3,3 input" "e\np\ne\n" "$abc" ""
25
26testing "cut -b overlaps" "cut -b 1-3,2-5,7-9,9-10 input" \
27 "one:to:th\nalphabeta\nthe qick \n" "$abc" ""
28testing "-b encapsulated" "cut -b 3-8,4-6 input" "e:two:\npha:be\ne quic\n" \
29 "$abc" ""
30# --output-delimiter not implemnted (yet?)
31#testing "cut -bO overlaps" \
32# "cut --output-delimiter ' ' -b 1-3,2-5,7-9,9-10 input" \
33# "one:t o:th\nalpha beta\nthe q ick \n" "$abc" ""
34testing "cut high-low error" "cut -b 8-3 abc.txt 2>/dev/null || echo err" "err\n" \
35 "$abc" ""
36
37testing "cut -c a-b" "cut -c 4-10 input" ":two:th\nha:beta\n quick \n" "$abc" ""
38testing "cut -c a-" "cut -c 41- input" "\ntheta:iota:kappa:lambda:mu\ndog\n" "$abc" ""
39testing "cut -c -b" "cut -c -39 input" \
40 "one:two:three:four:five:six:seven\nalpha:beta:gamma:delta:epsilon:zeta:eta\nthe quick brown fox jumps over the lazy\n" \
41 "$abc" ""
42testing "cut -c a" "cut -c 40 input" "\n:\n \n" "$abc" ""
43testing "cut -c a,b-c,d" "cut -c 3,5-7,10 input" "etwoh\npa:ba\nequi \n" "$abc" ""
44
45testing "cut -f a-" "cut -d ':' -f 5- input" "five:six:seven\nepsilon:zeta:eta:theta:iota:kappa:lambda:mu\nthe quick brown fox jumps over the lazy dog\n" "$abc" ""
46
47testing "cut show whole line with no delim" "cut -d ' ' -f 3 input" \
48 "one:two:three:four:five:six:seven\nalpha:beta:gamma:delta:epsilon:zeta:eta:theta:iota:kappa:lambda:mu\nbrown\n" "$abc" ""
49
50testing "cut with echo, -c (a-b)" "echo 'ref_categorie=test' | cut -c 1-15 " "ref_categorie=t\n" "" ""
51testing "cut with echo, -c (a)" "echo 'ref_categorie=test' | cut -c 14" "=\n" "" ""
52
53testing "cut with -c (a,b,c)" "cut -c 4,5,20 input" "det\n" "abcdefghijklmnopqrstuvwxyz" ""
54
55testing "cut with -b (a,b,c)" "cut -b 4,5,20 input" "det\n" "abcdefghijklmnopqrstuvwxyz" ""
56
57input="\
58406378:Sales:Itorre:Jan
59031762:Marketing:Nasium:Jim
60636496:Research:Ancholie:Mel
61396082:Sales:Jucacion:Ed
62"
63testing "cut with -d -f(:) -s" "cut -d: -f3 -s input" "Itorre\nNasium\nAncholie\nJucacion\n" "$input" ""
64testing "cut with -d -f( ) -s" "cut -d' ' -f3 -s input && echo yes" "yes\n" "$input" ""
65testing "cut with -d -f(a) -s" "cut -da -f3 -s input" "n\nsium:Jim\n\ncion:Ed\n" "$input" ""
66testing "cut with -d -f(a) -s -n" "cut -da -f3 -s -n input" "n\nsium:Jim\n\ncion:Ed\n" "$input" ""
67
68# substitute for awk
69testing "cut -DF" "cut -DF 2,7,5" \
70 "said and your\nare\nis demand. supply\nforecast :\nyou you better,\n\nEm: Took hate\n" "" \
71"Bother, said Pooh. It's your husband, and he has a gun.
72Cheerios are donut seeds.
73Talk is cheap because supply exceeds demand.
74Weather forecast for tonight : dark.
75Apple: you can buy better, but you can't pay more.
76Subcalifragilisticexpialidocious.
77Auntie Em: Hate you, hate Kansas. Took the dog. Dorothy."
78
79testing "cut empty field" "cut -d ':' -f 1-3" "a::b\n" "" "a::b\n"
80testing "cut empty field 2" "cut -d ':' -f 3-5" "b::c\n" "" "a::b::c:d\n"
81
18exit $FAILCOUNT 82exit $FAILCOUNT