document-build-0.2.0.0: src/Document/Build/CommandLine.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall #-}
{- | A program and its arguments.
A 'CommandLine' is a product: its fields are named 'commandLineProgram' and
'commandLineArguments'.
-}
module Document.Build.CommandLine (
-- * Type
CommandLine (..),
-- * Display
displayCommandLine,
shellQuote,
-- * Optics
CommandLineLike (..),
lawNamesCommandLine,
InheritCommandLine,
inheritCommandLine,
) where
import Control.DeepSeq (NFData)
import Control.Lens (Optic, cloneLens, view)
import Data.Data (Data)
import Data.Functor.Const (Const)
import Document.Build.Name.CommandLineArguments (CommandLineArgumentsLike (..))
import Document.Build.Name.CommandLineProgram (CommandLineProgramLike (..))
import GHC.Generics (Generic)
{- $setup
>>> import Control.Lens (cloneLens, view)
-}
{- | A program ('commandLineProgram') and its arguments
('commandLineArguments').
>>> view (cloneLens commandLineProgram) (CommandLine "pdfinfo" ["my file.pdf"])
"pdfinfo"
>>> view (cloneLens commandLineArguments) (CommandLine "pdfinfo" ["my file.pdf"])
["my file.pdf"]
@since 0.1.0.0
-}
data CommandLine
= CommandLine FilePath [String]
deriving stock (Eq, Ord, Show, Generic, Data)
deriving anyclass (NFData)
{- | A command line as it would be written in a POSIX shell.
>>> displayCommandLine (CommandLine "pdfinfo" ["my file.pdf", "-box"])
"pdfinfo 'my file.pdf' -box"
@since 0.1.0.0
-}
displayCommandLine :: CommandLine -> String
displayCommandLine c =
unwords (fmap shellQuote (view (cloneLens commandLineProgram) c : view (cloneLens commandLineArguments) c))
{- | Quote a word for a POSIX shell, if it needs quoting.
>>> shellQuote "public/report.pdf"
"public/report.pdf"
>>> shellQuote "it's"
"'it'\\''s'"
>>> shellQuote ""
"''"
@since 0.1.0.0
-}
shellQuote :: String -> String
shellQuote s =
let safe c = c `elem` ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_./=:,+%@" :: String)
quote c = if c == '\'' then "'\\''" else [c]
in if not (null s) && all safe s
then s
else "'" <> concatMap quote s <> "'"
{- | The class of 'CommandLine'.
@since 0.2.0.0
-}
class CommandLineLike p f x x' where
commandLine ::
Optic p f x x' CommandLine CommandLine
{- | Identity
@since 0.2.0.0
-}
instance (x' ~ CommandLine) => CommandLineLike p f CommandLine x' where
commandLine =
id
{-# INLINE commandLine #-}
{- | Lens
@since 0.2.0.0
-}
instance (p ~ (->), Functor f, x' ~ CommandLine) => CommandLineProgramLike p f CommandLine x' where
commandLineProgram f (CommandLine p as) =
fmap (`CommandLine` as) (f p)
{-# INLINE commandLineProgram #-}
{- | Lens
@since 0.2.0.0
-}
instance (p ~ (->), Functor f, x' ~ CommandLine) => CommandLineArgumentsLike p f CommandLine x' where
commandLineArguments f (CommandLine p as) =
fmap (CommandLine p) (f as)
{-# INLINE commandLineArguments #-}
{- | Rebuild a value using only the name of each field.
Laws:
> lawNamesCommandLine == id
@since 0.2.0.0
-}
lawNamesCommandLine :: CommandLine -> CommandLine
lawNamesCommandLine s =
CommandLine (view (cloneLens commandLineProgram) s) (view (cloneLens commandLineArguments) s)
{- | The classes that a data type holding 'CommandLine' inherits.
@since 0.2.0.0
-}
type InheritCommandLine s =
( CommandLineLike (->) (Const ()) s s
, CommandLineProgramLike (->) (Const ()) s s
, CommandLineArgumentsLike (->) (Const ()) s s
)
{- | Check, where it is applied, that @s@ inherits every name of
'CommandLine'.
@since 0.2.0.0
-}
inheritCommandLine :: (InheritCommandLine s) => proxy s -> ()
inheritCommandLine _ =
()