Execute Shell Script's `With input from` is for STDIN data – not code

After 2 hours of debugging, and searching this website, (and trying many things) I can't figure out how to pass pipe characters to the Execute Shell Script action, I want to do something like this (here's the simplest example that fails):

image

But if I write the variable to a text file, it works fine. It seems that when you try to execute the same code as a script, it fails to interpret the pipe command (|) as a pipe, but rather as a filename.

I don't want to use script files as a workaround, because that adds complications like adding execute permissions, and handling different paths for running different computers, etc. I just want Execute Shell Script to handle pipes, like files can handle pipes.

I'm able to get it to work for simple things using this approach:

image

But with complex commands, that doesn't work either. I'm not sure yet what causes it to break.

I believe that your first action is equivalent to the following bash shell command, which would use backticks to pass the result to the variable.

ShellCmd=`ls | cat`

If you try that in a bash shell script, you will find that it doesn't work either.

If I am on the right lines, this discussion on Stack Overflow is relevant: Use of pipes in backticks... but it won't help you much.

Why are piping ls to cat anyway? It's the same as ls. :thinking: It's late... Have I missed something obvious? :sleeping:

I'm not piping cat. As I said above, this is "the simplest example" of the problem.

The actual command I was trying to pass to a shell, at least one of several actual commands, is this:

ps -A -o %cpu | awk '{s+=$1} END {print int(0.5+s)}'

I didn't know that, but I believe you. I will check.

So why can I pass pipes sometimes if I use "zsh -c"?

I don't know your macro, of course, but you can get around the problem (also I don't know why it happens, perhaps @peternlewis can explain) by using two variables, and putting the pipes in the shell action itself.

var1=ps -A -o %cpu
var2=awk '{s+=$1} END {print int(0.5+s)}'

Shell command:

$KMVAR_var1 | $KMVAR_var2

Not as convenient as one variable, but it worked in my testing.

-rob.

1 Like

Ah yes, that's what I missed – or rather, forgot, while looking into this! [The quoted text is from before I swapped the ls and the cat – not that it's relevant anyway :slightly_smiling_face:].

I'll leave it to the experts and go to bed because my brain needs a reboot. Good luck!

Very interesting. Using two variables crossed my mind but I had no idea how to go about it.

That's a very good idea, but my macro is much more complicated than I've indicated so far.

I can't break up the command into subcommands because the macro that I'm writing is supposed to handle "unknown" command lines. It would be extremely difficult for my routine to "parse" input strings to find the pipe commands, and then pass each substring to a different shell. Not impossible, but I'm not Einstein. To prove my point, here's another one of the more difficult strings I wanted to process:

echo vm_stat | head -n 1 | tr -cd '[0-9]'\*vm_stat | head -n 2 | tail -n 1 | tr -cd '[0-9]' \/ 1024 \/ 1024 | bc

Notice how there are pipes within backquotes.(!!!) [EDIT: this command is so complicated that this website failed to handle it properly. I'll try to fix it.] This works from the command line. Why wouldn't it work in a shell, especially when it works in a file passed to a shell?

So I'm still trying to find out if a shell can receive a pipe character. I would be surprised if there was no way to do it.

EDIT: Here's a screen capture of some of the more complex commands I need to pass to a shell. In this screenshot you can see the pipes within backquotes.

image

Yea, that'd be tough! As I was intrigued by this, and had no clue what the issue was, I went searching for an answer. And I found one (an explanation, that is), and a fix that works perfectly, but the data source is one that isn't necessarily well received here, so I'll leave them anonymous for now. But they said this:

The issue arises because Keyboard Maestro's shell script action processes each variable as a separate entity and does not interpret the pipe (|) character as a command separator when it's part of a single variable. Instead, it treats the entire value of the variable as a single command argument, which is why it fails.

To address this, you can use the eval command in your shell script action to force the shell to interpret the contents of the variable as a command.

And indeed, that is the solution—this works perfectly with the last part of your complex example:

I never would have thought to try eval on my own, and my source's interpretation of the cause of the problem makes sense to me. But as with all answers, please evaluate against your own sources for confirmation. But eval definitely makes it work :).

-rob.

1 Like

I had to scream with laughter about 30 seconds after reading that comment, as it took me that long to realize who or what you were referring to there.

That's a stunning solution. I haven't tested it, but I believe you, that it works. I'm tempted to mark it as a solution even before I try it myself.

I'm working on a pretty cool utility. You can probably guess what data it might be dealing with from the sample code above. If this works, it will really help me out. Thainks.

I may be missing something in what you are attempting there, but remember, of course, that the With input from field just defines a string of characters passed into the command line, unevaluated, as STDIN

2 Likes

The use of eval was raised in the "Use of pipes in backticks" discussion that I linked to, with one poster linking to this unofficial Bash FAQ page: I'm trying to put a command in a variable, but the complex cases always fail!. I include the following quote just as a caution to bear in mind generally.

In bash, the only ways to generate, manipulate, or store code more complex than a simple command at runtime involve storing the code's plain text in a variable, file, stream, or function, and then using eval or sh to evaluate the stored code. Directly manipulating raw code strings is among the least robust of metaprogramming techniques and most common sources of bugs and security issues. That's because predicting all possible ways code might come together to form a valid construct and restricting it to never operate outside of what's expected requires great care and detailed knowledge of language quirks. Bash lacks all the usual kinds of abstractions that allow doing this safely. Excessive use can also obfuscate your code.

It's a necessary eval, perhaps!

1 Like

I'd never found a reason to use that setting, but that makes perfect sense, in theory. But I can't figure out how I'd use it even with the simple example @Airy started with, ls | cat. Looking at the example you linked, I think it'd be something like this:

But that doesn't do anything at all—no errors in the log, but no output. But if I change it to this:

Then it works as I'd expect based on what the linked thread explains. There's nothing in detail at all about "with input from" in the help for the shell script action, so I'm confused as to how to do this. The only way I made it work was basically as above: I eval the variable:

Can you explain what I'm missing, as clearly that's not the intent of "with input from," as that just seems like more steps to do the same thing?

thanks!
-rob.

A classic use might be to supply (pipe in, effectively) a standard input stream of characters to a wc word count.

Input from (STDIN from) provides a raw input to a command line,
but doesn't form part of it.

(Isn't evaluated syntactically by the command line processor)

with input from.kmmacros (4.2 KB)

I understand that use, but I'm trying to understand how it could be used—as it seems it should work—to handle the case here, where he's trying to execute a string of two commands. I tried this, which seems the closest version of your wc example:

But nothing happened. Nothing happened if I left it blank, too, which also makes logical sense to my brain, but clearly is meaningless to the shell (and I understand why). So I'm still stuck trying to understand how pass input, which seems ideal in that it won't process the associated data before passing it to the shell, can be used here.

(I'm not saying this isn't a very neat feature, and I appreciate learning what it can do in KM, but I'm stumped by how to actually use it for this use case.)

-rob.

I wonder if that arose from some confusion about the nature of With input from ?

What you are trying there is, I think, essentially

echo "ls | cat" | eval

which yields no output, either using the action, or directly entered at the command line.

Sorry for any confusion on my part. He just gave this as a simple example of a piped command set that fails in the Shell Script action:

ls | cat

Yes, it's a meaningless string of commands, but it does output a directory list in Terminal. But in the Shell Script action, the only way we found to get it working was by evaling the variable. I was thinking the pass input could work around that, based on how it's described?

-rob.

No – not as a string in itself – only when evaluated by the command line processor.

Raw input strings to a pipe are not evaluated in that way.

Ah, OK, thanks. So then could that method be used for what he's trying to do? That is, he needs to get the output of some complicated connected commands, like the one he gave later:

`echo ` vm_stat | head -n 1 | tr -cd '[0-9]'`\*` vm_stat | head -n 2 | tail -n 1 | tr -cd '[0-9]'` \/ 1024 \/ 1024 | bc`

Could he get that using the standard input option? That's what I'm trying to understand relative to the method of using eval.

thanks;
-rob.

I wonder if difficulty in finding documentation of with Input from didn't just briefly obscure the distinction between code and data ?

STDIN (the With input from channel) is no place to put code – it's a data channel.

If STDIN were evaluated as code, we couldn't use the shell for anything at all.

even writing:

echo "lorem ipsum dolor sit" | wc

or the Keyboard Maestro equivalent:

would choke unusably, because "lorem ipsum dolor sit" is not interpretable as shell syntax.

If you want to pipe data from one process to another, and for some reason wish to break that up between KM actions, you can just:

  1. capture output data from one process in a Keyboard Maestro variable, and
  2. supply it as input data to another shell process through a With input from field.

Thanks for the clarification; that makes perfect sense.

-rob.

1 Like