diff --git a/src/Solver/CommandLine.py b/src/Solver/CommandLine.py
index c922b86a..1a06d81f 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 9fb272e4..f8ff31aa 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 00000000..89e3fbe8
--- /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 .
+
+# -*- coding: utf-8 -*-
diff --git a/src/test_pamhyr.py b/src/test_pamhyr.py
new file mode 100644
index 00000000..29ddd83c
--- /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 .
+
+# -*- 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 218bc78d..d6e787b3 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 \
+ if cur == "\\":
+ # If previous char is a escape char, cancel next char
+ # skiping:
+ # \ -> skip as separator
+ # \\ -> do not skip
+ 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