Using tmux, expect and bash I made a script which you can use to automate practically anything on the command line in an easy way.

Related articles
semi-automated interactive stream editing: piping through expect and emacs // Bodacious Blog
Complex Dwarf Fortress macros with tcl/expect, emacs and tmux // Bodacious Blog
Automating TOR and TPB // Bodacious Blog

Build the x script

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
#!/bin/bash
export TTY

( hs "$(basename "$0")" "$@" </dev/tty ` # Disable tty to pipe content into hs ` )

# Consider using this
# man expect-lite

# Related: scripts using x
# $HOME/scripts/xs

# example 1: this adds and starts a git commit message
# x -m "\`" -m t -m e -m v -i

PANE_ID="$(tmux display-message -p -t $TMUX_PANE '#{pane_id}' 2>/dev/null)"

SHELL=zsh

input_filter() {
    # I don't want to escape \n
    sed 's/\([[;$"]\)/\\\1/g'

    # qne
    # cat
    return 0
}


export TMUX=

tf_script="$(ux tf script exp | ds xlast || echo /dev/null)"

script_append() {
    # printf -- "$1" >> "$tf_script"
    # echo -e "$1" >> "$tf_script"

    lit "$1" >> "$tf_script"

    return 0
}

script_append "#!/usr/bin/expect -f"

timeout=3600
uses_tmux=n
debug_mode=n
tmux_command=
attach_tmux=n
print_output=n
while [ $# -gt 0 ]; do opt="$1"; case "$opt" in
    -d) {
        debug_mode=y
        shift
    }
    ;;

    -n|-g|-dr) { #gen
        DRY_RUN=y
        shift
    }
    ;;

    -sh) {
        SHELL="$2"
        shift
        shift
    }
    ;;

    -shs) {
        SHELL="$(eval "nsfa arxiv $2")"
        shift
        shift
    }
    ;;

    -cd ) {
        cd "$2"
        export CWD="$1"
        shift
        shift
    }
    ;;

    -h) { # hide output until interactive
        script_append "log_user 0"
        # script_append "stty -echo"
        shift
    }
    ;;

    -tm) {
        uses_tmux=y
        if [ -z "$PANE_ID" ]; then
            echo "No tmux pane. Can't swap pane so not trying."
            exit 1
        fi
        tmux_session="$(TMUX= tmux new -F "#{session_id}" -P -d)"
        tmux_session_qne="$(p "$tmux_session" | input_filter)"
        SHELL=tmux
        shift
    }
    ;;

    -tmc) {
        uses_tmux=y
        tmux_session="$(TMUX= tmux new -F "#{session_id}" -P -d)"
        tmux_session_qne="$(p "$tmux_session" | input_filter)"
        SHELL=tmux
        tmux_command="$2"
        shift
        shift
    }
    ;;

    -to) {
        timeout="$2"
        shift
        shift
    }
    ;;

    -nto|-notimeout) { # No timeout
        timeout=-1
        shift
    }
    ;;

    -zsh) {
        SHELL="zsh"
        shift
    }
    ;;

    *) break;
esac; done

if ! test "$debug_mode" = "y"; then
    exec 2>/dev/null
fi

# I should design this script first to use tmux

# The final expect script:
# script=""

read -r -d '' expect_script <<'HEREDOC'

#trap sigwinch and pass it to the child we spawned
trap {
    set rows [stty rows]
    set cols [stty columns]
    stty rows $rows columns $cols < $spawn_out(slave,name)
} WINCH

proc getctrl {char} {
    set ctrl [expr ("$char" & 01xF)]
    return $ctrl
}

set force_conservative 0
if {$force_conservative} {
    set send_slow {1 .1}
    proc send {ignore arg} {
        sleep .1
        exp_send -s -- $arg
    }
}

# For send -h
set send_human {.4 .4 .2 .5 100}
HEREDOC
script_append "$expect_script"

read -r -d '' expect_script <<'HEREDOC'
set timeout -1
match_max 100000
HEREDOC
script_append "$expect_script"

# script_append "spawn -noecho \"\$env(SHELL)\""
if test "$SHELL" = "zsh"; then
    # script_append "spawn \"\$env(SHELL)\""
    script_append "spawn \"zsh\""
    script_append "expect -exact \"ยป\""
elif test "$SHELL" = "tmux"; then
    # sleep
    # tmux ls > /tmp/tms.txt
    if [ -n "$tmux_command" ]; then
        # this does what it's supposed to. the session ID is correct but
        # it must not be ready. if i call without the target, it
        # works though. how annoying.

        #script_append "spawn \"\$env(SHELL)\" \"respawn-pane\" \"-t\" \"${tmux_session_qne}:1.0\" \"-k\" \"$tmux_command\" \"\\;\" \"attach\" \"-t\" \"$tmux_session_qne\""

        # TODO For the moment, don't use the target. This is dodgy, I
        # know. Hmm, it's still not working.
        # script_append "spawn \"tmux\" \"respawn-pane\" \"-k\" \"$tmux_command\" \"\\;\" \"attach\" \"-t\" \"$tmux_session_qne\""

        # script_append "spawn \"tmux\" \"respawn-pane\" \"-k\" \"$tmux_command\""
        # It might be slightly more stable with the sleep here.
        script_append "sleep 0.2"
        # Write tmux explicitly instead of $env(SHELL)
        # This way I can run the script after printing it to stdout
        script_append "spawn \"tmux\" \"respawn-pane\" \"-t\" \"$tmux_session_qne\" \"-k\" $(aqf "$tmux_command")"
        # script_append "sleep 0.5"
        # Separating the 2 commands appears to make it a little more
        # stable.
        # Using \"\$env(SHELL)\" instead of tmux appears to make no
        # difference here
        script_append "spawn \"tmux\" \"attach\" \"-t\" \"$tmux_session_qne\""

        # script_append "spawn \"tmux\" \"respawn-pane\" \"-k\" \"$tmux_command\" \"\\;\" \"attach\" \"-t\" \"$tmux_session_qne\""
        # script_append "expect -exact \"sh\""
    else
        script_append "spawn \"\$env(SHELL)\" \"attach\" \"-t\" \"$tmux_session_qne\""
        script_append "expect -exact \"sh\""
    fi
else
    # script_append "spawn \"\$env(SHELL)\""
    # This is required if I want to spawn this way:
    # x -cd "$(pwd)" -sh "racket -iI racket" -e ">" -i
    script_append "spawn $SHELL"

    # script_append "expect -exact \"\r\""
fi

while [ $# -gt 0 ]; do opt="$1"; case "$opt" in
    -e) { # Expect something
        input="$(p "$2" | input_filter)"

        script_append "expect -exact \"$input\""
        shift
        shift
    }
    ;;

    -r) { # Expect a pattern / regex
        input="$(p "$2" | input_filter)"

        script_append "expect -re \"$input\""
        shift
        shift
    }
    ;;

    -u) { # Expect user input
        input="$(p "$2" | input_filter)"

        script_append "expect_user -timeout $timeout \"$input\";"
        script_append "set user_input \"\$expect_out(0,string)\""
        script_append "send -- \"\$user_input\\r\""
        shift
        shift
    }
    ;;

    -ur) { # Expect user input matching regex
        input="$(p "$2" | input_filter)"

        # example:
        # -ur "(.*)\[\r\n]"
        # x -nto -e ยป -s "vim\n" -ur "(.*hi.*)" -i

        # script_append "send -- {getctrl {l}}"
        script_append "expect_user -timeout $timeout -re \"$input\";"
        script_append "set user_input \"\$expect_out(1,string)\"" # 1 must be capture group 1. 0 is entire string
        script_append "send -- \"\$user_input\\r\""
        shift
        shift
    }
    ;;

    -p) { # Expect user input password
        input="$(p "$2" | input_filter)"

        script_append "stty -echo"
        script_append "expect_user -timeout $timeout \"$input\""
        script_append "set user_input \"\$expect_out(1,string)\"" # 1 must be capture group 1. 0 is entire string
        script_append "send -- \"\$user_input\\r\""
        script_append "stty echo"
        shift
        shift
    }
    ;;

    -pr) { # Expect user input password matching regex
        # input="$(p "$2" | input_filter)"

        input="$2"

        # example:
        # -ur "(.*)\[\r\n]"

        script_append "stty -echo"
        script_append "expect_user -timeout $timeout -re \"$input\""
        script_append "set user_input \"\$expect_out(1,string)\"" # 1 must be capture group 1. 0 is entire string
        script_append "send -- \"\$user_input\\r\""
        script_append "stty echo"
        shift
        shift
    }
    ;;

    -c|-scc) { # Send control character
        # script_append "send {getctrl {$2}}"
        script_append "send -- $(cchar $2)"
        shift
        shift
    }
    ;;

    -m) { # Send meta
        char="$(p "$2" | input_filter)"
        script_append "send -- \\033$char"
        shift
        shift
    }
    ;;

    -cm) { # Send control-meta
        script_append "send -- \\033$(cchar $2)"
        shift
        shift
    }
    ;;

    -sec) { # Send escape char
        script_append "send -- \\033$2"
        shift
        shift
    }
    ;;

    -esc) { # Send escape char
        script_append "send -- \\033"
        shift
    }
    ;;

    -sl) {
        script_append "sleep $2"
        shift
        shift
    }
    ;;

    -s1) {
        script_append "sleep 1"
        shift
    }
    ;;

    -sf|-send-file) { # Send contents of file
        input_fp="$2"
        # The bs have to be separated
        input_fp="$(printf -- "%s" "$input_fp" | bs "\\" | bs "[" | bs "]")"

        ## tcl
        ## set somevar [ exec cat "/home/shane/source/git/woodrush/py2hy/src/py2hy/py2hy.hy" ]

        script_append "set fp [ exec cat $(aqfd "$input_fp") ]"
        script_append "send -- \$fp"
        shift
        shift
    }
    ;;

    -s|-send) { # Send something
        # input="$(p "$2" | input_filter)"

        input="$2"
        # The bs have to be separated
        input="$(printf -- "%s" "$input" | bs "\\" | bs "[" | bs "]")"

        script_append "send -- $(aqfd "$input")"
        shift
        shift
    }
    ;;

    # Frustratingly, can't work out why this doesn't work
    -ss|-send-slow) { # Send something
        input="$(p "$2" | input_filter)"

        script_append "set send_slow {2 0.5}"
        script_append "send -s $(aqfd "$input")"
        shift
        shift
    }
    ;;

    -i) {
        if test "$uses_tmux" = "y"; then
            # This sleep appears to help the script catch the final
            # expect statement
            script_append "sleep 0.2"
            script_append "exit"
        else
            script_append "interact"
        fi
        shift
    }
    ;;

    -o) {
        if test "$uses_tmux" = "y"; then
            attach_tmux=n
            print_output=y
            # This sleep appears to help the script catch the final
            # expect statement
            script_append "sleep 0.2"
            script_append "exit"
        else
            script_append "interact"
        fi
        shift
    }
    ;;

    -a) {
        if test "$uses_tmux" = "y"; then
            attach_tmux=y
            print_output=n
            # This sleep appears to help the script catch the final
            # expect statement
            script_append "sleep 0.2"
            script_append "exit"
        else
            script_append "interact"
        fi
        shift
    }
    ;;

    -fc) {
        # Set some expect options

        script_append "set force_conservative 0"
        shift
    }
    ;;

    -ts) {
        # tm n "$opt :: NOT IMPLEMENTED"
        # Tmux-Send something

        input="$(p "$2" | input_filter)"
        script_append "spawn \"\$env(SHELL)\" \"send\" \"-t\" \"${tmux_session_qne}\" \"$input\""

        # script_append "$2"
        shift
        shift
    }
    ;;

    -tsl) {
        # tm n "$opt :: NOT IMPLEMENTED"
        # Tmux-Send something

        input="$(p "$2" | input_filter)"
        script_append "spawn \"\$env(SHELL)\" \"send\" \"-t\" \"${tmux_session_qne}\" -l \"$input\""

        # script_append "$2"
        shift
        shift
    }
    ;;

    -tssl) {
        # tm n "$opt :: NOT IMPLEMENTED"
        # Tmux-Send something

        input="$(p "$2" | input_filter)"
        script_append "spawn \"tm\" \"type\" \"$input\""

        # script_append "$2"
        shift
        shift
    }
    ;;

    *) break;
esac; done

read -r -d '' expect_script <<'HEREDOC'
expect eof
close
HEREDOC

script_append "$expect_script"

printf -- "%s" "$script" >> "$tf_script"

export SHELL

if test "$DRY_RUN" = "y"; then
    cat "$tf_script"
else
    if test "$print_output" = "y"; then
        expect -f "$tf_script" &>/dev/null
    else
        expect -f "$tf_script"
    fi
fi

# The session must be guaranteed to exist before attaching.
# This is risky

# sleep 1

if ! test "$DRY_RUN" = "y"; then
    if test "$attach_tmux" = "y"; then
        tmux attach -t "$tmux_session"
    elif test "$print_output" = "y"; then
        tm catp "$tmux_session"
        # cmd tm catp "$tmux_session"
        tmux kill-session -t "$tmux_session"
    elif test "$uses_tmux" = "y"; then
        tmux swap-pane -s "$tmux_session:1.0" -t "$PANE_ID" \; kill-session -t "$tmux_session"
    fi
fi

if ! test "$debug_mode" = "y"; then
    trap "rm \"$tf_script\" 2>/dev/null" 0
else
    echo
    echo "$tf_script" 1>&2
fi

Examples of usage

turtle

1
x -sh "ghci" -e "ghci>" -s ":set -XOverloadedStrings" -c m -s "import Turtle" -c m -e "ghci>" -s "echo $(aqf "hi")" -c m -i

Start ghci, load the Turtle DSL and enter one command into the repl.

Hand over interaction to the user.

asciinema recording

trello

1
x -sh 3llo -e ">" -s "board select" -c m -e Alpha -s "Alpha Sprint" -c m -e ">" -s "card list mine" -c m -i

fish

1
x -sh "fish" -e ">" -s "$CMD" -c i -i

Demonstration:

1
2
xs fish-complete vim -
xs fish-complete vim -C

asciinema recording