shell - 从编译的可执行文件生成 CLI shell 脚本代码?

标签 shell haskell scheme ocaml robustness

就目前而言,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引起辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visit the help center为指导。




8年前关闭。




问题、讨论话题

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

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

  • 我所描述的内容是否已经(公共(public))实现?
  • 与我所描述的(非常)相似的其他可能性有哪些,它们在哪些方面与此不同? (语言到语言的编译(从编译到 sh)浮现在脑海中,尽管这似乎很难实现。)

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

  • 背景

    实际应用

    你们中的一些人可能会怀疑我所描述的内容的实际用处。一个实际应用是根据各种条件定义一个 shell 配置文件(例如,配置文件所在的系统平台/操作系统、安全策略的后续内容、具体的 shell、登录/非登录类型) shell,交互/非交互类型的 shell )。作为 shell 脚本的(精心设计的)通用 shell 配置文件的优势在于性能的改进( native 代码可以生成压缩/优化的源代码而不是人工编写的脚本解释)、健壮性(类型检查、异常处理) 、功能的编译时验证、生成的二进制可执行文件的加密签名)、功能(较少或不依赖用户级 CLI 工具,不限制使用所有可能平台的 CLI 工具所涵盖的最低功能)和跨平台功能(在像单一 UNIX 规范这样的实践标准仅仅意味着这么多,而且许多 shell 配置文件概念也适用于 Windows 等非 Unix 平台,以及它的 PowerShell)。

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

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

    最佳答案

    没有用于此的 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)只需 str :

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

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

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

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

    现在我们需要一种方法来创建用户友好的 DSL,最好使用 Monad语法糖。这就是免费 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
    
    ScriptF仿函数代表我们 DSL 中的一个步骤。 Free基本上创建了一个列表 ScriptF步骤并定义一个 monad,我们可以在其中组合这些步骤的列表。你可以想到liftF功能就像采取一个步骤并通过一个 Action 创建一个列表。

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

    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))
    

    请注意我们的 Script即使我们的目标语言可能是无类型的,monad 也会强制执行类型安全。您不能不小心使用 String它期望 Integer 的字面量或相反亦然。您必须使用类型安全转换(如 Shown)在它们之间进行显式转换。 .

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

    这些抽象语法树是纯粹的,这意味着我们可以纯粹地检查和解释它们。我们可以定义几个后端,例如一个 Bash 后端,它可以转换我们的 Script monad 到等效的 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 程序,表示为字符串。当然,代表的选择完全取决于您。

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

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

    >>> 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 中惯用的实现的想法。如果你想了解更多关于 free monad 的使用,我推荐你阅读:
  • Why Free Monads Matter
  • Purify Code using Free Monads

  • 我还在此处附上了完整的代码:

    {-# 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 _ -> ""
    

    关于shell - 从编译的可执行文件生成 CLI shell 脚本代码?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14081799/

    相关文章:

    haskell - Cabal:有条件地覆盖标志默认值

    Haskell - 为什么多个声明不起作用?

    haskell - 尝试将映射应用于 Haskell 中函数的 "inner"参数

    scheme - 在 Racket 程序中使用 Scheme 库

    scheme - SICP 练习 3.52 : Is memo-proc necessary using Scheme (Guile)?

    scheme - SICP练习1.5和1.6

    shell - 渲染完成后运行 shell 命令(After Effects)

    c - 如何获取 C 语言中的本地 shell 变量?

    linux - 期望脚本错误发送 : spawn id exp4 not open while executing "send "password""

    linux - 创建空文件,提示文件是否已存在?