Skip to content

Commit

Permalink
add glob syntax and relative_path
Browse files Browse the repository at this point in the history
  • Loading branch information
tfeldmann committed Jul 6, 2018
1 parent d5fd8bf commit 98d8de0
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 159 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## v1.3
- Glob support in folder configuration.
- New variable {relative_path} is now available in actions.

## v1.2
- Shows the relative path to files in subfolders.

Expand Down
52 changes: 49 additions & 3 deletions docs/page/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Each rule defines ``folders``, ``filters`` (optional) and ``actions``.
Other optional per rule settings:

- ``enabled`` can be used to temporarily disable single rules. Default = true
- ``subfolders`` specifies whether subfolders should be included in the search. Default = false
- ``subfolders`` specifies whether subfolders should be included in the search. Default = false. Only applies if you don't use globstrings in the folders.
- ``system_files`` specifies whether to include system files (desktop.ini, thumbs.db, .DS_Store) in the search. Default = false


Expand All @@ -93,6 +93,51 @@ The easiest way is to define the rules like this:
actions: ...
Globstrings
-----------
You can use globstrings in the folder lists. For example to get all files with filenames ending with ``_ui`` and any file extension you can use:

.. code-block:: yaml
:caption: config.yaml
rules:
- folders:
- '~/Downloads/*_ui.*'
actions:
- Echo: '{path}'
You can use globstrings to recurse through subdirectories (alternatively you can use the ``subfolders: true`` setting as shown below)

.. code-block:: yaml
:caption: config.yaml
rules:
- folders:
- '~/Downloads/**/*.*'
actions:
- Echo: 'base {basedir}, path {path}, relative: {relative_path}'
# alternative syntax
- folders:
- ~/Downloads
subfolders: true
actions:
- Echo: 'base {basedir}, path {path}, relative: {relative_path}'
You can use globstrings to exclude folders based on name.

The following example recurses through all subdirectories in your downloads folder except ``~/Downloads/Software`` and finds files with ending in ``.c`` and ``.h``.

.. code-block:: yaml
:caption: config.yaml
rules:
- folders:
- '~/Downloads/*[!Software]/**/*.[c|h]'
actions:
- Echo: '{path}'
Aliases
-------
Instead of repeating the same folders in each and every rule you can use an alias for multiple folders which you can then reference in each rule.
Expand Down Expand Up @@ -220,13 +265,14 @@ Variable substitution (placeholders)
**You can use placeholder variables in your actions.**

Placeholder variables are used with curly braces ``{var}``.
You always have access to the variables ``{path}`` and ``{basedir}``:
You always have access to the variables ``{path}``, ``{basedir}`` and ``{relative_path}``:

- ``{path}`` -- is the full path to the current file
- ``{basedir}`` -- the current base folder (the base folder is the folder you
specify in your configuration).
- ``{relative_path}`` -- the relative path from ``{basedir}`` to ``{path}``

Use the dot notation to access properties of ``{path}`` and ``{basedir}``:
Use the dot notation to access properties of ``{path}``, ``{basedir}`` and ``{relative_path}``:

- ``{path}`` -- the full path to the current file
- ``{path.name}`` -- the full filename including extension
Expand Down
2 changes: 1 addition & 1 deletion organize/__version__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__title__ = 'organize-tool'
__description__ = 'The file management automation tool.'
__url__ = 'https://github.com/tfeldmann/organize'
__version__ = '1.2'
__version__ = '1.3'
__author__ = 'Thomas Feldmann'
__author_email__ = '[email protected]'
__license__ = 'MIT'
Expand Down
6 changes: 3 additions & 3 deletions organize/actions/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class TemplateAttributeError(Error):

class Action:

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool) -> Optional[Path]:
def run(self, attrs: dict, simulate: bool) -> Optional[Path]:
# if you change the file path, return the new path here
raise NotImplementedError

Expand All @@ -22,9 +22,9 @@ def print(self, msg):
puts('- [%s] %s' % (self.__class__.__name__, msg))

@staticmethod
def fill_template_tags(msg: str, basedir:Path, path: Path, attrs: dict) -> str:
def fill_template_tags(msg: str, attrs: dict) -> str:
try:
return msg.format(basedir=basedir, path=path, **attrs)
return msg.format(**attrs)
except AttributeError as exc:
cause = exc.args[0]
raise TemplateAttributeError(
Expand Down
15 changes: 8 additions & 7 deletions organize/actions/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,29 +76,30 @@ def __init__(self, dest: str, overwrite=False):
self.overwrite = overwrite
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
full_path = fullpath(path)
def run(self, attrs: dict, simulate: bool):
path = attrs['path']
basedir = attrs['basedir']

expanded_dest = self.fill_template_tags(self.dest, basedir, path, attrs)
expanded_dest = self.fill_template_tags(self.dest, attrs)
# if only a folder path is given we append the filename to have the full
# path. We use os.path for that because pathlib removes trailing slashes
if expanded_dest.endswith(('\\', '/')):
expanded_dest = os.path.join(expanded_dest, path.name)

new_path = fullpath(expanded_dest)
if new_path.exists() and not new_path.samefile(full_path):
if new_path.exists() and not new_path.samefile(path):
if self.overwrite:
self.print('File already exists')
Trash().run(basedir, path=new_path, attrs=attrs, simulate=simulate)
Trash().run({'path': new_path}, simulate=simulate)
else:
new_path = find_unused_filename(new_path)

self.print('Copy to "%s"' % new_path)
if not simulate:
self.log.info('Creating folder if not exists: %s', new_path.parent)
new_path.parent.mkdir(parents=True, exist_ok=True)
self.log.info('Copying "%s" to "%s"', full_path, new_path)
shutil.copy2(src=str(full_path), dst=str(new_path))
self.log.info('Copying "%s" to "%s"', path, new_path)
shutil.copy2(src=str(path), dst=str(new_path))

# the next actions should handle the original file
return None
Expand Down
5 changes: 3 additions & 2 deletions organize/actions/echo.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,12 @@ def __init__(self, msg):
self.msg = msg
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
def run(self, attrs: dict, simulate: bool):
path = attrs['path']
self.log.debug(
'Echo msg "%s" for path: "%s" with attrs: "%s"',
self.msg, path, attrs)
full_msg = self.fill_template_tags(self.msg, basedir, path, attrs)
full_msg = self.fill_template_tags(self.msg, attrs)
self.log.info('Console output: %s', full_msg)
self.print('%s' % full_msg)

Expand Down
18 changes: 9 additions & 9 deletions organize/actions/move.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,36 +82,36 @@ def __init__(self, dest: str, overwrite=False):
self.overwrite = overwrite
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
full_path = fullpath(path)
def run(self, attrs: dict, simulate: bool):
path = attrs['path']
basedir = attrs['basedir']

expanded_dest = self.fill_template_tags(
self.dest, basedir, path, attrs)
expanded_dest = self.fill_template_tags(self.dest, attrs)
# if only a folder path is given we append the filename to have the full
# path. We use os.path for that because pathlib removes trailing slashes
if expanded_dest.endswith(('\\', '/')):
expanded_dest = os.path.join(expanded_dest, path.name)

new_path = fullpath(expanded_dest)
new_path_exists = new_path.exists()
new_path_samefile = new_path_exists and new_path.samefile(full_path)
new_path_samefile = new_path_exists and new_path.samefile(path)
if new_path_exists and not new_path_samefile:
if self.overwrite:
self.print('File already exists')
Trash().run(basedir, path=new_path, attrs=attrs, simulate=simulate)
Trash().run({'path': new_path}, simulate=simulate)
else:
new_path = find_unused_filename(new_path)

if new_path_samefile and new_path == full_path:
if new_path_samefile and new_path == path:
self.print('Keep location')
else:
self.print('Move to "%s"' % new_path)
if not simulate:
self.log.info(
'Creating folder if not exists: %s', new_path.parent)
new_path.parent.mkdir(parents=True, exist_ok=True)
self.log.info('Moving "%s" to "%s"', full_path, new_path)
shutil.move(src=str(full_path), dst=str(new_path))
self.log.info('Moving "%s" to "%s"', path, new_path)
shutil.move(src=str(path), dst=str(new_path))
return new_path

def __str__(self):
Expand Down
5 changes: 2 additions & 3 deletions organize/actions/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,17 @@ def __init__(self, code):
self.code = code
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
def run(self, attrs: dict, simulate: bool):
if simulate:
self.print('Code not run in simulation')
else:
path = attrs['path']
self.log.info(
'Executing python script:\n"""\n%s""" with path="%s", args=%s',
self.code, path, attrs)
# local variables for inline function
locals_ = attrs.copy()
locals_['simulate'] = simulate
locals_['path'] = path
locals_['basedir'] = basedir
# replace default print function
globals_ = globals().copy()
globals_['print'] = self.print
Expand Down
18 changes: 9 additions & 9 deletions organize/actions/rename.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,30 +55,30 @@ def __init__(self, name: str, overwrite=False):
self.overwrite = overwrite
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool) -> Path:
full_path = fullpath(path)
expanded_name = self.fill_template_tags(self.name, basedir, full_path, attrs)
new_path = full_path.parent / expanded_name
def run(self, attrs: dict, simulate: bool) -> Path:
path = attrs['path']
expanded_name = self.fill_template_tags(self.name, attrs)
new_path = path.parent / expanded_name

# handle filename collisions
new_path_exists = new_path.exists()
new_path_samefile = new_path_exists and new_path.samefile(full_path)
new_path_samefile = new_path_exists and new_path.samefile(path)
if new_path_exists and not new_path_samefile:
if self.overwrite:
self.print('File already exists')
Trash().run(basedir, path=new_path, attrs=attrs, simulate=simulate)
Trash().run({'path': new_path}, simulate=simulate)
else:
new_path = find_unused_filename(new_path)

# do nothing if the new name is equal to the old name and the file is
# the same
if new_path_samefile and new_path == full_path:
if new_path_samefile and new_path == path:
self.print('Keep name')
else:
self.print('New name: "%s"' % new_path.name)
if not simulate:
self.log.info('Renaming "%s" to "%s".', full_path, new_path)
full_path.rename(new_path)
self.log.info('Renaming "%s" to "%s".', path, new_path)
path.rename(new_path)
return new_path

def __str__(self):
Expand Down
4 changes: 2 additions & 2 deletions organize/actions/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ def __init__(self, cmd: str):
self.cmd = cmd
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
full_cmd = self.fill_template_tags(self.cmd, basedir, path, attrs)
def run(self, attrs: dict, simulate: bool):
full_cmd = self.fill_template_tags(self.cmd, attrs)
self.print('$ %s' % full_cmd)
if not simulate:
# we use call instead of run to be compatible with python < 3.5
Expand Down
9 changes: 4 additions & 5 deletions organize/actions/trash.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import logging

from organize.utils import Path, fullpath
from .action import Action


Expand Down Expand Up @@ -31,10 +30,10 @@ class Trash(Action):
def __init__(self):
self.log = logging.getLogger(__name__)

def run(self, basedir: Path, path: Path, attrs: dict, simulate: bool):
def run(self, attrs: dict, simulate: bool):
path = attrs['path']
from send2trash import send2trash
self.print('Trash "%s"' % path)
if not simulate:
full_path = fullpath(path)
self.log.info('Moving file %s into trash.', full_path)
send2trash(str(full_path))
self.log.info('Moving file %s into trash.', path)
send2trash(str(path))
Loading

0 comments on commit 98d8de0

Please sign in to comment.