[project @ 1996-01-08 20:28:12 by partain]
[ghc-hetmet.git] / ghc / misc / examples / hsh / Hsh.hs
diff --git a/ghc/misc/examples/hsh/Hsh.hs b/ghc/misc/examples/hsh/Hsh.hs
new file mode 100644 (file)
index 0000000..ffe2f16
--- /dev/null
@@ -0,0 +1,284 @@
+module Main (main)
+where
+
+import LibPosix
+import LibSystem
+
+
+main =
+    initialize                                         >>
+    commandLoop
+
+{- 
+   Standard shell practice: move std descriptors out of the way so
+   it's more convenient to set them up for children.  Also set up an
+   interrupt handler which will put us back in the main loop.
+-}
+
+initialize :: IO ()
+initialize =
+    dupChannelTo stdInput myStdin                      >>
+    dupChannelTo stdOutput myStdout                    >>
+    dupChannelTo stdError myStderr                     >>
+    closeChannel stdInput                              >>
+    closeChannel stdOutput                             >>
+--    closeChannel stdError                            >>
+    installHandler sigINT (Catch intr) Nothing         >>
+    return ()
+
+myStdin = 16 :: Channel
+myStdout = 17 :: Channel
+myStderr = 18 :: Channel
+
+-- For user interrupts 
+
+intr :: IO ()
+intr =
+    writeChannel myStdout "\n"                         >>
+    commandLoop
+
+{-
+   Simple command loop: print a prompt, read a command, process the command.
+   Repeat as necessary.
+-}
+
+commandLoop :: IO ()    
+commandLoop =
+    writeChannel myStdout "$ "                         >>
+    try (readCommand myStdin)                          >>= 
+    either
+      (\ err -> case err of
+                 EOF -> return ()
+                 _ -> dieHorribly)
+      (\ cmd ->
+       try (processCommand cmd)                        >>=
+       either 
+         (\ err -> commandLoop) 
+         (\ succ -> commandLoop))
+  where
+    dieHorribly :: IO ()
+    dieHorribly =
+       errMsg "read failed"                            >>
+       exitWith (ExitFailure 1)
+
+{-
+   Read a command a character at a time (to allow for fancy processing later).
+   On newline, you're done, unless the newline was escaped by a backslash.
+-}
+
+readCommand :: Channel -> IO String
+readCommand chan = 
+    accumString ""                             >>= \ cmd ->
+    return cmd
+  where
+    accumString :: String -> IO String
+    accumString s =
+       myGetChar chan                          >>= \ c ->
+       case c of
+         '\\' ->
+           myGetChar chan                      >>= \ c' ->
+           accumString (c':c:s)
+         '\n' -> return (reverse s)
+          ch  -> accumString (ch:s)
+
+myGetChar :: Channel -> IO Char
+myGetChar chan =
+    readChannel chan 1                         >>= \ (s, len) ->
+    case len of
+      0 -> myGetChar chan
+      1 -> return (head s)
+
+{-
+   To process a command, first parse it into words, then do the necessary
+   redirections, and finally perform the desired command.  Built-ins are
+   checked first, and if none match, we execute an external command.
+-}
+
+processCommand :: String -> IO ()
+processCommand "" = return ()
+processCommand s =
+    parseCommand s                             >>= \ words ->
+    parseRedirection words                     >>= \ (inFile, outFile, words) ->
+    performRedirections inFile outFile         >>
+    let
+       cmd = head words
+       args = tail words
+    in
+        case builtin cmd of
+         Just f -> 
+           f args                              >>
+           closeChannel stdInput               >>
+           closeChannel stdOutput
+         Nothing -> 
+           exec cmd args
+
+{-
+   Redirections are a bit of a pain, really.  If none are specified, we
+   dupChannel our own file descriptors.  Otherwise, we try to open the files
+   as requested.
+-}
+
+performRedirections :: Maybe String -> Maybe String -> IO ()
+performRedirections inFile outFile =
+    (case inFile of
+       Nothing ->
+           dupChannelTo myStdin stdInput
+       Just x ->
+           try (openChannel x ReadOnly Nothing False False False False False)
+                                               >>=
+           either
+             (\ err ->
+               errMsg ("Can't redirect input from " ++ x)
+                                               >>
+               failWith (UserError "redirect"))
+             (\ succ -> return ()))            >>
+    (case outFile of
+       Nothing ->
+           dupChannelTo myStdout stdOutput
+       Just x ->
+           try (createFile x stdFileMode)
+                                               >>=
+           either
+             (\ err ->
+               errMsg ("Can't redirect output to " ++ x)
+                                               >>
+               closeChannel stdInput   >>
+               failWith (UserError "redirect"))
+             (\ succ -> return ()))
+
+{-
+   We parse a command line into words according to the following rules:
+    1) Anything inside pairs of "" or '' is parsed literally.
+    2) Anything (outside of quotes) escaped by \ is taken literally.
+    3) '<' and '>' are words all by themselves, unless escaped or quoted.
+    4) Whitespace separates words
+-}
+
+parseCommand :: String -> IO [String]
+parseCommand = getTokens []
+  where
+    getTokens :: [String] -> String -> IO [String]
+    getTokens ts "" = return (reverse ts)
+    getTokens ts s = 
+       getToken s                              >>= \ (t, s') ->
+       getTokens (t:ts) s'
+
+    getToken :: String -> IO (String, String)
+    getToken (c:cs)
+      | c == '<' || c == '>' = return ([c], cs)
+      | isSpace c = getToken cs
+      | c == '"' || c == '\'' = accumQuote c "" cs
+      | otherwise = accumToken [c] cs
+
+    accumToken :: [Char] -> String -> IO (String, String)
+    accumToken cs "" = return (reverse cs, "")
+    accumToken cs ('\\':c:s) = accumToken (c:cs) s
+    accumToken cs x@(c:s)
+      | isSpace c || c == '<' || c == '>' = return (reverse cs, x)
+      | c == '"' || c == '\'' = accumQuote c cs s
+      | otherwise = accumToken (c:cs) s
+
+    accumQuote :: Char -> [Char] -> String -> IO (String, String)
+    accumQuote q cs "" =
+       errMsg ("Unmatched " ++ [q])            >>
+       failWith (UserError "unmatched quote")
+    accumQuote q cs (c:s)
+      | c == q = accumToken cs s
+      | otherwise = accumQuote q (c:cs) s
+
+{-
+  Here we look for "<" and ">".  When we find one, we remove it and the
+  following word from the word list.  The arguments following the redirection
+  symbols and the remaining words are returned to our caller.  However, it's
+  an error to end a word list with a redirection or for the same redirection
+  to appear twice.
+-}
+
+parseRedirection :: [String] -> IO (Maybe String, Maybe String, [String])
+parseRedirection = redirect Nothing Nothing []
+  where
+    redirect inFile outFile args [] =
+       return (inFile, outFile, reverse args)
+    redirect inFile outFile args [arg]
+      | arg == "<" || arg == ">" =
+       errMsg "Missing name for redirect"      >>
+       failWith (UserError "parse redirect")
+      | otherwise =
+       return (inFile, outFile, reverse (arg:args))
+    redirect inFile outFile args ("<":name:more) 
+      | inFile == Nothing =
+       redirect (Just name) outFile args more
+      | otherwise =
+       errMsg "Ambiguous input redirect"       >>
+       failWith (UserError "parse redirect")
+    redirect inFile outFile args (">":name:more) 
+      | outFile == Nothing =
+       redirect inFile (Just name) args more
+      | otherwise =
+       errMsg "Ambiguous output redirect"      >>
+       failWith (UserError "parse redirect")
+    redirect inFile outFile args (arg:more) =
+       redirect inFile outFile (arg:args) more
+
+{- 
+  Executing an external command is pretty simple, but what if it fails?
+  Fortunately, we don't have any way to redirect stdError just yet,
+  so we let it complain and then exit.
+-}
+
+exec :: String -> [String] -> IO ()
+exec cmd args =
+    forkProcess                                        >>= \ maybe_pid ->
+    case maybe_pid of
+      Nothing ->
+        dupChannelTo myStderr stdError                 >>
+       closeChannel myStdin                            >>
+       closeChannel myStdout                           >>
+       closeChannel myStderr                           >>
+       executeFile cmd True args Nothing               `handle`
+       \ err -> 
+           writeChannel stdError ("command not found: " ++ cmd ++ ".\n") 
+                                                       >>
+           exitImmediately (ExitFailure 1)
+      Just pid -> 
+       closeChannel stdInput                           >>
+       closeChannel stdOutput                          >>
+--     closeChannel stdError                           >>
+       getProcessStatus True False pid                 >>
+        return ()
+
+{-
+    Builtins:
+       cd [arg] -> change directory (default to HOME)
+       exit ... -> exit successfully
+
+    Builtins must provide their own error messages, since the main command
+    loop ignores any errors.
+-}
+
+builtin :: String -> Maybe ([String] -> IO ())
+builtin "cd" = Just chdir
+builtin "exit" = Just exit
+builtin _ = Nothing
+
+chdir :: [String] -> IO ()
+chdir [] =
+    getEnvVar "HOME"                                   >>= \ home ->
+    changeWorkingDirectory home                                `handle`
+    \ err -> errMsg "cd: can't go home"
+
+chdir [dir] =
+    changeWorkingDirectory dir                         `handle`
+    \ err -> errMsg ("cd: can't chdir to " ++ dir)
+chdir _ =
+    errMsg "cd: too many arguments"
+
+exit :: [String] -> IO ()
+exit _ = exitWith ExitSuccess
+
+-- Print an error message to my std error.
+
+errMsg :: String -> IO ()
+errMsg msg =
+    writeChannel myStderr ("hsh: " ++ msg ++ ".\n")    >>
+    return ()