Search code examples
haskellhaskell-turtle

How can a `Shell Text` be converted to `Shell Line`?


In a Turtle script for getting passwords from a keyring, calling ssh-add with those passwords so they don't have to be filled out manually, is the following function:

processKey :: (T.Text, T.Text) -> Shell Line -> IO ()
processKey (kn, str) pwds = do
    -- Not sure why (text str) is needed here, but it won't typecheck without
    -- it (despite OverloadedStrings).
    let expectArg = do
        ml <- grep (contains (text str)) pwds
        let pass = getPwd $ cut tab (format l ml)
        return $ T.unlines [ "<< EOF"
                  , "spawn ssh-add"
                  , "expect \"Enter passphrase\""
                  , "send " <> pass
                  , "expect eof"
                  , "EOF"
                  ]
    view (inproc "expect" [] expectArg)
    where
       -- Safely get the third item `cut` from the list.
       getPwd xs = getPwd' 0 xs
       getPwd' _ []     = ""
       getPwd' n (x:xs) = if n == 2
                          then x
                          else getPwd' (n+1) xs

This function takes a tuple of (SSH key file name, string to search for in the text stored in the keyring), and pwds :: Shell Line that is the entire contents of the keyring taken from a shell command.

The purpose of the function is to grep the passwords, and call ssh-add with the key filename and password.

The problem is this function doesn't type check:

sshkeys-autopass.hs:45:30: error:
    • Couldn't match type ‘Text’ with ‘Line’
      Expected type: Shell Line
        Actual type: Shell Text
    • In the third argument of ‘inproc’, namely ‘expectArg’
      In the first argument of ‘view’, namely
        ‘(inproc "expect" [] expectArg)’
      In a stmt of a 'do' block: view (inproc "expect" [] expectArg)
   |
45 |     view (inproc "expect" [] expectArg)
   |                              ^^^^^^^^^

It seems like Shell Line needs to become Shell Text, how can this be done, please? I am open to the possibility this is structured badly or is not idiomatic Haskell (it does smell), if so please advise how this function could be better.


Solution

  • While I can't try out your code right now, it seems roundtripping your commands through Text (as T.unlines forces you to) is causing unnecessary trouble. According to the documentation (emphasis mine):

    A (Shell a) is a protected stream of a's with side effects

    As a Shell Line is a stream, it can supply multiple Lines. And sure enough, there is a function called select...

    select :: Foldable f => f a -> Shell a

    ... which will convert a list (or any other Foldable) to a Shell. You can use it to get the Shell Line you need directly:

    {-# LANGUAGE OverloadedStrings #-}
    -- etc.
        let expectArg = do
            ml <- grep (contains (text str)) pwds
            -- unsafeTextToLine is presumably safe here,
            -- as ml was a Line to begin with.
            let pass = unsafeTextToLine . getPwd $ cut tab (format l ml)
            select [ "<< EOF"
                , "spawn ssh-add"
                , "expect \"Enter passphrase\""
                , "send " <> pass
                , "expect eof"
                , "EOF"
                ]
        view (inproc "expect" [] expectArg)
    

    Side questions:

    Not sure why (text str) is needed here, but it won't typecheck without it (despite OverloadedStrings).

    The only thing OverloadedStrings does automatically is handling string literals. It won't silently convert Text values to other instances of IsString. An alternative to using text would be changing your signature so that the type of str is Pattern Text rather than Text.

    Safely get the third item cut from the list.

    Here is one way of writing getPwd without writing the recursive algorithm explicitly, using a few functions from Data.Maybe:

    getPwd = fromMaybe "" . listToMaybe . drop 2
    

    You might also like the atDef from the safe package:

    getPwd xs = atDef "" xs 2
    

    But that causes another problem: inproc does not want a Shell (NonEmpty Line). I have no idea what to do about this.

    NonEmpty is a type for lists that are guaranteed to have at least one element. In your case, the lack of a sensible way of going from NonEmpty Line to Line (concatenating the elements or picking the first one, for instance, wouldn't help at all) was a signal that a change of approach was necessary.