diff --git a/language-docker.cabal b/language-docker.cabal
--- a/language-docker.cabal
+++ b/language-docker.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 0acabc7fcd899e8d19189dd6a3e3541b451fb5e35c3ed9751f8f0261404d8197
+-- hash: 3b38c6eec0fc0543e696e51fa5a654074e9718ae41bd50f4114824964938c535
 
 name:           language-docker
-version:        10.0.2
+version:        10.1.0
 synopsis:       Dockerfile parser, pretty-printer and embedded DSL
 description:    All functions for parsing and pretty-printing Dockerfiles are exported through @Language.Docker@. For more fine-grained operations look for specific modules that implement a certain functionality.
                 See the <https://github.com/hadolint/language-docker GitHub project> for the source-code and examples.
@@ -81,8 +81,10 @@
   main-is: Spec.hs
   other-modules:
       Language.Docker.IntegrationSpec
+      Language.Docker.ParseCopySpec
       Language.Docker.ParsePragmaSpec
       Language.Docker.ParserSpec
+      Language.Docker.ParseRunSpec
       Language.Docker.PrettyPrintSpec
       TestHelper
       Paths_language_docker
diff --git a/src/Language/Docker/Parser/Arguments.hs b/src/Language/Docker/Parser/Arguments.hs
--- a/src/Language/Docker/Parser/Arguments.hs
+++ b/src/Language/Docker/Parser/Arguments.hs
@@ -15,9 +15,17 @@
 
 -- Parse arguments of a command in the shell form
 argumentsShell :: (?esc :: Char) => Parser (Arguments Text)
-argumentsShell = ArgumentsText <$> toEnd
+argumentsShell =
+  try (ArgumentsText <$> untilHeredoc)
+    <|> (ArgumentsText <$> toEnd)
   where
     toEnd = untilEol "the shell arguments"
 
+-- Parse arguments of a command in the heredoc format
+argumentsHeredoc :: Parser (Arguments Text)
+argumentsHeredoc = ArgumentsText <$> heredoc
+
 arguments :: (?esc :: Char) => Parser (Arguments Text)
-arguments = try argumentsExec <|> try argumentsShell
+arguments = try argumentsHeredoc
+  <|> try argumentsExec
+  <|> try argumentsShell
diff --git a/src/Language/Docker/Parser/Copy.hs b/src/Language/Docker/Parser/Copy.hs
--- a/src/Language/Docker/Parser/Copy.hs
+++ b/src/Language/Docker/Parser/Copy.hs
@@ -42,12 +42,13 @@
             case sourceFlags of
               [] -> NoSource
               f : _ -> f
-      fileList "COPY" (\src dest -> Copy (CopyArgs src dest cho chm fr))
+      try (heredocList (\src dest -> Copy (CopyArgs src dest cho chm fr)))
+        <|> fileList "COPY" (\src dest -> Copy (CopyArgs src dest cho chm fr))
 
 parseAdd :: (?esc :: Char) => Parser (Instruction Text)
 parseAdd = do
   reserved "ADD"
-  flags <- copyFlag `sepEndBy` requiredWhitespace
+  flags <- addFlag `sepEndBy` requiredWhitespace
   let chownFlags = [c | FlagChown c <- flags]
   let chmodFlags = [c | FlagChmod c <- flags]
   let invalidFlags = [i | FlagInvalid i <- flags]
@@ -66,6 +67,17 @@
                   c : _ -> c
       fileList "ADD" (\src dest -> Add (AddArgs src dest cho chm))
 
+heredocList :: (?esc :: Char) =>
+               (NonEmpty SourcePath -> TargetPath -> Instruction Text) ->
+               Parser (Instruction Text)
+heredocList constr = do
+  markers <- try (spaceSep1 heredocMarker) <?> "a list of heredoc markers"
+  target <- untilEol "target path"
+  case reverse markers of
+    [] -> customError $ FileListError "empty list of heredoc markers"
+    m:_ -> void $ heredocContent m
+  return $ constr (SourcePath <$> fromList markers) (TargetPath target)
+
 fileList :: (?esc :: Char) => Text ->
             (NonEmpty SourcePath -> TargetPath -> Instruction Text) ->
             Parser (Instruction Text)
@@ -86,11 +98,12 @@
 unexpectedFlag name _ = customFailure $ InvalidFlagError (T.unpack name)
 
 copyFlag :: (?esc :: Char) => Parser CopyFlag
-copyFlag =
-  (FlagChown <$> try chown <?> "only one --chown")
-    <|> (FlagChmod <$> try chmod <?> "only one --chmod")
-    <|> (FlagSource <$> try copySource <?> "only one --from")
-    <|> (FlagInvalid <$> try anyFlag <?> "no other flags")
+copyFlag = (FlagSource <$> try copySource <?> "only one --from") <|> addFlag
+
+addFlag :: (?esc :: Char) => Parser CopyFlag
+addFlag = (FlagChown <$> try chown <?> "--chown")
+  <|> (FlagChmod <$> try chmod <?> "--chmod")
+  <|> (FlagInvalid <$> try anyFlag <?> "other flag")
 
 chown :: (?esc :: Char) => Parser Chown
 chown = do
diff --git a/src/Language/Docker/Parser/Prelude.hs b/src/Language/Docker/Parser/Prelude.hs
--- a/src/Language/Docker/Parser/Prelude.hs
+++ b/src/Language/Docker/Parser/Prelude.hs
@@ -7,11 +7,16 @@
     reserved,
     natural,
     commaSep,
+    spaceSep1,
     stringLiteral,
     brackets,
+    heredoc,
+    heredocMarker,
+    heredocContent,
     whitespace,
     requiredWhitespace,
     untilEol,
+    untilHeredoc,
     symbol,
     onlySpaces,
     onlyWhitespaces,
@@ -126,14 +131,70 @@
 commaSep :: (?esc :: Char) => Parser a -> Parser [a]
 commaSep p = sepBy (p <* whitespace) (symbol ",")
 
+spaceSep1 :: Parser a -> Parser [a]
+spaceSep1 p = sepEndBy1 p onlySpaces
+
+-- | Note this is just an alias for compatibility
 stringLiteral :: Parser Text
-stringLiteral = do
-  void (char '"')
-  lit <- manyTill L.charLiteral (char '"')
-  return (T.pack lit)
+stringLiteral = doubleQuotedString
 
+singleQuotedString :: Parser Text
+singleQuotedString = quotedString '\''
+
+doubleQuotedString :: Parser Text
+doubleQuotedString = quotedString '\"'
+
+quotedString :: Char -> Parser Text
+quotedString c = do
+  void $ char c
+  lit <- manyTill L.charLiteral (char c)
+  return $ T.pack lit
+
 brackets :: (?esc :: Char) => Parser a -> Parser a
 brackets = between (symbol "[" *> whitespace) (whitespace *> symbol "]")
+
+justWhitespace :: Parser Text
+justWhitespace = do
+  c <- choice
+    [ char ' ',
+      char '\t',
+      char '\n'
+    ]
+  return (T.pack [c])
+
+untilWS :: Parser Text
+untilWS = do
+  s <- manyTill L.charLiteral justWhitespace
+  return $ T.pack s
+
+heredocMarker :: Parser Text
+heredocMarker = do
+  void $ string "<<"
+  void $ takeWhileP (Just "dash") (== '-')
+  m <- try doubleQuotedString <|> try singleQuotedString <|> untilWS
+  optional heredocRedirect
+  pure m
+
+heredocRedirect :: Parser Text
+heredocRedirect = do
+  void $ char '>'
+  takeWhileP (Just "heredoc path") (/= '\n')
+
+heredocContent :: Text -> Parser Text
+heredocContent marker = do
+  doc <- manyTill L.charLiteral (string marker)
+  return $ T.strip $ T.pack doc
+
+heredoc :: Parser Text
+heredoc = do
+  m <- heredocMarker
+  heredocContent m
+
+-- | Parses text until a heredoc is found. Will also consume the heredoc.
+untilHeredoc :: Parser Text
+untilHeredoc = do
+  txt <- manyTill L.charLiteral heredoc
+  return $ T.strip $ T.pack txt
 
 onlySpaces :: Parser Text
 onlySpaces = takeWhileP (Just "spaces") (\c -> c == ' ' || c == '\t')
diff --git a/test/Language/Docker/ParseCopySpec.hs b/test/Language/Docker/ParseCopySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Docker/ParseCopySpec.hs
@@ -0,0 +1,138 @@
+module Language.Docker.ParseCopySpec where
+
+import qualified Data.Text as Text
+import Language.Docker.Parser
+import Language.Docker.Syntax
+import TestHelper
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+
+spec :: Spec
+spec = do
+  describe "regular Copy" $ do
+    it "simple COPY" $
+      let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs [SourcePath "."] (TargetPath "/app") NoChown NoChmod NoSource,
+              Copy $ CopyArgs [SourcePath "baz"] (TargetPath "/some/long/path") NoChown NoChmod NoSource
+            ]
+    it "multifiles COPY" $
+      let file = Text.unlines ["COPY foo bar baz /app"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoChmod NoSource
+            ]
+    it "list of quoted files" $
+      let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoChmod NoSource
+            ]
+    it "supports windows paths" $
+      let file = Text.unlines ["COPY C:\\\\go C:\\\\go"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["C:\\\\go"]) (TargetPath "C:\\\\go") NoChown NoChmod NoSource
+            ]
+    it "does not get confused with trailing whitespace" $
+      let file = Text.unlines ["COPY a b  "]
+       in assertAst
+            file
+            [ Copy $ CopyArgs [SourcePath "a"] (TargetPath "b") NoChown NoChmod NoSource
+            ]
+
+  describe "Copy with flags" $ do
+    it "with chown flag" $
+      let file = Text.unlines ["COPY --chown=user:group foo bar"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") NoChmod NoSource
+            ]
+    it "with chmod flag" $
+      let file = Text.unlines ["COPY --chmod=777 foo bar"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown (Chmod "777") NoSource
+            ]
+    it "with from flag" $
+      let file = Text.unlines ["COPY --from=node foo bar"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown NoChmod (CopySource "node")
+            ]
+    it "with all three flags" $
+      let file = Text.unlines ["COPY --from=node --chmod=751 --chown=user:group foo bar"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (Chmod "751") (CopySource "node")
+            ]
+    it "with all three flags in different order" $
+      let file = Text.unlines ["COPY --chown=user:group --from=node --chmod=644 foo bar"]
+       in assertAst
+            file
+            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (Chmod "644") (CopySource "node")
+            ]
+
+  describe "Copy with Heredocs" $ do
+    it "empty heredoc" $
+      let file = Text.unlines ["COPY <<EOF /target", "EOF"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "EOF"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc" $
+      let file = Text.unlines ["COPY <<FOO /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc lowercase +extensions" $
+      let file = Text.unlines ["COPY <<foo.txt /target", "foo content", "line 2", "foo.txt"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "foo.txt"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc single quoted marker" $
+      let file = Text.unlines ["COPY <<\'FOO\' /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc double quoted marker" $
+      let file = Text.unlines ["COPY <<\"FOO\" /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc +dash" $
+      let file = Text.unlines ["COPY <<-FOO /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc single quoted marker +dash" $
+      let file = Text.unlines ["COPY <<-\'FOO\' /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "foo heredoc double quoted marker +dash" $
+      let file = Text.unlines ["COPY <<-\"FOO\" /target", "foo", "FOO"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "multiple heredocs" $
+      let file = Text.unlines ["COPY <<FOO <<BAR /target", "foo", "FOO", "bar", "BAR"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO", SourcePath "BAR"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
+    it "multiple heredocs with single/double quotes and dash mixed" $
+      let file = Text.unlines ["COPY <<FOO <<-\"BAR\" <<\'FIZZ\' <<-BUZZ /target", "foo", "FOO", "bar", "BAR", "fizz", "FIZZ", "buss", "BUZZ"]
+       in assertAst
+            file
+              [ Copy $ CopyArgs [SourcePath "FOO", SourcePath "BAR", SourcePath "FIZZ", SourcePath "BUZZ"] (TargetPath "/target") NoChown NoChmod NoSource
+              ]
diff --git a/test/Language/Docker/ParseRunSpec.hs b/test/Language/Docker/ParseRunSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Docker/ParseRunSpec.hs
@@ -0,0 +1,334 @@
+module Language.Docker.ParseRunSpec where
+
+import Data.Default.Class (def)
+import qualified Data.Text as Text
+import Language.Docker.Parser
+import Language.Docker.Syntax
+import TestHelper
+import Test.HUnit hiding (Label)
+import Test.Hspec
+
+
+spec :: Spec
+spec = do
+  describe "parse RUN instructions" $ do
+    it "escaped with space before" $
+      let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"]
+       in assertAst dockerfile [Run "yum install -y imagemagick mysql"]
+    it "does not choke on unmatched brackets" $
+      let dockerfile = Text.unlines ["RUN [foo"]
+       in assertAst dockerfile [Run "[foo"]
+    it "Distinguishes between text and a list" $
+      let dockerfile =
+            Text.unlines
+              [ "RUN echo foo",
+                "RUN [\"echo\", \"foo\"]"
+              ]
+       in assertAst dockerfile [Run $ RunArgs (ArgumentsText "echo foo") def, Run $ RunArgs (ArgumentsList "echo foo") def]
+    it "Accepts spaces inside the brackets" $
+      let dockerfile =
+            Text.unlines
+              [ "RUN [  \"echo\", \"foo\"  ]"
+              ]
+       in assertAst dockerfile [Run $ RunArgs (ArgumentsList "echo foo") def]
+
+  describe "RUN with experimental flags" $ do
+    it "--mount=type=bind and target" $
+      let file = Text.unlines ["RUN --mount=type=bind,target=/foo echo foo"]
+          flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount default to bind" $
+      let file = Text.unlines ["RUN --mount=target=/foo echo foo"]
+          flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=bind all modifiers" $
+      let file = Text.unlines ["RUN --mount=type=bind,target=/foo,source=/bar,from=ubuntu,ro echo foo"]
+          flags = def {mount = Just $ BindMount (BindOpts {bTarget = "/foo", bSource = Just "/bar", bFromImage = Just "ubuntu", bReadOnly = Just True})}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=cache with target" $
+      let file =
+            Text.unlines
+              [ "RUN --mount=type=cache,target=/foo echo foo",
+                "RUN --mount=type=cache,target=/bar echo foo",
+                "RUN --mount=type=cache,target=/baz echo foo"
+              ]
+          flags1 = def {mount = Just $ CacheMount (def {cTarget = "/foo"})}
+          flags2 = def {mount = Just $ CacheMount (def {cTarget = "/bar"})}
+          flags3 = def {mount = Just $ CacheMount (def {cTarget = "/baz"})}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags1,
+              Run $ RunArgs (ArgumentsText "echo foo") flags2,
+              Run $ RunArgs (ArgumentsText "echo foo") flags3
+            ]
+    it "--mount=type=cache with all modifiers" $
+      let file =
+            Text.unlines
+              [ "RUN --mount=type=cache,target=/foo,sharing=private,id=a,ro,from=ubuntu,source=/bar,mode=0700,uid=0,gid=0 echo foo"
+              ]
+          flags =
+            def
+              { mount =
+                  Just $
+                    CacheMount
+                      ( def
+                          { cTarget = "/foo",
+                            cSharing = Just Private,
+                            cCacheId = Just "a",
+                            cReadOnly = Just True,
+                            cFromImage = Just "ubuntu",
+                            cSource = Just "/bar",
+                            cMode = Just "0700",
+                            cUid = Just 0,
+                            cGid = Just 0
+                          }
+                      )
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=tmpfs" $
+      let file = Text.unlines ["RUN --mount=type=tmpfs,target=/foo echo foo"]
+          flags = def {mount = Just $ TmpfsMount (def {tTarget = "/foo"})}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=ssh" $
+      let file = Text.unlines ["RUN --mount=type=ssh echo foo"]
+          flags = def {mount = Just $ SshMount def}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=ssh,required=false" $
+      let file = Text.unlines ["RUN --mount=type=ssh,required=false echo foo"]
+          flags = def {mount = Just $ SshMount def {sIsRequired = Just False}}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=ssh,required=False" $
+      let file = Text.unlines ["RUN --mount=type=ssh,required=False echo foo"]
+          flags = def {mount = Just $ SshMount def {sIsRequired = Just False}}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=secret,required=true" $
+      let file = Text.unlines ["RUN --mount=type=secret,required=true echo foo"]
+          flags = def {mount = Just $ SecretMount def {sIsRequired = Just True}}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=secret,required=True" $
+      let file = Text.unlines ["RUN --mount=type=secret,required=True echo foo"]
+          flags = def {mount = Just $ SecretMount def {sIsRequired = Just True}}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=ssh all modifiers" $
+      let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
+          flags =
+            def
+              { mount =
+                  Just $
+                    SshMount
+                      ( def
+                          { sTarget = Just "/foo",
+                            sCacheId = Just "a",
+                            sIsRequired = Just True,
+                            sSource = Just "/bar",
+                            sMode = Just "0700",
+                            sUid = Just 0,
+                            sGid = Just 0
+                          }
+                      )
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=ssh all modifiers, required explicit" $
+      let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
+          flags =
+            def
+              { mount =
+                  Just $
+                    SshMount
+                      ( def
+                          { sTarget = Just "/foo",
+                            sCacheId = Just "a",
+                            sIsRequired = Just True,
+                            sSource = Just "/bar",
+                            sMode = Just "0700",
+                            sUid = Just 0,
+                            sGid = Just 0
+                          }
+                      )
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=secret all modifiers" $
+      let file = Text.unlines ["RUN --mount=type=secret,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
+          flags =
+            def
+              { mount =
+                  Just $
+                    SecretMount
+                      ( def
+                          { sTarget = Just "/foo",
+                            sCacheId = Just "a",
+                            sIsRequired = Just True,
+                            sSource = Just "/bar",
+                            sMode = Just "0700",
+                            sUid = Just 0,
+                            sGid = Just 0
+                          }
+                      )
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--mount=type=secret all modifiers, required explicit" $
+      let file = Text.unlines ["RUN --mount=type=secret,target=/foo,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
+          flags =
+            def
+              { mount =
+                  Just $
+                    SecretMount
+                      ( def
+                          { sTarget = Just "/foo",
+                            sCacheId = Just "a",
+                            sIsRequired = Just True,
+                            sSource = Just "/bar",
+                            sMode = Just "0700",
+                            sUid = Just 0,
+                            sGid = Just 0
+                          }
+                      )
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--network=none" $
+      let file = Text.unlines ["RUN --network=none echo foo"]
+          flags = def {network = Just NetworkNone}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--network=host" $
+      let file = Text.unlines ["RUN --network=host echo foo"]
+          flags = def {network = Just NetworkHost}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--network=default" $
+      let file = Text.unlines ["RUN --network=default echo foo"]
+          flags = def {network = Just NetworkDefault}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--security=insecure" $
+      let file = Text.unlines ["RUN --security=insecure echo foo"]
+          flags = def {security = Just Insecure}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "--security=sandbox" $
+      let file = Text.unlines ["RUN --security=sandbox echo foo"]
+          flags = def {security = Just Sandbox}
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+    it "allows all flags" $
+      let file = Text.unlines ["RUN --mount=target=/foo --network=none --security=sandbox echo foo"]
+          flags =
+            def
+              { security = Just Sandbox,
+                network = Just NetworkNone,
+                mount = Just $ BindMount $ def {bTarget = "/foo"}
+              }
+       in assertAst
+            file
+            [ Run $ RunArgs (ArgumentsText "echo foo") flags
+            ]
+
+  describe "parse RUN in heredocs format" $ do
+    it "foo heredoc" $
+      let file = Text.unlines [ "RUN <<EOF", "foo", "EOF"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc marker" $
+      let file = Text.unlines [ "RUN <<FOO", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc quoted marker" $
+      let file = Text.unlines [ "RUN <<\"FOO\"", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc single quoted marker" $
+      let file = Text.unlines [ "RUN <<\'FOO\'", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc +dash" $
+      let file = Text.unlines [ "RUN <<-FOO", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc quoted +dash" $
+      let file = Text.unlines [ "RUN <<-\"FOO\"", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "foo heredoc single quoted +dash" $
+      let file = Text.unlines [ "RUN <<-\'FOO\'", "foo", "FOO"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "multiline foo heredoc" $
+      let file = Text.unlines [ "RUN <<EOF", "foo", "bar", "", "dodo", "EOF"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo\nbar\n\ndodo") flags ]
+    it "heredoc with shebang/commend" $
+      let file = Text.unlines [ "RUN <<EOF", "#/usr/bin/env python", "", "#print foo", "print(\"foo\")", "EOF" ]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "#/usr/bin/env python\n\n#print foo\nprint(\"foo\")") flags ]
+    it "empty heredoc" $
+      let file = Text.unlines [ "RUN <<EOF", "EOF"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "") flags ]
+    it "evil heredoc" $
+      let file = Text.unlines [ "RUN <<EOF foo", "bar EOF"]
+          flags = def { security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo\nbar") flags ]
+    it "heredoc with redirection to file" $
+      let file = Text.unlines [ "RUN <<EOF > /file", "foo", "EOF" ]
+          flags = def {security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "foo") flags ]
+    it "heredoc to program stdin" $
+      let file = Text.unlines [ "RUN python <<EOF", "print(\"foo\")", "EOF" ]
+          flags = def {security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "python") flags ]
+    it "heredoc to program stdin with redirect to file" $
+      let file = Text.unlines [ "RUN python <<EOF > /file", "print(\"foo\")", "EOF" ]
+          flags = def {security = Nothing }
+       in assertAst file [ Run $ RunArgs (ArgumentsText "python") flags ]
diff --git a/test/Language/Docker/ParserSpec.hs b/test/Language/Docker/ParserSpec.hs
--- a/test/Language/Docker/ParserSpec.hs
+++ b/test/Language/Docker/ParserSpec.hs
@@ -174,26 +174,6 @@
             [ Env [("PHP_FPM_ACCESS_FORMAT", "%R - %u %t \"%m %r\" %s")]
             ]
        in assertAst dockerfile ast
-  describe "parse RUN" $ do
-    it "escaped with space before" $
-      let dockerfile = Text.unlines ["RUN yum install -y \\", "imagemagick \\", "mysql"]
-       in assertAst dockerfile [Run "yum install -y imagemagick mysql"]
-    it "does not choke on unmatched brackets" $
-      let dockerfile = Text.unlines ["RUN [foo"]
-       in assertAst dockerfile [Run "[foo"]
-    it "Distinguishes between text and a list" $
-      let dockerfile =
-            Text.unlines
-              [ "RUN echo foo",
-                "RUN [\"echo\", \"foo\"]"
-              ]
-       in assertAst dockerfile [Run $ RunArgs (ArgumentsText "echo foo") def, Run $ RunArgs (ArgumentsList "echo foo") def]
-    it "Accepts spaces inside the brackets" $
-      let dockerfile =
-            Text.unlines
-              [ "RUN [  \"echo\", \"foo\"  ]"
-              ]
-       in assertAst dockerfile [Run $ RunArgs (ArgumentsList "echo foo") def]
   describe "parse CMD" $ do
     it "one line cmd" $ assertAst "CMD true" [Cmd "true"]
     it "cmd over several lines" $
@@ -413,308 +393,4 @@
        in assertAst
             file
             [ Add $ AddArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") (Chown "user:group") NoChmod
-            ]
-  describe "COPY" $ do
-    it "simple COPY" $
-      let file = Text.unlines ["COPY . /app", "COPY baz /some/long/path"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs [SourcePath "."] (TargetPath "/app") NoChown NoChmod NoSource,
-              Copy $ CopyArgs [SourcePath "baz"] (TargetPath "/some/long/path") NoChown NoChmod NoSource
-            ]
-    it "multifiles COPY" $
-      let file = Text.unlines ["COPY foo bar baz /app"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoChmod NoSource
-            ]
-    it "list of quoted files" $
-      let file = Text.unlines ["COPY [\"foo\", \"bar\", \"baz\", \"/app\"]"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo", "bar", "baz"]) (TargetPath "/app") NoChown NoChmod NoSource
-            ]
-    it "with chown flag" $
-      let file = Text.unlines ["COPY --chown=user:group foo bar"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") NoChmod NoSource
-            ]
-    it "with chmod flag" $
-      let file = Text.unlines ["COPY --chmod=777 foo bar"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown (Chmod "777") NoSource
-            ]
-    it "with from flag" $
-      let file = Text.unlines ["COPY --from=node foo bar"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") NoChown NoChmod (CopySource "node")
-            ]
-    it "with all three flags" $
-      let file = Text.unlines ["COPY --from=node --chmod=751 --chown=user:group foo bar"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (Chmod "751") (CopySource "node")
-            ]
-    it "with all three flags in different order" $
-      let file = Text.unlines ["COPY --chown=user:group --from=node --chmod=644 foo bar"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["foo"]) (TargetPath "bar") (Chown "user:group") (Chmod "644") (CopySource "node")
-            ]
-    it "supports windows paths" $
-      let file = Text.unlines ["COPY C:\\\\go C:\\\\go"]
-       in assertAst
-            file
-            [ Copy $ CopyArgs (fmap SourcePath ["C:\\\\go"]) (TargetPath "C:\\\\go") NoChown NoChmod NoSource
-            ]
-    it "does not get confused with trailing whitespace" $
-      let file = Text.unlines ["COPY a b  "]
-       in assertAst
-            file
-            [ Copy $ CopyArgs [SourcePath "a"] (TargetPath "b") NoChown NoChmod NoSource
-            ]
-  describe "RUN with experimental flags" $ do
-    it "--mount=type=bind and target" $
-      let file = Text.unlines ["RUN --mount=type=bind,target=/foo echo foo"]
-          flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount default to bind" $
-      let file = Text.unlines ["RUN --mount=target=/foo echo foo"]
-          flags = def {mount = Just $ BindMount (def {bTarget = "/foo"})}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=bind all modifiers" $
-      let file = Text.unlines ["RUN --mount=type=bind,target=/foo,source=/bar,from=ubuntu,ro echo foo"]
-          flags = def {mount = Just $ BindMount (BindOpts {bTarget = "/foo", bSource = Just "/bar", bFromImage = Just "ubuntu", bReadOnly = Just True})}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=cache with target" $
-      let file =
-            Text.unlines
-              [ "RUN --mount=type=cache,target=/foo echo foo",
-                "RUN --mount=type=cache,target=/bar echo foo",
-                "RUN --mount=type=cache,target=/baz echo foo"
-              ]
-          flags1 = def {mount = Just $ CacheMount (def {cTarget = "/foo"})}
-          flags2 = def {mount = Just $ CacheMount (def {cTarget = "/bar"})}
-          flags3 = def {mount = Just $ CacheMount (def {cTarget = "/baz"})}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags1,
-              Run $ RunArgs (ArgumentsText "echo foo") flags2,
-              Run $ RunArgs (ArgumentsText "echo foo") flags3
-            ]
-    it "--mount=type=cache with all modifiers" $
-      let file =
-            Text.unlines
-              [ "RUN --mount=type=cache,target=/foo,sharing=private,id=a,ro,from=ubuntu,source=/bar,mode=0700,uid=0,gid=0 echo foo"
-              ]
-          flags =
-            def
-              { mount =
-                  Just $
-                    CacheMount
-                      ( def
-                          { cTarget = "/foo",
-                            cSharing = Just Private,
-                            cCacheId = Just "a",
-                            cReadOnly = Just True,
-                            cFromImage = Just "ubuntu",
-                            cSource = Just "/bar",
-                            cMode = Just "0700",
-                            cUid = Just 0,
-                            cGid = Just 0
-                          }
-                      )
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=tmpfs" $
-      let file = Text.unlines ["RUN --mount=type=tmpfs,target=/foo echo foo"]
-          flags = def {mount = Just $ TmpfsMount (def {tTarget = "/foo"})}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=ssh" $
-      let file = Text.unlines ["RUN --mount=type=ssh echo foo"]
-          flags = def {mount = Just $ SshMount def}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=ssh,required=false" $
-      let file = Text.unlines ["RUN --mount=type=ssh,required=false echo foo"]
-          flags = def {mount = Just $ SshMount def {sIsRequired = Just False}}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=ssh,required=False" $
-      let file = Text.unlines ["RUN --mount=type=ssh,required=False echo foo"]
-          flags = def {mount = Just $ SshMount def {sIsRequired = Just False}}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=secret,required=true" $
-      let file = Text.unlines ["RUN --mount=type=secret,required=true echo foo"]
-          flags = def {mount = Just $ SecretMount def {sIsRequired = Just True}}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=secret,required=True" $
-      let file = Text.unlines ["RUN --mount=type=secret,required=True echo foo"]
-          flags = def {mount = Just $ SecretMount def {sIsRequired = Just True}}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=ssh all modifiers" $
-      let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
-          flags =
-            def
-              { mount =
-                  Just $
-                    SshMount
-                      ( def
-                          { sTarget = Just "/foo",
-                            sCacheId = Just "a",
-                            sIsRequired = Just True,
-                            sSource = Just "/bar",
-                            sMode = Just "0700",
-                            sUid = Just 0,
-                            sGid = Just 0
-                          }
-                      )
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=ssh all modifiers, required explicit" $
-      let file = Text.unlines ["RUN --mount=type=ssh,target=/foo,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
-          flags =
-            def
-              { mount =
-                  Just $
-                    SshMount
-                      ( def
-                          { sTarget = Just "/foo",
-                            sCacheId = Just "a",
-                            sIsRequired = Just True,
-                            sSource = Just "/bar",
-                            sMode = Just "0700",
-                            sUid = Just 0,
-                            sGid = Just 0
-                          }
-                      )
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=secret all modifiers" $
-      let file = Text.unlines ["RUN --mount=type=secret,target=/foo,id=a,required,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
-          flags =
-            def
-              { mount =
-                  Just $
-                    SecretMount
-                      ( def
-                          { sTarget = Just "/foo",
-                            sCacheId = Just "a",
-                            sIsRequired = Just True,
-                            sSource = Just "/bar",
-                            sMode = Just "0700",
-                            sUid = Just 0,
-                            sGid = Just 0
-                          }
-                      )
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--mount=type=secret all modifiers, required explicit" $
-      let file = Text.unlines ["RUN --mount=type=secret,target=/foo,id=a,required=true,source=/bar,mode=0700,uid=0,gid=0 echo foo"]
-          flags =
-            def
-              { mount =
-                  Just $
-                    SecretMount
-                      ( def
-                          { sTarget = Just "/foo",
-                            sCacheId = Just "a",
-                            sIsRequired = Just True,
-                            sSource = Just "/bar",
-                            sMode = Just "0700",
-                            sUid = Just 0,
-                            sGid = Just 0
-                          }
-                      )
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--network=none" $
-      let file = Text.unlines ["RUN --network=none echo foo"]
-          flags = def {network = Just NetworkNone}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--network=host" $
-      let file = Text.unlines ["RUN --network=host echo foo"]
-          flags = def {network = Just NetworkHost}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--network=default" $
-      let file = Text.unlines ["RUN --network=default echo foo"]
-          flags = def {network = Just NetworkDefault}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--security=insecure" $
-      let file = Text.unlines ["RUN --security=insecure echo foo"]
-          flags = def {security = Just Insecure}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "--security=sandbox" $
-      let file = Text.unlines ["RUN --security=sandbox echo foo"]
-          flags = def {security = Just Sandbox}
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
-            ]
-    it "allows all flags" $
-      let file = Text.unlines ["RUN --mount=target=/foo --network=none --security=sandbox echo foo"]
-          flags =
-            def
-              { security = Just Sandbox,
-                network = Just NetworkNone,
-                mount = Just $ BindMount $ def {bTarget = "/foo"}
-              }
-       in assertAst
-            file
-            [ Run $ RunArgs (ArgumentsText "echo foo") flags
             ]
