如何将多个用户提示与返回提示的功能链接在一起?

How do I chain multiple user prompts together with the ability to go back a prompt?

我想知道如何制作一个包含多个菜单的 bash 脚本。

例如,这是用户在 运行 上会看到的内容:

Type the number of choosing:
1-play
2-load
3-exit

1

What is your name:

::上一个::

Type the number of choosing:
1-play
2-load
3-exit

1

What is your name:

布拉德

Where are you from, Brad?

宾夕法尼亚州

What is your favourite colour?
1-blue
2-red
3-green
4-grey
5-magenta

,sdfhljalk:J;

What is your favourite colour?
1-blue
2-red
3-green
4-grey
5-magenta

2

What is your favourite toy?

火车

What would you like on your sandwich?

::上一个::

What is your favourite toy?

~`!@#$%^& * ()_+=-{}[]|\"':;?/>。 <,

What is your favourite toy?

::退出::

Exiting....

抱歉篇幅太长,我只想涵盖我将要制作的新游戏的所有基础知识。我希望这成为结束所有问题的问题。

我希望能够在任何地方输入 ::prev:: 并让它返回到上一个问题,并且我希望 ::exit:: 退出脚本。此外,我希望在带有编号响应的问题中无法识别的输入只是重新加载问题而不继续,并且对于包含可能导致脚本中断的字符的输入(类似于 :;!@# ...)重新加载问题而不是破.

非常感谢任何帮助!

顺便说一下,我正在使用 OS X Yosemite

在这种情况下,首先要做的是尝试思考通​​常如何实现这样的事情。可能最大的复杂性增加是使用 ::prev:: 返回一个问题。这意味着我们需要以某种方式表示应用程序状态,以便我们可以向前或向后移动。

幸运的是,这非常简单:它基本上只是我们需要的堆栈的实现。一些视觉效果:

                                                                 ...
          <--pop--             <--pop-- Location prompt <--pop-- ...
                   Name prompt          Name prompt              ...
Main menu --push-> Main menu   --push-> Main menu       --push-> ...

这也意味着程序的每个单独部分都需要独立。我们可以在 shell 函数脚本中轻松做到这一点。

所以我们需要几件:

  • 显示菜单并允许用户选择值的函数。
  • 显示文本提示并允许用户选择值的函数。
  • 管理表示程序状态的堆栈的函数。
  • 程序的每个部分都有各自的功能。

我们先来写我们的菜单提示函数。这部分很简单。 Bash 将使用 select 循环完成大部分工作,它会为我们打印一个菜单。我们只是包装它,以便我们可以处理自定义逻辑,比如期望 ::exit::::prev:: 和一些漂亮的打印。

function show_menu {
  echo "" # Print the prompt
  PS3='> ' # Set prompt string 3 to '> '
  select selection in "${menu_items[@]}" # Print menu using the menu_items array
  do
    if [[ "$REPLY" =~ ^(::exit::|::prev::)$ ]]; then
      # If the user types ::exit:: or ::prev::, exit the select loop
      # and return 1 from the function, with $selection containing
      # the command the user entered.
      selection="$REPLY"
      return 1
    fi
    # $selection will be blank if the user did not choose a valid menu item.
    if [ -z "$selection" ]; then
      # Display error message if $selection is blank
      echo 'Invalid input. Please choose an option from the menu.'
    else
      # Otherwise, return a success return code.
      return 0
    fi
  done
}

我们现在可以像这样使用这个函数了:

menu_items=('Item 1' 'Item 2' 'Item 3')
if ! show_menu 'Please choose an item from the menu below.'; then
  echo "You entered the command $selection."
fi

echo "You chose $selection."

太棒了!现在进入议程的下一个项目,编写接受用户文本输入的代码。

# Prompt for a required text value.
function prompt_req {
  # do...while
  while : ; do
    # Print the prompt on one line, then '> ' on the next.
    echo ""
    printf '> '
    read -r selection # Read user input into $selection
    if [ -z "$selection" ]; then
      # Show error if $selection is empty.
      echo 'A value is required.'
      continue
    elif [[ "$selection" =~ ^(::exit::|::prev::)$ ]]; then
      # Return non-success return code if ::exit:: or ::prev:: were entered.
      return 1
    elif [[ "$selection" =~ [^a-zA-Z0-9\'\ ] ]]; then
      # Make sure input only contains a whitelist of allowed characters.
      # If it has other characters, print an error and retry.
      echo "Invalid characters in input. Allowed characters are a-z, A-Z, 0-9, ', and spaces."
      continue
    fi
    # This break statement only runs if no issues were found with the input.
    # Exits the while loop and the function returns a success return code.
    break
  done
}

太棒了。此函数的工作方式与第一个类似:

if ! prompt_req 'Please enter a value.'; then
  echo "You entered the command $selection."
fi

echo "You entered '$selection'."

现在我们已经处理了用户输入,我们需要用堆栈管理函数来处理程序流。这在 bash 中使用数组很容易实现。

当程序的一部分运行s 完成时,它会要求流程管理器运行 执行下一个函数。流程管理器会将下一个函数的名称压入堆栈,或者更确切地说,将其添加到数组的末尾,然后 运行 它。如果输入 ::prev::,它将从堆栈中弹出最后一个函数的名称,或者删除数组的最后一个元素,然后 运行 它之前的函数。

少说话,多代码:

# Program flow manager
function run_funcs {
  # Define our "stack" with its initial value being the function name
  # passed directly to run_funcs
  funcs=("")
  # do...while
  while : ; do
    # Reset next_func
    next_func=''
    # Call the last function name in funcs.
    if "${funcs[${#funcs[@]}-1]}"; then
      # If the function returned 0, then no :: command was run by the user.
      if [ -z "$next_func" ]; then
        # If the function didn't set the next function to run, exit the program.
        exit 0
      else
        # Otherwise, add the next function to run to the funcs array. (push)
        funcs+=("$next_func")
      fi
    else
      # If the function returned a value other than 0, a command was run.
      # The exact command run will be in $selection
      if [ "$selection" == "::prev::" ]; then
        if [ ${#funcs[@]} -lt 2 ]; then
          # If there is only 1 function in funcs, then we can't go back
          # because there's no other function to call.
          echo 'There is no previous screen to return to.'
        else
          # Remove the last function from funcs. (pop)
          unset funcs[${#funcs[@]}-1]
        fi
      else
        # If $selection isn't ::prev::, then it's ::exit::
        exit 0
      fi
    fi
    # Add a line break between function calls to keep the output clean.
    echo
  done
}

我们的 run_funcs 函数需要:

  • 以第一个函数的名称调用 运行,并且
  • 如果必须继续执行程序,运行s 的每个函数都会将下一个函数的名称输出到 运行 next_func

好的。这应该很容易使用。现在开始实际编写程序:

function main_menu {
  menu_items=('Play' 'Load' 'Exit')
  if ! show_menu 'Please choose from the menu below.'; then
    return 1
  fi

  if [ "$selection" == 'Exit' ]; then
    exit 0
  fi

  if [ "$selection" == 'Load' ]; then
    # Logic to load game state
    echo 'Game loaded.'
  fi

  next_func='prompt_name'
}

function prompt_name {
  if ! prompt_req 'What is your name?'; then
    return 1
  fi
  name="$selection"

  next_func='prompt_location'
}

function prompt_location {
  if ! prompt_req "Where are you from, $name?"; then
    return 1
  fi
  location="$selection"

  next_func='prompt_colour'
}

function prompt_colour {
  menu_items=('Blue' 'Red' 'Green' 'Grey' 'Magenta')
  if ! show_menu 'What is your favourite colour?'; then
    return 1
  fi
  colour="$selection"

  next_func='prompt_toy'
}

function prompt_toy {
  if ! prompt_req 'What is your favourite toy?'; then
    return 1
  fi
  toy="$selection"

  next_func='print_player_info'
}

function print_player_info {
  echo "Your name is $name."
  echo "You are from $location."
  echo "Your favourite colour is $colour."
  echo "Your favourite toy is $toy."

  # next_func is not set, so the program will exit after running this function.
}

echo 'My Bash Game'
echo
# Start the program, with main_menu as an entry point.
run_funcs main_menu

现在一切都井井有条。让我们试试我们的程序吧!

$ ./bash_game.sh
My Bash Game

Please choose from the menu below.
1) Play
2) Load
3) Exit
> ::prev::
There is no previous screen to return to.

Please choose from the menu below.
1) Play
2) Load
3) Exit
> 2
Game loaded.

What is your name?
> Stephanie

Where are you from, Stephanie?
> ::prev::

What is your name?
> Samantha

Where are you from, Samantha?
> Dubl*n
Invalid characters in input. Allowed characters are a-z, A-Z, 0-9, ', and spaces.
Where are you from, Samantha?
> Dublin

What is your favourite colour?
1) Blue
2) Red
3) Green
4) Grey
5) Magenta
> Aquamarine
Invalid input. Please choose an option from the menu.
> 8
Invalid input. Please choose an option from the menu.
> 1

What is your favourite toy?
> Teddie bear

Your name is Samantha.
You are from Dublin.
Your favourite colour is Blue.
Your favourite toy is Teddie bear.

给你了。