joetjen.net
EN DE
Konfigurationssprache

Grammatik

CASC ist durch eine PEG-Grammatik in Aether definiert, der Grammatiksprache von Ichor. Die Parser für Elixir und Praxis werden daraus erzeugt statt von Hand geschrieben; die Parser für Node.js und PHP übertragen sie Regel für Regel.

Format 1.0

Pragmas und Tokens

Dies ist die vollständige Grammatik aus priv/grammar/casc.aether, wie sie mit Cooper 0.7.0 veröffentlicht ist (unverändert seit 0.6.0), nur ohne Kommentare. Namen in Großbuchstaben sind Tokens, längste Übereinstimmung zuerst, bei Gleichstand gewinnt die frühere Deklaration; Namen in Kleinbuchstaben sind Regeln, die per geordneter Auswahl über den Tokens arbeiten. @skip TRIVIA erlaubt Leerraum und Kommentare zwischen je zwei Elementen einer Regel.

casc.aether
@grammar "casc"
@root file
@skip TRIVIA

COMMENT := "#" !("@" | ALPHA | "*") (!"\n" .)*
TRIVIA  := (SPACE | COMMENT)*

NIL_KW   := "nil"
TRUE_KW  := "true"
FALSE_KW := "false"
INF_KW   := ("+" | "-")? "inf" !(ALNUM | "_" | "+" | "-" | "?" | "!")

IDENT := ("_" | ALPHA) (ALNUM | "_" | "+" | "-")* ("?" | "!")?

DATE     := DIGIT{4} "-" DIGIT{2} "-" DIGIT{2}
TIME     := DIGIT{2} ":" DIGIT{2} ":" DIGIT{2} ("." DIGIT+)?
DATETIME := DATE "T" TIME (("Z") | (("+" | "-") DIGIT{2} ":" DIGIT{2}))?

HEX_GROUP := HEX{1,4}
IPV4 := DIGIT{1,3} "." DIGIT{1,3} "." DIGIT{1,3} "." DIGIT{1,3} ("/" DIGIT{1,3})?
IPV6 := (HEX_GROUP (":" HEX_GROUP){7} | (HEX_GROUP (":" HEX_GROUP)*)? "::" (HEX_GROUP (":" HEX_GROUP)*)?)
        ("/" DIGIT{1,3})?

DURATION_UNIT := "ns" | "us" | "µs" | "ms" | "d" | "h" | "m" | "s"
DURATION := DIGIT (DIGIT | "_")* ("." DIGIT (DIGIT | "_")*)? DURATION_UNIT
            (DIGIT (DIGIT | "_")* DURATION_UNIT)*

BYTE_UNIT := "PiB"i | "TiB"i | "GiB"i | "MiB"i | "KiB"i
           | "PB"i | "TB"i | "GB"i | "MB"i | "kB"i | "B"i
BYTES := DIGIT+ ("." DIGIT+)? BYTE_UNIT

INTEGER := ("+" | "-")? (
             "0x" HEX (HEX | "_")*
           | "0o" [0-7] ([0-7] | "_")*
           | "0b" [01] ([01] | "_")*
           | DIGIT (DIGIT | "_")*
           )
FLOAT := ("+" | "-")? DIGIT (DIGIT | "_")* (
           "." DIGIT (DIGIT | "_")* (("e" | "E") ("+" | "-")? DIGIT+)?
         | ("e" | "E") ("+" | "-")? DIGIT+
         )

TRIPLE_STRING := "\"\"\"" (!"\"\"\"" .)* "\"\"\""
DQ_STRING     := "\"" ("\\" . | !"\"" .)* "\""
SQ_STRING     := "'" (!"'" .)* "'"

RESOLVER_REF_RAW := @native("Cooper.Native.ResolverRef", "scan")

DIGIT, ALPHA, ALNUM, SPACE und HEX sind in jeder Aether-Grammatik vordefiniert. RESOLVER_REF_RAW ist das eine Token, das Elixir-Code statt eines Musters erkennt: Ein !{…}-Resolver-Verweis darf Klammern verschachteln, und das kann kein festes Token zählen.

Anweisungen

file := TRIVIA? version_header statement* TRIVIA?

version_header := "#" "@" kw:IDENT "="? version_number

version_head   := FLOAT | INTEGER
version_number := version_head ("." n:INTEGER)*

statement          := disabled_statement | conditional_statement | real_statement
disabled_statement := "#" real_statement

conditional_statement := env_guard real_statement
env_guard              := "${?" name:IDENT "}"

real_statement := var_decl | for_statement | import_statement | followed_delete
                | sigil_kv_statement | delete_statement

followed_delete := delete_statement &statement

var_decl := "@" private:"*"? name:IDENT "="? value

import_statement := import_kw:IDENT path:import_path
import_path := DQ_STRING | SQ_STRING

for_statement := for_kw:IDENT binding ("," binding)*
                  ((from_kw:IDENT template:key_path as_kw:IDENT dest:key_path)
                  | (as_kw:IDENT dest:key_path))
                  block

binding := "@" name:IDENT (in_kw:IDENT iterable:iterable_value)?

iterable_value := NIL_KW | TRUE_KW | FALSE_KW | INF_KW
                 | DATETIME | DATE | TIME | IPV6 | IPV4 | DURATION | BYTES
                 | FLOAT | INTEGER
                 | TRIPLE_STRING | DQ_STRING | SQ_STRING
                 | list | tuple
                 | at_ref | env_ref | config_ref | resolver_ref | tagged_ref

sigil_kv_statement := sigil:("~" | "+" | "-")? key_path "="? rhs
delete_statement    := "-" key_path

rhs := block | value

block := "{" (statement ","?)* "}"

key_path    := key_segment ("." key_segment)*
key_segment := secret:"*"? seg

seg := IDENT | DQ_STRING | SQ_STRING

Werte und Verweise

value := NIL_KW | TRUE_KW | FALSE_KW | INF_KW
       | DATETIME | DATE | TIME | IPV6 | IPV4 | DURATION | BYTES
       | FLOAT | INTEGER
       | TRIPLE_STRING | DQ_STRING | SQ_STRING
       | list | tuple
       | at_ref | env_ref | config_ref | resolver_ref | tagged_ref
       | atom_value

at_ref     := "@{" name:ref_name idx_suffix? ref_suffix? filters? "}"
env_ref    := "${" name:ref_name env_bracket? ref_suffix? filters? "}"
config_ref := "%{" path:key_path idx_suffix? ref_suffix? filters? "}"

ref_name   := IDENT | DQ_STRING

resolver_ref := raw:RESOLVER_REF_RAW
tagged_ref   := "!" name:IDENT "(" arg:value ")"

idx_suffix  := "[" INTEGER "]"
env_bracket := "[" INTEGER? "]"

ref_suffix := (":" "?" msg:DQ_STRING) | (":" "+" alt:value)
            | (":" signed_alt:signed_number) | (":" default:value)

signed_number := INTEGER | FLOAT | INF_KW

filters := ("|" filter)+
filter     := name:IDENT (":" arg:filter_arg)?
filter_arg := DQ_STRING | SQ_STRING

list  := "[" (element ","?)* "]"
tuple := "(" (element ","?)* ")"

element := block | value

atom_value := colon_atom | IDENT
colon_atom := ":" atom_word
atom_word  := IDENT | NIL_KW | TRUE_KW | FALSE_KW | INF_KW

Interpolation

Eine Zeichenkette in doppelten Anführungszeichen wird ein zweites Mal von einer eigenen Grammatik gelesen, priv/grammar/casc_interp.aether, die sie in Textstücke und eingebettete Verweise zerlegt. Jeder Verweis endet an seiner ersten schließenden Klammer.

casc_interp.aether
@grammar "casc_interp"
@root text
@noskip

AT_REF_RAW     := "@{" (!"}" .)* "}"
ENV_REF_RAW    := "${" (!"}" .)* "}"
CONFIG_REF_RAW := "%{" (!"}" .)* "}"

RESOLVER_REF_RAW := @native("Cooper.Native.ResolverRef", "scan")

TAG_NAME       := ALPHA (ALNUM | "_" | "+" | "-")* ("?" | "!")?
TAGGED_REF_RAW := "!" TAG_NAME "(" (!")" .)* ")"

BANG_TAG   := "!" TAG_NAME "("
TEXT_CHUNK := (!("@{" | "${" | "%{" | "!{" | BANG_TAG) .)+

text    := segment*
segment := AT_REF_RAW | ENV_REF_RAW | CONFIG_REF_RAW | RESOLVER_REF_RAW | TAGGED_REF_RAW | TEXT_CHUNK

Fehlerkategorien

Jeder Ladefehler gehört zu genau einer Kategorie, benannt nach der Stufe des Ladens, die das Dokument abgelehnt hat. Jede Implementierung meldet Fehler auf ihre Weise, muss die Kategorie aber angeben. Kategorien werden exakt verglichen; die Meldungstexte gehören nicht zur Spezifikation.

KategorieAusgelöst, wenn
lexersich der Text nicht in Tokens zerlegen lässt: eine nicht geschlossene Zeichenkette, ein Zeichen, mit dem kein Token beginnt
parserdie Tokens kein Dokument ergeben: eine fehlende oder fehlerhafte #@version-Zeile, eine Anweisung, für die die Grammatik keine Regel hat
actionein Literal wohlgeformt ist, aber nichts bedeutet: ein unmögliches Datum, eine Adresse außerhalb des Bereichs, eine fehlerhafte Zeitdauer oder Bytegröße
importein Import nicht gelesen werden kann: keine Datei, ein Zyklus, ein nicht registriertes Schema
mergesich zwei Werte nicht zusammenführen lassen: eine Listenoperation auf etwas, das keine Liste ist
loopein for nicht laufen kann: eine Quelle, die keine Liste ist, gebundene Listen ungleicher Länge, eine Schleife nur mit Index
resolveein Verweis keine Antwort findet: ein undefinierter Name ohne Default, ein Zyklus, ein nicht registrierter oder fehlschlagender Resolver oder Tag, ein interpolierter Wert ohne Textform
dotenvsich eine für ${…} gelesene .env-Datei nicht parsen lässt

Wo ein Dokument scheitert, sagt trotzdem jede Implementierung. Seit 0.7.0 meldet Cooper einen Parse-Fehler am weitesten Token, das der Parser erreicht hat, als unexpected X -- expected one of: …, und ein in der Stufe action abgelehntes Literal mit Zeile, Spalte und der Quellzeile darunter:

#@version = 1.0
name = "api"
allow = [10.0.0.1, 300.0.0.1]
3:20: invalid IP address: "300.0.0.1"
3 | allow = [10.0.0.1, 300.0.0.1]
  |                    ^

Wo die Implementierungen abweichen

An zwei Stellen ist die Spezifikation den Implementierungen voraus. Bis beide sich treffen, sind dies die portablen Entscheidungen:

SpezifikationDerzeitSchreibweise
Eigene Anweisungen gewinnen gegen ImportsStreng nach PositionImports zuerst
Ein \ am Zeilenende setzt einen nackten Wert fortNicht implementiert: ein Lexer-FehlerEine dreifach gequotete Zeichenkette

Vier frühere Abweichungen gibt es seit Cooper 0.5.0 nicht mehr: Kommentare dürfen vor der Kopfzeile stehen, key "value" ohne = ist eine Zuweisung, Zeitdauern nehmen _ als Trenner, und ein interpolierter Schlüssel funktioniert auch außerhalb einer Schleife. Eine fünfte verschwand mit node-cooper 0.2.2: Ein zugeordnetes Modul liest sich in einer Zeichenkette jetzt auch in Node.js als das, worauf es abgebildet ist, wie in Elixir und PHP.

Das Mischen von Listen nach Schlüssel (~items[id] = […]) ist in der Spezifikation vorgemerkt und bewusst noch nicht Teil der Sprache.