Skip to content

Commit

Permalink
Update docs and unittests
Browse files Browse the repository at this point in the history
  • Loading branch information
FreeSlave committed Oct 17, 2015
1 parent 0198ad0 commit 984fa8b
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 15 deletions.
4 changes: 0 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,3 @@ matrix:
- d: dmd-2.065.0
- d: gdc-4.8.2
- d: ldc-0.14.0

script:
- dub test --compiler=${DC}
- dub build desktopfile:desktoptest --compiler=${DC}
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ Please feel free to propose enchancements or report any related bugs to *Issues*
The library is crossplatform for the most part, though there's little sense to use it on systems that don't follow freedesktop specifications.
**desktopfile** is developed and tested on FreeBSD and Debian GNU/Linux.

## Features

### Implemented features

**desktopfile** provides basic features like reading and running desktop files, and more:

* [Exec](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s06.html) value unquoting and unescaping. Expanding field codes.
* Can rewrite desktop files preserving all comments and the original order of groups.
* Retrieving [Desktop file ID](http://standards.freedesktop.org/desktop-entry-spec/latest/ape.html).
* Support for [Additional application actions](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s10.html).
* Determining default terminal command to run applications with Terminal=true.

### Missing features

Features that currently should be handled by user, but may be implemented in the future versions of library.

* [D-Bus Activation](http://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html).
* Startup Notification Protocol.
* Copying files to local file system when %f field code is used.
* Starting several instances of application if it supports only %f and not %F.

## Generating documentation

Ddoc:
Expand Down Expand Up @@ -69,9 +90,9 @@ Use this example to check if the desktopfile library can parse all .desktop file

dub run desktopfile:desktoptest --build=release

To print all directories examined by desktoptest to stdout, build it in non-release mode:
To print all directories examined by desktoptest to stdout, add --verbose flag:

dub run desktopfile:desktoptest
dub run desktopfile:desktoptest -- --verbose

Start desktoptest on specified directories:

Expand Down
1 change: 0 additions & 1 deletion examples/desktoptest/dub.selections.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"fileVersion": 1,
"versions": {
"desktopfile": "0.5.0",
"standardpaths": "0.2.0",
"inilike": "0.3.0"
}
Expand Down
9 changes: 8 additions & 1 deletion examples/desktoptest/source/app.d
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import std.array;
import std.file;
import std.path;
import std.process;
import std.getopt;

import desktopfile;

void main(string[] args)
{
string[] desktopDirs;

bool verbose;

getopt(args, "verbose", "Print name of each examined desktop file to standard output", &verbose);

if (args.length > 1) {
desktopDirs = args[1..$];
} else {
Expand Down Expand Up @@ -43,7 +48,9 @@ void main(string[] args)

foreach(dir; desktopDirs.filter!(s => s.exists && s.isDir())) {
foreach(entry; dir.dirEntries(SpanMode.depth).filter!(a => a.isFile() && (a.extension == ".desktop" || a.extension == ".directory"))) {
debug writeln(entry);
if (verbose) {
writeln(entry);
}
try {
auto df = new DesktopFile(entry);
if (!df.execString().empty) {
Expand Down
101 changes: 94 additions & 7 deletions source/desktopfile.d
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ else version(Posix)
}
}


/**
* 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 = [
Expand Down Expand Up @@ -108,6 +111,13 @@ else version(Posix)
return doUnescape(arg, pairs);
}

///
unittest
{
assert(unescapeExecArgument("simple") == "simple");
assert(unescapeExecArgument(`with\&\"escaped\"\?symbols\$`) == `with&"escaped"?symbols$`);
}

private @trusted string unescapeQuotedArgument(string value) nothrow pure
{
static immutable Tuple!(char, char)[] pairs = [
Expand All @@ -119,6 +129,14 @@ private @trusted string unescapeQuotedArgument(string value) nothrow pure
return doUnescape(value, pairs);
}

/**
* 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).
* Throws:
* DesktopExecException if string can't be unquoted (e.g. no pair quote).
* Note:
* Although Desktop Entry Specification says that arguments must be quoted by double quote, for compatibility reasons this implementation also recognizes single quotes.
*/
@trusted auto unquoteExecString(string value) pure
{
string[] result;
Expand Down Expand Up @@ -191,12 +209,43 @@ unittest
assertThrown!DesktopExecException(unquoteExecString(`cmd "quoted arg`));
}


/**
* Convenient function used to unquote and unescape Exec value into an array of arguments.
* Note:
* Parsed arguments still may contain field codes that should be appropriately expanded before passing to spawnProcess.
* Throws:
* DesktopExecException if string can't be unquoted.
* See_Also:
* unquoteExecString, unescapeExecArgument
*/
@trusted string[] parseExecString(string execString) pure
{
return execString.unquoteExecString().map!(unescapeExecArgument).array;
}

@trusted string[] expandExecArgs(in string[] execArgs, in string[] urls, string iconName = null, string name = null, string fileName = null) pure
///
unittest
{
assert(equal(parseExecString(`"quoted cmd" new\nline "quoted\\\\arg" slash\\arg`), ["quoted cmd", "new\nline", `quoted\arg`, `slash\arg`]));
}

/**
* Expand Exec arguments (usually returned by parseExecString) replacing field codes with given values, making the array suitable for passing to spawnProcess. Deprecated field codes are ignored.
* Note:
* Returned array may be empty and should be checked before passing to spawnProcess.
* Params:
* execArgs = array of unquoted and unescaped arguments.
* urls = array of urls or file names that inserted in the place of %f, %F, %u or %U field codes. For %f and %u only the first element of array is used.
* iconName = icon name used to substitute %i field code by --icon iconName.
* name = name of application used that inserted in the place of %c field code.
* fileName = name of desktop file that inserted in the place of %k field code.
* Throws:
* DesktopExecException if command line contains unknown field code.
* See_Also:
* 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) {
Expand Down Expand Up @@ -239,7 +288,21 @@ unittest
return toReturn;
}

@trusted string[] expandExecString(string execString, in string[] urls, string iconName = null, string name = null, string fileName = null) pure
///
unittest
{
assert(expandExecArgs(["program name", "%%f", "%f", "%i"], ["one", "two"], "folder") == ["program name", "%f", "one", "--icon", "folder"]);
assertThrown!DesktopExecException(expandExecArgs(["program name", "%y"]));
}

/**
* Unquote, unescape Exec string and expand field codes substituting them with appropriate values.
* Throws:
* DesktopExecException if string can't be unquoted, unquoted command line is empty or it has unknown field code.
* See_Also:
* expandExecArgs, parseExecString
*/
@trusted string[] expandExecString(string execString, in string[] urls = null, string iconName = null, string name = null, string fileName = null) pure
{
auto execArgs = parseExecString(execString);
if (execArgs.empty) {
Expand All @@ -248,6 +311,15 @@ unittest
return expandExecArgs(execArgs, urls, iconName, name, fileName);
}

///
unittest
{
assert(expandExecString(`"quoted program" %i -w %c -f %k %U %D %u %f %F`, ["one", "two"], "folder", "Программа", "/example.desktop") == ["quoted program", "--icon", "folder", "-w", "Программа", "-f", "/example.desktop", "one", "two", "one", "one", "one", "two"]);

assertThrown!DesktopExecException(expandExecString(`program %f %y`)); //%y is unknown field code.
assertThrown!DesktopExecException(expandExecString(``));
}

/**
* 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 Expand Up @@ -992,10 +1064,11 @@ Type=Directory`;
Name=Program
Name[ru]=Программа
Exec="quoted program" %i -w %c -f %k %U %D %u %f %F
Icon=folder`;
Icon=folder
Icon[ru]=folder_ru`;
auto df = new DesktopFile(iniLikeStringReader(contents), DesktopFile.ReadOptions.noOptions, "/example.desktop");
assert(df.expandExecString(["one", "two"], "ru") ==
["quoted program", "--icon", "folder", "-w", "Программа", "-f", "/example.desktop", "one", "two", "one", "one", "one", "two"]);
["quoted program", "--icon", "folder_ru", "-w", "Программа", "-f", "/example.desktop", "one", "two", "one", "one", "one", "two"]);
}

/**
Expand All @@ -1014,7 +1087,7 @@ Icon=folder`;
* DesktopExecException if exec string is invalid.
* See_Also: getTerminalCommand, start, expandExecString
*/
@trusted Pid startApplication(in string[] urls = null, string locale = null, lazy string[] terminalCommand = getTerminalCommand) const
@trusted Pid startApplication(in string[] urls = null, string locale = null, lazy const(string)[] terminalCommand = getTerminalCommand) const
{
auto args = expandExecString(urls, locale);
if (terminal()) {
Expand All @@ -1032,7 +1105,7 @@ Icon=folder`;
}

///ditto, but uses the only url.
@trusted Pid startApplication(string url, string locale = null, lazy string[] terminalCommand = getTerminalCommand) const {
@trusted Pid startApplication(string url, string locale = null, lazy const(string)[] terminalCommand = getTerminalCommand) const {
return startApplication([url], locale, terminalCommand);
}

Expand Down Expand Up @@ -1083,6 +1156,17 @@ Icon=folder`;
}
}

///
unittest
{
string contents = "[Desktop Entry]\nType=Directory";
auto df = new DesktopFile(iniLikeStringReader(contents));
assertThrown(df.start());

df = new DesktopFile();
assertThrown(df.start());
}

private:
IniLikeGroup _desktopEntry;
}
Expand All @@ -1103,6 +1187,7 @@ Comment=Double Commander is a cross platform open source file manager with two p
Comment[ru]=Double Commander - кроссплатформенный файловый менеджер.
Terminal=false
Icon=doublecmd
Icon[ru]=doublecmd_ru
Exec=doublecmd %f
TryExec=doublecmd
Type=Application
Expand Down Expand Up @@ -1143,6 +1228,8 @@ Name=Notspecified Action`;
assert(df.localizedGenericName("ru_RU") == "Файловый менеджер");
assert(df.comment() == "Double Commander is a cross platform open source file manager with two panels side by side.");
assert(df.localizedComment("ru_RU") == "Double Commander - кроссплатформенный файловый менеджер.");
assert(df.iconName() == "doublecmd");
assert(df.localizedIconName("ru_RU") == "doublecmd_ru");
assert(df.tryExecString() == "doublecmd");
assert(!df.terminal());
assert(!df.noDisplay());
Expand Down

0 comments on commit 984fa8b

Please sign in to comment.