Search code examples
pythonpython-3.xpathpathlib

Best practice to pass multiple paths as command line argument accounting for paths with spaces


Background

The Python module I'm writing has some functionalities available from command line interface via python -m fun_module -p .... argument -p, --path takes multiple paths. Presently I'm defining this argument in the following manner:

parser = argparse.ArgumentParser(prog="FunProg", add_help=True)
parser.add_argument(
    "-p",
    "--path",
    type=utils.dir_path,
    nargs="+",
    help="Start path(s) used to conduct the search. Multiple paths should be separated with spaces.",
)
args = parser.parse_args()

dir_path function available via utils.py file

import os

def dir_path(str_path):
    if os.path.isdir(str_path):
        return str_path
    else:
        raise NotADirectoryError(str_path)

main method

def main(args):
    for iarg in args.path:
        print("Searching path: " + iarg)

Problem

Handling multiple paths separated with spaces and surrounded with inverted commas doesn't return the desired results.

Desired output

python -m fun_module -p ~/Documents ~/Downloads/
Searching path: /Users/thisuser/Documents
Searching path: /Users/thisuser/Downloads/

python -m fun_module -p '~/Documents' '~/Downloads/'
Searching path: /Users/thisuser/Documents
Searching path: /Users/thisuser/Downloads/

python -m fun_module -p "~/Documents" "~/Downloads/"
Searching path: /Users/thisuser/Documents
Searching path: /Users/thisuser/Downloads/

Error:

python -m fun_module -p '~/Documents' '~/Downloads/'                          
Traceback (most recent call last):
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/runpy.py", line 197, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/runpy.py", line 87, in _run_code
    exec(code, run_globals)
  File "/Users/thisuser/Dev/Python/FileLister/filelister/__main__.py", line 25, in <module>
    args = parser.parse_args()
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 1818, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 1851, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 2060, in _parse_known_args
    start_index = consume_optional(start_index)
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 2000, in consume_optional
    take_action(action, args, option_string)
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 1912, in take_action
    argument_values = self._get_values(action, argument_strings)
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 2461, in _get_values
    value = [self._get_value(action, v) for v in arg_strings]
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 2461, in <listcomp>
    value = [self._get_value(action, v) for v in arg_strings]
  File "/Users/thisuser/.pyenv/versions/3.9.0/lib/python3.9/argparse.py", line 2476, in _get_value
    result = type_func(arg_string)
  File "/Users/thisuser/Dev/Python/FileLister/filelister/utils.py", line 7, in dir_path
    raise NotADirectoryError(str_path)
NotADirectoryError: ~/Documents

Solution

  • The problem was two folded:

    1. As indicated in this post, you need to use os.path.expanduser when using the ~.
    2. When your input contains quotes (' or "), the os.path.expanduser won't properly handle the input.

    To solve the issues, first replace the quotes by empty strings. Using python 3.9 this can be done with:

    for quote in ['"', "'"]:
        str_path = str_path.removeprefix(quote).removesuffix(quote)
    

    otherwise you have to manually check and remove:

    for quote in ['"', "'"]:
       if str_path.startswith(quote):
          str_path = str_path[1:-1]
    

    And then you have to check for a tilde start:

    if str_path.startswith('~'):
        str_path = os.path.expanduser(str_path)
    

    Full code:

    def dir_path(str_path: str):
        for quote in ['"', "'"]:
            if str_path.startswith(quote):
                str_path = str_path[1:-1]
        if str_path.startswith('~'):
            str_path = os.path.expanduser(str_path)
        if os.path.isdir(str_path):
            return str_path
        raise NotADirectoryError(str_path)
    

    And testing with any of the following inputs will result in a full path:

    python -m fun_module -p '~/Documents' '~/Downloads'
    python -m fun_module -p "~/Documents" "~/Downloads"
    python -m fun_module -p ~/Documents ~/Downloads