11

background,

github address: https://github.com/lulu-up/record-shell

Have you ever forgotten how to spell a shell command? Or too lazy to type a long list of commands? For example, mine mac notebook tachbar occasionally It will be 'stuck', then I will enter killall ControlStrip command to restart tachbar , you can see this command and really don't bother to type.

There is also a new react project I have to enter every time npx create-react-app 项目名 --template typescript , in the company's daily development, I am used to writing new requirements every time I write a separate clone project and create it To develop a new branch, you need to go to gitlab to copy the project address and then locally git clone xxxxxxxxxx 新的项目名 , in theory, these operations are really repeated.

First of all, this time I will take you to use node to make a record together shell a small plug-in for the command. Of course, there are similar plug-ins on the Internet, but this time I made the most simple and rude one. The version of , I use the cool version myself, and I also want to take the opportunity to review the command line related knowledge.

1. Demonstration of usage

Let's see if this 'library' is really convenient:

1: Install
 npm install record-shell -g

After installation, your global will have more rs command:

image.png

2: add
 rs add

The name is arbitrary, and it is even more comfortable to use Chinese. Here is a demonstration of entering simple commands:

image.png

3: view + use'
 rs ls

image.png

Commands are optional, here I will add a few more commands to demonstrate:

image.png

You can press the up and down keys to move the selection, and press Enter to execute the command:

image.png

Of course, you can also view the command details, just -a parameters:

 rs ls -a

image.png

4: remove
 rs rm

image.png

image.png

5: add command with variables

Of course, our commands are not always written in 'dead' mode. For example, the command echo 内容 > a.txt means that I want to write --- 内容 to 目标文件 :

image.png

6: Use variables

When using the command, it will guide us to fill in the variables, so just write Chinese when defining:

image.png

image.png

image.png

2. Initialize your own node project

Next, we will make this library from scratch. Considering that some novice students may not have done this kind of global node package, I will talk about it in detail here.

There is nothing to say about the initialization project, just name it:

 npm init

Retrofit package.json file:

 "bin": {
    "rs": "./bin/www"
  },

It is specified here in bin that when running the rs command, access "./bin/www" .

image.png

 #! /usr/bin/env node
require('../src/index.js')
  1. #! This symbol usually appears at the beginning of the first line of basic in Unix systems to indicate the interpreter for this script file.
  2. /usr/bin/env Because you may install node to a different directory, here directly tell the system to search in the PATH directory, so that it is compatible with different node installation path.
  3. node Needless to say, this is to find our node command.

3. Initialization command + global installation

Here's how to hang our commands globally, so that you can use the global rs command anywhere:

 // cd 我们的项目

npm install . -g

It is easier to understand here, it is equivalent to directly installing the project in the global, we usually install xxx -g is to pull the remote, this command is to pull the current directory.

At this point, you write console.log('全局执行') to the index.js file, and then execute it globally rs and see the following effect is successful:

image.png

Fourth, commander.js (node command line solution)

Install first and then talk:

 npm install commander

commander can help us process user commands in a very standardized way. For example, when the user enters rs ls -a on the command line, I can input the original node first. args , 拆解出ls-a , 然后再写一堆if ls it is followed by -a how to do it, but obviously this is not standardized and the code is difficult to maintain, commander is here to help us standardize these writing methods:

Put the following code in the index.js file:

 const fs = require("fs");
const path = require("path");
const program = require('commander');
const packagePath = path.join(__dirname, "../package.json")
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));


program.version(packageData.version)

program
    .command('ls [-type]')
    .description('description')
    .action((value) => {
        console.log('你输入的是:', value)
    })
program.parse(process.argv)

Enter at the command line:

 rs ls 123456

image.png

Explain the code sentence by sentence:

  1. const program = require('commander') This clearly introduces commander .
  2. program.version(packageData.version) Here is the version that defines the current , when you enter rs -V , it will display the program.version method to get the value of fabc1fe875a19 The version field in the package.json is used directly.
  3. program.command('ls') defines a parameter named ls , when we enter rs ls will trigger our subsequent processing method, the reason why I wrote it as ---5c7b9db406087e1724423a36f796ab8f---, I wrote program.command('ls [-type]') --Because adding program.command('ls [-type]') [-type] after commander will think ls command can be followed by other parameters, of course you can call [xxxxx] , so that users can understand it.
  4. .description('description') As the name suggests, here is the brief description, when we enter rs -h it will appear:
    image.png
  5. .action method is commander The processing function when the current command is detected, the first parameter is the parameter passed in by the user, and the second parameter is the Command object , we will pop up the selection list here later.
  6. process.argv You must first know here process is the node in the global variables of the command line, of which argv are all the bbb05c72e58 command line parameters when starting.
  7. program.parse(process.argv) It's easy to understand after reading the above, pass the command line parameters to commander to start execution.
sideshow

If you configure program.option('ls', 'ls的介绍') , it will appear when the user enters rs -h , but I feel that it is a little messy, our plugin is simple, so we didn't add it.

Five, inquirer.js (node command line interactive plug-in)

 npm install inquirer

inquirer can help us generate various command line question and answer functions, just like vue-cli the same effect, you can enter the following code to try the 'single selection mode':

 program
    .command('ls [-type]')
    .description('description')
    .action(async (value) => {
        const answer = await inquirer.prompt([{
            name: "key",
            type: "rawlist",
            message: "message1",
            choices: [
                {
                    name: 'name1',
                    value: 'value1'
                },
                {
                    name: 'name2',
                    value: 'value2'
                }
            ]
        }])
        console.log(answer)
    })

image.png

image.png

Explain the code sentence by sentence:

  1. First here is a pattern of async and awite .
  2. inquirer.prompt parameter is a 数组 , because it can operate continuously, such as performing two single-selection list operations.
  3. name就是最终的key , name xxxx 1 , 则最终返回结果就是{xxxx:1} .
  4. type Specify the interaction type rawlist single selection list, input input, checkbox multiple selection.
  5. message is the prompt, before we let the user choose, we must tell him what we are doing here.
  6. choices array of options, name option name, value option value.

6. Add command: add

Officially started to do the first command, I created a new folder named env , and created a record-list.json file inside the file with the command to store the user:
image.png

add command is nothing more than adding content to the record-list.json file:

 program
    .command('add')
    .description('添加命令')
    .action(async () => {
        const answer = await inquirer.prompt([{
            name: "name",
            type: "input",
            message: "命令名称:",
            validate: ((name) => {
                if (name !== '') return true
            })
        }, {
            name: "command",
            type: "input",
            message: "命令语句, 可采用[var]的形式传入变量:",
            validate: ((command) => {
                if (command !== '') return true
            })
        }])
          let shellList = getShellList();
          shellList = shellList.filter((item) => item.name !== answer.name);
          shellList.push({
             "name": answer.name,
             "command": answer.command
          })
          fs.writeFileSync(dataPath, JSON.stringify(shellList));
    })

Explain the code sentence by sentence:

  1. First we use commander to define the add command;
  2. When the add command is triggered, we use inquirer to define two input boxes, the first input command name, the second input command statement.
  3. validate defines the verification of the input parameters, note: the user does not input the value is not undefined but 空字符串 , so use !== '' , If the verification fails, the operation cannot be continued.
  4. After filling in, the user will add data to record-list.json , and replace if it is a command with the same name.

The name may be repeated, but it doesn't matter, because its usage scenario determines that it does not need to be too restrictive.

Seven, remove command: rm

The principle here is to pull record-list.json data to delete, and then update record-list.json :

 program
    .command('rm')
    .description('移除命令')
    .action(async () => {
        let shellList = getShellList();
        const choices = shellList.map((item) => ({
            key: item.name,
            name: item.name,
            value: item.name,
        }));
        const answer = await inquirer.prompt([{
            name: "names",
            type: "checkbox",
            message: `请'选择'要删除的记录`,
            choices,
            validate: ((_choices) => {
                if (_choices.length) return true
            })
        }])

        shellList = shellList.filter((item) => {
            return !answer.names.includes(item.name)
        })
        fs.writeFileSync(dataPath, JSON.stringify(shellList));
    })

Explain the code sentence by sentence:

  1. choices is to define a set of optional options.
  2. Use checkbox multiple selection mode, allowing users to delete multiple commands at once.
  3. validate Check that nothing is deleted, because the user may forget to click the selection (space bar).
  4. Use filter to filter out commands with the same name.
  5. Last update record-list.json file.

Eight, view + use: ls

There is a little more content here. After all, one command is responsible for two capabilities. The core principle here is to pull record-list.json the content of the file and display it as 单选列表 , and then execute the command according to the value selected by the user. Execute, and finally return the execution result;

1: View ls, support parameter-a
 program
    .command('ls')
    .alias('l')
    .description('命令列表')
    .option('-a detailed')
    .action(async (_, options) => {
        const shellList = getShellList();
        const choices = shellList.map(item => ({
            key: item.name,
            name: `${item.name}${options.detailed ? ': ' + item.command : ''}`,
            value: item.command
        }));

        if (choices.length === 0) {
            console.log(`
            您当前没有录入命令, 可使用'rs add' 进行添加
            `)
            return
        }

        const answer = await inquirer.prompt([{
            name: "key",
            type: "rawlist",
            message: "选择要执行的命令",
            choices
        }])
    })

Explain the code sentence by sentence:

  1. option('-a detailed')定义了可以-a参数, ls -a , 并且如果用户传-a返回值{detailed: true} .
  2. If there is -a then the command itself will be displayed in the name attribute.
  3. choices is converted record-list.json list of data in the file.
  4. If the record-list.json data is empty, the user is prompted to use rs add to add it.
  5. Use inquirer to generate a radio list.
2: Determine if there is a variable in the command statement

Since the command input by the user is allowed to contain variables, such as the one demonstrated earlier echo [内容] > [文件名] , then I need to determine whether there is a variable in the command selected by the current user:

 const optionsReg = /\[.*?\]/g;

function getShellOptions(command) {
    const arr = command.match(optionsReg) || [];
    if (arr.length) {
        return arr.map((message) => ({
            name: message,
            type: "input",
            message,
        }));
    } else {
        return []
    }
}

Explain the code sentence by sentence:

  1. optionsReg matches all variables of '[this way of writing]'.
  2. If a variable is matched, an array is returned, and the length of this array is the number of variables, because each variable must have an opportunity to enter.
  3. There is no special treatment for the duplicate name , and name will become the return value key , so it cannot be repeated, otherwise it will cause only Process the first variable.
3: no variable -> execute

Here is a new concept:

 const child_process = require('child_process');

child_process node , child_process.exec The method is to start a system shell to parse the parameters, so it can be very complex commands, including pipelines and redirection.

 child_process.exec(command, function (error, stdout) {
        console.log(`${stdout}`)
        if (error !== null) {
            console.log('error: ' + error);
        }
    });

Explain the code sentence by sentence:

  1. command is the command to execute.
  2. stdout The output of the execution command, such as ls is to output the file information in the current directory.
  3. error This is also very important. If an error is reported, the user should know the error message, so it is also console .
4: There are variables -> execute

The core principle is to replace the command statement after parsing the 'variable', and then execute it normally:

 function answerOptions2Command(command, answerMap) {
    for (let key in answerMap) {
        command = command.replace(`[${key}]`, answerMap[key])
    }
    return command;
}

function handleExec(command) {
    child_process.exec(command, function (error, stdout) {
        console.log(`${stdout}`)
        if (error !== null) {
            console.log('error: ' + error);
        }
    });
}

 if (shellOptions.length) {
        const answerMap = await inquirer.prompt(shellOptions)
        const command = answerOptions2Command(answer.key, answerMap)
        handleExec(command)
    } else {
        handleExec(answer.key)
    }

Explain the code sentence by sentence:

  1. inquirer execution, it will return a dictionary, such as {[文本]:"xxxxx", [文件名]:"a.txt"} , because we set name and message use the same name.
  2. answerOptions2Command loop execution replace replace variables.
  3. handleExec is responsible for executing the statement.

Nine, let the text change color (chalk)

The function is complete, but our prompt text is still 'black and white', of course we want to be more colorful in the command line, use in node :

 var red = "\033[31m red \033[0m";
console.log('你好红色:', red)

image.png

\033 is c语言 in 转义字符 will not be expanded here, anyway, it is to operate the screen when we see him, but we can see the above writing Very unfriendly, we must package it, chalk.js is a good existing wheel, we will install it:

 npm install chalk

use:

 const chalk = require('chalk') 

chalk.red('你好: 红色')

You were happy too early, now there is something wrong!!

image.png

Other tutorials don't say how to solve it, in fact, you just need to chalk the version of ---657514b4d0693be6140348a5911ac23e--- to 4 and it will be ok!

end

That's it this time, hope to progress with you.


lulu_up
5.7k 声望6.9k 粉丝

自信自律, 终身学习, 创业者