diff --git a/README.md b/README.md
index 8c975c6..4e7b253 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ The entire interface can be summarized by the following excerpt of the manpage:
ns [OPTION]… [SOURCE]… [TARGET_DIR]
```
+>
> Each SOURCE is positional and may be one of:
>
> URI#ATTR select a devshell or package from a flake
@@ -23,6 +24,7 @@ ns [OPTION]… [SOURCE]… [TARGET_DIR]
>
> If a single SOURCE is provided and it is a directory, ns switches into
> it by default unless a TARGET_DIR is explicitly given.
+>
Try it from anywhere:
diff --git a/bin/main.ml b/bin/main.ml
index 30d82c3..f2a2594 100644
--- a/bin/main.ml
+++ b/bin/main.ml
@@ -2,7 +2,7 @@ open Ns
open Ns.Cmd
open Ns.Util
-let main ({ installables; target_info } : Cli.strategy) =
+let main ({ installables; target_info; printcmd } : Cli.strategy) =
let ({ entrypoint; attribute; subshell_dir } : Cli.target_info) = target_info in
Option.value ~default:(Option.value ~default:(Sys.getcwd ()) entrypoint) subshell_dir
|> Unix.cd;
@@ -26,7 +26,7 @@ let main ({ installables; target_info } : Cli.strategy) =
|>+ [ "--command"; Unix.shell ]
| None -> Cmd.builder "nix" |>+ [ "shell" ] |>+ installables
in
- ignore (Cmd.run cmd)
+ if printcmd then print_endline (Cmd.to_string cmd) else ignore (Cmd.run cmd)
;;
Cli.eval main
diff --git a/dune-project b/dune-project
index 7d2b2a5..59731f7 100644
--- a/dune-project
+++ b/dune-project
@@ -2,7 +2,7 @@
(name ns)
-(version 0.4.0)
+(version 0.4.1)
(generate_opam_files true)
diff --git a/flake.nix b/flake.nix
index 11ee820..482528a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -40,6 +40,7 @@
doCheck = true;
nativeCheckInputs = attrValues {
inherit (pkgs) ocamlformat;
+ inherit (pkgs) rustc clippy rustfmt;
};
checkPhase = ''
red() {
@@ -50,9 +51,24 @@
printf "$(red "Error:") Generated opam file does not match provided opam file\n"
exit 1
fi
+ if ! dune test --force --no-buffer --display=quiet; then
+ printf "\n"
+ printf "$(red "Error:") dune test reported test failures\n"
+ exit 1
+ fi
+ if ! clippy-driver --test "${finalAttrs.src}/test/test_ns.rs" -Dwarnings; then
+ printf "\n"
+ printf "$(red "Error:") test.rs contains warnings\n"
+ exit 1
+ fi
if ! dune fmt --diff-command="diff --color=always"; then
printf "\n"
- printf "$(red "Error:") Some files are not properly formatted\n"
+ printf "$(red "Error:") Some OCaml files are not properly formatted\n"
+ exit 1
+ fi
+ if ! rustfmt --check --color=always "${finalAttrs.src}/test/test_ns.rs"; then
+ printf "\n"
+ printf "$(red "Error:") test.rs is not properly formatted\n"
exit 1
fi
'';
@@ -78,7 +94,7 @@
default = pkgs.mkShell {
inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.default ];
packages = attrValues {
- inherit (pkgs) ocamlformat nil;
+ inherit (pkgs) ocamlformat nil rustc clippy rustfmt;
inherit (pkgs.ocamlPackages) ocaml-lsp odoc;
};
shellHook = ''
@@ -86,6 +102,9 @@
'';
};
});
+ checks = eachSystem (pkgs: {
+ inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) default;
+ });
formatter = eachSystem (pkgs: pkgs.nixpkgs-fmt);
};
}
diff --git a/lib/cli.ml b/lib/cli.ml
index 9229e32..ab0b812 100644
--- a/lib/cli.ml
+++ b/lib/cli.ml
@@ -17,14 +17,20 @@ module Cli = struct
type strategy =
{ installables : string list
; target_info : target_info
+ ; printcmd : bool
}
(** Positional URI arguments passed to the program via the command line. *)
let uris = Arg.(value & pos_all dirpath [] & info [])
+ let printcmd =
+ let doc = "Print the command to stdout instead of executing it." in
+ Arg.(value & flag & info [ "printcmd" ] ~doc)
+ ;;
+
(** Processes the args so that the main function knows what to do with them. *)
let make_strategy =
- let build original_args =
+ let build original_args printcmd =
let installables, target_info =
let default_installables = []
and default_target =
@@ -81,9 +87,9 @@ module Cli = struct
, { entrypoint = None; attribute = None; subshell_dir = Some subshell_dir } )
| _ -> Uri.parse_targets_tr original_args, default_target)
in
- { installables; target_info }
+ { installables; target_info; printcmd }
in
- Term.(const build $ uris)
+ Term.(const build $ uris $ printcmd)
;;
let cmd entrypoint =
@@ -104,6 +110,8 @@ module Cli = struct
; `P
"If a single SOURCE is provided and it is a directory, ns switches into it by \
default unless a TARGET_DIR is explicitly given."
+ ; `S Manpage.s_options
+ ; `S ""
; `S Manpage.s_common_options
; `S ""
; `S Manpage.s_examples
diff --git a/lib/cmd.ml b/lib/cmd.ml
index bd1ef80..828847c 100644
--- a/lib/cmd.ml
+++ b/lib/cmd.ml
@@ -16,6 +16,9 @@ module Cmd = struct
| args -> List.fold_left (fun cmd arg -> Cmd.add_arg cmd arg) cmd args
;;
+ (** Convert a command into a string *)
+ let to_string cmd = Cmd.to_string cmd
+
(** Run a command
Example:
diff --git a/ns.opam b/ns.opam
index 4dd23b4..e3a0d57 100644
--- a/ns.opam
+++ b/ns.opam
@@ -1,6 +1,6 @@
# This file is generated by dune, edit dune-project instead
opam-version: "2.0"
-version: "0.4.0"
+version: "0.4.1"
synopsis: "A unified interface for nix shell."
description:
"An intuitive nix shell interface that unifies the `nix shell`, `nix develop` and `nix-shell` commands."
diff --git a/test/dune b/test/dune
new file mode 100644
index 0000000..979cd4b
--- /dev/null
+++ b/test/dune
@@ -0,0 +1,14 @@
+;; Define the test stanza for dune test
+
+(test
+ (name test_ns)
+ (deps test_ns src/flake/flake.nix src/shell/shell.nix)
+ (flags (:standard)))
+
+;; Rule to compile the Rust test file into a binary
+
+(rule
+ (targets test_ns) ; the Dune target name
+ (deps test_ns.rs) ; path relative to this dune file
+ (action
+ (run rustc --test %{deps} -o %{targets} --allow warnings)))
diff --git a/test/src/flake/flake.nix b/test/src/flake/flake.nix
new file mode 100644
index 0000000..2550caa
--- /dev/null
+++ b/test/src/flake/flake.nix
@@ -0,0 +1,12 @@
+{
+ inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
+ outputs = { self, nixpkgs }:
+ let
+ inherit (nixpkgs.lib) genAttrs;
+ inherit (nixpkgs.lib.systems) flakeExposed;
+ eachSystem = f: genAttrs flakeExposed (system: f (import nixpkgs { inherit system; }));
+ in
+ {
+ packages = eachSystem (pkgs: { default = pkgs.hello; });
+ };
+}
diff --git a/test/src/shell/shell.nix b/test/src/shell/shell.nix
new file mode 100644
index 0000000..e586841
--- /dev/null
+++ b/test/src/shell/shell.nix
@@ -0,0 +1,6 @@
+{ pkgs ? import { } }:
+pkgs.mkShell {
+ buildInputs = builtins.attrValues {
+ inherit (pkgs) hello;
+ };
+}
diff --git a/test/test_ns.ml b/test/test_ns.ml
new file mode 100644
index 0000000..db2bd53
--- /dev/null
+++ b/test/test_ns.ml
@@ -0,0 +1,10 @@
+let color_mode =
+ Option.value
+ ~default:"auto"
+ (Option.map
+ (fun force -> if force = "1" then "always" else "never")
+ (Sys.getenv_opt "CLICOLOR_FORCE"))
+;;
+
+let cmd = Printf.sprintf {| sh -c ./test_ns --color=%s --no-capture |} color_mode in
+exit (Sys.command cmd)
diff --git a/test/test_ns.rs b/test/test_ns.rs
new file mode 100644
index 0000000..11a5dbe
--- /dev/null
+++ b/test/test_ns.rs
@@ -0,0 +1,369 @@
+use std::{env, path, process, sync};
+
+static DUNE_TEST_DIR: sync::OnceLock = sync::OnceLock::new();
+static ENV_SHELL: sync::OnceLock = sync::OnceLock::new();
+
+fn dune_test_dir() -> &'static path::PathBuf {
+ DUNE_TEST_DIR.get_or_init(|| env::current_dir().expect("failed to get test directory"))
+}
+fn env_shell() -> &'static String {
+ ENV_SHELL.get_or_init(|| env::var("SHELL").expect("failed to get shell from environment"))
+}
+
+fn ns() -> path::PathBuf {
+ dune_test_dir()
+ .join("..")
+ .join("bin")
+ .join("main.exe")
+ .canonicalize()
+ .expect("failed to canonicalize path to ns binary")
+}
+fn flake_dir() -> path::PathBuf {
+ dune_test_dir()
+ .join("src")
+ .join("flake")
+ .canonicalize()
+ .expect("failed to canonicalize path to ns binary")
+}
+fn shell_dir() -> path::PathBuf {
+ dune_test_dir()
+ .join("src")
+ .join("shell")
+ .canonicalize()
+ .expect("failed to canonicalize path to ns binary")
+}
+
+fn assert_stdout_eq(stdout: &[u8], expected: &[&str]) {
+ let stdout = String::from_utf8_lossy(stdout);
+ let stdout = stdout.replace('\'', "");
+ let stdout: Vec<&str> = stdout.split_whitespace().collect();
+
+ if expected != stdout.as_slice() {
+ panic!(
+ "\nExpected: \x1b[32m{:#?}\x1b[0m\nObserved: \x1b[31m{:#?}\x1b[0m",
+ expected,
+ stdout.as_slice()
+ )
+ }
+}
+
+mod nix_develop_tests {
+ use super::*;
+
+ #[test]
+ fn single_arg_no_attr() {
+ let output = process::Command::new(ns())
+ .args([flake_dir().as_os_str(), "--printcmd".as_ref()])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix",
+ "develop",
+ &flake_dir().to_string_lossy(),
+ "--command",
+ env_shell(),
+ ],
+ );
+ }
+
+ #[test]
+ fn single_arg_single_attr() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "develop", &flake_uri, "--command", env_shell()],
+ );
+ }
+
+ #[test]
+ fn two_args_no_attrs() {
+ let output = process::Command::new(ns())
+ .args([flake_dir().as_os_str(), ".".as_ref(), "--printcmd".as_ref()])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix",
+ "develop",
+ &flake_dir().to_string_lossy(),
+ "--command",
+ env_shell(),
+ ],
+ );
+ }
+
+ #[test]
+ fn two_args_single_attr_lhs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, ".", "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "develop", &flake_uri, "--command", env_shell()],
+ );
+ }
+}
+
+mod legacy_nix_shell_tests {
+ use super::*;
+
+ #[test]
+ fn single_arg_no_attr() {
+ let output = process::Command::new(ns())
+ .args([shell_dir().as_os_str(), "--printcmd".as_ref()])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix-shell",
+ &shell_dir().to_string_lossy(),
+ "--command",
+ env_shell(),
+ ],
+ );
+ }
+
+ #[test]
+ fn single_arg_single_attr() {
+ let shell_uri = format!("{}#default", shell_dir().display());
+ let output = process::Command::new(ns())
+ .args([&shell_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix-shell",
+ "--attr",
+ "default",
+ &shell_dir().to_string_lossy(),
+ "--command",
+ env_shell(),
+ ],
+ );
+ }
+}
+
+mod nix_shell_tests {
+ use super::*;
+
+ #[test]
+ fn single_arg_multi_attr() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]);
+ }
+
+ #[test]
+ fn two_args_single_attr_rhs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([
+ flake_dir().as_os_str(),
+ flake_uri.as_ref(),
+ "--printcmd".as_ref(),
+ ])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()],
+ );
+ }
+
+ #[test]
+ fn two_args_single_attrs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, &flake_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, &flake_uri]);
+ }
+
+ #[test]
+ fn two_args_multi_attr_lhs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, ".", "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]);
+ }
+
+ #[test]
+ fn two_args_multi_attr_rhs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([
+ flake_dir().as_os_str(),
+ flake_uri.as_ref(),
+ "--printcmd".as_ref(),
+ ])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()],
+ );
+ }
+
+ #[test]
+ fn two_args_multi_attrs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let output = process::Command::new(ns())
+ .args([&flake_uri, &flake_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, &flake_uri]);
+ }
+
+ #[test]
+ fn three_args_single_attrs_lhs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#hello";
+ let output = process::Command::new(ns())
+ .args([&flake_uri, remote_uri, ".", "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, remote_uri]);
+ }
+
+ #[test]
+ fn three_args_single_attrs_rhs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#hello";
+ let output = process::Command::new(ns())
+ .args([
+ flake_dir().as_os_str(),
+ flake_uri.as_ref(),
+ remote_uri.as_ref(),
+ "--printcmd".as_ref(),
+ ])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix",
+ "shell",
+ remote_uri,
+ &flake_uri,
+ &flake_dir().to_string_lossy(),
+ ],
+ );
+ }
+
+ #[test]
+ fn three_args_single_attrs() {
+ let flake_uri = format!("{}#default", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#hello";
+ let output = process::Command::new(ns())
+ .args([&flake_uri, &flake_uri, remote_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "shell", remote_uri, &flake_uri, &flake_uri],
+ );
+ }
+
+ #[test]
+ fn three_args_multi_attrs_lhs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}";
+ let output = process::Command::new(ns())
+ .args([&flake_uri, remote_uri, ".", "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, remote_uri]);
+ }
+
+ #[test]
+ fn three_args_multi_attrs_rhs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}";
+ let output = process::Command::new(ns())
+ .args([
+ flake_dir().as_os_str(),
+ flake_uri.as_ref(),
+ remote_uri.as_ref(),
+ "--printcmd".as_ref(),
+ ])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &[
+ "nix",
+ "shell",
+ remote_uri,
+ &flake_uri,
+ &flake_dir().to_string_lossy(),
+ ],
+ );
+ }
+
+ #[test]
+ fn three_args_multi_attrs() {
+ let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display());
+ let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}";
+ let output = process::Command::new(ns())
+ .args([&flake_uri, &flake_uri, remote_uri, "--printcmd"])
+ .output()
+ .expect("failed to get command output");
+
+ assert!(output.status.success());
+ assert_stdout_eq(
+ &output.stdout,
+ &["nix", "shell", remote_uri, &flake_uri, &flake_uri],
+ );
+ }
+}