Location>code7788 >text

Building Command Line Tools: Implementing the -a and -l Options to the ls Command

Popularity:124 ℃/2024-11-10 22:32:04

In our day-to-day front-end development, we often rely on a variety of scaffolding tools to speed up project construction and maintenance, such as thecreate-react-app It is possible to initialize aReact Project.eslint Instead, they help us keep our code neat and consistent. And internally, we often build our own scaffolding tools such as custom React or Vue frameworks, code inspection tools for internal use, etc. to better meet the needs of a particular business. This post is to share with you how to use the Implement a simple command-line tool that mimics the commonly usedls command, including its-a cap (a poem)-l function of the parameter.

Overview of the ls command

First, let's quickly reviewls Some basic usage of the command.

  • ls: Lists all non-hidden files in the current directory.
  • ls -a: Lists all files, including hidden files starting with the dot (.) Hidden files starting with a dot (.) are also shown, along with the current directory (.) and the parent directory (...) .
  • ls -l: Lists file details in long format, including file type, permissions, number of links, etc.
  • ls -al maybels -a -l: Combines the functions of -a and -l to show detailed information about all files.

In a nutshell.-a parameter is used to display hidden files and the current and parent directories, while the-l parameter provides more detailed information about the file.

As shown below, when run in the initialized new React project directory, thels command, you will see the following:

ls -l file information in detail

When we add the -l parameter, the ls command outputs more information about the file:

1、Document type: Take the first character, d for directory, - for file, l for link.
2、User Operating Privileges: The next nine characters are divided into three groups, representing the read, write, and execute permissions of the file owner, the group to which it belongs, and other users, respectively.
3、Number of links to documents: The number of hard links to a file or directory. For normal files, this number is usually 1. For directories, this number is at least 2, since each directory contains two special directories . and .
4、Document owner: the user name of the owner of the file.
5、Group to which the document belongs: The name of the user group to which the file belongs.
6、file size: The size of the file in bytes.
7、Last modified: Indicates when the document was last modified, in the format of month, day, hour and minute.
8、filename: The name of the file or directory.

Initialization Project

Next, let's actually implement a similar tool. First, create a new project folderice-lsand runnpm init -y to generate Documentation.

Then, in the project root directory, create abin folder and add a folder named file. This file is the entry point for our command line tool, and the header of the file adds the#!/usr/bin/env node so that it can be implemented directly.

#!/usr/bin/env node
('hello nodejs')

This can be done by./bin/ command to test if this code is working properly, you will see the output of "hello nodejs".

To make our tools even easier to use, the Medium Configurationbin field so that it can be called by a short name.

bin: {
    "ice-ls": "./bin/"
}

In order to be able to debug locally, use thenpm link command links the project to the globalnode_modules directory, so that you can use it like any other global command.ice-ls

parameterization

One of the great things about command line tools is that they support multiple parameters to change behavior. In our example, we need to handle the-a cap (a poem)-l parameters. To do this, create a file for parsing command line arguments.

function parseArgv() {
  const argvList = (2); // Ignore the first two default parameters
  let isAll = false;
  let isList = false;

  ((item) => {
    if (("a")) {
      isAll = true;
    }
    if (("l")) {
      isList = true;
    }
  });

  return {
    isAll,
    isList,
  };
}

 = {
  parseArgv,
};

Next, we need to add thebin/ The file introduces theparseArgv function and adjusts the output of the file according to the parsing result.

/usr/bin/env node /usr/bin/env node
const fs = require("fs");
const { parseArgv } = require(". /parseArgv");

const dir = (); // Get the current working directory.
let files = (dir); // Read the contents of the directory.
let output = ""; // Read the contents of the directory.

const { isAll, isList } = parseArgv();

if (isAll) {
  files = ["." , "..."] .concat(files); // add . and ...
} else {
  files = ((item) => (".") ! == 0); // Filter out hidden files.
}

let total = 0; // Initialize the total number of filesystem blocks.
if (!isList) {
  ((file) => {
    output += `${file} `; }); {
  }); }
} else {
  ((file, index) => {

    output += file; if (index !
    if (index ! == - 1) {
      output += "\n"; // newline if not last element
    }
  });
}

if (!isList) {
  (output); }
} else {
  (`total ${total}`); (output); } else {
  (output); } else { (`total ${total}`); (output); }
}

The output is shown below:

Handling file types and permissions

Created at the same level as the file file to determine whether the file type is a directory, a file, or a link. We can determine whether a file is a directory, a file or a link by using thefs module gets the file status information, where themode attribute contains information about the file type and permissions. This is accomplished by linking to thefs The constant module determines the file type by bitwise concatenation.

file system modulefs There are a number of constants, of which the following three are commonly used in relation to file types:

  • S_IFDIR: used to check if a file is a directory, value 0o040000 (octal)
  • S_IFREG: Used to check if a file is a normal file, value 0o100000 (octal)
  • S_IFLNK: Used to check if a file is a symbolic link, value: 0o120000 (octal)
const fs = require("fs");
function getFileType(mode) {
  const S_IFDIR = .S_IFDIR;
  const S_IFREG = .S_IFREG;
  const S_IFLINK = .S_IFLINK;

  if (mode & S_IFDIR) return "d";
  if (mode & S_IFREG) return "-";
  if (mode & S_IFLINK) return "l";
  
  return '?'; // If it is not recognized,then return the question mark
}

 = {
  getFileType,
};

On Unix systems, file permissions are divided into three categories:

  • Owner: The owner of the file.
  • Group: the user group to which the file belongs.
  • Others: Users other than the owner and group.

Each category of permissions is subdivided into three:

  • Read permission (Read, r): allows reading the contents of a file or listing the contents of a directory.
  • Write permission (Write, w): Allows you to modify the contents of a file or delete or rename a file in a directory.
  • Execute permission (Execute, x): allows execution of files or access to directories.

where the nodejs variables associated with the above permissions are:

  • S_IRUSR: Indicates the read permission of the file owner (value: 0o400, decimal: 256)
  • S_IWUSR: Write permission for the file owner (value: 0o200, decimal: 128)
  • S_IXUSR: Execution rights of the owner of the file (value: 0o100, decimal: 64)
  • S_IRGRP: Read permission for the group to which the file belongs (value: 0o040, decimal: 32)
  • S_IWGRP: Write permission for the group to which the file belongs (value: 0o020, decimal: 16)
  • S_IXGRP: Execution rights of the group to which the file belongs (value: 0o010, decimal: 8)
  • S_IROTH: Read access for other users (value: 0o004, decimal: 4)
  • S_IWOTH: Write permission for other users (value: 0o002, decimal: 2)
  • S_IXOTH: Execution privileges for other users (value: 0o001, decimal: 1)

Create files at the same level to handle file permission information:

const fs = require("fs");
function getAuth(mode) {
  const S_IRUSR = mode & .S_IRUSR ? "r" : "-";
  const S_IWUSR = mode & .S_IWUSR ? "w" : "-";
  const S_IXUSR = mode & .S_IXUSR ? "x" : "-";

  const S_IRGRP = mode & .S_IRGRP ? "r" : "-";
  const S_IWGRP = mode & .S_IWGRP ? "w" : "-";
  const S_IXGRP = mode & .S_IXGRP ? "x" : "-";

  const S_IROTH = mode & .S_IROTH ? "r" : "-";
  const S_IWOTH = mode & .S_IWOTH ? "w" : "-";
  const S_IXOTH = mode & .S_IXOTH ? "x" : "-";

  return (
    S_IRUSR +
    S_IWUSR +
    S_IXUSR +
    S_IRGRP +
    S_IWGRP +
    S_IXGRP +
    S_IROTH +
    S_IWOTH +
    S_IXOTH
  );
}

 = {
  getAuth,
};

Introduce these two modules in the bin/ file and use them to enrich the output of file information.

const path = require("path");
const { getAuth } = require("./getAuth");
const { getFileType } = require("./getFileType");

((file, index) => {
  const filePath = (dir, file);
  const stat = (filePath);
  const { mode } = stat;

  // Get Access
  const type = getFileType(mode);
  const auth = getAuth(mode);

  // Get filename,Add space
  const fileName = ` ${file}`;

  output += `${type}${auth}${fileName}`;
  // Except for the last element,All need line breaks
  if (index !== - 1) {
    output += "\n";
  }
});

The output is shown below:

Handles number of file links, total number, file size

existLinux maybeUnix On the system, when viewing the details of a file or directory from the command line, the number following the permission string does not directly indicate the number of files. For example.bin There are only four files under the folder, but the number is shown as 6. In reality, the number represents the number of file links, i.e., how many hard links point to entries within the directory.

In addition.ls -l in the first line of output of the commandtotal value, does not refer to the total number of files, but to the total amount of file system blocks used. It reflects the sum of the number of disk blocks occupied by all files and their subdirectories in the current directory.

To make it easier to understand and work with this data, we can use the (used form a nominal expression)() method to get information about the state of the file.

const { mode, size } = stat;

// Get the number of file links
const count = ().padStart(3, " ");

// Get the file size
const fileSize = ().padStart(5, " "); // Get the file size.

// Get the total number of file system blocks
total += ;

output += `${type}${auth}${count}${fileName}`;

The output is shown below:

Get user information

establish file that handles user names and group names. While it's possible to get the name of the user and group directly from the file status (stat) object to get the user ID (uid) and group ID (gid), but some conversion work is required to convert these IDs to their corresponding names.

Getting the user name is relatively simple and can be done by executing the commandid -un <uid> to realize. Getting the group name, on the other hand, is slightly more complicated, as we need to first get the group name via theid -G <uid> command to get a list of all group IDs associated with the user, then use theid -Gn <uid> Get a list of the names of these groups. Finally, determine the group name by finding where gid is in the list of all group IDs.

As shown below, in my system, the uid is 502, the gid is 20, the user name is xingchen, and the group name is staff.

Code Implementation:

const { execSync } = require("child_process");
function getFileUser(stat) {
  const { uid, gid } = stat;
  // Get User Name
  const username = execSync("id -un " + uid)
    .toString()
    .trim();

  // Get a list of group names and their correspondences
  const groupIds = execSync("id -G " + uid)
    .toString()
    .trim()
    .split(" ");
  const groupIdsName = execSync("id -Gn " + uid)
    .toString()
    .trim()
    .split(" ");

  const index = ((id) => +id === +gid);
  const groupName = groupIdsName[index];

  return {
    username,
    groupName,
  };
}

 = {
  getFileUser,
};

In the main entry file of the project into the just-createdgetFileUser module and call it to get user information about the file.

const { getFileUser } = require("./getFileUser");

Tweak the output a bit more

// Get User Name
const { username, groupName } = getFileUser(stat);
const u = (9, " ");
const g = (7, " ");

output += `${type}${auth}${count}${u}${g}${fileSize}${fileName}`;

The final output is shown in the figure:

Get modification time

In order to better present the time portion of the file information, we need to convert the original numeric time to a more readable format. This involves converting the month from numeric to abbreviated (e.g., 1 to "Jan"), and making sure that fields such as the date, hours, and minutes are preceded by zeros when they are less than two digits.

First of all, we are in the The file defines an object to map the numbers of the month to their corresponding acronyms:

// Define the month correspondences
const monthObj = {
  1: "Jan",
  2: "Feb",
  3: "Mar",
  4: "Apr",
  5: "May",
  6: "Jun",
  7: "Jul".
  8: "Aug",
  9: "Sep",
  10: "Oct",
  11: "Nov",
  12: "Dec",
}.

 = {
  monthObj.
}; }

Next, create file, which is used to retrieve a file from the file status object (stat) in the extract and format the modification time:

function getFileTime(stat) {
  const { mtimeMs } = stat;
  const mTime = new Date(mtimeMs);
  const month = () + 1; // Get month,take note ofJavaScriptMid-month from0start counting
  const date = ();
  // not enough2Bit by bit in the first place0
  const hour = ().toString().padStart(2, 0);
  const minute = ().toString().padStart(2, 0);

  return {
    month,
    date,
    hour,
    minute,
  };
}

 = {
  getFileTime,
};

In the main file in which we introduce the above two modules and use them to process and format time data:

const { getFileTime } = require("./getFileTime");
const { monthObj } = require("./config");
// ...Other codes...

// Get creation time
const { month, date, hour, minute } = getFileTime(stat);
const m = monthObj[month].toString().padStart(4, " ");
const d = ().padStart(3, " ");
const t = ` ${hour}:${minute}`;

output += `${type}${auth}${count}${u}${g}${fileSize}${m}${d}${t}${fileName}`;

By following the above steps, we have successfully implemented the-l option of all the files displayed under the function, the realization of the effect is shown in the figure:

post

After completing the development of all the features, we can prepare to publish the project to thenpm repository so that others can use the tool as well. First, you need to remove the localnpm link, which ensures that the release is up-to-date and not affected by the local development environment. You can remove the local link by executing the following command:

npm unlink

After executing this command, try running againice-ls command, you will be prompted that the command cannot be found because the local link has been removed. Next, log in to thenpm account, use the following command to log in:

npm login

Once logged in, you can publish the package to thenpm Warehouse:

npm publish

The realization is shown below:

At this point, we've successfully implemented an analog of theLinux systematicls command-line tool that supports-a cap (a poem)-l option, you can list all files (including hidden files) in the current directory and detailed file information.

If you're interested in front-end engineering or want to learn more about it, feel free to check out my other articles, which will be continuously updated and hopefully bring you more inspiration and technology sharing.

Full Code

The following is the complete code for the other files that have been posted during the analysis above.

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const { parseArgv } = require("./parseArgv");
const { getAuth } = require("./getAuth");
const { getFileType } = require("./getFileType");
const { getFileUser } = require("./getFileUser");
const { getFileTime } = require("./getFileTime");
const { monthObj } = require("./config");

const dir = ();
let files = (dir);
let output = "";

const { isAll, isList } = parseArgv();

if (isAll) {
  files = [".", ".."].concat(files);
} else {
  files = ((item) => (".") !== 0);
}

let total = 0;
if (!isList) {
  ((file) => {
    output += `${file}       `;
  });
} else {
  ((file, index) => {
    const filePath = (dir, file);
    const stat = (filePath);
    const { mode, size } = stat;

    // Get Access
    const type = getFileType(mode);
    const auth = getAuth(mode);

    // Get the number of links to the file
    const count = ().padStart(3, " ");

    // Get User Name
    const { username, groupName } = getFileUser(stat);
    const u = (9, " ");
    const g = (7, " ");

    // Get file size
    const fileSize = ().padStart(5, " ");

    // Get creation time
    const { month, date, hour, minute } = getFileTime(stat);
    const m = monthObj[month].toString().padStart(4, " ");
    const d = ().padStart(3, " ");
    const t = ` ${hour}:${minute}`;

    // Get filename
    const fileName = ` ${file}`;

    total += ;
    output += `${type}${auth}${count}${u}${g}${fileSize}${m}${d}${t}${fileName}`;
    // Except for the last element,All need line breaks
    if (index !== - 1) {
      output += "\n";
    }
  });
}

if (!isList) {
  (output);
} else {
  (`total ${total}`);
  (output);
}