on master help sign in
Merge pull request #198 from andreasabel/options-json2yaml

Re #195: json2yaml now uses optparse-applicative

commit81e9b2981e0662b2f539708171ec876297761909 authorMichael Snoyman <michael@snoyman.com> dateWed 15 Sep 2021, 14:58 (5 years ago) parentdb34ecf58a883c
6 files changed+154−56
yaml/README.md+8 yaml/exe/Common.hs+60 yaml/exe/json2yaml.hs+73−17 yaml/exe/yaml2json.hs+8−38 yaml/package.yaml+1 yaml/yaml.cabal+4−1
yaml/README.md+8
@@ -16,3 +16,11 @@ Usage examples can be found in the `Data.Yaml` documentation or in the [examples
1616* `Data.Yaml.Include` supports adding `!include` directives to your YAML files.
1717* `Data.Yaml.Builder` and `Data.Yaml.Parser` allow more fine-grained control of parsing an rendering, as opposed to just using the aeson typeclass and datatype system for parsing and rendering.
1818* `Data.Yaml.Aeson` is currently a re-export of `Data.Yaml` to explicitly choose to use the aeson-compatible API.
19+
20+### Executables
21+
22+Converters `json2yaml` and `yaml2json` can be built by disabling flag `no-exe`, e.g., one of:
23+```
24+cabal install yaml -f-no-exe
25+stack install yaml --flag yaml:-no-exe
26+```
yaml/exe/Common.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE LambdaCase #-}
3+
4+-- | Code shared between @yaml2json@ and @json2yaml@.
5+
6+module Common where
7+
8+import Data.List ( intercalate )
9+#if !MIN_VERSION_base(4,11,0)
10+import Data.Semigroup ( Semigroup(..) )
11+#endif
12+import Data.Version ( Version(versionBranch) )
13+
14+import Options.Applicative ( Parser, help, hidden, infoOption, long, short )
15+
16+import qualified Paths_yaml as Paths
17+
18+-- * Version info and option
19+---------------------------------------------------------------------------
20+
21+-- | Homepage for @yaml2json@ and @json2yaml@.
22+
23+homepage :: String
24+homepage = "https://github.com/snoyberg/yaml/"
25+
26+-- | Version in @x.y.z@ format.
27+
28+version :: String
29+version = intercalate "." $ map show $ versionBranch Paths.version
30+
31+-- | Option @--numeric-version@.
32+
33+numericVersionOption :: Parser (a -> a)
34+numericVersionOption =
35+ infoOption version
36+ $ long "numeric-version"
37+ <> hidden
38+ <> help "Show just version number."
39+
40+-- | Option @--version@.
41+
42+versionOption :: String -> Parser (a -> a)
43+versionOption self =
44+ infoOption (unwords versionWords)
45+ $ long "version"
46+ <> short 'V'
47+ <> hidden
48+ <> help "Show version info."
49+ where
50+ versionWords = [ self, "version", version, homepage ]
51+
52+-- * Misc
53+---------------------------------------------------------------------------
54+
55+-- | @Just@ unless argument is @"-"@ (denoting @stdin@ or @stdout@).
56+
57+dashToNothing :: FilePath -> Maybe FilePath
58+dashToNothing = \case
59+ "-" -> Nothing
60+ file -> Just file
yaml/exe/json2yaml.hs+73−17
@@ -1,22 +1,78 @@
1−import qualified Data.Aeson as J
2−import qualified Data.ByteString as S
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE NamedFieldPuns #-}
3+
4+import qualified Data.Aeson as J
5+import qualified Data.ByteString as S
36import qualified Data.ByteString.Lazy as L
4−import System.Environment (getArgs)
7+import qualified Data.Yaml as Y
8+#if !MIN_VERSION_base(4,11,0)
9+import Data.Semigroup ( Semigroup(..) )
10+#endif
11+
12+import Options.Applicative
13+ ( Parser
14+ , action, execParser, header, help, helper, info
15+ , metavar, strArgument, value
16+ )
17+
18+import System.Exit ( die )
19+
20+import Common ( dashToNothing, versionOption, numericVersionOption )
21+
22+data Options = Options
23+ { optInput :: Maybe FilePath
24+ -- ^ 'Nothing' means @stdin@.
25+ , optOutput :: Maybe FilePath
26+ -- ^ 'Nothing' means @stdout@.
27+ }
28+
29+-- | Name of the executable.
30+
31+self :: String
32+self = "json2yaml"
33+
34+-- | Parse options; handle parse errors, @--help@, @--version@.
535
6−import qualified Data.Yaml as Y
36+options :: IO Options
37+options =
38+ execParser $
39+ info (helper <*> versionOption self <*> numericVersionOption <*> programOptions)
40+ (header "Reads a JSON document and writes it out as YAML document.")
41+
42+ where
43+ programOptions = Options <$> oInput <*> oOutput
44+
45+ oInput :: Parser (Maybe FilePath)
46+ oInput = dashToNothing <$> do
47+ strArgument
48+ $ metavar "IN"
49+ <> value "-"
50+ <> action "file"
51+ <> help "The input file containing the JSON document; use '-' for stdin."
52+
53+ oOutput :: Parser (Maybe FilePath)
54+ oOutput = dashToNothing <$> do
55+ strArgument
56+ $ metavar "OUT"
57+ <> value "-"
58+ <> action "file"
59+ <> help "The file to hold the produced YAML document; use '-' for stdout."
60+
61+-- | Exit with 'self'-stamped error message.
62+
63+abort :: String -> IO a
64+abort msg = die $ concat [ self, ": ", msg ]
765
866main :: IO ()
967main = do
10− args <- getArgs
11− (input, output) <- case args ++ replicate (2 - length args) "-" of
12− [i, o] -> return (i, o)
13− _ -> fail "Usage: json2yaml [in] [out]"
14− mval <- fmap J.decode $
15− case input of
16− "-" -> L.getContents
17− _ -> L.readFile input
18− case mval of
19− Nothing -> error "Invalid input JSON"
20− Just val -> case output of
21− "-" -> S.putStr $ Y.encode (val :: Y.Value)
22− _ -> Y.encodeFile output val
68+ Options{ optInput, optOutput } <- options
69+
70+ -- Read JSON.
71+ mval <- J.decode <$> maybe L.getContents L.readFile optInput
72+
73+ -- Write YAML.
74+ case mval of
75+ Nothing -> abort "Invalid input JSON"
76+ Just val -> case optOutput of
77+ Nothing -> S.putStr $ Y.encode (val :: Y.Value)
78+ Just f -> Y.encodeFile f val
yaml/exe/yaml2json.hs+8−38
@@ -7,27 +7,26 @@ import Prelude hiding (putStr, getContents)
77import Data.Aeson (encode, Value)
88import Data.ByteString (getContents)
99import Data.ByteString.Lazy (putStr)
10−import Data.List (intercalate)
1110#if !MIN_VERSION_base(4,11,0)
1211import Data.Semigroup (Semigroup(..))
1312#endif
14−import Data.Version (Version(versionBranch))
1513import Data.Yaml (decodeFileEither, decodeEither')
1614
1715import Options.Applicative
1816 ( Parser
19− , action, execParser, header, help, helper, hidden
20− , info, infoOption, long, metavar, short, strArgument
17+ , action, execParser, header, help, helper, info
18+ , metavar, strArgument
2119 )
2220
2321import System.Exit
2422import System.IO (stderr, hPrint)
2523
26−import qualified Paths_yaml as Paths
24+import Common
25+ ( versionOption, numericVersionOption, dashToNothing )
2726
2827newtype Options = Options
2928 { optInput :: Maybe FilePath
30− -- ^ Nothing means @stdin@.
29+ -- ^ 'Nothing' means @stdin@.
3130 }
3231
3332-- | Name of the executable.
@@ -35,52 +34,23 @@ newtype Options = Options
3534self :: String
3635self = "yaml2json"
3736
38−-- | Homepage for this program.
39−
40−homepage :: String
41−homepage = "https://github.com/snoyberg/yaml/"
42−
43−-- | Version in @x.y.z@ format.
44−
45−version :: String
46−version = intercalate "." $ map show $ versionBranch Paths.version
47−
4837-- | Parse options; handle parse errors, @--help@, @--version@.
4938
5039options :: IO Options
5140options =
5241 execParser $
53− info (helper <*> versionOption <*> numericVersionOption <*> programOptions)
54− (header "Reads given YAML document and prints it JSON format to standard out.")
42+ info (helper <*> versionOption self <*> numericVersionOption <*> programOptions)
43+ (header "Reads given YAML document and prints it in JSON format to standard out.")
5544
5645 where
57− versionOption =
58− infoOption (unwords versionWords)
59− $ long "version"
60− <> short 'V'
61− <> hidden
62− <> help "Show version info."
63− versionWords = [ self, "version", version, homepage ]
64−
65− numericVersionOption =
66− infoOption version
67− $ long "numeric-version"
68− <> hidden
69− <> help "Show just version number."
70−
7146 programOptions = Options <$> oInput
7247
7348 oInput :: Parser (Maybe FilePath)
74− oInput = dashToStdin <$> do
49+ oInput = dashToNothing <$> do
7550 strArgument
7651 $ metavar "FILE"
7752 <> action "file"
7853 <> help "The input file containing the YAML document; use '-' for stdin."
79− where
80− -- dash is interpreted as stdin
81− dashToStdin = \case
82− "-" -> Nothing
83− file -> Just file
8454
8555showJSON :: Show a => Either a Value -> IO b
8656showJSON = \case
yaml/package.yaml+1
@@ -64,6 +64,7 @@ executables:
6464 main: json2yaml.hs
6565 source-dirs: exe
6666 dependencies:
67+ - optparse-applicative
6768 - yaml
6869 when:
6970 - condition: flag(no-exe)
yaml/yaml.cabal+4−1
@@ -5,7 +5,7 @@ cabal-version: 1.12
55-- see: https://github.com/sol/hpack
66
77name: yaml
8−version: 0.11.5.0
8+version: 0.11.6.0
99synopsis: Support for parsing and rendering YAML documents.
1010description: README and API documentation are available at <https://www.stackage.org/package/yaml>
1111category: Data
@@ -121,6 +121,7 @@ executable examples
121121executable json2yaml
122122 main-is: json2yaml.hs
123123 other-modules:
124+ Common
124125 Paths_yaml
125126 hs-source-dirs:
126127 exe
@@ -136,6 +137,7 @@ executable json2yaml
136137 , filepath
137138 , libyaml ==0.1.*
138139 , mtl
140+ , optparse-applicative
139141 , resourcet >=0.3 && <1.3
140142 , scientific >=0.3
141143 , template-haskell
@@ -151,6 +153,7 @@ executable json2yaml
151153executable yaml2json
152154 main-is: yaml2json.hs
153155 other-modules:
156+ Common
154157 Paths_yaml
155158 hs-source-dirs:
156159 exe