Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new rule for nix-shell #1393

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ The following rules are enabled by default on specific platforms only:
* `brew_update_formula` &ndash; turns `brew update <formula>` into `brew upgrade <formula>`;
* `dnf_no_such_command` &ndash; fixes mistyped DNF commands;
* `nixos_cmd_not_found` &ndash; installs apps on NixOS;
* `nix_shell` &ndash; re-runs your command in a `nix-shell`;
* `pacman` &ndash; installs app with `pacman` if it is not installed (uses `yay`, `pikaur` or `yaourt` if available);
* `pacman_invalid_option` &ndash; replaces lowercase `pacman` options with uppercase.
* `pacman_not_found` &ndash; fixes package name with `pacman`, `yay`, `pikaur` or `yaourt`.
Expand Down
90 changes: 90 additions & 0 deletions tests/rules/test_nix_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import pytest
from thefuck.rules.nix_shell import get_nixpkgs_names, match, get_new_command
from thefuck.types import Command
from unittest.mock import patch, MagicMock


@pytest.mark.parametrize(
"script,output,nixpkgs_names",
[
# output can be retrived by running `THEFUCK_DEBUG=true thefuck lsof`
(
"lsof",
"/nix/store/p6dlr3skfhxpyphipg2bqnj52999banh-bash-5.2-p15/bin/sh: line 1: lsof: command not found",
["lsof"],
),
],
)
def test_match(script, output, nixpkgs_names):
with patch("thefuck.rules.nix_shell.get_nixpkgs_names") as mocked_get_nixpkgs_names:
mocked_get_nixpkgs_names.return_value = nixpkgs_names
command = Command(script, output)
assert match(command)


@pytest.mark.parametrize(
"script,output,nixpkgs_names",
[
# output can be retrived by running `THEFUCK_DEBUG=true thefuck foo`
(
"foo",
"/nix/store/p6dlr3skfhxpyphipg2bqnj52999banh-bash-5.2-p15/bin/sh: line 1: foo: command not found",
[],
),
],
)
def test_not_match(script, output, nixpkgs_names):
with patch("thefuck.rules.nix_shell.get_nixpkgs_names") as mocked_get_nixpkgs_names:
mocked_get_nixpkgs_names.return_value = nixpkgs_names
command = Command(script, output)
assert not match(command)


@pytest.mark.parametrize(
"script,nixpkgs_names,new_command",
[
(
"lsof -i :3000",
["busybox", "lsof"],
[
'nix-shell -p busybox --run "lsof -i :3000"',
'nix-shell -p lsof --run "lsof -i :3000"',
],
),
("xev", ["xorg.xev"], ['nix-shell -p xorg.xev --run "xev"']),
],
)
def test_get_new_command(script, nixpkgs_names, new_command):
"""Check that flags and params are preserved in the new command"""

command = Command(script, "")
with patch("thefuck.rules.nix_shell.get_nixpkgs_names") as mocked_get_nixpkgs_names:
mocked_get_nixpkgs_names.return_value = nixpkgs_names
assert get_new_command(command) == new_command


# Mocks the stderr of `command-not-found QUERY`. Mock values are retrieved by
# running `THEFUCK_DEBUG=true thefuck command-not-found lsof`.
mocked_cnf_stderr = {
"lsof": "The program 'lsof' is not in your PATH. It is provided by several packages.\nYou can make it available in an ephemeral shell by typing one of the following:\n nix-shell -p busybox\n nix-shell -p lsof",
"xev": "The program 'xev' is not in your PATH. You can make it available in an ephemeral shell by typing:\n nix-shell -p xorg.xev",
"foo": "foo: command not found",
}


@pytest.mark.parametrize(
"bin,expected_nixpkgs_names,cnf_stderr",
[
("lsof", ["busybox", "lsof"], mocked_cnf_stderr["lsof"]),
("xev", ["xorg.xev"], mocked_cnf_stderr["xev"]),
("foo", [], mocked_cnf_stderr["foo"]),
],
)
def test_get_nixpkgs_names(bin, expected_nixpkgs_names, cnf_stderr):
"""Check that `get_nixpkgs_names` returns the correct names"""

with patch("subprocess.run") as mocked_run:
result = MagicMock()
result.stderr = cnf_stderr
mocked_run.return_value = result
assert get_nixpkgs_names(bin) == expected_nixpkgs_names
51 changes: 51 additions & 0 deletions thefuck/rules/nix_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from thefuck.specific.nix import nix_available
import subprocess

enabled_by_default = nix_available

# Set the priority just ahead of `fix_file` rule, which can generate low quality matches due
# to the sheer amount of paths in the nix store.
priority = 999


def get_nixpkgs_names(bin):
"""
Returns the name of the Nix package that provides the given binary. It uses the
`command-not-found` binary to do so, which is how nix-shell generates it's own suggestions.
"""

result = subprocess.run(
["command-not-found", bin], stderr=subprocess.PIPE, universal_newlines=True
)

# The suggestion, if any, will be found in stderr. Upstream definition: https://github.com/NixOS/nixpkgs/blob/b6fbd87328f8eabd82d65cc8f75dfb74341b0ace/nixos/modules/programs/command-not-found/command-not-found.nix#L48-L90
text = result.stderr

# return early if binary is not available through nix
if "nix-shell" not in text:
return []

nixpkgs_names = [
line.split()[-1] for line in text.splitlines() if "nix-shell -p" in line
]
return nixpkgs_names


def match(command):
bin = command.script_parts[0]
return (
"command not found" in command.output # only match commands which had exit code: 127 # noqa: E501
and get_nixpkgs_names(bin) # only match commands which could be made available through nix # noqa: E501
)


def get_new_command(command):
bin = command.script_parts[0]
nixpkgs_names = get_nixpkgs_names(bin)

# Construct a command for each package name
commands = [
'nix-shell -p {} --run "{}"'.format(name, command.script)
for name in nixpkgs_names
]
return commands