diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,12 @@
 # Version history for simple-cmd
 
+## 0.2.8 (2026-07-18)
+- +-+: use lastMay and headMay
+- removeTrailingNewline: use safe lastMay
+- add timeIOHelper
+- cmdLog_: use quoteCmd
+- cmdN: only quote arg if it contains space(s)
+
 ## 0.2.7 (2022-06-20)
 - allow building on Windows without unix (no sudo*)
 - add 'newline' to output newline
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017, Jens Petersen
+Copyright (c) 2017-2026, Jens Petersen
 
 All rights reserved.
 
diff --git a/simple-cmd.cabal b/simple-cmd.cabal
--- a/simple-cmd.cabal
+++ b/simple-cmd.cabal
@@ -1,5 +1,5 @@
 name:                simple-cmd
-version:             0.2.7
+version:             0.2.8
 synopsis:            Simple String-based process commands
 description:
             Simple wrappers over System.Process
@@ -11,16 +11,17 @@
 license-file:        LICENSE
 author:              Jens Petersen <juhpetersen@gmail.com>
 maintainer:          Jens Petersen <juhpetersen@gmail.com>
-copyright:           2017-2022 Jens Petersen <juhpetersen@gmail.com>
+copyright:           2017-2023,2025-2026 Jens Petersen <juhpetersen@gmail.com>
 category:            System
 homepage:            https://github.com/juhp/simple-cmd
 bug-reports:         https://github.com/juhp/simple-cmd/issues
 build-type:          Simple
 cabal-version:       >=1.10
 extra-source-files:  README.md ChangeLog.md TODO
-tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4,
-                     GHC == 8.6.5, GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2
-                     GHC == 9.2.3
+tested-with:         GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.4, GHC == 8.6.5,
+                     GHC == 8.8.4, GHC == 8.10.7, GHC == 9.0.2, GHC == 9.2.8
+                     GHC == 9.4.8, GHC == 9.6.7, GHC == 9.8.4, GHC == 9.10.3
+                     GHC == 9.12.4
 
 source-repository head
   type:     git
@@ -36,6 +37,7 @@
                        extra,
                        filepath,
                        process >= 1.4.3.0,
+                       safe,
                        time
   if !os(windows)
    build-depends:      unix
@@ -67,16 +69,3 @@
     default-language:  Haskell2010
 
     ghc-options:       -Wall
-  if impl(ghc >= 8.0)
-    ghc-options:       -Wcompat
-                       -Widentities
-                       -Wincomplete-uni-patterns
-                       -Wincomplete-record-updates
-                       -Wredundant-constraints
-  if impl(ghc >= 8.2)
-    ghc-options:       -fhide-source-paths
-  if impl(ghc >= 8.4)
-    ghc-options:       -Wmissing-export-lists
-                       -Wpartial-fields
-  if impl(ghc >= 8.10)
-    ghc-options:       -Wunused-packages
diff --git a/src/SimpleCmd.hs b/src/SimpleCmd.hs
--- a/src/SimpleCmd.hs
+++ b/src/SimpleCmd.hs
@@ -64,7 +64,7 @@
   whenM,
   filesWithExtension,
   fileWithExtension,
-  timeIO
+  timeIO, timeIOHelper
   ) where
 
 #if !MIN_VERSION_base(4,8,0)
@@ -83,6 +83,7 @@
 #if MIN_VERSION_time(1,9,0)
 import Data.Time.Format (formatTime, defaultTimeLocale)
 #endif
+import Safe (headMay, lastMay)
 import System.Directory (findExecutable, listDirectory)
 import System.Exit (ExitCode (..))
 import System.FilePath
@@ -100,9 +101,8 @@
                        waitForProcess, withCreateProcess)
 
 removeTrailingNewline :: String -> String
-removeTrailingNewline "" = ""
 removeTrailingNewline str =
-  if last str == '\n'
+  if lastMay str == Just '\n'
   then init str
   else str
 
@@ -181,7 +181,7 @@
 -- @since 0.2.7
 cmdLog_ :: String -> [String] -> IO ()
 cmdLog_ c args = do
-  logMsg $ unwords $ c:args
+  logMsg $ quoteCmd c args
   cmd_ c args
 
 -- FIXME change in 0.3
@@ -203,7 +203,10 @@
 
 -- | @cmdN c args@ dry-runs a command: prints command to stdout - more used for debugging
 cmdN :: String -> [String] -> IO ()
-cmdN c args = putStrLn $ unwords $ c : map show args
+cmdN c args =
+  putStrLn $ unwords $ map showQuote $ c : args
+  where
+    showQuote cs = if ' ' `elem` cs then '\'' : cs ++ "'" else cs
 
 -- | @cmdStdErr c args@ runs command in a process, returning stdout and stderr
 cmdStdErr :: String -> [String] -> IO (String, String)
@@ -327,7 +330,7 @@
      -> IO ()
 sudo_ = sudoInternal cmd_
 
--- | @sudo_ c args@ runs a command as sudo
+-- | @sudoLog c args@ runs a command as sudo, showing the command
 --
 -- @since 0.2.7
 sudoLog :: String -- ^ command
@@ -357,8 +360,8 @@
 (+-+) :: String -> String -> String
 "" +-+ s = s
 s +-+ "" = s
-s +-+ t | last s == ' ' = s ++ t
-        | head t == ' ' = s ++ t
+s +-+ t | lastMay s == Just ' ' = s ++ t
+        | headMay t == Just ' ' = s ++ t
 s +-+ t = s ++ " " ++ t
 
 -- singleLine :: String -> String
@@ -392,7 +395,7 @@
 --
 -- @since 0.2.7
 newline ::IO ()
-newline = putStrLn ""
+newline = putChar '\n'
 
 -- | Type alias for a command in a pipe
 --
@@ -520,13 +523,20 @@
 
 -- | Run an IO action and then print how long it took
 timeIO :: IO a -> IO a
-timeIO action = do
+timeIO = timeIOHelper "took"
+
+-- | General timeIO with arbitrary preceding message
+--
+-- @since 0.2.8
+timeIOHelper :: String -> IO a -> IO a
+timeIOHelper msg action = do
   bracket
     getCurrentTime
     (\start -> do
         end <- getCurrentTime
         let duration = diffUTCTime end start
-        putStrLn $ "took " ++ renderDuration duration)
+        -- FIXME use stderr?
+        putStrLn $ msg +-+ renderDuration duration)
     (const action)
   where
 #if MIN_VERSION_time(1,9,0)
