Генерация кода сценария оболочки CLI из скомпилированного исполняемого файла?
Вопрос, тема обсуждения
Я очень заинтересован в создании исходного кода сценариев оболочки командной строки из кода, написанного на более надежном, хорошо работающем и независимом от платформы скомпилированном языке (например, OCaml). По сути, вы должны программировать на скомпилированном языке для выполнения любых взаимодействий с ОС, которые вы хотите (я бы предложил: более сложные взаимодействия или те, которые нелегко сделать независимо от платформы), и, наконец, вы бы скомпилировали их. к собственному двоичному исполняемому файлу (предпочтительно), который будет генерировать сценарий оболочки, который воздействует на оболочку, что вы запрограммировали на скомпилированном языке. [ДОБАВЛЕНО]: Под "эффектами" я подразумеваю устанавливать переменные среды и параметры оболочки, выполнять определенные нестандартные команды (стандартный "скриптовый клей" скрипта будет обрабатываться скомпилированным исполняемым файлом и не попадать в сгенерированный скрипт оболочки).) и тому подобное.
Пока я не нашел такого решения. Кажется, это относительно просто * реализовать по сравнению с другими возможностями сегодняшнего дня, такими как компиляция OCaml для JavaScript.
- Есть ли уже (публичные) реализации того, что я описываю?
- Какие другие возможности (очень) похожи на то, что я описываю, и чем они отличаются от этого? (Компиляция от языка к языку (от скомпилированного до sh) приходит на ум, хотя это кажется излишне трудным для понимания.)
Что я не имею ввиду
- Альтернативная оболочка (например, Scsh). Системы, которыми вы управляете, могут не всегда позволять выбирать оболочки для пользователя или одного администратора, и я также надеюсь, что это решение для системного администрирования исключительно для других (клиентов, коллег и других), а также людей, от которых нельзя ожидать принять другую оболочку.
- Альтернативный интерпретатор, для которого обычно служат неинтерактивные сценарии оболочки (например, ocamlscript). Лично у меня нет проблем, чтобы избежать сценариев оболочки для этой цели. Я делаю это потому, что сценарии оболочки, как правило, сложнее поддерживать (например, чувствительны к определенным символам и манипулируют изменчивыми вещами, такими как "команды"), и сложнее создать такой же уровень функциональности, какой могут предложить популярные языки программирования общего назначения (для Например, сравните Bash с Python в этом отношении). Однако в некоторых случаях необходим собственный сценарий оболочки, например файл профиля оболочки, который создается оболочкой при запуске.
Фон
Практическое применение
Некоторые из вас могут сомневаться в практической полезности того, что я описываю. Одним из практических применений этого является определение профиля оболочки на основе различных условий (например, системная платформа / ОС, на которой создается профиль, что следует из политики безопасности, конкретная оболочка, тип входа / отсутствия входа в систему). оболочка, интерактивный / неинтерактивный тип оболочки). Преимущество перед (хорошо разработанным) универсальным профилем оболочки в качестве сценария оболочки будет улучшением производительности (машинный код, который может генерировать сжатый / оптимизированный исходный код вместо интерпретации рукописного сценария), надежностью (проверка типов, обработка исключений). проверка времени компиляции функциональности, криптографическое подписание результирующего двоичного исполняемого файла), возможности (меньше или не зависит от пользовательских инструментов CLI, нет ограничений на использование минимальной функциональности, охватываемой инструментами CLI всех возможных платформ) и кроссплатформенные функции (в Практические стандарты, такие как спецификация Single UNIX, значат очень много, и многие концепции профилей оболочки переносятся на не-Unix-платформы, такие как Windows, вместе с PowerShell).
Детали реализации, побочные вопросы
- Программист должен иметь возможность контролировать степень универсальности сгенерированного сценария оболочки. Например, может случиться так, что двоичный исполняемый файл запускается каждый раз и выдает соответствующий код профиля оболочки, или он может просто генерировать фиксированный файл сценария оболочки, адаптированный к обстоятельствам одного запуска. В последнем случае перечисленные преимущества, в частности преимущества в отношении надежности (например, обработка исключений и использование инструментов пользователя), гораздо более ограничены. [Добавлено]
- Вопрос о том, будет ли результирующий сценарий оболочки в какой-либо форме универсального сценария оболочки (как генерирует GNU autoconf) или сценарий оболочки, адаптированный (динамически или нет) к конкретной оболочке, не является для меня первичным вопросом.
- 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 _ -> ""