كتابة CLI على NodeJS


مساء الخير جميعاً.


حدثت مشكلة في كتابة CLI الغامرة على node.js. سبق استخدام vorpal لهذا الغرض. هذه المرة كنت أرغب في الاستغناء عن التبعيات غير الضرورية ، بالإضافة إلى ذلك ، فكرت في إمكانية أخذ حجج القيادة بشكل مختلف.


باستخدام vorpal ، تم كتابة الأوامر على النحو التالي:


setValue -s 1 -v 0 

موافق ، الكتابة -s كل مرة ليست مريحة للغاية.


في النهاية تحول الفريق إلى ما يلي:


 set 1: 0 

كيف يمكن تنفيذها - تحت القطع


  1. أيضًا ، المكافأة الجيدة هي نقل العديد من الوسائط في شكل قائمة من القيم ، مفصولة بمسافة وفي شكل صفيف.

إدخال النص


أستخدم 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 ، قررت استخدامها في تطبيقي.


قواعد


نحن نصف الأوامر والوسيطات في ملف القواعد.
بادئ ذي بدء ، حدد ما يلي:


  1. يتكون التعبير من سطر واحد. لسنا بحاجة إلى معالجة أكثر من سطرين.
  2. في بداية التعبير هو معرف الأمر. مزيد من الحجج.
  3. هناك عدد محدود من الأوامر ، لذلك يتم كتابة كل منها في ملف القواعد.

نقطة الدخول هي كما يلي:


 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* /* [ left square bracket */ END_ARRAY ::= WS* #x5D WS* /* ] right square bracket */ COMMA ::= WS* #x2C WS* /* , comma */ 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 

الروابط


  1. https://github.com/bobaoskit/bobaos.tool - مستودع المشاريع. يمكنك إلقاء نظرة على الرمز.
  2. http://menduz.com/ebnf-highlighter/ - يمكنك تعديل القواعد اللغوية وفحصها أثناء الطيران.

Source: https://habr.com/ru/post/ar426615/


All Articles