diff --git a/cli/GingerCLI.hs b/cli/GingerCLI.hs
--- a/cli/GingerCLI.hs
+++ b/cli/GingerCLI.hs
@@ -19,6 +19,7 @@
 import System.IO
 import System.IO.Error
 import qualified Data.ByteString as BS
+import qualified Data.ByteString.UTF8 as UTF8
 import qualified Data.ByteString.Lazy as LBS
 import Control.Monad.Trans.Class ( lift )
 import Control.Monad.Trans.Maybe
@@ -26,7 +27,85 @@
 import Data.Default ( def )
 import System.Process as Process
 import Text.Printf (printf)
+import Options (parseOptions, Options (..), TemplateSource (..), DataSource (..))
+import System.Exit
 
+main :: IO ()
+main = do
+    args <- getArgs
+    options <- parseOptions args
+    case options of
+      RunOptions tpl dat ->
+        run tpl dat
+
+loadData :: DataSource -> IO (Maybe (HashMap Text JSON.Value))
+loadData (DataFromFile fn) = decodeFile fn
+loadData DataFromStdin = decodeStdin
+loadData (DataLiteral str) = decodeString str
+
+loadTemplate :: TemplateSource -> IO (Template SourcePos)
+loadTemplate tplSrc = do
+    let resolve = loadFileMay
+    (tpl, src) <- case tplSrc of
+        TemplateFromFile fn -> (,) <$> parseGingerFile resolve fn <*> return Nothing
+        TemplateFromStdin -> getContents >>= \s -> (,) <$> parseGinger resolve Nothing s <*> return (Just s)
+
+    case tpl of
+        Left err -> do
+            tplSource <-
+                case src of
+                    Just s ->
+                        return (Just s)
+                    Nothing -> do
+                        let s = sourceName <$> peSourcePosition err
+                        case s of
+                            Nothing -> return Nothing
+                            Just sn -> Just <$> loadFile sn
+            printParserError tplSource err
+            exitFailure
+        Right t -> do
+            return t
+
+
+run :: TemplateSource -> DataSource -> IO ()
+run tplSrc dataSrc = do
+    scope <- loadData dataSrc
+    let scopeLookup key = toGVal (scope >>= HashMap.lookup key)
+    let contextLookup :: Text -> Run p IO Html (GVal (Run p IO Html))
+        contextLookup key =
+            case key of
+                "print" -> return printF
+                "system" -> return systemF
+                _ -> return $ scopeLookup key
+    let context =
+            makeContextHtmlExM
+                contextLookup
+                (putStr . Text.unpack . htmlSource)
+                (hPutStrLn stderr . show)
+
+    tpl <- loadTemplate tplSrc
+    runGingerT context tpl >>= either (hPutStrLn stderr . show) showOutput
+    where
+      showOutput value
+        | isNull value = return ()
+        | otherwise = putStrLn . show $ value
+
+
+printParserError :: Maybe String -> ParserError -> IO ()
+printParserError srcMay = putStrLn . formatParserError srcMay
+
+displayParserError :: String -> ParserError -> IO ()
+displayParserError src pe = do
+    case peSourcePosition pe of
+        Just pos -> do
+            let ln = Prelude.take 1 . Prelude.drop (sourceLine pos - 1) . Prelude.lines $ src
+            case ln of
+                [] -> return ()
+                x:_ -> do
+                    putStrLn x
+                    putStrLn $ Prelude.replicate (sourceColumn pos - 1) ' ' ++ "^"
+        _ -> return ()
+
 loadFile fn = openFile fn ReadMode >>= hGetContents
 
 loadFileMay fn =
@@ -40,6 +119,12 @@
 decodeFile :: (JSON.FromJSON v) => FilePath -> IO (Maybe v)
 decodeFile fn = YAML.decode <$> (openFile fn ReadMode >>= BS.hGetContents)
 
+decodeString :: (JSON.FromJSON v) => String -> IO (Maybe v)
+decodeString = return . YAML.decode . UTF8.fromString
+
+decodeStdin :: (JSON.FromJSON v) => IO (Maybe v)
+decodeStdin = YAML.decode <$> BS.getContents
+
 printF :: GVal (Run p IO Html)
 printF = fromFunction $ go
     where
@@ -66,69 +151,3 @@
 strToGVal :: String -> GVal m
 strToGVal = toGVal . Text.pack
 
-main = do
-    args <- getArgs
-    let (srcFn, scopeFn) = case args of
-            [] -> (Nothing, Nothing)
-            a:[] -> (Just a, Nothing)
-            a:b:[] -> (Just a, Just b)
-
-    scope <- case scopeFn of
-        Nothing -> return Nothing
-        Just fn -> (decodeFile fn :: IO (Maybe (HashMap Text JSON.Value)))
-
-    let scopeLookup key = toGVal (scope >>= HashMap.lookup key)
-        resolve = loadFileMay
-    let contextLookup :: Text -> Run p IO Html (GVal (Run p IO Html))
-        contextLookup key =
-            case key of
-                "print" -> return printF
-                "system" -> return systemF
-                _ -> return $ scopeLookup key
-
-    (tpl, src) <- case srcFn of
-            Just fn -> (,) <$> parseGingerFile resolve fn <*> return Nothing
-            Nothing -> getContents >>= \s -> (,) <$> parseGinger resolve Nothing s <*> return (Just s)
-
-    -- TODO: do some sort of arg parsing thing so that we can turn
-    -- template dumping on or off.
-    -- print tpl
-
-    case tpl of
-        Left err -> do
-            tplSource <-
-                case src of
-                    Just s ->
-                        return (Just s)
-                    Nothing -> do
-                        let s = sourceName <$> peSourcePosition err
-                        case s of
-                            Nothing -> return Nothing
-                            Just sn -> Just <$> loadFile sn
-            printParserError tplSource err
-        Right t -> do
-            let context =
-                    makeContextHtmlExM
-                        contextLookup
-                        (putStr . Text.unpack . htmlSource)
-                        (hPutStrLn stderr . show)
-            runGingerT context t >>= either (hPutStrLn stderr . show) showOutput
-    where
-      showOutput value
-        | isNull value = return ()
-        | otherwise = putStrLn . show $ value
-
-printParserError :: Maybe String -> ParserError -> IO ()
-printParserError srcMay = putStrLn . formatParserError srcMay
-
-displayParserError :: String -> ParserError -> IO ()
-displayParserError src pe = do
-    case peSourcePosition pe of
-        Just pos -> do
-            let ln = Prelude.take 1 . Prelude.drop (sourceLine pos - 1) . Prelude.lines $ src
-            case ln of
-                [] -> return ()
-                x:_ -> do
-                    putStrLn x
-                    putStrLn $ Prelude.replicate (sourceColumn pos - 1) ' ' ++ "^"
-        _ -> return ()
diff --git a/cli/Options.hs b/cli/Options.hs
new file mode 100644
--- /dev/null
+++ b/cli/Options.hs
@@ -0,0 +1,69 @@
+module Options
+where
+
+import Options.Applicative
+
+
+data TemplateSource
+  = TemplateFromFile FilePath
+  | TemplateFromStdin
+
+data DataSource
+  = DataFromFile FilePath
+  | DataLiteral String
+  | DataFromStdin
+
+data Options
+  = RunOptions TemplateSource DataSource
+
+parseOptions :: [String] -> IO Options
+parseOptions args =
+  execParser $ info (options <**> helper)
+    ( fullDesc
+    <> header "ginger - A command-line interface for the Ginger template language"
+    )
+
+options :: Parser Options
+options = runOptions
+
+runOptions :: Parser Options
+runOptions =
+  RunOptions <$> templateSource <*> dataSource
+
+templateSource :: Parser TemplateSource
+templateSource =
+  convert <$> option str
+      ( long "template"
+      <> short 't'
+      <> metavar "TEMPLATE"
+      <> help "Load ginger template from this file"
+      <> value "-"
+      )
+  where
+    convert "-" = TemplateFromStdin
+    convert f = TemplateFromFile f
+
+dataSource :: Parser DataSource
+dataSource =
+  dataFromFile <|> dataLiteral
+
+dataFromFile :: Parser DataSource
+dataFromFile =
+  convert <$> option str
+        ( long "data-file"
+        <> metavar "DATAFILE"
+        <> help "Load JSON or YAML data from this file (`-' to read from stdin)"
+        )
+  where
+    convert "-" = DataFromStdin
+    convert f = DataFromFile f
+
+dataLiteral :: Parser DataSource
+dataLiteral =
+  DataLiteral <$> option str
+        ( long "data"
+        <> short 'd'
+        <> metavar "DATA"
+        <> help "Use specified (JSON or YAML) DATA"
+        <> value "{}"
+        )
diff --git a/ginger.cabal b/ginger.cabal
--- a/ginger.cabal
+++ b/ginger.cabal
@@ -2,11 +2,11 @@
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 name:                ginger
-version:             0.8.4.0
+version:             0.8.4.1
 synopsis:            An implementation of the Jinja2 template language in Haskell
 description:         Ginger is Jinja, minus the most blatant pythonisms. Wants
                      to be feature complete, but isn't quite there yet.
-homepage:            https://bitbucket.org/tdammers/ginger
+homepage:            https://ginger.tobiasdammers.nl/
 license:             MIT
 license-file:        LICENSE
 author:              Tobias Dammers
@@ -20,6 +20,10 @@
 cabal-version:       >=1.10
 bug-reports:         https://github.com/tdammers/ginger/issues
 
+source-repository head
+    type: git
+    location: https://github.com/tdammers/ginger
+
 library
   exposed-modules: Text.Ginger
                  , Text.Ginger.AST
@@ -36,9 +40,9 @@
   -- other-modules:
   -- other-extensions:
   build-depends: base >=4.8 && <5
-               , aeson
-               , aeson-pretty
-               , bytestring
+               , aeson >=1.4.2.0 && <1.5
+               , aeson-pretty >=0.8.7 && <0.9
+               , bytestring >=0.10.8.2 && <0.11
                , data-default >= 0.5
                , filepath >= 1.3
                , http-types >= 0.8 && (< 0.11 || >= 0.12)
@@ -47,12 +51,12 @@
                , regex-tdfa >=1.2.3 && <=1.3
                , safe >= 0.3
                , scientific >= 0.3
-               , text
+               , text >=1.2.3.1 && <1.3
                , time >= 0.1.6.0
                , transformers >= 0.3
                , unordered-containers >= 0.2.5
-               , utf8-string
-               , vector
+               , utf8-string >=1.0.1.1 && <1.1
+               , vector >=0.12.0.2 && <0.13
   if !impl(ghc >= 8.0)
     build-depends: semigroups == 0.18.*
   hs-source-dirs:      src
@@ -60,18 +64,21 @@
 
 executable ginger
     main-is: GingerCLI.hs
+    other-modules: Options
     hs-source-dirs: cli
     default-language:    Haskell2010
     build-depends: base >= 4.8 && <5
-                 , bytestring
-                 , data-default >= 0.5
                  , ginger
-                 , aeson
-                 , process
-                 , text
-                 , transformers
+                 , aeson >=1.4.2.0 && <1.5
+                 , bytestring >=0.10.8.2 && <0.11
+                 , data-default >= 0.5
+                 , optparse-applicative >=0.14.3.0 && <0.15
+                 , process >=1.6.5.0 && <1.7
+                 , text >=1.2.3.1 && <1.3
+                 , transformers >=0.5.6.2 && <0.6
                  , unordered-containers >= 0.2.5
-                 , yaml
+                 , utf8-string >=1.0.1.1 && <1.1
+                 , yaml >=0.11.0.0 && <0.12
 
 test-suite tests
     type: exitcode-stdio-1.0
@@ -82,15 +89,15 @@
     default-language: Haskell2010
     build-depends: base >=4.8 && <5
                  , ginger
-                 , aeson
-                 , bytestring
-                 , data-default
-                 , mtl
-                 , tasty
-                 , tasty-hunit
-                 , tasty-quickcheck
-                 , text
-                 , time
-                 , transformers
+                 , aeson >=1.4.2.0 && <1.5
+                 , bytestring >=0.10.8.2 && <0.11
+                 , data-default >=0.5
+                 , mtl >=2.2.2 && <2.3
+                 , tasty >=1.2.1 && <1.3
+                 , tasty-hunit >=0.10.0.1 && <0.11
+                 , tasty-quickcheck >=0.10 && <0.11
+                 , text >=1.2.3.1 && <1.3
+                 , time >= 0.1.6.0
+                 , transformers >=0.5.6.2 && <0.6
                  , unordered-containers >= 0.2.5
-                 , utf8-string
+                 , utf8-string >=1.0.1.1 && <1.1
diff --git a/src/Text/Ginger/Run.hs b/src/Text/Ginger/Run.hs
--- a/src/Text/Ginger/Run.hs
+++ b/src/Text/Ginger/Run.hs
@@ -166,7 +166,7 @@
     , ("is_lt", fromFunction gfnLess)
     , ("iterable", fromFunction . unaryFunc $ toGVal . (\x -> isList x || isDict x))
     , ("json", fromFunction gfnJSON)
-    , ("length", fromFunction . unaryFunc $ toGVal . length)
+    , ("length", fromFunction gfnLength)
     , ("le", fromFunction gfnLessEquals)
     , ("less", fromFunction gfnLess)
     , ("lessthan", fromFunction gfnLess)
diff --git a/src/Text/Ginger/Run/Builtins.hs b/src/Text/Ginger/Run/Builtins.hs
--- a/src/Text/Ginger/Run/Builtins.hs
+++ b/src/Text/Ginger/Run/Builtins.hs
@@ -272,6 +272,18 @@
 gfnCenter ((_, s):(_, w):(_, pad):_) =
     return . toGVal $ center (asText s) (fromMaybe 80 $ Prelude.truncate <$> asNumber w) (asText pad)
 
+gfnLength :: Monad m => Function (Run p m h)
+gfnLength [] =
+  return def
+gfnLength ((_,x):_) =
+  case (asDictItems x, asList x) of
+    (Nothing, Nothing) ->
+      return . toGVal . Text.length . asText $ x
+    (Just items, _) ->
+      return . toGVal . Prelude.length $ items
+    (_, Just items) ->
+      return . toGVal . Prelude.length $ items
+
 gfnSlice :: Monad m => Function (Run p m h)
 gfnSlice args =
     let argValues =
diff --git a/test/Text/Ginger/SimulationTests.hs b/test/Text/Ginger/SimulationTests.hs
--- a/test/Text/Ginger/SimulationTests.hs
+++ b/test/Text/Ginger/SimulationTests.hs
@@ -745,10 +745,20 @@
         -- TODO
         -- \"iterable\"
         -- TODO
-        , testCase "\"length\"" $ do
-            mkTestHtml [] []
-                "{{ [1,2,3]|length }}"
-                "3"
+        , testGroup "\"length\""
+            [ testCase "list" $ do
+              mkTestHtml [] []
+                  "{{ [1,2,3]|length }}"
+                  "3"
+            , testCase "dict" $ do
+              mkTestHtml [] []
+                  "{{ {'foo':'bar', 'baz':'quux'}|length }}"
+                  "2"
+            , testCase "string" $ do
+              mkTestHtml [] []
+                  "{{ 'foo'|length }}"
+                  "3"
+            ]
         -- \"modulo\"
         -- \"num\"
         -- TODO
