diff --git a/README.rst b/README.rst
index 3143042d74263c271f5028cdb28d114df2a8f9e1..e15f6cebecf24e4fdc79b7f0af29060e894edf0c 100644
--- a/README.rst
+++ b/README.rst
@@ -17,6 +17,8 @@ EMBL utilities
 
 ``qs`` - collect queue status across different SGE clusters/queues. Requires key-based authentication with SSH.
 
+``mv_keep_parents`` - equivalent to ``cp --parents src/*.txt dest/`` but moving instead of copying. Optionally keeping source directories if empty with option ``-k``.
+
 Examples
 ========
 
diff --git a/bin/mv_keep_parents b/bin/mv_keep_parents
new file mode 100755
index 0000000000000000000000000000000000000000..82a6f362bd439449f256c2f85c0e556391471c61
--- /dev/null
+++ b/bin/mv_keep_parents
@@ -0,0 +1,127 @@
+#!/usr/bin/env bash
+
+ERROR="ERROR:"
+KEEPDIRS=0
+VERBOSE=0
+DRYRUN=""
+
+usage() {
+    echo >&2 ""
+    echo >&2 "Usage:"
+    echo >&2 "  $0 [-k] source1 [source2 [...]] destination"
+    echo >&2 ""
+    echo >&2 "Will move files from all specified sources to destination"
+    echo >&2 "while preserving the folder structure of each source"
+    echo >&2 ""
+    echo >&2 "Options:"
+    echo >&2 "  -k --keepdirs = keep source directories even if empty. Defaults to removing if empty"
+    echo >&2 "  -d --dryrun   = simulate execution without performing actions on the filesystem"
+    echo >&2 "                  echoes most actions that would be performed"
+    echo >&2 "                  removal of empty directories isn't simulated"
+    echo >&2 "  -v --verbose  = echo verbose info while executing. Defaults to non-verbose execution"
+    echo >&2 ""
+    echo >&2 "Example:"
+    echo >&2 "  $0 path/to/*.txt destination  ->  destination/path/to/*.txt"
+    echo >&2 ""
+}
+
+move_preserve_path() {
+    parent="$(dirname "$1")"
+
+    $DRYRUN mkdir -p "$2/$parent"
+    $DRYRUN mv "$1" "$2/$parent"
+
+    if [ "$3" -eq 0 ]; then
+        empty_dir_remove_recurse "$parent"
+    fi
+}
+
+empty_dir_remove_recurse() {
+    # If the dir to remove is the current, don't even try
+    if [ "$1" == "." ] || [ "$1" == "$(pwd)" ]; then
+        return 0
+    fi
+
+    # Remove original directory if empty (recurses path backwards)
+    if [ -z "$(ls -A "$1")" ]; then
+        $DRYRUN rmdir "$1"
+
+        empty_dir_remove_recurse "$(dirname "$1")"
+    fi
+}
+
+generic_error() {
+    usage
+    echo >&2 "${ERROR} $1"
+    echo >&2 ""
+    exit 1
+}
+
+ARG_PARSE="getopt -o vkdh -l verbose,keepdirs,dryrun,help -n $0 --"
+
+# We process arguments twice to handle any argument parsing error:
+ARG_ERROR=$($ARG_PARSE "$@" 2>&1 1>/dev/null)
+
+if [ $? -ne 0 ]; then
+    generic_error "$ARG_ERROR"
+fi
+
+# Abort on any errors from this point onwards
+set -euo pipefail
+
+# Parse args using getopt (instead of getopts) to allow arguments before options
+ARGS=$($ARG_PARSE "$@")
+
+# reorganize arguments as returned by getopt
+eval set -- "$ARGS"
+
+while true; do
+    case "$1" in
+        # Shift before to throw away option
+        # Shift after if option has a required positional argument
+        -v|--verbose)
+            shift
+            VERBOSE=1
+            ;;
+        -k|--keepdirs)
+            shift
+            KEEPDIRS=1
+            ;;
+        -d|--dryrun)
+            shift
+            DRYRUN="echo"
+            ;;
+        -h|--help)
+            shift
+            usage
+            exit 1
+            ;;
+        --)
+            shift
+            break
+            ;;
+    esac
+done
+
+if [ "$#" -lt 2 ]; then
+    generic_error "$0 requires at least 2 arguments - source and destination"
+fi
+
+# enable verbose execution
+if [ "$VERBOSE" -eq 1 ]; then
+    set -euxo pipefail
+fi
+
+DEST="${!#}" 
+
+if [ ! -d "$DEST" ]; then
+    generic_error "Destination $DEST doesn't exist"
+fi
+
+while [ "$#" -ge 2 ]; do
+    SRC="$1"
+    shift
+    move_preserve_path "$SRC" "$DEST" "$KEEPDIRS"
+done
+
+# vim: ai sts=4 et sw=4