Codebase list golang-github-a8m-tree / 33585c7
New upstream version 0.0~git20171213.cf42b1e Dr. Tobias Quathamer 6 years ago
18 changed file(s) with 1382 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 draft
1 coverage
0 language: go
1 sudo: false
2 go:
3 - 1.6.4
4 - 1.7.4
5 - 1.8.3
6 - tip
7 install:
8 - go get -t -v ./...
9 script:
10 - go test -v ./...
11 - ./compileall.sh
0 tree [![Build status][travis-image]][travis-url] [![License][license-image]][license-url]
1 ---
2 > An implementation of the [`tree`](http://mama.indstate.edu/users/ice/tree/) command written in Go, that can be used programmatically.
3
4 <img src="https://raw.githubusercontent.com/a8m/tree/assets/assets/tree.png" height="300" alt="tree command">
5
6 #### Installation:
7 ```sh
8 $ go get github.com/a8m/tree/cmd/tree
9 ```
10
11 #### How to use `tree` programmatically ?
12 You can take a look on [`cmd/tree`](https://github.com/a8m/tree/blob/master/cmd/tree/tree.go), and [s3tree](http://github.com/a8m/s3tree) or see the example below.
13 ```go
14 import (
15 "github.com/a8m/tree"
16 )
17
18 func main() {
19 opts := &tree.Options{
20 // Fs, and OutFile are required fields.
21 // fs should implement the tree file-system interface(see: tree.Fs),
22 // and OutFile should be type io.Writer
23 Fs: fs,
24 OutFile: os.Stdout,
25 // ...
26 }
27 inf.New("root-dir")
28 // Visit all nodes recursively
29 inf.Visit(opts)
30 // Print nodes
31 inf.Print(opts)
32 }
33 ```
34
35 ### License
36 MIT
37
38
39 [travis-image]: https://img.shields.io/travis/a8m/tree.svg?style=flat-square
40 [travis-url]: https://travis-ci.org/a8m/tree
41 [license-image]: http://img.shields.io/npm/l/deep-keys.svg?style=flat-square
42 [license-url]: LICENSE
0 package main
1
2 import (
3 "errors"
4 "flag"
5 "fmt"
6 "os"
7
8 "github.com/a8m/tree"
9 )
10
11 var (
12 // List
13 a = flag.Bool("a", false, "")
14 d = flag.Bool("d", false, "")
15 f = flag.Bool("f", false, "")
16 ignorecase = flag.Bool("ignore-case", false, "")
17 noreport = flag.Bool("noreport", false, "")
18 l = flag.Bool("l", false, "")
19 L = flag.Int("L", 3, "")
20 P = flag.String("P", "", "")
21 I = flag.String("I", "", "")
22 o = flag.String("o", "", "")
23 // Files
24 s = flag.Bool("s", false, "")
25 h = flag.Bool("h", false, "")
26 p = flag.Bool("p", false, "")
27 u = flag.Bool("u", false, "")
28 g = flag.Bool("g", false, "")
29 Q = flag.Bool("Q", false, "")
30 D = flag.Bool("D", false, "")
31 inodes = flag.Bool("inodes", false, "")
32 device = flag.Bool("device", false, "")
33 // Sort
34 U = flag.Bool("U", false, "")
35 v = flag.Bool("v", false, "")
36 t = flag.Bool("t", false, "")
37 c = flag.Bool("c", false, "")
38 r = flag.Bool("r", false, "")
39 dirsfirst = flag.Bool("dirsfirst", false, "")
40 sort = flag.String("sort", "", "")
41 // Graphics
42 i = flag.Bool("i", false, "")
43 C = flag.Bool("C", false, "")
44 )
45
46 var usage = `Usage: tree [options...] [paths...]
47
48 Options:
49 ------- Listing options -------
50 -a All files are listed.
51 -d List directories only.
52 -l Follow symbolic links like directories.
53 -f Print the full path prefix for each file.
54 -L Descend only level directories deep.
55 -P List only those files that match the pattern given.
56 -I Do not list files that match the given pattern.
57 --ignore-case Ignore case when pattern matching.
58 --noreport Turn off file/directory count at end of tree listing.
59 -o filename Output to file instead of stdout.
60 -------- File options ---------
61 -Q Quote filenames with double quotes.
62 -p Print the protections for each file.
63 -u Displays file owner or UID number.
64 -g Displays file group owner or GID number.
65 -s Print the size in bytes of each file.
66 -h Print the size in a more human readable way.
67 -D Print the date of last modification or (-c) status change.
68 --inodes Print inode number of each file.
69 --device Print device ID number to which each file belongs.
70 ------- Sorting options -------
71 -v Sort files alphanumerically by version.
72 -t Sort files by last modification time.
73 -c Sort files by last status change time.
74 -U Leave files unsorted.
75 -r Reverse the order of the sort.
76 --dirsfirst List directories before files (-U disables).
77 --sort X Select sort: name,version,size,mtime,ctime.
78 ------- Graphics options ------
79 -i Don't print indentation lines.
80 -C Turn colorization on always.
81 `
82
83 type fs struct{}
84
85 func (f *fs) Stat(path string) (os.FileInfo, error) {
86 return os.Lstat(path)
87 }
88 func (f *fs) ReadDir(path string) ([]string, error) {
89 dir, err := os.Open(path)
90 if err != nil {
91 return nil, err
92 }
93 names, err := dir.Readdirnames(-1)
94 dir.Close()
95 if err != nil {
96 return nil, err
97 }
98 return names, nil
99 }
100
101 func main() {
102 flag.Usage = func() { fmt.Fprint(os.Stderr, usage) }
103 var nd, nf int
104 var dirs = []string{"."}
105 flag.Parse()
106 // Make it work with leading dirs
107 if args := flag.Args(); len(args) > 0 {
108 dirs = args
109 }
110 // Output file
111 var outFile = os.Stdout
112 var err error
113 if *o != "" {
114 outFile, err = os.Create(*o)
115 if err != nil {
116 errAndExit(err)
117 }
118 }
119 defer outFile.Close()
120 // Check sort-type
121 if *sort != "" {
122 switch *sort {
123 case "version", "mtime", "ctime", "name", "size":
124 default:
125 msg := fmt.Sprintf("sort type '%s' not valid, should be one of: "+
126 "name,version,size,mtime,ctime", *sort)
127 errAndExit(errors.New(msg))
128 }
129 }
130 // Set options
131 opts := &tree.Options{
132 // Required
133 Fs: new(fs),
134 OutFile: outFile,
135 // List
136 All: *a,
137 DirsOnly: *d,
138 FullPath: *f,
139 DeepLevel: *L,
140 FollowLink: *l,
141 Pattern: *P,
142 IPattern: *I,
143 IgnoreCase: *ignorecase,
144 // Files
145 ByteSize: *s,
146 UnitSize: *h,
147 FileMode: *p,
148 ShowUid: *u,
149 ShowGid: *g,
150 LastMod: *D,
151 Quotes: *Q,
152 Inodes: *inodes,
153 Device: *device,
154 // Sort
155 NoSort: *U,
156 ReverSort: *r,
157 DirSort: *dirsfirst,
158 VerSort: *v || *sort == "version",
159 ModSort: *t || *sort == "mtime",
160 CTimeSort: *c || *sort == "ctime",
161 NameSort: *sort == "name",
162 SizeSort: *sort == "size",
163 // Graphics
164 NoIndent: *i,
165 Colorize: *C,
166 }
167 for _, dir := range dirs {
168 inf := tree.New(dir)
169 d, f := inf.Visit(opts)
170 nd, nf = nd+d, nf+f
171 inf.Print(opts)
172 }
173 // Print footer report
174 if !*noreport {
175 footer := fmt.Sprintf("\n%d directories", nd)
176 if !opts.DirsOnly {
177 footer += fmt.Sprintf(", %d files", nf)
178 }
179 fmt.Fprintln(outFile, footer)
180 }
181 }
182
183 func usageAndExit(msg string) {
184 if msg != "" {
185 fmt.Fprintf(os.Stderr, msg)
186 fmt.Fprintf(os.Stderr, "\n\n")
187 }
188 flag.Usage()
189 fmt.Fprintf(os.Stderr, "\n")
190 os.Exit(1)
191 }
192
193 func errAndExit(err error) {
194 fmt.Fprintf(os.Stderr, "tree: \"%s\"\n", err)
195 os.Exit(1)
196 }
0 package tree
1
2 import (
3 "fmt"
4 "os"
5 "path/filepath"
6 "strings"
7 )
8
9 const Escape = "\x1b"
10 const (
11 Reset int = 0
12 // Not used, remove.
13 Bold int = 1
14 Black int = iota + 28
15 Red
16 Green
17 Yellow
18 Blue
19 Magenta
20 Cyan
21 White
22 )
23
24 // ANSIColor
25 func ANSIColor(node *Node, s string) string {
26 var style string
27 var mode = node.Mode()
28 var ext = filepath.Ext(node.Name())
29 switch {
30 case contains([]string{".bat", ".btm", ".cmd", ".com", ".dll", ".exe"}, ext):
31 style = "1;32"
32 case contains([]string{".arj", ".bz2", ".deb", ".gz", ".lzh", ".rpm",
33 ".tar", ".taz", ".tb2", ".tbz2", ".tbz", ".tgz", ".tz", ".tz2", ".z",
34 ".zip", ".zoo"}, ext):
35 style = "1;31"
36 case contains([]string{".asf", ".avi", ".bmp", ".flac", ".gif", ".jpg",
37 "jpeg", ".m2a", ".m2v", ".mov", ".mp3", ".mpeg", ".mpg", ".ogg", ".ppm",
38 ".rm", ".tga", ".tif", ".wav", ".wmv",
39 ".xbm", ".xpm"}, ext):
40 style = "1;35"
41 case node.IsDir() || mode&os.ModeDir != 0:
42 style = "1;34"
43 case mode&os.ModeNamedPipe != 0:
44 style = "40;33"
45 case mode&os.ModeSocket != 0:
46 style = "40;1;35"
47 case mode&os.ModeDevice != 0 || mode&os.ModeCharDevice != 0:
48 style = "40;1;33"
49 case mode&os.ModeSymlink != 0:
50 if _, err := filepath.EvalSymlinks(node.path); err != nil {
51 style = "40;1;31"
52 } else {
53 style = "1;36"
54 }
55 case mode&modeExecute != 0:
56 style = "1;32"
57 default:
58 return s
59 }
60 return fmt.Sprintf("%s[%sm%s%s[%dm", Escape, style, s, Escape, Reset)
61 }
62
63 // case-insensitive contains helper
64 func contains(slice []string, str string) bool {
65 for _, val := range slice {
66 if val == strings.ToLower(str) {
67 return true
68 }
69 }
70 return false
71 }
72
73 // TODO: HTMLColor
0 package tree
1
2 import (
3 "os"
4 "syscall"
5 "testing"
6 )
7
8 var extsTests = []struct {
9 name string
10 expected string
11 }{
12 {"foo.jpg", "\x1b[1;35mfoo.jpg\x1b[0m"},
13 {"bar.tar", "\x1b[1;31mbar.tar\x1b[0m"},
14 {"baz.exe", "\x1b[1;32mbaz.exe\x1b[0m"},
15 }
16
17 func TestExtension(t *testing.T) {
18 for _, test := range extsTests {
19 fi := &file{name: test.name}
20 no := &Node{FileInfo: fi}
21 if actual := ANSIColor(no, fi.name); actual != test.expected {
22 t.Errorf("\ngot:\n%+v\nexpected:\n%+v", actual, test.expected)
23 }
24 }
25 }
26
27 var modeTests = []struct {
28 path string
29 name string
30 expected string
31 mode os.FileMode
32 }{
33 {"", "simple", "simple", os.FileMode(0)},
34 {"", "dir", "\x1b[1;34mdir\x1b[0m", os.ModeDir},
35 {"", "socket", "\x1b[40;1;35msocket\x1b[0m", os.ModeSocket},
36 {"", "fifo", "\x1b[40;33mfifo\x1b[0m", os.ModeNamedPipe},
37 {"", "block", "\x1b[40;1;33mblock\x1b[0m", os.ModeDevice},
38 {"", "char", "\x1b[40;1;33mchar\x1b[0m", os.ModeCharDevice},
39 {"", "exist-symlink", "\x1b[1;36mexist-symlink\x1b[0m", os.ModeSymlink},
40 {"fake-path-a8m", "fake-path", "\x1b[40;1;31mfake-path\x1b[0m", os.ModeSymlink},
41 {"", "exec", "\x1b[1;32mexec\x1b[0m", os.FileMode(syscall.S_IXUSR)},
42 }
43
44 func TestFileMode(t *testing.T) {
45 for _, test := range modeTests {
46 fi := &file{name: test.name, mode: test.mode}
47 no := &Node{FileInfo: fi, path: test.path}
48 if actual := ANSIColor(no, fi.name); actual != test.expected {
49 t.Errorf("\ngot:\n%+v\nexpected:\n%+v", actual, test.expected)
50 }
51 }
52 }
0 #!/bin/bash
1
2 go tool dist list >/dev/null || {
3 echo 1>&2 "go tool dist list not supported - can't check compile"
4 exit 0
5 }
6
7 while read -r line; do
8 parts=(${line//\// })
9 export GOOS=${parts[0]}
10 export GOARCH=${parts[1]}
11 echo Try GOOS=${GOOS} GOARCH=${GOARCH}
12 go install
13 done < <(go tool dist list)
0 //+build darwin freebsd netbsd
1
2 package tree
3
4 import (
5 "os"
6 "syscall"
7 )
8
9 func CTimeSort(f1, f2 os.FileInfo) bool {
10 s1, ok1 := f1.Sys().(*syscall.Stat_t)
11 s2, ok2 := f2.Sys().(*syscall.Stat_t)
12 // If this type of node isn't an os node then revert to ModSort
13 if !ok1 || !ok2 {
14 return ModSort(f1, f2)
15 }
16 return s1.Ctimespec.Sec < s2.Ctimespec.Sec
17 }
0 //+build !linux,!openbsd,!dragonfly,!android,!solaris,!darwin,!freebsd,!netbsd
1
2 package tree
3
4 // CtimeSort for unsupported OS - just compare ModTime
5 var CTimeSort = ModSort
0 //+build linux openbsd dragonfly android solaris
1
2 package tree
3
4 import (
5 "os"
6 "syscall"
7 )
8
9 func CTimeSort(f1, f2 os.FileInfo) bool {
10 s1, ok1 := f1.Sys().(*syscall.Stat_t)
11 s2, ok2 := f2.Sys().(*syscall.Stat_t)
12 // If this type of node isn't an os node then revert to ModSort
13 if !ok1 || !ok2 {
14 return ModSort(f1, f2)
15 }
16 return s1.Ctim.Sec < s2.Ctim.Sec
17 }
0 //+build dragonfly freebsd openbsd solaris windows
1
2 package tree
3
4 import "syscall"
5
6 const modeExecute = syscall.S_IXUSR
0 //+build android darwin linux nacl netbsd
1
2 package tree
3
4 import "syscall"
5
6 const modeExecute = syscall.S_IXUSR | syscall.S_IXGRP | syscall.S_IXOTH
0 //+build !dragonfly,!freebsd,!openbsd,!solaris,!windows,!android,!darwin,!linux,!nacl,!netbsd
1
2 package tree
3
4 const modeExecute = 0
0 package tree
1
2 import (
3 "errors"
4 "fmt"
5 "io"
6 "os"
7 "os/user"
8 "path/filepath"
9 "regexp"
10 "sort"
11 "strconv"
12 "strings"
13 )
14
15 // Node represent some node in the tree
16 // contains FileInfo, and its childs
17 type Node struct {
18 os.FileInfo
19 path string
20 depth int
21 err error
22 nodes Nodes
23 vpaths map[string]bool
24 }
25
26 // List of nodes
27 type Nodes []*Node
28
29 // To use this package programmatically, you must implement this
30 // interface.
31 // For example: PTAL on 'cmd/tree/tree.go'
32 type Fs interface {
33 Stat(path string) (os.FileInfo, error)
34 ReadDir(path string) ([]string, error)
35 }
36
37 // Options store the configuration for specific tree.
38 // Note, that 'Fs', and 'OutFile' are required (OutFile can be os.Stdout).
39 type Options struct {
40 Fs Fs
41 OutFile io.Writer
42 // List
43 All bool
44 DirsOnly bool
45 FullPath bool
46 IgnoreCase bool
47 FollowLink bool
48 DeepLevel int
49 Pattern string
50 IPattern string
51 // File
52 ByteSize bool
53 UnitSize bool
54 FileMode bool
55 ShowUid bool
56 ShowGid bool
57 LastMod bool
58 Quotes bool
59 Inodes bool
60 Device bool
61 // Sort
62 NoSort bool
63 VerSort bool
64 ModSort bool
65 DirSort bool
66 NameSort bool
67 SizeSort bool
68 CTimeSort bool
69 ReverSort bool
70 // Graphics
71 NoIndent bool
72 Colorize bool
73 }
74
75 // New get path and create new node(root).
76 func New(path string) *Node {
77 return &Node{path: path, vpaths: make(map[string]bool)}
78 }
79
80 // Visit all files under the given node.
81 func (node *Node) Visit(opts *Options) (dirs, files int) {
82 // visited paths
83 if path, err := filepath.Abs(node.path); err == nil {
84 path = filepath.Clean(path)
85 node.vpaths[path] = true
86 }
87 // stat
88 fi, err := opts.Fs.Stat(node.path)
89 if err != nil {
90 node.err = err
91 return
92 }
93 node.FileInfo = fi
94 if !fi.IsDir() {
95 return 0, 1
96 }
97 // increase dirs only if it's a dir, but not the root.
98 if node.depth != 0 {
99 dirs++
100 }
101 // DeepLevel option
102 if opts.DeepLevel > 0 && opts.DeepLevel <= node.depth {
103 return
104 }
105 names, err := opts.Fs.ReadDir(node.path)
106 if err != nil {
107 node.err = err
108 return
109 }
110 node.nodes = make(Nodes, 0)
111 for _, name := range names {
112 // "all" option
113 if !opts.All && strings.HasPrefix(name, ".") {
114 continue
115 }
116 nnode := &Node{
117 path: filepath.Join(node.path, name),
118 depth: node.depth + 1,
119 vpaths: node.vpaths,
120 }
121 d, f := nnode.Visit(opts)
122 if nnode.err == nil && !nnode.IsDir() {
123 // "dirs only" option
124 if opts.DirsOnly {
125 continue
126 }
127 var rePrefix string
128 if opts.IgnoreCase {
129 rePrefix = "(?i)"
130 }
131 // Pattern matching
132 if opts.Pattern != "" {
133 re, err := regexp.Compile(rePrefix + opts.Pattern)
134 if err == nil && !re.MatchString(name) {
135 continue
136 }
137 }
138 // IPattern matching
139 if opts.IPattern != "" {
140 re, err := regexp.Compile(rePrefix + opts.IPattern)
141 if err == nil && re.MatchString(name) {
142 continue
143 }
144 }
145 }
146 node.nodes = append(node.nodes, nnode)
147 dirs, files = dirs+d, files+f
148 }
149 // Sorting
150 if !opts.NoSort {
151 node.sort(opts)
152 }
153 return
154 }
155
156 func (node *Node) sort(opts *Options) {
157 var fn SortFunc
158 switch {
159 case opts.ModSort:
160 fn = ModSort
161 case opts.CTimeSort:
162 fn = CTimeSort
163 case opts.DirSort:
164 fn = DirSort
165 case opts.VerSort:
166 fn = VerSort
167 case opts.SizeSort:
168 fn = SizeSort
169 case opts.NameSort:
170 fn = NameSort
171 default:
172 fn = NameSort // Default should be sorted, not unsorted.
173 }
174 if fn != nil {
175 if opts.ReverSort {
176 sort.Sort(sort.Reverse(ByFunc{node.nodes, fn}))
177 } else {
178 sort.Sort(ByFunc{node.nodes, fn})
179 }
180 }
181 }
182
183 // Print nodes based on the given configuration.
184 func (node *Node) Print(opts *Options) { node.print("", opts) }
185
186 func dirRecursiveSize(opts *Options, node *Node) (size int64, err error) {
187 if opts.DeepLevel > 0 && node.depth >= opts.DeepLevel {
188 err = errors.New("Depth too high")
189 }
190
191 for _, nnode := range node.nodes {
192 if nnode.err != nil {
193 err = nnode.err
194 continue
195 }
196
197 if !nnode.IsDir() {
198 size += nnode.Size()
199 } else {
200 nsize, e := dirRecursiveSize(opts, nnode)
201 size += nsize
202 if e != nil {
203 err = e
204 }
205 }
206 }
207 return
208 }
209
210 func (node *Node) print(indent string, opts *Options) {
211 if node.err != nil {
212 err := node.err.Error()
213 if msgs := strings.Split(err, ": "); len(msgs) > 1 {
214 err = msgs[1]
215 }
216 fmt.Printf("%s [%s]\n", node.path, err)
217 return
218 }
219 if !node.IsDir() {
220 var props []string
221 ok, inode, device, uid, gid := getStat(node)
222 // inodes
223 if ok && opts.Inodes {
224 props = append(props, fmt.Sprintf("%d", inode))
225 }
226 // device
227 if ok && opts.Device {
228 props = append(props, fmt.Sprintf("%3d", device))
229 }
230 // Mode
231 if opts.FileMode {
232 props = append(props, node.Mode().String())
233 }
234 // Owner/Uid
235 if ok && opts.ShowUid {
236 uidStr := strconv.Itoa(int(uid))
237 if u, err := user.LookupId(uidStr); err != nil {
238 props = append(props, fmt.Sprintf("%-8s", uidStr))
239 } else {
240 props = append(props, fmt.Sprintf("%-8s", u.Username))
241 }
242 }
243 // Gorup/Gid
244 // TODO: support groupname
245 if ok && opts.ShowGid {
246 gidStr := strconv.Itoa(int(gid))
247 props = append(props, fmt.Sprintf("%-4s", gidStr))
248 }
249 // Size
250 if opts.ByteSize || opts.UnitSize {
251 var size string
252 if opts.UnitSize {
253 size = fmt.Sprintf("%4s", formatBytes(node.Size()))
254 } else {
255 size = fmt.Sprintf("%11d", node.Size())
256 }
257 props = append(props, size)
258 }
259 // Last modification
260 if opts.LastMod {
261 props = append(props, node.ModTime().Format("Jan 02 15:04"))
262 }
263 // Print properties
264 if len(props) > 0 {
265 fmt.Fprintf(opts.OutFile, "[%s] ", strings.Join(props, " "))
266 }
267 } else {
268 var props []string
269 // Size
270 if opts.ByteSize || opts.UnitSize {
271 var size string
272 rsize, err := dirRecursiveSize(opts, node)
273 if err != nil && rsize <= 0 {
274 if opts.UnitSize {
275 size = "????"
276 } else {
277 size = "???????????"
278 }
279 } else if opts.UnitSize {
280 size = fmt.Sprintf("%4s", formatBytes(rsize))
281 } else {
282 size = fmt.Sprintf("%11d", rsize)
283 }
284 props = append(props, size)
285 }
286 // Print properties
287 if len(props) > 0 {
288 fmt.Fprintf(opts.OutFile, "[%s] ", strings.Join(props, " "))
289 }
290 }
291 // name/path
292 var name string
293 if node.depth == 0 || opts.FullPath {
294 name = node.path
295 } else {
296 name = node.Name()
297 }
298 // Quotes
299 if opts.Quotes {
300 name = fmt.Sprintf("\"%s\"", name)
301 }
302 // Colorize
303 if opts.Colorize {
304 name = ANSIColor(node, name)
305 }
306 // IsSymlink
307 if node.Mode()&os.ModeSymlink == os.ModeSymlink {
308 vtarget, err := os.Readlink(node.path)
309 if err != nil {
310 vtarget = node.path
311 }
312 targetPath, err := filepath.EvalSymlinks(node.path)
313 if err != nil {
314 targetPath = vtarget
315 }
316 fi, err := opts.Fs.Stat(targetPath)
317 if opts.Colorize && fi != nil {
318 vtarget = ANSIColor(&Node{FileInfo: fi, path: vtarget}, vtarget)
319 }
320 name = fmt.Sprintf("%s -> %s", name, vtarget)
321 // Follow symbolic links like directories
322 if opts.FollowLink {
323 path, err := filepath.Abs(targetPath)
324 if err == nil && fi != nil && fi.IsDir() {
325 if _, ok := node.vpaths[filepath.Clean(path)]; !ok {
326 inf := &Node{FileInfo: fi, path: targetPath}
327 inf.vpaths = node.vpaths
328 inf.Visit(opts)
329 node.nodes = inf.nodes
330 } else {
331 name += " [recursive, not followed]"
332 }
333 }
334 }
335 }
336 // Print file details
337 // the main idea of the print logic came from here: github.com/campoy/tools/tree
338 fmt.Fprintln(opts.OutFile, name)
339 add := "│ "
340 for i, nnode := range node.nodes {
341 if opts.NoIndent {
342 add = ""
343 } else {
344 if i == len(node.nodes)-1 {
345 fmt.Fprintf(opts.OutFile, indent+"└── ")
346 add = " "
347 } else {
348 fmt.Fprintf(opts.OutFile, indent+"├── ")
349 }
350 }
351 nnode.print(indent+add, opts)
352 }
353 }
354
355 const (
356 _ = iota // ignore first value by assigning to blank identifier
357 KB int64 = 1 << (10 * iota)
358 MB
359 GB
360 TB
361 PB
362 EB
363 )
364
365 // Convert bytes to human readable string. Like a 2 MB, 64.2 KB, 52 B
366 func formatBytes(i int64) (result string) {
367 var n float64
368 sFmt, eFmt := "%.01f", ""
369 switch {
370 case i > EB:
371 eFmt = "E"
372 n = float64(i) / float64(EB)
373 case i > PB:
374 eFmt = "P"
375 n = float64(i) / float64(PB)
376 case i > TB:
377 eFmt = "T"
378 n = float64(i) / float64(TB)
379 case i > GB:
380 eFmt = "G"
381 n = float64(i) / float64(GB)
382 case i > MB:
383 eFmt = "M"
384 n = float64(i) / float64(MB)
385 case i > KB:
386 eFmt = "K"
387 n = float64(i) / float64(KB)
388 default:
389 sFmt = "%.0f"
390 n = float64(i)
391 }
392 if eFmt != "" && n >= 10 {
393 sFmt = "%.0f"
394 }
395 result = fmt.Sprintf(sFmt+eFmt, n)
396 result = strings.Trim(result, " ")
397 return
398 }
0 package tree
1
2 import (
3 "os"
4 "syscall"
5 "testing"
6 "time"
7 )
8
9 // Mock file/FileInfo
10 type file struct {
11 name string
12 size int64
13 files []*file
14 lastMod time.Time
15 stat interface{}
16 mode os.FileMode
17 }
18
19 func (f file) Name() string { return f.name }
20 func (f file) Size() int64 { return f.size }
21 func (f file) Mode() (o os.FileMode) {
22 if f.mode != o {
23 return f.mode
24 }
25 if f.stat != nil {
26 stat := (f.stat).(*syscall.Stat_t)
27 o = os.FileMode(stat.Mode)
28 }
29 return
30 }
31 func (f file) ModTime() time.Time { return f.lastMod }
32 func (f file) IsDir() bool { return nil != f.files }
33 func (f file) Sys() interface{} {
34 if f.stat == nil {
35 return new(syscall.Stat_t)
36 }
37 return f.stat
38 }
39
40 // Mock filesystem
41 type MockFs struct {
42 files map[string]*file
43 }
44
45 func NewFs() *MockFs {
46 return &MockFs{make(map[string]*file)}
47 }
48
49 func (fs *MockFs) clean() *MockFs {
50 fs.files = make(map[string]*file)
51 return fs
52 }
53
54 func (fs *MockFs) addFile(path string, file *file) *MockFs {
55 fs.files[path] = file
56 if file.IsDir() {
57 for _, f := range file.files {
58 fs.addFile(path+"/"+f.name, f)
59 }
60 }
61 return fs
62 }
63
64 func (fs *MockFs) Stat(path string) (os.FileInfo, error) {
65 return fs.files[path], nil
66 }
67 func (fs *MockFs) ReadDir(path string) ([]string, error) {
68 var names []string
69 for _, file := range fs.files[path].files {
70 names = append(names, file.Name())
71 }
72 return names, nil
73 }
74
75 // Mock output file
76 type Out struct {
77 str string
78 }
79
80 func (o *Out) equal(s string) bool {
81 return o.str == s
82 }
83
84 func (o *Out) Write(p []byte) (int, error) {
85 o.str += string(p)
86 return len(p), nil
87 }
88
89 func (o *Out) clear() {
90 o.str = ""
91 }
92
93 // FileSystem and Stdout mocks
94 var (
95 fs = NewFs()
96 out = new(Out)
97 )
98
99 type treeTest struct {
100 name string
101 opts *Options // test params.
102 expected string // expected output.
103 dirs int // expected dir count.
104 files int // expected file count.
105 }
106
107 var listTests = []treeTest{
108 {"basic", &Options{Fs: fs, OutFile: out}, `root
109 ├── a
110 ├── b
111 └── c
112 ├── d
113 └── e
114 `, 1, 4},
115 {"all", &Options{Fs: fs, OutFile: out, All: true, NoSort: true}, `root
116 ├── a
117 ├── b
118 └── c
119 ├── d
120 ├── e
121 └── .f
122 `, 1, 5},
123 {"dirs", &Options{Fs: fs, OutFile: out, DirsOnly: true}, `root
124 └── c
125 `, 1, 0},
126 {"fullPath", &Options{Fs: fs, OutFile: out, FullPath: true}, `root
127 ├── root/a
128 ├── root/b
129 └── root/c
130 ├── root/c/d
131 └── root/c/e
132 `, 1, 4},
133 {"deepLevel", &Options{Fs: fs, OutFile: out, DeepLevel: 1}, `root
134 ├── a
135 ├── b
136 └── c
137 `, 1, 2},
138 {"pattern", &Options{Fs: fs, OutFile: out, Pattern: "(a|e)"}, `root
139 ├── a
140 └── c
141 └── e
142 `, 1, 2},
143 {"ipattern", &Options{Fs: fs, OutFile: out, IPattern: "(a|e)"}, `root
144 ├── b
145 └── c
146 └── d
147 `, 1, 2},
148 {"ignore-case", &Options{Fs: fs, OutFile: out, Pattern: "(A)", IgnoreCase: true}, `root
149 ├── a
150 └── c
151 `, 1, 1}}
152
153 func TestSimple(t *testing.T) {
154 root := &file{
155 name: "root",
156 size: 200,
157 files: []*file{
158 {name: "a", size: 50},
159 {name: "b", size: 50},
160 {
161 name: "c",
162 size: 100,
163 files: []*file{
164 {name: "d", size: 50},
165 {name: "e", size: 50},
166 {name: ".f", size: 0},
167 },
168 },
169 },
170 }
171 fs.clean().addFile(root.name, root)
172 for _, test := range listTests {
173 inf := New(root.name)
174 d, f := inf.Visit(test.opts)
175 if d != test.dirs {
176 t.Errorf("wrong dir count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, d, test.dirs)
177 }
178 if f != test.files {
179 t.Errorf("wrong dir count for test %q:\ngot:\n%d\nexpected:\n%d", test.name, d, test.files)
180 }
181 inf.Print(test.opts)
182 if !out.equal(test.expected) {
183 t.Errorf("%s:\ngot:\n%+v\nexpected:\n%+v", test.name, out.str, test.expected)
184 }
185 out.clear()
186 }
187 }
188
189 var sortTests = []treeTest{
190 {"name-sort", &Options{Fs: fs, OutFile: out, NameSort: true}, `root
191 ├── a
192 ├── b
193 └── c
194 └── d
195 `, 1, 3},
196 {"dirs-first sort", &Options{Fs: fs, OutFile: out, DirSort: true}, `root
197 ├── c
198 │ └── d
199 ├── b
200 └── a
201 `, 1, 3},
202 {"reverse sort", &Options{Fs: fs, OutFile: out, ReverSort: true, DirSort: true}, `root
203 ├── b
204 ├── a
205 └── c
206 └── d
207 `, 1, 3},
208 {"no-sort", &Options{Fs: fs, OutFile: out, NoSort: true, DirSort: true}, `root
209 ├── b
210 ├── c
211 │ └── d
212 └── a
213 `, 1, 3},
214 {"size-sort", &Options{Fs: fs, OutFile: out, SizeSort: true}, `root
215 ├── a
216 ├── c
217 │ └── d
218 └── b
219 `, 1, 3},
220 {"last-mod-sort", &Options{Fs: fs, OutFile: out, ModSort: true}, `root
221 ├── a
222 ├── b
223 └── c
224 └── d
225 `, 1, 3},
226 {"c-time-sort", &Options{Fs: fs, OutFile: out, CTimeSort: true}, `root
227 ├── b
228 ├── c
229 │ └── d
230 └── a
231 `, 1, 3}}
232
233 func TestSort(t *testing.T) {
234 tFmt := "2006-Jan-02"
235 aTime, _ := time.Parse(tFmt, "2015-Aug-01")
236 bTime, _ := time.Parse(tFmt, "2015-Sep-01")
237 cTime, _ := time.Parse(tFmt, "2015-Oct-01")
238 root := &file{
239 name: "root",
240 size: 200,
241 files: []*file{
242 {name: "b", size: 11, lastMod: bTime},
243 {name: "c", size: 10, files: []*file{{name: "d", size: 10, lastMod: cTime}}, lastMod: cTime},
244 {name: "a", size: 9, lastMod: aTime},
245 },
246 }
247 fs.clean().addFile(root.name, root)
248 for _, test := range sortTests {
249 inf := New(root.name)
250 inf.Visit(test.opts)
251 inf.Print(test.opts)
252 if !out.equal(test.expected) {
253 t.Errorf("%s:\ngot:\n%+v\nexpected:\n%+v", test.name, out.str, test.expected)
254 }
255 out.clear()
256 }
257 }
258
259 var graphicTests = []treeTest{
260 {"no-indent", &Options{Fs: fs, OutFile: out, NoIndent: true}, `root
261 a
262 b
263 c
264 `, 0, 3},
265 {"quotes", &Options{Fs: fs, OutFile: out, Quotes: true}, `"root"
266 ├── "a"
267 ├── "b"
268 └── "c"
269 `, 0, 3},
270 {"byte-size", &Options{Fs: fs, OutFile: out, ByteSize: true}, `[ 12499] root
271 ├── [ 1500] a
272 ├── [ 9999] b
273 └── [ 1000] c
274 `, 0, 3},
275 {"unit-size", &Options{Fs: fs, OutFile: out, UnitSize: true}, `[ 12K] root
276 ├── [1.5K] a
277 ├── [9.8K] b
278 └── [1000] c
279 `, 0, 3},
280 {"show-gid", &Options{Fs: fs, OutFile: out, ShowGid: true}, `root
281 ├── [1 ] a
282 ├── [2 ] b
283 └── [1 ] c
284 `, 0, 3},
285 {"mode", &Options{Fs: fs, OutFile: out, FileMode: true}, `root
286 ├── [-rw-r--r--] a
287 ├── [-rwxr-xr-x] b
288 └── [-rw-rw-rw-] c
289 `, 0, 3},
290 {"lastMod", &Options{Fs: fs, OutFile: out, LastMod: true}, `root
291 ├── [Feb 11 00:00] a
292 ├── [Jan 28 00:00] b
293 └── [Jul 12 00:00] c
294 `, 0, 3}}
295
296 func TestGraphics(t *testing.T) {
297 tFmt := "2006-Jan-02"
298 aTime, _ := time.Parse(tFmt, "2015-Feb-11")
299 bTime, _ := time.Parse(tFmt, "2006-Jan-28")
300 cTime, _ := time.Parse(tFmt, "2015-Jul-12")
301 root := &file{
302 name: "root",
303 size: 11499,
304 files: []*file{
305 {name: "a", size: 1500, lastMod: aTime, stat: &syscall.Stat_t{Gid: 1, Mode: 0644}},
306 {name: "b", size: 9999, lastMod: bTime, stat: &syscall.Stat_t{Gid: 2, Mode: 0755}},
307 {name: "c", size: 1000, lastMod: cTime, stat: &syscall.Stat_t{Gid: 1, Mode: 0666}},
308 },
309 stat: &syscall.Stat_t{Gid: 1},
310 }
311 fs.clean().addFile(root.name, root)
312 for _, test := range graphicTests {
313 inf := New(root.name)
314 inf.Visit(test.opts)
315 inf.Print(test.opts)
316 if !out.equal(test.expected) {
317 t.Errorf("%s:\ngot:\n%+v\nexpected:\n%+v", test.name, out.str, test.expected)
318 }
319 out.clear()
320 }
321 }
322
323 var symlinkTests = []treeTest{
324 {"symlink", &Options{Fs: fs, OutFile: out}, `root
325 └── symlink -> root/symlink
326 `, 0, 1},
327 {"symlink-rec", &Options{Fs: fs, OutFile: out, FollowLink: true}, `root
328 └── symlink -> root/symlink [recursive, not followed]
329 `, 0, 1}}
330
331 func TestSymlink(t *testing.T) {
332 root := &file{
333 name: "root",
334 files: []*file{
335 &file{name: "symlink", mode: os.ModeSymlink, files: make([]*file, 0)},
336 },
337 }
338 fs.clean().addFile(root.name, root)
339 for _, test := range symlinkTests {
340 inf := New(root.name)
341 inf.Visit(test.opts)
342 inf.Print(test.opts)
343 if !out.equal(test.expected) {
344 t.Errorf("%s:\ngot:\n%+v\nexpected:\n%+v", test.name, out.str, test.expected)
345 }
346 out.clear()
347 }
348 }
349
350 func TestCount(t *testing.T) {
351 defer out.clear()
352 root := &file{
353 name: "root",
354 files: []*file{
355 &file{
356 name: "a",
357 files: []*file{
358 {
359 name: "b",
360 files: []*file{{name: "c"}},
361 },
362 {
363 name: "d",
364 files: []*file{
365 {
366 name: "e",
367 files: []*file{{name: "f"}, {name: "g"}},
368 },
369 },
370 },
371 {
372 name: "h",
373 files: []*file{
374 {
375 name: "i",
376 files: []*file{{name: "j"}},
377 },
378 {
379 name: "k",
380 files: []*file{{name: "l"}, {name: "m"}},
381 },
382 {name: "n"},
383 {name: "o"},
384 },
385 },
386 },
387 }},
388 }
389 fs.clean().addFile(root.name, root)
390 opt := &Options{Fs: fs, OutFile: out}
391 inf := New(root.name)
392 d, f := inf.Visit(opt)
393 if d != 7 || f != 8 {
394 inf.Print(opt)
395 t.Errorf("TestCount - expect (dir, file) count to be equal to (7, 8)\n%s", out.str)
396 }
397 }
0 package tree
1
2 import "os"
3
4 func (n Nodes) Len() int { return len(n) }
5 func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
6
7 type ByFunc struct {
8 Nodes
9 Fn SortFunc
10 }
11
12 func (b ByFunc) Less(i, j int) bool {
13 return b.Fn(b.Nodes[i].FileInfo, b.Nodes[j].FileInfo)
14 }
15
16 type SortFunc func(f1, f2 os.FileInfo) bool
17
18 func ModSort(f1, f2 os.FileInfo) bool {
19 return f1.ModTime().Before(f2.ModTime())
20 }
21
22 func DirSort(f1, f2 os.FileInfo) bool {
23 return f1.IsDir() && !f2.IsDir()
24 }
25
26 func SizeSort(f1, f2 os.FileInfo) bool {
27 return f1.Size() < f2.Size()
28 }
29
30 func NameSort(f1, f2 os.FileInfo) bool {
31 return f1.Name() < f2.Name()
32 }
33
34 func VerSort(f1, f2 os.FileInfo) bool {
35 return NaturalLess(f1.Name(), f2.Name())
36 }
37
38 func isdigit(b byte) bool { return '0' <= b && b <= '9' }
39
40 // NaturalLess compares two strings using natural ordering. This means that e.g.
41 // "abc2" < "abc12".
42 //
43 // Non-digit sequences and numbers are compared separately. The former are
44 // compared bytewise, while the latter are compared numerically (except that
45 // the number of leading zeros is used as a tie-breaker, so e.g. "2" < "02")
46 //
47 // Limitation: only ASCII digits (0-9) are considered.
48 // Code taken from:
49 // https://github.com/fvbommel/util/blob/master/sortorder/natsort.go
50 func NaturalLess(str1, str2 string) bool {
51 idx1, idx2 := 0, 0
52 for idx1 < len(str1) && idx2 < len(str2) {
53 c1, c2 := str1[idx1], str2[idx2]
54 dig1, dig2 := isdigit(c1), isdigit(c2)
55 switch {
56 case dig1 != dig2: // Digits before other characters.
57 return dig1 // True if LHS is a digit, false if the RHS is one.
58 case !dig1: // && !dig2, because dig1 == dig2
59 // UTF-8 compares bytewise-lexicographically, no need to decode
60 // codepoints.
61 if c1 != c2 {
62 return c1 < c2
63 }
64 idx1++
65 idx2++
66 default: // Digits
67 // Eat zeros.
68 for ; idx1 < len(str1) && str1[idx1] == '0'; idx1++ {
69 }
70 for ; idx2 < len(str2) && str2[idx2] == '0'; idx2++ {
71 }
72 // Eat all digits.
73 nonZero1, nonZero2 := idx1, idx2
74 for ; idx1 < len(str1) && isdigit(str1[idx1]); idx1++ {
75 }
76 for ; idx2 < len(str2) && isdigit(str2[idx2]); idx2++ {
77 }
78 // If lengths of numbers with non-zero prefix differ, the shorter
79 // one is less.
80 if len1, len2 := idx1-nonZero1, idx2-nonZero2; len1 != len2 {
81 return len1 < len2
82 }
83 // If they're not equal, string comparison is correct.
84 if nr1, nr2 := str1[nonZero1:idx1], str2[nonZero2:idx2]; nr1 != nr2 {
85 return nr1 < nr2
86 }
87 // Otherwise, the one with less zeros is less.
88 // Because everything up to the number is equal, comparing the index
89 // after the zeros is sufficient.
90 if nonZero1 != nonZero2 {
91 return nonZero1 < nonZero2
92 }
93 }
94 // They're identical so far, so continue comparing.
95 }
96 // So far they are identical. At least one is ended. If the other continues,
97 // it sorts last.
98 return len(str1) < len(str2)
99 }
0 //+build !plan9,!windows
1
2 package tree
3
4 import (
5 "os"
6 "syscall"
7 )
8
9 func getStat(fi os.FileInfo) (ok bool, inode, device, uid, gid uint64) {
10 sys := fi.Sys()
11 if sys == nil {
12 return false, 0, 0, 0, 0
13 }
14 stat, ok := sys.(*syscall.Stat_t)
15 if !ok {
16 return false, 0, 0, 0, 0
17 }
18 return true, uint64(stat.Ino), uint64(stat.Dev), uint64(stat.Uid), uint64(stat.Gid)
19 }
0 //+build plan9 windows
1
2 package tree
3
4 import "os"
5
6 func getStat(fi os.FileInfo) (ok bool, inode, device, uid, gid uint64) {
7 return false, 0, 0, 0, 0
8 }