diff --git a/commands/argument.go b/commands/argument.go index 8194c81e878..afc06e5731d 100644 --- a/commands/argument.go +++ b/commands/argument.go @@ -42,9 +42,6 @@ func FileArg(name string, required, variadic bool, description string) Argument // (`FileArg("file", ArgRequired, ArgStdin, ArgRecursive)`) func (a Argument) EnableStdin() Argument { - if a.Type == ArgString { - panic("Only FileArgs can be read from Stdin") - } a.SupportsStdin = true return a } diff --git a/commands/cli/parse.go b/commands/cli/parse.go index 1bee40df7b3..19da10785ea 100644 --- a/commands/cli/parse.go +++ b/commands/cli/parse.go @@ -56,6 +56,16 @@ func Parse(input []string, stdin *os.File, root *cmds.Command) (cmds.Request, *c } } + // This is an ugly hack to maintain our current CLI interface while fixing + // other stdin usage bugs. Let this serve as a warning, be careful about the + // choices you make, they will haunt you forever. + if len(path) == 2 && path[0] == "bootstrap" { + if (path[1] == "add" && opts["default"] == true) || + (path[1] == "rm" && opts["all"] == true) { + stdin = nil + } + } + stringArgs, fileArgs, err := parseArgs(stringVals, stdin, cmd.Arguments, recursive, hidden, root) if err != nil { return req, cmd, path, err @@ -276,6 +286,7 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi fileArgs := make(map[string]files.File) argDefIndex := 0 // the index of the current argument definition + for i := 0; i < numInputs; i++ { argDef := getArgDef(argDefIndex, argDefs) @@ -289,14 +300,17 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi } fillingVariadic := argDefIndex+1 > len(argDefs) - - if argDef.Type == cmds.ArgString { + switch argDef.Type { + case cmds.ArgString: if len(inputs) > 0 { stringArgs, inputs = append(stringArgs, inputs[0]), inputs[1:] - } else { - break + } else if stdin != nil && argDef.SupportsStdin && !fillingVariadic { + if err := printReadInfo(stdin, msgStdinInfo); err == nil { + fileArgs[stdin.Name()] = files.NewReaderFile("stdin", "", stdin, nil) + stdin = nil + } } - } else if argDef.Type == cmds.ArgFile { + case cmds.ArgFile: if len(inputs) > 0 { // treat stringArg values as file paths fpath := inputs[0] @@ -316,17 +330,13 @@ func parseArgs(inputs []string, stdin *os.File, argDefs []cmds.Argument, recursi } fileArgs[fpath] = file - } else { - if stdin != nil && argDef.SupportsStdin && - argDef.Required && !fillingVariadic { - if err := printReadInfo(stdin, msgStdinInfo); err != nil { - return nil, nil, err - } - fpath := stdin.Name() - fileArgs[fpath] = files.NewReaderFile("", fpath, stdin, nil) - } else { - break + } else if stdin != nil && argDef.SupportsStdin && + argDef.Required && !fillingVariadic { + if err := printReadInfo(stdin, msgStdinInfo); err != nil { + return nil, nil, err } + fpath := stdin.Name() + fileArgs[fpath] = files.NewReaderFile("", fpath, stdin, nil) } } @@ -411,15 +421,24 @@ func appendFile(fpath string, argDef *cmds.Argument, recursive, hidden bool) (fi // Inform the user if a file is waiting on input func printReadInfo(f *os.File, msg string) error { - fInfo, err := f.Stat() + isTty, err := isTty(f) if err != nil { - log.Error(err) return err } - if (fInfo.Mode() & os.ModeCharDevice) != 0 { + if isTty { fmt.Fprintf(os.Stderr, msg, f.Name()) } return nil } + +func isTty(f *os.File) (bool, error) { + fInfo, err := f.Stat() + if err != nil { + log.Error(err) + return false, err + } + + return (fInfo.Mode() & os.ModeCharDevice) != 0, nil +} diff --git a/commands/cli/parse_test.go b/commands/cli/parse_test.go index a1a23dc6527..5b86a3f243e 100644 --- a/commands/cli/parse_test.go +++ b/commands/cli/parse_test.go @@ -4,7 +4,6 @@ import ( "io" "io/ioutil" "os" - "path/filepath" "runtime" "strings" "testing" @@ -178,49 +177,32 @@ func TestArgumentParsing(t *testing.T) { commands.StringArg("b", true, false, "another arg"), }, }, - "FileArg": { + "stdinenabled": { Arguments: []commands.Argument{ - commands.FileArg("a", true, false, "some arg"), + commands.StringArg("a", true, true, "some arg").EnableStdin(), }, }, - "FileArg+Variadic": { - Arguments: []commands.Argument{ - commands.FileArg("a", true, true, "some arg"), - }, - }, - "FileArg+Stdin": { - Arguments: []commands.Argument{ - commands.FileArg("a", true, true, "some arg").EnableStdin(), - }, - }, - "StringArg+FileArg": { - Arguments: []commands.Argument{ - commands.StringArg("a", true, false, "some arg"), - commands.FileArg("a", true, false, "some arg"), - }, - }, - "StringArg+FileArg+Stdin": { + "stdinenabled2args": &commands.Command{ Arguments: []commands.Argument{ commands.StringArg("a", true, false, "some arg"), - commands.FileArg("a", true, true, "some arg").EnableStdin(), + commands.StringArg("b", true, true, "another arg").EnableStdin(), }, }, - "StringArg+FileArg+Variadic": { + "stdinenablednotvariadic": &commands.Command{ Arguments: []commands.Argument{ - commands.StringArg("a", true, false, "some arg"), - commands.FileArg("a", true, true, "some arg"), + commands.StringArg("a", true, false, "some arg").EnableStdin(), }, }, - "StringArg+FileArg+Variadic+Stdin": { + "stdinenablednotvariadic2args": &commands.Command{ Arguments: []commands.Argument{ commands.StringArg("a", true, false, "some arg"), - commands.FileArg("a", true, true, "some arg"), + commands.StringArg("b", true, false, "another arg").EnableStdin(), }, }, }, } - test := func(cmd words, f *os.File, exp words) { + test := func(cmd words, f *os.File, res words) { if f != nil { if _, err := f.Seek(0, os.SEEK_SET); err != nil { t.Fatal(err) @@ -230,18 +212,8 @@ func TestArgumentParsing(t *testing.T) { if err != nil { t.Errorf("Command '%v' should have passed parsing: %v", cmd, err) } - - parsedWords := make([]string, len(req.Arguments())) - copy(parsedWords, req.Arguments()) - - if files := req.Files(); files != nil { - for file, err := files.NextFile(); err != io.EOF; file, err = files.NextFile() { - parsedWords = append(parsedWords, file.FullPath()) - } - } - - if !sameWords(parsedWords, exp) { - t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, parsedWords, exp) + if !sameWords(req.Arguments(), res) { + t.Errorf("Arguments parsed from '%v' are '%v' instead of '%v'", cmd, req.Arguments(), res) } } @@ -281,52 +253,59 @@ func TestArgumentParsing(t *testing.T) { testFail([]string{"reversedoptional"}, nil, "didn't provide any args, 1 required") testFail([]string{"reversedoptional", "value1", "value2", "value3"}, nil, "provided too many args, only takes 1") - // Since FileArgs are presently stored ordered by Path, the enum string - // is used to construct a predictably ordered sequence of filenames. - tmpFile := func(t *testing.T, enum string) *os.File { - f, err := ioutil.TempFile("", enum) + // Use a temp file to simulate stdin + fileToSimulateStdin := func(t *testing.T, content string) *os.File { + fstdin, err := ioutil.TempFile("", "") if err != nil { t.Fatal(err) } - fn, err := filepath.EvalSymlinks(f.Name()) - if err != nil { - t.Fatal(err) - } - f.Close() - f, err = os.Create(fn) - if err != nil { + defer os.Remove(fstdin.Name()) + + if _, err := io.WriteString(fstdin, content); err != nil { t.Fatal(err) } - - return f + return fstdin } - file1 := tmpFile(t, "1") - file2 := tmpFile(t, "2") - file3 := tmpFile(t, "3") - defer os.Remove(file3.Name()) - defer os.Remove(file2.Name()) - defer os.Remove(file1.Name()) - - test([]string{"noarg"}, file1, []string{}) - test([]string{"FileArg", file1.Name()}, nil, []string{file1.Name()}) - test([]string{"FileArg+Variadic", file1.Name(), file2.Name()}, nil, - []string{file1.Name(), file2.Name()}) - test([]string{"FileArg+Stdin"}, file1, []string{file1.Name()}) - test([]string{"FileArg+Stdin", "-"}, file1, []string{file1.Name()}) - test([]string{"FileArg+Stdin", file1.Name(), "-"}, file2, - []string{file1.Name(), file2.Name()}) - test([]string{"StringArg+FileArg", - "foo", file1.Name()}, nil, []string{"foo", file1.Name()}) - test([]string{"StringArg+FileArg+Variadic", - "foo", file1.Name(), file2.Name()}, nil, - []string{"foo", file1.Name(), file2.Name()}) - test([]string{"StringArg+FileArg+Stdin", - "foo", file1.Name(), "-"}, file2, - []string{"foo", file1.Name(), file2.Name()}) - test([]string{"StringArg+FileArg+Variadic+Stdin", - "foo", file1.Name(), file2.Name()}, file3, - []string{"foo", file1.Name(), file2.Name()}) - test([]string{"StringArg+FileArg+Variadic+Stdin", - "foo", file1.Name(), file2.Name(), "-"}, file3, - []string{"foo", file1.Name(), file2.Name(), file3.Name()}) + + test([]string{"stdinenabled", "value1", "value2"}, nil, []string{"value1", "value2"}) + + fstdin := fileToSimulateStdin(t, "stdin1") + test([]string{"stdinenabled"}, fstdin, []string{"stdin1"}) + test([]string{"stdinenabled", "value1"}, fstdin, []string{"value1"}) + test([]string{"stdinenabled", "value1", "value2"}, fstdin, []string{"value1", "value2"}) + + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2") + test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2"}) + + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2\nstdin3") + test([]string{"stdinenabled"}, fstdin, []string{"stdin1", "stdin2", "stdin3"}) + + test([]string{"stdinenabled2args", "value1", "value2"}, nil, []string{"value1", "value2"}) + + fstdin = fileToSimulateStdin(t, "stdin1") + test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1"}) + test([]string{"stdinenabled2args", "value1", "value2"}, fstdin, []string{"value1", "value2"}) + test([]string{"stdinenabled2args", "value1", "value2", "value3"}, fstdin, []string{"value1", "value2", "value3"}) + + fstdin = fileToSimulateStdin(t, "stdin1\nstdin2") + test([]string{"stdinenabled2args", "value1"}, fstdin, []string{"value1", "stdin1", "stdin2"}) + + test([]string{"stdinenablednotvariadic", "value1"}, nil, []string{"value1"}) + + fstdin = fileToSimulateStdin(t, "stdin1") + test([]string{"stdinenablednotvariadic"}, fstdin, []string{"stdin1"}) + test([]string{"stdinenablednotvariadic", "value1"}, fstdin, []string{"value1"}) + + test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, nil, []string{"value1", "value2"}) + + fstdin = fileToSimulateStdin(t, "stdin1") + test([]string{"stdinenablednotvariadic2args", "value1"}, fstdin, []string{"value1", "stdin1"}) + test([]string{"stdinenablednotvariadic2args", "value1", "value2"}, fstdin, []string{"value1", "value2"}) + testFail([]string{"stdinenablednotvariadic2args"}, fstdin, "cant use stdin for non stdin arg") + + fstdin = fileToSimulateStdin(t, "stdin1") + test([]string{"noarg"}, fstdin, []string{}) + + fstdin = fileToSimulateStdin(t, "stdin1") + test([]string{"optionalsecond", "value1", "value2"}, fstdin, []string{"value1", "value2"}) } diff --git a/commands/command.go b/commands/command.go index d172f53e786..907e43dd08f 100644 --- a/commands/command.go +++ b/commands/command.go @@ -206,7 +206,7 @@ func (c *Command) GetOptions(path []string) (map[string]Option, error) { } func (c *Command) CheckArguments(req Request) error { - args := req.Arguments() + args := req.(*request).arguments // count required argument definitions numRequired := 0 @@ -218,7 +218,7 @@ func (c *Command) CheckArguments(req Request) error { // iterate over the arg definitions valueIndex := 0 // the index of the current value (in `args`) - for _, argDef := range c.Arguments { + for i, argDef := range c.Arguments { // skip optional argument definitions if there aren't // sufficient remaining values if len(args)-valueIndex <= numRequired && !argDef.Required || @@ -235,6 +235,11 @@ func (c *Command) CheckArguments(req Request) error { valueIndex++ } + // in the case of a non-variadic required argument that supports stdin + if !found && len(c.Arguments)-1 == i && argDef.SupportsStdin { + found = true + } + err := checkArgValue(v, found, argDef) if err != nil { return err @@ -281,6 +286,10 @@ func (c *Command) ProcessHelp() { // checkArgValue returns an error if a given arg value is not valid for the // given Argument func checkArgValue(v string, found bool, def Argument) error { + if def.Variadic && def.SupportsStdin { + return nil + } + if !found && def.Required { return fmt.Errorf("Argument '%s' is required", def.Name) } diff --git a/commands/http/client.go b/commands/http/client.go index 5db1ec05192..21e8b1b350f 100644 --- a/commands/http/client.go +++ b/commands/http/client.go @@ -128,7 +128,7 @@ func getQuery(req cmds.Request) (string, error) { query.Set(k, str) } - args := req.Arguments() + args := req.StringArguments() argDefs := req.Command().Arguments argDefIndex := 0 diff --git a/commands/reqlog.go b/commands/reqlog.go index 08701a8d53a..b1884fae325 100644 --- a/commands/reqlog.go +++ b/commands/reqlog.go @@ -56,7 +56,7 @@ func (rl *ReqLog) Add(req Request) *ReqLogEntry { Active: true, Command: strings.Join(req.Path(), "/"), Options: req.Options(), - Args: req.Arguments(), + Args: req.StringArguments(), ID: rl.nextID, req: req, log: rl, diff --git a/commands/request.go b/commands/request.go index 97909326c91..38fd5424034 100644 --- a/commands/request.go +++ b/commands/request.go @@ -1,6 +1,7 @@ package commands import ( + "bufio" "errors" "fmt" "io" @@ -70,6 +71,7 @@ type Request interface { SetOption(name string, val interface{}) SetOptions(opts OptMap) error Arguments() []string + StringArguments() []string SetArguments([]string) Files() files.File SetFiles(files.File) @@ -80,6 +82,7 @@ type Request interface { Command() *Command Values() map[string]interface{} Stdin() io.Reader + VarArgs(func(string) error) error ConvertOptions() error } @@ -166,8 +169,22 @@ func (r *request) SetOptions(opts OptMap) error { return r.ConvertOptions() } +func (r *request) StringArguments() []string { + return r.arguments +} + // Arguments returns the arguments slice func (r *request) Arguments() []string { + if r.haveVarArgsFromStdin() { + err := r.VarArgs(func(s string) error { + r.arguments = append(r.arguments, s) + return nil + }) + if err != nil && err != io.EOF { + log.Error(err) + } + } + return r.arguments } @@ -187,6 +204,51 @@ func (r *request) Context() context.Context { return r.rctx } +func (r *request) haveVarArgsFromStdin() bool { + // we expect varargs if we have a string argument that supports stdin + // and not arguments to satisfy it + if len(r.cmd.Arguments) == 0 { + return false + } + + last := r.cmd.Arguments[len(r.cmd.Arguments)-1] + return last.SupportsStdin && last.Type == ArgString && (last.Required || last.Variadic) && + len(r.arguments) < len(r.cmd.Arguments) +} + +// VarArgs can be used when you want string arguments as input +// and also want to be able to handle them in a streaming fashion +func (r *request) VarArgs(f func(string) error) error { + if len(r.arguments) >= len(r.cmd.Arguments) { + for _, arg := range r.arguments[len(r.cmd.Arguments)-1:] { + err := f(arg) + if err != nil { + return err + } + } + + return nil + } + + if r.files == nil { + return fmt.Errorf("expected more arguments from stdin") + } + + fi, err := r.files.NextFile() + if err != nil { + return err + } + + scan := bufio.NewScanner(fi) + for scan.Scan() { + err := f(scan.Text()) + if err != nil { + return err + } + } + return nil +} + func getContext(base context.Context, req Request) (context.Context, error) { tout, found, err := req.Option("timeout").String() if err != nil { diff --git a/core/commands/bitswap.go b/core/commands/bitswap.go index 4ac2441dd86..854a2a9a199 100644 --- a/core/commands/bitswap.go +++ b/core/commands/bitswap.go @@ -31,7 +31,7 @@ var unwantCmd = &cmds.Command{ Tagline: "Remove a given block from your wantlist.", }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist."), + cmds.StringArg("key", true, true, "Key(s) to remove from your wantlist.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { nd, err := req.InvocContext().GetNode() diff --git a/core/commands/block.go b/core/commands/block.go index e910e6fc74d..7a0e61a717f 100644 --- a/core/commands/block.go +++ b/core/commands/block.go @@ -55,7 +55,7 @@ on raw ipfs blocks. It outputs the following to stdout: }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."), + cmds.StringArg("key", true, false, "The base58 multihash of an existing block to stat.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { b, err := getBlockForKey(req, req.Arguments()[0]) @@ -88,7 +88,7 @@ It outputs to stdout, and is a base58 encoded multihash. }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get."), + cmds.StringArg("key", true, false, "The base58 multihash of an existing block to get.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { b, err := getBlockForKey(req, req.Arguments()[0]) diff --git a/core/commands/bootstrap.go b/core/commands/bootstrap.go index ffdcfb2ad1e..0c0e682a78c 100644 --- a/core/commands/bootstrap.go +++ b/core/commands/bootstrap.go @@ -47,20 +47,48 @@ in the bootstrap list). }, Arguments: []cmds.Argument{ - cmds.StringArg("peer", false, true, peerOptionDesc), + cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(), }, Options: []cmds.Option{ - cmds.BoolOption("default", "Add default bootstrap nodes.").Default(false), + cmds.BoolOption("default", "Add default bootstrap nodes. (Deprecated, use 'default' subcommand instead)"), + }, + Subcommands: map[string]*cmds.Command{ + "default": bootstrapAddDefaultCmd, }, Run: func(req cmds.Request, res cmds.Response) { - inputPeers, err := config.ParseBootstrapPeers(req.Arguments()) + deflt, _, err := req.Option("default").Bool() if err != nil { res.SetError(err, cmds.ErrNormal) return } + var inputPeers []config.BootstrapPeer + if deflt { + // parse separately for meaningful, correct error. + defltPeers, err := config.DefaultBootstrapPeers() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + inputPeers = defltPeers + } else { + parsedPeers, err := config.ParseBootstrapPeers(req.Arguments()) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + inputPeers = parsedPeers + } + + if len(inputPeers) == 0 { + res.SetError(errors.New("no bootstrap peers to add"), cmds.ErrClient) + return + } + r, err := fsrepo.Open(req.InvocContext().ConfigRoot) if err != nil { res.SetError(err, cmds.ErrNormal) @@ -73,29 +101,59 @@ in the bootstrap list). return } - deflt, _, err := req.Option("default").Bool() + added, err := bootstrapAdd(r, cfg, inputPeers) if err != nil { res.SetError(err, cmds.ErrNormal) return } - if deflt { - // parse separately for meaningful, correct error. - defltPeers, err := config.DefaultBootstrapPeers() - if err != nil { - res.SetError(err, cmds.ErrNormal) - return + res.SetOutput(&BootstrapOutput{config.BootstrapPeerStrings(added)}) + }, + Type: BootstrapOutput{}, + Marshalers: cmds.MarshalerMap{ + cmds.Text: func(res cmds.Response) (io.Reader, error) { + v, ok := res.Output().(*BootstrapOutput) + if !ok { + return nil, u.ErrCast() + } + + buf := new(bytes.Buffer) + if err := bootstrapWritePeers(buf, "added ", v.Peers); err != nil { + return nil, err } - inputPeers = append(inputPeers, defltPeers...) + return buf, nil + }, + }, +} + +var bootstrapAddDefaultCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Add default peers to the bootstrap list.", + ShortDescription: `Outputs a list of peers that were added (that weren't already +in the bootstrap list).`, + }, + Run: func(req cmds.Request, res cmds.Response) { + defltPeers, err := config.DefaultBootstrapPeers() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return } - if len(inputPeers) == 0 { - res.SetError(errors.New("no bootstrap peers to add"), cmds.ErrClient) + r, err := fsrepo.Open(req.InvocContext().ConfigRoot) + if err != nil { + res.SetError(err, cmds.ErrNormal) return } - added, err := bootstrapAdd(r, cfg, inputPeers) + defer r.Close() + cfg, err := r.Config() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + added, err := bootstrapAdd(r, cfg, defltPeers) if err != nil { res.SetError(err, cmds.ErrNormal) return @@ -129,13 +187,16 @@ var bootstrapRemoveCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ - cmds.StringArg("peer", false, true, peerOptionDesc), + cmds.StringArg("peer", false, true, peerOptionDesc).EnableStdin(), }, Options: []cmds.Option{ - cmds.BoolOption("all", "Remove all bootstrap peers.").Default(false), + cmds.BoolOption("all", "Remove all bootstrap peers. (Deprecated, use 'all' subcommand)"), + }, + Subcommands: map[string]*cmds.Command{ + "all": bootstrapRemoveAllCmd, }, Run: func(req cmds.Request, res cmds.Response) { - input, err := config.ParseBootstrapPeers(req.Arguments()) + all, _, err := req.Option("all").Bool() if err != nil { res.SetError(err, cmds.ErrNormal) return @@ -153,16 +214,16 @@ var bootstrapRemoveCmd = &cmds.Command{ return } - all, _, err := req.Option("all").Bool() - if err != nil { - res.SetError(err, cmds.ErrNormal) - return - } - var removed []config.BootstrapPeer if all { removed, err = bootstrapRemoveAll(r, cfg) } else { + input, perr := config.ParseBootstrapPeers(req.Arguments()) + if perr != nil { + res.SetError(perr, cmds.ErrNormal) + return + } + removed, err = bootstrapRemove(r, cfg, input) } if err != nil { @@ -187,6 +248,48 @@ var bootstrapRemoveCmd = &cmds.Command{ }, } +var bootstrapRemoveAllCmd = &cmds.Command{ + Helptext: cmds.HelpText{ + Tagline: "Removes all peers from the bootstrap list.", + ShortDescription: `Outputs the list of peers that were removed.`, + }, + + Run: func(req cmds.Request, res cmds.Response) { + r, err := fsrepo.Open(req.InvocContext().ConfigRoot) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + defer r.Close() + cfg, err := r.Config() + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + removed, err := bootstrapRemoveAll(r, cfg) + if err != nil { + res.SetError(err, cmds.ErrNormal) + return + } + + res.SetOutput(&BootstrapOutput{config.BootstrapPeerStrings(removed)}) + }, + Type: BootstrapOutput{}, + Marshalers: cmds.MarshalerMap{ + cmds.Text: func(res cmds.Response) (io.Reader, error) { + v, ok := res.Output().(*BootstrapOutput) + if !ok { + return nil, u.ErrCast() + } + + buf := new(bytes.Buffer) + err := bootstrapWritePeers(buf, "removed ", v.Peers) + return buf, err + }, + }, +} + var bootstrapListCmd = &cmds.Command{ Helptext: cmds.HelpText{ Tagline: "Show peers in the bootstrap list.", diff --git a/core/commands/cat.go b/core/commands/cat.go index 062dd78ab26..a42bc240624 100644 --- a/core/commands/cat.go +++ b/core/commands/cat.go @@ -20,7 +20,7 @@ var CatCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to be outputted."), + cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to be outputted.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { node, err := req.InvocContext().GetNode() diff --git a/core/commands/dht.go b/core/commands/dht.go index d9bc60178f2..2d1149427c4 100644 --- a/core/commands/dht.go +++ b/core/commands/dht.go @@ -459,7 +459,7 @@ NOTE: A value may not exceed 2048 bytes. Arguments: []cmds.Argument{ cmds.StringArg("key", true, false, "The key to store the value at."), - cmds.StringArg("value", true, false, "The value to store."), + cmds.StringArg("value", true, false, "The value to store.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("verbose", "v", "Print extra information.").Default(false), diff --git a/core/commands/dns.go b/core/commands/dns.go index c60fae48de8..3468aed2a19 100644 --- a/core/commands/dns.go +++ b/core/commands/dns.go @@ -44,7 +44,7 @@ The resolver can recursively resolve: }, Arguments: []cmds.Argument{ - cmds.StringArg("domain-name", true, false, "The domain-name name to resolve."), + cmds.StringArg("domain-name", true, false, "The domain-name name to resolve.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("recursive", "r", "Resolve until the result is not a DNS link.").Default(false), diff --git a/core/commands/get.go b/core/commands/get.go index 16b8cf91299..784f4ced759 100644 --- a/core/commands/get.go +++ b/core/commands/get.go @@ -37,7 +37,7 @@ may also specify the level of compression by specifying '-l=<1-9>'. }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, false, "The path to the IPFS object(s) to be outputted."), + cmds.StringArg("ipfs-path", true, false, "The path to the IPFS object(s) to be outputted.").EnableStdin(), }, Options: []cmds.Option{ cmds.StringOption("output", "o", "The path where the output should be stored."), diff --git a/core/commands/ls.go b/core/commands/ls.go index e304e678bbc..7cbefc10c7d 100644 --- a/core/commands/ls.go +++ b/core/commands/ls.go @@ -42,7 +42,7 @@ format: }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from."), + cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("headers", "v", "Print table headers (Hash, Size, Name).").Default(false), diff --git a/core/commands/object/object.go b/core/commands/object/object.go index 46fc389cb8e..0ae10a10574 100644 --- a/core/commands/object/object.go +++ b/core/commands/object/object.go @@ -78,7 +78,7 @@ is the raw data of the object. }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format."), + cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() @@ -108,7 +108,7 @@ multihash. }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format."), + cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("headers", "v", "Print table headers (Hash, Size, Name).").Default(false), @@ -179,7 +179,7 @@ This command outputs data in the following encodings: }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format."), + cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() @@ -246,7 +246,7 @@ var ObjectStatCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ - cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format."), + cmds.StringArg("key", true, false, "Key of the object to retrieve, in base58-encoded multihash format.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() diff --git a/core/commands/pin.go b/core/commands/pin.go index 1bcef79e57b..0842b35cae1 100644 --- a/core/commands/pin.go +++ b/core/commands/pin.go @@ -39,7 +39,7 @@ var addPinCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be pinned."), + cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be pinned.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("recursive", "r", "Recursively pin the object linked to by the specified object(s).").Default(true), @@ -103,7 +103,7 @@ collected if needed. (By default, recursively. Use -r=false for direct pins) }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be unpinned."), + cmds.StringArg("ipfs-path", true, true, "Path to object(s) to be unpinned.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("recursive", "r", "Recursively unpin the object linked to by the specified object(s).").Default(true), diff --git a/core/commands/ping.go b/core/commands/ping.go index ea80659f0ef..9659b2b9eb3 100644 --- a/core/commands/ping.go +++ b/core/commands/ping.go @@ -36,7 +36,7 @@ trip latency information. `, }, Arguments: []cmds.Argument{ - cmds.StringArg("peer ID", true, true, "ID of peer to be pinged."), + cmds.StringArg("peer ID", true, true, "ID of peer to be pinged.").EnableStdin(), }, Options: []cmds.Option{ cmds.IntOption("count", "n", "Number of ping messages to send.").Default(10), diff --git a/core/commands/publish.go b/core/commands/publish.go index 32fd548a18d..405aae6addf 100644 --- a/core/commands/publish.go +++ b/core/commands/publish.go @@ -47,7 +47,7 @@ Publish an to another public key (not implemented): }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, false, "IPFS path of the object to be published."), + cmds.StringArg("ipfs-path", true, false, "IPFS path of the object to be published.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("resolve", "Resolve given path before publishing.").Default(true), diff --git a/core/commands/refs.go b/core/commands/refs.go index 2da60dd2b9c..75ffffc88fd 100644 --- a/core/commands/refs.go +++ b/core/commands/refs.go @@ -46,7 +46,7 @@ NOTE: List all references recursively by using the flag '-r'. "local": RefsLocalCmd, }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "Path to the object(s) to list refs from."), + cmds.StringArg("ipfs-path", true, true, "Path to the object(s) to list refs from.").EnableStdin(), }, Options: []cmds.Option{ cmds.StringOption("format", "Emit edges with given format. Available tokens: .").Default(""), diff --git a/core/commands/resolve.go b/core/commands/resolve.go index c8eacc56677..692141a5700 100644 --- a/core/commands/resolve.go +++ b/core/commands/resolve.go @@ -56,7 +56,7 @@ Resolve the value of an IPFS DAG path: }, Arguments: []cmds.Argument{ - cmds.StringArg("name", true, false, "The name to resolve."), + cmds.StringArg("name", true, false, "The name to resolve.").EnableStdin(), }, Options: []cmds.Option{ cmds.BoolOption("recursive", "r", "Resolve until the result is an IPFS name.").Default(false), diff --git a/core/commands/swarm.go b/core/commands/swarm.go index a61a55aabf3..4703a6bfeb3 100644 --- a/core/commands/swarm.go +++ b/core/commands/swarm.go @@ -215,7 +215,7 @@ ipfs swarm connect /ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3 `, }, Arguments: []cmds.Argument{ - cmds.StringArg("address", true, true, "Address of peer to connect to."), + cmds.StringArg("address", true, true, "Address of peer to connect to.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { ctx := req.Context() @@ -283,7 +283,7 @@ it will reconnect. `, }, Arguments: []cmds.Argument{ - cmds.StringArg("address", true, true, "Address of peer to disconnect from."), + cmds.StringArg("address", true, true, "Address of peer to disconnect from.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() @@ -451,7 +451,7 @@ add your filters to the ipfs config file. `, }, Arguments: []cmds.Argument{ - cmds.StringArg("address", true, true, "Multiaddr to filter."), + cmds.StringArg("address", true, true, "Multiaddr to filter.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() @@ -523,7 +523,7 @@ remove your filters from the ipfs config file. `, }, Arguments: []cmds.Argument{ - cmds.StringArg("address", true, true, "Multiaddr filter to remove."), + cmds.StringArg("address", true, true, "Multiaddr filter to remove.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { n, err := req.InvocContext().GetNode() diff --git a/core/commands/tar.go b/core/commands/tar.go index f89a069b3ad..0744781841d 100644 --- a/core/commands/tar.go +++ b/core/commands/tar.go @@ -83,7 +83,7 @@ var tarCatCmd = &cmds.Command{ }, Arguments: []cmds.Argument{ - cmds.StringArg("path", true, false, "IPFS path of archive to export."), + cmds.StringArg("path", true, false, "IPFS path of archive to export.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { nd, err := req.InvocContext().GetNode() diff --git a/core/commands/unixfs/ls.go b/core/commands/unixfs/ls.go index ff3e9594de7..108d115c6b3 100644 --- a/core/commands/unixfs/ls.go +++ b/core/commands/unixfs/ls.go @@ -63,7 +63,7 @@ Example: }, Arguments: []cmds.Argument{ - cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from."), + cmds.StringArg("ipfs-path", true, true, "The path to the IPFS object(s) to list links from.").EnableStdin(), }, Run: func(req cmds.Request, res cmds.Response) { node, err := req.InvocContext().GetNode() diff --git a/test/sharness/t0022-init-default.sh b/test/sharness/t0022-init-default.sh index 8adb7fa7f0a..5d7d8be48e2 100755 --- a/test/sharness/t0022-init-default.sh +++ b/test/sharness/t0022-init-default.sh @@ -48,7 +48,7 @@ test_expect_success "ipfs config output looks good" ' test_cmp expected actual ' -test_launch_ipfs_daemon +test_launch_ipfs_daemon --offline test_kill_ipfs_daemon diff --git a/test/sharness/t0040-add-and-cat.sh b/test/sharness/t0040-add-and-cat.sh index b7e28d4ec3f..4ca68abe8e5 100755 --- a/test/sharness/t0040-add-and-cat.sh +++ b/test/sharness/t0040-add-and-cat.sh @@ -212,7 +212,7 @@ test_expect_success "ipfs cat output looks good" ' ' test_expect_success "ipfs cat accept hash from built input" ' - echo "$HASH" | xargs ipfs cat >actual + echo "$HASH" | ipfs cat >actual ' test_expect_success "ipfs cat output looks good" ' @@ -268,7 +268,7 @@ test_expect_success "'ipfs add' output looks good" ' ' test_expect_success "'ipfs cat' with built input succeeds" ' - echo "$HASH" | xargs ipfs cat >actual + echo "$HASH" | ipfs cat >actual ' test_expect_success "ipfs cat with built input output looks good" ' @@ -319,7 +319,7 @@ test_expect_success "'ipfs add -rn' output looks good" ' ' test_expect_success "ipfs cat accept many hashes from built input" ' - { echo "$MARS"; echo "$VENUS"; } | xargs ipfs cat >actual + { echo "$MARS"; echo "$VENUS"; } | ipfs cat >actual ' test_expect_success "ipfs cat output looks good" ' @@ -335,28 +335,9 @@ test_expect_success "ipfs cat output looks good" ' test_cmp expected actual ' -test_expect_success "ipfs cat with both arg and built input" ' - echo "$MARS" | xargs ipfs cat "$VENUS" >actual -' - -test_expect_success "ipfs cat output looks good" ' - cat mountdir/planets/venus.txt mountdir/planets/mars.txt >expected && - test_cmp expected actual -' - -test_expect_success "ipfs cat with two args and built input" ' - echo "$MARS" | xargs ipfs cat "$VENUS" "$VENUS" >actual -' - -test_expect_success "ipfs cat output looks good" ' - cat mountdir/planets/venus.txt mountdir/planets/venus.txt \ - mountdir/planets/mars.txt >expected && - test_cmp expected actual -' - test_expect_success "go-random is installed" ' - type random - ' + type random +' test_add_cat_5MB diff --git a/test/sharness/t0085-pins.sh b/test/sharness/t0085-pins.sh new file mode 100755 index 00000000000..45b61e98f52 --- /dev/null +++ b/test/sharness/t0085-pins.sh @@ -0,0 +1,52 @@ +#!/bin/sh +# +# Copyright (c) 2016 Jeromy Johnson +# MIT Licensed; see the LICENSE file in this repository. +# + +test_description="Test ipfs pinning operations" + +. lib/test-lib.sh + + +test_pins() { + test_expect_success "create some hashes" ' + HASH_A=$(echo "A" | ipfs add -q --pin=false) && + HASH_B=$(echo "B" | ipfs add -q --pin=false) && + HASH_C=$(echo "C" | ipfs add -q --pin=false) && + HASH_D=$(echo "D" | ipfs add -q --pin=false) && + HASH_E=$(echo "E" | ipfs add -q --pin=false) && + HASH_F=$(echo "F" | ipfs add -q --pin=false) && + HASH_G=$(echo "G" | ipfs add -q --pin=false) + ' + + test_expect_success "put all those hashes in a file" ' + echo $HASH_A > hashes && + echo $HASH_B >> hashes && + echo $HASH_C >> hashes && + echo $HASH_D >> hashes && + echo $HASH_E >> hashes && + echo $HASH_F >> hashes && + echo $HASH_G >> hashes + ' + + test_expect_success "pin those hashes via stdin" ' + cat hashes | ipfs pin add + ' + + test_expect_success "unpin those hashes" ' + cat hashes | ipfs pin rm + ' +} + +test_init_ipfs + +test_pins + +test_launch_ipfs_daemon --offline + +test_pins + +test_kill_ipfs_daemon + +test_done diff --git a/test/sharness/t0120-bootstrap.sh b/test/sharness/t0120-bootstrap.sh index ea310251cbd..b81c07b1538 100755 --- a/test/sharness/t0120-bootstrap.sh +++ b/test/sharness/t0120-bootstrap.sh @@ -117,6 +117,30 @@ test_bootstrap_cmd() { ' test_bootstrap_list_cmd + + test_expect_success "'ipfs bootstrap add' accepts args from stdin" ' + echo $BP1 > bpeers && + echo $BP2 >> bpeers && + echo $BP3 >> bpeers && + echo $BP4 >> bpeers && + cat bpeers | ipfs bootstrap add > add_stdin_actual + ' + + test_expect_success "output looks good" ' + test_cmp add_stdin_actual bpeers + ' + + test_bootstrap_list_cmd $BP1 $BP2 $BP3 $BP4 + + test_expect_success "'ipfs bootstrap rm' accepts args from stdin" ' + cat bpeers | ipfs bootstrap rm > rm_stdin_actual + ' + + test_expect_success "output looks good" ' + test_cmp rm_stdin_actual bpeers + ' + + test_bootstrap_list_cmd } # should work offline