From 0f05ec5d17189473c5d01cd6137d95228db89c43 Mon Sep 17 00:00:00 2001 From: Ernest Toth Date: Mon, 17 Feb 2020 15:43:49 +0100 Subject: [PATCH 1/2] Added multithreded version of apply (Win, Linux, Mac) --- git-store-meta.pl | 396 +++++++++++++++++++++++++++------------------- 1 file changed, 233 insertions(+), 163 deletions(-) diff --git a/git-store-meta.pl b/git-store-meta.pl index 226f743..ae76345 100755 --- a/git-store-meta.pl +++ b/git-store-meta.pl @@ -35,6 +35,11 @@ # -t|--target FILE Specify another filename to store metadata. Defaults to # ".git_store_meta" in the root of the working tree. # (available for: --store, --update, --apply, --install) +# -m|--multithread Use a multi-threading applying data, +# Number of threads is deduced from system +# NUMBER_OF_PROCESSORS on Win, '/proc/cpuinfo' on Linux, 'sysctl -n hw.logicalcpu' on Mac +# For custom threads count you can use env. variable GSM_THREAD_COUNT +# (available for: --apply, --install) # # FIELDs is a comma-separated string consisting of the following values: # mtime last modified time @@ -54,6 +59,8 @@ use utf8; use strict; +use threads; +use threads::shared; use version; our $VERSION = version->declare("v2.0.1"); use Getopt::Long; @@ -82,7 +89,7 @@ my $gitdir; my $topdir; -my $git_store_meta_filename; +my $git_store_meta_filename :shared; my $git_store_meta_file; my $git_store_meta_header; my $temp_file; @@ -93,40 +100,42 @@ my $cache_app; my $cache_version; my %cache_configs; -my @cache_fields; +my @cache_fields :shared; my $configs; # parse arguments my %argv = ( - "store" => 0, - "update" => 0, - "apply" => 0, - "install" => 0, - "help" => 0, - "version" => 0, - "target" => undef, - "fields" => undef, - "directory" => undef, - "topdir" => undef, - "force" => 0, - "dry-run" => 0, - "verbose" => 0, + "store" => 0, + "update" => 0, + "apply" => 0, + "install" => 0, + "help" => 0, + "version" => 0, + "target" => undef, + "fields" => undef, + "directory" => undef, + "topdir" => undef, + "force" => 0, + "dry-run" => 0, + "verbose" => 0, + "multithread" => undef, ); GetOptions( - "store|s" => \$argv{'store'}, - "update|u" => \$argv{'update'}, - "apply|a" => \$argv{'apply'}, - "install|i" => \$argv{'install'}, - "help|h" => \$argv{'help'}, - "version" => \$argv{'version'}, - "fields|f=s" => \@{$argv{'fields'}}, - "directory|d!" => \$argv{'directory'}, - "topdir!" => \$argv{'topdir'}, - "force" => \$argv{'force'}, - "dry-run|n" => \$argv{'dry-run'}, - "verbose|v" => \$argv{'verbose'}, - "target|t=s" => \$argv{'target'}, + "store|s" => \$argv{'store'}, + "update|u" => \$argv{'update'}, + "apply|a" => \$argv{'apply'}, + "install|i" => \$argv{'install'}, + "help|h" => \$argv{'help'}, + "version" => \$argv{'version'}, + "fields|f=s" => \@{$argv{'fields'}}, + "directory|d!" => \$argv{'directory'}, + "topdir!" => \$argv{'topdir'}, + "force" => \$argv{'force'}, + "dry-run|n" => \$argv{'dry-run'}, + "verbose|v" => \$argv{'verbose'}, + "target|t=s" => \$argv{'target'}, + "multithread|m" => \$argv{'multithread'}, ); # determine action @@ -408,6 +417,7 @@ sub install_hooks { my $t; my $f = defined($argv{'target'}) ? " -t " . escapeshellarg($argv{'target'}) : ""; my $f2 = escapeshellarg(defined($argv{'target'}) ? $argv{'target'} : $GIT_STORE_META_FILENAME); + my $mt = defined($argv{'multithread'}) ? "-m " : ""; $t = "$gitdir/hooks/pre-commit"; open(FILE, '>', $t) or die "error: failed to write to `$t': $!\n"; @@ -434,7 +444,7 @@ sub install_hooks { $t = "$gitdir/hooks/post-checkout"; open(FILE, '>', $t) or die "error: failed to write to `$t': $!\n"; - printf FILE <<'EOF', $f; + printf FILE <<'EOF', $mt, $f; #!/bin/sh # when running the hook, cwd is the top level of working tree @@ -447,7 +457,7 @@ sub install_hooks { # apply metadata only when HEAD is changed if [ ${sha_new} != ${sha_old} ]; then - "$script" --apply%s + "$script" %s--apply%s fi EOF close(FILE); @@ -456,7 +466,7 @@ sub install_hooks { $t = "$gitdir/hooks/post-merge"; open(FILE, '>', $t) or die "error: failed to write to `$t': $!\n"; - printf FILE <<'EOF', $f; + printf FILE <<'EOF', $mt, $f; #!/bin/sh # when running the hook, cwd is the top level of working tree @@ -467,7 +477,7 @@ sub install_hooks { # apply metadata after a successful non-squash merge if [ $is_squash -eq 0 ]; then - "$script" --apply%s + "$script" %s--apply%s fi EOF close(FILE); @@ -734,189 +744,249 @@ sub update { close(CMD); } +sub get_max_thread_count { + my $count = $ENV{"GSM_THREAD_COUNT"}; + if (defined($count) && $count < 1) { + return 1; + } + + #MS Windows - + $count = $ENV{"NUMBER_OF_PROCESSORS"}; + if (defined($count) && $count > 0) { + return $count; + } + + #linux + if (open my $handle, "/proc/cpuinfo") { + $count = scalar (map /^processor/, <$handle>); + close $handle; + return $count; + } + + #mac + if ($^O eq 'darwin') { + $count = `sysctl -n hw.logicalcpu`; + chomp($count); + if ($count eq $count+0 && $count > 0) { + return $count; + } + } + + return 1; +} + sub apply { my @fields = @{$argv{'fields'}}; my %fields_used = map { $_ => 1 } @fields; + + open(GIT_STORE_META_FILE, "<", $git_store_meta_file) or die; + chomp(my @lines = ); + close(GIT_STORE_META_FILE); # v1.0.0 ~ v2.0.* share same apply procedure # (files with a bad file name recorded in 1.0.* will be skipped) if (1.0.0 <= $cache_version && $cache_version < 2.1.0) { - my $count = 0; - open(GIT_STORE_META_FILE, "<", $git_store_meta_file) or die; - while () { - next if ++$count <= 2; # skip first 2 lines (header) - s/^\s+//; s/\s+$//; - next if $_ eq ""; - - # for each line, parse the record - my @rec = split("\t"); - my %data; - for (my $i=0; $i<=$#cache_fields; $i++) { - $data{$cache_fields[$i]} = $rec[$i]; + @lines = @lines[2 .. @lines]; + if ($argv{'multithread'}) { + my $numberofcores = get_max_thread_count(); + print "Max available thread count: $numberofcores...\n"; + my $count = @lines; + my $chunk_size = int($count/$numberofcores) + 1; + $chunk_size = 20 if ($chunk_size < 20); + while (@lines > $chunk_size) { + my @chunk = @lines[0 .. $chunk_size]; + @lines = @lines[$chunk_size .. @lines]; + threads->create(\&apply_chunk, \%fields_used, \@chunk); } + threads->create(\&apply_chunk, \%fields_used, \@lines) if (@lines > 0); + + #now we need to wait for all threads + foreach my $thr (threads->list()) { + $thr->join(); + } + } else { + apply_chunk(\%fields_used, \@lines); + } + } else { + die "error: `$git_store_meta_file' is using an unsupported version: $cache_version\n"; + } +} + +sub apply_chunk { + my ($ref_fields_used, $ref_lines) = (@_); + my %fields_used = %$ref_fields_used; + my @lines = @$ref_lines; + + foreach (@lines) { + next if (!defined($_)); + s/^\s+//; s/\s+$//; + next if $_ eq ""; - # check for existence and type - my $File = $data{'file'}; # escaped version, for printing - my $file = unescape_filename($File); # unescaped version, for using - next if $file eq $git_store_meta_filename; # skip data file - if (! -e $file && ! -l $file) { # -e tests symlink target instead of the symlink itself - warn "warn: `$File' does not exist, skip applying metadata\n"; + # for each line, parse the record + my @rec = split("\t"); + my %data; + for (my $i=0; $i<=$#cache_fields; $i++) { + $data{$cache_fields[$i]} = $rec[$i]; + } + + # check for existence and type + my $File = $data{'file'}; # escaped version, for printing + my $file = unescape_filename($File); # unescaped version, for using + next if $file eq $git_store_meta_filename; # skip data file + if (! -e $file && ! -l $file) { # -e tests symlink target instead of the symlink itself + warn "warn: `$File' does not exist, skip applying metadata\n"; + next; + } + my $type = $data{'type'}; + # a symbolic link could be checked out as a plain file, simply see them as equal + if ($type eq "f" || $type eq "l" ) { + if (! -f $file && ! -l $file) { + warn "warn: `$File' is not a file, skip applying metadata\n"; next; } - my $type = $data{'type'}; - # a symbolic link could be checked out as a plain file, simply see them as equal - if ($type eq "f" || $type eq "l" ) { - if (! -f $file && ! -l $file) { - warn "warn: `$File' is not a file, skip applying metadata\n"; - next; - } - } elsif ($type eq "d") { - if (! -d $file) { - warn "warn: `$File' is not a directory, skip applying metadata\n"; - next; - } - if (!$argv{'directory'}) { - next; - } - if ($file eq "." && !$argv{'topdir'}) { - next; - } - } else { - warn "warn: `$File' is recorded as an unknown type, skip applying metadata\n"; + } elsif ($type eq "d") { + if (! -d $file) { + warn "warn: `$File' is not a directory, skip applying metadata\n"; + next; + } + if (!$argv{'directory'}) { next; } + if ($file eq "." && !$argv{'topdir'}) { + next; + } + } else { + warn "warn: `$File' is recorded as an unknown type, skip applying metadata\n"; + next; + } - # apply metadata - my $check = 0; - set_user: { - if ($fields_used{'user'} && $data{'user'} ne "") { - my $uid = (getpwnam($data{'user'}))[2]; - my $gid = (lstat($file))[5]; - print "`$File' set user to '$data{'user'}'\n" if $argv{'verbose'}; - if (defined $uid) { - if (!$argv{'dry-run'}) { - if (! -l $file) { - $check = chown($uid, $gid, $file); - } else { - my $cmd = join(" ", ("chown", "-h", escapeshellarg($data{'user'}), escapeshellarg("./$file"), "2>&1")); - `$cmd`; $check = ($? == 0); - } - } else { - $check = 1; - } - warn "warn: `$File' cannot set user to '$data{'user'}'\n" if !$check; - last set_user if $check; - } else { - warn "warn: $data{'user'} is not a valid user.\n"; - } - } - if ($fields_used{'uid'} && $data{'uid'} ne "") { - my $uid = $data{'uid'}; - my $gid = (lstat($file))[5]; - print "`$File' set uid to '$uid'\n" if $argv{'verbose'}; + # apply metadata + my $check = 0; + set_user: { + if ($fields_used{'user'} && $data{'user'} ne "") { + my $uid = (getpwnam($data{'user'}))[2]; + my $gid = (lstat($file))[5]; + print "`$File' set user to '$data{'user'}'\n" if $argv{'verbose'}; + if (defined $uid) { if (!$argv{'dry-run'}) { if (! -l $file) { $check = chown($uid, $gid, $file); } else { - my $cmd = join(" ", ("chown", "-h", escapeshellarg($uid), escapeshellarg("./$file"), "2>&1")); + my $cmd = join(" ", ("chown", "-h", escapeshellarg($data{'user'}), escapeshellarg("./$file"), "2>&1")); `$cmd`; $check = ($? == 0); } } else { $check = 1; } - warn "warn: `$File' cannot set uid to '$uid'\n" if !$check; + warn "warn: `$File' cannot set user to '$data{'user'}'\n" if !$check; + last set_user if $check; + } else { + warn "warn: $data{'user'} is not a valid user.\n"; } } - set_group: { - if ($fields_used{'group'} && $data{'group'} ne "") { - my $uid = (lstat($file))[4]; - my $gid = (getgrnam($data{'group'}))[2]; - print "`$File' set group to '$data{'group'}'\n" if $argv{'verbose'}; - if (defined $gid) { - if (!$argv{'dry-run'}) { - if (! -l $file) { - $check = chown($uid, $gid, $file); - } else { - my $cmd = join(" ", ("chgrp", "-h", escapeshellarg($data{'group'}), escapeshellarg("./$file"), "2>&1")); - `$cmd`; $check = ($? == 0); - } - } else { - $check = 1; - } - warn "warn: `$File' cannot set group to '$data{'group'}'\n" if !$check; - last set_group if $check; + if ($fields_used{'uid'} && $data{'uid'} ne "") { + my $uid = $data{'uid'}; + my $gid = (lstat($file))[5]; + print "`$File' set uid to '$uid'\n" if $argv{'verbose'}; + if (!$argv{'dry-run'}) { + if (! -l $file) { + $check = chown($uid, $gid, $file); } else { - warn "warn: $data{'group'} is not a valid user group.\n"; + my $cmd = join(" ", ("chown", "-h", escapeshellarg($uid), escapeshellarg("./$file"), "2>&1")); + `$cmd`; $check = ($? == 0); } + } else { + $check = 1; } - if ($fields_used{'gid'} && $data{'gid'} ne "") { - my $uid = (lstat($file))[4]; - my $gid = $data{'gid'}; - print "`$File' set gid to '$gid'\n" if $argv{'verbose'}; + warn "warn: `$File' cannot set uid to '$uid'\n" if !$check; + } + } + set_group: { + if ($fields_used{'group'} && $data{'group'} ne "") { + my $uid = (lstat($file))[4]; + my $gid = (getgrnam($data{'group'}))[2]; + print "`$File' set group to '$data{'group'}'\n" if $argv{'verbose'}; + if (defined $gid) { if (!$argv{'dry-run'}) { if (! -l $file) { $check = chown($uid, $gid, $file); } else { - my $cmd = join(" ", ("chgrp", "-h", escapeshellarg($gid), escapeshellarg("./$file"), "2>&1")); + my $cmd = join(" ", ("chgrp", "-h", escapeshellarg($data{'group'}), escapeshellarg("./$file"), "2>&1")); `$cmd`; $check = ($? == 0); } } else { $check = 1; } - warn "warn: `$File' cannot set gid to '$gid'\n" if !$check; - } - } - if ($fields_used{'mode'} && $data{'mode'} ne "" && ! -l $file) { - my $mode = oct($data{'mode'}) & 07777; - print "`$File' set mode to '$data{'mode'}'\n" if $argv{'verbose'}; - $check = !$argv{'dry-run'} ? chmod($mode, $file) : 1; - warn "warn: `$File' cannot set mode to '$data{'mode'}'\n" if !$check; - } - if ($fields_used{'acl'} && $data{'acl'} ne "") { - print "`$File' set acl to '$data{'acl'}'\n" if $argv{'verbose'}; - if (!$argv{'dry-run'}) { - my $cmd = join(" ", ("setfacl", "-bm", escapeshellarg($data{'acl'}), escapeshellarg("./$file"), "2>&1")); - `$cmd`; $check = ($? == 0); + warn "warn: `$File' cannot set group to '$data{'group'}'\n" if !$check; + last set_group if $check; } else { - $check = 1; + warn "warn: $data{'group'} is not a valid user group.\n"; } - warn "warn: `$File' cannot set acl to '$data{'acl'}'\n" if !$check; } - if ($fields_used{'mtime'} && $data{'mtime'} ne "") { - my $mtime = gmtime_to_timestamp($data{'mtime'}); - my $atime = (lstat($file))[8]; - print "`$File' set mtime to '$data{'mtime'}'\n" if $argv{'verbose'}; + if ($fields_used{'gid'} && $data{'gid'} ne "") { + my $uid = (lstat($file))[4]; + my $gid = $data{'gid'}; + print "`$File' set gid to '$gid'\n" if $argv{'verbose'}; if (!$argv{'dry-run'}) { if (! -l $file) { - $check = utime($atime, $mtime, $file); + $check = chown($uid, $gid, $file); } else { - my $cmd = join(" ", ("touch", "-hcmd", escapeshellarg($data{'mtime'}), escapeshellarg("./$file"), "2>&1")); + my $cmd = join(" ", ("chgrp", "-h", escapeshellarg($gid), escapeshellarg("./$file"), "2>&1")); `$cmd`; $check = ($? == 0); } } else { $check = 1; } - warn "warn: `$File' cannot set mtime to '$data{'mtime'}'\n" if !$check; + warn "warn: `$File' cannot set gid to '$gid'\n" if !$check; } - if ($fields_used{'atime'} && $data{'atime'} ne "") { - my $mtime = (lstat($file))[9]; - my $atime = gmtime_to_timestamp($data{'atime'}); - print "`$File' set atime to '$data{'atime'}'\n" if $argv{'verbose'}; - if (!$argv{'dry-run'}) { - if (! -l $file) { - $check = utime($atime, $mtime, $file); - } else { - my $cmd = join(" ", ("touch", "-hcad", escapeshellarg($data{'atime'}), escapeshellarg("./$file"), "2>&1")); - `$cmd`; $check = ($? == 0); - } + } + if ($fields_used{'mode'} && $data{'mode'} ne "" && ! -l $file) { + my $mode = oct($data{'mode'}) & 07777; + print "`$File' set mode to '$data{'mode'}'\n" if $argv{'verbose'}; + $check = !$argv{'dry-run'} ? chmod($mode, $file) : 1; + warn "warn: `$File' cannot set mode to '$data{'mode'}'\n" if !$check; + } + if ($fields_used{'acl'} && $data{'acl'} ne "") { + print "`$File' set acl to '$data{'acl'}'\n" if $argv{'verbose'}; + if (!$argv{'dry-run'}) { + my $cmd = join(" ", ("setfacl", "-bm", escapeshellarg($data{'acl'}), escapeshellarg("./$file"), "2>&1")); + `$cmd`; $check = ($? == 0); + } else { + $check = 1; + } + warn "warn: `$File' cannot set acl to '$data{'acl'}'\n" if !$check; + } + if ($fields_used{'mtime'} && $data{'mtime'} ne "") { + my $mtime = gmtime_to_timestamp($data{'mtime'}); + my $atime = (lstat($file))[8]; + print "`$File' set mtime to '$data{'mtime'}'\n" if $argv{'verbose'}; + if (!$argv{'dry-run'}) { + if (! -l $file) { + $check = utime($atime, $mtime, $file); } else { - $check = 1; + my $cmd = join(" ", ("touch", "-hcmd", escapeshellarg($data{'mtime'}), escapeshellarg("./$file"), "2>&1")); + `$cmd`; $check = ($? == 0); } - warn "warn: `$File' cannot set atime to '$data{'atime'}'\n" if !$check; + } else { + $check = 1; } + warn "warn: `$File' cannot set mtime to '$data{'mtime'}'\n" if !$check; + } + if ($fields_used{'atime'} && $data{'atime'} ne "") { + my $mtime = (lstat($file))[9]; + my $atime = gmtime_to_timestamp($data{'atime'}); + print "`$File' set atime to '$data{'atime'}'\n" if $argv{'verbose'}; + if (!$argv{'dry-run'}) { + if (! -l $file) { + $check = utime($atime, $mtime, $file); + } else { + my $cmd = join(" ", ("touch", "-hcad", escapeshellarg($data{'atime'}), escapeshellarg("./$file"), "2>&1")); + `$cmd`; $check = ($? == 0); + } + } else { + $check = 1; + } + warn "warn: `$File' cannot set atime to '$data{'atime'}'\n" if !$check; } - close(GIT_STORE_META_FILE); - } else { - die "error: `$git_store_meta_file' is using an unsupported version: $cache_version\n"; } } From 87c31cf034ea4481098d6ede8bc7826c438ec343 Mon Sep 17 00:00:00 2001 From: Ernest Toth Date: Tue, 18 Feb 2020 11:57:05 +0100 Subject: [PATCH 2/2] Fixed a few things: 1) Removed info how to discover max. threads count 2) Added option to override deduced max. number of threads --- git-store-meta.pl | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/git-store-meta.pl b/git-store-meta.pl index ae76345..80ca89e 100755 --- a/git-store-meta.pl +++ b/git-store-meta.pl @@ -35,11 +35,11 @@ # -t|--target FILE Specify another filename to store metadata. Defaults to # ".git_store_meta" in the root of the working tree. # (available for: --store, --update, --apply, --install) -# -m|--multithread Use a multi-threading applying data, -# Number of threads is deduced from system -# NUMBER_OF_PROCESSORS on Win, '/proc/cpuinfo' on Linux, 'sysctl -n hw.logicalcpu' on Mac -# For custom threads count you can use env. variable GSM_THREAD_COUNT +# --mt Use a multi-threading applying data, max. count of threads +# is deduced from system # (available for: --apply, --install) +# --mt-count Use it to specify your own max. count of threads +# it must be used together with '--mt' option # # FIELDs is a comma-separated string consisting of the following values: # mtime last modified time @@ -119,7 +119,8 @@ "force" => 0, "dry-run" => 0, "verbose" => 0, - "multithread" => undef, + "mt" => undef, + "mt-count" => 0, ); GetOptions( "store|s" => \$argv{'store'}, @@ -135,7 +136,8 @@ "dry-run|n" => \$argv{'dry-run'}, "verbose|v" => \$argv{'verbose'}, "target|t=s" => \$argv{'target'}, - "multithread|m" => \$argv{'multithread'}, + "mt" => \$argv{'mt'}, + "mt-count=i" => \$argv{'mt-count'}, ); # determine action @@ -417,7 +419,8 @@ sub install_hooks { my $t; my $f = defined($argv{'target'}) ? " -t " . escapeshellarg($argv{'target'}) : ""; my $f2 = escapeshellarg(defined($argv{'target'}) ? $argv{'target'} : $GIT_STORE_META_FILENAME); - my $mt = defined($argv{'multithread'}) ? "-m " : ""; + my $mt_count = defined($argv{'mt-count'}) ? "--mt-count ".$argv{'mt-count'} : ""; + my $mt = defined($argv{'mt'}) ? "--m ".$mt_count." " : ""; $t = "$gitdir/hooks/pre-commit"; open(FILE, '>', $t) or die "error: failed to write to `$t': $!\n"; @@ -745,24 +748,23 @@ sub update { } sub get_max_thread_count { - my $count = $ENV{"GSM_THREAD_COUNT"}; - if (defined($count) && $count < 1) { - return 1; + if ($argv{'mt-count'}) { + my $count = $argv{'mt-count'}; + if ($count eq $count+0 && $count > 0) { + return $count; + } } - - #MS Windows - - $count = $ENV{"NUMBER_OF_PROCESSORS"}; + #MS Windows - + my $count = $ENV{"NUMBER_OF_PROCESSORS"}; if (defined($count) && $count > 0) { return $count; } - #linux if (open my $handle, "/proc/cpuinfo") { $count = scalar (map /^processor/, <$handle>); close $handle; return $count; } - #mac if ($^O eq 'darwin') { $count = `sysctl -n hw.logicalcpu`; @@ -771,14 +773,13 @@ sub get_max_thread_count { return $count; } } - return 1; } sub apply { my @fields = @{$argv{'fields'}}; my %fields_used = map { $_ => 1 } @fields; - + open(GIT_STORE_META_FILE, "<", $git_store_meta_file) or die; chomp(my @lines = ); close(GIT_STORE_META_FILE); @@ -787,7 +788,7 @@ sub apply { # (files with a bad file name recorded in 1.0.* will be skipped) if (1.0.0 <= $cache_version && $cache_version < 2.1.0) { @lines = @lines[2 .. @lines]; - if ($argv{'multithread'}) { + if ($argv{'mt'}) { my $numberofcores = get_max_thread_count(); print "Max available thread count: $numberofcores...\n"; my $count = @lines; @@ -799,14 +800,13 @@ sub apply { threads->create(\&apply_chunk, \%fields_used, \@chunk); } threads->create(\&apply_chunk, \%fields_used, \@lines) if (@lines > 0); - #now we need to wait for all threads foreach my $thr (threads->list()) { $thr->join(); } } else { apply_chunk(\%fields_used, \@lines); - } + } } else { die "error: `$git_store_meta_file' is using an unsupported version: $cache_version\n"; } @@ -816,7 +816,6 @@ sub apply_chunk { my ($ref_fields_used, $ref_lines) = (@_); my %fields_used = %$ref_fields_used; my @lines = @$ref_lines; - foreach (@lines) { next if (!defined($_)); s/^\s+//; s/\s+$//;