Skip to content

Commit a760d4f

Browse files
authored
testscript: add unquote command (#60)
We add an unquote command to the standard set of commands, because it's not that uncommon a requirement. We add a `-quote` flag to txtar-savedir that will automatically quote files if needed, adding an unquote command to the comment section. Future work could add a way of quoting arbitrary data (e.g. binary data) but this should do for now.
1 parent 7b233a2 commit a760d4f

File tree

9 files changed

+234
-22
lines changed

9 files changed

+234
-22
lines changed

cmd/testscript/main_test.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package main
66

77
import (
88
"bytes"
9-
"io/ioutil"
109
"os"
1110
"os/exec"
1211
"path/filepath"
@@ -52,7 +51,6 @@ func TestScripts(t *testing.T) {
5251
return nil
5352
},
5453
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
55-
"unquote": unquote,
5654
"dropgofrompath": dropgofrompath,
5755
"setfilegoproxy": setfilegoproxy,
5856
},
@@ -63,21 +61,6 @@ func TestScripts(t *testing.T) {
6361
testscript.Run(t, p)
6462
}
6563

66-
func unquote(ts *testscript.TestScript, neg bool, args []string) {
67-
if neg {
68-
ts.Fatalf("unsupported: ! unquote")
69-
}
70-
for _, arg := range args {
71-
file := ts.MkAbs(arg)
72-
data, err := ioutil.ReadFile(file)
73-
ts.Check(err)
74-
data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1)
75-
data = bytes.TrimPrefix(data, []byte(">"))
76-
err = ioutil.WriteFile(file, data, 0666)
77-
ts.Check(err)
78-
}
79-
}
80-
8164
func dropgofrompath(ts *testscript.TestScript, neg bool, args []string) {
8265
if neg {
8366
ts.Fatalf("unsupported: ! dropgofrompath")

cmd/txtar-savedir/savedir.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
package main
1515

1616
import (
17-
"flag"
17+
"bytes"
18+
stdflag "flag"
1819
"fmt"
1920
"io/ioutil"
2021
"log"
@@ -26,20 +27,27 @@ import (
2627
"github.com/rogpeppe/go-internal/txtar"
2728
)
2829

30+
var flag = stdflag.NewFlagSet(os.Args[0], stdflag.ContinueOnError)
31+
2932
func usage() {
3033
fmt.Fprintf(os.Stderr, "usage: savedir dir >saved.txt\n")
31-
os.Exit(2)
34+
flag.PrintDefaults()
3235
}
3336

37+
var quoteFlag = flag.Bool("quote", false, "quote files that contain txtar file markers instead of failing")
38+
3439
func main() {
3540
os.Exit(main1())
3641
}
3742

3843
func main1() int {
3944
flag.Usage = usage
40-
flag.Parse()
45+
if flag.Parse(os.Args[1:]) != nil {
46+
return 2
47+
}
4148
if flag.NArg() != 1 {
4249
usage()
50+
return 2
4351
}
4452

4553
log.SetPrefix("txtar-savedir: ")
@@ -71,7 +79,27 @@ func main1() int {
7179
log.Printf("%s: ignoring invalid UTF-8 data", path)
7280
return nil
7381
}
74-
a.Files = append(a.Files, txtar.File{Name: strings.TrimPrefix(path, dir+string(filepath.Separator)), Data: data})
82+
if len(data) > 0 && !bytes.HasSuffix(data, []byte("\n")) {
83+
log.Printf("%s: adding final newline", path)
84+
data = append(data, '\n')
85+
}
86+
filename := strings.TrimPrefix(path, dir+string(filepath.Separator))
87+
if txtar.NeedsQuote(data) {
88+
if !*quoteFlag {
89+
log.Printf("%s: ignoring file with txtar marker in", path)
90+
return nil
91+
}
92+
data, err = txtar.Quote(data)
93+
if err != nil {
94+
log.Printf("%s: ignoring unquotable file: %v", path, err)
95+
return nil
96+
}
97+
a.Comment = append(a.Comment, []byte("unquote "+filename+"\n")...)
98+
}
99+
a.Files = append(a.Files, txtar.File{
100+
Name: filename,
101+
Data: data,
102+
})
75103
return nil
76104
})
77105

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
unquote blah/withsep
2+
unquote expect
3+
txtar-savedir blah
4+
stderr 'txtar-savedir: blah.withsep: ignoring file with txtar marker in'
5+
cmp stdout expect
6+
7+
-- blah/withsep --
8+
>-- separator --
9+
>foo
10+
-- blah/nosep --
11+
bar
12+
-- expect --
13+
>-- nosep --
14+
>bar
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
unquote blah/withsep
2+
unquote expect
3+
txtar-savedir -quote blah
4+
! stderr .+
5+
cmp stdout expect
6+
7+
-- blah/withsep --
8+
>-- separator --
9+
>foo
10+
-- expect --
11+
>unquote withsep
12+
>-- withsep --
13+
>>-- separator --
14+
>>foo

cmd/txtar-savedir/testdata/txtar-savedir-self.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
# define a txtar archive-generated file that is the golden output from txtar-savedir
33
# So we can't actually test the output for now, instead we just rely on a simple
44
# stdout check
5+
unquote expect
56
txtar-savedir blah
67
! stderr .+
7-
stdout '^module example.com/blah$'
8+
cmp stdout expect
89

910
-- blah/go.mod --
1011
module example.com/blah
@@ -17,3 +18,15 @@ import "fmt"
1718
func main() {
1819
fmt.Println("Hello, world!")
1920
}
21+
-- expect --
22+
>-- go.mod --
23+
>module example.com/blah
24+
>
25+
>-- main.go --
26+
>package main
27+
>
28+
>import "fmt"
29+
>
30+
>func main() {
31+
> fmt.Println("Hello, world!")
32+
>}

testscript/cmd.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"strings"
1616

1717
"github.com/rogpeppe/go-internal/internal/textutil"
18+
"github.com/rogpeppe/go-internal/txtar"
1819
)
1920

2021
// scriptCmds are the script command implementations.
@@ -34,6 +35,7 @@ var scriptCmds = map[string]func(*TestScript, bool, []string){
3435
"grep": (*TestScript).cmdGrep,
3536
"mkdir": (*TestScript).cmdMkdir,
3637
"rm": (*TestScript).cmdRm,
38+
"unquote": (*TestScript).cmdUnquote,
3739
"skip": (*TestScript).cmdSkip,
3840
"stdin": (*TestScript).cmdStdin,
3941
"stderr": (*TestScript).cmdStderr,
@@ -303,6 +305,22 @@ func (ts *TestScript) cmdMkdir(neg bool, args []string) {
303305
}
304306
}
305307

308+
// unquote unquotes files.
309+
func (ts *TestScript) cmdUnquote(neg bool, args []string) {
310+
if neg {
311+
ts.Fatalf("unsupported: ! unquote")
312+
}
313+
for _, arg := range args {
314+
file := ts.MkAbs(arg)
315+
data, err := ioutil.ReadFile(file)
316+
ts.Check(err)
317+
data, err = txtar.Unquote(data)
318+
ts.Check(err)
319+
err = ioutil.WriteFile(file, data, 0666)
320+
ts.Check(err)
321+
}
322+
}
323+
306324
// rm removes files or directories.
307325
func (ts *TestScript) cmdRm(neg bool, args []string) {
308326
if neg {

testscript/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ The predefined commands are:
166166
- mkdir path...
167167
Create the listed directories, if they do not already exists.
168168
169+
- unquote file...
170+
Rewrite each file by replacing any leading ">" characters from
171+
each line. This enables a file to contain substrings that look like
172+
txtar file markers.
173+
See also https://godoc.org/github.com/rogpeppe/go-internal/txtar#Unquote
174+
169175
- rm file...
170176
Remove the listed files or directories.
171177

txtar/archive.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ package txtar
3333

3434
import (
3535
"bytes"
36+
"errors"
3637
"fmt"
3738
"io/ioutil"
3839
"os"
3940
"path/filepath"
4041
"strings"
42+
"unicode/utf8"
4143
)
4244

4345
// An Archive is a collection of files.
@@ -89,6 +91,55 @@ func Parse(data []byte) *Archive {
8991
return a
9092
}
9193

94+
// NeedsQuote reports whether the given data needs to
95+
// be quoted before it's included as a txtar file.
96+
func NeedsQuote(data []byte) bool {
97+
_, _, after := findFileMarker(data)
98+
return after != nil
99+
}
100+
101+
// Quote quotes the data so that it can be safely stored in a txtar
102+
// file. This copes with files that contain lines that look like txtar
103+
// separators.
104+
//
105+
// The original data can be recovered with Unquote. It returns an error
106+
// if the data cannot be quoted (for example because it has no final
107+
// newline or it holds unprintable characters)
108+
func Quote(data []byte) ([]byte, error) {
109+
if len(data) == 0 {
110+
return nil, nil
111+
}
112+
if data[len(data)-1] != '\n' {
113+
return nil, errors.New("data has no final newline")
114+
}
115+
if !utf8.Valid(data) {
116+
return nil, fmt.Errorf("data contains non-UTF-8 characters")
117+
}
118+
var nd []byte
119+
prev := byte('\n')
120+
for _, b := range data {
121+
if prev == '\n' {
122+
nd = append(nd, '>')
123+
}
124+
nd = append(nd, b)
125+
prev = b
126+
}
127+
return nd, nil
128+
}
129+
130+
// Unquote unquotes data as quoted by Quote.
131+
func Unquote(data []byte) ([]byte, error) {
132+
if len(data) == 0 {
133+
return nil, nil
134+
}
135+
if data[0] != '>' || data[len(data)-1] != '\n' {
136+
return nil, errors.New("data does not appear to be quoted")
137+
}
138+
data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1)
139+
data = bytes.TrimPrefix(data, []byte(">"))
140+
return data, nil
141+
}
142+
92143
var (
93144
newlineMarker = []byte("\n-- ")
94145
marker = []byte("-- ")

txtar/archive_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,88 @@ func TestWrite(t *testing.T) {
104104
t.Fatalf("expected %v; got %v", want, err)
105105
}
106106
}
107+
108+
var unquoteErrorTests = []struct {
109+
testName string
110+
data string
111+
expectError string
112+
}{{
113+
testName: "no final newline",
114+
data: ">hello",
115+
expectError: `data does not appear to be quoted`,
116+
}, {
117+
testName: "no initial >",
118+
data: "hello\n",
119+
expectError: `data does not appear to be quoted`,
120+
}}
121+
122+
func TestUnquote(t *testing.T) {
123+
for _, test := range unquoteErrorTests {
124+
t.Run(test.testName, func(t *testing.T) {
125+
_, err := Unquote([]byte(test.data))
126+
if err == nil {
127+
t.Fatalf("unexpected success")
128+
}
129+
if err.Error() != test.expectError {
130+
t.Fatalf("unexpected error; got %q want %q", err, test.expectError)
131+
}
132+
})
133+
}
134+
}
135+
136+
var quoteTests = []struct {
137+
testName string
138+
data string
139+
expect string
140+
expectError string
141+
}{{
142+
testName: "empty",
143+
data: "",
144+
expect: "",
145+
}, {
146+
testName: "one line",
147+
data: "foo\n",
148+
expect: ">foo\n",
149+
}, {
150+
testName: "several lines",
151+
data: "foo\nbar\n-- baz --\n",
152+
expect: ">foo\n>bar\n>-- baz --\n",
153+
}, {
154+
testName: "bad data",
155+
data: "foo\xff\n",
156+
expectError: `data contains non-UTF-8 characters`,
157+
}, {
158+
testName: "no final newline",
159+
data: "foo",
160+
expectError: `data has no final newline`,
161+
}}
162+
163+
func TestQuote(t *testing.T) {
164+
for _, test := range quoteTests {
165+
t.Run(test.testName, func(t *testing.T) {
166+
got, err := Quote([]byte(test.data))
167+
if test.expectError != "" {
168+
if err == nil {
169+
t.Fatalf("unexpected success")
170+
}
171+
if err.Error() != test.expectError {
172+
t.Fatalf("unexpected error; got %q want %q", err, test.expectError)
173+
}
174+
return
175+
}
176+
if err != nil {
177+
t.Fatalf("quote error: %v", err)
178+
}
179+
if string(got) != test.expect {
180+
t.Fatalf("unexpected result; got %q want %q", got, test.expect)
181+
}
182+
orig, err := Unquote(got)
183+
if err != nil {
184+
t.Fatal(err)
185+
}
186+
if string(orig) != test.data {
187+
t.Fatalf("round trip failed; got %q want %q", orig, test.data)
188+
}
189+
})
190+
}
191+
}

0 commit comments

Comments
 (0)