
مساء الخير جميعاً.
حدثت مشكلة في كتابة CLI الغامرة على node.js. سبق استخدام vorpal لهذا الغرض. هذه المرة كنت أرغب في الاستغناء عن التبعيات غير الضرورية ، بالإضافة إلى ذلك ، فكرت في إمكانية أخذ حجج القيادة بشكل مختلف.
باستخدام vorpal ، تم كتابة الأوامر على النحو التالي:
setValue -s 1 -v 0
موافق ، الكتابة -s
كل مرة ليست مريحة للغاية.
في النهاية تحول الفريق إلى ما يلي:
set 1: 0
كيف يمكن تنفيذها - تحت القطع
- أيضًا ، المكافأة الجيدة هي نقل العديد من الوسائط في شكل قائمة من القيم ، مفصولة بمسافة وفي شكل صفيف.
إدخال النص
أستخدم readline
لإدخال نص. بالطريقة التالية ، ننشئ واجهة مع دعم للإكمال التلقائي:
let commandlist = []; commandlist.push("set", "get", "stored", "read", "description"); commandlist.push("watch", "unwatch"); commandlist.push("getbyte", "getitem", "progmode"); commandlist.push("ping", "state", "reset", "help"); function completer(line) { const hits = commandlist.filter(c => c.startsWith(line)); // show all completions if none found return [hits.length ? hits : commandlist, line]; } /// init repl const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "bobaos> ", completer: completer }); const console_out = msg => { process.stdout.clearLine(); process.stdout.cursorTo(0); console.log(msg); rl.prompt(true); };
يعمل console.log
كما هو متوقع ، أي يعرض النص على السطر الحالي ويلف السطر ، وإذا تم تشغيله بواسطة أي حدث خارجي مستقل عن إدخال النص ، فسيتم عرض البيانات على سطر الإدخال. لذلك ، نستخدم وظيفة console_out ، والتي ، بعد الإخراج إلى وحدة التحكم ، تستدعي خط إدخال خط القراءة.
محلل
يبدو أنه يمكنك تقسيم السلسلة إلى مسافات ، وفصل الأجزاء الفردية ومعالجتها. ولكن بعد ذلك سيكون من المستحيل تمرير معلمات سلسلة تحتوي على مسافة ؛ وعلى أي حال ، سيكون من الضروري إزالة المسافات وعلامات التبويب الإضافية.
في البداية ، كان يخطط لتطبيق المحلل اللغوي نفسه ، وإعادة كتابة المحلل التنازلي التنازلي من كتاب Herbert Schildt على لغة C إلى JS. أثناء التنفيذ ، تقرر تبسيط المحلل اللغوي ، ولكن في النهاية لم يكن من الممكن تنفيذه ، لأنه في عملية الكتابة ، وجدت حزمة ebnf ، وبعد أن أصبحت مهتمًا ومعروفًا بنظم تعريف بناء الجملة BNF / EBNF ، قررت استخدامها في تطبيقي.
قواعد
نحن نصف الأوامر والوسيطات في ملف القواعد.
بادئ ذي بدء ، حدد ما يلي:
- يتكون التعبير من سطر واحد. لسنا بحاجة إلى معالجة أكثر من سطرين.
- في بداية التعبير هو معرف الأمر. مزيد من الحجج.
- هناك عدد محدود من الأوامر ، لذلك يتم كتابة كل منها في ملف القواعد.
نقطة الدخول هي كما يلي:
command ::= (set|get|stored|read|description|getbyte|watch|unwatch|ping|state|reset|getitem|progmode|help) WS*
WS * تعني المسافات البيضاء - مسافة أو أحرف جدولة. يوصف على النحو التالي:
WS ::= [#x20#x09#x0A#x0D]+
مما يعني مساحة الحرف أو علامة التبويب أو فاصل الأسطر ، التي تحدث مرة واحدة أو أكثر.
دعنا ننتقل إلى الفرق.
أبسط ، بدون حجج:
ping ::= "ping" WS* state ::= "state" WS* reset ::= "reset" WS* help ::= "help" WS*
علاوة على ذلك ، الأوامر التي تأخذ قائمة بالأرقام الطبيعية مفصولة بمسافة أو صفيف.
BEGIN_ARRAY ::= WS* #x5B WS* END_ARRAY ::= WS* #x5D WS* COMMA ::= WS* #x2C WS* uint ::= [0-9]* UIntArray ::= BEGIN_ARRAY (uint WS* (COMMA uint)*) END_ARRAY UIntList ::= (uint WS*)* get ::= "get" WS* ( UIntList | UIntArray )
وبالتالي ، فإن الأمثلة التالية صحيحة للأمر get:
get 1 get 1 2 3 5 get [1, 2, 3, 5, 10]
بعد ذلك ، الأمر set ، الذي يأخذ معرّف زوج الإدخال: value أو صفيفًا من القيم.
COLON ::= WS* ":" WS* Number ::= "-"? ("0" | [1-9] [0-9]*) ("." [0-9]+)? (("e" | "E") ( "-" | "+" )? ("0" | [1-9] [0-9]*))? String ::= '"' [^"]* '"' | "'" [^']* "'" Null ::= "null" Bool ::= "true" | "false" Value ::= Number | String | Null | Bool DatapointValue ::= uint COLON Value DatapointValueArray ::= BEGIN_ARRAY (DatapointValue WS* (COMMA DatapointValue)*)? END_ARRAY set ::= "set" WS* ( DatapointValue | DatapointValueArray )
وبالتالي ، بالنسبة للأمر set ، تكون نماذج الترميز التالية صحيحة:
set 1: true set 2: 255 set 3: 21.42 set [1: false, 999: "hello, friend"]
عملية في شبيبة
نقرأ الملف ، وننشئ كائن المحلل.
const grammar = fs.readFileSync(`${__dirname}/grammar`, "utf8"); const parser = new Grammars.W3C.Parser(grammar);
علاوة على ذلك ، عند إدخال البيانات ، يشير مثيل كائن readline إلى حدث الخط ، الذي تتم معالجته بواسطة الوظيفة التالية:
let parseCmd = line => { let res = parser.getAST(line.trim()); if (res.type === "command") { let cmdObject = res.children[0]; return processCmd(cmdObject); } };
إذا تمت كتابة الأمر بشكل صحيح ، فإن المحلل اللغوي يقوم بإرجاع شجرة ، حيث يكون لكل عنصر نوع وحقل تابع وحقل نص. يأخذ حقل النوع قيمة النوع للعنصر الحالي. على سبيل المثال إذا مررنا الأمر ping إلى المحلل اللغوي ، ستبدو الشجرة بمثابة أثر. الطريقة:
{ "type": "command", "text": "ping", "children": [{ "type": "ping", "text": "ping", "children": [] }] }
نكتب في الشكل:
command ping Text = "ping"
لأمر "الحصول على 1 2 3" ،
command get UIntList uint Text = "1" uint Text = "2" uint Text = "3"
بعد ذلك ، نقوم بمعالجة كل أمر ، ونقوم بالإجراءات اللازمة ونعرض النتيجة في وحدة التحكم.
والنتيجة هي واجهة مريحة للغاية تسرع العمل مع الحد الأدنى من التبعيات. سأشرح:
في الواجهة الرسومية (ETS) لقراءة عناوين المجموعة (على سبيل المثال) ، تحتاج إلى إدخال عنوان مجموعة واحد في حقل الإدخال ، ثم استخدم زر الماوس (أو عدة علامات تبويب) لإرسال طلب.
في الواجهة المنفذة من خلال vorpal ، يكون الأمر كما يلي:
readValue -s 1
أو:
readValues -s "1, 3"
باستخدام المحلل اللغوي ، يمكنك تجنب عناصر "-s" وعلامات الاقتباس غير الضرورية.
read 1 3
الروابط
- https://github.com/bobaoskit/bobaos.tool - مستودع المشاريع. يمكنك إلقاء نظرة على الرمز.
- http://menduz.com/ebnf-highlighter/ - يمكنك تعديل القواعد اللغوية وفحصها أثناء الطيران.