从编译的可执行文件生成CLI shell脚本代码? [英] CLI shell script code generation from compiled executable?

查看:133
本文介绍了从编译的可执行文件生成CLI shell脚本代码?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

问题,讨论话题



我对代码中的命令行shell脚本源代码的生成非常感兴趣,这些代码以更强大的性能,更好的性能平台无关的编译语言(例如OCaml)。基本上,你会用编译语言编程来执行与你想要的操作系统的任何交互(我会建议:更复杂的交互或不能以独立于平台的方式执行的交互),最后你会编译它到一个本地的二进制可执行文件(最好),它会生成一个shell脚本,在shell中编译您编译的语言中编写的内容。 [ ADDED ]:对于'effects',我的意思是设置环境变量和shell选项,执行某些非标准命令(标准脚本'glue'将由编译后的可执行文件处理,保留在生成的shell脚本之外)等。

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




  • 我所描述的实现是什么?

  • 什么是(非常)类似于我描述的其他可能性,以及它们与哪些不同? (想到语言到语言的编译(从编译到编译),虽然这似乎不必要地难以实现。)



我做的不是 的意思


  1. 另一个shell(如Scsh)。您所管理的系统可能并不总是允许用户或一位管理员选择shell,我也希望它成为仅供其他人(客户,同事和其他人)使用的系统管理解决方案,不能期望接受不同的shell。

  2. 另一种解释器,用于非交互式shell脚本通常提供的目的(如ocamlscript)。就我个人而言,我没有为此目的避免shell脚本的问题。我这样做是因为shell脚本通常比较难以维护(例如,对某些字符敏感,对'命令'等可变内容的操作),而且难以达到与流行的通用编程语言所能提供的功能相同的级别例如,在这方面比较Bash到Python)。但是,在某些情况下,需要使用本地shell脚本,例如shell启动时由shell提供的shell配置文件。



背景



实际应用

你们中的一些人可能会怀疑我所描述的实际用途。其中一个实际应用是基于各种条件定义一个shell配置文件(例如,配置文件来源的系统平台/操作系统,安全策略后面的内容,具体的shell,登录/非登录类型外壳,交互式/非交互式外壳类型)。 (精心制作的)通用shell配置文件作为shell脚本的优势将会提高性能(本机机器码可能会生成压缩/优化的源代码,而不是人为编写的脚本解释),健壮性(类型检查,异常处理,编译时功能验证,生成的二进制可执行文件的加密签名),功能(更少或不依赖于用户界面CLI工具,不限制使用所有可能平台的CLI工具覆盖的最小功能)和跨平台功能像Single UNIX Specification这样的实践标准仅仅意味着这么多,而许多shell配置文件概念通过PowerShell也可以传递给非Windows平台,比如Windows。)

strong>实现细节,侧面问题



  1. 程序员应该能够控制生成的shell脚本的通用程度。例如,可能会每次运行二进制可执行文件并提供适当的shell配置文件代码,或者可以简单地根据一次运行的情况生成一个固定的shell脚本文件。在后一种情况下,列出的优势 - 特别是那些用于稳健性(例如异常处理和对用户级工具的依赖)的优势要受限得多。
  2. 是否生成的shell脚本以某种形式的通用shell脚本(如GNU autoconf生成)或shell自适应脚本(动态或动态地不是)到特定的外壳对我来说不是一个主要问题。

  3. easy *:在我看来,这可以通过基本的可用函数在基本shell内建库中实现。这样的函数可以简单地将它自己加上传递的参数转换成语义上合适的,语法上正确的shell脚本语句(作为字符串)。

感谢您的任何进一步的想法,尤其是具体的建议! 没有Haskell库,但是你可以使用抽象语法树来实现它。我将构建一个简单的玩具示例,它构建一个抽象的与语言无关的语法树,然后应用将该树转换为等效Bash脚本的后端。



我将在Haskell中使用两种技巧来建模语法树:


  • 模型使用GADT输入Bash表达式

  • 使用免费monads实现DSL



GADT技巧相当简单,我使用几种语言扩展来增加语法:

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

导入Data.String
导入前导隐藏((++))

类型UniqueID =整数

newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID

data Expr a where
StrL :: String - > Expr字符串 - 字符串文字
IntL :: Integer - > Expr Integer - 整数文字
StrV :: VStr - > Expr字符串 - 字符串变量
IntV :: VInt - > Expr Integer - 整型变量
Plus :: Expr Integer - > Expr整数 - > Expr Integer
Concat :: Expr String - > Expr字符串 - > Expr字符串
显示:: Expr Integer - > Expr字符串

实例Num(Expr Integer)其中
fromInteger = IntL
(+)= Plus
(*)=未定义
abs =未定义
signum = undefined

实例IsString(Expr字符串)其中
fromString = StrL

(++):: Expr字符串 - > Expr字符串 - > Expr字符串
(++)= Concat

这让我们可以在我们的DSL。我只实现了一些简单的操作,但你可以很容易地想象如何将它与其他扩展。



如果我们没有使用任何语言扩展,我们可能会写表达式例如:

Concat(StrLTest)(显示(Plus(IntL 4) 5))):: Expr String

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


$ b

  Concat(StrLTest)显示(加4 5)):: Expr字符串

同样,我有 Expr Integer implements Num ,这样您就可以使用 + 添加数字文字:

Concat(StrLTest)(显示(4 + 5)):: Expr字符串

同样,我使用 OverloadedStrings 所有出现的(StrL str) str



< pre class =lang-hs prettyprint-override> ConcatTest(显示(4 + 5)):: Expr字符串

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

Test++显示(4 + 5):: Expr字符串

除了显示从整数转换为字符串外,它看起来就像本地Haskell代码。整洁!

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



一个免费的monads使用一个代表语法树中单个步骤的函子,并从中创建一个语法树。作为奖励,它总是任何函子的monad,所以你可以使用 do 符号来组装这些语法树。



<为了演示它,我将在前面的代码段中添加一些代码: $ b

  - 这是除了前面的代码
{ - #LANGUAGE DeriveFunctor# - }

import Control.Monad.Free

data ScriptF next
= NewInt(Expr Integer)(VInt - > next)
| NewStr(Expr字符串)(VStr - >下一个)
| SetStr VStr(Expr String)next
| SetInt VInt(Expr Integer)下一个
| Echo(Expr字符串)下一个
|退出(Expr整数)
派生(Functor)

类型Script =免费ScriptF

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

newStr :: Expr String - >脚本VStr
newStr str = liftF $ NewStr str id
$ b $ setStr :: VStr - > Expr字符串 - > Script()
setStr v expr = liftF $ SetStr v expr()

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

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

exit :: Expr Integer - >脚本r
出口expr = liftF $退出expr

ScriptF functor代表我们DSL中的一个步骤。 Free 本质上创建了一个 ScriptF 步骤的列表,并定义了一个monad,我们可以在这里组装这些步骤的列表。您可以将 liftF 函数想象成只需一个步骤并通过一个动作创建列表。



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

script :: Script r
script = do
hello< - newStrHello,
世界< - newStr世界!
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的所有优良特性,包括对单元组合器的支持,如 forM _

  import Control.Monad 

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

注意我们的 Script monad强制执行类型安全性,即使我们的目标语言可能是无类型的。如果期望整数,反之亦然,您不能意外地使用字符串字面值。您必须使用类型安全的转换(如 Shown )显式转换它们。

c>脚本 monad会在exit语句后吞下任何命令。在他们到达口译员之前他们被忽略了。当然,您可以通过重写 Exit 构造函数来接受后续下一步步骤来更改此行为。



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

  bashExpr :: Expr a  - >字符串
bashExpr expr =
的字符串表达式strL str - > str
IntL int - > show int
StrV(VStr nID) - > $ {S<>显示nID<> }
IntV(VInt nID) - > $ {I<>显示nID<> }
Plus expr1 expr2 - >
concat [$((,bashExpr expr1,+,bashExpr expr2,))]
Concat expr1 expr2 - > bashExpr expr1<> bashExpr expr2
显示expr' - > bashExpr expr'

bashBackend :: Script r - >字符串
bashBackend脚本=去0 0 script其中
去nstrs nInts脚本=

的脚本脚本Free f - >案例f的
NewInt e k - >
我<>显示nInts<> =<> bashExpr e<> \\\
<>
去nStrs(nInts + 1)(k(VInt nInts))
NewStr e k - >
S<>显示nStrs<> =<> bashExpr e<> \\\
<>
go(nStrs + 1)nInts(k(VStr nStrs))
SetStr(VStr nID)e script' - >
S<>显示nID<> =<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
SetInt(VInt nID)e script' - >
我<>显示nID<> =<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
Echo e script' - >
echo<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
退出e - >
退出<> bashExpr e<> \\\

Pure _ - >

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

这个解释器会在每次 Script monad请求一个新的变量。



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

 >>> putStr $ bashBackend脚本
S0 =你好,
S1 =世界!
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中实现这一惯例的想法。如果您想了解更多关于使用免费monads的信息,我建议您阅读:



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


$ b

  { - #LANGUAGE GADT $ 
,FlexibleInstances
,RebindableSyntax
,DeriveFunctor
,OverloadedStrings# - }

导入Control.Monad.Free
导入控制.Monad
导入Data.Monoid
导入Data.String
导入前导隐藏((++))

类型UniqueID =整数

newtype VStr = VStr UniqueID
newtype VInt = VInt UniqueID

data Expr a where
StrL :: String - > Expr字符串 - 字符串文字
IntL :: Integer - > Expr Integer - 整数文字
StrV :: VStr - > Expr字符串 - 字符串变量
IntV :: VInt - > Expr Integer - 整型变量
Plus :: Expr Integer - > Expr整数 - > Expr Integer
Concat :: Expr String - > Expr字符串 - > Expr字符串
显示:: Expr Integer - > Expr字符串

实例Num(Expr Integer)其中
fromInteger = IntL
(+)= Plus
(*)=未定义
abs =未定义
signum = undefined

实例IsString(Expr字符串)其中
fromString = StrL

(++):: Expr字符串 - > Expr字符串 - > Expr String
(++)= Concat

data ScriptF next
= NewInt(Expr Integer)(VInt - > next)
| NewStr(Expr字符串)(VStr - >下一个)
| SetStr VStr(Expr String)next
| SetInt VInt(Expr Integer)下一个
| Echo(Expr字符串)下一个
|退出(Expr整数)
派生(Functor)

类型Script =免费ScriptF

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

newStr :: Expr String - >脚本VStr
newStr str = liftF $ NewStr str id
$ b $ setStr :: VStr - > Expr字符串 - > Script()
setStr v expr = liftF $ SetStr v expr()

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

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

exit :: Expr Integer - >脚本r
exit expr = liftF $退出expr

脚本::脚本r
脚本=执行
hello< - newStrHello,
世界< - newStr世界!
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 - > (IntV x)

bashExpr :: Expr a(
x) - >字符串
bashExpr expr =
的字符串表达式strL str - > str
IntL int - > show int
StrV(VStr nID) - > $ {S<>显示nID<> }
IntV(VInt nID) - > $ {I<>显示nID<> }
Plus expr1 expr2 - >
concat [$((,bashExpr expr1,+,bashExpr expr2,))]
Concat expr1 expr2 - > bashExpr expr1<> bashExpr expr2
显示expr' - > bashExpr expr'

bashBackend :: Script r - >字符串
bashBackend脚本=去0 0 script其中
去nstrs nInts脚本=

的脚本脚本Free f - >案例f的
NewInt e k - >
我<>显示nInts<> =<> bashExpr e<> \\\
<>
去nStrs(nInts + 1)(k(VInt nInts))
NewStr e k - >
S<>显示nStrs<> =<> bashExpr e<> \\\
<>
go(nStrs + 1)nInts(k(VStr nStrs))
SetStr(VStr nID)e script' - >
S<>显示nID<> =<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
SetInt(VInt nID)e script' - >
我<>显示nID<> =<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
Echo e script' - >
echo<> bashExpr e<> \\\
<>
转到nStrs nInt脚本'
退出e - >
退出<> bashExpr e<> \\\

Pure _ - >


Question, topic of discussion

I am very interested in generation of command line shell scripting source code from code written in a more robustness-promoting, well-performant and platform-independent compiled language (OCaml, for instance). Basically, you would program in a compiled language to perform any interactions with the OS that you want (I would propose: the more complex interactions or ones that are not easy to do in a platform-independent way), and finally you would compile it to a native binary executable (preferably), which would generate a shell script that effects in the shell what you programmed in the compiled language. [ADDED]: With 'effects', I mean to set the environment variables and shell options, execute certain non-standard commands (the standard scripting 'glue' would be handled by the compiled executable and would be kept out of the generated shell script) and such.

I have not found any such solution so far. It seems to be relatively easy* to realize compared to other possibilities of today, like compiling OCaml to JavaScript.

  • Are there already (public) implementations of what I describe?
  • What are other possibilities that are (very) similar to what I describe, and in what ways do they differ from that? (Language-to-language compilation (from compiled to sh) comes to mind, although that seems unnecessarily hard to realize.)

What I do not mean

  1. An alternative shell (like Scsh). The systems you administer may not always allow shells to be chosen by the user or by one administrator, and I also hope it to be a system administration solution exclusively for others (customers, colleagues and others) as well, people who cannot be expected to accept a different shell.
  2. An alternative interpreter, for the purpose that non-interactive shell scripts normally serve (like ocamlscript). Personally, I do not have a problem in avoiding shell scripting for this purpose. I do so because shell scripts are generally harder to maintain (for example, sensitive to certain characters and manipulation of mutable things like 'commands') and harder to craft to the same level of functionality that popular general-purpose programming languages could offer (for example, compare Bash to Python in this regard). However, there are cases where a native shell script is needed, for instance a shell profile file that is sourced by a shell when it is launched.

Background

Practical applications

Some of you may doubt the practical usefulness of what I describe. One practical application of this is to define a shell profile based on various conditions (for example the system platform/OS on which the profile is being sourced, what follows from the security policy, the concrete shell, login/non-login type of the shell, interactive/non-interactive type of shell). The advantage over a (well-crafted) generic shell profile as a shell script would be improvement in performance (native machine code that may generate a compressed/optimized source code instead of human-written script interpretation), robustness (type checking, exception handling, compile time verification of functionality, cryptographic signing of the resultant binary executable), capabilities (less or no reliance on userland CLI tools, no limitation to use minimum functionality covered by the CLI tools of all possible platforms) and cross-platform functionality (in practice standards like the Single UNIX Specification only mean so much, and many shell profile concepts carry over to Non-Unix platforms like Windows, with its PowerShell, too).

Implementation details, side issues

  1. The programmer should be able to control the degree of genericity of the generated shell script. For example, it could be that the binary executable is run every time and puts out the shell profile code that is appropriate, or it could simply generate a fixed shell script file tailored to the circumstances of one run. In the latter case, the listed advantages - in particular those for robustness (e.g. exception handling and reliance on userland tools) are far more limited. [ADDED]
  2. Whether the resultant shell script would be in some form of universal shell script (like GNU autoconf generates) or shell-native script adapted (dynamically or not) to a specific shell is not a primary question to me.
  3. easy*: It seems to me that this can be realized by basically having available functions in a library for the basic shell builtins. Such a function would simply convert itself plus the passed arguments to a semantically appropriate and syntactically correct shell script statement (as a string).

Thank you for any further thoughts, and especially for concrete suggestions!

解决方案

There are no Haskell libraries for this, but you can implement this using abstract syntax trees. I'll build up a simple toy example that builds an abstract language-independent syntax tree and then applies a back-end that converts the tree into the equivalent Bash script.

I will use two tricks for modelling syntax trees in Haskell:

  • Model typed Bash expressions using a GADT
  • Implement a DSL using free monads

The GADT trick is rather simple, and I use several language extensions to sweeten the syntax:

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

This lets us build typed Bash expression in our DSL. I only implemented a few primitive operations, but you could easily imagine how you could extend it with others.

If we didn't use any language extensions, we might write expressions like:

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

This is okay, but not very sexy. The above code uses RebindableSyntax to override numeric literals so that you can replace (IntL n) with just n:

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

Similarly, I have Expr Integer implement Num, so that you can add numeric literals using +:

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

Similarly, I use OverloadedStrings so that you can replace all occurrences of (StrL str) with just str:

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

I also override the Prelude (++) operator so that we can concatenate expressions as if they were Haskell strings:

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

Other than the Shown cast from integers to strings, it looks just like native Haskell code. Neat!

Now we need a way to create a user-friendly DSL, preferably with Monad syntactic sugar. This is where free monads come in.

A free monads take a functor representing a single step in a syntax tree and creates a syntax tree from it. As a bonus, it is always a monad for any functor, so you can assemble these syntax trees using do notation.

To demonstrate it, I'll add some more code to the previous code segment:

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

The ScriptF functor represents a single step in our DSL. Free essentially creates a list of ScriptF steps and defines a monad where we can assemble lists of these steps. You can think of the liftF function as taking a single step and creating a list with one action.

We can then use do notation to assemble these steps, where do notation concatenates these lists of actions:

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)

This shows how we assemble the primitive steps we just defined. This has all the nice properties of monads, including support for monadic combinators, like 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))

Notice how our Script monad enforces type safety even though our target language might be untyped. You can't accidentally use a String literal where it expects an Integer or vice versa. You must explicitly convert between them using type-safe conversions like Shown.

Also note that the Script monad swallows any commands after the exit statement. They are ignored before they even reach the interpreter. Of course, you can change this behavior by rewriting the Exit constructor to accept a subsequent next step.

These abstract syntax trees are pure, meaning that we can inspect and interpret them purely. We can define several backends, such as a Bash backend that converts our Script monad to the equivalent Bash script:

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

I defined two interpreters: one for the expression syntax tree and one for the monadic DSL syntax tree. These two interpreters compile any language-independent program into the equivalent Bash program, represented as a String. Of course, the choice of representation is entirely up to you.

This interpreter automatically creates fresh unique variables each time our Script monad requests a new variable.

Let's try out this interpreter and see if it works:

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

It generates a bash script that executes the equivalent language-indepent program. Similarly, it translates script2 just fine, too:

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

So this is obviously not comprehensive, but hopefully that gives you some ideas for how you would implement this idiomatically in Haskell. If you want to learn more about the use of free monads, I recommend you read:

I've also attached the complete code here:

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

这篇关于从编译的可执行文件生成CLI shell脚本代码?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆