diff --git a/.idea/php.xml b/.idea/php.xml index 4f6c9ac..6a31454 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -15,9 +15,7 @@ - - diff --git a/README.md b/README.md index f47d235..e296c45 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,173 @@ One of the biggest advantages of using something like ConfigLib is that it will allow for more complicated software to be configured more easily by following the documented instructions on how to alter configuration files, optionally you could use a builtin editor to edit the configuration -file manually. \ No newline at end of file +file manually. + + +## Table of contents + + +* [ConfigLib](#configlib) + * [Table of contents](#table-of-contents) + * [Installation](#installation) + * [Compile from source](#compile-from-source) + * [Requirements](#requirements) + * [Documentation](#documentation) + * [Storage Location](#storage-location) + * [Creating a new configuration file](#creating-a-new-configuration-file) + * [Setting default values](#setting-default-values) + * [Command-line usage](#command-line-usage) + * [Editing a configuration file](#editing-a-configuration-file) + * [Using an external editor](#using-an-external-editor) + * [Inline command line editor](#inline-command-line-editor) + * [License](#license) + + +## Installation + +The library can be installed using ncc: + +```bash +ncc install -p "nosial/libs.config=latest@n64" +``` + +or by adding the following to your project.json file under the `build.dependencies` section: +```json +{ + "name": "net.nosial.configlib", + "version": "latest", + "source_type": "remote", + "source": "nosial/libs.config=latest@n64" +} +``` + +If you don't have the n64 source configured, you can add it by running the following command: +```bash +ncc source add --name n64 --type gitlab --host git.n64.cc +``` + +## Compile from source + +To compile the library from source, you need to have [ncc](https://git.n64.cc/nosial/ncc) installed, then run the +following command: + +```bash +ncc build +``` + +## Requirements + +The library requires PHP 8.0 or higher. + +## Documentation + +ConfigLib is both a library and a command line tool, the library can be used within your program to create a new +configuration file and load in default entries, either on the first run or automatically during the installation +process. + +The goal to ConfigLib is to make it easy to setup configuration parameters for your program and to make it easy +for a user to edit the configuration file without having to manually edit the file. This part of the documentation +will explain both how to implement the library into your program and how to use the command line tool to edit the +configuration file. + + +### Storage Location + +Configuration files are stored as json files in the data directory of ConfigLib which is located at +`/var/ncc/data/net.nosial.configlib`, this directory is created automatically when the library is installed. + +### Creating a new configuration file + +To create a new configuration file, you can create a new `\ConfigLib\Configuration()` object and pass in the +name of the configuration file, for example: + +```php +require 'ncc'; +import('net.nosial.configlib'); + +$config = new \ConfigLib\Configuration('myconfig'); +``` + +This will only initialize the object, to save the configuration file you need to call the `save()` method: + +```php +$config->save(); +``` + +### Setting default values + +You can set default values for the configuration file which will be created if the values do not exist in the +configuration file, + +```php +require 'ncc'; +import('net.nosial.configlib'); + +$config = new \ConfigLib\Configuration('com.symfony.yaml'); + +$config->setDefault('database.host', '127.0.0.1'); +$config->setDefault('database.port', 3306); +$config->setDefault('database.username', 'root'); +$config->setDefault('database.password', null); +$config->setDefault('database.name', 'test'); + +$config->save(); +``` + +## Command-line usage + +The command line interface can be executed by running `configlib` from the command line or by running +`ncc exec --package="net.nosial.configlib` if `configlib` isn't in your global path. + +For the rest of this documentation, we will assume that you have the `configlib` command in your global path. + +### Editing a configuration file + +There are two ways to edit a configuration file using ConfigLib + + 1. Using an external editor + 2. Inline command line editor + +#### Using an external editor + +When you use an external editor, ConfigLib will create a temporary YAML file and open it in the specified editor, +when you save and close the file, ConfigLib will parse the YAML file and save the configuration file. If the YAML +file is invalid, ConfigLib will not save the configuration file. + +This is the recommended way to edit configuration files as it allows you to use your preferred editor and it +allows you to use the full power of YAML. + +To edit a configuration file using an external editor, run the following command: + +```bash +configlib --config --editor nano +``` + + > Note: Changes will only be applied if you save the file and close the editor. + +#### Inline command line editor + +The inline command line editor is a simple editor that allows you to edit the configuration file from the command +line, this is useful for automated scripts. + +To view the contents of a configuration file, run the following command: + +```bash +configlib --config +``` + +To view the value of a specific property, use the `--property` option: + +```bash +configlib --config --property database.username +``` + +To edit a property, specify both the `--property` and `--value` options: + +```bash +configlib --config --property database.username --value root +``` + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details \ No newline at end of file diff --git a/project.json b/project.json index 9f49a20..9fffe1f 100644 --- a/project.json +++ b/project.json @@ -42,6 +42,9 @@ "source_path": "src", "default_configuration": "release", "main": "main", + "define_constants": { + "version": "%ASSEMBLY.VERSION%" + }, "dependencies": [ { "name": "net.nosial.optslib", diff --git a/src/ConfigLib/Configuration.php b/src/ConfigLib/Configuration.php index a6bda76..0f0c285 100644 --- a/src/ConfigLib/Configuration.php +++ b/src/ConfigLib/Configuration.php @@ -10,6 +10,7 @@ use RuntimeException; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; + use Symfony\Component\Yaml\Yaml; class Configuration { @@ -57,7 +58,6 @@ // Figure out the path to the configuration file try { - /** @noinspection PhpUndefinedClassInspection */ $this->Path = Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . $name . '.conf'; } catch (Exception $e) @@ -232,8 +232,6 @@ { if(!self::validateKey($key)) return false; - if(!isset($this->Configuration[$key])) - return false; $path = explode('.', $key); $current = $this->Configuration; @@ -281,6 +279,7 @@ try { $fs->dumpFile($this->Path, $json); + $fs->chmod($this->Path, 0777); } catch (IOException $e) { @@ -356,6 +355,11 @@ return $this->Configuration; } + public function toYaml(): string + { + return Yaml::dump($this->Configuration, 4, 2); + } + /** * Public Destructor */ @@ -373,4 +377,38 @@ } } } + + /** + * Imports a YAML file into the configuration + * + * @param string $path + * @return void + * @throws Exception + */ + public function import(string $path) + { + $fs = new Filesystem(); + + if(!$fs->exists($path)) + throw new Exception(sprintf('Unable to import configuration file "%s", file does not exist', $path)); + + $yaml = file_get_contents($path); + $data = Yaml::parse($yaml); + + $this->Configuration = array_replace_recursive($this->Configuration, $data); + $this->Modified = true; + } + + /** + * Exports the configuration to a YAML file + * + * @param string $path + * @return void + */ + public function export(string $path) + { + $fs = new Filesystem(); + $fs->dumpFile($path, $this->toYaml()); + $fs->chmod($path, 0777); + } } \ No newline at end of file diff --git a/src/ConfigLib/Program.php b/src/ConfigLib/Program.php index 843ab73..6884b5a 100644 --- a/src/ConfigLib/Program.php +++ b/src/ConfigLib/Program.php @@ -3,9 +3,17 @@ namespace ConfigLib; use Exception; + use JetBrains\PhpStorm\NoReturn; + use ncc\Exceptions\InvalidPackageNameException; + use ncc\Exceptions\InvalidScopeException; + use ncc\Exceptions\PackageLockException; + use ncc\Exceptions\PackageNotFoundException; + use ncc\Runtime; use OptsLib\Parse; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Process\Process; + use Symfony\Component\Yaml\Exception\ParseException; + use Symfony\Component\Yaml\Yaml; class Program { @@ -14,22 +22,21 @@ * * @return void */ - public static function main(): void + #[NoReturn] public static function main(): void { $args = Parse::getArguments(); if(isset($args['help']) || isset($args['h'])) self::help(); - if(isset($args['version']) || isset($args['v'])) - self::version(); - - if(isset($args['name']) || isset($args['n'])) + if(isset($args['conf']) || isset($args['config'])) { - $configuration_name = $args['name'] ?? $args['n'] ?? null; - $property = $args['property'] ?? $args['p'] ?? null; - $value = $args['value'] ?? $args['v'] ?? null; - $editor = $args['editor'] ?? $args['e'] ?? null; + $configuration_name = $args['conf'] ?? $args['config'] ?? null; + $property = $args['prop'] ?? $args['property'] ?? null; + $value = $args['val'] ?? $args['value'] ?? null; + $editor = $args['editor'] ?? @$args['e'] ?? null; + $export = $args['export'] ?? null; + $import = $args['import'] ?? null; if($configuration_name === null) { @@ -39,21 +46,71 @@ $configuration = new Configuration($configuration_name); + // Check if the configuration exists first. + if(!file_exists($configuration->getPath())) + { + print(sprintf('Configuration \'%s\' does not exist, aborting' . PHP_EOL, $configuration->getName())); + exit(1); + } + + if($import !== null) + { + try + { + $configuration->import((string)$import); + $configuration->save(); + } + catch (Exception $e) + { + print($e->getMessage() . PHP_EOL); + exit(1); + } + + print(sprintf('Configuration \'%s\' imported from \'%s\'' . PHP_EOL, $configuration->getName(), $import)); + exit(0); + } + + if($export !== null) + { + if(!is_string($export)) + $export = sprintf('%s.yml', $configuration->getName()); + + try + { + $configuration->export($export); + } + catch (Exception $e) + { + print($e->getMessage() . PHP_EOL); + exit(1); + } + + print(sprintf('Configuration \'%s\' exported to \'%s\'' . PHP_EOL, $configuration->getName(), $export)); + exit(0); + } + if($editor !== null) { - self::edit($args); - return; + try + { + self::edit($args, $configuration); + } + catch(Exception $e) + { + print($e->getMessage() . PHP_EOL); + exit(1); + } } if($property === null) { - print(json_encode($configuration->getConfiguration(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL); + print($configuration->toYaml() . PHP_EOL); } else { if($value === null) { - print(json_encode($configuration->get($property, '(not set)'), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL); + print(Yaml::dump($configuration->get($property), 4, 2) . PHP_EOL); return; } @@ -81,25 +138,27 @@ * * @return void */ - private static function help(): void + #[NoReturn] private static function help(): void { + print('ConfigLib v' . Runtime::getConstant('net.nosial.configlib', 'version') . PHP_EOL . PHP_EOL); + print('Usage: configlib [options]' . PHP_EOL); - print(' -h, --help Displays the help menu' . PHP_EOL); - print(' -v, --version Displays the version of the program' . PHP_EOL); - print(' -n, --name The name of the configuration' . PHP_EOL); - print(' -p, --path The property name to select/read (eg; foo.bar.baz) (Inline)' . PHP_EOL); - print(' -v, --value The value to set the property (Inline)' . PHP_EOL); - print(' -e, --editor (Optional) The editor to use (eg; nano, vim, notepad) (External)' . PHP_EOL); - print(' --nc (Optional) Disables type casting (eg; \'true\' > True) will always be a string' . PHP_EOL); - print(' --export (Optional) Exports the configuration to a file' . PHP_EOL); - print(' --import (Optional) Imports the configuration from a file' . PHP_EOL); - print('Examples:' . PHP_EOL); - print(' configlib -n com.example.package' . PHP_EOL); - print(' configlib -n com.example.package -e nano' . PHP_EOL); - print(' configlib -n com.example.package -p foo.bar.baz -v 123' . PHP_EOL); - print(' configlib -n com.example.package -p foo.bar.baz -v 123 --nc' . PHP_EOL); - print(' configlib -n com.example.package --export config.json' . PHP_EOL); - print(' configlib -n com.example.package --import config.json' . PHP_EOL); + print(' -h, --help Displays the help menu' . PHP_EOL); + print(' --conf, --config The name of the configuration' . PHP_EOL); + print(' --prop, --property The property name to select/read (eg; foo.bar.baz) (Inline)' . PHP_EOL); + print(' --val, --value The value to set the property (Inline)' . PHP_EOL); + print(' -e, --editor (Optional) The editor to use (eg; nano, vim, notepad) (External)' . PHP_EOL); + print(' --export (Optional) Exports the configuration to a file' . PHP_EOL); + print(' --import (Optional) Imports the configuration from a file' . PHP_EOL); + print(' --nc (Optional) Disables type casting (eg; \'true\' > True) will always be a string' . PHP_EOL); + + print('Examples:' . PHP_EOL . PHP_EOL); + print(' configlib --conf test View the configuration' . PHP_EOL); + print(' configlib --conf test --prop foo View a specific property' . PHP_EOL); + print(' configlib --conf test --prop foo --val bar Set a specific property' . PHP_EOL); + print(' configlib --conf test --editor nano Edit the configuration' . PHP_EOL); + print(' configlib --conf test --export out.json Export the configuration' . PHP_EOL); + print(' configlib --conf test --import in.json Import a configuration' . PHP_EOL); exit(0); } @@ -108,16 +167,19 @@ * Edits an existing configuration file or creates a new one if it doesn't exist * * @param array $args + * @param Configuration $configuration * @return void + * @throws InvalidPackageNameException + * @throws InvalidScopeException + * @throws PackageLockException + * @throws PackageNotFoundException */ - private static function edit(array $args): void + #[NoReturn] static function edit(array $args, Configuration $configuration): void { $editor = $args['editor'] ?? 'vi'; if(isset($args['e'])) $editor = $args['e']; - $name = $args['name'] ?? 'default'; - if($editor == null) { print('No editor specified' . PHP_EOL); @@ -125,21 +187,25 @@ } // Determine the temporary path to use - $tempPath = null; - - if(function_exists('ini_get')) + if(file_exists(DIRECTORY_SEPARATOR . 'tmp')) { - $tempPath = ini_get('upload_tmp_dir'); - if($tempPath == null) - $tempPath = ini_get('session.save_path'); - if($tempPath == null) - $tempPath = ini_get('upload_tmp_dir'); - if($tempPath == null) - $tempPath = sys_get_temp_dir(); + $tempPath = DIRECTORY_SEPARATOR . 'tmp'; } + else + { + if(!file_exists(Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . 'tmp')) + { + mkdir(Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . 'tmp', 0777, true); - if($tempPath == null && function_exists('sys_get_temp_dir')) - $tempPath = sys_get_temp_dir(); + if(!file_exists(Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . 'tmp')) + { + print('Unable to create the temporary path to use' . PHP_EOL); + exit(1); + } + } + + $tempPath = Runtime::getDataPath('net.nosial.configlib') . DIRECTORY_SEPARATOR . 'tmp'; + } if($tempPath == null) { @@ -147,26 +213,16 @@ exit(1); } - // Prepare the temporary file - - try - { - $configuration = new Configuration($name); - } - catch (Exception $e) - { - print($e->getMessage() . PHP_EOL); - exit(1); - } - $fs = new Filesystem(); - $tempFile = $tempPath . DIRECTORY_SEPARATOR . $name . '.conf'; - $fs->copy($configuration->getPath(), $tempFile); + + // Convert the configuration from JSON to YAML for editing purposes + $tempFile = $tempPath . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16)) . '.yaml'; + $fs->dumpFile($tempFile, $configuration->toYaml()); $original_hash = hash_file('sha1', $tempFile); - // Open the editor try { + // Open the editor $process = new Process([$editor, $tempFile]); $process->setTimeout(0); $process->setTty(true); @@ -177,10 +233,6 @@ print('Unable to open the editor, ' . $e->getMessage() . PHP_EOL); exit(1); } - finally - { - $fs->remove($tempFile); - } // Check if the file has changed and if so, update the configuration if($fs->exists($tempFile)) @@ -188,26 +240,29 @@ $new_hash = hash_file('sha1', $tempFile); if($original_hash != $new_hash) { - $fs->copy($tempFile, $configuration->getPath()); - } - else - { - print('No changes detected' . PHP_EOL); - } + // Convert the YAML back to JSON + $yaml = file_get_contents($tempFile); - $fs->remove($tempFile); + try + { + $json = Yaml::parse($yaml); + } + catch (ParseException $e) + { + print('Unable to parse the YAML file, ' . $e->getMessage() . PHP_EOL); + exit(1); + } + + $path = $configuration->getPath(); + $fs->dumpFile($path, json_encode($json, JSON_PRETTY_PRINT)); + print('Configuration updated' . PHP_EOL); + } } - } + // Remove the temporary file + if($fs->exists($tempFile)) + $fs->remove($tempFile); - /** - * Prints out the version of the program - * - * @return void - */ - private static function version(): void - { - print('ConfigLib v1.0.0' . PHP_EOL); exit(0); } } \ No newline at end of file diff --git a/tests/default.php b/tests/default.php index a1db638..3629d94 100644 --- a/tests/default.php +++ b/tests/default.php @@ -3,10 +3,12 @@ require 'ncc'; import('net.nosial.configlib'); - $config = new \ConfigLib\Configuration('test'); + $config = new \ConfigLib\Configuration('com.symfony.yaml'); $config->setDefault('database.host', '127.0.0.1'); $config->setDefault('database.port', 3306); $config->setDefault('database.username', 'root'); $config->setDefault('database.password', null); - $config->setDefault('database.name', 'test'); \ No newline at end of file + $config->setDefault('database.name', 'test'); + + $config->save(); \ No newline at end of file diff --git a/tests/edit_test.php b/tests/edit_test.php new file mode 100644 index 0000000..4abcd55 --- /dev/null +++ b/tests/edit_test.php @@ -0,0 +1,11 @@ +set('database.host', '192.168.1.1'); + $config->set('database.username', 'super_root'); + + $config->save(); \ No newline at end of file