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 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
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
40 changes: 40 additions & 0 deletions tests/rules/test_nix_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from thefuck.rules.nix_shell import get_nixpkgs_name, get_new_command
from thefuck.types import Command
from unittest.mock import patch

mocked_nixpkgs = {
"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(
"command, output", [("lsof", "lsof"), ("xev", "xorg.xev"), ("foo", "")]
)
def test_get_nixpkgs_name(command, output):
"""Check that `get_nixpkgs_name` returns the correct name"""

with patch("subprocess.run") as mocked_run:
instance = mocked_run.return_value
instance.stderr = mocked_nixpkgs[command]
assert get_nixpkgs_name(command) == output


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

command = Command(command_script, "")
with patch("subprocess.run") as mocked_run:
instance = mocked_run.return_value
instance.stderr = mocked_nixpkgs[command.script_parts[0]]
assert get_new_command(command) == new_command
40 changes: 40 additions & 0 deletions thefuck/rules/nix_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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_name(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
)

# return early if package is not available through nix
if "nix-shell" not in result.stderr:
return ""

nixpkgs_name = result.stderr.split()[-1] if result.stderr.split() else ""
return nixpkgs_name


def match(command):
bin = command.script_parts[0]
return (
"nix-shell" not in command.script
and "command not found" in command.output
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example:

$ ponysay moo
The program 'ponysay' is not in your PATH. You can make it available in an
ephemeral shell by typing:
  nix-shell -p ponysay

$ fuck
nix-shell -p ponysay --run "ponysay moo" [enter/↑/↓/ctrl+c]

command not found is not part of the output. Didn't you mean:

Suggested change
and "command not found" in command.output
and "command not found" not in command.output

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your example, command not found is not part of the output

For commands that could be made available through nix:

  1. Indeed, the visible output in the terminal does not contain the text "command not found".
$ spt
The program 'spt' is not in your PATH. You can make it available in an
ephemeral shell by typing:
  nix-shell -p spotify-tui
  1. But when we log the value of command to file, we do see "command not found":
Command(script=spt, output=/nix/store/p6dlr3skfhxpyphipg2bqnj52999banh-bash-5.2-p15/bin/sh: line 1: spt: command not found)

I'm not sure exactly why they're different, but it works 🤷🏼‍♂️.

Admittedly, it could be simpler if we could do:

- and "command not found" in command.output
+ and command.exitcode is 127

But I couldn't find a way to access the numeric exit code (127) from within the match function. Only the human error message (command not found) is available it seems.

Finally, I've annotated the match conditions to make it clear what my intentions are for each.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. That's because the command is executed through the shell — /bin/sh. Would you please run thefuck in debug mode and look for the line that says DEBUG: Received output:? e.g.:

THEFUCK_DEBUG=true thefuck spt

Among the debug messages there will be one with the output generated by the shell. Could you then please update the output values in mocked_nixpkgs?

Also, how about writing tests for match? 😊

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests for match

done

I've cleaned up the PR a fair bit. As for the mock output, I've indicated which command I've used to get them so their purpose should be more clear now. If you notice that I've missed anything let me know. Thank you.

and get_nixpkgs_name(bin)
)


def get_new_command(command):
bin = command.script_parts[0]
return 'nix-shell -p {0} --run "{1}"'.format(get_nixpkgs_name(bin), command.script)