├── .gitignore ├── tests ├── glob1.bash ├── glob2.bash ├── example.bash ├── passes.bash ├── fails.bash └── meta.bash ├── basht.go ├── circle.yml ├── Makefile ├── LICENSE ├── include └── basht.bash ├── README.md └── bindata.go /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /tests/glob1.bash: -------------------------------------------------------------------------------- 1 | 2 | T_glob1() { 3 | true 4 | } 5 | -------------------------------------------------------------------------------- /tests/glob2.bash: -------------------------------------------------------------------------------- 1 | 2 | T_glob2() { 3 | true 4 | } 5 | -------------------------------------------------------------------------------- /tests/example.bash: -------------------------------------------------------------------------------- 1 | 2 | T_additionUsingBC() { 3 | result="$(echo 2+2 | bc)" 4 | [[ "$result" -eq 4 ]] 5 | } 6 | 7 | T_additionUsingDC() { 8 | result="$(echo 2 2+p | dc)" 9 | [[ "$result" -eq 4 ]] 10 | } 11 | -------------------------------------------------------------------------------- /tests/passes.bash: -------------------------------------------------------------------------------- 1 | 2 | success() { 3 | true 4 | } 5 | 6 | T_passEquals() { 7 | [[ "foobar" == "foobar" ]] 8 | } 9 | 10 | T_passReturn() { 11 | return 12 | } 13 | 14 | T_passSuccess() { 15 | success 16 | } 17 | -------------------------------------------------------------------------------- /tests/fails.bash: -------------------------------------------------------------------------------- 1 | 2 | fails() { 3 | false 4 | } 5 | 6 | T_failEquals() { 7 | [[ "yes" == "no" ]] 8 | } 9 | 10 | T_failReturn() { 11 | return 1 12 | } 13 | 14 | T_failSuccess() { 15 | fails 16 | } 17 | 18 | T_failMessage() { 19 | false || $T_fail "This is a fail message." 20 | } 21 | -------------------------------------------------------------------------------- /basht.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/progrium/go-basher" 8 | ) 9 | 10 | var Version string 11 | 12 | func main() { 13 | if len(os.Args) == 2 && os.Args[1] == "--version" { 14 | fmt.Println(Version) 15 | os.Exit(0) 16 | } 17 | os.Setenv("VERSION", Version) 18 | basher.Application(nil, []string{ 19 | "include/basht.bash", 20 | }, Asset, true) 21 | } 22 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | pre: 3 | - rm ~/.gitconfig 4 | - make deps 5 | override: 6 | - make build 7 | post: 8 | - tar -czf $CIRCLE_ARTIFACTS/basht-linux.tgz -C build/Linux basht 9 | - tar -czf $CIRCLE_ARTIFACTS/basht-darwin.tgz -C build/Darwin basht 10 | 11 | test: 12 | override: 13 | - make test 14 | 15 | deployment: 16 | release: 17 | branch: release 18 | commands: 19 | - make release 20 | -------------------------------------------------------------------------------- /tests/meta.bash: -------------------------------------------------------------------------------- 1 | 2 | T_fails() { 3 | $SELF "$(dirname $BASH_SOURCE)/fails.bash" &> /dev/null 4 | [[ $? == 4 ]] 5 | } 6 | 7 | T_passes() { 8 | $SELF "$(dirname $BASH_SOURCE)/passes.bash" &> /dev/null 9 | } 10 | 11 | T_glob() { 12 | $SELF $(dirname $BASH_SOURCE)/glob*.bash \ 13 | | grep "Ran 2 tests." > /dev/null 14 | } 15 | 16 | T_failMessage() { 17 | $SELF "$(dirname $BASH_SOURCE)/fails.bash" \ 18 | | grep "This is a fail message." > /dev/null 19 | } 20 | 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME=basht 2 | VERSION=0.1.0 3 | 4 | build: 5 | go-bindata include 6 | mkdir -p build/Linux && GOOS=linux go build -ldflags "-X main.Version $(VERSION)" -o build/Linux/$(NAME) 7 | mkdir -p build/Darwin && GOOS=darwin go build -ldflags "-X main.Version $(VERSION)" -o build/Darwin/$(NAME) 8 | 9 | deps: 10 | go get -u github.com/jteeuwen/go-bindata/... 11 | go get -u github.com/progrium/gh-release/... 12 | go get || true 13 | 14 | test: 15 | build/$(shell uname -s)/basht tests/meta.bash 16 | 17 | release: 18 | rm -rf release && mkdir release 19 | tar -zcf release/$(NAME)_$(VERSION)_Linux_$(shell uname -m).tgz -C build/Linux $(NAME) 20 | tar -zcf release/$(NAME)_$(VERSION)_Darwin_$(shell uname -m).tgz -C build/Darwin $(NAME) 21 | gh-release create progrium/$(NAME) $(VERSION) \ 22 | $(shell git rev-parse --abbrev-ref HEAD) v$(VERSION) 23 | 24 | .PHONY: build release 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 Jeff Lindsay 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /include/basht.bash: -------------------------------------------------------------------------------- 1 | 2 | __contains() { 3 | local e; for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done; return 1 4 | } 5 | 6 | __failMacro() { 7 | local line="$1"; shift 8 | __test_status=1 9 | __test_message="$line: $*" 10 | } 11 | readonly T_fail='eval __failMacro $BASH_SOURCE:$LINENO' 12 | 13 | main() { 14 | local run failed start stop duration 15 | declare -a processed 16 | run=0 17 | failed=0 18 | for file in "$@"; do 19 | source "$file" 20 | for t in $(declare -F | grep 'declare -f T_' | awk '{print $3}'); do 21 | if ! __contains "$t" "${processed[@]}"; then 22 | unset __test_status __test_message 23 | echo "=== RUN $t" 24 | start="$SECONDS" 25 | $t 26 | __test_status=${__test_status:-$?} 27 | stop="$SECONDS" 28 | duration=$((stop-start)) 29 | processed+=("$t") 30 | run=$((run+1)) 31 | if [[ "$__test_status" == 0 ]]; then 32 | echo "--- PASS $t (${duration}s)" 33 | else 34 | failed=$((failed+1)) 35 | echo "--- FAIL $t (${duration}s)" 36 | if [[ "$__test_message" ]]; then 37 | echo " $__test_message" 38 | echo 39 | fi 40 | fi 41 | fi 42 | done 43 | done 44 | echo 45 | if [[ "$failed" == "0" ]]; then 46 | echo "Ran $run tests." 47 | echo 48 | echo "PASS" 49 | else 50 | echo "Ran $run tests. $failed failed." 51 | echo 52 | echo "FAIL" 53 | exit $failed 54 | fi 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bashT [![Circle CI](https://circleci.com/gh/progrium/basht.png?style=shield)](https://circleci.com/gh/progrium/basht) 2 | 3 | Basht is a minimalist Bash testing utility. You can write tests in pure Bash, just pass it one or more Bash source files that define tests. Tests are functions that start with `T_`: 4 | 5 | ``` 6 | T_additionUsingBC() { 7 | result="$(echo 2+2 | bc)" 8 | [[ "$result" -eq 4 ]] 9 | } 10 | 11 | T_additionUsingDC() { 12 | result="$(echo 2 2+p | dc)" 13 | [[ "$result" -eq 4 ]] 14 | } 15 | ``` 16 | 17 | Tests that return non-zero fail. Only the return value will fail a test, as `set -e` is not used. You can fail a test at any time by explicitly calling `return` with non-zero. 18 | 19 | ## Getting basht 20 | 21 | Download and uncompress the latest binary tarball from [releases](https://github.com/progrium/basht/releases). 22 | 23 | Alternatively, if you happen to also be using Go, you can install with `go get`: 24 | 25 | $ go get github.com/progrium/basht 26 | 27 | ## Running tests 28 | 29 | Any filenames passed to basht are loaded and any tests found will be run. Take advantage of globbing for multiple files or directories of tests. 30 | 31 | ``` 32 | $ basht tests/example.bash 33 | === RUN T_additionUsingBC 34 | --- PASS T_additionUsingBC (0s) 35 | === RUN T_additionUsingDC 36 | --- PASS T_additionUsingDC (0s) 37 | 38 | Ran 2 tests. 39 | 40 | PASS 41 | ``` 42 | 43 | If tests pass, basht will exit zero. If any tests failed, basht exists non-zero with the number of failed tests. 44 | 45 | ``` 46 | $ basht tests/fails.bash 47 | === RUN T_failEquals 48 | --- FAIL T_failEquals (0s) 49 | === RUN T_failMessage 50 | --- FAIL T_failMessage (0s) 51 | tests/fails.bash:19: This is a fail message. 52 | 53 | === RUN T_failReturn 54 | --- FAIL T_failReturn (0s) 55 | === RUN T_failSuccess 56 | --- FAIL T_failSuccess (0s) 57 | 58 | Ran 4 tests. 4 failed. 59 | 60 | FAIL 61 | ``` 62 | 63 | ## Macros 64 | 65 | Basht provides no special assertions or helpers. However, there is one macro basht provides: 66 | 67 | ### $T_fail 68 | 69 | Calling `$T_fail ` marks a test as failed and includes a failure message. There is an example above of how this is shown in the output. It includes the filename and line number with the message. 70 | 71 | Keep in mind it does not return, so if used before the end of a test, you must return after. 72 | 73 | ``` 74 | T_failMessage() { 75 | false || $T_fail "This is a fail message." 76 | } 77 | 78 | T_failMultiple() { 79 | if ! something; then 80 | $T_fail "Something failed." 81 | return 82 | fi 83 | if ! another; then 84 | $T_fail "Another failed." 85 | return 86 | fi 87 | } 88 | ``` 89 | 90 | ## Why not Bats or shunit2? 91 | 92 | Good question. I've used both and enjoyed using them until I got tired of what I didn't like. With Bats these issues bothered me: 93 | 94 | * Useless syntactic sugar making test files require Bats instead of just Bash to process, making Bats more complicated and harder to debug. 95 | * Using `set -e` means you have to use the `run` helper for nearly everythng in larger tests. 96 | * The other Bats-specific helpers that could be replaced with idiomatic Bash-isms. 97 | * The multi-file, multi-directory install. 98 | 99 | On the plus side, it did take a much more lightweight approach to unit testing. But the issues made me turn to shunit2. It addressed all the above issues, but then I ran into: 100 | 101 | * For what it did and what I used, it was not worth the 1000 lines of overly portable, overly clever shell script. 102 | * With all it did, I had to modify it anyway, and ended up putting it in the source of every project that used it. 103 | 104 | Basht might not be for everybody, but I wrote it and use it because: 105 | 106 | * It's written in and made for pure Bash. 107 | * It's distributed as a single file binary. 108 | * It's only about 50 lines and does everything I need. 109 | * It's inspired in design and appearance by Go's testing. 110 | 111 | ## License 112 | 113 | BSD 114 | -------------------------------------------------------------------------------- /bindata.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "os" 10 | "time" 11 | "io/ioutil" 12 | "path" 13 | "path/filepath" 14 | ) 15 | 16 | func bindata_read(data []byte, name string) ([]byte, error) { 17 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 18 | if err != nil { 19 | return nil, fmt.Errorf("Read %q: %v", name, err) 20 | } 21 | 22 | var buf bytes.Buffer 23 | _, err = io.Copy(&buf, gz) 24 | gz.Close() 25 | 26 | if err != nil { 27 | return nil, fmt.Errorf("Read %q: %v", name, err) 28 | } 29 | 30 | return buf.Bytes(), nil 31 | } 32 | 33 | type asset struct { 34 | bytes []byte 35 | info os.FileInfo 36 | } 37 | 38 | type bindata_file_info struct { 39 | name string 40 | size int64 41 | mode os.FileMode 42 | modTime time.Time 43 | } 44 | 45 | func (fi bindata_file_info) Name() string { 46 | return fi.name 47 | } 48 | func (fi bindata_file_info) Size() int64 { 49 | return fi.size 50 | } 51 | func (fi bindata_file_info) Mode() os.FileMode { 52 | return fi.mode 53 | } 54 | func (fi bindata_file_info) ModTime() time.Time { 55 | return fi.modTime 56 | } 57 | func (fi bindata_file_info) IsDir() bool { 58 | return false 59 | } 60 | func (fi bindata_file_info) Sys() interface{} { 61 | return nil 62 | } 63 | 64 | var _include_basht_bash = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x74\x53\x5d\x6f\xd3\x30\x14\x7d\x8e\x7f\xc5\x25\x8a\x96\x94\x29\xa8\x85\xb7\x4e\x11\x1b\xa3\x13\x93\x46\x8b\x1a\xf6\x54\x55\x91\x95\x3a\x6b\x44\x66\x57\xb6\xc3\x87\x4a\xfe\x3b\xd7\x1f\xcd\x92\x02\x7d\x88\xeb\xeb\x73\xcf\xb9\xf7\x5c\x9b\x14\x45\x29\xb8\xa6\x35\x57\xc9\x04\x8e\x24\x68\x44\x49\x1b\x60\x57\x50\x09\x09\x0c\x6a\x0e\x61\x74\xbc\x9e\xbf\xed\xc2\x2b\xd8\x09\xd8\x6c\x70\xcf\x42\xc8\x32\x5c\x67\x21\x6c\xb7\x70\x71\x01\x92\xe9\x56\x72\x98\x1a\x08\xc7\x5c\xbf\x9f\x91\x8e\xa0\x40\x45\xeb\xe6\x33\x2d\xa5\x18\x2a\x34\x35\x67\x99\xa1\xb8\x02\xb5\xaf\x2b\x4d\x82\xa2\xd0\x4c\xe9\x42\x69\xaa\x5b\x95\xcd\xfa\xc0\x33\x53\x8a\x3e\x19\xb0\xc9\x99\x43\xf4\x3a\x44\x5e\xc9\x28\x6a\x35\xbf\xe0\xab\xe5\xcf\x62\xf6\x1d\x59\x07\x62\x10\x7d\xb8\xc9\x3f\x15\xf9\xea\x71\x7d\xbb\x98\x47\x0f\xf7\xcb\xc5\x72\x15\x13\xf2\x8c\xbd\x0e\x0b\x91\x2d\x07\x93\xc4\x76\x80\xd2\x52\xe3\x57\x1c\x60\xd7\x4a\xaa\x6b\xc1\x49\xb0\x63\x65\x43\x25\x83\x94\xc2\x41\x8a\x12\x8b\x61\x3b\x12\x60\x56\x36\x25\x81\x4b\xb4\xff\xd0\xae\x0a\x37\xce\xb1\x6b\xeb\x16\x09\x02\x25\x5a\x59\x32\x8c\x98\xb3\x10\x03\x06\xa7\x0d\x28\x4a\x7a\xe6\x3b\xf8\x0d\x4f\x92\x1d\x20\xee\x43\x15\xf6\x15\x63\x98\xfe\xf8\x06\xf1\xf1\x20\x6b\xae\x21\x7a\xd7\xc5\x13\xcf\x1b\xd4\x15\xbc\x82\x97\xe1\xa1\x82\x0e\xcd\xa8\xfa\x1a\x37\xd7\x5b\x33\x33\xbd\x67\xdc\xe0\x83\x96\x2b\xa6\x61\x64\x32\x8c\x1d\xb6\x30\x56\xee\x05\x84\x19\xce\x77\xfd\xb8\x04\x24\xb5\x51\xeb\x0c\x4e\x20\x5f\xdc\xae\x96\x1f\x73\x17\x8c\xb4\x5d\xc6\x73\x8b\x8e\xa3\xfd\x3c\x8d\xde\x77\x9e\x42\x1c\xce\x19\x4e\x2e\x67\x51\x92\x98\xf3\xd4\xea\x4c\x26\xf6\xb0\xef\xe4\x32\x4b\x4c\x77\x2e\x6a\x8c\x47\x34\x2e\x97\x33\x0f\x44\x2b\xec\xb5\x1c\x09\xdb\x2b\x3a\xc5\xfb\x39\xb0\xc0\x37\x97\xa6\x29\x7c\xb9\xc9\x73\xec\x0e\x92\xe8\x78\x2a\xa2\x53\x13\x57\x15\x6b\x94\xf3\xe2\x34\x5e\xd4\x73\xff\x7a\xc9\x01\xd3\xdd\xcd\xfd\xc3\xff\x98\xce\x4b\xf3\x46\x87\x67\x65\x79\x36\xc0\xdf\x39\x72\x00\xf0\x25\xd5\xe4\x65\xb1\x5f\xf3\xe4\x88\xff\x3a\xdc\x49\xd5\x15\xed\x1e\xeb\x74\x24\xea\x04\xd7\x14\xaf\xa1\xb9\xff\x46\x52\xbd\x09\x49\x2f\xe4\xce\x8d\x49\x18\xf4\x7e\xfc\x33\x07\xbc\x88\x7f\x42\x7f\x73\x18\x7b\x6c\xf0\x67\xad\x4f\x60\x62\x0a\xef\xc8\x9f\x00\x00\x00\xff\xff\xf9\xb9\x4e\xae\x7c\x04\x00\x00") 65 | 66 | func include_basht_bash_bytes() ([]byte, error) { 67 | return bindata_read( 68 | _include_basht_bash, 69 | "include/basht.bash", 70 | ) 71 | } 72 | 73 | func include_basht_bash() (*asset, error) { 74 | bytes, err := include_basht_bash_bytes() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | info := bindata_file_info{name: "include/basht.bash", size: 1148, mode: os.FileMode(420), modTime: time.Unix(1426447696, 0)} 80 | a := &asset{bytes: bytes, info: info} 81 | return a, nil 82 | } 83 | 84 | // Asset loads and returns the asset for the given name. 85 | // It returns an error if the asset could not be found or 86 | // could not be loaded. 87 | func Asset(name string) ([]byte, error) { 88 | cannonicalName := strings.Replace(name, "\\", "/", -1) 89 | if f, ok := _bindata[cannonicalName]; ok { 90 | a, err := f() 91 | if err != nil { 92 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 93 | } 94 | return a.bytes, nil 95 | } 96 | return nil, fmt.Errorf("Asset %s not found", name) 97 | } 98 | 99 | // MustAsset is like Asset but panics when Asset would return an error. 100 | // It simplifies safe initialization of global variables. 101 | func MustAsset(name string) []byte { 102 | a, err := Asset(name) 103 | if (err != nil) { 104 | panic("asset: Asset(" + name + "): " + err.Error()) 105 | } 106 | 107 | return a 108 | } 109 | 110 | // AssetInfo loads and returns the asset info for the given name. 111 | // It returns an error if the asset could not be found or 112 | // could not be loaded. 113 | func AssetInfo(name string) (os.FileInfo, error) { 114 | cannonicalName := strings.Replace(name, "\\", "/", -1) 115 | if f, ok := _bindata[cannonicalName]; ok { 116 | a, err := f() 117 | if err != nil { 118 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 119 | } 120 | return a.info, nil 121 | } 122 | return nil, fmt.Errorf("AssetInfo %s not found", name) 123 | } 124 | 125 | // AssetNames returns the names of the assets. 126 | func AssetNames() []string { 127 | names := make([]string, 0, len(_bindata)) 128 | for name := range _bindata { 129 | names = append(names, name) 130 | } 131 | return names 132 | } 133 | 134 | // _bindata is a table, holding each asset generator, mapped to its name. 135 | var _bindata = map[string]func() (*asset, error){ 136 | "include/basht.bash": include_basht_bash, 137 | } 138 | 139 | // AssetDir returns the file names below a certain 140 | // directory embedded in the file by go-bindata. 141 | // For example if you run go-bindata on data/... and data contains the 142 | // following hierarchy: 143 | // data/ 144 | // foo.txt 145 | // img/ 146 | // a.png 147 | // b.png 148 | // then AssetDir("data") would return []string{"foo.txt", "img"} 149 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 150 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 151 | // AssetDir("") will return []string{"data"}. 152 | func AssetDir(name string) ([]string, error) { 153 | node := _bintree 154 | if len(name) != 0 { 155 | cannonicalName := strings.Replace(name, "\\", "/", -1) 156 | pathList := strings.Split(cannonicalName, "/") 157 | for _, p := range pathList { 158 | node = node.Children[p] 159 | if node == nil { 160 | return nil, fmt.Errorf("Asset %s not found", name) 161 | } 162 | } 163 | } 164 | if node.Func != nil { 165 | return nil, fmt.Errorf("Asset %s not found", name) 166 | } 167 | rv := make([]string, 0, len(node.Children)) 168 | for name := range node.Children { 169 | rv = append(rv, name) 170 | } 171 | return rv, nil 172 | } 173 | 174 | type _bintree_t struct { 175 | Func func() (*asset, error) 176 | Children map[string]*_bintree_t 177 | } 178 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 179 | "include": &_bintree_t{nil, map[string]*_bintree_t{ 180 | "basht.bash": &_bintree_t{include_basht_bash, map[string]*_bintree_t{ 181 | }}, 182 | }}, 183 | }} 184 | 185 | // Restore an asset under the given directory 186 | func RestoreAsset(dir, name string) error { 187 | data, err := Asset(name) 188 | if err != nil { 189 | return err 190 | } 191 | info, err := AssetInfo(name) 192 | if err != nil { 193 | return err 194 | } 195 | err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755)) 196 | if err != nil { 197 | return err 198 | } 199 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 200 | if err != nil { 201 | return err 202 | } 203 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 204 | if err != nil { 205 | return err 206 | } 207 | return nil 208 | } 209 | 210 | // Restore assets under the given directory recursively 211 | func RestoreAssets(dir, name string) error { 212 | children, err := AssetDir(name) 213 | if err != nil { // File 214 | return RestoreAsset(dir, name) 215 | } else { // Dir 216 | for _, child := range children { 217 | err = RestoreAssets(dir, path.Join(name, child)) 218 | if err != nil { 219 | return err 220 | } 221 | } 222 | } 223 | return nil 224 | } 225 | 226 | func _filePath(dir, name string) string { 227 | cannonicalName := strings.Replace(name, "\\", "/", -1) 228 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 229 | } 230 | 231 | --------------------------------------------------------------------------------