#!/usr/bin/perl -w # # A script for making backups of InnoDB and MyISAM tables, indexes and .frm # files. # # Copyright 2003 Innobase Oy # use strict; use Getopt::Long; use POSIX "strftime"; use POSIX ":sys_wait_h"; use POSIX "tmpnam"; use FileHandle; # version of this script my $innobackup_version = '1.3.0'; # copyright notice my $copyright_notice = "InnoDB Backup Utility v${innobackup_version}; Copyright 2003-2005 Innobase Oy This software is published under the GNU GENERAL PUBLIC LICENSE Version 2, June 1991. "; # required Perl version (5.005) my @required_perl_version = (5, 0, 5); my $required_perl_version_old_style = 5.005; # force flush after every write and print $| = 1; ###################################################################### # modifiable parameters ###################################################################### # maximum number of files in a database directory which are # separately printed when a backup is made my $backup_file_print_limit = 9; # timeout in seconds for a reply from mysql my $mysql_response_timeout = 900; # default compression level (this is an argument to ibbackup) my $default_compression_level = 1; # time in seconds after which a dummy query is sent to mysql server # in order to keep the database connection alive my $mysql_keep_alive_timeout = 1800; ###################################################################### # end of modifiable parameters ###################################################################### # command line options my $option_help = ''; my $option_version = ''; my $option_apply_log = ''; my $option_copy_back = ''; my $option_include = ''; my $option_sleep = ''; my $option_compress = 999; my $option_uncompress = ''; my $option_use_memory = ''; my $option_mysql_password = ''; my $option_mysql_user = ''; my $option_mysql_port = ''; my $option_mysql_socket = ''; my $option_no_timestamp = ''; my $option_ibbackup_binary = 'ibbackup'; # name of the my.cnf configuration file my $config_file = ''; # root of the backup directory my $backup_root = ''; # backup directory pathname my $backup_dir = ''; # name of the ibbackup suspend-at-end file my $suspend_file = ''; # home directory of innoDB log files my $innodb_log_group_home_dir = ''; # backup my.cnf file my $backup_config_file = ''; # options from the options file my %config; # options from the backup options file my %backup_config; # prefix for output lines my $prefix = 'innobackup:'; # process id of mysql client program (runs as a child process of this script) my $mysql_pid = ''; # mysql server version string my $mysql_server_version = ''; # name of the file where stderr of mysql process is directed my $mysql_stderr; # name of the file where stdout of mysql process is directed my $mysql_stdout; # name of the file where binlog position info is written my $binlog_info; # mysql binlog position as given by "SHOW MASTER STATUS" command my $mysql_binlog_position = ''; # time of the most recent mysql_check call. (value returned by time() function) my $mysql_last_access_time = 0; # process id of ibbackup program (runs as a child process of this script) my $ibbackup_pid = ''; # a counter for numbering mysql connection checks my $hello_id = 0; # the request which has been sent to mysqld, but to which # mysqld has not yet replied. Empty string denotes that no # request has been sent to mysqld or that mysqld has replied # to all requests. my $current_mysql_request = ''; # escape sequences for options files my %option_value_escapes = ('b' => "\b", 't' => "\t", 'n' => "\n", 'r' => "\r", "\\" => "\\", 's' => ' '); # signal that is sent to child processes when they are killed my $kill_signal = 15; ###################################################################### # program execution begins here ###################################################################### # check command-line args check_args(); # print program version and copyright print_version(); # initialize global variables and perform some checks init(); if ($option_copy_back) { # copy files from backup directory back to their original locations copy_back(); } elsif ($option_apply_log) { # expand data files in backup directory by applying log files to them apply_log(); } else { # make a backup of InnoDB and MyISAM tables, indexes and .frm files. backup(); } # program has completed successfully print "$prefix innobackup completed OK!\n"; exit 0; ###################################################################### # end of program execution ###################################################################### # # print_version subroutine prints program version and copyright. # sub print_version { printf($copyright_notice); } # # usage subroutine prints instructions of how to use this program to stdout. # sub usage { print <$mysql_stdout 2>$mysql_stderr ") or Die "Failed to spawn mysql child process: $!"; MYSQL_WRITER->autoflush(1); print "$prefix Connected to database with mysql child process (pid=$mysql_pid)\n"; mysql_check(); } # # mysql_check subroutine checks that the connection to mysql child process # is ok. # sub mysql_check { my $mysql_pid_copy = $mysql_pid; # send a dummy query to mysql child process $hello_id++; my $hello_message = "innobackup hello $hello_id"; print MYSQL_WRITER "select '$hello_message';\n" or Die "Connection to mysql child process failed: $!"; # wait for reply eval { local $SIG{ALRM} = sub { die "alarm clock restart" }; my $stdout = ''; my $stderr = ''; alarm $mysql_response_timeout; while (index($stdout, $hello_message) < 0) { sleep 2; if ($mysql_pid && $mysql_pid == waitpid($mysql_pid, &WNOHANG)) { my $reason = `cat $mysql_stderr`; $mysql_pid = ''; die "mysql child process has died: $reason"; } $stdout = `cat $mysql_stdout`; $stderr = `cat $mysql_stderr`; if ($stderr) { # mysql has reported an error, do exit die "mysql error: $stderr"; } } alarm 0; }; if ($@ =~ /alarm clock restart/) { Die "Connection to mysql child process (pid=$mysql_pid_copy) timedout." . " (Time limit of $mysql_response_timeout seconds exceeded." . " You may adjust time limit by editing the value of parameter" . " \"\$mysql_response_timeout\" in this script.)"; } elsif ($@) { Die $@; } $mysql_last_access_time = time(); } # # mysql_keep_alive subroutine tries to keep connection to the mysqld database # server alive by sending a dummy query when the connection has been idle # for the specified time. # sub mysql_keep_alive { if ((time() - $mysql_last_access_time) > $mysql_keep_alive_timeout) { # too long idle, send a dummy query mysql_check(); } } # # mysql_send subroutine send a request string to mysql child process. # This subroutine appends a newline character to the request and checks # that mysqld receives the query. # Parameters: # request request string # sub mysql_send { my $request = shift; $current_mysql_request = $request; print MYSQL_WRITER "$request\n"; mysql_check(); $current_mysql_request = ''; } # # mysql_close subroutine terminates mysql child process gracefully. # sub mysql_close { print MYSQL_WRITER "quit\n"; print "$prefix Connection to database server closed\n"; $mysql_pid = ''; } # # write_binlog_info subroutine retrieves MySQL binlog position and # saves it in a file. It also prints it to stdout. # sub write_binlog_info { my @lines; my @info_lines; my $position = ''; my $filename = ''; # get binlog position mysql_send "show master status;"; # get "show master status" output lines (2) from mysql output file_to_array($mysql_stdout, \@lines); foreach my $line (@lines) { if ($line =~ m/innobackup hello/) { # this is a hello message, ignore it } else { # this is output line from "show master status" push(@info_lines, $line); } } # write binlog info file open(FILE, ">$binlog_info") || Die "Failed to open file '$binlog_info': $!"; print FILE "$info_lines[1]\n"; close(FILE); # get the name of the last binlog file and position in it ($filename, $position) = $info_lines[1] =~ /^\s*([^\s]+)\s+(.*)$/; $mysql_binlog_position = "filename '$filename', position $position"; } # # mysql_lockall subroutine puts a read lock on all tables in all databases. # sub mysql_lockall { mysql_send "USE mysql;"; mysql_send "DROP TABLE IF EXISTS ibbackup_binlog_marker;"; mysql_send "CREATE TABLE ibbackup_binlog_marker(a INT) TYPE=INNODB;"; mysql_send "SET AUTOCOMMIT=0;"; mysql_send "INSERT INTO ibbackup_binlog_marker VALUES (1);"; if (compare_versions($mysql_server_version, '4.0.22') == 0 || compare_versions($mysql_server_version, '4.1.7') == 0) { # MySQL server version is 4.0.22 or 4.1.7 mysql_send "COMMIT;"; mysql_send "FLUSH TABLES WITH READ LOCK;"; } else { # MySQL server version is other than 4.0.22 or 4.1.7 mysql_send "FLUSH TABLES WITH READ LOCK;"; mysql_send "COMMIT;"; } write_binlog_info; print "$prefix All tables locked and flushed to disk\n"; } # # mysql_unlockall subroutine releases read locks on all tables in all # databases. # sub mysql_unlockall { mysql_send "UNLOCK TABLES;"; mysql_send "DROP TABLE IF EXISTS ibbackup_binlog_marker;"; print "$prefix All tables unlocked\n"; } # # catch_sigpipe subroutine is a signal handler for SIGPIPE. # sub catch_sigpipe { my $rcode; if ($mysql_pid && (-1 == ($rcode = waitpid($mysql_pid, &WNOHANG)) || $rcode == $mysql_pid)) { my $reason = `cat $mysql_stderr`; print "Pipe to mysql child process broken: $reason at"; system("date +'%H:%M:%S'"); exit(1); } else { Die "Broken pipe"; } } # # kill_child_processes subroutine kills all child processes of this process. # sub kill_child_processes { if ($ibbackup_pid) { kill($kill_signal, $ibbackup_pid); $ibbackup_pid = ''; } if ($mysql_pid) { kill($kill_signal, $mysql_pid); $mysql_pid = ''; } } # # require_external subroutine checks that an external program is runnable # via the shell. This is tested by calling the program with the # given arguments. It is checked that the program returns 0 and does # not print anything to stderr. If this check fails, this subroutine exits. # Parameters: # program pathname of the program file # args arguments to the program # pattern a string containing a regular expression for finding # the program version. # this pattern should contain a subpattern enclosed # in parentheses which is matched with the version. # version_ref a reference to a variable where the program version # string is returned. Example "2.0-beta2". # sub require_external { my $program = shift; my $args = shift; my $pattern = shift; my $version_ref = shift; my @lines; my $tmp_stdout = tmpnam(); my $tmp_stderr = tmpnam(); my $rcode; my $error; $rcode = system("$program $args >$tmp_stdout 2>$tmp_stderr"); if ($rcode) { $error = $!; } my $stderr = `cat $tmp_stderr`; if ($stderr ne '') { # failure Die "Couldn't run $program: $stderr"; } elsif ($rcode) { # failure Die "Couldn't run $program: $error"; } # success my $stdout = `cat $tmp_stdout`; @lines = split(/\n|;/,$stdout); print "$prefix Using $lines[0]\n"; # get version string from the first output line of the program ${$version_ref} = ''; if ($lines[0] =~ /$pattern/) { ${$version_ref} = $1; } } # compare_versions subroutine compares two GNU-style version strings. # A GNU-style version string consists of three decimal numbers delimitted # by dots, and optionally followed by extra attributes. # Examples: "4.0.1", "4.1.1-alpha-debug". # Parameters: # str1 a GNU-style version string # str2 a GNU-style version string # Return value: # -1 if str1 < str2 # 0 if str1 == str2 # 1 is str1 > str2 sub compare_versions { my $str1 = shift; my $str2 = shift; my $extra1 = ''; my $extra2 = ''; my @array1 = (); my @array2 = (); my $i; # remove possible extra attributes ($str1, $extra1) = $str1 =~ /^([0-9.]*)(.*)/; ($str2, $extra2) = $str2 =~ /^([0-9.]*)(.*)/; # split "dotted" decimal number string into an array @array1 = split('\.', $str1); @array2 = split('\.', $str2); # compare in lexicographic order for ($i = 0; $i <= $#array1 && $i <= $#array2; $i++) { if ($array1[$i] < $array2[$i]) { return -1; } elsif ($array1[$i] > $array2[$i]) { return 1; } } if ($#array1 < $#array2) { return -1; } elsif ($#array1 > $#array2) { return 1; } else { return 0; } } # # init subroutine initializes global variables and performs some checks on the # system we are running on. # sub init { my $mysql_version = ''; my $ibbackup_version = ''; my $run = ''; # print some instructions to the user if (!$option_apply_log && !$option_copy_back) { $run = 'backup'; } elsif ($option_copy_back) { $run = 'copy-back'; } else { $run = 'apply-log'; } print "IMPORTANT: Please check that the $run run completes successfully.\n"; print " At the end of a successful $run run innobackup\n"; print " prints \"innobackup completed OK!\".\n\n"; # check that MySQL client program and InnoDB Hot Backup program # are runnable via shell if (!$option_copy_back) { # we are making a backup or applying log to backup if (!$option_apply_log) { # we are making a backup, we need mysql server my $output = ''; my @lines = (); # check that we have mysql client program require_external('mysql', '--version', 'Ver ([^,]+)', \$mysql_version); # get mysql server version my $options = get_mysql_options(); @lines = split('\n', `echo "select \@\@version;" | mysql $options`); $mysql_server_version = $lines[1]; print "$prefix Using mysql server version $mysql_server_version\n"; } require_external($option_ibbackup_binary, '--license', 'version (\S+)', \$ibbackup_version); print "\n"; if ($option_include && $ibbackup_version && $ibbackup_version le "2.0") { # --include option was given, but ibbackup is too # old to support it Die "--include option was given, but ibbackup is too old" . " to support it. You must upgrade to InnoDB Hot Backup" . " v2.0 in order to use --include option.\n"; } } # set signal handlers $SIG{PIPE} = \&catch_sigpipe; # read MySQL options file read_config_file($config_file, \%config); # get innodb log home directory from options file $innodb_log_group_home_dir = get_option(\%config, 'mysqld', 'innodb_log_group_home_dir'); if (!$option_apply_log && !$option_copy_back) { # we are making a backup, create a new backup directory $backup_dir = make_backup_dir(); print "$prefix Created backup directory $backup_dir\n"; $backup_config_file = $backup_dir . '/backup-my.cnf'; write_backup_config_file($backup_config_file); $suspend_file = $backup_dir . '/ibbackup_suspended'; $mysql_stdout = $backup_dir . '/mysql-stdout'; $mysql_stderr = $backup_dir . '/mysql-stderr'; $binlog_info = $backup_dir . '/ibbackup_binlog_info'; } elsif ($option_copy_back) { $backup_config_file = $backup_dir . '/backup-my.cnf'; read_config_file($backup_config_file, \%backup_config); } } # # write_backup_config_file subroutine creates a backup options file for # ibbackup program. It writes to the file only those options that # are required by ibbackup. # Parameters: # filename name for the created options file # sub write_backup_config_file { my $filename = shift; my $innodb_data_file_path = get_option(\%config, 'mysqld', 'innodb_data_file_path'); my $innodb_log_files_in_group = get_option(\%config, 'mysqld', 'innodb_log_files_in_group'); my $innodb_log_file_size = get_option(\%config, 'mysqld', 'innodb_log_file_size'); open(FILE, "> $filename") || Die "Failed to open file '$filename': $!"; print FILE "# This MySQL options file was generated by innobackup.\n\n" . "# The MySQL server\n" . "[mysqld]\n" . "datadir=$backup_dir\n" . "innodb_data_home_dir=$backup_dir\n" . "innodb_data_file_path=$innodb_data_file_path\n" . "innodb_log_group_home_dir=$backup_dir\n" . "innodb_log_files_in_group=$innodb_log_files_in_group\n" . "innodb_log_file_size=$innodb_log_file_size\n"; close(FILE); } # # check_args subroutine checks command line arguments. If there is a problem, # this subroutine prints error message and exits. # sub check_args { my $i; my $rcode; my $buf; my $perl_version; # check the version of the perl we are running if (!defined $^V) { # this perl is prior to 5.6.0 and uses old style version string my $required_version = $required_perl_version_old_style; if ($] lt $required_version) { print "$prefix Warning: " . "Your perl is too old! Innobackup requires\n"; print "$prefix Warning: perl $required_version or newer!\n"; } } else { $perl_version = chr($required_perl_version[0]) . chr($required_perl_version[1]) . chr($required_perl_version[2]); if ($^V lt $perl_version) { my $version = chr(48 + $required_perl_version[0]) . "." . chr(48 + $required_perl_version[1]) . "." . chr(48 + $required_perl_version[2]); print "$prefix Warning: " . "Your perl is too old! Innobackup requires\n"; print "$prefix Warning: perl $version or newer!\n"; } } if (@ARGV == 0) { # no command line arguments usage(); exit(1); } # read command line options $rcode = GetOptions('compress:i' => \$option_compress, 'help' => \$option_help, 'version' => \$option_version, 'sleep=i' => \$option_sleep, 'apply-log' => \$option_apply_log, 'copy-back' => \$option_copy_back, 'include=s' => \$option_include, 'use-memory=i' => \$option_use_memory, 'uncompress' => \$option_uncompress, 'password=s' => \$option_mysql_password, 'user=s' => \$option_mysql_user, 'port=s' => \$option_mysql_port, 'socket=s' => \$option_mysql_socket, 'no-timestamp' => \$option_no_timestamp, 'ibbackup=s' => \$option_ibbackup_binary); if (!$rcode) { # failed to read options print "$prefix Bad command line arguments\n"; usage(); exit(1); } if ($option_help) { # print help text and exit usage(); exit(0); } if ($option_version) { # print program version and copyright print_version(); exit(0); } if ($option_compress == 0) { # compression level no specified, use default level $option_compress = $default_compression_level; } if ($option_compress == 999) { # compress option not given in the command line $option_compress = 0; } if (@ARGV < 2) { print "$prefix Missing command line argument\n"; usage(); exit(1); } elsif (@ARGV > 2) { print "$prefix Too many command line arguments\n"; usage(); exit(1); } # get options file name $config_file = $ARGV[0]; if (!$option_apply_log && !$option_copy_back) { # we are making a backup, get backup root directory $backup_root = $ARGV[1]; } else { # get backup directory $backup_dir = $ARGV[1]; } print "\n"; } # # make_backup_dir subroutine creates a new backup directory and returns # its name. # sub make_backup_dir { my $dir; my $innodb_data_file_path = get_option(\%config, 'mysqld', 'innodb_data_file_path'); # create backup directory $dir = $backup_root; $dir .= '/' . strftime("%Y-%m-%d_%H-%M-%S", localtime()) unless $option_no_timestamp; mkdir($dir, 0777) || Die "Failed to create backup directory $dir: $!"; # create subdirectories for ibdata files if needed foreach my $a (split(/;/, $innodb_data_file_path)) { my $path = (split(/:/,$a))[0]; my @relative_path = split(/\/+/, $path); pop @relative_path; if (@relative_path) { # there is a non-trivial path from the backup directory # to the directory of this backup ibdata file, check # that all the directories in the path exist. create_path_if_needed($dir, \@relative_path); } } return $dir; } # # create_path_if_needed subroutine checks that all components # in the given relative path are directories. If the # directories do not exist, they are created. # Parameters: # root a path to the root directory of the relative pathname # relative_path a relative pathname (a reference to an array of # pathname components) # sub create_path_if_needed { my $root = shift; my $relative_path = shift; my $path; $path = $root; foreach $a (@{$relative_path}) { $path = $path . "/" . $a; if (! -d $path) { # this directory does not exist, create it ! mkdir($path, 0777) || Die "Failed to create backup directory: $!"; } } } # # remove_from_array subroutine removes excluded element from the array. # Parameters: # array_ref a reference to an array of strings # excluded a string to be excluded from the copy # sub remove_from_array { my $array_ref = shift; my $excluded = shift; my @copy = (); my $size = 0; foreach my $str (@{$array_ref}) { if ($str ne $excluded) { $copy[$size] = $str; $size = $size + 1; } } @{$array_ref} = @copy; } # # backup_files subroutine copies .frm, .MRG, .MYD and .MYI files to # backup directory. # sub backup_files { my $source_dir = get_option(\%config, 'mysqld', 'datadir'); my @list; my $file; my $database; my $wildcard = '*.{frm,MYD,MYI,MRG}'; opendir(DIR, $source_dir) || Die "Can't open directory '$source_dir': $!\n"; print "\n$prefix Starting to backup .frm, .MRG, .MYD and .MYI files in \n"; print "$prefix subdirectories of '$source_dir'\n"; # loop through all database directories while (defined($database = readdir(DIR))) { my $print_each_file = 0; my $file_c; # skip files that are not database directories if ($database eq '.' || $database eq '..') { next; } next unless -d "$source_dir/$database"; if (! -e "$backup_dir/$database") { # create database directory for the backup mkdir("$backup_dir/$database", 0777) || Die "Couldn't create directory '$backup_dir/$database': $!"; } # copy files of this database @list = glob("$source_dir/$database/" . $wildcard); $file_c = @list; if ($file_c <= $backup_file_print_limit) { $print_each_file = 1; } else { print "$prefix Backing up files " . "'$source_dir/$database/$wildcard' ($file_c files)\n"; } foreach $file (@list) { # copying may take a long time, so we have to prevent # mysql connection from timing out mysql_keep_alive(); if ($print_each_file) { print "$prefix Backing up file '$file'\n"; } system("cp -p $file $backup_dir/$database") and Die "Failed to copy file '$file': $!"; } } closedir(DIR); print "$prefix Finished backing up .frm, .MRG, .MYD and .MYI files\n\n"; } # # file_to_array subroutine reads the given text file into an array and # stores each line as an element of the array. The end-of-line # character(s) are removed from the lines stored in the array. # Parameters: # filename name of a text file # lines_ref a reference to an array # sub file_to_array { my $filename = shift; my $lines_ref = shift; open(FILE, $filename) || Die "can't open file '$filename': $!"; @{$lines_ref} = ; close(FILE) || Die "can't close file '$filename': $!"; foreach my $a (@{$lines_ref}) { chomp($a); } } # # unescape_string subroutine expands escape sequences found in the string and # returns the expanded string. It also removes possible single or double quotes # around the value. # Parameters: # value a string # Return value: # a string with expanded escape sequences # sub unescape_string { my $value = shift; my $result = ''; my $offset = 0; # remove quotes around the value if they exist if (length($value) >= 2) { if ((substr($value, 0, 1) eq "'" && substr($value, -1, 1) eq "'") || (substr($value, 0, 1) eq '"' && substr($value, -1, 1) eq '"')) { $value = substr($value, 1, -1); } } # expand escape sequences while ($offset < length($value)) { my $pos = index($value, "\\", $offset); if ($pos < 0) { $pos = length($value); $result = $result . substr($value, $offset, $pos - $offset); $offset = $pos; } else { my $replacement = substr($value, $pos, 2); my $escape_code = substr($value, $pos + 1, 1); if (exists $option_value_escapes{$escape_code}) { $replacement = $option_value_escapes{$escape_code}; } $result = $result . substr($value, $offset, $pos - $offset) . $replacement; $offset = $pos + 2; } } return $result; } # # read_config_file subroutine reads MySQL options file and # returns the options in a hash containing one hash per group. # Parameters: # filename name of a MySQL options file # groups_ref a reference to hash variable where the read # options are returned # sub read_config_file { my $filename = shift; my $groups_ref = shift; my @lines ; my $i; my $group; my $group_hash_ref; # read file to an array, one line per element file_to_array($filename, \@lines); # classify lines and save option values $group = 'default'; $group_hash_ref = {}; ${$groups_ref}{$group} = $group_hash_ref; # this pattern described an option value which may be # quoted with single or double quotes. This pattern # does not work by its own. It assumes that the first # opening parenthesis in this string is the second opening # parenthesis in the full pattern. my $value_pattern = q/((["'])([^\\\3]|(\\[^\3]))*\3)|([^\s]+)/; for ($i = 0; $i < @lines; $i++) { SWITCH: for ($lines[$i]) { # comment /^\s*(#|;)/ && do { last; }; # group /^\s*\[(.*)\]/ && do { $group = $1; if (!exists ${$groups_ref}{$group}) { $group_hash_ref = {}; ${$groups_ref}{$group} = $group_hash_ref; } else { $group_hash_ref = ${$groups_ref}{$group}; } last; }; # option /^\s*([^\s=]+)\s*(#.*)?$/ && do { ${$group_hash_ref}{$1} = ''; last; }; # set-variable = option = value /^\s*set-variable\s*=\s*([^\s=]+)\s*=\s*($value_pattern)\s*(#.*)?$/ && do { ${$group_hash_ref}{$1} = unescape_string($2); last; }; # option = value /^\s*([^\s=]+)\s*=\s*($value_pattern)\s*(#.*)?$/ && do { ${$group_hash_ref}{$1} = unescape_string($2); last; }; # empty line /^\s*$/ && do { last; }; # unknown print("$prefix: Warning: Ignored unrecognized line ", $i + 1, " in options file '$filename': '${lines[$i]}'\n" ); } } } # # get_option subroutine returns the value of given option in the config # structure. If option is missing, this subroutine calls exit. # Parameters: # config_ref a reference to a config data # group option group name # option_name name of the option # Return value: # option value as a string # sub get_option { my $config_ref = shift; my $group = shift; my $option_name = shift; my $group_hash_ref; if (!exists $config{$group}) { # no group print "$prefix fatal error: no '$group' group in MySQL options file '$config_file\n"; exit(1); } $group_hash_ref = ${$config_ref}{$group}; if (!exists ${$group_hash_ref}{$option_name}) { # no option print "$prefix fatal error: no '$option_name' option in group '$group' in MySQL options file '$config_file\n"; exit(1); } return ${$group_hash_ref}{$option_name}; }