diff options
-rw-r--r-- | installer/README.md | 47 | ||||
-rw-r--r-- | installer/mod.ts | 156 | ||||
-rw-r--r-- | installer/test.ts | 299 | ||||
-rw-r--r-- | installer/testdata/args.ts | 9 | ||||
-rw-r--r-- | installer/testdata/echo.ts | 6 |
5 files changed, 388 insertions, 129 deletions
diff --git a/installer/README.md b/installer/README.md index 7c7e9e5a7..6f0435730 100644 --- a/installer/README.md +++ b/installer/README.md @@ -4,50 +4,65 @@ Install remote or local script as executables. ## Installation -`installer` can be install using iteself: +`installer` can be install using itself: ```sh deno -A https://deno.land/std/installer/mod.ts deno_installer https://deno.land/std/installer/mod.ts -A ``` -Installer uses `~/.deno/bin` to store installed scripts so make sure it's in `$PATH` - -``` -echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # change this to your shell -``` - ## Usage Install script ```sh +# remote script $ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read -> Downloading: https://deno.land/std/http/file_server.ts +> [1/1] Compiling https://deno.land/std/http/file_server.ts > > ✅ Successfully installed file_server. +> ~/.deno/bin/file_server # local script $ deno_installer file_server ./deno_std/http/file_server.ts --allow-net --allow-read -> Looking for: /dev/deno_std/http/file_server.ts +> [1/1] Compiling file:///dev/deno_std/http/file_server.ts > > ✅ Successfully installed file_server. +> ~/.deno/bin/file_server ``` -Use installed script: +Run installed script: ```sh $ file_server HTTP server listening on http://0.0.0.0:4500/ ``` -Update installed script +## Custom installation directory + +By default installer uses `~/.deno/bin` to store installed scripts so make sure it's in your `$PATH`. + +``` +echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc # change this to your shell +``` + +If you prefer to change installation directory use `-d` or `--dir` flag. + +``` +$ deno_installer --dir /usr/local/bin file_server ./deno_std/http/file_server.ts --allow-net --allow-read +> [1/1] Compiling file:///dev/deno_std/http/file_server.ts +> +> ✅ Successfully installed file_server. +> /usr/local/bin/file_server +``` + +## Update installed script ```sh $ deno_installer file_server https://deno.land/std/http/file_server.ts --allow-net --allow-read > ⚠️ file_server is already installed, do you want to overwrite it? [yN] > y > -> Downloading: https://deno.land/std/http/file_server.ts +> [1/1] Compiling file:///dev/deno_std/http/file_server.ts > > ✅ Successfully installed file_server. ``` @@ -60,10 +75,14 @@ $ deno_installer --help Install remote or local script as executables. USAGE: - deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...] + deno -A https://deno.land/std/installer/mod.ts [OPTIONS] EXE_NAME SCRIPT_URL [FLAGS...] ARGS: EXE_NAME Name for executable SCRIPT_URL Local or remote URL of script to install - [FLAGS...] List of flags for script, both Deno permission and script specific flag can be used. + [FLAGS...] List of flags for script, both Deno permission and script specific + flag can be used. + +OPTIONS: + -d, --dir <PATH> Installation directory path (defaults to ~/.deno/bin) ``` diff --git a/installer/mod.ts b/installer/mod.ts index f247c6fd3..d7ab125b3 100644 --- a/installer/mod.ts +++ b/installer/mod.ts @@ -1,25 +1,34 @@ #!/usr/bin/env deno --allow-all // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -const { - args, - env, - readDirSync, - mkdirSync, - writeFile, - exit, - stdin, - chmod, - remove, - run -} = Deno; +const { env, stdin, args, exit, writeFile, chmod, run } = Deno; +import { parse } from "../flags/mod.ts"; import * as path from "../fs/path.ts"; import { exists } from "../fs/exists.ts"; +import { ensureDir } from "../fs/ensure_dir.ts"; const encoder = new TextEncoder(); const decoder = new TextDecoder("utf-8"); -const isWindows = Deno.platform.os === "win"; // Regular expression to test disk driver letter. eg "C:\\User\username\path\to" const driverLetterReg = /^[c-z]:/i; +const isWindows = Deno.platform.os === "win"; + +function showHelp(): void { + console.log(`deno installer + Install remote or local script as executables. + +USAGE: + deno -A https://deno.land/std/installer/mod.ts [OPTIONS] EXE_NAME SCRIPT_URL [FLAGS...] + +ARGS: + EXE_NAME Name for executable + SCRIPT_URL Local or remote URL of script to install + [FLAGS...] List of flags for script, both Deno permission and script specific + flag can be used. + +OPTIONS: + -d, --dir <PATH> Installation directory path (defaults to ~/.deno/bin) +`); +} enum Permission { Read, @@ -67,6 +76,20 @@ function getFlagFromPermission(perm: Permission): string { return ""; } +function getInstallerDir(): string { + // In Windows's Powershell $HOME environmental variable maybe null + // if so use $HOMEPATH instead. + let { HOME, HOMEPATH } = env(); + + const HOME_PATH = HOME || HOMEPATH; + + if (!HOME_PATH) { + throw new Error("$HOME is not defined."); + } + + return path.join(HOME_PATH, ".deno", "bin"); +} + async function readCharacter(): Promise<string> { const byteArray = new Uint8Array(1024); await stdin.read(byteArray); @@ -81,14 +104,6 @@ async function yesNoPrompt(message: string): Promise<boolean> { return input === "y" || input === "Y"; } -function createDirIfNotExists(path: string): void { - try { - readDirSync(path); - } catch (e) { - mkdirSync(path, true); - } -} - function checkIfExistsInPath(filePath: string): boolean { // In Windows's Powershell $PATH not exist, so use $Path instead. // $HOMEDRIVE is only used on Windows. @@ -120,33 +135,16 @@ function checkIfExistsInPath(filePath: string): boolean { return false; } -function getInstallerDir(): string { - // In Windows's Powershell $HOME environmental variable maybe null - // if so use $HOMEPATH instead. - let { HOME, HOMEPATH } = env(); - - const HOME_PATH = HOME || HOMEPATH; - - if (!HOME_PATH) { - throw new Error("$HOME is not defined."); - } - - return path.join(HOME_PATH, ".deno", "bin"); +export function isRemoteUrl(url: string): boolean { + return /^https?:\/\//.test(url); } -function showHelp(): void { - console.log(`deno installer - Install remote or local script as executables. - -USAGE: - deno https://deno.land/std/installer/mod.ts EXE_NAME SCRIPT_URL [FLAGS...] - -ARGS: - EXE_NAME Name for executable - SCRIPT_URL Local or remote URL of script to install - [FLAGS...] List of flags for script, both Deno permission and script specific - flag can be used. -`); +function validateModuleName(moduleName: string): boolean { + if (/^[a-z][\w-]*$/i.test(moduleName)) { + return true; + } else { + throw new Error("Invalid module name: " + moduleName); + } } async function generateExecutable( @@ -176,7 +174,7 @@ async function generateExecutable( } // generate Shell script - const template = `#/bin/sh + const template = `#!/bin/sh # ${templateHeader} basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") @@ -200,17 +198,28 @@ exit $ret export async function install( moduleName: string, moduleUrl: string, - flags: string[] + flags: string[], + installationDir?: string ): Promise<void> { - const installerDir = getInstallerDir(); - createDirIfNotExists(installerDir); + if (!installationDir) { + installationDir = getInstallerDir(); + } + await ensureDir(installationDir); + + // if install local module + if (!isRemoteUrl(moduleUrl)) { + moduleUrl = path.resolve(moduleUrl); + } - const filePath = path.join(installerDir, moduleName); + validateModuleName(moduleName); + const filePath = path.join(installationDir, moduleName); if (await exists(filePath)) { const msg = - "⚠️ ${moduleName} is already installed, " + - "do you want to overwrite it?"; + "⚠️ " + + moduleName + + " is already installed" + + ", do you want to overwrite it?"; if (!(await yesNoPrompt(msg))) { return; } @@ -218,7 +227,7 @@ export async function install( // ensure script that is being installed exists const ps = run({ - args: ["deno", "fetch", moduleUrl], + args: ["deno", "fetch", "--reload", moduleUrl], stdout: "inherit", stderr: "inherit" }); @@ -254,44 +263,35 @@ export async function install( console.log(`✅ Successfully installed ${moduleName}`); console.log(filePath); - if (!checkIfExistsInPath(installerDir)) { - console.log("\nℹ️ Add ~/.deno/bin to PATH"); + if (!checkIfExistsInPath(installationDir)) { + console.log(`\nℹ️ Add ${installationDir} to PATH`); console.log( - " echo 'export PATH=\"$HOME/.deno/bin:$PATH\"' >> ~/.bashrc # change" + + " echo 'export PATH=\"" + + installationDir + + ":$PATH\"' >> ~/.bashrc # change" + " this to your shell" ); } } -export async function uninstall(moduleName: string): Promise<void> { - const installerDir = getInstallerDir(); - const filePath = path.join(installerDir, moduleName); - - if (!(await exists(filePath))) { - throw new Error(`ℹ️ ${moduleName} not found`); - } - - await remove(filePath); - if (isWindows) { - await remove(filePath + ".cmd"); - } - console.log(`ℹ️ Uninstalled ${moduleName}`); -} - async function main(): Promise<void> { - if (args.length < 3) { + const parsedArgs = parse(args.slice(1), { stopEarly: true }); + + if (parsedArgs.h || parsedArgs.help) { return showHelp(); } - if (["-h", "--help"].includes(args[1])) { + if (parsedArgs._.length < 2) { return showHelp(); } - const moduleName = args[1]; - const moduleUrl = args[2]; - const flags = args.slice(3); + const moduleName = parsedArgs._[0]; + const moduleUrl = parsedArgs._[1]; + const flags = parsedArgs._.slice(2); + const installationDir = parsedArgs.d || parsedArgs.dir; + try { - await install(moduleName, moduleUrl, flags); + await install(moduleName, moduleUrl, flags, installationDir); } catch (e) { console.log(e); exit(1); diff --git a/installer/test.ts b/installer/test.ts index 536e74c4f..c1baa0be1 100644 --- a/installer/test.ts +++ b/installer/test.ts @@ -1,13 +1,13 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -const { run, stat, makeTempDir, remove, env } = Deno; +const { run, stat, makeTempDir, remove, env, readAll } = Deno; import { test, runIfMain, TestFunction } from "../testing/mod.ts"; -import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts"; +import { assert, assertEquals } from "../testing/asserts.ts"; import { BufReader, EOF } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; -import { install, uninstall } from "./mod.ts"; import * as path from "../fs/path.ts"; import * as fs from "../fs/mod.ts"; +import { install, isRemoteUrl } from "./mod.ts"; let fileServer: Deno.Process; const isWindows = Deno.platform.os === "win"; @@ -37,13 +37,15 @@ function killFileServer(): void { fileServer.stdout!.close(); } -function installerTest(t: TestFunction): void { +function installerTest(t: TestFunction, useOriginHomeDir = false): void { const fn = async (): Promise<void> => { await startFileServer(); const tempDir = await makeTempDir(); const envVars = env(); const originalHomeDir = envVars["HOME"]; - envVars["HOME"] = tempDir; + if (!useOriginHomeDir) { + envVars["HOME"] = tempDir; + } try { await t(); @@ -58,10 +60,14 @@ function installerTest(t: TestFunction): void { } installerTest(async function installBasic(): Promise<void> { - await install("file_srv", "http://localhost:4500/http/file_server.ts", []); + await install( + "echo_test", + "http://localhost:4500/installer/testdata/echo.ts", + [] + ); const { HOME } = env(); - const filePath = path.resolve(HOME, ".deno/bin/file_srv"); + const filePath = path.resolve(HOME, ".deno/bin/echo_test"); const fileInfo = await stat(filePath); assert(fileInfo.isFile()); @@ -71,11 +77,11 @@ installerTest(async function installBasic(): Promise<void> { /* eslint-disable max-len */ `% This executable is generated by Deno. Please don't modify it unless you know what it means. % @IF EXIST "%~dp0\deno.exe" ( - "%~dp0\deno.exe" "run" "http://localhost:4500/http/file_server.ts" %* + "%~dp0\deno.exe" "run" "http://localhost:4500/installer/testdata/echo.ts" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.TS;=;% - "deno" "run" "http://localhost:4500/http/file_server.ts" %* + "deno" "run" "http://localhost:4500/installer/testdata/echo.ts" %* ) ` /* eslint-enable max-len */ @@ -85,7 +91,7 @@ installerTest(async function installBasic(): Promise<void> { assertEquals( await fs.readFileStr(filePath), /* eslint-disable max-len */ - `#/bin/sh + `#!/bin/sh # This executable is generated by Deno. Please don't modify it unless you know what it means. basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") @@ -94,10 +100,10 @@ case \`uname\` in esac if [ -x "$basedir/deno" ]; then - "$basedir/deno" "run" "http://localhost:4500/http/file_server.ts" "$@" + "$basedir/deno" "run" "http://localhost:4500/installer/testdata/echo.ts" "$@" ret=$? else - "deno" "run" "http://localhost:4500/http/file_server.ts" "$@" + "deno" "run" "http://localhost:4500/installer/testdata/echo.ts" "$@" ret=$? fi exit $ret @@ -106,15 +112,73 @@ exit $ret ); }); -installerTest(async function installWithFlags(): Promise<void> { - await install("file_server", "http://localhost:4500/http/file_server.ts", [ - "--allow-net", - "--allow-read", - "--foobar" - ]); +installerTest(async function installCustomDir(): Promise<void> { + const tempDir = await makeTempDir(); + + await install( + "echo_test", + "http://localhost:4500/installer/testdata/echo.ts", + [], + tempDir + ); + + const filePath = path.resolve(tempDir, "echo_test"); + const fileInfo = await stat(filePath); + assert(fileInfo.isFile()); + + if (isWindows) { + assertEquals( + await fs.readFileStr(filePath + ".cmd"), + /* eslint-disable max-len */ + `% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "http://localhost:4500/installer/testdata/echo.ts" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "http://localhost:4500/installer/testdata/echo.ts" %* +) +` + /* eslint-enable max-len */ + ); + } + + assertEquals( + await fs.readFileStr(filePath), + /* eslint-disable max-len */ + `#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "http://localhost:4500/installer/testdata/echo.ts" "$@" + ret=$? +else + "deno" "run" "http://localhost:4500/installer/testdata/echo.ts" "$@" + ret=$? +fi +exit $ret +` + /* eslint-enable max-len */ + ); +}); + +installerTest(async function installLocalModule(): Promise<void> { + let localModule = path.join(Deno.cwd(), "installer", "testdata", "echo.ts"); + await install("echo_test", localModule, []); const { HOME } = env(); - const filePath = path.resolve(HOME, ".deno/bin/file_server"); + const filePath = path.resolve(HOME, ".deno/bin/echo_test"); + const fileInfo = await stat(filePath); + assert(fileInfo.isFile()); + + if (isWindows) { + localModule = localModule.replace(/\\/g, "\\\\"); + } if (isWindows) { assertEquals( @@ -122,11 +186,11 @@ installerTest(async function installWithFlags(): Promise<void> { /* eslint-disable max-len */ `% This executable is generated by Deno. Please don't modify it unless you know what it means. % @IF EXIST "%~dp0\deno.exe" ( - "%~dp0\deno.exe" "run" "--allow-net" "--allow-read" "http://localhost:4500/http/file_server.ts" "--foobar" %* + "%~dp0\deno.exe" "run" "${localModule}" %* ) ELSE ( @SETLOCAL @SET PATHEXT=%PATHEXT:;.TS;=;% - "deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/http/file_server.ts" "--foobar" %* + "deno" "run" "${localModule}" %* ) ` /* eslint-enable max-len */ @@ -136,7 +200,7 @@ installerTest(async function installWithFlags(): Promise<void> { assertEquals( await fs.readFileStr(filePath), /* eslint-disable max-len */ - `#/bin/sh + `#!/bin/sh # This executable is generated by Deno. Please don't modify it unless you know what it means. basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") @@ -145,10 +209,10 @@ case \`uname\` in esac if [ -x "$basedir/deno" ]; then - "$basedir/deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/http/file_server.ts" "--foobar" "$@" + "$basedir/deno" "run" "${localModule}" "$@" ret=$? else - "deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/http/file_server.ts" "--foobar" "$@" + "deno" "run" "${localModule}" "$@" ret=$? fi exit $ret @@ -157,26 +221,187 @@ exit $ret ); }); -installerTest(async function uninstallBasic(): Promise<void> { - await install("file_server", "http://localhost:4500/http/file_server.ts", []); +installerTest(async function installWithFlags(): Promise<void> { + await install( + "echo_test", + "http://localhost:4500/installer/testdata/echo.ts", + ["--allow-net", "--allow-read", "--foobar"] + ); const { HOME } = env(); - const filePath = path.resolve(HOME, ".deno/bin/file_server"); + const filePath = path.resolve(HOME, ".deno/bin/echo_test"); - await uninstall("file_server"); + if (isWindows) { + assertEquals( + await fs.readFileStr(filePath + ".cmd"), + /* eslint-disable max-len */ + `% This executable is generated by Deno. Please don't modify it unless you know what it means. % +@IF EXIST "%~dp0\deno.exe" ( + "%~dp0\deno.exe" "run" "--allow-net" "--allow-read" "http://localhost:4500/installer/testdata/echo.ts" "--foobar" %* +) ELSE ( + @SETLOCAL + @SET PATHEXT=%PATHEXT:;.TS;=;% + "deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/installer/testdata/echo.ts" "--foobar" %* +) +` + /* eslint-enable max-len */ + ); + } - assert(!(await fs.exists(filePath))); - assert(!(await fs.exists(filePath + ".cmd"))); + assertEquals( + await fs.readFileStr(filePath), + /* eslint-disable max-len */ + `#!/bin/sh +# This executable is generated by Deno. Please don't modify it unless you know what it means. +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +if [ -x "$basedir/deno" ]; then + "$basedir/deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/installer/testdata/echo.ts" "--foobar" "$@" + ret=$? +else + "deno" "run" "--allow-net" "--allow-read" "http://localhost:4500/installer/testdata/echo.ts" "--foobar" "$@" + ret=$? +fi +exit $ret +` + /* eslint-enable max-len */ + ); }); -installerTest(async function uninstallNonExistentModule(): Promise<void> { - await assertThrowsAsync( - async (): Promise<void> => { - await uninstall("file_server"); - }, - Error, - "file_server not found" +installerTest(async function installLocalModuleAndRun(): Promise<void> { + const localModule = path.join(Deno.cwd(), "installer", "testdata", "echo.ts"); + await install("echo_test", localModule, ["hello"]); + + const { HOME } = env(); + const filePath = path.resolve(HOME, ".deno/bin/echo_test"); + const fileInfo = await stat(filePath); + assert(fileInfo.isFile()); + + const ps = run({ + args: ["echo_test" + (isWindows ? ".cmd" : ""), "foo"], + stdout: "piped" + }); + + if (!ps.stdout) { + assert(!!ps.stdout, "There should have stdout."); + return; + } + + let thrown = false; + + try { + const b = await readAll(ps.stdout); + + const s = new TextDecoder("utf-8").decode(b); + + assertEquals(s, "hello, foo"); + } catch (err) { + console.error(err); + thrown = true; + } finally { + await remove(filePath); + ps.close(); + } + + assert(!thrown, "It should not throw an error"); +}, true); // set true to install module in your real $HOME dir. + +installerTest(async function installAndMakesureItCanRun(): Promise<void> { + await install( + "echo_test", + "http://localhost:4500/installer/testdata/echo.ts", + ["hello"] ); + + const { HOME } = env(); + const filePath = path.resolve(HOME, ".deno/bin/echo_test"); + const fileInfo = await stat(filePath); + assert(fileInfo.isFile()); + + const ps = run({ + args: ["echo_test" + (isWindows ? ".cmd" : ""), "foo"], + stdout: "piped" + }); + + if (!ps.stdout) { + assert(!!ps.stdout, "There should have stdout."); + return; + } + + let thrown = false; + + try { + const b = await readAll(ps.stdout); + + const s = new TextDecoder("utf-8").decode(b); + + assertEquals(s, "hello, foo"); + } catch (err) { + console.error(err); + thrown = true; + } finally { + await remove(filePath); + ps.close(); + } + + assert(!thrown, "It should not throw an error"); +}, true); // set true to install module in your real $HOME dir. + +installerTest(async function installAndMakesureArgsRight(): Promise<void> { + await install( + "args_test", + "http://localhost:4500/installer/testdata/args.ts", + ["arg1", "--flag1"] + ); + + const { HOME } = env(); + const filePath = path.resolve(HOME, ".deno/bin/args_test"); + const fileInfo = await stat(filePath); + assert(fileInfo.isFile()); + + const ps = run({ + args: ["args_test" + (isWindows ? ".cmd" : ""), "arg2", "--flag2"], + stdout: "piped" + }); + + if (!ps.stdout) { + assert(!!ps.stdout, "There should have stdout."); + return; + } + + let thrown = false; + + try { + const b = await readAll(ps.stdout); + + const s = new TextDecoder("utf-8").decode(b); + + const obj = JSON.parse(s); + + assertEquals(obj[0], "arg1"); + assertEquals(obj[1], "--flag1"); + assertEquals(obj[2], "arg2"); + assertEquals(obj[3], "--flag2"); + } catch (err) { + console.error(err); + thrown = true; + } finally { + await remove(filePath); + ps.close(); + } + + assert(!thrown, "It should not throw an error"); +}, true); // set true to install module in your real $HOME dir. + +test(function testIsRemoteUrl(): void { + assert(isRemoteUrl("https://deno.land/std/http/file_server.ts")); + assert(isRemoteUrl("http://deno.land/std/http/file_server.ts")); + assert(!isRemoteUrl("file:///dev/deno_std/http/file_server.ts")); + assert(!isRemoteUrl("./dev/deno_std/http/file_server.ts")); }); runIfMain(import.meta); diff --git a/installer/testdata/args.ts b/installer/testdata/args.ts new file mode 100644 index 000000000..484cab5ab --- /dev/null +++ b/installer/testdata/args.ts @@ -0,0 +1,9 @@ +function args(args: string[]) { + const map = {}; + for (let i = 0; i < args.length; i++) { + map[i] = args[i]; + } + Deno.stdout.write(new TextEncoder().encode(JSON.stringify(map))); +} + +args(Deno.args.slice(1)); diff --git a/installer/testdata/echo.ts b/installer/testdata/echo.ts new file mode 100644 index 000000000..62ddd6d05 --- /dev/null +++ b/installer/testdata/echo.ts @@ -0,0 +1,6 @@ +function echo(args: string[]) { + const msg = args.join(", "); + Deno.stdout.write(new TextEncoder().encode(msg)); +} + +echo(Deno.args.slice(1)); |