whichコマンドを実装してみました。

ファイルパスの扱いが面倒なので、動作するのはWindows環境のみです。簡単そうなコマンドなのですが、あんまりシンプルになりませんでした。こういうとりあえず全部展開してから後でフィルタかけるようなプログラムを書いていると、遅延評価は素晴しいと実感しますね。効率とか難しいこと考えなくてもいいので。

module Main (main) where

import Control.Monad (filterM)
import System.Directory (doesFileExist)
import System.Environment (getArgs, getEnv, getProgName)
import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

splitEnvPath :: String -> [FilePath]
splitEnvPath [] = []
splitEnvPath s = 
  let (s1, s2) = span (/= ';') $ dropWhile (== ';') s
  in s1 : splitEnvPath s2

addPath :: FilePath -> FilePath -> FilePath
addPath p1 p2 = 
  let sep = if last p1 == '\\' then "" else "\\"
  in p1 ++ sep ++ p2

extracttFilePaths :: [FilePath] -> [FilePath] -> [FilePath]
extracttFilePaths cmds = concatMap (flip f cmds)
  where
    f path = concatMap (map (addPath path) . f')

    f' cmd
      | all (/= '.') cmd = map (cmd ++) ["", ".exe", ".bat", ".com"]
      | otherwise        = [cmd]

usage :: String -> IO a
usage p = do
  hPutStrLn stderr $ "usage: " ++ p ++ " [-a] command..."
  exitFailure

main :: IO ()
main = do
  path <- getEnv "PATH"
  prog <- getProgName
  args <- getArgs
  (allf, cmds) <-
    case args of
      []            -> usage prog
      ("-a" : [])   -> usage prog
      ("-a" : cmds) -> return (id,     cmds)
      cmds          -> return (take 1, cmds)
  founds <- filterM doesFileExist $ extracttFilePaths cmds $ splitEnvPath path
  mapM_ putStrLn $ allf founds

実行例:

$ ./which.exe tree
c:\usr\home\s-tanaka\bin\tree.exe
$ ./which.exe -a tree
c:\usr\home\s-tanaka\bin\tree.exe
c:\WINDOWS\system32\tree.com