{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -- | -- -- Case-for-case port from Python: -- -- module ShellWords.ParseSpec ( spec ) where import Prelude hiding (truncate) import Data.Foldable (for_, traverse_) import ShellWords.Parse (parser, runParser) import Test.Hspec import Text.Megaparsec.Char (string) import Text.Megaparsec.Compat (between) testCases :: [(String, [String])] testCases = [ ("var --bar=baz", ["var", "--bar=baz"]) , ("var --bar=\"baz\"", ["var", "--bar=baz"]) , ("var \"--bar=baz\"", ["var", "--bar=baz"]) , ("var \"--bar='baz'\"", ["var", "--bar='baz'"]) , ("var --bar=`baz`", ["var", "--bar=`baz`"]) , ("var \"--bar=\\\"baz'\"", ["var", "--bar=\"baz'"]) , ("var \"--bar=\\'baz\\'\"", ["var", "--bar=\\'baz\\'"]) , ("var \"--bar baz\"", ["var", "--bar baz"]) , ("var --\"bar baz\"", ["var", "--bar baz"]) , ("var --\"bar baz\"", ["var", "--bar baz"]) , -- Additional test cases for whitespace ("var --bar=baz ", ["var", "--bar=baz"]) , (" var --bar=baz ", ["var", "--bar=baz"]) , (" var --bar=baz ", ["var", "--bar=baz"]) , -- Additional test cases for escaped spaces ("var --bar\\ baz", ["var", "--bar baz"]) , ("var --bar=baz\\ bat", ["var", "--bar=baz bat"]) , ("var --bar baz\\ bat", ["var", "--bar", "baz bat"]) , -- N.B. sh preserves escapes in quoted values, python-shellwords does not. -- we behave like sh in this regard. ("var --bar 'baz\\ bat'", ["var", "--bar", "baz\\ bat"]) , ("var --bar \"baz\\ bat\"", ["var", "--bar", "baz\\ bat"]) , ("var \"--bar=\\\\'baz\\\\'\"", ["var", "--bar=\\'baz\\'"]) , ("var --bar='\\\\'", ["var", "--bar=\\"]) ] errorCases :: [String] errorCases = [ "foo '" , "foo \"" -- -- Doesn't need to error since we don't have parse_backtick either. -- -- , "foo `" ] runTestCase :: HasCallStack => String -> [String] -> Spec runTestCase input expected = it ("parses |" <> truncate 50 input <> "| correctly") $ do runParser parser input `shouldBeParsed` expected where truncate n x | length x > n = take n x <> "..." | otherwise = x spec :: Spec spec = describe "parser" $ do traverse_ (uncurry runTestCase) testCases for_ errorCases $ \input -> do it ("errors on |" <> input <> "|") $ do expectParseError $ runParser parser input context "Issue #3" $ do runTestCase "-LC:/Users/Vitor\\ Coimbra/AppData/Local/Programs/stack/x86_64-windows/msys2-20150512/mingw64/lib -ltag\n" [ "-LC:/Users/Vitor Coimbra/AppData/Local/Programs/stack/x86_64-windows/msys2-20150512/mingw64/lib" , "-ltag" ] context "Issue #7" $ do runTestCase "foo=123 bar" ["foo=123", "bar"] runTestCase "foo bar=123 cow" ["foo", "bar=123", "cow"] runTestCase "foo bar'();='bar cow" ["foo", "bar();=bar", "cow"] runTestCase "foo 'ba'\"r\" cow" ["foo", "bar", "cow"] context "As part of a larger Parser" $ do let parseDelimited input = runParser (between (string "FOO=$(") (string ")") parser) $ "FOO=$(" <> input <> ")" it "parses within delimiters" $ do parseDelimited "echo \"hi\"" `shouldBeParsed` ["echo", "hi"] it "handles quoted delimiter" $ do parseDelimited "echo \"hi (quietly)\"" `shouldBeParsed` ["echo", "hi (quietly)"] it "works with white space" $ do parseDelimited " echo \n\"hi\" " `shouldBeParsed` ["echo", "hi"] it "works with newlines" $ do parseDelimited "echo \n\"hi\"" `shouldBeParsed` ["echo", "hi"] it "works with final newline" $ do parseDelimited "echo \n\"hi\"\n" `shouldBeParsed` ["echo", "hi"] expectParseError :: (Show a, HasCallStack) => Either String a -> Expectation expectParseError = \case Left {} -> pure () Right a' -> expectationFailure $ "Expected parse error, got:" <> show a' shouldBeParsed :: (Show a, Eq a, HasCallStack) => Either String a -> a -> Expectation a `shouldBeParsed` b = case a of Left e -> expectationFailure e Right a' -> a' `shouldBe` b {- Features I'm not sure I'll be porting, certainly not yet. def test_backtick(): goversion, err = shell_run("go version") assert not err s = ShellWords(parse_backtick=True) args = s.parse("echo `go version`") expected = ["echo", goversion.strip('\n')] assert args == expected def test_backtick_error(): s = ShellWords(parse_backtick=True) try: s.parse("echo `go Version`") except: pass else: raise Exception("Should be an error") def test_env(): os.environ["FOO"] = "bar" s = ShellWords(parse_env=True) args = s.parse("echo $FOO") expected = ["echo", "bar"] assert args == expected def test_no_env(): s = ShellWords(parse_env=True) args = s.parse("echo $BAR") expected = ["echo", ""] assert args == expected def test_dup_env(): os.environ["FOO"] = "bar" os.environ["FOO_BAR"] = "baz" s = ShellWords(parse_env=True) args = s.parse("echo $$FOO$") expected = ["echo", "$bar$"] assert args == expected args = s.parse("echo $${FOO_BAR}$") expected = ["echo", "$baz$"] assert args == expected -}