diff --git a/shelly.cabal b/shelly.cabal
--- a/shelly.cabal
+++ b/shelly.cabal
@@ -1,6 +1,6 @@
 Name:       shelly
 
-Version:     1.4.1
+Version:     1.4.2
 Synopsis:    shell-like (systems) programming in Haskell
 
 Description: Shelly provides convenient systems programming in Haskell,
diff --git a/src/Shelly.hs b/src/Shelly.hs
--- a/src/Shelly.hs
+++ b/src/Shelly.hs
@@ -50,7 +50,7 @@
          , tag, trace, show_command
 
          -- * Querying filesystem.
-         , ls, lsT, test_e, test_f, test_d, test_s, which
+         , ls, lsT, test_e, test_f, test_d, test_s, test_px, which
 
          -- * Filename helpers
          , absPath, (</>), (<.>), canonic, canonicalize, relPath, relativeTo, path
@@ -69,7 +69,7 @@
          , bracket_sh, catchany, catch_sh, finally_sh, ShellyHandler(..), catches_sh, catchany_sh
 
          -- * convert between Text and FilePath
-         , toTextIgnore, toTextWarn, fromText
+         , toTextIgnore, toTextWarn, FP.fromText
 
          -- * Utility Functions
          , whenM, unlessM, time, sleep
@@ -239,9 +239,6 @@
   where
     encodeError f = echo ("Invalid encoding for file: " <> f)
 
-fromText :: Text -> FilePath
-fromText = FP.fromText
-
 -- | Transfer from one handle to another
 -- For example, send contents of a process output to stdout.
 -- does not close the write handle.
@@ -543,6 +540,11 @@
               where unrollRoot x = foldr1 phi $ map Node $ splitDirectories x
                     phi a b = a . return . b
 
+
+isExecutable :: FilePath -> IO Bool
+isExecutable f = (executable `fmap` getPermissions (encodeString f)) `catch` (\(_ :: IOError) -> return False)
+
+
 -- | Get a full path to an executable by looking at the @PATH@ environement
 -- variable. Windows normally looks in additional places besides the
 -- @PATH@: this does not duplicate that behavior.
@@ -569,7 +571,7 @@
         lookupPath =
             (pathDirs >>=) $ findMapM $ \dir -> do
                 let fullFp = dir </> fp
-                res <- liftIO $ isExecutable $ encodeString $ fullFp
+                res <- liftIO $ isExecutable fullFp
                 return $ toMaybe res fullFp
 
         lookupCache = do
@@ -579,10 +581,7 @@
                 L.find (S.member fp . snd) pathExecutables
 
 
-        pathDirs = mapM absPath =<< ((map fromText . T.split (== searchPathSeparator)) `fmap` get_env_text "PATH")
-
-        isExecutable f = (executable `fmap` getPermissions f) `catch`
-                           (\(_ :: IOError) -> return False)
+        pathDirs = mapM absPath =<< ((map FP.fromText . T.split (== searchPathSeparator)) `fmap` get_env_text "PATH")
 
         cachedPathExecutables :: Sh [(FilePath, S.Set FilePath)]
         cachedPathExecutables = do
@@ -594,7 +593,7 @@
                 executables <- forM dirs (\dir -> do
                     files <- (liftIO . listDirectory) dir `catch_sh` (\(_ :: IOError) -> return [])
                     exes <- fmap (map snd) $ liftIO $ filterM (isExecutable . fst) $
-                      map (\f -> (encodeString f, filename f)) files
+                      map (\f -> (f, filename f)) files
                     return $ S.fromList exes
                   )
                 let cachedExecutables = zip dirs executables
@@ -630,6 +629,14 @@
 -- | Does a path point to an existing file?
 test_f :: FilePath -> Sh Bool
 test_f = absPath >=> liftIO . isFile
+
+-- | Test that a file is in the PATH and also executable
+test_px :: FilePath -> Sh Bool
+test_px exe = do
+  mFull <- which exe
+  case mFull of
+    Nothing -> return False
+    Just full -> liftIO $ isExecutable full
 
 -- | A swiss army cannon for removing things. Actually this goes farther than a
 -- normal rm -rf, as it will circumvent permission problems for the files we
