csvtool: "pastecol" with "preserving" semantics.
[csv/csv.git] / examples / csvtool.ml
1 (* Handy tool for managing CSV files.
2    @author Richard Jones <rjones@redhat.com>
3 *)
4
5 open Printf
6
7 (*------------------------------ start of code from extlib *)
8 exception Invalid_string
9
10 let find str sub =
11   let sublen = String.length sub in
12   if sublen = 0 then
13     0
14   else
15     let found = ref 0 in
16     let len = String.length str in
17     try
18       for i = 0 to len - sublen do
19         let j = ref 0 in
20         while String.unsafe_get str (i + !j) = String.unsafe_get sub !j do
21           incr j;
22           if !j = sublen then begin found := i; raise Exit; end;
23         done;
24       done;
25       raise Invalid_string
26     with
27       Exit -> !found
28
29 let split str sep =
30   let p = find str sep in
31   let len = String.length sep in
32   let slen = String.length str in
33   String.sub str 0 p, String.sub str (p + len) (slen - p - len)
34
35 let nsplit str sep =
36   if str = "" then []
37   else (
38     let rec nsplit str sep =
39       try
40         let s1 , s2 = split str sep in
41         s1 :: nsplit s2 sep
42       with
43         Invalid_string -> [str]
44     in
45     nsplit str sep
46   )
47
48 type 'a mut_list =  {
49         hd: 'a;
50         mutable tl: 'a list
51 }
52 external inj : 'a mut_list -> 'a list = "%identity"
53
54 let dummy_node () = { hd = Obj.magic (); tl = [] }
55
56 let rec drop n = function
57   | _ :: l when n > 0 -> drop (n-1) l
58   | l -> l
59
60 let take n l =
61   let rec loop n dst = function
62     | h :: t when n > 0 ->
63         let r = { hd = h; tl = [] } in
64         dst.tl <- inj r;
65         loop (n-1) r t
66     | _ ->
67         ()
68   in
69   let dummy = dummy_node() in
70   loop n dummy l;
71   dummy.tl
72 (*------------------------------ end of extlib code *)
73
74 (* Parse column specs. *)
75 type colspec = range list
76 and range =
77   | Col of int (* 0 *)
78   | Range of int * int (* 2-5 *)
79   | ToEnd of int (* 7- *)
80
81 let parse_colspec ~count_zero colspec =
82   let cols = nsplit colspec "," in
83   let cols = List.map (
84     fun col ->
85       try
86         (try
87            let first, second = split col "-" in
88            if second <> "" then
89              Range (int_of_string first, int_of_string second)
90            else
91              ToEnd (int_of_string first)
92          with
93            Invalid_string ->
94              Col (int_of_string col)
95         )
96       with
97         Failure "int_of_string" ->
98           failwith (colspec ^ ":" ^ col ^ ": invalid column-spec")
99   ) cols in
100
101   (* Adjust so columns always count from zero. *)
102   if not count_zero then
103     List.map (
104       function
105       | Col c -> Col (c-1)
106       | Range (s, e) -> Range (s-1, e-1)
107       | ToEnd e -> ToEnd (e-1)
108     ) cols
109   else
110     cols
111
112 let rec width_of_colspec = function
113   | [] -> 0
114   | Col c :: rest -> 1 + width_of_colspec rest
115   | Range (s, e) :: rest -> (e-s+1) + width_of_colspec rest
116   | ToEnd _ :: _ ->
117       failwith "width_of_colspec: cannot calculate width of an open column spec (one which contains 'N-')"
118
119 (* For closed column specs, this preserves the correct width in the
120  * result.
121  *)
122 let cols_of_colspec colspec row =
123   let rec loop = function
124     | [] -> []
125     | Col c :: rest ->
126         (try List.nth row c
127          with Failure "nth" -> "") :: loop rest
128     | Range (s, e) :: rest ->
129         let width = e-s+1 in
130         let range = take width (drop s row) in
131         let range = List.hd (Csv.set_columns width [range]) in
132         List.append range (loop rest)
133     | ToEnd e :: rest ->
134         List.append (drop e row) (loop rest)
135   in
136   loop colspec
137
138 (* The actual commands. *)
139 let cmd_cols ~input_sep ~output_sep ~chan colspec files =
140   List.iter (
141     fun filename ->
142       let csv = Csv.load ~separator:input_sep filename in
143       let csv = List.map (cols_of_colspec colspec) csv in
144       Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
145   ) files
146
147 let cmd_namedcols ~input_sep ~output_sep ~chan names files =
148   List.iter (
149     fun filename ->
150       let csv = Csv.load ~separator:input_sep filename in
151       let header, data =
152         match csv with
153         | [] -> failwith "no rows in this CSV file"
154         | h :: t -> h, t in
155       (* Do the headers requested exist in the CSV file?  If not,
156        * throw an error.
157        *)
158       List.iter (
159         fun name ->
160           if not (List.mem name header) then
161             failwith ("namedcol: requested header not in CSV file: " ^ name)
162       ) names;
163       let data = Csv.associate header data in
164       let data = List.map (
165         fun row -> List.map (fun name -> List.assoc name row) names
166       ) data in
167       let data = names :: data in
168       Csv.output_all (Csv.to_channel ~separator:output_sep chan) data
169   ) files
170
171 let cmd_width ~input_sep ~chan files =
172   let width = List.fold_left (
173     fun width filename ->
174       let csv = Csv.load ~separator:input_sep filename in
175       let width = max width (Csv.columns csv) in
176       width
177   ) 0 files in
178   fprintf chan "%d\n" width
179
180 let cmd_height ~input_sep ~chan files =
181   let height = List.fold_left (
182     fun height filename ->
183       let csv = Csv.load ~separator:input_sep filename in
184       let height = height + Csv.lines csv in
185       height
186   ) 0 files in
187   fprintf chan "%d\n" height
188
189 let cmd_readable ~input_sep ~chan files =
190   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
191   Csv.save_out_readable chan csv
192
193 let cmd_cat ~input_sep ~output_sep ~chan files =
194   (* Avoid loading the whole file into memory. *)
195   let chan = Csv.to_channel ~separator:output_sep chan in
196   let f row =
197     Csv.output_record chan row
198   in
199   List.iter (
200     fun filename ->
201       let in_chan, close =
202         match filename with
203         | "-" -> stdin, false
204         | filename -> open_in filename, true in
205       Csv.iter f (Csv.of_channel ~separator:input_sep in_chan);
206       if close then close_in in_chan
207   ) files
208
209 let cmd_paste ~input_sep ~output_sep ~chan files =
210   (* Return the 1st row, concatenation of all 1st rows; whether all
211      CSV files are empty; and the CSV files without their 1st row. *)
212   let rec add_columns = function
213     | [] -> ([], true, []) (* empty CSV file list *)
214     | [] :: csvs -> (* exhausted the first CSV file *)
215        let row, empty, csvs = add_columns csvs in
216        (row, empty, [] :: csvs)
217     | (r :: csv0) :: csvs ->
218        let row, empty, csvs = add_columns csvs in
219        (r @ row, false, csv0 :: csvs) in
220   let rec paste_rows csvs final_csv =
221     let row, empty, csvs = add_columns csvs in
222     if empty then List.rev final_csv
223     else paste_rows csvs (row :: final_csv)
224   in
225   let csvs = List.map (Csv.load ~separator:input_sep) files in
226   let csv = paste_rows csvs [] in
227   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
228
229
230 (* Given [colspec1] and [colspec2], return an associative list that
231    indicates the correspondence between the i th column specified by
232    [colspec1] and the corresponding one in [colspec2]. *)
233 let rec colspec_map colspec1 colspec2 =
234   match colspec1 with
235   | [] -> []
236   | Col i :: tl1 ->
237      (match colspec2 with
238       | Col k :: tl2 -> (i,k) :: colspec_map tl1 tl2
239       | Range(k,l) :: tl2 ->
240          let colspec2 = if k < l then Range(k+1, l) :: tl2
241                         else if k = l then tl2
242                         else (* k > l *) Range(k-1, l) :: tl2 in
243          (i,k) :: colspec_map tl1 colspec2
244       | ToEnd k :: _ ->
245          (i, k) :: colspec_map tl1 [ToEnd(k+1)]
246       | [] -> failwith "pastecol: the second range does not contain \
247                        enough columns")
248   | Range(i,j) :: tl1 ->
249      let colspec1 = if i < j then Range(i+1, j) :: tl1
250                     else if i = j then tl1
251                     else (* i > j *) Range(i-1, j) :: tl1 in
252      (match colspec2 with
253       | Col k :: tl2 ->  (i,k) :: colspec_map colspec1 tl2
254       | Range(k,l) :: tl2 ->
255          let colspec2 = if k < l then Range(k+1, l) :: tl2
256                         else if k = l then tl2
257                         else (* k > l *) Range(k-1, l) :: tl2 in
258          (i,k) :: colspec_map colspec1 colspec2
259       | ToEnd k :: _ ->
260          (i,k) :: colspec_map colspec1 [ToEnd(k+1)]
261       | [] -> failwith "pastecol: the second range does not contain \
262                        enough columns")
263   | ToEnd i :: _ ->
264      let m = sprintf "pastecol: the first range cannot contain an open \
265                       range like %i-" i in
266      failwith m
267
268 (* When several bindings are defined for an initial column, use the
269    last one.  ASSUME that the associative map is sorted w.r.t. the
270    first data. *)
271 let rec reduce_colspec_map = function
272   | (i,_) :: (((j,_) :: _) as tl) when (i: int) = j ->
273      reduce_colspec_map tl (* maybe (j,_) is also supplanted *)
274   | m :: tl -> m :: reduce_colspec_map tl
275   | [] -> []
276
277 let cmd_pastecol ~input_sep ~output_sep ~chan colspec1 colspec2 file1 file2 =
278   let csv1 = Csv.load ~separator:input_sep file1 in
279   let csv2 = Csv.load ~separator:input_sep file2 in
280   let m = colspec_map colspec1 colspec2 in
281   let m = List.stable_sort (fun (i,_) (j,_) -> compare (i:int) j) m in
282   let m = reduce_colspec_map m in
283   let rec update m curr_col row1 row2 =
284     match m with
285     | [] -> row1 (* substitutions exhausted *)
286     | (i, j) :: m_tl ->
287        match row1 with
288        | [] -> (* row exhausted but some remaining substitutions must
289                  be performed.  Create new columns. *)
290           if curr_col = i then
291             let c = try List.nth row2 j with _ -> "" in
292             c :: update m_tl (curr_col + 1) [] row2
293           else (* curr_col < i because the mapping (i,j) is dropped after *)
294             "" :: update m (curr_col + 1) [] row2
295        | c :: row1_tl ->
296           if curr_col = i then
297             let c' = try let c' = List.nth row2 j in
298                          if c' = "" then c else c'
299                      with _ -> c in
300             c' :: update m_tl (curr_col + 1) row1_tl row2
301           else (* curr_col < i *)
302             c :: update m (curr_col + 1) row1_tl row2
303   in
304   let csv = List.map2 (update m 0) csv1 csv2 in
305   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
306
307
308 let cmd_set_columns ~input_sep ~output_sep ~chan cols files =
309   (* Avoid loading the whole file into memory. *)
310   let f row =
311     let csv = [row] in
312     let csv = Csv.set_columns cols csv in
313     Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
314   in
315   List.iter (
316     fun filename ->
317       let in_chan, close =
318         match filename with
319         | "-" -> stdin, false
320         | filename -> open_in filename, true in
321       Csv.iter f (Csv.of_channel ~separator:input_sep in_chan);
322       if close then close_in in_chan
323   ) files
324
325 let cmd_set_rows ~input_sep ~output_sep ~chan rows files =
326   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
327   let csv = Csv.set_rows rows csv in
328   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
329
330 let cmd_head ~input_sep ~output_sep ~chan rows files =
331   (* Avoid loading the whole file into memory, or even loading
332    * later files.
333    *)
334   let nr_rows = ref rows in
335   let chan = Csv.to_channel ~separator:output_sep chan in
336   let f row =
337     if !nr_rows > 0 then (
338       decr nr_rows;
339       Csv.output_record chan row
340     )
341   in
342   List.iter (
343     fun filename ->
344       if !nr_rows > 0 then (
345         let in_chan, close =
346           match filename with
347           | "-" -> stdin, false
348           | filename -> open_in filename, true in
349         Csv.iter f (Csv.of_channel ~separator:input_sep in_chan);
350         if close then close_in in_chan
351       )
352   ) files
353
354 let cmd_drop ~input_sep ~output_sep ~chan rows files =
355   (* Avoid loading the whole file into memory. *)
356   let nr_rows = ref rows in
357   let chan = Csv.to_channel ~separator:output_sep chan in
358   let f row =
359     if !nr_rows = 0 then
360       Csv.output_record chan row
361     else
362       decr nr_rows
363   in
364   List.iter (
365     fun filename ->
366       let in_chan, close =
367         match filename with
368         | "-" -> stdin, false
369         | filename -> open_in filename, true in
370       Csv.iter f (Csv.of_channel ~separator:input_sep in_chan);
371       if close then close_in in_chan
372   ) files
373
374 let cmd_square ~input_sep ~output_sep ~chan files =
375   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
376   let csv = Csv.square csv in
377   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
378
379 let cmd_sub ~input_sep ~output_sep ~chan r c rows cols files =
380   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
381   let csv = Csv.sub r c rows cols csv in
382   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
383
384 let cmd_replace ~input_sep ~output_sep ~chan colspec update files =
385   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
386
387   (* Load the update CSV file in. *)
388   let update = Csv.load ~separator:input_sep update in
389
390   (* Compare two rows for equality by considering only the columns
391    * in colspec.
392    *)
393   let equal row1 row2 =
394     let row1 = cols_of_colspec colspec row1 in
395     let row2 = cols_of_colspec colspec row2 in
396     0 = Csv.compare [row1] [row2]
397   in
398
399   (* Look for rows in the original to be replaced by rows from the
400    * update file.  This is an ugly O(n^2) hack (XXX).
401    *)
402   let csv = List.filter (
403     fun row -> not (List.exists (equal row) update)
404   ) csv in
405   let csv = csv @ update in
406   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
407
408 let cmd_transpose ~input_sep ~output_sep ~chan files =
409   List.iter (fun file ->
410              let tr = Csv.transpose (Csv.load ~separator:input_sep file) in
411              Csv.output_all (Csv.to_channel ~separator:output_sep chan) tr
412             ) files
413
414 let cmd_call ~input_sep ~output_sep ~chan command files =
415   (* Avoid loading the whole file into memory. *)
416   let f row =
417     let cmd =
418       command ^ " " ^ String.concat " " (List.map Filename.quote row) in
419     let code = Sys.command cmd in
420     if code <> 0 then (
421       eprintf "%s: terminated with exit code %d\n" command code;
422       exit code
423     )
424   in
425   List.iter (
426     fun filename ->
427       let in_chan, close =
428         match filename with
429         | "-" -> stdin, false
430         | filename -> open_in filename, true in
431       Csv.iter f (Csv.of_channel ~separator:input_sep in_chan);
432       if close then close_in in_chan
433   ) files
434
435 let rec uniq = function
436   | [] -> []
437   | [x] -> [x]
438   | x :: y :: xs when Pervasives.compare x y = 0 ->
439       uniq (x :: xs)
440   | x :: y :: xs ->
441       x :: uniq (y :: xs)
442
443 let cmd_join ~input_sep ~output_sep ~chan colspec1 colspec2 files =
444   (* Load in the files separately. *)
445   let csvs = List.map (Csv.load ~separator:input_sep) files in
446
447   (* For each CSV file, construct a hash table from row class (key) to
448    * the (possibly empty) output columns (values).
449    * Also construct a hash which has the unique list of row classes.
450    *)
451   let keys = Hashtbl.create 1023 in
452   let hashes = List.map (
453     fun csv ->
454       let hash = Hashtbl.create 1023 in
455       List.iter (
456         fun row ->
457           let key = cols_of_colspec colspec1 row in
458           let value = cols_of_colspec colspec2 row in
459           if not (Hashtbl.mem keys key) then Hashtbl.add keys key true;
460           Hashtbl.add hash key value
461       ) csv;
462       hash
463   ) csvs in
464
465   (* Get the keys. *)
466   let keys = Hashtbl.fold (fun key _ xs -> key :: xs) keys [] in
467
468   let value_width = width_of_colspec colspec2 in
469   let empty_value =
470     List.hd (Csv.set_columns value_width [[""]]) in
471   let multiple_values =
472     List.hd (Csv.set_columns value_width [["!MULTIPLE VALUES"]]) in
473
474   (* Generate output CSV. *)
475   let keys = List.sort Pervasives.compare keys in
476   let keys = List.map (fun key -> key, []) keys in
477   let csv = List.fold_left (
478     fun keys hash ->
479       List.map (
480         fun (key, values) ->
481           let value = try Hashtbl.find_all hash key with Not_found -> [] in
482           let value =
483             match value with
484             | [] -> empty_value
485             | [value] -> value
486             | _::_ -> multiple_values in
487           key, (value :: values)
488       ) keys
489   ) keys hashes in
490   let csv = List.map (
491     fun (key, values) ->
492       key @ List.flatten (List.rev values)
493   ) csv in
494   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
495
496 let rec cmd_trim ~input_sep ~output_sep ~chan (top, left, right, bottom) files =
497   let csv = List.concat (List.map (Csv.load ~separator:input_sep) files) in
498   let csv = Csv.trim ~top ~left ~right ~bottom csv in
499   Csv.output_all (Csv.to_channel ~separator:output_sep chan) csv
500
501 and trim_flags flags =
502   let set c =
503     try ignore (String.index flags c); true with Not_found -> false
504   in
505   let top = set 't' in
506   let left = set 'l' in
507   let right = set 'r' in
508   let bottom = set 'b' in
509   (top, left, right, bottom)
510
511 (* Process the arguments. *)
512 let usage =
513   "csvtool - Copyright (C) 2005-2006 Richard W.M. Jones, Merjis Ltd.
514
515 csvtool is a tool for performing manipulations on CSV files from shell scripts.
516
517 Summary:
518   csvtool [-options] command [command-args] input.csv [input2.csv [...]]
519
520 Commands:
521   col <column-spec>
522     Return one or more columns from the CSV file.
523
524     For <column-spec>, see below.
525
526       Example: csvtool col 1-3,6 input.csv > output.csv
527
528   namedcol <names>
529     Assuming the first row of the CSV file is a list of column headings,
530     this returned the column(s) with the named headings.
531
532     <names> is a comma-separated list of names.
533
534       Example: csvtool namedcol Account,Cost input.csv > output.csv
535
536   width
537     Print the maximum width of the CSV file (number of columns in the
538     widest row).
539
540   height
541     Print the number of rows in the CSV file.
542
543     For most CSV files this is equivalent to 'wc -l', but note that
544     some CSV files can contain a row which breaks over two (or more)
545     lines.
546
547   setcolumns cols
548     Set the number of columns to cols (this also makes the CSV file
549     square).  Any short rows are padding with blank cells.  Any
550     long rows are truncated.
551
552   setrows rows
553     'setrows n' sets the number of rows to 'n'.  If there are fewer
554     than 'n' rows in the CSV files, then empty blank lines are added.
555
556   head rows
557   take rows
558     'head n' and 'take n' (which are synonyms) take the first 'n'
559     rows.  If there are fewer than 'n' rows, padding is not added.
560
561   drop rows
562     Drop the first 'rows' rows and return the rest (if any).
563
564       Example:
565         To remove the headings from a CSV file with headings:
566           csvtool drop 1 input.csv > output.csv
567
568         To extract rows 11 through 20 from a file:
569           csvtool drop 10 input.csv | csvtool take 10 - > output.csv
570
571   cat
572     This concatenates the input files together and writes them to
573     the output.  You can use this to change the separator character.
574
575       Example: csvtool -t TAB -u COMMA cat input.tsv > output.csv
576
577   paste
578     Concatenate the columns of the files together and write them to the
579     output.
580
581       Example: csvtool paste input1.csv input2.csv > output.csv
582
583   pastecol <column-spec1> <column-spec2> input.csv update.csv
584     Replace the content of the columns referenced by <column-spec1> in the
585     file input.csv with the one of the corresponding column specified by
586     <column-spec2> in update.csv.  If a column in update.csv is empty
587     (or does not exists), the content of input.csv is left unchanged.
588
589       Example: csvtool pastecol 2-3 1- input.csv update.csv.csv > output.csv
590
591   join <column-spec1> <column-spec2>
592     Join (collate) multiple CSV files together.
593
594     <column-spec1> controls which columns are compared.
595
596     <column-spec2> controls which columns are copied into the new file.
597
598       Example:
599         csvtool join 1 2 coll1.csv coll2.csv > output.csv
600
601         In the above example, if coll1.csv contains:
602           Computers,$40
603           Software,$100
604         and coll2.csv contains:
605           Computers,$50
606         then the output will be:
607           Computers,$40,$50
608           Software,$100,
609
610   square
611     Make the CSV square, so all rows have the same length.
612
613       Example: csvtool square input.csv > input-square.csv
614
615   trim [tlrb]+
616     Trim empty cells at the top/left/right/bottom of the CSV file.
617
618       Example:
619         csvtool trim t input.csv    # trims empty rows at the top only
620         csvtool trim tb input.csv   # trims empty rows at the top & bottom
621         csvtool trim lr input.csv   # trims empty columns at left & right
622         csvtool trim tlrb input.csv # trims empty rows/columns all around
623
624   sub r c rows cols
625     Take a square subset of the CSV, top left at row r, column c, which
626     is rows deep and cols wide.  'r' and 'c' count from 1, or
627     from 0 if -z option is given.
628
629   replace <column-spec> update.csv original.csv
630     Replace rows in original.csv with rows from update.csv.  The columns
631     in <column-spec> only are used to compare rows in input.csv and
632     update.csv to see if they are candidates for replacement.
633
634       Example:
635         csvtool replace 3 updates.csv original.csv > new.csv
636         mv new.csv original.csv
637
638   transpose input.csv
639     Transpose the lines and columns of the CSV file.
640
641   call command
642     This calls the external command (or shell function) 'command'
643     followed by a parameter for each column in the CSV file.  The
644     external command is called once for each row in the CSV file.
645     If any command returns a non-zero exit code then the whole
646     program terminates.
647
648       Tip:
649         Use the shell command 'export -f funcname' to export
650         a shell function for use as a command.  Within the
651         function, use the positional parameters $1, $2, ...
652         to refer to the columns.
653
654       Example (with a shell function):
655         function test {
656           echo Column 1: $1
657           echo Column 2: $2
658         }
659         export -f test
660         csvtool call test my.csv
661
662         In the above example, if my.csv contains:
663           how,now
664           brown,cow
665         then the output is:
666           Column 1: how
667           Column 2: now
668           Column 1: brown
669           Column 2: cow
670
671   readable
672     Print the input CSV in a readable format.
673
674 Column specs:
675   A <column-spec> is a comma-separated list of column numbers
676   or column ranges.
677
678     Examples:
679       1                       Column 1 (the first, leftmost column)
680       2,5,7                   Columns 2, 5 and 7
681       1-3,5                   Columns 1, 2, 3 and 5
682       1,5-                    Columns 1, 5 and up.
683
684   Columns are numbered starting from 1 unless the -z option is given.
685
686 Input files:
687   csvtool takes a list of input file(s) from the command line.
688
689   If an input filename is '-' then take input from stdin.
690
691 Output file:
692   Normally the output is written to stdout.  Use the -o option
693   to override this.
694
695 Separators:
696   The default separator character is , (comma).  To change this
697   on input or output see the -t and -u options respectively.
698
699   Use -t TAB or -u TAB (literally T-A-B!) to specify tab-separated
700   files.
701
702 Options:"
703
704 let () =
705   let input_sep = ref ',' in
706   let set_input_sep = function
707     | "TAB" -> input_sep := '\t'
708     | "COMMA" -> input_sep := ','
709     | "SPACE" -> input_sep := ' '
710     | s -> input_sep := s.[0]
711   in
712
713   let output_sep = ref ',' in
714   let set_output_sep = function
715     | "TAB" -> output_sep := '\t'
716     | "COMMA" -> output_sep := ','
717     | "SPACE" -> output_sep := ' '
718     | s -> output_sep := s.[0]
719   in
720
721   let count_zero = ref false in
722
723   let output_file = ref "" in
724
725   let rest = ref [] in
726   let set_rest str =
727     rest := str :: !rest
728   in
729
730   let argspec = [
731     "-t", Arg.String set_input_sep,
732     "Input separator char.  Use -t TAB for tab separated input.";
733     "-u", Arg.String set_output_sep,
734     "Output separator char.  Use -u TAB for tab separated output.";
735     "-o", Arg.Set_string output_file,
736     "Write output to file (instead of stdout)";
737     "-z", Arg.Set count_zero,
738     "Number columns from 0 instead of 1";
739     "-", Arg.Unit (fun () -> set_rest "-"),
740     "" (* Hack to allow '-' for input from stdin. *)
741   ] in
742
743   Arg.parse argspec set_rest usage;
744
745   let input_sep = !input_sep in
746   let output_sep = !output_sep in
747   let count_zero = !count_zero in
748   let output_file = !output_file in
749   let rest = List.rev !rest in
750
751   (* Set up the output file. *)
752   let chan =
753     if output_file <> "" then open_out output_file
754     else stdout in
755
756   (match rest with
757      | ("col"|"cols") :: colspec :: files ->
758          let colspec = parse_colspec ~count_zero colspec in
759          cmd_cols ~input_sep ~output_sep ~chan colspec files
760      | ("namedcol"|"namedcols") :: names :: files ->
761          let names = nsplit names "," in
762          cmd_namedcols ~input_sep ~output_sep ~chan names files
763      | ("width"|"columns") :: files ->
764          cmd_width ~input_sep ~chan files
765      | ("height"|"rows") :: files ->
766          cmd_height ~input_sep ~chan files
767      | "readable" :: files ->
768          cmd_readable ~input_sep ~chan files
769      | ("cat"|"concat") :: files ->
770          cmd_cat ~input_sep ~output_sep ~chan files
771      | "paste" :: files ->
772          cmd_paste ~input_sep ~output_sep ~chan files
773      | "pastecol" :: colspec1 :: colspec2 :: file1 :: file2 :: _ ->
774          let colspec1 = parse_colspec ~count_zero colspec1 in
775          let colspec2 = parse_colspec ~count_zero colspec2 in
776          cmd_pastecol ~input_sep ~output_sep ~chan colspec1 colspec2 file1 file2
777      | ("join"|"collate") :: colspec1 :: colspec2 :: ((_::_::_) as files) ->
778          let colspec1 = parse_colspec ~count_zero colspec1 in
779          let colspec2 = parse_colspec ~count_zero colspec2 in
780          cmd_join ~input_sep ~output_sep ~chan colspec1 colspec2 files
781      | "square" :: files ->
782          cmd_square ~input_sep ~output_sep ~chan files
783      | "sub" :: r :: c :: rows :: cols :: files ->
784          let r = int_of_string r in
785          let r = if not count_zero then r-1 else r in
786          let c = int_of_string c in
787          let c = if not count_zero then c-1 else c in
788          let rows = int_of_string rows in
789          let cols = int_of_string cols in
790          cmd_sub ~input_sep ~output_sep ~chan r c rows cols files
791      | "replace" :: colspec :: update :: files ->
792          let colspec = parse_colspec ~count_zero colspec in
793          cmd_replace ~input_sep ~output_sep ~chan colspec update files
794      | ("setcolumns"|"set_columns"|"set-columns"|
795             "setcols"|"set_cols"|"set-cols") :: cols :: files ->
796          let cols = int_of_string cols in
797          cmd_set_columns ~input_sep ~output_sep ~chan cols files
798      | ("setrows"|"set_rows"|"set-rows") :: rows :: files ->
799          let rows = int_of_string rows in
800          cmd_set_rows ~input_sep ~output_sep ~chan rows files
801      | ("head"|"take") :: rows :: files ->
802          let rows = int_of_string rows in
803          cmd_head ~input_sep ~output_sep ~chan rows files
804      | "drop" :: rows :: files ->
805          let rows = int_of_string rows in
806          cmd_drop ~input_sep ~output_sep ~chan rows files
807      | "transpose" :: files ->
808          cmd_transpose ~input_sep ~output_sep ~chan files
809      | "call" :: command :: files ->
810          cmd_call ~input_sep ~output_sep ~chan command files
811      | "trim" :: flags :: files ->
812          let flags = trim_flags flags in
813          cmd_trim ~input_sep ~output_sep ~chan flags files
814      | _ ->
815          prerr_endline (Sys.executable_name ^ " --help for usage");
816          exit 2
817   );
818
819   if output_file <> "" then close_out chan