modular design v_0
authorFrantišek Kučera <franta-hg@frantovo.cz>
Wed, 16 Jan 2019 17:23:05 +0100
branchv_0
changeset 4 d44ed75822e7
parent 3 62eac7ab4cf4
child 5 ec661baf433a
modular design
nbproject/configurations.xml
src/AttributeFinder.h
src/CLIParser.h
src/CMakeLists.txt
src/Configuration.h
src/FileAttributeFinder.h
src/FilesystemCommand.h
src/RequestedField.h
src/XattrAttributeFinder.h
src/relpipe-in-filesystem.cpp
--- a/nbproject/configurations.xml	Sun Jan 13 16:44:11 2019 +0100
+++ b/nbproject/configurations.xml	Wed Jan 16 17:23:05 2019 +0100
@@ -42,6 +42,12 @@
   <logicalFolder name="root" displayName="root" projectFiles="true" kind="ROOT">
     <df root="." name="0">
       <df name="src">
+        <in>AttributeFinder.h</in>
+        <in>CLIParser.h</in>
+        <in>Configuration.h</in>
+        <in>FileAttributeFinder.h</in>
+        <in>RequestedField.h</in>
+        <in>XattrAttributeFinder.h</in>
         <in>relpipe-in-filesystem.cpp</in>
       </df>
     </df>
@@ -91,6 +97,18 @@
           <preBuildFirst>true</preBuildFirst>
         </preBuild>
       </makefileType>
+      <item path="src/AttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/CLIParser.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/Configuration.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/FileAttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/RequestedField.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/XattrAttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
       <item path="src/relpipe-in-filesystem.cpp" ex="false" tool="1" flavor2="0">
         <ccTool flags="0">
         </ccTool>
@@ -130,6 +148,18 @@
           <preBuildFirst>true</preBuildFirst>
         </preBuild>
       </makefileType>
+      <item path="src/AttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/CLIParser.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/Configuration.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/FileAttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/RequestedField.h" ex="false" tool="3" flavor2="0">
+      </item>
+      <item path="src/XattrAttributeFinder.h" ex="false" tool="3" flavor2="0">
+      </item>
       <item path="src/relpipe-in-filesystem.cpp" ex="false" tool="1" flavor2="0">
         <ccTool flags="0">
         </ccTool>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/AttributeFinder.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,97 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+#include <filesystem>
+
+#include <relpipe/writer/typedefs.h>
+#include <relpipe/writer/AttributeMetadata.h>
+#include <relpipe/writer/RelationalWriter.h>
+
+#include "RequestedField.h"
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+namespace fs = std::filesystem;
+using namespace relpipe::writer;
+
+class AttributeFinder {
+public:
+
+	/**
+	 * Single requested fields might generate multiple attributes in the relation.
+	 * But usually it is 1:1.
+	 * @param field requested field from the user (usually from CLI arguments)
+	 * @return attribute metadata to be used in the RelationalWriter.startRelation()
+	 */
+	virtual vector<AttributeMetadata> toMetadata(const RequestedField& field) = 0;
+
+	/**
+	 * Writing of the record for current file is starting.
+	 * Following writeField() calls are related to this file.
+	 * @param file
+	 */
+	virtual void startFile(const fs::path& file) = 0;
+
+	/**
+	 * Writing of the record for current file is finished. All resources linked to this file should be released.
+	 */
+	virtual void endFile() = 0;
+
+	/**
+	 * Writes field attribute(s). The attribute count must match with count of AttributeMetadata returned in toMetadata().
+	 * @param writer
+	 * @param field
+	 */
+	virtual void writeField(RelationalWriter* writer, const RequestedField& field) = 0;
+
+	/**
+	 * Writes empty attribute(s) in case of non-existent file or an error. 
+	 * The attribute count must match with count of AttributeMetadata returned in toMetadata().
+	 * @param writer
+	 * @param field
+	 */
+	virtual void writeEmptyField(RelationalWriter* writer, const RequestedField& field) {
+		// TODO: better handling of null values (when null values are supported by the format specification)
+		for (AttributeMetadata m : toMetadata(field)) {
+			switch (m.typeId) {
+				case TypeId::BOOLEAN:
+					writer->writeAttribute(L"false");
+					break;
+				case TypeId::INTEGER:
+					writer->writeAttribute(L"0");
+					break;
+				case TypeId::STRING:
+					writer->writeAttribute(L"");
+					break;
+				default:
+					throw RelpipeWriterException(L"Unsupported attribute type in writeEmptyField().");
+			}
+		}
+	}
+
+	virtual ~AttributeFinder() {
+	}
+};
+
+}
+}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/CLIParser.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,57 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+
+#include <relpipe/writer/typedefs.h>
+
+#include "Configuration.h"
+#include "FileAttributeFinder.h"
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+using namespace relpipe::writer;
+
+class CLIParser {
+public:
+
+	Configuration parse(const std::vector<string_t>& arguments) {
+		Configuration c;
+		// TODO: parse arguments
+		c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_PATH_ORIGINAL));
+		// c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_PATH_ABSOLUTE));
+		// c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_PATH_CANONICAL));
+		// c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_NAME));
+		c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_TYPE));
+		c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_SIZE));
+		c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_OWNER));
+		c.fields.push_back(RequestedField(RequestedField::GROUP_FILE, FileAttributeFinder::FIELD_GROUP));
+		c.fields.push_back(RequestedField(RequestedField::GROUP_XATTR, L"user.xdg.origin.url"));
+		return c;
+	}
+
+	virtual ~CLIParser() {
+	}
+};
+
+}
+}
+}
--- a/src/CMakeLists.txt	Sun Jan 13 16:44:11 2019 +0100
+++ b/src/CMakeLists.txt	Wed Jan 16 17:23:05 2019 +0100
@@ -1,5 +1,5 @@
 # Relational pipes
-# Copyright © 2018 František Kučera (Frantovo.cz, GlobalCode.info)
+# Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/Configuration.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,40 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+
+#include <relpipe/writer/typedefs.h>
+
+#include "RequestedField.h"
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+class Configuration {
+public:
+	std::vector<RequestedField> fields;
+
+	virtual ~Configuration() {
+	}
+};
+
+}
+}
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/FileAttributeFinder.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,144 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+#include <filesystem>
+
+#include <relpipe/writer/typedefs.h>
+#include <relpipe/writer/AttributeMetadata.h>
+#include <relpipe/writer/RelationalWriter.h>
+
+#include "RequestedField.h"
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+namespace fs = std::filesystem;
+using namespace relpipe::writer;
+
+class FileAttributeFinder : public AttributeFinder {
+private:
+	std::wstring_convert<codecvt_utf8<wchar_t>> convertor; // TODO: support also other encodings.
+
+	fs::path currentFile;
+	string_t currentOwner;
+	string_t currentGroup;
+
+	string_t getType(const fs::path& file) {
+		// TODO: Use whole words? (letters are compatible with find -type)
+		if (fs::is_regular_file(file)) return L"f";
+		else if (fs::is_symlink(file)) return L"l"; // symlinks to directories are both symlinks and directories
+		else if (fs::is_directory(file)) return L"d";
+		else if (fs::is_fifo(file)) return L"p";
+		else if (fs::is_socket(file)) return L"s";
+		else if (fs::is_block_file(file)) return L"b";
+		else if (fs::is_character_file(file)) return L"c";
+		else return L"o";
+	}
+
+	void fetchOwner(const fs::path& file, string_t& owner, string_t& group) {
+		// TODO: throw exception on error
+		// TODO: get user and group in C++ way?
+		struct stat info;
+		stat(file.c_str(), &info);
+		/**
+		 * The return value may point to a static area, and may  be
+		 * overwritten  by  subsequent calls to getpwent(3), getpw‐
+		 * nam(), or getpwuid().  (Do not pass the returned pointer
+		 * to free(3).)
+		 */
+		struct passwd* pw = getpwuid(info.st_uid);
+		struct group* gr = getgrgid(info.st_gid);
+		owner = convertor.from_bytes(pw->pw_name);
+		group = convertor.from_bytes(gr->gr_name);
+	}
+public:
+
+	static const string_t FIELD_PATH_ORIGINAL;
+	static const string_t FIELD_NAME;
+	static const string_t FIELD_SIZE;
+	static const string_t FIELD_PATH_ABSOLUTE;
+	static const string_t FIELD_PATH_CANONICAL;
+	static const string_t FIELD_TYPE;
+	static const string_t FIELD_OWNER;
+	static const string_t FIELD_GROUP;
+
+	virtual vector<AttributeMetadata> toMetadata(const RequestedField& field) override {
+		if (field.group == RequestedField::GROUP_FILE) {
+			if (field.name == FIELD_SIZE) return { AttributeMetadata{field.name, TypeId::INTEGER}};
+			else return { AttributeMetadata{field.name, TypeId::STRING}};
+		} else {
+			return {};
+		}
+	}
+
+	void startFile(const fs::path& file) override {
+		currentFile = file;
+	};
+
+	void endFile() override {
+		currentOwner.clear();
+		currentGroup.clear();
+	};
+
+	virtual void writeField(RelationalWriter* writer, const RequestedField& field) override {
+		if (field.group == RequestedField::GROUP_FILE) {
+			if (field.name == FIELD_NAME) {
+				writer->writeAttribute(currentFile.filename().wstring());
+			} else if (field.name == FIELD_PATH_ORIGINAL) {
+				writer->writeAttribute(currentFile.wstring());
+			} else if (field.name == FIELD_PATH_ABSOLUTE) {
+				writer->writeAttribute(fs::absolute(currentFile).wstring());
+			} else if (field.name == FIELD_PATH_CANONICAL) {
+				writer->writeAttribute(fs::canonical(currentFile).wstring());
+			} else if (field.name == FIELD_TYPE) {
+				writer->writeAttribute(getType(currentFile));
+			} else if (field.name == FIELD_SIZE) {
+				integer_t size = fs::is_regular_file(currentFile) ? fs::file_size(currentFile) : 0;
+				writer->writeAttribute(&size, typeid (size));
+			} else if (field.name == FIELD_OWNER) {
+				if (currentOwner.empty()) fetchOwner(currentFile, currentOwner, currentGroup);
+				writer->writeAttribute(currentOwner);
+			} else if (field.name == FIELD_GROUP) {
+				if (currentOwner.empty()) fetchOwner(currentFile, currentOwner, currentGroup);
+				writer->writeAttribute(currentGroup);
+			} else {
+				// TODO: should not happend; check supported attributes in toMetadata()?
+				writer->writeAttribute(L"");
+			}
+		}
+	}
+
+	virtual ~FileAttributeFinder() override {
+	}
+};
+
+const string_t FileAttributeFinder::FIELD_PATH_ORIGINAL = L"path";
+const string_t FileAttributeFinder::FIELD_NAME = L"name";
+const string_t FileAttributeFinder::FIELD_SIZE = L"size";
+const string_t FileAttributeFinder::FIELD_PATH_ABSOLUTE = L"path_absolute";
+const string_t FileAttributeFinder::FIELD_PATH_CANONICAL = L"path_canonical";
+const string_t FileAttributeFinder::FIELD_TYPE = L"type";
+const string_t FileAttributeFinder::FIELD_OWNER = L"owner";
+const string_t FileAttributeFinder::FIELD_GROUP = L"group";
+
+}
+}
+}
--- a/src/FilesystemCommand.h	Sun Jan 13 16:44:11 2019 +0100
+++ b/src/FilesystemCommand.h	Wed Jan 16 17:23:05 2019 +0100
@@ -1,6 +1,6 @@
 /**
  * Relational pipes
- * Copyright © 2018 František Kučera (Frantovo.cz, GlobalCode.info)
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,9 +19,10 @@
 
 #include <cstdlib>
 #include <iostream>
+#include <sstream>
 #include <string>
 #include <vector>
-#include <array>
+#include <map>
 #include <algorithm>
 #include <filesystem>
 
@@ -33,6 +34,11 @@
 
 #include <relpipe/writer/typedefs.h>
 
+#include "Configuration.h"
+#include "AttributeFinder.h"
+#include "FileAttributeFinder.h"
+#include "XattrAttributeFinder.h"
+
 namespace relpipe {
 namespace in {
 namespace filesystem {
@@ -41,8 +47,16 @@
 using namespace relpipe::writer;
 
 class FilesystemCommand {
+private:
 	std::wstring_convert<codecvt_utf8<wchar_t>> convertor; // TODO: support also other encodings.
 
+	FileAttributeFinder fileAttributeFinder;
+	XattrAttributeFinder xattrAttributeFinder;
+
+	std::map<string_t, AttributeFinder*> attributeFinders{
+		{RequestedField::GROUP_FILE, &fileAttributeFinder},
+		{RequestedField::GROUP_XATTR, &xattrAttributeFinder}};
+
 	void reset(std::stringstream& stream) {
 		stream.str("");
 		stream.clear();
@@ -56,81 +70,23 @@
 		return originalName.tellp();
 	}
 
-	string_t getType(const fs::path& file) {
-		// TODO: Use whole words? (letters are compatible with find -type)
-		if (fs::is_regular_file(file)) return L"f";
-		else if (fs::is_symlink(file)) return L"l"; // symlinks to directories are both symlinks and directories
-		else if (fs::is_directory(file)) return L"d";
-		else if (fs::is_fifo(file)) return L"p";
-		else if (fs::is_socket(file)) return L"s";
-		else if (fs::is_block_file(file)) return L"b";
-		else if (fs::is_character_file(file)) return L"c";
-		else return L"o";
-	}
-
-	void fetchOwner(const fs::path& file, string_t& owner, string_t& group) {
-		// TODO: throw exception on error
-		// TODO: get user and group in C++ way?
-		struct stat info;
-		stat(file.c_str(), &info);
-		/**
-		 * The return value may point to a static area, and may  be
-		 * overwritten  by  subsequent calls to getpwent(3), getpw‐
-		 * nam(), or getpwuid().  (Do not pass the returned pointer
-		 * to free(3).)
-		 */
-		struct passwd* pw = getpwuid(info.st_uid);
-		struct group* gr = getgrgid(info.st_gid);
-		owner = convertor.from_bytes(pw->pw_name);
-		group = convertor.from_bytes(gr->gr_name);
-	}
-
-	string_t getXattr(const fs::path& file, const string_t& attributeName) {
-		// TODO: review, test on multiple platforms
-		// TODO: improve documentation in xattr.h in glibc
-		// TODO: check XATTR_NAME_MAX in limits.h and if it is reasonably small on our system, allocate buffer for it instead of reading twice
-		// TODO: avoid race condition (another process might change the attribute between reading its length and its value)
-		ssize_t length = getxattr(file.c_str(), convertor.to_bytes(attributeName).c_str(), nullptr, 0);
-		if (length > 0) {
-			std::vector<char> buffer(length + 1);
-			getxattr(file.c_str(), convertor.to_bytes(attributeName).c_str(), buffer.data(), buffer.size());
-			return convertor.from_bytes(buffer.data());
-		} else {
-			return L"";
-		}
-	}
-
 public:
 
-	void process(std::istream& input, std::ostream& output) {
+	void process(std::istream& input, std::ostream& output, Configuration& configuration) {
 		std::shared_ptr<RelationalWriter> writer(Factory::create(output));
 
-		// TODO: redesign: parametrized from CLI + groups of information fetched together and used for multiple attributes
-		bool enableName = true;
-		bool enableExists = false;
-		bool enableNameAbsolute = false;
-		bool enableNameCanonical = false;
-		bool enableType = true;
-		bool enableSize = true;
-		bool enableOwner = true;
-		bool enableGroup = true;
-		bool enableXattrUrl = true; // just a demo
-
 		std::vector<AttributeMetadata> attributesMetadata;
-		if (enableName) attributesMetadata.push_back({L"name", TypeId::STRING});
-		if (enableExists) attributesMetadata.push_back({L"exists", TypeId::BOOLEAN});
-		if (enableNameAbsolute) attributesMetadata.push_back({L"name_absolute", TypeId::STRING});
-		if (enableNameCanonical) attributesMetadata.push_back({L"name_canonical", TypeId::STRING});
-		if (enableType) attributesMetadata.push_back({L"type", TypeId::STRING});
-		if (enableSize) attributesMetadata.push_back({L"size", TypeId::INTEGER});
-		if (enableOwner) attributesMetadata.push_back({L"owner", TypeId::STRING});
-		if (enableGroup) attributesMetadata.push_back({L"group", TypeId::STRING});
-		if (enableXattrUrl) attributesMetadata.push_back({L"xattr_url", TypeId::STRING});
+		for (RequestedField field : configuration.fields) {
+			AttributeFinder* finder = attributeFinders[field.group];
+			if (finder) for (AttributeMetadata m : finder->toMetadata(field)) attributesMetadata.push_back(m);
+			else throw RelpipeWriterException(L"Unsupported field group: " + field.group);
+		}
 
 		writer->startRelation(L"filesystem", attributesMetadata, true);
 
+
 		for (std::stringstream originalName; readNext(input, originalName); reset(originalName)) {
-			if (enableName) writer->writeAttribute(convertor.from_bytes(originalName.str()));
+
 			fs::path file(originalName.str());
 			bool exists = false;
 
@@ -140,46 +96,20 @@
 				// we probably do not have permissions to given directory → pretend that the file does not exist
 			}
 
-			if (exists) {
-				if (enableExists) writer->writeAttribute(L"true");
-				if (enableNameAbsolute) writer->writeAttribute(fs::absolute(file).wstring());
-				if (enableNameCanonical) writer->writeAttribute(fs::canonical(file).wstring());
-				if (enableType) writer->writeAttribute(getType(file));
-				if (enableSize) {
-					integer_t size = fs::is_regular_file(file) ? fs::file_size(file) : 0;
-					writer->writeAttribute(&size, typeid (size));
-				}
+			for (auto& finder : attributeFinders) finder.second->startFile(file);
 
-				if (enableOwner || enableGroup) {
-					string_t owner;
-					string_t group;
-					fetchOwner(file, owner, group);
+			for (RequestedField field : configuration.fields) {
+				AttributeFinder* finder = attributeFinders[field.group]; // should not be nullptr, because already checked while writing the relation metadata
 
-					if (enableOwner) writer->writeAttribute(owner);
-					if (enableGroup) writer->writeAttribute(group);
-				}
-
-				if (enableXattrUrl) writer->writeAttribute(getXattr(file, L"user.xdg.origin.url"));
-
-			} else {
-				// Only original name was written → write remaining attributes
-				// TODO: null values (requires adding null support to the format specification)
-				for (int i = 1; i < attributesMetadata.size(); i++) {
-					switch (attributesMetadata[i].typeId) {
-						case TypeId::BOOLEAN: writer->writeAttribute(L"false");
-							break;
-						case TypeId::INTEGER: writer->writeAttribute(L"0");
-							break;
-						case TypeId::STRING: writer->writeAttribute(L"");
-							break;
-						default:
-							throw RelpipeWriterException(L"Unsupported data type.");
-
-					}
+				if (exists || (field.group == RequestedField::GROUP_FILE && field.name == FileAttributeFinder::FIELD_PATH_ORIGINAL)) {
+					finder->writeField(writer.get(), field);
+				} else {
+					finder->writeEmptyField(writer.get(), field);
 				}
 			}
+			
+			for (auto& finder : attributeFinders) finder.second->endFile();
 		}
-
 	}
 };
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/RequestedField.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,51 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+
+#include <relpipe/writer/typedefs.h>
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+using namespace relpipe::writer;
+
+class RequestedField {
+public:
+	static const string_t GROUP_FILE;
+	static const string_t GROUP_XATTR;
+	string_t group;
+	string_t name;
+	std::vector<string_t> aliases;
+	std::map<string_t, string_t> options;
+
+	RequestedField(const string_t& group, const string_t& name, const std::vector<string_t>& aliases = {}, const std::map<string_t, string_t>& options = {}) : group(group), name(name), aliases(aliases), options(options) {
+	}
+
+	virtual ~RequestedField() {
+	}
+};
+
+const string_t RequestedField::GROUP_FILE = L"file";
+const string_t RequestedField::GROUP_XATTR = L"xattr";
+
+}
+}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/XattrAttributeFinder.h	Wed Jan 16 17:23:05 2019 +0100
@@ -0,0 +1,80 @@
+/**
+ * Relational pipes
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+#pragma once
+
+#include <vector>
+#include <filesystem>
+
+#include <relpipe/writer/typedefs.h>
+#include <relpipe/writer/AttributeMetadata.h>
+#include <relpipe/writer/RelationalWriter.h>
+
+#include "RequestedField.h"
+
+namespace relpipe {
+namespace in {
+namespace filesystem {
+
+namespace fs = std::filesystem;
+using namespace relpipe::writer;
+
+class XattrAttributeFinder : public AttributeFinder {
+private:
+	std::wstring_convert<codecvt_utf8<wchar_t>> convertor; // TODO: support also other encodings.
+
+	fs::path currentFile;
+
+	string_t getXattr(const fs::path& file, const string_t& attributeName) {
+		// TODO: review, test on multiple platforms
+		// TODO: improve documentation in xattr.h in glibc
+		// TODO: check XATTR_NAME_MAX in limits.h and if it is reasonably small on our system, allocate buffer for it instead of reading twice
+		// TODO: avoid race condition (another process might change the attribute between reading its length and its value)
+		ssize_t length = getxattr(file.c_str(), convertor.to_bytes(attributeName).c_str(), nullptr, 0);
+		if (length > 0) {
+			std::vector<char> buffer(length + 1);
+			getxattr(file.c_str(), convertor.to_bytes(attributeName).c_str(), buffer.data(), buffer.size());
+			return convertor.from_bytes(buffer.data());
+		} else {
+			return L"";
+		}
+	}
+public:
+
+	virtual vector<AttributeMetadata> toMetadata(const RequestedField& field) override {
+		if (field.group == RequestedField::GROUP_XATTR) return { AttributeMetadata{field.name, TypeId::STRING}};
+		else return {};
+	}
+
+	void startFile(const fs::path& file) override {
+		currentFile = file;
+	};
+
+	void endFile() override {
+	};
+
+	virtual void writeField(RelationalWriter* writer, const RequestedField& field) override {
+		if (field.group == RequestedField::GROUP_XATTR) writer->writeAttribute(getXattr(currentFile, field.name));
+	}
+
+	virtual ~XattrAttributeFinder() override {
+	}
+};
+
+}
+}
+}
\ No newline at end of file
--- a/src/relpipe-in-filesystem.cpp	Sun Jan 13 16:44:11 2019 +0100
+++ b/src/relpipe-in-filesystem.cpp	Wed Jan 16 17:23:05 2019 +0100
@@ -1,6 +1,6 @@
 /**
  * Relational pipes
- * Copyright © 2018 František Kučera (Frantovo.cz, GlobalCode.info)
+ * Copyright © 2019 František Kučera (Frantovo.cz, GlobalCode.info)
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -30,6 +30,7 @@
 #include <relpipe/cli/CLI.h>
 
 #include "FilesystemCommand.h"
+#include "CLIParser.h"
 
 using namespace relpipe::cli;
 using namespace relpipe::writer;
@@ -43,8 +44,10 @@
 	int resultCode = CLI::EXIT_CODE_UNEXPECTED_ERROR;
 
 	try {
+		CLIParser cliParser;
+		Configuration configuration = cliParser.parse(cli.arguments());
 		FilesystemCommand command;
-		command.process(cin, cout);
+		command.process(cin, cout, configuration);
 		resultCode = CLI::EXIT_CODE_SUCCESS;
 	} catch (RelpipeWriterException e) {
 		fwprintf(stderr, L"Caught Writer exception: %ls\n", e.getMessge().c_str());