diff --git a/htools/Ganeti/Common.hs b/htools/Ganeti/Common.hs
index 06bbbe56e06021822398834cffd42fcb1ef3b471..6a41548974edecb4635f259722f0ad22ab45c87f 100644
--- a/htools/Ganeti/Common.hs
+++ b/htools/Ganeti/Common.hs
@@ -42,20 +42,25 @@ module Ganeti.Common
   , parseYesNo
   , parseOpts
   , parseOptsInner
+  , parseOptsCmds
+  , genericMainCmds
   ) where
 
 import Control.Monad (foldM)
 import Data.Char (toLower)
-import Data.List (intercalate, stripPrefix)
+import Data.List (intercalate, stripPrefix, sortBy)
 import Data.Maybe (fromMaybe)
+import Data.Ord (comparing)
 import qualified Data.Version
 import System.Console.GetOpt
+import System.Environment
 import System.Exit
 import System.Info
 import System.IO
 import Text.Printf (printf)
 
 import Ganeti.BasicTypes
+import qualified Ganeti.Constants as C
 import qualified Ganeti.Version as Version (version)
 
 -- | Parameter type.
@@ -189,6 +194,38 @@ reqWithConversion conversion_fn updater_fn =
             parsed_value <- conversion_fn string_opt
             updater_fn parsed_value opts)
 
+-- | Max command length when formatting command list output.
+maxCmdLen :: Int
+maxCmdLen = 60
+
+-- | Formats usage for a multi-personality program.
+formatCmdUsage :: (StandardOptions a) => String -> PersonalityList a -> String
+formatCmdUsage prog personalities =
+  let mlen = min maxCmdLen . maximum $ map (length . fst) personalities
+      sorted = sortBy (comparing fst) personalities
+      header = [ printf "Usage: %s {command} [options...] [argument...]" prog
+               , printf "%s <command> --help to see details, or man %s"
+                   prog prog
+               , ""
+               , "Commands:"
+               ]
+      rows = map (\(cmd, _) ->
+                    printf " %-*s" mlen cmd::String) sorted
+  in unlines $ header ++ rows
+
+-- | Displays usage for a program and exits.
+showCmdUsage :: (StandardOptions a) =>
+                String            -- ^ Program name
+             -> PersonalityList a -- ^ Personality list
+             -> Bool              -- ^ Whether the exit code is success or not
+             -> IO b
+showCmdUsage prog personalities success = do
+  let usage = formatCmdUsage prog personalities
+  putStr usage
+  if success
+    then exitSuccess
+    else exitWith $ ExitFailure C.exitFailure
+
 -- | Command line parser, using a generic 'Options' structure.
 parseOpts :: (StandardOptions a) =>
              a                      -- ^ The default options
@@ -206,6 +243,35 @@ parseOpts defaults argv progname options arguments =
     Right result ->
       return result
 
+-- | Command line parser, for programs with sub-commands.
+parseOptsCmds :: (StandardOptions a) =>
+                 a                      -- ^ The default options
+              -> [String]               -- ^ The command line arguments
+              -> String                 -- ^ The program name
+              -> PersonalityList a      -- ^ The supported commands
+              -> [GenericOptType a]     -- ^ Generic options
+              -> IO (a, [String], a -> [String] -> IO ())
+                     -- ^ The resulting options and leftover arguments
+parseOptsCmds defaults argv progname personalities genopts = do
+  let usage = showCmdUsage progname personalities
+      check c = case c of
+                  -- hardcoded option strings here!
+                  "--version" -> putStrLn (versionInfo progname) >> exitSuccess
+                  "--help"    -> usage True
+                  _           -> return c
+  (cmd, cmd_args) <- case argv of
+                       cmd:cmd_args -> do
+                         cmd' <- check cmd
+                         return (cmd', cmd_args)
+                       [] -> usage False
+  case cmd `lookup` personalities of
+    Nothing -> usage False
+    Just (mainfn, optdefs, argdefs) -> do
+      optdefs' <- optdefs
+      (opts, args) <- parseOpts defaults cmd_args progname
+                      (optdefs' ++ genopts) argdefs
+      return (opts, args, mainfn)
+
 -- | Inner parse options. The arguments are similar to 'parseOpts',
 -- but it returns either a 'Left' composed of exit code and message,
 -- or a 'Right' for the success case.
@@ -236,3 +302,17 @@ parseOptsInner defaults argv progname options arguments  =
     (_, _, errs) ->
       Left (ExitFailure 2, "Command line error: "  ++ concat errs ++ "\n" ++
             usageHelp progname options)
+
+-- | Parse command line options and execute the main function of a
+-- multi-personality binary.
+genericMainCmds :: (StandardOptions a) =>
+                   a
+                -> PersonalityList a
+                -> [GenericOptType a]
+                -> IO ()
+genericMainCmds defaults personalities genopts = do
+  cmd_args <- getArgs
+  prog <- getProgName
+  (opts, args, fn) <-
+    parseOptsCmds defaults cmd_args prog personalities genopts
+  fn opts args
diff --git a/htools/mon-collector.hs b/htools/mon-collector.hs
index a5a6be846ec97e372fd9eee9074501be3eea5657..d434e3248fc016d9d94397606de1237406ce32ff 100644
--- a/htools/mon-collector.hs
+++ b/htools/mon-collector.hs
@@ -25,38 +25,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Main (main) where
 
-import Data.Char (toLower)
-import System.Environment
-import System.IO
-
 import Ganeti.Common
-import Ganeti.Utils
 import Ganeti.DataCollectors.CLI (genericOptions, defaultOptions)
 import Ganeti.DataCollectors.Program (personalities)
 
--- | Display usage and exit.
-usage :: String -> IO ()
-usage name = do
-  hPutStrLn stderr $ "Unrecognised personality '" ++ name ++ "'."
-  hPutStrLn stderr "This program must be executed specifying one of the \
-                    \following names as the first parameter:"
-  mapM_ (hPutStrLn stderr . ("  - " ++) . fst) personalities
-  exitErr "Please specify the desired role."
-
+-- | Simple main function.
 main :: IO ()
-main = do
-  cmd_args <- getArgs
-  let binary =
-        if null cmd_args
-          then ""
-          else head cmd_args
-      name = map toLower binary
-      boolnames = map (\(x, y) -> (x == name, Just y)) personalities
-  case select Nothing boolnames of
-    Nothing -> usage name
-    Just (fn, options, arguments) -> do
-         let actual_args = tail cmd_args
-         real_options <- options
-         (opts, args) <- parseOpts defaultOptions actual_args name
-                           (real_options ++ genericOptions) arguments
-         fn opts args
+main = genericMainCmds defaultOptions personalities genericOptions