6

Given the following ed script,

$ cat helloworld
a
hello
world
.
,n
,s,o,O,g
,n
Q

I would like to obtain somehow the interactive output

$ ed
a
hello
world
.
,n
1   hello
2   world
,s,o,O,g
,n
1   hellO
2   wOrld
Q
$

instead of the expected

$ cat helloworld | ed
1   hello
2   world
1   hellO
2   wOrld
$

Is that possible, maybe using a third-party utility? Thanks in advance!

Edit: I guess I should add some motivation. I would like to produce some sample ed sessions, and probably save them with script(1). If this process is performed "live", any modifications would basically imply typing the whole tutorial again (bad) or alternatively "figuring out" the output, potentially based on a long editing session (worse).

3 Answers3

4

It sounds like you want to simulate an interactive terminal session, in which the user types a command, waits for a response (or, given that the target is ed, sometimes waits for a response), then types another command, etc. You could write an expect script to do this, but it may be good enough to just send a line at a time to both the terminal and the target process, with a brief pause between each line.

    $ while IFS= read -r line
      do
        printf '%s\n' "$line" >/dev/tty
        printf '%s\n' "$line"
        sleep 0.5
      done < helloworld | ed
a
hello
world
.
,n
1   hello
2   world
,s,o,O,g
,n
1   hellO
2   wOrld
Q

To better distinguish input from output, you could add color or other highlighting to the
echo "$line" >/dev/tty line, or, in this particular case, enable the prompt character in ed (the P command) so that a * will show up in front of each ed command.

Mark Plotnick
  • 25,413
  • 3
  • 64
  • 82
  • @don_crissti Thank you. I've updated the answer to include your fix. – Mark Plotnick Dec 30 '14 at 14:39
  • Thanks for you reply. Although I could follow your approach more easily, I went with Don's solutions as they turn out to be more portable. It turns out read -r doesn't really capture a raw line everywhere, or at least not in OpenBSD ("\" becomes "", much to my puzzlement!).

    One more note: running Don's scripts it looks like sleep 0.5 is way too cautious, as it can be in fact lowered to sleep 0.

    – ezequiel-garzon Dec 30 '14 at 18:20
  • 1
    @eze Glad you found a good solution. Shell scripting, as in my answer, sometimes results in adequate, but not optimal, solutions. – Mark Plotnick Dec 30 '14 at 23:59
  • 1
    In case anybody likes this solution, use printf '%s\n' "$line" instead of echo "$line" for greater portability. – ezequiel-garzon Dec 31 '14 at 16:48
  • 1
    @ezequiel-garzon Thanks. I've changed the echos to printfs. – Mark Plotnick Mar 01 '15 at 21:24
4

Well, after monkeying around with it, this is what I'd use:

 awk '{ print; system("sleep 0") }' edscript | tee /dev/tty | ed

or, without tee:

awk '{ print >"/dev/stderr"; print | "ed"; system("sleep 0") }' edscript

If print >"/dev/stderr" doesn't work on your system you could use print | "cat >&2".


With gnu sed:

sed -u -n -e 'p;w /dev/stderr' -e 's|.*||e' edscript | ed

Another way that works just as well:
Use split to split your edscript on each line:

split -l1 edscript

this will produce pieces like xaa, xab...xah.
You could then use the pieces like this:

for i in x*; do awk '{ print >"/dev/stderr"; print }' $i; done | ed

or

for i in x*; do sed -n -e 'p;w /dev/stderr' $i; done | ed

to get the expected result. Then you rm x*...

don_crissti
  • 82,805
  • Thanks for your answers! If I understand correctly the last approach is exclusively pedagogic, as it is longer, involves splitting the script, and has the line/pattern addressing issue which is not present in the first two solutions. (Right?) – ezequiel-garzon Dec 30 '14 at 18:17
  • Thanks again, Don. (Sorry for the name update in the middle of this thread.) I guess your second method can become way simpler by running rm x* first and then simply for i in x*; do cat $x | tee /dev/tty; done | ed. I will probably feel most comfortable adapting Mark's answer with printf '%s\n' "$line" instead of echo "$line", removing IFS= as there's only one variable and changing sleep 0.5 to sleep 0 (noticing the many options you said I would have). I learned a lot with your feedback! Thanks! – ezequiel-garzon Jan 01 '15 at 22:11
  • Thanks again! Now I see why IFS= is there, even if the whole line is to be read into a single variable. Some information may be lost. Unlikely in this case, but nice to know this best practice. If I may ask, did you pick all of this up "by doing and learning", or was there a single source/book that you followed carefully and would suggest. Thanks once more! Happy 2015! – ezequiel-garzon Jan 02 '15 at 18:44
  • [Meta-question: for some reason my "at"+"your username" doesn't stick. Maybe because I asked the question?] I forgot about split(1). Yes, I'd be inclined to delete all files beginning with x, maybe in a temporary directory. I'm pretty sure split(1) generates file names in alphabetical order, so that cat $x* gives you back the original file. I've used it in that basic way a few times. The UNIX founders got a lot of things right! – ezequiel-garzon Jan 02 '15 at 19:10
3

I did something... complicated. I've been looking into ex/ed recently - I'm not very good with either - and this marked an opportunity to dive a little deeper. This parses the ed script first and passes it off to ed in-stream:

b='[:blank:]'
sed -e 'h;/\n/!i\' -e 0i -e 's/^\(.*[^\]\)*\(\\\\\)*\\$//;tn'"
/^\n*\([0-9;$,.$b]*[gGvV].*\\\\\n[$b]*\)*\([0-9,$.;${b}]*[aic][$b]*\)\
\(\n\(.*\)\n\.\)*\(\n.*\)*$/{ s//\4/;:n" -e 'G;//{N;D
    };g;s//\1\2/;l;x;s//\4/;l;H;s/.*/./;a\' -e '.
};l;g;i\' -e .\\ -e 1,.p\\ -e u <ed_script | ed

It is less complicated then before - and now virtually all of the complication lies in a single regex spanning two lines. That one long regex handles virtually all of the testing for the entire script.

The idea is that, as near as I can tell, you can only get to insert mode with one of either the append, insert, or change commands. insert mode then takes all input literally up to the next occurring line consisting of only a . dot. Any other continued command that spans multiple lines - even a sequence of such where G, g, V, or v are involved - is necessarily continued to the next line with a trailing \backslash - though, as usual, a \backslash escapes itself in that context.

So, while it's entirely possible I'm mistaken, I think this handles all cases. For every input line that doesn't match a [aic] ... . dot series sed inserts a series of commands that looks like:

0i
command-line$
.
1,.p
u

...instructing ed to insert an unequivocal look (as written by sed) at its own command, then to print it, and last to undo the whole operation - which has the very convenient result of getting the edit done, printing it, reversing it, and restoring the last address in a single action.

For those that lines that do match in a sequence of either trailing backslashes or an [aic] ... . series it is a little more complicated. In those cases sed recursively pulls them in until it encounters the end of the series before doing its look. I was careful to separate the [aic], ., and actual literal input into separate prints - each of those types will get its own look - such that the literal input is strung together as much as possible (sed will break a look output at 80 chars by default).

I guess it's easier just to show you. You'll notice the ? prompt below - this occurs because the g command given before it is not valid command - not because sed mangles the input (I hope). Here is the output from a modified version of your example dataset:

g \\\n  a$
hello\nworld\\\n\n  0a\n  world\\\nworld\nworld$
.$
?
,n$
1       hello
2       world\
3
4         0a
5         world\
6       world
7       world
,s,o,O,g$
4$
  0a
.,$n$
4         0a
5         wOrld\
6       wOrld
7       wOrld
,s,$,\\\n\\\n\\\\$
\
,n$
1       hellO
2
3       \
4       wOrld\
5
6       \
7
8
9       \
10        0a
11
12      \
13        wOrld\
14
15      \
16      wOrld
17
18      \
19      wOrld
20
21      \
Q$
mikeserv
  • 58,310
  • Wow! That looks hard to debug, let alone understand in my case... Moreover, the output is not quite what I described. Thanks for your input in any event! – ezequiel-garzon Jan 01 '15 at 22:16
  • @don_crissti - thanks Don. Hmmm... I thought I handled that by adding the /\n/!s/$/ \\/ part which is supposed to ensure that any line within one of those blocks never gets a doubled up \ at the end. Or do I defeat my own purpose there? Now I forget... probably ezequiel makes a good point. This is just experimentation... I want to do a readline type of thing without readline - which does not at all suit my tastes - and I think vi/ex/ed are probably my best bets. Try stty raw isig onlret opost min 1 time 0; dd bs=8 conv=sync </dev/tty | sed -nzue /./l... Now to get that into ex... – mikeserv Jan 02 '15 at 15:16
  • @don_crissti - oh, wait. I see why it breaks for those... I exchange at the end of the completed [aic] test and again after testing for ...\\\(\n.*\)*$ that exchange needs to change - I did it like that because I wanted to alternate correctly between hold pattern spaces depending on whether I had just gathered \escaped input or [aic] input. In the former case the data I want at the end is in hold space when I'm through verifying it, but in the latter case it's in pattern space. I just need to rearrange that a little bit and the [aic] block won't test positive for \\\n. – mikeserv Jan 02 '15 at 16:06
  • @don_crissti - I pretty much rewrote it, I guess. It's here now if you wanna play around with it. – mikeserv Jan 03 '15 at 05:20
  • @don_crissti - what's it doing there? s/.*//e? It seems... weird. Pretty much though if you just use w /dev/fd/2 you've moved from relying on a single sed implementation to a system that supports /dev/fd/[num] links. They tend to be a little more portable by number. Also fun is 4<<SED\n$(sed '... w /dev/fd/4' >/dev/null 2>&1\nSED\n – mikeserv Mar 18 '15 at 02:32
  • @don_crissti - are you using that e to do complete writes? Because when the shell it runs up and kills immediately opens() and closes() its output on that pipe it should look like a write to the pipe, right? – mikeserv Mar 18 '15 at 02:37
  • @don_crissti - I'm playing around with it - and it's pretty cool - but I don't understand how it works. – mikeserv Mar 18 '15 at 02:48
  • @don_crissti - I would think maybe dd could be used to limit bytes per write... I once did something where I made a shell kill -PIPE $$ itself for every iteration of a loop with a trap where it would consistently exec 3>&1 >&-; exec >&3 so it would keep reopening the pipe for every write. I never made anything useful of it, because I was never able to figure out for sure why it worked - was it the kill ... trap stuff or was it the exec ... stuff? Or both? I still don't know, and so I'm not sure how your thing works. I wish I could upvote it again, though. – mikeserv Mar 18 '15 at 03:14
  • @mikeserv, +1 for being a mad genius :P – roblogic Aug 27 '19 at 10:16