1

Using zsh or bash, I want to run a script that may prompt the user for multiple commands and store them as a function, however I'm finding that eval "function $FNCNAME() {" or echo "function $FNCNAME() {" | zsh -is is not valid unless typed directly since the script will not wait for more input before either continuing to the next line or exiting.

How do I block the script process while accepting user input, and continue only when the user has concluded the function definition with a final (unquoted) }?

Requirements are:

  • The shells own context aware autocomplete functionality is active.
  • The script should not immediately execute the deferred commands but should be able to call or print the definition of the new function

This question is limited to a general piping and interaction problem. I am not asking for:

  • How to provide prewritten commands to a script in any way.
  • How to provide "the best user experience" or how to improve it.
  • Why I should not solve this problem and do something else instead.
6
  • 3
    Note that this is a really, really bad idea. Prompting for input is sometimes the right approach, but very rarely and asking for commands is certainly not a good use of prompts. It is too easy to have typos, which are hard to fix and even harder to notice. If I give 5 commands and realize the 1st had a typo, I need to redo everything. Why not make your script take a file as input and your users can have the commands in the file instead?
    – terdon
    Jan 2 at 12:04
  • Fair criticism. I have updated the question with an extensive "why". Inconsequential to this more general (and easier) Q&A, my real usage area is a zsh zle accept-line override with new syntax' potential to wrap functions with additional behavior. Jan 2 at 13:06
  • I honestly don't know if what you want is possible. I suspect it either isn't, or it will be absurdly complex. I still think the whole thing is misguided: allowing your users to enter the name of a file with the commands which they have prepared outside your script allows them to use the shell's autocompletion natively. You are first restricting your users and forcing them to use a cumbersome and error-prone approach (manually typing things) and then want to get the benefit of autocompletion etc, which you can get by simply not forcing them to manually enter commands in the first place.
    – terdon
    Jan 2 at 13:10
  • I suspect the solution to be just a correct way of prepending/injecting 'fnc() {' to stdin, starting a subshell and appending 'typeset -f fnc; exit' for capturing output. Jan 2 at 13:23
  • 2
    This seems like an XY Problem. Give them an editor window where they can complete all they want.
    – tripleee
    Jan 2 at 14:45

2 Answers 2

1

In zsh, it could be something like:

make-function() {
  local psvar=$1
  local line= body=
  print -ru2 Please enter the definition of the $psvar function, one line at a time, terminate with an empty line.
  while
    line=
    vared -p '%v> ' line
    [[ -n $line ]]
  do
    body+=$line$'\n'
  done
  functions[$psvar]=$body
}

Then for example:

$ make-function f
Please enter the definition of the f function, one line at a time, terminate with an empty line.
f> echo test
f> return 42
f>
$ f
test
$ echo $?
42
$ functions f
f () {
        echo test
        return 42
}

You could also just do:

vared 'functions[f]'

To edit the body of the f function in the line editor. Note that Enter leaves the editor and you need Alt+Enter or Esc followed by Enter (or Ctrl+v, Ctrl+j like in bash's readline) to enter a literal newline in the body of the function as usual.

vared (variable editor) is a builtin that invokes the zsh line editor (zle) to edit the contents of a variable, $functions is a special associative array that maps function names to a representation of the code in their body.

In bash, you can do something approaching by using IFS= read -rep "$psvar> " line where -e uses readline to edit $line, and you can use eval "$psvar() { $body; }" to create the function from the name and body.

0

Is this what you're trying to do?

$ cat ./tst.sh
#!/usr/bin/env bash

tmp=$(mktemp) || exit 1
trap 'rm -f "$tmp"; exit' EXIT

fncname='foo'

printf 'List commands, terminated with Control-D\n' >&2

{
    printf '%s () {\n' "$fncname"
    while IFS= read -r line; do
        printf '\t%s\n' "$line"
    done
    printf '}\n'
} > "$tmp" &&
. "$tmp"

declare -f "$fncname"

"$fncname"

$ ./tst.sh
List commands, terminated with Control-D
echo 'hello'
date
echo 'world'
        <- I hit Control-D here
foo ()
{
    echo 'hello';
    date;
    echo 'world'
}
hello
Wed Jan  3 15:48:38 CST 2024
world

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .