8

问题,讨论的话题

我对从以更健壮性、性能良好且与平台无关的编译语言(例如 OCaml)编写的代码生成命令行 shell 脚本源代码非常感兴趣。基本上,您将使用编译语言进行编程以执行您想要的与操作系统的任何交互(我建议:更复杂的交互或不容易以独立于平台的方式进行的交互),最后您将编译它到本机二进制可执行文件(最好),它将生成一个 shell 脚本,该脚本会在 shell 中影响您用编译语言编写的内容。[已添加]:对于“效果”,我的意思是设置环境变量和 shell 选项,执行某些非标准命令(标准脚本“胶水”将由编译的可执行文件处理,并将被排除在生成的 shell 脚本之外)和这样的。

到目前为止,我还没有找到任何这样的解决方案。与今天的其他可能性相比,它似乎相对容易*实现,例如将 OCaml 编译为 JavaScript。

  • 我描述的内容是否已经(公共)实现?
  • 还有哪些与我所描述的(非常)相似的可能性,它们在哪些方面与此不同?(我想到了语言到语言的编译(从编译到 sh),尽管这似乎不必要地难以实现。)

不是什么意思

  1. 另一种外壳(如 Scsh)。您管理的系统可能并不总是允许用户或一个管理员选择 shell,我也希望它是一个专门为其他人(客户、同事和其他人)提供的系统管理解决方案,那些不能期望的人接受不同的外壳。
  2. 一个替代解释器,用于非交互式 shell 脚本通常服务的目的(如 ocamlscript)。就个人而言,我在避免为此目的编写 shell 脚本方面没有问题。我这样做是因为 shell 脚本通常更难维护(例如,对某些字符敏感以及对诸如“命令”之类的可变事物的操作)并且更难制作到与流行的通用编程语言可以提供的相同级别的功能(例如例如,在这方面将 Bash 与 Python 进行比较)。但是,在某些情况下需要本机 shell 脚本,例如 shell 启动时由 shell 提供的 shell 配置文件。

背景

实际应用

你们中的一些人可能会怀疑我所描述的实际用途。一个实际应用是根据各种条件定义 shell 配置文件(例如,配置文件所在的系统平台/操作系统、安全策略遵循的内容、具体的 shell、登录/非登录类型shell,交互/非交互类型的 shell)。与(精心设计的)通用 shell 配置文件作为 shell 脚本相比的优势在于性能(可以生成压缩/优化源代码而不是人工编写的脚本解释的本机机器代码)、稳健性(类型检查、异常处理,功能的编译时验证,生成的二进制可执行文件的加密签名),功能(较少或不依赖用户级 CLI 工具,

实施细节,附带问题

  1. 程序员应该能够控制生成的 shell 脚本的通用程度。例如,可能每次运行二进制可执行文件并输出适当的 shell 配置文件代码,或者它可以简单地生成一个固定的 shell 脚本文件,以适应一次运行的情况。在后一种情况下,列出的优势——特别是那些健壮性(例如异常处理和对用户空间工具的依赖)的优势要有限得多。[添加]
  2. 生成的 shell 脚本是某种形式的通用 shell 脚本(如 GNU autoconf 生成)还是适应特定 shell(动态或非动态)的 shell-native 脚本对我来说不是主要问题。
  3. easy*:在我看来,这可以通过在基本 shell 内置函数的库中提供可用函数来实现。这样的函数将简单地将自身加上传递的参数转换为语义适当且语法正确的 shell 脚本语句(作为字符串)。

感谢您的任何进一步想法,尤其是具体建议!

4

1 回答 1

13

没有为此的 Haskell 库,但您可以使用抽象语法树来实现它。我将构建一个简单的玩具示例,它构建一个与语言无关的抽象语法树,然后应用一个将树转换为等效 Bash 脚本的后端。

我将使用两个技巧在 Haskell 中对语法树进行建模:

  • 使用 GADT 对类型化的 Bash 表达式进行建模
  • 使用免费的 monad 实现 DSL

GADT 技巧相当简单,我使用了几种语言扩展来使语法更甜美:

{-# LANGUAGE GADTs
           , FlexibleInstances
           , RebindableSyntax
           , OverloadedStrings #-}

import Data.String
import Prelude hiding ((++))

type UniqueID = Integer

newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID

data Expr a where
    StrL   :: String  -> Expr String  -- String  literal
    IntL   :: Integer -> Expr Integer -- Integer literal
    StrV   :: VStr    -> Expr String  -- String  variable
    IntV   :: VInt    -> Expr Integer -- Integer variable
    Plus   :: Expr Integer -> Expr Integer -> Expr Integer
    Concat :: Expr String  -> Expr String  -> Expr String
    Shown  :: Expr Integer -> Expr String

instance Num (Expr Integer) where
    fromInteger = IntL
    (+)         = Plus
    (*)    = undefined
    abs    = undefined
    signum = undefined

instance IsString (Expr String) where
    fromString = StrL

(++) :: Expr String -> Expr String -> Expr String
(++) = Concat

这让我们可以在 DSL 中构建类型化的 Bash 表达式。我只实现了一些原始操作,但您可以轻松想象如何与其他人一起扩展它。

如果我们不使用任何语言扩展,我们可能会编写如下表达式:

Concat (StrL "Test") (Shown (Plus (IntL 4) (IntL 5))) :: Expr String

这没关系,但不是很性感。上面的代码用于RebindableSyntax覆盖数字文字,以便您可以替换(IntL n)n

Concat (StrL "Test") (Shown (Plus 4 5)) :: Expr String

同样,我已经Expr Integer实现Num,因此您可以使用以下方法添加数字文字+

Concat (StrL "Test") (Shown (4 + 5)) :: Expr String

同样,我使用OverloadedStrings这样您就可以(StrL str)用 just替换所有出现的str

Concat "Test" (Shown (4 + 5)) :: Expr String

我还覆盖了 Prelude(++)运算符,以便我们可以连接表达式,就好像它们是 Haskell 字符串一样:

"Test" ++ Shown (4 + 5) :: Expr String

除了Shown从整数到字符串的转换之外,它看起来就像原生的 Haskell 代码。整洁的!

现在我们需要一种方法来创建用户友好的 DSL,最好使用Monad语法糖。这就是免费单子的用武之地。

一个自由的 monad 采用一个代表语法树中单个步骤的函子,并从中创建一个语法树。作为奖励,它始终是任何函子的 monad,因此您可以使用do符号来组装这些语法树。

为了演示它,我将在前面的代码段中添加更多代码:

-- This is in addition to the previous code
{-# LANGUAGE DeriveFunctor #-}

import Control.Monad.Free

data ScriptF next
    = NewInt (Expr Integer) (VInt -> next)
    | NewStr (Expr String ) (VStr -> next)
    | SetStr VStr (Expr String ) next
    | SetInt VInt (Expr Integer) next
    | Echo (Expr String) next
    | Exit (Expr Integer)
  deriving (Functor)

type Script = Free ScriptF

newInt :: Expr Integer -> Script VInt
newInt n = liftF $ NewInt n id

newStr :: Expr String -> Script VStr
newStr str = liftF $ NewStr str id

setStr :: VStr -> Expr String -> Script ()
setStr v expr = liftF $ SetStr v expr ()

setInt :: VInt -> Expr Integer -> Script ()
setInt v expr = liftF $ SetInt v expr ()

echo :: Expr String -> Script ()
echo expr = liftF $ Echo expr ()

exit :: Expr Integer -> Script r
exit expr = liftF $ Exit expr

函子代表了我们 DSL 中的ScriptF一个步骤。 Free本质上创建了一个ScriptF步骤列表并定义了一个 monad,我们可以在其中组装这些步骤的列表。您可以将该liftF功能视为采取一个步骤并通过一个操作创建一个列表。

然后我们可以使用do符号来组装这些步骤,其中do符号连接这些动作列表:

script :: Script r
script = do
    hello <- newStr "Hello, "
    world <- newStr "World!"
    setStr hello (StrV hello ++ StrV world)
    echo ("hello: " ++ StrV hello)
    echo ("world: " ++ StrV world)
    x <- newInt 4
    y <- newInt 5
    exit (IntV x + IntV y)

这显示了我们如何组装我们刚刚定义的原始步骤。这具有 monad 的所有优点,包括对 monadic 组合子的支持,例如forM_

import Control.Monad

script2 :: Script ()
script2 = forM_ [1..5] $ \i -> do
    x <- newInt (IntL i)
    setInt x (IntV x + 5)
    echo (Shown (IntV x))

请注意我们的Scriptmonad 是如何强制类型安全的,即使我们的目标语言可能是无类型的。您不能String在期望的地方意外使用文字,Integer反之亦然。您必须使用类型安全的转换(如Shown.

另请注意,Scriptmonad 会在 exit 语句之后吞下任何命令。他们甚至在到达口译员之前就被忽略了。当然,您可以通过重写Exit构造函数以接受后续next步骤来更改此行为。

这些抽象语法树是纯粹的,这意味着我们可以纯粹地检查和解释它们。我们可以定义多个后端,例如将我们的Scriptmonad 转换为等效 Bash 脚本的 Bash 后端:

bashExpr :: Expr a -> String
bashExpr expr = case expr of
    StrL str           -> str
    IntL int           -> show int
    StrV (VStr nID)    -> "${S" <> show nID <> "}"
    IntV (VInt nID)    -> "${I" <> show nID <> "}"
    Plus   expr1 expr2 ->
        concat ["$((", bashExpr expr1, "+", bashExpr expr2, "))"]
    Concat expr1 expr2 -> bashExpr expr1 <> bashExpr expr2
    Shown  expr'       -> bashExpr expr'

bashBackend :: Script r -> String
bashBackend script = go 0 0 script where
    go nStrs nInts script =
        case script of
            Free f -> case f of
                NewInt e k ->
                    "I" <> show nInts <> "=" <> bashExpr e <> "\n" <>
                        go nStrs (nInts + 1) (k (VInt nInts))
                NewStr e k ->
                    "S" <> show nStrs <> "=" <> bashExpr e <> "\n" <>
                        go (nStrs + 1) nInts (k (VStr nStrs))
                SetStr (VStr nID) e script' ->
                    "S" <> show nID <> "=" <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                SetInt (VInt nID) e script' ->
                    "I" <> show nID <> "=" <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                Echo e script' ->
                    "echo " <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                Exit e ->
                    "exit " <> bashExpr e <> "\n"
            Pure _ -> ""

我定义了两个解释器:一个用于表达式语法树,一个用于 monadic DSL 语法树。这两个解释器将任何与语言无关的程序编译成等效的 Bash 程序,以字符串表示。当然,代表的选择完全取决于您。

每次我们的Scriptmonad 请求一个新变量时,这个解释器都会自动创建新的唯一变量。

让我们试试这个解释器,看看它是否有效:

>>> putStr $ bashBackend script
S0=Hello, 
S1=World!
S0=${S0}${S1}
echo hello: ${S0}
echo world: ${S1}
I0=4
I1=5
exit $((${I0}+${I1}))

它生成一个执行等效的独立于语言的程序的 bash 脚本。同样,它也翻译script2得很好:

>>> putStr $ bashBackend script2
I0=1
I0=$((${I0}+5))
echo ${I0}
I1=2
I1=$((${I1}+5))
echo ${I1}
I2=3
I2=$((${I2}+5))
echo ${I2}
I3=4
I3=$((${I3}+5))
echo ${I3}
I4=5
I4=$((${I4}+5))
echo ${I4}

所以这显然不是全面的,但希望这能给你一些想法,让你知道如何在 Haskell 中惯用地实现它。如果您想了解更多关于使用免费 monad 的信息,我建议您阅读:

我还在这里附上了完整的代码:

{-# LANGUAGE GADTs
           , FlexibleInstances
           , RebindableSyntax
           , DeriveFunctor
           , OverloadedStrings #-}

import Control.Monad.Free
import Control.Monad
import Data.Monoid
import Data.String
import Prelude hiding ((++))

type UniqueID = Integer

newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID

data Expr a where
    StrL   :: String  -> Expr String  -- String  literal
    IntL   :: Integer -> Expr Integer -- Integer literal
    StrV   :: VStr    -> Expr String  -- String  variable
    IntV   :: VInt    -> Expr Integer -- Integer variable
    Plus   :: Expr Integer -> Expr Integer -> Expr Integer
    Concat :: Expr String  -> Expr String  -> Expr String
    Shown  :: Expr Integer -> Expr String

instance Num (Expr Integer) where
    fromInteger = IntL
    (+)         = Plus
    (*)    = undefined
    abs    = undefined
    signum = undefined

instance IsString (Expr String) where
    fromString = StrL

(++) :: Expr String -> Expr String -> Expr String
(++) = Concat

data ScriptF next
    = NewInt (Expr Integer) (VInt -> next)
    | NewStr (Expr String ) (VStr -> next)
    | SetStr VStr (Expr String ) next
    | SetInt VInt (Expr Integer) next
    | Echo (Expr String) next
    | Exit (Expr Integer)
  deriving (Functor)

type Script = Free ScriptF

newInt :: Expr Integer -> Script VInt
newInt n = liftF $ NewInt n id

newStr :: Expr String -> Script VStr
newStr str = liftF $ NewStr str id

setStr :: VStr -> Expr String -> Script ()
setStr v expr = liftF $ SetStr v expr ()

setInt :: VInt -> Expr Integer -> Script ()
setInt v expr = liftF $ SetInt v expr ()

echo :: Expr String -> Script ()
echo expr = liftF $ Echo expr ()

exit :: Expr Integer -> Script r
exit expr = liftF $ Exit expr

script :: Script r
script = do
    hello <- newStr "Hello, "
    world <- newStr "World!"
    setStr hello (StrV hello ++ StrV world)
    echo ("hello: " ++ StrV hello)
    echo ("world: " ++ StrV world)
    x <- newInt 4
    y <- newInt 5
    exit (IntV x + IntV y)

script2 :: Script ()
script2 = forM_ [1..5] $ \i -> do
    x <- newInt (IntL i)
    setInt x (IntV x + 5)
    echo (Shown (IntV x))

bashExpr :: Expr a -> String
bashExpr expr = case expr of
    StrL str           -> str
    IntL int           -> show int
    StrV (VStr nID)    -> "${S" <> show nID <> "}"
    IntV (VInt nID)    -> "${I" <> show nID <> "}"
    Plus   expr1 expr2 ->
        concat ["$((", bashExpr expr1, "+", bashExpr expr2, "))"]
    Concat expr1 expr2 -> bashExpr expr1 <> bashExpr expr2
    Shown  expr'       -> bashExpr expr'

bashBackend :: Script r -> String
bashBackend script = go 0 0 script where
    go nStrs nInts script =
        case script of
            Free f -> case f of
                NewInt e k ->
                    "I" <> show nInts <> "=" <> bashExpr e <> "\n" <> 
                        go nStrs (nInts + 1) (k (VInt nInts))
                NewStr e k ->
                    "S" <> show nStrs <> "=" <> bashExpr e <> "\n" <>
                        go (nStrs + 1) nInts (k (VStr nStrs))
                SetStr (VStr nID) e script' ->
                    "S" <> show nID <> "=" <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                SetInt (VInt nID) e script' ->
                    "I" <> show nID <> "=" <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                Echo e script' ->
                    "echo " <> bashExpr e <> "\n" <>
                        go nStrs nInts script'
                Exit e ->
                    "exit " <> bashExpr e <> "\n"
            Pure _ -> ""
于 2012-12-29T18:26:05.460 回答