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