Repository: rsteube/carapace (go)

Issue:
add `Batch` function for parallel invocation
e.g.:
```go
carapace.Batch(ActionA(),ActionB()).Merge().ToA()
```

Pull request description:
add `Batch` function
fix #321


Patch:
diff --git a/batch.go b/batch.go
new file mode 100644
index 0000000..bb33280
--- /dev/null
+++ b/batch.go
@@ -0,0 +1,54 @@
+package carapace
+
+import "sync"
+
+type batch []Action
+type invokedBatch []InvokedAction
+
+// Batch creates a batch of Actions that can be invoked in parallel
+func Batch(actions ...Action) batch {
+	return batch(actions)
+}
+
+// Invoke invokes contained Actions of the batch using groutines
+func (b batch) Invoke(c Context) invokedBatch {
+	invokedActions := make([]InvokedAction, len(b))
+	functions := make([]func(), len(b))
+
+	for index, action := range b {
+		localIndex := index
+		localAction := action
+		functions[index] = func() {
+			invokedActions[localIndex] = localAction.Invoke(c)
+		}
+	}
+	parallelize(functions...)
+	return invokedActions
+}
+
+// Merge merges Actions of a batch
+func (b invokedBatch) Merge() InvokedAction {
+	switch len(b) {
+	case 0:
+		return ActionValues().Invoke(Context{})
+	case 1:
+		return b[0]
+	default:
+		return b[0].Merge(b[1:]...)
+	}
+}
+
+// Parallelize parallelizes the function calls (https://stackoverflow.com/a/44402936)
+func parallelize(functions ...func()) {
+	var waitGroup sync.WaitGroup
+	waitGroup.Add(len(functions))
+
+	defer waitGroup.Wait()
+
+	for _, function := range functions {
+		go func(copy func()) {
+			defer waitGroup.Done()
+			copy()
+		}(function)
+	}
+}
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index f040779..c3c9bb4 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -22,6 +22,7 @@
     - [Suffix](./carapace/invokedAction/suffix.md)
     - [ToA](./carapace/invokedAction/toA.md)
     - [ToMultiPartsA](./carapace/invokedAction/toMultiPartsA.md)
+  - [Batch](./carapace/batch.md)
   - [Cache](./carapace/cache.md)
   - [Standalone](./carapace/standalone.md)
     - [caraparse](./carapace/standalone/caraparse.md)
diff --git a/docs/src/carapace/batch.md b/docs/src/carapace/batch.md
new file mode 100644
index 0000000..921d3fb
--- /dev/null
+++ b/docs/src/carapace/batch.md
@@ -0,0 +1,13 @@
+# Batch
+
+[`Batch`](https://pkg.go.dev/github.com/rsteube/carapace#Action.Batch) bundles [callback actions](./action/actionCallback.md) so they can be [invoked](https://pkg.go.dev/github.com/rsteube/carapace#Action.Invoke) in parallel using goroutines.
+
+```go
+carapace.ActionCallback(func(c carapace.Context) carapace.Action {
+	return carapace.Batch(
+		carapace.ActionValues("A", "B"),
+		carapace.ActionValues("C", "D"),
+		carapace.ActionValues("E", "F"),
+	).Invoke(c).Merge().ToA()
+})
+```
diff --git a/docs/src/development/anatomyOfAShellCLI.md b/docs/src/development/anatomyOfAShellCLI.md
index 5bcefd2..ad2c1ae 100644
--- a/docs/src/development/anatomyOfAShellCLI.md
+++ b/docs/src/development/anatomyOfAShellCLI.md
@@ -1,6 +1,6 @@
 # Anatomy of a shell CLI
 
-See [Anatomy_of_a_shell_CLI](https://en.wikipedia.org/wiki/Command-line_interface#Anatomy_of_a_shell_CLI) for a detailed description.
+See [Anatomy_of_a_shell_CLI](https://en.wikipedia.org/wiki/Command-line_interface#Anatomy_of_a_shell_CLI) for a detailed (and accurate) description.
 
 ## Parameters
 Generally a command accepts 0..n parameters:
diff --git a/example/cmd/batch.go b/example/cmd/batch.go
new file mode 100644
index 0000000..a2ac83f
--- /dev/null
+++ b/example/cmd/batch.go
@@ -0,0 +1,26 @@
+package cmd
+
+import (
+	"github.com/rsteube/carapace"
+	"github.com/spf13/cobra"
+)
+
+var batchCmd = &cobra.Command{
+	Use:   "batch",
+	Short: "batch example",
+	Run:   func(cmd *cobra.Command, args []string) {},
+}
+
+func init() {
+	rootCmd.AddCommand(batchCmd)
+
+	carapace.Gen(batchCmd).PositionalCompletion(
+		carapace.ActionCallback(func(c carapace.Context) carapace.Action {
+			return carapace.Batch(
+				carapace.ActionValues("A", "B"),
+				carapace.ActionValues("C", "D"),
+				carapace.ActionValues("E", "F"),
+			).Invoke(c).Merge().ToA()
+		}),
+	)
+}


Test patch:
diff --git a/batch_test.go b/batch_test.go
new file mode 100644
index 0000000..5fb187b
--- /dev/null
+++ b/batch_test.go
@@ -0,0 +1,46 @@
+package carapace
+
+import (
+	"testing"
+
+	"github.com/rsteube/carapace/internal/common"
+)
+
+func TestBatch(t *testing.T) {
+	b := Batch(
+		ActionValues("A", "B"),
+		ActionValues("B", "C"),
+		ActionValues("C", "D"),
+	)
+	expected := InvokedAction{
+		rawValues: common.RawValuesFrom("A", "B", "C", "D"),
+		nospace:   false,
+		skipcache: false,
+	}
+	actual := b.Invoke(Context{}).Merge()
+	assertEqual(t, expected, actual)
+}
+
+func TestBatchSingle(t *testing.T) {
+	b := Batch(
+		ActionValues("A", "B"),
+	)
+	expected := InvokedAction{
+		rawValues: common.RawValuesFrom("A", "B"),
+		nospace:   false,
+		skipcache: false,
+	}
+	actual := b.Invoke(Context{}).Merge()
+	assertEqual(t, expected, actual)
+}
+
+func TestBatchNone(t *testing.T) {
+	b := Batch()
+	expected := InvokedAction{
+		rawValues: common.RawValuesFrom(),
+		nospace:   false,
+		skipcache: false,
+	}
+	actual := b.Invoke(Context{}).Merge()
+	assertEqual(t, expected, actual)
+}