├── Article_CN.md
├── README.md
├── go.mod
├── graph.go
├── graph_test.go
└── main.go
/Article_CN.md:
--------------------------------------------------------------------------------
1 | > [本文](http://poloxue.com/go/go-module-relationship-visible-tool)首发于[我的博客](http://poloxue.com),如果觉得有用,欢迎点赞收藏,让更多的朋友看到。
2 |
3 | 最近,我开发了一个非常简单的小工具,总的代码量 200 行不到。今天,简单介绍下它。这是个什么工具呢?它是一个用于可视化展示 Go Module 依赖关系的工具。
4 |
5 | # 为何开发
6 |
7 | 为什么会想到开发这个工具?主要有两点原因:
8 |
9 | 一是最近经常看到大家在社区讨论 Go Module。于是,我也花了一些时间研究了下。期间,遇到了一个需求,如何清晰地识别模块中依赖项之间的关系。一番了解后,发现了 `go mod graph`。
10 |
11 | 效果如下:
12 |
13 | ```
14 | $ go mod graph
15 | github.com/poloxue/testmod golang.org/x/text@v0.3.2
16 | github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0
17 | github.com/poloxue/testmod rsc.io/sampler@v1.3.1
18 | golang.org/x/text@v0.3.2 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e
19 | rsc.io/quote/v3@v3.1.0 rsc.io/sampler@v1.3.0
20 | rsc.io/sampler@v1.3.1 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
21 | rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
22 | ```
23 |
24 | 每一行的格式是 `模块 依赖模块`,基本能满足要求,但总觉得还是不那么直观。
25 |
26 | 二是我之前手里有一个项目,包管理一直用的是 dep。于是,我也了解了下它,把官方文档仔细读了一遍。其中的[某个章节](https://golang.github.io/dep/docs/daily-dep.html)介绍了依赖项可视化展示的方法。
27 |
28 | 文档中给出的包关系图:
29 |
30 |
31 |
32 |
33 |
34 | 看到这张图的时候,眼睛瞬间就亮了,图形化就是优秀,不同依赖之间的关系一目了然。这不就是我想要的效果吗?666,点个赞。
35 |
36 | 但 ...,随之而来的问题是,go mod 没这个能力啊。怎么办?
37 |
38 | # 如何实现
39 |
40 | 先看看是不是已经有人做了这件事了。网上搜了下,没找到。那是不是能自己实现?应该可以借鉴下 dep 的思路吧?
41 |
42 | 如下是 dep 依赖实现可视化的方式:
43 |
44 | ```bash
45 | # linux
46 | $ sudo apt-get install graphviz
47 | $ dep status -dot | dot -T png | display
48 |
49 | # macOS
50 | $ brew install graphviz
51 | $ dep status -dot | dot -T png | open -f -a /Applications/Preview.app
52 |
53 | # Windows
54 | > choco install graphviz.portable
55 | > dep status -dot | dot -T png -o status.png; start status.png
56 | ```
57 |
58 | 这里展示了三大系统下的使用方式,它们都安装了一个软件包,graphviz。从名字上看,这应该是一个用来实现可视化的软件,即用来画图的。事实也是这样,可以看看它的[官网](http://www.graphviz.org/documentation/)。
59 |
60 | 再看下它的使用,发现都是通过管道命令组合的方式,而且前面的部分基本相同,都是 `dep status -dot | dot -T png`。后面的部分在不同的系统就不同了,Linux 是 `display`,MacOS 是 `open -f -a /Applications/Preview.app`,Window 是 `start status.png`。
61 |
62 | 稍微分析下就会明白,前面是生成图片,后面是显示图片。因为不同系统的图片展示命令不同,所以后面的部分也就不同了。
63 |
64 | 现在关心的重点在前面,即 `dep status -dot | dot -T png` 干了啥,它究竟是如何实现绘图的?大致猜测,dot -T png 是由 dep status -dot 提供的数据生成图片。继续看下 `dep status -dot` 的执行效果吧。
65 |
66 | ```bash
67 | $ dep status -dot
68 | digraph {
69 | node [shape=box];
70 | 2609291568 [label="github.com/poloxue/hellodep"];
71 | 953278068 [label="rsc.io/quote\nv3.1.0"];
72 | 3852693168 [label="rsc.io/sampler\nv1.0.0"];
73 | 2609291568 -> 953278068;
74 | 953278068 -> 3852693168;
75 | }
76 | ```
77 |
78 | 咋一看,输出的是一段看起来不知道是啥的代码,这应该是 graphviz 用于绘制图表的语言。那是不是还有学习下?当然不用啊,这里用的很简单,直接套用就行了。
79 |
80 | 试着分析一下吧,前面两行可以不用关心,这应该是 graphviz 特定的写法,表示要画的是什么图。我们主要关心如何将数据以正确形式提供出来。
81 |
82 | ```bash
83 | 2609291568 [label="github.com/poloxue/hellodep"];
84 | 953278068 [label="rsc.io/quote\nv3.1.0"];
85 | 3852693168 [label="rsc.io/sampler\nv1.0.0"];
86 | 2609291568 -> 953278068;
87 | 953278068 -> 3852693168;
88 | ```
89 |
90 | 一看就知道,这里有两种结构,分别是为依赖项关联 ID ,和通过 ID 和 `->` 表示依赖间的关系。
91 |
92 | 按上面的猜想,我们可以试着画出一个简单的图, 用于表示 a 模块依赖 b 模块。执行命令如下,将绘图代码通过 `each` 管道的方式发送给 `dot` 命令。
93 |
94 | ```
95 | $ echo 'digraph {
96 | node [shape=box];
97 | 1 [label="a"];
98 | 2 [label="b"];
99 | 1 -> 2;
100 | }' | dot -T png | open -f -a /Applications/Preview.app
101 | ```
102 |
103 | 效果如下:
104 |
105 |
106 |
107 |
108 |
109 | 绘制一个依赖关系图竟然这么简单。
110 |
111 | 看到这里,是不是发现问题已经变得非常简单了。我们只要将 `go mod graph` 的输出转化为类似的结构就能实现可视化了。
112 |
113 | # 开发流程介绍
114 |
115 | 接下来,开发这个小程序吧,我将这个小程序命名为 `modv`,即 module visible 的意思。项目源码位于 [poloxue/modv](https://github.com/poloxue/modv)。
116 |
117 | # 接收管道的输入
118 |
119 | 先要检查数据输入管道是否正常。
120 |
121 | 我们的目标是使用类似 `dep` 中作图的方式,`go mod graph` 通过管道将数据传递给 `modv`。因此,要先检查 `os.Stdin`,即检查标准输入状态是否正常, 以及是否是管道传输。
122 |
123 | 下面是 main 函数的代码,位于 [main.go](https://github.com/poloxue/modv/blob/master/main.go) 中。
124 |
125 | ```go
126 | func main() {
127 | info, err := os.Stdin.Stat()
128 | if err != nil {
129 | fmt.Println("os.Stdin.Stat:", err)
130 | PrintUsage()
131 | os.Exit(1)
132 | }
133 |
134 | // 是否是管道传输
135 | if info.Mode()&os.ModeNamedPipe == 0 {
136 | fmt.Println("command err: command is intended to work with pipes.")
137 | PrintUsage()
138 | os.Exit(1)
139 | }
140 | ```
141 |
142 | 一旦确认输入设备一切正常,我们就可以进入到数据读取、解析与渲染的流程了。
143 |
144 | ```go
145 | mg := NewModuleGraph(os.Stdin)
146 | mg.Parse()
147 | mg.Render(os.Stdout)
148 | }
149 | ```
150 |
151 | 接下来,开始具体看看如何实现数据的处理流程。
152 |
153 | # 抽象实现结构
154 |
155 | 先定义一个结构体,并大致定义整个流程。
156 |
157 | ```go
158 | type ModGraph struct {
159 | Reader io.Reader // 读取数据流
160 | }
161 |
162 | func NewModGraph(r io.Reader) *ModGraph {
163 | return &ModGraph{Reader: r}
164 | }
165 |
166 | // 执行数据的处理转化
167 | func (m *ModGraph) Parse() error {}
168 |
169 | // 结果渲染与输出
170 | func (m *ModGraph) Render(w io.Writer) error {}
171 | ```
172 |
173 | 再看下 `go mod graph` 的输出吧,如下:
174 |
175 | ```bash
176 | github.com/poloxue/testmod golang.org/x/text@v0.3.2
177 | github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0
178 | ...
179 | ```
180 |
181 | 每一行的结构是 `模块 依赖项`。现在的目标是要它解析成下面这样的结构:
182 |
183 | ```bash
184 | digraph {
185 | node [shape=box];
186 | 1 github.com/poloxue/testmod;
187 | 2 golang.org/x/text@v0.3.2;
188 | 3 rsc.io/quote/v3@v3.1.0;
189 | 1 -> 2;
190 | 1 -> 3;
191 | }
192 | ```
193 |
194 | 前面说过,这里包含了两种不同的结构,分别是模块与 ID 关联关系,以及模块 ID 表示模块间的依赖关联。为 `ModGraph` 结构体增加两个成员表示它们。
195 |
196 | ```go
197 | type ModGraph struct {
198 | r io.Reader // 数据流读取实例,这里即 os.Stdin
199 |
200 | // 每一项名称与 ID 的映射
201 | Mods map[string]int
202 | // ID 和依赖 ID 关系映射,一个 ID 可能依赖多个项
203 | Dependencies map[int][]int
204 | }
205 | ```
206 |
207 | 要注意的是,增加了两个 map 成员后,记住要在 `NewModGraph` 中初始化下它们。
208 |
209 | # mod graph 输出解析
210 |
211 | 如何进行解析?
212 |
213 | 介绍到这里,目标已经很明白了。就是要将输入数据解析到 `Mods` 和 `Dependencies` 两个成员中,实现代码都在 `Parse` 方法中。
214 |
215 | 为了方便进行数据读取,首先,我们利用 `bufio` 基于 `reader` 创建一个新的 `bufReader`,
216 |
217 | ```go
218 | func (m *ModGraph) Parse() error {
219 | bufReader := bufio.NewReader(m.Reader)
220 | ...
221 | ```
222 |
223 | 为便于按行解析数据,我们通过 bufReader 的 `ReadBytes()` 方法循环一行一行地读取 os.Stdin 中的数据。然后,对每一行数据按空格切分,获取到依赖关系的两项。代码如下:
224 |
225 | ```go
226 | for {
227 | relationBytes, err := bufReader.ReadBytes('\n')
228 | if err != nil {
229 | if err == io.EOF {
230 | return nil
231 | }
232 | return err
233 | }
234 |
235 | relation := bytes.Split(relationBytes, []byte(" "))
236 | // module and dependency
237 | mod, depMod := strings.TrimSpace(string(relation[0])), strings.TrimSpace(string(relation[1]))
238 |
239 | ...
240 | }
241 | ```
242 |
243 | 接下来,就是将解析出来的依赖关系组织到 `Mods` 和 `Dependencies` 两个成员中。模块 ID 是生成规则采用的是最简单的实现方式,从 1 自增。实现代码如下:
244 |
245 | ```go
246 | modId, ok := m.Mods[mod]
247 | if !ok {
248 | modId = serialID
249 | m.Mods[mod] = modId
250 | serialID += 1
251 | }
252 |
253 | depModId, ok := m.Mods[depMod]
254 | if !ok {
255 | depModId = serialID
256 | m.Mods[depMod] = depModId
257 | serialID += 1
258 | }
259 |
260 | if _, ok := m.Dependencies[modId]; ok {
261 | m.Dependencies[modId] = append(m.Dependencies[modId], depModId)
262 | } else {
263 | m.Dependencies[modId] = []int{depModId}
264 | }
265 | ```
266 |
267 | 解析的工作到这里就结束了。
268 |
269 | # 渲染解析的结果
270 |
271 | 这个小工具还剩下最后一步工作要做,即将解析出来的数据渲染出来,以满足 `graphviz` 工具的作图要求。实现代码是 `Render`部分:
272 |
273 | 首先,定义一个模板,以生成满足要求的输出格式。
274 |
275 | ```go
276 | var graphTemplate = `digraph {
277 | node [shape=box];
278 | {{ range $mod, $modId := .mods -}}
279 | {{ $modId }} [label="{{ $mod }}"];
280 | {{ end -}}
281 | {{- range $modId, $depModIds := .dependencies -}}
282 | {{- range $_, $depModId := $depModIds -}}
283 | {{ $modId }} -> {{ $depModId }};
284 | {{ end -}}
285 | {{- end -}}
286 | }
287 | `
288 | ```
289 |
290 | 这一块没啥好介绍的,主要是要熟悉 Go 中的 `text/template` 模板的语法规范。为了展示友好,这里通过 `-` 实现换行的去除,整体而言不影响阅读。
291 |
292 | 接下来,看 `Render` 方法的实现,把前面解析出来的 `Mods` 和 `Dependencies` 放入模板进行渲染。
293 |
294 | ```go
295 | func (m *ModuleGraph) Render(w io.Writer) error {
296 | templ, err := template.New("graph").Parse(graphTemplate)
297 | if err != nil {
298 | return fmt.Errorf("templ.Parse: %v", err)
299 | }
300 |
301 | if err := templ.Execute(w, map[string]interface{}{
302 | "mods": m.Mods,
303 | "dependencies": m.Dependencies,
304 | }); err != nil {
305 | return fmt.Errorf("templ.Execute: %v", err)
306 | }
307 |
308 | return nil
309 | }
310 | ```
311 |
312 | 现在,全部工作都完成了。最后,将这个流程整合到 main 函数。接下来就是使用了。
313 |
314 | # 使用体验
315 |
316 | 开始体验下吧。补充一句,这个工具,我现在只测试了 Mac 下的使用,如有问题,欢迎提出来。
317 |
318 | 首先,要先安装一下 `graphviz`,安装的方式在本文开头已经介绍了,选择你的系统安装方式。
319 |
320 | 接着是安装 `modv`,命令如下:
321 |
322 | ```bash
323 | $ go get github.com/poloxue/modv
324 | ```
325 |
326 | 安装完成!简单测试下它的使用。
327 |
328 | 以 MacOS 为例。先下载测试库,github.com/poloxue/testmod。 进入 testmod 目录执行命令:
329 |
330 | ```bash
331 | $ go mod graph | modv | dot -T png | open -f -a /Applications/Preview.app
332 | ```
333 |
334 | 如果执行成功,将看到如下的效果:
335 |
336 | 
337 |
338 | 完美地展示了各个模块之间的依赖关系。
339 |
340 | # 一些思考
341 |
342 | 本文是篇实践性的文章,从一个简单想法到成功呈现出一个可以使用的工具。虽然,开发起来并不难,从开发到完成,仅仅花了一两个小时。但我的感觉,这确实是个有实际价值的工具。
343 |
344 | 还有一些想法没有实现和验证,比如一旦项目较大,是否可以方便的展示某个指定节点的依赖树,而非整个项目。还有,在其他项目向 Go Module 迁移的时候,这个小工具是否能产生一些价值。
345 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a module dependency visualizer for go mod.
2 |
3 | # Usage
4 |
5 | Modv's usage is different in different systems.
6 |
7 | ## Linux
8 |
9 | Install `graphviz`. For Ubuntu/Debian
10 | ```bash
11 | $ sudo apt-get install graphviz
12 | ```
13 |
14 | For ArchLinux
15 |
16 | ```
17 | $ sudo pacman -S --needed graphviz
18 | ```
19 |
20 | Install `modv` and use it.
21 |
22 | ```
23 | $ go install github.com/poloxue/modv
24 | $ go mod graph | modv | dot -T svg -o /tmp/modv.svg && xdg-open /tmp/modv.svg
25 | ```
26 |
27 |
28 | ## MacOS
29 |
30 | ```bash
31 | $ brew install graphviz
32 | $ go get github.com/poloxue/modv
33 | ```
34 |
35 | Try the following.
36 |
37 | ```
38 | $ go mod graph | modv | dot -T png | open -f -a /Applications/Preview.app
39 | ```
40 |
41 | If error accured, for eaxmple,`FSPathMakeRef(/Applications/Preview.app) failed with error -43.`,try the command:
42 |
43 | ```
44 | $ go mod graph | modv | dot -T png | open -f -a /System/Applications/Preview.app
45 | ```
46 |
47 | ## Windows
48 |
49 | First, install `graphviz`:
50 |
51 | ```bash
52 | $ choco install graphviz.portable
53 | ```
54 | For [MSYS2](https://www.msys2.org/)
55 |
56 | ```bash
57 | $ pacman -S mingw-w64-x86_64-graphviz
58 | ```
59 |
60 | Try it.
61 |
62 | ```bash
63 | $ go get github.com/poloxue/modv
64 | $ go mod graph | modv | dot -T svg -o graph.svg; start graph.svg
65 | ```
66 |
67 | # Demo
68 |
69 | If MacOS, tye the following:
70 |
71 | ```bash
72 | $ git clone https://github.com/poloxue/testmod
73 | $ cd testmod
74 | $ go mod graph | modv | dot -T png | open -f -a /System/Applications/Preview.app
75 | ```
76 |
77 | Output:
78 |
79 | 
80 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/poloxue/modv
2 |
3 | go 1.12
4 |
--------------------------------------------------------------------------------
/graph.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "io"
8 | "strings"
9 | "text/template"
10 | )
11 |
12 | var graphTemplate = `digraph {
13 | {{- if eq .direction "horizontal" -}}
14 | rankdir=LR;
15 | {{ end -}}
16 | node [shape=box];
17 | {{ range $mod, $modId := .mods -}}
18 | {{ $modId }} [label="{{ $mod }}"];
19 | {{ end -}}
20 | {{- range $modId, $depModIds := .dependencies -}}
21 | {{- range $_, $depModId := $depModIds -}}
22 | {{ $modId }} -> {{ $depModId }};
23 | {{ end -}}
24 | {{- end -}}
25 | }
26 | `
27 |
28 | type ModuleGraph struct {
29 | Reader io.Reader
30 |
31 | Mods map[string]int
32 | Dependencies map[int][]int
33 | }
34 |
35 | func NewModuleGraph(r io.Reader) *ModuleGraph {
36 | return &ModuleGraph{
37 | Reader: r,
38 |
39 | Mods: make(map[string]int),
40 | Dependencies: make(map[int][]int),
41 | }
42 | }
43 |
44 | func (m *ModuleGraph) Parse() error {
45 | bufReader := bufio.NewReader(m.Reader)
46 |
47 | serialID := 1
48 | for {
49 | relationBytes, err := bufReader.ReadBytes('\n')
50 | if err != nil {
51 | if err == io.EOF {
52 | return nil
53 | }
54 | return err
55 | }
56 |
57 | relation := bytes.Split(relationBytes, []byte(" "))
58 | mod, depMod := strings.TrimSpace(string(relation[0])), strings.TrimSpace(string(relation[1]))
59 |
60 | mod = strings.Replace(mod, "@", "\n", 1)
61 | depMod = strings.Replace(depMod, "@", "\n", 1)
62 |
63 | modId, ok := m.Mods[mod]
64 | if !ok {
65 | modId = serialID
66 | m.Mods[mod] = modId
67 | serialID += 1
68 | }
69 |
70 | depModId, ok := m.Mods[depMod]
71 | if !ok {
72 | depModId = serialID
73 | m.Mods[depMod] = depModId
74 | serialID += 1
75 | }
76 |
77 | m.Dependencies[modId] = append(m.Dependencies[modId], depModId)
78 | }
79 | }
80 |
81 | func (m *ModuleGraph) Render(w io.Writer) error {
82 | templ, err := template.New("graph").Parse(graphTemplate)
83 | if err != nil {
84 | return fmt.Errorf("templ.Parse: %v", err)
85 | }
86 |
87 | var direction string
88 | if len(m.Dependencies) > 15 {
89 | direction = "horizontal"
90 | }
91 |
92 | if err := templ.Execute(w, map[string]interface{}{
93 | "mods": m.Mods,
94 | "dependencies": m.Dependencies,
95 | "direction": direction,
96 | }); err != nil {
97 | return fmt.Errorf("templ.Execute: %v", err)
98 | }
99 |
100 | return nil
101 | }
102 |
--------------------------------------------------------------------------------
/graph_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "testing"
8 | )
9 |
10 | func TestModuleGraph_Parse(t *testing.T) {
11 | type args struct {
12 | reader io.Reader
13 | }
14 | tests := []struct {
15 | name string
16 | args args
17 | want []byte
18 | }{
19 | {
20 | name: "full",
21 | args: args{
22 | bytes.NewReader([]byte(`github.com/poloxue/testmod golang.org/x/text@v0.3.2
23 | github.com/poloxue/testmod rsc.io/quote/v3@v3.1.0
24 | github.com/poloxue/testmod rsc.io/sampler@v1.3.1
25 | golang.org/x/text@v0.3.2 golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e
26 | rsc.io/quote/v3@v3.1.0 rsc.io/sampler@v1.3.0
27 | rsc.io/sampler@v1.3.1 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
28 | rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c`))},
29 | want: []byte(`digraph {
30 | node [shape=box];
31 | 1 [label="github.com/poloxue/testmod"];
32 | 2 [label="golang.org/x/text@v0.3.2"];
33 | 3 [label="rsc.io/quote/v3@v3.1.0"];
34 | 4 [label="rsc.io/sampler@v1.3.1"];
35 | 5 [label="golang.org/x/tools@v0.0.0-20180917221912-90fa682c2a6e"];
36 | 6 [label="rsc.io/sampler@v1.3.0"];
37 | 7 [label="golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c"]
38 | 1 -> 2;
39 | 1 -> 3;
40 | 1 -> 4;
41 | 2 -> 5;
42 | 3 -> 6;
43 | 4 -> 7;
44 | 6 -> 7;
45 | }`),
46 | },
47 | }
48 | for _, tt := range tests {
49 | t.Run(tt.name, func(t *testing.T) {
50 | moduleGraph := NewModuleGraph(tt.args.reader)
51 | moduleGraph.Parse()
52 | for k, v := range moduleGraph.Mods {
53 | fmt.Println(v, k)
54 | }
55 |
56 | for k, v := range moduleGraph.Dependencies {
57 | fmt.Println(k)
58 | fmt.Println(v)
59 | fmt.Println()
60 | }
61 | })
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "runtime"
7 | )
8 |
9 | func PrintUsage() {
10 | fmt.Printf("\nUsages:\n\n")
11 | switch runtime.GOOS {
12 | case "darwin":
13 | fmt.Printf("\tgo mod graph | modv | dot -T svg | open -f -a /System/Applications/Preview.app")
14 | case "linux":
15 | fmt.Printf("\tgo mod graph | modv | dot -T svg -o /tmp/modv.svg | xdg-open /tmp/modv.svg")
16 | case "windows":
17 | fmt.Printf("\tgo mod graph | modv | dot -T png -o graph.png; start graph.png")
18 | }
19 |
20 | fmt.Printf("\n\n")
21 | }
22 |
23 | func main() {
24 | info, err := os.Stdin.Stat()
25 | if err != nil {
26 | fmt.Println("os.Stdin.Stat:", err)
27 | PrintUsage()
28 | os.Exit(1)
29 | }
30 |
31 | if info.Mode()&os.ModeNamedPipe == 0 {
32 | fmt.Println("command err: command is intended to work with pipes.")
33 | PrintUsage()
34 | os.Exit(1)
35 | }
36 |
37 | mg := NewModuleGraph(os.Stdin)
38 | if err := mg.Parse(); err != nil {
39 | fmt.Println("mg.Parse: ", err)
40 | PrintUsage()
41 | os.Exit(1)
42 | }
43 |
44 | if err := mg.Render(os.Stdout); err != nil {
45 | fmt.Println("mg.Render: ", err)
46 | PrintUsage()
47 | os.Exit(1)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------