{-# LANGUAGE QuasiQuotes #-}
module RunCommand where
import Control.Monad.Logger.CallStack (MonadLogger, logError, logInfo)
import Data.ByteString qualified as ByteString
import Data.ByteString.Lazy qualified as Lazy
import Data.Char qualified as Char
import Data.List qualified as List
import Data.Text qualified as Text
import PossehlAnalyticsPrelude
import System.Exit (ExitCode (ExitFailure, ExitSuccess))
import System.Exit qualified as Exit
import System.Exit qualified as System
import System.IO (Handle)
import System.Process.Typed qualified as Process
runCommand :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> m (Exit.ExitCode, ByteString)
runCommand :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m (ExitCode, ByteString)
runCommand FilePath
executable [Text]
args = do
let bashArgs :: Text
bashArgs = [Text] -> Text
prettyArgsForBash ((FilePath
executable forall a b. a -> (a -> b) -> b
& FilePath -> Text
stringToText) forall a. a -> [a] -> [a]
: [Text]
args)
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logInfo [fmt|Running: $ {bashArgs}|]
FilePath -> [FilePath] -> ProcessConfig () () ()
Process.proc
FilePath
executable
([Text]
args forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> Text -> FilePath
textToString)
forall a b. a -> (a -> b) -> b
& forall (m :: Type -> Type) stdin stdoutIgnored stderr.
MonadIO m =>
ProcessConfig stdin stdoutIgnored stderr
-> m (ExitCode, ByteString)
Process.readProcessStdout
forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> forall (p :: Type -> Type -> Type) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second ByteString -> ByteString
toStrictBytes
forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> forall (p :: Type -> Type -> Type) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second ByteString -> ByteString
stripWhitespaceFromEnd
runCommandNoStdout :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> m Exit.ExitCode
runCommandNoStdout :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m ExitCode
runCommandNoStdout FilePath
executable [Text]
args = do
let bashArgs :: Text
bashArgs = [Text] -> Text
prettyArgsForBash ((FilePath
executable forall a b. a -> (a -> b) -> b
& FilePath -> Text
stringToText) forall a. a -> [a] -> [a]
: [Text]
args)
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logInfo [fmt|Running: $ {bashArgs}|]
FilePath -> [FilePath] -> ProcessConfig () () ()
Process.proc
FilePath
executable
([Text]
args forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> Text -> FilePath
textToString)
forall a b. a -> (a -> b) -> b
& forall (m :: Type -> Type) stdin stdout stderr.
MonadIO m =>
ProcessConfig stdin stdout stderr -> m ExitCode
Process.runProcess
stripWhitespaceFromEnd :: ByteString -> ByteString
stripWhitespaceFromEnd :: ByteString -> ByteString
stripWhitespaceFromEnd = ByteString -> ByteString
ByteString.reverse forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Word8 -> Bool) -> ByteString -> ByteString
ByteString.dropWhile (\Word8
w -> Word8
w forall a. Eq a => a -> a -> Bool
== Char -> Word8
charToWordUnsafe Char
'\n') forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString
ByteString.reverse
runCommandWithStdin :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> Lazy.ByteString -> m (Exit.ExitCode, ByteString)
runCommandWithStdin :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> ByteString -> m (ExitCode, ByteString)
runCommandWithStdin FilePath
executable [Text]
args ByteString
stdin = do
let bashArgs :: Text
bashArgs = [Text] -> Text
prettyArgsForBash ((FilePath
executable forall a b. a -> (a -> b) -> b
& FilePath -> Text
stringToText) forall a. a -> [a] -> [a]
: [Text]
args)
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logInfo [fmt|Running: $ {bashArgs}|]
FilePath -> [FilePath] -> ProcessConfig () () ()
Process.proc
FilePath
executable
([Text]
args forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> Text -> FilePath
textToString)
forall a b. a -> (a -> b) -> b
& forall stdin stdin0 stdout stderr.
StreamSpec 'STInput stdin
-> ProcessConfig stdin0 stdout stderr
-> ProcessConfig stdin stdout stderr
Process.setStdin (ByteString -> StreamSpec 'STInput ()
Process.byteStringInput ByteString
stdin)
forall a b. a -> (a -> b) -> b
& forall (m :: Type -> Type) stdin stdoutIgnored stderr.
MonadIO m =>
ProcessConfig stdin stdoutIgnored stderr
-> m (ExitCode, ByteString)
Process.readProcessStdout
forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> forall (p :: Type -> Type -> Type) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second ByteString -> ByteString
toStrictBytes
forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> forall (p :: Type -> Type -> Type) b c a.
Bifunctor p =>
(b -> c) -> p a b -> p a c
second ByteString -> ByteString
stripWhitespaceFromEnd
runCommandInteractiveExpect0 :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> m ()
runCommandInteractiveExpect0 :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m ()
runCommandInteractiveExpect0 FilePath
executable [Text]
args = do
let bashArgs :: Text
bashArgs = [Text] -> Text
prettyArgsForBash ((FilePath
executable forall a b. a -> (a -> b) -> b
& FilePath -> Text
stringToText) forall a. a -> [a] -> [a]
: [Text]
args)
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logInfo [fmt|Running interactively: $ {bashArgs}|]
( forall (m :: Type -> Type) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
forall (m :: Type -> Type) stdin stdout stderr.
MonadIO m =>
ProcessConfig stdin stdout stderr -> m ExitCode
Process.runProcess forall a b. (a -> b) -> a -> b
$
FilePath -> [FilePath] -> ProcessConfig () () ()
Process.proc
FilePath
executable
([Text]
args forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> Text -> FilePath
textToString)
)
forall (m :: Type -> Type) a b. Monad m => m a -> (a -> m b) -> m b
>>= forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> ExitCode -> m ()
checkStatus0 FilePath
executable
runCommandPipeToHandle :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> Handle -> m Exit.ExitCode
runCommandPipeToHandle :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> Handle -> m ExitCode
runCommandPipeToHandle FilePath
executable [Text]
args Handle
handle = do
let bashArgs :: Text
bashArgs = [Text] -> Text
prettyArgsForBash ((FilePath
executable forall a b. a -> (a -> b) -> b
& FilePath -> Text
stringToText) forall a. a -> [a] -> [a]
: [Text]
args)
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logInfo [fmt|Running: $ {bashArgs}|]
forall (m :: Type -> Type) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$
forall (m :: Type -> Type) stdin stdout stderr.
MonadIO m =>
ProcessConfig stdin stdout stderr -> m ExitCode
Process.runProcess
( FilePath -> [FilePath] -> ProcessConfig () () ()
Process.proc
FilePath
executable
([Text]
args forall (f :: Type -> Type) a b. Functor f => f a -> (a -> b) -> f b
<&> Text -> FilePath
textToString)
forall a b. a -> (a -> b) -> b
& forall stdout stdin stdout0 stderr.
StreamSpec 'STOutput stdout
-> ProcessConfig stdin stdout0 stderr
-> ProcessConfig stdin stdout stderr
Process.setStdout (forall (anyStreamType :: StreamType).
Handle -> StreamSpec anyStreamType ()
Process.useHandleClose Handle
handle)
)
runCommandExpect0 :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> m ByteString
runCommandExpect0 :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m ByteString
runCommandExpect0 FilePath
executable [Text]
args =
forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m (ExitCode, ByteString)
runCommand FilePath
executable [Text]
args forall (m :: Type -> Type) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
(ExitCode
ex, ByteString
stdout) -> do
forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> ExitCode -> m ()
checkStatus0 FilePath
executable ExitCode
ex
pure ByteString
stdout
runCommandExpect0NoStdout :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> m ()
runCommandExpect0NoStdout :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m ()
runCommandExpect0NoStdout FilePath
executable [Text]
args =
forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> m ExitCode
runCommandNoStdout FilePath
executable [Text]
args forall (m :: Type -> Type) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
ExitCode
ex -> forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> ExitCode -> m ()
checkStatus0 FilePath
executable ExitCode
ex
runCommandWithStdinExpect0 :: (MonadLogger m, MonadIO m) => FilePath -> [Text] -> Lazy.ByteString -> m ByteString
runCommandWithStdinExpect0 :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> ByteString -> m ByteString
runCommandWithStdinExpect0 FilePath
executable [Text]
args ByteString
stdin =
forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> [Text] -> ByteString -> m (ExitCode, ByteString)
runCommandWithStdin FilePath
executable [Text]
args ByteString
stdin forall (m :: Type -> Type) a b. Monad m => m a -> (a -> m b) -> m b
>>= \case
(ExitCode
ex, ByteString
stdout) -> do
forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> ExitCode -> m ()
checkStatus0 FilePath
executable ExitCode
ex
pure ByteString
stdout
checkStatus0 :: (MonadLogger m, MonadIO m) => FilePath -> ExitCode -> m ()
checkStatus0 :: forall (m :: Type -> Type).
(MonadLogger m, MonadIO m) =>
FilePath -> ExitCode -> m ()
checkStatus0 FilePath
executable = \case
ExitCode
ExitSuccess -> forall (f :: Type -> Type) a. Applicative f => a -> f a
pure ()
ExitFailure Int
status -> do
forall (m :: Type -> Type) b.
(HasCallStack, MonadLogger m, MonadIO m) =>
Text -> m b
logCritical [fmt|Command `{executable}` did not exit with status 0 (success), but status {status}|]
logCritical :: (HasCallStack, MonadLogger m, MonadIO m) => Text -> m b
logCritical :: forall (m :: Type -> Type) b.
(HasCallStack, MonadLogger m, MonadIO m) =>
Text -> m b
logCritical Text
msg = do
forall (m :: Type -> Type).
(HasCallStack, MonadLogger m) =>
Text -> m ()
logError Text
msg
forall (m :: Type -> Type) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. IO a
System.exitFailure
prettyArgsForBash :: [Text] -> Text
prettyArgsForBash :: [Text] -> Text
prettyArgsForBash = Text -> [Text] -> Text
Text.intercalate Text
" " forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a -> b) -> [a] -> [b]
map Text -> Text
simpleBashEscape
simpleBashEscape :: Text -> Text
simpleBashEscape :: Text -> Text
simpleBashEscape Text
t = do
case (Char -> Bool) -> Text -> Maybe Char
Text.find (Bool -> Bool
not forall b c a. (b -> c) -> (a -> b) -> a -> c
. Char -> Bool
isSimple) Text
t of
Just Char
_ -> Text -> Text
escapeSingleQuote Text
t
Maybe Char
Nothing -> Text
t
where
isSimple :: Char -> Bool
isSimple Char
c =
Char -> Bool
Char.isAsciiLower Char
c
Bool -> Bool -> Bool
|| Char -> Bool
Char.isAsciiUpper Char
c
Bool -> Bool -> Bool
|| Char -> Bool
Char.isDigit Char
c
Bool -> Bool -> Bool
|| forall (t :: Type -> Type) a.
(Foldable t, Eq a) =>
a -> t a -> Bool
List.elem Char
c [Char
'-', Char
'.', Char
':', Char
'/']
escapeSingleQuote :: Text -> Text
escapeSingleQuote Text
t' = Text
"'" forall a. Semigroup a => a -> a -> a
<> Text -> Text -> Text -> Text
Text.replace Text
"'" Text
"'\\''" Text
t' forall a. Semigroup a => a -> a -> a
<> Text
"'"