Loading Shell Script Files to Access Their Functions In Haskell - haskell

I have a shell script file named /path/to/shell_script.sh which contains a function defintion shell_function() { ...}.
I'd like to be able to do something in Haskell like readProcessWithExitCode shell_function [] "" to eventually get a hold of an IO (String).
How to do this?

If you have a shell script like
#!/bin/bash
foo() {
echo "foo"
}
you can use the readCreateProcess function from the process package to source the script and execute the function in one go, like this:
module Main where
import System.Process
main :: IO ()
main = do
-- I had to put the full path of the script for it to work
result <- readCreateProcess ((shell ". /tmp/foo.sh && foo")) ""
print result
This solution assumes that the script only does things like defining functions and setting environment variables, without running undesired "effectful" code each time it is sourced.

Related

How do I execute a list of commands in Haskell?

I'm new to Haskell and I have been using system to execute shell commands for me, since I'm only interested in exit status. I noticed that since I can do something like:
ls <- system "ls"
pwd <- system "pwd"
and this correctly executes the two commands. I was thinking about executing an array of these commands eg
lsAndPwd <- return $ system <$> ["ls", "pwd"]
and I'm surprised that this doesn't actually execute anything. This compiles and I checked that lsAndPwd has the correct type of [IO GHC.IO.Exception.ExitCode] but the commands are never executed. What's going on and how can I get this to work?
You should use mapM instead of <$> so you get an IO [ExitCode], not an [IO ExitCode]. This way, it's collected into one IO monad that you can run:
lsAndPwd <- mapM system ["ls", "pwd"]
Alternatively, call sequence on the whole thing:
lsAndPwd <- sequence $ system <$> ["ls", "pwd"]

How to execute commands from Haskell as root?

Consider the following Haskell function:
eraseFile :: FilePath -> IO ()
eraseFile basename =
do let cmd' = ">"
args' = ("/path/to/file/" ++ basename) :: String
(exitcode', stdout', stderr') <- readProcessWithExitCode cmd' [args'] ""
return ()
When I try to run this in a stack ghci repl, or from the main function, I get a permission denied error from the console. Normally, in a bash console, you could just run this command as sudo, but this doesn't seem to work when invoked from Haskell.
Question: How to execute system commands in Haskell as root?
As already pointed out in the comments, you can just run the entire stack/ghc under root, but I daresay that's a bad idea. Preferrably, I'd just invoke sudo as a process from within your program. The particular command – emptying a file, if I have understood that correctly? – is then easiest done with tee:
do let cmd' = "sudo"
args' = ["tee", "/path/to/file/" ++ basename :: String]
(exitcode', stdout', stderr') <- readProcessWithExitCode cmd' args' ""
As Zeta remarks, truncate --size 0 would probably be a cleaner command.
To get around password entering, you probably also want to make an exception in the sudoers file. It's a hairy matter; of course the really best thing would be if you could avoid needing root permissions altogether.

multiline contents of a IO handle in haskell display nothing

I have been experimenting with Haskell. I am trying to write a web crawler and I need to use external curl binary (due to some proxy settings, curl needs to have some special arguments which seem to be impossible/hard to set inside the haskell code, so i rather just pass it as a command line option. but that is another story...)
In the code at the bottom, if I change the marked line with curl instead of curl --help the output renders properly and gives:
"curl: try 'curl --help' or 'curl --manual' for more information
"
otherwise the string is empty - as the `curl --help' response is multiline.
I suspect that in haskell the buffer is cleared with every new line. (same goes for other simple shell commands like ls versus ls -l etc.)
How do I fix it?
The code:
import System.Process
import System.IO
main = do
let sp = (proc "curl --help"[]){std_out=CreatePipe} -- *** THIS LINE ***
(_,Just out_h,_,_)<- createProcess sp
out <-hGetContents out_h
print out
proc takes as a first argument the name of the executable, not a shell command. That, is when you use proc "foo bar" you are not referring to a foo executable, but to an executable named exactly foo bar, with the space in its file name.
This is a useful feature in practice, because sometimes you do have spaces in there (e.g. on Windows you might have c:\Program Files\Foo\Foo.exe). Using a shell command you would have to escape spaces in your command string. Worse, a few other characters need to be escaped as well, and it's cumbersome to check what exactly those are. proc sidesteps the issue by not using the shell at all but passing the string as it is to the OS.
For the executable arguments, proc takes a separate argument list. E.g.
proc "c:\\Program Files\\Foo\\Foo.exe" ["hello world!%$"]
Note that the arguments need no escaping as well.
If you want to pass arguments to curl you have to pass that it in the list:
sp = (proc "/usr/bin/curl" ["--help"]) {std_out=CreatePipe}
Then you will get the complete output in the entire string.

How do I proc out with tilde expansion AND $PATH searching in Haskell?

I'm trying to run the elm-reactor project, which is written in Haskell. It fails because it's trying to proc out to the elm command like this:
createProcess (proc "elm" $ args fileName)
My elm executable is sitting in ~/.cabal/bin, which is in my PATH.
The System.Process.proc command searches the $PATH for its command argument, but it doesn't do tilde (~) expansion, so it doesn't find elm.
System.Process.shell has the opposite problem. It does tilde expansion, but it doesn't search the $PATH, apparently.
From the source of the System.Process command, it looks like most everything rests on a foreign ccall to "runInteractiveProcess", which I assume is doing whatever $PATH searching is being done. I don't know where the source for runInteractiveProcess would be, and my C is about 15 years worth of rusty.
I can work around this issue by
a) adding the fully-expanded cabal/bin path to my PATH or
b) symlinking an elm from the working directory to its location in cabal/bin.
However, I'd like to offer a suggested fix to the elm project, to save future adopters the trouble I've gone through. Is there a System.Process call that they should be making here that I haven't tried? Or is there a different method they should be using? I suppose at worst they could getEnv for the PATH and HOME, and implement their own file search using that before calling proc - but that breaks cross-platform compatibility. Any other suggestions?
Try using shell instead of proc, i.e.:
createProcess (shell "elm")
This should invoke elm via a shell, which hopefully will interpret tildes in $PATH as desired.
Update: Here is the experiment I performed to test what shell does...
Compile the following program (I called it run-foofoo):
import System.Process
main = do
(,,_,h) <- createProcess $ shell "foofoo"
ec <- waitForProcess h
print ec
Create a new directory ~/new-bin and place the following perl script there as the file foofoo:
#!/usr/bin/perl
print "Got here and PATH is $ENV{PATH}\n";
Run: chmod a+rx ~/new-bin/foofoo
Test with:
PATH="/bin:/usr/bin:/sbin" ./run-foofoo # should fail
PATH="$HOME/new-bin:/bin:/usr/bin:/sbin" ./run-foofoo # should succeed
PATH="~/new-bin:/bin:/usr/bin:/sbin" ./run-foofoo # ???
On my OSX system, the third test reports:
Got here and PATH is ~/new-bin:/bin:/usr/bin:/sbin
ExitSuccess

Prevent Haskell's getArgs from parsing glob expressions

I'm using getArgs in my Haskell program to parse arguments from the command line. However, I've noticed that I have problems when I call my program like this:
runhaskell Main *.hs .
As you can imagine, Main looks at *.hs files in the . directory.
But there's a problem. The code below doesn't behave as I'd expect.
import System
main :: IO ()
main = do
args <- getArgs
print args
I want to see
args = ["*.hs","."]
but instead I see
args = ["file1.hs","file2.hs","file3.hs","."]
My program needs to expand the glob expression itself because it does it in multiple directories. But how can I have getArgs return the raw list of arguments instead of trying to be magical and do the parsing itself?
This isn't haskell, this is your shell.
Normally file globs (like *.hs) are interpreted by the shell before the command is passed to haskell.
To see this try running
runhaskell Main '*.hs' .
The single quotes will prevent your shell from interpreting the glob, and pass the argument as-is to runhaskell.
getArgs doesn't parse the glob expressison - getArgs never sees the glob expression. The glob expression is parsed and expanded by your shell before your program even starts.
There's nothing you can do inside your program to prevent this - your only option is to escape the glob in the shell (by adding a backslash before the * for example or surrounding the argument with single quotes).

Resources