add cncount and cndel commands

This commit is contained in:
Gregory Gauthier 2026-01-30 13:35:14 +00:00
parent 1c60165908
commit b68b09b777
8 changed files with 869 additions and 4 deletions

View File

@ -18,6 +18,14 @@ ECHO Compiling cndump.exe...
TCC -A -w -ml -I.\include -ebuild\cndump.exe src\cndump.c TCC -A -w -ml -I.\include -ebuild\cndump.exe src\cndump.c
IF ERRORLEVEL 1 GOTO ERROR IF ERRORLEVEL 1 GOTO ERROR
ECHO Compiling cncount.exe...
TCC -A -w -ml -I.\include -ebuild\cncount.exe src\cncount.c
IF ERRORLEVEL 1 GOTO ERROR
ECHO Compiling cndel.exe...
TCC -A -w -ml -I.\include -ebuild\cndel.exe src\cndel.c
IF ERRORLEVEL 1 GOTO ERROR
ECHO. ECHO.
ECHO Build successful! ECHO Build successful!
ECHO. ECHO.

26
CMakeLists.txt Normal file
View File

@ -0,0 +1,26 @@
cmake_minimum_required(VERSION 4.1)
project(cnotes C)
# Force strict C90/ANSI compliance
set(CMAKE_C_STANDARD 90)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
# Strict warning flags to match Makefile
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ansi -Wpedantic -Wall -Wextra")
include_directories(include)
# Platform-specific definitions for Unix (IDE/static analysis support)
# These match the defaults in platform.h for non-Windows/non-DOS builds
add_compile_definitions(
PATH_SEP_STR="/"
HOME_ENV="HOME"
CNOTES_PATH_ENV="CNOTES_PATH"
)
# Separate executables
add_executable(cnadd src/cnadd.c)
add_executable(cndump src/cndump.c)
add_executable(cncount src/cncount.c)
add_executable(cndel src/cndel.c)

View File

@ -23,7 +23,7 @@ BUILDDIR = build
.c.obj: .c.obj:
$(CC) $(CFLAGS) -c -o$*.obj $< $(CC) $(CFLAGS) -c -o$*.obj $<
all: $(BUILDDIR) $(BUILDDIR)\cnadd.exe $(BUILDDIR)\cndump.exe all: $(BUILDDIR) $(BUILDDIR)\cnadd.exe $(BUILDDIR)\cndump.exe $(BUILDDIR)\cncount.exe $(BUILDDIR)\cndel.exe
$(BUILDDIR): $(BUILDDIR):
if not exist $(BUILDDIR) mkdir $(BUILDDIR) if not exist $(BUILDDIR) mkdir $(BUILDDIR)
@ -34,6 +34,12 @@ $(BUILDDIR)\cnadd.exe: $(SRCDIR)\cnadd.c $(INCDIR)\platform.h $(INCDIR)\config.h
$(BUILDDIR)\cndump.exe: $(SRCDIR)\cndump.c $(INCDIR)\platform.h $(INCDIR)\config.h $(BUILDDIR)\cndump.exe: $(SRCDIR)\cndump.c $(INCDIR)\platform.h $(INCDIR)\config.h
$(CC) $(CFLAGS) -e$(BUILDDIR)\cndump.exe $(SRCDIR)\cndump.c $(CC) $(CFLAGS) -e$(BUILDDIR)\cndump.exe $(SRCDIR)\cndump.c
$(BUILDDIR)\cncount.exe: $(SRCDIR)\cncount.c $(INCDIR)\platform.h $(INCDIR)\config.h
$(CC) $(CFLAGS) -e$(BUILDDIR)\cncount.exe $(SRCDIR)\cncount.c
$(BUILDDIR)\cndel.exe: $(SRCDIR)\cndel.c $(INCDIR)\platform.h $(INCDIR)\config.h
$(CC) $(CFLAGS) -e$(BUILDDIR)\cndel.exe $(SRCDIR)\cndel.c
clean: clean:
if exist $(BUILDDIR)\*.exe del $(BUILDDIR)\*.exe if exist $(BUILDDIR)\*.exe del $(BUILDDIR)\*.exe
if exist $(BUILDDIR)\*.obj del $(BUILDDIR)\*.obj if exist $(BUILDDIR)\*.obj del $(BUILDDIR)\*.obj

View File

@ -9,10 +9,10 @@ SRCDIR = src
INCDIR = include INCDIR = include
BUILDDIR = build BUILDDIR = build
SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c $(SRCDIR)/cncount.c $(SRCDIR)/cndel.c
HEADERS = $(INCDIR)/platform.h $(INCDIR)/config.h HEADERS = $(INCDIR)/platform.h $(INCDIR)/config.h
TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump $(BUILDDIR)/cncount $(BUILDDIR)/cndel
.PHONY: all clean install uninstall .PHONY: all clean install uninstall
@ -27,6 +27,12 @@ $(BUILDDIR)/cnadd: $(SRCDIR)/cnadd.c $(HEADERS)
$(BUILDDIR)/cndump: $(SRCDIR)/cndump.c $(HEADERS) $(BUILDDIR)/cndump: $(SRCDIR)/cndump.c $(HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndump.c $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndump.c
$(BUILDDIR)/cncount: $(SRCDIR)/cncount.c $(HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cncount.c
$(BUILDDIR)/cndel: $(SRCDIR)/cndel.c $(HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndel.c
clean: clean:
rm -rf $(BUILDDIR) rm -rf $(BUILDDIR)
@ -34,6 +40,8 @@ clean:
install: $(TARGETS) install: $(TARGETS)
install -m 755 $(BUILDDIR)/cnadd /usr/local/bin/ install -m 755 $(BUILDDIR)/cnadd /usr/local/bin/
install -m 755 $(BUILDDIR)/cndump /usr/local/bin/ install -m 755 $(BUILDDIR)/cndump /usr/local/bin/
install -m 755 $(BUILDDIR)/cncount /usr/local/bin/
install -m 755 $(BUILDDIR)/cndel /usr/local/bin/
uninstall: uninstall:
rm -f /usr/local/bin/cnadd /usr/local/bin/cndump rm -f /usr/local/bin/cnadd /usr/local/bin/cndump /usr/local/bin/cncount /usr/local/bin/cndel

189
README.md Normal file
View File

@ -0,0 +1,189 @@
# cnotes
A simple command-line note-taking system written in strict C89/ANSI C for maximum portability.
## Overview
cnotes provides quick, one-line note capture from the terminal. Notes are stored in a simple CSV format and can be displayed, sorted, and filtered.
## Supported Platforms
- Linux/Unix (GCC)
- macOS (GCC/Clang)
- Windows (Turbo C++ 3.0, MinGW)
- DOS 6.22 (Turbo C++ 3.0)
## Building
### Linux/Unix/macOS
```bash
make
```
Binaries are placed in the `build/` directory.
### DOS/Windows (Turbo C++ 3.0)
```batch
make -f MAKEFILE.TC
```
Or use the batch file if `make` is unavailable:
```batch
BUILD.BAT
```
### Installation (Unix-like systems)
```bash
sudo make install # installs to /usr/local/bin
sudo make uninstall # removes from /usr/local/bin
```
## Commands
### cnadd
Add a new timestamped note entry.
```bash
cnadd "Remember to call Bob"
cnadd -c Work "Meeting at 3pm"
cnadd -c Personal "Buy groceries"
```
Options:
- `-c CATEGORY` - Specify category (max 10 chars, default: "General")
### cndump
Display all notes in a formatted table.
```bash
cndump # display all notes
cndump -d # sort by date (newest first)
cndump -c # sort by category
cndump -r # reverse sort order
cndump -d -r # sort by date, oldest first
```
Options:
- `-d, --date` - Sort by date/time
- `-c, --category` - Sort by category
- `-r, --reverse` - Reverse sort order
### cncount
Display note statistics.
```bash
cncount # total entry count
cncount -c # count by category
cncount -d # count by date
```
Options:
- `-c, --category` - Show counts per category
- `-d, --date` - Show counts per date
### cndel
Delete note entries.
```bash
cndel -n 5 # delete entry at line 5
cndel -l # delete the last (most recent) entry
cndel -d 2025-01-30 # delete all entries from a specific date
cndel -n 5 -y # delete without confirmation prompt
```
Options:
- `-n LINE_NUMBER` - Delete entry at specified line number
- `-d DATE` - Delete all entries matching DATE (YYYY-MM-DD)
- `-l` - Delete the last (most recent) entry
- `-y` - Skip confirmation prompt
## Configuration
### Notes Location
By default, notes are stored in:
| Platform | Location |
|----------|----------|
| Unix/Linux | `~/.local/share/cnotes/cnotes.csv` |
| Windows | `%USERPROFILE%\.cnotes\cnotes.csv` |
| DOS | Current directory or `%CNOTES_HOME%\cnotes.csv` |
### Environment Variables
- `CNOTES_PATH` - Override the notes directory location (highest priority)
- On DOS, set `CNOTES_HOME` if `HOME` is not available
Example:
```bash
export CNOTES_PATH=/custom/path/to/notes
```
### Compile-time Configuration
Override defaults by defining at compile time:
```bash
gcc -DCNOTES_DIR=\".mynotes\" -DMAX_ENTRIES=1000 ...
```
## Data Format
Notes are stored in CSV format:
```
DATE,TIME,CATEGORY ,"MESSAGE"
2025-01-30,14:30,Work ,"Meeting with team"
2025-01-30,09:15,Personal ,"Buy groceries"
```
Fields:
- Date: `YYYY-MM-DD` (10 chars)
- Time: `HH:MM` (5 chars)
- Category: padded to 10 chars
- Message: quoted, max 125 chars
## TODO
Future commands to implement:
- [ ] **cnfind** - Search notes by keyword, category, or date
```
cnfind "meeting" # search in message text
cnfind -c Work # filter by category
cnfind -d 2025-01-30 # filter by date
```
- [ ] **cntail** - Show last N entries (quick view)
```
cntail # last 5 entries
cntail -n 10 # last 10 entries
```
- [ ] **cncat** - List unique categories
```
cncat # list categories
cncat -c # with counts
```
- [ ] **cnexport** - Export to other formats
```
cnexport > notes.txt # plain text
cnexport -t > notes.tsv # tab-separated
```
## License
[Your license here]
## Author
Gregory Gauthier

22
compile_commands.json Normal file
View File

@ -0,0 +1,22 @@
[
{
"directory": ".",
"command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cnadd.c -o build/cnadd.o",
"file": "src/cnadd.c"
},
{
"directory": ".",
"command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cndump.c -o build/cndump.o",
"file": "src/cndump.c"
},
{
"directory": ".",
"command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cncount.c -o build/cncount.o",
"file": "src/cncount.c"
},
{
"directory": ".",
"command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cndel.c -o build/cndel.o",
"file": "src/cndel.c"
}
]

266
src/cncount.c Normal file
View File

@ -0,0 +1,266 @@
/*
* cncount - Display note statistics
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#include "config.h"
#define MAX_LENGTH 500
#define DATE_LENGTH 10
#define TIME_LENGTH 5
#define MAX_UNIQUE 500 /* Max unique categories or dates */
typedef enum {
COUNT_TOTAL,
COUNT_BY_CATEGORY,
COUNT_BY_DATE
} CountMode;
typedef struct {
char key[CATEGORY_LENGTH + 1];
int count;
} CountEntry;
/* Parse date field from CSV line */
static int parse_date(const char *line, char *date) {
if (strlen(line) < DATE_LENGTH) return 0;
strncpy(date, line, DATE_LENGTH);
date[DATE_LENGTH] = '\0';
return 1;
}
/* Parse category field from CSV line */
static int parse_category(const char *line, char *category) {
const char *ptr;
int i;
/* Skip date and time fields: DATE,TIME,CATEGORY */
if (strlen(line) < DATE_LENGTH + 1 + TIME_LENGTH + 1) return 0;
ptr = line + DATE_LENGTH + 1 + TIME_LENGTH + 1;
/* Copy category until comma */
i = 0;
while (*ptr != ',' && *ptr != '\0' && i < CATEGORY_LENGTH) {
category[i++] = *ptr++;
}
category[i] = '\0';
/* Trim trailing spaces */
while (i > 0 && category[i - 1] == ' ') {
category[--i] = '\0';
}
return 1;
}
/* Find or add a key in the counts array */
static int find_or_add(CountEntry *counts, int *num_entries, const char *key) {
int i;
/* Search for existing key */
for (i = 0; i < *num_entries; i++) {
if (strcmp(counts[i].key, key) == 0) {
return i;
}
}
/* Add new key if room */
if (*num_entries >= MAX_UNIQUE) {
return -1;
}
strncpy(counts[*num_entries].key, key, CATEGORY_LENGTH);
counts[*num_entries].key[CATEGORY_LENGTH] = '\0';
counts[*num_entries].count = 0;
(*num_entries)++;
return *num_entries - 1;
}
/* Comparison function for sorting by key */
static int compare_by_key(const void *a, const void *b) {
const CountEntry *ea = (const CountEntry *)a;
const CountEntry *eb = (const CountEntry *)b;
return strcmp(ea->key, eb->key);
}
/* Comparison function for sorting by count (descending) */
static int compare_by_count(const void *a, const void *b) {
const CountEntry *ea = (const CountEntry *)a;
const CountEntry *eb = (const CountEntry *)b;
return eb->count - ea->count;
}
int get_cnotes_path(char *buffer, size_t bufsize) {
const char *custom_path;
const char *home;
size_t len;
custom_path = getenv(CNOTES_PATH_ENV);
if (custom_path != NULL && strlen(custom_path) > 0) {
if (strlen(custom_path) + strlen(CNOTES_FILE) + 2 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s", custom_path, CNOTES_FILE);
return 1;
}
home = getenv(HOME_ENV);
if (home == NULL) {
fprintf(stderr, "Error: %s environment variable not set\n", HOME_ENV);
fprintf(stderr, "Hint: Set %s to specify notes directory\n", CNOTES_PATH_ENV);
return 0;
}
len = strlen(home);
if (strlen(CNOTES_DIR) == 0) {
if (len + strlen(CNOTES_FILE) + 2 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s", home, CNOTES_FILE);
} else {
if (len + strlen(CNOTES_DIR) + strlen(CNOTES_FILE) + 3 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s" PATH_SEP_STR "%s", home, CNOTES_DIR, CNOTES_FILE);
}
return 1;
}
void print_usage(const char *prog_name) {
fprintf(stderr, "Usage: %s [-c|--category] [-d|--date]\n", prog_name);
fprintf(stderr, " -c, --category Count entries by category\n");
fprintf(stderr, " -d, --date Count entries by date\n");
fprintf(stderr, " (no options) Show total count only\n");
}
int main(int argc, char *argv[]) {
int i;
FILE *notesfile;
char file_path[600];
char line[MAX_LENGTH + 1];
char key[CATEGORY_LENGTH + 1];
int total_count = 0;
CountMode mode = COUNT_TOTAL;
CountEntry *counts;
int num_entries = 0;
int idx;
/* Parse command line arguments */
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--category") == 0) {
if (mode != COUNT_TOTAL) {
fprintf(stderr, "cncount: cannot specify multiple count modes\n");
print_usage(argv[0]);
return 1;
}
mode = COUNT_BY_CATEGORY;
}
else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--date") == 0) {
if (mode != COUNT_TOTAL) {
fprintf(stderr, "cncount: cannot specify multiple count modes\n");
print_usage(argv[0]);
return 1;
}
mode = COUNT_BY_DATE;
}
else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
return 0;
}
else {
fprintf(stderr, "cncount: unknown option '%s'\n", argv[i]);
print_usage(argv[0]);
return 1;
}
}
/* Allocate counts array if needed */
counts = NULL;
if (mode != COUNT_TOTAL) {
counts = (CountEntry *)malloc(MAX_UNIQUE * sizeof(CountEntry));
if (counts == NULL) {
fprintf(stderr, "Error: Cannot allocate memory\n");
return 1;
}
}
/* Get cnotes file path */
if (!get_cnotes_path(file_path, sizeof(file_path))) {
if (counts) free(counts);
return 1;
}
notesfile = fopen(file_path, "r");
if (notesfile == NULL) {
fprintf(stderr, "Error: Cannot open file '%s'\n", file_path);
fprintf(stderr, "Hint: Use 'cnadd' to create your first entry\n");
if (counts) free(counts);
return 1;
}
/* Read and count entries */
while (fgets(line, MAX_LENGTH, notesfile) != NULL) {
/* Skip empty lines */
if (strlen(line) < DATE_LENGTH) continue;
total_count++;
if (mode == COUNT_BY_DATE) {
if (parse_date(line, key)) {
idx = find_or_add(counts, &num_entries, key);
if (idx >= 0) {
counts[idx].count++;
}
}
}
else if (mode == COUNT_BY_CATEGORY) {
if (parse_category(line, key)) {
idx = find_or_add(counts, &num_entries, key);
if (idx >= 0) {
counts[idx].count++;
}
}
}
}
fclose(notesfile);
/* Print results */
if (mode == COUNT_TOTAL) {
printf("Total entries: %d\n", total_count);
}
else {
/* Sort results */
if (mode == COUNT_BY_DATE) {
qsort(counts, num_entries, sizeof(CountEntry), compare_by_key);
} else {
qsort(counts, num_entries, sizeof(CountEntry), compare_by_count);
}
/* Print header */
if (mode == COUNT_BY_DATE) {
printf("%-12s %s\n", "Date", "Count");
printf("%-12s %s\n", "----------", "-----");
} else {
printf("%-12s %s\n", "Category", "Count");
printf("%-12s %s\n", "----------", "-----");
}
/* Print counts */
for (i = 0; i < num_entries; i++) {
printf("%-12s %d\n", counts[i].key, counts[i].count);
}
printf("%-12s %s\n", "----------", "-----");
printf("%-12s %d\n", "Total", total_count);
}
if (counts) free(counts);
return 0;
}

340
src/cndel.c Normal file
View File

@ -0,0 +1,340 @@
/*
* cndel - Delete note entries
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "platform.h"
#include "config.h"
#define MAX_LENGTH 500
#define DATE_LENGTH 10
#define TIME_LENGTH 5
int get_cnotes_path(char *buffer, size_t bufsize) {
const char *custom_path;
const char *home;
size_t len;
custom_path = getenv(CNOTES_PATH_ENV);
if (custom_path != NULL && strlen(custom_path) > 0) {
if (strlen(custom_path) + strlen(CNOTES_FILE) + 2 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s", custom_path, CNOTES_FILE);
return 1;
}
home = getenv(HOME_ENV);
if (home == NULL) {
fprintf(stderr, "Error: %s environment variable not set\n", HOME_ENV);
fprintf(stderr, "Hint: Set %s to specify notes directory\n", CNOTES_PATH_ENV);
return 0;
}
len = strlen(home);
if (strlen(CNOTES_DIR) == 0) {
if (len + strlen(CNOTES_FILE) + 2 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s", home, CNOTES_FILE);
} else {
if (len + strlen(CNOTES_DIR) + strlen(CNOTES_FILE) + 3 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s" PATH_SEP_STR "%s", home, CNOTES_DIR, CNOTES_FILE);
}
return 1;
}
/* Get temporary file path */
int get_temp_path(char *buffer, size_t bufsize, const char *original_path) {
size_t len = strlen(original_path);
if (len + 5 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s.tmp", original_path);
return 1;
}
/* Parse and display a line for confirmation */
void display_entry(int line_num, const char *line) {
char date[DATE_LENGTH + 1];
char time_str[TIME_LENGTH + 1];
char category[CATEGORY_LENGTH + 1];
const char *ptr;
const char *msg_start;
const char *msg_end;
int i;
/* Parse date */
if (strlen(line) < DATE_LENGTH) return;
strncpy(date, line, DATE_LENGTH);
date[DATE_LENGTH] = '\0';
/* Parse time */
ptr = line + DATE_LENGTH + 1;
if (strlen(ptr) < TIME_LENGTH) return;
strncpy(time_str, ptr, TIME_LENGTH);
time_str[TIME_LENGTH] = '\0';
/* Parse category */
ptr = ptr + TIME_LENGTH + 1;
i = 0;
while (*ptr != ',' && *ptr != '\0' && i < CATEGORY_LENGTH) {
category[i++] = *ptr++;
}
category[i] = '\0';
/* Trim trailing spaces */
while (i > 0 && category[i - 1] == ' ') {
category[--i] = '\0';
}
/* Find message (quoted) */
msg_start = strchr(ptr, '"');
if (msg_start) {
msg_start++;
msg_end = strchr(msg_start, '"');
if (msg_end) {
printf(" Line %d: [%s %s] [%s] ", line_num, date, time_str, category);
/* Print message without trailing quote */
while (msg_start < msg_end) {
putchar(*msg_start++);
}
putchar('\n');
return;
}
}
/* Fallback: just show the raw line */
printf(" Line %d: %s", line_num, line);
}
void print_usage(const char *prog_name) {
fprintf(stderr, "Usage: %s -n LINE_NUMBER [-y]\n", prog_name);
fprintf(stderr, " %s -d DATE [-y]\n", prog_name);
fprintf(stderr, " %s -l [-y]\n", prog_name);
fprintf(stderr, "\n");
fprintf(stderr, "Options:\n");
fprintf(stderr, " -n LINE_NUMBER Delete entry at specified line number\n");
fprintf(stderr, " -d DATE Delete all entries matching DATE (YYYY-MM-DD)\n");
fprintf(stderr, " -l Delete the last (most recent) entry\n");
fprintf(stderr, " -y Skip confirmation prompt\n");
}
int main(int argc, char *argv[]) {
int i;
FILE *infile;
FILE *outfile;
char file_path[600];
char temp_path[610];
char line[MAX_LENGTH + 1];
int line_num;
int target_line = -1;
char *target_date = NULL;
int delete_last = 0;
int skip_confirm = 0;
int total_lines = 0;
int deleted_count = 0;
int confirm;
char response[10];
/* Parse command line arguments */
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-n") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "cndel: -n requires a line number\n");
print_usage(argv[0]);
return 1;
}
target_line = atoi(argv[++i]);
if (target_line < 1) {
fprintf(stderr, "cndel: invalid line number '%s'\n", argv[i]);
return 1;
}
}
else if (strcmp(argv[i], "-d") == 0) {
if (i + 1 >= argc) {
fprintf(stderr, "cndel: -d requires a date (YYYY-MM-DD)\n");
print_usage(argv[0]);
return 1;
}
target_date = argv[++i];
if (strlen(target_date) != DATE_LENGTH) {
fprintf(stderr, "cndel: invalid date format '%s' (use YYYY-MM-DD)\n", target_date);
return 1;
}
}
else if (strcmp(argv[i], "-l") == 0) {
delete_last = 1;
}
else if (strcmp(argv[i], "-y") == 0) {
skip_confirm = 1;
}
else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
print_usage(argv[0]);
return 0;
}
else {
fprintf(stderr, "cndel: unknown option '%s'\n", argv[i]);
print_usage(argv[0]);
return 1;
}
}
/* Validate options */
if (target_line < 0 && target_date == NULL && !delete_last) {
fprintf(stderr, "cndel: must specify -n LINE_NUMBER, -d DATE, or -l\n");
print_usage(argv[0]);
return 1;
}
if ((target_line > 0) + (target_date != NULL) + delete_last > 1) {
fprintf(stderr, "cndel: cannot combine -n, -d, and -l options\n");
print_usage(argv[0]);
return 1;
}
/* Get file paths */
if (!get_cnotes_path(file_path, sizeof(file_path))) {
return 1;
}
if (!get_temp_path(temp_path, sizeof(temp_path), file_path)) {
return 1;
}
/* If deleting last entry, first count total lines */
if (delete_last) {
infile = fopen(file_path, "r");
if (infile == NULL) {
fprintf(stderr, "Error: Cannot open file '%s'\n", file_path);
return 1;
}
while (fgets(line, MAX_LENGTH, infile) != NULL) {
if (strlen(line) >= DATE_LENGTH) {
total_lines++;
}
}
fclose(infile);
if (total_lines == 0) {
fprintf(stderr, "cndel: no entries to delete\n");
return 1;
}
target_line = total_lines;
}
/* First pass: show what will be deleted and count matches */
infile = fopen(file_path, "r");
if (infile == NULL) {
fprintf(stderr, "Error: Cannot open file '%s'\n", file_path);
return 1;
}
printf("The following entries will be deleted:\n\n");
line_num = 0;
while (fgets(line, MAX_LENGTH, infile) != NULL) {
if (strlen(line) < DATE_LENGTH) continue;
line_num++;
if (target_line > 0 && line_num == target_line) {
display_entry(line_num, line);
deleted_count++;
}
else if (target_date != NULL && strncmp(line, target_date, DATE_LENGTH) == 0) {
display_entry(line_num, line);
deleted_count++;
}
}
fclose(infile);
if (deleted_count == 0) {
if (target_line > 0) {
fprintf(stderr, "cndel: line %d does not exist (file has %d entries)\n",
target_line, line_num);
} else {
fprintf(stderr, "cndel: no entries found matching date '%s'\n", target_date);
}
return 1;
}
printf("\n%d entry(s) will be deleted.\n", deleted_count);
/* Confirm deletion */
if (!skip_confirm) {
printf("Are you sure? [y/N]: ");
fflush(stdout);
if (fgets(response, sizeof(response), stdin) == NULL) {
printf("Aborted.\n");
return 1;
}
confirm = (response[0] == 'y' || response[0] == 'Y');
if (!confirm) {
printf("Aborted.\n");
return 0;
}
}
/* Second pass: copy all lines except deleted ones to temp file */
infile = fopen(file_path, "r");
if (infile == NULL) {
fprintf(stderr, "Error: Cannot open file '%s'\n", file_path);
return 1;
}
outfile = fopen(temp_path, "w");
if (outfile == NULL) {
fprintf(stderr, "Error: Cannot create temporary file '%s'\n", temp_path);
fclose(infile);
return 1;
}
line_num = 0;
deleted_count = 0;
while (fgets(line, MAX_LENGTH, infile) != NULL) {
if (strlen(line) < DATE_LENGTH) {
/* Preserve empty/invalid lines as-is */
fputs(line, outfile);
continue;
}
line_num++;
if (target_line > 0 && line_num == target_line) {
deleted_count++;
continue; /* Skip this line */
}
else if (target_date != NULL && strncmp(line, target_date, DATE_LENGTH) == 0) {
deleted_count++;
continue; /* Skip this line */
}
/* Keep this line */
fputs(line, outfile);
}
fclose(infile);
fclose(outfile);
/* Replace original with temp file */
if (remove(file_path) != 0) {
fprintf(stderr, "Error: Cannot remove original file\n");
remove(temp_path);
return 1;
}
if (rename(temp_path, file_path) != 0) {
fprintf(stderr, "Error: Cannot rename temporary file\n");
return 1;
}
printf("Deleted %d entry(s).\n", deleted_count);
return 0;
}