2

I try to create a one-liner for opening less on the last screen of an multi-screen output coming from standard input. The reason for this is that I am working on a program that produces a long AST and I need to be able to traverse up and down through it but I would prefer to start at the bottom. I came up with this:

$ python a.py 2>&1 | tee >(lines=+$(( $(wc -l) - $LINES))) | less +$lines

First, I need to compute number of lines in output and subtract $LINES from it so I know what's the uppermost line of the last screen. I will need to reuse a.py output later so I use tee with process substitution for that purpose. As the last step I point less to open an original stdout on a particular line. Of course, it doesn't work in Bash because $lines is not set in last step as every subcommand is run in a subshell. In ZSH, even though pipe commands are not run in a subshell, process substitution still is and therefore it doesn't work neither. It's not a homework or a work task, I just wonder whether it's possible to do what I want without creating a temporary file in Bash or ZSH. Any ideas?

fedorqui
  • 275,237
  • 103
  • 548
  • 598
user1042840
  • 1,925
  • 2
  • 16
  • 32

2 Answers2

4

less supports this innately. The + syntax you're using accepts any less command you could enter while it's running, including G for go-to-end.

... | less +G

does exactly what you want.

This is actually mentioned explicitly as an example in the man page (search for "+G").

Michael Homer
  • 1,107
  • 8
  • 16
  • cool, thanks, I must have missed that. It completely solves my problem but just of curiosity do you know any way to do that without `+G`? I know it's a bit pointless but I just want to make sure that it's not possible w/o creating temporary files from more experienced users. – user1042840 Jan 07 '16 at 23:41
  • If you have a question about setting variables within a pipeline, I suggest you [ask](https://unix.stackexchange.com/questions/ask) a new one with that context explicitly. It is likely to be shell-dependent at least. – Michael Homer Jan 07 '16 at 23:53
  • I tagged this question with `Bash` and `ZSH` as I am only interested in these two. I think that setting variables within a pipeline it's just not possible by design in `Bash` and not possible when process substitution is in use somewhere along the way in `ZSH`. I am afraid that such question would probably receive no more attention than this. This problem is very specific but indeed it comes down to setting variables inside subshells so an ideal solution for it would be to avoid subshells at all but I have no idea how to do this, or whether it's even possible. – user1042840 Jan 08 '16 at 00:02
1

The real answer to your question should be the option +G to less, but you indicated that the problem definition is not representative for the abstract problem you want to solve. Therefore, please consideer this alternative problem:

python a.py 2>&1 | \
awk '
  {a[NR]=$0}
  END{
    print NR
    for (i=1;i<=NR;i++)print a[i]
   }
 ' | {
     read -r l
     less -j-1 +$l
 }

The awk command is printing the number of lines, and then all the lines in sequence. We define the first line to contain some meta information. This is piped to a group of commands delimited by { and }. The first line is consumed by read, which stores it in variable $l. The rest of the lines are taken by less, where this variable can be used. -j-1 is used, so the matched line is at the bottom of the screen.

joepd
  • 4,681
  • 2
  • 26
  • 27
  • nice, I learned that it's possible to save variables from `stdin` using `read` like this: `echo one two | { read l; echo $l ;}`. However, I am not exactly sure how would this work with output that is not made solely of numbers so it's not useful for my specific problem but nice anyway. – user1042840 Jan 08 '16 at 00:31
  • 1
    @user1042840: Try replacing the first `for`-loop with `python a.py 2>&1`. `awk`'s `print NR` will make the metadata a usable number, regardless of input. – joepd Jan 08 '16 at 00:34
  • I did it myself, as I must admit it was possibly confusing. Try the current command. – joepd Jan 08 '16 at 00:38
  • ok, FANTASTIC, it works, I just need to understand it. Thank you. – user1042840 Jan 08 '16 at 00:39
  • ok, so what `awk` does in order not to lose standard input is copying whole of it + adds a number of lines on the top which is later set by `read`? Indeed, very smart. I tried using `tee` for that purpose but wasn't able to set a variable in the pipeline, this solution solves this problem. – user1042840 Jan 08 '16 at 00:43
  • 1
    You got it. It is an ugly hack to get around the lack of metadata in text streams, by abusing the first line(s) somehow :) – joepd Jan 08 '16 at 00:46
  • 2
    If the total data plus overhead exceeds available memory, this will thrash or die or both. `awk` can run the 'less' with `'{a[NR]=$0;} END{ for(i=1;i<=NR;i++) print a[i] | "less -j-1 +" NR }'`. – dave_thompson_085 Jan 08 '16 at 04:42