1

This must be the most frustrating research I've ever attempted (so far). So, there is script(1). I've been trying to add it to the start of a bash script I have (I was looking for a way to log every input and output without having to tee or use a function or something like that in every step of the script), but whenever I run said script, all I get is:

Script started, outlog log file is 'name'.

And it seems to die. But then, when I exit or Ctrl+d, it runs the bash script. I'm trying to research about it, but considering "bash script" is a subject in itself, it's proving beyond my Google-fu skill this far.

Answers? Links? Ideas?

Kamil Maciorowski
  • 69,815
  • 22
  • 136
  • 202
João Ciocca
  • 154
  • 9
  • 3
    How are you launching your scripts? It is not recommended to run `script` in non-interactive shells or command pipes, because its internal shell is interactive. If you're kicking off a script that doesn't have a tty assigned, you get nothing. If you want to log the input/output for a bash script, why not use `tee`? – Cpt.Whale Mar 13 '23 at 19:39
  • I was looking for a way to log every input and output without having to tee or use a function or something like that in every step of the script. – João Ciocca Mar 13 '23 at 20:10
  • Have you tried `logger`? – l0b0 Mar 13 '23 at 22:32
  • thanks for the suggestion @l0b0, but still adds to "I need to add logger at every step of the script", which is what I'm trying to avoid. – João Ciocca Mar 13 '23 at 23:08

1 Answers1

2

tl;dr

See the "custom interpreter" section at the end.


Preliminary note

script works by providing a tty for whatever it runs. In some cases this tty may affect things. I assume you really want to use script automatically in your shell script.


Analysis, problems, ideas

script in your shell script starts an interactive shell. When you exit the interactive shell, the shell script continues. In this matter it's as if you run bash -i inside the shell script.

The only(?) way to make script run something else is to use script -c. From the manual (you linked to):

-c, --command command
Run the command rather than an interactive shell. […]

script -c does not build a command to run from an array of arguments (compare to sudo that does use an array: sudo executable arg1 arg2 …). command is a single argument. It's not explicitly stated in the manual, but command will be passed to a shell. It will be like invoking "$SHELL" -c command.

Therefore technically this example script should work:

#!/bin/sh -

exec script -c '
# shell code here
# i.e. the actual shell script you want to run
'

Downsides:

  • The actual shell code must be a single argument. In the example it's single-quoted. For this reason quoting inside the argument gets somewhat complicated.

  • There is no simple way to pass arguments to the actual script. If the shell code you want to run uses $@ or $1 or such then it's a problem. You may be tempted to make the outer shell expand such tokens, this would be as wrong as embedding {} in shell code started from find -exec sh -c …. Technically a contraption like this should work:

    #!/bin/bash -
    
    exec script -c "exec sh -c '
    # shell code here
    # i.e. the actual shell script you want to run
    ' sh ${@@Q}"
    

    where Q in ${@@Q} handles quoting of arguments. bash (in the shebang) is needed for this ${@@Q} to work, sh (after exec) is a shell of choice and the second sh is explained here: What is the second sh in sh -c 'some shell code' sh?

    But now the actual shell code is double-quoted in the outer shell script, single-quoted for the shell invoked by script -c. Proper quoting inside the code gets even more complicated; it will be easy to accidentally make the wrong shell expand things prematurely. In some cases such mishap may even seem to work, but it's a bag of worms (i.e. a bag of potential bugs).

Alternatively a here document should work:

#!/bin/sh -

exec script -c 'exec sh' <<'EOF'
# shell code here
# i.e. the actual shell script you want to run
EOF

But:

  • Passing arguments is again somewhat complicated. You may want not to quote EOF (see this answer) and make the outer shell expand (some) parameters. The expanded values will be interpreted by the inner shell, so you should rather use the already mentioned Q modifier (and for this you need the outer shell to be bash).
  • The here document will get to script via its stdin, ultimately to the inner shell via the tty provided by script. The inner shell won't be able to use the stdin of the outer script.
  • script will log the here document as input.
  • Formally the inner shell will be interactive, it will behave as such (e.g. it will display prompts).

Custom interpreter

If you really want your script to use script automatically, consider the following custom interpreter:

#!/bin/bash -

if [ "$#" -lt 2 ]; then
   >&2 printf 'Usage: %s interpreter file arg ...' "$0"
   exit 1
fi
set -- "${@@Q}"
IFS=$' \t\n'
exec script -c "exec $*" "${SCRIPT_OUT:-./typescript $(date --rfc-3339=seconds) $$}"

Save it as script-c, make it executable (chmod +x script-c). Then build your script using this custom shebang:

#!/path/to/script-c /path/to/actual/interpreter

where /path/to/actual/interpreter is the actual interpreter you want for the script (e.g. /bin/bash).

Execute the script and our custom interpreter will do all the magic to make script -c invoke the actual interpreter for the script, with arguments.

Due to limitations of the shebang you cannot specify more than one argument to script-c in the shebang of your script. I chose the argument to mean the actual interpreter. If you use this trick with env then probably you will be able to pass more arguments (and obviously you will need to modify script-c accordingly); e.g. maybe you want to specify some additional options to script itself, or the path to a file for script to write to.

I chose not to complicate things. script-c writes to a file specified in SCRIPT_OUT environment variable. This means you can invoke a script (that uses our custom shebang) like this:

SCRIPT_OUT=./mylog ./myscript arg1 arg2 …

If the variable is unset or empty then ./typescript $(date --rfc-3339=seconds) $$ will be evaluated and used. Your date may or may not support --rfc-3339, use whatever format you like and can. This allows you to simply run:

./myscript arg1 arg2 …

and a fairly unique name will be used.

Kamil Maciorowski
  • 69,815
  • 22
  • 136
  • 202
  • wow. just... wow. I'll need a day or two reading this again and again to grasp it, but this is by far the most amazing answer I've ever read in this site. It probably goes to show that it's too much work for too little benefit (I'm a lazy person, I meant for an easy solution not to fill the script with "log task xyz" stuff), but this answer clearly explains there isn't one. Thank you very very much, Kamil! – João Ciocca Mar 14 '23 at 02:00