1package script
2
3import (
4	"fmt"
5	"github.com/hashicorp/go-multierror"
6	"io"
7	"io/ioutil"
8	"os"
9	"path/filepath"
10)
11
12// Files is a stream of a list of files. A user can either use the file list directly or the the
13// created stream. In the stream, each line contains a path to a file.
14type Files struct {
15	Stream
16	Files []FileInfo
17}
18
19// File contains information about a file.
20type FileInfo struct {
21	// FileInfo contains information about the file.
22	os.FileInfo
23	// Path is the path of the file. It may be relative or absolute, depending on how the `Ls`
24	// command was invoked.
25	Path string
26}
27
28// Ls returns a stream of a list files. In the returned stream, each line will contain a path to
29// a single file.
30//
31// If the provided paths list is empty, the local directory will be listed.
32//
33// The provided paths may be relative to the local directory or absolute - this will influence the
34// format of the returned paths in the output.
35//
36// If some provided paths correlate to the arguments correlate to the same file, it will also appear
37// multiple times in the output.
38//
39// If any of the paths fails to be listed, it will result in an error in the output, but the stream
40// will still conain all paths that were successfully listed.
41//
42// Shell command: `ls`.
43func Ls(paths ...string) Files {
44	// Default to local directory.
45	if len(paths) == 0 {
46		paths = append(paths, ".")
47	}
48
49	var (
50		files  []FileInfo
51		errors *multierror.Error
52	)
53
54	for _, path := range paths {
55		info, err := os.Stat(path)
56		if err != nil {
57			errors = multierror.Append(errors, fmt.Errorf("stat path: %s", err))
58			continue
59		}
60
61		// Path is a single file.
62		if !info.IsDir() {
63			files = append(files, FileInfo{Path: path, FileInfo: info})
64			continue
65		}
66
67		// Path is a directory.
68		infos, err := ioutil.ReadDir(path)
69		if err != nil {
70			errors = multierror.Append(errors, fmt.Errorf("read dir: %s", err))
71			continue
72		}
73
74		for _, info := range infos {
75			files = append(files, FileInfo{Path: filepath.Join(path, info.Name()), FileInfo: info})
76		}
77	}
78
79	return Files{
80		Stream: Stream{
81			stage: fmt.Sprintf("ls (%+v)", paths),
82			r:     &filesReader{files: files},
83			err:   errors.ErrorOrNil(),
84		},
85		Files: files,
86	}
87}
88
89// filesReader reads from a file info list.
90type filesReader struct {
91	files []FileInfo
92	// seek indicates which file to write for the next Read function call.
93	seek int
94}
95
96func (f *filesReader) Read(out []byte) (int, error) {
97	if f.seek >= len(f.files) {
98		return 0, io.EOF
99	}
100
101	line := []byte(f.files[f.seek].Path + "\n")
102	f.seek++
103
104	n := copy(out, line)
105	return n, nil
106}
107