packages feed

file-templates (empty) → 1.0.0.0

raw patch · 3 files changed

+219/−0 lines, 3 filesdep +attoparsecdep +basedep +bytestringsetup-changed

Dependencies added: attoparsec, base, bytestring, directory, filepath, foundation, transformers, unordered-containers

Files

+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ file-templates.cabal view
@@ -0,0 +1,27 @@+name: file-templates+version: 1.0.0.0+cabal-version: >=1.10+build-type: Simple+license: BSD3+copyright: BSD3+maintainer: anfelor@posteo.de+homepage: https://github.com/anfelor/file-templates#readme+synopsis: Use templates for files and directories+category: Web+author: Anton Felix Lorenzen++executable new+    main-is: Main.hs+    build-depends:+        base >=4.7 && <5,+        foundation >=0.0.9 && <0.1,+        directory >=1.3.0.0 && <1.4,+        attoparsec >=0.13.1.0 && <0.14,+        filepath >=1.4.1.1 && <1.5,+        bytestring >=0.10.8.1 && <0.11,+        unordered-containers >=0.2.8.0 && <0.3,+        transformers >=0.5.2.0 && <0.6+    default-language: Haskell2010+    default-extensions: NoImplicitPrelude OverloadedStrings+    hs-source-dirs: src+
+ src/Main.hs view
@@ -0,0 +1,190 @@+module Main where++import Foundation+import Control.Exception+import Control.Monad+import Control.Monad.Trans.Class+import Control.Monad.Trans.State.Strict+import Data.ByteString (ByteString)+import Data.HashMap.Strict (HashMap)+import Data.Traversable+import qualified Data.Attoparsec.ByteString as Attoparsec+import qualified Data.ByteString+import qualified Data.ByteString.Char8+import qualified Data.Char+import qualified Data.HashMap.Strict as HashMap+import qualified Data.List+import qualified GHC.Base+import qualified System.Environment+import qualified System.Exit+import System.Directory+import System.FilePath+++-- | Files and file names are parsed into a list of this type.+data Content+  = PromptVar ByteString -- ^ prompt the user for a substitution+  | EnvVar ByteString -- ^ a shell environment variable+  | String ByteString -- ^ normal text+++-- | Parse one chunk of 'Content'.+parseChunk :: Attoparsec.Parser Content+parseChunk = Attoparsec.choice+  [ do s <- Attoparsec.takeWhile1 (/=questionMark) -- '?' question mark+       pure (String s)+  , Attoparsec.try $ do+      Attoparsec.word8 questionMark+      mode <- Attoparsec.takeWhile validChar+      Attoparsec.word8 questionMark+      name <- Attoparsec.takeWhile validChar+      Attoparsec.word8 questionMark+      case mode of+        "env" -> pure $ EnvVar name+        "" -> pure $ PromptVar name+        _ -> fail $ "Couldn't recognize mode: " <> bsToChars mode++    -- Allow for stand-alone question marks, like in the common ternary operator ? :+  , (String . Data.ByteString.singleton) <$> Attoparsec.word8 questionMark+  ]+  where+    questionMark = 63+    space = 32+    validChar c = c /= questionMark+               && c /= space+++bsToChars :: ByteString -> [Char]+bsToChars = fmap (Data.Char.chr . fromIntegral) . Data.ByteString.unpack+++charsToBs :: [Char] -> ByteString+charsToBs = Data.ByteString.pack . fmap (fromIntegral . Data.Char.ord)+++parseBS :: ByteString -> Either GHC.Base.String [Content]+parseBS = Attoparsec.parseOnly (Attoparsec.many' parseChunk <* Attoparsec.endOfInput)+++-- | Given a list of variables, that the user has already given a meaning to,+-- convert a list of 'Content's into a bytestring, by substituting the strings+-- given by the user and the content of environment variables for the 'Content' variables+-- and concatenating the resulting strings (without spaces, etc.).+convert :: [Content] -> StateT (HashMap ByteString ByteString) IO ByteString+convert con = do+  asked <- get+  bss <- forM con $ \c -> case c of+    String bs -> pure bs+    EnvVar var -> do+      menv <- lift $ System.Environment.lookupEnv (bsToChars var)+      case menv of+        Nothing -> lift $ throwIO $ CouldntFindEnvVar var+        Just str -> pure $ charsToBs str+    PromptVar var ->+      case HashMap.lookup var asked of+        Just res -> pure res+        Nothing -> do+          lift $ Data.ByteString.putStr $ "Please give a value for '" <> var <> "': "+          res <- lift Data.ByteString.getLine+          modify' (HashMap.insert var res)+          pure res+  pure (mconcat bss)+++-- | Return all files in a directory.+-- > allFiles "/home/xyz"+-- [".zsh_rc", "Documents/CV.docx", ..]+allFiles :: FilePath -> IO [FilePath]+allFiles = flip go ""+  where+    go dir prefix = do+      content <- listDirectory dir+      dirs <- filterM (doesDirectoryExist . (</>) dir) content+      subfiles <- mconcat <$> traverse (\d -> go (dir </> d) (prefix </> d)) dirs+      let files = fmap (prefix </>) $ diff content dirs+      pure $ files <> subfiles++    -- Unlike \\, this works in O(n) time relative to the first list.+    diff (x:xs) (y:ys) | x == y = diff xs ys+    diff (x:xs) (y:ys) | x /= y = x : diff xs (y:ys)+    diff xs [] = xs+    diff [] ys = undefined+++data TemplateExceptions+  = CouldntParseFilename ByteString GHC.Base.String -- ^ name and attoparsec info+  | CouldntParseFile ByteString GHC.Base.String -- ^ name and attoparsec info+  | CouldntFindEnvVar ByteString+  deriving (Eq, Show)++instance Exception TemplateExceptions where+  displayException (CouldntParseFilename name info) =+    "Couldn't parse filename '" <> bsToChars name <> "': \n" <> info+  displayException (CouldntParseFile name info) =+    "Couldn't parse file '" <> bsToChars name <> "': \n" <> info+  displayException (CouldntFindEnvVar var) =+    "Couldn't find the environment variable '" <> bsToChars var <> "'."+++-- | Print the first argument and then the second,+-- and wait for 'j' or 'n' (spaces get trimmed, case-insensitive).+-- When the user enters something else, print the second argument+-- and wait for another input and so forth.+-- > askUser "This is bad." "Continue? [j/N]"+askUser :: ByteString -> ByteString -> IO Bool+askUser msg repeat = do+  Data.ByteString.Char8.putStrLn msg+  go+  where+    go = do+      Data.ByteString.Char8.putStr repeat+      r <- Data.ByteString.Char8.getLine+      let trimmed = fst $ Data.ByteString.Char8.spanEnd Data.Char.isSpace+                        $ Data.ByteString.Char8.dropWhile Data.Char.isSpace r+      let lower = Data.ByteString.Char8.map Data.Char.toLower trimmed+      case lower of+        "j" -> pure True+        "n" -> pure False+        _ -> go+++main :: IO ()+main = do+  [name] <- getArgs+  templateDir <- (</> toList name) <$> getAppUserDataDirectory "templates"+  templateFiles <- allFiles templateDir++  -- Parse the filenames and resolve the variables in them.+  (destFiles, asked) <- runStateT+    (mapM (convert <=< parseFilename . charsToBs) templateFiles)+    HashMap.empty++  -- Check whether some files are already present.+  existingFiles <- filterM (doesFileExist . bsToChars) destFiles+  when (not $ null existingFiles) $ do+    overwrite <- askUser+      ("The following files already exist:\n"+      <> Data.ByteString.Char8.unlines (fmap ("  "<>) existingFiles)+      ) "Overwrite? [y/N]"+    when (not overwrite) $ do+      System.Exit.exitFailure++  -- Copy the file content.+  flip evalStateT asked+    $ forM_ (Data.List.zip (fmap (templateDir </>) templateFiles)+                           (fmap bsToChars destFiles))+    $ \(tf, df) -> do+      bs <- lift $ Data.ByteString.readFile tf+      cs <- parseFile df bs+      completed <- convert cs+      lift $ createDirectoryIfMissing True (dropFileName df)+      lift $ Data.ByteString.writeFile df completed++  where+    parseFilename bs = case parseBS bs of+      Right a -> pure a+      Left s -> lift $ throwIO $ CouldntParseFilename bs s++    parseFile name bs = case parseBS bs of+      Right a -> pure a+      Left s -> lift $ throwIO $ CouldntParseFile (Data.ByteString.Char8.pack name) s