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

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

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

问题

  1. 这显示了我想做的事情的精神,它几乎可以工作(只是不适用于我的当前nix设置):
  2. ```haskell
  3. echo :: IO ()
  4. echo = getLine >>= putStrLn
  5. testEcho :: IO ()
  6. testEcho = do
  7. (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  8. <- createProcess $
  9. (shell "nix run .#echo")
  10. { std_in = CreatePipe
  11. , std_out = CreatePipe
  12. }
  13. hPutStrLn stdin_hdl "Hello, echo!"
  14. hClose stdin_hdl
  15. !response <- hGetContents stdout_hdl
  16. cleanupProcess (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  17. if response == "Hello, echo!" then
  18. putStrLn "Success!"
  19. 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调用时运行良好。

  1. <details>
  2. <summary>英文:</summary>
  3. This shows the spirit of what I want to do and it almost works (just not within within my current nix setup):
  4. ```haskell
  5. echo :: IO ()
  6. echo = getLine &gt;&gt;= putStrLn
  7. testEcho :: IO ()
  8. testEcho = do
  9. (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  10. &lt;- createProcess $
  11. (shell (&quot;nix run .#echo&quot;))
  12. { std_in = CreatePipe
  13. , std_out = CreatePipe
  14. }
  15. hPutStrLn stdin_hdl &quot;Hello, echo!&quot;
  16. hClose stdin_hdl
  17. !response &lt;- hGetContents stdout_hdl
  18. cleanupProcess (Just stdin_hdl, Just stdout_hdl, m_stderr_hdl, p_hdl)
  19. if response == &quot;Hello, echo!&quot; then
  20. putStrLn &quot;Success!&quot;
  21. 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。它还提供了为通信设置一些新的文件描述符所需的内容。下面是一个完整的示例:

  1. import System.IO
  2. import System.Posix.Process
  3. import System.Posix.IO
  4. import System.Posix.Types
  5. echo :: IO ()
  6. echo = getLine >>= putStrLn
  7. forkWithStandardFds :: IO () -> IO (ProcessID, Handle, Handle, Handle)
  8. forkWithStandardFds act = do
  9. (r0, w0) <- createPipe
  10. (r1, w1) <- createPipe
  11. (r2, w2) <- createPipe
  12. pid <- forkProcess $ do
  13. -- 六个closeFd不是严格必要的,
  14. -- 但它们是良好的卫生习惯
  15. closeFd w0 >> dupTo r0 stdInput
  16. closeFd r1 >> dupTo w1 stdOutput
  17. closeFd r2 >> dupTo w2 stdError
  18. act
  19. hIn <- closeFd r0 >> fdToHandle w0
  20. hOut <- closeFd w1 >> fdToHandle r1
  21. hErr <- closeFd w2 >> fdToHandle r2
  22. pure (pid, hIn, hOut, hErr)
  23. main :: IO ()
  24. main = do
  25. (pid, hIn, hOut, hErr) <- forkWithStandardFds echo
  26. hPutStrLn hIn "Hello, echo!"
  27. hFlush hIn
  28. response <- hGetLine hOut
  29. if response == "Hello, echo!" then putStrLn "Success!" else error "Failure"
  30. getProcessStatus True False pid >>= print

运行时,它会打印:

  1. Success!
  2. 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:

  1. import System.IO
  2. import System.Posix.Process
  3. import System.Posix.IO
  4. import System.Posix.Types
  5. echo :: IO ()
  6. echo = getLine &gt;&gt;= putStrLn
  7. forkWithStandardFds :: IO () -&gt; IO (ProcessID, Handle, Handle, Handle)
  8. forkWithStandardFds act = do
  9. (r0, w0) &lt;- createPipe
  10. (r1, w1) &lt;- createPipe
  11. (r2, w2) &lt;- createPipe
  12. pid &lt;- forkProcess $ do
  13. -- the six closeFd&#39;s aren&#39;t strictly speaking necessary,
  14. -- but they&#39;re good hygiene
  15. closeFd w0 &gt;&gt; dupTo r0 stdInput
  16. closeFd r1 &gt;&gt; dupTo w1 stdOutput
  17. closeFd r2 &gt;&gt; dupTo w2 stdError
  18. act
  19. hIn &lt;- closeFd r0 &gt;&gt; fdToHandle w0
  20. hOut &lt;- closeFd w1 &gt;&gt; fdToHandle r1
  21. hErr &lt;- closeFd w2 &gt;&gt; fdToHandle r2
  22. pure (pid, hIn, hOut, hErr)
  23. main :: IO ()
  24. main = do
  25. (pid, hIn, hOut, hErr) &lt;- forkWithStandardFds echo
  26. hPutStrLn hIn &quot;Hello, echo!&quot;
  27. hFlush hIn
  28. response &lt;- hGetLine hOut
  29. if response == &quot;Hello, echo!&quot; then putStrLn &quot;Success!&quot; else error &quot;Failure&quot;
  30. getProcessStatus True False pid &gt;&gt;= print

When run, it prints

  1. Success!
  2. Just (Exited ExitSuccess)

答案2

得分: 2

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

  1. import Control.Exception (bracket)
  2. import GHC.IO.Handle (hDuplicate, hDuplicateTo)
  3. import System.Directory (getTemporaryDirectory, removeFile)
  4. import Test.Hspec
  5. echo :: IO ()
  6. echo = getLine >>= putStrLn
  7. main :: IO ()
  8. main = hspec $ do
  9. specify "echo" $ do
  10. let line = "Hello, echo!\n"
  11. captureStdout (provideStdin line echo)
  12. `shouldReturn`
  13. line
  14. provideStdin :: String -> IO a -> IO a
  15. provideStdin input action =
  16. withTemporaryFile $ \h -> do
  17. hPutStr h input
  18. hSeek h AbsoluteSeek 0
  19. redirectStdin h action
  20. captureStdout :: IO a -> IO String
  21. captureStdout action =
  22. withTemporaryFile $ \h -> do
  23. redirectStdout h $ action *> hFlush stdout
  24. hSeek h AbsoluteSeek 0
  25. hGetContents' h
  26. redirectStdin :: Handle -> IO a -> IO a
  27. redirectStdin h action =
  28. bracket
  29. (hDuplicate stdin <* hDuplicateTo h stdin)
  30. (\saved -> hDuplicateTo saved stdin *> hClose saved)
  31. (const action)
  32. redirectStdout :: Handle -> IO a -> IO a
  33. redirectStdout h action =
  34. bracket
  35. (hFlush stdout *> hDuplicate stdout <* hDuplicateTo h stdout)
  36. (\saved -> hDuplicateTo saved stdout *> hClose saved)
  37. (const action)
  38. withTemporaryFile :: (Handle -> IO a) -> IO a
  39. withTemporaryFile inner = do
  40. tmp <- getTemporaryDirectory
  41. bracket
  42. (openBinaryTempFile tmp "wtf-")
  43. (\(name, h) -> hClose h >> removeFile name)
  44. (inner . snd)

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

英文:

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

  1. import Control.Exception (bracket)
  2. import GHC.IO.Handle (hDuplicate, hDuplicateTo)
  3. import System.Directory (getTemporaryDirectory, removeFile)
  4. import Test.Hspec
  5. echo :: IO ()
  6. echo = getLine &gt;&gt;= putStrLn
  7. main :: IO ()
  8. main = hspec $ do
  9. specify &quot;echo&quot; $ do
  10. let line = &quot;Hello, echo!\n&quot;
  11. captureStdout (provideStdin line echo)
  12. `shouldReturn`
  13. line
  14. provideStdin :: String -&gt; IO a -&gt; IO a
  15. provideStdin input action =
  16. withTemporaryFile $ \h -&gt; do
  17. hPutStr h input
  18. hSeek h AbsoluteSeek 0
  19. redirectStdin h action
  20. captureStdout :: IO a -&gt; IO String
  21. captureStdout action =
  22. withTemporaryFile $ \h -&gt; do
  23. redirectStdout h $ action *&gt; hFlush stdout
  24. hSeek h AbsoluteSeek 0
  25. hGetContents&#39; h
  26. redirectStdin :: Handle -&gt; IO a -&gt; IO a
  27. redirectStdin h action =
  28. bracket
  29. (hDuplicate stdin &lt;* hDuplicateTo h stdin)
  30. (\saved -&gt; hDuplicateTo saved stdin *&gt; hClose saved)
  31. (const action)
  32. redirectStdout :: Handle -&gt; IO a -&gt; IO a
  33. redirectStdout h action =
  34. bracket
  35. (hFlush stdout *&gt; hDuplicate stdout &lt;* hDuplicateTo h stdout)
  36. (\saved -&gt; hDuplicateTo saved stdout *&gt; hClose saved)
  37. (const action)
  38. withTemporaryFile :: (Handle -&gt; IO a) -&gt; IO a
  39. withTemporaryFile inner = do
  40. tmp &lt;- getTemporaryDirectory
  41. bracket
  42. (openBinaryTempFile tmp &quot;wtf-&quot;)
  43. (\(name, h) -&gt; hClose h &gt;&gt; removeFile name)
  44. (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:

确定