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"
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;
}