homely.files
============


.. _homely-files-blockinfile:

homely.files.blockinfile()
--------------------------

``blockinfile()`` will add a several line of text to a target file in a single block. The lines of text are surrounded by ``prefix`` and ``suffix`` lines so that the block can be maintained by **homely** over time.

``blockinfile(filename, contents, where=None, *, prefix=None, suffix=None)``

``filename``
    The name of the file to modify. If ``filename`` begins with a ``/`` it will be treated as an absolute path, otherwise it is assumed to be relative to ``$HOME``.  You can also use ``~`` and environment variables like ``$HOME`` directly in the filename string.
``contents``
    A sequence of strings representing the lines of text to add to the file. The individual lines must not contain ``\n`` or ``\r``.
``where``
    One of ``None``, ``homely.files.WHERE_TOP``, ``homely.files.WHERE_BOT`` or
    ``homely.files.WHERE_ANY``. If set to ``None`` the behaviour of
    ``homely.files.WHERE_ANY`` will be used.
``prefix=None``
    A string containing a line of text which is guaranteed to be unique in the file and can be used to determine where the block starts.
``suffix=None``
    A string containing a line of text which is guaranteed to be unique in the file and can be used to determine where the block ends.

If ``prefix=None`` or ``suffix=None`` then a prefix or suffix (respectively) will be generated for you automatically. The generated line will start with ``#`` since this represents a comment in most config file languages. If the target file is named ``.vimrc`` or ends in ``.vim``, the vim comment character ``"`` will be used instead.

``block()`` is idempotent and will also make sure there is only one occurrence of the ``prefix...suffix`` block in the file, at the location specified by ``where``:

``where=homely.files.WHERE_TOP``
    The block will be added or moved to the top of the file.
``where=homely.files.WHERE_BOT``
    The block will be added or moved to the bottom of the file.
``where=homely.files.WHERE_ANY`` or ``where=None``
    The block will be added to the bottom of the file, but if it is already present in the file it will be left wherever it is. If there are multiple occurrences of the block already in the file, the first will be kept and subsequent occurrences will be removed.


Examples
^^^^^^^^

You want to include `powerline` config in your ``.tmux.conf``, but the powerline config file is in different locations depending on the host operating system. You could find the location of the powerline module programmatically and then use blockinfile() to insert the config into your ``.tmux.conf`` using ``blockinfile()``::

    from os.path import dirname
    from homely.files import blockinfile, WHERE_TOP
    from homely.system import execute

    # use python to import powerline and find out where it is installed
    cmd = ['python', '-c', 'import powerline; print(powerline.__file__)']
    stdout = execute(cmd, stdout=True)[1]
    powerline_pkg = dirname(stdout.strip().decode('utf-8'))
    lines = [
        'run-shell "powerline-daemon --replace -q"',
        'source "{}/bindings/tmux/powerline.conf"'.format(powerline_pkg),
    ])
    blockinfile('.tmux.conf', lines, WHERE_BOT)


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If homely does modify a file using ``blockinfile()``, it will remember this
fact so that it can [possibly] perform automatic cleanup in the future. Each
time you run :any:`homely-update` homely will check to see if ``blockinfile()``
was called with the same prefix/suffix combination, and if it wasn't then the
block will be removed from the file. This means that you don't need to remember
to remove the lines you aren't using any more -- simply remove the call to
``blockinfile()`` and homely will clean it up for you.

**Note:** after cleaning up a ``blockinfile()`` section, **homely** will re-run all ``lineinfile()`` and ``blockinfile()`` functions that targetted that file. This ensures that when a block is removed from a file, it won't accidentally remove something that was still wanted by a ``lineinfile()``.
See :ref:`cleaning_modified_files` for more information about this feature.

.. _homely-files-download:

homely.files.download()
-----------------------

``download()`` will download a single file from a target URL.

``download(url, dest, expiry=None)``

``url``
    The URL of the file to be downloaded.
``dest``
    Path where the file should be downloaded to. If ``dest`` begins with a
    ``/`` it will be treated as an absolute path, otherwise it is assumed to be
    relative to ``$HOME``.  You can also use ``~`` and environment variables
    like ``$HOME`` directly in the path string.
``expiry``
    The file will be downloaded again when the local copy is ``<expiry>``
    seconds old. When ``expiry=0`` the file will be downloaded every time you
    run :any:`homely-update`. When ``expiry=-1`` the file will never be
    downloaded again. When ``expiry=None`` it will default to ``60*60*24*14``
    (2 weeks).


Examples
^^^^^^^^

Download git completion script for bash::

    from homely.files import download

    url = 'https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash'
    download(url, '~/src/git-completion.bash')


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If homely creates the file at ``dest``, it will remember this fact so that it can
[possibly] perform automatic cleanup in the future. Each time you run ``homely
update`` homely will check to see if ``download()`` was called with the same
``dest``, and if it wasn't then the file will be removed.
See :ref:`automatic_cleanup` for more information.


.. _homely-files-lineinfile:

homely.files.lineinfile()
-------------------------

``lineinfile()`` will add a single line of text to a target file.

``lineinfile(filename, contents, where=None)``

``filename``
    The name of the file to modify. If ``filename`` begins with a ``/`` it
    will be treated as an absolute path, otherwise it is assumed to be relative
    to ``$HOME``.  You can also use ``~`` and environment variables
    like ``$HOME`` directly in the filename string.
``contents``
    The line of text to add to the file. ``contents`` must not contain ``\n`` or ``\r``.
``where``
    One of ``None``, ``homely.files.WHERE_TOP``, ``homely.files.WHERE_BOT`` or
    ``homely.files.WHERE_ANY``. If set to ``None`` the behaviour of
    ``homely.files.WHERE_ANY`` will be used.

``lineinfile()`` is idempotent and will also make sure there is only one
occurrence of the line contents in the file, at the location specified by
``where``:

``where=homely.files.WHERE_TOP``
    The line will be added or moved to the top of the file.
``where=homely.files.WHERE_BOT``
    The line will be added or moved to the bottom of the file.
``where=homely.files.WHERE_ANY`` or ``where=None``
    The line will be added to the bottom of the file, but if it is already
    present in the file it will be left wherever it is. If there are multiple
    occurrences of the line already in the file, the first will be kept and
    subsequent occurrences will be removed.


Examples
^^^^^^^^

Use ``lineinfile()`` to add a line to the end of your ``.bashrc``::

    from homely.files import lineinfile, WHERE_BOT
    lineinfile('.bashrc', 'PATH=$HOME/dotfiles/bin:$PATH', WHERE_BOT)

Use ``lineinfile()`` to add a line to the top of your ``~/.vimrc`` which
sources a shared vimrc inside your dotfiles repo::

    from homely.files import lineinfile, WHERE_TOP
    lineinfile('~/.vimrc', 'source $HOME/dotfiles/vimrc.vim', WHERE_TOP)


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If homely does modify a file using ``lineinfile()``, it will remember this fact
so that it can [possibly] perform automatic cleanup in the future. Each time
you run :any:`homely-update` homely will check to see if ``lineinfile()`` was
called with the same arguments, and if it wasn't then the line will be removed
from the file. This means that you don't need to remember to remove the lines
you aren't using any more -- simply remove the call to ``lineinfile()`` and
homely will clean it up for you.

**Note:** after cleaning up line added by a ``lineinfile()`` that is no longer present, **homely** will re-run all ``lineinfile()`` and ``blockinfile()`` functions that targetted that file. This ensures that when a line is removed from a file, it won't accidentally remove something that was still wanted by another ``lineinfile()`` or ``blockinfile()``.  See :ref:`cleaning_modified_files` for more information about this feature.


homely.files.mkdir()
--------------------

``mkdir()`` will create the nominated directory if it doesn't already exist.

``mkdir(path)``

``path``
    The path to be created. If ``path`` begins with a ``/`` it will be treated
    as an absolute path, otherwise it is assumed to be relative to ``$HOME``.
    You can also use ``~`` and environment variables like ``$HOME`` directly in
    the path string.

Examples
^^^^^^^^

Different ways to create ``~/bin`` directory::

    from homely.files import mkdir

    # absolute path
    mkdir('/home/peter/bin')

    # path implicitly relative to $HOME
    mkdir('bin')

    # "~" expansion works
    mkdir('~/bin')

    # Environment variables are also expanded
    mkdir('$HOME/bin')


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If homely does create the directory, it will remember this fact so that it can
[possibly] perform automatic cleanup in the future. Each time you run
:any:`homely-update` homely will check to see if ``mkdir()`` was called, and if
it wasn't then the directory will be removed. This means that you don't need to
remember to delete directories you aren't using any more - simply remove the
call to ``mkdir()`` and homely will clean it up for you. Note that the
directory *won't* be cleaned up if it is still in use.  See
:ref:`automatic_cleanup` for more information.


homely.files.symlink()
----------------------

``symlink()`` will create a symlink if it doesn't already exist.

``symlnk(target, linkname=None)``

``target``
    The file or directory to symlink to. Typically this will be the name of a
    file in your dotfiles repo. If ``target`` begins with a ``/`` it
    will be treated as an absolute path, otherwise it is assumed to be relative
    to the current dotfiles repo. You can also use ``~`` and environment
    variables like ``$HOME`` directly in the target string.
``linkname``
    Where to create the symlink. If this parameter is omitted, it will default
    to ``$HOME+basename(target)``. E.g., if ``target`` was ``'.bashrc'``, then
    ``linkname`` would default to ``'~/.bashrc'``. If ``linkname`` begins with
    a ``/`` it will be treated as an absolute path, otherwise it is assumed to
    be relative to ``$HOME``. You can also use ``~`` and environment variables
    like ``$HOME`` directly in the target string.


Examples
^^^^^^^^

Create a symlink to ``~/.bashrc`` to ``[dotfiles]/shell/.bashrc``::

    from homely.files import symlink

    # absolute linkname
    symlink('shell/.bashrc', '/home/peter/.bashrc')

    # linkname implicitly relative to $HOME
    symlink('shell/.bashrc', '.bashrc')

    # automatic linkname=$HOME+basename(target)
    symlink('shell/.bashrc')


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If homely creates the symlink, it will remember this fact so that it can
[possibly] perform automatic cleanup in the future. Each time you run
:any:`homely-update` homely will check to see if ``symlink()`` was called with
the same target/linkname, and if it wasn't then the symlink will be removed.
This means that you don't need to remember to delete symlinks you aren't using
any more - simply remove the call to ``symlink()`` and homely will clean it up
for you.  Note that the symlink *won't* be cleaned up if it has been modified
by something other than homely, or replaced with a regular file or directory.
See :ref:`automatic_cleanup` for more information.


homely.files.writefile()
----------------------

``writefile()`` gives you a file handle for creating or overwriting a file on disk.

``writefile(filename)``

``filename``
    The name of the file to create or overwrite. If ``filename`` begins with a ``/`` it will be
    treated as an absolute path, otherwise it is assumed to be relative to ``$HOME``.  You can also
    use ``~`` and environment variables like ``$HOME`` directly in the filename string.


Examples
^^^^^^^^

Write out a config file for the `Composer <https://getcomposer.org/>`_ dependency manager for PHP.
An additional private repository will be conditionally added to the config JSON if the user answers
``Y`` when asked.::

    import json

    from homely.files import mkdir, writefile

    # base config
    composer_config = {
        'minimum-stability': 'dev',
        'prefer-stable': True,
    }

    # optional extra repository
    if yesno('use_my_private_composer_repo', 'Use my private composer repo?'):
        composer_config['repositories'] = [
            {'type': 'composer', 'url': 'http://packages.example.com'}
        ]

    # now write the JSON file
    mkdir('~/.config')
    mkdir('~/.config/composer')
    with writefile('~/.config/composer/composer.json') as f:
        json.dump(composer_config, f)


Automatic Cleanup
^^^^^^^^^^^^^^^^^

If a file doesn't exist and Homely has to create it, then Homely will take ownership of the file and
will [possibly] perform automatic cleanup in the future. Each time you run ``homely update`` homely
will check to see if ``writefile()`` was used to write the same file again, and if it wasn't then
the file will be removed.

But if the file already exists before Homely writes to it for the first time, then homely won't
take ownership of the file and won't automatically remove it aftewards.

See :ref:`automatic_cleanup` for more information.
