0

In the following script (form here)

#!/bin/sh
# Using external pipe with st, give a dmenu prompt of recent commands,
# allowing the user to copy the output of one.
# xclip required for this script.
# By Jaywalker and Luke
tmpfile=$(mktemp /tmp/st-cmd-output.XXXXXX)
trap 'rm "$tmpfile"' 0 1 15
sed -n "w $tmpfile"
sed -i 's/\x0//g' "$tmpfile"
ps1="$(grep "\S" "$tmpfile" | tail -n 1 | sed 's/^\s*//' | cut -d' ' -f1)"
chosen="$(grep -F "$ps1" "$tmpfile" | sed '$ d' | tac | dmenu -p "Copy which command's output?" -i -l 10 | sed 's/[^^]/[&]/g; s/\^/\\^/g')"
eps1="$(echo "$ps1" | sed 's/[^^]/[&]/g; s/\^/\\^/g')"
awk "/^$chosen$/{p=1;print;next} p&&/$eps1/{p=0};p" "$tmpfile" | xclip -selection clipboard

Why script gets stuck at sed -n "w $tmpfile"

I looked for w command in sed man page, and in the above there is not a pattern nor a input file.

It used to work on my machine! but not sure what happened and what caused it to be like this...

Ali
  • 366
  • 4
  • 11
  • What are you expecting that line to do? – Shawn Aug 06 '21 at 22:47
  • @Shawn Not sure. Sadly I'm only the user of the script. not sure what each specific line do. The author of this script doesn't respond to this issue on github. It seems something went wrong on my machine. here tough it seems it tries to write something to a file but as you can see there is no input file there. – Ali Aug 06 '21 at 23:04
  • That line, `sed -n "w $tmpfile"`, copies piped input to a file whose name is stored in `"$tmpfile"`. You could've just used `cat > "$tmpfile"` instead. Run `tmpfile='foo'; echo 17 | sed -n "w $tmpfile"; cat foo` to see what it does. – Ed Morton Aug 06 '21 at 23:14
  • Yes exactly. it looks for stdin on that line. but this script don't require any input from the user. – Ali Aug 06 '21 at 23:21
  • It requires input from **somewhere**, whether that's the user or not idk. – Ed Morton Aug 06 '21 at 23:39

1 Answers1

3

With sed -n "w $tmpfile"; sed -i 's/\x0//g' "$tmpfile" you're copying piped input to a temp file then using sed with an "inplace" editing option to modify that temp file. That doesn't make sense vs just 1 call to sed: sed 's/\x0//g' > "$tmpfile"

As for why it's hanging:

  1. Guess 1: you're not piping any input to it (the first comment says # Using external pipe... thereby telling you the command requires piped input even if you can't read the script).
  2. Guess 2: you're now on a machine that has a version of sed that requires a backup file name (e.g. BSD sed) and so sed -i 's/\x0//g' "$tmpfile" is using 's/\x0//g' as the backup file name and "$tmpfile" as the script with no input.

The command has several other portability, robustness, and efficiency issues - you might want to throw it away and write a different script.

I took another look at that script and you should definitely throw it away as it gets a lot wrong, e.g. line by line and without knowing what the input/output is so guessing in parts:

#!/bin/sh
  • Should be #!/usr/bin/env bash
tmpfile=$(mktemp /tmp/st-cmd-output.XXXXXX)
  • should be tmpfile=$(mktemp /tmp/st-cmd-output.XXXXXX) || exit
trap 'rm "$tmpfile"' 0 1 15
  • Should be trap 'rm -f "$tmpfile"; exit' 0 1 15 and the 1 and 15 probably aren't necessary.
sed -n "w $tmpfile"
sed -i 's/\x0//g' "$tmpfile"
  • Should be just 1 command, sed 's/\x0//g' > "$tmpfile".
ps1="$(grep "\S" "$tmpfile" | tail -n 1 | sed 's/^\s*//' | cut -d' ' -f1)"
  • Should be ps1=$(awk 'NF{line=$1} END{print line}' "$tmpfile")
chosen="$(grep -F "$ps1" "$tmpfile" | sed '$ d' | tac | dmenu -p "Copy which command's output?" -i -l 10 | sed 's/[^^]/[&]/g; s/\^/\\^/g')"
  • Should be chosen=$(tac "$tmpfile" | awk -v ps1="$ps1" 'index($0,ps1) && c++' | dmenu -p "Copy which command's output?" -i -l 10)
eps1="$(echo "$ps1" | sed 's/[^^]/[&]/g; s/\^/\\^/g')"
awk "/^$chosen$/{p=1;print;next} p&&/$eps1/{p=0};p" "$tmpfile" | xclip -selection clipboard
  • Should be just 1 command awk -v chosen="$chosen" -v ps1="$ps1" '$0==chosen{p=1;print;next} p&&index($0,ps1){p=0};p' "$tmpfile" | xclip -selection clipboard

So overall the script would be more clear, brief, robust, portable, and efficient if written as:

#!/usr/bin/env bash
tmpfile=$(mktemp /tmp/st-cmd-output.XXXXXX) || exit

trap 'rm -f "$tmpfile"; exit' 0 1 15

sed 's/\x0//g' > "$tmpfile"

ps1=$(awk 'NF{line=$1} END{print line}' "$tmpfile")

chosen=$(tac "$tmpfile" | awk -v ps1="$ps1" 'index($0,ps1) && c++' | dmenu -p "Copy which command's output?" -i -l 10)

awk -v chosen="$chosen" -v ps1="$ps1" '$0==chosen{p=1;print;next} p&&index($0,ps1){p=0};p' "$tmpfile" | xclip -selection clipboard

Those sed 's/[^^]/[&]/g; s/\^/\\^/g' commands in the original script are trying to escape all characters in a string to make any regexp metachars get treated as literal when used in regexp in awk, but they're doing that substitution incorrectly (see Is it possible to escape regex metacharacters reliably with sed) and it's not necessary anyway if you just use strings instead of regexps in awk.

I may have got some of that very slightly wrong due to having nothing to test it against, and there may be further improvement possible, but the commands I provided will be much closer to a portable, robust, efficient script.

Ed Morton
  • 188,023
  • 17
  • 78
  • 185
  • let me elaborate more on the script. This script is meant to work with a terminal emulator (st). The purpose of it is to copy output of a specific command previously ran in the terminal to clipboard. It prompt the user via dmenu to chose the command whose output you want to copy to clipboard. The author somewhere mentioned that this script tries to guess the location of the prompt to do its taks. – Ali Aug 06 '21 at 23:29
  • All I can do is explain what the script does when run in a Unix shell and tell you that something has to provide input to that command in a pipe or it will hang (but as I mentioned it uses non-portable constructs and so could hang, produce incorrect output, error messages, and or no output for other reasons too). – Ed Morton Aug 06 '21 at 23:34
  • sorry I forgot to mention, I am using linux. – Ali Aug 06 '21 at 23:36
  • You did tag your question with that but that doesn't matter in regards to what the script does. – Ed Morton Aug 06 '21 at 23:38
  • 1
    Thanks a lot for your explanation. I'll dig deeper to learn. – Ali Aug 06 '21 at 23:39
  • yeah, but I thought using gnu version of sed there shouldn't be any problem from sed. as gnu is more bloated than others. – Ali Aug 06 '21 at 23:42
  • Being on Linux doesn't **necessarily** mean you're using GNU sed AFAIK. – Ed Morton Aug 06 '21 at 23:44
  • 1
    sorry. I know nothing :). my version is surely gnu. `sed (GNU sed) 4.8` – Ali Aug 06 '21 at 23:47
  • 1
    The suggestions I made will work with any version of the tools - GNU, BSD, whatever. – Ed Morton Aug 06 '21 at 23:52
  • could you please explain why should it be bash rather than sh? `#!/usr/bin/env bash`. I checked it with `checkbashism` and it didn't complain. – Ali Aug 06 '21 at 23:54
  • 1
    Because you're using constructs like `$(...)` that aren't supported by `sh` - if it's working on your system then it's because `sh` is aliased to `bash` or some other shell that supports that syntax for command substitution. I don't know what `checkbashism` is, sorry. – Ed Morton Aug 06 '21 at 23:55
  • on my machine I symlinked `sh` to `dash`. in dash it seems `$(..)` construct works. according to [here](https://sourceforge.net/projects/checkbaskisms/): "checkbashism is a program that Performs basic checks on shell scripts for the presence of non portable syntax." – Ali Aug 07 '21 at 00:03
  • @Shawn I wouldn't trust that `sh` on any box supports `$(...)` and I certainly wouldn't write or use any script that requires it to. – Ed Morton Aug 07 '21 at 00:21
  • You'd probably have to be using a system that hasn't been updated since the 80's and only having an original Bourne shell to find one that doesn't support `$()`. Absolutely no reason not to use it, and [a lot of reasons](https://mywiki.wooledge.org/BashFAQ/082) to prefer it over backticks. – Shawn Aug 07 '21 at 00:42
  • I'm just suggesting using a shebang that calls a specific shell that definitely supports `$(...)` everywhere that shell appears, I'm definitely not suggesting using backticks in favor of `$(...)`. – Ed Morton Aug 07 '21 at 00:44
  • Again, sh supports `$(...)` command substitution. Unless you're digging out some 30-odd year old long obsolete non-standard version just to be contrary, of course. – Shawn Aug 07 '21 at 00:53
  • I'm not digging out anything. Versions of `sh` exist that don't support `$(...)` so I wouldn't assume on any given box that that isn't the case - end of story. Feel free to assume otherwise if you like of course. If you do make sure to check if the `|| exit` I added is also supported by `sh` or whatever shell you have `sh` aliased to as all I know for sure is it is supported by `bash`. – Ed Morton Aug 07 '21 at 00:55
  • 1
    The final script solved my issue. I learned a lot from this post. Thanks!! Also feel free to edit/suggest me to edit the question body or title to help others reach this explanation. – Ali Aug 07 '21 at 03:56
  • 1
    on the last line there is `ps2` which I think it should be `ps1`. – Ali Aug 07 '21 at 04:20