elliot.science

 elliot.science

Rename by Timestamp

2025-09-10
/tools#c#linux#cli#file-management

Preface

A lightweight C utility to batch-rename files based on their modification timestamp. It addresses the common workflow issue of disorganized downloads or exported media by enforcing a consistent YYYY-MM-DD_HH-MM-SS naming convention. The tool is designed to be non-destructive, skipping files that already match the expected timestamp format.

Timestamp Detection Logic

To prevent unnecessary operations or data loss, the utility includes a strict validation check before renaming. It inspects the first 19 characters of a filename to ensure they conform exactly to the target date-time format.

int is_timestamp_formatted(const char *filename) {
  if (strlen(filename) < 19) return 0;

  for (int i = 0; i < 4; i++) {
    if (!is_digit(filename[i])) return 0;
  }

  if (filename[4] != '-' || filename[7] != '-' || filename[10] != '_' ||
      filename[13] != '-' || filename[16] != '-') return 0;

  for (int i = 5; i < 19; i += 3) {
    if (!is_digit(filename[i]) || !is_digit(filename[i + 1])) return 0;
  }

  return 1;
}

If a file passes this check, the program compares the embedded timestamp against the actual st_mtime retrieved via stat(). If they match, the file is safely skipped.

Path Resolution & Extensions

The utility carefully handles directory prefixes and file extensions. It isolates the base filename using strrchr() to locate the final /. It then checks for an existing extension using strrchr(filename, '.').

The new path is constructed dynamically to preserve the directory structure:

char new_path[512];
if (last_slash) {
    int dir_len = last_slash - filepath + 1;
    if (dir_len > 0) {
        snprintf(new_path, sizeof(new_path), "%.*s%s", dir_len, filepath, new_filename);
    } else {
        snprintf(new_path, sizeof(new_path), "%s", new_filename);
    }
} else {
    snprintf(new_path, sizeof(new_path), "%s", new_filename);
}

This ensures that files located in subdirectories retain their original parent paths while only the base name is altered.

Wildcard Support via Globbing

Unlike basic wrappers that only accept single paths, this utility leverages the POSIX glob() library. This allows seamless integration with shell expansion rules. The code detects wildcards (*, ?, [) in the input argument and processes them as pattern sets.

int has_wildcard = strpbrk(pattern, "*?[") != NULL;

if (has_wildcard) {
    glob_t g;
    if (glob(pattern, GLOB_TILDE, NULL, &g) == 0) {
        for (size_t j = 0; j < g.gl_pathc; j++) {
            struct stat st;
            if (stat(g.gl_pathv[j], &st) == 0 && S_ISREG(st.st_mode)) {
                process_file(g.gl_pathv[j]);
            }
        }
        globfree(&g);
    }
}

Directories are explicitly filtered out using S_ISDIR() checks before any system calls are made, ensuring the utility only operates on regular files.

Compilation & Usage

Built strictly with C99 standards and compiled with strict warning flags (-Wall -Wextra -pedantic), the utility is highly portable across POSIX-compliant systems.

# Build
make

# Run
./bin/rename-by-timestamp *.jpg *.txt

The Makefile provides a clean install target that copies the binary to /usr/local/bin, making it available system-wide.