Repository: ansible/ansible

Issue:
composer: Unable to use "working-dir" with spaces
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
composer

##### ANSIBLE VERSION
```
$ ansible --version
ansible 2.2.0.0
  config file = 
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
N/A

##### OS / ENVIRONMENT
N/A

##### SUMMARY
The composer module is not able to use "working-dir" with directories that contain a space.

##### STEPS TO REPRODUCE
`mkdir /tmp/test\ directory`

```
# Create a simple composer.json with contents:
{
    "require": {
        "rfd/imagemagick": "*"
    }
}
```
```
ansible localhost -m composer -a "working-dir=\"/tmp/test directory\""
```

##### EXPECTED RESULTS
Composer is able descend into the requested directory.

##### ACTUAL RESULTS
Composer complains that the directory doesn't exist, but the error message lists the wrong directory.
`[RuntimeException] Invalid working directory specified, /tmp/test does not exist.`

```
$ ansible localhost -m composer -a "working-dir=\"/tmp/test directory\"" -vvv
No config file found; using defaults
 [WARNING]: Host file not found: /etc/ansible/hosts

 [WARNING]: provided hosts list is empty, only localhost is available

Using module file /Library/Python/2.7/site-packages/ansible/modules/extras/packaging/language/composer.py
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: tylerlawson
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732 `" && echo ansible-tmp-1484583081.77-153904708810732="` echo $HOME/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732 `" ) && sleep 0'
<127.0.0.1> PUT /var/folders/wj/lx9yl6t56nvdwgmq90c642f00000gn/T/tmpmIQHYO TO /Users/tylerlawson/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732/composer.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /Users/tylerlawson/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732/ /Users/tylerlawson/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732/composer.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /Users/tylerlawson/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732/composer.py; rm -rf "/Users/tylerlawson/.ansible/tmp/ansible-tmp-1484583081.77-153904708810732/" > /dev/null 2>&1 && sleep 0'
localhost | FAILED! => {
    "changed": false, 
    "failed": true, 
    "invocation": {
        "module_args": {
            "arguments": "", 
            "command": "install", 
            "ignore_platform_reqs": false, 
            "no_dev": true, 
            "no_plugins": false, 
            "no_scripts": false, 
            "optimize_autoloader": true, 
            "prefer_dist": false, 
            "prefer_source": false, 
            "working-dir": "/tmp/test directory", 
            "working_dir": "/tmp/test directory"
        }, 
        "module_name": "composer"
    }, 
    "msg": "[RuntimeException] Invalid working directory specified, /tmp/test does not exist.", 
    "stdout": "\n                                                                  \n  [RuntimeException]                                              \n  Invalid working directory specified, /tmp/test does not exist.  \n                                                                  \n\n", 
    "stdout_lines": [
        "", 
        "                                                                  ", 
        "  [RuntimeException]                                              ", 
        "  Invalid working directory specified, /tmp/test does not exist.  ", 
        "                                                                  ", 
        ""
    ]
}
```


I tried reporting this to the extras repo according to http://docs.ansible.com/ansible/community.html#i-d-like-to-report-a-bug but I guess it's not up-to-date with recent changes in process.


Discussion:
cc @dmtrs @resmo
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/packaging/language/composer.py b/lib/ansible/modules/packaging/language/composer.py
--- a/lib/ansible/modules/packaging/language/composer.py
+++ b/lib/ansible/modules/packaging/language/composer.py
@@ -32,82 +32,87 @@
 short_description: Dependency Manager for PHP
 version_added: "1.6"
 description:
-    - Composer is a tool for dependency management in PHP. It allows you to declare the dependent libraries your project needs and it will install them in your project for you
+    - >
+      Composer is a tool for dependency management in PHP. It allows you to
+      declare the dependent libraries your project needs and it will install
+      them in your project for you.
 options:
     command:
         version_added: "1.8"
         description:
-            - Composer command like "install", "update" and so on
+            - Composer command like "install", "update" and so on.
         required: false
         default: install
     arguments:
         version_added: "2.0"
         description:
-            - Composer arguments like required package, version and so on
+            - Composer arguments like required package, version and so on.
         required: false
         default: null
     working_dir:
         description:
-            - Directory of your project ( see --working-dir )
+            - Directory of your project (see --working-dir).
         required: true
         default: null
         aliases: [ "working-dir" ]
     prefer_source:
         description:
-            - Forces installation from package sources when possible ( see --prefer-source )
+            - Forces installation from package sources when possible (see --prefer-source).
         required: false
-        default: "no"
-        choices: [ "yes", "no" ]
+        default: false
+        choices: [ true, false]
         aliases: [ "prefer-source" ]
     prefer_dist:
         description:
-            - Forces installation from package dist even for dev versions ( see --prefer-dist )
+            - Forces installation from package dist even for dev versions (see --prefer-dist).
         required: false
-        default: "no"
-        choices: [ "yes", "no" ]
+        default: false
+        choices: [ true, false]
         aliases: [ "prefer-dist" ]
     no_dev:
         description:
-            - Disables installation of require-dev packages ( see --no-dev )
+            - Disables installation of require-dev packages (see --no-dev).
         required: false
-        default: "yes"
-        choices: [ "yes", "no" ]
+        default: true
+        choices: [ true, false]
         aliases: [ "no-dev" ]
     no_scripts:
         description:
-            - Skips the execution of all scripts defined in composer.json ( see --no-scripts )
+            - Skips the execution of all scripts defined in composer.json (see --no-scripts).
         required: false
-        default: "no"
-        choices: [ "yes", "no" ]
+        default: false
+        choices: [ true, false]
         aliases: [ "no-scripts" ]
     no_plugins:
         description:
-            - Disables all plugins ( see --no-plugins )
+            - Disables all plugins ( see --no-plugins ).
         required: false
-        default: "no"
-        choices: [ "yes", "no" ]
+        default: false
+        choices: [ true, false]
         aliases: [ "no-plugins" ]
     optimize_autoloader:
         description:
-            - Optimize autoloader during autoloader dump ( see --optimize-autoloader ). Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default.
+            - Optimize autoloader during autoloader dump (see --optimize-autoloader).
+            - Convert PSR-0/4 autoloading to classmap to get a faster autoloader.
+            - Recommended especially for production, but can take a bit of time to run so it is currently not done by default.
         required: false
-        default: "yes"
-        choices: [ "yes", "no" ]
+        default: true
+        choices: [ true, false]
         aliases: [ "optimize-autoloader" ]
     ignore_platform_reqs:
         version_added: "2.0"
         description:
             - Ignore php, hhvm, lib-* and ext-* requirements and force the installation even if the local machine does not fulfill these.
         required: false
-        default: "no"
-        choices: [ "yes", "no" ]
+        default: false
+        choices: [ true, false]
         aliases: [ "ignore-platform-reqs" ]
 requirements:
     - php
     - composer installed in bin path (recommended /usr/local/bin)
 notes:
     - Default options that are always appended in each execution are --no-ansi, --no-interaction and --no-progress if available.
-    - We received reports about issues on macOS if composer was installed by Homebrew. Please use the official install method to avoid it.
+    - We received reports about issues on macOS if composer was installed by Homebrew. Please use the official install method to avoid issues.
 '''
 
 EXAMPLES = '''
@@ -129,25 +134,18 @@
     prefer_dist: yes
 '''
 
-import os
 import re
-
-try:
-    import json
-except ImportError:
-    try:
-        import simplejson as json
-    except ImportError:
-        # Let snippet from module_utils/basic.py return a proper error in this case
-        pass
+from ansible.module_utils.basic import AnsibleModule
 
 
 def parse_out(string):
     return re.sub("\s+", " ", string).strip()
 
+
 def has_changed(string):
     return "Nothing to install or update" not in string
 
+
 def get_available_options(module, command='install'):
     # get all available options from a composer command using composer help to json
     rc, out, err = composer_command(module, "help %s --format=json" % command)
@@ -155,28 +153,30 @@ def get_available_options(module, command='install'):
         output = parse_out(err)
         module.fail_json(msg=output)
 
-    command_help_json = json.loads(out)
+    command_help_json = module.from_json(out)
     return command_help_json['definition']['options']
 
-def composer_command(module, command, arguments = "", options=[]):
-    php_path      = module.get_bin_path("php", True, ["/usr/local/bin"])
+
+def composer_command(module, command, arguments="", options=[]):
+    php_path = module.get_bin_path("php", True, ["/usr/local/bin"])
     composer_path = module.get_bin_path("composer", True, ["/usr/local/bin"])
-    cmd           = "%s %s %s %s %s" % (php_path, composer_path, command, " ".join(options), arguments)
+    cmd = "%s %s %s %s %s" % (php_path, composer_path, command, " ".join(options), arguments)
     return module.run_command(cmd)
 
+
 def main():
     module = AnsibleModule(
-        argument_spec = dict(
-            command              = dict(default="install", type="str", required=False),
-            arguments            = dict(default="", type="str", required=False),
-            working_dir          = dict(aliases=["working-dir"], required=True),
-            prefer_source        = dict(default="no", type="bool", aliases=["prefer-source"]),
-            prefer_dist          = dict(default="no", type="bool", aliases=["prefer-dist"]),
-            no_dev               = dict(default="yes", type="bool", aliases=["no-dev"]),
-            no_scripts           = dict(default="no", type="bool", aliases=["no-scripts"]),
-            no_plugins           = dict(default="no", type="bool", aliases=["no-plugins"]),
-            optimize_autoloader  = dict(default="yes", type="bool", aliases=["optimize-autoloader"]),
-            ignore_platform_reqs = dict(default="no", type="bool", aliases=["ignore-platform-reqs"]),
+        argument_spec=dict(
+            command=dict(default="install", type="str", required=False),
+            arguments=dict(default="", type="str", required=False),
+            working_dir=dict(type="path", aliases=["working-dir"], required=True),
+            prefer_source=dict(default=False, type="bool", aliases=["prefer-source"]),
+            prefer_dist=dict(default=False, type="bool", aliases=["prefer-dist"]),
+            no_dev=dict(default=True, type="bool", aliases=["no-dev"]),
+            no_scripts=dict(default=False, type="bool", aliases=["no-scripts"]),
+            no_plugins=dict(default=False, type="bool", aliases=["no-plugins"]),
+            optimize_autoloader=dict(default=True, type="bool", aliases=["optimize-autoloader"]),
+            ignore_platform_reqs=dict(default=False, type="bool", aliases=["ignore-platform-reqs"]),
         ),
         supports_check_mode=True
     )
@@ -203,17 +203,17 @@ def main():
             option = "--%s" % option
             options.append(option)
 
-    options.extend(['--working-dir', os.path.abspath(module.params['working_dir'])])
+    options.extend(['--working-dir', "'%s'" % module.params['working_dir']])
 
     option_params = {
-        'prefer_source':        'prefer-source',
-        'prefer_dist':          'prefer-dist',
-        'no_dev':               'no-dev',
-        'no_scripts':           'no-scripts',
-        'no_plugins':           'no_plugins',
-        'optimize_autoloader':  'optimize-autoloader',
+        'prefer_source': 'prefer-source',
+        'prefer_dist': 'prefer-dist',
+        'no_dev': 'no-dev',
+        'no_scripts': 'no-scripts',
+        'no_plugins': 'no_plugins',
+        'optimize_autoloader': 'optimize-autoloader',
         'ignore_platform_reqs': 'ignore-platform-reqs',
-        }
+    }
 
     for param, option in option_params.items():
         if module.params.get(param) and option in available_options:
@@ -231,9 +231,8 @@ def main():
     else:
         # Composer version > 1.0.0-alpha9 now use stderr for standard notification messages
         output = parse_out(out + err)
-        module.exit_json(changed=has_changed(output), msg=output, stdout=out+err)
+        module.exit_json(changed=has_changed(output), msg=output, stdout=out + err)
+
 
-# import module snippets
-from ansible.module_utils.basic import *
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/test/sanity/pep8/legacy-files.txt b/test/sanity/pep8/legacy-files.txt
--- a/test/sanity/pep8/legacy-files.txt
+++ b/test/sanity/pep8/legacy-files.txt
@@ -158,7 +158,6 @@ lib/ansible/modules/network/snmp_facts.py
 lib/ansible/modules/notification/hall.py
 lib/ansible/modules/notification/sendgrid.py
 lib/ansible/modules/notification/sns.py
-lib/ansible/modules/packaging/language/composer.py
 lib/ansible/modules/packaging/language/maven_artifact.py
 lib/ansible/modules/packaging/os/apt.py
 lib/ansible/modules/packaging/os/apt_key.py