Skip to content

Commit

Permalink
Add ExecBuilder
Browse files Browse the repository at this point in the history
  • Loading branch information
FreeSlave committed Apr 10, 2016
1 parent ac68dab commit 2d44c64
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 44 deletions.
4 changes: 2 additions & 2 deletions dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"copyright": "Copyright © 2015-2016, Roman Chistokhodov",
"authors": ["Roman Chistokhodov"],
"dependencies": {
"inilike": "~>0.5.0",
"xdgpaths" : "~>0.1.2"
"inilike": "~>0.5.1",
"xdgpaths" : "~>0.2.1"
},
"targetName" : "desktopfile",
"targetPath" : "lib",
Expand Down
4 changes: 2 additions & 2 deletions dub.selections.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"fileVersion": 1,
"versions": {
"isfreedesktop": "0.1.0",
"inilike": "0.5.0",
"xdgpaths": "0.1.2"
"inilike": "0.5.1",
"xdgpaths": "0.2.1"
}
}
183 changes: 143 additions & 40 deletions source/desktopfile/utils.d
Original file line number Diff line number Diff line change
Expand Up @@ -98,42 +98,22 @@ class DesktopExecException : Exception
}
}

/**
* Unescape Exec argument as described in [specification](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html).
* Returns: Unescaped string.
*/
@trusted string unescapeExecArgument(string arg) nothrow pure
{
static immutable Tuple!(char, char)[] pairs = [
tuple('s', ' '),
tuple('n', '\n'),
tuple('r', '\r'),
tuple('t', '\t'),
tuple('"', '"'),
tuple('\'', '\''),
tuple('\\', '\\'),
tuple('>', '>'),
tuple('<', '<'),
tuple('~', '~'),
tuple('|', '|'),
tuple('&', '&'),
tuple(';', ';'),
tuple('$', '$'),
tuple('*', '*'),
tuple('?', '?'),
tuple('#', '#'),
tuple('(', '('),
tuple(')', ')'),
tuple('`', '`'),
];
return doUnescape(arg, pairs);
}

///
unittest
private @safe bool needQuoting(string arg) nothrow pure
{
assert(unescapeExecArgument("simple") == "simple");
assert(unescapeExecArgument(`with\&\"escaped\"\?symbols\$`) == `with&"escaped"?symbols$`);
import std.uni : isWhite;
for (size_t i=0; i<arg.length; ++i)
{
switch(arg[i]) {
case ' ': case '\t': case '\n': case '\r': case '"':
case '\\': case '\'': case '>': case '<': case '~':
case '|': case '&': case ';': case '$': case '*':
case '?': case '#': case '(': case ')': case '`':
return true;
default:
break;
}
}
return false;
}

private @trusted string unescapeQuotedArgument(string value) nothrow pure
Expand All @@ -147,9 +127,19 @@ private @trusted string unescapeQuotedArgument(string value) nothrow pure
return doUnescape(value, pairs);
}

private @trusted string escapeQuotedArgument(string value) pure {
return value.replace("`", "\\`").replace("\\", `\\`).replace("$", `\$`).replace("\"", `\"`);
}

private @trusted string quoteIfNeeded(string value, char quote = '"') pure {
if (value.needQuoting) {
return quote ~ value.escapeQuotedArgument() ~ quote;
}
return value;
}

/**
* Unquote Exec value into an array of escaped arguments.
* If an argument was quoted then unescaping of quoted arguments is applied automatically. Note that unescaping of quoted argument is not the same as unquoting argument in general. Read more in [specification](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html).
* Apply unquoting to Exec value making it into an array of escaped arguments. It automatically performs quote-related unescaping. Returned values are still escaped as by general rule. Read more: [specification](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html).
* Throws:
* DesktopExecException if string can't be unquoted (e.g. no pair quote).
* Note:
Expand Down Expand Up @@ -237,11 +227,11 @@ unittest
* Throws:
* DesktopExecException if string can't be unquoted.
* See_Also:
* unquoteExecString, unescapeExecArgument
* unquoteExecString, expandExecArgs
*/
@trusted string[] parseExecString(string execString) pure
{
return execString.unquoteExecString().map!(unescapeExecArgument).array;
return execString.unquoteExecString().map!(s => unescapeValue(s)).array;
}

///
Expand All @@ -266,7 +256,7 @@ unittest
* parseExecString
*/
@trusted string[] expandExecArgs(in string[] execArgs, in string[] urls = null, string iconName = null, string name = null, string fileName = null) pure
{
{
string[] toReturn;
foreach(token; execArgs) {
if (token == "%f") {
Expand Down Expand Up @@ -340,6 +330,119 @@ unittest
assertThrown!DesktopExecException(expandExecString(``));
}

/**
* Helper struct to build Exec string for desktop file.
*/
struct ExecBuilder
{
/**
* Construct ExecBuilder.
* Params:
* executable = path to executable. Value will be escaped and quoted as needed.
*/
@safe this(string executable) {
escapedArgs ~= executable.escapeValue().quoteIfNeeded();
}

/**
* Add literal argument which is not field code.
* Params:
* arg = Literal argument. Value will be escaped and quoted as needed.
* Returns: this object for chained calls.
*/
@safe ExecBuilder argument(string arg) {
escapedArgs ~= arg.escapeValue().quoteIfNeeded();
return this;
}

/**
* Add "%i" field code.
* Returns: this object for chained calls.
*/
@safe ExecBuilder icon() {
escapedArgs ~= "%i";
return this;
}


/**
* Add "%f" field code.
* Returns: this object for chained calls.
*/
@safe ExecBuilder file() {
escapedArgs ~= "%f";
return this;
}

/**
* Add "%F" field code.
* Returns: this object for chained calls.
*/
@safe ExecBuilder files() {
escapedArgs ~= "%F";
return this;
}

/**
* Add "%u" field code.
* Returns: this object for chained calls.
*/
@safe ExecBuilder url() {
escapedArgs ~= "%u";
return this;
}

/**
* Add "%U" field code.
* Returns: this object for chained calls.
*/
@safe ExecBuilder urls() {
escapedArgs ~= "%U";
return this;
}

/**
* Add "%c" field code (name of application).
* Returns: this object for chained calls.
*/
@safe ExecBuilder name() {
escapedArgs ~= "%c";
return this;
}

/**
* Add "%k" field code (location of desktop file).
* Returns: this object for chained calls.
*/
@safe ExecBuilder location() {
escapedArgs ~= "%k";
return this;
}

/**
* Get resulting string that can be set to Exec field of Desktop Entry.
*/
@trusted string result() const {
static if( __VERSION__ < 2066 ) {
return escapedArgs.map!(s => s).join(" ");
} else {
return escapedArgs.join(" ");
}
}

private:
string[] escapedArgs;
}

///
unittest
{
assert(ExecBuilder("quoted program").icon()
.argument("-w").name()
.argument("-f").location()
.urls().url().file().files().result() == `"quoted program" %i -w %c -f %k %U %u %f %F`);
}

/**
* Detect command which will run program in terminal emulator.
* On Freedesktop it looks for x-terminal-emulator first. If found ["/path/to/x-terminal-emulator", "-e"] is returned.
Expand Down

0 comments on commit 2d44c64

Please sign in to comment.