Locations of hypothetical or real directories in the filing system.

§1. About pathnames. We use the word "pathname" to mean a file-system location of a directory, and "filename" to mean a location of a file. For example:


is a pathname, whereas


is a filename. All references to directory locations in the filing system will be held internally as pathname objects, and all references to file locations as filename objects. Once created, these are never destroyed or modified, so that it's safe to store a pointer to a pathname or filename anywhere.

Note that a pathname may well be hypothetical, that is, it may well describe a directory which doesn't exist on disc.

A full path is a linked list, but reverse-ordered: thus,


would be represented as a pointer to the pathname for "Documents", which in turn points to one for "rblackmore", which in turn points to "/Users". Thus the root of the filing system is represented by the null pointer.

Each pathname can represent only a single level in the hierarchy, and its textual name is not allowed to contain the FOLDER_SEPARATOR character, with just one exception: the pathname at the end of the chain is allowed to begin with FOLDER_SEPARATOR to denote that it's at the root of the host file system.

typedef struct pathname {
    struct text_stream *intermediate;
    struct pathname *pathname_of_parent;
    int known_to_exist;  corresponds to a directory in the filing system
} pathname;

§2. Home directory. We get the path to the user's home directory from the environment variable HOME, if it exists.

pathname *home_path = NULL;
void Pathnames::start(void) {
    char *home = (char *) (Platform::getenv("HOME"));
    if (home) {
        text_stream *H = Str::new_from_locale_string(home);
        home_path = Pathnames::from_text(H);
        home_path->known_to_exist = TRUE;

§3. Installation directory.

pathname *installation_path = NULL;
void Pathnames::set_installation_path(pathname *P) {
    installation_path = P;
pathname *Pathnames::installation_path(const char *V, text_stream *def) {
    if (installation_path) return installation_path;
    inchar32_t where[4*MAX_FILENAME_LENGTH];
    where[0] = 0;
    Platform::where_am_i(where, 4*MAX_FILENAME_LENGTH);
    if (where[0]) {
        text_stream *v = Str::new_from_wide_string(where);
        filename *F = Filenames::from_text(v);
        pathname *P = Filenames::up(F);
        if ((P) && (Str::eq(P->intermediate, I"Tangled")))
            P = P->pathname_of_parent;
        return P;
    if (V) {
        char *val = Platform::getenv(V);
        if ((val) && (val[0])) {
            text_stream *v = Str::new_from_locale_string(val);
            return Pathnames::from_text(v);
    if (def) return Pathnames::from_text(def);
    return NULL;

§4. Creation. A subdirectory is made by taking an existing pathname (or possible NULL) and then going one level deeper, using the supplied name.

pathname *Pathnames::down(pathname *P, text_stream *dir_name) {
    return Pathnames::primitive(dir_name, 0, Str::len(dir_name), P);

pathname *Pathnames::primitive(text_stream *str, int from, int to, pathname *par) {
    pathname *P = CREATE(pathname);
    P->pathname_of_parent = par;
    P->known_to_exist = FALSE;
    if (to-from <= 0) internal_error("empty intermediate pathname");
    P->intermediate = Str::new_with_capacity(to-from+1);
    if (str)
        for (int i = from; i < to; i++)
            PUT_TO(P->intermediate, Str::get(Str::at(str, i)));
    return P;

§5. Text to pathnames. The following takes a text of a name and returns a pathname, possibly relative to the home directory. Empty directory names are ignored except possibly for an initial slash, so for example paris/roubaix, paris//roubaix and paris/roubaix/ are indistinguishable here, but /paris/roubaix is different.

pathname *Pathnames::from_text(text_stream *path) {
    return Pathnames::from_text_relative(NULL, path);

pathname *Pathnames::from_text_relative(pathname *P, text_stream *path) {
    pathname *at = P;
    int i = 0, pos = 0;
    if ((Str::get(Str::start(path))) && (P == NULL)) i++;
    for (; i < Str::len(path); i++)
        if (Platform::is_folder_separator(Str::get(Str::at(path, i)))) {
            if (i > pos) at = Pathnames::primitive(path, pos, i, at);
            pos = i+1;
    if (i > pos) at = Pathnames::primitive(path, pos, i, at);
    return at;

§6. Writer. Conversely, by the miracle of depth-first recursion:

void Pathnames::writer(OUTPUT_STREAM, char *format_string, void *vP) {
    pathname *P = (pathname *) vP;
    inchar32_t divider = FOLDER_SEPARATOR;
    if (format_string[0] == '/') divider = '/';
    if (P) Pathnames::writer_r(OUT, P, divider); else WRITE(".");

void Pathnames::writer_r(OUTPUT_STREAM, pathname *P, inchar32_t divider) {
    if (P->pathname_of_parent) {
        Pathnames::writer_r(OUT, P->pathname_of_parent, divider);
    WRITE("%S", P->intermediate);

§7. Relative pathnames. Occasionally we want to shorten a pathname relative to another one: for example,


relative to


would be


If the two pathnames are the same, the relative pathname is the empty text, and so nothing is output.

Note that this does not correctly handle symlinks, ., .. and so on, so it's probably not wise to use it with filenames typed in at the command line.

void Pathnames::to_text_relative(OUTPUT_STREAM, pathname *P, pathname *R) {
    WRITE_TO(rt, "%p", R);
    WRITE_TO(pt, "%p", P);
    int n = Str::len(pt);
    if ((Str::prefix_eq(rt, pt, n)) && (Platform::is_folder_separator(Str::get_at(rt, n)))) {
        Str::delete_n_characters(rt, n+1);
        WRITE("%S", rt);
    } else if (Str::eq(rt, pt) == FALSE)
        internal_error("pathname not relative to pathname");

void Pathnames::to_text_relative_forward_slashed(OUTPUT_STREAM, pathname *P, pathname *R) {
    WRITE_TO(rt, "%/p", R);
    WRITE_TO(pt, "%/p", P);
    int n = Str::len(pt);
    if ((Str::prefix_eq(rt, pt, n)) && (Str::get_at(rt, n) == '/')) {
        Str::delete_n_characters(rt, n+1);
        WRITE("%S", rt);
    } else if (Str::eq(rt, pt) == FALSE)
        internal_error("pathname not relative to pathname");

pathname *Pathnames::up(pathname *P) {
    if (P == NULL) internal_error("can't go up from root directory");
    return P->pathname_of_parent;

text_stream *Pathnames::directory_name(pathname *P) {
    if (P == NULL) return NULL;
    return P->intermediate;

§8. Relative URLs. Suppose a web page in the directory at from wants to link to a page in the directory to. The following composes a minimal-length URL to do so: possibly, if they are in fact the same directory, an empty one.

void Pathnames::relative_URL(OUTPUT_STREAM, pathname *from, pathname *to) {
    int found = FALSE;
    for (pathname *P = to; P && (found == FALSE); P = Pathnames::up(P)) {
        WRITE_TO(PT, "%p", P);
        int q_up_count = 0;
        for (pathname *Q = from; Q && (found == FALSE); Q = Pathnames::up(Q)) {
            WRITE_TO(QT, "%p", Q);
            if (Str::eq(PT, QT)) {
                for (int i=0; i<q_up_count; i++) WRITE_TO(url, "../");
                WRITE_TO(FPT, "%p", to);
                Str::substr(url, Str::at(FPT, Str::len(PT) + 1), Str::end(FPT));
                found = TRUE;
    if (found == FALSE) {
        for (pathname *Q = from; Q; Q = Pathnames::up(Q)) WRITE_TO(url, "../");
        WRITE_TO(url, "%p", to);
    WRITE("%S", url);
    if ((Str::len(url) > 0) && (Str::get_last_char(url) != '/')) WRITE("/");

§9. Existence in the file system. Just because we have a pathname, it doesn't follow that any directory exists on the file system with that path.

int Pathnames::create_in_file_system(pathname *P) {
    if (P == NULL) return TRUE;  the root of the file system always exists
    if (P->known_to_exist) return TRUE;
    char transcoded_pathname[4*MAX_FILENAME_LENGTH];
    WRITE_TO(pn, "%p", P);
    Str::copy_to_locale_string(transcoded_pathname, pn, 4*MAX_FILENAME_LENGTH);
    P->known_to_exist = Platform::mkdir(transcoded_pathname);
    return P->known_to_exist;

§10. Directory synchronisation. Both pathnames here represent directories which do exist. The function makes the dest tree an exact copy of the source tree (and therefore deletes anything different which was originally in dest).

int Pathnames::rsync(pathname *source, pathname *dest) {
    char transcoded_source[4*MAX_FILENAME_LENGTH];
    WRITE_TO(pn, "%p", source);
    Str::copy_to_locale_string(transcoded_source, pn, 4*MAX_FILENAME_LENGTH);
    char transcoded_dest[4*MAX_FILENAME_LENGTH];
    WRITE_TO(pn2, "%p", dest);
    Str::copy_to_locale_string(transcoded_dest, pn2, 4*MAX_FILENAME_LENGTH);
    return Platform::rsync(transcoded_source, transcoded_dest);

§11. Moving.

int Pathnames::move_directory(pathname *from, pathname *to) {
    WRITE_TO(from_path, "%p", from);
    WRITE_TO(to_path, "%p", to);
    char from_name_written_out[4*MAX_FILENAME_LENGTH];
    Str::copy_to_locale_string(from_name_written_out, from_path, 4*MAX_FILENAME_LENGTH);
    char to_name_written_out[4*MAX_FILENAME_LENGTH];
    Str::copy_to_locale_string(to_name_written_out, to_path, 4*MAX_FILENAME_LENGTH);
    int rv = Platform::rename_directory(from_name_written_out, to_name_written_out);
    return rv;