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

Hi all, I am writing a database management program, and I make extensive use of IO::Prompt. If a user is making a database entry but decides to change his mind, the database needs to be saved to a file. This is because the database is read into memory when the user begins manipulating it, but the user has the option of making multiple entries in one sitting. I think it's wasteful to constantly write the database out to a file just to read it back in literally one second later, so I just keep adding to the database in memory, then write it all out later.

The thing is, the user needs to be able to quit data entry and go back to do other things by hitting the ESC key. I can't just return out of the add_to_db() subroutine without first doing a write_to_db() first.

What I've been doing thus is:

my @database = read_from_db(); while (1){ my $id = prompt "Enter the product ID: ", -escape; return write_to_db($database_type, @database) if $id eq "\e"; my $comments = prompt "Enter any comments about this product: ", - +escape; return write_to_db($database_type, @database) if $comments eq "\e" +; print "Confirm the following data:\n"; print "Product ID: $id\n"; print "Comments: $comments\n\n"; my $yn = prompt "Is this information correct? y/n: ", -yn; #no esc +ape here since this info will get lost if we escape redo if ! $yn; push(@database, [$id, $comments]); $yn = prompt "Add another entry? y\n: ", -yn, -escape; last if !yn || $yn eq "\e"; } write_to_db($database_type, @database);
For entries that have lots of fields, it gets unwieldy to write out that return if "\e" statement. I wrote a subroutine "clean_return" that writes the database to a file when called, but that doesn't change the fact that I can have up to 16 different return-if-ESC statements.

What I want to do is set up an interrupt-type thing: so if at any time the ESC key is pressed, it should do a clean_return. This way I only need to write that out the one time instead of up to 16 different explicit clean_return statements.

If you could help me out, I'd greatly appreciate it. Thanks.

Replies are listed 'Best First'.
Re: Custom interrupt?
by graff (Chancellor) on Jul 03, 2008 at 04:22 UTC
    You seem to be suggesting that different entries may have different numbers of fields to be entered. How is the true field count determined for a given entry, and where to the field labels come from? (In your example, there are just two fields per entry, with the labels "id" and "comments".)

    You also seem to want the user to be able to write incomplete entries, escaping out of the field-entry loop with only one or more of the initial fields filled in, and one or more of the final fields in the sequence left undefined (null), and these get written to the database file along with complete entries.

    I think what you might want here is a subroutine that takes two array refs, one containing the field labels and the other containing prompt strings, with both arrays ordered according to the desired sequence for user input; the sub would return a hash with field labels as keys and user input as values. Whatever is returned by the sub can then be written to the database. Something like this (not tested):

    sub entry_prompter { my ( $labels, $prompts ) = @_; my %entry; for my $i ( 0 .. $#$labels ) { my $user_input = prompt $$prompts[$i], -escape; last if ( $user_input eq "\e" ); $entry{$$labels[$i]} = $user_input; } return %entry; }
    WIth that, it's up to the caller to set up the appropriate arrays, handle the iteration that prompts for "Add another entry? y/n: ", and read/write the database file.

    If I were you, I'd add another array that provides conditions for sanity checks on the fields, so that the "entry_prompter()" sub can test $user_input -- e.g. should be numeric, or "y/n", or not longer than 10 characters, or whatever.

    Then again, if I were you, I'd use a real database (not a flat file), and I'd use Tk; to provide a nice GUI with appropriate user input widgets to fully populate a record as efficiently and reliably as possible, along with a "Submit" button.

    (updated to simplify some of the wording in the text)

      It's a very good point you bring up. I'm actually using Text::CSV_XS and using the print method to print the arrayref. The reason why I'm doing this is because other programs have to interact with the CSV files.

      I'm keeping two different databases for information about receiving images: one for partial images, one for missing images. (I don't want to get into more one-off detail, but I will say that this is in support of the Phoenix Mars Lander...) Depending on the database you're manipulating, there could be 5 or 8 different fields, and the process is always evolving.

      I would write a GUI for this but quite frankly I don't know how. I taught myself Perl a few months ago to get my job done effectively, so what I've been doing is pretty quick and dirty.

Re: Custom interrupt?
by jethro (Monsignor) on Jul 03, 2008 at 04:25 UTC
    I don't understand why you have to save and reread the database because "a user is making a database entry but decides to change his mind". I would have guessed that you just have to reread the previously saved database in such a case.

    Just as a reminder, there are excellent database moduls on CPAN, for example DBM::Deep. They are well tested and might spare you a lot of work.

    Your immediate problem might be solved by putting the prompt in a subroutine with the escape checking. Parameters of the subroutine would a flag whether ESC is allowed and the parameters to prompt(). Return values are the return value of prompt() plus a boolean to show whether ESC was pressed. Inside this subroutine you could do the write_to_db

    If you really want an interrupt thingy, that can be simulated with putting your code inside an eval and die'ing when someone hits ESC. This is sadly the meager substitute for exceptions/interrupts in perl5.

Re: Custom interrupt?
by pc88mxer (Vicar) on Jul 03, 2008 at 04:55 UTC
    This is a hack, but you can probably make it work. When the user hits ESC, you can die. Then you can catch it and perform whatever cleanup that needs to be done:
    sub enter_products { my @database = read_from_db(); eval { while (1) { ... $comment = my_prompt "Enter comment"; $descript = my_prompt "Enter description"; $sku = my_prompt "Enter sku"; $weight = my_prompt "Enter weight"; $color = my_prompt "Enter color"; ... $yn = my_prompt "Add another entry?", -yn; ... } }; unless ($@ =~ /\AESC PRESSED/) { die $@; # propagate non-ESC exceptions } write_to_db(...); } sub my_prompt { my $input = prompt @_, -escape if ($input eq "\e") { die "ESC PRESSED"; } return $input; }
Re: Custom interrupt?
by pc88mxer (Vicar) on Jul 03, 2008 at 05:40 UTC
    It appears that goto to a label also unwinds the stack. Perhaps other monks can verify this.

    If that is the case, here's another 'solution':

    our $ESC_EXIT_LABEL; sub my_prompt { my $input = prompt @_, -escape if ($input eq "\e" && $ESC_EXIT_LABEL) { goto $ESC_EXIT_LABEL; } return $input; } sub enter_products { ... local($ESC_EXIT_LABEL) = "enter_products_esc_label"; while (1) { ... call my_prompt many times... } enter_products_esc_label: write_database(...); }
Re: Custom interrupt?
by TGI (Parson) on Jul 04, 2008 at 00:59 UTC

    Any time I find myself writing a big list of similar code, I know I need to do some data driven programming. By putting my variables in a datastructure, I can iterate over the structure with a small chunk of code, and do the same work with much less typing.

    I do my work on Win32 systems, so I couldn't use IO::Prompt. Instead I hacked in a very ugly fake prompt function--ignore it, it's just a stand in for the real deal. I added a couple of other dummy functions to get things working--ignore these as well.

    Here's how I approached your problem:

    • I created a list of fields to process.
    • I turned your while loop inside out: while( get_data ) {...}.
    • In the loop, we prompt to continue entry. If user says to stop, we exit the loop.

    With my method there is no need for exception catching or any dodgy goto tomfoolery. Also, you can use the same code to collect data for several different field sets.

    use strict; use warnings; use Scalar::Util qw( reftype ); #use IO::Prompt; # half-assed prompt routine to stand in for the nonportable IO::Prompt sub prompt { print join "\n", @_; print "\n"; my $in = <>; $in = "\e" if $in =~ /ESC/; chomp $in; return 0 if uc($in) eq 'N'; return $in; } # return to 'real' code. # Field definitions to reduce repeated code. # This could also be stored as an AoA # ( [ id => 'Product ID' ], # [ comments => 'Comments' ], # ); my @fields = ( { name => 'id', description => 'Product ID', }, { name => 'comments', description => 'Comments', }, # And so forth. In the order you want to process them. ); # It looks like your code was in a sub rountine call. # So I enclosed my code in a subroutine as well. Get_Data_For_DB( \@fields ); # the main sub sub Get_Data_For_DB { my $fields = shift; # Get field specification my @database = read_from_db(); my $database_type = 'foo'; # give this variable a value. while ( collect_user_data( \@database, $fields ) ) { last unless continue_data_entry(); } # finalize changes and return result. return write_to_db($database_type, @database); } # Attempt to get data from user. # Return TRUE if we have data or a SKIP condition # Return FALSE on an ABORT condition. sub collect_user_data { my $db = shift; my $fields = shift; my $entry = get_data_entry( $fields ); # this should probably be replaced with # a dispatch table if any more conditions get added. if ( reftype $entry eq 'ARRAY' ) { push @$db, $entry; return 1; } elsif ( $entry eq 'SKIP' ) { return 1; } elsif ( $entry eq 'ABORT' ) { return; } die "Unexpected result: $entry"; } # Get one entry worth of data. # If entry succeeds, return an array ref containing data. # Else return a status string- # 'ABORT' to abort data entry # 'SKIP' to store no new data in db. sub get_data_entry { my $fields = shift; # Initial entry my @values; # Ordered list. foreach my $field ( @$fields ) { my $value = prompt "Enter the $field->{description}: ", '-esca +pe'; return 'ABORT' if $value eq "\e"; # what happens if user types "foo\e" # Store data for return. push @values, $value; } # Confirmation - print field/value pairs print "Confirm the following data:\n"; print map { " $fields->[$_]{description}: $values[$_]\n" } 0..$#$fields; print "\n"; my $yn = prompt "Is this information correct? y/n: ", '-yn'; return $yn ? \@values : 'SKIP'; } # Prompt user for continuing entry. # Return TRUE for Y # Return FALSE for N or ESC sub continue_data_entry { my $continue = prompt "Add another entry? y/n: ", '-yn', '-escape' +; $continue = 0 if $continue eq "\e"; return $continue; } # Dummy routine for test puproses. sub read_from_db { return; } # Dummy routine for test puproses. sub write_to_db { my $type = shift; use Data::Dumper; print Dumper \@_; }


    TGI says moo

      I didn't thoroughly look through this code, but it looks like it can get the job done very nicely. Right now my feeble attempt is being used, but I'll see if I can swap it out and add this in instead.

      Thanks!