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. Thenull
argument is for custom replacer function (which we don't need), and2
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:
- Loads tasks from our JSON file.
- Checks if there are any tasks.
- 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:
- Loads existing tasks.
- 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 }
. - 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:
- We load the existing tasks.
- We check if the provided index is valid (not negative and not beyond the array length).
- If valid, we set the
completed
property of the task at that index totrue
. - We save the updated tasks back to the file.
- 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:
printHelp()
function: This simply logs usage instructions to the console.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 theargs
array.The
switch
statement: This checks thecommand
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.
- For 'list', we simply call
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
andpath
) 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.