Skip to content

EpiphanySoft/phylo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

90 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

phylo

Build Status Dependencies Status npm version MIT Licence

Phylo (pronounced "File-o") is a File operations class designed for maximum convenience and clarity of expression. The primary export of phylo is the File class which is used to wrap a file-system path string.

Consider some examples:

screenshot

The root value is determined by looking for a directory with a '.git' file or folder in it, starting at cwd and climbing up as necessary. When that location is found, it is returned as a File object. Note, this is not the '.git' folder itself, but the folder that contains the '.git' folder (that is, the VCS root).

The pkgFile value is determined in a similar manner but with two differences. The first is that the location for which we are searching must contain a file (not a folder) with the name 'package.json'. Secondly, it is the 'package.json' file that is returned as a File instance, not the location that contained it.

The load() method will read the file and parse the contents into an object (since the file is type '.json').

If you like infinite loops, you can try this on Windows:

var path = require('path');

for (var d = process.cwd(); d; d = path.resolve(d, '..')) {
    // climb up...
}

This innocent loop works on Linux and Mac OS X because path.resolve('/', '..') returns a falsy value. On Windows, however, path.resolve('C:\\', '..') returns... well 'C:\\'!

Compare the above to the same idea using File:

for (var d = File.cwd(); d; d = d.parent) {
    // climb up...
}

Conventions

The File API strives to be purely consistent on these points:

  • Methods that take path parameters accept String or File instances.
  • Methods that end in Path return a String. Otherwise they return a File instance (when paths are involved).
  • Asynchronous methods are named with the "async" prefix and return a Promise.
  • Callbacks passed to async methods can return immediate results or Promises.
  • As much as possible, exceptions and null return values are avoided. For example, stat() returns an object in all cases but that object may have an error property.
  • Where reasonable, objects are cached to avoid GC pressure. For example, things like access masks, file attributes, status errors, directory list modes, etc. are lazily cached as immutable (Object.freeze() enforced) instances and reused as needed.

The conflict between Node.js path and fs API's is a major reason for these naming conventions. Consider:

let s = path.join(process.cwd(), 'foo');   // sync

fs.mkdir(s);  // async!

Using File:

let f = File.cwd().join('foo');   // sync (of course);

f.mkdir();  // also sync

f.asyncMkdir().then(...  // async obviously

It is intended that a File instance immutably describes a single path. What is (or is not) on disk at that location can change of course, but the description is constant.

Path Manipulation

Much of the functionality provided by the File class is in the form of "lexical" path manipulation. These are only provided in synchronous form since they operate on path strings (like the path module).

Properties

Instances of File provide these readonly properties:

  • path - The path to the file as a String (passed to the constructor).
  • extent - The file's type as a String (e.g., 'json').
  • name - The file's name as a String (e.g., 'package.json').
  • parent - The File for the parent directory (null at root).
  • fspath - The path string resolved for '~' (usable by fs or path modules)

Methods

The methods that perform work on the path text and return File instances as a result are:

  • absolutify() - Calls path.resolve(this.path)
  • join() - Joins all arguments using path.join()
  • nativize() - Make all separators native ('\' on Windows, '/' elsewhere)
  • normalize()- Calls path.normalize(this.path)
  • relativize()- Calls path.relative()
  • resolve()- Calls path.resolve() on all the arguments
  • slashify()- Make all separators '/' (Windows does understand them)
  • terminate() - Ensure there is a trailing separator
  • unterminate() - Ensure there is no trailing separator

To retrieve strings as a result, you can use these methods:

  • absolutePath() - Same as absolutify but returns a string
  • joinPath() - Same as join but returns a string
  • nativePath() - Same as nativize but returns a string
  • normalizedPath() - Same as normalize but returns a string
  • relativePath() - Same as relativize but returns a string
  • resolvePath() - Same as resolve but returns a string
  • slashifiedPath() - Same as slashify but returns a string
  • terminatedPath() - Same as terminate but returns a string
  • unterminatedPath() - Same as unterminate but returns a string

Some path operations perform I/O to the file-system and so provide both synchronous and asynchronous versions.

  • canonicalize() - Calls fs.realpathSync(this.path) and returns a File
  • canonicalPath() - Same as canonicalize but returns a String

In asynchronous form:

  • asyncCanonicalize() - Same as canonicalize but Promises a File
  • asyncCanonicalPath() - Same as asyncCanonicalize but Promises a String

Canonicalization will result in null if there is no real file.

Path Info and Comparison

Some useful information about a file path:

  • isAbsolute() - Returns true if the file an absolute path (path.isAbsolute())
  • isRelative() - Returns true if the file a relative path (path.isRelative())

You can compare two paths in a few different ways:

  • compare(o,first) - Returns -1, 0 or 1 if this is less-than, equal or greater-than o. By default, directories sort before files (first = 'd'). To instead group files before directories, pass 'f'. To compare only paths, pass false. in which case files sort before directories.
  • equals(o) - Returns true if this is equal to o (compare(o) === 0)
  • prefixes(o) - Returns true if this is a path prefix of o. It is recommended to use absolutify() on both instances first to avoid confusion with .. segments.

There are some static sort methods that can be used by Array.sort():

  • File.sorter - Calls f1.compare(f2, 'd') to group directories before files.
  • File.sorterFilesFirst - Calls f1.compare(f2, 'f') to group files first.
  • File.sorterByPath - Calls f1.compare(f2, false) to sort only by path.

File name comparisons are case-insensitive on Windows and Mac OS X, so we have

var f1 = File.from('abc');
var f2 = File.from('ABC');

console.log(f1.equals(f2));

> true   (on Windows and Mac)
> false  (on Linux)

File-System Information

To get information about the file on disk (synchronously):

  • access() - Returns a File.Access object. If the file does not exist (or some other error is encountered), this object will have an error property.
  • can(mode) - Returns true if this exists with the desired access (mode is 'r', 'rw', 'rwx', 'w', 'wx' or 'x').
  • exists() - Returns true if the file exists.
  • has(rel) - Returns true if a file or folder exists at the rel path from this file.
  • hasDir(rel) - Returns true if a folder exists at the rel path from this file.
  • hasFile(rel) - Returns true if a file exists at the rel path from this file.
  • isHidden() - Returns true if this file does not exist or is hidden. Note that on Windows, hidden state is not based on a file name convention (".hidden") but is a bit stored in the file-system (see below).
  • stat() / restat() - Returns fs.statSync(this.path) (an fs.Stats). If the file does not exist (or some other error is encountered), this object will have an error property.
  • statLink() / restatLink() - Returns fs.lstatSync(this.path) (an fs.Stats). If the file does not exist (or some other error is encountered), this object will have an error property.

The error property will be a value like 'ENOENT' (for file/folder not found), and 'EACCES' or 'EPERM' for permission denied. These codes come directly from the underlying API.

In asynchronous form:

  • asyncAccess() - Promises a File.Access
  • asyncCan(mode) - Promises true or false.
  • asyncExists() - Promises true or false.
  • asyncHas(rel) - TODO
  • asyncHasDir(rel) - TODO
  • asyncHasFile(rel) - TODO
  • asyncIsHidden() - Promises true or false
  • asyncStat() / asyncRestat() - Promises an fs.Stats via fs.stat()
  • asyncStatLink() / asyncRestatLink() - Promises an fs.Stats via fs.lstat()

File Status

The fs.Stat structure is augmented with an attrib property. This is an instance of File.Attribute and will have these boolean properties:

  • A - Archive
  • C - Compressed
  • E - Encrypted
  • H - Hidden
  • O - Offline
  • R - Readonly
  • S - System

For example:

if (File.cwd().join('package.json').stat().attrib.H) {
    // If the package.json file is hidden... (wat?)
}

Note, if there is no 'package.json' file, the stat() method will return an object with an error property and an empty attrib object (it won't have H set).

The fswin module is used to retrieve this information on Windows. On other platforms, this object contains false values for all of the above properties.

An fs.Stat object is cached on the File instance by the stat() family of methods and a separate instance is cached on by the statLink() family. These are lazily retrieved and then stored for future use. To get fresh copies, use the restat() family of methods.

File.Access

File.Access objects are descriptors of read, write and execute permission masks. These are much simpler to use than the fs.constants.R_OK, fs.constants.W_OK and fs.constants.X_OK bit-masks. For example:

try {
    let mode = fs.statSync(file).mode;
    
    if (mode & fs.constants.R_OK && mode & fs.constants.W_OK) {
        // file exists and is R and W
    }
}
catch (e) {
    // ignore... file does not exist
}

Or using File and File.Access:

if (file.access().rw) {
    // file exists and is R & W
}

Alternatively, there is the can() method:

if (file.can('rw')) {
    // file exists and is R & W
}

When the file does not exist, or an error is encountered, the object returned by the access() method will have an error property. Since the access bits are all false in this case, this distinction if often unimportant (as above).

To check for errors:

var acc = file.accecss();

if (acc.rw) {
    // file exists and has R/W access
}
else if (acc.error === 'ENOENT') {
    // file does not exist...
}
else if (acc.error === 'EACCES' || acc.error === 'EPERM') {
    // access or permission error...
}
...

There are a fixed set of immutable File.Access objects, one for each combination of R, W and X permissions: r, rw, rx, rwx, w, wx, x. Each instance also has these same properties as boolean values. The full set of properties is a bit larger:

  • r - True if R_OK is set.
  • rw - True if R_OK and W_OK are both set.
  • rx - True if R_OK and X_OK are both set.
  • rwx - True if R_OK, W_OK and X_OK are all set.
  • w - True if W_OK is set.
  • wx - True if W_OK and X_OK are both set.
  • x - True if X_OK is set.
  • mask - The combination of fs.constants flags R_OK, W_OK and/or X_OK
  • name - The string 'r', 'rw', 'rx', 'rwx', 'w', 'wx' or 'x'

Classification

It is often important to know if a file is a directory or other type of entity. This information is fundamentally the business of the stat() family but for convenience is also provided on the File instance:

  • isDirectory(mode)
  • isFile(mode)
  • isBlockDevice(mode)
  • isCharacterDevice(mode)
  • isFIFO(mode)
  • isSocket(mode)
  • isSymbolicLink(mode)

In addition, the following shorthand methods are also available:

  • isDir(mode) (alias for isDirectory())
  • isSymLink(mode) (alias for isSymbolicLink())

These are also available as async methods:

  • asyncIsDir(mode)
  • asyncIsDirectory(mode)
  • asyncIsFile(mode)
  • asyncIsBlockDevice(mode)
  • asyncIsCharacterDevice(mode)
  • asyncIsFIFO(mode)
  • asyncIsSocket(mode)
  • asyncIsSymLink(mode)
  • asyncIsSymbolicLink(mode)

The optional mode parameter can be 'l' (lowercase-L) to use the statLink() (or asyncStatLink()) method to determine the result.

Since the nature of a file seldom changes on a whim, these methods use the stat() methods and their cached information. If this is undesired, these results can be refreshed using the restat() family of methods.

Directory Listing

You can get a directory listing of File objects using:

  • list(mode, matcher)
  • asyncList(mode, matcher)

The mode parameter is a string that consists of the following single letter codes with the described meaning:

  • A - All files are listed, even hidden files. (default is false)
  • d - List only directories. (default is false)
  • f - List only files (non-directories). (default is false)
  • l - Cache the result of statLink for each file. (default is false)
  • o - Order the items by sorter. (default is true)
  • O - Order the items by sorterFilesFirst. (default is false)
  • s - Cache the result of stat for each file. (default is false)
  • w - Indicates that Windows hidden flag alone determines hidden status (default is false so that files names starting with dots are hidden on all platforms).
  • T - Throw (or reject) on failure instead of returning (or resolving) null.

Some examples:

// List non-hidden files/folders:
dir.list();

// lists all files/folders (including hidden):
dir.list('A');

// lists non-hidden files/folders and cache stat info:
dir.list('s');

// lists all files (no folders) and cache stat info:
dir.list('Asf');

// lists all files/folders and cache stat info but do not sort:
dir.list('As-o');

The s option can be useful during an asyncList() operation to allow subsequent use of the simpler, synchronous stat() method since it will use the cached stat object.

The matcher can be a function to call for each candidate. This function receives the arguments (name, file). For example:

dir.list(name => {
    return name.endsWith('.txt');
});

dir.list((name, f) => {
    return f.extent === 'txt';  // f is a File instance
});

The matcher can also be a RegExp:

dir.list(/\.txt$/i);

Lastly, matcher can be a "glob" (a shell-like wildcard). In this case, since this is also a string, the mode must be passed first:

dir.list('Af', '*.txt');

Globs

The basic form of globs is a file name and extension pattern (like '*.txt'). The '*' character matches only file name characters and not path separators ('/' and '\' on Windows).

Internally globs are converted into RegExp objects. The conversion of '*.txt' is platform-specific. For Linux, it is:

/^[^/]*\.txt$/

On Windows, it converts to this:

/^[^\\/]*\.txt$/i

This is because Windows uses either '/' and '\' as path separators and filenames are case-insensitive.

To match paths, you can use a "glob star" such as '**/*.txt'. This glob converts to this on Linux:

/^(?:[^/]*(?:[/]|$))*[^/]*\.txt$/

Globs also support groups inside '{' and '}' such as: '*.{txt,js}':

/^[^/]*\.(txt|js)$/

A character set like '*.{txt,js}[abc]' converts to:

/^[^/]*\.(txt|js)[abc]$/

Explicit Glob Conversion

The glob parser has some advanced options via the File.glob() method. The File.glob() method converts a glob string into a RegExp. This conversion can be customized using the second argument as the options. This string can contain any of these characters:

  • C - Case-sensitivity is manual (disables auto-detection by platform)
  • G - Greedy '*' expansion changes '*' to match path separators (i.e., '/')
  • S - Simple pattern mode (disables grouping and character sets)

All other characters are passed as the RegExp flags (e.g., 'i' and 'g').

The 'S' options enables "simple" glob mode which disables groups and character sets. For example:

dir.list(File.glob('*.{txt,js}', 'S'));

== /^[^/]*\.{txt\,js}$/

This would be useful when dealing with files that have '{' in their name.

To force case-sensitive comparison (e.g., on Windows):

let re = File.glob('*.txt', 'C');

/^[^\\/]*\.txt$/

To force case-insensitive comparison (e.g., on Linux), you need to use 'C' to make this a manual choice, and 'i' to make the RegExp ignore case:

let re = File.glob('*.txt', 'Ci');

/^[^/]*\.txt$/i

File-System Traversal

Ascent

To climb the file-system to find a parent folder that passes a test function or has a particular file or folder relatively locatable from there:

  • up(test) - Starting at this, climb until test passes.
  • upDir(rel) - Use up() with hasDir(rel) as the test.
  • upFile(rel) - Use up() with hasFile(rel) as the test.

To climb the file-system and find a relatively locatable item:

  • upTo(rel) - Starting at this, climb until has(rel) is true and then return join(rel) from that location.
  • upToDir(rel) - Same as upTo() but using hasDir(rel) as the test.
  • upToFile(rel) - Same as upTo() but using hasFile(rel) as the test.

The different between these forms can be seen best by example:

var file = File.cwd().up('.git');

// file is the parent directory that has '.git', not the '.git'
// folder itself. The file may be File.cwd() or some parent.

var git = File.cwd().upTo('.git');

// git is the '.git' folder from perhaps File.cwd() or some other
// parent folder.

Asynchronous forms (TODO - not implemented yet):

  • asyncUp(test) - TODO
  • asyncUpDir(rel) - TODO
  • asyncUpFile(rel) - TODO
  • asyncUpTo(rel) - TODO
  • asyncUpToDir(rel) - TODO
  • asyncUpToFile(rel) - TODO

Descent

  • tips(mode, test) - Returns a File[] of the top-most items passing the test. Once a match is found, no descent into that folder is made (hence, the "tips" of the sub-tree). Uses walk(mode) to descend the file-system.
  • walk(mode, matcher, before, after) - Calls before for all items that list(mode, matcher) generates recursively, then processes those items and lastly calls after. Both before and after are optional but one should be provided.

The walk method's before and after handlers looks like this:

function beforeOrAfter (file, state) {
    if (file.isDir() && ...) {
        return false;  // do not recurse into file (before only)
    }
    
    if (...) {
        state.stop = true;  // stop all further walking
    }
}

The optional matcher can be a String or a RegExp and have the same meaning as with list(). The matcher cannot, however, be a function. This is because it would be ambiguous with before and would really offer no advantage over handling things in the before method anyway.

The state object has the following members:

  • at - The current File being processed.
  • root - The File used to start the descent.
  • stack - A File[] of instances starting with the File used to start things.
  • stop - A boolean property that can be set to true to abort the walk.

The tips method's test looks like this:

function test (file, state) {
    if (file.hasFile('package.json')) {
        return true; // file is a tip so gather it up and don't descend
    }
    
    return false; // keep going and/or descending
}

The state parameter is the same as for the walk method.

Asynchronous forms:

  • asyncTips(mode, test)
  • asyncWalk(mode, matcher, before, after)

The test, before and after handlers of the asynchronous methods accept the same parameters and return the same results as with the synchronous forms. They can, alternatively, return a Promise if their determination is also async.

Reading and Writing Files

Basic file reading and decoding/parsing are provided by these methods:

  • asyncLoad(options) - Same as load() except a Promise is returned.
  • load(options) - Reads, decodes and parses the file according to the provided options.

And serializing, encoding and writing is provided by:

  • asyncSave(data, options) - Same as save() except a Promise is returned.
  • save(data, options) - Serializes and encodes the data and writes it to this file using the specified options.

The act of loading a file consists initially of reading the data (obviously). To get this part right, you need an encoding option which is tedious to setup in the fs API, especially if the file name holds the clues you need.

Compare:

var pkg = path.join(dir, 'package.json');

var data = JSON.parse(fs.readfileSync(pkg, 'utf8'));

To loading using File:

var pkg = dir.join('package.json');

var data = pkg.load();

The basic advantage of the File approach is the error messages you get when things go wrong. Using the first snippet you would get errors like these (based on the parser used):

Unexpected number in JSON at position 427

Using load() the message would be:

Cannot parse ~/code/package.json: Unexpected number in JSON at position 427

With File there is hope in tracking down what has gone wrong.

When it is time to save the data, the process looks very symmetric:

pkg.save(data);

Instead of the manual alternative:

fs.writeFileSync(pkg, JSON.stringify(data, null, '    '), 'utf8');

NOTE: Unlike most of the File API, these methods throw exceptions (or reject Promises) on failure.

Predefined Readers

Readers are objects that manage options for reading and parsing files. The following readers come predefined:

  • bin - An alias for binary.
  • binary - Reads a file as a buffer.
  • json - Extends the text reader and provides a parse method to deserialize JSON data. This uses the json5 module to tolerate human friendly JSON.
  • json:strict - Extends text reader and uses strict JSON.parse().
  • text - Reads a file as utf8 encoding.
  • txt - An alias for text.

Predefined Writers

Writers are objects that manage options for serializing and writing files. The following writers come predefined:

  • bin - An alias for binary.
  • binary - Writes a file from a buffer.
  • json - Extends the text writer and provides a serialize method to write JSON data.
  • json5 - Extends json writer and uses json5.stringify().
  • text - Writes a file as utf8 encoding. Accepts a join string option to join the data if the data is an array (of lines perhaps).
  • txt - An alias for text.

Reader Options

The default reader is selected based on the file's type, but we can override this:

var data = pkg.load('text'); // load as a simple text (not parsed)

Other options can be specified (e.g. to split by new-line):

var data = pkg.load({
    type: 'text',
    split: /\n/g
});

Readers support the following configuration properties:

  • parse - A function called to parse the file content. The method accepts two arguments: data and reader. The data parameter is the file's content and the reader is the fully configured reader instance.
  • split - An optional RegExp or String for a call to String.split(). This is used by the default parse method.

In addition to reader configuration, the fs.readFile() options can be supplied:

var content = file.load({
    // The options object is passed directly to fs.readFile()
    options: {
        ...
    }
});

The encoding can be specified in the options or directly to the reader:

var content = file.load({
    encoding: 'utf16'
});

// Or on the fs options:

var content = file.load({
    options: {
        encoding: 'utf16'
    }
});

Writer Options

The default writer is selected based on the file's type, but we can override this:

pkg.save(data, 'text');

Other options can be specified (e.g. to join lines in an array with new-lines):

pkg.save(data, {
    type: 'text',
    join: '\n'
});

Writers support the following configuration properties:

  • serialize - A function called to convert the data and return what will be written to disk. The method accepts two arguments: data and writer. The data parameter is the raw file data and the writer is the fully configured writer instance.
  • join - An optional String for a call to Array.join() when file data is an array. This is used by the default serialize method.

The json writer also supports these properties:

  • indent maps to the space parameter for JSON.stringify.
  • replacer map to the replacer parameter for JSON.stringify.

In addition to writer configuration, the fs.writeFile() options can be supplied:

file.save(data, {
    // The options object is passed directly to fs.writeFile()
    options: {
        ...
    }
});

The encoding can be specified in the options or directly to the writer:

file.save(data, {
    encoding: 'utf16'
});

// Or on the fs options:

file.save(data, {
    options: {
        encoding: 'utf16'
    }
});

Removing Files and Folders

To remove a file or empty folder, you can use remove():

file.remove();

Internally, remove() calls either fs.unlinkSync() or fs.rmdirSync().

A folder tree can be removed by passing the 'r' option:

dir.remove('r');

This will synchronously remove all children of dir and then remove dir itself. Internally, this is handled by rimraf.

The asynchronous form of remove() is:

dir.asyncRemove().then(() => {
    // dir is gone if it was empty
});

Or:

dir.asyncRemove('r').then(() => {
    // dir and its children are gone
});

Static Methods

The most useful static methods are for conversion.

var file = File.from(dir);

Regardless if the value of dir above is a String or File, file is a File instance. If dir is null or '' then file will be null.

In reverse:

var s = File.path(file);

The path() method accepts String or File and returns the path (the original string or the path property of the File). Similar to from(), the path() method returns '' when passed null. That value is still falsy but won't throw null reference errors if used.

There is also fspath() that resolves '~' path elements:

var s = File.fspath(file);

If the argument is already a String it is simply returned (just like the path() method). If the string may contain '~' elements, the safe conversion would be:

var s = File.from(file).fspath;

Utility Methods

  • access(fs) - Returns a FileAccess for the File or String.
  • exists(fs) - Returns true if the File or String exists.
  • isDir(fs) - Returns true if the File or String is an existing directory.
  • isFile(fs) - Returns true if the File or String is an existing file.
  • join(fs...) - Return path.join() on the File or String args as a File.
  • joinPath(fs...) - Return path.join() on the File or String args as a String.
  • resolve(fs...) - Return path.resolve() on the File or String args as a File.
  • resolvePath(fs...) - Return path.resolve() on the File or String args as a String.
  • split(fs)- Returns a String[] from the File or String.
  • stat(fs) - Returns the stat() for the File or String.
  • sorter(fs1, fs2) - Calls File.from(fs1).compare(fs2) (useful for sorting File[] and String[]).

There are no asynchronous forms of these utility methods since they wouldn't really save much:

Since this is not provided:

File.asyncExists(file).then(exists => {
    ...
});

Instead just do this:

File.from(file).asyncExists().then(exists => {
    ...
});

Locating Special Folders

  • cwd() - Wraps process.cwd() as a File.
  • home() - Wraps os.homedir() as a File.
  • profile() - Returns the platform-favored storage folder for app data.
  • temp() / asyncTemp() - Temporary folder for this application.
  • which(name,opts) - Searches the PATH for a program by name. The opts can be a replacement PATH or an object with a path property that is the replacement. The asynchronous form is asyncWhich(name,opts).

Temp

The temp() and asyncTemp() static methods use the tmp module to generate a temporary folder in the appropriate location for the platform.

When these methods are called with no options argument, they lazily create (and cache for future requests) a single temporary folder.

Profile

The profile() method handles the various OS preferences for storing application data.

  • Windows: C:\Users\Name\AppData\Roaming\Company
  • Mac OS X: /Users/Name/Library/Application Support/Company
  • Linux: /home/name/.local/share/data/company
  • Default: /home/name/.company

The "Company" part can be passed as the argument to profile() but is better left to the top-level application to set File.COMPANY.

File.COMPANY = 'Acme';

Now all libraries that use phylo will automatically store their profile data in the proper folder for the user-facing application. In such a scenario it would be wise to use the module name in the filename to ensure no collisions occur.

The Magic Tilde

A common pseudo-root folder for the user's home folder is '~'. One often sees paths like this:

var dir = new File('~/.acme');

The '~' pseudo-root is recognized throughout File methods. It is resolved to the actual location using absolutify() or canonicalize() (or their other flavors). In other cases the pseudo-root is preserved. For example:

var dir = new File('~/.acme');

console.log(dir.parent); // just '~'
console.log(dir.join('foo'));  // ~/acme/foo

These File instances can be read using load() as well:

var data = File.from('~/.acme/settings.json').load();

In addition there is also the '~~/' pseudo-root that maps the the profile() directory instead of the raw homedir.

That is:

File.COMPANY = 'Acme';

console.log(File.from('~/foo').absolutePath());
console.log(File.from('~~/foo').absolutePath());

// Windows:
> C:\Users\MyName\foo
> C:\Users\MyName\AppData\Roaming\Acme\foo

// Mac OS X:
> /Users/MyName/foo
> /Users/MyName/Library/Application Support/foo

Finding Programs

The which() method can be used like the standard which shell command:

let nodejs = File.which('node');

If the program is not found, null is returned. Other problems will result in an thrown Error.

The asynchronous form is similar:

File.asyncWhich('node').then(nodejs => {
    if (nodejs) {
        // found it
    }
    else {
        // not found
    }
},
e => {
    // some error
});

You can also customize the search to your own list of directories:

let app = File.which('app', [ '~/bin', '.' ]);

Creating Directories

You can create a directory structure using the mkdir() method (or asyncMkdir()). These methods create as many directory levels as needed to create the path described by the File instance.

var dir = File.from('~~/foo').mkdir();

The mkdir() method returns the File instance after creating the directory tree.

Unlike many other File methods, if mkdir() fails it will throw an Error.

Credits

Phylo owes many thanks to the following modules and, of course, their authors and maintainers (in alphabetic order):

Conclusion

For some opinions on when to use async methods see here.

Enjoy!

Copyright (c) 2016 Donald Griffin