Skip to content

Commit 8416749

Browse files
authored
Merge pull request #35 from Kristina-Pianykh/add-cli-arg-for-output-file
Add cli arg for output file
2 parents 0060840 + 26b6e77 commit 8416749

File tree

7 files changed

+299
-27
lines changed

7 files changed

+299
-27
lines changed

.github/workflows/hype.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: Generate README with Hype
2-
on: [pull_request]
2+
on:
3+
push:
4+
branches:
5+
- main
36
jobs:
47
build:
58
runs-on: ubuntu-latest
@@ -16,7 +19,7 @@ jobs:
1619
- name: Install hype
1720
run: go install github.com/gopherguides/hype/cmd/hype@latest
1821
- name: Run hype
19-
run: hype export -format=markdown -f hype.md > README.md
22+
run: hype export -format=markdown -f hype.md -o README.md
2023
- name: Commit README back to the repo
2124
run: |-
2225
git rev-parse --abbrev-ref HEAD

.github/workflows/tests.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
name: Go Test and Lint
2-
on: [push]
2+
on:
3+
push:
4+
branches:
5+
- main
6+
pull_request:
37
jobs:
48
tests-on:
59
name: ${{matrix.go-version}} ${{matrix.os}}
@@ -19,11 +23,10 @@ jobs:
1923
- name: Verify Go Modules
2024
run: go mod verify
2125
- name: Build
22-
run: go build -v ./cmd/hype/
26+
run: make build
2327
- name: Run tests with Race Detector
2428
run: |
25-
go_dirs=$(go list ./... | grep -v '/docs')
26-
go test -race -vet=off $go_dirs
29+
make test
2730
- name: Install staticcheck
2831
run: go install honnef.co/go/tools/cmd/staticcheck@latest
2932
- name: Run staticcheck

Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
default: test install hype
22

33
test:
4-
go test -timeout 10s -count 1 -race -cover $$(go list ./... | grep -v /docs/)
4+
go test -count 1 -race -vet=off -cover $$(go list ./... | grep -v /docs/)
55

66
cov:
77
go test -coverprofile=coverage.out ./...
@@ -15,4 +15,11 @@ install:
1515
go install -v ./cmd/hype
1616

1717
hype:
18-
hype export -format=markdown -f hype.md > ./README.md
18+
hype export -format=markdown -f hype.md -o ./README.md
19+
20+
.PHONY: build docs
21+
build:
22+
go build -o hype ./cmd/hype/
23+
24+
docs: build
25+
./hype export -f hype.md -format markdown -o README.md

cli_errors_test.go renamed to cli_integration_test.go

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,20 @@ func getProjectWd() string {
2828
}
2929

3030
func Test_Cli_Error(t *testing.T) {
31-
t.Parallel()
3231
r := require.New(t)
3332

3433
binPath := filepath.Join(t.TempDir(), "hype")
3534
buildBin(binPath)
3635

3736
root := filepath.Join(getProjectWd(), "testdata", "to_md", "source_code", "broken")
38-
err := os.Chdir(root)
39-
r.NoError(err)
37+
t.Chdir(root)
4038

4139
args := []string{"export", "-format=markdown", "-f", "hype.md"}
4240

4341
t.Logf("Running command: %s %s, root: %s", binPath, strings.Join(args, " "), root)
4442

45-
hypeCmd := exec.Command(binPath, args...)
46-
var stderr bytes.Buffer
47-
var stdout bytes.Buffer
48-
hypeCmd.Stderr = &stderr
49-
hypeCmd.Stdout = &stdout
50-
51-
err = hypeCmd.Run()
43+
hypeCmd, stdout, _ := setupCmd(binPath, args)
44+
err := hypeCmd.Run()
5245
r.Error(err)
5346

5447
r.Equal(0, stdout.Len())
@@ -60,26 +53,31 @@ func Test_Cli_Error(t *testing.T) {
6053
r.Equal(1, exitCode)
6154
}
6255

56+
func setupCmd(binPath string, args []string) (*exec.Cmd, *bytes.Buffer, *bytes.Buffer) {
57+
cmd := exec.Command(binPath, args...)
58+
var stderr bytes.Buffer
59+
var stdout bytes.Buffer
60+
cmd.Stderr = &stderr
61+
cmd.Stdout = &stdout
62+
63+
return cmd, &stdout, &stderr
64+
}
65+
6366
func Test_Cli_Success(t *testing.T) {
64-
t.Parallel()
6567
r := require.New(t)
6668

6769
binPath := filepath.Join(t.TempDir(), "hype")
6870
buildBin(binPath)
6971

7072
root := filepath.Join(getProjectWd(), "testdata", "to_md", "source_code", "full")
71-
err := os.Chdir(root)
72-
r.NoError(err)
73+
t.Chdir(root)
7374

7475
args := []string{"export", "-format=markdown", "-f", "hype.md"}
76+
7577
t.Logf("Running command: %s %s", binPath, strings.Join(args, " "))
76-
hypeCmd := exec.Command(binPath, args...)
77-
var stderr bytes.Buffer
78-
var stdout bytes.Buffer
79-
hypeCmd.Stderr = &stderr
80-
hypeCmd.Stdout = &stdout
8178

82-
err = hypeCmd.Run()
79+
hypeCmd, stdout, stderr := setupCmd(binPath, args)
80+
err := hypeCmd.Run()
8381
if err != nil {
8482
fmt.Printf("%s\n", stderr.String())
8583
}
@@ -97,3 +95,58 @@ func Test_Cli_Success(t *testing.T) {
9795

9896
r.Equal(exp, act)
9997
}
98+
99+
func Test_Output_Flag_Error(t *testing.T) {
100+
r := require.New(t)
101+
102+
binPath := filepath.Join(t.TempDir(), "hype")
103+
buildBin(binPath)
104+
105+
root := filepath.Join(getProjectWd(), "testdata", "to_md", "source_code", "broken")
106+
t.Chdir(root)
107+
108+
inputFile := "hype.md"
109+
outputFile := inputFile
110+
111+
args := []string{"export", "-format=markdown", "-f", inputFile, "-o", outputFile}
112+
hypeCmd, _, _ := setupCmd(binPath, args)
113+
err := hypeCmd.Run()
114+
r.Error(err)
115+
116+
info, err := os.Stat(filepath.Join(root, inputFile))
117+
r.NoError(err)
118+
119+
r.True(info.Size() > 0)
120+
}
121+
122+
func Test_Output_Flag_Success(t *testing.T) {
123+
r := require.New(t)
124+
125+
binPath := filepath.Join(t.TempDir(), "hype")
126+
buildBin(binPath)
127+
128+
root := filepath.Join(getProjectWd(), "testdata", "to_md", "source_code", "full")
129+
t.Chdir(root)
130+
131+
inputFile := "hype.md"
132+
outputFile := filepath.Join(t.TempDir(), "output.md")
133+
134+
args := []string{"export", "-format=markdown", "-f", inputFile, "-o", outputFile}
135+
hypeCmd, _, _ := setupCmd(binPath, args)
136+
err := hypeCmd.Run()
137+
r.NoError(err)
138+
139+
info, err := os.Stat(outputFile)
140+
r.NoError(err)
141+
r.True(info.Size() > 0)
142+
143+
bExp, err := os.ReadFile(filepath.Join(root, "hype.gold"))
144+
r.NoError(err)
145+
bAct, err := os.ReadFile(outputFile)
146+
r.NoError(err)
147+
148+
exp := strings.TrimSpace(string(bExp))
149+
act := strings.TrimSpace(string(bAct))
150+
151+
r.Equal(exp, act)
152+
}

cmd/hype/cli/export.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"bytes"
45
"context"
56
"flag"
67
"fmt"
@@ -23,6 +24,7 @@ type Export struct {
2324

2425
// a folder containing all chapters of a book, for example
2526
File string // optional file name to preview
27+
OutPath OutPath // path to a file to write the output on success; default: nil
2628
Timeout time.Duration // default: 30s
2729
Parser *hype.Parser // If nil, a default parser is used.
2830
Verbose bool // default: false
@@ -91,6 +93,7 @@ Examples:
9193
hype export -format html
9294
hype export -f README.md -format html
9395
hype export -f README.md -format markdown -timeout=10s
96+
hype export -f input.md -format markdown -o README.md
9497
`
9598

9699
if err := cmd.validate(); err != nil {
@@ -110,6 +113,7 @@ Examples:
110113
cmd.flags.StringVar(&cmd.File, "f", "hype.md", "optional file name to preview, if not provided, defaults to hype.md")
111114
cmd.flags.BoolVar(&cmd.Verbose, "v", false, "enable verbose output for debugging")
112115
cmd.flags.StringVar(&cmd.Format, "format", "markdown", "content type to export to: markdown, html")
116+
cmd.flags.Var(&cmd.OutPath, "o", "path to the output file; if not provided, output is written to stdout")
113117

114118
cmd.flags.Usage = func() {
115119
fmt.Fprintf(stderr, "Usage of %s:\n", os.Args[0])
@@ -145,6 +149,8 @@ func (cmd *Export) main(ctx context.Context, pwd string, args []string) error {
145149
return err
146150
}
147151

152+
cmd.initOutputFilePath()
153+
148154
flags, err := cmd.Flags(cmd.Stderr())
149155
if err != nil {
150156
return err
@@ -153,6 +159,10 @@ func (cmd *Export) main(ctx context.Context, pwd string, args []string) error {
153159
if err := flags.Parse(args); err != nil {
154160
return err
155161
}
162+
163+
var stdoutBuffer bytes.Buffer
164+
cmd.setOut(&stdoutBuffer)
165+
156166
if cmd.Verbose {
157167
// enable debugging
158168
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))
@@ -173,9 +183,40 @@ func (cmd *Export) main(ctx context.Context, pwd string, args []string) error {
173183
return err
174184
}
175185

186+
if !cmd.OutPath.Exists() {
187+
_, err := stdoutBuffer.WriteTo(os.Stdout)
188+
if err != nil {
189+
return fmt.Errorf("failed to write to os.Stdout: %s", err)
190+
}
191+
} else {
192+
path := cmd.OutPath.Value()
193+
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
194+
if err != nil {
195+
return err
196+
}
197+
defer file.Close()
198+
199+
_, err = stdoutBuffer.WriteTo(file)
200+
if err != nil {
201+
return fmt.Errorf("failed to write to %s: %v", path, err)
202+
}
203+
}
204+
176205
return nil
177206
}
178207

208+
func (cmd *Export) initOutputFilePath() {
209+
cmd.mu.Lock()
210+
cmd.OutPath = OutPath{val: nil}
211+
cmd.mu.Unlock()
212+
}
213+
214+
func (cmd *Export) setOut(writer io.Writer) {
215+
cmd.mu.Lock()
216+
cmd.Out = writer
217+
cmd.mu.Unlock()
218+
}
219+
179220
func (cmd *Export) execute(ctx context.Context, pwd string) error {
180221
err := cmd.validate()
181222
if err != nil {

cmd/hype/cli/output.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
)
9+
10+
type OutPath struct {
11+
val *string
12+
}
13+
14+
func (o *OutPath) String() string {
15+
if o.val == nil {
16+
return ""
17+
}
18+
return o.Value()
19+
}
20+
21+
func (o *OutPath) Value() string {
22+
return *o.val
23+
}
24+
25+
func (o *OutPath) Exists() bool {
26+
return o.val != nil
27+
}
28+
29+
func dirExists(path string) error {
30+
pInfo, err := os.Stat(path)
31+
32+
// path exists
33+
if err == nil {
34+
// path exists but it's a directory
35+
if pInfo.IsDir() {
36+
return fmt.Errorf("expected a file path but received path to a directory: %s. ", path)
37+
}
38+
return nil
39+
}
40+
41+
// path doesn't exist, check if the dir exists
42+
dir := filepath.Dir(path)
43+
if _, err = os.Stat(dir); err != nil {
44+
return err
45+
}
46+
return nil
47+
}
48+
49+
func cleanPath(p string) (string, error) {
50+
if filepath.IsAbs(p) {
51+
return filepath.Clean(p), nil
52+
}
53+
54+
pwd, err := os.Getwd()
55+
if err != nil {
56+
return "", fmt.Errorf("failed to identify current working directory: %s", err.Error())
57+
}
58+
filepath.Clean(p)
59+
fullPath := filepath.Join(pwd, p)
60+
return fullPath, nil
61+
}
62+
63+
func (o *OutPath) Set(path string) error {
64+
path = strings.TrimSpace(path)
65+
66+
// path not set
67+
if len(path) == 0 {
68+
return nil
69+
}
70+
71+
fullPath, err := cleanPath(path)
72+
if err != nil {
73+
return err
74+
}
75+
76+
err = dirExists(fullPath)
77+
if err != nil {
78+
return err
79+
}
80+
81+
o.val = &fullPath
82+
return nil
83+
}

0 commit comments

Comments
 (0)