后台,github地址:https://github.com/lulu-up/record-shell你体验过吗?忘记了如何拼写某个shell命令?或者懒得输入一长串命令的经历?比如我的mac笔记本的tachbar偶尔‘卡死’,那我得输入killallControlStrip命令重启tachbar,你也看到了这个命令,真是懒得打了。还有一个新的反应项目。每次都要输入npxcreate-react-app项目名--templatetypescript。在公司日常开发中,我习惯每次写一个新的需求,就单独clone项目,新建一个分支进行开发。有时候需要去gitlab复制项目地址,然后在本地gitclonexxxxxxxxxx新项目名。从理论上讲,这些操作确实是重复的。首先,这次我会带大家使用node做一个记录shell命令的小插件。当然网上也有类似的插件,不过这次我做了最简单粗暴的版本,自己用起来也爽,也想借此机会温习一下命令行的知识。一、使用演示看看这个‘库’是不是真的方便:1:安装npminstallrecord-shell-g安装后,全局会多出rs命令:2:添加rsadd名字可选,甚至都是中文比较舒服,这里有一个输入简单命令的演示:3:view+use'rsls命令是可选的,这里我再补充几个命令来演示:可以按上下键移动选择,按输入执行命令:当然也可以查看命令详情,只需要使用-a参数即可:rsls-a4:removersrm5:addcommandwithvariables当然我们的命令也不会全部写成'dead'模式,比如命令echocontent>a.txt,这里表示我要将内容写入目标文件:6:使用变量使用命令时,会引导我们填写变量,这样就可以了定义的时候写中文:2.初始化自己的节点工程接下来,我们就从我从零开始做这个库开始吧。考虑到有些新手同学可能没有做过这种全局节点包,这里就详细说一下。初始化项目没什么好说的,随便起个名字:npminit改造package.json文件:"bin":{"rs":"./bin/www"},这个是在bin中指定的,当运行rs命令,访问“./bin/www”。#!/usr/bin/envnoderequire('../src/index.js')#!在Unix系统的基础知识中,这个符号通常出现在第一行的开头,用来表示这个脚本文件的解释器。/usr/bin/env因为可能会把node安装在不同的目录下,这里直接告诉系统去PATH目录下搜索,这样就可以兼容不同的node安装路径了。node不用说了,就是找我们的node命令。3.初始化命令+全局安装这里我们讲一下如何全局挂我们的命令,这样就可以在任何地方使用全局rs命令://cdourprojectnpminstall。-g这里比较容易理解,相当于直接全局安装项目。我们一般安装xxx-g从远端拉取,这个命令拉取的是当前目录。这时候你把console.log('全局执行')写入index.js文件,然后全局执行rs,看到如下效果,说明成功:4.Commander.js(node命令行解决方案)先安装再聊天:npminstallcommandercommander可以帮助我们非常规范的处理用户命令。比如用户在命令行输入rsls-a,我可以先把inputargs在nativenode的情况下反汇编,再反汇编ls和-a,然后写一堆if判断怎么做如果是ls,后面有-a,但是显然这个不规范,代码很难维护。Commander就是来帮助我们规范这些写法的:把下面的代码放到index.js文件中:constfs=require("fs");constpath=require("路径");constprogram=require('指挥官');constpackagePath=path.join(__dirname,"../package.json")constpackageData=JSON.parse(fs.readFileSync(packagePath,'utf-8'));program.version(packageData.version)program.command('ls[-type]').description('description').action((value)=>{console.log('Youentered:',value)})program.parse(process.argv)进入命令行:rsls123456逐句解释代码:constprogram=require('commander')Commander显然这里引入了。program.version(packageData.version)这里定义了当前库的版本。当你输入rs-V时,会显示program.version方法得到的值。这里直接使用了package.json中的version字段。program.command('ls')定义了一个名为ls的参数。当我们输入rsls时,就会触发我们后续的处理方法。之所以写program.command('ls[-type]')是因为加了[-type]后,指挥官会认为ls命令后面可以跟其他参数。当然你也可以叫它[xxxxx],方便用户理解。.description('description')顾名思义,这里是简要说明。当我们输入rs-h时,会出现:.actionmethod是指挥官检测到当前命令被触发时的处理函数。第一个参数是用户传入的参数。第二个参数是Command对象,后面我们会在这里弹出选择列表。对于process.argv,首先要知道process是node中的一个全局变量,其中argv是启动命令行时的所有参数。program.parse(process.argv)看了上面就很容易理解了,将命令行参数传递给commander开始执行。ExtraStory如果配置program.option('ls','lsintroduction'),用户输入rs-h时就会出现,但是我觉得加起来有点乱。我们的插件追求简单,所以没有添加。5.inquirer.js(node命令行交互插件)npminstallinquirerinquirer可以帮我们生成各种命令行问答功能,就像vue-cli的效果一样,可以输入下面的代码试试'单选模式':program.command('ls[-type]').description('description').action(async(value)=>{constanswer=awaitinquirer.prompt([{name:"key",类型:“rawlist”,消息:“message1”,选择:[{name:'name1',value:'value1'},{name:'name2',value:'value2'}]}])console.log(答案)})by一句话解释代码:首先,这里是一个async和awite模式。inquirer.prompt参数是一个数组,因为它可以连续操作,比如进行两次单选列表操作。名称是最终的关键。例如xxxx用户name为1,则最终返回结果为{xxxx:1}。type指定交互类型rawlist单选列表、input输入、checkbox多选列表等,message为提示。在让用户选择之前,我们必须告诉他他在这里做什么。choices选项数组,name选项名称,value选项值。6、添加命令:add正式开始制作第一个命令,我新建了一个文件夹env,在里面新建了一个record-list.json文件,用来存放用户命令:add命令无非就是添加record-list。在文件中添加内容:program.command('add').description('addcommand').action(async()=>{constanswer=awaitinquirer.prompt([{name:"name",type:“输入”,消息:“命令名称:”,验证:((名称)=>{如果(名称!=='')返回真})},{名称:“命令”,类型:“输入”,消息:"命令语句,变量可以以[var]:的形式传入",validate:((command)=>{if(command!=='')returntrue})}])letshellList=getShellList();shellList=shellList.filter((item)=>item.name!==answer.name);shellList.push({"name":answer.name,"command":answer.c命令})fs.writeFileSync(dataPath,JSON.stringify(shellList));})逐句解释代码:首先,我们使用commander来定义add命令;当触发添加命令时,我们使用查询器定义两个输入框。一是输入命令名,二是输入命令语句validate定义输入参数的校验,注意:用户输入的不是undefined而是空字符串的值,所以使用!=='',如果校验失败则无法继续。用户填写后,在record-list.json中添加数据,如果是同名命令则替换。名字可能重复,但没关系,因为它的使用场景决定了它不需要太过拘束。7、删除命令:rm这里的原理是拉取record-list.json数据删除,然后更新record-list.json:program.command('rm').description('removecommand').action(async()=>{letshellList=getShellList();constchoices=shellList.map((item)=>({key:item.name,name:item.name,value:item.name,}));constanswer=awaitinquirer.prompt([{name:"names",type:"checkbox",message:`Please'select'therecordtobedeleted`,choices,validate:((_choices)=>{if(_choices.length)返回真})}])shellList=shellList.filter((item)=>{return!answer.names.includes(item.name)})fs.writeFileSync(dataPath,JSON.stringify(shellList));})逐句解释代码:choices定义了一组选项。使用复选框多选模式允许用户一次删除多个命令。validate检查不删除任何东西的情况,因为用户可能忘记点击选择(空格键)。使用filter过滤掉同名的命令。最后更新record-list.json文件。8.查看+使用:ls这里的内容有点多,毕竟一个命令负责两个能力,这里的核心原理是拉取record-list.json文件的内容,并显示为radiolist,然后根据用户选择的值执行命令Execute,最后返回执行结果;1:查看ls,支持传参-aprogram.command('ls').alias('l').description('commandlist').option('-adetailed')。action(async(_,options)=>{constshellList=getShellList();constchoices=shellList.map(item=>({key:item.name,name:`${item.name}${options.detailed?':'+item.command:''}`,value:item.command}));if(choices.length===0){console.log(`你目前没有输入命令,你可以使用'rsadd'Add`)return}constanswer=awaitinquirer.prompt([{name:"key",type:"rawlist",message:"Selectcommandtoexecute",choices}])})解释代码语句为一句:option('-adetailed')定义了可以接收-a参数,比如ls-a,如果用户通过-a,就会得到返回值{detailed:true}。如果有-a,则命令本身显示在名称属性中。choices是record-list.json文件中的数据转换后的列表数据。如果record-list.json数据为空,提示用户使用rsadd添加。使用查询器生成单选列表。2:判断命令语句中是否存在变量既然用户输入的命令是允许包含变量的,比如前面演示的echo[内容]>[文件名],那么我需要判断命令语句中是否存在变量用户当前选择的命令:constoptionsReg=/\[.*?\]/g;functiongetShellOptions(command){constarr=command.match(optionsReg)||[];if(arr.length){returnarr.map((message)=>({name:message,type:"input",message,}));}else{return[]}}‖‖‖逐句解释代码:optionsReg匹配'[这种写法]'的所有变量。如果匹配到一个变量,则返回一个数组,这个数组的长度就是变量的个数,因为每个变量都有机会被输入。对于重复的名字没有特殊处理,名字会成为返回值的key,所以名字不能重复。如果名称重复,则只处理第一个变量。3:Novariable->execute这里有一个新的概念:constchild_process=require('child_process');child_process可以生成节点的'子进程',child_process.exec方法是启动一个系统shell来解析参数,所以它可以是非常复杂的命令,包括管道和重定向。child_process.exec(command,function(error,stdout){console.log(`${stdout}`)if(error!==null){console.log('error:'+error);}});通过语句解释代码:command是要执行的命令。stdout执行命令的输出,例如ls输出当前目录下的文件信息。错误在这里也很重要。如果报错,用户应该知道错误信息,所以也是console。4:有变量->Execute核心原理是解析'变量'后替换command语句,然后正常执行:functionanswerOptions2Command(command,answerMap){for(letkeyinanswerMap){replace(`[${key}]`,answerMap[key])}returncommand;}functionhandleExec(command){child_process.exec(command,function(error,stdout){console.log(`${stdout}`)if(error!==null){console.log('error:'+error);}});}if(shellOptions.length){constanswerMap=awaitinquirer.prompt(shellOptions)constcommand=answerOptions2Command(answer.:"a.txt"},因为我们将名称设置为使用与消息相同的名称。answerOptions2Command在循环中执行replace以替换变量。handleExec负责执行语句。9.让文字变色(粉笔)功能完成,但是我们的提示文字还是'黑白色',我们当然希望命令行能多点色彩,在node中使用:varred="\033[31mred\033[0m";console.log('Hellored:',red)\033是c语言中的转义符,这里就不展开了。反正我们看到就需要对屏幕进行操作,但是可以看到上面的写法很不友好,必须要封装。chalk.js是一个很好的现有轮子。让我们安装它:npminstallchalk使用:constchalk=require('chalk')chalk.red('YouGood:red')你太高兴了,现在有问题了!!其他教程没说怎么解决,其实只要把chalk版本降到4就可以了!end?这次就是这样,希望大家一起进步。
