howl.io.File

.basename returns the basename of the path

assert.equal 'base.ext', File('/foo/base.ext').basename

.extension returns the extension of the path

assert.equal File('/foo/base.ext').extension, 'ext'
assert.equal File('/foo/base.ex+').extension, 'ex+'

.path returns the path of the file

assert.equal '/foo/base.ext', File('/foo/base.ext').path

.uri returns an URI representing the path

assert.equal File('/foo.txt').uri, 'file:///foo.txt'

.exists returns true if the path exists

File.with_tmpfile (file) -> assert.is_true file.exists

.parent return the parent of the file

assert.equal File('/bin/ls').parent.path, '/bin'

.children returns a table of children

with_tmpdir (dir) ->
  dir\join('child1')\mkdir!
  dir\join('child2')\touch!
  kids = dir.children
  table.sort kids, (a,b) -> a.path < b.path
  assert.same [v.basename for v in *kids], { 'child1', 'child2' }

it '.children_async returns a table of children', (done) ->

howl_async ->
  with_tmpdir (dir) ->
    dir\join('child1')\mkdir!
    dir\join('child2')\touch!
    kids = dir.children_async
    table.sort kids, (a,b) -> a.path < b.path
    assert.same [v.basename for v in *kids], { 'child1', 'child2' }
    done!

.file_type is a string describing the file type

assert.equal 'directory', File('/bin').file_type
assert.equal 'regular', File('/bin/ls').file_type
assert.equal 'special', File('/dev/null').file_type

.writeable is true if the file represents a entry that can be written to

with_tmpdir (dir) ->
  assert.is_true dir.writeable
  file = dir / 'file.txt'
  assert.is_true file.writeable
  file\touch!
  assert.is_true file.writeable

assert.is_false File('/no/such/directory/orfile.txt').writeable

.readable is true if the file represents a entry that can be read

with_tmpdir (dir) ->
  assert.is_true dir.readable
  file = dir / 'file.txt'
  assert.is_false file.readable
  file\touch!
  assert.is_true file.readable

.etag is a string that can be used to check for modification

File.with_tmpfile (file) ->
  assert.is.not_nil file.etag
  assert.equal type(file.etag), 'string'

.modified_at is a the unix time when the file was last modified

File.with_tmpfile (file) ->
  assert.is.not_nil file.modified_at

read(..) is a short hand for doing a read(..) on the Lua file handle

File.with_tmpfile (file) ->
  file.contents = 'first line\n'
  assert.same { 'first', ' line' }, { file\read 5, '*l' }

join() returns a new file representing the specified child

assert.equal File('/bin')\join('ls').path, '/bin/ls'

relative_to_parent() returns a path relative to the specified parent

parent = File '/bin'
file = File '/bin/ls'
assert.equal 'ls', file\relative_to_parent(parent)

is_below(dir) returns true if the file is located beneath <dir>

parent = File '/bin'
assert.is_true File('/bin/ls')\is_below parent
assert.is_true File('/bin/sub/ls')\is_below parent
assert.is_false File('/usr/bin/ls')\is_below parent
assert.equal File.rm, File.delete
assert.equal File.unlink, File.delete

rm_r is an alias for delete_all

assert.equal File.rm_r, File.delete_all

tmpfile()

returns a file instance pointing to an existing file

file = File.tmpfile!
assert.is_true file.exists
file\delete!

with_tmpfile(f)

invokes <f> with the file

f = spy.new (file) ->
  assert.equals 'File', typeof(file)

File.with_tmpfile f
assert.spy(f).was_called(1)

removes the temporary file even if <f> raises an error

local tmpfile
f = (file) ->
  tmpfile = file
  error 'noo'

assert.raises 'noo', -> File.with_tmpfile f
assert.is_false tmpfile.exists

tmpdir()

returns a file instance pointing to an existing directory

file = File.tmpdir!
assert.is_true file.exists
assert.is_true file.is_directory
file\delete_all!

expand_path(path)

expands "~" into the full path of the home directory

assert.equals "#{os.getenv('HOME')}/foo.txt", (File.expand_path '~/foo.txt')
assert.equals "#{os.getenv('HOME')}/foo.txt", (File.expand_path '/blah/~/foo.txt')

handles multiple "~/" by replacing the deepest one

assert.equals "#{os.getenv('HOME')}/foo.txt", (File.expand_path '/a/b/~/c/~/foo.txt')

does not expand "~" when part of another word

assert.equals "/dir~/foo.txt", (File.expand_path '/dir~/foo.txt')

does not expand trailing "~" without "/" suffix

assert.equals "/dir/~", (File.expand_path '/dir/~')

new(p, cwd, opts = {})

accepts a string as denothing a path

File '/bin/ls'

accepts other files as well

f = File '/bin/ls'
f2 = File f
assert.equal f, f2

accepts an optional type specifying the file's type

f = File '/notherenothere', nil, type: File.TYPE_DIRECTORY
assert.is_true f.is_directory

(when <cwd> is specified)

resolves a string <p> relative to <cwd>

assert.equal '/bin/ls', File('ls', '/bin').path

resolves an absolute string <p> as the absolute path

assert.equal '/bin/ls', File('/bin/ls', '/home').path

accepts other Files as <cwd>

assert.equal '/bin/ls', File('ls', File('/bin')).path

.is_absolute

returns true if the given path is absolute

assert.is_true File.is_absolute '/bin/ls'
assert.is_true File.is_absolute 'c:\\\\bin\\ls'

returns false if the given path is absolute

assert.is_false File.is_absolute 'bin/ls'
assert.is_false File.is_absolute 'bin\\ls'

.display_name

is the same as the basename for files

assert.equal 'base.ext', File('/foo/base.ext').display_name

has a trailing separator for directories

assert.equal 'bin/', File('/usr/bin').display_name

.short_path

returns the path with the home directory replace by "~"

assert.equal '~', File(os.getenv('HOME')).short_path
file = File(os.getenv('HOME')) / 'foo.txt'
assert.equal '~/foo.txt', file.short_path

does not replace a directory the home directory is a prefix of directory

home_path = File(os.getenv('HOME')).path .. '-suffix'
home = File(home_path)
file = home / 'foo.txt'
assert.equal home.path, home.short_path
assert.equal file.path, file.short_path

contents

assigning a string writes the string to the file

File.with_tmpfile (file) ->
  file.contents = 'hello world'
  f = io.open file.path
  read_back = f\read '*all'
  f\close!
  assert.equal read_back, 'hello world'

returns the contents of the file

File.with_tmpfile (file) ->
  f = io.open file.path, 'wb'
  f\write 'hello world'
  f\close!
  assert.equal file.contents, 'hello world'

open([mode, function])

(when <function> is nil)

returns a Lua file handle

File.with_tmpfile (file) ->
  file.contents = 'first line\nsecond line\n'
  fh = file\open!
  assert.equal 'first line', fh\read!
  assert.equal 'second line\n', fh\read '*L'
  fh\close!

(when <function> is provided)

it is invoked with the file handle

File.with_tmpfile (file) ->
  file.contents = 'first line\nsecond line\n'
  local first_line
  file\open 'r', (fh) ->
    first_line = fh\read!

  assert.equal 'first line', first_line

returns the returns values of the function

File.with_tmpfile (file) ->
  assert.same { 'callback', nil, 'last' }, { file\open 'r', -> 'callback', nil, 'last' }

closes the file automatically after invoking <function>

File.with_tmpfile (file) ->
  local handle
  file\open 'r', (fh) -> handle = fh
  assert.has_errors -> handle\read!

(.. when <function> raises an error)

propagates that error

File.with_tmpfile (file) ->
  assert.raises 'kaboom', -> file\open 'r', -> error 'kaboom'

still closes the file

File.with_tmpfile (file) ->
  local handle
  pcall -> file\open 'r', (fh) ->
    handle = fh
    error 'kaboom'

  assert.has_errors -> handle\read!

mkdir()

creates a directory for the path specified by the file

File.with_tmpfile (file) ->
  file\delete!
  file\mkdir!
  assert.is_true file.exists and file.is_directory

raises an error if the directory could not be created

assert.has_error -> File('/aksdjskjdgudfkj')\mkdir!

mkdir_p()

creates a directory for the path specified by the file, including parents

File.with_tmpfile (file) ->
  file\delete!
  file = file\join 'sub/foo'
  file\mkdir_p!
  assert.is_true file.exists and file.is_directory

delete()

deletes the target file

File.with_tmpfile (file) ->
  file\delete!
  assert.is_false file.exists

raise an error if the file does not exist

file = File.tmpfile!
file\delete!
assert.error -> file\delete!

delete_all()

raise an error if the file does not exist

File.with_tmpfile (file) ->
  file\delete!
  assert.error -> file\delete!

(for a regular file)

deletes the target file

File.with_tmpfile (file) ->
  file\delete_all!
  assert.is_false file.exists

(for a directory)

deletes the directory and all sub entries

with_tmpdir (dir) ->
  dir\join('child1')\mkdir!
  dir\join('child1/sub_child')\touch!
  dir\join('child2')\touch!
  dir\delete_all!
  assert.is_false dir.exists

touch()

creates the file if does not exist

File.with_tmpfile (file) ->
  file\delete!
  file\touch!
  assert.is_true file.exists

raises an error if the file could not be created

file = File '/no/does/not/exist'
assert.error -> file\touch!

tostring()

returns a string containing the path

File.with_tmpfile (file) ->
  to_s = file\tostring!
  assert.equal 'string', typeof to_s
  assert.equal to_s, file.path

find()

with_populated_dir = (f) ->
  with_tmpdir (dir) ->
    dir\join('child1')\mkdir!
    dir\join('child1/sub_dir')\mkdir!
    dir\join('child1/sub_dir/deep.lua')\touch!
    dir\join('child1/sub_child.txt')\touch!
    dir\join('child1/sandwich.lua')\touch!
    dir\join('child2')\touch!
    f dir

raises an error if the file is not a directory

file = File '/no/does/not/exist'
assert.error -> file\find!

(with no parameters given)

returns a list of all sub entries

with_populated_dir (dir) ->
  files = dir\find!
  table.sort files, (a,b) -> a.path < b.path
  normalized = [f\relative_to_parent dir for f in *files]
  assert.same {
    'child1',
    'child1/sandwich.lua',
    'child1/sub_child.txt',
    'child1/sub_dir',
    'child1/sub_dir/deep.lua',
    'child2'
  }, normalized

(when the sort parameter is given)

returns a list of all sub entries in a pleasing order

with_populated_dir (dir) ->
  files = dir\find sort: true
  normalized = [f\relative_to_parent dir for f in *files]
  assert.same normalized, {
    'child2',
    'child1',
    'child1/sandwich.lua',
    'child1/sub_child.txt',
    'child1/sub_dir',
    'child1/sub_dir/deep.lua',
  }

(when filter: is passed as an option)

excludes files for which <filter(file)> returns true

with_populated_dir (dir) ->
  files = dir\find filter: (file) ->
    file.basename != 'sandwich.lua' and file.basename != 'child1'

  assert.same { 'child1', 'sandwich.lua' }, [f.basename for f in *files]

(when the on_enter parameter is given)

is called once for each directory with the dir and files so far

with_populated_dir (dir) ->
  dirs = {}
  total_files = 0
  dir\find on_enter: (enter_dir, files) ->
    dirs[#dirs + 1] = enter_dir
    total_files = files

  assert.equal 3, #dirs
  assert.equal 6, #total_files
  table.sort dirs
  assert.same {
    dir,
    dir\join('child1'),
    dir\join('child1')\join('sub_dir'),
  }, dirs

(.. and it returns "break")

causes an early return

with_populated_dir (dir) ->
  files, cancelled = dir\find on_enter: (enter_dir, files) ->
    if enter_dir.basename == 'child1'
      return 'break'

  assert.equal true, cancelled
  table.sort files
  assert.same {
    dir\join('child1'),
    dir\join('child2'),
  }, files

find_paths(opts = {})

with_populated_dir = (f) ->
  with_tmpdir (dir) ->
    dir\join('child1')\mkdir!
    dir\join('child1/sub_dir')\mkdir!
    dir\join('child1/sub_dir/deep.lua')\touch!
    dir\join('child1/sub_child.txt')\touch!
    dir\join('child1/sandwich.lua')\touch!
    dir\join('child2')\touch!
    f dir

raises an error if the file is not a directory

file = File '/no/does/not/exist'
assert.error -> file\find_paths!

(with no option specified)

returns a list of all regular and directory sub paths

with_populated_dir (dir) ->
  paths = dir\find_paths!
  table.sort paths
  assert.same {
    'child1/',
    'child1/sandwich.lua',
    'child1/sub_child.txt',
    'child1/sub_dir/',
    'child1/sub_dir/deep.lua',
    'child2'
  }, paths

(with the exclude_directories option specified)

returns a list of all regular sub paths

with_populated_dir (dir) ->
  paths = dir\find_paths exclude_directories: true
  table.sort paths
  assert.same {
    'child1/sandwich.lua',
    'child1/sub_child.txt',
    'child1/sub_dir/deep.lua',
    'child2'
  }, paths

(with the exclude_non_directories option specified)

returns a list of all directory paths

with_populated_dir (dir) ->
  paths = dir\find_paths exclude_non_directories: true
  table.sort paths
  assert.same {
    'child1/',
    'child1/sub_dir/',
  }, paths

(when filter: is passed as an option)

excludes paths for which <filter(path)> returns true

with_populated_dir (dir) ->
  paths = dir\find_paths filter: (path) ->
    not (path\ends_with('sandwich.lua') or path == 'child1/')

  assert.same { 'child1/', 'child1/sandwich.lua' }, paths

(when the on_enter parameter is given)

is called once for each directory with the dir and paths so far

with_populated_dir (dir) ->
  dirs = {}
  total_files = 0
  dir\find_paths on_enter: (enter_dir, files) ->
    dirs[#dirs + 1] = enter_dir
    total_files = files

  assert.equal 3, #dirs
  assert.equal 6, #total_files
  table.sort dirs
  assert.same {
    './',
    'child1/',
    'child1/sub_dir/',
  }, dirs

(.. and it returns "break")

causes an early return

with_populated_dir (dir) ->
  local count
  paths, cancelled = dir\find_paths on_enter: (enter_dir, cur_paths) ->
    if enter_dir == 'child1/'
      count = #cur_paths
      return 'break'

  assert.equal true, cancelled
  assert.equal count, #paths

copy(dest)

copies the given file

with_tmpdir (dir) ->
  a = dir/'a.txt'
  b = dir/'b.txt'
  a.contents = 'hello'
  a\copy b
  assert.same b.contents, 'hello'

  a.contents = 'hello 2'
  assert.has_errors -> a\copy b
  assert.same b.contents, 'hello'

  a\copy b, {'COPY_OVERWRITE'}
  assert.same b.contents, 'hello 2'

meta methods

/ and .. joins the file with the specified argument

file = File('/bin')
assert.equal (file / 'ls').path, '/bin/ls'
assert.equal (file .. 'ls').path, '/bin/ls'

tostring returns the result of File.tostring

file = File '/bin/ls'
assert.equal file\tostring!, tostring file

== returns true if the files point to the same path

assert.equal File('/bin/ls'), File('/bin/ls')