New upstream version 0.0~git20171213.cf42b1e
Dr. Tobias Quathamer
6 years ago
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 | } |