shell-escape (empty) → 0.0.0
raw patch · 13 files changed
+514/−0 lines, 13 filesdep +basedep +binarydep +bytestringsetup-changed
Dependencies added: base, binary, bytestring, containers, vector
Files
- LICENSE +28/−0
- Makefile +47/−0
- README +1/−0
- Setup.hs +2/−0
- Text/ShellEscape.hs +14/−0
- Text/ShellEscape/Bash.hs +75/−0
- Text/ShellEscape/Escape.hs +21/−0
- Text/ShellEscape/EscapeVector.hs +39/−0
- Text/ShellEscape/Put.hs +24/−0
- Text/ShellEscape/Sh.hs +64/−0
- bench/Bench.hs +26/−0
- shell-escape.cabal +33/−0
- test/Echo.hs +140/−0
+ LICENSE view
@@ -0,0 +1,28 @@++ ©2010 Jason Dusek.++ Redistribution and use in source and binary forms, with or without+ modification, are permitted provided that the following conditions are met:++ . Redistributions of source code must retain the above copyright notice,+ this list of conditions and the following disclaimer.++ . Redistributions in binary form must reproduce the above copyright notice,+ this list of conditions and the following disclaimer in the documentation+ and/or other materials provided with the distribution.++ . Names of the contributors to this software may not be used to endorse or+ promote products derived from this software without specific prior written+ permission.++ This software is provided by the contributors "as is" and any express or+ implied warranties, including, but not limited to, the implied warranties of+ merchantability and fitness for a particular purpose are disclaimed. In no+ event shall the contributors be liable for any direct, indirect, incidental,+ special, exemplary, or consequential damages (including, but not limited to,+ procurement of substitute goods or services; loss of use, data, or profits;+ or business interruption) however caused and on any theory of liability,+ whether in contract, strict liability, or tort (including negligence or+ otherwise) arising in any way out of the use of this software, even if+ advised of the possibility of such damage. +
+ Makefile view
@@ -0,0 +1,47 @@++OPTS ?= --bash+RUNS ?= 10000+GHC = ghc --make -outputdir+++clean:+ rm -rf */build */echo bench/bench test/lengths* test/chars*+++.PHONY: bench+bench: bench/bench+ cd bench && ./bench++bench/bench: bench/Bench.hs+ mkdir -p bench/build+ ${GHC} bench/build -O $< -o $@+++.PHONY: prof+prof: prof/echo+ cd prof && ./echo ${RUNS} +RTS -hb -RTS && hp2ps echo.hp++prof/echo: test/Echo.hs+ mkdir -p prof/build+ ${GHC} prof/build test/Echo.hs -o prof/echo -prof -auto-all+++test: test/chars.histogram test/lengths.histogram++test/echo: test/Echo.hs+ mkdir -p test/build+ ${GHC} test/build -O2 test/Echo.hs -o test/echo++.PHONY: test_run+test_run: test/echo+ rm -f test/chars* test/lengths*+ cd test && ./echo ${OPTS} ${RUNS}++test/chars: test_run+test/chars.histogram: test/chars+ sort < $< | uniq -c > $@++test/lengths: test_run+test/lengths.histogram: test/lengths+ sort -n < $< | uniq -c > $@+
+ README view
@@ -0,0 +1,1 @@+ Library for generating fully escaped strings for use with the shell.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Text/ShellEscape.hs view
@@ -0,0 +1,14 @@++{-| Typed shell escaping for Bourne Shell and Bash.+ -}++module Text.ShellEscape+ ( Text.ShellEscape.Escape.Escape(..)+ , Text.ShellEscape.Sh.Sh()+ , Text.ShellEscape.Bash.Bash()+ ) where++import Text.ShellEscape.Escape+import Text.ShellEscape.Sh+import Text.ShellEscape.Bash+
+ Text/ShellEscape/Bash.hs view
@@ -0,0 +1,75 @@++module Text.ShellEscape.Bash where++import Data.Maybe+import Data.Char+import Text.Printf+import Data.ByteString (ByteString)++import qualified Data.Vector as Vector++import Text.ShellEscape.Escape+import qualified Text.ShellEscape.Put as Put+import Text.ShellEscape.EscapeVector++{-| A Bash escaped 'ByteString'. The strings are wrapped in @$\'...\'@ if any+ bytes within them must be escaped; otherwise, they are left as is.+ Newlines and other control characters are represented as ANSI escape+ sequences. High bytes are represented as hex codes. Thus Bash escaped+ strings will always fit on one line and never contain non-ASCII bytes.+ -}+newtype Bash = Bash (EscapeVector EscapingMode)+ deriving (Eq, Ord, Show)+++instance Escape Bash where+ escape = Bash . escWith classify+ unescape (Bash v) = stripEsc v+ bytes (Bash v) | literal v = stripEsc v+ | otherwise = interpretEsc v renderANSI' end (begin, Literal)+ where+ literal = isNothing . Vector.find ((/= Literal) . snd)+ begin = [ Put.putString "$'"]+ end = const (Put.putChar '\'')+ renderANSI' _ (c, e) = (renderANSI c, e)+++{-| Bash escaping modes.+ -}+data EscapingMode = ANSIHex | ANSIBackslash | Literal | Quoted+ deriving (Eq, Ord, Show)++renderANSI c =+ case classify c of+ Literal -> Put.putChar c+ Quoted -> Put.putChar c+ ANSIHex -> Put.putString $ hexify c+ ANSIBackslash -> Put.putString $ backslashify c++backslashify :: Char -> String+backslashify '\ESC' = "\\e"+backslashify c = (take 2 . drop 1 . show) c++hexify :: Char -> String+hexify = printf "\\x%02X" . ord++classify :: Char -> EscapingMode+classify c | c <= '\ACK' = ANSIHex+ | c <= '\r' = ANSIBackslash+ | c <= '\SUB' = ANSIHex+ | c == '\ESC' = ANSIBackslash+ | c <= '\US' = ANSIHex+ | c <= '&' = Quoted+ | c == '\'' = ANSIBackslash+ | c <= '+' = Quoted+ | c <= '9' = Literal+ | c <= '?' = Quoted+ | c <= 'Z' = Literal+ | c == '[' = Quoted+ | c == '\\' = ANSIBackslash+ | c <= '`' = Quoted+ | c <= 'z' = Literal+ | c <= '~' = Quoted+ | c == '\DEL' = ANSIHex+ | otherwise = ANSIHex+
+ Text/ShellEscape/Escape.hs view
@@ -0,0 +1,21 @@++module Text.ShellEscape.Escape where++import Data.ByteString++{-| A type class for objects that represent an intermediate state of+ escaping.+ -}+class Escape t where+ -- | Transform a 'ByteString' into the escaped intermediate form.+ escape :: ByteString -> t+ -- | Recover the original 'ByteString'.+ unescape :: t -> ByteString+ -- | Yield the escaped 'ByteString'.+ bytes :: t -> ByteString++instance Escape ByteString where+ escape = id+ unescape = id+ bytes = id+
+ Text/ShellEscape/EscapeVector.hs view
@@ -0,0 +1,39 @@++module Text.ShellEscape.EscapeVector where++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as ByteString++import Data.Vector (Vector)+import qualified Data.Vector as Vector+import qualified Data.Vector.Mutable as Vector++import qualified Text.ShellEscape.Put as Put+++type EscapeVector escapingMode = Vector (Char, escapingMode)++escWith :: (Char -> escapingMode) -> ByteString -> EscapeVector escapingMode+escWith cf b = Vector.create $ do+ v <- Vector.new (ByteString.length b)+ sequence_ . snd $ ByteString.foldl' (f v) (0, []) b+ return v+ where+ f v (i, ops) c = (i + 1, Vector.write v i (c, cf c) : ops)+++stripEsc :: Vector (Char, escapingMode) -> ByteString+stripEsc v = ByteString.unfoldr f . fst $ Vector.unzip v+ where+ f v | Vector.null v = Nothing+ | otherwise = Just (Vector.unsafeHead v, Vector.unsafeTail v)++interpretEsc v f finish init = (eval . (finish lastMode :)) instructions+ where+ eval = Put.runPut' . sequence_ . reverse+ (instructions, lastMode) = Vector.foldl' f' init v+ where+ f' (list, mode) (c, e) = (put:list, mode')+ where+ (put, mode') = f mode (c, e)+
+ Text/ShellEscape/Put.hs view
@@ -0,0 +1,24 @@++module Text.ShellEscape.Put+ ( module Data.Binary.Put+ , putChar+ , putString+ , runPut'+ ) where++import Prelude hiding (putChar)+import Data.Binary.Put+import Data.Char+import Data.ByteString.Lazy+import Data.ByteString+import Data.ByteString.Char8++putChar :: Char -> Put+putChar = putWord8 . toEnum . fromEnum++putString :: String -> Put+putString = putByteString . Data.ByteString.Char8.pack++runPut' :: Put -> Data.ByteString.ByteString+runPut' = Data.ByteString.concat . Data.ByteString.Lazy.toChunks . runPut +
+ Text/ShellEscape/Sh.hs view
@@ -0,0 +1,64 @@++module Text.ShellEscape.Sh where++import Data.ByteString (ByteString)++import Text.ShellEscape.Escape+import qualified Text.ShellEscape.Put as Put+import Text.ShellEscape.EscapeVector+++{-| A Bourne Shell escaped 'ByteString'. An oddity of Bourne shell escaping is+ the absence of escape codes for newline and other ASCII control+ characters. These bytes are simply placed literally in single quotes; the+ effect is that a Bourne Shell escaped string may cover several lines and+ contain non-ASCII bytes. Runs of bytes that must be escaped are wrapped in+ @\'...\'@; bytes that are acceptable as literals in Bourne Shell are left+ as is.+ -}+newtype Sh = Sh (EscapeVector EscapingMode)+ deriving (Eq, Ord, Show)++instance Escape Sh where+ escape = Sh . escWith classify+ unescape (Sh v) = stripEsc v+ bytes (Sh v) = interpretEsc v act finish ([], Literal)+ where+ finish Quote = Put.putChar '\''+ finish Backslash = Put.putChar '\\'+ finish Literal = return ()+++{-| Accept the present escaping mode and desired escaping mode and yield an+ action and the resulting mode.+ -}+act :: EscapingMode -> (Char, EscapingMode) -> (Put.Put, EscapingMode)+act Quote (c, Quote) = (Put.putChar c , Quote)+act Quote (c, Literal) = (Put.putString ['\'', c] , Literal)+act Quote (c, Backslash) = (Put.putString ['\'', '\\', c] , Literal)+act Backslash (c, Backslash) = (Put.putChar c , Literal)+act Backslash (c, Quote) = (Put.putString ['\\', '\'', c] , Quote)+act Backslash (c, Literal) = (Put.putString ['\\', c] , Literal)+act Literal (c, Literal) = (Put.putChar c , Literal)+act Literal (c, Backslash) = (Put.putString ['\\', c] , Literal)+act Literal (c, Quote) = (Put.putString ['\'', c] , Quote)++classify :: Char -> EscapingMode+classify c | c <= '&' = Quote -- 0x00..0x26+ | c == '\'' = Backslash -- 0x27+ | c <= ',' = Quote -- 0x28..0x2c+ | c <= '9' = Literal -- 0x2d..0x39+ | c <= '?' = Quote -- 0x3a..0x3f+ | c <= 'Z' = Literal -- 0x40..0x5a+ | c <= '^' = Quote -- 0x5b..0x5e+ | c == '_' = Literal -- 0x5f+ | c == '`' = Quote -- 0x60+ | c <= 'z' = Literal -- 0x61..0x7a+ | c <= '\DEL' = Quote -- 0x7b..0x7f+ | otherwise = Quote -- 0x80..0xff++{-| Bourne Shell escaping modes. + -}+data EscapingMode = Backslash | Literal | Quote+ deriving (Eq, Ord, Show)+
+ bench/Bench.hs view
@@ -0,0 +1,26 @@++{-# LANGUAGE OverloadedStrings+ #-}++import Data.ByteString.Char8 (ByteString)++import Criterion.Main++import Text.ShellEscape+++strings :: [ByteString]+strings =+ [ "echo * $PWD"+ , ""+ , "~/Music/M.I.A. & Diplo - Piracy Funds Terrorism Vol. 1 (2004)"+ , "abcds"+ , "\x00\n\204\DEL"+ , "\x00\n\204\DELecho * $PWD" ]++main = (defaultMain . concat . fmap (`fmap` strings)) [benchBash, benchSh]+ where+ bench' d f s = bench (d ++ show s) (whnf f s)+ benchBash = bench' "bash:" (escape :: ByteString -> Bash)+ benchSh = bench' "sh:" (escape :: ByteString -> Sh)+
+ shell-escape.cabal view
@@ -0,0 +1,33 @@+name : shell-escape+version : 0.0.0+category : Text+license : BSD3+license-file : LICENSE+author : Jason Dusek+maintainer : oss@solidsnack.be+homepage : http://github.com/solidsnack/shell-escape+synopsis : Shell escaping library.+description :+ Shell escaping library, offering both Bourne shell and Bash style escaping+ of ByteStrings.++cabal-version : >= 1.6+build-type : Simple+extra-source-files : README+ , bench/Bench.hs+ , test/Echo.hs+ , Makefile++library+ build-depends : base >= 2 && <= 5+ , binary+ , containers+ , vector >= 0.6.0.2+ , bytestring >= 0.9+ exposed-modules : Text.ShellEscape+ other-modules : Text.ShellEscape.Escape+ Text.ShellEscape.EscapeVector+ Text.ShellEscape.Sh+ Text.ShellEscape.Bash+ Text.ShellEscape.Put+
+ test/Echo.hs view
@@ -0,0 +1,140 @@+{-# LANGUAGE OverloadedStrings+ #-}++import System.Environment+import System.Process (runInteractiveProcess, waitForProcess, ProcessHandle)+import System.IO (Handle, stderr, stdout, stdin)+import Data.ByteString (ByteString, pack, hGetContents, hPutStrLn)+import qualified Data.ByteString+import Data.ByteString.Char8 (unpack)+import qualified Data.ByteString.Char8 as Char8+import Data.Word+import Control.Monad+import qualified Text.Printf+import Data.IORef+import Data.List++import Test.QuickCheck+import Test.QuickCheck.Monadic++import Text.ShellEscape+++-- It is best to implement the echo test with `printf':+--+-- . Some echo implementations always interprete backslash escapes like \3+-- and give us no way to turn it off. Dash is like this.+--+-- . GNU echo can not be made to simply ignore options like `--help'.+--+-- The `printf' implementations available to me -- Bash and Dash --+-- consistently ignore backslash escape sequences and any options that follow+-- the format argument.+--+printf :: (Shell t, Escape t) => t -> IO ByteString+printf escaped = do+ (i, o, e, p) <- shell escaped cmd+ exit <- waitForProcess p+ hGetContents o+ where+ cmd = "printf '%s' " ++ unpack raw+ raw = bytes escaped+++prop_echoBash :: ByteString -> Property+prop_echoBash = something_prop escapeBash++prop_echoSh :: ByteString -> Property+prop_echoSh = something_prop escapeSh++something_prop esc b = monadicIO $ do+ assert =<< run (test printf b (esc b))++escapeSh :: ByteString -> Sh+escapeSh b = escape b++escapeBash :: ByteString -> Bash+escapeBash b = escape b++++test cmd original escaped = do+ b <- cmd escaped+ Data.ByteString.appendFile "./lengths" (displayLength original)+ Data.ByteString.appendFile "./chars" (Char8.unlines $ displayBytes original)+ when (b /= original) $ do+ err "Result differs from unescaped result:"+ err "Escaped form:"+ err (Char8.unwords . displayBytes $ bytes escaped)+ err "Output:"+ err (Char8.unwords . displayBytes $ b)+ err "Original:"+ err (Char8.unwords . displayBytes $ original)+ return (b == original)+++displayBytes = fmap Char8.pack+ . fmap (\w -> Text.Printf.printf "0x%02x" w)+ . Data.ByteString.unpack++displayLength = Char8.pack+ . (\w -> Text.Printf.printf "%4d\n" w)+ . Data.ByteString.length++class (Escape t) => Shell t where+ shell :: t -> String -> IO (Handle, Handle, Handle, ProcessHandle)+instance Shell Sh where+ shell _ = sh+instance Shell Bash where+ shell _ = bash+++sh s = runInteractiveProcess "sh" ["-c", s] Nothing Nothing+bash s = runInteractiveProcess "bash" ["-c", s] Nothing Nothing++instance Arbitrary ByteString where+ arbitrary = do+ bytes <- arbitrary :: Gen [Word8]+ NonEmpty bytes' <- arbitrary :: Gen (NonEmptyList Word8)+ pack `fmap` elements [bytes', bytes', bytes, bytes', bytes']++instance Arbitrary Word8 where+ arbitrary = fmap fromIntegral (choose ( 0x01 :: Int+ , 0xFF :: Int ))++main = do+ runSh <- newIORef True+ runBash <- newIORef True+ testsR <- newIORef 10000+ let testsRwrite = writeIORef testsR+ noBash = writeIORef runBash False+ noSh = writeIORef runSh False+ args <- getArgs+ case (sort . nub) args of+ ["--bash", "--sh", d] -> testsRwrite (read d)+ ["--bash", "--sh" ] -> return ()+ ["--bash", d] -> noSh >> testsRwrite (read d)+ [ "--sh", d] -> noBash >> testsRwrite (read d)+ [ d] -> testsRwrite (read d)+ [ ] -> return ()+ _ -> error "Invalid arguments."+ tests <- readIORef testsR+ let msg = "Performing " ++ show tests ++ " tests."+ qcArgs = Args Nothing tests tests 32+ qc = quickCheckWith qcArgs+ err "Tests are random ByteStrings, containing any byte but null."+ runSh ?> do+ err "Testing Sh escaping."+ err (Char8.pack msg)+ qc prop_echoSh+ runBash ?> do+ err "Testing Bash escaping."+ err (Char8.pack msg)+ qc prop_echoBash++err = hPutStrLn stderr++(?>) :: IORef Bool -> IO () -> IO ()+ref ?> action = readIORef ref >>= (`when` action)++