parser: an escaped line break must not be pulled into the token that precedes it - #114
Merged
m-ildefons merged 1 commit intoAug 11, 2026
Conversation
A line continuation that is followed by whitespace stands for a space, and someUnless turned that space into part of the token it was building. Where a space ends the token -- as it does for the file paths of COPY and ADD -- the two operands end up merged into one, which either fails with "At least two arguments are required" or silently produces a path with a space in the middle. Keep such a line break out of the token and leave it unconsumed, so that it is parsed as the separator it is. related-to: hadolint/hadolint#1060 Signed-off-by: Eljees <3.14hell@gmail.com>
m-ildefons
approved these changes
Aug 11, 2026
m-ildefons
left a comment
Member
There was a problem hiding this comment.
Thanks a lot again, this is awesome.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
related-to: hadolint/hadolint#1060
What I did
COPY/ADDreject a valid Dockerfile when a line continuation follows the source pathwithout a space before the backslash:
COPY foo.json\ /root/foo.jsonDocker builds this file: the line continuation joins the lines and the leading spaces of the
next line separate the two arguments.
The same defect has a silent form, which is arguably worse than the error above — the
instruction parses, but the AST is wrong:
COPY a b\⏎c /app["a", "b c"]["a", "b", "c"]COPY --from=build a\⏎b /app["a b"]["a", "b"]COPY --chown=root:root a\⏎b /app["a b"]["a", "b"]A path that contains a space in the middle is produced out of two separate paths.
How I did it
someUnless(src/Language/Docker/Parser/Prelude.hs) builds a token out of threealternatives, and the first one is
castToSpace <$> escapedLineBreaksescapedLineBreaksreturnsFoundWhitespacewhen the continuation is followed by spaces, andcastToSpaceturns that into a literal space inside the token. The third alternative alreadyrespects the
predicate(“do not swallow this character if it is the delimiter”); the first onedid not. For
fileList, which callssomeUnless "a file" (== ' ') \sepEndBy1` requiredWhitespace, that means both file paths end up in a single token, andsepEndBy1` sees one path instead of two.The patch keeps the line break out of the token whenever whitespace ends it — i.e. exactly when
predicate ' 'holds — and leaves it unconsumed so thatrequiredWhitespacecan parse it as theseparator it is. Two callers change behaviour:
COPY/ADDpaths and the flags that end at aspace; everything that ends at a newline (
--from=,RUN,ENV, …) is untouched.The
tryadded to the third alternative is what makes the token end rather than the parsefail: that branch consumes the backslash before its
notFollowedByfails, which used to beunreachable because the first branch always matched a backslash + newline first.
The boundary case is preserved deliberately:
COPY foo.json\ /root/foo.jsonDocker joins these two lines without inserting a space, so
COPYreally does get a singleargument and must fail. There is a test for that (
expectFail), so the two cases cannot driftapart.
How to verify it
test/Language/Docker/ParseCopySpec.hsandtest/Language/Docker/ParseAddSpec.hs;301 examples, 3 failures, two of them with the exact error from theissue and one “ASTs are not equal” for the multi-file case;
301 examples, 0 failures(the 297 pre-existing tests are untouched);hlint src/ test/reports the same 5 hints before and after the patch.Environment: GHC 9.8.4,
cabal testas in.github/workflows/haskell.yml.