🔸

型安全なシェルスクリプトを書けるプログラミング言語Amberを使ってみた

2024/05/24に公開

シェルスクリプトを型安全に書きたいと考える人もいると思います。
そういった人におすすめのRust製プログラミング言語Amberを使ってみたので簡単に紹介します。

https://amber-lang.com/

Write your scripts in a modern type-safe and runtime-safe programming language that handles many bugs and mistakes during compilation process.

GitHub Star Historyによると最近一気にGitHubのスター数を獲得したようです。

この記事の内容は現時点(2024/05/19)の以下の公式ドキュメントをベースに、追加でソースコードを調べて試した結果を基に書いています。
Discordで活発に議論が行われており、READMEや公式ドキュメントも頻繁に更新されているようなので、この記事の情報もそのうち古くなると思います。

https://docs.amber-lang.com/

Getting Started | はじめに

特徴をピックアップすると以下のようなものが挙げられています。

  • Bashスクリプトにコンパイルされるプログラミング言語
  • Bashで提供されない最新の構文、安全性、型安全性、実用的な機能を備えて設計されている
  • ECMAスクリプトに基づいた構文
    • RustやPythonなどの言語の機能を取り入れている
  • 安全機能
    • エッジケースが処理されない場合、コンパイルされない
  • 型安全性
    • コンパイル時に単純なバグやエラーを識別するのに役立つわかりやすい型システムが付属している
  • 追加機能
    • 浮動小数点演算、配列を処理するわかりやすい方法、コピーではなく参照による変数の受け渡しなど。さらにテキストのトリミング、配列内のすべての数値の合計、テキストの分割などの機能を含む標準ライブラリが付属している

Installation | インストール

動作環境は以下。

  • Linux x86 and ARM
  • macOS x86 and ARM (Apple Silicon)

Windowsの場合はWSL2で動作する。

インストールのコマンドは以下。

curl -s "https://raw.githubusercontent.com/Ph0enixKM/AmberNative/master/setup/install.sh" | $(echo /bin/bash)

アンインストールのコマンドは以下。

curl -s "https://raw.githubusercontent.com/Ph0enixKM/AmberNative/master/setup/uninstall.sh" | $(echo /bin/bash)

Usage | 使い方

Syntax Highlighting | シンタックスハイライト

Running | 実行方法

Amberのコードfile.abをスクリプトファイルとして実行します。

amber file.ab

Bash Scriptにコンパイルされ、実行されます。

コードを引数にして実行することもできます。-e(execute) フラグに続けてコードを渡して実行します。

$ amber -e 'echo upper("hello world!")'
HELLO WORLD!

このように、コードを引数に渡した場合はAmberの標準ライブラリの関数は自動的に読み込まれて使うことができます。

Compiling | コンパイル

Amberのコードをスクリプトにコンパイルします。

amber input.ab output.sh

Amberが自動的に権限を付与するため、コンパイルされたスクリプトは以下のようにすぐ実行できます。

./output.sh

Furthermore, Amber adds a shebang at the top of the compiled script. This enables you to run the code simply without any additional commands.

Basic Syntax | 基本構文

公式ドキュメントはプログラミングの基礎を理解していることを前提としています。
以下はAmberの機能を説明するコードスニペットが載っています。

// Define variables
let name = "John"
let age = 30

// Display a greeting
echo "Hello, my name is {name}"

// Perform conditional checks
if age < 18 {
    echo "I'm not an adult yet"
} else {
    echo "I'm an adult"
}

// Loop through an array
let fruits = ["apple", "banana", "cherry", "date"]
echo "My favorite fruits are:"
loop fruit in fruits {
    echo fruit
}

Data Types | データ型

Amberには5つの型があります。

  • Text - テキストデータ型。他のプログラミング言語でのString
  • Num - 数値データ型。基本的には任意の数値
  • Bool - ブーリアンデータ型。trueまたはfalse
  • Null - Nothingデータ型。
  • [] - 配列データ型。

Text | テキストデータ型

内部的にも単なる文字列。

// `Text` literal:
"Welcome to the jungle"

Number | 数値データ型

内部的にはBashと同じように文字列として保存されます。違いは、Amber が浮動小数点をサポートする数値の演算に標準コマンドを適用することです。

// `Num` data type
// Can be an integer
42
// or a floating point
-123.456

Boolean | ブーリアンデータ型

// `Bool` data type
true
false

内部的には、true = 1false = 0として保存される。

Null | Nothingデータ型

最も一般的な使い方として、関数が値を返さないことを宣言する場合ですが、現実的な例はないないとのことでした。

Array | 配列データ型

配列の型宣言はなく、配列に値を入れるだけです。

// `[Num]` data type
[1, 2, 3]

空の配列を作成する場合は、その配列に含まれるデータ型を指定する必要があります。

// Example of a value that represents empty array of text
[Text]

2次元配列はサポートされていません。

Expressions | 式

データ型リテラルを演算子と組み合わせることができます。演算子は同じデータ型でのみ機能します。

Addition operator + | 加算演算子

加算は数値、テキスト、配列に対して実行できます。データ型ごとに異なる結果が生成されます。

  • Num - 数値の合計
  • Text - 文字列の連結
  • [] - 配列の結合
12 + 42 // 54
"Hello " + "World!" // "Hello World!"
[1, 2] + [3, 4] // [1, 2, 3, 4]

Arithmetic operations | 算術演算

算術演算はNumデータ型に対してのみ使用できます。使用可能なすべての演算のリストは次の通りです。

  • + - 加算
  • - - 減算
  • * - 乗算
  • / - 除算
  • % - 剰余
((12 + 34) * 9) % 4

Comparison operations | 比較演算

比較演算もNumデータ型に対してのみ使用できます。これらは基本的に他のプログラミング言語と同じです: ==, !=, >, <, >=, <=

42 != 24

Logical operations | 論理演算

論理演算はBoolデータ型にのみ使用できます。スクリプトプログラミング言語の性質により適しているため、Python的なアプローチを選択しました: and, or, not

18 >= 12 and not false

Shorthand operator | ショートハンド演算子

加算演算子などの算術演算子をのショートハンドが存在します。

let age = 18
age += 5
echo age // Outputs: 23

Text interpolation | テキスト補間

テキスト内で変数の値を代入することができます。

let name = "John"
let age = 18
echo "Hi, I'm {name}. I'm {age} years old."
// Outputs: Hi, I'm John. I'm 18 years old

Variables | 変数

変数宣言にはletを使います。

let name = "John"

変数宣言がされている場合は再代入できます。

name = "Rob"

変数の参照は次の通り。

echo name // Outputs: "Rob"

Overshadowing | 変数の再宣言

同じ変数名で何度でも変数宣言できます。

// `result` is a `Num`
let result = 123
// `result` is a `Text`
let result = "Hello my friend"

Conditions | 条件分岐

条件付きロジックを実行する方法は 3 つあります。

  • if文 - 通常のif文
  • Ifチェーン - if-elseの糖衣構文
  • 三項式 - 式内で条件付きロジックを表す方法

If Statement | If文

普通のif文です。

if age >= 16 {
    echo "Welcome"
}

以下はelseを追加した場合。

if age >= 16 {
    echo "Welcome"
} else {
    echo "Entry not allowed"
}

条件分岐した後に実行するコマンドが一行の場合は:を使って次のように書くことができます。

if age >= 16: echo "Welcome"
else: echo "Entry not allowed"

// Or

if age >= 16:
    echo "Welcome"
else:
    echo "Entry not allowed"

If chain | Ifチェーン

if チェーンは、一連の if-else ブロックを簡略化する手法です

if {
    drink == "water" {
        echo "Have a natural, mineralized water"
    }
    drink == "cola" {
        echo "Here is your fresh cola"
    }
    else {
        echo "Sorry, we have none of that"
    }
}

// Alternatively, as previously mentioned:

if {
    drink == "water": echo "Have a natural, mineralized water"
    drink == "cola": echo "Here is your fresh cola"
    else: echo "Sorry, we have none of that"
}

以下のように書くことができます。

if drink == "water" {
    echo "Have a natural, mineralized water"
} else {
    if drink == "cola" {
            echo "Here is your fresh cola"
    } else {
            echo "Sorry, we have none of that"
    }
}

Ternary expression | 三項式

三項式を使うことで以下のような値の代入ができます。

let candy = count > 1
    then "candies"
    else "candy"

echo "I have {count} {candy}"

短縮形で書く場合は以下のようになります。

let candy = count > 1 then "candies" else "candy"

Commands | シェルコマンド

Amberでシェルコマンドを扱う中で重要なのはfailできるということです。

  • failed - 最も推奨される、コマンドが失敗したときに実行する追加ロジックを記述できるもの
  • ? - 呼び出し元に失敗を伝播するための省略形です。この演算子は、mainブロック内または関数内でのみ使用できます
  • unsafe - 式を処理するための非推奨の方法です。このコマンド修飾子はコマンドが正常終了していなくても正常に完了したものとして扱います
// Command statement
$mv file.txt dest.txt$ failed {
    echo "It seems that the file.txt does not exist"
}

// Command expression
let result = $cat file.txt | grep "READY"$
echo result

コマンド式内で変数を参照できます。

let filePath = "/path/to/file"
$cat {filePath}$ failed {
    echo "Could not open '{filePath}'"
}

Getting the exit code | 終了コードの取得

statusキーワードでコマンドの終了ステータスを取得できます。

let filePath = "/path/to/file"
$cat {filePath}$ failed {
    echo "Error! Exit code: {status}"
}
echo "The status code is: {status}"

Failure propagation | 失敗の伝播

失敗を伝播するには、疑問符構文?を使います。
シェルスクリプトにコンパイルするとわかりますが、終了ステータスが0以外の場合にreturn $?するのと同じ動作です。

$test -d /path/to/file$?
// Which is the same as

$test -d /path/to/file$ failed {
    fail status
}

Command Modifiers | コマンド修飾子

コマンド修飾子はコマンドの動作を変更するキーワードです。

  • silent - コマンドが結果を標準出力に表示しないようにします
  • unsafe - エラーハンドリングを要求するAmberのメカニズムを無効にします

各コマンド修飾子の詳細については、次の章で説明します。

コマンド修飾子の使用例を次に示します。

silent unsafe $my command$

コマンド修飾子を修飾子スコープとして使用できます。これにより、複数のコマンドで同じことを繰り返す必要がなくなります。

silent unsafe {
    $first command$
    if isReady:
            $second command$
    $third command$
}

Unsafe command execution | 安全でないコマンドの実行

unsafe $test -d /path/to/file$

これは、Bashと同じように失敗しても、コードの実行を継続します。この動作は、Amberを構築するときに回避しようとしていたものです。この方法が推奨されるケースは次のとおりです。

  • このコマンドが正常に完了することを確信している
  • コマンドが失敗するかどうかは気にしない

Silencing commands | コマンドのサイレンシング

特定のコマンドを簡単に無音にすることができます。

silent $very loud command$

Arrays | 配列

配列は 0 からインデックス付けされます。
配列の特定のインデックスに値を保存または取得するには、次の構文を使用できます。

let groceries = ["Apple", "Banana", "Orange"]
groceries[0] = "Almond"
echo groceries[1]
// Outputs: Banana

配列全体をechoすることもできます。

echo groceries
// Outputs: Almond Banana Orange

配列に要素を追加するには加算演算子を使用します。

let capitals = ["London", "Paris"]
capitals += ["Warsaw"]

let cities = capitals + ["Barcelona", "Florence"]

joinlensumなどの関数については標準ライブラリのドキュメントを参照するよう書かれていますが、見つけられませんでした。

Ranges | 配列の範囲

特定の範囲の数値の配列[Num]を生成する機能を提供します。範囲には2種類あります:

  • .. - a から b までで b を除く
  • ..= - a から b まででb を含む
echo 0..10
// Outputs: 0 1 2 3 4 5 6 7 8 9

echo 0..=10
// Outputs: 0 1 2 3 4 5 6 7 8 9 10

Loops | ループ

Amber は次の 2 種類のループをサポートしています。

  • breakキーワードでのみ解除できる無限ループ
  • 配列を反復するイテレータループ

ループのコンテキストでは、break キーワードと continue キーワードを使用してフローを制御できます。

Infinite loop | 無限ループ

無限ループに何を入れても、breakするまで無限に実行されます。

let i = 0
let sum = 0
loop {
    if i == 5:
        break
    i += 1
    sum += i
}
echo sum
// Outputs: 15

Iterator loop | イテレータループ

配列を反復処理する最も推奨される方法です。前の章の例は、より簡潔なバージョンに書き直すことができます。

let sum = 0
loop i in 0..5 {
    sum += i
}
echo sum
// Outputs: 10

以下は、反復ループの動作を示す別の例です。

let files = ["config.json", "file.txt", "audio.mp3"]

loop index, file in files {
    $mv {file} {index}{file}$ failed {
        echo "Failed to rename {file}"
    }
}

上記の例では、配列内のすべてのファイルを反復処理し、配列内の順序に従ってインデックスを付けます。その結果、これらのファイルの名前は0config.json、、1file.txtおよび2audio.mp3に変更されます。

Functions | 関数

関数は、コードの構造を再利用可能なコンポーネントに整理するのに役立ちます。関数を宣言する方法は次のとおりです。

fun myFunction(arg1, arg2) {
    let result = arg1 + arg2
    return result
}

echo myFunction(2, 3)
// Outputs: 5
echo myFunction("Hello", " World")
// Outputs: Hello World

上記の例で宣言された関数にはmyFunctionという名前があり、任意の型の2つの引数arg1arg2を取ることができます。

特定の型の引数を取る関数を宣言したい場合は、そうすることをお勧めします。ただし、一貫性を保つために、戻り値の型も指定する必要があります。

fun myFunction(arg1: Num, arg2: Num): Num {
    let result = arg1 + arg2
    return result
}

関数に関する興味深い事実は、関数は使用されない限り解析されないということです。Amberでは型の指定を省略できるためこのような挙動になります。そのような関数を使用すると、使用された型を持つこの関数のさまざまなバリアントが生成されます (重複なし)。

Modifiers | 修飾子

関数呼び出しにもコマンド修飾子を適用できます。この方法を使うと、silent修飾子で出力を抑制したり、unsafeキーワードを使用して失敗する可能性のある関数を失敗しないかのように実行することができます(ただし、これは推奨されません)。

Failing | 失敗

関数はさらに失敗することがあります。これを失敗可能な関数(failable functions)と呼びます。失敗可能な関数は失敗する可能性のあるタイプの関数です。関数を失敗させるには、failキーワードを使用し、終了コードを続けて記述します。

以下は失敗する関数のもう一つの例です:

fun failing(name) {
    $command$?
    parse(name)?
}

? 演算子を使用すると、失敗した操作の status コードで自動的に失敗します。

Status code | ステータスコード

ステータスコードには、最新の失敗した関数または実行されたコマンドに関する情報が含まれます。ステータスにアクセスするのは、status キーワードを使用するだけで簡単です。

fun safeDivision(a: Num, b: Num): Num {
    if b == 0:
        fail 1
    return a / b
}

では、このコードがさまざまなシナリオでどのように動作するかを見てみましょう:

let result = unsafe safeDivision(24, 4)
echo "{result}, {status}"
// Outputs: 6, 0

これはうまくいった場合です。次に、ゼロで除算するとどうなるかを見てみましょう:

let result = safeDivision(15, 0) failed {
    echo "function Failed"
    echo status
}
// Outputs:
// function Failed
// 1

Variable references ref | 変数参照

変数を参照渡しで受け取ることができます。これを行うには、ref キーワードを使用します。

fun push(ref array, value) {
    array += [value]
}

let groceries = ["apples", "bananas"]
push(groceries, "oranges")
echo groceries
// Outputs: apples bananas oranges

このキーワードの動作は、他のC系プログラミング言語の & に非常に似ています。

Importing | インポート

Amberでは、他のファイルから関数をインポートできます。関数を外部ファイルからアクセス可能にするには、その関数が定義されているファイル内で public として宣言する必要があります。

Public Functions | パブリック関数

関数をパブリックとして宣言するには、pub キーワードを使用します。pub キーワードは、関数を宣言する fun キーワードの前に使用する必要があります。

pub fun sum(left: Num, right: Num): Num {
    return left + right
}
Importing from other files | 他のファイルからのインポート

関数を個別にインポートすることができます...

import { foo, bar } from "./my-file.ab"

foo()
bar()

...または、すべての関数を一度にインポートすることもできます。

import * from "./arith.ab"

echo sum(1, sub(2, mul(4, 5)))

Public imports | パブリックインポート

インポートしたものを再度エクスポートしたい場合もあります。その場合は、次のように簡単に行えます:

pub import * from "my/path/file.ab"

これにより、file.ab に定義されたすべての関数がインポートされ、それらはこのファイルから再びパブリックに利用可能になります。

Main block | メインブロック

ファイルが直接実行されるときに特定のコードを実行したい場合があります。この問題はPythonでも解決できます:

if __name__ == 'main':
    # 実行するコード

Amberにはこのパターンのための特別な構文があります。ただの糖衣構文ではありません。メインスコープでは、? 演算子を使用することができ、この場合、終了コードは単に外部シェルに伝播されます。

echo "Running indirectly"

main {
    $some command$?
    echo "Running directly"
}

このファイルを実行すると、出力は次のようになります:

Running indirectly
Running directly

次に、ファイルをインポートした場合の動作を示します。

import * from "./file.ab"
// Outputs: Running indirectly

Advanced Syntax | 高度な構文

次のセクションでは、上級の Amber または Shell Script 開発者に推奨される Amber 構文について詳しく説明します。

As Cast | 型変換

Asキャストは、最初は型を簡単に変換するためのツールのように見えるかもしれません。では、なぜこの機能が上級カテゴリーに位置づけられているのでしょうか?それは大きな力には大きな責任が伴うからです。BoolからNumへのキャストのように意味のあるキャストを行うこともできますが、TextNumに変換するような「無意味な」キャストを行うことも可能です。

Regular casts | 通常のキャスト

Bool型の変数をNum型を受け取る関数に渡したい場合があります。BoolNumは互換性のある型なので、次のように簡単にキャストできます。

let isReady = systemIsReady()
processStatus(isReady as Num)

Absurd casts | 不合理な型変換

Amberでは、任意のデータ型を他の任意のデータ型にキャストすることができます。これは避けるべきであり、必要な場合にのみ使用するべきです。

let a = "12"
let b = a as Num

この例では、大きなバグを引き起こす可能性があることがわかります。例えば、"12"の代わりに"abc"を文字列として渡すと、それはNum型の有効な値ではありません。文字列を数値に変換するには、標準ライブラリのparse()関数を使用する方が良いでしょう。

import { parse } from "std"

let a = "12"
let b = parse(a)

echo b + 12
// Outputs: 24

Nameof | 変数名の取得

時には、難読化された変数名を必要とする高度なコマンドを書きたい場合があります。この値は、提供された nameof キーワードを使用して取得できます。例えば、次のようなことが可能になります:

let variable = null

unsafe ${nameof variable}=12$
// Which is the same as:
let variable = 12

Type Condition | 型条件

ジェネリック関数を構築する際に、渡された値がサポートされているデータ型の1つであるかを追加で検証することが非常に便利であると感じることがあります。

fun getObject(value) {
    if {
        value is Text: getByName(value)
        value is Num: getById(value)
    }
}

Compiler Flags | コンパイラフラグ

コンパイラフラグを使用すると、関数の与えられたスコープに対してコンパイラの振る舞いを変更できます。一部の警告は理由があるため存在します。なぜある警告が出力されているのか理解できない場合は、基本構文についてもっと学んでください。以下は、使用可能なすべてのコンパイラフラグのリストです:

  • allow_nested_if_else - if else チェーンを処理するために設計された構文を使用するように開発者に勧める警告をオフにします。
  • allow_generic_return - 具体的な型の引数が使用されている場合、具体的な戻り値の型を指定するようにユーザーに指示する警告をオフにします。
  • allow_absurd_cast - 指定された強制型の結果が不合理である可能性があることをユーザーに伝える警告をオフにします。

例:

#[allow_nested_if_else]
fun foo() {
    // ...
}

以上が公式ガイドの内容です。以下は公式ガイドに記載のない部分の補足です。

標準関数について

標準ライブラリの関数についてドキュメントが見つかりませんでした。ソースコードを探したところsrc/std/main.abにAmberの標準ライブラリと思われる関数定義を見つけました。

このファイルに記載された関数はコマンドラインから直接呼び出すamber -e 'echo replace_once("hello world!", "world", "Amber")'といった実行形式で使えます。

ファイルに記述して使う場合は以下のようにインポートすることで使えます。

$ cat ./sample.ab
import * from "std"
echo replace_once("Hello world!", "world", "Amber")

$ amber ./sample.ab
Hello Amber!

以下に標準ライブラリの関数の使用例を記載します。

関数 input

標準入力を読み取る関数です。シェル環境で対話的に利用できます。

$ amber -e '
> echo "Please enter your name:"
> let name = input()
> echo "Hello, " + name + "!"
> '
Please enter your name:
Amber
Hello, Amber!

関数 replace_once

1回だけパターンを置換する関数。

$ amber -e 'echo replace_once("hello world!", "world", "Amber")'
hello Amber!

関数 replace

すべてのパターンを置換する関数。

$ amber -e 'echo replace("banana banana", "banana", "apple")'
apple apple

関数 replace_regex

正規表現を用いてパターンを置換する関数。

$ amber -e 'echo replace_regex("abc123def", "[0-9][0-9]*", "456")'
abc456def

関数 file_read

ファイルを読み込む関数。

$ cat file.txt
This is sample file.

$ amber -e 'let f = file_read("./file.txt") failed {echo "Failed"} echo f'
This is sample file.

関数 file_write

ファイルに書き込む関数。

$ amber -e 'unsafe file_write("./file.txt", "Hello, Amber!")'
$ cat file.txt
Hello, Amber!

関数 file_append

ファイルに追記する関数。

$ amber -e 'unsafe file_append("./file.txt", "Appending this line.")'
$ cat file.txt
Hello, Amber!
Appending this line.

関数 split

文字列を指定したデリミタで分割する関数。

$ amber -e 'let array = split("apple,banana,cherry", ","); echo array[1]'
banana

関数 join

リストを指定したデリミタで結合する関数。

$ amber -e 'echo join(["apple", "banana", "cherry"], ", ")'
apple,banana,cherry

関数 trim

文字列の両端の空白を取り除く関数。

$ amber -e 'echo trim("  hello world  ")'
hello world

関数 trim_left

文字列の左端の空白を取り除く関数。

$ amber -e 'echo trim_left("  hello world  ")'
# 期待するOutput: hello world  

関数 trim_right

文字列の右端の空白を取り除く関数。

$ amber -e 'echo trim_right("  hello world  ")'
# 期待するOutput:   hello world

関数 lower

文字列を小文字に変換する関数。

$ amber -e 'echo lower("HELLO WORLD")'
hello world

関数 upper

文字列を大文字に変換する関数。

$ amber -e 'echo upper("hello world")'
HELLO WORLD

関数 len

文字列やリストの長さを取得する関数。

$ amber -e 'echo len("hello")'
5

$ amber -e 'echo len([1, 2, 3, 4])'
4

関数 parse

文字列を数値に変換する関数。

$ amber -e 'main{echo parse("123")?}'
123

関数 chars

文字列を文字ごとのリストに変換する関数。

$ amber -e 'echo chars("hello")'
h e l l o

関数 sum

数値のリストの合計を計算する関数。

$ amber -e 'echo sum([1, 2, 3, 4])'
10

関数 has_failed

コマンドが失敗したかどうかを判定する関数。

$ cat ./sample.ab
import * from "std"

if has_failed("ls /nonexistent") {
        echo "Command failed"
        } else {
        echo "Command succeeded"
}

$ amber ./sample.ab
Command failed

関数 exit

指定したコードでプログラムを終了する関数。

$ amber -e 'exit(0)'
# Output: (プログラムが終了する)

関数 includes

リストに値が含まれているかどうかを判定する関数。

$ cat ./sample.ab
import * from "std"

if includes(["apple", "banana", "cherry"], "banana"){
        echo "Found"
} else {
        echo "Not Found"
}

$ amber ./sample.ab
Found

Amberの検討材料

  1. コンパイルしたシェルスクリプトを実行する必要がある場合に適している
    • コンパイルを行うのにAmberの実行環境が必要ですが、コンパイル後のシェルスクリプトを使うのであれば単純にコピーするだけで使えます
  2. エラーハンドリングの強制
    • 素のシェルスクリプトを書いているとエラーハンドリングを書き忘れることがあります
    • Amberでは失敗する可能性のあるコマンド実行にエラーハンドリングを書かないとコンパイルが通らないため、考慮漏れを防ぎやすいと思います

GitHubのスター数が増えるなど、これから注目度が高まりそうな予兆があります。機能が発展していけばシェルスクリプトの代替として有力な候補になるのではないでしょうか。

Discussion