Repository: ansible/ansible-modules-extras

Issue:
homebrew_cask should allow / in name parameter
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

packaging/os/homebrew_cask
##### ANSIBLE VERSION

```
ansible 2.0.1.0
```
##### CONFIGURATION

None
##### OS / ENVIRONMENT

OS X 10.11.4 (15E65) (running against itself on localhost)
##### SUMMARY

When tapping a particular set of Casks, you can install them via tap and the short name, or the full path to the Cask.

e.g.

```
brew cask install fgimian/fgimian/microsoft-office
```

This is also handy when two packages exist with the same name and you wish to install a particular one.

Currently the module does not allow / in the Cask name:

```
# ansible localhost -m homebrew_cask -a "name=fgimian/fgimian/microsoft-office state=present"
localhost | FAILED! => {
    "changed": false, 
    "failed": true, 
    "msg": "Invalid cask: fgimian/fgimian/microsoft-office."
}
```
##### STEPS TO REPRODUCE

Simply run the command:

```
ansible localhost -m homebrew_cask -a "name=fgimian/fgimian/microsoft-office state=present"
```

And to demonstrate that this does work with Homebrew Cask, try to install the package manually:

```
brew cask install fgimian/fgimian/microsoft-office
```
##### EXPECTED RESULTS

Package is installed successfully
##### ACTUAL RESULTS

Error is displayed instead (see above)



Discussion:
<!-- @fgimian: Thanks for filing this bug report! -->

@indrajitr ping, this issue is waiting for your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->


Resolving patch:
diff --git a/packaging/os/homebrew_cask.py b/packaging/os/homebrew_cask.py
--- a/packaging/os/homebrew_cask.py
+++ b/packaging/os/homebrew_cask.py
@@ -35,17 +35,33 @@
         description:
             - name of cask to install/remove
         required: true
+        aliases: ['pkg', 'package', 'cask']
+    path:
+        description:
+            - "':' separated list of paths to search for 'brew' executable."
+        required: false
+        default: '/usr/local/bin'
     state:
         description:
             - state of the cask
         choices: [ 'present', 'absent' ]
         required: false
         default: present
+    update_homebrew:
+        description:
+            - update homebrew itself first. Note that C(brew cask update) is
+              a synonym for C(brew update).
+        required: false
+        default: no
+        choices: [ "yes", "no" ]
+        aliases: ['update-brew']
+        version_added: "2.2"
     install_options:
         description:
             - options flags to install a package
         required: false
         default: null
+        aliases: ['options']
         version_added: "2.2"
 '''
 EXAMPLES = '''
@@ -84,6 +100,7 @@ class HomebrewCask(object):
         \s                  # spaces
         :                   # colons
         {sep}               # the OS-specific path separator
+        .                   # dots
         -                   # dashes
     '''.format(sep=os.path.sep)
 
@@ -91,11 +108,14 @@ class HomebrewCask(object):
         \w                  # alphanumeric characters (i.e., [a-zA-Z0-9_])
         \s                  # spaces
         {sep}               # the OS-specific path separator
+        .                   # dots
         -                   # dashes
     '''.format(sep=os.path.sep)
 
     VALID_CASK_CHARS = r'''
         \w                  # alphanumeric characters (i.e., [a-zA-Z0-9_])
+        .                   # dots
+        /                   # slash (for taps)
         -                   # dashes
     '''
 
@@ -113,6 +133,7 @@ def valid_path(cls, path):
          - a string containing only:
              - alphanumeric characters
              - dashes
+             - dots
              - spaces
              - colons
              - os.path.sep
@@ -137,6 +158,7 @@ def valid_brew_path(cls, brew_path):
          - a string containing only:
              - alphanumeric characters
              - dashes
+             - dots
              - spaces
              - os.path.sep
         '''
@@ -185,6 +207,7 @@ def valid_module(cls, module):
         '''A valid module is an instance of AnsibleModule.'''
 
         return isinstance(module, AnsibleModule)
+
     # /class validations ------------------------------------------- }}}
 
     # class properties --------------------------------------------- {{{
@@ -266,11 +289,14 @@ def current_cask(self, cask):
             return cask
     # /class properties -------------------------------------------- }}}
 
-    def __init__(self, module, path=None, casks=None, state=None,
-            install_options=None):
+    def __init__(self, module, path=path, casks=None, state=None,
+                 update_homebrew=False, install_options=None):
+        if not install_options:
+            install_options = list()
         self._setup_status_vars()
         self._setup_instance_vars(module=module, path=path, casks=casks,
-                                  state=state, install_options=install_options)
+                                  state=state, update_homebrew=update_homebrew,
+                                  install_options=install_options,)
 
         self._prep()
 
@@ -287,13 +313,8 @@ def _setup_instance_vars(self, **kwargs):
             setattr(self, key, val)
 
     def _prep(self):
-        self._prep_path()
         self._prep_brew_path()
 
-    def _prep_path(self):
-        if not self.path:
-            self.path = ['/usr/local/bin']
-
     def _prep_brew_path(self):
         if not self.module:
             self.brew_path = None
@@ -340,8 +361,12 @@ def _current_cask_is_installed(self):
             self.message = 'Invalid cask: {0}.'.format(self.current_cask)
             raise HomebrewCaskException(self.message)
 
-        cmd = [self.brew_path, 'cask', 'list']
-        rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0])
+        cmd = [
+            "{brew_path}".format(brew_path=self.brew_path),
+            "cask",
+            "list"
+        ]
+        rc, out, err = self.module.run_command(cmd)
 
         if 'nothing to list' in err:
             return False
@@ -356,6 +381,9 @@ def _current_cask_is_installed(self):
 
     # commands ----------------------------------------------------- {{{
     def _run(self):
+        if self.update_homebrew:
+            self._update_homebrew()
+
         if self.state == 'installed':
             return self._install_casks()
         elif self.state == 'absent':
@@ -369,7 +397,7 @@ def _update_homebrew(self):
         rc, out, err = self.module.run_command([
             self.brew_path,
             'update',
-        ], path_prefix=self.path[0])
+        ])
         if rc == 0:
             if out and isinstance(out, basestring):
                 already_updated = any(
@@ -417,8 +445,7 @@ def _install_current_cask(self):
         )
 
         cmd = [opt for opt in opts if opt]
-
-        rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0])
+        rc, out, err = self.module.run_command(cmd)
 
         if self._current_cask_is_installed():
             self.changed_count += 1
@@ -463,7 +490,7 @@ def _uninstall_current_cask(self):
                for opt in (self.brew_path, 'cask', 'uninstall', self.current_cask)
                if opt]
 
-        rc, out, err = self.module.run_command(cmd, path_prefix=self.path[0])
+        rc, out, err = self.module.run_command(cmd)
 
         if not self._current_cask_is_installed():
             self.changed_count += 1
@@ -488,8 +515,16 @@ def _uninstall_casks(self):
 def main():
     module = AnsibleModule(
         argument_spec=dict(
-            name=dict(aliases=["cask"], required=False),
-            path=dict(required=False),
+            name=dict(
+                aliases=["pkg", "package", "cask"],
+                required=False,
+                type='list',
+            ),
+            path=dict(
+                default="/usr/local/bin",
+                required=False,
+                type='path',
+            ),
             state=dict(
                 default="present",
                 choices=[
@@ -497,6 +532,11 @@ def main():
                     "absent", "removed", "uninstalled",
                 ],
             ),
+            update_homebrew=dict(
+                default=False,
+                aliases=["update-brew"],
+                type='bool',
+            ),
             install_options=dict(
                 default=None,
                 aliases=['options'],
@@ -511,15 +551,13 @@ def main():
     p = module.params
 
     if p['name']:
-        casks = p['name'].split(',')
+        casks = p['name']
     else:
         casks = None
 
     path = p['path']
     if path:
         path = path.split(':')
-    else:
-        path = ['/usr/local/bin']
 
     state = p['state']
     if state in ('present', 'installed'):
@@ -527,13 +565,14 @@ def main():
     if state in ('absent', 'removed', 'uninstalled'):
         state = 'absent'
 
-
+    update_homebrew = p['update_homebrew']
     p['install_options'] = p['install_options'] or []
     install_options = ['--{0}'.format(install_option)
                        for install_option in p['install_options']]
 
     brew_cask = HomebrewCask(module=module, path=path, casks=casks,
-                             state=state, install_options=install_options)
+                             state=state,  update_homebrew=update_homebrew,
+                             install_options=install_options)
     (failed, changed, message) = brew_cask.run()
     if failed:
         module.fail_json(msg=message)


Test patch: