The Wayback Machine - https://web.archive.org/web/20200911053453/https://github.com/bitfield/script/issues/51
Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get user input #51

Open
bitfield opened this issue Apr 26, 2020 · 10 comments
Open

Get user input #51

bitfield opened this issue Apr 26, 2020 · 10 comments

Comments

@bitfield
Copy link
Owner

@bitfield bitfield commented Apr 26, 2020

It's common when writing scripts that install or configure things to need input from the user (at a minimum, something like 'Press Enter to continue'; at a maximum, to be able to prompt the user for input, with an optional default value, and return that value).

Let's use this issue to design how that would look, and I invite suggestions!

@mathantunes
Copy link

@mathantunes mathantunes commented Apr 26, 2020

Hi @bitfield ,

I'm quite new to the Open Source Community (read this is my first try) but I have some experience with Go.

So I sketched out a couple of the possibilities here.
You mentioned installation scripts and configuration, I also thought basic CLI menus should be included here.

So here are my thoughts, please feel totally free to change anything or everything.

Specification on Reading User input for Script

Input Types

Read from a user input

  • Read String (e.g. What's your name ?)
  • Read Number (e.g. How old are you ?)
  • Read Array (e.g. List car brands you like)

Protected

  • Read Password

Menu Items

  • Read from a Menu

Tell a user to pick from a certain set of options
A menu can be defined as an array of choices

Optional parameters

  • A message echoed to the user, on the same line as
    What is your name? _______

  • A default value
    Would you like to continue [Y/n] default(Y):

  • An StdErr message and end of command execution for non-opntional reads
    When a user types a string instead of number, Pipe.WithError

type Options struct {
	Required bool
	Default interface{}
	OnError func(in interface{}) *Pipe //WithError
}

opts := *&Options{
	Required : true,
	Default : nil,
}

vStr := p.ReadString("Input your name:", opts) //string
vFlt := p.ReadNumber("Input your age:", opts) //float64
vPwd := p.ReadPassword("Password: ", opts) //string

(p *Pipe) ReadString(message string, opts Options) *Pipe {
//Execute read
//Return New Pipe
}

vArr := p.ReadArray("Input cars you like", opts)

items := []string{"1 - Skip", "2 - Continue", "3 - Quit"}
(p *Pipe) DisplayMenu(items []string, opts Options, f func(chosen string)*Pipe) *Pipe {
//Display List
//Read user input
//Call f with item picked
//Return new pipe created on f
})
@bitfield
Copy link
Owner Author

@bitfield bitfield commented Apr 28, 2020

Hey @manthantunes, great suggestions, thank you! This looks like a good starting point.

Let's see if we can put together an example program which uses the input feature for some real-world application. (This is often a great way to start designing something: use it!)

For example, a software installer which needs to get the installation directory from the user.

@mathantunes
Copy link

@mathantunes mathantunes commented Apr 29, 2020

Hi @bitfield,

Thinking about use cases before modeling is a very good idea, thank you.

So, I sketched out your example on an installation script, here it is:

        p := script.Echo("Installation Script for ABCD")
	welcome, err := p.String()
	fmt.Println(welcome)

	dir, _ := p.GetPrompter().Input("Please provide a directory for instalation: ", script.PrompOpts{Default: "/usr/share"}).String()

        //Act based on the dir variable
	str, _ := script.Exec(fmt.Sprintf("touch %s/install.sh", dir)).String()

When trying to sketch it through the script api, it came to me that things could get confusing if we implement all user input methods under the Pipe struct.

I researched a bit and took the Prompter API as reference for the implementation.

Now instead of having all methods implemented under Pipe, a single method calls a prompter pointer which then implements all different kinds of prompts.

@bitfield
Copy link
Owner Author

@bitfield bitfield commented Apr 29, 2020

How about something like this:

script.Echo("Installation Script for ABCD").Stdout()
script.PromptDefault("Install location", "/usr/local/abcd").ExecForEach("touch {{.}}/install.sh").Stdout()
@mathantunes
Copy link

@mathantunes mathantunes commented Apr 29, 2020

Makes sense to have the implementation under the package itself, since we can start a pipe and then chain from the user input.

So, on the same example, say the script verifies if its binaries have already been installed and gives you a couple of options like:

script.Echo("Installation Script for ABCD \n").Stdout()
dir, _ := script.PromptDefault("Install location ",  "/usr/local/abcd").String()
choice, _ := script.IfExists(dir+"/install.sh").
        PromptMenu("It seems that ABCD is already installed, choose an action: ",
	[]string{"0 - Quit", "1 - Repair", "2 - Uninstall"}).String()
//Take actions based on choice.

This requires the PromptMenu call from *Pipe though.

@bitfield
Copy link
Owner Author

@bitfield bitfield commented Apr 30, 2020

It's interesting how, once they've settled into a coherent design concept, libraries more or less design themselves. Here, for example, there's really nothing in script that doesn't make sense as part of a pipe: it either reads or creates data, and that flows through into some other pipeline stage.

It's not clear that something like PromptMenu fits that pattern, because we almost certainly need to take different code paths depending on the user's choice. I can't think of a way to fit that into the script paradigm, can you?

@mathantunes
Copy link

@mathantunes mathantunes commented Apr 30, 2020

Interesting point of view. A script usually follows a straight flow to its single objective and falls at errors if anything unexpected happens.

A PromptMenu is more likely to be seen at a CLI, for example, which might not be the scope of the package.

By making a short research, I see there are many other libraries for Menu Prompting.

Although I think that not having it limits the scope of the package, a developer that needs it can easily go get specific packages for this.

So far we have a signature for:

script.PromptDefault("Install location", "/usr/local/abcd")

What do you think of the PromptPassword ? (It's basically PromptDefault without Echo)

Where a script might have a predefined password for installation or even ask for a password for later use some functionality

@bitfield
Copy link
Owner Author

@bitfield bitfield commented Apr 30, 2020

Although I think that not having it limits the scope of the package, a developer that needs it can easily go get specific packages for this.

Exactly. Sometimes the hardest design decision is when to leave something out.

We might try writing a short install script using one of those menu libraries, to see how it plays with script, and if there's anything we can do to improve integration.

I had imagined PromptDefault as a companion to Prompt, which in addition to the prompt string, would take a default value. But I'm not sure there's much use for a prompt method that doesn't have a default value, so perhaps it should just be Prompt, and if you don't want to set a default, you just supply an empty string for that argument:

script.Prompt("Install location", "/usr/local/abcd").ExecForEach("touch {{.}}/install.sh").Stdout()
@mathantunes
Copy link

@mathantunes mathantunes commented May 1, 2020

It sure is!

Yes, I think the basic prompt should always have a default value so it's better named as just Prompt.

script.Prompt("Install location", "/usr/local/abcd").ExecForEach("touch {{.}}/install.sh").Stdout()

For now, I believe the script API should only implement the Prompt method to interact with the user. If demanded, we can check out different prompt handlers, what do you think ?

Thinking of ways we can integrate, I found this really cool interactive prompt package

Which I integrated by

...
err := script.IfExists(dir + "/install.sh").Error()
if err == nil {
  //Already exists
  prompt := promptui.....
}

image

@bitfield
Copy link
Owner Author

@bitfield bitfield commented May 1, 2020

That looks good! Maybe you'd like to put together a PR with a new examples/ program showing how this can be used?

mathantunes added a commit to mathantunes/script that referenced this issue May 1, 2020
mathantunes added a commit to mathantunes/script that referenced this issue May 1, 2020
mathantunes added a commit to mathantunes/script that referenced this issue May 2, 2020
mathantunes added a commit to mathantunes/script that referenced this issue May 2, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.