~siborgium/dumbme

c455fe30def07e20a1def33307df1cfba6e9c773 — Sergey Smirnykh 2 years ago
initial commit
5 files changed, 348 insertions(+), 0 deletions(-)

A Makefile
A PKGBUILD
A dumbme-1.1.tar.zst
A dumbme.c
A read_lines.h
A  => Makefile +9 -0
@@ 1,9 @@

CC?=gcc
CFLAGS?=-Wall -Wextra -Wconversion -Werror -Wuninitialized -Winit-self -Wno-unused-result -pedantic -pipe -O2 -march=native -fomit-frame-pointer

dumbme:
	$(CC) $(CFLAGS) dumbme.c -o dumbme

clean:
	rm dumbme

A  => PKGBUILD +16 -0
@@ 1,16 @@

pkgname=dumbme
pkgver=1.1
pkgrel=1
pkgdesk="A simple wrapper around `rename`, designed for bulk renaming"
arch=('any')
license=('GPL3')
depends=('glibc') # use musl if you want to
source=("$pkgname-$pkgver.tar.zst")
sha256sums=('SKIP')
build() {
        make
}
package() {
        install -D -o root -g root -m 755 dumbme "$pkgdir/usr/bin/dumbme"
}

A  => dumbme-1.1.tar.zst +0 -0
A  => dumbme.c +133 -0
@@ 1,133 @@

#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>

#define TMPFILE_TEMPLATE "dumbme-XXXXXX"
#define XDG_TMP_DIR "XDG_RUNTIME_DIR"
#define LINE_BUF_CAPACITY 120
#define TEXT_BUF_CAPACITY 16

#include "read_lines.h"

int main() {
    int had_err;
    int err;

    char* editor;
    pid_t editor_pid;
    int   editor_status;

    FILE* tmp_stream;
    int   tmp_file;

    char*  tmp_dir_name;
    size_t tmp_dir_name_len;
    char*  tmp_file_name;

    // Move from
    struct leader*    lines_1;
    struct lines_iter lines_1_iter;
    char*             lines_1_tmp;
    size_t            lines_1_capacity;
    
    // Move           to
    struct leader*    lines_2;
    struct lines_iter lines_2_iter;
    char*             lines_2_tmp;
    size_t            lines_2_capacity;

    lines_1 = read_lines (STDIN_FILENO);
    assert (lines_1);

    // Compose tmp file name
    tmp_dir_name = getenv ("XDG_RUNTIME_DIR");
    if (!tmp_dir_name) {
        tmp_dir_name = "";
    }
    tmp_dir_name_len = strlen (tmp_dir_name) + 1;
    tmp_file_name = malloc (tmp_dir_name_len + sizeof(TMPFILE_TEMPLATE));
    strcpy (tmp_file_name, tmp_dir_name);
    tmp_file_name[tmp_dir_name_len - 1] = '/';
    strcat (tmp_file_name, TMPFILE_TEMPLATE);
    assert (tmp_file_name);
    // Create tmp file
    tmp_file = mkstemp (tmp_file_name);
    assert (tmp_file != -1);
    tmp_stream = fdopen (tmp_file, "r+");
    assert (tmp_stream);

    // Fill tmp file
    make_lines_iter (lines_1, &lines_1_iter);

    lines_1_capacity = LINE_BUF_CAPACITY;
    lines_1_tmp      = malloc (lines_1_capacity);

    while (iter_getline (&lines_1_iter, &lines_1_tmp, &lines_1_capacity)) {
        if (strlen(lines_1_tmp) > 0) {
            fprintf (tmp_stream, "%s\n", lines_1_tmp);
        }
    }
    fclose (tmp_stream);

    // Run editor
    if ((editor_pid = fork()) == 0) {
        editor = getenv ("EDITOR");
        assert (editor);
        editor = strdup (editor);
        err = dup2 (STDOUT_FILENO, STDIN_FILENO);
        assert (err != -1);
        if (execlp (editor, editor, tmp_file_name, (char*)0) == -1) {
            perror ("Error: Failed to run editor");
            exit (-1);
        }
    }
    
    // Only continue if editor exited with success
    waitpid (editor_pid, &editor_status, 0);
    if (!WIFEXITED(editor_status)) {
        err = errno;
        fprintf (stderr, "Error: EDITOR exited with non-zero exit code\n    errno = %d", err);
        _exit (EXIT_FAILURE);
    }

    // Reopen file
    // TODO: is it possible to keep fd and just reopen?
    tmp_file = open (tmp_file_name, O_RDONLY);
    assert (tmp_file >= 0);
    
    // Read new lines
    lines_2_capacity = LINE_BUF_CAPACITY;
    lines_2_tmp      = malloc(lines_2_capacity);

    lines_2 = read_lines (tmp_file);
    assert (lines_2);

    make_lines_iter (lines_1, &lines_1_iter);
    make_lines_iter (lines_2, &lines_2_iter);

    had_err = 0;
    while (iter_getline (&lines_1_iter, &lines_1_tmp, &lines_1_capacity) &&
           iter_getline (&lines_2_iter, &lines_2_tmp, &lines_2_capacity))
    {
        if (!!strcmp (lines_1_tmp, lines_2_tmp)) {
            err = rename (lines_1_tmp, lines_2_tmp);
            if (!err) {
                printf ("renamed '%s' -> '%s'\n", lines_1_tmp, lines_2_tmp);
            } else {
                fprintf (stderr, "failed to rename '%s' -> '%s'\n", lines_1_tmp, lines_2_tmp);
                had_err = 1;
            }
        } else {
            printf ("skipped unchanged line '%s'\n", lines_1_tmp);
        }
    }

    // no need to free memory, OS will clean it up on its own
    // just unlink the file and call it a day
    close (tmp_file);
    unlink (tmp_file_name);

    return had_err;
}

A  => read_lines.h +190 -0
@@ 1,190 @@

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

struct leader {
    struct leader*  next;
    struct trailer* newlines;
    char*           buffer_start;
    char*           buffer_end;
};

struct trailer {
    struct trailer* next;
    char*           end;
};

/// Follows the same convention as `write(2)`
/// returns -1 on error
///          0 on eof
///         >0 bytes read
ssize_t read_buffer (int file, char** buf) {
    char*   buffer_start;
    char*   buffer_ptr;
    ssize_t bytes_read;

    buffer_start = malloc (TEXT_BUF_CAPACITY);
    buffer_ptr = buffer_start;
   
read_buffer_start:
    bytes_read = buffer_ptr - buffer_start;
    assert (bytes_read >= 0);
    bytes_read = read (file, buffer_ptr, (size_t)(TEXT_BUF_CAPACITY - (size_t)(bytes_read)));
    if (bytes_read < 0) {
        return bytes_read;
    }
    if (bytes_read == 0) {
        *buf = buffer_start;
        return buffer_ptr - buffer_start;
    }
    buffer_ptr += bytes_read;
    goto read_buffer_start;
}

struct trailer* find_newlines (char* start, char* end) {
    struct trailer  dummy;
    struct trailer* tail;
    char*           pos;

    dummy.next = NULL;
    tail = &dummy;
    pos = start;
    while (pos <= end) {
        if (*pos == '\n') {
            tail->next = malloc (sizeof (struct trailer));            
            tail = tail->next;

            tail->end  = pos;
            tail->next = NULL;
        }
        pos++;
    }
    return dummy.next;
}

// Reads exactly min (TEXT_BUF_CAPACITY, bytes left in file) bytes from file
// and assigns them to `chunk`
// returning the number of bytes actually read
ssize_t read_chunk (int file, struct leader* chunk) {
    struct trailer* newlines;
    char*           buffer_start;
    ssize_t         bytes_read;

    bytes_read = read_buffer (file, &buffer_start);
    if (bytes_read < 0) {
        return bytes_read;
    }
    newlines = find_newlines (buffer_start, buffer_start + bytes_read);

    chunk->buffer_start = buffer_start;
    chunk->buffer_end   = buffer_start + bytes_read;
    chunk->newlines     = newlines;

    return bytes_read;
}

// Consumes all content of handle `file`
struct leader* read_lines (int file) {
    ssize_t        bytes_read;
    struct leader  dummy;
    struct leader* tail;

    tail = &dummy;
    do {
        tail->next = malloc (sizeof (struct leader));
        tail = tail->next;
        bytes_read = read_chunk (file, tail);
        if (bytes_read < 0) {
            return NULL;
        }
    } while (bytes_read == TEXT_BUF_CAPACITY);
    tail->next = NULL;
    return dummy.next;
}

struct lines_iter {
    struct leader*  text;
    struct trailer* newlines;
    char*           start;
};

// Creates iterator over pieces of text
void make_lines_iter (struct leader* text, struct lines_iter* iter) {
    iter->text     = text;
    iter->newlines = text->newlines;
    iter->start    = text->buffer_start;
}

int is_exhausted (struct lines_iter* iter) {
    return !iter->text;
}

int view_next_line_piece (struct lines_iter* iter, char** start, char** end) {
    ssize_t res;
    *start = iter->start;
    if (iter->newlines) {
        res            = iter->newlines->end == iter->text->buffer_end;
        *end           = iter->newlines->end;
        if (res) {
            goto view_next_line_piece_text_update;
        }
        iter->start    = iter->newlines->end + !res;
        iter->newlines = iter->newlines->next;
        return 1;
    }
    *end = iter->text->buffer_end;

view_next_line_piece_text_update:
    iter->text = iter->text->next;
    if (iter->text) {
        iter->start    = iter->text->buffer_start;
        iter->newlines = iter->text->newlines;
        return 2;
    }
    return -1;
}

// Returns owned null-terminated string containing current line
// Will `realloc` provided buffer if needed, capacity will be changed accordingly
int iter_getline (struct lines_iter* iter, char** buf, size_t* buf_capacity) {
    int    res = 42;
    size_t buf_len = 0;
    char*  str_start;
    char*  str_end;

    ssize_t len;
    while (!is_exhausted (iter) && res != 1) {
        res = view_next_line_piece (iter, &str_start, &str_end);
        len = str_end - str_start;
        if (len < 0) { // triggers on empty text
            return 0;
        }
        if (buf_len + (size_t)len >= *buf_capacity) {
            *buf_capacity += (size_t)len;
            *buf = realloc (*buf, *buf_capacity * 2);
        }
        memcpy (*buf + buf_len, str_start, (size_t)len);
        buf_len += (size_t)len;
    }

    res = res != 42; // check if anything was read
    if (res) {
        (*buf)[buf_len] = '\0';
    }
    return res;
}

int iter_countlines (struct lines_iter* iter) {
    size_t buf_capacity = LINE_BUF_CAPACITY;
    char*  buffer = malloc (buf_capacity);
    int    counter = 0;

    // todo: no allocs, no copying
    while (iter_getline(iter, &buffer, &buf_capacity)) {
        counter++;
    }
    return counter;
}