'."\n"; ?> tree.ml :: a small ocaml version of the 'tree' command-line utility
#!/usr/bin/env ocaml
(* {{{ COPYING *)
(*
 * +--------------------------------------------------------------------+
 * | Copyright (C) 2005 2006 Florent Monnier                            |
 * +--------------------------------------------------------------------+
 * | This is a small implementation of the 'tree' command-line utility. |
 * +--------------------------------------------------------------------+
 * |                                                                    |
 * | This program is free software; you can redistribute it and/or      |
 * | modify it under the terms of the GNU General Public License        |
 * | as published by the Free Software Foundation; either version 2     |
 * | of the License, or (at your option) any later version.             |
 * |                                                                    |
 * | This program is distributed in the hope that it will be useful,    |
 * | but WITHOUT ANY WARRANTY; without even the implied warranty of     |
 * | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the      |
 * | GNU General Public License for more details.                       |
 * |                                                                    |
 * | http://www.fsf.org/licensing/licenses/gpl.html                     |
 * |                                                                    |
 * | You should have received a copy of the GNU General Public License  |
 * | along with this program; if not,                                   |
 * | write to the Free Software Foundation, Inc.,                       |
 * | 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA       |
 * |                                                                    |
 * +--------------------------------------------------------------------+
 * | Author: Florent Monnier <monnier.florent(€)gmail.com>          |
 * +--------------------------------------------------------------------+
 *
 * }}} *)
#load "unix.cma"

let concat = Filename.concat ;;
(* {{{ usage *)

let usage() =
  print_endline "
  display file times:
    -mt  --modification-time
    -at  --last-access-time
    -ct  --status-change-time

  display with colors:
    -c  --colors
";
  exit(1);
;;
(* }}} *)
(* {{{ round *)

let round nb dec =
  let rec _loop level =
    if level <= 0
    then 1
    else 10 * _loop (pred level)
  in
  let mult = float(_loop dec) in
  let nb = nb *. mult in
  let nb_floor = floor nb
  and nb_ceil  = ceil nb
  in
  if (nb -. nb_floor) < (nb_ceil -. nb)
  then nb_floor /. mult
  else nb_ceil  /. mult
;;
(* }}} *)
(* {{{ string_of_round *)

let string_of_round_nopad nb dec =
  let rounded = round nb dec in
  if rounded = (floor nb)
  then string_of_int (int_of_float nb)
  else string_of_float rounded
;;

let string_of_round nb dec =
  let rounded = round nb dec in
  if rounded = (floor nb)
  then Printf.sprintf "%4d" (int_of_float nb)
  else Printf.sprintf "%4.f" rounded
;;
(* }}} *)
(* {{{ human_size *)

let units =
  let u = Array.make 4 " " in
  u.(1) <- "K";
  u.(2) <- "M";
  u.(3) <- "G";
  u
;;

let human_size ~size =
  let rec aux size level =
    if size < 1024.0 || level = 3
    then (string_of_round size 1), units.(level)
    else aux (size /. 1024.0) (succ level)
  in
  let size = float_of_int size in
  let (size, _unit) = aux size 0 in
  if _unit = " "
  then Printf.sprintf "[%s%s]" _unit size
  else Printf.sprintf "[%s%s]" size _unit
;;

let units =
  let u = Array.make 4 " " in
  u.(1) <- "K";
  u.(2) <- "M";
  u.(3) <- "G";
  u
;;

let human_size ~size =
  let rec aux size level =
    if size < 1024.0 || level = 3
    then Printf.sprintf "%6.1f%s" size units.(level)
    else aux (size /. 1024.0) (succ level) in
  let size = float_of_int size in
  aux size 0
;;
(* }}} *)
(* {{{ padding *)

let padding ~last ~depth =
  let rec aux  depth last pad_acc =
    if depth <= 0
    then pad_acc
    else begin
      let branch =
        match last with
        | false :: _ -> "|   "
        | true  :: _ -> "    "
        | [] -> "xxxx"  (* this case should never occur *)
      in
      aux (pred depth) (List.tl last) (branch ^ pad_acc)
    end
  in
  let last =
    match last with
    | head::tail -> tail  (* the head is printed from branch_mark *)
    | _ -> []
  in
  aux depth last ""
;;


let right_pad filenames =
  let rec pass_1 width rev = function [] -> (rev, width)
    | (filename,s)::t ->
        let len = String.length filename in
        pass_1 (max width len) ((filename,len,s)::rev) t
  in
  let rev, width = pass_1 0 [] filenames in

  let rec pass_2 padded = function [] -> padded
    | (filename,len,s)::t ->
        let n = width - len in
        let pad = String.make n ' ' in
        let str = (filename ^ pad) in
        pass_2 ((str,s)::padded) t
  in
  pass_2 [] rev
;;

(* }}} *)
(* {{{ branch_mark *)

let branch_mark ~last =
  match last with
  | false :: _ -> "|--"
  | true  :: _ -> "`--"
  | [] -> "" (* only from boot_dump for base_dir *)
;;

let branch_dir_mark ~last =
  match last with
  | false :: _ -> "+-- "
  | true  :: _ -> "`-- "
  | [] -> "" (* only from boot_dump for base_dir *)
;;
(* }}} *)
(* {{{ color *)
let g = char_of_int 27 ;;

let color color_name ?(label="") str () =
  let col_code = match color_name with
  (*
  | `blue      -> "01;34"
  | `yellow    -> "01;33"
  *)
  | `dark_red  -> "02;31"
  | `purple    -> "03;35"
  | `dark_grey -> "01;30"
  | `test      -> "01;32"

  |     `red -> "31;49"
  |   `green -> "32;49"
  |  `yellow -> "33;49"
  |    `blue -> "34;49"
  | `magenta -> "35;49"
  |    `cyan -> "36;49"
  |   `white -> "37;49"
  | `default -> "39;49"
  in
  Printf.sprintf "%c[%sm%s%s%c[00m" g col_code  label str  g
;;
(* }}} *)
(* {{{ human_perms *)

let human_perms ~perms =
  let string_of_octal = function
    | '1' -> "--x"
    | '2' -> "-w-"
    | '3' -> "-wx"
    | '4' -> "r--"
    | '5' -> "r-x"
    | '6' -> "rw-"
    | '7' -> "rwx"
    |  _  -> "---"
  in
  let octal_str =
    Printf.sprintf "%04o" perms
  in
  let u = string_of_octal  octal_str.[1]
  and g = string_of_octal  octal_str.[2]
  and o = string_of_octal  octal_str.[3]
  in
  (u ^ g ^ o)
;;
(* }}} *)
(* {{{ dump_total_size *)

let dump_total_size ~size ~depth ~last ~colors =
  if size = 0 then () else begin
    let pad = padding ~last:(true::last) ~depth in
    let h_size = human_size ~size in

    if colors then begin
      Printf.printf "%s" (color `yellow (pad ^ "o--->  ") ());
      Printf.printf "%s" (color `dark_red h_size ());
    end else begin
      Printf.printf "%s" (pad ^ "o--->  ");
      Printf.printf "%s" h_size;
    end;
    Printf.printf "\n";
  end
;;
(* }}} *)
(* {{{ human_time *)

let human_time time =
  (* Unix.gmtime  UTC (Coordinated Universal Time), aka GMT *)
  let t = Unix.localtime time in (* local time zone *)
  Printf.sprintf "%d-%02d-%02d/%02d:%02d"
     (t.Unix.tm_year + 1900)
      t.Unix.tm_mon
      t.Unix.tm_mday
      t.Unix.tm_hour
      t.Unix.tm_min
;;
(* }}} *)
(* {{{ which times *)

let (mt, at, ct) =
  let argc = Array.length Sys.argv in
  let (mt, at, ct) =
    (ref false, ref false, ref false)
  in
  for i = 1 to pred argc do
    if Sys.argv.(i) = "-h"  || Sys.argv.(i) = "--help"  then usage();
    if Sys.argv.(i) = "-mt" || Sys.argv.(i) = "--modification-time"  then mt := true;
    if Sys.argv.(i) = "-at" || Sys.argv.(i) = "--last-access-time"   then at := true;
    if Sys.argv.(i) = "-ct" || Sys.argv.(i) = "--status-change-time" then ct := true;
  done;
  (!mt, !at, !ct)
;;

(* }}} *)
(* {{{ dump_file *)

let dump_file ~name ~stats ~depth ~last ~colors =
  let file_name  = name in

  let perms = stats.Unix.st_perm
  and size  = stats.Unix.st_size
  in
  let atime = stats.Unix.st_atime   (* Last access time *)
  and mtime = stats.Unix.st_mtime   (* Last modification time *)
  and ctime = stats.Unix.st_ctime   (* Last status change time *)
  in
  let h_size = human_size ~size in
  let pad = padding ~last ~depth in

  let show_times = true in

  if colors then begin
    Printf.printf "%s" (color `yellow (pad ^ (branch_mark ~last)) ());
    Printf.printf " %s" (color `purple ~label:"-" (human_perms ~perms) ());
  (*Printf.printf "%s " (color `purple ~label:"perms:" (Printf.sprintf "%03o" perms) ()); (* Octal *) *)
    Printf.printf " %s"  (color `dark_red h_size ());
    Printf.printf " %s" (Filename.basename file_name);
    if show_times then begin
      if mt then Printf.printf "  %s" (color `dark_grey ~label:"mt:" (human_time mtime) ());
      if at then Printf.printf "  %s" (color `dark_grey ~label:"at:" (human_time atime) ());
      if ct then Printf.printf "  %s" (color `dark_grey ~label:"ct:" (human_time ctime) ());
    end;
  end else begin
    Printf.printf "%s" (pad ^ (branch_mark ~last));
    Printf.printf " -%s" (human_perms ~perms);
  (*Printf.printf "perms:%s " (Printf.sprintf "%03o" perms); (* Octal *) *)
    Printf.printf " %s"  (h_size);
    Printf.printf " %s" (Filename.basename file_name);
    if show_times then begin
      if mt then Printf.printf "  %s" ("mt:" ^ (human_time mtime));
      if at then Printf.printf "  %s" ("at:" ^ (human_time atime));
      if ct then Printf.printf "  %s" ("ct:" ^ (human_time ctime));
    end;
  end;
  Printf.printf "\n";
  (size)
;;
(* Unix.stats.Unix.st_mtime *)
(* }}} *)
(* {{{ dump_file_list *)

let dump_file_list  ~files ~dirs ~parent_dir ~depth ~last ~colors =

  let rec file_loop ~files ~dirs size_acc =
    match files, dirs with
    | [], [] | [], _ ->   (* this case matches a directory containing sub-directories *)
        dump_total_size ~size:(size_acc) ~depth ~last ~colors;
        (size_acc)

    | (file_name, stats)::[], [] ->  (* this case matches a file leaf *)
        let file_size = dump_file
          ~name:(concat parent_dir file_name) ~stats ~depth ~last:(true::last) ~colors
        in
        let total_size = file_size + size_acc in
        dump_total_size ~size:(total_size) ~depth ~last ~colors;
        (total_size)

    | (file_name, stats)::tail_files, dirs ->  (* iteration loop *)
        let file_size = dump_file
          ~name:(concat parent_dir file_name) ~stats ~depth ~last:(false::last) ~colors
        in
        file_loop ~files:tail_files ~dirs (file_size + size_acc)
  in
  file_loop ~files ~dirs 0;
;;

(* }}} *)
(* {{{ dump_dir[_content] *)

let rec dump_dir ~name ~stats ~depth ~last ~colors =
  let parent_dir  = name
  and parent_dir_stats = stats in

  let pad = padding ~last ~depth:(pred depth) in

  if colors then
    Printf.printf "%s%s\n"  (* with colors *)
        (color `yellow (pad ^ (branch_dir_mark ~last)) ())
        (color `blue (parent_dir ^ "/") ())
  else
    Printf.printf "%s%s%s/\n" pad (branch_dir_mark ~last) parent_dir;  (* without colors *)

  let contents = Sys.readdir parent_dir in
  let contents = Array.to_list contents in
  let rec sort c ~reg_acc ~dir_acc =
    match c with
    | [] -> (reg_acc, dir_acc)
    | name :: tl ->
        let stats = Unix.lstat (concat parent_dir name) in
        match stats.Unix.st_kind with
        (* Accumulate directories and regular files in 2 different lists: dir_acc & reg_acc *)
        | Unix.S_REG -> sort tl ~dir_acc ~reg_acc:((name,stats) :: reg_acc)
        | Unix.S_DIR -> sort tl ~reg_acc ~dir_acc:((name,stats) :: dir_acc)
        | Unix.S_LNK | Unix.S_CHR | Unix.S_BLK | Unix.S_FIFO | Unix.S_SOCK ->
            sort tl ~dir_acc ~reg_acc (* XXX *)
  in
  let (files, dirs) = sort contents ~reg_acc:[] ~dir_acc:[] in
  let files = right_pad files in

  (* For a better readablility, the files are printed before directories contents. *)
  let files_size = dump_file_list ~files ~dirs ~parent_dir ~depth ~last ~colors in

  (* {{{ print directories *)
  let rec dir_loop dirs size_acc =
    match dirs with
    | [] -> size_acc
    | (dir_name, stats) :: [] ->   (* this case matches the last directory *)
        let content_size = dump_dir
          ~name:(concat parent_dir dir_name) ~stats ~depth:(succ depth) ~last:(true::last) ~colors;
        in
        (content_size + size_acc)

    | (dir_name, stats) :: tail_dirs ->
        let content_size = dump_dir
          ~name:(concat parent_dir dir_name) ~stats ~depth:(succ depth) ~last:(false::last) ~colors;
        in
        dir_loop tail_dirs (content_size + size_acc)
  in
  let rec_content_size = dir_loop dirs files_size in
  (rec_content_size)
  (* }}} *)
;;

(* }}} *)
(* {{{ main *)

let boot_dump  base_dir  options =
  let stats = Unix.lstat base_dir in
  let colors =
    List.mem "--colors" options || List.mem "-c" options
  in
  let all_size = dump_dir ~name:base_dir ~stats ~depth:0 ~last:[] ~colors in
  if colors then
    Printf.printf "\n%s\n" (color `green ~label:"Total size:" (human_size all_size) ())
  else
    Printf.printf "\nTotal size:%s\n" (human_size all_size);
;;

let () =
  let argc = Array.length Sys.argv in

  let this_dir d yet options =
    if Sys.file_exists d
    then (boot_dump d options; print_newline(); succ yet, options)
    else ((*Printf.printf "Warning: '%s' does not exist\n" d;*) yet, d::options)
  in
  let rec parse_arg argi yet options =
    if argi < argc then begin
      let (yet, options) =
        this_dir Sys.argv.(argi) yet options
      in
      parse_arg (succ argi) yet options
    end else if yet = 0 then
      ignore(this_dir "." 0 options) (* Sys.getcwd () *)
  in
  parse_arg 1 0 [];
;;
(* }}} *)

(* vim:cindent sw=2 sts=2 ts=2 et fdm=marker
 *)