diff --git a/README.md b/README.md index f6d7ad4..231c77e 100644 --- a/README.md +++ b/README.md @@ -1 +1,126 @@ -# plated +Plated +====== + +[![Hackage](https://img.shields.io/badge/hackage-latest-green.svg)](https://hackage.haskell.org/package/plated) + +A dead-simple templating utility for simple shell interpolation. +Use at your own risk and only on trusted templates. + +Here's a simple use-case: + +```md +{{ # inside "post.md" }} +# My Blog Post + +by {{ echo $AUTHOR }} +Published on {{ date +'%B %d, %Y' }} + +Here's my blog post! + +--- +{{ cat ./footer.md | tr 'a-z' 'A-Z' }} +``` + +```md +{{ # inside "footer.md" }} +Copyright 2017 Chris Penner +Check me out on twitter @chrislpenner! +See you next time! +``` + +Then we can interpolate the template: + +``` +$ plated ./post.md +# My Blog Post + +by Chris Penner +Published on April 22, 2017 + +Here's my blog post! + +--- +COPYRIGHT 2017 CHRIS PENNER +CHECK ME OUT ON TWITTER @CHRISLPENNER! +SEE YOU NEXT TIME! +``` + +If you want you can add a shebang to the top of your template and just run it +as an executable, plated will strip the shebang for you automagically: + +> test.txt +``` +#!/path/to/plated +interpolate {{ echo $THIS }} +``` + +```bash +$ chmod +x test.txt +$ export THIS="that" +$ ./test.txt +interpolate that +``` + +[**Examples Here**](https://github.com/ChrisPenner/plated/examples/) + +Installation +============ + +`stack install plated` + +FAQ +===== + +There's really not much to it; parses the file and runs anything +inside `{{ }}` as a shell expression and pipes stdout into its spot. +If you're clever you can do pretty much anything you want with this. + +### Variables? + +Sure; It's bash. + +```bash +Hello, my name is {{echo $USER}} +``` + +You can set up environment overrides in `env.yaml`, plated looks up through +the file-system to find an `env.yaml` from the cwd NOT the template location. + +Here's an example env.yaml; we can do simple strings or commands here; just make +sure to quote any entries that start with `{{` or the YAML parser gets mad. + +> env.yaml +```yaml +PROJECT: Plated +DATE: "{{ date +'%B %d, %Y' }}" +``` + +Then you can use them just like normal variables. + +### For Loops? + +It's bash; go for it: +```bash +{{ for i in 99 98 97 ; do + cat < output: +``` +99 bottles of beer on the wall +98 bottles of beer on the wall +97 bottles of beer on the wall +``` + +### \_\_: command not found? + +Chances are you're forgetting to echo an env-var; +`{{ $TITLE }}` will try to run the contents of `$TITLE` as a command, you want +`{{ echo "$TITLE" }}`. + +### Isn't this whole thing a security risk? + +Probably. diff --git a/app/Main.hs b/app/Main.hs index e59bf3b..8849fa1 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -4,6 +4,7 @@ import System.Environment import System.Directory import Data.Foldable +import qualified Data.Map as M import Control.Monad.Reader import Plated.Options @@ -15,13 +16,16 @@ main = do envVars <- getEnvVars filenames <- getArgs templates <- traverse (templateFromFile >=> handleTemplateError) filenames - runReaderT (renderOutput templates) (Just envVars) + runReaderT (renderOutput templates) envVars where renderOutput = traverse_ (interpTemplate >=> liftIO . putStr) getEnvVars :: IO EnvVars getEnvVars = do cwd <- getCurrentDirectory - envVars <- getProjectOptions cwd - envTemplates <- traverse (handleTemplateError . parseTemplate "env.yaml") envVars - runReaderT (traverse interpTemplate envTemplates) mempty + globalEnvVars <- getEnvironment + localEnvVars <- getProjectOptions cwd + envTemplates <- traverse (handleTemplateError . parseTemplate "env.yaml") localEnvVars + interpolatedLocalEnvVars <- runReaderT (traverse interpTemplate envTemplates) mempty + -- Precedence to local env vars; last in list has precedence + return (globalEnvVars ++ M.toList interpolatedLocalEnvVars) diff --git a/examples/env.yaml b/examples/env.yaml index d8a056d..3af30e9 100644 --- a/examples/env.yaml +++ b/examples/env.yaml @@ -1,3 +1,2 @@ -date: "{{ date +'%B %d, %Y' }}" -title: Testing Templating -hello: "{{echo $hello}}" +DATE: "{{ date +'%B %d, %Y' }}" +TITLE: Basic Templating diff --git a/examples/footer.md b/examples/footer.md new file mode 100644 index 0000000..6a59131 --- /dev/null +++ b/examples/footer.md @@ -0,0 +1,3 @@ +Copyright 2017 Chris Penner +Check me out on twitter @chrislpenner! +See you next time! diff --git a/examples/simple.md b/examples/simple.md new file mode 100644 index 0000000..940afe5 --- /dev/null +++ b/examples/simple.md @@ -0,0 +1,15 @@ +{{ # inside "post.md" }} +# {{ echo $TITLE | tr 'a-z' 'A-Z' }} +by {{ echo $USER }} +Published on {{ echo $DATE }} + +Here's my blog post! + +{{ for i in 99 98 97 ; do + cat <=1.10 @@ -18,26 +18,25 @@ library exposed-modules: Plated.Template , Plated.Parser , Plated.Options - build-depends: base >= 4.7 && < 5 - , text + build-depends: base >= 4.9 && < 5 + , containers + , directory + , filepath + , mtl , parsec , process , yaml - , containers - , filepath - , directory - , mtl default-language: Haskell2010 -executable plated-exe +executable plated hs-source-dirs: app main-is: Main.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N - build-depends: base - , plated - , text + build-depends: base >= 4.9 && < 5 , directory , mtl + , plated + , containers default-language: Haskell2010 test-suite plated-test diff --git a/src/Plated/Options.hs b/src/Plated/Options.hs index 82399a6..c3f03d5 100644 --- a/src/Plated/Options.hs +++ b/src/Plated/Options.hs @@ -15,16 +15,17 @@ import System.FilePath import System.Directory import System.Exit -type EnvVars = M.Map String String +type EnvVars = [(String, String)] +type EnvMap = M.Map String String -getProjectOptions :: FilePath -> IO EnvVars +getProjectOptions :: FilePath -> IO EnvMap getProjectOptions path = do mProjSettingsFile <- findProjSettings path mOptions <- traverse optionsFromFilename mProjSettingsFile return $ fromMaybe mempty mOptions -- Retrieve an options object from a yaml file -optionsFromFilename :: FilePath -> IO EnvVars +optionsFromFilename :: FilePath -> IO EnvMap optionsFromFilename = Y.decodeFileEither >=> \case Left err -> die . prettyPrintParseException $ err @@ -40,4 +41,4 @@ recurseUp :: FilePath -> [FilePath] recurseUp = unfoldr go where go "/" = Nothing - go path = Just (takeDirectory path, takeDirectory path) + go path = Just (path, takeDirectory path) diff --git a/src/Plated/Parser.hs b/src/Plated/Parser.hs index b559cc9..c09f91d 100644 --- a/src/Plated/Parser.hs +++ b/src/Plated/Parser.hs @@ -31,7 +31,7 @@ handleTemplateError (Right temp) = return temp templateP :: Parser (Template Command) templateP = "template" ?> do - optional shebangP + optional (try shebangP) contents <- many (cmd <|> txt) eof return $ Template contents @@ -41,10 +41,11 @@ templateP = "template" ?> do shebangP :: Parser String shebangP = "shebang" ?> - liftA2 (++) (string "#!") (manyTill anyChar (char '\n')) + liftA2 (++) (lookAhead (string "#!") *> string "#!") (manyTill anyChar (char '\n')) commandP :: Parser Command commandP = "command" ?> do _ <- string "{{" cmdString <- manyTill anyChar (string "}}") + optional newline return $ Command cmdString diff --git a/src/Plated/Template.hs b/src/Plated/Template.hs index 20396b7..888ceb4 100644 --- a/src/Plated/Template.hs +++ b/src/Plated/Template.hs @@ -10,7 +10,6 @@ import System.Process import Control.Monad.Reader import Data.Foldable -import qualified Data.Map as M import Plated.Options @@ -18,7 +17,7 @@ data Template a = Template [Either String a] deriving Show -interpTemplate :: (MonadReader (Maybe EnvVars) m, MonadIO m) => Template Command -> m String +interpTemplate :: (MonadReader EnvVars m, MonadIO m) => Template Command -> m String interpTemplate (Template elems) = fold <$> mapM toText elems where toText = either return interpCommand @@ -27,8 +26,8 @@ data Command = Command String deriving Show -interpCommand :: (MonadReader (Maybe EnvVars) m, MonadIO m) => Command -> m String +interpCommand :: (MonadReader EnvVars m, MonadIO m) => Command -> m String interpCommand (Command cmd) = do envVars <- ask - let process = (shell cmd){env=M.toList <$> envVars} + let process = (shell cmd){env=Just envVars} liftIO $ readCreateProcess process ""