diff --git a/cli/flags.rs b/cli/flags.rs index c1cc2c443e..866b368206 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -53,10 +53,10 @@ pub enum DenoSubcommand { file: Option, }, Install { - root: Option, - exe_name: String, module_url: String, args: Vec, + name: Option, + root: Option, force: bool, }, Repl, @@ -358,22 +358,21 @@ fn install_parse(flags: &mut Flags, matches: &clap::ArgMatches) { }; let force = matches.is_present("force"); - let exe_name = matches.value_of("exe_name").unwrap().to_string(); + let name = matches.value_of("name").map(|s| s.to_string()); let cmd_values = matches.values_of("cmd").unwrap(); - let mut cmd_args = vec![]; - + let mut cmd = vec![]; for value in cmd_values { - cmd_args.push(value.to_string()); + cmd.push(value.to_string()); } - let module_url = cmd_args[0].to_string(); - let args = cmd_args[1..].to_vec(); + let module_url = cmd[0].to_string(); + let args = cmd[1..].to_vec(); flags.subcommand = DenoSubcommand::Install { - root, - exe_name, + name, module_url, args, + root, force, }; } @@ -641,6 +640,18 @@ fn repl_subcommand<'a, 'b>() -> App<'a, 'b> { fn install_subcommand<'a, 'b>() -> App<'a, 'b> { permission_args(SubCommand::with_name("install")) .setting(AppSettings::TrailingVarArg) + .arg( + Arg::with_name("cmd") + .required(true) + .multiple(true) + .allow_hyphen_values(true)) + .arg( + Arg::with_name("name") + .long("name") + .short("n") + .help("Executable file name") + .takes_value(true) + .required(false)) .arg( Arg::with_name("root") .long("root") @@ -653,26 +664,26 @@ fn install_subcommand<'a, 'b>() -> App<'a, 'b> { .short("f") .help("Forcefully overwrite existing installation") .takes_value(false)) - .arg( - Arg::with_name("exe_name") - .required(true) - ) - .arg( - Arg::with_name("cmd") - .required(true) - .multiple(true) - .allow_hyphen_values(true) - ) .arg(ca_file_arg()) .arg(unstable_arg()) .about("Install script as an executable") .long_about( "Installs a script as an executable in the installation root's bin directory. - deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts - deno install colors https://deno.land/std/examples/colors.ts + deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts + deno install https://deno.land/std/examples/colors.ts + +To change the executable name, use -n/--name: + deno install --allow-net --allow-read -n serve https://deno.land/std/http/file_server.ts + +The executable name is inferred by default: + - Attempt to take the file stem of the URL path. The above example would + become 'file_server'. + - If the file stem is something generic like 'main', 'mod', 'index' or 'cli', + and the path has no parent, take the file name of the parent path. Otherwise + settle with the generic name. To change the installation root, use --root: - deno install --allow-net --allow-read --root /usr/local file_server https://deno.land/std/http/file_server.ts + deno install --allow-net --allow-read --root /usr/local https://deno.land/std/http/file_server.ts The installation root is determined, in order of precedence: - --root option @@ -2159,17 +2170,16 @@ mod tests { let r = flags_from_vec_safe(svec![ "deno", "install", - "deno_colors", "https://deno.land/std/examples/colors.ts" ]); assert_eq!( r.unwrap(), Flags { subcommand: DenoSubcommand::Install { - root: None, - exe_name: "deno_colors".to_string(), + name: None, module_url: "https://deno.land/std/examples/colors.ts".to_string(), args: vec![], + root: None, force: false, }, ..Flags::default() @@ -2184,6 +2194,7 @@ mod tests { "install", "--allow-net", "--allow-read", + "-n", "file_server", "https://deno.land/std/http/file_server.ts" ]); @@ -2191,10 +2202,10 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Install { - root: None, - exe_name: "file_server".to_string(), + name: Some("file_server".to_string()), module_url: "https://deno.land/std/http/file_server.ts".to_string(), args: vec![], + root: None, force: false, }, allow_net: true, @@ -2214,6 +2225,7 @@ mod tests { "-f", "--allow-net", "--allow-read", + "-n", "file_server", "https://deno.land/std/http/file_server.ts", "arg1", @@ -2223,10 +2235,10 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Install { - root: Some(PathBuf::from("/usr/local")), - exe_name: "file_server".to_string(), + name: Some("file_server".to_string()), module_url: "https://deno.land/std/http/file_server.ts".to_string(), args: svec!["arg1", "arg2"], + root: Some(PathBuf::from("/usr/local")), force: true, }, allow_net: true, @@ -2610,6 +2622,7 @@ mod tests { "install", "--cert", "example.crt", + "-n", "deno_colors", "https://deno.land/std/examples/colors.ts" ]); @@ -2617,10 +2630,10 @@ mod tests { r.unwrap(), Flags { subcommand: DenoSubcommand::Install { - root: None, - exe_name: "deno_colors".to_string(), + name: Some("deno_colors".to_string()), module_url: "https://deno.land/std/examples/colors.ts".to_string(), args: vec![], + root: None, force: false, }, ca_file: Some("example.crt".to_owned()), diff --git a/cli/installer.rs b/cli/installer.rs index 472f9a3eba..e402b8d03b 100644 --- a/cli/installer.rs +++ b/cli/installer.rs @@ -28,13 +28,13 @@ pub fn is_remote_url(module_url: &str) -> bool { lower.starts_with("http://") || lower.starts_with("https://") } -fn validate_exec_name(exec_name: &str) -> Result<(), Error> { +fn validate_name(exec_name: &str) -> Result<(), Error> { if EXEC_NAME_RE.is_match(exec_name) { Ok(()) } else { Err(Error::new( ErrorKind::Other, - format!("Invalid module name: {}", exec_name), + format!("Invalid executable name: {}", exec_name), )) } } @@ -103,12 +103,28 @@ fn get_installer_root() -> Result { Ok(home_path) } +fn infer_name_from_url(url: &Url) -> Option { + let path = PathBuf::from(url.path()); + let stem = match path.file_stem() { + Some(stem) => stem.to_string_lossy().to_string(), + None => return None, + }; + if let Some(parent_path) = path.parent() { + if stem == "main" || stem == "mod" || stem == "index" || stem == "cli" { + if let Some(parent_name) = parent_path.file_name() { + return Some(parent_name.to_string_lossy().to_string()); + } + } + } + Some(stem) +} + pub fn install( flags: Flags, - root: Option, - exec_name: &str, module_url: &str, args: Vec, + name: Option, + root: Option, force: bool, ) -> Result<(), Error> { let root = if let Some(root) = root { @@ -144,8 +160,18 @@ pub fn install( Url::from_file_path(module_path).expect("Path should be absolute") }; - validate_exec_name(exec_name)?; - let mut file_path = installation_dir.join(exec_name); + let name = name.or_else(|| infer_name_from_url(&module_url)); + + let name = match name { + Some(name) => name, + None => return Err(Error::new( + ErrorKind::Other, + "An executable name was not provided. One could not be inferred from the URL. Aborting.", + )), + }; + + validate_name(name.as_str())?; + let mut file_path = installation_dir.join(&name); if cfg!(windows) { file_path = file_path.with_extension("cmd"); @@ -154,7 +180,7 @@ pub fn install( if file_path.exists() && !force { return Err(Error::new( ErrorKind::Other, - "Existing installation found. Aborting (Use -f to overwrite)", + "Existing installation found. Aborting (Use -f to overwrite).", )); }; @@ -187,7 +213,7 @@ pub fn install( generate_executable_file(file_path.to_owned(), executable_args)?; - println!("✅ Successfully installed {}", exec_name); + println!("✅ Successfully installed {}", name); println!("{}", file_path.to_string_lossy()); let installation_dir_str = installation_dir.to_string_lossy(); @@ -229,6 +255,61 @@ mod tests { assert!(!is_remote_url("./dev/deno_std/http/file_server.ts")); } + #[test] + fn install_infer_name_from_url() { + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/server.ts").unwrap() + ), + Some("server".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/main.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/mod.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/index.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url( + &Url::parse("https://example.com/abc/cli.ts").unwrap() + ), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("https://example.com/main.ts").unwrap()), + Some("main".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("https://example.com").unwrap()), + None + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///abc/server.ts").unwrap()), + Some("server".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///abc/main.ts").unwrap()), + Some("abc".to_string()) + ); + assert_eq!( + infer_name_from_url(&Url::parse("file:///main.ts").unwrap()), + Some("main".to_string()) + ); + assert_eq!(infer_name_from_url(&Url::parse("file:///").unwrap()), None); + } + #[test] fn install_basic() { let temp_dir = TempDir::new().expect("tempdir fail"); @@ -244,10 +325,10 @@ mod tests { install( Flags::default(), - None, - "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec![], + Some("echo_test".to_string()), + None, false, ) .expect("Install failed"); @@ -273,6 +354,60 @@ mod tests { } } + #[test] + fn install_inferred_name() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/echo_server.ts", + vec![], + None, + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("echo_server"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/echo_server.ts""#)); + } + + #[test] + fn install_inferred_name_from_parent() { + let temp_dir = TempDir::new().expect("tempdir fail"); + let bin_dir = temp_dir.path().join("bin"); + std::fs::create_dir(&bin_dir).unwrap(); + + install( + Flags::default(), + "http://localhost:4545/cli/tests/subdir/main.ts", + vec![], + None, + Some(temp_dir.path().to_path_buf()), + false, + ) + .expect("Install failed"); + + let mut file_path = bin_dir.join("subdir"); + if cfg!(windows) { + file_path = file_path.with_extension("cmd"); + } + + assert!(file_path.exists()); + let content = fs::read_to_string(file_path).unwrap(); + assert!(content + .contains(r#""run" "http://localhost:4545/cli/tests/subdir/main.ts""#)); + } + #[test] fn install_custom_dir_option() { let temp_dir = TempDir::new().expect("tempdir fail"); @@ -281,10 +416,10 @@ mod tests { install( Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Install failed"); @@ -309,10 +444,10 @@ mod tests { install( Flags::default(), - None, - "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec![], + Some("echo_test".to_string()), + None, false, ) .expect("Install failed"); @@ -341,10 +476,10 @@ mod tests { log_level: Some(Level::Error), ..Flags::default() }, - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec!["--foobar".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Install failed"); @@ -370,10 +505,10 @@ mod tests { install( Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", &local_module_str, vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Install failed"); @@ -396,10 +531,10 @@ mod tests { install( Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/echo_server.ts", vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Install failed"); @@ -413,10 +548,10 @@ mod tests { // No force. Install failed. let no_force_result = install( Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/cat.ts", // using a different URL vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ); assert!(no_force_result.is_err()); @@ -431,10 +566,10 @@ mod tests { // Force. Install success. let force_result = install( Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/cat.ts", // using a different URL vec![], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), true, ); assert!(force_result.is_ok()); diff --git a/cli/lib.rs b/cli/lib.rs index fc37ff2af9..673340cb9c 100644 --- a/cli/lib.rs +++ b/cli/lib.rs @@ -291,10 +291,10 @@ async fn info_command( async fn install_command( flags: Flags, - root: Option, - exe_name: String, module_url: String, args: Vec, + name: Option, + root: Option, force: bool, ) -> Result<(), ErrBox> { // Firstly fetch and compile module, this step ensures that module exists. @@ -304,7 +304,7 @@ async fn install_command( let main_module = ModuleSpecifier::resolve_url_or_path(&module_url)?; let mut worker = create_main_worker(global_state, main_module.clone())?; worker.preload_module(&main_module).await?; - installer::install(flags, root, &exe_name, &module_url, args, force) + installer::install(flags, &module_url, args, name, root, force) .map_err(ErrBox::from) } @@ -583,13 +583,14 @@ pub fn main() { } DenoSubcommand::Info { file } => info_command(flags, file).boxed_local(), DenoSubcommand::Install { - root, - exe_name, module_url, args, + name, + root, force, - } => install_command(flags, root, exe_name, module_url, args, force) - .boxed_local(), + } => { + install_command(flags, module_url, args, name, root, force).boxed_local() + } DenoSubcommand::Repl => run_repl(flags).boxed_local(), DenoSubcommand::Run { script } => run_command(flags, script).boxed_local(), DenoSubcommand::Test { diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 66df5a70ff..953f92c660 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -203,10 +203,10 @@ fn installer_test_local_module_run() { let local_module_str = local_module.to_string_lossy(); deno::installer::install( deno::flags::Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", &local_module_str, vec!["hello".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Failed to install"); @@ -241,10 +241,10 @@ fn installer_test_remote_module_run() { std::fs::create_dir(&bin_dir).unwrap(); deno::installer::install( deno::flags::Flags::default(), - Some(temp_dir.path().to_path_buf()), - "echo_test", "http://localhost:4545/cli/tests/echo.ts", vec!["hello".to_string()], + Some("echo_test".to_string()), + Some(temp_dir.path().to_path_buf()), false, ) .expect("Failed to install"); @@ -1712,6 +1712,7 @@ fn cafile_install_remote_module() { .arg(cafile) .arg("--root") .arg(temp_dir.path()) + .arg("-n") .arg("echo_test") .arg("https://localhost:5545/cli/tests/echo.ts") .output() diff --git a/cli/tests/subdir/main.ts b/cli/tests/subdir/main.ts new file mode 100644 index 0000000000..29acf42e09 --- /dev/null +++ b/cli/tests/subdir/main.ts @@ -0,0 +1,3 @@ +if (import.meta.main) { + console.log("Hello, world!"); +} diff --git a/std/examples/README.md b/std/examples/README.md index ea85da542d..1c47d258ed 100644 --- a/std/examples/README.md +++ b/std/examples/README.md @@ -16,13 +16,13 @@ deno --allow-net https://deno.land/std/examples/echo_server.ts Or ```shell -deno install --allow-net echo_server https://deno.land/std/examples/echo_server.ts +deno install --allow-net https://deno.land/std/examples/echo_server.ts ``` ### cat - print file to standard output ```shell -deno install --allow-read deno_cat https://deno.land/std/examples/cat.ts +deno install --allow-read -n deno_cat https://deno.land/std/examples/cat.ts deno_cat file.txt ``` @@ -31,7 +31,7 @@ deno_cat file.txt A very useful command by Soheil Rashidi ported to Deno. ```shell -deno install --allow-read catj https://deno.land/std/examples/catj.ts +deno install --allow-read https://deno.land/std/examples/catj.ts catj example.json catj file1.json file2.json echo example.json | catj - @@ -47,7 +47,7 @@ deno --allow-net=deno.land https://deno.land/std/examples/curl.ts https://deno.l ``` export GIST_TOKEN=ABC # Generate at https://github.com/settings/tokens -deno install --allow-net --allow-env gist https://deno.land/std/examples/gist.ts +deno install --allow-net --allow-env https://deno.land/std/examples/gist.ts gist --title "Example gist 1" script.ts gist --t "Example gist 2" script2.ts ``` diff --git a/std/examples/catj.ts b/std/examples/catj.ts index 3ef14ce0b1..bb2e9051b6 100644 --- a/std/examples/catj.ts +++ b/std/examples/catj.ts @@ -4,7 +4,7 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Install using `deno install` -// $ deno install --allow-read catj https://deno.land/std/examples/catj.ts +// $ deno install --allow-read https://deno.land/std/examples/catj.ts /* eslint-disable @typescript-eslint/no-use-before-define */ import { parse } from "../flags/mod.ts"; diff --git a/std/http/file_server.ts b/std/http/file_server.ts index cc92e0d47f..ded930db50 100755 --- a/std/http/file_server.ts +++ b/std/http/file_server.ts @@ -63,7 +63,7 @@ if (serverArgs.h ?? serverArgs.help) { Serves a local directory in HTTP. INSTALL: - deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts + deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts USAGE: file_server [path] [options] diff --git a/std/manual.md b/std/manual.md index d1cd56e798..29abe2c661 100644 --- a/std/manual.md +++ b/std/manual.md @@ -289,7 +289,7 @@ await Deno.remove("request.log"); This one serves a local directory in HTTP. ```bash -deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts +deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts ``` Run it: @@ -876,8 +876,8 @@ Or you could import it into another ES module to consume: Deno provides `deno install` to easily install and distribute executable code. -`deno install [FLAGS...] [EXE_NAME] [URL] [SCRIPT_ARGS...]` will install the -script available at `URL` under the name `EXE_NAME`. +`deno install [OPTIONS...] [URL] [SCRIPT_ARGS...]` will install the script +available at `URL` under the name `EXE_NAME`. This command creates a thin, executable shell script which invokes `deno` using the specified CLI flags and main module. It is place in the installation root's @@ -886,17 +886,31 @@ the specified CLI flags and main module. It is place in the installation root's Example: ```shell -$ deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts +$ deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts [1/1] Compiling https://deno.land/std/http/file_server.ts ✅ Successfully installed file_server. /Users/deno/.deno/bin/file_server ``` +To change the executable name, use `-n`/`--name`: + +```shell + deno install --allow-net --allow-read -n serve https://deno.land/std/http/file_server.ts +``` + +The executable name is inferred by default: + +- Attempt to take the file stem of the URL path. The above example would become + 'file_server'. +- If the file stem is something generic like 'main', 'mod', 'index' or 'cli', + and the path has no parent, take the file name of the parent path. Otherwise + settle with the generic name. + To change the installation root, use `--root`: ```shell -$ deno install --allow-net --allow-read --root /usr/local file_server https://deno.land/std/http/file_server.ts +$ deno install --allow-net --allow-read --root /usr/local https://deno.land/std/http/file_server.ts ``` The installation root is determined, in order of precedence: @@ -915,7 +929,7 @@ You must specify permissions that will be used to run the script at installation time. ```shell -$ deno install --allow-net --allow-read file_server https://deno.land/std/http/file_server.ts 8080 +$ deno install --allow-net --allow-read https://deno.land/std/http/file_server.ts 8080 ``` The above command creates an executable called `file_server` that runs with @@ -944,7 +958,7 @@ example installation command to your repository: ```shell # Install using deno install -$ deno install awesome_cli https://example.com/awesome/cli.ts +$ deno install -n awesome_cli https://example.com/awesome/cli.ts ``` ## Proxies