Skip to content

Trim trailing newlines in ExitCodeException Show instance #90

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 58 additions & 32 deletions src/System/Process/Typed/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import qualified Control.Exception as E
import Control.Exception hiding (bracket, finally, handle)
import Control.Monad (void)
import qualified System.Process as P
import Data.List (dropWhileEnd)
import Data.Typeable (Typeable)
import System.IO (Handle, hClose, IOMode(ReadWriteMode), withBinaryFile)
import Control.Concurrent.Async (async)
Expand Down Expand Up @@ -88,29 +89,37 @@ data ProcessConfig stdin stdout stderr = ProcessConfig
#endif
}
instance Show (ProcessConfig stdin stdout stderr) where
show pc = concat
[ case pcCmdSpec pc of
P.ShellCommand s -> "Shell command: " ++ s
P.RawCommand x xs -> "Raw command: " ++ unwords (map escape (x:xs))
, "\n"
, case pcWorkingDir pc of
Nothing -> ""
Just wd -> concat
[ "Run from: "
, wd
, "\n"
]
, case pcEnv pc of
Nothing -> ""
Just e -> unlines
$ "Modified environment:"
: map (\(k, v) -> concat [k, "=", v]) e
]
show pc = concat $
command
++ workingDir
++ env
where
escape x
| any (`elem` " \\\"'") x = show x
| x == "" = "\"\""
| otherwise = x

command =
case pcCmdSpec pc of
P.ShellCommand s -> ["Shell command: ", s]
P.RawCommand program args ->
["Raw command:"]
++ do arg <- program:args
[" ", escape arg]

workingDir =
case pcWorkingDir pc of
Nothing -> []
Just wd -> ["\nRun from: ", wd]

env =
case pcEnv pc of
Nothing -> []
Just env' ->
["\nModified environment:"]
++ do (key, value) <- env'
["\n", key, "=", value]

instance (stdin ~ (), stdout ~ (), stderr ~ ())
=> IsString (ProcessConfig stdin stdout stderr) where
fromString s
Expand Down Expand Up @@ -607,20 +616,37 @@ data ExitCodeException = ExitCodeException
deriving Typeable
instance Exception ExitCodeException
instance Show ExitCodeException where
show ece = concat
[ "Received "
, show (eceExitCode ece)
, " when running\n"
-- Too much output for an exception if we show the modified
-- environment, so hide it
, show (eceProcessConfig ece) { pcEnv = Nothing }
, if L.null (eceStdout ece)
then ""
else "Standard output:\n\n" ++ L8.unpack (eceStdout ece)
, if L.null (eceStderr ece)
then ""
else "Standard error:\n\n" ++ L8.unpack (eceStderr ece)
]
show ece =
let stdout = trimTrailingAsciiNewlines $ L8.unpack $ eceStdout ece
stderr = trimTrailingAsciiNewlines $ L8.unpack $ eceStderr ece

isAsciiNewline char = case char of
'\n' -> True
'\r' -> True
_ -> False
trimTrailingAsciiNewlines = dropWhileEnd isAsciiNewline


stdout' = if null stdout
then []
else [ "\n\nStandard output:\n"
, stdout
]
stderr' = if null stderr
then []
else [ "\n\nStandard error:\n"
, stderr
]
in concat $
[ "Received "
, show (eceExitCode ece)
, " when running\n"
-- Too much output for an exception if we show the modified
-- environment, so hide it.
, show (eceProcessConfig ece) { pcEnv = Nothing }
]
++ stdout'
++ stderr'

-- | Wrapper for when an exception is thrown when reading from a child
-- process, used by 'byteStringOutput'.
Expand Down
214 changes: 212 additions & 2 deletions test/System/Process/TypedSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import System.Exit
import System.IO.Temp
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.String (IsString)
import Data.String (IsString(..))
import Data.Monoid ((<>))
import qualified Data.ByteString.Base64 as B64

Expand Down Expand Up @@ -168,5 +168,215 @@ spec = do
L.take (L.length expected) lbs1 `shouldBe` expected

it "empty param are showed" $
let expected = "Raw command: podman exec --detach-keys \"\" ctx bash\n"
let expected = "Raw command: podman exec --detach-keys \"\" ctx bash"
in show (proc "podman" ["exec", "--detach-keys", "", "ctx", "bash"]) `shouldBe` expected

describe "Show ProcessConfig" $ do
it "shell-escapes arguments" $ do
let processConfig = proc "echo" ["a", "", "\"b\"", "'c'", "\\d"]
-- I promise this escaping behavior is correct; paste it into GHCi
-- `putStrLn` and then paste it into `sh` to verify.
show processConfig `shouldBe`
"Raw command: echo a \"\" \"\\\"b\\\"\" \"'c'\" \"\\\\d\""

it "displays working directory" $ do
let processConfig = setWorkingDir "puppy/doggy" $ proc "true" []
show processConfig `shouldBe`
"Raw command: true\n"
++ "Run from: puppy/doggy"

it "displays environment (inherited)" $ do
let processConfig = setEnvInherit $ proc "true" []
show processConfig `shouldBe`
"Raw command: true"

it "displays environment (cleared)" $ do
let processConfig = setEnv [] $ proc "true" []
show processConfig `shouldBe`
"Raw command: true\n"
++ "Modified environment:" -- lol

it "displays environment (1 variable)" $ do
let processConfig = setEnv [("PUPPY", "DOGGY")] $ proc "true" []
show processConfig `shouldBe`
"Raw command: true\n"
++ "Modified environment:\n"
++ "PUPPY=DOGGY"

it "displays environment (multiple variables)" $ do
let processConfig =
setEnv [ ("PUPPY", "DOGGY")
, ("SOUND", "AWOO")
, ("HOWLING", "RIGHT_NOW")
]
$ proc "true" []
show processConfig `shouldBe`
"Raw command: true\n"
++ "Modified environment:\n"
++ "PUPPY=DOGGY\n"
++ "SOUND=AWOO\n"
++ "HOWLING=RIGHT_NOW"

it "displays working directory and environment" $ do
let processConfig =
setEnv [ ("PUPPY", "DOGGY")
, ("SOUND", "AWOO")
]
$ setWorkingDir "puppy/doggy"
$ proc "true" []
show processConfig `shouldBe`
"Raw command: true\n"
++ "Run from: puppy/doggy\n"
++ "Modified environment:\n"
++ "PUPPY=DOGGY\n"
++ "SOUND=AWOO"


describe "Show ExitCodeException" $ do
it "shows ExitCodeException" $ do
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "cp" ["a", "b"]
, eceStdout = fromString "Copied OK\n"
, eceStderr = fromString "Uh oh!\n"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: cp a b\n"
++ "\n"
++ "Standard output:\n"
++ "Copied OK\n"
++ "\n"
++ "Standard error:\n"
++ "Uh oh!"

context "without stderr" $ do
it "shows ExitCodeException" $ do
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "show-puppy" []
, eceStdout = fromString "No puppies found???\n"
, eceStderr = fromString ""
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: show-puppy\n"
++ "\n"
++ "Standard output:\n"
++ "No puppies found???"

context "without stdout" $ do
it "shows ExitCodeException" $ do
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "show-puppy" []
, eceStdout = fromString ""
, eceStderr = fromString "No puppies found???\n"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: show-puppy\n"
++ "\n"
++ "Standard error:\n"
++ "No puppies found???"

it "trims newlines from stdout/stderr" $ do
-- This keeps the `Show` output looking nice regardless of how many
-- newlines (if any) the command outputs.
--
-- This also makes sure that the `Show` output doesn't end with a
-- spurious trailing newline, making it easier to compose `Show`
-- instances together.
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "detect-doggies" []
, eceStdout = fromString "puppy\n\n"
, eceStderr = fromString "doggy\r\n"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: detect-doggies\n"
++ "\n"
++ "Standard output:\n"
++ "puppy\n"
++ "\n"
++ "Standard error:\n"
++ "doggy"

it "adds newlines to stdout/stderr" $ do
-- This keeps the `Show` output looking nice when the output
-- doesn't include a trailing newline.
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "detect-doggies" []
, eceStdout = fromString "puppy"
, eceStderr = fromString "doggy"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: detect-doggies\n"
++ "\n"
++ "Standard output:\n"
++ "puppy\n"
++ "\n"
++ "Standard error:\n"
++ "doggy"

it "trims newlines but not other whitespace from stdout/stderr" $ do
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "detect-doggies" []
, eceStdout = fromString "\n\npuppy\n\n \n"
, eceStderr = fromString "\t \ndoggy\n \t\n"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: detect-doggies\n"
++ "\n"
++ "Standard output:\n"
++ "\n\npuppy\n\n "
++ "\n\n"
++ "Standard error:\n"
++ "\t \ndoggy\n \t"

context "without newlines in stdout" $ do
it "shows ExitCodeException" $ do
-- Sometimes, commands don't output _any_ newlines!
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "detect-doggies" []
, eceStdout = fromString "puppy"
, eceStderr = fromString ""
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: detect-doggies\n"
++ "\n"
++ "Standard output:\n"
++ "puppy"

context "without newlines in stdout or stderr" $ do
it "shows ExitCodeException" $ do
let exitCodeException =
ExitCodeException
{ eceExitCode = ExitFailure 1
, eceProcessConfig = proc "detect-doggies" []
, eceStdout = fromString "puppy"
, eceStderr = fromString "doggy"
}
show exitCodeException `shouldBe`
"Received ExitFailure 1 when running\n"
++ "Raw command: detect-doggies\n"
++ "\n"
++ "Standard output:\n"
++ "puppy\n"
++ "\n"
++ "Standard error:\n"
++ "doggy"