Pkl(コンフィグレーション構成生成:静的型付言語)

アプリ/Web開発etc…で利用するJSON/YAMLやXMLで書かれたコンフィグファイル生成言語がAppleからOpenSourceで公開されました。
コンフィグファイル生成に便利なのでサンプルを作ってみました。

Apple、コンフィグレーション生成用の静的型付き言語「Pkl」をオープンソースで公開、単一コードからJSONやYAML、XMLなどを生成

https://www.publickey1.jp/blog/24/applepkljsonyamlxml.html

マニュアル

手順

  1. インストール(curlでローカルへコピー&動作テスト)

  2. Pathが通っている場所へコピー

  3. pklコマンドの実行

インストール

各プラットフォーム向けのインストール手順に添います。
まず、Console(Terminal)を使って自分のマシンタイプを調べます。
以下のコマンドをConsoleで実行します。(※①)

% uname -m
※① マシンタイプの表示
今回は、M1 Macなので、arm64系ということが判明しました。

マシンタイプはarm64系のMacOSなので、

% curl -L -o pkl https://github.com/apple/pkl/releases/download/0.25.1/pkl-macos-amd64
chmod +x pkl
./pkl --version

を実行します。
※ 任意の場所で実行すると、pklというファイル名で実行ファイルがダウンロードされていると思います。
正常にダウンロード(インストール)できていれば、

(※2)
$ ./pkl --version 

が実行され、Pklのバージョンを表示します。

※2. 実行結果

コピー

正常にインストールが完了されたことを確認後、
pkl(実行ファイル)をシステム環境(PATH)に追加しておくといつでもConsole(Terminal)から利用できます。
brew環境の場合は、which brewでPATHを検索します。

# brewのPATHを検索
$ which brew
# 結果
/usr/local/bin/brew

pklを/usr/local/bin/に移動し、
同様にwhich pklを実行してみましよう。

$ mv ./pkl /usr/local/bin/pkl
# pklのPATHを検索(確認)

$ which pkl
# 結果: /usr/local/bin/pkl

brew PATHにpklが追加されました。

pklコマンドの実行

環境ができたので実行していきます。

$ pkl -h
$ pkl eval -f json [Pklファイル]
...

コマンドラインヘルプ

$ pkl -h
$ pkl --help

初めてのコンフィグファイル作成(intro.pkl)

任意の場所にintro.pklというファイルを作成します。

// intro.pkl
name = "Pkl: Configure your Systems in New Ways"
attendants = 100
isInteractive = true
amountLearned = 13.37

出力

作成したintro.pklをjson形式で出力します。

$ pkl -f json intro.pkl

実行結果:

// 出力結果
{
  "name": "Pkl: Configure your Systems in New Ways",
  "attendants": 100,
  "isInteractive": true,
  "amountLearned": 13.37
}

ファイル出力

作成したintro.pklをjsonファイルに書き出す場合は以下の通りにします。

$ pkl -f json intro.pkl -o intro.json

対応ファイルフォーマット

JSON/YAML/XML/plist …

Pythonでも利用できるようにしてみる

# File: pkl.py
# License: MIT License
# Date: 2024.02.17
# Written by oeOvOao
import os, sys, argparse, subprocess
class PklCLI:
    # pkl実行ファイルまでのパスは環境に応じて適宜変更
    pkl = "/usr/local/bin/pkl"
    source=None
    file_type=None
    command_line=False
    output_file=None
    shell=[]
    shell_option=[]
    def __init__(self, source=None, type=None):
        self.source = source

        if type is None or type == '':
            self.file_type = "json"
        else:
            self.file_type = type
    
    def set_cli(self, command_line=True):
        self.command_line = command_line
        print('[Show Help]: $ python3 pkl.py -h')

    def get_cli(self):
        return self.command_line

    def cli(self):
        parser = argparse.ArgumentParser(
            prog="PKL Python CLI(v.1.0)",
            usage=f'%(prog)s [HELP] -h/--help',
        )
        parser.add_argument("-s", "-source", default='intro.pkl', help=f"-s/-source [Pkl File]", required=True)
        parser.add_argument("--t", "--type", default='json', help=f"--t/--type [json, yaml, plist, xml]")
        return parser.parse_args()

    def output(self):
        split_file = self.source.split("/")[-1]
        file = ".".join(split_file.split(".")[:-1])
        self.output_file = f"{file}.{self.file_type}"
        self.shell_option = ["-o", self.output_file]
        self.shell += self.shell_option
    
    def print_file(self, view=False):
        message = f'''[CLI]: {self.get_cli()}
[File Type(Output)]: {self.file_type}
[Source File]: {self.source}
[Output File]: {self.output_file}'''
        if view:
            print(message)

    def run(self, output=False):
        if self.get_cli():
            args = self.cli()
            # program 
            self.source = args.s
            if os.path.isfile(self.source)==False:
                print('Source File Not Found.')
                exit()
            self.file_type = args.t

        self.shell = [self.pkl, "eval", "-f", self.file_type, self.source]
        if self.source is None or (os.path.isfile(self.source) == False):
            print("Source File Not found.")
            exit()
        print(f'[Preview]: {self.source}')
        if output:
            subprocess.run(self.shell)
            self.output()
        subprocess.run(self.shell)
        return self.output_file
# main.py
from pkl import PklCLI
import os
# Example
# WWW ... etc
def main():

    pkl=PklCLI(
        '/Users/ryohei/Pictures/Drawthings/opencv/config/sample.pkl', 
        'json'
    )

    # コマンドラインで呼び出したい場合、
    # pkl.set_cli()
    # command line help: $ python3 pkl.py -h

    # pklからコンフィグ生成(JSON/YAML...) : run(output=True)
    config_file = pkl.run(output=False)
    file_text = None
    if config_file is None:
        message = '''##########################################################################
PklCLI run method argument is not True
PklCLI (Example): 
        pkl = PklCLI('/pth/to/xxxx.pkl', 'json')
        config_file = pkl.run(True)
        or
        config_file = PklCLI('/pth/to/xxxx.pkl', 'json').run(True)
##########################################################################
    '''
        print(message)
        exit()

    if os.path.isfile(config_file):
        with open(config_file, 'r') as fp:
            file_text = fp.read()
        print(f'Config: {config_file}')
        print(file_text)

if __name__ == '__main__':
    main()
# 実行は、
$ python3 main.py

これでpythonからpklを扱ってJSON…etcの書き出しに対応できたので、FlaskみたいなWebApplicationとも連携できますね。
あとは、node.jsで変換できると良いのかな?

Node.jsでも利用してみる。

// pkl.js
/* *****************************************
    Code: PKL2Node.js
    License: MIT License
    Written by oeOvOao
    Date: 2024/02/17

    $ node index.js [PKL File] [File Type(JSON/YAML/plist/XML)]
    example: 
    const PKLCli = require('./pkl').PKL;
    const pkl = new PKLCli('/path/to/xxxx.pkl', 'json');
    pkl.run()
********************************************/
const exec = require('child_process').exec;
const fs = require('fs');

class PKLCli{
        // pklコマンドは適宜変更ください。
    #pkl  = '/usr/local/bin/pkl';
    constructor(file, type){
        this.file = file;
        this.type = type;
    }
    run(){
        const argv = process.argv;
        const pkl = this.#pkl
        const pkl_file = (argv[2] != null) ? argv[2]: `${this.file}`;

        if(!fs.existsSync(pkl_file)){
            console.log(`File Not Found > ${pkl_file}`);
            return 0
        }

        const file_type = (argv[3] != null) ? argv[3] : `${this.type}`
        const file_name = `${process.env.PWD}/${pkl_file.split('/')[pkl_file.split('/').length-1].split('.')[0]}.${file_type}`
        exec(`${pkl} eval -f ${file_type} -o ${file_name} ${pkl_file}`, (err, stdout, stderr) => {
            if (err) { console.log(err); }
            console.log(stdout);
        });
        console.log(`Edit FIle: ${pkl_file}`);
        return file_name;      
    }
}
module.exports = {
    PKL: PKLCli
}
// index.js
const fs = require('fs');
const PKLCli = require('./pkl').PKL

const main = ()=>{
    const pkl = new PKLCli(`${process.env.PWD}/intro.pkl`, 'json')
    const file_name = pkl.run()
    if (fs.existsSync( file_name )){
        const txt = fs.readFileSync(file_name, {encoding: 'utf-8'})
        // const _json = JSON.parse(txt);
        
        console.log(`Read File > ${file_name}`)
        console.log(txt);
    }
    else{
        console.log(`File Create > ${file_name}`)
    }
}

main()
# 実行は、
$ node index.js
or
$ node index.js /path/to/xxxxx.pkl

Chrome 拡張機能のmanifest(v.3)を書いてみる

// manifest.pkl
name="Chrome Apps(v3) Package Sample"
version="1.0.0"
manifest_version=3
description="Your Chrome App Description"
icons=new {
    `128`="icons/icon_app_128.png"
}
content_scripts=new {
    new {
        matches = new {
            "*://*/*"
        }
        js = new {
            "js/content.js"
        }
    }
}
action = new {
    default_title = "Template Title"
    default_popup = "popup/popup.html"
}
permissions=new {
    "scripting"
    "activeTab"
}

※ keyが数値・記号の場合、`(バッククォート)で数値・記号を囲みます。

# Preview
$ pkl eval -f json /path/to/manifest.pkl
// 生成結果(Preview)
{
  "name": "Chrome Apps(v3) Package Sample",
  "version": "1.0.0",
  "manifest_version": 3,
  "description": "Your Chrome App Description",
  "icons": {
    "128": "icons/icon_app_128.png"
  },
  "content_scripts": [
    {
      "matches": [
        "*://*/*"
      ],
      "js": [
        "js/content.js"
      ]
    }
  ],
  "action": {
    "default_title": "Template Title",
    "default_popup": "popup/popup.html"
  },
  "permissions": [
    "scripting",
    "activeTab"
  ]
}

マニフェストファイル形式参考:

開発でいろいろな場面でJSON/YAMLを管理する場面が多いので、計算もできるPKLを使う場面も増えそうな感じがしました。
私は利用してます。

最後まで読んでいただきありがとうございました。
掲載のサンプルのライセンスは、
MIT Licenseで掲載しています。
では、また👋

この記事が気に入ったらサポートをしてみませんか?