Real Projects: CLI Tools
In this chapter, we dive into practical C programming by building real-world command-line tools. These projects bridge theory and practice, helping you understand how to create efficient, user-friendly applications that solve everyday problems without GUI overhead. Let’s start with two foundational tools: a File Manager and a Text Processor.
File Manager
A CLI file manager is essential for navigating your system efficiently. Unlike GUI tools, it’s lightweight, fast, and integrates seamlessly with your shell workflow. We’ll build a minimal but functional file manager that lists directory contents and handles directory changes—perfect for environments where speed and resource efficiency matter.
Core Concepts
Before coding, understand these key C concepts:
- Directory navigation: Using
chdir()fromto switch directories - File listing: Iterating through directory entries with
opendir(),readdir(), andcloseset() - Error handling: Checking return values from system calls (critical for robust CLI tools)
Building a Minimal File Manager
Here’s a step-by-step implementation of a CLI file manager with two commands: ls (list files) and cd (change directory). This tool avoids complex features (like file operations) to focus on core navigation.
<code class="language-c">#include <stdio.h>
<p>#include <stdlib.h></p>
<p>#include <dirent.h></p>
<p>#include <string.h></p>
<p>// Helper function to print directory entries</p>
<p>void print<em>dir</em>entries(DIR *dir) {</p>
<p> struct dirent *entry;</p>
<p> while ((entry = readdir(dir)) != NULL) {</p>
<p> if (strcmp(entry->d<em>name, ".") != 0 && strcmp(entry->d</em>name, "..") != 0) {</p>
<p> printf("%s\n", entry->d_name);</p>
<p> }</p>
<p> }</p>
<p>}</p>
<p>int main(int argc, char *argv[]) {</p>
<p> if (argc < 2) {</p>
<p> printf("Usage: file_manager ls [directory] | cd [directory]\n");</p>
<p> return 1;</p>
<p> }</p>
<p> if (strcmp(argv[1], "ls") == 0) {</p>
<p> // List current directory by default</p>
<p> DIR *dir = opendir(".");</p>
<p> if (dir == NULL) {</p>
<p> perror("opendir");</p>
<p> return 1;</p>
<p> }</p>
<p> print<em>dir</em>entries(dir);</p>
<p> closedir(dir);</p>
<p> } else if (strcmp(argv[1], "cd") == 0) {</p>
<p> // Change directory</p>
<p> char *path = argv[2];</p>
<p> if (chdir(path) != 0) {</p>
<p> perror("chdir");</p>
<p> return 1;</p>
<p> }</p>
<p> printf("Changed directory to: %s\n", path);</p>
<p> } else {</p>
<p> printf("Unknown command. Use 'ls' or 'cd'.\n");</p>
<p> return 1;</p>
<p> }</p>
<p> return 0;</p>
<p>}</code>
Why this works:
- The
lscommand lists all files (excluding.and..). - The
cdcommand changes the current working directory usingchdir(). - Error handling ensures the tool doesn’t crash unexpectedly (e.g., invalid paths).
Real-World Usage
Run this in your terminal:
<code class="language-bash"># List current directory <p>file_manager ls</p> <h1>Change to a subdirectory</h1> <p>file_manager cd ./docs</code>
Pro Tip: Add stat() calls to show file sizes or dates for richer output. This is a great starting point for expanding into full-featured file managers.
Text Processor
Text processors handle text manipulation—perfect for tasks like log analysis, data cleaning, or quick file transformations. We’ll build a lightweight tool that replaces text patterns and counts words in files, demonstrating how C excels at low-level text operations.
Core Concepts
- File reading:
fopen(),fread(), andfgetc()for line-by-line processing - String manipulation:
strcat(),strlen(), andstrstr()for pattern matching - Command-line arguments: Parsing flags (e.g.,
-sfor search,-rfor replace)
Building a Text Processor
Here’s a minimal text processor with two commands: replace (find-and-replace) and wordcount (count words). This tool focuses on clarity and practical utility.
<code class="language-c">#include <stdio.h>
<p>#include <stdlib.h></p>
<p>#include <string.h></p>
<p>// Helper function to count words in a file</p>
<p>int count_words(FILE *file) {</p>
<p> int word_count = 0;</p>
<p> int in_word = 0;</p>
<p> int c;</p>
<p> </p>
<p> while ((c = fgetc(file)) != EOF) {</p>
<p> if (c == ' ' || c == '\n' || c == '\t') {</p>
<p> if (in_word) {</p>
<p> word_count++;</p>
<p> in_word = 0;</p>
<p> }</p>
<p> } else {</p>
<p> if (!in_word) {</p>
<p> in_word = 1;</p>
<p> }</p>
<p> }</p>
<p> }</p>
<p> return word_count;</p>
<p>}</p>
<p>// Helper function to replace text</p>
<p>void replace_text(FILE <em>file, const char </em>search, const char *replace) {</p>
<p> char buffer[1024];</p>
<p> size_t len;</p>
<p> while (fgets(buffer, sizeof(buffer), file) != NULL) {</p>
<p> len = strlen(buffer);</p>
<p> // Replace all occurrences (simplified for demo)</p>
<p> char *pos = strstr(buffer, search);</p>
<p> while (pos) {</p>
<p> size<em>t replace</em>len = strlen(replace);</p>
<p> size<em>t search</em>len = strlen(search);</p>
<p> char *new<em>buffer = malloc(len - search</em>len + replace_len + 1);</p>
<p> if (new_buffer) {</p>
<p> strncpy(new_buffer, buffer, pos - buffer);</p>
<p> strncat(new<em>buffer, replace, replace</em>len);</p>
<p> strncat(new<em>buffer, buffer + pos + search</em>len, len - (pos - buffer) - search_len);</p>
<p> new<em>buffer[len - search</em>len + replace_len] = '\0';</p>
<p> // Write new_buffer back to file (simplified)</p>
<p> printf("Replaced: %s -> %s\n", search, replace);</p>
<p> // In real use: write new_buffer to file</p>
<p> free(new_buffer);</p>
<p> }</p>
<p> pos = strstr(pos + search_len, search);</p>
<p> }</p>
<p> }</p>
<p>}</p>
<p>int main(int argc, char *argv[]) {</p>
<p> if (argc < 3) {</p>
<p> printf("Usage: text_processor replace [search] [replace] | wordcount [file]\n");</p>
<p> return 1;</p>
<p> }</p>
<p> if (strcmp(argv[1], "replace") == 0) {</p>
<p> FILE *file = fopen(argv[2], "r");</p>
<p> if (!file) {</p>
<p> perror("fopen");</p>
<p> return 1;</p>
<p> }</p>
<p> replace_text(file, argv[3], argv[4]);</p>
<p> fclose(file);</p>
<p> } else if (strcmp(argv[1], "wordcount") == 0) {</p>
<p> FILE *file = fopen(argv[2], "r");</p>
<p> if (!file) {</p>
<p> perror("fopen");</p>
<p> return 1;</p>
<p> }</p>
<p> int count = count_words(file);</p>
<p> printf("Word count: %d\n", count);</p>
<p> fclose(file);</p>
<p> } else {</p>
<p> printf("Unknown command. Use 'replace' or 'wordcount'.\n");</p>
<p> return 1;</p>
<p> }</p>
<p> return 0;</p>
<p>}</code>
Why this works:
wordcountuses a state machine to count words without regex (efficient for C).replace_textfinds and replaces patterns line-by-line (ideal for small files).- The tool handles errors gracefully (e.g., missing files).
Real-World Usage
Run these in your terminal:
<code class="language-bash"># Count words in a log file <p>text_processor wordcount logs.txt</p> <h1>Replace "error" with "issue" in a file</h1> <p>text_processor replace "error" "issue" data.log</code>
Pro Tip: For production use, add file buffering and error logging. This foundation scales to handle larger files with minimal overhead.
Summary
In this chapter, we built two practical CLI tools that showcase C’s strengths in system-level programming: a File Manager for efficient directory navigation and a Text Processor for text manipulation tasks. Both projects emphasize error handling, resource efficiency, and real-world usability—key principles for professional CLI development. These tools are a great starting point for expanding into more complex applications while maintaining C’s performance advantages. 💡