Skip to content

Commit f3705d0

Browse files
authored
Merge pull request #33 from ribice/output-types
Add support for csv/json formats and file output
2 parents 2dd9314 + a2ffc59 commit f3705d0

File tree

5 files changed

+164
-36
lines changed

5 files changed

+164
-36
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Golang license and dependency checker. Prints list of all dependencies, their UR
88

99
## Introduction
1010

11-
glice analyzes the go.mod file of your project and prints it in a tabular format - name, URL, and license short-name (MIT, GPL...).
11+
glice analyzes the go.mod file of your project and prints it in a tabular format [csv and json available as well] - name, URL, and license short-name (MIT, GPL...).
1212

1313
## Installation
1414

@@ -40,9 +40,9 @@ Alternatively, you can provide path which you want to be scanned with -p flag:
4040

4141
By default glice:
4242

43-
- Prints only to stdout
43+
- Prints to stdout
4444

45-
- Gets dependencies from go.mod
45+
- Gets dependencies from go.mod
4646

4747
- Fetches licenses for dependencies hosted on GitHub
4848

@@ -56,7 +56,8 @@ All flags are optional. Glice supports the following flags:
5656
- p [string - path] // Path to be scanned in form of github.com/author/repo
5757
- t [boolean - thanks] // if GitHub API key is provided, setting this flag will star all GitHub repos from dependency. __In order to do this, API key must have access to public_repo__
5858
- v (boolean - verbose) // If enabled, will log dependencies before fetching and printing them.
59-
59+
- fmt (string - format) // Format of the output. Defaults to table, other available options are `csv` and `json`.
60+
- o (string - otuput) // Destination of the output, defaults to stdout. Other option is `file`.
6061
```
6162

6263
Don't forget `-help` flag for detailed usage information.

api.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package glice
22

33
import (
44
"context"
5-
"fmt"
65
"net/http"
76

87
"github.com/fatih/color"
@@ -28,13 +27,14 @@ var licenseCol = map[string]licenseFormat{
2827

2928
// Repository holds information about the repository
3029
type Repository struct {
31-
Name string
32-
Shortname string
33-
URL string
34-
Host string
35-
Author string
36-
Project string
37-
Text string
30+
Name string `json:"name,omitempty"`
31+
Shortname string `json:"-"`
32+
URL string `json:"url,omitempty"`
33+
Host string `json:"host,omitempty"`
34+
Author string `json:"author,omitempty"`
35+
Project string `json:"project,omitempty"`
36+
Text string `json:"-"`
37+
License string `json:"license"`
3838
}
3939

4040
func newGitClient(c context.Context, keys map[string]string, star bool) *gitClient {
@@ -72,7 +72,6 @@ func (gc *gitClient) GetLicense(ctx context.Context, r *Repository) error {
7272
case "github.com":
7373
rl, _, err := gc.gh.Repositories.License(ctx, r.Author, r.Project)
7474
if err != nil {
75-
fmt.Println(r.Author, r.Project)
7675
return err
7776
}
7877

@@ -82,6 +81,7 @@ func (gc *gitClient) GetLicense(ctx context.Context, r *Repository) error {
8281
clr = color.FgYellow
8382
}
8483
r.Shortname = color.New(clr).Sprintf(name)
84+
r.License = name
8585
r.Text = rl.GetContent()
8686

8787
if gc.star && gc.gh.logged {

cmd/glice/main.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"flag"
5+
"fmt"
56
"io"
67
"log"
78
"os"
@@ -16,6 +17,13 @@ func main() {
1617
path = flag.String("p", "", `Path of desired directory to be scanned with Glice (e.g. "github.com/ribice/glice/v2")`)
1718
thx = flag.Bool("t", false, "Stars dependent repos. Needs GITHUB_API_KEY env variable to work")
1819
verbose = flag.Bool("v", false, "Adds verbose logging")
20+
format = flag.String("fmt", "table", "Output format [table | json | csv]")
21+
output = flag.String("o", "stdout", "Output location [stdout | file]")
22+
extension = map[string]string{
23+
"table": "txt",
24+
"json": "json",
25+
"csv": "csv",
26+
}
1927
)
2028

2129
flag.Parse()
@@ -31,12 +39,21 @@ func main() {
3139
log.SetFlags(0)
3240
}
3341

34-
cl, err := glice.NewClient(*path)
42+
cl, err := glice.NewClient(*path, *format, *output)
3543
checkErr(err)
3644

3745
checkErr(cl.ParseDependencies(*indirect, *thx))
3846

39-
cl.Print(os.Stdout)
47+
switch *output {
48+
case "stdout":
49+
cl.Print(os.Stdout)
50+
case "file":
51+
fileName := fmt.Sprintf("dependencies.%s", extension[*format])
52+
f, err := os.Create(fileName)
53+
checkErr(err)
54+
cl.Print(f)
55+
f.Close()
56+
}
4057

4158
if *fileWrite {
4259
checkErr(cl.WriteLicensesToFile())

glice.go

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package glice
33
import (
44
"context"
55
"encoding/base64"
6+
"encoding/csv"
7+
"encoding/json"
68
"errors"
79
"fmt"
810
"io"
@@ -25,19 +27,41 @@ var (
2527

2628
// ErrNoAPIKey is returned when thanks flag is enabled without providing GITHUB_API_KEY env variable
2729
ErrNoAPIKey = errors.New("cannot use thanks feature without github api key")
30+
31+
validFormats = map[string]bool{
32+
"table": true,
33+
"json": true,
34+
"csv": true,
35+
}
36+
37+
// validOutputs to print to
38+
validOutputs = map[string]bool{
39+
"stdout": true,
40+
"file": true,
41+
}
2842
)
2943

3044
type Client struct {
3145
dependencies []*Repository
3246
path string
47+
format string
48+
output string
3349
}
3450

35-
func NewClient(path string) (*Client, error) {
51+
func NewClient(path, format, output string) (*Client, error) {
52+
if !validFormats[format] {
53+
return nil, fmt.Errorf("invalid format provided (%s) - allowed ones are [table, json, csv]", output)
54+
}
55+
56+
if !validOutputs[output] {
57+
return nil, fmt.Errorf("invalid output provided (%s) - allowed ones are [stdout, file]", output)
58+
}
59+
3660
if !mod.Exists(path) {
3761
return nil, ErrNoGoMod
3862
}
3963

40-
return &Client{path: path}, nil
64+
return &Client{path: path, format: format, output: output}, nil
4165
}
4266

4367
func (c *Client) ParseDependencies(includeIndirect, thanks bool) error {
@@ -65,20 +89,51 @@ func (c *Client) ParseDependencies(includeIndirect, thanks bool) error {
6589
return nil
6690
}
6791

68-
func (c *Client) Print(output io.Writer) {
92+
var (
93+
headerRow = []string{"Dependency", "RepoURL", "License"}
94+
)
95+
96+
func (c *Client) Print(writeTo io.Writer) error {
6997
if len(c.dependencies) < 1 {
70-
return
98+
return nil
7199
}
72-
tw := tablewriter.NewWriter(output)
73-
tw.SetHeader([]string{"Dependency", "RepoURL", "License"})
74-
for _, d := range c.dependencies {
75-
tw.Append([]string{d.Name, color.BlueString(d.URL), d.Shortname})
100+
101+
switch c.format {
102+
case "table":
103+
tw := tablewriter.NewWriter(writeTo)
104+
tw.SetHeader(headerRow)
105+
for _, d := range c.dependencies {
106+
tw.Append([]string{d.Name, color.BlueString(d.URL), d.Shortname})
107+
}
108+
tw.Render()
109+
case "json":
110+
return json.NewEncoder(writeTo).Encode(c.dependencies)
111+
case "csv":
112+
csvW := csv.NewWriter(writeTo)
113+
defer csvW.Flush()
114+
err := csvW.Write(headerRow)
115+
if err != nil {
116+
return err
117+
}
118+
for _, d := range c.dependencies {
119+
err = csvW.Write([]string{d.Project, d.URL, d.License})
120+
if err != nil {
121+
return err
122+
}
123+
}
124+
return csvW.Error()
76125
}
77-
tw.Render()
126+
127+
// shouldn't be possible to get this error
128+
return fmt.Errorf("invalid output provided (%s) - allowed ones are [stdout, json, csv]", c.output)
78129
}
79130

80131
func Print(path string, indirect bool, writeTo io.Writer) error {
81-
c, err := NewClient(path)
132+
return PrintTo(path, "table", "stdout", indirect, writeTo)
133+
}
134+
135+
func PrintTo(path, format, output string, indirect bool, writeTo io.Writer) error {
136+
c, err := NewClient(path, format, output)
82137
if err != nil {
83138
return err
84139
}
@@ -98,10 +153,9 @@ func ListRepositories(path string, withIndirect bool) ([]*Repository, error) {
98153
return nil, err
99154
}
100155

101-
var repos []*Repository
102-
103-
for _, mods := range modules {
104-
repos = append(repos, getRepository(mods))
156+
repos := make([]*Repository, len(modules))
157+
for i, mods := range modules {
158+
repos[i] = getRepository(mods)
105159
}
106160

107161
return repos, nil

glice_test.go

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestClient_ParseDependencies(t *testing.T) {
4646
}
4747
for name, tt := range tests {
4848
t.Run(name, func(t *testing.T) {
49-
c := &Client{path: tt.path}
49+
c := &Client{path: tt.path, format: "table", output: "stdout"}
5050
if err := c.ParseDependencies(tt.includeIndirect, tt.thanks); (err != nil) != tt.wantErr {
5151
t.Errorf("ParseDependencies() error = %v, wantErr %v", err, tt.wantErr)
5252
}
@@ -67,7 +67,6 @@ func TestClient_ParseDependencies(t *testing.T) {
6767
}
6868

6969
func TestPrint(t *testing.T) {
70-
7170
tests := map[string]struct {
7271
path string
7372
wantWriteOutput bool
@@ -97,6 +96,48 @@ func TestPrint(t *testing.T) {
9796
}
9897
}
9998

99+
func TestPrintTo(t *testing.T) {
100+
tests := map[string]struct {
101+
path string
102+
format string
103+
wantWriteOutput bool
104+
wantErr bool
105+
}{
106+
"invalid path": {
107+
path: "invalid",
108+
wantErr: true,
109+
},
110+
"json format": {
111+
path: wd(),
112+
wantWriteOutput: true,
113+
format: "json",
114+
},
115+
"csv format": {
116+
path: wd(),
117+
wantWriteOutput: true,
118+
format: "csv",
119+
},
120+
"valid path": {
121+
path: wd(),
122+
wantWriteOutput: true,
123+
format: "table",
124+
},
125+
}
126+
for name, tt := range tests {
127+
t.Run(name, func(t *testing.T) {
128+
writeTo := &bytes.Buffer{}
129+
err := PrintTo(tt.path, tt.format, "stdout", false, writeTo)
130+
if (err != nil) != tt.wantErr {
131+
t.Errorf("Print() error = %v, wantErr %v", err, tt.wantErr)
132+
return
133+
}
134+
if (writeTo.String() != "") != tt.wantWriteOutput {
135+
t.Error("wantWriteOutput and gotOutput do not match")
136+
}
137+
})
138+
}
139+
}
140+
100141
func TestClient_Print(t *testing.T) {
101142
tests := map[string]struct {
102143
dependencies []*Repository
@@ -110,7 +151,7 @@ func TestClient_Print(t *testing.T) {
110151
}
111152
for name, tt := range tests {
112153
t.Run(name, func(t *testing.T) {
113-
c := &Client{dependencies: tt.dependencies}
154+
c := &Client{dependencies: tt.dependencies, format: "table", output: "stdout"}
114155
output := &bytes.Buffer{}
115156
c.Print(output)
116157
if (output.String() != "") != tt.wantOutput {
@@ -150,7 +191,7 @@ func TestClient_WriteLicensesToFile(t *testing.T) {
150191
}
151192
for name, tt := range tests {
152193
t.Run(name, func(t *testing.T) {
153-
c := &Client{dependencies: tt.dependencies}
194+
c := &Client{dependencies: tt.dependencies, format: "table", output: "stdout"}
154195
err := c.WriteLicensesToFile()
155196
if (err != nil) != tt.wantErr {
156197
t.Errorf("Print() error = %v, wantErr %v", err, tt.wantErr)
@@ -202,19 +243,34 @@ func TestListRepositories(t *testing.T) {
202243
func TestNewClient(t *testing.T) {
203244
tests := map[string]struct {
204245
path string
246+
output string
247+
format string
205248
wantErr bool
206249
}{
250+
"invalid format": {
251+
format: "invalid",
252+
wantErr: true,
253+
},
254+
"invalid output": {
255+
format: "csv",
256+
output: "invalid",
257+
wantErr: true,
258+
},
207259
"invalid path": {
260+
format: "csv",
261+
output: "stdout",
208262
path: "invalid",
209263
wantErr: true,
210264
},
211-
"valid path": {
212-
path: wd(),
265+
"invalid path": {
266+
format: "csv",
267+
output: "stdout",
268+
path: wd(),
213269
},
214270
}
215271
for name, tt := range tests {
216272
t.Run(name, func(t *testing.T) {
217-
_, err := NewClient(tt.path)
273+
_, err := NewClient(tt.path, tt.format, tt.output)
218274
if (err != nil) != tt.wantErr {
219275
t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
220276
return

0 commit comments

Comments
 (0)