在Haskell中,我如何与`IO ()`的标准输入(stdin)交互?

huangapple go评论76阅读模式
英文:

In haskell, how can I interact with stdin of an `IO ()`?

问题

这显示了我想做的事情的精神,它几乎可以工作(只是不适用于我的当前nix设置):

```haskell
echo :: IO ()
echo = getLine >>= putStrLn

testEcho :: IO ()
testEcho = do
  (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
    <- createProcess $
         (shell "nix run .#echo")
           { std_in = CreatePipe
           , std_out = CreatePipe
           }
  hPutStrLn stdin_hdl "Hello, echo!"
  hClose stdin_hdl
  !response <- hGetContents stdout_hdl
  cleanupProcess (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  if response == "Hello, echo!" then
    putStrLn "Success!"
  else error "Failure"

基本上我想要类似于createProcess的东西,但其类型为createProcess :: IO () -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle),而不是实际上的类型(createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) )。

对于这段代码的精神部分,你需要将echo作为你的nix flake的一个应用,以便nix run .#echo能够正常工作。


关于testEcho不满意的地方在于需要使用一个shell命令(nix run .#echo),而echo可以作为Haskell代码在那里使用。testEcho在我的nix环境中目前也出现了故障,但在从cabal repl调用时运行良好。


<details>
<summary>英文:</summary>

This shows the spirit of what I want to do and it almost works (just not within within my current nix setup):

```haskell
echo :: IO ()
echo = getLine &gt;&gt;= putStrLn

testEcho :: IO ()
testEcho = do
  (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
    &lt;- createProcess $
         (shell (&quot;nix run .#echo&quot;))
           { std_in = CreatePipe
           , std_out = CreatePipe
           }
  hPutStrLn stdin_hdl &quot;Hello, echo!&quot;
  hClose stdin_hdl
  !response &lt;- hGetContents stdout_hdl
  cleanupProcess (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  if response == &quot;Hello, echo!&quot; then
    putStrLn &quot;Success!&quot;
  else error &quot;Failure&quot;

basically I want something like createProcess but with a type of createProcess :: IO () -&gt; -&gt; IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) instead of what it actually is (createProcess :: CreateProcess -&gt; IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) )

For the spirit code, you would have to make echo an app of your nix flake for nix run .#echo to work


What is unsatisfactory about testEcho is needing to use a shell command (nix run .#echo) when echo is available there as haskell code. testEcho also currently malfunctions within my nix environment, but works well when called from cabal repl.

答案1

得分: 5

你可以降级到unix包,它提供了forkProcess。它还提供了为通信设置一些新的文件描述符所需的内容。下面是一个完整的示例:

import System.IO
import System.Posix.Process
import System.Posix.IO
import System.Posix.Types

echo :: IO ()
echo = getLine >>= putStrLn

forkWithStandardFds :: IO () -> IO (ProcessID, Handle, Handle, Handle)
forkWithStandardFds act = do
    (r0, w0) <- createPipe
    (r1, w1) <- createPipe
    (r2, w2) <- createPipe
    pid <- forkProcess $ do
        -- 六个closeFd不是严格必要的,
        -- 但它们是良好的卫生习惯
        closeFd w0 >> dupTo r0 stdInput
        closeFd r1 >> dupTo w1 stdOutput
        closeFd r2 >> dupTo w2 stdError
        act
    hIn  <- closeFd r0 >> fdToHandle w0
    hOut <- closeFd w1 >> fdToHandle r1
    hErr <- closeFd w2 >> fdToHandle r2
    pure (pid, hIn, hOut, hErr)

main :: IO ()
main = do
    (pid, hIn, hOut, hErr) <- forkWithStandardFds echo
    hPutStrLn hIn "Hello, echo!"
    hFlush hIn
    response <- hGetLine hOut
    if response == "Hello, echo!" then putStrLn "Success!" else error "Failure"
    getProcessStatus True False pid >>= print

运行时,它会打印:

Success!
Just (Exited ExitSuccess)
英文:

You can drop down a level to the unix package, which offers forkProcess. It also offers the stuff you need to set up a few fresh fds for comms. Here's a complete worked example:

import System.IO
import System.Posix.Process
import System.Posix.IO
import System.Posix.Types

echo :: IO ()
echo = getLine &gt;&gt;= putStrLn

forkWithStandardFds :: IO () -&gt; IO (ProcessID, Handle, Handle, Handle)
forkWithStandardFds act = do
    (r0, w0) &lt;- createPipe
    (r1, w1) &lt;- createPipe
    (r2, w2) &lt;- createPipe
    pid &lt;- forkProcess $ do
        -- the six closeFd&#39;s aren&#39;t strictly speaking necessary,
        -- but they&#39;re good hygiene
        closeFd w0 &gt;&gt; dupTo r0 stdInput
        closeFd r1 &gt;&gt; dupTo w1 stdOutput
        closeFd r2 &gt;&gt; dupTo w2 stdError
        act
    hIn  &lt;- closeFd r0 &gt;&gt; fdToHandle w0
    hOut &lt;- closeFd w1 &gt;&gt; fdToHandle r1
    hErr &lt;- closeFd w2 &gt;&gt; fdToHandle r2
    pure (pid, hIn, hOut, hErr)

main :: IO ()
main = do
	(pid, hIn, hOut, hErr) &lt;- forkWithStandardFds echo
	hPutStrLn hIn &quot;Hello, echo!&quot;
	hFlush hIn
	response &lt;- hGetLine hOut
	if response == &quot;Hello, echo!&quot; then putStrLn &quot;Success!&quot; else error &quot;Failure&quot;
	getProcessStatus True False pid &gt;&gt;= print

When run, it prints

Success!
Just (Exited ExitSuccess)

答案2

得分: 2

你可以在不创建另一个进程的情况下完成这个操作,通过重定向 stdinstdout。这个方法效果很好:

import Control.Exception (bracket)
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import System.Directory (getTemporaryDirectory, removeFile)
import Test.Hspec

echo :: IO ()
echo = getLine >>= putStrLn

main :: IO ()
main = hspec $ do
  specify "echo" $ do
    let line = "Hello, echo!\n"
    captureStdout (provideStdin line echo)
      `shouldReturn`
      line

provideStdin :: String -> IO a -> IO a
provideStdin input action =
  withTemporaryFile $ \h -> do
    hPutStr h input
    hSeek h AbsoluteSeek 0
    redirectStdin h action

captureStdout :: IO a -> IO String
captureStdout action =
  withTemporaryFile $ \h -> do
    redirectStdout h $ action *> hFlush stdout
    hSeek h AbsoluteSeek 0
    hGetContents' h

redirectStdin :: Handle -> IO a -> IO a
redirectStdin h action =
  bracket
    (hDuplicate stdin <* hDuplicateTo h stdin)
    (\saved -> hDuplicateTo saved stdin *> hClose saved)
    (const action)

redirectStdout :: Handle -> IO a -> IO a
redirectStdout h action =
  bracket
    (hFlush stdout *> hDuplicate stdout <* hDuplicateTo h stdout)
    (\saved -> hDuplicateTo saved stdout *> hClose saved)
    (const action)

withTemporaryFile :: (Handle -> IO a) -> IO a
withTemporaryFile inner = do
  tmp <- getTemporaryDirectory
  bracket
    (openBinaryTempFile tmp "wtf-")
    (\(name, h) -> hClose h >> removeFile name)
    (inner . snd)

我相信你可以连续执行多个这样的操作,而不会影响原始的 stdinstdout,但我没有测试过。

英文:

You can do it without creating another process, by redirecting stdin and stdout. This works nicely:

import Control.Exception (bracket)
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import System.Directory (getTemporaryDirectory, removeFile)
import Test.Hspec

echo :: IO ()
echo = getLine &gt;&gt;= putStrLn

main :: IO ()
main = hspec $ do
  specify &quot;echo&quot; $ do
    let line = &quot;Hello, echo!\n&quot;
    captureStdout (provideStdin line echo)
      `shouldReturn`
      line

provideStdin :: String -&gt; IO a -&gt; IO a
provideStdin input action =
  withTemporaryFile $ \h -&gt; do
    hPutStr h input
    hSeek h AbsoluteSeek 0
    redirectStdin h action

captureStdout :: IO a -&gt; IO String
captureStdout action =
  withTemporaryFile $ \h -&gt; do
    redirectStdout h $ action *&gt; hFlush stdout
    hSeek h AbsoluteSeek 0
    hGetContents&#39; h

redirectStdin :: Handle -&gt; IO a -&gt; IO a
redirectStdin h action =
  bracket
    (hDuplicate stdin &lt;* hDuplicateTo h stdin)
    (\saved -&gt; hDuplicateTo saved stdin *&gt; hClose saved)
    (const action)

redirectStdout :: Handle -&gt; IO a -&gt; IO a
redirectStdout h action =
  bracket
    (hFlush stdout *&gt; hDuplicate stdout &lt;* hDuplicateTo h stdout)
    (\saved -&gt; hDuplicateTo saved stdout *&gt; hClose saved)
    (const action)

withTemporaryFile :: (Handle -&gt; IO a) -&gt; IO a
withTemporaryFile inner = do
  tmp &lt;- getTemporaryDirectory
  bracket
    (openBinaryTempFile tmp &quot;wtf-&quot;)
    (\(name, h) -&gt; hClose h &gt;&gt; removeFile name)
    (inner . snd)

I believe you could do several of these in a row without messing up the original stdin and stdout, but I haven't tested that.

huangapple
  • 本文由 发表于 2023年7月14日 06:18:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76683594.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定