commit f9d2d8c3cdbfeb1e48e0a7efbe8ebeb9ffa46e59
parent 9ba9f604de5c56574755a5fb5ca58de81ca4a399
Author: Friedel Schön <[email protected]>
Date:   Fri, 22 Dec 2023 20:44:47 +0100
Merge branch 'gizmomogwai-master'
Diffstat:
7 files changed, 200 insertions(+), 214 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1 @@
+bin
diff --git a/README.md b/README.md
@@ -42,24 +42,11 @@ This won't install the command globally, you always have to run `dub run imports
 
 ## Usage
 
+see
 ```bash
-$ importsort-d [-h] [-v] [-r] [-m] [-i] [-o <out>] [-k] [-a] [-r] <input...>
+$ importsort-d --help
+$ dub run importsort-d -- --help
 ```
-`input` may be omitted or set to `-` to read from STDIN
-
-| option                | description                                    |
-| --------------------- | ---------------------------------------------- |
-| `-h, --help`          | prints a help message                          |
-| `-v, --verbose`       | prints useful debug messages                   |
-|                       |                                                |
-| `-k, --keep`          | keeps the line as-is instead of formatting     |
-| `-a, --attribute`     | public and static imports first                |
-| `-b, --binding`       | sorts by binding rather then the original      |
-| `-m, --merge`         | merge imports which uses same file             |
-|                       |                                                |
-| `-r, --recursive`     | recursively search in directories              |
-| `-i, --inline`        | changes the input                              |
-| `-o, --output <path>` | writes to `path` rather then writing to STDOUT |
 
 ## Documentation
 
@@ -74,7 +61,7 @@ Look at the documentation at [`dpldocs.info`](https://importsort-d.dpldocs.info/
 "emeraldwalk.runonsave": {
     "commands": [
         {
-            "cmd": "importsort-d -i ${file}",
+            "cmd": "importsort-d --inplace --inputs=${file}",
             "match": "\\.d$"
         }
     ]
@@ -85,7 +72,7 @@ Look at the documentation at [`dpldocs.info`](https://importsort-d.dpldocs.info/
 ### How to add `importsort-d` to VIM/NeoVIM?
 > Just add this to your `.vimrc` or `init.vim`
 ```vim
-:autocmd BufWritePost * silent !importsort-d -i <afile>
+:autocmd BufWritePost * silent !importsort-d --inplace --inputs=<afile>
 ```
 
 ### Are cats cool?
@@ -127,4 +114,4 @@ This whole project is licensed under the beautiful terms of the `zlib-license`.
 
 Further information [here](LICENSE).
 
-> made with love and a lot of cat memes
-\ No newline at end of file
+> made with love and a lot of cat memes
diff --git a/assets/help.txt b/assets/help.txt
@@ -1,17 +0,0 @@
-{binary} v{version}
-
-Usage: {binary} [-h] [-v] [-r] [-i] [-o <out>] [-k] [-a] [-r] <input...>
-  <input> can be set to '-' to read from stdin
-` 
-Options:
-  -h, --help .......... prints this message
-  -v, --verbose ....... prints useful messages
-
-  -k, --keep .......... keeps the line as-is instead of formatting
-  -a, --attribute ..... public and static imports first
-  -b, --binding ....... sorts by binding rather then the original
-  -m, --merge ......... merge imports which uses same file
-  
-  -r, --recursive ..... recursively search in directories
-  -i, --inline ........ writes to the input
-  -o, --output <path> . writes to `path` instead of stdout
-\ No newline at end of file
diff --git a/dub.sdl b/dub.sdl
@@ -3,8 +3,8 @@ description "sort imports of a .d-file"
 authors "Friedel Schoen"
 copyright "Copyright © 2022, Friedel Schoen"l
 license "zlib"
-
-dflags "-Jassets"
+dependency "argparse" version="~>1.3.0"
+dflags "-J$PACKAGE_DIR/assets"
 targetType "executable"
 targetPath "bin"
 targetName "importsort-d"
diff --git a/dub.selections.json b/dub.selections.json
@@ -0,0 +1,6 @@
+{
+	"fileVersion": 1,
+	"versions": {
+		"argparse": "1.3.0+commit.17.g85d259e"
+	}
+}
diff --git a/src/main.d b/src/main.d
@@ -1,155 +1,87 @@
 // (c) 2022 Friedel Schon <[email protected]>
 
 module importsort.main;
-
-import core.stdc.stdlib : exit;import importsort.sort : Import, SortConfig, sortImports;import std.array : replace;import std.conv : ConvException, parse;import std.file : DirEntry, SpanMode, dirEntries, exists, isDir, isFile;import std.functional : unaryFun;import std.stdio : File, stderr, stdin, stdout;import std.string : endsWith;
-/// name of binary (for help)
-enum BINARY = "importsort-d";
-
-/// current version (and something I always forget to update oops)
-enum VERSION = "0.3.0";
-
-/// the help-message from `help.txt`
-enum HELP = import("help.txt")
-		.replace("{binary}", BINARY)
-		.replace("{version}", VERSION);
+import importsort.sort : SortConfig;
+import argparse : CLI;
+import core.stdc.stdlib : exit;
+import importsort.sort : Import, sortImports;
+import std.array : replace;
+import std.file : DirEntry, SpanMode, dirEntries, exists, isDir, isFile;
+import std.functional : unaryFun;
+import std.stdio : File, stderr, stdin, stdout;
+import std.range : empty;
+import std.string : endsWith;
 
 /// list entries (`ls`) from all arguments
-DirEntry[] listEntries(alias F = "true")(string[] input, bool recursive) {
+DirEntry[] listEntries(alias F = "true")(string[] input, bool recursive)
+{
 	alias filterFunc = unaryFun!F;
 
 	DirEntry[] entries;
 
-	foreach (path; input) {
-		if (!exists(path)) {
+	foreach (path; input)
+	{
+		if (!exists(path))
+		{
 			stderr.writef("error: '%s' does not exist\n", path);
-			exit(1);
-		} else if (isDir(path)) {
-			foreach (entry; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow)) {
+			exit(19);
+		}
+		else if (isDir(path))
+		{
+			foreach (entry; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow))
+			{
 				if (entry.isFile && entry.name.endsWith(".d") && filterFunc(entry.name))
 					entries ~= entry;
 			}
-		} else if (isFile(path)) {
-			if (!path.endsWith(".d")) {
+		}
+		else if (isFile(path))
+		{
+			if (!path.endsWith(".d"))
+			{
 				stderr.writef("error: '%s' is not a .d-file\n", path);
-				exit(1);
+				exit(11);
 			}
 			if (filterFunc(path))
 				entries ~= DirEntry(path);
-		} else {
+		}
+		else
+		{
 			stderr.writef("error: '%s' is not a file or directory\n", path);
-			exit(1);
+			exit(12);
 		}
 	}
 	return entries;
 }
 
-/// the main-function (nothing to explain)
-void main(string[] args) {
-	SortConfig config;
-	bool inline;
-	string output;
-	string[] input;
-	bool watcher;
-	bool watcherDelaySet;
-	double watcherDelay = 0.1; // sec
-	bool recursive;
-
-	// -*- option parser -*-
-
-	bool nextOutput;
-	bool nextWatcherDelay;
-	foreach (arg; args[1 .. $]) {
-		if (nextOutput) {
-			output = arg;
-			nextOutput = false;
-		} else if (nextWatcherDelay) {
-			try {
-				watcherDelay = parse!double(arg);
-			} catch (ConvException) {
-				stderr.writef("error: cannot parse delay '%s' to an integer\n", arg);
-				exit(1);
-			}
-			watcherDelaySet = true;
-			nextWatcherDelay = false;
-		} else if (arg == "--help" || arg == "-h") {
-			stdout.writeln(HELP);
-			return;
-		} else if (arg == "--verbose" || arg == "-v") {
-			config.verbose = true;
-		} else if (arg == "--keep" || arg == "-k") {
-			config.keepLine = true;
-		} else if (arg == "--attribute" || arg == "-a") {
-			config.byAttribute = true;
-		} else if (arg == "--binding" || arg == "-b") {
-			config.byBinding = true;
-		} else if (arg == "--merge" || arg == "-m") {
-			config.merge = true;
-		} else if (arg == "--inline" || arg == "-i") {
-			inline = true;
-		} else if (arg == "--recursive" || arg == "-r") {
-			recursive = true;
-			// TODO: --watch
-			/*} else if (arg == "--watch" || arg == "-w") {
-			watcher = true;
-		} else if (arg == "--delay" || arg == "-d") {
-			if (watcherDelaySet) {
-				stderr.writeln("error: watcher-delay already specified");
-				stderr.writeln(HELP);
-				exit(1);
-			}
-			nextWatcherDelay = true;*/
-		} else if (arg == "--output" || arg == "-o") {
-			if (output != null) {
-				stderr.writeln("error: output already specified");
-				stderr.writeln(HELP);
-				exit(1);
-			}
-			nextOutput = true;
-		} else if (arg[0] == '-') {
-			stderr.writef("error: unknown option '%s'\n", arg);
-			stderr.writeln(HELP);
-			exit(1);
-		} else {
-			input ~= arg;
-		}
-	}
-	if (recursive && input.length == 0) {
+int _main(SortConfig config)
+{
+	if (config.recursive && config.inputs.empty)
+	{
 		stderr.writeln("error: cannot use '--recursive' and specify no input");
 		exit(1);
 	}
-	if (inline && input.length == 0) {
-		stderr.writeln("error: cannot use '--inline' and read from stdin");
-		exit(1);
+	if (config.inplace && config.inputs.empty)
+	{
+		stderr.writeln("error: cannot use inplace and read from stdin");
+		exit(2);
 	}
-	if ((!inline || output.length > 0) && input.length > 0) {
-		stderr.writeln("error: if you use inputs you must use '--inline'");
-		exit(1);
+	if (!config.inputs.empty && (!config.inplace || !config.output.empty))
+	{
+		stderr.writeln(
+				"error: if you use inputs you must use inplace sorting or provide an output");
+		exit(3);
 	}
-	// -*- operation -*-
-
-	/*	if (watcher) {
-		stderr.writeln("\033[1;34mwatching files...\033[0m");
-		SysTime[string] lastModified;
-		for (;;) {
-			auto entries = listEntries!(x => x !in lastModified
-					|| lastModified[x] != x.timeLastModified)(input, recursive);
-
-			foreach (entry; entries) {
-				lastModified[entry.name] = entry.timeLastModified;
-			}
-			entries.sortImports(config);
-			Thread.sleep(Duration!"msecs"(cast(long) watcherDelay * 1000));
-		}
-	} else 
-	*/
-	if (input == null) {
-		File outfile = (output == null) ? stdout : File(output);
 
+	if (config.inputs.empty)
+	{
+		auto outfile = config.output.empty ? stdout : File(config.output);
 		sortImports(stdin, outfile, config);
-		if (output)
-			outfile.close();
-	} else {
-		listEntries(input, recursive).sortImports(config);
 	}
+	else
+	{
+		listEntries(config.inputs, config.recursive).sortImports(config);
+	}
+	return 0;
 }
+
+mixin CLI!(SortConfig).main!((config) { return _main(config); });
diff --git a/src/sort.d b/src/sort.d
@@ -10,30 +10,64 @@ import std.stdio : File, stderr;
 import std.string : strip, stripLeft;
 import std.traits : isIterable;
 import std.typecons : Yes;
+import std.conv : to;
+import std.uni : asLowerCase;
+import argparse;
 
-/// the pattern to determinate a line is an import or not
-enum PATTERN = ctRegex!`^(\s*)(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?\s*;[ \t]*([\n\r]*)$`;
+/// current version (and something I always forget to update oops)
+enum VERSION = "0.3.0";
 
 /// configuration for sorting imports
-struct SortConfig {
-	/// won't format the line, keep it as-is
-	bool keepLine = false;
+@(Command("importsort-d").Description("Sorts dlang imports").Epilog("Version: v" ~ VERSION))
+struct SortConfig
+{
+	@(ArgumentGroup("Input/Output arguments").Description("Define in- and output behavior"))
+	{
+		@(NamedArgument(["recursive", "r"]).Description("recursively search in directories"))
+		bool recursive = false;
+
+		@(NamedArgument(["inplace", "i"]).Description("writes to the input"))
+		bool inplace = false;
+
+		@(NamedArgument(["output", "o"]).Description("writes to `path` instead of stdout"))
+		string output;
+
+		@(NamedArgument(["inputs", "in"])
+				.Description("input files or directories, can be set to '-' to read from stdin"))
+		string[] inputs;
+	}
+
+	@(ArgumentGroup("Sorting arguments").Description("Tune import sorting algorithms"))
+	{
+		/// won't format the line, keep it as-is
+		@(NamedArgument(["keep", "k"]).Description("keeps the line as-is instead of formatting"))
+		bool keepLine = false;
+
+		@(NamedArgument(["attribute", "a"]).Description("public and static imports first"))
+		 /// sort by attributes (public/static first)
+		bool byAttribute = false;
 
-	/// sort by attributes (public/static first)
-	bool byAttribute = false;
+		@(NamedArgument(["binding", "b"]).Description("sorts by binding rather then the original"))
+		 /// sort by binding instead of the original
+		bool byBinding = false;
 
-	/// sort by binding instead of the original
-	bool byBinding = false;
+		@(NamedArgument(["merge", "m"]).Description("merge imports which uses same file"))
+		 /// merges imports of the same source
+		bool merge = false;
 
-	/// print interesting messages (TODO)
-	bool verbose = false;
+		/// ignore case when sorting
+		@(NamedArgument(["ignoreCase", "c"]).Description("ignore case when comparing elements"))
+		bool ignoreCase = false;
+	}
 
-	/// merges imports of the same source
-	bool merge = false;
 }
 
+/// the pattern to determinate a line is an import or not
+enum PATTERN = ctRegex!`^(\s*)(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?\s*;[ \t]*([\n\r]*)$`;
+
 /// helper-struct for identifiers and its bindings
-struct Identifier {
+struct Identifier
+{
 	/// SortConfig::byBinding
 	bool byBinding;
 
@@ -44,13 +78,14 @@ struct Identifier {
 	string binding;
 
 	/// wether this import has a binding or not
-	@property
-	bool hasBinding() {
+	@property bool hasBinding()
+	{
 		return binding != null;
 	}
 
 	/// the string to sort
-	string sortBy() {
+	string sortBy()
+	{
 		if (byBinding)
 			return hasBinding ? binding : original;
 		else
@@ -59,7 +94,8 @@ struct Identifier {
 }
 
 /// the import statement description
-struct Import {
+struct Import
+{
 	/// SortConfig::byAttribute
 	bool byAttribute;
 
@@ -85,23 +121,34 @@ struct Import {
 	string end;
 
 	/// the string to sort
-	string sortBy() {
+	string sortBy()
+	{
 		if (byAttribute && (public_ || static_))
 			return '\0' ~ name.sortBy;
 		return name.sortBy;
 	}
 }
 
+bool less(SortConfig config, string a, string b)
+{
+	return config.ignoreCase ? a.asLowerCase.to!string < b.asLowerCase.to!string : a < b;
+}
+
 /// write import-statements to `outfile` with `config`
-void writeImports(File outfile, SortConfig config, Import[] matches) {
+void writeImports(File outfile, SortConfig config, Import[] matches)
+{
 	if (!matches)
 		return;
 
-	if (config.merge) {
-		for (int i = 0; i < matches.length; i++) {
-			for (int j = i + 1; j < matches.length; j++) {
+	if (config.merge)
+	{
+		for (int i = 0; i < matches.length; i++)
+		{
+			for (int j = i + 1; j < matches.length; j++)
+			{
 				if (matches[i].name.original == matches[j].name.original
-					&& matches[i].name.binding == matches[j].name.binding) {
+						&& matches[i].name.binding == matches[j].name.binding)
+				{
 
 					matches[i].line = null;
 					matches[i].idents ~= matches[j].idents;
@@ -112,30 +159,41 @@ void writeImports(File outfile, SortConfig config, Import[] matches) {
 		}
 	}
 
-	matches.sort!((a, b) => a.sortBy < b.sortBy);
+	matches.sort!((a, b) => less(config, a.sortBy, b.sortBy));
 	bool first;
 
-	foreach (m; matches) {
-		if (config.keepLine && m.line.length > 0) {
+	foreach (m; matches)
+	{
+		if (config.keepLine && m.line.length > 0)
+		{
 			outfile.write(m.line);
-		} else {
+		}
+		else
+		{
 			outfile.write(m.begin);
 			if (m.public_)
 				outfile.write("public ");
 			if (m.static_)
 				outfile.write("static ");
-			if (m.name.hasBinding) {
+			if (m.name.hasBinding)
+			{
 				outfile.writef("import %s = %s", m.name.binding, m.name.original);
-			} else {
+			}
+			else
+			{
 				outfile.write("import " ~ m.name.original);
 			}
 			first = true;
-			foreach (ident; m.idents) {
+			foreach (ident; m.idents)
+			{
 				auto begin = first ? " : " : ", ";
 				first = false;
-				if (ident.hasBinding) { // hasBinding
+				if (ident.hasBinding)
+				{ // hasBinding
 					outfile.writef("%s%s = %s", begin, ident.binding, ident.original);
-				} else {
+				}
+				else
+				{
 					outfile.write(begin ~ ident.original);
 				}
 			}
@@ -146,11 +204,13 @@ void writeImports(File outfile, SortConfig config, Import[] matches) {
 
 /// sort imports of an entry (file) (entries: DirEntry[])
 void sortImports(alias P = "true", R)(R entries, SortConfig config)
-		if (isIterable!R && is(ElementType!R == DirEntry)) {
+		if (isIterable!R && is(ElementType!R == DirEntry))
+{
 	alias postFunc = unaryFun!P;
 
 	File infile, outfile;
-	foreach (entry; entries) {
+	foreach (entry; entries)
+	{
 		stderr.writef("\033[34msorting \033[0;1m%s\033[0m\n", entry.name);
 
 		infile = File(entry.name);
@@ -168,23 +228,30 @@ void sortImports(alias P = "true", R)(R entries, SortConfig config)
 }
 
 /// raw-implementation of sort file (infile -> outfile)
-void sortImports(File infile, File outfile, SortConfig config) {
+void sortImports(File infile, File outfile, SortConfig config)
+{
 	string softEnd = null;
 	Import[] matches;
 
-	foreach (line; infile.byLine(Yes.keepTerminator)) {
+	foreach (line; infile.byLine(Yes.keepTerminator))
+	{
 		auto linestr = line.idup;
-		if (auto match = linestr.matchFirst(PATTERN)) { // is import
-			if (softEnd) {
+		if (auto match = linestr.matchFirst(PATTERN))
+		{ // is import
+			if (softEnd)
+			{
 				if (!matches)
 					outfile.write(softEnd);
 				softEnd = null;
 			}
 
 			auto im = Import(config.byAttribute, linestr);
-			if (match[3]) {
+			if (match[3])
+			{
 				im.name = Identifier(config.byBinding, match[4], match[3]);
-			} else {
+			}
+			else
+			{
 				im.name = Identifier(config.byBinding, match[4]);
 			}
 			im.begin = match[1];
@@ -195,26 +262,38 @@ void sortImports(File infile, File outfile, SortConfig config) {
 			else if (match[2] == "public")
 				im.public_ = true;
 
-			if (match[5]) {
-				foreach (id; match[5][1 .. $].split(",")) {
-					if (auto pair = id.findSplit("=")) { // has alias
+			if (match[5])
+			{
+				foreach (id; match[5][1 .. $].split(","))
+				{
+					if (auto pair = id.findSplit("="))
+					{ // has alias
 						im.idents ~= Identifier(config.byBinding, pair[2].strip, pair[0].strip);
-					} else {
+					}
+					else
+					{
 						im.idents ~= Identifier(config.byBinding, id.strip);
 					}
 				}
-				im.idents.sort!((a, b) => a.sortBy < b.sortBy);
+				im.idents.sort!((a, b) => less(config, a.sortBy, b.sortBy));
 			}
 			matches ~= im;
-		} else {
-			if (!softEnd && linestr.stripLeft == "") {
+		}
+		else
+		{
+			if (!softEnd && linestr.stripLeft == "")
+			{
 				softEnd = linestr;
-			} else {
-				if (matches) {
+			}
+			else
+			{
+				if (matches)
+				{
 					outfile.writeImports(config, matches);
 					matches = [];
 				}
-				if (softEnd) {
+				if (softEnd)
+				{
 					outfile.write(softEnd);
 					softEnd = null;
 				}