Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

General pattern for interactive text-mode script?

by Anonymous Monk
on Jul 03, 2008 at 16:46 UTC ( [id://695388]=perlquestion: print w/replies, xml ) Need Help??

Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

What's a good way to structure an interactive text-mode script? That is, one that presents to the user multiple questions, with subsequent questions depending upon answers already given.

Here's the kind of pseudo-code that immediately comes to mind, but which gets unmanageable very quickly:

my $main_choice; while ($main_choice = prompt(...)) { if ($main_choice eq 'task_1') { my $next_choice = prompt(...); if ($next_choice eq 'task_1_a') { #... } elsif ($next_choice eq 'task_1_b') { my $deeper_choice = prompt(); if ($deeper_choice eq 'task_1_b_1') { #... } elsif ($next_choice eq 'task_1_b_2') { #... } else { #... } } elsif ($next_choice eq 'task_1_c') { #... } else { #... } } elsif ($main_choice eq 'task_2') { #... } else { #... } }

Replies are listed 'Best First'.
Re: General pattern for interactive text-mode script?
by apl (Monsignor) on Jul 03, 2008 at 17:35 UTC
    I'd construct a finite state machine.

    Assume you have a hash where one entry has a question, the list of possible answers, and the list of hash keys associated with those answers. Your loop above becomes much simpler.

      Could you please give an example of such a data structure? Is it anything like the following? :

      #!/usr/bin/env perl use strict; use warnings; #... sub task_1_a_1 { print "Performing task 1 a 1 ...\n"; } #... my %stuff = ( 'Major task to perform?' => ['task_1','task_2','task_2'], 'task_1' => {'Sub-task for task_1?' => ['task_1_a','task_1_b','tas +k_1_c']}, 'task_2' => {'Sub-task for task_2?' => ['task_2_a','task_2_b','tas +k_2_c']}, 'task_3' => {'Sub-task for task_3?' => ['task_3_a','task_3_b','tas +k_3_c']}, 'task_1_a' => {'Sub-sub-task for task_1_a?' => ['task_1_a_1','task +_1_a_2']}, #... 'task_1_a_1' => \&task_1_a_1, ); $stuff{task_1_a_1}->();

      Incidentally, if you happen to know of a simple example of a FSM (preferably implemented in Perl), I'd love to see it.

        You have the sense of it, though I'd change:
        'Major task to perform?' => ['task_1','task_2','task_2'],
        to something like
        'Major task to perform?' => { answers => [ 'Add', 'Delete', 'Rename' ], keys => [ 'task_1','task_2', 'task_2' ], },
        This way if the user enters 'Add'. the next hash key you'd use is 'task_1'. (Make certain to programmatically test that there are as many keys as there are answers.)

        This handles compressing all of the prompts you were talking about. The next problem is: what happens when you get to the end of the user input? You always need to keep track of the last key you used, and you need some way to indicate that you're at a terminal node. (Perhaps a blank key could indicate this.)

        Your example would then become:

        my $current_key = 'Major task to perform?'; while ( my $choice = prompt( $current_key, ...)) { # set INDEX to offset of $choice in $structure{ $current_key }{ans +wers} # if $structure{$current_key}{keys}[INDEX] is empty # break out of loop; you;'re in a terminal state # else $current_key = $structure{$current_key}{keys}[INDEX] } <p>A lot of the code is left as an exercise to the reader. Good luck w +ith your class.
Re: General pattern for interactive text-mode script?
by moritz (Cardinal) on Jul 03, 2008 at 17:35 UTC
    If you want to keep your sanity while programming, you'll encode all of the nesting logic into a data structure, and all the code blocks marked with #... in your example into a dispatch table.

    Then you write a small dispatching program that walks through the data structure, prints the prompt, analyses the results, makes a transition into the next state and calls your callback (maybe in a different order).

    Searching cpan for state might give some interesting results, chances are that somebody already implemented something similar.

      If you want to keep your sanity while programming,

      Highly desirable. :)

      you'll encode all of the nesting logic into a data structure,

      Any chance I could get you to give a short example of such a data structure?

      and all the code blocks marked with #... in your example into a dispatch table.

      Hm. Haven't used dispatch tables before. Searching around a bit, it seems to be just a hash where the keys are "command" names and the values are references to functions. Will have a closer look at "When should I use a dispatch table?" and "Implementing Dispatch Tables", because I'm still not sure what it buys me over just calling the functions directly.

      Then you write a small dispatching program that walks through the data structure, prints the prompt, analyses the results, makes a transition into the next state and calls your callback (maybe in a different order).

      Oooh. Would love to see pseudocode if you were of a mind to take the time to write it out.

        Here is some stupid example code that doesn't do anything useful, but should give you an idea of how such a dispatch table and a state automaton might work:
        use strict; use warnings; my %states = ( main => { next => [ qw(intermediate end) ], on_enter => sub { print "You entered state main\n" }, }, intermediate => { next => [ qw(intermediate end) ], on_enter => sub { print "stupid side effect\n"; }, }, end => { next => [], on_enter => sub { print "Never called\n" }, }, ); my $current = $states{main}; while (@{$current->{next}}){ $current->{on_enter}->(); print "You can go to the following states from here:\n"; for (@{$current->{next}}){ print "\t$_\n"; } my $next = <STDIN>; chomp $next; if ($states{$next}){ # go to the next state: # TODO: check that this state transition is allowed $current = $states{$next}; } else { print "Sorry, don't understand your input, try again please\n" +; } }

        An ethereal Anonymous Monk writes:

        "...because I'm still not sure what it buys me over just calling the functions directly."
        Because you store references to the functions in the hash and can call the function indirectly by de-referencing the hash value instead of hard coding the subroutine name into a fixed control structure. Much easier, and safer, store a reference than to indirect reference via a string. It is very important for you future programing success that you learn to write and understand dispatch tables. You will encounter them in many programs and use them often in your own code.


        s//----->\t/;$~="JAPH";s//\r<$~~/;{s|~$~-|-~$~|||s |-$~~|$~~-|||s,<$~~,<~$~,,s,~$~>,$~~>,, $|=1,select$,,$,,$,,1e-1;print;redo}
Re: General pattern for interactive text-mode script?
by pc88mxer (Vicar) on Jul 03, 2008 at 16:55 UTC
    IO::Prompt's menu feature might be suitable for your application. It supports nested menus.
Re: General pattern for interactive text-mode script?
by pc88mxer (Vicar) on Jul 03, 2008 at 18:50 UTC
    Here's a simple FSM implementation. Each subroutine returns the next 'state' (just a subroutine name) to pass control to. To exit, return undef.
    my $state = "start"; while (my $next = $state->()) { $state = $next; } sub start { ...get input... if (/1/) { return "menu_1" } elsif (/2/) { return "menu_2" } ... } sub menu_1 { ... } sub menu_2 { ... }
      Hu, this breaks strictures, better use code references :
      use strict; use warnings; my $state = \&start; while (my $next = $state->()) { $state = $next; } sub start { ...get input... if (/1/) { return \&menu_1 } elsif (/2/) { return \&menu_2 } ... } sub menu_1 { ... } sub menu_2 { ... }
      By the way instead of chaining multiple if () {} blocks, using a dispatch table is more efficient and looks nicer:
      use strict; use warnings; my $state = \&start; while (my $next = $state->()) { $state = $next; } sub start { my %next_action = ( 1 => \&menu_1, 2 => \&menu_2, 3 => \&menu_3, ); return $next_action{$_} if exists $next_action{$_} }

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://695388]
Approved by pc88mxer
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others scrutinizing the Monastery: (6)
As of 2024-03-19 14:05 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found