헬: 하스켈 방언 기반의 셸 스크립팅 언어

박준규 @curry@hackers.pub

은 제가 개인적인 셸 스크립팅 용도로 만든, 하스켈의 작은 방언 형태의 셸 스크립팅 언어입니다. 2월부터는 Hakyll 대신 헬을 사용해 이 블로그를 생성하고 있습니다.[1]

업데이트: 2024년 10월 3일 기준으로, 저는 업무에서 테라폼과 여러 API와 함께 약 2천 줄 규모의 다양한 대형 스크립트에 헬을 사용하고 있습니다.

#!/usr/bin/env hell
main = do
  Text.putStrLn "Please enter your name and hit ENTER:"
  name <- Text.getLine
  Text.putStrLn "Thanks, your name is: "
  Text.putStrLn name

제 2024년 새해 결심은 자동화를 위해 더 많은 셸 스크립트를 작성하는 것입니다. 지금까지는 bash의 단점 때문에 이런 작업을 피해왔습니다. 그리고 그 밖의 문제들도 있습니다.

bash, zsh, fish 등은 다음과 같은 문제를 가지고 있습니다.

  • 이해할 수 없는 난해한 문법입니다.
  • 인용문 사용(x=$(ls -1) 등)으로 인해 실수하기 쉽습니다.
  • 기본적인 작업조차 서브 프로세스에 지나치게 의존합니다.
  • 따라서 비교, 산술, 순서 지정 등과 같은 것들은 전혀 원칙적이지 않으며, 함정이 가득합니다.[2][3]

하지만 bash에도 장점이 있습니다. 안정적이고, 단순하며, 모든 환경에서 동일하게 동작합니다. bash 스크립트를 작성하면 수년간 코드를 한 번도 수정하지 않고 계속 실행할 수 있습니다. 지난해 작성한 코드가 내년에도 그대로 동작한다는 점은 대부분의 인기 있는 프로그래밍 언어에서는 해당되지 않습니다. 하스켈을 한번 보세요.

그러므로 제가 실제로 사용하고 싶은 언어를 정의하기 위해, 셸 스크립팅 언어의 구조를 논의해보겠습니다.

  • 매우 기본적이어야 합니다.
  • 즉시 실행되어야 합니다.(눈에 보이는 컴파일 과정이 없어야 함)
  • 모듈 시스템이 없어야 합니다.
  • 패키지 시스템이 없어야 합니다.[4]
  • 추상화 기능(클래스, 복잡한 데이터 타입, 다형 함수 등)이 없어야 합니다.
  • 이전 버전과 호환되지 않게 변경되어서는 안 됩니다.[5]

왜 모듈이나 패키지 시스템이 없어야 할까요? 그런 시스템은 프로그램을 “완성” 상태로 유지하기 어렵게 만듭니다. 항상 다른 통합을 할 수 있고, 추가할 기능이 남아 있기 마련입니다. 저는 헬이 냉정하게 완결된 소프트웨어이길 원합니다. 완성된 소프트웨어에는 아름다움이 있습니다.

위 내용을 바탕으로, 저는 스크립팅 한계를 정의할 수 있습니다. 즉, 모듈 시스템이나 패키지 시스템, 추상화 기능이 필요해지거나, 표준 라이브러리 이상의 기능이 필요해질 때, 그때는 아마 일반 목적의 프로그래밍 언어를 사용하는 것이 적절합니다.

이를 고려하여, 저는 하스켈 방언[6]을 만들기로 선택했습니다. 그 이유는 다음과 같습니다.

  • 하스켈을 잘 알고 있습니다.
  • 제가 가장 자주 사용하는 언어입니다.
  • 비교, 순서 지정 등과 관련해 탄탄한 개념을 가지고 있습니다.
  • 동시성을 손쉽게 처리할 수 있는 좋은 런타임을 가지고 있습니다.
  • 가비지 컬렉션이 있습니다.
  • 바이트와 텍스트를 적절히 구분합니다.
  • 정적 Linux x86 바이너리로 컴파일할 수 있습니다.
  • 성능이 좋습니다.
  • 정적 타입을 지원합니다!

언어를 설계하면서 저는 다음과 같은 결정을 내렸습니다.

  • 충실한 하스켈 문법 파서를 사용합니다.
  • 이렇게 하는 것이 더 낫습니다. 직관과 코드를 재사용할 수 있습니다.
  • 임포트, 모듈, 패키지는 지원하지 않습니다.
  • 재귀 정의는 지원하지 않지만, fix를 사용하면 가능합니다.
  • 기본적인 타입 클래스만 지원합니다.(Eq, Ord, Show, Monad) 이는 예를 들어 List.lookup이나 일반적인 비교 연산 등에 필요합니다.
  • 다형 타입은 지원하지 않습니다. 이것은 일종의 추상화이며, 필요하지 않습니다.
  • 하스켈에서 이미 사용되는 이름(List.lookup, Monad.forM, Async.race 등)을 그대로 사용합니다. 이를 통해 기존 하스켈에 대한 직관을 재사용할 수 있습니다.

릴리스 페이지에서 정적 링크된 리눅스 바이너리를 다운로드할 수 있습니다. 구현 내부에 대해 알아보려면, 제가 업무에서 헬을 소개할 때 만든 헬 소개 슬라이드를 참고하세요.


  1. 이런 문제들에는 이제 진절머리가 납니다. ↩︎

  2. ShellCheck에서 발견되는 방대한 코드 점검 문제 목록을 한번 살펴보세요. ↩︎

  3. 코드 실행에 관한 이 블로그 글을 참고하세요. ↩︎

  4. 이 기준은 믿기 어려울 정도로 Node.js 생태계 위에서 동작하는 zx와 같은 스크립팅 언어는 제외합니다. ↩︎

  5. 참고: 이전 버전과의 비호환성이라는 햄스터 바퀴에서 벗어나기 ↩︎

  6. 다른 대체 셸 스크립팅 언어를 사용하지 않고, ElixirOil도 사용하지 않았습니다. ↩︎

8

No comments

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/019933ce-93f7-7af6-940f-4929ec7f1f18 on your instance and reply to it.