Repository: ansible-collections/community.general

Issue:
Homebrew module incorrectly fails executing task when package is already installed
<!--- Verify first that your issue is not already reported on GitHub -->
<!--- Also test if the latest release and devel branch are affected too -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
```
- name: Install wkhtmltopdf
  homebrew:
    name: homebrew/cask/wkhtmltopdf 
    state: present
```

results in

```
PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Install wkhtmltopdf] *****************************************************
fatal: [localhost]: FAILED! => changed=false 
  msg: |-
    Updating Homebrew...
    Warning: Cask 'wkhtmltopdf' is already installed.
  
    To re-install wkhtmltopdf, run:
      brew cask reinstall wkhtmltopdf

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   
```

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
[Homebrew module](https://docs.ansible.com/ansible/latest/modules/homebrew_module.html)

##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes -->
```paste below
$ ansible --version
ansible 2.9.13
  config file = /Users/mpereira/git/macbook-playbook/ansible.cfg
  configured module search path = ['/Users/mpereira/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/mpereira/Library/Python/3.8/lib/python/site-packages/ansible
  executable location = /Users/mpereira/Library/Python/3.8/bin/ansible
  python version = 3.8.5 (v3.8.5:580fbb018f, Jul 20 2020, 12:11:27) [Clang 6.0 (clang-600.0.57)]
```

##### CONFIGURATION
<!--- Paste verbatim output from "ansible-config dump --only-changed" between quotes -->
```paste below
$ ansible-config dump --only-changed
DEFAULT_STDOUT_CALLBACK(/Users/mpereira/git/macbook-playbook/ansible.cfg) = yaml
```

##### OS / ENVIRONMENT
<!--- Provide all relevant information below, e.g. target OS versions, network device firmware, etc. -->
```
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.5
BuildVersion:	19F101
```

##### STEPS TO REPRODUCE
<!--- Describe exactly how to reproduce the problem, using a minimal test-case -->

<!--- Paste example playbooks or commands between quotes below -->
Refer to _summary_.

<!--- HINT: You can paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- Describe what you expected to happen when running the steps above -->
I expect the task to complete successfully when a package is already installed and a `homebrew` task is executed with either `state: present` or `state: latest`.

##### ACTUAL RESULTS
<!--- Describe what actually happened. If possible run with extra verbosity (-vvvv) -->

The task fails, as shown in the _summary_.


Discussion:
Files identified in the description:
* [`plugins/modules/packaging/os/homebrew.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/packaging/os/homebrew.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @akasurde @andrew-d @danieljaouen @indrajitr @kyleabenson @martinm82
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
The issue seems to only happen with casks. e.g., the following works:
```
- name: Install Hugo
  homebrew:
    name: hugo
```
```
PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [hugo : Install Hugo] *****************************************************
ok: [localhost] => changed=false 
  msg: 'Package already installed: hugo'

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```
/assign
It depends on the expected usage. For installing casks `homebrew_cask` should be used.
Testing with `homebrew_cask` as below works fine:
~~~ PLAYBOOK
    - name: Install Packages through Homebrew
      homebrew_cask:
        name: visual-studio-code
        state: present
~~~
~~~ OUTPUT
ok: [localhost] => {
    "changed": false,
    "invocation": {
        "module_args": {
            "accept_external_apps": false,
            "greedy": false,
            "install_options": [],
            "name": [
                "visual-studio-code"
            ],
            "path": "/usr/local/bin",
            "state": "present",
            "sudo_password": null,
            "update_homebrew": false,
            "upgrade_all": false
        }
    },
    "msg": "Cask already installed: visual-studio-code"
}
~~~


However when using the `homebrew` package to install casks fails for me in general.
~~~ PLAYBOOK
    - name: Remove visual-studio-code
      homebrew_cask:
        name: visual-studio-code
        state: absent
    - name: Install Packages through Homebrew
      homebrew:
        name: visual-studio-code
        state: present
~~~

~~~ OUTPUT
fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "install_options": [],
            "name": [
                "visual-studio-code"
            ],
            "path": "/usr/local/bin",
            "state": "present",
            "update_homebrew": false,
            "upgrade_all": false,
            "upgrade_options": []
        }
    },
    "msg": ""
}
~~~

Seems like we should deal with the output message better but not sure if the `homebrew` module should manage cask installs

@felixfontein Do you want the homebrew module to be more friendly with failing on cask issues;
Error something like "This Homebrew package is a cask and requires using the homebrew_cask module" or do you think this case should just be closed?
@MichaelWasher I don't use or maintain the module, so I'm not the right person to ask. In general, having better error messages is always a good thing though :)
I think this would be preferred behaviour, seems unnecessary to have to split package installs between two separate modules when it could/should stay in `homebrew` / `community.general.homebrew`.

```
TASK [Ensure apps are installed] *********************************************************************************************************************************************************************************************
failed: [localhost] (item=cheatsheet) => {"ansible_loop_var": "item", "changed": false, "item": "cheatsheet", "msg": "Warning: Cask 'cheatsheet' is already installed.\n\nTo re-install cheatsheet, run:\n  brew reinstall cheatsheet"}
ok: [localhost] => (item=docker)
failed: [localhost] (item=firefox) => {"ansible_loop_var": "item", "changed": false, "item": "firefox", "msg": "Error: It seems there is already an App at '/Applications/Firefox.app'."}
failed: [localhost] (item=gimp) => {"ansible_loop_var": "item", "changed": false, "item": "gimp", "msg": "Warning: Cask 'gimp' is already installed.\n\nTo re-install gimp, run:\n  brew reinstall gimp"}
ok: [localhost] => (item=gnupg)
failed: [localhost] (item=google-chrome) => {"ansible_loop_var": "item", "changed": false, "item": "google-chrome", "msg": "Error: It seems there is already an App at '/Applications/Google Chrome.app'."}
failed: [localhost] (item=gpg-suite) => {"ansible_loop_var": "item", "changed": false, "item": "gpg-suite", "msg": "Warning: Cask 'gpg-suite' is already installed.\n\nTo re-install gpg-suite, run:\n  brew reinstall gpg-suite"}
failed: [localhost] (item=inkscape) => {"ansible_loop_var": "item", "changed": false, "item": "inkscape", "msg": "Warning: Cask 'inkscape' is already installed.\n\nTo re-install inkscape, run:\n  brew reinstall inkscape"}
failed: [localhost] (item=iterm2) => {"ansible_loop_var": "item", "changed": false, "item": "iterm2", "msg": "Warning: Cask 'iterm2' is already installed.\n\nTo re-install iterm2, run:\n  brew reinstall iterm2"}
ok: [localhost] => (item=jq)
ok: [localhost] => (item=maven)
ok: [localhost] => (item=md5sha1sum)
ok: [localhost] => (item=openjdk)
ok: [localhost] => (item=pass)
failed: [localhost] (item=pgadmin3) => {"ansible_loop_var": "item", "changed": false, "item": "pgadmin3", "msg": "Warning: Cask 'pgadmin3' is already installed.\n\nTo re-install pgadmin3, run:\n  brew reinstall pgadmin3"}
failed: [localhost] (item=pgp2) => {"ansible_loop_var": "item", "changed": false, "item": "pgp2", "msg": "Error: No available formula or cask with the name \"pgp2\".\nError: No previously deleted formula found.\nError: No similarly named formulae found.\n==> Searching taps on GitHub...\nError: No formulae found in taps."}
failed: [localhost] (item=sequel pro) => {"ansible_loop_var": "item", "changed": false, "item": "sequel pro", "msg": "Invalid package: sequel pro."}
ok: [localhost] => (item=terraform)
ok: [localhost] => (item=tree)
failed: [localhost] (item=vagrant) => {"ansible_loop_var": "item", "changed": false, "item": "vagrant", "msg": "Warning: Cask 'vagrant' is already installed.\n\nTo re-install vagrant, run:\n  brew reinstall vagrant"}
failed: [localhost] (item=virtualbox) => {"ansible_loop_var": "item", "changed": false, "item": "virtualbox", "msg": "Warning: Cask 'virtualbox' is already installed.\n\nTo re-install virtualbox, run:\n  brew reinstall virtualbox"}
failed: [localhost] (item=virtualbox-extension-pack) => {"ansible_loop_var": "item", "changed": false, "item": "virtualbox-extension-pack", "msg": "Warning: Cask 'virtualbox-extension-pack' is already installed.\n\nTo re-install virtualbox-extension-pack, run:\n  brew reinstall virtualbox-extension-pack"}
failed: [localhost] (item=visual-studio-code) => {"ansible_loop_var": "item", "changed": false, "item": "visual-studio-code", "msg": "Warning: Cask 'visual-studio-code' is already installed.\n\nTo re-install visual-studio-code, run:\n  brew reinstall visual-studio-code"}
ok: [localhost] => (item=watch)
ok: [localhost] => (item=wireshark)
failed: [localhost] (item=xquartz) => {"ansible_loop_var": "item", "changed": false, "item": "xquartz", "msg": "Warning: Cask 'xquartz' is already installed.\n\nTo re-install xquartz, run:\n  brew reinstall xquartz"}

PLAY RECAP *******************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0 
```
It all really depends on what everyone wants.

But the issue for failing on already installed is due to the below lines, and could be resolved with the snippet below:
https://github.com/ansible-collections/community.general/blob/main/plugins/modules/packaging/os/homebrew.py#L462-L481

```
    # NOTE: This command will output both installed packages and casks
        cmd = [
            "{brew_path}".format(brew_path=self.brew_path), "list", "-1"
        ]
        rc, out, err = self.module.run_command(cmd)
        for package in out.split('\n'):
            if package == self.current_package:
                return True

        return False
```


Actually looks like HomeBrew doesn't provode the same output depending on TTY and pipes... 👎 
https://github.com/Homebrew/brew/blob/master/Library/Homebrew/cmd/list.rb#L90-L96

Something more like this would fix:
```
        cmd0 = ["{brew_path}".format(brew_path=self.brew_path), "list", "--casks"]
        cmd1 = ["{brew_path}".format(brew_path=self.brew_path), "list", "--formula"]

        rc, out, err = self.module.run_command(cmd0)
        rc1, out1, err1 = self.module.run_command(cmd1)

        out += '\n' + out1
        for package in out.split('\n'):
            if package == self.current_package:
                return True
```
The latest updates to the homebrew & homebrew_cask modules that caught the regex issue for catching dashes is enough for me, it appears that was an underlying issues I hadn't noticed and it was forcing me to try to install some casks using homebrew which then wasn't idempotent.
I ran into this problem as well and the issue is the following:

At time X I ran `brew install python3` and brew itself got updated. At that moment the formula was pointing to Python 3.8 (`python@3.8`). The output of `brew info python3` was:

```bash
brew info python3
python@3.8: stable 3.8.5 (bottled)
Interpreted, interactive, object-oriented programming language
https://www.python.org/
/usr/local/Cellar/python@3.8/3.8.5 (4,372 files, 67.6MB) *
  Poured from bottle on 2020-09-22 at 09:17:06
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/python@3.8.rb
License: Python-2.0

[...]
```

At time X+n I ran again `brew install python3` and brew updated itself again fetching a new formula Python pointing to `python@3.9`. The output of `brew info python3` is suddenly this:

```
brew info python3
python@3.9: stable 3.9.0 (bottled)
Interpreted, interactive, object-oriented programming language
https://www.python.org/
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/python@3.9.rb
License: Python-2.0

[...]
```

As you can see it says that the packge is not installed.

If I use `brew info python@3.8` one receives the expected output:

```bash
brew info python@3.8
python@3.8: stable 3.8.6 (bottled) [keg-only]
Interpreted, interactive, object-oriented programming language
https://www.python.org/
/usr/local/Cellar/python@3.8/3.8.5 (4,372 files, 67.6MB) *
  Poured from bottle on 2020-09-22 at 09:17:06
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/python@3.8.rb
License: Python-2.0

[...]
```

@MichaelWasher the fix you provided would not solve my issue in case I would use `python3` instead `python@3.8`. 

Of course one could argue whether I should actually use `python3` as it is clearly not pinning to a `Major.Minor` version of a package. I am also thinking to use always `update_homebrew: no` in each call to avoid updating brew itself.

But still I thing this is an issue on brew side that it returns information about a package that it actually does not have locally.

It would seem the homebrew_cask no longer works and is failing with the following error:
`Error: Calling brew cask install is disabled! Use brew install [--cask] instead.`
My target host is running macOS 11.1 and  Homebrew version2.7.1.

Installing a cask with homebrew fails with the following:
`failed: [192.168.122.242] (item=sublime-text) => {"ansible_loop_var": "item", "changed": false, "item": "sublime-text", "msg": ""}`
 but actually does install the package. 

It looks like the module(s) require(s) updating to support Homebrew version 2.7.1 correctly.
@Sergong your problem is a duplicate of #1524.
I'm having similar problem.

**My task**

```yaml
---
- name: install cask
  when: ansible_distribution == 'MacOSX'
  homebrew_cask:
    name:
      - docker
      - firefox
      - google-chrome
      - password-gorilla
      - slack
      - spotify
      - visual-studio
    state: present
```

**Error I see:**
```
TASK [osx : install cask] ************************************************************************************
task path: /Users/denis.costa/projects/dotfiles/roles/osx/tasks/main.yml:2
<127.0.0.1> ESTABLISH LOCAL CONNECTION FOR USER: denis.costa
<127.0.0.1> EXEC /bin/sh -c 'echo ~denis.costa && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /Users/denis.costa/.ansible/tmp `"&& mkdir "` echo /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702 `" && echo ansible-tmp-1624454349.499657-26582-53145583098702="` echo /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702 `" ) && sleep 0'
redirecting (type: modules) ansible.builtin.homebrew_cask to community.general.homebrew_cask
Using module file /Users/denis.costa/.pyenv/versions/3.9.1/lib/python3.9/site-packages/ansible_collections/community/general/plugins/modules/homebrew_cask.py
<127.0.0.1> PUT /Users/denis.costa/.ansible/tmp/ansible-local-24467fsn_s279/tmp0g39rtcq TO /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702/AnsiballZ_homebrew_cask.py
<127.0.0.1> EXEC /bin/sh -c 'chmod u+x /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702/ /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702/AnsiballZ_homebrew_cask.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c '/usr/bin/python /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702/AnsiballZ_homebrew_cask.py && sleep 0'
<127.0.0.1> EXEC /bin/sh -c 'rm -f -r /Users/denis.costa/.ansible/tmp/ansible-tmp-1624454349.499657-26582-53145583098702/ > /dev/null 2>&1 && sleep 0'
fatal: [127.0.0.1]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "accept_external_apps": false,
            "greedy": false,
            "install_options": [],
            "name": [
                "docker",
                "firefox",
                "google-chrome",
                "password-gorilla",
                "slack",
                "spotify",
                "visual-studio"
            ],
            "path": "/usr/local/bin:/opt/homebrew/bin",
            "state": "present",
            "sudo_password": null,
            "update_homebrew": false,
            "upgrade_all": false
        }
    },
    "msg": "Updating Homebrew...\n==> Auto-updated Homebrew!\nUpdated 1 tap (homebrew/core).\n==> Updated Formulae\nUpdated 1 formula.\n\nError: It seems there is already an App at '/Applications/Firefox.app'."
}

```

**Ansible version:**
```
$ ansible --version
ansible 2.10.9
  config file = /Users/denis.costa/projects/dotfiles/ansible.cfg
  configured module search path = ['/Users/denis.costa/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /Users/denis.costa/.pyenv/versions/3.9.1/lib/python3.9/site-packages/ansible
  executable location = /Users/denis.costa/.pyenv/versions/3.9.1/bin/ansible
  python version = 3.9.1 (default, May  6 2021, 10:38:39) [Clang 12.0.0 (clang-1200.0.32.29)]
```

**My OS info:**
![Screen Shot 2021-06-23 at 10 16 11](https://user-images.githubusercontent.com/358777/123103093-10b62280-d40c-11eb-916f-32bbb691f283.png)

I would also expect using `ansible.builtin.package` to invoke the homebrew_cask and not the homebrew module.
Right now this doesn't happen and I have to explicitly use `homebrew_cask` for my play to work.

```
ansible [core 2.11.2]
  config file = None
  configured module search path = ['~/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/local/Cellar/ansible/4.2.0/libexec/lib/python3.9/site-packages/ansible
  ansible collection location = ~/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/local/bin/ansible
  python version = 3.9.6 (default, Jun 29 2021, 05:25:02) [Clang 12.0.5 (clang-1205.0.22.9)]
  jinja version = 3.0.1
  libyaml = True
```
@lockejan not sure how that is related to this bug report? Also we have no influence on what the package module does, that module is part of ansible-core.
@felixfontein my bad.
In my case it failed when invoking the wrong variant of the homebrew module, but worked when explicitily calling the "correct" one, which is somewhat different on second sight to what the initial issuer seems to experience.

My initial thought was: why is it not possible to have one homebrew module for both variants (casks and non-casks)?
Such as one is used to when using any linux package manager.
Sorry for the noise. I'll might open a new issue then.
> My initial thought was: why is it not possible to have one homebrew module for both variants (casks and non-casks)?

I guess that would be great. I know too little about homebrew and casks to be able to say whether this is possible in a backwards-compatible way for the `homebrew` module. (I don't use homebrew at all.)
Facing the same issue. If an application is already installed without using homebrew, ansible install fails.
Example:
`- name: Install slack
      homebrew_cask:
        name: slack
        state: present`

Error: `fatal: [localhost]: FAILED! => {
    "changed": false,
    "invocation": {
        "module_args": {
            "accept_external_apps": false,
            "greedy": false,
            "install_options": [],
            "name": [
                "slack"
            ],
            "path": "/usr/local/bin:/opt/homebrew/bin",
            "state": "present",
            "sudo_password": null,
            "update_homebrew": false,
            "upgrade_all": false
        }
    },
    "msg": "Running `brew update --preinstall`...\n==> Auto-updated Homebrew!\nUpdated 1 tap (homebrew/core).\n==> Updated Formulae\nUpdated 6 formulae.\n\nError: It seems there is already an App at '/Applications/Slack.app'."
}
`

Specs:
Mac OS Monterey
M1 MacBook Pro
@prajain12 this issue is about apps installed by homebrew. If you install an app another way and try to install it with homebrew, that error is expected as far as I can tell. This is not a bug in the module, but expected behavior from homebrew.
I am facing this issue as well. Since Homebrew removed the cask install option, I have added my cask install to the regular package mgmt module, which causes brew to fail if the package is already installed. Are there any updates to this, or is there a new issue tracking this problem?
@jackfarzan You can add logic to check whether the app already exists at the destination or not. If it does not already exist, then you can move ahead with installing using homebrew.
As this is not an ansible problem, this can be addressed in homebrew.
Hey folks, sorry for the delay. Could you please check if #4177 works for you and let me know? Thanks in advance.
resolved_by_pr #4177 
I applied the changes in https://github.com/ansible-collections/community.general/pull/4177/files (homebrew.py) and that fixes the issue with detecting already installed casks.

+1 for not having to separate between `homebrew` and `homebrew_cask` from me.
I applied the last PR #4177 only the `homebrew.py` file and I still get that message

> Warning: Cask 'alfred' is already installed. To re-install Alfred, run: brew reinstall - - cask alfred

I'm using the task as `community.general.homebrew`

I don't know if I've to to a force reinstall with ansible first or what. 
I also ran into this issue with several package installations. The only package that works is zoxide because on `brew info zoxide` `Poured from bottle on...` is returned.
The condition on line https://github.com/ansible-collections/community.general/blob/main/plugins/modules/packaging/os/homebrew.py#L480 checks if `Built from source` or `Poured from bottle` is returned and maybe I'm to naive but couldn't we just check if `Not installed` is not returned in the info command?
In my short test for packages that are not installed the `brew info package` command always had the `Not installed` line in it and all packages that are installed don't have this line in their output.
Files identified in the description:
* [`plugins/modules/homebrew.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/homebrew.py)

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
@prajain12  i solved it with following work around

```
- name: Check the list of installed apps
  stat: path=/Applications/{{item.path}}
  with_items:
    - { path: "GoLand.app", name: "goland" }
    - { path: "pgAdmin 4.app", name: "pgadmin4" }
    - { path: "Google Chrome.app", name: "google-chrome" }
    - { path: "Postman.app", name: "postman" }
    - { path: "Slack.app", name: "slack" }
    - { path: "Visual Studio Code.app", name: "visual-studio-code" }
  register: apps_install_status

- name: Install Apps not in the applications folder
  homebrew_cask: name={{ item.item.name }} state=present
  with_items: "{{ apps_install_status.results }}"
  when: not item.stat.exists
```
Hello, I've been using this module from the `ansible.builtin.package` and I've also ran into this issue. 

I tried to evaluate whether a package is installed with the command `brew info <package>` and noticed that when a package isn't installed, regardless of whether it's a cask or formulae, the `Not installed` line is added to the output. 

Could this be a valid solution?

I'm considering to open a pull request to try to solve this.
> Could this be a valid solution?

Sounds sensible to me, assuming that it isn't possible that `Not installed` shows up even when the package is installed, for example if it is part of the package's description as a single line.

(I have no idea how the `brew info` output looks like, so maybe this is impossible.)
> Sounds sensible to me, assuming that it isn't possible that Not installed shows up even when the package is installed, for example if it is part of the package's description as a single line.

Well thought, is unlikely but not impossible. I think that by using the JSON output of `brew info` we can get a more robust solution, I've done some tests of it using `jq` for formulae and casks, and this is what I found.

# Formulae

## Check if installed

```shell
brew info --json=v2 <formula> | jq '.formulae[0].installed'
```

## Installed output

Not empty JSON array

```JSON
[
  {
    "version": "3.3a_3",
    "used_options": [],
    "built_as_bottle": true,
    "poured_from_bottle": true,
    "time": 1705034723,
    "runtime_dependencies": [
       ...
    ]
  }
]
```

## Not installed output

Empty JSON array

```JSON
[]
```

# Casks

## Check if installed

```shell
brew info --json=v2 <cask> | jq '.casks[0].installed'
```

## Installed output

Not empty JSON string

```JSON
"121.0.1"
```

## Not installed output

`null`

```JSON
null
```

Seems to me that if we check those fields of the JSON output and any of them are different from `null` and `[]` the formula/cask is installed.
Using JSON output is generally the best way forward. Since when has JSON output been available for homebrew? The module doesn't seem to specify which versions of homebrew it supports, so this could be a bit tricky.
> Since when has JSON output been available for homebrew?

Looks like at least a decade, according to blame on the [docs](https://github.com/Homebrew/brew/blame/302e83e9aaa0a3a4c0644f43a0302a582128ed1f/docs/Querying-Brew.md#L13). Brew v1 was [released in 2016](https://github.com/Homebrew/brew/releases/tag/1.0.0). Seems like a safe change to me.
To clarify, according to [docs](https://docs.brew.sh/Querying-Brew#brew-info---json), only the `v2` version of  `brew info --json` supports formulae and casks, this got [introduced](https://github.com/Homebrew/brew/pull/8893) on October 2020 in the  [2.5.7](https://github.com/Homebrew/brew/releases/tag/2.5.7) release of brew. If we choose to use the `v1` version, we have to handle casks in a different way.

Resolving patch:
diff --git a/plugins/modules/homebrew.py b/plugins/modules/homebrew.py
--- a/plugins/modules/homebrew.py
+++ b/plugins/modules/homebrew.py
@@ -165,6 +165,7 @@
     version_added: '0.2.0'
 '''
 
+import json
 import os.path
 import re
 
@@ -184,6 +185,10 @@ def _create_regex_group_complement(s):
     chars = filter(None, (line.split('#')[0].strip() for line in lines))
     group = r'[^' + r''.join(chars) + r']'
     return re.compile(group)
+
+
+def _check_package_in_json(json_output, package_type):
+    return bool(json_output.get(package_type, []) and json_output[package_type][0].get("installed"))
 # /utils ------------------------------------------------------------------ }}}
 
 
@@ -479,17 +484,13 @@ def _current_package_is_installed(self):
         cmd = [
             "{brew_path}".format(brew_path=self.brew_path),
             "info",
+            "--json=v2",
             self.current_package,
         ]
         rc, out, err = self.module.run_command(cmd)
-        for line in out.split('\n'):
-            if (
-                re.search(r'Built from source', line)
-                or re.search(r'Poured from bottle', line)
-            ):
-                return True
-
-        return False
+        data = json.loads(out)
+
+        return _check_package_in_json(data, "formulae") or _check_package_in_json(data, "casks")
 
     def _current_package_is_outdated(self):
         if not self.valid_package(self.current_package):


Test patch:
diff --git a/tests/integration/targets/homebrew/tasks/casks.yml b/tests/integration/targets/homebrew/tasks/casks.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/homebrew/tasks/casks.yml
@@ -0,0 +1,99 @@
+---
+####################################################################
+# WARNING: These are designed specifically for Ansible tests       #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+# Test code for the homebrew module.
+# Copyright (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Find brew binary
+  command: which brew
+  register: brew_which
+  when: ansible_distribution in ['MacOSX']
+
+- name: Get owner of brew binary
+  stat:
+    path: "{{ brew_which.stdout }}"
+  register: brew_stat
+  when: ansible_distribution in ['MacOSX']
+
+#- name: Use ignored-pinned option while upgrading all
+#  homebrew:
+#    upgrade_all: true
+#    upgrade_options: ignore-pinned
+#  become: true
+#  become_user: "{{ brew_stat.stat.pw_name }}"
+#  register: upgrade_option_result
+#  environment:
+#    HOMEBREW_NO_AUTO_UPDATE: True
+
+#- assert:
+#    that:
+#      - upgrade_option_result.changed
+
+- vars:
+    package_name: kitty
+
+  block:
+  - name: Make sure {{ package_name }} package is not installed
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+
+  - name: Install {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: present
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - package_result.changed
+
+  - name: Again install {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: present
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - not package_result.changed
+
+  - name: Uninstall {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - package_result.changed
+
+  - name: Again uninstall {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - not package_result.changed
diff --git a/tests/integration/targets/homebrew/tasks/formulae.yml b/tests/integration/targets/homebrew/tasks/formulae.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/homebrew/tasks/formulae.yml
@@ -0,0 +1,99 @@
+---
+####################################################################
+# WARNING: These are designed specifically for Ansible tests       #
+# and should not be used as examples of how to write Ansible roles #
+####################################################################
+
+# Test code for the homebrew module.
+# Copyright (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+- name: Find brew binary
+  command: which brew
+  register: brew_which
+  when: ansible_distribution in ['MacOSX']
+
+- name: Get owner of brew binary
+  stat:
+    path: "{{ brew_which.stdout }}"
+  register: brew_stat
+  when: ansible_distribution in ['MacOSX']
+
+#- name: Use ignored-pinned option while upgrading all
+#  homebrew:
+#    upgrade_all: true
+#    upgrade_options: ignore-pinned
+#  become: true
+#  become_user: "{{ brew_stat.stat.pw_name }}"
+#  register: upgrade_option_result
+#  environment:
+#    HOMEBREW_NO_AUTO_UPDATE: True
+
+#- assert:
+#    that:
+#      - upgrade_option_result.changed
+
+- vars:
+    package_name: gnu-tar
+
+  block:
+  - name: Make sure {{ package_name }} package is not installed
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+
+  - name: Install {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: present
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - package_result.changed
+
+  - name: Again install {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: present
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - not package_result.changed
+
+  - name: Uninstall {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - package_result.changed
+
+  - name: Again uninstall {{ package_name }} package using homebrew
+    homebrew:
+      name: "{{ package_name }}"
+      state: absent
+      update_homebrew: false
+    become: true
+    become_user: "{{ brew_stat.stat.pw_name }}"
+    register: package_result
+
+  - assert:
+      that:
+        - not package_result.changed
diff --git a/tests/integration/targets/homebrew/tasks/main.yml b/tests/integration/targets/homebrew/tasks/main.yml
--- a/tests/integration/targets/homebrew/tasks/main.yml
+++ b/tests/integration/targets/homebrew/tasks/main.yml
@@ -9,91 +9,9 @@
 # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
 # SPDX-License-Identifier: GPL-3.0-or-later
 
-- name: Find brew binary
-  command: which brew
-  register: brew_which
-  when: ansible_distribution in ['MacOSX']
-
-- name: Get owner of brew binary
-  stat:
-    path: "{{ brew_which.stdout }}"
-  register: brew_stat
-  when: ansible_distribution in ['MacOSX']
-
-#- name: Use ignored-pinned option while upgrading all
-#  homebrew:
-#    upgrade_all: true
-#    upgrade_options: ignore-pinned
-#  become: true
-#  become_user: "{{ brew_stat.stat.pw_name }}"
-#  register: upgrade_option_result
-#  environment:
-#    HOMEBREW_NO_AUTO_UPDATE: True
-
-#- assert:
-#    that:
-#      - upgrade_option_result.changed
-
-- vars:
-    package_name: gnu-tar
-
+- block:
+  - include_tasks: 'formulae.yml'
+ 
+- when: ansible_distribution in ['MacOSX']
   block:
-  - name: Make sure {{ package_name }} package is not installed
-    homebrew:
-      name: "{{ package_name }}"
-      state: absent
-      update_homebrew: false
-    become: true
-    become_user: "{{ brew_stat.stat.pw_name }}"
-
-  - name: Install {{ package_name }} package using homebrew
-    homebrew:
-      name: "{{ package_name }}"
-      state: present
-      update_homebrew: false
-    become: true
-    become_user: "{{ brew_stat.stat.pw_name }}"
-    register: package_result
-
-  - assert:
-      that:
-        - package_result.changed
-
-  - name: Again install {{ package_name }} package using homebrew
-    homebrew:
-      name: "{{ package_name }}"
-      state: present
-      update_homebrew: false
-    become: true
-    become_user: "{{ brew_stat.stat.pw_name }}"
-    register: package_result
-
-  - assert:
-      that:
-        - not package_result.changed
-
-  - name: Uninstall {{ package_name }} package using homebrew
-    homebrew:
-      name: "{{ package_name }}"
-      state: absent
-      update_homebrew: false
-    become: true
-    become_user: "{{ brew_stat.stat.pw_name }}"
-    register: package_result
-
-  - assert:
-      that:
-        - package_result.changed
-
-  - name: Again uninstall {{ package_name }} package using homebrew
-    homebrew:
-      name: "{{ package_name }}"
-      state: absent
-      update_homebrew: false
-    become: true
-    become_user: "{{ brew_stat.stat.pw_name }}"
-    register: package_result
-
-  - assert:
-      that:
-        - not package_result.changed
+    - include_tasks: 'casks.yml'