diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py index c922b86ab7276aecfa2f4273e4759cce12031d59..1a06d81f9df32452ac506a879f100693e53658b2 100644 --- a/src/Solver/CommandLine.py +++ b/src/Solver/CommandLine.py @@ -19,7 +19,7 @@ import os import logging -from tools import timer +from tools import timer, parse_command_line try: # Installation allow Unix-like signal @@ -146,9 +146,8 @@ class CommandLineSolver(AbstractSolver): Returns: The executable and list of arguments """ - # HACK: Works in most case... Trust me i'm an engineer cmd = cmd.replace("@install_dir", self._install_dir()) - cmd = cmd.replace("@path", path.replace(" ", "%20")) + cmd = cmd.replace("\"@path\"", path) cmd = cmd.replace("@input", self.input_param()) cmd = cmd.replace("@output", self.output_param()) cmd = cmd.replace("@dir", self._process.workingDirectory()) @@ -156,26 +155,9 @@ class CommandLineSolver(AbstractSolver): logger.debug(f"! {cmd}") - if cmd[0] == "\"": - # Command line executable path is between " char - cmd = cmd.split("\"") - exe = cmd[1].replace("%20", " ") - args = list( - filter( - lambda s: s != "", - "\"".join(cmd[2:]).split(" ")[1:] - ) - ) - else: - # We suppose the command line executable path as no space char - cmd = cmd.replace("\\ ", "&_&").split(" ") - exe = cmd[0].replace("&_&", " ") - args = list( - filter( - lambda s: s != "", - map(lambda s: s.replace("&_&", " "), cmd[1:]) - ) - ) + words = parse_command_line(cmd) + exe = words[0] + args = words[1:] logger.info(f"! {exe} {args}") return exe, args diff --git a/src/Solver/GenericSolver.py b/src/Solver/GenericSolver.py index 9fb272e4fc2763c1f7e12952ea18f1ba36062bf5..f8ff31aa3779ad0fd6bc2fa85b928fd8f00b5cdf 100644 --- a/src/Solver/GenericSolver.py +++ b/src/Solver/GenericSolver.py @@ -18,6 +18,7 @@ from Solver.CommandLine import CommandLineSolver + class GenericSolver(CommandLineSolver): _type = "generic" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..89e3fbe8b5c9d03daa6dcdb50838fb054f67a946 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,17 @@ +# __init__.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- diff --git a/src/test_pamhyr.py b/src/test_pamhyr.py new file mode 100644 index 0000000000000000000000000000000000000000..29ddd83c098d1a76cf46f0e20848dc0a59863368 --- /dev/null +++ b/src/test_pamhyr.py @@ -0,0 +1,100 @@ +# test_pamhyr.py -- Pamhyr +# Copyright (C) 2023 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import os +import unittest +import tempfile + +from tools import parse_command_line + + +class ToolsCMDParserTestCase(unittest.TestCase): + def test_trivial(self): + cmd = "foo" + expect = ["foo"] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_unix_simple(self): + cmd = "/foo/bar a -b -c" + expect = ["/foo/bar", "a", '-b', "-c"] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_unix_quoted(self): + cmd = "\"/foo/bar\" -a -b -c" + expect = ["/foo/bar", "-a", '-b', "-c"] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_unix_quoted_with_space(self): + cmd = "\"/foo/bar baz\" -a -b -c" + expect = ["/foo/bar baz", "-a", '-b', "-c"] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_unix_quoted_args(self): + cmd = "/foo/bar -a -b -c=\"baz\"" + expect = ["/foo/bar", "-a", '-b', "-c=\"baz\""] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_unix_quoted_args_with_space(self): + cmd = "/foo/bar -a -b -c=\"baz bazz\"" + expect = ["/foo/bar", "-a", '-b', "-c=\"baz bazz\""] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_windows_prog_files(self): + cmd = "\"C:\\Program Files (x86)\foo\bar\" a -b -c" + expect = ["C:\\Program Files (x86)\foo\bar", "a", '-b', "-c"] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) + + def test_windows_prog_files_args(self): + cmd = "\"C:\\Program Files (x86)\foo\bar\" a -b=\"baz bazz\" -c" + expect = [ + "C:\\Program Files (x86)\foo\bar", + "a", '-b=\"baz bazz\"', "-c" + ] + + res = parse_command_line(cmd) + + for i, s in enumerate(expect): + self.assertEqual(res[i], s) diff --git a/src/tools.py b/src/tools.py index 218bc78d1f30c1db7d01f8a111174417e4b9ce30..d6e787b3dfc10e5a5b9996cb1b75ed6d37835f1e 100644 --- a/src/tools.py +++ b/src/tools.py @@ -341,3 +341,112 @@ class SQL(object): def _load(self): logger.warning("TODO: LOAD") + + +####################### +# COMMAND LINE PARSER # +####################### + +parser_special_char = ["\"", "\'"] + + +def parse_command_line(cmd): + """Parse command line string and return list of string arguments + + Parse command line string and returns the list of separate + arguments as string, this function take in consideration space + separator and quoted expression + + Args: + cmd: The command line to parce + + Returns: + List of arguments as string + """ + words = [] + rest = cmd + + while True: + if len(rest) == 0: + break + + word, rest = _parse_next_word(rest) + words.append(word) + + return words + + +def _parse_next_word(words): + """Parse the next word in words string + + Args: + words: The words string + + Returns: + the next word and rests of words + """ + if len(words) == 1: + return words, "" + + # Remove useless space + words = words.strip() + + # Parse + if words[0] == "\"": + word, rest = _parse_word_up_to_next_sep(words, sep="\"") + elif words[0] == "\'": + word, rest = _parse_word_up_to_next_sep(words, sep="\'") + else: + word, rest = _parse_word_up_to_next_sep(words, sep=" ") + + return word, rest + + +def _parse_word_up_to_next_sep(words, sep=" "): + word = "" + + i = 0 if sep == " " else 1 + cur = words[i] + skip_next = False + while True: + # Exit conditions + if cur == "": + break + + if cur == sep: + if not skip_next: + break + + # Take in consideration escape char in case of \<sep> + if cur == "\\": + # If previous char is a escape char, cancel next char + # skiping: + # \<sep> -> skip <sep> as separator + # \\<sep> -> do not skip <sep> + skip_next = not skip_next + else: + skip_next = False + + word += cur + + # Current word contain a word with different separator, + # typicaly, the string '-c="foo bar"' with ' ' seperator must + # be parse as one word. + # + # Correct: '-c="foo bar" baz' -> '-c="foo bar"', 'baz' + # Not correct: '-c="foo bar" baz' -> '-c="foo', 'bar" baz' + if cur in parser_special_char: + # Recursive call to parse this word + sub_word, rest = _parse_word_up_to_next_sep(words[i:], sep=cur) + i += len(sub_word) + 1 + word += sub_word + cur + + # Get next symbol + i += 1 + if i == len(words): + cur = "" + else: + cur = words[i] + + rest = words[i+1:] + return word, rest