15 Commits

Author SHA1 Message Date
e29f9fcd88 Add another psr-4 prefix, update composer packages
Previously it was just "gullevek\dotEnv" but now also "gullevek\dotenv"
will work.

Update phan/phpstan composer dev requirement verisons
2023-03-03 09:32:02 +09:00
ae0eb1f939 Convert phan config to PSR12 2023-01-19 12:45:21 +09:00
c608201de1 Switch to standard PSR-12 with spaces instead of tab 2023-01-19 12:38:37 +09:00
8e062ff114 Add comment to unit test skip condition 2023-01-12 15:07:12 +09:00
225e3e7929 Unit test fixes with permissions
- in case the unit test is run as root, skip test for cannot read (0000)
- set read (0664) for all must read files
- write .env file int all folders for test so that __DIR__ base will
  always find one
2023-01-12 11:42:15 +09:00
44bcb39e51 Delete .gitlab-ci.yml 2023-01-11 08:53:12 +00:00
5729a0c977 Update .gitlab-ci.yml file 2023-01-11 08:49:23 +00:00
9af7790b61 Fix Readme file 2023-01-11 17:15:37 +09:00
0579a075dc Remove bitbucket pipeline, update readme with test tools 2023-01-11 17:14:47 +09:00
58a6d994ca bitbucket pipeline test 2023-01-11 17:06:24 +09:00
233f9fbf81 Dev install phpstan, phan and phplint 2023-01-11 17:00:57 +09:00
8ae06efe4e Remove symlinked .env files 2023-01-11 16:48:28 +09:00
4079d7c66e Merge bitbucket.org:egplusww/code-blocks-dotenv 2023-01-11 16:31:30 +09:00
dd2274f3b1 Auto create .env files 2023-01-11 16:30:51 +09:00
5742314581 Initial Bitbucket Pipelines configuration 2023-01-11 06:46:28 +00:00
10 changed files with 391 additions and 304 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
vendor vendor
.phpunit.result.cache .phpunit.result.cache
.phplint-cache
composer.lock composer.lock

View File

@@ -26,72 +26,72 @@
// use Phan\Config; // use Phan\Config;
return [ return [
// If true, missing properties will be created when // If true, missing properties will be created when
// they are first seen. If false, we'll report an // they are first seen. If false, we'll report an
// error message. // error message.
"allow_missing_properties" => false, "allow_missing_properties" => false,
// Allow null to be cast as any type and for any // Allow null to be cast as any type and for any
// type to be cast to null. // type to be cast to null.
"null_casts_as_any_type" => false, "null_casts_as_any_type" => false,
// Backwards Compatibility Checking // Backwards Compatibility Checking
'backward_compatibility_checks' => true, 'backward_compatibility_checks' => true,
// Run a quick version of checks that takes less // Run a quick version of checks that takes less
// time // time
"quick_mode" => false, "quick_mode" => false,
// Only emit critical issues to start with // Only emit critical issues to start with
// (0 is low severity, 5 is normal severity, 10 is critical) // (0 is low severity, 5 is normal severity, 10 is critical)
"minimum_severity" => 10, "minimum_severity" => 10,
// default false for include path check // default false for include path check
"enable_include_path_checks" => true, "enable_include_path_checks" => true,
"include_paths" => [ "include_paths" => [
], ],
'ignore_undeclared_variables_in_global_scope' => true, 'ignore_undeclared_variables_in_global_scope' => true,
"file_list" => [ "file_list" => [
], ],
// A list of directories that should be parsed for class and // A list of directories that should be parsed for class and
// method information. After excluding the directories // method information. After excluding the directories
// defined in exclude_analysis_directory_list, the remaining // defined in exclude_analysis_directory_list, the remaining
// files will be statically analyzed for errors. // files will be statically analyzed for errors.
// //
// Thus, both first-party and third-party code being used by // Thus, both first-party and third-party code being used by
// your application should be included in this list. // your application should be included in this list.
'directory_list' => [ 'directory_list' => [
// Change this to include the folders you wish to analyze // Change this to include the folders you wish to analyze
// (and the folders of their dependencies) // (and the folders of their dependencies)
'.' '.'
// 'www', // 'www',
// To speed up analysis, we recommend going back later and // To speed up analysis, we recommend going back later and
// limiting this to only the vendor/ subdirectories your // limiting this to only the vendor/ subdirectories your
// project depends on. // project depends on.
// `phan --init` will generate a list of folders for you // `phan --init` will generate a list of folders for you
], ],
// A list of directories holding code that we want // A list of directories holding code that we want
// to parse, but not analyze // to parse, but not analyze
"exclude_analysis_directory_list" => [ "exclude_analysis_directory_list" => [
'vendor', 'vendor',
'test' 'test'
], ],
'exclude_file_list' => [ 'exclude_file_list' => [
], ],
// what not to show as problem // what not to show as problem
'suppress_issue_types' => [ 'suppress_issue_types' => [
// 'PhanUndeclaredMethod', // 'PhanUndeclaredMethod',
'PhanEmptyFile', 'PhanEmptyFile',
], ],
// Override to hardcode existence and types of (non-builtin) globals in the global scope. // Override to hardcode existence and types of (non-builtin) globals in the global scope.
// Class names should be prefixed with `\`. // Class names should be prefixed with `\`.
// //
// (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`) // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
'globals_type_map' => [], 'globals_type_map' => [],
]; ];

View File

@@ -60,3 +60,19 @@ ESCAPE="String \" inside \" other "
DOUBLE="I will be used" DOUBLE="I will be used"
DOUBLE="This will be ignored" DOUBLE="This will be ignored"
``` ```
## Development
### Phan
`vendor/bin/phan --analyze-twice`
### PHPstan
`vendor/bin/phpstan`
### PHPUnit
Unit tests have to be run from base folder with
`vendor/bin/phpunit test/phpUnitTests/`

View File

@@ -6,7 +6,8 @@
"license": "MIT", "license": "MIT",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"gullevek\\dotEnv\\": "src/" "gullevek\\dotEnv\\": "src/",
"gullevek\\dotenv\\": "src/"
} }
}, },
"authors": [ "authors": [
@@ -25,6 +26,8 @@
"exclude": ["/test/", "/test/*", "/phpstan.neon", "/psalm.xml", "/.phan/", "/.vscode/", "/phpunit.xml"] "exclude": ["/test/", "/test/*", "/phpstan.neon", "/psalm.xml", "/.phan/", "/.vscode/", "/phpunit.xml"]
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^9" "phpunit/phpunit": "^9",
"phpstan/phpstan": "^1.10",
"phan/phan": "^5.4"
} }
} }

View File

@@ -6,105 +6,105 @@ namespace gullevek\dotEnv;
class DotEnv class DotEnv
{ {
/** @var string constant comment char, set to # */ /** @var string constant comment char, set to # */
private const COMMENT_CHAR = '#'; private const COMMENT_CHAR = '#';
/** /**
* parses .env file * parses .env file
* *
* Rules for .env file * Rules for .env file
* variable is any alphanumeric string followed by = on the same line * variable is any alphanumeric string followed by = on the same line
* content starts with the first non space part * content starts with the first non space part
* strings can be contained in " * strings can be contained in "
* strings MUST be contained in " if they are multiline * strings MUST be contained in " if they are multiline
* if string starts with " it will match until another " is found * if string starts with " it will match until another " is found
* anything AFTER " is ignored * anything AFTER " is ignored
* if there are two variables with the same name only the first is used * if there are two variables with the same name only the first is used
* variables are case sensitive * variables are case sensitive
* *
* @param string $path Folder to file, default is __DIR__ * @param string $path Folder to file, default is __DIR__
* @param string $env_file What file to load, default is .env * @param string $env_file What file to load, default is .env
* @return int -1 other error * @return int -1 other error
* 0 for success full load * 0 for success full load
* 1 for file loadable, no data or data already loaded * 1 for file loadable, no data or data already loaded
* 2 for file not readable or open failed * 2 for file not readable or open failed
* 3 for file not found * 3 for file not found
*/ */
public static function readEnvFile( public static function readEnvFile(
string $path = __DIR__, string $path = __DIR__,
string $env_file = '.env' string $env_file = '.env'
): int { ): int {
// default -1; // default -1;
$status = -1; $status = -1;
$env_file_target = $path . DIRECTORY_SEPARATOR . $env_file; $env_file_target = $path . DIRECTORY_SEPARATOR . $env_file;
// this is not a file -> abort // this is not a file -> abort
if (!is_file($env_file_target)) { if (!is_file($env_file_target)) {
$status = 3; $status = 3;
return $status; return $status;
} }
// cannot open file -> abort // cannot open file -> abort
if (!is_readable($env_file_target)) { if (!is_readable($env_file_target)) {
$status = 2; $status = 2;
return $status; return $status;
} }
// open file // open file
if (($fp = fopen($env_file_target, 'r')) === false) { if (($fp = fopen($env_file_target, 'r')) === false) {
$status = 2; $status = 2;
return $status; return $status;
} }
// set to readable but not yet any data loaded // set to readable but not yet any data loaded
$status = 1; $status = 1;
$block = false; $block = false;
$var = ''; $var = '';
while ($line = fgets($fp)) { while ($line = fgets($fp)) {
// main match for variable = value part // main match for variable = value part
if (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) { if (preg_match("/^\s*([\w_.]+)\s*=\s*((\"?).*)/", $line, $matches)) {
$var = $matches[1]; $var = $matches[1];
$value = $matches[2]; $value = $matches[2];
$quotes = $matches[3]; $quotes = $matches[3];
// write only if env is not set yet, and write only the first time // write only if env is not set yet, and write only the first time
if (empty($_ENV[$var])) { if (empty($_ENV[$var])) {
if (!empty($quotes)) { if (!empty($quotes)) {
// match greedy for first to last so we move any " if there are // match greedy for first to last so we move any " if there are
if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) { if (preg_match('/^"(.*[^\\\])"/U', $value, $matches)) {
$value = $matches[1]; $value = $matches[1];
} else { } else {
// this is a multi line // this is a multi line
$block = true; $block = true;
// first " in string remove // first " in string remove
// add removed new line back because this is a multi line // add removed new line back because this is a multi line
$value = ltrim($value, '"') . PHP_EOL; $value = ltrim($value, '"') . PHP_EOL;
} }
} else { } else {
// strip any quotes at end for unquoted single line // strip any quotes at end for unquoted single line
// an right hand spaces are removed too // an right hand spaces are removed too
$value = false !== ($pos = strpos($value, self::COMMENT_CHAR)) ? $value = false !== ($pos = strpos($value, self::COMMENT_CHAR)) ?
rtrim(substr($value, 0, $pos)) : $value; rtrim(substr($value, 0, $pos)) : $value;
} }
// if block is set, we strip line of slashes // if block is set, we strip line of slashes
$_ENV[$var] = $block === true ? stripslashes($value) : $value; $_ENV[$var] = $block === true ? stripslashes($value) : $value;
// set successful load // set successful load
$status = 0; $status = 0;
} }
} elseif ($block === true) { } elseif ($block === true) {
// read line until there is a unescaped " // read line until there is a unescaped "
// this also strips everything after the last " // this also strips everything after the last "
if (preg_match("/(.*[^\\\])\"/", $line, $matches)) { if (preg_match("/(.*[^\\\])\"/", $line, $matches)) {
$block = false; $block = false;
// strip ending " and EVERYTHING that follows after that // strip ending " and EVERYTHING that follows after that
$line = $matches[1]; $line = $matches[1];
} }
// just be sure it is init before we fill // just be sure it is init before we fill
if (!isset($_ENV[$var])) { if (!isset($_ENV[$var])) {
$_ENV[$var] = ''; $_ENV[$var] = '';
} }
// strip line of slashes // strip line of slashes
$_ENV[$var] .= stripslashes($line); $_ENV[$var] .= stripslashes($line);
} }
} }
fclose($fp); fclose($fp);
return $status; return $status;
} }
} }
// __END__ // __END__

View File

@@ -1 +0,0 @@
phpUnitTests/dotenv/test.env

0
test/env/.gitignore vendored Normal file
View File

View File

@@ -13,149 +13,197 @@ use PHPUnit\Framework\TestCase;
*/ */
final class DotEnvTest extends TestCase final class DotEnvTest extends TestCase
{ {
/** /**
* Undocumented function * setup the .env files before test run
* *
* @return array * @return void
*/ */
public function envFileProvider(): array public static function setUpBeforeClass(): void
{ {
$dot_env_content = [ // create .env files
'SOMETHING' => 'A', $file_content = __DIR__ . DIRECTORY_SEPARATOR
'OTHER' => 'B IS B', . 'dotenv' . DIRECTORY_SEPARATOR
'Complex' => 'A B \"D is F', . 'test.env';
'HAS_SPACE' => 'ABC', // copy to all folder levels
'HAS_COMMENT_QUOTES_SPACE' => 'Comment at end with quotes and space', $env_files = [
'HAS_COMMENT_QUOTES_NO_SPACE' => 'Comment at end with quotes no space', __DIR__ . DIRECTORY_SEPARATOR
'HAS_COMMENT_NO_QUOTES_SPACE' => 'Comment at end no quotes and space', . 'dotenv' . DIRECTORY_SEPARATOR
'HAS_COMMENT_NO_QUOTES_NO_SPACE' => 'Comment at end no quotes no space', . '.env',
'COMMENT_IN_TEXT_QUOTES' => 'Foo bar # comment in here', __DIR__ . DIRECTORY_SEPARATOR
'FAILURE' => 'ABC', . '.env',
'SIMPLEBOX' => 'A B C', __DIR__ . DIRECTORY_SEPARATOR
'TITLE' => '1', . '..' . DIRECTORY_SEPARATOR
'FOO' => '1.2', . '.env',
'SOME.TEST' => 'Test Var', ];
'SOME.LIVE' => 'Live Var', // if not found, skip -> all will fail
'A_TEST1' => 'foo', if (is_file($file_content)) {
'A_TEST2' => '${TEST1:-bar}', foreach ($env_files as $env_file) {
'A_TEST3' => '${TEST4:-bar}', copy($file_content, $env_file);
'A_TEST5' => 'null', }
'A_TEST6' => '${TEST5-bar}', }
'A_TEST7' => '${TEST6:-bar}', }
'B_TEST1' => 'foo',
'B_TEST2' => '${TEST1:=bar}',
'B_TEST3' => '${TEST4:=bar}',
'B_TEST5' => 'null',
'B_TEST6' => '${TEST5=bar}',
'B_TEST7' => '${TEST6=bar}',
'Test' => 'A',
'TEST' => 'B',
'LINE' => "ABC\nDEF",
'OTHERLINE' => "ABC\nAF\"ASFASDF\nMORESHIT",
'SUPERLINE' => '',
'__FOO_BAR_1' => 'b',
'__FOOFOO' => 'f ',
123123 => 'number',
'EMPTY' => '',
];
// 0: folder relative to test folder, if unset __DIR__
// 1: file, if unset .env
// 2: status to be returned
// 3: _ENV file content to be set
// 4: override chmod as octect in string
return [
'default' => [
'folder' => null,
'file' => null,
'status' => 3,
'content' => [],
'chmod' => null,
],
'cannot open file' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'cannot_read.env',
'status' => 2,
'content' => [],
'chmod' => '000',
],
'empty file' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'empty.env',
'status' => 1,
'content' => [],
'chmod' => null,
],
'override all' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'test.env',
'status' => 0,
'content' => $dot_env_content,
'chmod' => null,
],
'override directory' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => null,
'status' => 0,
'content' => $dot_env_content,
'chmod' => null,
],
];
}
/** /**
* test read .env file * Undocumented function
* *
* @covers ::readEnvFile * @return array
* @dataProvider envFileProvider */
* @testdox Read _ENV file from $folder / $file with expected status: $expected_status [$_dataName] public function envFileProvider(): array
* {
* @param string|null $folder $dot_env_content = [
* @param string|null $file 'SOMETHING' => 'A',
* @param int $expected_status 'OTHER' => 'B IS B',
* @param array $expected_env 'Complex' => 'A B \"D is F',
* @param string|null $chmod 'HAS_SPACE' => 'ABC',
* @return void 'HAS_COMMENT_QUOTES_SPACE' => 'Comment at end with quotes and space',
*/ 'HAS_COMMENT_QUOTES_NO_SPACE' => 'Comment at end with quotes no space',
public function testReadEnvFile( 'HAS_COMMENT_NO_QUOTES_SPACE' => 'Comment at end no quotes and space',
?string $folder, 'HAS_COMMENT_NO_QUOTES_NO_SPACE' => 'Comment at end no quotes no space',
?string $file, 'COMMENT_IN_TEXT_QUOTES' => 'Foo bar # comment in here',
int $expected_status, 'FAILURE' => 'ABC',
array $expected_env, 'SIMPLEBOX' => 'A B C',
?string $chmod 'TITLE' => '1',
): void { 'FOO' => '1.2',
// if we have file + chmod set 'SOME.TEST' => 'Test Var',
$old_chmod = null; 'SOME.LIVE' => 'Live Var',
if ( 'A_TEST1' => 'foo',
is_file($folder . DIRECTORY_SEPARATOR . $file) && 'A_TEST2' => '${TEST1:-bar}',
!empty($chmod) 'A_TEST3' => '${TEST4:-bar}',
) { 'A_TEST5' => 'null',
// get the old permissions 'A_TEST6' => '${TEST5-bar}',
$old_chmod = fileperms($folder . DIRECTORY_SEPARATOR . $file); 'A_TEST7' => '${TEST6:-bar}',
chmod($folder . DIRECTORY_SEPARATOR . $file, octdec($chmod)); 'B_TEST1' => 'foo',
} 'B_TEST2' => '${TEST1:=bar}',
if ($folder !== null && $file !== null) { 'B_TEST3' => '${TEST4:=bar}',
$status = \gullevek\dotEnv\DotEnv::readEnvFile($folder, $file); 'B_TEST5' => 'null',
} elseif ($folder !== null) { 'B_TEST6' => '${TEST5=bar}',
$status = \gullevek\dotEnv\DotEnv::readEnvFile($folder); 'B_TEST7' => '${TEST6=bar}',
} else { 'Test' => 'A',
$status = \gullevek\dotEnv\DotEnv::readEnvFile(); 'TEST' => 'B',
} 'LINE' => "ABC\nDEF",
$this->assertEquals( 'OTHERLINE' => "ABC\nAF\"ASFASDF\nMORESHIT",
$status, 'SUPERLINE' => '',
$expected_status, '__FOO_BAR_1' => 'b',
'Assert returned status equal' '__FOOFOO' => 'f ',
); 123123 => 'number',
// now assert read data 'EMPTY' => '',
$this->assertEquals( ];
$_ENV, // 0: folder relative to test folder, if unset __DIR__
$expected_env, // 1: file, if unset .env
'Assert _ENV correct' // 2: status to be returned
); // 3: _ENV file content to be set
// if we have file and chmod unset // 4: override chmod as octect in string
if ($old_chmod !== null) { return [
chmod($folder . DIRECTORY_SEPARATOR . $file, $old_chmod); 'default' => [
} 'folder' => null,
} 'file' => null,
'status' => 3,
'content' => [],
'chmod' => null,
],
'cannot open file' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'cannot_read.env',
'status' => 2,
'content' => [],
// 0000
'chmod' => '100000',
],
'empty file' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'empty.env',
'status' => 1,
'content' => [],
// 0664
'chmod' => '100664',
],
'override all' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => 'test.env',
'status' => 0,
'content' => $dot_env_content,
// 0664
'chmod' => '100664',
],
'override directory' => [
'folder' => __DIR__ . DIRECTORY_SEPARATOR . 'dotenv',
'file' => null,
'status' => 0,
'content' => $dot_env_content,
'chmod' => null,
],
];
}
/**
* test read .env file
*
* @covers ::readEnvFile
* @dataProvider envFileProvider
* @testdox Read _ENV file from $folder / $file with expected status: $expected_status [$_dataName]
*
* @param string|null $folder
* @param string|null $file
* @param int $expected_status
* @param array $expected_env
* @param string|null $chmod
* @return void
*/
public function testReadEnvFile(
?string $folder,
?string $file,
int $expected_status,
array $expected_env,
?string $chmod
): void {
// skip if chmod is set to 10000 (000 no rights) if we are root
// as root there is no stop reading a file
if (
!empty($chmod) &&
$chmod == '100000' &&
getmyuid() == 0
) {
$this->markTestSkipped(
"Skip cannot read file test because run user is root"
);
return;
}
// reset $_ENV for clean compare
$_ENV = [];
// previous file perm
$old_chmod = null;
// if we have change permission for file
if (
is_file($folder . DIRECTORY_SEPARATOR . $file) &&
!empty($chmod)
) {
// get the old permissions
$old_chmod = fileperms($folder . DIRECTORY_SEPARATOR . $file);
chmod($folder . DIRECTORY_SEPARATOR . $file, octdec($chmod));
}
if ($folder !== null && $file !== null) {
$status = \gullevek\dotEnv\DotEnv::readEnvFile($folder, $file);
} elseif ($folder !== null) {
$status = \gullevek\dotEnv\DotEnv::readEnvFile($folder);
} else {
$status = \gullevek\dotEnv\DotEnv::readEnvFile();
}
$this->assertEquals(
$status,
$expected_status,
'Assert returned status equal'
);
// now assert read data
$this->assertEquals(
$_ENV,
$expected_env,
'Assert _ENV correct'
);
// if we have file and chmod unset
if ($old_chmod !== null) {
chmod($folder . DIRECTORY_SEPARATOR . $file, $old_chmod);
}
}
} }
// __END__ // __END__

View File

@@ -1 +0,0 @@
test.env

View File

@@ -6,12 +6,33 @@ $loader = require '../vendor/autoload.php';
$loader->addPsr4('gullevek\\', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src'); $loader->addPsr4('gullevek\\', __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'src');
use gullevek\dotEnv\DotEnv; use gullevek\dotEnv\DotEnv;
print "BASE: " . __DIR__ . "<br>"; // copy test file to .env file in env folder
print "ORIG: <pre>" . file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . '.env') . "</pre>"; $file_content = __DIR__ . DIRECTORY_SEPARATOR
. 'phpUnitTests' . DIRECTORY_SEPARATOR
. 'dotenv' . DIRECTORY_SEPARATOR
. 'test.env';
// env folder
$env_file = __DIR__ . DIRECTORY_SEPARATOR
. 'env' . DIRECTORY_SEPARATOR
. '.env';
if (!is_file($file_content)) {
die("Cannot read $file_content");
}
if (copy($file_content, $env_file) === false) {
die("Cannot copy $file_content to $env_file");
}
$status = DotEnv::readEnvFile(__DIR__); print "BASE: " . __DIR__ . "<br>";
print "ENV: " . $env_file . "<br>";
print "ORIG: <pre>" . file_get_contents($env_file) . "</pre>";
$status = DotEnv::readEnvFile(__DIR__ . DIRECTORY_SEPARATOR . 'env');
print "STATUS: " . (string)$status . "<br>"; print "STATUS: " . (string)$status . "<br>";
print "ENV: <pre>" . print_r($_ENV, true) . "</pre><br>"; print "ENV: <pre>" . print_r($_ENV, true) . "</pre><br>";
$status = gullevek\dotenv\DotEnv::readEnvFile(__DIR__ . DIRECTORY_SEPARATOR . 'env');
print "STATUS B: " . (string)$status . "<br>";
print "ENV B: <pre>" . print_r($_ENV, true) . "</pre><br>";
// __END__ // __END__