Генерация кода сценария оболочки CLI из скомпилированного исполняемого файла?

Вопрос, тема обсуждения

Я очень заинтересован в создании исходного кода сценариев оболочки командной строки из кода, написанного на более надежном, хорошо работающем и независимом от платформы скомпилированном языке (например, OCaml). По сути, вы должны программировать на скомпилированном языке для выполнения любых взаимодействий с ОС, которые вы хотите (я бы предложил: более сложные взаимодействия или те, которые нелегко сделать независимо от платформы), и, наконец, вы бы скомпилировали их. к собственному двоичному исполняемому файлу (предпочтительно), который будет генерировать сценарий оболочки, который воздействует на оболочку, что вы запрограммировали на скомпилированном языке. [ДОБАВЛЕНО]: Под "эффектами" я подразумеваю устанавливать переменные среды и параметры оболочки, выполнять определенные нестандартные команды (стандартный "скриптовый клей" скрипта будет обрабатываться скомпилированным исполняемым файлом и не попадать в сгенерированный скрипт оболочки).) и тому подобное.

Пока я не нашел такого решения. Кажется, это относительно просто * реализовать по сравнению с другими возможностями сегодняшнего дня, такими как компиляция OCaml для JavaScript.

  • Есть ли уже (публичные) реализации того, что я описываю?
  • Какие другие возможности (очень) похожи на то, что я описываю, и чем они отличаются от этого? (Компиляция от языка к языку (от скомпилированного до sh) приходит на ум, хотя это кажется излишне трудным для понимания.)

Что я не имею ввиду

  1. Альтернативная оболочка (например, Scsh). Системы, которыми вы управляете, могут не всегда позволять выбирать оболочки для пользователя или одного администратора, и я также надеюсь, что это решение для системного администрирования исключительно для других (клиентов, коллег и других), а также людей, от которых нельзя ожидать принять другую оболочку.
  2. Альтернативный интерпретатор, для которого обычно служат неинтерактивные сценарии оболочки (например, ocamlscript). Лично у меня нет проблем, чтобы избежать сценариев оболочки для этой цели. Я делаю это потому, что сценарии оболочки, как правило, сложнее поддерживать (например, чувствительны к определенным символам и манипулируют изменчивыми вещами, такими как "команды"), и сложнее создать такой же уровень функциональности, какой могут предложить популярные языки программирования общего назначения (для Например, сравните Bash с Python в этом отношении). Однако в некоторых случаях необходим собственный сценарий оболочки, например файл профиля оболочки, который создается оболочкой при запуске.

Фон

Практическое применение

Некоторые из вас могут сомневаться в практической полезности того, что я описываю. Одним из практических применений этого является определение профиля оболочки на основе различных условий (например, системная платформа / ОС, на которой создается профиль, что следует из политики безопасности, конкретная оболочка, тип входа / отсутствия входа в систему). оболочка, интерактивный / неинтерактивный тип оболочки). Преимущество перед (хорошо разработанным) универсальным профилем оболочки в качестве сценария оболочки будет улучшением производительности (машинный код, который может генерировать сжатый / оптимизированный исходный код вместо интерпретации рукописного сценария), надежностью (проверка типов, обработка исключений). проверка времени компиляции функциональности, криптографическое подписание результирующего двоичного исполняемого файла), возможности (меньше или не зависит от пользовательских инструментов CLI, нет ограничений на использование минимальной функциональности, охватываемой инструментами CLI всех возможных платформ) и кроссплатформенные функции (в Практические стандарты, такие как спецификация Single UNIX, значат очень много, и многие концепции профилей оболочки переносятся на не-Unix-платформы, такие как Windows, вместе с PowerShell).

Детали реализации, побочные вопросы

  1. Программист должен иметь возможность контролировать степень универсальности сгенерированного сценария оболочки. Например, может случиться так, что двоичный исполняемый файл запускается каждый раз и выдает соответствующий код профиля оболочки, или он может просто генерировать фиксированный файл сценария оболочки, адаптированный к обстоятельствам одного запуска. В последнем случае перечисленные преимущества, в частности преимущества в отношении надежности (например, обработка исключений и использование инструментов пользователя), гораздо более ограничены. [Добавлено]
  2. Вопрос о том, будет ли результирующий сценарий оболочки в какой-либо форме универсального сценария оболочки (как генерирует GNU autoconf) или сценарий оболочки, адаптированный (динамически или нет) к конкретной оболочке, не является для меня первичным вопросом.
  3. easy *: мне кажется, что это может быть реализовано путем наличия в библиотеке доступных функций для базовых встроенных функций оболочки. Такая функция просто преобразует себя плюс переданные аргументы в семантически подходящую и синтаксически правильную инструкцию сценария оболочки (в виде строки).

Спасибо за любые дальнейшие мысли, и особенно за конкретные предложения!

1 ответ

Решение

Для этого нет библиотек на Haskell, но вы можете реализовать это с помощью абстрактных синтаксических деревьев. Я создам простой игрушечный пример, который строит абстрактное независимое от языка синтаксическое дерево, а затем применяет бэкэнд, который преобразует дерево в эквивалентный скрипт Bash.

Я буду использовать два трюка для моделирования синтаксических деревьев в Haskell:

  • Типизированные выражения Bash с использованием GADT
  • Реализация 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

Это позволяет нам создавать типизированное выражение Bash в нашем DSL. Я реализовал только несколько примитивных операций, но вы могли легко представить, как вы можете расширить его с другими.

Если бы мы не использовали какие-либо языковые расширения, мы могли бы написать такие выражения, как:

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

Я также отменяю прелюдию (++) оператор, чтобы мы могли объединять выражения, как если бы они были строками Haskell:

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

Кроме Shown приведенный от целых чисел к строкам, он выглядит как нативный код на Haskell. Ухоженная!

Теперь нам нужен способ создать удобный DSL, желательно с 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 шаги и определяет монаду, где мы можем собрать списки этих шагов. Вы можете думать о 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)

Это показывает, как мы собираем примитивные шаги, которые мы только что определили. У этого есть все хорошие свойства монад, включая поддержку монадических комбинаторов, таких как 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 Монада проглатывает любые команды после оператора выхода. Они игнорируются, даже не доходя до переводчика. Конечно, вы можете изменить это поведение, переписав Exit конструктор, чтобы принять последующий next шаг.

Эти абстрактные синтаксические деревья чисты, а это означает, что мы можем их чисто исследовать и интерпретировать. Мы можем определить несколько бэкэндов, таких как Bash, который преобразует наши Script монады к эквивалентному скрипту 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 _ -> ""

Я определил два интерпретатора: один для синтаксического дерева выражений и один для монадического синтаксического дерева DSL. Эти два интерпретатора компилируют любую независимую от языка программу в эквивалентную программу 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. Если вы хотите узнать больше об использовании бесплатных монад, я рекомендую вам прочитать:

Я также приложил полный код здесь:

{-# 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 _ -> ""
Другие вопросы по тегам