packages feed

hsdev-0.1.3.0: src/System/Win32/PowerShell.hs

{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverlappingInstances, GeneralizedNewtypeDeriving, LambdaCase #-}

module System.Win32.PowerShell (
	-- * Run PowerShell
	ps,
	-- * PowerShell functions
	startProcess, codepage, outNull,
	-- * PowerShell monad
	PS(..), seqPS, emit, emit_, (=:), invoke,
	-- * Expression
	Args(..), CmdLet(..), Expr(..), compile,
	raw, name, lit, var, bra, cbra, (.=), flag, named, call, cmdlet, lambda, (.|), foreach, filter,
	-- * Convertible
	ToPS(..),
	-- * Escape functions
	translate, translateArg,
	quote, quoteDouble,
	escape
	) where

import Prelude hiding (filter)

import Control.Applicative (Applicative(..))
import Control.Monad
import Control.Monad.Writer
import Data.Char (isAlphaNum)
import Data.List (intercalate)
import qualified Data.List as List (filter)
import Data.Map (Map)
import qualified Data.Map as M

import HsDev.Tools.Base (ToolM, tool_)

ps :: PS a -> ToolM String
ps script = tool_ "powershell" ["-Command", compile $ seqPS script]

-- start-process
startProcess :: String -> [String] -> CmdLet
startProcess n as = cmdlet "start-process" [name n, lit $ intercalate ", " (map translateArg as)] [named "WindowStyle" $ lit "Hidden"]

-- chcp
codepage :: Int -> CmdLet
codepage n = cmdlet "chcp" [lit n] []

-- out-null
outNull :: CmdLet
outNull = cmdlet "out-null" [] []

newtype PS a = PS { unPS :: Writer [Expr] a }
	deriving (Applicative, Functor, Monad, MonadWriter [Expr])

seqPS :: PS a -> Expr
seqPS (PS act) = case execWriter act of
	[] -> error "No expressions"
	es -> foldr1 Sequence es

emit :: Expr -> PS Expr
emit e = PS $ tell [e] >> return e

emit_ :: Expr -> PS ()
emit_ = void . emit

infixr 6 =:
(=:) :: String -> Expr -> PS Expr
n =: expr = emit_ (n .= expr) >> return (var n)

-- | Invoke cmdlet
invoke :: CmdLet -> PS ()
invoke = emit_ . Invoke

-- | Positional and named args
data Args = Args [Expr] (Map String (Maybe Expr))

instance Monoid Args where
	mempty = Args [] M.empty
	mappend (Args lp ln) (Args rp rn) = Args (lp ++ rp) (M.union ln rn)

-- | Call to cmdlet
data CmdLet = CmdLet {
	cmdLet :: Expr,
	cmdArgs :: Args }

-- | Expression
data Expr =
	Emit String |
	-- ^ native expression
	Literal String |
	-- ^ literal
	Var String |
	-- ^ $name
	Bracket Expr |
	-- ^ (expr)
	Assign [String] [Expr] |
	-- ^ $x, $y, ... = expr1, expr2, ...
	Invoke CmdLet |
	-- ^ cmd args... named...
	Lambda [String] ([Expr] -> Expr) |
	-- ^ { param($...); expr }
	Sequence Expr Expr |
	-- ^ expr1; expr2
	Pipe Expr CmdLet
	-- ^ expr | cmd

compile :: Expr -> String
compile (Emit s) = s
compile (Literal l) = l
compile (Var v) = '$':v
compile (Bracket e) = "(" ++ compile e ++ ")"
compile (Assign vs es) = intercalate ", " (map ('$':) vs) ++ " = " ++ intercalate ", " (map (compile . cbra) es)
compile (Invoke (CmdLet e (Args p ns))) = unwords $ List.filter (not . null) [invoke', p', ns'] where
	invoke' = case e of
		Var n -> n
		_ -> unwords ["&", compile $ cbra e]
	p' = intercalate " " $ map (compile . cbra) p
	ns' = intercalate " " $ concatMap named' $ M.toList ns where
		named' :: (String, Maybe Expr) -> [String]
		named' (n, Just v) = ['-':n, compile $ cbra v]
		named' (n, Nothing) = ['-':n]
-- { param($File); $args[0]; }
compile (Lambda ns body) = unwords ["{", param', body', "}"] where
	param' = "param(" ++ intercalate "," ns ++ ");"
	body' = compile $ body (map var ns)
compile (Sequence l r) = compile l ++ "; " ++ compile r
compile (Pipe e c) = compile e ++ " | " ++ compile (Invoke c)

raw :: String -> Expr
raw = Emit

name :: String -> Expr
name = Literal

lit :: ToPS a => a -> Expr
lit = Literal . toPS

var :: String -> Expr
var = Var

bra :: Expr -> Expr
bra = Bracket

cbra :: Expr -> Expr
cbra e@(Literal _) = e
cbra v@(Var _) = v
cbra b = bra b

infixr 6 .= 
(.=) :: String -> Expr -> Expr
v .= e = Assign [v] [e]

flag :: String -> (String, Maybe Expr)
flag n = (n, Nothing)

named :: String -> Expr -> (String, Maybe Expr)
named n e = (n, Just e)

call :: Expr -> [Expr] -> [(String, Maybe Expr)] -> CmdLet
call f pos named' = CmdLet f $ Args pos (M.fromList named')

cmdlet :: String -> [Expr] -> [(String, Maybe Expr)] -> CmdLet
cmdlet n pos = call (name n) pos

lambda :: [String] -> ([Expr] -> Expr) -> Expr
lambda = Lambda

infixr 6 .|
(.|) :: Expr -> CmdLet -> Expr
e .| c = Pipe e c

foreach :: (Expr -> Expr) -> CmdLet
foreach f = cmdlet "%" [f $ var "_"] []

filter :: (Expr -> Expr) -> CmdLet
filter p = cmdlet "?" [p $ var "_"] []

class ToPS a where
	toPS :: a -> String

instance ToPS Int where
	toPS = show

instance ToPS String where
	toPS = translate

instance ToPS Bool where
	toPS True = "$true"
	toPS False = "$false"

instance ToPS a => ToPS [a] where
	toPS = intercalate ", " . map toPS

translate :: String -> String
translate s
	| all (\ch -> isAlphaNum ch || ch `elem` "-_") s = s
	| otherwise = '"' : snd (foldr escape' (True, "\"") s)
	where
		escape' '"' (_, s') = (True, '\\' : '"' : s')
		escape' '\\' (True, s') = (True, '\\' : '\\' : s')
		escape' '\\' (False, s') = (False, '\\' : s')
		escape' c (_, s') = (False, c : s')

translateArg :: String -> String
translateArg s
	| all isAlphaNum s = s
	| otherwise = "'" ++ translate s ++ "'"

quote :: String -> String
quote s = "'" ++ concatMap (\case { '\'' -> "''"; ch -> [ch] }) s ++ "'"

quoteDouble :: String -> String
quoteDouble s = "\"" ++ concatMap (\case { '"' -> "\"\""; ch -> [ch] }) s ++ "\""

escape :: (String -> String) -> String -> String
escape fn s
	| all (\ch -> isAlphaNum ch || ch `elem` "-_") s = s
	| otherwise = fn s