Task Manager CLI in Node.js with fs and path

We made a simple CLI tool in a previous article. Now, we'll create a more practical CLI task manager that demonstrates the use of key Node.js features and some of the built-in modules we introduced in the last section.

We'll build a task management application that allows users to:

  • Add new tasks
  • List existing tasks
  • Mark tasks as complete
  • Delete tasks

This project will help you with these skills:

  • File operations with the fs module
  • Path handling with the path module
  • Command-line argument parsing
  • Basic data manipulation
  • Working with JSON for data storage

Setting Up the Project

First, create a new file named task-cli.js. This will be our main application file.

Importing Required Modules

At the top of task-cli.js, import the necessary built-in modules:

const fs = require('fs');
const path = require('path');

The fs (File System) module will handle file operations, allowing us to read from and write to files. The path module ensures cross-platform compatibility for file paths, which is crucial for creating applications that work on different operating systems.

Choosing a Data Storage Format: JSON

For our task manager, we'll store our tasks in JSON (JavaScript Object Notation). But why JSON?

  • Native to JavaScript: JSON is derived from JavaScript object syntax, making it easy to work with in Node.js.
  • Human-readable: JSON is easy for humans to read and write, which is helpful for debugging and manual edits if necessary.
  • Lightweight: JSON has a minimal, text-based structure, keeping our data storage efficient.
  • Built-in parsing: JavaScript (and thus Node.js) has built-in methods to parse JSON strings into JavaScript objects (JSON.parse()) and stringify JavaScript objects into JSON strings (JSON.stringify()).

Here's how we'll set up our file path and functions for loading and saving tasks:

const tasksFile = path.join(process.cwd(), 'tasks.json');

function loadTasks() {
    try {
        const data = fs.readFileSync(tasksFile, 'utf8');
        return JSON.parse(data);
    } catch (error) {
        return [];
    }
}

function saveTasks(tasks) {
    fs.writeFileSync(tasksFile, JSON.stringify(tasks, null, 2));
}

Let's break this down:

  • path.join(process.cwd(), 'tasks.json'): This creates a file path that works on any operating system. process.cwd() returns the current working directory.
  • fs.readFileSync(): This reads the entire contents of a file synchronously (meaning it blocks other code from running until it finishes).
  • JSON.parse(data): This converts the JSON string from the file into a JavaScript object (in this case, an array of tasks).
  • JSON.stringify(tasks, null, 2): This converts our tasks array back into a JSON string. The null argument is for custom replacer function (which we don't need), and 2 specifies the number of spaces to use for indentation, making the file more readable.

If the file doesn't exist or is empty, we catch the error and return an empty array, ensuring our application doesn't crash.

Listing Tasks

Now, let's implement the function to display our tasks:

function listTasks() {
    const tasks = loadTasks();
    if (tasks.length === 0) {
        console.log('No tasks found.');
    } else {
        console.log('Tasks:');
        tasks.forEach((task, index) => {
            console.log(`${index + 1}. [${task.completed ? 'X' : ' '}] ${task.description}`);
        });
    }
}

This function does the following:

  1. Loads tasks from our JSON file.
  2. Checks if there are any tasks.
  3. If tasks exist, it loops through them using forEach(), displaying each task's number, completion status, and description.

The task.completed ? 'X' : ' ' is a ternary operator. It's a shorthand way of writing an if-else statement. If task.completed is true, it displays 'X', otherwise, it displays a space.

Adding a Task

Here's how we'll add new tasks:

function addTask(description) {
    const tasks = loadTasks();
    tasks.push({ description, completed: false });
    saveTasks(tasks);
    console.log('Task added successfully.');
}

This function:

  1. Loads existing tasks.
  2. Adds a new task object to the array. { description, completed: false } is using shorthand object property notation. It's equivalent to { description: description, completed: false }.
  3. Saves the updated tasks array back to the JSON file.

In the next part, we'll cover completing tasks, deleting tasks, and handling user input through command-line arguments.

Completing a Task

Now let's implement the function to mark a task as complete:

function completeTask(index) {
    const tasks = loadTasks();
    if (index >= 0 && index < tasks.length) {
        tasks[index].completed = true;
        saveTasks(tasks);
        console.log('Task marked as complete.');
    } else {
        console.log('Invalid task index.');
    }
}

Let's break this down:

  1. We load the existing tasks.
  2. We check if the provided index is valid (not negative and not beyond the array length).
  3. If valid, we set the completed property of the task at that index to true.
  4. We save the updated tasks back to the file.
  5. If the index is invalid, we log an error message.

Note: Array indices in JavaScript start at 0, but we'll use 1-based numbering when displaying tasks to users, as it's more intuitive. That's why we subtract one from the user's input when calling this function.

Deleting a Task

The function to delete a task is similar:

function deleteTask(index) {
    const tasks = loadTasks();
    if (index >= 0 && index < tasks.length) {
        tasks.splice(index, 1);
        saveTasks(tasks);
        console.log('Task deleted successfully.');
    } else {
        console.log('Invalid task index.');
    }
}

The main difference here is the use of splice(). This array method removes elements from an array and returns them. The first argument is the start index, and the second is how many elements to remove. So splice(index, 1) removes one element at the specified index.

Handling User Input

Now comes the part where we tie everything together - handling user commands. We'll use command-line arguments for this:

function printHelp() {
    console.log(`
Task Manager CLI

Usage:
  node task-cli.js <command> [arguments]

Commands:
  list                     List all tasks
  add <task description>   Add a new task
  complete <task index>    Mark a task as complete
  delete <task index>      Delete a task
  help                     Show this help message
    `);
}

const [,, command, ...args] = process.argv;

switch (command) {
    case 'list':
        listTasks();
        break;
    case 'add':
        if (args.length > 0) {
            addTask(args.join(' '));
        } else {
            console.log('Please provide a task description.');
        }
        break;
    case 'complete':
        if (args.length > 0) {
            completeTask(parseInt(args[0]) - 1);
        } else {
            console.log('Please provide a task index.');
        }
        break;
    case 'delete':
        if (args.length > 0) {
            deleteTask(parseInt(args[0]) - 1);
        } else {
            console.log('Please provide a task index.');
        }
        break;
    case 'help':
    default:
        printHelp();
        break;
}

Let's break this down:

  1. printHelp() function: This simply logs usage instructions to the console.

  2. const [,, command, ...args] = process.argv;: This is using array destructuring. process.argv is an array containing the command line arguments. The first two elements are the path to Node.js and the path to our script, which we don't need, so we skip them with the two commas. We then extract the command and put all remaining arguments into the args array.

  3. The switch statement: This checks the command and calls the appropriate function.

    • For 'list', we simply call listTasks().
    • For 'add', we join all the arguments into a single string (in case the task description has spaces) and pass it to addTask().
    • For 'complete' and 'delete', we parse the first argument as an integer and subtract 1 (remember, we're using 1-based indexing for user input, but 0-based indexing in our array).
    • If the command isn't recognized or is 'help', we show the help message.

Using the Task Manager

With our CLI tool complete, here's how to use it:

  • List all tasks:

    node task-cli.js list
    
  • Add a new task:

    node task-cli.js add Buy groceries
    
  • Mark a task as complete:

    node task-cli.js complete 1
    
  • Delete a task:

    node task-cli.js delete 2
    
  • Show help:

    node task-cli.js help
    

This CLI task manager demonstrates several key concepts in Node.js development:

  • Using built-in modules (fs and path) for file operations and path handling.
  • Working with JSON for data storage and retrieval.
  • Parsing command-line arguments to create an interactive tool.
  • Implementing CRUD (Create, Read, Update, Delete) operations on a simple data structure.
  • Error handling for file operations and user input.

Bonus points

To further enhance your understanding and skills, try building these features to your task manager:

  • Add due dates to tasks.
  • Add a 'search' function to find tasks by keyword.
  • Implement data validation (e.g., prevent duplicate tasks).
  • Add colorful console output using a library like chalk.

These enhancements will push you to learn more about JavaScript, Node.js, and CLI application development. You'll learn more when you start bumping into errors yourself and not just follow along.

BeginnerNodejs
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.