diff --git a/src/Ganeti/Common.hs b/src/Ganeti/Common.hs
index 75dca4bb46c59236790f8e0cfa50dbc199816eb2..17750b42f8374d2e04f63cceed33127cd5b9d751 100644
--- a/src/Ganeti/Common.hs
+++ b/src/Ganeti/Common.hs
@@ -7,7 +7,7 @@ HTools and any other programs.
 
 {-
 
-Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -62,6 +62,7 @@ import Text.Printf (printf)
 
 import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
+import Ganeti.Utils (wrap)
 import qualified Ganeti.Version as Version (version)
 
 -- | Parameter type.
@@ -203,10 +204,14 @@ maxCmdLen = 60
 -- | Formats the description of various commands.
 formatCommands :: (StandardOptions a) => PersonalityList a -> [String]
 formatCommands personalities =
-  -- FIXME: add wrapping of descriptions
-  map (\(cmd, (_, _, _, desc)) -> printf " %-*s - %s" mlen cmd desc::String) $
+  concatMap (\(cmd, (_, _, _, desc)) ->
+              fmtDesc cmd (wrap maxWidth desc) "-" []) $
   sortBy (comparing fst) personalities
     where mlen = min maxCmdLen . maximum $ map (length . fst) personalities
+          maxWidth = 79 - 3 - mlen
+          fmtDesc _ [] _ acc = reverse acc
+          fmtDesc cmd (d : ds) sep acc =
+            fmtDesc "" ds " " (printf " %-*s %s %s" mlen cmd sep d : acc)
 
 -- | Formats usage for a multi-personality program.
 formatCmdUsage :: (StandardOptions a) => String -> PersonalityList a -> String
diff --git a/src/Ganeti/Utils.hs b/src/Ganeti/Utils.hs
index 95f4280a549da951db500a9c4c5ac692bc8aa948..74ad060c764cdeae0c7808a0a3e8efa769657784 100644
--- a/src/Ganeti/Utils.hs
+++ b/src/Ganeti/Utils.hs
@@ -48,6 +48,8 @@ module Ganeti.Utils
   , getCurrentTime
   , clockTimeToString
   , chompPrefix
+  , wrap
+  , trim
   ) where
 
 import Data.Char (toUpper, isAlphaNum, isDigit, isSpace)
@@ -326,3 +328,34 @@ chompPrefix pfx str =
   if pfx `isPrefixOf` str || str == init pfx
     then Just $ drop (length pfx) str
     else Nothing
+
+-- | Breaks a string in lines with length \<= maxWidth.
+--
+-- NOTE: The split is OK if:
+--
+-- * It doesn't break a word, i.e. the next line begins with space
+--   (@isSpace . head $ rest@) or the current line ends with space
+--   (@null revExtra@);
+--
+-- * It breaks a very big word that doesn't fit anyway (@null revLine@).
+wrap :: Int      -- ^ maxWidth
+     -> String   -- ^ string that needs wrapping
+     -> [String] -- ^ string \"broken\" in lines
+wrap maxWidth = filter (not . null) . map trim . wrap0
+  where wrap0 :: String -> [String]
+        wrap0 text
+          | length text <= maxWidth = [text]
+          | isSplitOK               = line : wrap0 rest
+          | otherwise               = line' : wrap0 rest'
+          where (line, rest) = splitAt maxWidth text
+                (revExtra, revLine) = break isSpace . reverse $ line
+                (line', rest') = (reverse revLine, reverse revExtra ++ rest)
+                isSplitOK =
+                  null revLine || null revExtra || startsWithSpace rest
+                startsWithSpace (x:_) = isSpace x
+                startsWithSpace _     = False
+
+-- | Removes surrounding whitespace. Should only be used in small
+-- strings.
+trim :: String -> String
+trim = reverse . dropWhile isSpace . reverse . dropWhile isSpace
diff --git a/test/hs/Test/Ganeti/Common.hs b/test/hs/Test/Ganeti/Common.hs
index 4f0f1ae2e272c8f316e66c66871868eef61ebd6d..923ec725261ae1e585c8732e2390ddc1248e5808 100644
--- a/test/hs/Test/Ganeti/Common.hs
+++ b/test/hs/Test/Ganeti/Common.hs
@@ -7,7 +7,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011, 2012 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
@@ -44,6 +44,9 @@ import Test.Ganeti.TestCommon
 
 import Ganeti.BasicTypes
 import Ganeti.Common
+import Ganeti.HTools.Program.Main (personalities)
+
+{-# ANN module "HLint: ignore Use camelCase" #-}
 
 -- | Helper to check for correct parsing of an option.
 checkOpt :: (StandardOptions b) =>
@@ -122,7 +125,40 @@ prop_parse_yes_no def testval val =
               then result ==? Ok (actual_val == "yes")
               else property $ isBad result
 
+-- | Check that formatCmdUsage works similar to Python _FormatUsage.
+case_formatCommands :: Assertion
+case_formatCommands =
+  assertEqual "proper wrap for HTools Main"
+    resCmdTest (formatCommands personalities)
+  where resCmdTest :: [String]
+        resCmdTest =
+          [ " hail    - Ganeti IAllocator plugin that implements the instance\
+            \ placement and"
+          , "           movement using the same algorithm as hbal(1)"
+          , " hbal    - cluster balancer that looks at the current state of\
+            \ the cluster and"
+          , "           computes a series of steps designed to bring the\
+            \ cluster into a"
+          , "           better state"
+          , " hcheck  - cluster checker; prints information about cluster's\
+            \ health and checks"
+          , "           whether a rebalance done using hbal would help"
+          , " hinfo   - cluster information printer; it prints information\
+            \ about the current"
+          , "           cluster state and its residing nodes/instances"
+          , " hroller - cluster rolling maintenance helper; it helps\
+            \ scheduling node reboots"
+          , "           in a manner that doesn't conflict with the instances'\
+            \ topology"
+          , " hscan   - tool for scanning clusters via RAPI and saving their\
+            \ data in the"
+          , "           input format used by hbal(1) and hspace(1)"
+          , " hspace  - computes how many additional instances can be fit on a\
+            \ cluster, while"
+          , "           maintaining N+1 status."
+          ]
 
 testSuite "Common"
           [ 'prop_parse_yes_no
+          , 'case_formatCommands
           ]
diff --git a/test/hs/Test/Ganeti/Utils.hs b/test/hs/Test/Ganeti/Utils.hs
index 2385e71561ea75f0363dfce2616de6a691860cda..548e4cfa318aaabc27a15e56019d593e576a5dda 100644
--- a/test/hs/Test/Ganeti/Utils.hs
+++ b/test/hs/Test/Ganeti/Utils.hs
@@ -270,6 +270,23 @@ prop_chompPrefix_nothing =
           (\s -> not (pfx `isPrefixOf` s) && s /= init pfx)) $ \str ->
   chompPrefix pfx str ==? Nothing
 
+-- | Tests 'trim'.
+prop_trim :: NonEmptyList Char -> Property
+prop_trim (NonEmpty str) =
+  forAll (listOf1 $ elements " \t\n\r\f") $ \whitespace ->
+  forAll (choose (0, length whitespace)) $ \n ->
+  let (preWS, postWS) = splitAt n whitespace in
+  conjoin [ printTestCase "arb. string first and last char are not space" $
+              case trim str of
+                [] -> True
+                xs -> (not . isSpace . head) xs && (not . isSpace . last) xs
+          , printTestCase "whitespace is striped" $
+              trim str ==? trim (preWS ++ str ++ postWS)
+          , printTestCase "whitespace reduced to null" $
+              trim whitespace ==? ""
+          , printTestCase "idempotent on empty strings" $
+              trim "" ==? ""
+          ]
 
 -- | Test list for the Utils module.
 testSuite "Utils"
@@ -287,6 +304,7 @@ testSuite "Utils"
             , 'prop_niceSort_numbers
             , 'prop_niceSortKey_equiv
             , 'prop_rStripSpace
+            , 'prop_trim
 #ifndef NO_REGEX_PCRE
             , 'case_new_uuid
 #endif