diff --git a/src/main/java/de/prob2/jupyter/CommandUtils.java b/src/main/java/de/prob2/jupyter/CommandUtils.java
index 786e4f16fc2518ae20f6a9e65675b65b23f2153f..35713d5e2441416d480e1798ff8788ac838d2629 100644
--- a/src/main/java/de/prob2/jupyter/CommandUtils.java
+++ b/src/main/java/de/prob2/jupyter/CommandUtils.java
@@ -89,53 +89,61 @@ public final class CommandUtils {
 		}
 	}
 	
-	private static <T> @NotNull String parseSingleArg(final @NotNull ParsedArguments parsed, final @NotNull String remainingArgs, final @NotNull PositionalParameter<T> param) {
-		final Parameter.ParseResult<T> parsedSingleArg = param.parse(remainingArgs);
-		parsed.put(param, parsedSingleArg.getParsedArg());
-		return parsedSingleArg.getRemainingArgString();
-	}
-	
-	private static <T> void checkParsedParameter(final @NotNull ParsedArguments parsed, final @NotNull PositionalParameter<T> param) {
-		if (!parsed.containsKey(param)) {
-			if (param.isOptional()) {
-				parsed.put(param, param.getDefaultValue());
-			} else {
-				throw new UserErrorException("Missing required parameter " + param.getIdentifier());
-			}
-		}
-	}
-	
-	public static @NotNull ParsedArguments parseArgs(final @NotNull Parameters parameters, final @NotNull String argString) {
-		final ParsedArguments parsed = new ParsedArguments(Collections.emptyMap());
+	public static @NotNull SplitResult splitArgs(final @NotNull Parameters parameters, final @NotNull String argString) {
+		final SplitArguments splitArgs = new SplitArguments(Collections.emptyMap());
 		String remainingArgs;
 		if (parameters.getBodyParam().isPresent()) {
-			final String[] split = argString.split("\n", 2);
-			final PositionalParameter.RequiredRemainder bodyParam = parameters.getBodyParam().get();
-			if (split.length < 2) {
-				throw new UserErrorException("Missing required body " + bodyParam.getIdentifier());
+			final String[] argsAndBody = argString.split("\n", 2);
+			remainingArgs = argsAndBody[0];
+			if (argsAndBody.length > 1) {
+				splitArgs.add(parameters.getBodyParam().get(), argsAndBody[1]);
 			}
-			remainingArgs = split[0];
-			parsed.put(bodyParam, split[1]);
 		} else {
 			remainingArgs = argString;
 		}
 		
-		for (final PositionalParameter<?> param : parameters.getPositionalParameters()) {
+		for (int i = 0; i < parameters.getPositionalParameters().size();) {
+			final PositionalParameter<?> param = parameters.getPositionalParameters().get(i);
 			if (remainingArgs.isEmpty()) {
 				break;
 			}
 			
-			remainingArgs = parseSingleArg(parsed, remainingArgs, param);
+			final Parameter.SplitResult splitSingleArg = param.split(remainingArgs);
+			splitArgs.add(param, splitSingleArg.getSplitArg());
+			remainingArgs = splitSingleArg.getRemainingArgString();
+			
+			if (!param.isRepeating()) {
+				i++;
+			}
 		}
-		if (!remainingArgs.isEmpty()) {
-			throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + remainingArgs);
+		
+		return new SplitResult(splitArgs, remainingArgs);
+	}
+	
+	private static <T> void validateSplitParameter(final @NotNull ParsedArguments parsed, final @NotNull SplitArguments splitArgs, final @NotNull Parameter<T> param) {
+		parsed.put(param, param.validate(splitArgs.get(param)));
+	}
+	
+	public static @NotNull ParsedArguments validateSplitArgs(final @NotNull Parameters parameters, final SplitResult split) {
+		if (!split.getRemaining().isEmpty()) {
+			throw new UserErrorException("Expected at most " + parameters.getPositionalParameters().size() + " arguments, got extra argument: " + split.getRemaining());
 		}
+		
+		final ParsedArguments parsed = new ParsedArguments(Collections.emptyMap());
+		
 		for (final PositionalParameter<?> param : parameters.getPositionalParameters()) {
-			checkParsedParameter(parsed, param);
+			validateSplitParameter(parsed, split.getArguments(), param);
 		}
+		
+		parameters.getBodyParam().ifPresent(bodyParam -> validateSplitParameter(parsed, split.getArguments(), bodyParam));
+		
 		return parsed;
 	}
 	
+	public static @NotNull ParsedArguments parseArgs(final @NotNull Parameters parameters, final @NotNull String argString) {
+		return validateSplitArgs(parameters, splitArgs(parameters, argString));
+	}
+	
 	public static @NotNull Map<@NotNull String, @NotNull String> parsePreferences(final @NotNull List<@NotNull String> args) {
 		final Map<String, String> preferences = new HashMap<>();
 		for (final String arg : args) {
diff --git a/src/main/java/de/prob2/jupyter/Parameter.java b/src/main/java/de/prob2/jupyter/Parameter.java
index bb1fcb93a474bfe18f1b44919d2eb60692d3a84e..f609eb13a7f6e9dfaca9bc096e55e1dafe841992 100644
--- a/src/main/java/de/prob2/jupyter/Parameter.java
+++ b/src/main/java/de/prob2/jupyter/Parameter.java
@@ -1,23 +1,25 @@
 package de.prob2.jupyter;
 
+import java.util.List;
+
 import com.google.common.base.MoreObjects;
 
 import org.jetbrains.annotations.NotNull;
 
 public abstract class Parameter<T> {
-	public static final class ParseResult<T> {
-		private final T parsedArg;
+	public static final class SplitResult {
+		private final @NotNull String splitArg;
 		private final @NotNull String remainingArgString;
 		
-		public ParseResult(final T parsedArg, final @NotNull String remainingArgString) {
+		public SplitResult(final @NotNull String splitArg, final @NotNull String remainingArgString) {
 			super();
 			
-			this.parsedArg = parsedArg;
+			this.splitArg = splitArg;
 			this.remainingArgString = remainingArgString;
 		}
 		
-		public T getParsedArg() {
-			return this.parsedArg;
+		public @NotNull String getSplitArg() {
+			return this.splitArg;
 		}
 		
 		public @NotNull String getRemainingArgString() {
@@ -37,11 +39,11 @@ public abstract class Parameter<T> {
 		return this.identifier;
 	}
 	
-	public abstract boolean isOptional();
+	public abstract boolean isRepeating();
 	
-	public abstract T getDefaultValue();
+	public abstract Parameter.SplitResult split(final @NotNull String argString);
 	
-	public abstract Parameter.ParseResult<T> parse(final @NotNull String argString);
+	public abstract T validate(final @NotNull List<@NotNull String> argValues);
 	
 	@Override
 	public String toString() {
diff --git a/src/main/java/de/prob2/jupyter/PositionalParameter.java b/src/main/java/de/prob2/jupyter/PositionalParameter.java
index 35c89887dc6bd2d487f9a8e1996c0aa25e511401..f3d448e6b19f4484879437e63c08d1db4dc4f8c9 100644
--- a/src/main/java/de/prob2/jupyter/PositionalParameter.java
+++ b/src/main/java/de/prob2/jupyter/PositionalParameter.java
@@ -1,54 +1,72 @@
 package de.prob2.jupyter;
 
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 
 import org.jetbrains.annotations.NotNull;
 
 public abstract class PositionalParameter<T> extends Parameter<T> {
-	public static final class RequiredSingle extends PositionalParameter<@NotNull String> {
-		public RequiredSingle(final @NotNull String identifier) {
+	public abstract static class ExactlyOne extends PositionalParameter<@NotNull String> {
+		protected ExactlyOne(final @NotNull String identifier) {
 			super(identifier);
 		}
 		
 		@Override
-		public boolean isOptional() {
+		public boolean isRepeating() {
 			return false;
 		}
 		
 		@Override
-		public @NotNull String getDefaultValue() {
-			throw new UnsupportedOperationException("Not an optional parameter");
+		public @NotNull String validate(final @NotNull List<@NotNull String> argValues) {
+			if (argValues.isEmpty()) {
+				throw new UserErrorException("Missing required parameter " + this.getIdentifier());
+			} else if (argValues.size() > 1) {
+				throw new AssertionError("Regular (single) required parameter " + this.getIdentifier() + " has more than one value, this should never happen!");
+			}
+			
+			return argValues.get(0);
+		}
+	}
+	
+	public abstract static class ZeroOrOne extends PositionalParameter<@NotNull Optional<String>> {
+		protected ZeroOrOne(final @NotNull String identifier) {
+			super(identifier);
+		}
+		
+		@Override
+		public boolean isRepeating() {
+			return false;
 		}
 		
 		@Override
-		public @NotNull Parameter.ParseResult<@NotNull String> parse(final @NotNull String argString) {
-			final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2);
-			return new Parameter.ParseResult<>(split[0], split.length > 1 ? split[1] : "");
+		public @NotNull Optional<String> validate(final @NotNull List<@NotNull String> argValues) {
+			if (argValues.size() > 1) {
+				throw new AssertionError("Regular (single) optional parameter " + this.getIdentifier() + " has more than one value, this should never happen!");
+			}
+			
+			return argValues.stream().findAny();
 		}
 	}
 	
-	public static final class OptionalSingle extends PositionalParameter<@NotNull Optional<String>> {
-		public OptionalSingle(final @NotNull String identifier) {
+	public static final class RequiredSingle extends PositionalParameter.ExactlyOne {
+		public RequiredSingle(final @NotNull String identifier) {
 			super(identifier);
 		}
 		
 		@Override
-		public boolean isOptional() {
-			return true;
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return splitOnce(argString);
 		}
-		
-		@Override
-		public @NotNull Optional<String> getDefaultValue() {
-			return Optional.empty();
+	}
+	
+	public static final class OptionalSingle extends PositionalParameter.ZeroOrOne {
+		public OptionalSingle(final @NotNull String identifier) {
+			super(identifier);
 		}
 		
 		@Override
-		public @NotNull Parameter.ParseResult<@NotNull Optional<String>> parse(final @NotNull String argString) {
-			final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2);
-			return new Parameter.ParseResult<>(Optional.of(split[0]), split.length > 1 ? split[1] : "");
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return splitOnce(argString);
 		}
 	}
 	
@@ -58,19 +76,22 @@ public abstract class PositionalParameter<T> extends Parameter<T> {
 		}
 		
 		@Override
-		public boolean isOptional() {
-			return false;
+		public boolean isRepeating() {
+			return true;
 		}
 		
 		@Override
-		public @NotNull List<@NotNull String> getDefaultValue() {
-			throw new UnsupportedOperationException("Not an optional parameter");
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return splitOnce(argString);
 		}
 		
 		@Override
-		public @NotNull Parameter.ParseResult<@NotNull List<@NotNull String>> parse(final @NotNull String argString) {
-			final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString);
-			return new Parameter.ParseResult<>(Arrays.asList(split), "");
+		public @NotNull List<@NotNull String> validate(final @NotNull List<@NotNull String> argValues) {
+			if (argValues.isEmpty()) {
+				throw new UserErrorException("Missing required parameter " + this.getIdentifier());
+			}
+			
+			return argValues;
 		}
 	}
 	
@@ -80,61 +101,40 @@ public abstract class PositionalParameter<T> extends Parameter<T> {
 		}
 		
 		@Override
-		public boolean isOptional() {
+		public boolean isRepeating() {
 			return true;
 		}
 		
 		@Override
-		public @NotNull List<@NotNull String> getDefaultValue() {
-			return Collections.emptyList();
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return splitOnce(argString);
 		}
 		
 		@Override
-		public @NotNull Parameter.ParseResult<@NotNull List<@NotNull String>> parse(final @NotNull String argString) {
-			final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString);
-			return new Parameter.ParseResult<>(Arrays.asList(split), "");
+		public @NotNull List<@NotNull String> validate(final @NotNull List<@NotNull String> argValues) {
+			return argValues;
 		}
 	}
 	
-	public static final class RequiredRemainder extends PositionalParameter<@NotNull String> {
+	public static final class RequiredRemainder extends PositionalParameter.ExactlyOne {
 		public RequiredRemainder(final @NotNull String identifier) {
 			super(identifier);
 		}
 		
 		@Override
-		public boolean isOptional() {
-			return false;
-		}
-		
-		@Override
-		public @NotNull String getDefaultValue() {
-			throw new UnsupportedOperationException("Not an optional parameter");
-		}
-		
-		@Override
-		public Parameter.ParseResult<@NotNull String> parse(final @NotNull String argString) {
-			return new Parameter.ParseResult<>(argString, "");
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return new Parameter.SplitResult(argString, "");
 		}
 	}
 	
-	public static final class OptionalRemainder extends PositionalParameter<@NotNull Optional<String>> {
+	public static final class OptionalRemainder extends PositionalParameter.ZeroOrOne {
 		public OptionalRemainder(final @NotNull String identifier) {
 			super(identifier);
 		}
 		
 		@Override
-		public boolean isOptional() {
-			return true;
-		}
-		
-		@Override
-		public @NotNull Optional<String> getDefaultValue() {
-			return Optional.empty();
-		}
-		
-		@Override
-		public Parameter.ParseResult<@NotNull Optional<String>> parse(final @NotNull String argString) {
-			return new Parameter.ParseResult<>(Optional.of(argString), "");
+		public @NotNull Parameter.SplitResult split(final @NotNull String argString) {
+			return new Parameter.SplitResult(argString, "");
 		}
 	}
 	
@@ -142,4 +142,9 @@ public abstract class PositionalParameter<T> extends Parameter<T> {
 		super(identifier);
 	}
 	
+	@NotNull
+	static Parameter.SplitResult splitOnce(final @NotNull String argString) {
+		final String[] split = CommandUtils.ARG_SPLIT_PATTERN.split(argString, 2);
+		return new Parameter.SplitResult(split[0], split.length > 1 ? split[1] : "");
+	}
 }
diff --git a/src/main/java/de/prob2/jupyter/SplitArguments.java b/src/main/java/de/prob2/jupyter/SplitArguments.java
new file mode 100644
index 0000000000000000000000000000000000000000..906b1fb7860efca50c9a300f3c679f8826c3db91
--- /dev/null
+++ b/src/main/java/de/prob2/jupyter/SplitArguments.java
@@ -0,0 +1,40 @@
+package de.prob2.jupyter;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.base.MoreObjects;
+
+import org.jetbrains.annotations.NotNull;
+
+public final class SplitArguments {
+	private final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values;
+	
+	public SplitArguments(final @NotNull Map<@NotNull Parameter<?>, @NotNull List<@NotNull String>> values) {
+		super();
+		
+		this.values = new HashMap<>(values);
+	}
+	
+	public boolean containsKey(final @NotNull Parameter<?> parameter) {
+		return this.values.containsKey(parameter);
+	}
+	
+	public @NotNull List<@NotNull String> get(final @NotNull Parameter<?> parameter) {
+		return this.values.getOrDefault(parameter, Collections.emptyList());
+	}
+	
+	public void add(final @NotNull Parameter<?> parameter, final String value) {
+		this.values.computeIfAbsent(parameter, p -> new ArrayList<>()).add(value);
+	}
+	
+	@Override
+	public String toString() {
+		return MoreObjects.toStringHelper(this)
+			.add("values", this.values)
+			.toString();
+	}
+}
diff --git a/src/main/java/de/prob2/jupyter/SplitResult.java b/src/main/java/de/prob2/jupyter/SplitResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..deed42c5d62fe013f2f959fba02cfdf1074d8f1b
--- /dev/null
+++ b/src/main/java/de/prob2/jupyter/SplitResult.java
@@ -0,0 +1,23 @@
+package de.prob2.jupyter;
+
+import org.jetbrains.annotations.NotNull;
+
+public final class SplitResult {
+	private final @NotNull SplitArguments arguments;
+	private final @NotNull String remaining;
+	
+	public SplitResult(final @NotNull SplitArguments arguments, final @NotNull String remaining) {
+		super();
+		
+		this.arguments = arguments;
+		this.remaining = remaining;
+	}
+	
+	public @NotNull SplitArguments getArguments() {
+		return this.arguments;
+	}
+	
+	public @NotNull String getRemaining() {
+		return this.remaining;
+	}
+}