Skip to content

Instantly share code, notes, and snippets.

@lojikil
Last active May 20, 2020 13:43
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lojikil/aae3c9aa948a421b894e361688f4ab87 to your computer and use it in GitHub Desktop.
Save lojikil/aae3c9aa948a421b894e361688f4ab87 to your computer and use it in GitHub Desktop.
A tiny, ed-like directory editor
*.swp
microdired
*.o
microdired.dSYM/*

Overview

microdired is an experiment that came about when I first saw nnn, which features vim-like keystrokes. It looks extremely interesting, but I thought to myself "what if we made an ed(1)-like file editor? what would that look like?" This started as a simple gist and grew into something fun to tinker with. Probably the most useful part of this program is the parser for ed(1)-like command languages: the parser parses:

  • ranges
  • globs
  • commands
  • arguments

into a simple data structure. I've thought about turning microdired into something like Holzmann's pico(1) but I have not gotten there just yet.

Language

The language that this ed(1)-alike parser consumes is as follows:

n,m(glob)cmd arg0 arg1 ... argN

  • n,m: a start line, and optional completion line
  • (glob): a Unix-shell glob-alike
  • cmd: an actual command, like l or def
  • argN: character string arguments

Commands:

  • .: print the current working directory
  • ..: go up to the parent directory
  • /somedirectory: go to /somedirectory
  • l/L: print directory with or without entry numbers
  • p/P: pretty print directory with or without entry numbers
  • c/C: create a file/directory
  • f: print only files
  • F: pretty print only files
  • d: print only directories
  • D: pretty print only directories
  • e: invoke $EDITOR, falling back on internal ed(1)
  • E: invoke internal ed(1)
  • m: more, but built in
  • M: mode, as in chmod
  • t: test-like interface
  • !: execute a unix shell
Copyright (c) 2016-2017, Stefan Edwards
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#include <sys/stat.h>
#define nil NULL
#define nul '\0'
#define debugprnt printf("here? %d\n", __LINE__)
/* next step would be to
* make the cmd into a
* char *, so that we can
* have longer command names.
*/
typedef struct _CMD {
char cmd;
int start;
int stop;
int glob_len;
char glob[256];
char arg[256];
} Command;
/* a simple directory buffer;
* since I am mainly using strings
* right now, just keep this as a simple
* list of strings and integers. The
* integer is the offset of the original;
* if it is nil, then the offset ==
* the index in the buffer. Why do this?
* because I want to reuse the same buffer
* interface for all transactions, which means
* sometimes we have to have that information
* available. For example, if we enter the
* command:
* > 7,13([A-Z]?.txt)l
* we want to know _which_ lines matched
* the glob, not just that there *are*
* lines that match the glob.
*/
typedef struct _DIRBUF {
size_t count;
int *offsets;
char **buffer;
} DirectoryBuffer;
int parse(Command *, const char *, int);
DirectoryBuffer *cachedirectory(char *);
DirectoryBuffer *filtercache(Command *, DirectoryBuffer *);
void cleancache(DirectoryBuffer *);
int
main(int ac, char **al, char **el) {
DirectoryBuffer *curbuf = nil, *view = nil, *dtmp = nil;
int dot = 0, len = 0, tmp = 0, ret = 0;
char prompt[32] = {'>', ' ', nul}, linebuf[512] = {0}, curdir[512] = {0};
const char *editor = getenv("EDITOR"), *home = getenv("HOME");
FILE *fp = nil;
Command com;
(void)getcwd(curdir, 512);
curbuf = cachedirectory(curdir);
if(curbuf == nil) {
printf("Cannot open current directory!\n");
return 1;
}
while(1) {
printf("%s", prompt);
/* probably should have fdin, so that we can script this... */
fgets(linebuf, 512, stdin);
if(feof(stdin)) {
break;
}
/* chop off the '\n' */
len = strnlen(linebuf, 512);
linebuf[len - 1] = nul;
if(!strncmp(linebuf, "q", 512)) {
break;
} else if(!strncmp(linebuf, "~", 512)) {
strlcpy(curdir, home, 512);
cleancache(curbuf);
curbuf = cachedirectory(curdir);
} else if(linebuf[0] == '/') {
dtmp = cachedirectory(linebuf);
if(dtmp == nil) {
printf("cannot open directory: %s\n", linebuf);
} else {
cleancache(curbuf);
curbuf = dtmp;
strlcpy(curdir, linebuf, 512);
}
} else if(!strncmp(linebuf, ".", 512)) {
printf("%s\n", curdir);
} else if(!strncmp(linebuf, "..", 512)) {
char *tmp = strrchr(curdir, '/');
if(tmp != nil) {
tmp[0] = nul;
dtmp = cachedirectory(curdir);
if(dtmp == nil) {
printf("cannot open directory %s\n", curdir);
} else {
cleancache(curbuf);
curbuf = dtmp;
}
}
} else {
ret = parse(&com, &linebuf[0], 512);
view = filtercache(&com, curbuf);
switch(com.cmd) {
case 'l':
// list directory with numbers
for(tmp = 0; tmp < view->count ; tmp++) {
if(view->offsets != nil) {
printf("%d\t%s\n", view->offsets[tmp], view->buffer[tmp]);
} else {
printf("%d\t%s\n", tmp, view->buffer[tmp]);
}
}
break;
case 'L':
// list directory sans numbers
for(tmp = 0; tmp < view->count ; tmp++) {
printf("%s\n", view->buffer[tmp]);
}
break;
case 'p':
// pretty print
for(tmp = 0; tmp < view->count; tmp++) {
if(view->offsets != nil) {
printf("%d\t%s\n", view->offsets[tmp], view->buffer[tmp]);
} else {
printf("%d\t%s\n", tmp, view->buffer[tmp]);
}
}
break;
case 'P':
// pretty print sans numbers
for(tmp = 0; tmp < view->count ; tmp++) {
printf("%s\n", view->buffer[tmp]);
}
break;
case 'c': // create a file
fp = fopen(com.arg, "w");
if(fp == nil) {
printf("could not create file \"%s\"\n", com.arg);
} else {
fclose(fp);
fp = nil;
if(view != curbuf) {
cleancache(view);
}
dtmp = cachedirectory(curdir);
cleancache(curbuf);
curbuf = dtmp;
view = dtmp;
}
break;
case 'C': // create a directory
tmp = mkdir(com.arg, 0700);
if(tmp == -1) {
printf("could not create directory \"%s\"\n", com.arg);
} else {
if(view != curbuf) {
cleancache(view);
}
dtmp = cachedirectory(curdir);
cleancache(curbuf);
curbuf = dtmp;
view = dtmp;
}
break;
case 'f': // only print files
break;
case 'F': // pretty print only files
break;
case 'd': // only print directories
break;
case 'D': // pretty print only directories
break;
case '!': // execute a shell command w/ args
break;
case 'e': // invoke $EDITOR on arguments
break;
case 'E': // ignore $EDITOR, invoke built-in ed(1)
break;
case 't': // test/[-like interface
break;
case 'm': // more, like the pager
break;
case 'M': // mode, like chmod
break;
default:
printf("?\n");
break;
}
if(view != curbuf) {
cleancache(view);
}
}
}
cleancache(curbuf);
return 0;
}
int
parse(Command *com, const char *buffer, int len) {
int start = -1, stop = -1, offset = 0, state = 0, goffset = 0;
com->glob_len = 0;
com->glob[0] = nul;
com->arg[0] = nul;
while(offset < len) {
switch(state) {
case 0:
if(isalpha(buffer[offset])) {
com->start = start;
com->stop = stop;
com->cmd = buffer[offset];
// +2, so as to skip the ' '
strlcpy(com->arg, &buffer[offset + 2], 256);
return 0;
} else if(buffer[offset] >= '0' && buffer[offset] <= '9') {
start = buffer[offset] - 48;
state = 1;
} else if(buffer[offset] == '$') {
start = INT_MAX;
state = 1;
} else if(buffer[offset] == '(') {
state = 3;
} else {
return 1;
}
offset++;
break;
case 1:
if(buffer[offset] >= '0' && buffer[offset] <= '9') {
start = (start * 10) + (buffer[offset] - 48);
offset++;
} else if(buffer[offset] == ',') {
state = 2;
stop = 0;
offset++;
} else if(buffer[offset] == '(') {
state = 3;
offset++;
} else {
state = 0;
}
break;
case 2:
if(buffer[offset] >= '0' && buffer[offset] <= '9') {
stop = (stop * 10) + (buffer[offset] - 48);
offset++;
} else if(buffer[offset] == '$') {
stop = INT_MAX;
state = 0;
offset++;
} else if(isalpha(buffer[offset])) {
state = 0;
} else if(buffer[offset] == '(') {
state = 3;
offset++;
}
break;
case 3:
if(buffer[offset] == ')') {
state = 0;
com->glob[goffset] = nul;
com->glob_len = goffset;
} else if(buffer[offset] == '\\') {
// XXX not safe; could end a line with an escape
com->glob[goffset] = buffer[offset + 1];
offset += 1;
} else {
com->glob[goffset] = buffer[offset];
}
goffset += 1;
offset += 1;
break;
}
}
return 0;
}
DirectoryBuffer *
cachedirectory(char *directory) {
DIR *dtmp = opendir(directory);
DirectoryBuffer *ret = nil;
char **newstack = nil;
struct dirent *fp = nil;
size_t count = 0;
if(dtmp == nil) {
return nil;
}
/* this is a *really* funny way
* of avoiding complex allocations
* buuuuuuut... we can avoid or
* elide doing a ton of allocations
* for some sort of stack by reading
* the directory *twice*. Probably
* should look at the impact of that
*/
while(1) {
fp = readdir(dtmp);
if(fp == nil) {
break;
}
count += 1;
}
rewinddir(dtmp);
newstack = (char **)malloc(sizeof(char *) * count);
for(int idx = 0; idx < count; idx++) {
fp = readdir(dtmp);
/* technically, something else could
* have removed a file or the like
* from this directory; avoid a
* TOCTOU bug here by not assuming
* that the count above == count now.
* We, of course, run ths risk that
* we miss a new file now. That's the
* idea with recaching ever N operations
* as well too.
*/
if(fp == nil) {
break;
}
newstack[idx] = strdup(fp->d_name);
}
closedir(dtmp);
ret = (DirectoryBuffer *)malloc(sizeof(DirectoryBuffer));
ret->count = count;
ret->offsets = nil;
ret->buffer = newstack;
return ret;
}
DirectoryBuffer *
filtercache(Command *com, DirectoryBuffer* dirb) {
int len = 0, end = 0, count = 0, start = 0;
DirectoryBuffer *ret = nil;
if(com->start == -1 && com->stop == -1) {
return dirb;
} else if(com->start != -1 && com->stop == -1) {
if(com->start > dirb->count) {
return nil;
}
ret = (DirectoryBuffer *)malloc(sizeof(DirectoryBuffer));
ret->count = 1;
ret->offsets = (int *)malloc(sizeof(int) * 1);
ret->offsets[0] = com->start;
ret->buffer = (char **)malloc(sizeof(char *));
ret->buffer[0] = strdup(dirb->buffer[com->start]);
} else {
ret = (DirectoryBuffer *)malloc(sizeof(DirectoryBuffer));
end = com->stop > dirb->count ? dirb->count : com->stop;
start = com->start;
ret->count = end - start;
count = ret->count;
ret->offsets = (int *)malloc(sizeof(int) * count);
ret->buffer = (char **)malloc(sizeof(char *) * count);
for(int idx = 0; idx < count; idx++) {
ret->offsets[idx] = idx + start;
ret->buffer[idx] = strdup(dirb->buffer[idx + start]);
}
}
return ret;
}
void
cleancache(DirectoryBuffer *dirb) {
if(dirb->offsets != nil) {
free(dirb->offsets);
}
for(size_t idx = 0; idx < dirb->count; idx++) {
if(dirb->buffer[idx] != nil) {
free(dirb->buffer[idx]);
}
}
free(dirb->buffer);
/* I wonder if we should **actually**
* do this; could refactor and reuse these
* objects more frequently... that
* actually might make more sense.
*/
free(dirb);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment