0

I have an interactive script that returns a path for cd to use in the parent process. Like this:

$ cd $(myscript)

During the execution of the script, I want to be able to display some information in less. Unfortunately, using less during a command substitution seems not to work :

$ echo $(less <<< 'some text') # Not working
some text

So my questions : why is it not working ? Is there a work around ?

I'm trying to display an interactive help screen during the execution of the script. And I need that script to be in a command substitution as it is my only way to cd to the path yielded in the parent process. Might be a better way to do that but that is my best bet so far.

1
  • It is still not clear what you are trying to do. Oct 4, 2019 at 18:58

2 Answers 2

3

In general, echo $(somecmd) isn't very useful, for any somecmd. The output from somecmd would be expanded to the arguments of echo, and then echo would take its arguments and print them to its output. Usually, you could just directly run somecmd instead.

The difference between those two being the usual: word-splitting and globbing. echo $(somecmd) will compress whitespace and expand anything that looks like a filename glob (*.txt). These of course won't happen when running just somecmd or when running echo "$(somecmd)" with the quotes, though that removes all but the last newline. there's also the chance that your echo expands backslash-escapes.

As for less, its purpose is to allow interactive browsing of a file or pipe input, and that pretty much requires a terminal. When you run it in a command substitution, less's output is connected to a pipe instead, and it falls back to behaving pretty much like cat. (Seems to, I can't find a mention of the exact behavior in the man page.)


Now, if you seriously need a script that runs inside a command substitution, allowing the user to browse some output, and still returning some other text through the command substitution, you could do some creative use of redirections (paraphrasing mosvy's comment):

$ cat myscript
#!/bin/bash
echo this is the output

cat >&2 <<EOF
you can
browse this
with less
EOF
$ foo=$( ./myscript 2> >(less > /dev/tty))

The script's standard output goes to the command substitution and its error output to less, so foo gets set to this is the output. I'm not sure if this is useful in any way, since you won't be able to modify the value going to foo interactively, at least not with just less.

Also, running less like that messes with job control, so hitting Ctrl-C when it's running produces weird behaviour etc.


Here, it would probably be better to just use a temporary file. Have myscript write the result to a file named as an argument (i.e. echo "$result" > "$1"), then run:

tmp=$(mktemp)
myscript "$tmp"
cd "$(cat "$tmp")"
rm "$tmp"

Now myscript can launch less or another interactive program in the normal way, and there shouldn't be issues with job control.

4
  • great answer, thx. It is indeed messing with job control. So I guess I need to use trap to avoid that.
    – ogr
    Oct 4, 2019 at 13:21
  • 1
    @ogr, there is also the option of using a temporary file instead of a direct command substitution.
    – ilkkachu
    Oct 4, 2019 at 13:30
  • @ikkachu indeed, that would probably be the safest option here. I'd like to avoid passing additional parameters to the script in my particular case however.
    – ogr
    Oct 4, 2019 at 17:56
  • @ogr, sure, it's not as straightforward. If it's just the extra argument, you could have a hard-coded temp file (with the associated problems) or pass the name through the environment (which really isn't that different from an argument, though)
    – ilkkachu
    Oct 4, 2019 at 18:00
3
(trap : INT; echo "$(less <<< 'some text' >/dev/tty; echo done)")

less will devolve into a kind of (un)fancy cat when its stdout is not a tty, as when used in a command substitution (where its stdout is a pipe) [1].

When testing in an interactive bash (where the job control is enabled by default), the (...) subshell will make sure that that the less command is run as part of a proper foreground job in the terminal, without which it may not be possible to suspend it with ^Z, terminate it with ^C and it will misbehave in other ways [2].

The trap : INT will cause the subshell to ignore a ^C, without passing that disposition to its childen.

If the command is part of a shell script, all the processes will be run as part of the same process group / job, but the (...) may still be useful to limit the scope of the INT trap.

[1] You cannot even use it to print binary data nicely escaped when its stdout is not a tty:

$ printf '\xee' | less -FXR
<EE>
$ printf '\xee' | less -FXR | cat
�$ 

[2] There are many possible scenarios for what may happen, all bad. For instance, in

$ ls -d $(less <<<'some text' >/dev/tty; pwd)

bash will fork a separate process in which to exec the ls binary, and will run the $(...) process substitution as a child of it before moving it into a separate process group / job and executing the binary.

When you press ^Z inside less, it will be handled by less normally, but will either fail to stop it (if the shell and consequently less's job is the session leader), or will cause only less, but not its parent process to stop.

When you press ^C inside less, it will caught and handled by less, but a SIGINT will also be sent to the main shell (which is foreground job on the terminal), which will cause it to return the next prompt, and the bash's readline and less to compete for input and change the terminal properties unaware of each other, resulting in a complete cockup.

Note

All this applies to any program run from a command substitution, not just to the $(less ...) example, eg.

$ cat >foo <<'EOT'; chmod 755 foo
t=$(mktemp)
vi "$t" </dev/tty >/dev/tty 2>&1
echo "$t"
EOT
$ cat $(./foo)
# ^Z doesn't work in vi
3
  • Nice ! It works, but I don't really understand why. the (...) subshell will make sure that that the less command is run as part of the foreground job in the terminal,. How does that happens ? (I'm not super familiar with process group) Also, any reason I should use trap here ? Will it misbehave without it ?
    – ogr
    Oct 4, 2019 at 17:52
  • 1
    @ogr I've added some description -- this is by no way exhaustive, and may change from one version of bash to another.
    – mosvy
    Oct 7, 2019 at 8:36
  • Thx for the follow up. It's more clear for me now.
    – ogr
    Oct 7, 2019 at 18:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.