8

質問、議論のトピック

私は、より堅牢性を促進し、パフォーマンスが高く、プラットフォームに依存しないコンパイル言語(OCamlなど)で記述されたコードからコマンドラインシェルスクリプトのソースコードを生成することに非常に興味があります。基本的には、コンパイルされた言語でプログラミングして、必要なOSとの対話を実行し(提案します:より複雑な対話またはプラットフォームに依存しない方法で実行するのが容易ではない対話)、最後にコンパイルしますネイティブのバイナリ実行可能ファイル(できれば)に変換します。これにより、コンパイルされた言語でプログラムした内容をシェルで実行するシェルスクリプトが生成されます。[追加]:「効果」とは、環境変数とシェルオプションを設定し、特定の非標準コマンドを実行することを意味します(標準スクリプト「glue」はコンパイルされた実行可能ファイルによって処理され、生成されたシェルスクリプトから除外されます)。そのような。

私は今のところそのような解決策を見つけていません。OCamlをJavaScriptにコンパイルするなど、今日の他の可能性と比較して、実現するのは比較的簡単*のようです。

  • 私が説明していることの(公開されている)実装はすでにありますか?
  • 私が説明しているものと(非常に)類似している他の可能性は何ですか、そしてそれらはそれとどのように異なりますか?(言語から言語へのコンパイル(コンパイルからshへ)が思い浮かびますが、それは不必要に実現するのが難しいようです。)

私が意味しないこと

  1. 代替シェル(Scshなど)。管理するシステムでは、ユーザーや1人の管理者がシェルを選択できるとは限りません。また、他の人(顧客、同僚など)だけでなく、期待できない人のためのシステム管理ソリューションになることを願っています。別のシェルを受け入れます。
  2. 非対話型シェルスクリプト(ocamlscriptなど)が通常機能するための代替インタープリター。個人的には、この目的でシェルスクリプトを回避することに問題はありません。私がそうするのは、シェルスクリプトは一般に保守が難しく(たとえば、特定の文字や「コマンド」などの変更可能なものの操作に敏感)、一般的な汎用プログラミング言語が提供できるのと同じレベルの機能を作成するのが難しいためです(たとえば、この点でBashとPythonを比較してください)。ただし、ネイティブシェルスクリプトが必要な場合があります。たとえば、起動時にシェルから供給されるシェルプロファイルファイルなどです。

バックグラウンド

実用的なアプリケーション

私が説明することの実用的な有用性を疑う人もいるかもしれません。これの実用的なアプリケーションの1つは、さまざまな条件に基づいてシェルプロファイルを定義することです(たとえば、プロファイルのソースとなるシステムプラットフォーム/ OS、セキュリティポリシーに続くもの、具体的なシェル、ログイン/非ログインタイプシェル、インタラクティブ/非インタラクティブタイプのシェル)。シェルスクリプトとしての(巧妙に作成された)汎用シェルプロファイルに対する利点は、パフォーマンス(人間が作成したスクリプト解釈の代わりに、圧縮/最適化されたソースコードを生成する可能性のあるネイティブマシンコード)、堅牢性(型チェック、例外処理)の向上です。 、機能のコンパイル時検証、結果のバイナリ実行可能ファイルの暗号化署名)、機能(ユーザーランドCLIツールへの依存度が低いかまったくない、

実装の詳細、副次的な問題

  1. プログラマーは、生成されたシェルスクリプトの一般性の程度を制御できる必要があります。たとえば、バイナリ実行可能ファイルが毎回実行され、適切なシェルプロファイルコードが出力される場合もあれば、1回の実行の状況に合わせて調整された固定シェルスクリプトファイルを生成する場合もあります。後者の場合、リストされている利点、特に堅牢性(例外処理やユーザーランドツールへの依存など)の利点ははるかに制限されています。[追加した]
  2. 結果として得られるシェルスクリプトが何らかの形のユニバーサルシェルスクリプト(GNU autoconfが生成するような)であるか、特定のシェルに(動的にまたはそうでない)適応されたシェルネイティブスクリプトであるかは、私にとって主要な問題ではありません。
  3. 簡単*:これは、基本的に基本的なシェルビルトインのライブラリで使用可能な関数を使用することで実現できるように思われます。このような関数は、それ自体と渡された引数を、意味的に適切で構文的に正しいシェルスクリプトステートメント(文字列として)に変換するだけです。

さらに考えていただき、特に具体的な提案をありがとうございます。

4

1 に答える 1

13

このための Haskell ライブラリはありませんが、抽象構文ツリーを使用して実装できます。言語に依存しない抽象的な構文ツリーを作成し、そのツリーを同等の Bash スクリプトに変換するバックエンドを適用する、簡単なおもちゃの例を作成します。

Haskell で構文ツリーをモデル化するために、次の 2 つのトリックを使用します。

  • GADT を使用して型指定された Bash 式をモデル化する
  • free モナドを使って 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 コードのように見えます。きちんとした!

ここで、できればMonadシンタックス シュガーを使用して、ユーザー フレンドリーな DSL を作成する方法が必要です。ここでフリーモナドの出番です。

フリー モナドは、構文ツリーの 1 つのステップを表すファンクタを取り、そこから構文ツリーを作成します。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 の 1 つのステップを表します 。Free基本的にステップのリストを作成しScriptF、これらのステップのリストを組み立てることができるモナドを定義します。liftFこの関数は、1 つのステップを実行し、1 つのアクションでリストを作成するものと考えることができます。

次に、表記法を使用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)

これは、定義したばかりのプリミティブ ステップを組み立てる方法を示しています。これには、次のようなモナドコンビネータのサポートを含む、モナドのすべての優れたプロパティがあります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ターゲット言語が型付けされていない場合でも、モナドがどのように型安全性を強制するかに注目してください。Stringが予期される場所で誤ってリテラルを使用しIntegerたり、その逆を行ったりすることはできません。のようなタイプセーフな変換を使用して、それらの間で明示的に変換する必要がありますShown

Scriptまた、モナドは exit ステートメントの後のすべてのコマンドを飲み込むことに注意してください。インタープリターに到達する前に無視されます。もちろん、後続のステップExitを受け入れるようにコンストラクターを書き直すことで、この動作を変更できます。next

これらの抽象構文木は純粋です。つまり、それらを純粋に調べて解釈することができます。Scriptモナドを同等の 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 _ -> ""

2 つのインタープリターを定義しました。1 つは式構文ツリー用で、もう 1 つはモナディック DSL 構文ツリー用です。これら 2 つのインタープリターは、言語に依存しないプログラムを、文字列として表される同等の Bash プログラムにコンパイルします。もちろん、表現の選択は完全にあなた次第です。

Scriptこのインタープリターは、モナドが新しい変数を要求するたびに、新しい一意の変数を自動的に作成します。

このインタープリターを試して、動作するかどうかを確認しましょう:

>>> 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 モナドの使い方についてもっと知りたい場合は、以下を読むことをお勧めします:

また、ここに完全なコードを添付しました。

{-# 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 に答える