1

Trying to do a little script to automate some tasks. Chose deno as a nice self-contained way to do random automation tasks I need - and a learning opportunity.

One of the things I'm trying to do is extract an archive using 7z and I can't figure out why it's not working.

let cmdProcess = Deno.run({
    cmd: ["7z", "e ProcessExplorer.zip"],
    stdout: "piped",
    stderr: "piped"
})
const output = await cmdProcess.output()
const outStr = new TextDecoder().decode(output);
console.log(outStr)

const errout = await cmdProcess.stderrOutput()
const errStr = new TextDecoder().decode(errout);
console.log(errStr)

7z does run, according to the normal output. But I receive the following error no matter what parameters I try to pass to 7z:

Command Line Error:
Unsupported command:
x ProcessExplorer.zip

It doesn't matter if I supply the full path or relative, or what command I give.

It's possible that I'm supplying the wrong arguments to Deno.run, but I've been unable to google Deno.run() because most search result end up being for the deno run CLI command.

I am on Win 10 21H2.
deno v1.19.3

martixy
  • 784
  • 1
  • 10
  • 26

2 Answers2

3

It should work if you split the e subcommand from the argument ProcessExplorer.zip:

const cmdProcess = Deno.run({
  cmd: ["7z", "e", "ProcessExplorer.zip"],
  stdout: "piped",
  stderr: "piped",
});

With Deno.run you need to split all the different subcommands/options/flags of a command into separate strings in the cmd array, as is mentioned in this thread

For documentation on the Deno namespace API you can find it at https://doc.deno.land. For Deno.run specifically you can find it here.

Zwiers
  • 3,040
  • 2
  • 12
  • 18
  • Ah, that's the silly thing I was missing. Thanks. It does make supplying and handling individual arguments significantly easier. – martixy Mar 13 '22 at 13:06
1

Here's a basic functional abstraction for extracting archives with 7z using Deno:

./process.ts:

const decoder = new TextDecoder();

export type ProcessOutput = {
  status: Deno.ProcessStatus;
  stderr: string;
  stdout: string;
};

/**
 * Convenience wrapper around subprocess API.
 * Requires permission `--allow-run`.
 */
export async function getProcessOutput(cmd: string[]): Promise<ProcessOutput> {
  const process = Deno.run({ cmd, stderr: "piped", stdout: "piped" });

  const [status, stderr, stdout] = await Promise.all([
    process.status(),
    decoder.decode(await process.stderrOutput()),
    decoder.decode(await process.output()),
  ]);

  process.close();
  return { status, stderr, stdout };
}

./7z.ts:

import { getProcessOutput, type ProcessOutput } from "./process.ts";

export { type ProcessOutput };

// Ref: https://sevenzip.osdn.jp/chm/cmdline/index.htm
export type ExtractOptions = {
  /**
   * Extract nested files and folders to the same output directory.
   * Default: `false`
   */
  flat?: boolean;

  /**
   * Destination directory for extraction of archive contents.
   * 7-Zip replaces the `"*"` character with the name of the archive.
   * Default: (the current working directory)
   */
  outputDir?: string;

  /** Overwrite existing files. Default: `true` */
  overwrite?: boolean;
};

/**
 * Extract the contents of an archive to the filesystem using `7z`.
 * Requires `7z` to be in your `$PATH`.
 * Requires permission `--allow-run=7z`.
 *
 * @param archivePath - Path to the target archive file
 * @param options - Extraction options
 */
export function extractArchive(
  archivePath: string,
  options: ExtractOptions = {},
): Promise<ProcessOutput> {
  const {
    flat = false,
    outputDir,
    overwrite = true,
  } = options;

  const cmd = ["7z"];

  // https://sevenzip.osdn.jp/chm/cmdline/commands/extract.htm
  // https://sevenzip.osdn.jp/chm/cmdline/commands/extract_full.htm
  cmd.push(flat ? "e" : "x");

  // https://sevenzip.osdn.jp/chm/cmdline/switches/overwrite.htm
  cmd.push(overwrite ? "-aoa" : "-aos");

  // https://sevenzip.osdn.jp/chm/cmdline/switches/output_dir.htm
  if (outputDir) cmd.push(`-o${outputDir}`);

  // Disable interaction
  // https://sevenzip.osdn.jp/chm/cmdline/switches/yes.htm
  cmd.push("-y");

  cmd.push(archivePath);

  return getProcessOutput(cmd);
}


Example usage:

./main.ts:

import { extractArchive, type ExtractOptions } from "./7z.ts";

async function main() {
  const { status: { code, success }, stdout, stderr } = await extractArchive(
    "example.zip",
  );

  if (!success) { // Something went wrong
    console.error(`7-Zip exited with code: ${code}`);
    console.error(stderr);
    Deno.exit(1);
  }

  // Extraction was successful
  console.log(stdout);
}

if (import.meta.main) main();

> deno run --allow-run=7z main.ts
Check file:///C:/Users/deno/so-71445897/main.ts

7-Zip 21.07 (x64) : Copyright (c) 1999-2021 Igor Pavlov : 2021-12-26

Scanning the drive for archives:
1 file, 774 bytes (1 KiB)

Extracting archive: example.zip
--
Path = example.zip
Type = zip
Physical Size = 774

Everything is Ok

Folders: 3
Files: 2
Size:       38
Compressed: 774
jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • 1
    I've never used typescript before. The code is admittedly easy to parse, especially if you're familiar with other typed languages. I'm just not sure I need that big of an abstraction - or any at all. It sure was instructive for a deno noob like me though. Thank you. – martixy Mar 13 '22 at 13:04
  • Upon closer examination, there is something funny going on with the `Promise.all`. Like why await INSIDE it, then outside as well. Is it just to skip the two lines of code decoding it outside? I've seen what promise hell looks like and this is triggering me. :) – martixy Mar 13 '22 at 20:29
  • @martixy To avoid having to create two extra useless variables. (See https://tsplay.dev/NVapnw). In the case of a subprocess, none of the promises resolve at meaningfully different times (because they're all tied to the termination of the program), and even if they did, we're not interested in any of the values independently in this function (`await Promise.all(/* ... */)`). – jsejcksn Mar 14 '22 at 06:45
  • @martixy (cont.) We just want to capture and return the status, and std i/o streams (as `string`s), so performing the text decoding ops as dependencies of promise resolution helps keep the variable namespace minimal, and even allows for GC of the typed arrays before the function exits (even though that's unlikely). – jsejcksn Mar 14 '22 at 06:46
  • This is getting academic at this point, but it's interesting seeing how sync and async don't mix well together in either direction. It'd be a fun exercise to figure out how to remove all `await`s, even if it doesn't matter for practical purposes. – martixy Mar 14 '22 at 12:05
  • 1
    @martixy "_sync and async don't mix well together in either direction_": I don't understand what you mean by that. "_how to remove all `await`s_": Do you mean this? https://tsplay.dev/WoDElw – jsejcksn Mar 14 '22 at 12:43
  • Yes. Exactly that. I like that version best, but that's just my preference. My first comment was related to the fact that I didn't immediately spot the most optimal way and got stuck along a different branch of logic. Which is moot now. Although I noticed you removed the `async` from the function(admittedly not necessary now). I had some vague memories, but I went digging just to be sure - `async` functions are indeed magic. If you explicitly return a promise from `async`, you don't get a promise of a promise: https://stackoverflow.com/questions/35302431/async-await-implicitly-returns-promise – martixy Mar 14 '22 at 13:56