当前位置: 首页 > 科技观察

用Nodejs写Bash脚本的终极解决方案!

时间:2023-03-17 14:50:53 科技观察

前言最近在学习bash脚本语法,但是如果不熟悉bash语法,很容易出错,比如:显示未定义的变量shell中的变量没有定义,还是可以用的,但它的结果可能不是你所期望的。例如:#!/bin/bash#这里是判断变量var是否等于字符串abc,但是没有声明变量varif["$var"="abc"]then#如果if判断为真,打印"notabc"ontheconsoleecho"notabc"else#如果if判断为false,则在控制台打印"abc"。为了弥补这些错误,我们学会了添加:在脚本开头设置\-u。这个命令的意思是脚本把它加到头部,遇到不存在的变量就会报错,停止执行。再运行会提示:test.sh:3:test.sh:num:parameternotset再来想象一下,你本来想删除的:rm\-rf$dir/*那么当dir为空的时候,what变成?rm\-rf是一个删除命令。如果$dir为空,相当于执行rm\-rf/*,也就是删除所有的文件和文件夹。..然后,你的系统就没了。这就是传说中的删库跑路吗~~~~如果是node或者浏览器环境,我们直接var==='abc'肯定会报错,也就是说很多javascript编程经验不能在bash中重用。如果能重复使用就太好了。后来,我开始摸索。如果我使用节点脚本而不是bash,那就太好了。折腾了一天,渐渐发现了一个神器,谷歌的zx库。放心,我不会介绍这个库。我们来看看目前主流的node.js使用情况。写个bash脚本,你就知道为什么是神器了。node执行bash脚本:勉强解决方案:child_processAPI如exec命令在child_processAPIconst{exec}=require("child_process");exec("ls-la",(error,stdout,stderr)=>{if(error){console.log(`error:${error.message}`);return;}if(stderr){console.log(`stderr:${stderr}`);return;}console.log(`stdout:${标准输出}`);});复制代码这里需要注意的是,首先exec是异步的,但是我们的很多bash脚本命令都是同步的。并注意:错误对象与stderr不同。当child_process模块??无法执行命令时,error对象不为空。比如找一个文件,找不到文件,error对象不为null。但是,如果命令成功运行并将消息写入标准错误流,则此stderr对象不会为空。当然我们可以使用同步exec命令,execSync//从child_process模块??引入exec命令const{execSync}=require("child_process");//同步创建一个hello文件夹execSync("mkdirhello");复制代码很容易引入child_process的其他可以执行bash命令的API。spawn:启动子进程执行命令exec:启动子进程执行命令。与spawn不同,它有一个回调函数,可以知道子进程的状态execFile:start子进程执行可执行文件fork:与spawn类似,不同的是需要指定子进程需要的javascript文件执行。exec和ececFile的区别是exec适合执行命令,eexecFile适合执行文件。Node执行bash脚本:高级解决方案shelljsconstshell=require('shelljs');#删除文件命令shell.rm('-rf','out/Release');//复制文件命令shell.cp('-R','stuff/','out/Release');#切换到lib目录,列出目录到.js结尾的文件,替换文件内容(sed-i是替换文本命令)shell.cd('lib');shell.ls('*.js').forEach(function(file){shell.sed('-i','BUILD_VERSION','v0.1.2',file);shell.sed('-i',/^.*REMOVE_THIS_LINE.*$/,'',file);shell.sed('-i',/.*REPLACE_LINE_WITH_MACRO.*\n/,shell.cat('macro.js'),file);});shell.cd('..');#除非另有说明,否则同步执行给定的命令。在同步模式下,这将返回一个ShellString#(与ShellJSv0.6.x兼容,它返回一个{code:...,stdout:...,stderr:...}形式的对象)。#否则,这将返回子进程对象,并且回调接收参数(代码、标准输出、标准错误)。if(shell.exec('gitcommit-am"Auto-commit"').code!==0){shell.echo('Error:Gitcommitfailed');shell.exit(1);}从上面复制代码code看来shelljs真的是一个非常好的写bash脚本的nodejs解决方案。如果你的node环境不能随便升级,我觉得shelljs真的够用了。然后再来看今天的主角zx,开局已经是17.4k了。zx库官网:www.npmjs.com/package/zx[1]先来看看如何使用吧#!/usr/bin/envzxawait$`catpackage.json|grepname`letbranch=await$`gitbranch--show-current`await$`depdeploy--branch=${branch}`awaitPromise.all([$`sleep1;echo1`,$`sleep2;echo2`,$`sleep3;echo3`,])letname='foobar'await$`mkdir/tmp/${name}复制代码你们怎么看?它只是编写linux命令吗?bash的语法可以忽略很多,直接上js就行了,它的优点还不止这些,有些功能还是挺有意思的1.支持ts,自动编译.ts到.mjs文件,.mjs文件就是结尾支持高版本node自带的es6模块的文件,即这个文件可以直接导入模块,不需要其他工具转义。2.自转义Pipe方法,支持管道操作3.自带fetch库,可以进行网络请求,自带chalk库,可以打印彩色字体,自带错误处理nothrow方法,如果bash命令出错,可以用这个方法包起来忽略错误CompleteChinesedocument(翻译水平一般,请见谅)#!/usr/bin/envzxawait$`catpackage.json|grepname`letbranch=await$`gitbranch--show-current`await$`depdeploy--branch=${branch}`awaitPromise.all([$`sleep1;echo1`,$`sleep2;echo2`,$`sleep3;echo3`,])letname='foobar'await$`mkdir/tmp/${name}复制代码Bash很好,但是在编写脚本时,人们通常会选择更方便的编程语言。JavaScript非常适合,但标准的Node.js库需要一些额外的东西才能使用。zx基于child_process,转义参数并提供合理的默认值。安装npmi-gzx所需环境复制代码Node.js>=14.8.0将编写脚本的代码复制到扩展名为.mjs的文件中,这样顶层就可以使用await了。将以下shebang添加到zx脚本的开头:#!/usr/bin/envzx现在您将能够像这样运行脚本:chmod+x./script.mjs./script.mjs复制代码或通过zx可执行文件:zx./script.mjs复制代码所有函数($、cd、fetch等)都可以直接使用,无需任何导入。$`command`使用child_process包中的spawn函数执行给定的字符串并返回ProcessPromise.letcount=parseInt(await$`ls-1|wc-l`)console.log(`Filescount:${count}`)复制代码例如并行上传文件:如果执行的程序返回非零退出码,ProcessOutput将被抛出try{await$`exit1`}catch(p){console.log(`Exitcode:${p.exitCode}`)console.log(`Error:${p.stderr}`)}复制代码ProcessPromise,下面是promisetypescript的接口定义:Promisepipe(dest):ProcessPromise}pipe()方法可用于重定向标准输出:await$`catfile.txt`.pipe(process.stdout)在github上阅读有关管道的更多信息。com/google/zx/b...[2]ProcessOutput类的Typescript接口定义ProcessOutput{readonlystdout:stringreadonlystderr:stringreadonlyexitCode:numbertoString():string}复制代码函数:cd()改变当前工作目录cd('/tmp')await$`pwd`//outputs/tmp复制代码获取()节点获取包。letresp=awaitfetch('http://wttr.in')if(resp.ok){console.log(awaitresp.text())}复制代码question()readlinepackageletbear=awaitquestion('Whatkindofbearisbest?')lettoken=awaitquestion('Chooseenvvariable:',{choices:Object.keys(process.env)})复制代码在第二个参数中,您可以指定用于自动完成选项卡的选项数组。下面是接口定义functionquestion(query?:string,options?:QuestionOptions):PromisetypeQuestionOptions={choices:string[]}复制代码sleep()基于setTimeout函数awaitsleep(1000)复制代码nothrow()改变$的行为,如果exitcode不为0,不运行Exception.ts接口定义函数nothrow

(p:P):P管道中复制代码awaitnothrow($`grepsomethingfrom-file`)//:await$`find./examples-typef-print0`.pipe(nothrow($`xargs-0grepsomething`)).pipe($`wc-l`)复制代码下面的包,不用导入,直接使用chalkconsole.log(chalk.blue('Helloworld!'))复制代码fs类似如下用法import{promisesasfs}from'fs'letcontent=awaitfs.readFile('./package.json')复制代码osawait$`cd${os.homedir()}&&mkdirexample`复制代码配置:$.shell指定使用bash.$.shell='/usr/bin/bash'复制代码$.quote指定用于转义特殊函数命令替换期间的字符u默认使用shq包。注意:两个变量__filename&__dirname在commonjs中,我们使用以.mjs结尾的es6模块。在ESM模块中,Node.js不提供__filename和__dirname全局变量。由于这样的全局变量在脚本中非常方便,zx提供这些用于.mjs文件(当使用zx可执行文件时)require也是commonjs中的导入模块方法,在ESM模块中,没有require()函数。zx提供了一个require()函数,因此它可以与.mjs文件中的导入一起使用(当使用zx可执行文件时)传递环境变量process.env.FOO='bar'await$`echo$FOO`传递一个数组如果一个值数组作为参数传递给$,数组的项将被单独转义并通过空格连接以使用$和其他函数#!/usr/bin/envnodeimport{$}from'zx'await$`date`复制代码zx可以将.ts脚本编译成.mjs并执行zxexamples/typescript.ts复制代码