From 37f0378f6e88db84178ba1dbb67acf54ae98b3da Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sat, 15 Aug 2026 10:13:57 +0000 Subject: [PATCH] Keep quoted sections intact when pretty printing shell form arguments prettyPrintArguments split the argument text with Text.words, so a quoted section was cut into words like any other text. Two things followed. An && written inside quotes matched the helper that inserts a line break, and the spacing inside the quotes was rewritten when the words were joined back with a single space each. RUN grep -F 'cd dir && ./run.sh ' /opt/Makefile came back out as RUN grep -F 'cd dir \ && ./run.sh ' /opt/Makefile which is a different command, and RUN echo 'a b' came back as RUN echo 'a b'. Split on whitespace only outside quotes, so a quoted section stays part of the word it belongs to. An escaped character is kept with the word as well, since an escaped space does not end one. Words are still joined with a single space, so nothing changes for unquoted text and a real && still breaks the line. Fixes #90 Fixes #57 --- src/Language/Docker/PrettyPrint.hs | 34 ++++++++++++++++++++++++- test/Language/Docker/PrettyPrintSpec.hs | 12 +++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Language/Docker/PrettyPrint.hs b/src/Language/Docker/PrettyPrint.hs index 02343f6..0535c13 100644 --- a/src/Language/Docker/PrettyPrint.hs +++ b/src/Language/Docker/PrettyPrint.hs @@ -8,6 +8,7 @@ module Language.Docker.PrettyPrint where +import Data.Char (isSpace) import Data.List.NonEmpty as NonEmpty (NonEmpty (..), toList) import Data.Set (Set) import Data.String (fromString) @@ -94,11 +95,42 @@ prettyPrintPair (k, v) = pretty k <> pretty '=' <> doubleQoute v prettyPrintArguments :: (?esc :: Char) => Arguments Text -> Doc ann prettyPrintArguments (ArgumentsList as) = prettyPrintJSON (Text.words as) -prettyPrintArguments (ArgumentsText as) = hsep (fmap helper (Text.words as)) +prettyPrintArguments (ArgumentsText as) = hsep (fmap helper (quotedWords as)) where helper "&&" = pretty ?esc <> "\n &&" helper a = pretty a +data Quoting + = Unquoted + | InSingle + | InDouble + deriving (Eq) + +-- | Like 'Text.words', except that a quoted section stays part of the word it +-- belongs to. Cutting a quoted section into words would let its spacing be +-- rewritten, and would let an operator written inside it, such as @&&@, pass +-- for one that separates commands. +quotedWords :: Text -> [Text] +quotedWords = go Unquoted [] [] + where + go quoting word acc text = + case Text.uncons text of + Nothing -> reverse (emit word acc) + Just (c, rest) + | quoting == Unquoted, isSpace c -> go Unquoted [] (emit word acc) rest + | c == '\\', + quoting /= InSingle, + Just (c', rest') <- Text.uncons rest -> + go quoting (c' : c : word) acc rest' + | c == '\'', quoting == Unquoted -> go InSingle (c : word) acc rest + | c == '\'', quoting == InSingle -> go Unquoted (c : word) acc rest + | c == '"', quoting == Unquoted -> go InDouble (c : word) acc rest + | c == '"', quoting == InDouble -> go Unquoted (c : word) acc rest + | otherwise -> go quoting (c : word) acc rest + + emit [] acc = acc + emit word acc = Text.pack (reverse word) : acc + prettyPrintJSON :: (?esc :: Char) => [Text] -> Doc ann prettyPrintJSON args = list (fmap doubleQoute args) diff --git a/test/Language/Docker/PrettyPrintSpec.hs b/test/Language/Docker/PrettyPrintSpec.hs index 4105818..5b072e0 100644 --- a/test/Language/Docker/PrettyPrintSpec.hs +++ b/test/Language/Docker/PrettyPrintSpec.hs @@ -184,6 +184,18 @@ spec = do let run = Run ( RunArgs ( ArgumentsText "foobar" ) def ) in assertPretty "RUN foobar" run + it "keeps && inside a quoted string" $ do + let run = Run ( RunArgs ( ArgumentsText "grep -F 'cd dir && ./run.sh ' /opt/Makefile" ) def ) + in assertPretty "RUN grep -F 'cd dir && ./run.sh ' /opt/Makefile" run + + it "keeps repeated spaces inside a quoted string" $ do + let run = Run ( RunArgs ( ArgumentsText "echo 'a b'" ) def ) + in assertPretty "RUN echo 'a b'" run + + it "still breaks the line on an unquoted &&" $ do + let run = Run ( RunArgs ( ArgumentsText "apt-get update && apt-get install -y curl" ) def ) + in assertPretty "RUN apt-get update \\\n && apt-get install -y curl" run + it "RUN in JSON format" $ do let run = Run ( RunArgs ( ArgumentsList "foobar barfoo" ) def ) in assertPretty "RUN [\"foobar\", \"barfoo\"]" run