Skip to content

Conversation

@smemsh
Copy link

@smemsh smemsh commented Aug 29, 2021

Taskwarrior can have 'include' directives in the rcfile which use pathnames relative to the rcfile location. Currently in taskw library, these will fail because the file is opened without attempting to search for it or qualify the path beyond expanding tildes and normalizing the path. It only can work if given as absolute path or if the file is in cwd of process executing the library code.

With this PR, if we try to load the file and it doesn't exist, we will try it again as a path in the rcfile directory. This will work in most cases of a relative path and follows the same algorithm as TaskWarrior itself (partial implementation: TaskWarrior also tries other places, see commit message for details).

Fixes #150

(superseded by #170)

@smemsh
Copy link
Author

smemsh commented Oct 10, 2021

@ralphbean could we merge this trivial patch, so relative includes work in taskrc?

@smemsh smemsh force-pushed the rcfile-include-realpath branch from 8fe01a9 to fa9362b Compare October 10, 2021 16:16
@smemsh smemsh force-pushed the rcfile-include-realpath branch from fa9362b to 49a12f9 Compare May 7, 2022 15:55
@smemsh
Copy link
Author

smemsh commented May 7, 2022

rebased for 1.3.1, looks like @coddingtonbear is the one to merge these?

@coddingtonbear
Copy link
Collaborator

Can, theoretically, but it's going to take some time before I can reasonably have a look at this.

Taskwarrior itself tries includes as absolute path, then cwd, then
relative to rcfile, then in various search paths (see
GothenburgBitFactory/libshared -> src/Configuration.cpp ->
Configuration::parse()).

We won't try to duplicate that whole arrangement here, but at least look
relative to the rcfile in addition to cwd/absolute, like taskwarrior
does.  This will allow specification as relative path in most cases.
Otherwise, we'd have to chdir anyways because includes are always tried
as-is by taskw.  They would only work if specified as absolute paths or
if in cwd

We use a TaskRc() class variable to store the rcdir because there could
be an include chain and all the instances will need to know the same
one, but will be processing different paths, so we have to capture the
directory of the first one processed, ie the base rcfile.

Fixes ralphbean#150
@ryaminal
Copy link

ryaminal commented Jun 5, 2023

Hi all. Wondering if this fix is still up to date or if there is another solution? seeing this problem emerge from bugwarrior.

@RaitoBezarius
Copy link

Hi there, @coddingtonbear is there something we can do to help you review this change? It is a very sharp edge currently with taskw :/.

@RaitoBezarius
Copy link

Here's an alternative patch making use of os.path and fixing the case where TaskRc.rcdir is None.

diff --git c/taskw/taskrc.py w/taskw/taskrc.py
index 1b6f8e5..b72dee6 100644
--- c/taskw/taskrc.py
+++ w/taskw/taskrc.py
@@ -1,5 +1,6 @@
 import logging
 import os
 
 from taskw.fields import (
     ChoiceField,
@@ -39,6 +40,7 @@ class TaskRc(dict):
 
     """
 
+    rcdir = None
     UDA_TYPE_MAP = {
         'date': DateField,
         'duration': DurationField,
@@ -54,6 +56,8 @@ class TaskRc(dict):
                     path
                 )
             )
+            if not self.rcdir:
+                TaskRc.rcdir = os.path.dirname(os.path.realpath(self.path))
             config = self._read(self.path)
         else:
             self.path = None
@@ -92,6 +96,17 @@ class TaskRc(dict):
 
     def _read(self, path):
         config = {}
+        if not os.path.exists(path) and TaskRc.rcdir is not None:
+            # include path may be given relative to dir of rcfile
+            oldpath = path
+            path = os.path.join(TaskRc.rcdir, oldpath)
+            if not os.path.exists(path):
+                logger.error(
+                    "rcfile does not exist, tried %s and %s",
+                    oldpath, path
+                )
+                raise FileNotFoundError
+
         with open(path, 'r') as config_file:
             for raw_line in config_file.readlines():
                 line = sanitize(raw_line)

@RaitoBezarius
Copy link

I also sent a PR downstream in nixpkgs to apply the patch for the time being: NixOS/nixpkgs#265899.

@RaitoBezarius
Copy link

RaitoBezarius commented Nov 6, 2023

Actually, you need at least:

diff --git c/taskw/taskrc.py w/taskw/taskrc.py
index 1b6f8e5..9a6610e 100644
--- c/taskw/taskrc.py
+++ w/taskw/taskrc.py
@@ -1,5 +1,6 @@
 import logging
 import os
+import shutil
 
 from taskw.fields import (
     ChoiceField,
@@ -39,6 +40,7 @@ class TaskRc(dict):
 
     """
 
+    rcdir = None
     UDA_TYPE_MAP = {
         'date': DateField,
         'duration': DurationField,
@@ -48,16 +50,31 @@ class TaskRc(dict):
 
     def __init__(self, path=None, overrides=None):
         self.overrides = overrides if overrides else {}
+        binary_path = shutil.which("task")
+        share_rc_path = None
+        if binary_path is not None:
+            binary_path = os.path.realpath(binary_path) # Normalizes any potential symlinks.
+            prefix = os.path.dirname(os.path.dirname(binary_path)) # $prefix/bin/task → $prefix
+            share_rc_path = os.path.join(prefix, "share", "doc", "task", "rc") # $prefix/share/doc/task/rc contains additional files.
+
+        self.fallback_prefixes = list(filter(None, [
+            share_rc_path
+        ]))
+
         if path:
             self.path = os.path.normpath(
                 os.path.expanduser(
                     path
                 )
             )
+            if not self.rcdir:
+                TaskRc.rcdir = os.path.dirname(os.path.realpath(self.path))
+                self.fallback_prefixes.append(TaskRc.rcdir)
             config = self._read(self.path)
         else:
             self.path = None
             config = {}
+
         super(TaskRc, self).__init__(config)
 
     def _add_to_tree(self, config, key, value):
@@ -92,6 +109,28 @@ class TaskRc(dict):
 
     def _read(self, path):
         config = {}
+
+        if not os.path.exists(path):
+            # include path may be given relative to dir of rcfile
+            oldpath = path
+            tries = []
+            for fallback_prefix in self.fallback_prefixes:
+                path = os.path.join(fallback_prefix, oldpath)
+                if not os.path.exists(path):
+                    tries.append(path)
+                    logger.warn(
+                        "tried %s and %s with no success",
+                        oldpath, path
+                    )
+                else:
+                    break
+            if len(tries) == self.fallback_prefixes:
+                logger.error(
+                    "failed to find a fallback prefix for %s",
+                    oldpath
+                )
+                raise FileNotFoundError
+
         with open(path, 'r') as config_file:
             for raw_line in config_file.readlines():
                 line = sanitize(raw_line)

to make this work for cases where the relative path is related to the default share data installed as part of TaskWarrior.

bergercookie added a commit to bergercookie/taskw-ng that referenced this pull request Jan 22, 2024
1. from the CWD (deprecated behavior in taskwarrior)
2. in $TASK_RCDIR (custom path - just to allow flexibility from the end-user side
3. relative to the TASKRC file
4. in `{/usr/,/usr/local/}/share/doc/task/rc/`

This fixes the taskw repo bug
ralphbean/taskw#150 . See also the relevant PR
discussion in ralphbean/taskw#151
@bergercookie
Copy link
Contributor

Hi all, I'm attempting to fix this issue in my taskw-ng fork (since AFAICT development has stalled unfortunately in this repo)

Anyone knows what' the use of the .rcdir variable mentioned in this PR ?

            if not self.rcdir:
                TaskRc.rcdir = os.path.dirname(os.path.realpath(self.path))

https://github.com/ralphbean/taskw/pull/151/files#diff-eb2e93c64ee7759abb84b6b1a876071bc5c87c146daf0c99d5dce59bd63168a9R42-R59

@smemsh
Copy link
Author

smemsh commented Jun 21, 2024

@bergercookie not sure what you mean by "what's the use," rcdir is a class attribute which is populated with the self.path dir in TaskRc instance constructor. The code tries both as an absolute, and relative path later in _read() method.

@smemsh smemsh changed the title RcFile: _read: try taskrc directory when trying to load includes [superseded] RcFile: _read: try taskrc directory when trying to load includes Aug 26, 2025
@smemsh
Copy link
Author

smemsh commented Aug 26, 2025

marking this as superseded by #170 since I'm no longer using this library and the author of that one appears to still be active.

@smemsh smemsh closed this Aug 26, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

taskrc includes as relative paths cause FileNotFoundError in taskw

5 participants