You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by np...@apache.org on 2018/09/04 15:37:33 UTC

[sling-org-apache-sling-pipes] branch master updated: SLING-7678 introduce gogo capability

This is an automated email from the ASF dual-hosted git repository.

npeltier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-pipes.git


The following commit(s) were added to refs/heads/master by this push:
     new d26b373  SLING-7678 introduce gogo capability
d26b373 is described below

commit d26b37383e422fb5a2621ea42a722bff3d2fae49
Author: Nicolas Peltier <pe...@gmail.com>
AuthorDate: Tue Sep 4 17:27:19 2018 +0200

    SLING-7678 introduce gogo capability
    
    - add run, build, execute, help gogo commands,
    - use a PipeExecutor annotation to PipeBuilder API to centralize API commands in one place,
    - extract command utilities to CommandUtil class,
    - unit tests
---
 src/main/java/org/apache/sling/pipes/BasePipe.java |   9 +-
 .../java/org/apache/sling/pipes/OutputWriter.java  |  11 +
 src/main/java/org/apache/sling/pipes/Pipe.java     |   3 +
 .../java/org/apache/sling/pipes/PipeBindings.java  |   4 +-
 .../java/org/apache/sling/pipes/PipeBuilder.java   |  71 ++++
 .../java/org/apache/sling/pipes/PipeExecutor.java  |  31 ++
 src/main/java/org/apache/sling/pipes/Plumber.java  |  52 ++-
 .../java/org/apache/sling/pipes/SuperPipe.java     |   2 +
 .../sling/pipes/internal/AuthorizablePipe.java     |   1 +
 .../apache/sling/pipes/internal/CommandUtil.java   |  47 +++
 .../apache/sling/pipes/internal/GogoCommands.java  | 457 +++++++++++++++++++++
 .../apache/sling/pipes/internal/PackagePipe.java   |   4 +-
 .../sling/pipes/internal/PipeBuilderImpl.java      |  29 +-
 .../apache/sling/pipes/internal/PlumberImpl.java   |  55 +--
 .../apache/sling/pipes/internal/TraversePipe.java  |   1 +
 .../org/apache/sling/pipes/internal/WritePipe.java |   2 +
 .../sling/pipes/internal/GogoCommandsTest.java     | 122 ++++++
 17 files changed, 827 insertions(+), 74 deletions(-)

diff --git a/src/main/java/org/apache/sling/pipes/BasePipe.java b/src/main/java/org/apache/sling/pipes/BasePipe.java
index 04ebff6..6e8230e 100644
--- a/src/main/java/org/apache/sling/pipes/BasePipe.java
+++ b/src/main/java/org/apache/sling/pipes/BasePipe.java
@@ -146,6 +146,7 @@ public class BasePipe implements Pipe {
     /**
      * Get pipe's expression, instanciated or not
      * @return configured expression
+     * @throws ScriptException in case computed expression goes wrong
      */
     public String getExpr() throws ScriptException {
         return bindings.instantiateExpression(getRawExpression());
@@ -161,6 +162,7 @@ public class BasePipe implements Pipe {
     /**
      * Get pipe's path, instanciated or not
      * @return configured path (can be empty)
+     * @throws ScriptException in case computed path goes wrong
      */
     public String getPath() throws ScriptException {
         String rawPath = getRawPath();
@@ -169,7 +171,7 @@ public class BasePipe implements Pipe {
 
     /**
      * @return computed path: getPath, with relative path taken in account
-     * @throws ScriptException
+     * @throws ScriptException in case computed path goes wrong
      */
     protected String getComputedPath() throws ScriptException {
         String path = getPath();
@@ -282,9 +284,8 @@ public class BasePipe implements Pipe {
     }
 
     /**
-     *
-     * @return
-     * @throws ScriptException
+     * @return outputs of the pipe, as an iterator of resources
+     * @throws ScriptException if any exception has occured
      */
     protected Iterator<Resource> computeOutput() throws Exception {
         Resource input = getInput();
diff --git a/src/main/java/org/apache/sling/pipes/OutputWriter.java b/src/main/java/org/apache/sling/pipes/OutputWriter.java
index ef28c3c..cd14d2a 100644
--- a/src/main/java/org/apache/sling/pipes/OutputWriter.java
+++ b/src/main/java/org/apache/sling/pipes/OutputWriter.java
@@ -98,6 +98,13 @@ public abstract class OutputWriter {
     }
 
     /**
+     * @param customOutputs custom outputs
+     */
+    public void setCustomOutputs(Map<String, Object> customOutputs) {
+        this.customOutputs = customOutputs;
+    }
+
+    /**
      * Specifically init the response
      * @param response response on which to write
      */
@@ -180,4 +187,8 @@ public abstract class OutputWriter {
     public String toString() {
         return writer.toString();
     }
+
+    public Map<String, Object> getCustomOutputs() {
+        return customOutputs;
+    }
 }
diff --git a/src/main/java/org/apache/sling/pipes/Pipe.java b/src/main/java/org/apache/sling/pipes/Pipe.java
index 416fa24..8a194dc 100644
--- a/src/main/java/org/apache/sling/pipes/Pipe.java
+++ b/src/main/java/org/apache/sling/pipes/Pipe.java
@@ -85,6 +85,7 @@ public interface Pipe {
      * Get pipe current's resource *before* next execution, meaning either the
      * configured resource, either previous' pipe output resource
      * @return input, configured or previous pipe
+     * @throws ScriptException in case computed input goes wrong
      */
     Resource getInput() throws ScriptException;
 
@@ -115,11 +116,13 @@ public interface Pipe {
 
     /**
      * to be executed before output is retrieved
+     * @throws Exception in case anything goes wrong
      */
     void before() throws Exception;
 
     /**
      * to be executed before output is retrieved
+     * @throws Exception in case anything goes wrong
      */
     void after() throws Exception;
 
diff --git a/src/main/java/org/apache/sling/pipes/PipeBindings.java b/src/main/java/org/apache/sling/pipes/PipeBindings.java
index eb5826a..34bfccd 100644
--- a/src/main/java/org/apache/sling/pipes/PipeBindings.java
+++ b/src/main/java/org/apache/sling/pipes/PipeBindings.java
@@ -277,7 +277,7 @@ public class PipeBindings {
      * we implement here as a String
      * @param expr ecma like expression
      * @return String that is the result of the expression
-     * @throws ScriptException
+     * @throws ScriptException in case expression computing went wrong
      */
     public String instantiateExpression(String expr) throws ScriptException {
         return (String)evaluate(expr);
@@ -287,7 +287,7 @@ public class PipeBindings {
      * Instantiate object from expression
      * @param expr ecma expression
      * @return instantiated object
-     * @throws ScriptException
+     * @throws ScriptException in case object computing went wrong
      */
     public Object instantiateObject(String expr) throws ScriptException {
         Object result = evaluate(expr);
diff --git a/src/main/java/org/apache/sling/pipes/PipeBuilder.java b/src/main/java/org/apache/sling/pipes/PipeBuilder.java
index 6e81756..41fdc32 100644
--- a/src/main/java/org/apache/sling/pipes/PipeBuilder.java
+++ b/src/main/java/org/apache/sling/pipes/PipeBuilder.java
@@ -18,6 +18,27 @@ package org.apache.sling.pipes;
 
 import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.event.jobs.Job;
+import org.apache.sling.pipes.internal.AuthorizablePipe;
+import org.apache.sling.pipes.internal.FilterPipe;
+import org.apache.sling.pipes.internal.MovePipe;
+import org.apache.sling.pipes.internal.MultiPropertyPipe;
+import org.apache.sling.pipes.internal.NotPipe;
+import org.apache.sling.pipes.internal.PackagePipe;
+import org.apache.sling.pipes.internal.PathPipe;
+import org.apache.sling.pipes.internal.ReferencePipe;
+import org.apache.sling.pipes.internal.RemovePipe;
+import org.apache.sling.pipes.internal.TraversePipe;
+import org.apache.sling.pipes.internal.WritePipe;
+import org.apache.sling.pipes.internal.XPathPipe;
+import org.apache.sling.pipes.internal.inputstream.CsvPipe;
+import org.apache.sling.pipes.internal.inputstream.JsonPipe;
+import org.apache.sling.pipes.internal.inputstream.RegexpPipe;
+import org.apache.sling.pipes.internal.slingquery.ChildrenPipe;
+import org.apache.sling.pipes.internal.slingquery.ClosestPipe;
+import org.apache.sling.pipes.internal.slingquery.FindPipe;
+import org.apache.sling.pipes.internal.slingquery.ParentPipe;
+import org.apache.sling.pipes.internal.slingquery.ParentsPipe;
+import org.apache.sling.pipes.internal.slingquery.SiblingsPipe;
 import org.osgi.annotation.versioning.ProviderType;
 
 import java.util.Map;
@@ -39,6 +60,8 @@ public interface PipeBuilder {
      * @param expr target of the resource to move
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "mv", resourceType = MovePipe.RESOURCE_TYPE, pipeClass = MovePipe.class,
+            description = "move current resource to expr (more on https://sling.apache.org/documentation/bundles/sling-pipes/writers.html)")
     PipeBuilder mv(String expr);
 
     /**
@@ -47,6 +70,8 @@ public interface PipeBuilder {
      * @return updated instance of PipeBuilder
      * @throws IllegalAccessException in case it's called with bad configuration
      */
+    @PipeExecutor(command = "write", resourceType = WritePipe.RESOURCE_TYPE, pipeClass = WritePipe.class,
+            description = "write following key=value pairs to the current resource")
     PipeBuilder write(Object... conf) throws IllegalAccessException;
 
     /**
@@ -55,6 +80,8 @@ public interface PipeBuilder {
      * @return updated instance of PipeBuilder
      * @throws IllegalAccessException in case it's called with bad configuration
      */
+    @PipeExecutor(command = "grep", resourceType = FilterPipe.RESOURCE_TYPE, pipeClass = FilterPipe.class,
+            description = "filter current resources with following key=value pairs")
     PipeBuilder grep(Object... conf) throws IllegalAccessException;
 
     /**
@@ -63,6 +90,8 @@ public interface PipeBuilder {
      * @return updated instance of PipeBuilder
      * @throws IllegalAccessException in case it's called with bad configuration
      */
+    @PipeExecutor(command = "auth", resourceType = AuthorizablePipe.RESOURCE_TYPE, pipeClass = AuthorizablePipe.class,
+            description = "convert current resource as authorizable")
     PipeBuilder auth(Object... conf) throws IllegalAccessException;
 
     /**
@@ -70,6 +99,8 @@ public interface PipeBuilder {
      * @param expr xpath expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "xpath", resourceType = XPathPipe.RESOURCE_TYPE, pipeClass = XPathPipe.class,
+            description = "create following xpath query's result as output resources")
     PipeBuilder xpath(String expr);
 
     /**
@@ -77,6 +108,8 @@ public interface PipeBuilder {
      * @param expr sling query expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "children", resourceType = ChildrenPipe.RESOURCE_TYPE, pipeClass = ChildrenPipe.class,
+            description = "list current resource's immediate children")
     PipeBuilder children(String expr);
 
     /**
@@ -84,12 +117,16 @@ public interface PipeBuilder {
      * @param expr sling query expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "siblings", resourceType = SiblingsPipe.RESOURCE_TYPE, pipeClass = SiblingsPipe.class,
+        description = "list current resource's siblings")
     PipeBuilder siblings(String expr);
 
     /**
      * attach a rm pipe to the current context
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "rm", resourceType = RemovePipe.RESOURCE_TYPE, pipeClass =  RemovePipe.class,
+            description = "remove current resource")
     PipeBuilder rm();
 
     /**
@@ -97,6 +134,8 @@ public interface PipeBuilder {
      * @param expr csv expr or URL or path in the resource tree
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "csv", resourceType = CsvPipe.RESOURCE_TYPE, pipeClass = CsvPipe.class,
+        description = "read expr's csv and output each line in the bindings")
     PipeBuilder csv(String expr);
 
     /**
@@ -104,6 +143,8 @@ public interface PipeBuilder {
      * @param expr json expr or URL or path in the resource tree
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "json", resourceType = JsonPipe.RESOURCE_TYPE, pipeClass = JsonPipe.class,
+            description = "read expr's json array and output each object in the bindings")
     PipeBuilder json(String expr);
 
     /**
@@ -111,6 +152,8 @@ public interface PipeBuilder {
      * @param expr text expr or URL or path in the resource tree
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "egrep", resourceType = RegexpPipe.RESOURCE_TYPE, pipeClass = RegexpPipe.class,
+            description = "read expr's txt and output each found pattern in the binding")
     PipeBuilder egrep(String expr);
 
     /**
@@ -118,6 +161,8 @@ public interface PipeBuilder {
      * @param expr path to create
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "mkdir", resourceType = PathPipe.RESOURCE_TYPE, pipeClass = PathPipe.class,
+            description = "create expr path")
     PipeBuilder mkdir(String expr);
 
     /**
@@ -125,18 +170,24 @@ public interface PipeBuilder {
      * @param path pipe path
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "echo", resourceType = BasePipe.RESOURCE_TYPE, pipeClass = BasePipe.class,
+            description = "output input's path")
     PipeBuilder echo(String path);
 
     /**
      * attach a traverse pipe to the current context
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "traverse", resourceType = TraversePipe.RESOURCE_TYPE, pipeClass = TraversePipe.class,
+            description = "traverse current resource")
     PipeBuilder traverse();
 
     /**
      * attach a sling query parent pipe to the current context
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "parent", resourceType = ParentPipe.RESOURCE_TYPE, pipeClass = ParentPipe.class,
+            description = "return current's resource parent")
     PipeBuilder parent();
 
     /**
@@ -144,6 +195,8 @@ public interface PipeBuilder {
      * @param expr expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "parents", resourceType = ParentsPipe.RESOURCE_TYPE, pipeClass = ParentsPipe.class,
+            description = "return current's resource parents")
     PipeBuilder parents(String expr);
 
     /**
@@ -151,6 +204,8 @@ public interface PipeBuilder {
      * @param expr expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "closest", resourceType = ClosestPipe.RESOURCE_TYPE, pipeClass = ClosestPipe.class,
+            description = "return closest resource of the current")
     PipeBuilder closest(String expr);
 
     /**
@@ -158,6 +213,8 @@ public interface PipeBuilder {
      * @param expr expression
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "$", resourceType = FindPipe.RESOURCE_TYPE, pipeClass = FindPipe.class,
+            description = "find resource from the current, with the given expression as a parameter")
     PipeBuilder $(String expr);
 
     /**
@@ -165,6 +222,8 @@ public interface PipeBuilder {
      * @param expr reference
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "ref", resourceType = ReferencePipe.RESOURCE_TYPE, pipeClass = ReferencePipe.class,
+            description = "reference passed pipe")
     PipeBuilder ref(String expr);
 
     /**
@@ -172,6 +231,8 @@ public interface PipeBuilder {
      * @param expr path of the pipe
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "pkg", resourceType = PackagePipe.RESOURCE_TYPE, pipeClass = PackagePipe.class,
+            description = "package up current resource in given package")
     PipeBuilder pkg(String expr);
 
     /**
@@ -179,9 +240,19 @@ public interface PipeBuilder {
      * @param expr reference
      * @return updated instance of PipeBuilder
      */
+    @PipeExecutor(command = "not", resourceType = NotPipe.RESOURCE_TYPE, pipeClass = NotPipe.class,
+            description = "invert output: if input, return nothing, if no input, return single resource")
     PipeBuilder not(String expr);
 
     /**
+     * attach a multi value property pipe to the current context
+     * @return updated instance of PipeBuilder
+     */
+    @PipeExecutor(command = "mp", resourceType = MultiPropertyPipe.RESOURCE_TYPE, pipeClass = MultiPropertyPipe.class,
+            description = "read multi property, and output each value in the bindings")
+    PipeBuilder mp();
+
+    /**
      * parameterized current pipe in the context
      * @param params key value pair of parameters
      * @return updated instance of PipeBuilder
diff --git a/src/main/java/org/apache/sling/pipes/PipeExecutor.java b/src/main/java/org/apache/sling/pipes/PipeExecutor.java
new file mode 100644
index 0000000..105b3b6
--- /dev/null
+++ b/src/main/java/org/apache/sling/pipes/PipeExecutor.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.pipes;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PipeExecutor {
+    String command();
+    String resourceType();
+    String description();
+    Class pipeClass();
+}
diff --git a/src/main/java/org/apache/sling/pipes/Plumber.java b/src/main/java/org/apache/sling/pipes/Plumber.java
index 421bf66..53f695d 100644
--- a/src/main/java/org/apache/sling/pipes/Plumber.java
+++ b/src/main/java/org/apache/sling/pipes/Plumber.java
@@ -33,7 +33,9 @@ public interface Plumber {
 
     /**
      * Instantiate a pipe from the given resource and returns it
+     *
      * @param resource configuration resource
+     *
      * @return pipe instantiated from the resource, null otherwise
      */
     Pipe getPipe(Resource resource);
@@ -41,8 +43,10 @@ public interface Plumber {
 
     /**
      * Instantiate a pipe from the given resource and returns it
-     * @param resource configuration resource
+     *
+     * @param resource      configuration resource
      * @param upperBindings already set binding we want to initiate our pipe with
+     *
      * @return pipe instantiated from the resource, null otherwise
      */
     Pipe getPipe(Resource resource, PipeBindings upperBindings);
@@ -50,48 +54,59 @@ public interface Plumber {
 
     /**
      * executes in a background thread
+     *
      * @param resolver resolver used for registering the execution (id will be checked against the configuration)
-     * @param path path of the pipe to execute
+     * @param path     path of the pipe to execute
      * @param bindings additional bindings to use when executing
+     *
      * @return Job if registered, null otherwise
      */
     Job executeAsync(ResourceResolver resolver, String path, Map bindings);
 
     /**
      * executes in a background thread
-     * @param path path of the pipe to execute
+     *
+     * @param path     path of the pipe to execute
      * @param bindings additional bindings to use when executing
+     *
      * @return Job if registered, null otherwise
      */
     Job executeAsync(String path, Map bindings);
 
     /**
      * Executes a pipe at a certain path
+     *
      * @param resolver resource resolver with which pipe will be executed
-     * @param path path of a valid pipe configuration
+     * @param path     path of a valid pipe configuration
      * @param bindings bindings to add to the execution of the pipe, can be null
-     * @param writer output of the pipe
-     * @param save in case that pipe writes anything, wether the plumber should save changes or not
-     * @throws Exception in case execution fails
+     * @param writer   output of the pipe
+     * @param save     in case that pipe writes anything, wether the plumber should save changes or not
+     *
      * @return instance of <code>ExecutionResult</code>
+     *
+     * @throws Exception in case execution fails
      */
     ExecutionResult execute(ResourceResolver resolver, String path, Map bindings, OutputWriter writer, boolean save) throws Exception;
 
     /**
      * Executes a given pipe
+     *
      * @param resolver resource resolver with which pipe will be executed
-     * @param pipe pipe to execute
+     * @param pipe     pipe to execute
      * @param bindings bindings to add to the execution of the pipe, can be null
-     * @param writer output of the pipe
-     * @param save in case that pipe writes anything, wether the plumber should save changes or not
-     * @throws Exception in case execution fails
+     * @param writer   output of the pipe
+     * @param save     in case that pipe writes anything, wether the plumber should save changes or not
+     *
      * @return instance of <code>ExecutionResult</code>
+     *
+     * @throws Exception in case execution fails
      */
     ExecutionResult execute(ResourceResolver resolver, Pipe pipe, Map bindings, OutputWriter writer, boolean save) throws Exception;
 
     /**
      * Registers
-     * @param type resource type of the pipe to register
+     *
+     * @param type      resource type of the pipe to register
      * @param pipeClass class of the pipe to register
      */
     void registerPipe(String type, Class<? extends BasePipe> pipeClass);
@@ -99,29 +114,42 @@ public interface Plumber {
 
     /**
      * returns wether or not a pipe type is registered
+     *
      * @param type resource type tested
+     *
      * @return true if the type is registered, false if not
      */
     boolean isTypeRegistered(String type);
 
     /**
      * status of the pipe
+     *
      * @param pipeResource resource corresponding to the pipe
+     *
      * @return status of the pipe, can be blank, 'started' or 'finished'
      */
     String getStatus(Resource pipeResource);
 
     /**
      * Provides a builder helping quickly build and execute a pipe
+     *
      * @param resolver resource resolver that will be used for building the pipe
+     *
      * @return instance of PipeBuilder
      */
     PipeBuilder newPipe(ResourceResolver resolver);
 
     /**
      * returns true if the pipe is considered to be running
+     *
      * @param pipeResource resource corresponding to the pipe
+     *
      * @return true if still running
      */
     boolean isRunning(Resource pipeResource);
+
+    /**
+     * @return service user that has been configured for executing pipes;
+     */
+    Map getServiceUser();
 }
diff --git a/src/main/java/org/apache/sling/pipes/SuperPipe.java b/src/main/java/org/apache/sling/pipes/SuperPipe.java
index e83260b..022fbcf 100644
--- a/src/main/java/org/apache/sling/pipes/SuperPipe.java
+++ b/src/main/java/org/apache/sling/pipes/SuperPipe.java
@@ -55,11 +55,13 @@ public abstract class SuperPipe extends BasePipe {
 
     /**
      * build the subpipes pipes list
+     * @throws Exception in case one of the child building has went wrong
      */
     public abstract void buildChildren() throws Exception;
 
     /**
      * @return output of this super pipe's subpipes
+     * @throws Exception in case one of the outputs computation went wrong
      */
     protected abstract Iterator<Resource> computeSubpipesOutput() throws Exception;
 
diff --git a/src/main/java/org/apache/sling/pipes/internal/AuthorizablePipe.java b/src/main/java/org/apache/sling/pipes/internal/AuthorizablePipe.java
index 2522223..0fd4779 100644
--- a/src/main/java/org/apache/sling/pipes/internal/AuthorizablePipe.java
+++ b/src/main/java/org/apache/sling/pipes/internal/AuthorizablePipe.java
@@ -71,6 +71,7 @@ public class AuthorizablePipe extends BasePipe {
      * public constructor
      * @param plumber plumber instance
      * @param resource configuration resource
+     * @param upperBindings bindings coming from super pipe
      * @throws Exception bad configuration handling
      */
     public AuthorizablePipe(Plumber plumber, Resource resource, PipeBindings upperBindings) throws Exception {
diff --git a/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java b/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java
new file mode 100644
index 0000000..03fbfc1
--- /dev/null
+++ b/src/main/java/org/apache/sling/pipes/internal/CommandUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.pipes.internal;
+
+import java.util.Map;
+
+/**
+ * utilities for user input
+ */
+public class CommandUtil {
+    /**
+     * Checks arguments and throws exception if there is an issue
+     * @param params arguments to check
+     * @throws IllegalArgumentException exception thrown in case arguments are wrong
+     */
+    public static void checkArguments(Object... params) throws IllegalArgumentException {
+        if (params.length % 2 > 0){
+            throw new IllegalArgumentException("there should be an even number of arguments");
+        }
+    }
+
+    /**
+     * write key/value pairs into a map
+     * @param map target map
+     * @param params key/value pairs to write into the map
+     */
+    public static void writeToMap(Map map, Object... params){
+        for (int i = 0; i < params.length - 1; i += 2){
+            map.put(params[i], params[i + 1]);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java b/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
new file mode 100644
index 0000000..7c225c2
--- /dev/null
+++ b/src/main/java/org/apache/sling/pipes/internal/GogoCommands.java
@@ -0,0 +1,457 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.pipes.internal;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.pipes.ExecutionResult;
+import org.apache.sling.pipes.OutputWriter;
+import org.apache.sling.pipes.PipeBuilder;
+import org.apache.sling.pipes.PipeExecutor;
+import org.apache.sling.pipes.Plumber;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.sling.pipes.internal.CommandUtil.writeToMap;
+
+@Component(immediate = true,
+        service = GogoCommands.class,
+        property = {
+            "osgi.command.scope=pipe",
+            "osgi.command.function=build",
+            "osgi.command.function=run",
+            "osgi.command.function=execute",
+            "osgi.command.function=help"
+        })
+
+public class GogoCommands {
+    final Logger log = LoggerFactory.getLogger(GogoCommands.class);
+
+    protected final static String SEPARATOR = "/";
+    protected final static String PARAMS = "@";
+    protected final static String KEY_VALUE_SEP = "=";
+    protected final static String KEY_NAME = "name";
+    protected final static String KEY_PATH = "path";
+    protected final static String KEY_EXPR = "expr";
+
+    @Reference
+    ResourceResolverFactory factory;
+
+    @Reference
+    Plumber plumber;
+
+    Map<String, Method> methodMap;
+
+    Map<String, PipeExecutor> executorMap;
+
+    /**
+     * run command handler
+     * @param cmds string tokens coming with run command
+     * @throws Exception in case anything went wrong
+     */
+    public void run(String... cmds) throws Exception {
+        try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
+            PipeBuilder builder = parse(resolver, cmds);
+            System.out.println(builder.run());
+        }
+    }
+
+    /**
+     * build command handler
+     * @param cmds string tokens coming with build command
+     * @throws Exception in case anything went wrong
+     */
+    public void build(String... cmds) throws Exception {
+        try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
+            PipeBuilder builder = parse(resolver, cmds);
+            System.out.println(builder.build().getResource().getPath());
+        }
+    }
+
+    /**
+     * execute command handler
+     * @param path pipe path
+     * @param options string tokens coming with run command
+     * @throws Exception in case anything went wrong
+     */
+    public void execute(String path, String... options) throws Exception {
+        try (ResourceResolver resolver = factory.getServiceResourceResolver(plumber.getServiceUser())) {
+            System.out.println(executeInternal(resolver, path, options));
+        } catch(Exception e){
+            log.error("Unable to execute {}", path, e);
+            throw(e);
+        }
+    }
+
+    /**
+     * internal execution command handler
+     * @param resolver
+     * @param path
+     * @param optionTokens
+     * @return Execution results
+     * @throws Exception
+     */
+    protected ExecutionResult executeInternal(ResourceResolver resolver, String path, String... optionTokens) throws Exception {
+        Resource resource = resolver.getResource(path);
+        if (resource == null){
+            throw new IllegalArgumentException(String.format("%s resource does not exist", path));
+        }
+        Options options = getOptions(optionTokens);
+        Map bMap = null;
+        if (options.bindings != null) {
+            bMap = new HashMap();
+            writeToMap(bMap, options.bindings);
+        }
+        OutputWriter writer = new NopWriter();
+        if (options.writer != null){
+            writer = options.writer;
+        }
+        writer.starts();
+        return plumber.execute(resolver, path, bMap, writer, true);
+    }
+
+    /**
+     * help command handler
+     */
+    public void help(){
+        System.out.format("Available commands are \n- execute <path> <options>(execute a pipe already built at a given path)" +
+                                                    "\n- build (build pipe as configured in arguments)" +
+                                                    "\n- run (run pipe as configured in arguments)" +
+                                                    "\n- help (print this help)" +
+                                                    "\n\nfor pipe configured in argument, do 'pipe:<run|build|runAsync> <pipe token> (/ <pipe token> )*\n" +
+                                                    "\n a <pipe token> is <pipe> <expr|conf>? (<options>)?" +
+                                                    "\n <options> are (@ <option>)* form with <option> being either" +
+                                                    "\n\t'name pipeName' (used in bindings), " +
+                                                    "\n\t'expr pipeExpression' (when not directly as <args>)" +
+                                                    "\n\t'path pipePath' (when not directly as <args>)" +
+                                                    "\n\t'bindings key=value ...'" +
+                                                    "\n\t'outputs key=value ...'" +
+                                                    "\n and <pipe> is one of the following :\n");
+        for (Map.Entry<String, PipeExecutor> entry : getExecutorMap().entrySet()){
+            System.out.format("\t%s : %s\n", entry.getKey(), entry.getValue().description() );
+        }
+    }
+
+    /**
+     * @param resolver resource resolver with which pipe will build the pipe
+     * @param cmds list of commands for building the pipe
+     * @return PipeBuilder instance (that can be used to finalize the command)
+     * @throws InvocationTargetException can happen in case the mapping with PB api went wrong
+     * @throws IllegalAccessException can happen in case the mapping with PB api went wrong
+     */
+    protected PipeBuilder parse(ResourceResolver resolver, String...cmds) throws InvocationTargetException, IllegalAccessException {
+        PipeBuilder builder = plumber.newPipe(resolver);
+        for (Token token : parseTokens(cmds)){
+            Method method = getMethodMap().get(token.pipeKey);
+            if (method == null){
+                throw new IllegalArgumentException(token.pipeKey + " is not a valid pipe");
+            }
+            if (isExpressionExpected(method)){
+                method.invoke(builder, token.args.get(0));
+            } else if (isConfExpected(method)){
+                method.invoke(builder, (Object)keyValuesToArray(token.args));
+            } else if (isWithoutExpectedParameter(method)){
+                method.invoke(builder);
+            }
+
+            if (token.options != null){
+                token.options.writeToBuilder(builder);
+            }
+        }
+        return builder;
+
+    }
+
+    /**
+     * builds utility maps
+     */
+    protected void computeMaps(){
+        executorMap = new HashMap<>();
+        methodMap = new HashMap<>();
+        for (Method method : PipeBuilder.class.getDeclaredMethods()) {
+            PipeExecutor executor = method.getAnnotation(PipeExecutor.class);
+            if (executor != null) {
+                methodMap.put(executor.command(), method);
+                executorMap.put(executor.command(), executor);
+            }
+        }
+    }
+
+    /**
+     * @return map of command to PB api method
+     */
+    protected Map<String, Method> getMethodMap() {
+        if (methodMap == null) {
+            computeMaps();
+        }
+        return methodMap;
+    }
+
+    /**
+     * @return map of command to Annotation information around the PB api
+     */
+    protected Map<String, PipeExecutor> getExecutorMap() {
+        if (executorMap == null) {
+            computeMaps();
+        }
+        return executorMap;
+    }
+
+    /**
+     * @param method corresponding PB api
+     * @return true if the api does expect an expression (meaning a string)
+     */
+    protected boolean isExpressionExpected(Method method) {
+        return method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(String.class);
+    }
+
+    /**
+     * @param method corresponding PB api
+     * @return true if the api does expect a configuration (meaning a list of key value pairs)
+     */
+    protected boolean isConfExpected(Method method) {
+        return method.getParameterCount() == 1 && method.getParameterTypes()[0].equals(Object[].class);
+    }
+
+    /**
+     * @param method corresponding PB api
+     * @return true if the api does not expect parameters
+     */
+    protected boolean isWithoutExpectedParameter(Method method){
+        return method.getParameterCount() == 0;
+    }
+
+    /**
+     * @param o list of key value strings key1:value1,key2:value2,...
+     * @return String [] key1,value1,key2,value2,... corresponding to the pipe builder API
+     */
+    private String[] keyValuesToArray(List<String> o) {
+        List<String> args = new ArrayList<>();
+        for (String pair : o){
+            args.addAll(Arrays.asList(pair.split(KEY_VALUE_SEP)));
+        }
+        return args.toArray(new String[args.size()]);
+    }
+
+
+    /**
+     * @param commands full list of command tokens
+     * @return Token list corresponding to the string ones
+     */
+    protected List<Token> parseTokens(String... commands) {
+        List<Token> returnValue = new ArrayList();
+        Token currentToken = new Token();
+        returnValue.add(currentToken);
+        List currentList = new ArrayList();
+        for (String token : commands){
+            if (currentToken.pipeKey == null){
+                currentToken.pipeKey = token;
+            } else {
+                switch (token){
+                    case GogoCommands.SEPARATOR:
+                        finishToken(currentToken, currentList);
+                        currentList = new ArrayList();
+                        currentToken = new Token();
+                        returnValue.add(currentToken);
+                        break;
+                    case GogoCommands.PARAMS:
+                        currentToken.args = currentList;
+                        currentList = new ArrayList();
+                        currentList.add(PARAMS);
+                        break;
+                    default:
+                        currentList.add(token);
+                }
+            }
+        }
+        finishToken(currentToken, currentList);
+        return returnValue;
+    }
+
+    /**
+     * ends up processing of current token
+     * @param currentToken token being processed
+     * @param currentList list of argument that have been collected so far
+     */
+    protected void finishToken(Token currentToken, List<String> currentList){
+        if (currentToken.args != null){
+            //it means we have already parse args here, so we need to set current list as options
+            currentToken.options = getOptions(currentList);
+        } else {
+            currentToken.args = currentList;
+        }
+        log.debug("current token : {}", currentToken);
+    }
+
+    /**
+     * Pipe token, used to hold information of a "sub pipe" configuration
+     */
+    protected class Token {
+        String pipeKey;
+        List args;
+        Options options;
+
+        @Override
+        public String toString() {
+            return "Token{" +
+                    "pipeKey='" + pipeKey + '\'' +
+                    ", args=" + args +
+                    ", options=" + options +
+                    '}';
+        }
+    }
+
+    /**
+     * @param tokens array of tokens
+     * @return options from array
+     */
+    protected Options getOptions(String[] tokens){
+        return getOptions(Arrays.asList(tokens));
+    }
+
+    /**
+     * @param tokens  list of toekns
+     * @return options from token list
+     */
+    protected Options getOptions(List<String> tokens){
+        return new Options(tokens);
+    }
+
+    /**
+     * Options for a pipe execution
+     */
+    protected class Options {
+        String name;
+        String path;
+        String expr;
+        String[] bindings;
+        OutputWriter writer;
+
+        @Override
+        public String toString() {
+            return "Options{" +
+                    "name='" + name + '\'' +
+                    ", path='" + path + '\'' +
+                    ", expr='" + expr + '\'' +
+                    ", bindings=" + Arrays.toString(bindings) +
+                    ", writer=" + writer +
+                    '}';
+        }
+
+        /**
+         * Constructor
+         * @param options string list from where options will be built
+         */
+        protected Options(List<String> options){
+            Map<String, Object> optionMap = new HashMap<>();
+            String currentKey = null;
+            List<String> currentList = null;
+            for (String optionToken : options) {
+                if (PARAMS.equals(optionToken)){
+                    finishOption(currentKey, currentList, optionMap);
+                    currentList = new ArrayList<>();
+                    currentKey = null;
+                } else if (currentKey == null){
+                    currentKey = optionToken;
+                } else {
+                    currentList.add(optionToken);
+                }
+            }
+            finishOption(currentKey, currentList, optionMap);
+            for (Map.Entry<String, Object> entry : optionMap.entrySet()){
+                switch (entry.getKey()) {
+                    case "name" : {
+                        this.name = (String)entry.getValue();
+                        break;
+                    }
+                    case "path" : {
+                        this.path = (String)entry.getValue();
+                        break;
+                    }
+                    case "expr" : {
+                        this.expr = (String)entry.getValue();
+                        break;
+                    }
+                    case "with" : {
+                        this.bindings = keyValuesToArray((List<String>)entry.getValue());
+                        break;
+                    }
+                    case "outputs" : {
+                        this.writer = new JsonWriter();
+                        String[] list = keyValuesToArray((List<String>)entry.getValue());
+                        Map outputs = new HashMap();
+                        CommandUtil.writeToMap(outputs, list);
+                        this.writer.setCustomOutputs(outputs);
+                        break;
+                    }
+                    default: {
+                        throw new IllegalArgumentException(String.format("%s is an unknown option", entry.getKey()));
+                    }
+                }
+            }
+
+        }
+
+        /**
+         * wrap up current option
+         * @param currentKey option key
+         * @param currentList list being processed
+         * @param optionMap option map
+         */
+        protected void finishOption(String currentKey, List<String> currentList, Map<String, Object> optionMap){
+            if (currentList != null){
+                if (currentKey.equals(KEY_NAME) || currentKey.equals(KEY_EXPR) || currentKey.equals(KEY_PATH)) {
+                    optionMap.put(currentKey, currentList.get(0));
+                } else {
+                    optionMap.put(currentKey, currentList);
+                }
+            }
+        }
+
+        /**
+         * write options to current builder
+         * @param builder current builder
+         * @throws IllegalAccessException
+         */
+        void writeToBuilder(PipeBuilder builder) throws IllegalAccessException {
+            if (StringUtils.isNotBlank(name)){
+                builder.name(name);
+            }
+            if (StringUtils.isNotBlank(path)){
+                builder.path(path);
+            }
+            if (StringUtils.isNotBlank(expr)){
+                builder.expr(expr);
+            }
+            if (bindings != null){
+                builder.with(bindings);
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/pipes/internal/PackagePipe.java b/src/main/java/org/apache/sling/pipes/internal/PackagePipe.java
index 73e46cb..ff2ae5b 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PackagePipe.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PackagePipe.java
@@ -65,7 +65,7 @@ public class PackagePipe extends BasePipe {
      *
      * @param plumber  plumber
      * @param resource configuration resource
-     *
+     * @param upperBindings super pipe's bindings
      * @throws Exception in case configuration is not working
      */
     public PackagePipe(Plumber plumber, Resource resource, PipeBindings upperBindings) throws Exception {
@@ -105,6 +105,8 @@ public class PackagePipe extends BasePipe {
      * computes configured package based on expression configuration (either existing or creating it)
      * @throws IOException problem with binary
      * @throws RepositoryException problem with package persistence
+     * @throws IOException problem with package build
+     * @throws ScriptException problem with some expression/path compute
      */
     protected void init() throws IOException, RepositoryException, ScriptException {
         if (jcrPackage == null){
diff --git a/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java b/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
index 775e55c..cc825e7 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PipeBuilderImpl.java
@@ -51,6 +51,8 @@ import static org.apache.sling.jcr.resource.JcrResourceConstants.NT_SLING_FOLDER
 import static org.apache.sling.jcr.resource.JcrResourceConstants.NT_SLING_ORDERED_FOLDER;
 import static org.apache.sling.jcr.resource.JcrResourceConstants.SLING_RESOURCE_TYPE_PROPERTY;
 
+import static org.apache.sling.pipes.internal.CommandUtil.checkArguments;
+import static org.apache.sling.pipes.internal.CommandUtil.writeToMap;
 /**
  * Implementation of the PipeBuilder interface
  */
@@ -213,6 +215,11 @@ public class PipeBuilderImpl implements PipeBuilder {
     }
 
     @Override
+    public PipeBuilder mp() {
+        return pipe(MultiPropertyPipe.RESOURCE_TYPE);
+    }
+
+    @Override
     public PipeBuilder pkg(String expr) {
         try {
             pipeWithExpr(PackagePipe.RESOURCE_TYPE, expr).with(PackagePipe.PN_FILTERCOLLECTIONMODE, true);
@@ -238,28 +245,6 @@ public class PipeBuilderImpl implements PipeBuilder {
     }
 
     /**
-     * Checks arguments and throws exception if there is an issue
-     * @param params arguments to check
-     * @throws IllegalArgumentException exception thrown in case arguments are wrong
-     */
-    protected void checkArguments(Object... params) throws IllegalArgumentException {
-        if (params.length % 2 > 0){
-            throw new IllegalArgumentException("there should be an even number of arguments");
-        }
-    }
-
-    /**
-     * write key/value pairs into a map
-     * @param map target map
-     * @param params key/value pairs to write into the map
-     */
-    protected void writeToMap(Map map, Object... params){
-        for (int i = 0; i < params.length; i += 2){
-            map.put(params[i], params[i + 1]);
-        }
-    }
-
-    /**
      * Add some configurations to current's Step node defined by name (if null, will be step's properties)
      * @param name name of the configuration node, can be null in which case it's the subpipe itself
      * @param params key/value pair list of configuration
diff --git a/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java b/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
index 6f84674..470527e 100644
--- a/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
+++ b/src/main/java/org/apache/sling/pipes/internal/PlumberImpl.java
@@ -39,17 +39,9 @@ import org.apache.sling.pipes.OutputWriter;
 import org.apache.sling.pipes.Pipe;
 import org.apache.sling.pipes.PipeBindings;
 import org.apache.sling.pipes.PipeBuilder;
+import org.apache.sling.pipes.PipeExecutor;
 import org.apache.sling.pipes.Plumber;
 import org.apache.sling.pipes.PlumberMXBean;
-import org.apache.sling.pipes.internal.inputstream.CsvPipe;
-import org.apache.sling.pipes.internal.inputstream.JsonPipe;
-import org.apache.sling.pipes.internal.inputstream.RegexpPipe;
-import org.apache.sling.pipes.internal.slingquery.ChildrenPipe;
-import org.apache.sling.pipes.internal.slingquery.ClosestPipe;
-import org.apache.sling.pipes.internal.slingquery.FindPipe;
-import org.apache.sling.pipes.internal.slingquery.ParentPipe;
-import org.apache.sling.pipes.internal.slingquery.ParentsPipe;
-import org.apache.sling.pipes.internal.slingquery.SiblingsPipe;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Deactivate;
@@ -67,6 +59,7 @@ import javax.jcr.query.Query;
 import javax.management.MBeanServer;
 import javax.management.ObjectName;
 import java.lang.management.ManagementFactory;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
@@ -134,33 +127,29 @@ public class PlumberImpl implements Plumber, JobConsumer, PlumberMXBean {
         serviceUser = configuration.serviceUser() != null ? Collections.singletonMap(SUBSERVICE, configuration.serviceUser()) : null;
         allowedUsers = Arrays.asList(configuration.authorizedUsers());
         registry = new HashMap<>();
-        registerPipe(BasePipe.RESOURCE_TYPE, BasePipe.class);
-        registerPipe(ContainerPipe.RESOURCE_TYPE, ContainerPipe.class);
-        registerPipe(ChildrenPipe.RESOURCE_TYPE, ChildrenPipe.class);
-        registerPipe(WritePipe.RESOURCE_TYPE, WritePipe.class);
-        registerPipe(JsonPipe.RESOURCE_TYPE, JsonPipe.class);
-        registerPipe(MultiPropertyPipe.RESOURCE_TYPE, MultiPropertyPipe.class);
-        registerPipe(AuthorizablePipe.RESOURCE_TYPE, AuthorizablePipe.class);
-        registerPipe(XPathPipe.RESOURCE_TYPE, XPathPipe.class);
-        registerPipe(ReferencePipe.RESOURCE_TYPE, ReferencePipe.class);
-        registerPipe(RemovePipe.RESOURCE_TYPE, RemovePipe.class);
-        registerPipe(ParentsPipe.RESOURCE_TYPE, ParentsPipe.class);
-        registerPipe(MovePipe.RESOURCE_TYPE, MovePipe.class);
-        registerPipe(PathPipe.RESOURCE_TYPE, PathPipe.class);
-        registerPipe(FilterPipe.RESOURCE_TYPE, FilterPipe.class);
-        registerPipe(NotPipe.RESOURCE_TYPE, NotPipe.class);
-        registerPipe(TraversePipe.RESOURCE_TYPE, TraversePipe.class);
-        registerPipe(CsvPipe.RESOURCE_TYPE, CsvPipe.class);
-        registerPipe(ParentPipe.RESOURCE_TYPE, ParentPipe.class);
-        registerPipe(SiblingsPipe.RESOURCE_TYPE, SiblingsPipe.class);
-        registerPipe(ClosestPipe.RESOURCE_TYPE, ClosestPipe.class);
-        registerPipe(FindPipe.RESOURCE_TYPE, FindPipe.class);
-        registerPipe(RegexpPipe.RESOURCE_TYPE, RegexpPipe.class);
-        registerPipe(PackagePipe.RESOURCE_TYPE, PackagePipe.class);
+        registerPipes();
         toggleJmxRegistration(this, PlumberMXBean.class.getName(), true);
         refreshMonitoredPipes();
     }
 
+    /**
+     * Register all pipes declared in pipe builder
+     */
+    protected void registerPipes(){
+        registerPipe(ContainerPipe.RESOURCE_TYPE, ContainerPipe.class);
+        for (Method method : PipeBuilder.class.getDeclaredMethods()){
+            PipeExecutor executor = method.getAnnotation(PipeExecutor.class);
+            if (executor != null){
+                registerPipe(executor.resourceType(), executor.pipeClass());
+            }
+        }
+    }
+
+    @Override
+    public Map getServiceUser() {
+        return serviceUser;
+    }
+
     @Deactivate
     public void deactivate(){
         toggleJmxRegistration(null, PlumberMXBean.class.getName(), false);
@@ -441,7 +430,7 @@ public class PlumberImpl implements Plumber, JobConsumer, PlumberMXBean {
         if (serviceUser != null) {
             try (ResourceResolver resolver = factory.getServiceResourceResolver(serviceUser)) {
                 for (Iterator<Resource> resourceIterator = resolver.findResources(MONITORED_PIPES_QUERY, Query.XPATH); resourceIterator.hasNext(); ) {
-                    beans.add(new org.apache.sling.pipes.internal.PipeMonitor(this, getPipe(resourceIterator.next())));
+                    beans.add(new PipeMonitor(this, getPipe(resourceIterator.next())));
                 }
             } catch (LoginException e) {
                 log.error("unable to retrieve resolver for collecting exposed pipes", e);
diff --git a/src/main/java/org/apache/sling/pipes/internal/TraversePipe.java b/src/main/java/org/apache/sling/pipes/internal/TraversePipe.java
index 66f3bf9..42f19c0 100644
--- a/src/main/java/org/apache/sling/pipes/internal/TraversePipe.java
+++ b/src/main/java/org/apache/sling/pipes/internal/TraversePipe.java
@@ -45,6 +45,7 @@ public class TraversePipe extends BasePipe {
      *
      * @param plumber  plumber
      * @param resource configuration resource
+     * @param upperBindings super pipe's bindings
      * @throws Exception in case configuration is not working
      */
     public TraversePipe(Plumber plumber, Resource resource, PipeBindings upperBindings) throws Exception {
diff --git a/src/main/java/org/apache/sling/pipes/internal/WritePipe.java b/src/main/java/org/apache/sling/pipes/internal/WritePipe.java
index b2f1526..5e2293b 100644
--- a/src/main/java/org/apache/sling/pipes/internal/WritePipe.java
+++ b/src/main/java/org/apache/sling/pipes/internal/WritePipe.java
@@ -55,6 +55,7 @@ public class WritePipe extends BasePipe {
      * public constructor
      * @param plumber plumber instance
      * @param resource configuration resource
+     * @param upperBindings super pipe's bindings
      * @throws Exception bad configuration handling
      */
     public WritePipe(Plumber plumber, Resource resource, PipeBindings upperBindings) throws Exception {
@@ -80,6 +81,7 @@ public class WritePipe extends BasePipe {
      * @param key property to which value will be written
      * @param expression configured value to write
      * @return actual value to write to the resource
+     * @throws ScriptException in case value computation went wrong
      */
     protected Object computeValue(Resource resource, String key, String expression) throws ScriptException {
         Object value = bindings.instantiateObject((String) expression);
diff --git a/src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java b/src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java
new file mode 100644
index 0000000..ef838af
--- /dev/null
+++ b/src/test/java/org/apache/sling/pipes/internal/GogoCommandsTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.pipes.internal;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.pipes.AbstractPipeTest;
+import org.apache.sling.pipes.ExecutionResult;
+import org.apache.sling.pipes.PipeBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.*;
+
+public class GogoCommandsTest extends AbstractPipeTest {
+
+    GogoCommands commands;
+
+    @Before
+    public void setup() throws PersistenceException {
+        super.setup();
+        commands = new GogoCommands();
+        commands.plumber = plumber;
+    }
+
+    @Test
+    public void testParseTokens(){
+        List<GogoCommands.Token> tokens = commands.parseTokens("some", "isolated", "items");
+        assertEquals("there should be 1 token", 1, tokens.size());
+        GogoCommands.Token token = tokens.get(0);
+        assertEquals("pipe key should be 'some'","some", token.pipeKey);
+        assertEquals("pipe args should be isolated, items", Arrays.asList("isolated","items"), token.args);
+        String tokenString = "first arg / second firstarg secondarg @ name second / third blah";
+        tokens = commands.parseTokens(tokenString.split("\\s"));
+        assertEquals("there should be 3 tokens", 3, tokens.size());
+        assertEquals("keys check", Arrays.asList("first","second", "third"), tokens.stream().map(t -> t.pipeKey).collect(Collectors.toList()));
+        assertEquals("params check", "second", tokens.get(1).options.name);
+    }
+
+    @Test
+    public void testSimpleExpression() throws Exception {
+        PipeBuilder builder = commands.parse(context.resourceResolver(),"echo","/content/fruits");
+        assertTrue("there should be a resource", builder.build().getOutput().hasNext());
+    }
+
+    @Test
+    public void testSimpleChainedConf() throws Exception {
+        PipeBuilder builder = commands.parse(context.resourceResolver(),"echo /content/fruits / write some=test key=value".split("\\s"));
+        assertNotNull("there should be a resource", builder.run());
+        ValueMap props = context.currentResource(PATH_FRUITS).getValueMap();
+        assertEquals("there should some=test", "test", props.get("some"));
+        assertEquals("there should key=value", "value", props.get("key"));
+    }
+
+    @Test
+    public void testOptions() {
+        String expected = "works";
+        String optionString = "@ name works @ path works @ expr works @ with one=works two=works @ outputs one=works two=works";
+        GogoCommands.Options options = commands.getOptions(optionString.split("\\s"));
+        assertEquals("check name", expected, options.name);
+        assertEquals("check expr", expected, options.expr);
+        assertEquals("check path", expected, options.path);
+        Map bindings = new HashMap();
+        CommandUtil.writeToMap(bindings, options.bindings);
+        assertEquals("check bindings first", expected, bindings.get("one"));
+        assertEquals("check bindings second", expected, bindings.get("two"));
+        assertNotNull("a writer should have been created", options.writer);
+        Map outputs = options.writer.getCustomOutputs();
+        assertEquals("check writer first", expected, outputs.get("one"));
+        assertEquals("check writer second", expected, outputs.get("two"));
+    }
+
+    @Test
+    public void testOptionsListsWithOneItem() {
+        String expected = "works";
+        String optionString = "@ with one=works @ outputs one=works";
+        GogoCommands.Options options = commands.getOptions(optionString.split("\\s"));
+        Map bindings = new HashMap();
+        CommandUtil.writeToMap(bindings, options.bindings);
+        assertEquals("check bindings first", expected, bindings.get("one"));
+        assertNotNull("a writer should have been created", options.writer);
+        Map outputs = options.writer.getCustomOutputs();
+        assertEquals("check writer first", expected, outputs.get("one"));
+    }
+
+    @Test
+    public void testChainedConfWithInternalOptions() throws Exception {
+        PipeBuilder builder = commands.parse(context.resourceResolver(),
+        "echo /content/fruits @ name fruits / write some=${path.fruits} key=value".split("\\s"));
+        assertNotNull("there should be a resource", builder.run());
+        ValueMap props = context.currentResource(PATH_FRUITS).getValueMap();
+        assertEquals("there should some=/content/fruits", PATH_FRUITS, props.get("some"));
+        assertEquals("there should key=value", "value", props.get("key"));
+    }
+
+    @Test
+    public void testExecuteWithWriter() throws Exception {
+        PipeBuilder builder = plumber.newPipe(context.resourceResolver()).echo("/content/${node}").$("nt:base");
+        String path = builder.build().getResource().getPath();
+        ExecutionResult result = commands.executeInternal(context.resourceResolver(), path, "@ outputs title=jcr:title desc=jcr:description @ with node=fruits");
+    }
+}
\ No newline at end of file