Skip to content

Commit

Permalink
Improve handling of script name by dlb_launcher
Browse files Browse the repository at this point in the history
- Normalize silently to make it less surprising on MS Windows
- Disallow non-upwards path (as already documented)
- Improve diagnostic messages for invalid script name
  • Loading branch information
dlu-ch committed May 22, 2022
1 parent cb42e1c commit c16aee6
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 21 deletions.
17 changes: 14 additions & 3 deletions doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ polite one::

$ echo 'print("hello there!")' > build.py


Run dlb
^^^^^^^

Now, we can use :file:`dlb` to run :file:`build.py`::

$ dlb build
Expand All @@ -92,11 +96,18 @@ working in a subdirectory of the :term:`working tree` or when you need modules f
hello there!
$ cd ..

See ``dlb --help`` for a detailed description.
See ``dlb --help`` (or :ref:`here <dlbexe>`) for a detailed description of ``dlb``.

The effect of ``dlb`` - with or without parameters - is independent of the current working directory
(as long as the current working directory is inside the working tree).
This allows to define convenience shell aliases for projects that use multiple dlb scripts with lengthy script names
or command line arguments.

Example (Bash): ``alias b='dlb build-description/prepare-env --no-doc'``.


Run a custom tool in an execution context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Execute a custom tool in an execution context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Replace the content of :file:`build.py` with this::

Expand Down
30 changes: 21 additions & 9 deletions src/dlb_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,28 @@ def complete_module_search_path(dlbroot_path, script_abs_path):
sys.path.insert(0, os.path.dirname(script_abs_path))


def check_and_normalize_script_name(script_name):
if not script_name or script_name[0] == '-':
raise Exception(f'not a valid script name: {script_name!r}')

if os.path.isabs(script_name):
raise Exception(f'not a valid script name (since absolute): {script_name!r}')

normalized_script_name = os.path.normpath(script_name)
if f'{os.path.sep}..{os.path.sep}' in f'{os.path.sep}{normalized_script_name}{os.path.sep}':
raise Exception(f'not a valid script name (since upwards path): {script_name!r}')
# avoids (uncontrollable) dependency on content outside working tree and security risk when restored
# from history file

return normalized_script_name


def complete_command_line(history_file_path, arguments):
if arguments:
script_name = arguments[0]
if script_name and not script_name.endswith('.py'):
script_name += '.py'
return script_name, arguments[1:]
return check_and_normalize_script_name(script_name), arguments[1:]

last_arguments = None
try:
Expand Down Expand Up @@ -74,10 +90,6 @@ def complete_command_line(history_file_path, arguments):


def find_script(script_name):
if not script_name or script_name[0] == '-' or \
os.path.isabs(script_name) or os.path.normpath(script_name) != script_name:
raise Exception(f'not a script name: {script_name!r}')

script_abs_path = os.path.abspath(script_name)
if not os.path.isfile(script_abs_path):
raise Exception(f'not an existing script: {script_name!r}')
Expand All @@ -101,10 +113,10 @@ def get_help():
When called with '--help' as the first parameter, displays this help and exits.
When called with a least one parameter and the first parameter is not '--help',
the first parameter must be a dlb script as a normalized, non-upwards path
relative to the root of the working tree that does not start with '-'. '.py' is
appended if it does not end with '.py'. All other parameters are forwarded to
the dlb script.
the first parameter must be a dlb script as a non-upwards path relative to the
root of the working tree that does not start with '-'.
'.py' is appended if it does not end with '.py'.
All other parameters are forwarded to the dlb script.
When called without a parameter, the parameters from the last successful call of
this script with the same 'os.name' are used.
Expand Down
75 changes: 66 additions & 9 deletions test/dlb/0/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,31 +161,88 @@ def test_outputs_usage_without_parameters(self):
class ScriptTest(testenv.CommandlineToolTestCase,
testenv.TemporaryWorkingDirectoryTestCase):

def test_fails_for_invalid_scriptname(self):
def test_fails_for_empty_scriptname(self):
sys.stderr = io.StringIO()
script_name = ''
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f'error: not a valid script name: {script_name!r}\n', sys.stderr.getvalue())

def test_fails_for_scriptname_that_starts_with_minus(self):
for script_name in ['', '-build.py']:
sys.stderr = io.StringIO()
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f'error: not a valid script name: {script_name!r}\n', sys.stderr.getvalue())

def test_fails_for_absolute_path(self):
sys.stderr = io.StringIO()
script_name = os.path.abspath('build.py')
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f'error: not a valid script name (since absolute): {script_name!r}\n', sys.stderr.getvalue())

def test_fails_for_path_with_dotdot(self): # except at end
invalid_script_names = [
'',
'-build.py',
os.path.join('build', '..', 'all', '.', '.py'),
os.path.abspath('build.py')
os.path.join('..', 'build'),
os.path.join('build', '..', '..', 'build')
]
for script_name in invalid_script_names:
sys.stderr = io.StringIO()
sys.argv = [sys.argv[0]] + [script_name]
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f'error: not a script name: {script_name!r}\n', sys.stderr.getvalue())
self.assertEqual(f"error: not a valid script name (since upwards path): {script_name + '.py'!r}\n",
sys.stderr.getvalue())

def test_fails_for_nonexistent(self):
script_name = 'build'
sys.argv = [sys.argv[0]] + [script_name]
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f"error: not an existing script: {script_name + '.py'!r}\n", sys.stderr.getvalue())

def test_normalizes_nonupwards_path(self):
script_name = os.path.join('build', '..', 'all', '.', 'build')
normalized_script_name = os.path.join('all', 'build.py')
os.mkdir('all')
open(normalized_script_name, 'w').close()
sys.stderr = io.StringIO()
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(0, r)

def test_add_py_if_not_present(self):
sys.stderr = io.StringIO()
script_name = 'build'
sys.argv = [sys.argv[0], script_name]
open('build.py', 'w').close()
r = dlb_launcher.main()
self.assertEqual(0, r)

def test_accepts_dot_at_end(self):
sys.stderr = io.StringIO()
script_name = '.'
sys.argv = [sys.argv[0], script_name]
open('..py', 'w').close()
r = dlb_launcher.main()
self.assertEqual(0, r)

def test_accepts__dotdot_at_end(self):
sys.stderr = io.StringIO()
script_name = '..'
sys.argv = [sys.argv[0], script_name]
open('...py', 'w').close()
r = dlb_launcher.main()
self.assertEqual(0, r)

def test_fails_for_directory(self):
os.mkdir('build.py')
script_name = 'build'
sys.argv = [sys.argv[0]] + [script_name]
sys.argv = [sys.argv[0], script_name]
r = dlb_launcher.main()
self.assertEqual(1, r)
self.assertEqual(f"error: not an existing script: {script_name + '.py'!r}\n", sys.stderr.getvalue())
Expand Down

0 comments on commit c16aee6

Please sign in to comment.