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