1 -- This file is part of Diohsc
2 -- Copyright (C) 2020 Martin Bays <mbays@sdf.org>
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of version 3 of the GNU General Public License as
6 -- published by the Free Software Foundation, or any later version.
8 -- You should have received a copy of the GNU General Public License
9 -- along with this program. If not, see http://www.gnu.org/licenses/.
11 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
13 module RunExternal
where
15 import Control
.Concurrent
(forkIO
)
16 import Control
.Monad
(msum, void
)
17 import Control
.Monad
.Catch
(bracket_, finally
)
18 import Control
.Monad
.State
(State
, put
, runState
)
19 import Control
.Monad
.Trans
.Maybe (MaybeT
(..), runMaybeT
)
20 import Data
.Char (isAlphaNum)
21 import Data
.List
(uncons
)
22 import System
.Environment
(lookupEnv
, setEnv
, unsetEnv
)
23 import System
.Exit
(ExitCode)
25 import System
.IO.Temp
(withTempFile
)
28 import qualified Data
.ByteString
.Lazy
as BL
36 -- |Wrapper to ensure we don't accidentally allow use of shell commands in
38 newtype RestrictedIO a
= RestrictedIO
(IO a
)
39 deriving (Functor
,Applicative
,Monad
)
41 runRestrictedIO
:: RestrictedIO a
-> IO a
42 runRestrictedIO
(RestrictedIO m
) = m
45 subPercentOrAppend
:: String -> String -> String
46 subPercentOrAppend sub str
=
47 let (s
',subbed
) = subPercent str `runState`
False
48 in if subbed
then s
' else s
' ++ (' ':shellQuote sub
)
50 -- |based on specification for $BROWSER in 'man 1 man'
51 subPercent
:: String -> State
Bool String
52 subPercent
"" = return []
53 subPercent
('%':'%':s
) = ('%':) <$> subPercent s
54 subPercent
('%':'c
':s
) = (':':) <$> subPercent s
55 subPercent
('%':'S
':s
) = put
True >> (sub
++) <$> subPercent s
56 subPercent
('%':'s
':s
) = put
True >> (shellQuote sub
++) <$> subPercent s
57 subPercent
(c
:s
) = (c
:) <$> subPercent s
59 |
all shellSafe s
&& not (null s
) = s
60 |
otherwise = '\'' : substAll
'\'' "'\\''" s
<> "'"
61 shellSafe c
= isAlphaNum c || c `
elem`
".,_-+="
63 |
(s
',_
:t
) <- break (== c
) s
= s
' <> r
<> substAll c r t
66 confirmShell
:: String -> String -> IO Bool
67 confirmShell desc cmd
= promptYN
True False $
68 desc
++ " following shell command?: " ++ withBoldStr cmd
70 pipeToCmdLazily
:: String -> [String] -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
71 pipeToCmdLazily cmd cArgs
= pipeLazily
$ proc cmd cArgs
73 pipeToShellLazily
:: String -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
74 pipeToShellLazily
= pipeLazily
. shell
76 filterShell
:: String -> [(String,String)] -> BL
.ByteString
-> RestrictedIO BL
.ByteString
77 filterShell
= filterProcess
. shell
79 withExtraEnv
:: [(String,String)] -> IO a
-> IO a
80 withExtraEnv envir
= bracket_
81 (mapM_ (uncurry setEnv
) envir
)
82 (mapM_ (unsetEnv
. fst) envir
)
84 pipeLazily
:: CreateProcess
-> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
85 pipeLazily cp envir b
= RestrictedIO
. withExtraEnv envir
$ do
86 (Just inp
, _
, _
, pid
) <- createProcess
$
87 cp
{ std_in
= CreatePipe
, std_out
= Inherit
}
88 hSetBuffering inp NoBuffering
89 ignoreIOErr
. finally
(BL
.hPut inp b
) . void
$ do
93 filterProcess
:: CreateProcess
-> [(String,String)] -> BL
.ByteString
-> RestrictedIO BL
.ByteString
94 filterProcess cp envir b
= RestrictedIO
. withExtraEnv envir
$ do
95 (Just inp
, Just outp
, _
, pid
) <- createProcess
$
96 cp
{ std_in
= CreatePipe
, std_out
= CreatePipe
}
97 hSetBuffering inp NoBuffering
98 hSetBuffering outp NoBuffering
99 _
<- forkIO
$ ignoreIOErr
. finally
(BL
.hPut inp b
) . void
$ do
104 runMailcap
:: Bool -> String -> String -> String -> BL
.ByteString
-> RestrictedIO
()
105 runMailcap noConfirm action dir mimetype b
=
106 RestrictedIO
. withTempFile dir
"runtmp" $ \path h
-> do
109 let cArgs
= ["--action=" ++ action
, mimetype
++ ":" ++ path
]
110 rawSystem
"run-mailcap" ("--norun":cArgs
) >>
111 (if noConfirm
then return True else promptYN
True False "Run this command?") >>?
112 void
$ rawSystem
"run-mailcap" cArgs
114 runShellCmd
:: String -> [(String,String)] -> RestrictedIO
ExitCode
115 runShellCmd cmd envir
= RestrictedIO
. withExtraEnv envir
$ spawnCommand cmd
>>= waitForProcess
117 editInteractively
:: Bool -> String -> String -> RestrictedIO
String
118 editInteractively ansi dir s
=
119 RestrictedIO
. withTempFile dir
"runtmp" $ \path h
-> do
122 mEditor
<- runMaybeT
. msum $ MaybeT
<$>
123 [ lookupEnv
"EDITOR", lookupEnv
"VISUAL" ]
125 Nothing
-> printErrFancy ansi
"EDITOR environment variable unset" >> pure s
127 void
$ rawSystem editor
[path
]
128 (stripNewline
<$>) $ openFile path ReadMode
>>= hGetContents
130 stripNewline
:: String -> String
131 stripNewline
= (. reverse) $ reverse . \cs
->
133 Just
('\n',cs
') -> cs
'
136 shellOnData
:: Bool -> String -> FilePath -> [(String,String)] -> BL
.ByteString
-> RestrictedIO
()
137 shellOnData noConfirm cmd dir envir b
= RestrictedIO
. withTempFile dir
"runtmp" $ \path h
->
138 let cmd
' = subPercentOrAppend path cmd
139 in (if noConfirm
then return True else confirmShell
"Run" cmd
') >>? void
$ do
140 BL
.hPut h b
>> hClose h
141 runRestrictedIO
$ runShellCmd cmd
' envir