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:
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...
}
The File
API strives to be purely consistent on these points:
- Methods that take path parameters accept
String
orFile
instances. - Methods that end in
Path
return aString
. Otherwise they return aFile
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 anerror
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.
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).
Instances of File
provide these readonly properties:
path
- The path to the file as aString
(passed to theconstructor
).extent
- The file's type as aString
(e.g.,'json'
).name
- The file's name as aString
(e.g.,'package.json'
).parent
- TheFile
for the parent directory (null
at root).fspath
- Thepath
string resolved for'~'
(usable byfs
orpath
modules)
The methods that perform work on the path text and return File
instances as a
result are:
absolutify()
- Callspath.resolve(this.path)
join()
- Joins all arguments usingpath.join()
nativize()
- Make all separators native ('\'
on Windows,'/'
elsewhere)normalize()
- Callspath.normalize(this.path)
relativize()
- Callspath.relative()
resolve()
- Callspath.resolve()
on all the argumentsslashify()
- Make all separators'/'
(Windows does understand them)terminate()
- Ensure there is a trailing separatorunterminate()
- Ensure there is no trailing separator
To retrieve strings as a result, you can use these methods:
absolutePath()
- Same asabsolutify
but returns a stringjoinPath()
- Same asjoin
but returns a stringnativePath()
- Same asnativize
but returns a stringnormalizedPath()
- Same asnormalize
but returns a stringrelativePath()
- Same asrelativize
but returns a stringresolvePath()
- Same asresolve
but returns a stringslashifiedPath()
- Same asslashify
but returns a stringterminatedPath()
- Same asterminate
but returns a stringunterminatedPath()
- Same asunterminate
but returns a string
Some path operations perform I/O to the file-system and so provide both synchronous and asynchronous versions.
canonicalize()
- Callsfs.realpathSync(this.path)
and returns aFile
canonicalPath()
- Same ascanonicalize
but returns aString
In asynchronous form:
asyncCanonicalize()
- Same ascanonicalize
but Promises aFile
asyncCanonicalPath()
- Same asasyncCanonicalize
but Promises aString
Canonicalization will result in null
if there is no real file.
Some useful information about a file path:
isAbsolute()
- Returnstrue
if the file an absolute path (path.isAbsolute()
)isRelative()
- Returnstrue
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 ifthis
is less-than, equal or greater-thano
. By default, directories sort before files (first ='d'
). To instead group files before directories, pass'f'
. To compare only paths, passfalse
. in which case files sort before directories.equals(o)
- Returnstrue
ifthis
is equal too
(compare(o) === 0
)prefixes(o)
- Returnstrue
ifthis
is a path prefix ofo
. It is recommended to useabsolutify()
on both instances first to avoid confusion with..
segments.
There are some static sort methods that can be used by Array.sort()
:
File.sorter
- Callsf1.compare(f2, 'd')
to group directories before files.File.sorterFilesFirst
- Callsf1.compare(f2, 'f')
to group files first.File.sorterByPath
- Callsf1.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)
To get information about the file on disk (synchronously):
access()
- Returns aFile.Access
object. If the file does not exist (or some other error is encountered), this object will have anerror
property.can(mode)
- Returnstrue
if this exists with the desired access (mode
is'r'
,'rw'
,'rwx'
,'w'
,'wx'
or'x'
).exists()
- Returnstrue
if the file exists.has(rel)
- Returnstrue
if a file or folder exists at therel
path from this file.hasDir(rel)
- Returnstrue
if a folder exists at therel
path from this file.hasFile(rel)
- Returnstrue
if a file exists at therel
path from this file.isHidden()
- Returnstrue
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()
- Returnsfs.statSync(this.path)
(anfs.Stats
). If the file does not exist (or some other error is encountered), this object will have anerror
property.statLink()
/restatLink()
- Returnsfs.lstatSync(this.path)
(anfs.Stats
). If the file does not exist (or some other error is encountered), this object will have anerror
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 aFile.Access
asyncCan(mode)
- Promisestrue
orfalse
.asyncExists()
- Promisestrue
orfalse
.asyncHas(rel)
- TODOasyncHasDir(rel)
- TODOasyncHasFile(rel)
- TODOasyncIsHidden()
- Promisestrue
orfalse
asyncStat()
/asyncRestat()
- Promises anfs.Stats
viafs.stat()
asyncStatLink()
/asyncRestatLink()
- Promises anfs.Stats
viafs.lstat()
The fs.Stat
structure is augmented
with an attrib
property. This is an instance of File.Attribute
and will have these
boolean properties:
A
- ArchiveC
- CompressedE
- EncryptedH
- HiddenO
- OfflineR
- ReadonlyS
- 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
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 ifR_OK
is set.rw
- True ifR_OK
andW_OK
are both set.rx
- True ifR_OK
andX_OK
are both set.rwx
- True ifR_OK
,W_OK
andX_OK
are all set.w
- True ifW_OK
is set.wx
- True ifW_OK
andX_OK
are both set.x
- True ifX_OK
is set.mask
- The combination offs.constants
flagsR_OK
,W_OK
and/orX_OK
name
- The string'r'
,'rw'
,'rx'
,'rwx'
,'w'
,'wx'
or'x'
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 forisDirectory()
)isSymLink(mode)
(alias forisSymbolicLink()
)
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.
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 isfalse
)d
- List only directories. (default isfalse
)f
- List only files (non-directories). (default isfalse
)l
- Cache the result ofstatLink
for each file. (default isfalse
)o
- Order the items bysorter
. (default istrue
)O
- Order the items bysorterFilesFirst
. (default isfalse
)s
- Cache the result ofstat
for each file. (default isfalse
)w
- Indicates that Windows hidden flag alone determines hidden status (default isfalse
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');
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]$/
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
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 untiltest
passes.upDir(rel)
- Useup()
withhasDir(rel)
as thetest
.upFile(rel)
- Useup()
withhasFile(rel)
as thetest
.
To climb the file-system and find a relatively locatable item:
upTo(rel)
- Starting at this, climb untilhas(rel)
istrue
and then returnjoin(rel)
from that location.upToDir(rel)
- Same asupTo()
but usinghasDir(rel)
as thetest
.upToFile(rel)
- Same asupTo()
but usinghasFile(rel)
as thetest
.
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)
- TODOasyncUpDir(rel)
- TODOasyncUpFile(rel)
- TODOasyncUpTo(rel)
- TODOasyncUpToDir(rel)
- TODOasyncUpToFile(rel)
- TODO
tips(mode, test)
- Returns aFile[]
of the top-most items passing thetest
. Once a match is found, no descent into that folder is made (hence, the "tips" of the sub-tree). Useswalk(mode)
to descend the file-system.walk(mode, matcher, before, after)
- Callsbefore
for all items thatlist(mode, matcher)
generates recursively, then processes those items and lastly callsafter
. Bothbefore
andafter
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 currentFile
being processed.root
- TheFile
used to start the descent.stack
- AFile[]
of instances starting with theFile
used to start things.stop
- A boolean property that can be set totrue
to abort thewalk
.
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.
Basic file reading and decoding/parsing are provided by these methods:
asyncLoad(options)
- Same asload()
except a Promise is returned.load(options)
- Reads, decodes and parses the file according to the providedoptions
.
And serializing, encoding and writing is provided by:
asyncSave(data, options)
- Same assave()
except a Promise is returned.save(data, options)
- Serializes and encodes the data and writes it to this file using the specifiedoptions
.
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.
Readers are objects that manage options for reading and parsing files. The following readers come predefined:
bin
- An alias forbinary
.binary
- Reads a file as a buffer.json
- Extends thetext
reader and provides aparse
method to deserialize JSON data. This uses thejson5
module to tolerate human friendly JSON.json:strict
- Extendstext
reader and uses strictJSON.parse()
.text
- Reads a file asutf8
encoding.txt
- An alias fortext
.
Writers are objects that manage options for serializing and writing files. The following writers come predefined:
bin
- An alias forbinary
.binary
- Writes a file from a buffer.json
- Extends thetext
writer and provides aserialize
method to write JSON data.json5
- Extendsjson
writer and usesjson5.stringify()
.text
- Writes a file asutf8
encoding. Accepts ajoin
string option to join the data if the data is an array (of lines perhaps).txt
- An alias fortext
.
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
andreader
. Thedata
parameter is the file's content and thereader
is the fully configuredreader
instance.split
- An optionalRegExp
orString
for a call toString.split()
. This is used by the defaultparse
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'
}
});
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
andwriter
. Thedata
parameter is the raw file data and thewriter
is the fully configuredwriter
instance.join
- An optionalString
for a call toArray.join()
when file data is an array. This is used by the defaultserialize
method.
The json
writer also supports these properties:
indent
maps to thespace
parameter forJSON.stringify
.replacer
map to thereplacer
parameter forJSON.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'
}
});
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
});
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;
access(fs)
- Returns aFileAccess
for theFile
orString
.exists(fs)
- Returns true if theFile
orString
exists.isDir(fs)
- Returns true if theFile
orString
is an existing directory.isFile(fs)
- Returns true if theFile
orString
is an existing file.join(fs...)
- Returnpath.join()
on theFile
orString
args as aFile
.joinPath(fs...)
- Returnpath.join()
on theFile
orString
args as aString
.resolve(fs...)
- Returnpath.resolve()
on theFile
orString
args as aFile
.resolvePath(fs...)
- Returnpath.resolve()
on theFile
orString
args as aString
.split(fs)
- Returns aString[]
from theFile
orString
.stat(fs)
- Returns thestat()
for theFile
orString
.sorter(fs1, fs2)
- CallsFile.from(fs1).compare(fs2)
(useful for sortingFile[]
andString[]
).
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 => {
...
});
cwd()
- Wrapsprocess.cwd()
as aFile
.home()
- Wrapsos.homedir()
as aFile
.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. Theopts
can be a replacement PATH or an object with apath
property that is the replacement. The asynchronous form isasyncWhich(name,opts)
.
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.
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.
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
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', '.' ]);
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
.
Phylo owes many thanks to the following modules and, of course, their authors and maintainers (in alphabetic order):
- fswin npm / GitHub
- json5 npm / GitHub
- mkdirp npm / GitHub
- rimraf npm / GitHub
- tmp npm / GitHub
- which npm / GitHub
For some opinions on when to use async methods see here.
Enjoy!
Copyright (c) 2016 Donald Griffin