1 /**
2  * Copyright: Copyright Jason White, 2016
3  * License:   MIT
4  * Authors:   Jason White
5  */
6 module button.command;
7 
8 import button.exceptions;
9 import button.resource;
10 import button.context;
11 
12 /**
13  * Escapes the argument according to the rules of bash, the most commonly used
14  * shell. This is mostly used for cosmetic purposes when printing out argument
15  * arrays where they could be copy-pasted into a shell.
16  */
17 string escapeShellArg(string arg) pure
18 {
19     import std.array : appender;
20     import std.algorithm.searching : findAmong;
21     import std.range : empty;
22     import std.exception : assumeUnique;
23 
24     if (arg.empty)
25         return `""`;
26 
27     // Characters that require the string to be quoted.
28     static immutable special = " '~*[]?";
29 
30     immutable quoted = !arg.findAmong(special).empty;
31 
32     auto result = appender!(char[]);
33 
34     if (quoted)
35         result.put('"');
36 
37     foreach (c; arg)
38     {
39         // Characters to escape
40         if (c == '\\' || c == '"' || c == '$' || c == '`')
41         {
42             result.put("\\");
43             result.put(c);
44         }
45         else
46         {
47             result.put(c);
48         }
49     }
50 
51     if (quoted)
52         result.put('"');
53 
54     return assumeUnique(result.data);
55 }
56 
57 unittest
58 {
59     assert(escapeShellArg(``) == `""`);
60     assert(escapeShellArg(`foo`) == `foo`);
61     assert(escapeShellArg(`foo bar`) == `"foo bar"`);
62     assert(escapeShellArg(`foo'bar`) == `"foo'bar"`);
63     assert(escapeShellArg(`foo?bar`) == `"foo?bar"`);
64     assert(escapeShellArg(`foo*.c`) == `"foo*.c"`);
65     assert(escapeShellArg(`foo.[ch]`) == `"foo.[ch]"`);
66     assert(escapeShellArg(`~foobar`) == `"~foobar"`);
67     assert(escapeShellArg(`$PATH`) == `\$PATH`);
68     assert(escapeShellArg(`\`) == `\\`);
69     assert(escapeShellArg(`foo"bar"`) == `foo\"bar\"`);
70     assert(escapeShellArg("`pwd`") == "\\`pwd\\`");
71 }
72 
73 /**
74  * A single command.
75  */
76 struct Command
77 {
78     /**
79      * Arguments to execute. The first argument is the name of the executable.
80      */
81     immutable(string)[] args;
82 
83     alias args this;
84 
85     // Root of the build directory. This is used to normalize implicit resource
86     // paths.
87     string buildRoot;
88 
89     /**
90      * The result of executing a command.
91      */
92     struct Result
93     {
94         import core.time : Duration;
95 
96         /**
97          * Implicit input and output resources this command used.
98          */
99         Resource[] inputs, outputs;
100 
101         /**
102          * How long it took the command to run from start to finish.
103          */
104         Duration duration;
105     }
106 
107     this(immutable(string)[] args)
108     {
109         assert(args.length > 0, "A command must have >0 arguments");
110 
111         this.args = args;
112     }
113 
114     /**
115      * Compares this command with another.
116      */
117     int opCmp()(const auto ref typeof(this) that) const pure nothrow
118     {
119         import std.algorithm.comparison : cmp;
120         return cmp(this.args, that.args);
121     }
122 
123     /// Ditto
124     bool opEquals()(const auto ref typeof(this) that) const pure nothrow
125     {
126         return this.opCmp(that) == 0;
127     }
128 
129     unittest
130     {
131         import std.algorithm.comparison : cmp;
132 
133         static assert(Command(["a", "b"]) == Command(["a", "b"]));
134         static assert(Command(["a", "b"]) != Command(["a", "c"]));
135         static assert(Command(["a", "b"]) <  Command(["a", "c"]));
136         static assert(Command(["b", "a"]) >  Command(["a", "b"]));
137 
138         static assert(cmp([Command(["a", "b"])], [Command(["a", "b"])]) == 0);
139         static assert(cmp([Command(["a", "b"])], [Command(["a", "c"])]) <  0);
140         static assert(cmp([Command(["a", "c"])], [Command(["a", "b"])]) >  0);
141     }
142 
143     /**
144      * Returns a string representation of the command.
145      *
146      * Since the command is in argv format, we format it into a string as one
147      * would enter into a shell.
148      */
149     string toPrettyString() const pure
150     {
151         import std.array : join;
152         import std.algorithm.iteration : map;
153 
154         return args.map!(arg => arg.escapeShellArg).join(" ");
155     }
156 
157     /**
158      * Returns a short string representation of the command.
159      */
160     @property string toPrettyShortString() const pure nothrow
161     {
162         return args[0];
163     }
164 }