propellor spin
This commit is contained in:
parent
4e442f4bcf
commit
61d8214d9d
|
@ -0,0 +1,100 @@
|
||||||
|
module CmdLine where
|
||||||
|
|
||||||
|
import System.Environment
|
||||||
|
import Data.List
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
import Common
|
||||||
|
import Utility.FileMode
|
||||||
|
|
||||||
|
data CmdLine
|
||||||
|
= Run HostName
|
||||||
|
| Spin HostName
|
||||||
|
| Boot HostName
|
||||||
|
| Set HostName PrivDataField String
|
||||||
|
|
||||||
|
processCmdLine :: IO CmdLine
|
||||||
|
processCmdLine = go =<< getArgs
|
||||||
|
where
|
||||||
|
go ("--help":_) = usage
|
||||||
|
go ("--spin":h:[]) = return $ Spin h
|
||||||
|
go ("--boot":h:[]) = return $ Boot h
|
||||||
|
go ("--set":h:f:v:[]) = case readish f of
|
||||||
|
Just pf -> return $ Set h pf v
|
||||||
|
Nothing -> error $ "Unknown privdata field " ++ f
|
||||||
|
go (h:[]) = return $ Run h
|
||||||
|
go [] = do
|
||||||
|
s <- takeWhile (/= '\n') <$> readProcess "hostname" ["-f"]
|
||||||
|
if null s
|
||||||
|
then error "Cannot determine hostname! Pass it on the command line."
|
||||||
|
else return $ Run s
|
||||||
|
go _ = usage
|
||||||
|
|
||||||
|
usage :: IO a
|
||||||
|
usage = do
|
||||||
|
putStrLn $ unlines
|
||||||
|
[ "Usage:"
|
||||||
|
, " propellor"
|
||||||
|
, " propellor hostname"
|
||||||
|
, " propellor --spin hostname"
|
||||||
|
, " propellor --set hostname field value"
|
||||||
|
]
|
||||||
|
exitFailure
|
||||||
|
|
||||||
|
defaultMain :: (HostName -> [Property]) -> IO ()
|
||||||
|
defaultMain getprops = go =<< processCmdLine
|
||||||
|
where
|
||||||
|
go (Run host) = ensureProperties (getprops host)
|
||||||
|
go (Spin host) = spin host
|
||||||
|
go (Boot host) = boot (getprops host)
|
||||||
|
go (Set host field val) = setPrivData host field val
|
||||||
|
|
||||||
|
spin :: HostName -> IO ()
|
||||||
|
spin host = do
|
||||||
|
url <- getUrl
|
||||||
|
privdata <- gpgDecrypt (privDataFile host)
|
||||||
|
void $ boolSystem "git" [Param "commit", Param "-a", Param "-m", Param "propellor spin"]
|
||||||
|
withHandle StdinHandle createProcessSuccess
|
||||||
|
(proc "ssh" ["root@"++host, bootstrap url]) $ \h -> do
|
||||||
|
hPutStr h $ unlines $ map (privDataMarker ++) $ lines privdata
|
||||||
|
hClose h
|
||||||
|
where
|
||||||
|
bootstrap url = shellWrap $ intercalate " && "
|
||||||
|
[ intercalate " ; "
|
||||||
|
[ "if [ ! -d " ++ localdir ++ " ]"
|
||||||
|
, "then"
|
||||||
|
, intercalate " && "
|
||||||
|
[ "apt-get -y install git"
|
||||||
|
, "git clone " ++ url ++ " " ++ localdir
|
||||||
|
]
|
||||||
|
, "fi"
|
||||||
|
]
|
||||||
|
, "cd " ++ localdir
|
||||||
|
, "make pull build"
|
||||||
|
, "./propellor --boot " ++ host
|
||||||
|
]
|
||||||
|
|
||||||
|
boot :: [Property] -> IO ()
|
||||||
|
boot props = do
|
||||||
|
privdata <- map (drop $ length privDataMarker )
|
||||||
|
. filter (privDataMarker `isPrefixOf`)
|
||||||
|
. lines
|
||||||
|
<$> getContents
|
||||||
|
writeFileProtected privDataLocal (unlines privdata)
|
||||||
|
ensureProperties props
|
||||||
|
|
||||||
|
localdir :: FilePath
|
||||||
|
localdir = "/usr/local/propellor"
|
||||||
|
|
||||||
|
getUrl :: IO String
|
||||||
|
getUrl = fromMaybe nourl <$> getM get urls
|
||||||
|
where
|
||||||
|
urls = ["remote.deploy.url", "remote.origin.url"]
|
||||||
|
nourl = error $ "Cannot find deploy url in " ++ show urls
|
||||||
|
get u = do
|
||||||
|
v <- catchMaybeIO $
|
||||||
|
takeWhile (/= '\n')
|
||||||
|
<$> readProcess "git" ["config", u]
|
||||||
|
return $ case v of
|
||||||
|
Just url | not (null url) -> Just url
|
||||||
|
_ -> Nothing
|
|
@ -1,8 +1,11 @@
|
||||||
module Common (module X) where
|
module Common (module X) where
|
||||||
|
|
||||||
|
import Types as X
|
||||||
import Property as X
|
import Property as X
|
||||||
import Property.Cmd as X
|
import Property.Cmd as X
|
||||||
|
import PrivData as X
|
||||||
|
|
||||||
|
import Utility.PartialPrelude as X
|
||||||
import Control.Applicative as X
|
import Control.Applicative as X
|
||||||
import Control.Monad as X
|
import Control.Monad as X
|
||||||
import Utility.Process as X
|
import Utility.Process as X
|
||||||
|
|
18
HostName.hs
18
HostName.hs
|
@ -1,18 +0,0 @@
|
||||||
module HostName where
|
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
import System.Environment
|
|
||||||
|
|
||||||
import Utility.Process
|
|
||||||
|
|
||||||
type HostName = String
|
|
||||||
|
|
||||||
getHostName :: IO HostName
|
|
||||||
getHostName = go =<< getArgs
|
|
||||||
where
|
|
||||||
go (h:_) = return h
|
|
||||||
go [] = do
|
|
||||||
s <- takeWhile (/= '\n') <$> readProcess "hostname" ["-f"]
|
|
||||||
if null s
|
|
||||||
then error "Cannot determine hostname! Pass it on the command line."
|
|
||||||
else return s
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Common
|
import Common
|
||||||
import HostName
|
import CmdLine
|
||||||
import qualified Property.File as File
|
import qualified Property.File as File
|
||||||
import qualified Property.Apt as Apt
|
import qualified Property.Apt as Apt
|
||||||
import qualified Property.Ssh as Ssh
|
import qualified Property.Ssh as Ssh
|
||||||
|
@ -10,7 +10,7 @@ import qualified Property.Tor as Tor
|
||||||
import qualified Property.GitHome as GitHome
|
import qualified Property.GitHome as GitHome
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = ensureProperties . getProperties =<< getHostName
|
main = defaultMain getProperties
|
||||||
|
|
||||||
{- This is where the system's HostName, either as returned by uname
|
{- This is where the system's HostName, either as returned by uname
|
||||||
- or one specified on the command line, is converted into a list of
|
- or one specified on the command line, is converted into a list of
|
||||||
|
@ -21,6 +21,7 @@ getProperties hostname@"clam.kitenet.net" =
|
||||||
, standardSystem Apt.Unstable
|
, standardSystem Apt.Unstable
|
||||||
-- Clam is a tor bridge.
|
-- Clam is a tor bridge.
|
||||||
, Tor.isBridge
|
, Tor.isBridge
|
||||||
|
, Apt.installed ["docker.io"]
|
||||||
-- This is not an important system so I don't want to need to
|
-- This is not an important system so I don't want to need to
|
||||||
-- manually upgrade it.
|
-- manually upgrade it.
|
||||||
, Apt.unattendedUpgrades True
|
, Apt.unattendedUpgrades True
|
||||||
|
|
19
Property.hs
19
Property.hs
|
@ -6,27 +6,10 @@ import System.Console.ANSI
|
||||||
import System.Exit
|
import System.Exit
|
||||||
import System.IO
|
import System.IO
|
||||||
|
|
||||||
|
import Types
|
||||||
import Utility.Monad
|
import Utility.Monad
|
||||||
import Utility.Exception
|
import Utility.Exception
|
||||||
|
|
||||||
data Property = Property
|
|
||||||
{ propertyDesc :: Desc
|
|
||||||
-- must be idempotent; may run repeatedly
|
|
||||||
, propertySatisfy :: IO Result
|
|
||||||
}
|
|
||||||
|
|
||||||
type Desc = String
|
|
||||||
|
|
||||||
data Result = NoChange | MadeChange | FailedChange
|
|
||||||
deriving (Show, Eq)
|
|
||||||
|
|
||||||
combineResult :: Result -> Result -> Result
|
|
||||||
combineResult FailedChange _ = FailedChange
|
|
||||||
combineResult _ FailedChange = FailedChange
|
|
||||||
combineResult MadeChange _ = MadeChange
|
|
||||||
combineResult _ MadeChange = MadeChange
|
|
||||||
combineResult NoChange NoChange = NoChange
|
|
||||||
|
|
||||||
makeChange :: IO () -> IO Result
|
makeChange :: IO () -> IO Result
|
||||||
makeChange a = a >> return MadeChange
|
makeChange a = a >> return MadeChange
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ module Property.Cmd (
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
|
|
||||||
import Property
|
import Types
|
||||||
import Utility.Monad
|
import Utility.Monad
|
||||||
import Utility.SafeCommand
|
import Utility.SafeCommand
|
||||||
import Utility.Env
|
import Utility.Env
|
||||||
|
|
|
@ -3,8 +3,6 @@ module Property.Hostname where
|
||||||
import Common
|
import Common
|
||||||
import qualified Property.File as File
|
import qualified Property.File as File
|
||||||
|
|
||||||
type HostName = String
|
|
||||||
|
|
||||||
set :: HostName -> Property
|
set :: HostName -> Property
|
||||||
set hostname = "/etc/hostname" `File.hasContent` [hostname]
|
set hostname = "/etc/hostname" `File.hasContent` [hostname]
|
||||||
`onChange` cmdProperty "hostname" [Param hostname]
|
`onChange` cmdProperty "hostname" [Param hostname]
|
||||||
|
|
|
@ -4,8 +4,6 @@ import System.Posix
|
||||||
|
|
||||||
import Common
|
import Common
|
||||||
|
|
||||||
type UserName = String
|
|
||||||
|
|
||||||
data Eep = YesReallyDeleteHome
|
data Eep = YesReallyDeleteHome
|
||||||
|
|
||||||
sshAccountFor :: UserName -> Property
|
sshAccountFor :: UserName -> Property
|
||||||
|
@ -24,6 +22,14 @@ nuked user _ = check (isJust <$> homedir user) $ cmdProperty "userdel"
|
||||||
]
|
]
|
||||||
`describe` ("nuked user " ++ user)
|
`describe` ("nuked user " ++ user)
|
||||||
|
|
||||||
|
setPassword :: UserName -> Property
|
||||||
|
setPassword user = Property (user ++ " password set") $
|
||||||
|
withPrivData (Password user) $ \password -> makeChange $
|
||||||
|
withHandle StdinHandle createProcessSuccess
|
||||||
|
(proc "chpasswd" []) $ \h -> do
|
||||||
|
hPutStrLn h $ user ++ ":" ++ password
|
||||||
|
hClose h
|
||||||
|
|
||||||
lockedPassword :: UserName -> Property
|
lockedPassword :: UserName -> Property
|
||||||
lockedPassword user = check (not <$> isLockedPassword user) $ cmdProperty "passwd"
|
lockedPassword user = check (not <$> isLockedPassword user) $ cmdProperty "passwd"
|
||||||
[ Param "--lock"
|
[ Param "--lock"
|
||||||
|
|
19
README
19
README
|
@ -11,9 +11,6 @@ to a system, and "make" can be used to pull down any new changes,
|
||||||
and compile and run propellor. This can be done by a cron job. Or something
|
and compile and run propellor. This can be done by a cron job. Or something
|
||||||
can ssh in and run it.
|
can ssh in and run it.
|
||||||
|
|
||||||
For bootstrapping, propellor compiles to a single binary file,
|
|
||||||
which can be transferred to a host and run.
|
|
||||||
|
|
||||||
Properties are defined using Haskell. Edit Propellor.hs
|
Properties are defined using Haskell. Edit Propellor.hs
|
||||||
|
|
||||||
There is no special language as used in puppet, chef, ansible, etc, just
|
There is no special language as used in puppet, chef, ansible, etc, just
|
||||||
|
@ -26,4 +23,20 @@ of which classes and share which configuration. It might be nice to use
|
||||||
reclass[1], but then again a host is configured using simply haskell code,
|
reclass[1], but then again a host is configured using simply haskell code,
|
||||||
and so it's easy to factor out things like classes of hosts as desired.
|
and so it's easy to factor out things like classes of hosts as desired.
|
||||||
|
|
||||||
|
To bootstrap propellor on a new host, use: propellor --spin $host
|
||||||
|
This looks up the git repository's remote.origin.url (or remote.deploy.url
|
||||||
|
if available) and logs into the host, clones the url (if not already
|
||||||
|
done), and sets up and runs propellor in /usr/local/propellor
|
||||||
|
|
||||||
|
Private data such as passwords, ssh private keys, etc should not be checked
|
||||||
|
into a propellor git repository in the clear, unless you want to restrict
|
||||||
|
access to the repository. Which would probably involve a separate fork
|
||||||
|
for each host and be annoying.
|
||||||
|
|
||||||
|
Instead, propellor --spin $host looks for a privdata/$host.gpg file and
|
||||||
|
if found decrypts it and sends it to the host using ssh. To set a field
|
||||||
|
in such a file, use: propellor --set $host $field $value
|
||||||
|
The field name is will be something like 'Password "root"'; see PrivData.hs
|
||||||
|
for available fields.
|
||||||
|
|
||||||
[1] http://reclass.pantsfullofunix.net/
|
[1] http://reclass.pantsfullofunix.net/
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
module Types where
|
||||||
|
|
||||||
|
type HostName = String
|
||||||
|
type UserName = String
|
||||||
|
|
||||||
|
data Property = Property
|
||||||
|
{ propertyDesc :: Desc
|
||||||
|
-- must be idempotent; may run repeatedly
|
||||||
|
, propertySatisfy :: IO Result
|
||||||
|
}
|
||||||
|
|
||||||
|
type Desc = String
|
||||||
|
|
||||||
|
data Result = NoChange | MadeChange | FailedChange
|
||||||
|
deriving (Show, Eq)
|
||||||
|
|
||||||
|
combineResult :: Result -> Result -> Result
|
||||||
|
combineResult FailedChange _ = FailedChange
|
||||||
|
combineResult _ FailedChange = FailedChange
|
||||||
|
combineResult MadeChange _ = MadeChange
|
||||||
|
combineResult _ MadeChange = MadeChange
|
||||||
|
combineResult NoChange NoChange = NoChange
|
|
@ -0,0 +1,155 @@
|
||||||
|
{- File mode utilities.
|
||||||
|
-
|
||||||
|
- Copyright 2010-2012 Joey Hess <joey@kitenet.net>
|
||||||
|
-
|
||||||
|
- Licensed under the GNU GPL version 3 or higher.
|
||||||
|
-}
|
||||||
|
|
||||||
|
{-# LANGUAGE CPP #-}
|
||||||
|
|
||||||
|
module Utility.FileMode where
|
||||||
|
|
||||||
|
import System.IO
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Exception (bracket)
|
||||||
|
import System.PosixCompat.Types
|
||||||
|
#ifndef mingw32_HOST_OS
|
||||||
|
import System.Posix.Files
|
||||||
|
#endif
|
||||||
|
import Foreign (complement)
|
||||||
|
|
||||||
|
import Utility.Exception
|
||||||
|
|
||||||
|
{- Applies a conversion function to a file's mode. -}
|
||||||
|
modifyFileMode :: FilePath -> (FileMode -> FileMode) -> IO ()
|
||||||
|
modifyFileMode f convert = void $ modifyFileMode' f convert
|
||||||
|
modifyFileMode' :: FilePath -> (FileMode -> FileMode) -> IO FileMode
|
||||||
|
modifyFileMode' f convert = do
|
||||||
|
s <- getFileStatus f
|
||||||
|
let old = fileMode s
|
||||||
|
let new = convert old
|
||||||
|
when (new /= old) $
|
||||||
|
setFileMode f new
|
||||||
|
return old
|
||||||
|
|
||||||
|
{- Adds the specified FileModes to the input mode, leaving the rest
|
||||||
|
- unchanged. -}
|
||||||
|
addModes :: [FileMode] -> FileMode -> FileMode
|
||||||
|
addModes ms m = combineModes (m:ms)
|
||||||
|
|
||||||
|
{- Removes the specified FileModes from the input mode. -}
|
||||||
|
removeModes :: [FileMode] -> FileMode -> FileMode
|
||||||
|
removeModes ms m = m `intersectFileModes` complement (combineModes ms)
|
||||||
|
|
||||||
|
{- Runs an action after changing a file's mode, then restores the old mode. -}
|
||||||
|
withModifiedFileMode :: FilePath -> (FileMode -> FileMode) -> IO a -> IO a
|
||||||
|
withModifiedFileMode file convert a = bracket setup cleanup go
|
||||||
|
where
|
||||||
|
setup = modifyFileMode' file convert
|
||||||
|
cleanup oldmode = modifyFileMode file (const oldmode)
|
||||||
|
go _ = a
|
||||||
|
|
||||||
|
writeModes :: [FileMode]
|
||||||
|
writeModes = [ownerWriteMode, groupWriteMode, otherWriteMode]
|
||||||
|
|
||||||
|
readModes :: [FileMode]
|
||||||
|
readModes = [ownerReadMode, groupReadMode, otherReadMode]
|
||||||
|
|
||||||
|
executeModes :: [FileMode]
|
||||||
|
executeModes = [ownerExecuteMode, groupExecuteMode, otherExecuteMode]
|
||||||
|
|
||||||
|
{- Removes the write bits from a file. -}
|
||||||
|
preventWrite :: FilePath -> IO ()
|
||||||
|
preventWrite f = modifyFileMode f $ removeModes writeModes
|
||||||
|
|
||||||
|
{- Turns a file's owner write bit back on. -}
|
||||||
|
allowWrite :: FilePath -> IO ()
|
||||||
|
allowWrite f = modifyFileMode f $ addModes [ownerWriteMode]
|
||||||
|
|
||||||
|
{- Turns a file's owner read bit back on. -}
|
||||||
|
allowRead :: FilePath -> IO ()
|
||||||
|
allowRead f = modifyFileMode f $ addModes [ownerReadMode]
|
||||||
|
|
||||||
|
{- Allows owner and group to read and write to a file. -}
|
||||||
|
groupSharedModes :: [FileMode]
|
||||||
|
groupSharedModes =
|
||||||
|
[ ownerWriteMode, groupWriteMode
|
||||||
|
, ownerReadMode, groupReadMode
|
||||||
|
]
|
||||||
|
|
||||||
|
groupWriteRead :: FilePath -> IO ()
|
||||||
|
groupWriteRead f = modifyFileMode f $ addModes groupSharedModes
|
||||||
|
|
||||||
|
checkMode :: FileMode -> FileMode -> Bool
|
||||||
|
checkMode checkfor mode = checkfor `intersectFileModes` mode == checkfor
|
||||||
|
|
||||||
|
{- Checks if a file mode indicates it's a symlink. -}
|
||||||
|
isSymLink :: FileMode -> Bool
|
||||||
|
#ifdef mingw32_HOST_OS
|
||||||
|
isSymLink _ = False
|
||||||
|
#else
|
||||||
|
isSymLink = checkMode symbolicLinkMode
|
||||||
|
#endif
|
||||||
|
|
||||||
|
{- Checks if a file has any executable bits set. -}
|
||||||
|
isExecutable :: FileMode -> Bool
|
||||||
|
isExecutable mode = combineModes executeModes `intersectFileModes` mode /= 0
|
||||||
|
|
||||||
|
{- Runs an action without that pesky umask influencing it, unless the
|
||||||
|
- passed FileMode is the standard one. -}
|
||||||
|
noUmask :: FileMode -> IO a -> IO a
|
||||||
|
#ifndef mingw32_HOST_OS
|
||||||
|
noUmask mode a
|
||||||
|
| mode == stdFileMode = a
|
||||||
|
| otherwise = withUmask nullFileMode a
|
||||||
|
#else
|
||||||
|
noUmask _ a = a
|
||||||
|
#endif
|
||||||
|
|
||||||
|
withUmask :: FileMode -> IO a -> IO a
|
||||||
|
#ifndef mingw32_HOST_OS
|
||||||
|
withUmask umask a = bracket setup cleanup go
|
||||||
|
where
|
||||||
|
setup = setFileCreationMask umask
|
||||||
|
cleanup = setFileCreationMask
|
||||||
|
go _ = a
|
||||||
|
#else
|
||||||
|
withUmask _ a = a
|
||||||
|
#endif
|
||||||
|
|
||||||
|
combineModes :: [FileMode] -> FileMode
|
||||||
|
combineModes [] = undefined
|
||||||
|
combineModes [m] = m
|
||||||
|
combineModes (m:ms) = foldl unionFileModes m ms
|
||||||
|
|
||||||
|
isSticky :: FileMode -> Bool
|
||||||
|
#ifdef mingw32_HOST_OS
|
||||||
|
isSticky _ = False
|
||||||
|
#else
|
||||||
|
isSticky = checkMode stickyMode
|
||||||
|
|
||||||
|
stickyMode :: FileMode
|
||||||
|
stickyMode = 512
|
||||||
|
|
||||||
|
setSticky :: FilePath -> IO ()
|
||||||
|
setSticky f = modifyFileMode f $ addModes [stickyMode]
|
||||||
|
#endif
|
||||||
|
|
||||||
|
{- Writes a file, ensuring that its modes do not allow it to be read
|
||||||
|
- or written by anyone other than the current user,
|
||||||
|
- before any content is written.
|
||||||
|
-
|
||||||
|
- When possible, this is done using the umask.
|
||||||
|
-
|
||||||
|
- On a filesystem that does not support file permissions, this is the same
|
||||||
|
- as writeFile.
|
||||||
|
-}
|
||||||
|
writeFileProtected :: FilePath -> String -> IO ()
|
||||||
|
writeFileProtected file content = withUmask 0o0077 $
|
||||||
|
withFile file WriteMode $ \h -> do
|
||||||
|
void $ tryIO $ modifyFileMode file $
|
||||||
|
removeModes
|
||||||
|
[ groupReadMode, otherReadMode
|
||||||
|
, groupWriteMode, otherWriteMode
|
||||||
|
]
|
||||||
|
hPutStr h content
|
|
@ -0,0 +1,68 @@
|
||||||
|
{- Parts of the Prelude are partial functions, which are a common source of
|
||||||
|
- bugs.
|
||||||
|
-
|
||||||
|
- This exports functions that conflict with the prelude, which avoids
|
||||||
|
- them being accidentially used.
|
||||||
|
-}
|
||||||
|
|
||||||
|
module Utility.PartialPrelude where
|
||||||
|
|
||||||
|
import qualified Data.Maybe
|
||||||
|
|
||||||
|
{- read should be avoided, as it throws an error
|
||||||
|
- Instead, use: readish -}
|
||||||
|
read :: Read a => String -> a
|
||||||
|
read = Prelude.read
|
||||||
|
|
||||||
|
{- head is a partial function; head [] is an error
|
||||||
|
- Instead, use: take 1 or headMaybe -}
|
||||||
|
head :: [a] -> a
|
||||||
|
head = Prelude.head
|
||||||
|
|
||||||
|
{- tail is also partial
|
||||||
|
- Instead, use: drop 1 -}
|
||||||
|
tail :: [a] -> [a]
|
||||||
|
tail = Prelude.tail
|
||||||
|
|
||||||
|
{- init too
|
||||||
|
- Instead, use: beginning -}
|
||||||
|
init :: [a] -> [a]
|
||||||
|
init = Prelude.init
|
||||||
|
|
||||||
|
{- last too
|
||||||
|
- Instead, use: end or lastMaybe -}
|
||||||
|
last :: [a] -> a
|
||||||
|
last = Prelude.last
|
||||||
|
|
||||||
|
{- Attempts to read a value from a String.
|
||||||
|
-
|
||||||
|
- Ignores leading/trailing whitespace, and throws away any trailing
|
||||||
|
- text after the part that can be read.
|
||||||
|
-
|
||||||
|
- readMaybe is available in Text.Read in new versions of GHC,
|
||||||
|
- but that one requires the entire string to be consumed.
|
||||||
|
-}
|
||||||
|
readish :: Read a => String -> Maybe a
|
||||||
|
readish s = case reads s of
|
||||||
|
((x,_):_) -> Just x
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
{- Like head but Nothing on empty list. -}
|
||||||
|
headMaybe :: [a] -> Maybe a
|
||||||
|
headMaybe = Data.Maybe.listToMaybe
|
||||||
|
|
||||||
|
{- Like last but Nothing on empty list. -}
|
||||||
|
lastMaybe :: [a] -> Maybe a
|
||||||
|
lastMaybe [] = Nothing
|
||||||
|
lastMaybe v = Just $ Prelude.last v
|
||||||
|
|
||||||
|
{- All but the last element of a list.
|
||||||
|
- (Like init, but no error on an empty list.) -}
|
||||||
|
beginning :: [a] -> [a]
|
||||||
|
beginning [] = []
|
||||||
|
beginning l = Prelude.init l
|
||||||
|
|
||||||
|
{- Like last, but no error on an empty list. -}
|
||||||
|
end :: [a] -> [a]
|
||||||
|
end [] = []
|
||||||
|
end l = [Prelude.last l]
|
|
@ -0,0 +1,19 @@
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
Version: GnuPG v1
|
||||||
|
|
||||||
|
hQIMA7ODiaEXBlRZAQ//V77CfPgJXpqO0W3F41MaSV/sa1PIoB9B/JsLGog+2ZGW
|
||||||
|
sbLevCdlQNoWwmzuBzDBsAdIDgsAQX3o5ldqOaJ6jZSc0AVUUE6QQQ6ggCz5kCre
|
||||||
|
/JU/7YXTthmQv/9zu1tGeX7tjIaHQBkihq+3lS0TQjQqVBQmZJBJjgL+wZJ0fshF
|
||||||
|
T+hrCBi5s9DS/YIGoghpuQVJ0yA16fU7aLaH5jaF0CEsRm8Q+Qvn4v+1YlEi+d7t
|
||||||
|
Mk5AQ0YKE1kC2eIA8TjK1hIF/4NEEY/wnonJTcAhJ6op4gqjmhhQ/sXwaobg8UmQ
|
||||||
|
vew4q3+wiYpLdTGbYMfI3pNV2FpltZXg8DLKjxZFH6H/0cL5xZDG7ZiDJh6hyHJN
|
||||||
|
unpjgLm0UxvSLb/Jp4vycycP8SXz+XSo1ZhQDq8Qof1Sg5LBSXdzouM1xSX+kPqm
|
||||||
|
+0C9eabqNCG7deVDDQe9V25+CUIVMM70WavtRRICwNrUZCrChCO85gMZPzpYYYr2
|
||||||
|
+3z+ygPw8s41waCBPH6EH+9Qw5PmIyqBYoFbnPNY8g2hmu2oFlEcN7REzYduZL3D
|
||||||
|
MNRahF/l40Ek4l3TYDI4OPyIqRb8sCifZTphyjhIULlTGlfuO4gwRiHIdssSpH7t
|
||||||
|
TjhcgJfqBPSmyq4oIRUUxoBwMsaL9OunFi+7pHInbGksLY7Lfv07P3WpBdYjlwXS
|
||||||
|
cgGqVY+WDTMvde+LYu459OkZW80VH+WgJb7NWpRiFqJyTyOtqeLIT+33/noO4f4y
|
||||||
|
VYRb94zsB4n8u3tZfIFAGj2G6pbJPMhyEvST6ePkL0q63rC+BjwsQ0Q7wXVPLqMu
|
||||||
|
mxr+K75/DTbP5ft8gmY80sDrHQ==
|
||||||
|
=vDG9
|
||||||
|
-----END PGP MESSAGE-----
|
|
@ -19,7 +19,8 @@ Executable propellor
|
||||||
Main-Is: Propellor.hs
|
Main-Is: Propellor.hs
|
||||||
GHC-Options: -Wall
|
GHC-Options: -Wall
|
||||||
Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5,
|
Build-Depends: MissingH, directory, filepath, base >= 4.5, base < 5,
|
||||||
IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal
|
IfElse, process, bytestring, hslogger, unix-compat, ansi-terminal,
|
||||||
|
containers
|
||||||
|
|
||||||
if (! os(windows))
|
if (! os(windows))
|
||||||
Build-Depends: unix
|
Build-Depends: unix
|
||||||
|
|
Loading…
Reference in New Issue