@cryptotaxi247 / kubo / commits / 1aeda7eb6

ignore those last bits, this time its for real

License: MIT Signed-off-by: Jeromy <why@ipfs.io>

Jeromy committed Jul 8, 2016 at 14:35 UTC 1aeda7eb6ed21aca3e290f9e9c92c7e5bd942c84
9 files changed +229 -188
commands/cli/parse.go
+22 -6
@@ -296,10 +296,17 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi
296 stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:]
297 } else {
298 if stdin != nil && argDef.SupportsStdin && !fillingVariadic {
299 - if err := printReadInfo(stdin, msgStdinInfo); err == nil {
300 - fileArgs[stdin.Name()] = files.NewReaderFile("", stdin.Name(), stdin, nil)
301 - stdin = nil
299 + fname := ""
300 + istty, err := isTty(stdin)
301 + if err != nil {
302 + return nil, nil, err
303 + }
304 + if istty {
305 + fname = "*stdin*"
306 }
307 +
308 + fileArgs[stdin.Name()] = files.NewReaderFile(fname, "", stdin, nil)
309 + stdin = nil
310 }
311 }
312 case cmds.ArgFile:
@@ -417,15 +424,24 @@ func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (fi
424
425 // Inform the user if a file is waiting on input
426 func printReadInfo(f *os.File, msg string) error {
420 - fInfo, err := f.Stat()
427 + isTty, err := isTty(f)
428 if err != nil {
422 - log.Error(err)
429 return err
430 }
431
426 - if (fInfo.Mode() & os.ModeCharDevice) != 0 {
432 + if isTty {
433 fmt.Fprintf(os.Stderr, msg, f.Name())
434 }
435
436 return nil
437 }
438 +
439 +func isTty(f *os.File) (bool, error) {
440 + fInfo, err := f.Stat()
441 + if err != nil {
442 + log.Error(err)
443 + return false, err
444 + }
445 +
446 + return (fInfo.Mode() & os.ModeCharDevice) != 0, nil
447 +}
commands/cli/parse_test.go
+60 -81
@@ -4,7 +4,6 @@ import (
4 "io"
5 "io/ioutil"
6 "os"
7 - "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
@@ -178,49 +177,32 @@ func TestArgumentParsing(t *testing.T) {
177 commands.StringArg("b", true, false, "another arg"),
178 },
179 },
181 - "FileArg": {
180 + "stdinenabled": {
181 Arguments: []commands.Argument{
183 - commands.FileArg("a", true, false, "some arg"),
182 + commands.StringArg("a", true, true, "some arg").EnableStdin(),
183 },
184 },
186 - "FileArg+Variadic": {
187 - Arguments: []commands.Argument{
188 - commands.FileArg("a", true, true, "some arg"),
189 - },
190 - },
191 - "FileArg+Stdin": {
192 - Arguments: []commands.Argument{
193 - commands.FileArg("a", true, true, "some arg").EnableStdin(),
194 - },
195 - },
196 - "StringArg+FileArg": {
197 - Arguments: []commands.Argument{
198 - commands.StringArg("a", true, false, "some arg"),
199 - commands.FileArg("a", true, false, "some arg"),
200 - },
201 - },
202 - "StringArg+FileArg+Stdin": {
185 + "stdinenabled2args": &commands.Command{
186 Arguments: []commands.Argument{
187 commands.StringArg("a", true, false, "some arg"),
205 - commands.FileArg("a", true, true, "some arg").EnableStdin(),
188 + commands.StringArg("b", true, true, "another arg").EnableStdin(),
189 },
190 },
208 - "StringArg+FileArg+Variadic": {
191 + "stdinenablednotvariadic": &commands.Command{
192 Arguments: []commands.Argument{
210 - commands.StringArg("a", true, false, "some arg"),
211 - commands.FileArg("a", true, true, "some arg"),
193 + commands.StringArg("a", true, false, "some arg").EnableStdin(),
194 },
195 },
214 - "StringArg+FileArg+Variadic+Stdin": {
196 + "stdinenablednotvariadic2args": &commands.Command{
197 Arguments: []commands.Argument{
198 commands.StringArg("a", true, false, "some arg"),
217 - commands.FileArg("a", true, true, "some arg"),
199 + commands.StringArg("b", true, false, "another arg").EnableStdin(),
200 },
201 },
202 },
203 }
204
223 - test := func(cmd words, f *os.File, exp words) {
205 + test := func(cmd words, f *os.File, res words) {
206 if f != nil {
207 if _, err := f.Seek(0, os.SEEK_SET); err != nil {
208 t.Fatal(err)
@@ -230,18 +212,8 @@ func TestArgumentParsing(t *testing.T) {
212 if err != nil {
213 t.Errorf("Command '%v' should have passed parsing: %v", cmd, err)
214 }
233 -
234 - parsedWords := make([]string, len(req.Arguments()))
235 - copy(parsedWords, req.Arguments())
236 -
237 - if files := req.Files(); files != nil {
238 - for file, err := files.NextFile(); err != io.EOF; file, err = files.NextFile() {
239 - parsedWords = append(parsedWords, file.FullPath())
240 - }
241 - }
242 -
243 - if !sameWords(parsedWords, exp) {
244 - t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, parsedWords, exp)
215 + if !sameWords(req.Arguments(), res) {
216 + t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, req.Arguments(), res)
217 }
218 }
219
@@ -281,52 +253,59 @@ func TestArgumentParsing(t *testing.T) {
253 testFail([]string{"reversedoptional"}, nil, "didn't provide any args, 1 required")
254 testFail([]string{"reversedoptional", "value1", "value2", "value3"}, nil, "provided too many args, only takes 1")
255
284 - // Since FileArgs are presently stored ordered by Path, the enum string
285 - // is used to construct a predictably ordered sequence of filenames.
286 - tmpFile := func(t *testing.T, enum string) *os.File {
287 - f, err := ioutil.TempFile("", enum)
256 + // Use a temp file to simulate stdin
257 + fileToSimulateStdin := func(t *testing.T, content string) *os.File {
258 + fstdin, err := ioutil.TempFile("", "")
259 if err != nil {
260 t.Fatal(err)
261 }
291 - fn, err := filepath.EvalSymlinks(f.Name())
292 - if err != nil {
293 - t.Fatal(err)
294 - }
295 - f.Close()
296 - f, err = os.Create(fn)
297 - if err != nil {
262 + defer os.Remove(fstdin.Name())
263 +
264 + if _, err := io.WriteString(fstdin, content); err != nil {
265 t.Fatal(err)
266 }
300 -
301 - return f
267 + return fstdin
268 }
303 - file1 := tmpFile(t, "1")
304 - file2 := tmpFile(t, "2")
305 - file3 := tmpFile(t, "3")
306 - defer os.Remove(file3.Name())
307 - defer os.Remove(file2.Name())
308 - defer os.Remove(file1.Name())
309 -
310 - test([]string{"noarg"}, file1, []string{})
311 - test([]string{"FileArg", file1.Name()}, nil, []string{file1.Name()})
312 - test([]string{"FileArg+Variadic", file1.Name(), file2.Name()}, nil,
313 - []string{file1.Name(), file2.Name()})
314 - test([]string{"FileArg+Stdin"}, file1, []string{file1.Name()})
315 - test([]string{"FileArg+Stdin", "-"}, file1, []string{file1.Name()})
316 - test([]string{"FileArg+Stdin", file1.Name(), "-"}, file2,
317 - []string{file1.Name(), file2.Name()})
318 - test([]string{"StringArg+FileArg",
319 - "foo", file1.Name()}, nil, []string{"foo", file1.Name()})
320 - test([]string{"StringArg+FileArg+Variadic",
321 - "foo", file1.Name(), file2.Name()}, nil,
322 - []string{"foo", file1.Name(), file2.Name()})
323 - test([]string{"StringArg+FileArg+Stdin",
324 - "foo", file1.Name(), "-"}, file2,
325 - []string{"foo", file1.Name(), file2.Name()})
326 - test([]string{"StringArg+FileArg+Variadic+Stdin",
327 - "foo", file1.Name(), file2.Name()}, file3,
328 - []string{"foo", file1.Name(), file2.Name()})
329 - test([]string{"StringArg+FileArg+Variadic+Stdin",
330 - "foo", file1.Name(), file2.Name(), "-"}, file3,
331 - []string{"foo", file1.Name(), file2.Name(), file3.Name()})
269 +
270 + test([]string{"stdinenabled", "value1", "value2"}, nil, []string{"value1", "value2"})
271 +
272 + fstdin := fileToSimulateStdin(t, "stdin1")
273 + test([]string{"stdinenabled"}, fstdin, []string{"stdin1"})
274 + test([]string{"stdinenabled", "value1"}, fstdin, []string{"value1"})
275 + test([]string{"stdinenabled", "value1", "value2"}, fstdin, []string{"value1", "value2"})
276 +
277 + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
278 + test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2"})
279 +
280 + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2\nstdin3")
281 + test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2", "stdin3"})
282 +
283 + test([]string{"stdinenabled2args", "value1", "value2"}, nil, []string{"value1", "value2"})
284 +
285 + fstdin = fileToSimulateStdin(t, "stdin1")
286 + test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1"})
287 + test([]string{"stdinenabled2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
288 + test([]string{"stdinenabled2args", "value1", "value2", "value3"}, fstdin, []string{"value1", "value2", "value3"})
289 +
290 + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2")
291 + test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1", "stdin2"})
292 +
293 + test([]string{"stdinenablednotvariadic", "value1"}, nil, []string{"value1"})
294 +
295 + fstdin = fileToSimulateStdin(t, "stdin1")
296 + test([]string{"stdinenablednotvariadic"}, fstdin, []string{"stdin1"})
297 + test([]string{"stdinenablednotvariadic", "value1"}, fstdin, []string{"value1"})
298 +
299 + test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, nil, []string{"value1", "value2"})
300 +
301 + fstdin = fileToSimulateStdin(t, "stdin1")
302 + test([]string{"stdinenablednotvariadic2args", "value1"}, fstdin, []string{"value1", "stdin1"})
303 + test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, fstdin, []string{"value1", "value2"})
304 + testFail([]string{"stdinenablednotvariadic2args"}, fstdin, "cant use stdin for non stdin arg")
305 +
306 + fstdin = fileToSimulateStdin(t, "stdin1")
307 + test([]string{"noarg"}, fstdin, []string{})
308 +
309 + fstdin = fileToSimulateStdin(t, "stdin1")
310 + test([]string{"optionalsecond", "value1", "value2"}, fstdin, []string{"value1", "value2"})
311 }
commands/command.go
+7 -2
@@ -206,7 +206,7 @@ func (c *Command) GetOptions(path []string) (map[string]Option, error) {
206 }
207
208 func (c *Command) CheckArguments(req Request) error {
209 - args := req.Arguments()
209 + args := req.(*request).arguments
210
211 // count required argument definitions
212 numRequired := 0
@@ -218,7 +218,7 @@ func (c *Command) CheckArguments(req Request) error {
218
219 // iterate over the arg definitions
220 valueIndex := 0 // the index of the current value (in `args`)
221 - for _, argDef := range c.Arguments {
221 + for i, argDef := range c.Arguments {
222 // skip optional argument definitions if there aren't
223 // sufficient remaining values
224 if len(args)-valueIndex <= numRequired && !argDef.Required ||
@@ -235,6 +235,11 @@ func (c *Command) CheckArguments(req Request) error {
235 valueIndex++
236 }
237
238 + // in the case of a non-variadic required argument that supports stdin
239 + if !found && len(c.Arguments)-1 == i && argDef.SupportsStdin {
240 + found = true
241 + }
242 +
243 err := checkArgValue(v, found, argDef)
244 if err != nil {
245 return err
commands/request.go
+40 -10
@@ -170,6 +170,16 @@ func (r *request) SetOptions(opts OptMap) error {
170
171 // Arguments returns the arguments slice
172 func (r *request) Arguments() []string {
173 + if r.haveVarArgsFromStdin() {
174 + err := r.VarArgs(func(s string) error {
175 + r.arguments = append(r.arguments, s)
176 + return nil
177 + })
178 + if err != nil && err != io.EOF {
179 + log.Error(err)
180 + }
181 + }
182 +
183 return r.arguments
184 }
185
@@ -189,10 +199,22 @@ func (r *request) Context() context.Context {
199 return r.rctx
200 }
201
202 +func (r *request) haveVarArgsFromStdin() bool {
203 + // we expect varargs if we have a variadic required argument and no arguments to
204 + // fill it
205 + if len(r.cmd.Arguments) == 0 {
206 + return false
207 + }
208 +
209 + last := r.cmd.Arguments[len(r.cmd.Arguments)-1]
210 + return last.SupportsStdin && last.Type == ArgString &&
211 + len(r.arguments) < len(r.cmd.Arguments)
212 +}
213 +
214 func (r *request) VarArgs(f func(string) error) error {
215 var i int
216 for i = 0; i < len(r.cmd.Arguments); i++ {
195 - if r.cmd.Arguments[i].Variadic {
217 + if r.cmd.Arguments[i].Variadic || r.cmd.Arguments[i].SupportsStdin {
218 break
219 }
220 }
@@ -208,19 +230,27 @@ func (r *request) VarArgs(f func(string) error) error {
230
231 return nil
232 } else {
211 - fi, err := r.files.NextFile()
212 - if err != nil {
213 - return err
214 - }
215 -
216 - scan := bufio.NewScanner(fi)
217 - for scan.Scan() {
218 - err := f(scan.Text())
233 + if r.files != nil {
234 + fi, err := r.files.NextFile()
235 if err != nil {
236 return err
237 }
238 +
239 + if fi.FileName() == "*stdin*" {
240 + fmt.Fprintln(os.Stderr, "ipfs: Reading from stdin; send Ctrl-d to stop.")
241 + }
242 +
243 + scan := bufio.NewScanner(fi)
244 + for scan.Scan() {
245 + err := f(scan.Text())
246 + if err != nil {
247 + return err
248 + }
249 + }
250 + return nil
251 + } else {
252 + return fmt.Errorf("expected more arguments from stdin")
253 }
223 - return nil
254 }
255 }
256
core/commands/bitswap.go
+1 -1
@@ -31,7 +31,7 @@ var unwantCmd = &cmds.Command{
31 Tagline: "Remove a given block from your wantlist.",
32 },
33 Arguments: []cmds.Argument{
34 - cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist."),
34 + cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist.").EnableStdin(),
35 },
36 Run: func(req cmds.Request, res cmds.Response) {
37 nd, err := req.InvocContext().GetNode()
core/commands/block.go
+2 -2
@@ -55,7 +55,7 @@ on raw ipfs blocks. It outputs the following to stdout:
55 },
56
57 Arguments: []cmds.Argument{
58 - cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."),
58 + cmds.StringArg("key", true, false, "The base58 multihash of an existing block to stat.").EnableStdin(),
59 },
60 Run: func(req cmds.Request, res cmds.Response) {
61 b, err := getBlockForKey(req, req.Arguments()[0])
@@ -88,7 +88,7 @@ It outputs to stdout, and <key> is a base58 encoded multihash.
88 },
89
90 Arguments: []cmds.Argument{
91 - cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."),
91 + cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get.").EnableStdin(),
92 },
93 Run: func(req cmds.Request, res cmds.Response) {
94 b, err := getBlockForKey(req, req.Arguments()[0])
core/commands/bootstrap.go
+23 -20
@@ -47,7 +47,7 @@ in the bootstrap list).
47 },
48
49 Arguments: []cmds.Argument{
50 - cmds.StringArg("peer", false, true, peerOptionDesc),
50 + cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(),
51 },
52
53 Options: []cmds.Option{
@@ -55,30 +55,13 @@ in the bootstrap list).
55 },
56
57 Run: func(req cmds.Request, res cmds.Response) {
58 - inputPeers, err := config.ParseBootstrapPeers(req.Arguments())
59 - if err != nil {
60 - res.SetError(err, cmds.ErrNormal)
61 - return
62 - }
63 -
64 - r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
65 - if err != nil {
66 - res.SetError(err, cmds.ErrNormal)
67 - return
68 - }
69 - defer r.Close()
70 - cfg, err := r.Config()
71 - if err != nil {
72 - res.SetError(err, cmds.ErrNormal)
73 - return
74 - }
75 -
58 deflt, _, err := req.Option("default").Bool()
59 if err != nil {
60 res.SetError(err, cmds.ErrNormal)
61 return
62 }
63
64 + var inputPeers []config.BootstrapPeer
65 if deflt {
66 // parse separately for meaningful, correct error.
67 defltPeers, err := config.DefaultBootstrapPeers()
@@ -87,7 +70,15 @@ in the bootstrap list).
70 return
71 }
72
90 - inputPeers = append(inputPeers, defltPeers...)
73 + inputPeers = defltPeers
74 + } else {
75 + parsedPeers, err := config.ParseBootstrapPeers(req.Arguments())
76 + if err != nil {
77 + res.SetError(err, cmds.ErrNormal)
78 + return
79 + }
80 +
81 + inputPeers = parsedPeers
82 }
83
84 if len(inputPeers) == 0 {
@@ -95,6 +86,18 @@ in the bootstrap list).
86 return
87 }
88
89 + r, err := fsrepo.Open(req.InvocContext().ConfigRoot)
90 + if err != nil {
91 + res.SetError(err, cmds.ErrNormal)
92 + return
93 + }
94 + defer r.Close()
95 + cfg, err := r.Config()
96 + if err != nil {
97 + res.SetError(err, cmds.ErrNormal)
98 + return
99 + }
100 +
101 added, err := bootstrapAdd(r, cfg, inputPeers)
102 if err != nil {
103 res.SetError(err, cmds.ErrNormal)
core/commands/pin.go
+22 -66
@@ -61,25 +61,13 @@ var addPinCmd = &cmds.Command{
61 return
62 }
63
64 - out := make(chan interface{})
65 - go func(ctx context.Context) {
66 - defer close(out)
67 - err := req.VarArgs(func(arg string) error {
68 - added, err := corerepo.Pin(n, ctx, []string{arg}, recursive)
69 - if err != nil {
70 - return err
71 - }
72 -
73 - out <- &PinOutput{added}
74 - return nil
75 - })
64 + added, err := corerepo.Pin(n, req.Context(), req.Arguments(), recursive)
65 + if err != nil {
66 + res.SetError(err, cmds.ErrNormal)
67 + return
68 + }
69
77 - if err != nil {
78 - res.SetError(err, cmds.ErrNormal)
79 - return
80 - }
81 - }(req.Context())
82 - res.SetOutput((<-chan interface{})(out))
70 + res.SetOutput(&PinOutput{added})
71 },
72 Marshalers: cmds.MarshalerMap{
73 cmds.Text: func(res cmds.Response) (io.Reader, error) {
@@ -91,28 +79,17 @@ var addPinCmd = &cmds.Command{
79 pintype = "directly"
80 }
81
94 - marshalPinOutput := func(po *PinOutput) io.Reader {
95 - buf := new(bytes.Buffer)
96 - for _, k := range po.Pins {
97 - fmt.Fprintf(buf, "pinned %s %s\n", k, pintype)
98 - }
99 - return buf
100 - }
101 -
102 - out, ok := res.Output().(<-chan interface{})
82 + po, ok := res.Output().(*PinOutput)
83 if !ok {
84 return nil, u.ErrCast()
85 }
86
107 - marshal := func(i interface{}) (io.Reader, error) {
108 - return marshalPinOutput(i.(*PinOutput)), nil
87 + buf := new(bytes.Buffer)
88 + for _, k := range po.Pins {
89 + fmt.Fprintf(buf, "pinned %s %s\n", k, pintype)
90 }
91 + return buf, nil
92
111 - return &cmds.ChannelMarshaler{
112 - Res: res,
113 - Marshaler: marshal,
114 - Channel: out,
115 - }, nil
93 },
94 },
95 }
@@ -147,47 +124,26 @@ collected if needed. (By default, recursively. Use -r=false for direct pins)
124 return
125 }
126
150 - out := make(chan interface{})
151 - go func() {
152 - defer close(out)
153 - err = req.VarArgs(func(arg string) error {
154 - removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive)
155 - if err != nil {
156 - return err
157 - }
158 -
159 - out <- &PinOutput{removed}
160 - return nil
161 - })
162 - if err != nil {
163 - res.SetError(err, cmds.ErrNormal)
164 - return
165 - }
166 - }()
127 + removed, err := corerepo.Unpin(n, req.Context(), req.Arguments(), recursive)
128 + if err != nil {
129 + res.SetError(err, cmds.ErrNormal)
130 + return
131 + }
132
168 - res.SetOutput((<-chan interface{})(out))
133 + res.SetOutput(&PinOutput{removed})
134 },
135 Marshalers: cmds.MarshalerMap{
136 cmds.Text: func(res cmds.Response) (io.Reader, error) {
172 - outch, ok := res.Output().(<-chan interface{})
137 + added, ok := res.Output().(*PinOutput)
138 if !ok {
139 return nil, u.ErrCast()
140 }
141
177 - marshal := func(i interface{}) (io.Reader, error) {
178 - added := i.(*PinOutput)
179 - buf := new(bytes.Buffer)
180 - for _, k := range added.Pins {
181 - fmt.Fprintf(buf, "unpinned %s\n", k)
182 - }
183 - return buf, nil
142 + buf := new(bytes.Buffer)
143 + for _, k := range added.Pins {
144 + fmt.Fprintf(buf, "unpinned %s\n", k)
145 }
185 -
186 - return &cmds.ChannelMarshaler{
187 - Res: res,
188 - Marshaler: marshal,
189 - Channel: outch,
190 - }, nil
146 + return buf, nil
147 },
148 },
149 }
test/sharness/t0085-pins.sh new
+52
@@ -0,0 +1,52 @@
1 +#!/bin/sh
2 +#
3 +# Copyright (c) 2016 Jeromy Johnson
4 +# MIT Licensed; see the LICENSE file in this repository.
5 +#
6 +
7 +test_description="Test ipfs pinning operations"
8 +
9 +. lib/test-lib.sh
10 +
11 +
12 +test_pins() {
13 + test_expect_success "create some hashes" '
14 + HASH_A=$(echo "A" | ipfs add -q --pin=false) &&
15 + HASH_B=$(echo "B" | ipfs add -q --pin=false) &&
16 + HASH_C=$(echo "C" | ipfs add -q --pin=false) &&
17 + HASH_D=$(echo "D" | ipfs add -q --pin=false) &&
18 + HASH_E=$(echo "E" | ipfs add -q --pin=false) &&
19 + HASH_F=$(echo "F" | ipfs add -q --pin=false) &&
20 + HASH_G=$(echo "G" | ipfs add -q --pin=false)
21 + '
22 +
23 + test_expect_success "put all those hashes in a file" '
24 + echo $HASH_A > hashes &&
25 + echo $HASH_B >> hashes &&
26 + echo $HASH_C >> hashes &&
27 + echo $HASH_D >> hashes &&
28 + echo $HASH_E >> hashes &&
29 + echo $HASH_F >> hashes &&
30 + echo $HASH_G >> hashes
31 + '
32 +
33 + test_expect_success "pin those hashes via stdin" '
34 + cat hashes | ipfs pin add
35 + '
36 +
37 + test_expect_success "unpin those hashes" '
38 + cat hashes | ipfs pin rm
39 + '
40 +}
41 +
42 +test_init_ipfs
43 +
44 +test_pins
45 +
46 +test_launch_ipfs_daemon --offline
47 +
48 +test_pins
49 +
50 +test_kill_ipfs_daemon
51 +
52 +test_done