Skip to content

Commit dd20056

Browse files
authored
cmd/txtar-x: new command (#33)
This allows the easy extraction of files from a txtar file on the command line. Also add functionality to testscript to make it possible to set up the standard input of an execed command.
1 parent 3f553c3 commit dd20056

File tree

12 files changed

+247
-5
lines changed

12 files changed

+247
-5
lines changed

cmd/txtar-savedir/savedir.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ func usage() {
3131
os.Exit(2)
3232
}
3333

34-
const goCmd = "go"
35-
3634
func main() {
3735
os.Exit(main1())
3836
}
@@ -44,7 +42,7 @@ func main1() int {
4442
usage()
4543
}
4644

47-
log.SetPrefix("savedir: ")
45+
log.SetPrefix("txtar-savedir: ")
4846
log.SetFlags(0)
4947

5048
dir := flag.Arg(0)

cmd/txtar-savedir/script_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ func TestMain(m *testing.M) {
1414
}
1515

1616
func TestScripts(t *testing.T) {
17-
p := testscript.Params{Dir: "testdata"}
18-
testscript.Run(t, p)
17+
testscript.Run(t, testscript.Params{
18+
Dir: "testdata",
19+
})
1920
}

cmd/txtar-x/extract.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The txtar-x command extracts a txtar archive to a filesystem.
6+
//
7+
// Usage:
8+
//
9+
// txtar-x [-C root-dir] saved.txt
10+
//
11+
// See https://godoc.org/github.com/rogpeppe/go-internal/txtar for details of the format
12+
// and how to parse a txtar file.
13+
//
14+
package main
15+
16+
import (
17+
"flag"
18+
"fmt"
19+
"io/ioutil"
20+
"log"
21+
"os"
22+
"path/filepath"
23+
"strings"
24+
25+
"github.com/rogpeppe/go-internal/txtar"
26+
)
27+
28+
var (
29+
extractDir = flag.String("C", ".", "directory to extract files into")
30+
)
31+
32+
func usage() {
33+
fmt.Fprintf(os.Stderr, "usage: txtar-x [flags] [file]\n")
34+
flag.PrintDefaults()
35+
}
36+
37+
func main() {
38+
os.Exit(main1())
39+
}
40+
41+
func main1() int {
42+
flag.Usage = usage
43+
flag.Parse()
44+
if flag.NArg() > 1 {
45+
usage()
46+
return 2
47+
}
48+
log.SetPrefix("txtar-x: ")
49+
log.SetFlags(0)
50+
51+
var a *txtar.Archive
52+
if flag.NArg() == 0 {
53+
data, err := ioutil.ReadAll(os.Stdin)
54+
if err != nil {
55+
log.Printf("cannot read stdin: %v", err)
56+
return 1
57+
}
58+
a = txtar.Parse(data)
59+
} else {
60+
a1, err := txtar.ParseFile(flag.Arg(0))
61+
if err != nil {
62+
log.Print(err)
63+
return 1
64+
}
65+
a = a1
66+
}
67+
if err := extract(a); err != nil {
68+
log.Print(err)
69+
return 1
70+
}
71+
return 0
72+
}
73+
74+
func extract(a *txtar.Archive) error {
75+
for _, f := range a.Files {
76+
if err := extractFile(f); err != nil {
77+
return fmt.Errorf("cannot extract %q: %v", f.Name, err)
78+
}
79+
}
80+
return nil
81+
}
82+
83+
func extractFile(f txtar.File) error {
84+
path := filepath.Clean(filepath.FromSlash(f.Name))
85+
if isAbs(path) || strings.HasPrefix(path, ".."+string(filepath.Separator)) {
86+
return fmt.Errorf("outside parent directory")
87+
}
88+
path = filepath.Join(*extractDir, path)
89+
if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
90+
return err
91+
}
92+
// Avoid overwriting existing files by using O_EXCL.
93+
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
94+
if err != nil {
95+
return err
96+
}
97+
defer out.Close()
98+
if _, err := out.Write(f.Data); err != nil {
99+
return err
100+
}
101+
return nil
102+
}
103+
104+
func isAbs(p string) bool {
105+
// Note: under Windows, filepath.IsAbs(`\foo`) returns false,
106+
// so we need to check for that case specifically.
107+
return filepath.IsAbs(p) || strings.HasPrefix(p, string(filepath.Separator))
108+
}

cmd/txtar-x/extract_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"os"
7+
"testing"
8+
9+
"github.com/rogpeppe/go-internal/testscript"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
os.Exit(testscript.RunMain(m, map[string]func() int{
14+
"txtar-x": main1,
15+
}))
16+
}
17+
18+
func TestScripts(t *testing.T) {
19+
testscript.Run(t, testscript.Params{
20+
Dir: "testdata",
21+
Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
22+
"unquote": unquote,
23+
},
24+
})
25+
}
26+
27+
func unquote(ts *testscript.TestScript, neg bool, args []string) {
28+
if neg {
29+
ts.Fatalf("unsupported: ! unquote")
30+
}
31+
for _, arg := range args {
32+
file := ts.MkAbs(arg)
33+
data, err := ioutil.ReadFile(file)
34+
ts.Check(err)
35+
data = bytes.Replace(data, []byte("\n>"), []byte("\n"), -1)
36+
data = bytes.TrimPrefix(data, []byte(">"))
37+
err = ioutil.WriteFile(file, data, 0666)
38+
ts.Check(err)
39+
}
40+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
unquote file.txtar
2+
txtar-x -C x/y file.txtar
3+
cmp x/y/foo expect/foo
4+
cmp x/y/a/b/bar expect/a/b/bar
5+
6+
-- file.txtar --
7+
>some comment
8+
>
9+
>-- foo --
10+
>foo
11+
>-- a/b/bar --
12+
>bar
13+
-- expect/foo --
14+
foo
15+
-- expect/a/b/bar --
16+
bar
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
unquote file1.txtar file2.txtar
2+
! txtar-x file1.txtar
3+
stderr 'cannot extract "\.\./foo": outside parent directory'
4+
5+
! txtar-x file2.txtar
6+
stderr 'cannot extract "/foo": outside parent directory'
7+
8+
-- file1.txtar --
9+
>-- ../foo --
10+
>foo
11+
-- file2.txtar --
12+
>-- /foo --
13+
>foo
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
unquote file.txtar
2+
stdin file.txtar
3+
txtar-x
4+
cmp foo expect/foo
5+
cmp a/b/bar expect/a/b/bar
6+
7+
-- file.txtar --
8+
>some comment
9+
>
10+
>-- foo --
11+
>foo
12+
>-- a/b/bar --
13+
>bar
14+
-- expect/foo --
15+
foo
16+
-- expect/a/b/bar --
17+
bar

cmd/txtar-x/testdata/extract.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
unquote file.txtar
2+
txtar-x file.txtar
3+
cmp foo expect/foo
4+
cmp a/b/bar expect/a/b/bar
5+
6+
-- file.txtar --
7+
>some comment
8+
>
9+
>-- foo --
10+
>foo
11+
>-- a/b/bar --
12+
>bar
13+
-- expect/foo --
14+
foo
15+
-- expect/a/b/bar --
16+
bar

testscript/cmd.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var scriptCmds = map[string]func(*TestScript, bool, []string){
3535
"mkdir": (*TestScript).cmdMkdir,
3636
"rm": (*TestScript).cmdRm,
3737
"skip": (*TestScript).cmdSkip,
38+
"stdin": (*TestScript).cmdStdin,
3839
"stderr": (*TestScript).cmdStderr,
3940
"stdout": (*TestScript).cmdStdout,
4041
"stop": (*TestScript).cmdStop,
@@ -322,6 +323,18 @@ func (ts *TestScript) cmdSkip(neg bool, args []string) {
322323
ts.t.Skip()
323324
}
324325

326+
func (ts *TestScript) cmdStdin(neg bool, args []string) {
327+
if neg {
328+
ts.Fatalf("unsupported: ! stdin")
329+
}
330+
if len(args) != 1 {
331+
ts.Fatalf("usage: stdin filename")
332+
}
333+
data, err := ioutil.ReadFile(ts.MkAbs(args[0]))
334+
ts.Check(err)
335+
ts.stdin = string(data)
336+
}
337+
325338
// stdout checks that the last go command standard output matches a regexp.
326339
func (ts *TestScript) cmdStdout(neg bool, args []string) {
327340
scriptMatch(ts, neg, args, ts.stdout, "stdout")

testscript/doc.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ The predefined commands are:
150150
test. At the end of the test, any remaining background processes are
151151
terminated using os.Interrupt (if supported) or os.Kill.
152152
153+
Standard input can be provided using the stdin command; this will be
154+
cleared after exec has been called.
155+
153156
- [!] exists [-readonly] file...
154157
Each of the listed files or directories must (or must not) exist.
155158
If -readonly is given, the files or directories must be unwritable.
@@ -167,6 +170,9 @@ The predefined commands are:
167170
- skip [message]
168171
Mark the test skipped, including the message if given.
169172
173+
- stdin file
174+
Set the standard input for the next exec command to the contents of the given file.
175+
170176
- [!] stderr [-count=N] pattern
171177
Apply the grep command (see above) to the standard error
172178
from the most recent exec or wait command.

0 commit comments

Comments
 (0)