Skip to content

Print the executed command? #455

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

Closed
borekb opened this issue Apr 14, 2021 · 12 comments · Fixed by #466
Closed

Print the executed command? #455

borekb opened this issue Apr 14, 2021 · 12 comments · Fixed by #466

Comments

@borekb
Copy link

borekb commented Apr 14, 2021

I can pass command to execa via execa.command('echo unicorns') but it would also be great if execa could give me a string version of a command when I use the main syntax. Like this (pseudocode):

const execa = require('execa');

(async () => {
  const { command } = await execa('echo', ['unicorns']);
  console.log(command);
  //=> 'echo unicorns'
})();

More realistic example where I'd use this:

const execa = require("execa");

(async () => {
  const { command } = await execa("git", [
    "log",
    "-n",
    "1",
    "--pretty=format:%h",
    "--",
    ...paths,
  ]);
  console.log(command);
  //=> 'git log -n 1 --pretty=format:%h -- a.txt b.txt c.txt'
})();

The problem is probably shell-specific escaping, and the Git example above should be more safely something like

//=> "git" "log" "-n" "1" "--pretty=format:%h" "--" "a.txt" "b.txt" "c.txt"

I realize that there are many different shells, from Bash to Zsh to PowerShell to cmd.exe, and that it might be difficult to create a universal command. But it's a feature I was thinking about several times already so at least I wanted to write it up to see if it's a total nonsense or not.

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 14, 2021

Hi @borekb,
Thanks for reaching out.

Unless you use the shell option, no shell is being used. Based on this, it might be inaccurate to represent the command as a string with shell-specific quotes. IMHO This might create some confusion around the difference between a no-shell execution (command + arguments array, no escaping needed) vs a shell execution (single string, escaping needed).

On the other hand, if the shell option is used, then the argument should be a single string, which can then be directly printed.

const command = 'git "log" "-n" "1" "--pretty=format:%h" "--" "a.txt" "b.txt" "c.txt"'
await execa(command, { shell: true })
console.log(command);

I am curious what your thoughts on this are, and also @sindresorhus.

@borekb
Copy link
Author

borekb commented Apr 14, 2021

I just want to say that I understand the difference between strings and cmd+args, and that it's not really possible to convert cmd+args to strings in a fully reliable way (it depends on a shell where the result will be pasted to).

I'm thinking about two options:

1: Have a .command property that would produce a string that is a valid input into execa.command. It's not necessarily copy-paste-able into any shell but it's at least something.

Pseudocode:

const execa = require('execa');

(async () => {
  const { command } = await execa('echo', ['unicorns']);
  await execa.command(command); // works; `command` is 'echo unicorns'
})();

2: Make it obvious that it's not perfect but a rough approximation. Maybe use something like require('shell-quote').quote behind the scenes (I know that that specific package is unmaintained and not very good).

Pseudocode:

const execa = require('execa');

(async () => {
  const { roughlyEquivalentBashCommand } = await execa('echo', ['unicorns']);
  console.log(roughlyEquivalentBashCommand);
  //=> 'echo unicorns'
})();

Still useful e.g. to log the command to logs and similar.

@borekb
Copy link
Author

borekb commented Apr 14, 2021

(A somewhat related issue from the past, in the opposite direction: #112. That one was rightfully closed but this is a bit different as it's safer – mostly for logging etc.)

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 14, 2021

Thanks for this thorough response @borekb 👍

For the first example, is there a use case that would not be solved by storing the array of command + arguments instead? Like:

const command = { binary: 'echo', args: ['unicorns'] }
await execa(command.binary, command.args);
await execa(command.binary, command.args); // No need to use `execa.command()` for the second call

For the second example, I think many users are confused about shell escaping, mostly: a) when escaping is needed (shell vs non-shell), b) that escaping is shell-specific. Returning a string that uses Bash-like escaping would contribute to that confusion.

Also, it would not be cross-platform. If a user was to either copy/paste that in their terminal, or pass it to execa with shell: true, this might not work if their shell is not sh-like.

@borekb
Copy link
Author

borekb commented Apr 15, 2021

I know it's not perfect / cross-platform, it would be just a little helper.

For example, let's say I have this code:

const { stdout } = await execa("git", [
  "log",
  "-n",
  "1",
  "--pretty=format:%h",
  "--",
  "a.txt",
  "b.txt",
  "c.txt",
]);
process(stdout);

And I want to "debug" the command in my shell, so it would be great if I could do this:

const { stdout, command } = await execa("git", [
  "log",
  "-n",
  "1",
  "--pretty=format:%h",
  "--",
  "a.txt",
  "b.txt",
  "c.txt",
]);
console.log(command);
// -> "git log -n 1 --pretty=format:%h -- a.txt b.txt c.txt"
process(stdout);

Then copy the string to my shell, update it slightly (for example, put quotes around "format" or filenames if necessary) and keep going.

I know that execa cannot produce a perfect string for all shells but it could produce a string that is sort-of correct, and it already has a format for that – the string accepted by execa.command(...). That is good-enough for me even though it's not directly copy-pastable into shells without issues.

Anyway, I'd understand if this was rejected, I just wanted to explore how feasible / useful the feature would be since I wanted it several times already. Thank you for discussing this with me 😄.

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 15, 2021

Thanks for explaining your use case @borekb

For the reasons mentioned above, I would personally still have some concerns about this addition. However, I would be curious to see what @sindresorhus thinks.

Either way, thanks a lot for discussing this through 👍

@sindresorhus
Copy link
Owner

I think this could be useful, but it should be clearly noted as being for logging/debugging. There have been times in the past where I needed this and ended up copying the flag array into a REPL and joining it into a string so I could test the command.

Maybe:

const {debugString} = await execa();

?

@borekb
Copy link
Author

borekb commented Apr 18, 2021

Naming this is a bit tricky but I like "debug string" 👍.

@ehmicky
Copy link
Collaborator

ehmicky commented Apr 19, 2021

Would a common use case would be to print this not only after the command executed, but before? For example, it is common to print a command on the terminal as it is about to be executed. For this, it seems to me that a return value property might not work, but a separate function would?

Also, how should the return value be quoted? I see several options:

  1. No quoting, only joining arguments with spaces. This option will fail when users try to copy/paste a command with arguments that have spaces.
  2. Escape spaces with a backslash in arguments, then join them with spaces. This option will fail when users try to copy/paste a command with arguments that have shell symbols like * or quotes (since it will now be interpreted by a shell).
  3. Escape double quotes with a backslash in arguments, then surround them with double quotes, then join them with spaces. This option might work with both Unix shells and cmd.exe, although I am not 100% sure this would work perfectly in all shells, with any shell options.
  4. Same as 3., but do not use double quotes if the arguments only contains safe characters like [a-zA-Z0-9.-_].

Example output for each option with execa('command', ['a b', 'c', 'd "e" f', '*']):

1. command a b c d "e" f *
2. command a\ b c d "e" f *
3. command "a b" "c" "d \"e\" f" "*"
4. command "a b" c "d \"e\" f" "*"

@sindresorhus
Copy link
Owner

Would a common use case would be to print this not only after the command executed, but before?

I personally just needed it for debugging, where it's fine to print it afterward.


I would do 4, but probably needs some manual testing.

@borekb
Copy link
Author

borekb commented Apr 21, 2021

No. 4 looks great 👍.

@ehmicky
Copy link
Collaborator

ehmicky commented Jun 1, 2021

I implemented this in #466.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants