mirror of
https://github.com/go-task/task.git
synced 2026-05-18 13:15:41 +02:00
Compare commits
663 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd2116c897 | ||
|
|
c5566b3e94 | ||
|
|
30cbf02bff | ||
|
|
9e4e9b4f1a | ||
|
|
6f290f28b6 | ||
|
|
6ff3c9015b | ||
|
|
e28b82b2b7 | ||
|
|
3edf124f96 | ||
|
|
fb72b46a3c | ||
|
|
49bf395f61 | ||
|
|
eab14b6c49 | ||
|
|
8b962fb8e8 | ||
|
|
e5a3c861cb | ||
|
|
c7bb3d63b0 | ||
|
|
e6b543c15e | ||
|
|
8137517d93 | ||
|
|
572f6a7fab | ||
|
|
c6d9201680 | ||
|
|
7dcb3af944 | ||
|
|
4bc183a8a1 | ||
|
|
9f83311931 | ||
|
|
10986d3a7c | ||
|
|
f4f6efa547 | ||
|
|
bf64259af3 | ||
|
|
6141ba84ce | ||
|
|
329902f0db | ||
|
|
bfcaa7a443 | ||
|
|
45915bf0ed | ||
|
|
ee7f2a541f | ||
|
|
59a00eae98 | ||
|
|
df8293bee6 | ||
|
|
935216f179 | ||
|
|
f56bbd46fd | ||
|
|
9f0f18c5c4 | ||
|
|
191c34c9c4 | ||
|
|
6a604b3002 | ||
|
|
5a435b533e | ||
|
|
4b027722b1 | ||
|
|
68ce8642b1 | ||
|
|
4913b6a0f1 | ||
|
|
aee0ab05f4 | ||
|
|
b44432f24a | ||
|
|
86be13ff1f | ||
|
|
ee95df0e57 | ||
|
|
442c29f020 | ||
|
|
739037fc37 | ||
|
|
f8252020aa | ||
|
|
38b87439fe | ||
|
|
116879f7ea | ||
|
|
2eafb2f067 | ||
|
|
c36f0f6f7f | ||
|
|
bfc033959b | ||
|
|
814f350b6d | ||
|
|
05db8ce582 | ||
|
|
3fd36a0c72 | ||
|
|
cbb12b29bd | ||
|
|
6ed30f1add | ||
|
|
a044c41c66 | ||
|
|
fb78e53a14 | ||
|
|
acfbbaa549 | ||
|
|
d52d74c64c | ||
|
|
d36f73de01 | ||
|
|
628c4a7b5f | ||
|
|
ca07a663e1 | ||
|
|
66d008391e | ||
|
|
eef84bda26 | ||
|
|
e0defe71aa | ||
|
|
069257151e | ||
|
|
3f80a3b39e | ||
|
|
b2a56161bb | ||
|
|
5e75639244 | ||
|
|
cb2cd3e10f | ||
|
|
0acb911d6a | ||
|
|
17ad7060b3 | ||
|
|
f38ba7fcd3 | ||
|
|
a3464068bd | ||
|
|
347ecc028f | ||
|
|
94ac60fa09 | ||
|
|
d567e23e50 | ||
|
|
8ff81562d2 | ||
|
|
7a8142ed92 | ||
|
|
eaba1b9cc8 | ||
|
|
b08b3546d2 | ||
|
|
7453e688fd | ||
|
|
32b097b3f2 | ||
|
|
22394def78 | ||
|
|
68ecb7fbdd | ||
|
|
de98a53b43 | ||
|
|
1c9fbf92c6 | ||
|
|
c068b05232 | ||
|
|
15338ecb18 | ||
|
|
01e9a8f720 | ||
|
|
4bdfe64afb | ||
|
|
b7b752b92f | ||
|
|
b7bcd204b4 | ||
|
|
ec934ba3c0 | ||
|
|
7373639f57 | ||
|
|
d718527a1f | ||
|
|
48add0f293 | ||
|
|
a4685229c9 | ||
|
|
f0bc4d26a0 | ||
|
|
1d3b93d88d | ||
|
|
62752ba7e1 | ||
|
|
6a4f420187 | ||
|
|
6640632683 | ||
|
|
09d5d802d0 | ||
|
|
fea23ed6d4 | ||
|
|
10a6c4dc7a | ||
|
|
4cdaa72224 | ||
|
|
27bc1ca5d1 | ||
|
|
1ea49188c9 | ||
|
|
3084ef129c | ||
|
|
c0d112f858 | ||
|
|
2265dda84c | ||
|
|
263b094cab | ||
|
|
fbd13614a5 | ||
|
|
9eab74b595 | ||
|
|
5acdb041a9 | ||
|
|
0494d7ebe3 | ||
|
|
9a8442c946 | ||
|
|
e1dcd0b441 | ||
|
|
a152db7054 | ||
|
|
b9e092674e | ||
|
|
4162b5f41d | ||
|
|
67ae6f210f | ||
|
|
f6c5a46626 | ||
|
|
d6f7e01c53 | ||
|
|
46463e4e24 | ||
|
|
bc99509395 | ||
|
|
5c420f3a34 | ||
|
|
393712ead2 | ||
|
|
d3060b0060 | ||
|
|
14d7f04a81 | ||
|
|
1a28e5e0d4 | ||
|
|
884cd0d636 | ||
|
|
6a7a3c0ae8 | ||
|
|
948e6bd57c | ||
|
|
78595fba0b | ||
|
|
8020284b12 | ||
|
|
d6a49da870 | ||
|
|
84da80356d | ||
|
|
bcbb85eac3 | ||
|
|
0e1d8a72e6 | ||
|
|
bbdd698869 | ||
|
|
bae1e1ee9f | ||
|
|
7138785500 | ||
|
|
8f684ffa6d | ||
|
|
9be3666fe7 | ||
|
|
b7785678f4 | ||
|
|
d8005b4cf6 | ||
|
|
52028fc3bc | ||
|
|
5285ec23ae | ||
|
|
3c882e5c57 | ||
|
|
1a33f9168b | ||
|
|
ccae3d7383 | ||
|
|
847651a90a | ||
|
|
1b8998e7a2 | ||
|
|
dc8fb79759 | ||
|
|
6b0935d6cf | ||
|
|
d1183ce272 | ||
|
|
a1aec8178a | ||
|
|
ad569a8a36 | ||
|
|
0d9fdbaac1 | ||
|
|
f5cd3eab9e | ||
|
|
cb6fe4bb59 | ||
|
|
db36bc67f1 | ||
|
|
e0f72a6193 | ||
|
|
1ee684b7c0 | ||
|
|
93005512b4 | ||
|
|
8987cd64a0 | ||
|
|
fac51dcf03 | ||
|
|
01101a4c9b | ||
|
|
d561e40817 | ||
|
|
b8094fd771 | ||
|
|
af5d9c952d | ||
|
|
ce4e187cbc | ||
|
|
821c80b61e | ||
|
|
5a6fb7c973 | ||
|
|
0cb298ebdf | ||
|
|
0f385f9f4e | ||
|
|
a149368725 | ||
|
|
afeefe8259 | ||
|
|
690d3c27a2 | ||
|
|
3d56ea5ce5 | ||
|
|
fdff7f80a3 | ||
|
|
fe6978b107 | ||
|
|
57db6865d2 | ||
|
|
d235d5ab28 | ||
|
|
c47c15ee47 | ||
|
|
613dfe06d3 | ||
|
|
6803ad2e59 | ||
|
|
d5a791b470 | ||
|
|
a312d61d68 | ||
|
|
e414c1f7b0 | ||
|
|
955359b073 | ||
|
|
26e0c0887a | ||
|
|
4c295b564a | ||
|
|
2eb52da0db | ||
|
|
d8bfb3ab13 | ||
|
|
d970e93507 | ||
|
|
e6255081a8 | ||
|
|
623db0ed94 | ||
|
|
0e575e9c25 | ||
|
|
fb23ba9878 | ||
|
|
4e09fc7f43 | ||
|
|
64cfdd815f | ||
|
|
f6f31e0a8d | ||
|
|
bd5fb9be03 | ||
|
|
762714de68 | ||
|
|
82a3651a18 | ||
|
|
dd9cdb0ec9 | ||
|
|
7f082a821d | ||
|
|
abe0352de9 | ||
|
|
4cee4aa5a8 | ||
|
|
9c68c7c50b | ||
|
|
0608782cfa | ||
|
|
edeaf3794a | ||
|
|
fe2b8c8afa | ||
|
|
b66bf58064 | ||
|
|
957dfa9cdf | ||
|
|
cc9264854e | ||
|
|
d1463b3e24 | ||
|
|
f1082520e1 | ||
|
|
733c563194 | ||
|
|
0200d043c3 | ||
|
|
9c475c36e7 | ||
|
|
c663c5c507 | ||
|
|
1e93c38307 | ||
|
|
81baf808c9 | ||
|
|
74537689dc | ||
|
|
12ab01d5e6 | ||
|
|
044d3a0ff9 | ||
|
|
659cae6a4c | ||
|
|
8efc38ad82 | ||
|
|
bd5882f0f0 | ||
|
|
6ff9ba9df9 | ||
|
|
b2df398a12 | ||
|
|
83d618e1eb | ||
|
|
f0768b3af1 | ||
|
|
0233ce52ed | ||
|
|
6e6f337509 | ||
|
|
1546415b8f | ||
|
|
20725c69bf | ||
|
|
90613220c6 | ||
|
|
659fd2ae93 | ||
|
|
29d899f7da | ||
|
|
902a0a01a9 | ||
|
|
8001fb3915 | ||
|
|
e81e2802f0 | ||
|
|
1ee066ec42 | ||
|
|
53d54d1c4a | ||
|
|
10082b60b8 | ||
|
|
c5b9773922 | ||
|
|
de11323d28 | ||
|
|
9f269e1a95 | ||
|
|
e4204168a0 | ||
|
|
9c350f8ef1 | ||
|
|
db19fdac29 | ||
|
|
d516b238b1 | ||
|
|
f9330f6cd9 | ||
|
|
360da29e1f | ||
|
|
9cfac1642a | ||
|
|
db90e87d10 | ||
|
|
b7564080bc | ||
|
|
1d783bf6c7 | ||
|
|
1025c2e3a1 | ||
|
|
4fd82ab222 | ||
|
|
8eadfc1bf6 | ||
|
|
f66edbad50 | ||
|
|
c7f17b5319 | ||
|
|
23c4adcef6 | ||
|
|
808542bed0 | ||
|
|
93bfd57856 | ||
|
|
7e7e1bccba | ||
|
|
34f6da86c3 | ||
|
|
15c0381c3c | ||
|
|
c2f4a57e02 | ||
|
|
f945cf2343 | ||
|
|
5bca3cfd71 | ||
|
|
26ce4e6886 | ||
|
|
f5f0e0c376 | ||
|
|
9dea1e7f3e | ||
|
|
c2e0f8c81f | ||
|
|
d341bc25ce | ||
|
|
0379e2b51b | ||
|
|
e79026b840 | ||
|
|
fc34d6b56f | ||
|
|
2a1571a99e | ||
|
|
c158608255 | ||
|
|
3ca590b185 | ||
|
|
3f8ee21849 | ||
|
|
845b88a193 | ||
|
|
e252972c7f | ||
|
|
a9012ebfc5 | ||
|
|
5cfd9bbbbd | ||
|
|
c82a7240bb | ||
|
|
a4a20d92a4 | ||
|
|
890996f595 | ||
|
|
474f27c6d3 | ||
|
|
33f3894372 | ||
|
|
24436ac76e | ||
|
|
3ee66ef705 | ||
|
|
a1765e1d33 | ||
|
|
765e3dbf72 | ||
|
|
80f5cee599 | ||
|
|
4dcb124693 | ||
|
|
31ecf167cc | ||
|
|
3999480d64 | ||
|
|
9dbb503c23 | ||
|
|
a98f803d87 | ||
|
|
9e9ffeb5d5 | ||
|
|
33d4ad4d84 | ||
|
|
d05d418c4c | ||
|
|
06d0af7a1d | ||
|
|
9a3b726068 | ||
|
|
2676ab9a59 | ||
|
|
a1837d553e | ||
|
|
fdbc130d8d | ||
|
|
4b3cea3812 | ||
|
|
1c3082ffa6 | ||
|
|
0446cfdba0 | ||
|
|
db1d3183b6 | ||
|
|
fb666394fc | ||
|
|
1054c89a9d | ||
|
|
8dd87dc482 | ||
|
|
b2edbf05a1 | ||
|
|
6fb53a406b | ||
|
|
b05fa0821d | ||
|
|
0a808b1212 | ||
|
|
f1d83e92a7 | ||
|
|
31b60f7f60 | ||
|
|
c0f9af5daa | ||
|
|
b25a9e8884 | ||
|
|
3c0cf3cd55 | ||
|
|
1ac6f17e6a | ||
|
|
399a2b38f3 | ||
|
|
b97221cdb2 | ||
|
|
0164bc21ea | ||
|
|
5a23250d32 | ||
|
|
80d88d9789 | ||
|
|
31ead854c7 | ||
|
|
4b64fcb8a4 | ||
|
|
a951f2403d | ||
|
|
f9adeba7f1 | ||
|
|
5c823d51d0 | ||
|
|
9be7521b83 | ||
|
|
c73ddc3552 | ||
|
|
4b7f058f41 | ||
|
|
07221a1b20 | ||
|
|
13614fb3c4 | ||
|
|
4fa983bde7 | ||
|
|
9cb1db8c0a | ||
|
|
5738436d55 | ||
|
|
5e49b38c33 | ||
|
|
0c94adaff9 | ||
|
|
f8a6c5d06c | ||
|
|
21e66c7c02 | ||
|
|
902f0d3ac4 | ||
|
|
713ecd35f6 | ||
|
|
27b35157cd | ||
|
|
f8fb639870 | ||
|
|
14f41ae619 | ||
|
|
a026d72924 | ||
|
|
2cb070f5b3 | ||
|
|
1dec956e99 | ||
|
|
310394aa60 | ||
|
|
468ff18243 | ||
|
|
44a63580f0 | ||
|
|
4ac1fa43aa | ||
|
|
6f992a3cf7 | ||
|
|
fd4ce656d5 | ||
|
|
9ed2dca427 | ||
|
|
dfb804fe3f | ||
|
|
4f2a84b426 | ||
|
|
14a127b6b3 | ||
|
|
06000533fb | ||
|
|
7722aba403 | ||
|
|
4817d8c67f | ||
|
|
9a062d90d1 | ||
|
|
959eb45373 | ||
|
|
a42f2af9eb | ||
|
|
4ddad68212 | ||
|
|
aac6c5a1c7 | ||
|
|
5572e31fd4 | ||
|
|
233b8bf81a | ||
|
|
2ae3810f80 | ||
|
|
736165876c | ||
|
|
61b3fca9a3 | ||
|
|
469863b7b3 | ||
|
|
5238bc55fd | ||
|
|
57a01aa6ff | ||
|
|
9361dbc39e | ||
|
|
11d257cb26 | ||
|
|
a928ab75e3 | ||
|
|
55a240c82e | ||
|
|
f8aedf438b | ||
|
|
9f1bb9a42e | ||
|
|
0ed7274610 | ||
|
|
df032b09a7 | ||
|
|
7dba742e4c | ||
|
|
83dbe78965 | ||
|
|
95b75c5330 | ||
|
|
780bd08490 | ||
|
|
a9b1f38a7c | ||
|
|
4ed4ad9852 | ||
|
|
81f172315c | ||
|
|
54666221a4 | ||
|
|
5327d702f8 | ||
|
|
a701ea3007 | ||
|
|
f0fb7d9661 | ||
|
|
3cbc89769d | ||
|
|
cb95200be3 | ||
|
|
a52f6c0acf | ||
|
|
77f9e3dd41 | ||
|
|
259d3e2df1 | ||
|
|
6eee5421b0 | ||
|
|
8004e9c943 | ||
|
|
2ab8511f45 | ||
|
|
7514ff53c9 | ||
|
|
309cfb1499 | ||
|
|
a567f7ed20 | ||
|
|
5720936247 | ||
|
|
5d9de14ca3 | ||
|
|
f519f56078 | ||
|
|
5eb1a1f7f5 | ||
|
|
5a28560177 | ||
|
|
db280adf55 | ||
|
|
b77fcd6c8a | ||
|
|
b5b2649283 | ||
|
|
318f9b216d | ||
|
|
61247a0b2a | ||
|
|
849a418273 | ||
|
|
2e63a62e08 | ||
|
|
6ccf1f2a3c | ||
|
|
e298256b82 | ||
|
|
787e5b2e29 | ||
|
|
4aa1e8b093 | ||
|
|
9ee224c36b | ||
|
|
08263c0597 | ||
|
|
347fe87229 | ||
|
|
b65a0a3a8d | ||
|
|
9a5a1e2253 | ||
|
|
687b4ec837 | ||
|
|
8bdf5c554d | ||
|
|
f4a18e531f | ||
|
|
df951a0c7c | ||
|
|
a6cac2691b | ||
|
|
a9f5179066 | ||
|
|
687e2699cf | ||
|
|
491da0ceb9 | ||
|
|
1bac40bc58 | ||
|
|
fb9061480d | ||
|
|
a04cf100b4 | ||
|
|
76253bf516 | ||
|
|
feaf70922d | ||
|
|
c70343a5bc | ||
|
|
550c116aea | ||
|
|
a5f31a4280 | ||
|
|
27fc4c4ca8 | ||
|
|
90a5f17f58 | ||
|
|
108cb91d95 | ||
|
|
00a0755ff3 | ||
|
|
3f7e8c88eb | ||
|
|
1c7ca94d49 | ||
|
|
31273cd6ff | ||
|
|
14e39dd745 | ||
|
|
cc6f7b6088 | ||
|
|
da1b0c9558 | ||
|
|
9f294b4d10 | ||
|
|
13f60bae41 | ||
|
|
67105b332f | ||
|
|
18961e3d07 | ||
|
|
fe31f5050d | ||
|
|
ab8549adea | ||
|
|
05600601ff | ||
|
|
c541356289 | ||
|
|
467c4360ca | ||
|
|
3b152a38b0 | ||
|
|
f4d3855528 | ||
|
|
09eab770a7 | ||
|
|
102f8ab74e | ||
|
|
a830dba5da | ||
|
|
3f13a50ca3 | ||
|
|
7c456f2ab9 | ||
|
|
bae95cd6f6 | ||
|
|
bbd6e443b0 | ||
|
|
db0d847e03 | ||
|
|
0afb453fed | ||
|
|
dbc79b4311 | ||
|
|
ac6008c33c | ||
|
|
e540e752f2 | ||
|
|
cdbe821eb8 | ||
|
|
6be994f1ca | ||
|
|
a407b0a8eb | ||
|
|
051ff35878 | ||
|
|
8b3c34c308 | ||
|
|
2cb2668803 | ||
|
|
4dccdb95b9 | ||
|
|
96db9a9410 | ||
|
|
0cd34bbebc | ||
|
|
15f50c0e58 | ||
|
|
7fca9732e7 | ||
|
|
0ea8c3ed28 | ||
|
|
0af9600e92 | ||
|
|
328e3725e5 | ||
|
|
2183e1e9f5 | ||
|
|
120d0be84c | ||
|
|
5649f75a8d | ||
|
|
a209f7d6be | ||
|
|
d48a2f3ccf | ||
|
|
c1ae36866e | ||
|
|
4f368923a5 | ||
|
|
7c02097d93 | ||
|
|
51998f706f | ||
|
|
1a3df08aca | ||
|
|
975f262ac0 | ||
|
|
1cb4a3b8d5 | ||
|
|
35f4b2f686 | ||
|
|
407ec91ca7 | ||
|
|
12c0d18932 | ||
|
|
2d4ca37226 | ||
|
|
afe6744e97 | ||
|
|
19d4b8b7f7 | ||
|
|
3556942516 | ||
|
|
87a200e42c | ||
|
|
152fc0ad38 | ||
|
|
3212ae4713 | ||
|
|
040cef1479 | ||
|
|
f5f70d7a75 | ||
|
|
42509cf2f5 | ||
|
|
6f74c2d823 | ||
|
|
e23a6dc9f1 | ||
|
|
134c6b79c4 | ||
|
|
00ff1447ee | ||
|
|
78f6cb08d8 | ||
|
|
dfd890c8a6 | ||
|
|
7457b3668b | ||
|
|
71e7cd5808 | ||
|
|
57e42af238 | ||
|
|
e065dcb816 | ||
|
|
9619c7f54d | ||
|
|
2508bed363 | ||
|
|
c469632ee0 | ||
|
|
44a52359dc | ||
|
|
2022551b26 | ||
|
|
baac067a1a | ||
|
|
f4216dd67f | ||
|
|
60186bdcd5 | ||
|
|
2c2eb1684b | ||
|
|
33b167215d | ||
|
|
c53db134c6 | ||
|
|
0513a21e25 | ||
|
|
2fc32414f5 | ||
|
|
309bc4ee4c | ||
|
|
7977e6fb16 | ||
|
|
abb19dfbf8 | ||
|
|
14676dc3f8 | ||
|
|
c16f8a4d46 | ||
|
|
48bf09da21 | ||
|
|
c295a1998a | ||
|
|
95f7b9443f | ||
|
|
0160f5dd30 | ||
|
|
f3097845b4 | ||
|
|
5e72de4ba2 | ||
|
|
7a64530e83 | ||
|
|
36f3be9979 | ||
|
|
451b965fb0 | ||
|
|
2b2852aad7 | ||
|
|
72bfd94329 | ||
|
|
300376b0b1 | ||
|
|
e67792177d | ||
|
|
0f24fd593e | ||
|
|
23ec6c721d | ||
|
|
26761e5445 | ||
|
|
e78e4e6a2e | ||
|
|
e765b7a9c4 | ||
|
|
c210e34ce3 | ||
|
|
ddd063f29e | ||
|
|
a2c96e9cdd | ||
|
|
f2416d68b8 | ||
|
|
1eccb61d44 | ||
|
|
cf2b189fbd | ||
|
|
394b69676a | ||
|
|
9704dc5734 | ||
|
|
f54dde4f78 | ||
|
|
70ef9fbcfe | ||
|
|
bb1aff84cf | ||
|
|
dc1ec77da5 | ||
|
|
7077b20a54 | ||
|
|
09e84c2583 | ||
|
|
e3ac6f9e01 | ||
|
|
f91bbe9397 | ||
|
|
31faf05c3a | ||
|
|
55672410cd | ||
|
|
8a66857fb9 | ||
|
|
d0b37df615 | ||
|
|
dc6cb68327 | ||
|
|
72250b32d3 | ||
|
|
726130be09 | ||
|
|
968a29d869 | ||
|
|
ce27e973be | ||
|
|
2607866c49 | ||
|
|
e8e914b11c | ||
|
|
d22b3b0d88 | ||
|
|
5ece1d74f6 | ||
|
|
ac7ab42d94 | ||
|
|
38a3f538f5 | ||
|
|
998935ea55 | ||
|
|
e781ac2512 | ||
|
|
ef2695974d | ||
|
|
b552cc2b12 | ||
|
|
4ae9c2445d | ||
|
|
769e25f080 | ||
|
|
86a23849e0 | ||
|
|
a586fef414 | ||
|
|
a548c63a92 | ||
|
|
5268df6bfd | ||
|
|
82e1c0f810 | ||
|
|
81b0ffb7f4 | ||
|
|
0da130ee2c | ||
|
|
6e880c9027 | ||
|
|
082fa321cb | ||
|
|
ff1c49f111 | ||
|
|
50f592c540 | ||
|
|
7a7f66dfdc | ||
|
|
a1140aa62f | ||
|
|
2dd3564da1 | ||
|
|
fb4b0a187e | ||
|
|
ac48ee066e | ||
|
|
06031efc09 | ||
|
|
9bea80b862 | ||
|
|
92ecb1c7ec | ||
|
|
645f77b849 | ||
|
|
2f9381065d | ||
|
|
774ef61c2f | ||
|
|
dd2c66d2e1 | ||
|
|
0deb2d78fb | ||
|
|
fdd7e7f2a8 | ||
|
|
ad1a440576 | ||
|
|
222b5cb587 | ||
|
|
a1d1f73fe7 | ||
|
|
e7f9ace559 | ||
|
|
cb72c404f5 | ||
|
|
01b9bf5289 | ||
|
|
a52a66ec1c | ||
|
|
313d7089da | ||
|
|
06d80e92eb | ||
|
|
e1fc3aa4fb | ||
|
|
b8fe8d465e | ||
|
|
196d3cb13d | ||
|
|
a3bfa13670 | ||
|
|
2ace0defd0 | ||
|
|
023a902f61 | ||
|
|
789a4c03df | ||
|
|
ecfd8e8a62 | ||
|
|
9ba44f3e6e | ||
|
|
03fd5c84ec | ||
|
|
81e0f170ef | ||
|
|
7e06ba1728 | ||
|
|
f8a5825083 | ||
|
|
a14f7f215c | ||
|
|
8393e8c52f | ||
|
|
81d221667b |
@@ -7,8 +7,7 @@ insert_final_newline = true
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
[*.{md,yml,yaml,json,toml}]
|
||||
[*.{md,yml,yaml,json,toml,htm,html}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
46
.github/CODE_OF_CONDUCT.md
vendored
Normal file
46
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at andrey.nering@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
6
.github/CONTRIBUTING.md
vendored
Normal file
6
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
* Bug reports and feature requests are welcome in [the issues][issues]
|
||||
* Pull Requests are welcome. For more complex changes and features it's
|
||||
recommended to open an issue with the feature request first
|
||||
* Documentation contributions are as important as code contributions
|
||||
|
||||
[issues]: https://github.com/go-task/task/issues
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
open_collective: task
|
||||
patreon: andreynering
|
||||
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<!--
|
||||
If relevant, include the following information:
|
||||
- Task version
|
||||
- OS
|
||||
- Example Taskfile showing the issue
|
||||
-->
|
||||
26
.github/workflows/release.yml
vendored
Normal file
26
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14.x
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v1
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
30
.github/workflows/test.yml
vendored
Normal file
30
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Test
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.13.x, 1.14.x]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{matrix.platform}}
|
||||
steps:
|
||||
- name: Set up Go ${{matrix.go-version}}
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: ${{matrix.go-version}}
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download Go modules
|
||||
run: go mod download
|
||||
env:
|
||||
GOPROXY: https://proxy.golang.org
|
||||
|
||||
- name: Build
|
||||
run: go build -o ./bin/task -v ./cmd/task
|
||||
|
||||
- name: Test
|
||||
run: ./bin/task test
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -14,12 +14,16 @@
|
||||
.glide/
|
||||
|
||||
./task
|
||||
.task
|
||||
dist/
|
||||
|
||||
vendor/**
|
||||
!vendor/**/*.go
|
||||
!vendor/**/LICENSE
|
||||
!vendor/**/COPYING
|
||||
!vendor/**/README
|
||||
!vendor/**/README.md
|
||||
vendor/**/*_test.go
|
||||
.DS_Store
|
||||
|
||||
# intellij idea/goland
|
||||
.idea/
|
||||
|
||||
# exuberant ctags
|
||||
tags
|
||||
|
||||
/bin
|
||||
/testdata/vars/v1
|
||||
|
||||
43
.goreleaser.yml
Normal file
43
.goreleaser.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
build:
|
||||
binary: task
|
||||
main: cmd/task/task.go
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
archives:
|
||||
- name_template: "{{.Binary}}_{{.Os}}_{{.Arch}}"
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
release:
|
||||
draft: true
|
||||
|
||||
snapshot:
|
||||
name_template: "{{.Tag}}"
|
||||
|
||||
checksum:
|
||||
name_template: "task_checksums.txt"
|
||||
|
||||
nfpms:
|
||||
- vendor: Task
|
||||
homepage: https://github.com/go-task/task
|
||||
maintainer: Andrey Nering <andrey.nering@gmail.com>
|
||||
description: Simple task runner written in Go
|
||||
license: MIT
|
||||
conflicts:
|
||||
- taskwarrior
|
||||
formats:
|
||||
- deb
|
||||
- rpm
|
||||
file_name_template: "{{.ProjectName}}_{{.Os}}_{{.Arch}}"
|
||||
@@ -1,9 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.7
|
||||
- 1.8
|
||||
script:
|
||||
- go install github.com/go-task/task/cmd/task
|
||||
- task dl-deps
|
||||
- task lint
|
||||
- task test
|
||||
280
CHANGELOG.md
Normal file
280
CHANGELOG.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Changelog
|
||||
|
||||
## v3.0.0
|
||||
|
||||
- On `v3`, all CLI variables will be considered global variables
|
||||
([#336](https://github.com/go-task/task/issues/336), [#341](https://github.com/go-task/task/pull/341))
|
||||
- Add support to `.env` like files
|
||||
([#324](https://github.com/go-task/task/issues/324), [#356](https://github.com/go-task/task/pull/356)).
|
||||
- Add `label:` to task so you can override the task name in the logs
|
||||
([#321](https://github.com/go-task/task/issues/321]), [#337](https://github.com/go-task/task/pull/337)).
|
||||
- Refactor how variables work on version 3
|
||||
([#311](https://github.com/go-task/task/pull/311)).
|
||||
- Disallow `expansions` on v3 since it has no effect.
|
||||
- `Taskvars.yml` is not automatically included anymore.
|
||||
- `Taskfile_{{OS}}.yml` is not automatically included anymore.
|
||||
- Allow interpolation on `includes`, so you can manually include a Taskfile
|
||||
based on operation system, for example.
|
||||
- Expose `.TASK` variable in templates with the task name
|
||||
([#252](https://github.com/go-task/task/issues/252)).
|
||||
- Implement short task syntax
|
||||
([#194](https://github.com/go-task/task/issues/194), [#240](https://github.com/go-task/task/pull/240)).
|
||||
- Added option to make included Taskfile run commands on its own directory
|
||||
([#260](https://github.com/go-task/task/issues/260), [#144](https://github.com/go-task/task/issues/144))
|
||||
- Taskfiles in version 1 are not supported anymore
|
||||
([#237](https://github.com/go-task/task/pull/237)).
|
||||
- Added global `method:` option. With this option, you can set a default
|
||||
method to all tasks in a Taskfile
|
||||
([#246](https://github.com/go-task/task/issues/246)).
|
||||
- Changed default method from `timestamp` to `checksum`
|
||||
([#246](https://github.com/go-task/task/issues/246)).
|
||||
- New magic variables are now available when using `status:`:
|
||||
`.TIMESTAMP` which contains the greatest modification date
|
||||
from the files listed in `sources:`, and `.CHECKSUM`, which
|
||||
contains a checksum of all files listed in `status:`.
|
||||
This is useful for manual checking when using external, or even remote,
|
||||
artifacts when using `status:`
|
||||
([#216](https://github.com/go-task/task/pull/216)).
|
||||
- We're now using [slim-sprig](https://github.com/go-task/slim-sprig) instead of
|
||||
[sprig](https://github.com/Masterminds/sprig), which allowed a file size
|
||||
reduction of about 22%
|
||||
([#219](https://github.com/go-task/task/pull/219)).
|
||||
- We now use some colors on Task output to better distinguish message types -
|
||||
commands are green, errors are red, etc
|
||||
([#207](https://github.com/go-task/task/pull/207)).
|
||||
|
||||
## v2.8.1 - 2019-05-20
|
||||
|
||||
- Fix error code for the `--help` flag
|
||||
([#300](https://github.com/go-task/task/issues/300), [#330](https://github.com/go-task/task/pull/330)).
|
||||
- Print version to stdout instead of stderr
|
||||
([#299](https://github.com/go-task/task/issues/299), [#329](https://github.com/go-task/task/pull/329)).
|
||||
- Supress `context` errors when using the `--watch` flag
|
||||
([#313](https://github.com/go-task/task/issues/313), [#317](https://github.com/go-task/task/pull/317)).
|
||||
- Support templating on description
|
||||
([#276](https://github.com/go-task/task/issues/276), [#283](https://github.com/go-task/task/pull/283)).
|
||||
|
||||
## v2.8.0 - 2019-12-07
|
||||
|
||||
- Add `--parallel` flag (alias `-p`) to run tasks given by the command line in
|
||||
parallel
|
||||
([#266](https://github.com/go-task/task/pull/266)).
|
||||
- Fixed bug where calling the `task` CLI only informing global vars would not
|
||||
execute the `default` task.
|
||||
- Add hability to silent all tasks by adding `silent: true` a the root of the
|
||||
Taskfile.
|
||||
|
||||
## v2.7.1 - 2019-11-10
|
||||
|
||||
- Fix error being raised when `exit 0` was called
|
||||
([#251](https://github.com/go-task/task/issues/251)).
|
||||
|
||||
## v2.7.0 - 2019-09-22
|
||||
|
||||
- Fixed panic bug when assigning a global variable
|
||||
([#229](https://github.com/go-task/task/issues/229), [#243](https://github.com/go-task/task/issues/234)).
|
||||
- A task with `method: checksum` will now re-run if generated files are deleted
|
||||
([#228](https://github.com/go-task/task/pull/228), [#238](https://github.com/go-task/task/issues/238)).
|
||||
|
||||
## v2.6.0 - 2019-07-21
|
||||
|
||||
- Fixed some bugs regarding minor version checks on `version:`.
|
||||
- Add `preconditions:` to task
|
||||
([#205](https://github.com/go-task/task/pull/205)).
|
||||
- Create directory informed on `dir:` if it doesn't exist
|
||||
([#209](https://github.com/go-task/task/issues/209), [#211](https://github.com/go-task/task/pull/211)).
|
||||
- We now have a `--taskfile` flag (alias `-t`), which can be used to run
|
||||
another Taskfile (other than the default `Taskfile.yml`)
|
||||
([#221](https://github.com/go-task/task/pull/221)).
|
||||
- It's now possible to install Task using Homebrew on Linux
|
||||
([go-task/homebrew-tap#1](https://github.com/go-task/homebrew-tap/pull/1)).
|
||||
|
||||
## v2.5.2 - 2019-05-11
|
||||
|
||||
- Reverted YAML upgrade due issues with CRLF on Windows
|
||||
([#201](https://github.com/go-task/task/issues/201), [go-yaml/yaml#450](https://github.com/go-yaml/yaml/issues/450)).
|
||||
- Allow setting global variables through the CLI
|
||||
([#192](https://github.com/go-task/task/issues/192)).
|
||||
|
||||
## 2.5.1 - 2019-04-27
|
||||
|
||||
- Fixed some issues with interactive command line tools, where sometimes
|
||||
the output were not being shown, and similar issues
|
||||
([#114](https://github.com/go-task/task/issues/114), [#190](https://github.com/go-task/task/issues/190), [#200](https://github.com/go-task/task/pull/200)).
|
||||
- Upgraded [go-yaml/yaml](https://github.com/go-yaml/yaml) from v2 to v3.
|
||||
|
||||
## v2.5.0 - 2019-03-16
|
||||
|
||||
- We moved from the taskfile.org domain to the new fancy taskfile.dev domain.
|
||||
While stuff is being redirected, we strongly recommend to everyone that use
|
||||
[this install script](https://taskfile.dev/#/installation?id=install-script)
|
||||
to use the new taskfile.dev domain on scripts from now on.
|
||||
- Fixed to the ZSH completion
|
||||
([#182](https://github.com/go-task/task/pull/182)).
|
||||
- Add [`--summary` flag along with `summary:` task attribute](https://taskfile.org/#/usage?id=display-summary-of-task)
|
||||
([#180](https://github.com/go-task/task/pull/180)).
|
||||
|
||||
## v2.4.0 - 2019-02-21
|
||||
|
||||
- Allow calling a task of the root Taskfile from an included Taskfile
|
||||
by prefixing it with `:`
|
||||
([#161](https://github.com/go-task/task/issues/161), [#172](https://github.com/go-task/task/issues/172)),
|
||||
- Add flag to override the `output` option
|
||||
([#173](https://github.com/go-task/task/pull/173));
|
||||
- Fix bug where Task was persisting the new checksum on the disk when the Dry
|
||||
Mode is enabled
|
||||
([#166](https://github.com/go-task/task/issues/166));
|
||||
- Fix file timestamp issue when the file name has spaces
|
||||
([#176](https://github.com/go-task/task/issues/176));
|
||||
- Mitigating path expanding issues on Windows
|
||||
([#170](https://github.com/go-task/task/pull/170)).
|
||||
|
||||
## v2.3.0 - 2019-01-02
|
||||
|
||||
- On Windows, Task can now be installed using [Scoop](https://scoop.sh/)
|
||||
([#152](https://github.com/go-task/task/pull/152));
|
||||
- Fixed issue with file/directory globing
|
||||
([#153](https://github.com/go-task/task/issues/153));
|
||||
- Added ability to globally set environment variables
|
||||
(
|
||||
[#138](https://github.com/go-task/task/pull/138),
|
||||
[#159](https://github.com/go-task/task/pull/159)
|
||||
).
|
||||
|
||||
## v2.2.1 - 2018-12-09
|
||||
|
||||
- This repository now uses Go Modules (#143). We'll still keep the `vendor` directory in sync for some time, though;
|
||||
- Fixing a bug when the Taskfile has no tasks but includes another Taskfile (#150);
|
||||
- Fix a bug when calling another task or a dependency in an included Taskfile (#151).
|
||||
|
||||
## v2.2.0 - 2018-10-25
|
||||
|
||||
- Added support for [including other Taskfiles](https://taskfile.org/#/usage?id=including-other-taskfiles) (#98)
|
||||
- This should be considered experimental. For now, only including local files is supported, but support for including remote Taskfiles is being discussed. If you have any feedback, please comment on #98.
|
||||
- Task now have a dedicated documentation site: https://taskfile.org
|
||||
- Thanks to [Docsify](https://docsify.js.org/) for making this pretty easy. To check the source code, just take a look at the [docs](https://github.com/go-task/task/tree/master/docs) directory of this repository. Contributions to the documentation is really appreciated.
|
||||
|
||||
## v2.1.1 - 2018-09-17
|
||||
|
||||
- Fix suggestion to use `task --init` not being shown anymore (when a `Taskfile.yml` is not found)
|
||||
- Fix error when using checksum method and no file exists for a source glob (#131)
|
||||
- Fix signal handling when the `--watch` flag is given (#132)
|
||||
|
||||
## v2.1.0 - 2018-08-19
|
||||
|
||||
- Add a `ignore_error` option to task and command (#123)
|
||||
- Add a dry run mode (`--dry` flag) (#126)
|
||||
|
||||
## v2.0.3 - 2018-06-24
|
||||
|
||||
- Expand environment variables on "dir", "sources" and "generates" (#116)
|
||||
- Fix YAML merging syntax (#112)
|
||||
- Add ZSH completion (#111)
|
||||
- Implement new `output` option. Please check out the [documentation](https://github.com/go-task/task#output-syntax)
|
||||
|
||||
## v2.0.2 - 2018-05-01
|
||||
|
||||
- Fix merging of YAML anchors (#112)
|
||||
|
||||
## v2.0.1 - 2018-03-11
|
||||
|
||||
- Fixes panic on `task --list`
|
||||
|
||||
## v2.0.0 - 2018-03-08
|
||||
|
||||
Version 2.0.0 is here, with a new Taskfile format.
|
||||
|
||||
Please, make sure to read the [Taskfile versions](https://github.com/go-task/task/blob/master/TASKFILE_VERSIONS.md) document, since it describes in depth what changed for this version.
|
||||
|
||||
* New Taskfile version 2 (https://github.com/go-task/task/issues/77)
|
||||
* Possibility to have global variables in the `Taskfile.yml` instead of `Taskvars.yml` (https://github.com/go-task/task/issues/66)
|
||||
* Small improvements and fixes
|
||||
|
||||
## v1.4.4 - 2017-11-19
|
||||
|
||||
- Handle SIGINT and SIGTERM (#75);
|
||||
- List: print message with there's no task with description;
|
||||
- Expand home dir ("~" symbol) on paths (#74);
|
||||
- Add Snap as an installation method;
|
||||
- Move examples to its own repo;
|
||||
- Watch: also walk on tasks called on on "cmds", and not only on "deps";
|
||||
- Print logs to stderr instead of stdout (#68);
|
||||
- Remove deprecated `set` keyword;
|
||||
- Add checksum based status check, alternative to timestamp based.
|
||||
|
||||
## v1.4.3 - 2017-09-07
|
||||
|
||||
- Allow assigning variables to tasks at run time via CLI (#33)
|
||||
- Added suport for multiline variables from sh (#64)
|
||||
- Fixes env: remove square braces and evaluate shell (#62)
|
||||
- Watch: change watch library and few fixes and improvements
|
||||
- When use watching, cancel and restart long running process on file change (#59 and #60)
|
||||
|
||||
## v1.4.2 - 2017-07-30
|
||||
|
||||
- Flag to set directory of execution
|
||||
- Always echo command if is verbose mode
|
||||
- Add silent mode to disable echoing of commands
|
||||
- Fixes and improvements of variables (#56)
|
||||
|
||||
## v1.4.1 - 2017-07-15
|
||||
|
||||
- Allow use of YAML for dynamic variables instead of $ prefix
|
||||
- `VAR: {sh: echo Hello}` instead of `VAR: $echo Hello`
|
||||
- Add `--list` (or `-l`) flag to print existing tasks
|
||||
- OS specific Taskvars file (e.g. `Taskvars_windows.yml`, `Taskvars_linux.yml`, etc)
|
||||
- Consider task up-to-date on equal timestamps (#49)
|
||||
- Allow absolute path in generates section (#48)
|
||||
- Bugfix: allow templating when calling deps (#42)
|
||||
- Fix panic for invalid task in cyclic dep detection
|
||||
- Better error output for dynamic variables in Taskvars.yml (#41)
|
||||
- Allow template evaluation in parameters
|
||||
|
||||
## v1.4.0 - 2017-07-06
|
||||
|
||||
- Cache dynamic variables
|
||||
- Add verbose mode (`-v` flag)
|
||||
- Support to task parameters (overriding vars) (#31) (#32)
|
||||
- Print command, also when "set:" is specified (#35)
|
||||
- Improve task command help text (#35)
|
||||
|
||||
## v1.3.1 - 2017-06-14
|
||||
|
||||
- Fix glob not working on commands (#28)
|
||||
- Add ExeExt template function
|
||||
- Add `--init` flag to create a new Taskfile
|
||||
- Add status option to prevent task from running (#27)
|
||||
- Allow interpolation on `generates` and `sources` attributes (#26)
|
||||
|
||||
## v1.3.0 - 2017-04-24
|
||||
|
||||
- Migrate from os/exec.Cmd to a native Go sh/bash interpreter
|
||||
- This is a potentially breaking change if you use Windows.
|
||||
- Now, `cmd` is not used anymore on Windows. Always use Bash-like syntax for your commands, even on Windows.
|
||||
- Add "ToSlash" and "FromSlash" to template functions
|
||||
- Use functions defined on github.com/Masterminds/sprig
|
||||
- Do not redirect stdin while running variables commands
|
||||
- Using `context` and `errgroup` packages (this will make other tasks to be cancelled, if one returned an error)
|
||||
|
||||
## v1.2.0 - 2017-04-02
|
||||
|
||||
- More tests and Travis integration
|
||||
- Watch a task (experimental)
|
||||
- Possibility to call another task
|
||||
- Fix "=" not being reconized in variables/environment variables
|
||||
- Tasks can now have a description, and help will print them (#10)
|
||||
- Task dependencies now run concurrently
|
||||
- Support for a default task (#16)
|
||||
|
||||
## v1.1.0 - 2017-03-08
|
||||
|
||||
- Support for YAML, TOML and JSON (#1)
|
||||
- Support running command in another directory (#4)
|
||||
- `--force` or `-f` flag to force execution of task even when it's up-to-date
|
||||
- Detection of cyclic dependencies (#5)
|
||||
- Support for variables (#6, #9, #14)
|
||||
- Operation System specific commands and variables (#13)
|
||||
|
||||
## v1.0.0 - 2017-02-28
|
||||
|
||||
- Add LICENSE file
|
||||
105
Gopkg.lock
generated
105
Gopkg.lock
generated
@@ -1,105 +0,0 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
packages = ["."]
|
||||
revision = "b26d9c308763d68093482582cea63d69be07a0f0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Masterminds/semver"
|
||||
packages = ["."]
|
||||
revision = "abff1900528dbdaf6f3f5aa92c398be1eaf2a9f7"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/Masterminds/sprig"
|
||||
packages = ["."]
|
||||
revision = "e039e20e500c2c025d9145be375e27cf42a94174"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/aokoli/goutils"
|
||||
packages = ["."]
|
||||
revision = "3391d3790d23d03408670993e957e8f408993c34"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
packages = ["."]
|
||||
revision = "4da3e2cfbabc9f751898f250b49f2439785783a1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/huandu/xstrings"
|
||||
packages = ["."]
|
||||
revision = "3959339b333561bf62a38b424fd41517c2c90f40"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/imdario/mergo"
|
||||
packages = ["."]
|
||||
revision = "d806ba8c21777d504a2090a2ca4913c750dd3a33"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-zglob"
|
||||
packages = [".","fastwalk"]
|
||||
revision = "95345c4e1c0ebc9d16a3284177f09360f4d20fab"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/mvdan/sh"
|
||||
packages = ["interp","syntax"]
|
||||
revision = "c79708369f1c3cde582a511d52173966c25edefd"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/satori/go.uuid"
|
||||
packages = ["."]
|
||||
revision = "5bf94b69c6b68ee1b541973bb8e1144db23a194b"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/crypto"
|
||||
packages = ["pbkdf2","scrypt"]
|
||||
revision = "e1a4589e7d3ea14a3352255d04b6f1a418845e5e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/net"
|
||||
packages = ["context"]
|
||||
revision = "e4fa1c5465ad6111f206fc92186b8c83d64adbe1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
revision = "f52d1811a62927559de87708c8913c1650ce4f26"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sys"
|
||||
packages = ["unix"]
|
||||
revision = "b90f89a1e7a9c1f6b918820b3daa7f08488c8594"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
packages = ["."]
|
||||
revision = "cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "2f626389e1dc4d5ac2e6b83c6b9a50cab0dd3187bbc4e03694810df03a08b3fc"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
103
Gopkg.toml
103
Gopkg.toml
@@ -1,103 +0,0 @@
|
||||
|
||||
## Gopkg.toml example (these lines may be deleted)
|
||||
|
||||
## "metadata" defines metadata about the project that could be used by other independent
|
||||
## systems. The metadata defined here will be ignored by dep.
|
||||
# [metadata]
|
||||
# key1 = "value that convey data to other systems"
|
||||
# system1-data = "value that is used by a system"
|
||||
# system2-data = "value that is used by another system"
|
||||
|
||||
## "required" lists a set of packages (not projects) that must be included in
|
||||
## Gopkg.lock. This list is merged with the set of packages imported by the current
|
||||
## project. Use it when your project needs a package it doesn't explicitly import -
|
||||
## including "main" packages.
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
|
||||
## "ignored" lists a set of packages (not projects) that are ignored when
|
||||
## dep statically analyzes source code. Ignored packages can be in this project,
|
||||
## or in a dependency.
|
||||
# ignored = ["github.com/user/project/badpkg"]
|
||||
|
||||
## Constraints are rules for how directly imported projects
|
||||
## may be incorporated into the depgraph. They are respected by
|
||||
## dep whether coming from the Gopkg.toml of the current project or a dependency.
|
||||
# [[constraint]]
|
||||
## Required: the root import path of the project being constrained.
|
||||
# name = "github.com/user/project"
|
||||
#
|
||||
## Recommended: the version constraint to enforce for the project.
|
||||
## Only one of "branch", "version" or "revision" can be specified.
|
||||
# version = "1.0.0"
|
||||
# branch = "master"
|
||||
# revision = "abc123"
|
||||
#
|
||||
## Optional: an alternate location (URL or import path) for the project's source.
|
||||
# source = "https://github.com/myfork/package.git"
|
||||
#
|
||||
## "metadata" defines metadata about the dependency or override that could be used
|
||||
## by other independent systems. The metadata defined here will be ignored by dep.
|
||||
# [metadata]
|
||||
# key1 = "value that convey data to other systems"
|
||||
# system1-data = "value that is used by a system"
|
||||
# system2-data = "value that is used by another system"
|
||||
|
||||
## Overrides have the same structure as [[constraint]], but supersede all
|
||||
## [[constraint]] declarations from all projects. Only [[override]] from
|
||||
## the current project's are applied.
|
||||
##
|
||||
## Overrides are a sledgehammer. Use them only as a last resort.
|
||||
# [[override]]
|
||||
## Required: the root import path of the project being constrained.
|
||||
# name = "github.com/user/project"
|
||||
#
|
||||
## Optional: specifying a version constraint override will cause all other
|
||||
## constraints on this project to be ignored; only the overridden constraint
|
||||
## need be satisfied.
|
||||
## Again, only one of "branch", "version" or "revision" can be specified.
|
||||
# version = "1.0.0"
|
||||
# branch = "master"
|
||||
# revision = "abc123"
|
||||
#
|
||||
## Optional: specifying an alternate source location as an override will
|
||||
## enforce that the alternate location is used for that project, regardless of
|
||||
## what source location any dependent projects specify.
|
||||
# source = "https://github.com/myfork/package.git"
|
||||
|
||||
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/BurntSushi/toml"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/Masterminds/sprig"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/fsnotify/fsnotify"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/imdario/mergo"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mattn/go-zglob"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/mvdan/sh"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/spf13/pflag"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "golang.org/x/sync"
|
||||
|
||||
[[constraint]]
|
||||
branch = "v2"
|
||||
name = "gopkg.in/yaml.v2"
|
||||
388
README.md
388
README.md
@@ -1,385 +1,23 @@
|
||||
[](https://github.com/go-task/task/blob/master/LICENSE)
|
||||
[](https://gitter.im/go-task/task?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://gophers.slack.com/messages/task)
|
||||
[](http://www.somsubhra.com/github-release-stats/?username=go-task&repository=task)
|
||||
[](https://travis-ci.org/go-task/task)
|
||||

|
||||

|
||||
|
||||
# Task - Simple task runner / "Make" alternative
|
||||
# Task
|
||||
|
||||
Task is a simple tool that allows you to easily run development and build
|
||||
tasks. Task is written in Golang, but can be used to develop any language.
|
||||
It aims to be simpler and easier to use then [GNU Make][make].
|
||||
Task is a task runner / build tool that aims to be simpler and easier to use
|
||||
than, for example, [GNU Make](https://www.gnu.org/software/make/).
|
||||
|
||||
## Installation
|
||||
See [taskfile.dev](https://taskfile.dev) for documentation.
|
||||
|
||||
If you have a [Golang][golang] environment setup, you can simply run:
|
||||
---
|
||||
|
||||
```bash
|
||||
go get -u -v github.com/go-task/task/cmd/task
|
||||
```
|
||||
## Sponsors
|
||||
|
||||
Or you can download from the [releases][releases] page and add to your `PATH`.
|
||||
[](https://opencollective.com/task)
|
||||
|
||||
## Usage
|
||||
## Backers
|
||||
|
||||
Create a file called `Taskfile.yml` in the root of the project.
|
||||
(`Taskfile.toml` and `Taskfile.json` are also supported, but YAML is used in
|
||||
the documentation). The `cmds` attribute should contains the commands of a
|
||||
task:
|
||||
[](https://opencollective.com/task)
|
||||
|
||||
```yml
|
||||
build:
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
## Contributors
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- gulp
|
||||
```
|
||||
|
||||
Running the tasks is as simple as running:
|
||||
|
||||
```bash
|
||||
task assets build
|
||||
```
|
||||
|
||||
Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh
|
||||
interpreter. So you can write sh/bash commands and it will work even on
|
||||
Windows, where `sh` or `bash` is usually not available. Just remember any
|
||||
executable called must be available by the OS or in PATH.
|
||||
|
||||
If you ommit a task name, "default" will be assumed.
|
||||
|
||||
### Environment
|
||||
|
||||
You can specify environment variables that are added when running a command:
|
||||
|
||||
```yml
|
||||
build:
|
||||
cmds:
|
||||
- echo $hallo
|
||||
env:
|
||||
hallo: welt
|
||||
```
|
||||
|
||||
### OS specific task support
|
||||
|
||||
If you add a `Taskfile_{{GOOS}}` you can override or amend your taskfile based
|
||||
on the operating system.
|
||||
|
||||
Example:
|
||||
|
||||
Taskfile.yml:
|
||||
|
||||
```yml
|
||||
build:
|
||||
cmds:
|
||||
- echo "default"
|
||||
```
|
||||
|
||||
Taskfile_linux.yml:
|
||||
|
||||
```yml
|
||||
build:
|
||||
cmds:
|
||||
- echo "linux"
|
||||
```
|
||||
|
||||
Will print out `linux` and not default
|
||||
|
||||
### Running task in another dir
|
||||
|
||||
By default, tasks will be executed in the directory where the Taskfile is
|
||||
located. But you can easily make the task run in another folder informing
|
||||
`dir`:
|
||||
|
||||
```yml
|
||||
js:
|
||||
dir: www/public/js
|
||||
cmds:
|
||||
- gulp
|
||||
```
|
||||
|
||||
### Task dependencies
|
||||
|
||||
You may have tasks that depends on others. Just pointing them on `deps` will
|
||||
make them run automatically before running the parent task:
|
||||
|
||||
```yml
|
||||
build:
|
||||
deps: [assets]
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- gulp
|
||||
```
|
||||
|
||||
In the above example, `assets` will always run right before `build` if you run
|
||||
`task build`.
|
||||
|
||||
A task can have only dependencies and no commands to group tasks together:
|
||||
|
||||
```yml
|
||||
assets:
|
||||
deps: [js, css]
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- npm run buildjs
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- npm run buildcss
|
||||
```
|
||||
|
||||
Each task can only be run once. If it is included from another dependend task causing
|
||||
a cyclomatic dependency, execution will be stopped.
|
||||
|
||||
```yml
|
||||
task1:
|
||||
deps: [task2]
|
||||
|
||||
task2:
|
||||
deps: [task1]
|
||||
```
|
||||
|
||||
Will stop at the moment the dependencies of `task2` are executed.
|
||||
|
||||
### Calling a task from another task
|
||||
|
||||
When a task has many dependencies, they are executed concurrently. This will
|
||||
often result in a faster build pipeline. But in some situations you may need
|
||||
to call other tasks serially. For this just prefix a command with `^`:
|
||||
|
||||
```yml
|
||||
a-task:
|
||||
cmds:
|
||||
- ^another-task
|
||||
- ^even-another-task
|
||||
- echo "Both done"
|
||||
|
||||
another-task:
|
||||
cmds:
|
||||
- ...
|
||||
|
||||
even-another-task:
|
||||
cmds:
|
||||
- ...
|
||||
```
|
||||
|
||||
### Prevent task from running when not necessary
|
||||
|
||||
If a task generates something, you can inform Task the source and generated
|
||||
files, so Task will prevent to run them if not necessary.
|
||||
|
||||
```yml
|
||||
build:
|
||||
deps: [js, css]
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- npm run buildjs
|
||||
sources:
|
||||
- js/src/**/*.js
|
||||
generates:
|
||||
- public/bundle.js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- npm run buildcss
|
||||
sources:
|
||||
- css/src/*.css
|
||||
generates:
|
||||
- public/bundle.css
|
||||
```
|
||||
|
||||
`sources` and `generates` should be file patterns. When both are given, Task
|
||||
will compare the modification date/time of the files to determine if it's
|
||||
necessary to run the task. If not, it will just print
|
||||
`Task "js" is up to date`.
|
||||
|
||||
Alternatively, you can inform a sequence of tests as `status`. If no error
|
||||
is returned (exit status 0), the task is considered up-to-date:
|
||||
|
||||
```yml
|
||||
generate-files:
|
||||
cmds:
|
||||
- mkdir directory
|
||||
- touch directory/file1.txt
|
||||
- touch directory/file2.txt
|
||||
# test existence of files
|
||||
status:
|
||||
- test -d directory
|
||||
- test -f directory/file1.txt
|
||||
- test -f directory/file2.txt
|
||||
```
|
||||
|
||||
You can use `--force` or `-f` if you want to force a task to run even when
|
||||
up-to-date.
|
||||
|
||||
### Variables
|
||||
|
||||
```yml
|
||||
build:
|
||||
deps: [setvar]
|
||||
cmds:
|
||||
- echo "{{.PREFIX}} {{.THEVAR}}"
|
||||
vars:
|
||||
PREFIX: "Path:"
|
||||
|
||||
setvar:
|
||||
cmds:
|
||||
- echo "{{.PATH}}"
|
||||
set: THEVAR
|
||||
```
|
||||
|
||||
The above sample saves the path into a new variable which is then again echoed.
|
||||
|
||||
You can use environment variables, task level variables and a file called
|
||||
`Taskvars.yml` (or `Taskvars.toml` or `Taskvars.json`) as source of variables.
|
||||
|
||||
They are evaluated in the following order:
|
||||
|
||||
Task local variables are overwritten by variables found in `Taskvars` file.
|
||||
Variables found in `Taskvars` file are overwritten with variables from the
|
||||
environment. The output of the last command is stored in the environment. So
|
||||
you can do something like this:
|
||||
|
||||
```yml
|
||||
build:
|
||||
deps: [setvar]
|
||||
cmds:
|
||||
- echo "{{.PREFIX}} '{{.THEVAR}}'"
|
||||
vars:
|
||||
PREFIX: "Result: "
|
||||
|
||||
setvar:
|
||||
cmds:
|
||||
- echo -n "a"
|
||||
- echo -n "{{.THEVAR}}b"
|
||||
- echo -n "{{.THEVAR}}c"
|
||||
set: THEVAR
|
||||
```
|
||||
|
||||
The result of a run of build would be:
|
||||
|
||||
```
|
||||
a
|
||||
ab
|
||||
abc
|
||||
Result: 'abc'
|
||||
```
|
||||
|
||||
#### Dynamic variables
|
||||
|
||||
If you prefix a variable with `$`, then the variable is considered a dynamic
|
||||
variable. The value after the $-symbol will be treated as a command and the
|
||||
output assigned.
|
||||
|
||||
```yml
|
||||
build:
|
||||
cmds:
|
||||
- go build -ldflags="-X main.Version={{.LAST_GIT_COMMIT}}" main.go
|
||||
vars:
|
||||
LAST_GIT_COMMIT: $git log -n 1 --format=%h
|
||||
```
|
||||
|
||||
### Go's template engine
|
||||
|
||||
Task parse commands as [Go's template engine][gotemplate] before executing
|
||||
them. Variables are acessible through dot syntax (`.VARNAME`).
|
||||
|
||||
All functions by the Go's [sprig lib](http://masterminds.github.io/sprig/)
|
||||
are available. The following example gets the current date in a given format:
|
||||
|
||||
```go
|
||||
print-date:
|
||||
cmds:
|
||||
- echo {{now | date "2006-01-02"}}
|
||||
```
|
||||
|
||||
Task also adds the following functions:
|
||||
|
||||
- `OS`: Returns operating system. Possible values are "windows", "linux",
|
||||
"darwin" (macOS) and "freebsd".
|
||||
- `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm"
|
||||
or "s390x".
|
||||
- `ToSlash`: Does nothing on Unix, but on Windows converts a string from `\`
|
||||
path format to `/`.
|
||||
- `FromSlash`: Oposite of `ToSlash`. Does nothing on Unix, but on Windows
|
||||
converts a string from `\` path format to `/`.
|
||||
- `ExeExt`: Returns the right executable extension for the current OS
|
||||
(`".exe"` for Windows, `""` for others).
|
||||
|
||||
Example:
|
||||
|
||||
```yml
|
||||
printos:
|
||||
cmds:
|
||||
- echo '{{OS}} {{ARCH}}'
|
||||
- "echo '{{if eq OS \"windows\"}}windows-command{{else}}unix-command{{end}}'"
|
||||
# This will be path/to/file on Unix but path\to\file on Windows
|
||||
- "{{FromSlash \"path/to/file\"}}"
|
||||
```
|
||||
|
||||
### Help
|
||||
|
||||
Running `task help` lists all tasks with a description. The following taskfile:
|
||||
|
||||
```yml
|
||||
build:
|
||||
desc: Build the go binary.
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
test:
|
||||
desc: Run all the go tests.
|
||||
cmds:
|
||||
- go test -race ./...
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- npm run buildjs
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- npm run buildcss
|
||||
```
|
||||
|
||||
would print the following output:
|
||||
|
||||
```bash
|
||||
build Build the go binary.
|
||||
test Run all the go tests.
|
||||
```
|
||||
|
||||
## Watch tasks (experimental)
|
||||
|
||||
If you give a `--watch` or `-w` argument, task will watch for files changes
|
||||
and run the task again. This requires the `sources` attribute to be given,
|
||||
so task know which files to watch.
|
||||
|
||||
## Alternative task runners
|
||||
|
||||
- YAML based:
|
||||
- [tj/robo][robo]
|
||||
- [dogtools/dog][dog]
|
||||
- [goeuro/myke][myke]
|
||||
- Go based:
|
||||
- [go-godo][godo]
|
||||
- [markbates/grift][grift]
|
||||
|
||||
[make]: https://www.gnu.org/software/make/
|
||||
[releases]: https://github.com/go-task/task/releases
|
||||
[golang]: https://golang.org/
|
||||
[gotemplate]: https://golang.org/pkg/text/template/
|
||||
[robo]: https://github.com/tj/robo
|
||||
[dog]: https://github.com/dogtools/dog
|
||||
[myke]: https://github.com/goeuro/myke
|
||||
[godo]: https://github.com/go-godo/godo
|
||||
[grift]: https://github.com/markbates/grift
|
||||
[sh]: https://github.com/mvdan/sh
|
||||
[](https://github.com/go-task/task/graphs/contributors)
|
||||
|
||||
117
Taskfile.yml
117
Taskfile.yml
@@ -1,49 +1,82 @@
|
||||
# compiles current source code and make "task" executable available on
|
||||
# $GOPATH/bin/task{.exe}
|
||||
install:
|
||||
desc: Installs Task
|
||||
cmds:
|
||||
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
|
||||
vars:
|
||||
GIT_COMMIT: $git log -n 1 --format=%h
|
||||
version: '3'
|
||||
|
||||
dl-deps:
|
||||
desc: Downloads cli dependencies
|
||||
cmds:
|
||||
- go get -u github.com/golang/lint/golint
|
||||
- go get -u github.com/goreleaser/goreleaser
|
||||
- go get -u github.com/golang/dep/cmd/dep
|
||||
includes:
|
||||
docs:
|
||||
taskfile: ./docs
|
||||
dir: ./docs
|
||||
|
||||
update-deps:
|
||||
cmds:
|
||||
- dep ensure
|
||||
- dep prune
|
||||
vars:
|
||||
GIT_COMMIT:
|
||||
sh: git log -n 1 --format=%h
|
||||
|
||||
clean:
|
||||
desc: Cleans temp files and folders
|
||||
cmds:
|
||||
- rm -rf dist/
|
||||
GO_PACKAGES:
|
||||
sh: go list ./...
|
||||
|
||||
lint:
|
||||
desc: Runs golint
|
||||
cmds:
|
||||
- golint .
|
||||
- golint ./execext
|
||||
- golint ./cmd/task
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: test
|
||||
|
||||
test:
|
||||
desc: Runs test suite
|
||||
deps: [install]
|
||||
cmds:
|
||||
- go test -v
|
||||
install:
|
||||
desc: Installs Task
|
||||
cmds:
|
||||
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
|
||||
env:
|
||||
CGO_ENABLED: '0'
|
||||
|
||||
# https://github.com/goreleaser/goreleaser
|
||||
release:
|
||||
desc: Release Task
|
||||
cmds:
|
||||
- goreleaser
|
||||
dl-deps:
|
||||
desc: Downloads cli dependencies
|
||||
cmds:
|
||||
- task: go-get
|
||||
vars: {REPO: golang.org/x/lint/golint}
|
||||
- task: go-get
|
||||
vars: {REPO: github.com/goreleaser/goreleaser}
|
||||
- task: go-get
|
||||
vars: {REPO: github.com/goreleaser/godownloader}
|
||||
|
||||
test-release:
|
||||
desc: Tests release process without publishing
|
||||
cmds:
|
||||
- goreleaser --skip-validate --skip-publish
|
||||
update-deps:
|
||||
desc: Updates dependencies
|
||||
cmds:
|
||||
- dep ensure
|
||||
- dep ensure -update
|
||||
|
||||
clean:
|
||||
desc: Cleans temp files and folders
|
||||
cmds:
|
||||
- rm -rf dist/
|
||||
|
||||
lint:
|
||||
desc: Runs golint
|
||||
cmds:
|
||||
- golint {{catLines .GO_PACKAGES}}
|
||||
silent: true
|
||||
|
||||
test:
|
||||
desc: Runs test suite
|
||||
deps: [install]
|
||||
cmds:
|
||||
- go test {{catLines .GO_PACKAGES}}
|
||||
|
||||
test-release:
|
||||
desc: Tests release process without publishing
|
||||
cmds:
|
||||
- goreleaser --snapshot --rm-dist
|
||||
|
||||
generate-install-script:
|
||||
desc: Generate install script using https://github.com/goreleaser/godownloader
|
||||
cmds:
|
||||
- godownloader --repo go-task/task -o install-task.sh
|
||||
- cp ./install-task.sh ./docs/install.sh
|
||||
|
||||
ci:
|
||||
- task: go-get
|
||||
vars: {REPO: golang.org/x/lint/golint}
|
||||
- task: lint
|
||||
- task: test
|
||||
|
||||
go-get: go get -u {{.REPO}}
|
||||
|
||||
packages:
|
||||
cmds:
|
||||
- echo '{{.GO_PACKAGES}}'
|
||||
silent: true
|
||||
|
||||
169
cmd/task/task.go
169
cmd/task/task.go
@@ -1,10 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/go-task/task"
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/internal/args"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
@@ -13,66 +21,175 @@ var (
|
||||
version = "master"
|
||||
)
|
||||
|
||||
const usage = `Usage: task [-ilfwvsd] [--init] [--list] [--force] [--watch] [--verbose] [--silent] [--dir] [--taskfile] [--dry] [--summary] [task...]
|
||||
|
||||
Runs the specified task(s). Falls back to the "default" task if no task name
|
||||
was specified, or lists all tasks if an unknown task name was specified.
|
||||
|
||||
Example: 'task hello' with the following 'Taskfile.yml' file will generate an
|
||||
'output.txt' file with the content "hello".
|
||||
|
||||
'''
|
||||
version: '3'
|
||||
tasks:
|
||||
hello:
|
||||
cmds:
|
||||
- echo "I am going to write a file named 'output.txt' now."
|
||||
- echo "hello" > output.txt
|
||||
generates:
|
||||
- output.txt
|
||||
'''
|
||||
|
||||
Options:
|
||||
`
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
pflag.Usage = func() {
|
||||
fmt.Println(`task [target1 target2 ...]: Runs commands under targets like make.
|
||||
|
||||
Example: 'task hello' with the following 'Taskfile.yml' file will generate
|
||||
an 'output.txt' file.
|
||||
'''
|
||||
hello:
|
||||
cmds:
|
||||
- echo "I am going to write a file named 'output.txt' now."
|
||||
- echo "hello" > output.txt
|
||||
generates:
|
||||
- output.txt
|
||||
'''
|
||||
`)
|
||||
log.Print(usage)
|
||||
pflag.PrintDefaults()
|
||||
}
|
||||
|
||||
var (
|
||||
versionFlag bool
|
||||
helpFlag bool
|
||||
init bool
|
||||
list bool
|
||||
status bool
|
||||
force bool
|
||||
watch bool
|
||||
verbose bool
|
||||
silent bool
|
||||
dry bool
|
||||
summary bool
|
||||
parallel bool
|
||||
dir string
|
||||
entrypoint string
|
||||
output string
|
||||
color bool
|
||||
)
|
||||
|
||||
pflag.BoolVar(&versionFlag, "version", false, "show Task version")
|
||||
pflag.BoolVarP(&helpFlag, "help", "h", false, "shows Task usage")
|
||||
pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yml in the current folder")
|
||||
pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile")
|
||||
pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date")
|
||||
pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date")
|
||||
pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task")
|
||||
pflag.BoolVarP(&verbose, "verbose", "v", false, "enables verbose mode")
|
||||
pflag.BoolVarP(&silent, "silent", "s", false, "disables echoing")
|
||||
pflag.BoolVarP(¶llel, "parallel", "p", false, "executes tasks provided on command line in parallel")
|
||||
pflag.BoolVar(&dry, "dry", false, "compiles and prints tasks in the order that they would be run, without executing them")
|
||||
pflag.BoolVar(&summary, "summary", false, "show summary about a task")
|
||||
pflag.StringVarP(&dir, "dir", "d", "", "sets directory of execution")
|
||||
pflag.StringVarP(&entrypoint, "taskfile", "t", "", `choose which Taskfile to run. Defaults to "Taskfile.yml"`)
|
||||
pflag.StringVarP(&output, "output", "o", "", "sets output style: [interleaved|group|prefixed]")
|
||||
pflag.BoolVarP(&color, "color", "c", true, "colored output")
|
||||
pflag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
log.Printf("Task version: %s\n", version)
|
||||
fmt.Printf("Task version: %s\n", version)
|
||||
return
|
||||
}
|
||||
|
||||
if helpFlag {
|
||||
pflag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if init {
|
||||
if err := task.InitTaskfile(); err != nil {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := task.InitTaskfile(os.Stdout, wd); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if dir != "" && entrypoint != "" {
|
||||
log.Fatal("task: You can't set both --dir and --taskfile")
|
||||
return
|
||||
}
|
||||
if entrypoint != "" {
|
||||
dir = filepath.Dir(entrypoint)
|
||||
entrypoint = filepath.Base(entrypoint)
|
||||
} else {
|
||||
entrypoint = "Taskfile.yml"
|
||||
}
|
||||
|
||||
e := task.Executor{
|
||||
Force: force,
|
||||
Watch: watch,
|
||||
Force: force,
|
||||
Watch: watch,
|
||||
Verbose: verbose,
|
||||
Silent: silent,
|
||||
Dir: dir,
|
||||
Dry: dry,
|
||||
Entrypoint: entrypoint,
|
||||
Summary: summary,
|
||||
Parallel: parallel,
|
||||
Color: color,
|
||||
|
||||
Stdin: os.Stdin,
|
||||
Stdout: os.Stdout,
|
||||
Stderr: os.Stderr,
|
||||
|
||||
OutputStyle: output,
|
||||
}
|
||||
if err := e.ReadTaskfile(); err != nil {
|
||||
if err := e.Setup(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
args := pflag.Args()
|
||||
if len(args) == 0 {
|
||||
log.Println("task: No argument given, trying default task")
|
||||
args = []string{"default"}
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Run(args...); err != nil {
|
||||
log.Fatal(err)
|
||||
if list {
|
||||
e.PrintTasksHelp()
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
calls []taskfile.Call
|
||||
globals *taskfile.Vars
|
||||
)
|
||||
if v >= 3.0 {
|
||||
calls, globals = args.ParseV3(pflag.Args()...)
|
||||
} else {
|
||||
calls, globals = args.ParseV2(pflag.Args()...)
|
||||
}
|
||||
e.Taskfile.Vars.Merge(globals)
|
||||
|
||||
ctx := context.Background()
|
||||
if !watch {
|
||||
ctx = getSignalContext()
|
||||
}
|
||||
|
||||
if status {
|
||||
if err := e.Status(ctx, calls...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Run(ctx, calls...); err != nil {
|
||||
e.Logger.Errf(logger.Red, "%v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getSignalContext() context.Context {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
sig := <-ch
|
||||
log.Printf("task: signal received: %s", sig)
|
||||
cancel()
|
||||
}()
|
||||
return ctx
|
||||
}
|
||||
|
||||
21
completion/bash/task.bash
Normal file
21
completion/bash/task.bash
Normal file
@@ -0,0 +1,21 @@
|
||||
_task_completion()
|
||||
{
|
||||
local scripts;
|
||||
local curr_arg;
|
||||
|
||||
# Remove colon from word breaks
|
||||
COMP_WORDBREAKS=${COMP_WORDBREAKS//:}
|
||||
|
||||
scripts=$(task -l | sed '1d' | awk '{ print $2 }' | sed 's/:$//');
|
||||
|
||||
curr_arg="${COMP_WORDS[COMP_CWORD]:-"."}"
|
||||
|
||||
# Do not accept more than 1 argument
|
||||
if [ "${#COMP_WORDS[@]}" != "2" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
COMPREPLY=($(compgen -c | echo "$scripts" | grep $curr_arg));
|
||||
}
|
||||
|
||||
complete -F _task_completion task
|
||||
10
completion/ps/task.ps1
Normal file
10
completion/ps/task.ps1
Normal file
@@ -0,0 +1,10 @@
|
||||
$scriptBlock = {
|
||||
param($commandName, $wordToComplete, $cursorPosition)
|
||||
$curReg = "task{.exe}? (.*?)$"
|
||||
$startsWith = $wordToComplete | Select-String $curReg -AllMatches | ForEach-Object { $_.Matches.Groups[1].Value }
|
||||
$reg = "\* ($startsWith.+?):"
|
||||
$listOutput = $(task -l)
|
||||
$listOutput | Select-String $reg -AllMatches | ForEach-Object { $_.Matches.Groups[1].Value + " " }
|
||||
}
|
||||
|
||||
Register-ArgumentCompleter -Native -CommandName task -ScriptBlock $scriptBlock
|
||||
25
completion/zsh/_task
Executable file
25
completion/zsh/_task
Executable file
@@ -0,0 +1,25 @@
|
||||
#compdef task
|
||||
|
||||
# Listing commands from Taskfile.yml
|
||||
function __list() {
|
||||
local -a scripts
|
||||
|
||||
if [ -f Taskfile.yml ]; then
|
||||
scripts=($(task -l | sed '1d' | sed 's/^\* //' | awk '{ print $1 }' | sed 's/:$//' | sed 's/:/\\:/g'))
|
||||
_describe 'script' scripts
|
||||
fi
|
||||
}
|
||||
|
||||
_arguments \
|
||||
'(-d --dir)'{-d,--dir}': :_files' \
|
||||
'(--dry)'--dry \
|
||||
'(-f --force)'{-f,--force} \
|
||||
'(-i --init)'{-i,--init} \
|
||||
'(-l --list)'{-l,--list} \
|
||||
'(-s --silent)'{-s,--silent} \
|
||||
'(--status)'--status \
|
||||
'(-v --verbose)'{-v,--verbose} \
|
||||
'(--version)'--version \
|
||||
'(-w --watch)'{-w,--watch} \
|
||||
'(- *)'{-h,--help} \
|
||||
'*: :__list' \
|
||||
29
cyclic.go
29
cyclic.go
@@ -1,29 +0,0 @@
|
||||
package task
|
||||
|
||||
// HasCyclicDep checks if a task tree has any cyclic dependency
|
||||
func (e *Executor) HasCyclicDep() bool {
|
||||
visits := make(map[string]struct{}, len(e.Tasks))
|
||||
|
||||
var checkCyclicDep func(string, *Task) bool
|
||||
checkCyclicDep = func(name string, t *Task) bool {
|
||||
if _, ok := visits[name]; ok {
|
||||
return false
|
||||
}
|
||||
visits[name] = struct{}{}
|
||||
defer delete(visits, name)
|
||||
|
||||
for _, d := range t.Deps {
|
||||
if !checkCyclicDep(d, e.Tasks[d]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
for k, v := range e.Tasks {
|
||||
if !checkCyclicDep(k, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package task_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task"
|
||||
)
|
||||
|
||||
func TestCyclicDepCheck(t *testing.T) {
|
||||
isCyclic := &task.Executor{
|
||||
Tasks: task.Tasks{
|
||||
"task-a": &task.Task{
|
||||
Deps: []string{"task-b"},
|
||||
},
|
||||
"task-b": &task.Task{
|
||||
Deps: []string{"task-a"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if !isCyclic.HasCyclicDep() {
|
||||
t.Error("Task should be cyclic")
|
||||
}
|
||||
|
||||
isNotCyclic := &task.Executor{
|
||||
Tasks: task.Tasks{
|
||||
"task-a": &task.Task{
|
||||
Deps: []string{"task-c"},
|
||||
},
|
||||
"task-b": &task.Task{
|
||||
Deps: []string{"task-c"},
|
||||
},
|
||||
"task-c": &task.Task{},
|
||||
},
|
||||
}
|
||||
|
||||
if isNotCyclic.HasCyclicDep() {
|
||||
t.Error("Task should not be cyclic")
|
||||
}
|
||||
}
|
||||
0
docs/.nojekyll
Normal file
0
docs/.nojekyll
Normal file
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
taskfile.dev
|
||||
59
docs/README.md
Normal file
59
docs/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Task
|
||||
|
||||
Task is a task runner / build tool that aims to be simpler and easier to use
|
||||
than, for example, [GNU Make][make].
|
||||
|
||||
Since it's written in [Go][go], Task is just a single binary and has no other
|
||||
dependencies, which means you don't need to mess with any complicated install
|
||||
setups just to use a build tool.
|
||||
|
||||
Once [installed](installation.md), you just need to describe your build tasks
|
||||
using a simple [YAML][yaml] schema in a file called `Taskfile.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
hello:
|
||||
cmds:
|
||||
- echo 'Hello World from Task!'
|
||||
silent: true
|
||||
```
|
||||
|
||||
And call it by running `task hello` from you terminal.
|
||||
|
||||
The above example is just the start, you can take a look at the [usage](usage.md)
|
||||
guide to check the full schema documentation and Task features.
|
||||
|
||||
## Features
|
||||
|
||||
- [Easy installation](installation.md): just download a single binary, add to
|
||||
$PATH and you're done! Or you can also install using [Homebrew][homebrew],
|
||||
[Snapcraft][snapcraft], or [Scoop][scoop] if you want;
|
||||
- Available on CIs: by adding [this simple command](installation.md#install-script)
|
||||
to install on your CI script and you're done to use Task as part of your CI pipeline;
|
||||
- Truly cross-platform: while most build tools only work well on Linux or macOS,
|
||||
Task also supports Windows thanks to [this awesome shell interpreter for Go][sh];
|
||||
- Great for code generation: you can easily [prevent a task from running](usage.md#prevent-unnecessary-work)
|
||||
if a given set of files haven't changed since last run (based either on its
|
||||
timestamp or content).
|
||||
|
||||
## Sponsors
|
||||
|
||||
[](https://opencollective.com/task)
|
||||
|
||||
## Backers
|
||||
|
||||
[](https://opencollective.com/task)
|
||||
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/go-task/task/graphs/contributors)
|
||||
|
||||
[make]: https://www.gnu.org/software/make/
|
||||
[go]: https://golang.org/
|
||||
[yaml]: http://yaml.org/
|
||||
[homebrew]: https://brew.sh/
|
||||
[snapcraft]: https://snapcraft.io/
|
||||
[scoop]: https://scoop.sh/
|
||||
[sh]: https://mvdan.cc/sh
|
||||
12
docs/Taskfile.yml
Normal file
12
docs/Taskfile.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
install:
|
||||
desc: Installs docsify to work the on the documentation site
|
||||
cmds:
|
||||
- npm install docsify-cli -g
|
||||
|
||||
serve:
|
||||
desc: Serves the documentation site locally
|
||||
cmds:
|
||||
- docsify serve .
|
||||
8
docs/_sidebar.md
Normal file
8
docs/_sidebar.md
Normal file
@@ -0,0 +1,8 @@
|
||||
- [Installation](installation.md)
|
||||
- [Usage](usage.md)
|
||||
- [Styleguide](styleguide.md)
|
||||
- [Taskfile Versions](taskfile_versions.md)
|
||||
- [Examples](examples.md)
|
||||
- [Releasing Task](releasing_task.md)
|
||||
- [Alternative Task Runners](alternative_task_runners.md)
|
||||
- [GitHub](https://github.com/go-task/task)
|
||||
17
docs/alternative_task_runners.md
Normal file
17
docs/alternative_task_runners.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Alternative task runners
|
||||
|
||||
## YAML based
|
||||
|
||||
- [rliebz/tusk][tusk]
|
||||
|
||||
## Go based
|
||||
|
||||
- [magefile/mage][mage]
|
||||
|
||||
## Make similar
|
||||
|
||||
- [casey/just][just]
|
||||
|
||||
[tusk]: https://github.com/rliebz/tusk
|
||||
[mage]: https://github.com/magefile/mage
|
||||
[just]: https://github.com/casey/just
|
||||
7
docs/examples.md
Normal file
7
docs/examples.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Examples
|
||||
|
||||
The [go-task/examples][examples] intends to be a collection of Taskfiles for
|
||||
various use cases.
|
||||
(It still lacks many examples, though. Contributions are welcome).
|
||||
|
||||
[examples]: https://github.com/go-task/examples
|
||||
BIN
docs/favicon.ico
Normal file
BIN
docs/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
31
docs/index.html
Normal file
31
docs/index.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Task</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="A task runner / simpler Make alternative written in Go">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/docsify-themeable/dist/css/theme-simple.css">
|
||||
<meta name="google-site-verification" content="VGAYkbdmuaciIDGkBe-eAg9yfZg0C6ostgonbGxxOa0" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'Task',
|
||||
repo: 'go-task/task',
|
||||
themeColor: '#00add8',
|
||||
loadSidebar: true,
|
||||
auto2top: true,
|
||||
maxLevel: 3,
|
||||
subMaxLevel: 3
|
||||
}
|
||||
</script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify/lib/docsify.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-themeable/dist/js/docsify-themeable.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/docsify-tabs"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-bash.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/npm/prismjs/components/prism-yaml.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
390
docs/install.sh
Executable file
390
docs/install.sh
Executable file
@@ -0,0 +1,390 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Code generated by godownloader on 2018-04-07T17:47:38Z. DO NOT EDIT.
|
||||
#
|
||||
|
||||
usage() {
|
||||
this=$1
|
||||
cat <<EOF
|
||||
$this: download go binaries for go-task/task
|
||||
|
||||
Usage: $this [-b] bindir [-d] [tag]
|
||||
-b sets bindir or installation directory, Defaults to ./bin
|
||||
-d turns on debug logging
|
||||
[tag] is a tag from
|
||||
https://github.com/go-task/task/releases
|
||||
If tag is missing, then the latest will be used.
|
||||
|
||||
Generated by godownloader
|
||||
https://github.com/goreleaser/godownloader
|
||||
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
#BINDIR is ./bin unless set be ENV
|
||||
# over-ridden by flag below
|
||||
|
||||
BINDIR=${BINDIR:-./bin}
|
||||
while getopts "b:dh?" arg; do
|
||||
case "$arg" in
|
||||
b) BINDIR="$OPTARG" ;;
|
||||
d) log_set_priority 10 ;;
|
||||
h | \?) usage "$0" ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
TAG=$1
|
||||
}
|
||||
# this function wraps all the destructive operations
|
||||
# if a curl|bash cuts off the end of the script due to
|
||||
# network, either nothing will happen or will syntax error
|
||||
# out preventing half-done work
|
||||
execute() {
|
||||
tmpdir=$(mktmpdir)
|
||||
log_debug "downloading files into ${tmpdir}"
|
||||
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
|
||||
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
|
||||
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
|
||||
srcdir="${tmpdir}"
|
||||
(cd "${tmpdir}" && untar "${TARBALL}")
|
||||
install -d "${BINDIR}"
|
||||
for binexe in "task" ; do
|
||||
if [ "$OS" = "windows" ]; then
|
||||
binexe="${binexe}.exe"
|
||||
fi
|
||||
install "${srcdir}/${binexe}" "${BINDIR}/"
|
||||
log_info "installed ${BINDIR}/${binexe}"
|
||||
done
|
||||
}
|
||||
is_supported_platform() {
|
||||
platform=$1
|
||||
found=1
|
||||
case "$platform" in
|
||||
windows/386) found=0 ;;
|
||||
windows/amd64) found=0 ;;
|
||||
darwin/386) found=0 ;;
|
||||
darwin/amd64) found=0 ;;
|
||||
linux/386) found=0 ;;
|
||||
linux/amd64) found=0 ;;
|
||||
esac
|
||||
case "$platform" in
|
||||
darwin/386) found=1 ;;
|
||||
esac
|
||||
return $found
|
||||
}
|
||||
check_platform() {
|
||||
if is_supported_platform "$PLATFORM"; then
|
||||
# optional logging goes here
|
||||
true
|
||||
else
|
||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
tag_to_version() {
|
||||
if [ -z "${TAG}" ]; then
|
||||
log_info "checking GitHub for latest tag"
|
||||
else
|
||||
log_info "checking GitHub for tag '${TAG}'"
|
||||
fi
|
||||
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
|
||||
if test -z "$REALTAG"; then
|
||||
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
|
||||
exit 1
|
||||
fi
|
||||
# if version starts with 'v', remove it
|
||||
TAG="$REALTAG"
|
||||
VERSION=${TAG#v}
|
||||
}
|
||||
adjust_format() {
|
||||
# change format (tar.gz or zip) based on ARCH
|
||||
case ${ARCH} in
|
||||
windows) FORMAT=zip ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
adjust_os() {
|
||||
# adjust archive name based on OS
|
||||
true
|
||||
}
|
||||
adjust_arch() {
|
||||
# adjust archive name based on ARCH
|
||||
true
|
||||
}
|
||||
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
https://github.com/client9/shlib - portable posix shell functions
|
||||
Public domain - http://unlicense.org
|
||||
https://github.com/client9/shlib/blob/master/LICENSE.md
|
||||
but credit (and pull requests) appreciated.
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
is_command() {
|
||||
command -v "$1" >/dev/null
|
||||
}
|
||||
echoerr() {
|
||||
echo "$@" 1>&2
|
||||
}
|
||||
log_prefix() {
|
||||
echo "$0"
|
||||
}
|
||||
_logp=6
|
||||
log_set_priority() {
|
||||
_logp="$1"
|
||||
}
|
||||
log_priority() {
|
||||
if test -z "$1"; then
|
||||
echo "$_logp"
|
||||
return
|
||||
fi
|
||||
[ "$1" -le "$_logp" ]
|
||||
}
|
||||
log_tag() {
|
||||
case $1 in
|
||||
0) echo "emerg" ;;
|
||||
1) echo "alert" ;;
|
||||
2) echo "crit" ;;
|
||||
3) echo "err" ;;
|
||||
4) echo "warning" ;;
|
||||
5) echo "notice" ;;
|
||||
6) echo "info" ;;
|
||||
7) echo "debug" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
log_debug() {
|
||||
log_priority 7 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
|
||||
}
|
||||
log_info() {
|
||||
log_priority 6 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
|
||||
}
|
||||
log_err() {
|
||||
log_priority 3 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
|
||||
}
|
||||
log_crit() {
|
||||
log_priority 2 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
|
||||
}
|
||||
uname_os() {
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case "$os" in
|
||||
msys_nt) os="windows" ;;
|
||||
esac
|
||||
echo "$os"
|
||||
}
|
||||
uname_arch() {
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64) arch="amd64" ;;
|
||||
x86) arch="386" ;;
|
||||
i686) arch="386" ;;
|
||||
i386) arch="386" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
armv5*) arch="arm5" ;;
|
||||
armv6*) arch="arm6" ;;
|
||||
armv7*) arch="arm7" ;;
|
||||
esac
|
||||
echo ${arch}
|
||||
}
|
||||
uname_os_check() {
|
||||
os=$(uname_os)
|
||||
case "$os" in
|
||||
darwin) return 0 ;;
|
||||
dragonfly) return 0 ;;
|
||||
freebsd) return 0 ;;
|
||||
linux) return 0 ;;
|
||||
android) return 0 ;;
|
||||
nacl) return 0 ;;
|
||||
netbsd) return 0 ;;
|
||||
openbsd) return 0 ;;
|
||||
plan9) return 0 ;;
|
||||
solaris) return 0 ;;
|
||||
windows) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
uname_arch_check() {
|
||||
arch=$(uname_arch)
|
||||
case "$arch" in
|
||||
386) return 0 ;;
|
||||
amd64) return 0 ;;
|
||||
arm64) return 0 ;;
|
||||
armv5) return 0 ;;
|
||||
armv6) return 0 ;;
|
||||
armv7) return 0 ;;
|
||||
ppc64) return 0 ;;
|
||||
ppc64le) return 0 ;;
|
||||
mips) return 0 ;;
|
||||
mipsle) return 0 ;;
|
||||
mips64) return 0 ;;
|
||||
mips64le) return 0 ;;
|
||||
s390x) return 0 ;;
|
||||
amd64p32) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
untar() {
|
||||
tarball=$1
|
||||
case "${tarball}" in
|
||||
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
|
||||
*.tar) tar -xf "${tarball}" ;;
|
||||
*.zip) unzip "${tarball}" ;;
|
||||
*)
|
||||
log_err "untar unknown archive format for ${tarball}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
mktmpdir() {
|
||||
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
|
||||
mkdir -p "${TMPDIR}"
|
||||
echo "${TMPDIR}"
|
||||
}
|
||||
http_download_curl() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
|
||||
else
|
||||
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
log_debug "http_download_curl received HTTP status $code"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
http_download_wget() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
wget -q -O "$local_file" "$source_url"
|
||||
else
|
||||
wget -q --header "$header" -O "$local_file" "$source_url"
|
||||
fi
|
||||
}
|
||||
http_download() {
|
||||
log_debug "http_download $2"
|
||||
if is_command curl; then
|
||||
http_download_curl "$@"
|
||||
return
|
||||
elif is_command wget; then
|
||||
http_download_wget "$@"
|
||||
return
|
||||
fi
|
||||
log_crit "http_download unable to find wget or curl"
|
||||
return 1
|
||||
}
|
||||
http_copy() {
|
||||
tmp=$(mktemp)
|
||||
http_download "${tmp}" "$1" "$2" || return 1
|
||||
body=$(cat "$tmp")
|
||||
rm -f "${tmp}"
|
||||
echo "$body"
|
||||
}
|
||||
github_release() {
|
||||
owner_repo=$1
|
||||
version=$2
|
||||
test -z "$version" && version="latest"
|
||||
giturl="https://github.com/${owner_repo}/releases/${version}"
|
||||
json=$(http_copy "$giturl" "Accept:application/json")
|
||||
test -z "$json" && return 1
|
||||
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
|
||||
test -z "$version" && return 1
|
||||
echo "$version"
|
||||
}
|
||||
hash_sha256() {
|
||||
TARGET=${1:-/dev/stdin}
|
||||
if is_command gsha256sum; then
|
||||
hash=$(gsha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command sha256sum; then
|
||||
hash=$(sha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command shasum; then
|
||||
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command openssl; then
|
||||
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f a
|
||||
else
|
||||
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
hash_sha256_verify() {
|
||||
TARGET=$1
|
||||
checksums=$2
|
||||
if [ -z "$checksums" ]; then
|
||||
log_err "hash_sha256_verify checksum file not specified in arg2"
|
||||
return 1
|
||||
fi
|
||||
BASENAME=${TARGET##*/}
|
||||
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
if [ -z "$want" ]; then
|
||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||
return 1
|
||||
fi
|
||||
got=$(hash_sha256 "$TARGET")
|
||||
if [ "$want" != "$got" ]; then
|
||||
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
End of functions from https://github.com/client9/shlib
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
|
||||
PROJECT_NAME="task"
|
||||
OWNER=go-task
|
||||
REPO="task"
|
||||
BINARY=task
|
||||
FORMAT=tar.gz
|
||||
OS=$(uname_os)
|
||||
ARCH=$(uname_arch)
|
||||
PREFIX="$OWNER/$REPO"
|
||||
|
||||
# use in logging routines
|
||||
log_prefix() {
|
||||
echo "$PREFIX"
|
||||
}
|
||||
PLATFORM="${OS}/${ARCH}"
|
||||
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
|
||||
uname_os_check "$OS"
|
||||
uname_arch_check "$ARCH"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
check_platform
|
||||
|
||||
tag_to_version
|
||||
|
||||
adjust_format
|
||||
|
||||
adjust_os
|
||||
|
||||
adjust_arch
|
||||
|
||||
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
|
||||
|
||||
NAME=${BINARY}_${OS}_${ARCH}
|
||||
TARBALL=${NAME}.${FORMAT}
|
||||
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
|
||||
CHECKSUM=task_checksums.txt
|
||||
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
|
||||
|
||||
|
||||
execute
|
||||
133
docs/installation.md
Normal file
133
docs/installation.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# Installation
|
||||
|
||||
Task offers many installation methods. Check out the available methods below.
|
||||
|
||||
## Package Managers
|
||||
|
||||
<!-- tabs:start -->
|
||||
|
||||
#### **Homebrew**
|
||||
|
||||
If you're on macOS or Linux and have [Homebrew][homebrew] installed, getting
|
||||
Task is as simple as running:
|
||||
|
||||
```bash
|
||||
brew install go-task/tap/go-task
|
||||
```
|
||||
|
||||
#### **Snap**
|
||||
|
||||
Task is available in [Snapcraft][snapcraft], but keep in mind that your
|
||||
Linux distribution should allow classic confinement for Snaps to Task work
|
||||
right:
|
||||
|
||||
```bash
|
||||
sudo snap install task --classic
|
||||
```
|
||||
|
||||
#### **Scoop**
|
||||
|
||||
If you're on Windows and have [Scoop][scoop] installed, use `extras` bucket
|
||||
to install Task like:
|
||||
|
||||
```cmd
|
||||
scoop bucket add extras
|
||||
scoop install task
|
||||
```
|
||||
|
||||
This installation method is community owned. After a new release of Task, it
|
||||
may take some time until it's available on Scoop.
|
||||
|
||||
#### **AUR**
|
||||
|
||||
If you're on Arch Linux you can install Task from
|
||||
[AUR](https://aur.archlinux.org/packages/taskfile-git) using your favorite
|
||||
package manager such as `yay`, `pacaur` or `yaourt`:
|
||||
|
||||
```cmd
|
||||
yay -S taskfile-git
|
||||
```
|
||||
|
||||
This installation method is community owned, but since it's `-git` version of
|
||||
the package, it's always latest available version based on the Git repository.
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
## Get The Binary
|
||||
|
||||
<!-- tabs:start -->
|
||||
|
||||
#### **Binary**
|
||||
|
||||
You can download the binary from the [releases page on GitHub][releases] and
|
||||
add to your `$PATH`.
|
||||
|
||||
DEB and RPM packages are also available.
|
||||
|
||||
The `task_checksums.txt` file contains the SHA-256 checksum for each file.
|
||||
|
||||
#### **Install Script**
|
||||
|
||||
We also have a [install script][installscript], which is very useful on
|
||||
scenarios like CIs. Many thanks to [GoDownloader][godownloader] for allowing
|
||||
easily generating this script.
|
||||
|
||||
```bash
|
||||
curl -sL https://taskfile.dev/install.sh | sh
|
||||
```
|
||||
|
||||
> This method will download the binary on the local `./bin` directory by default.
|
||||
|
||||
#### **GitHub Actions**
|
||||
|
||||
If you want to install Task in GitHub Actions you can try using
|
||||
[this action](https://github.com/arduino/actions/tree/master/setup-taskfile)
|
||||
by the Arduino team:
|
||||
|
||||
```yaml
|
||||
- name: Install Task
|
||||
uses: Arduino/actions/setup-taskfile@master
|
||||
```
|
||||
|
||||
This installation method is community owned.
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
## Build From Source
|
||||
|
||||
<!-- tabs:start -->
|
||||
|
||||
#### **Go Modules**
|
||||
|
||||
First, make sure you have [Go][go] properly installed and setup.
|
||||
|
||||
Task requires [Go Modules](https://github.com/golang/go/wiki/Modules) and
|
||||
doesn't officially support installing via `go get` anymore.
|
||||
|
||||
Installing in another directory:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/go-task/task
|
||||
cd task
|
||||
|
||||
# Compiling binary to $GOPATH/bin
|
||||
go install -v ./cmd/task
|
||||
|
||||
# Compiling it to another location.
|
||||
# Use -o ./task.exe on Windows.
|
||||
go build -v -o ./task ./cmd/task
|
||||
```
|
||||
|
||||
> For CI environments we recommend using the [Install Script](#get-the-binary)
|
||||
> instead, which is faster and more stable, since it'll just download the latest
|
||||
> released binary, instead of compiling the edge (master branch) version.
|
||||
|
||||
<!-- tabs:end -->
|
||||
|
||||
[go]: https://golang.org/
|
||||
[snapcraft]: https://snapcraft.io/task
|
||||
[homebrew]: https://brew.sh/
|
||||
[installscript]: https://github.com/go-task/task/blob/master/install-task.sh
|
||||
[releases]: https://github.com/go-task/task/releases
|
||||
[godownloader]: https://github.com/goreleaser/godownloader
|
||||
[scoop]: https://scoop.sh/
|
||||
39
docs/releasing_task.md
Normal file
39
docs/releasing_task.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Releasing Task
|
||||
|
||||
The release process of Task is done with the help of
|
||||
[GoReleaser][goreleaser]. You can test the release process locally by calling
|
||||
the `test-release` task of the Taskfile.
|
||||
|
||||
[GitHub Actions](https://github.com/go-task/task/actions) should release
|
||||
artifacts automatically when a new Git tag is pushed to master
|
||||
(raw executables and DEB and RPM packages).
|
||||
|
||||
# Homebrew
|
||||
|
||||
To release a new version on the [Homebrew tap][homebrewtap] edit the
|
||||
[Formula/go-task.rb][gotaskrb] file, updating with the new version, download
|
||||
URL and sha256.
|
||||
|
||||
# Snapcraft
|
||||
|
||||
The exception is the publishing of a new version of the
|
||||
[snap package][snappackage]. This current require two steps after publishing
|
||||
the binaries:
|
||||
|
||||
* Updating the current version on [snapcraft.yaml][snapcraftyaml];
|
||||
* Moving both `i386` and `amd64` new artifacts to the stable channel on
|
||||
the [Snapcraft dashboard][snapcraftdashboard]
|
||||
|
||||
# Scoop
|
||||
|
||||
Scoop is a community owned installation method. Scoop owners usually take care
|
||||
of updating versions there by editing
|
||||
[this file](https://github.com/lukesampson/scoop-extras/blob/master/bucket/task.json).
|
||||
If you think its Task version is outdated, open an issue to let us know.
|
||||
|
||||
[goreleaser]: https://goreleaser.com/#continuous_integration
|
||||
[homebrewtap]: https://github.com/go-task/homebrew-tap
|
||||
[gotaskrb]: https://github.com/go-task/homebrew-tap/blob/master/Formula/go-task.rb
|
||||
[snappackage]: https://github.com/go-task/snap
|
||||
[snapcraftyaml]: https://github.com/go-task/snap/blob/master/snap/snapcraft.yaml#L2
|
||||
[snapcraftdashboard]: https://dashboard.snapcraft.io/
|
||||
213
docs/styleguide.md
Normal file
213
docs/styleguide.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Styleguide
|
||||
|
||||
This is the official Task styleguide for `Taskfile.yml` files. This guide
|
||||
contains some basic instructions to keep your Taskfile clean and familiar to
|
||||
other users.
|
||||
|
||||
This contains general guidelines, but don't necessarely need to be strictly
|
||||
followed. Feel free to disagree and proceed differently in some point if you
|
||||
need or want to. Also, feel free to open issues or pull requests with
|
||||
improvements to this guide.
|
||||
|
||||
## Use `Taskfile.yml` and not `taskfile.yml`
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
taskfile.yml
|
||||
|
||||
|
||||
# good
|
||||
Taskfile.yml
|
||||
```
|
||||
|
||||
This is important especially for Linux users. Windows and macOS have case
|
||||
insensitive filesystems, so `taskfile.yml` will end up working, even that not
|
||||
officially supported. On Linux, only `Taskfile.yml` will work, though.
|
||||
|
||||
## Use the correct order of keywords
|
||||
|
||||
- `version:`
|
||||
- `includes:`
|
||||
- Configuration ones, like `output:`, `expansions:` or `silent:`
|
||||
- `vars:`
|
||||
- `env:`
|
||||
- `tasks:`
|
||||
|
||||
## Use 2 spaces for indentation
|
||||
|
||||
This is the most common convention for YAML files, and Task follows it.
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
tasks:
|
||||
foo:
|
||||
cmds:
|
||||
- echo 'foo'
|
||||
|
||||
|
||||
# good
|
||||
tasks:
|
||||
foo:
|
||||
cmds:
|
||||
- echo 'foo'
|
||||
```
|
||||
|
||||
## Separate with spaces the mains sections
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
version: '3'
|
||||
includes:
|
||||
docker: ./docker/Taskfile.yml
|
||||
output: prefixed
|
||||
expansions: 3
|
||||
vars:
|
||||
FOO: bar
|
||||
env:
|
||||
BAR: baz
|
||||
tasks:
|
||||
# ...
|
||||
|
||||
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
docker: ./docker/Taskfile.yml
|
||||
|
||||
output: prefixed
|
||||
expansions: 3
|
||||
|
||||
vars:
|
||||
FOO: bar
|
||||
|
||||
env:
|
||||
BAR: baz
|
||||
|
||||
tasks:
|
||||
# ...
|
||||
```
|
||||
|
||||
## Add spaces between tasks
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
foo:
|
||||
cmds:
|
||||
- echo 'foo'
|
||||
bar:
|
||||
cmds:
|
||||
- echo 'bar'
|
||||
baz:
|
||||
cmds:
|
||||
- echo 'baz'
|
||||
|
||||
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
foo:
|
||||
cmds:
|
||||
- echo 'foo'
|
||||
|
||||
bar:
|
||||
cmds:
|
||||
- echo 'bar'
|
||||
|
||||
baz:
|
||||
cmds:
|
||||
- echo 'baz'
|
||||
```
|
||||
|
||||
## Use upper-case variable names
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
binary_name: myapp
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -o {{.binary_name}} .
|
||||
|
||||
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
BINARY_NAME: myapp
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -o {{.BINARY_NAME}} .
|
||||
```
|
||||
|
||||
## Don't wrap vars in spaces when templating
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo '{{ .MESSAGE }}'
|
||||
|
||||
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo '{{.MESSAGE}}'
|
||||
```
|
||||
|
||||
This convention is also used by most people for any Go templating.
|
||||
|
||||
## Separate task name words with a dash
|
||||
|
||||
```yaml
|
||||
# bad
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
do_something_fancy:
|
||||
cmds:
|
||||
- echo 'Do something'
|
||||
|
||||
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
do-something-fancy:
|
||||
cmds:
|
||||
- echo 'Do something'
|
||||
```
|
||||
|
||||
## Use colon for task namespacing
|
||||
|
||||
```yaml
|
||||
# good
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
docker:build:
|
||||
cmds:
|
||||
- docker ...
|
||||
|
||||
docker:run:
|
||||
cmds:
|
||||
- docker-compose ...
|
||||
```
|
||||
|
||||
This is also done automatically when using included Taskfiles.
|
||||
225
docs/taskfile_versions.md
Normal file
225
docs/taskfile_versions.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Taskfile Versions
|
||||
|
||||
The Taskfile syntax and features changed with time. This document explains what
|
||||
changed on each version and how to upgrade your Taskfile.
|
||||
|
||||
## What the Taskfile version mean
|
||||
|
||||
The Taskfile version follows the Task version. E.g. the change to Taskfile
|
||||
version `2` means that Task `v2.0.0` should be release to support it.
|
||||
|
||||
The `version:` key on Taskfile accepts a semver string, so either `2`, `2.0` or
|
||||
`2.0.0` is accepted. If you choose to use `2.0` Task will not enable future
|
||||
`2.1` features, but if you choose to use `2`, then any `2.x.x` features will be
|
||||
available, but not `3.0.0+`.
|
||||
|
||||
## Version 1
|
||||
|
||||
> NOTE: Taskfiles in version 1 are not supported on Task >= v3.0.0 anymore.
|
||||
|
||||
In the first version of the `Taskfile`, the `version:` key was not available,
|
||||
because the tasks was in the root of the YAML document. Like this:
|
||||
|
||||
```yaml
|
||||
echo:
|
||||
cmds:
|
||||
- echo "Hello, World!"
|
||||
```
|
||||
|
||||
The variable priority order was also different:
|
||||
|
||||
1. Call variables
|
||||
2. Environment
|
||||
3. Task variables
|
||||
4. `Taskvars.yml` variables
|
||||
|
||||
## Version 2.0
|
||||
|
||||
At version 2, we introduced the `version:` key, to allow us to evolve Task
|
||||
with new features without breaking existing Taskfiles. The new syntax is as
|
||||
follows:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- echo "Hello, World!"
|
||||
```
|
||||
|
||||
Version 2 allows you to write global variables directly in the Taskfile,
|
||||
if you don't want to create a `Taskvars.yml`:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
```
|
||||
|
||||
The variable priority order changed to the following:
|
||||
|
||||
1. Task variables
|
||||
2. Call variables
|
||||
3. Taskfile variables
|
||||
4. Taskvars file variables
|
||||
5. Environment variables
|
||||
|
||||
A new global option was added to configure the number of variables expansions
|
||||
(which default to 2):
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
expansions: 3
|
||||
|
||||
vars:
|
||||
FOO: foo
|
||||
BAR: bar
|
||||
BAZ: baz
|
||||
FOOBAR: "{{.FOO}}{{.BAR}}"
|
||||
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.FOOBARBAZ}}"
|
||||
```
|
||||
|
||||
## Version 2.1
|
||||
|
||||
Version 2.1 includes a global `output` option, to allow having more control
|
||||
over how commands output are printed to the console
|
||||
(see [documentation][output] for more info):
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
output: prefixed
|
||||
|
||||
tasks:
|
||||
server:
|
||||
cmds:
|
||||
- go run main.go
|
||||
prefix: server
|
||||
```
|
||||
|
||||
From this version it's also possible to ignore errors of a command or task
|
||||
(check documentation [here][ignore_errors]):
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
tasks:
|
||||
example-1:
|
||||
cmds:
|
||||
- cmd: exit 1
|
||||
ignore_error: true
|
||||
- echo "This will be print"
|
||||
|
||||
example-2:
|
||||
cmds:
|
||||
- exit 1
|
||||
- echo "This will be print"
|
||||
ignore_error: true
|
||||
```
|
||||
|
||||
## Version 2.2
|
||||
|
||||
Version 2.2 comes with a global `includes` options to include other
|
||||
Taskfiles:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
includes:
|
||||
docs: ./documentation # will look for ./documentation/Taskfile.yml
|
||||
docker: ./DockerTasks.yml
|
||||
```
|
||||
|
||||
## Version 2.6
|
||||
|
||||
Version 2.6 comes with `preconditions` stanza in tasks.
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
tasks:
|
||||
upload_environment:
|
||||
preconditions:
|
||||
- test -f .env
|
||||
cmds:
|
||||
- aws s3 cp .env s3://myenvironment
|
||||
```
|
||||
|
||||
Please check the [documentation][includes]
|
||||
|
||||
[output]: usage.md#output-syntax
|
||||
[ignore_errors]: usage.md#ignore-errors
|
||||
[includes]: usage.md#including-other-taskfiles
|
||||
|
||||
## Version 3
|
||||
|
||||
These are some major changes done on `v3`:
|
||||
|
||||
- Task's output will now be colored
|
||||
- Added support for `.env` like files
|
||||
- Added `label:` setting to task so one can override how the task name
|
||||
appear in the logs
|
||||
- A global `method:` was added to allow setting the default method,
|
||||
and Task's default changed to `checksum`
|
||||
- Two magic variables were added when using `status:`: `CHECKSUM` and
|
||||
`TIMESTAMP` which contains, respectively, the md5 checksum and greatest
|
||||
modification timestamp of the files listed on `sources:`
|
||||
- Also, the `TASK` variable is always available with the current task name
|
||||
- CLI variables are always treated as global variables
|
||||
- Added `dir:` option to `includes` to allow choosing on which directory an
|
||||
included Taskfile will run:
|
||||
|
||||
```yaml
|
||||
includes:
|
||||
docs:
|
||||
taskfile: ./docs
|
||||
dir: ./docs
|
||||
```
|
||||
|
||||
- Implemented short task syntax. All below syntaxes are equivalent:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print:
|
||||
cmds:
|
||||
- echo "Hello, World!"
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print:
|
||||
- echo "Hello, World!"
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print: echo "Hello, World!"
|
||||
```
|
||||
|
||||
- There was a major refactor on how variables are handled. They're now easier
|
||||
to understand. The `expansions:` setting was removed as it became unncessary.
|
||||
This is the order in which Task will process variables, each level can see
|
||||
the variables set by the previous one and override those.
|
||||
- Environment variables
|
||||
- Global + CLI variables
|
||||
- Call variables
|
||||
- Task variables
|
||||
940
docs/usage.md
Normal file
940
docs/usage.md
Normal file
@@ -0,0 +1,940 @@
|
||||
# Usage
|
||||
|
||||
## Getting started
|
||||
|
||||
Create a file called `Taskfile.yml` in the root of your project.
|
||||
The `cmds` attribute should contain the commands of a task.
|
||||
The example below allows compiling a Go app and uses [Minify][minify] to concat
|
||||
and minify multiple CSS files into a single one.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
```
|
||||
|
||||
Running the tasks is as simple as running:
|
||||
|
||||
```bash
|
||||
task assets build
|
||||
```
|
||||
|
||||
Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh
|
||||
interpreter. So you can write sh/bash commands and it will work even on
|
||||
Windows, where `sh` or `bash` are usually not available. Just remember any
|
||||
executable called must be available by the OS or in PATH.
|
||||
|
||||
If you omit a task name, "default" will be assumed.
|
||||
|
||||
## Environment variables
|
||||
|
||||
### Task
|
||||
|
||||
You can use `env` to set custom environment variables for a specific task:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo $GREETING
|
||||
env:
|
||||
GREETING: Hey, there!
|
||||
```
|
||||
|
||||
Additionally, you can set globally environment variables, that'll be available
|
||||
to all tasks:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
env:
|
||||
GREETING: Hey, there!
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo $GREETING
|
||||
```
|
||||
|
||||
> NOTE: `env` supports expansion and retrieving output from a shell command
|
||||
> just like variables, as you can see on the [Variables](#variables) section.
|
||||
|
||||
### .env files
|
||||
|
||||
You can also ask Task to include `.env` like files by using the `dotenv:`
|
||||
setting:
|
||||
|
||||
```
|
||||
# .env
|
||||
KEYNAME=VALUE
|
||||
```
|
||||
|
||||
|
||||
```yaml
|
||||
# Taskfile.yml
|
||||
|
||||
version: '3'
|
||||
|
||||
dotenv: ['.env']
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo "Using $KEYNAME"
|
||||
```
|
||||
|
||||
## Operating System specific tasks
|
||||
|
||||
If you add a `Taskfile_{{GOOS}}.yml` you can override or amend your Taskfile
|
||||
based on the operating system.
|
||||
|
||||
Example:
|
||||
|
||||
Taskfile.yml:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "default"
|
||||
```
|
||||
|
||||
Taskfile_linux.yml:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- echo "linux"
|
||||
```
|
||||
|
||||
Will print out `linux` and not `default`.
|
||||
|
||||
Keep in mind that the version of the files should match. Also, when redefining
|
||||
a task the whole task is replaced, properties of the task are not merged.
|
||||
|
||||
It's also possible to have an OS specific `Taskvars.yml` file, like
|
||||
`Taskvars_windows.yml`, `Taskvars_linux.yml`, or `Taskvars_darwin.yml`. See the
|
||||
[variables section](#variables) below.
|
||||
|
||||
## Including other Taskfiles
|
||||
|
||||
> This feature is still experimental and may have bugs.
|
||||
|
||||
If you want to share tasks between different projects (Taskfiles), you can use
|
||||
the importing mechanism to include other Taskfiles using the `includes` keyword:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
docs: ./documentation # will look for ./documentation/Taskfile.yml
|
||||
docker: ./DockerTasks.yml
|
||||
```
|
||||
|
||||
The tasks described in the given Taskfiles will be available with the informed
|
||||
namespace. So, you'd call `task docs:serve` to run the `serve` task from
|
||||
`documentation/Taskfile.yml` or `task docker:build` to run the `build` task
|
||||
from the `DockerTasks.yml` file.
|
||||
|
||||
### Directory of included Taskfile
|
||||
|
||||
By default, included Taskfile's tasks are ran in the current directory, even
|
||||
if the Taskfile is in another directory, but you can force its tasks to run
|
||||
in another directory by using this alternative syntax:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
docs:
|
||||
taskfile: ./docs/Taskfile.yml
|
||||
dir: ./docs
|
||||
```
|
||||
|
||||
> The included Taskfiles must be using the same schema version the main
|
||||
> Taskfile uses.
|
||||
|
||||
> Also, for now included Taskfiles can't include other Taskfiles.
|
||||
> This was a deliberate decision to keep use and implementation simple.
|
||||
> If you disagree, open an GitHub issue and explain your use case. =)
|
||||
|
||||
## Task directory
|
||||
|
||||
By default, tasks will be executed in the directory where the Taskfile is
|
||||
located. But you can easily make the task run in another folder informing
|
||||
`dir`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
serve:
|
||||
dir: public/www
|
||||
cmds:
|
||||
# run http server
|
||||
- caddy
|
||||
```
|
||||
|
||||
If the directory doesn't exist, `task` creates it.
|
||||
|
||||
## Task dependencies
|
||||
|
||||
> Dependencies run in parallel, so dependencies of a task shouldn't depend one
|
||||
> another. If you want to force tasks to run serially take a look at the
|
||||
> [Calling Another Task](#calling-another-task) section below.
|
||||
|
||||
You may have tasks that depend on others. Just pointing them on `deps` will
|
||||
make them run automatically before running the parent task:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
deps: [assets]
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
assets:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
```
|
||||
|
||||
In the above example, `assets` will always run right before `build` if you run
|
||||
`task build`.
|
||||
|
||||
A task can have only dependencies and no commands to group tasks together:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
assets:
|
||||
deps: [js, css]
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
```
|
||||
|
||||
If there is more than one dependency, they always run in parallel for better
|
||||
performance.
|
||||
|
||||
> You can also make the tasks given by the command line run in parallel by
|
||||
> using the `--parallel` flag (alias `-p`). Example: `task --parallel js css`.
|
||||
|
||||
If you want to pass information to dependencies, you can do that the same
|
||||
manner as you would to [call another task](#calling-another-task):
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps:
|
||||
- task: echo_sth
|
||||
vars: {TEXT: "before 1"}
|
||||
- task: echo_sth
|
||||
vars: {TEXT: "before 2"}
|
||||
cmds:
|
||||
- echo "after"
|
||||
|
||||
echo_sth:
|
||||
cmds:
|
||||
- echo {{.TEXT}}
|
||||
```
|
||||
|
||||
## Calling another task
|
||||
|
||||
When a task has many dependencies, they are executed concurrently. This will
|
||||
often result in a faster build pipeline. But in some situations you may need
|
||||
to call other tasks serially. In this case, just use the following syntax:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
main-task:
|
||||
cmds:
|
||||
- task: task-to-be-called
|
||||
- task: another-task
|
||||
- echo "Both done"
|
||||
|
||||
task-to-be-called:
|
||||
cmds:
|
||||
- echo "Task to be called"
|
||||
|
||||
another-task:
|
||||
cmds:
|
||||
- echo "Another task"
|
||||
```
|
||||
|
||||
Overriding variables in the called task is as simple as informing `vars`
|
||||
attribute:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
main-task:
|
||||
cmds:
|
||||
- task: write-file
|
||||
vars: {FILE: "hello.txt", CONTENT: "Hello!"}
|
||||
- task: write-file
|
||||
vars: {FILE: "world.txt", CONTENT: "World!"}
|
||||
|
||||
write-file:
|
||||
cmds:
|
||||
- echo "{{.CONTENT}}" > {{.FILE}}
|
||||
```
|
||||
|
||||
The above syntax is also supported in `deps`.
|
||||
|
||||
> NOTE: If you want to call a task declared in the root Taskfile from within an
|
||||
> [included Taskfile](#including-other-taskfiles), add a leading `:` like this:
|
||||
> `task: :task-name`.
|
||||
|
||||
## Prevent unnecessary work
|
||||
|
||||
### By fingerprinting locally generated files and their sources
|
||||
|
||||
If a task generates something, you can inform Task the source and generated
|
||||
files, so Task will prevent to run them if not necessary.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
deps: [js, css]
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
sources:
|
||||
- src/js/**/*.js
|
||||
generates:
|
||||
- public/script.js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
sources:
|
||||
- src/css/**/*.css
|
||||
generates:
|
||||
- public/style.css
|
||||
```
|
||||
|
||||
`sources` and `generates` can be files or file patterns. When both are given,
|
||||
Task will compare the checksum of the files to determine if it's
|
||||
necessary to run the task. If not, it will just print a message like
|
||||
`Task "js" is up to date`.
|
||||
You will probably want to ignore the `.task` folder in your `.gitignore` file
|
||||
(It's there that Task stores the last checksum).
|
||||
|
||||
If you prefer this check to be made by the modification timestamp of the files,
|
||||
instead of its checksum (content), just set the `method` property to `timestamp`.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build .
|
||||
sources:
|
||||
- ./*.go
|
||||
generates:
|
||||
- app{{exeExt}}
|
||||
method: checksum
|
||||
```
|
||||
|
||||
> TIP: method `none` skips any validation and always run the task.
|
||||
|
||||
### Using programmatic checks to indicate a task is up to date.
|
||||
|
||||
Alternatively, you can inform a sequence of tests as `status`. If no error
|
||||
is returned (exit status 0), the task is considered up-to-date:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
generate-files:
|
||||
cmds:
|
||||
- mkdir directory
|
||||
- touch directory/file1.txt
|
||||
- touch directory/file2.txt
|
||||
# test existence of files
|
||||
status:
|
||||
- test -d directory
|
||||
- test -f directory/file1.txt
|
||||
- test -f directory/file2.txt
|
||||
```
|
||||
|
||||
Normally, you would use `sources` in combination with
|
||||
`generates` - but for tasks that generate remote artifacts (Docker images,
|
||||
deploys, CD releases) the checksum source and timestamps require either
|
||||
access to the artifact or for an out-of-band refresh of the `.checksum`
|
||||
fingerprint file.
|
||||
|
||||
Two special variables `{{.CHECKSUM}}` and `{{.TIMESTAMP}}` are available
|
||||
for interpolation within `status` commands, depending on the method assigned
|
||||
to fingerprint the sources. Only `source` globs are fingerprinted.
|
||||
|
||||
Note that the `{{.TIMESTAMP}}` variable is a "live" Go `time.Time` struct, and
|
||||
can be formatted using any of the methods that `time.Time` responds to.
|
||||
|
||||
See [the Go Time documentation](https://golang.org/pkg/time/) for more information.
|
||||
|
||||
You can use `--force` or `-f` if you want to force a task to run even when
|
||||
up-to-date.
|
||||
|
||||
Also, `task --status [tasks]...` will exit with a non-zero exit code if any of
|
||||
the tasks are not up-to-date.
|
||||
|
||||
### Using programmatic checks to cancel execution of an task and it's dependencies
|
||||
|
||||
In addition to `status` checks, there are also `preconditions` checks, which are
|
||||
the logical inverse of `status` checks. That is, if you need a certain set of
|
||||
conditions to be _true_ you can use the `preconditions` stanza.
|
||||
`preconditions` are similar to `status` lines except they support `sh`
|
||||
expansion and they SHOULD all return 0.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
generate-files:
|
||||
cmds:
|
||||
- mkdir directory
|
||||
- touch directory/file1.txt
|
||||
- touch directory/file2.txt
|
||||
# test existence of files
|
||||
preconditions:
|
||||
- test -f .env
|
||||
- sh: "[ 1 = 0 ]"
|
||||
msg: "One doesn't equal Zero, Halting"
|
||||
```
|
||||
|
||||
Preconditions can set specific failure messages that can tell
|
||||
a user what steps to take using the `msg` field.
|
||||
|
||||
If a task has a dependency on a sub-task with a precondition, and that
|
||||
precondition is not met - the calling task will fail. Note that a task
|
||||
executed with a failing precondition will not run unless `--force` is
|
||||
given.
|
||||
|
||||
Unlike `status` which will skip a task if it is up to date, and continue
|
||||
executing tasks that depend on it, a `precondition` will fail a task, along
|
||||
with any other tasks that depend on it.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
tasks:
|
||||
task_will_fail:
|
||||
preconditions:
|
||||
- sh: "exit 1"
|
||||
|
||||
task_will_also_fail:
|
||||
deps:
|
||||
- task_will_fail
|
||||
|
||||
task_will_still_fail:
|
||||
cmds:
|
||||
- task: task_will_fail
|
||||
- echo "I will not run"
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
When doing interpolation of variables, Task will look for the below.
|
||||
They are listed below in order of importance (e.g. most important first):
|
||||
|
||||
- Variables declared in the task definition
|
||||
- Variables given while calling a task from another.
|
||||
(See [Calling another task](#calling-another-task) above)
|
||||
- Variables declared in the `vars:` option in the `Taskfile`
|
||||
- Variables available in the `Taskvars.yml` file
|
||||
- Environment variables
|
||||
|
||||
Example of sending parameters with environment variables:
|
||||
|
||||
```bash
|
||||
$ TASK_VARIABLE=a-value task do-something
|
||||
```
|
||||
|
||||
> TIP: A special variable `.TASK` is always available containg the task name.
|
||||
|
||||
Since some shells don't support above syntax to set environment variables
|
||||
(Windows) tasks also accepts a similar style when not in the beginning of
|
||||
the command.
|
||||
```bash
|
||||
$ task write-file FILE=file.txt "CONTENT=Hello, World!" print "MESSAGE=All done!"
|
||||
```
|
||||
|
||||
Example of locally declared vars:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print-var:
|
||||
cmds:
|
||||
- echo "{{.VAR}}"
|
||||
vars:
|
||||
VAR: Hello!
|
||||
```
|
||||
|
||||
Example of global vars in a `Taskfile.yml`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello from Taskfile!
|
||||
|
||||
tasks:
|
||||
greet:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
```
|
||||
|
||||
Example of `Taskvars.yml` file:
|
||||
|
||||
```yaml
|
||||
PROJECT_NAME: My Project
|
||||
DEV_MODE: production
|
||||
GIT_COMMIT: {sh: git log -n 1 --format=%h}
|
||||
```
|
||||
|
||||
### Variables expansion
|
||||
|
||||
Variables are expanded 2 times by default. You can change that by setting the
|
||||
`expansions:` option. Change that will be necessary if you compose many
|
||||
variables together:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
expansions: 3
|
||||
|
||||
vars:
|
||||
FOO: foo
|
||||
BAR: bar
|
||||
BAZ: baz
|
||||
FOOBAR: "{{.FOO}}{{.BAR}}"
|
||||
FOOBARBAZ: "{{.FOOBAR}}{{.BAZ}}"
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.FOOBARBAZ}}"
|
||||
```
|
||||
|
||||
### Dynamic variables
|
||||
|
||||
The below syntax (`sh:` prop in a variable) is considered a dynamic variable.
|
||||
The value will be treated as a command and the output assigned. If there is one
|
||||
or more trailing newlines, the last newline will be trimmed.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- go build -ldflags="-X main.Version={{.GIT_COMMIT}}" main.go
|
||||
vars:
|
||||
GIT_COMMIT:
|
||||
sh: git log -n 1 --format=%h
|
||||
```
|
||||
|
||||
This works for all types of variables.
|
||||
|
||||
## Go's template engine
|
||||
|
||||
Task parse commands as [Go's template engine][gotemplate] before executing
|
||||
them. Variables are accessible through dot syntax (`.VARNAME`).
|
||||
|
||||
All functions by the Go's [slim-sprig lib](https://go-task.github.io/slim-sprig/)
|
||||
are available. The following example gets the current date in a given format:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print-date:
|
||||
cmds:
|
||||
- echo {{now | date "2006-01-02"}}
|
||||
```
|
||||
|
||||
Task also adds the following functions:
|
||||
|
||||
- `OS`: Returns operating system. Possible values are "windows", "linux",
|
||||
"darwin" (macOS) and "freebsd".
|
||||
- `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm"
|
||||
or "s390x".
|
||||
- `splitLines`: Splits Unix (\n) and Windows (\r\n) styled newlines.
|
||||
- `catLines`: Replaces Unix (\n) and Windows (\r\n) styled newlines with a space.
|
||||
- `toSlash`: Does nothing on Unix, but on Windows converts a string from `\`
|
||||
path format to `/`.
|
||||
- `fromSlash`: Opposite of `toSlash`. Does nothing on Unix, but on Windows
|
||||
converts a string from `\` path format to `/`.
|
||||
- `exeExt`: Returns the right executable extension for the current OS
|
||||
(`".exe"` for Windows, `""` for others).
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
print-os:
|
||||
cmds:
|
||||
- echo '{{OS}} {{ARCH}}'
|
||||
- echo '{{if eq OS "windows"}}windows-command{{else}}unix-command{{end}}'
|
||||
# This will be path/to/file on Unix but path\to\file on Windows
|
||||
- echo '{{fromSlash "path/to/file"}}'
|
||||
enumerated-file:
|
||||
vars:
|
||||
CONTENT: |
|
||||
foo
|
||||
bar
|
||||
cmds:
|
||||
- |
|
||||
cat << EOF > output.txt
|
||||
{{range $i, $line := .CONTENT | splitLines -}}
|
||||
{{printf "%3d" $i}}: {{$line}}
|
||||
{{end}}EOF
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
Running `task --list` (or `task -l`) lists all tasks with a description.
|
||||
The following Taskfile:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build the go binary.
|
||||
cmds:
|
||||
- go build -v -i main.go
|
||||
|
||||
test:
|
||||
desc: Run all the go tests.
|
||||
cmds:
|
||||
- go test -race ./...
|
||||
|
||||
js:
|
||||
cmds:
|
||||
- minify -o public/script.js src/js
|
||||
|
||||
css:
|
||||
cmds:
|
||||
- minify -o public/style.css src/css
|
||||
```
|
||||
|
||||
would print the following output:
|
||||
|
||||
```bash
|
||||
* build: Build the go binary.
|
||||
* test: Run all the go tests.
|
||||
```
|
||||
|
||||
## Display summary of task
|
||||
|
||||
Running `task --summary task-name` will show a summary of a task.
|
||||
The following Taskfile:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
release:
|
||||
deps: [build]
|
||||
summary: |
|
||||
Release your project to github
|
||||
|
||||
It will build your project before starting the release.
|
||||
Please make sure that you have set GITHUB_TOKEN before starting.
|
||||
cmds:
|
||||
- your-release-tool
|
||||
|
||||
build:
|
||||
cmds:
|
||||
- your-build-tool
|
||||
```
|
||||
|
||||
with running ``task --summary release`` would print the following output:
|
||||
|
||||
```
|
||||
task: release
|
||||
|
||||
Release your project to github
|
||||
|
||||
It will build your project before starting the release.
|
||||
Please make sure that you have set GITHUB_TOKEN before starting.
|
||||
|
||||
dependencies:
|
||||
- build
|
||||
|
||||
commands:
|
||||
- your-release-tool
|
||||
```
|
||||
If a summary is missing, the description will be printed.
|
||||
If the task does not have a summary or a description, a warning is printed.
|
||||
|
||||
Please note: *showing the summary will not execute the command*.
|
||||
|
||||
## Overriding task name
|
||||
|
||||
Sometimes you may want to override the task name print on summary, up-to-date
|
||||
messates to STDOUT, etc. In this case you can just set `label:`, which can also
|
||||
be interpolated with variables:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
- task: print
|
||||
vars:
|
||||
MESSAGE: hello
|
||||
- task: print
|
||||
vars:
|
||||
MESSAGE: world
|
||||
|
||||
print:
|
||||
label: 'print-{{.MESSAGE}}'
|
||||
cmds:
|
||||
- echo "{{.MESSAGE}}"
|
||||
```
|
||||
|
||||
## Silent mode
|
||||
|
||||
Silent mode disables echoing of commands before Task runs it.
|
||||
For the following Taskfile:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- echo "Print something"
|
||||
```
|
||||
|
||||
Normally this will be print:
|
||||
|
||||
```sh
|
||||
echo "Print something"
|
||||
Print something
|
||||
```
|
||||
|
||||
With silent mode on, the below will be print instead:
|
||||
|
||||
```sh
|
||||
Print something
|
||||
```
|
||||
|
||||
There are four ways to enable silent mode:
|
||||
|
||||
* At command level:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- cmd: echo "Print something"
|
||||
silent: true
|
||||
```
|
||||
|
||||
* At task level:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- echo "Print something"
|
||||
silent: true
|
||||
```
|
||||
|
||||
* Globally at Taskfile level:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
silent: true
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- echo "Print something"
|
||||
```
|
||||
|
||||
* Or globally with `--silent` or `-s` flag
|
||||
|
||||
If you want to suppress STDOUT instead, just redirect a command to `/dev/null`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- echo "This will print nothing" > /dev/null
|
||||
```
|
||||
|
||||
## Dry run mode
|
||||
|
||||
Dry run mode (`--dry`) compiles and steps through each task, printing the commands
|
||||
that would be run without executing them. This is useful for debugging your Taskfiles.
|
||||
|
||||
## Ignore errors
|
||||
|
||||
You have the option to ignore errors during command execution.
|
||||
Given the following Taskfile:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- exit 1
|
||||
- echo "Hello World"
|
||||
```
|
||||
|
||||
Task will abort the execution after running `exit 1` because the status code `1` stands for `EXIT_FAILURE`.
|
||||
However it is possible to continue with execution using `ignore_error`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
echo:
|
||||
cmds:
|
||||
- cmd: exit 1
|
||||
ignore_error: true
|
||||
- echo "Hello World"
|
||||
```
|
||||
|
||||
`ignore_error` can also be set for a task, which mean errors will be suppressed
|
||||
for all commands. But keep in mind this option won't propagate to other tasks
|
||||
called either by `deps` or `cmds`!
|
||||
|
||||
## Output syntax
|
||||
|
||||
By default, Task just redirect the STDOUT and STDERR of the running commands
|
||||
to the shell in real time. This is good for having live feedback for log
|
||||
printed by commands, but the output can become messy if you have multiple
|
||||
commands running at the same time and printing lots of stuff.
|
||||
|
||||
To make this more customizable, there are currently three different output
|
||||
options you can choose:
|
||||
|
||||
- `interleaved` (default)
|
||||
- `group`
|
||||
- `prefixed`
|
||||
|
||||
To choose another one, just set it to root in the Taskfile:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
output: 'group'
|
||||
|
||||
tasks:
|
||||
# ...
|
||||
```
|
||||
|
||||
The `group` output will print the entire output of a command once, after it
|
||||
finishes, so you won't have live feedback for commands that take a long time
|
||||
to run.
|
||||
|
||||
The `prefix` output will prefix every line printed by a command with
|
||||
`[task-name] ` as the prefix, but you can customize the prefix for a command
|
||||
with the `prefix:` attribute:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
output: prefixed
|
||||
|
||||
tasks:
|
||||
default:
|
||||
deps:
|
||||
- task: print
|
||||
vars: {TEXT: foo}
|
||||
- task: print
|
||||
vars: {TEXT: bar}
|
||||
- task: print
|
||||
vars: {TEXT: baz}
|
||||
|
||||
print:
|
||||
cmds:
|
||||
- echo "{{.TEXT}}"
|
||||
prefix: "print-{{.TEXT}}"
|
||||
silent: true
|
||||
```
|
||||
|
||||
```bash
|
||||
$ task default
|
||||
[print-foo] foo
|
||||
[print-bar] bar
|
||||
[print-baz] baz
|
||||
```
|
||||
|
||||
> The `output` option can also be specified by the `--output` or `-o` flags.
|
||||
|
||||
## Short task syntax
|
||||
|
||||
Starting on Task v3, you can now write tasks with a shorter syntax if they
|
||||
have the default settings (e.g. no custom `env:`, `vars:`, `silent:` , etc):
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build: go build -v -o ./app{{exeExt}} .
|
||||
|
||||
build:
|
||||
- task: build
|
||||
- ./app{{exeExt}} -h localhost -p 8080
|
||||
```
|
||||
|
||||
## Watch tasks
|
||||
|
||||
If you give a `--watch` or `-w` argument, task will watch for file changes
|
||||
and run the task again. This requires the `sources` attribute to be given,
|
||||
so task know which files to watch.
|
||||
|
||||
[gotemplate]: https://golang.org/pkg/text/template/
|
||||
[minify]: https://github.com/tdewolff/minify/tree/master/cmd/minify
|
||||
33
errors.go
33
errors.go
@@ -6,20 +6,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCyclicDependencyDetected is returned when a cyclic dependency was found in the Taskfile
|
||||
ErrCyclicDependencyDetected = errors.New("task: cyclic dependency detected")
|
||||
// ErrTaskfileAlreadyExists is returned on creating a Taskfile if one already exists
|
||||
ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists")
|
||||
)
|
||||
|
||||
type taskFileNotFound struct {
|
||||
taskFile string
|
||||
}
|
||||
|
||||
func (err taskFileNotFound) Error() string {
|
||||
return fmt.Sprintf(`task: No task file found (is it named "%s"?). Use "task --init" to create a new one`, err.taskFile)
|
||||
}
|
||||
|
||||
type taskNotFoundError struct {
|
||||
taskName string
|
||||
}
|
||||
@@ -37,18 +27,17 @@ func (err *taskRunError) Error() string {
|
||||
return fmt.Sprintf(`task: Failed to run task "%s": %v`, err.taskName, err.err)
|
||||
}
|
||||
|
||||
type cyclicDepError struct {
|
||||
taskName string
|
||||
// MaximumTaskCallExceededError is returned when a task is called too
|
||||
// many times. In this case you probably have a cyclic dependendy or
|
||||
// infinite loop
|
||||
type MaximumTaskCallExceededError struct {
|
||||
task string
|
||||
}
|
||||
|
||||
func (err *cyclicDepError) Error() string {
|
||||
return fmt.Sprintf(`task: Cyclic dependency of task "%s" detected`, err.taskName)
|
||||
}
|
||||
|
||||
type cantWatchNoSourcesError struct {
|
||||
taskName string
|
||||
}
|
||||
|
||||
func (err *cantWatchNoSourcesError) Error() string {
|
||||
return fmt.Sprintf(`task: Can't watch task "%s" because it has no specified sources`, err.taskName)
|
||||
func (e *MaximumTaskCallExceededError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
`task: maximum task call exceeded (%d) for task "%s": probably an cyclic dep or infinite loop`,
|
||||
MaximumTaskCall,
|
||||
e.task,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"hello": {
|
||||
"cmds": [
|
||||
"echo \"I am going to write a file named 'output.txt' now.\"",
|
||||
"echo \"hello\" > output.txt"
|
||||
],
|
||||
"generates": [
|
||||
"output.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
[hello]
|
||||
cmds = [
|
||||
"echo \"I am going to write a file named 'output.txt' now.\"",
|
||||
"echo \"hello\" > output.txt"
|
||||
]
|
||||
generates = ["output.txt"]
|
||||
@@ -1,6 +0,0 @@
|
||||
hello:
|
||||
cmds:
|
||||
- echo "I am going to write a file named 'output.txt' now."
|
||||
- echo "hello" > output.txt
|
||||
generates:
|
||||
- output.txt
|
||||
@@ -1,53 +0,0 @@
|
||||
package execext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mvdan/sh/interp"
|
||||
"github.com/mvdan/sh/syntax"
|
||||
)
|
||||
|
||||
// RunCommandOptions is the options for the RunCommand func
|
||||
type RunCommandOptions struct {
|
||||
Context context.Context
|
||||
Command string
|
||||
Dir string
|
||||
Env []string
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNilOptions is returned when a nil options is given
|
||||
ErrNilOptions = errors.New("execext: nil options given")
|
||||
)
|
||||
|
||||
// RunCommand runs a shell command
|
||||
func RunCommand(opts *RunCommandOptions) error {
|
||||
if opts == nil {
|
||||
return ErrNilOptions
|
||||
}
|
||||
|
||||
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := interp.Runner{
|
||||
Context: opts.Context,
|
||||
File: p,
|
||||
Dir: opts.Dir,
|
||||
Env: opts.Env,
|
||||
Stdin: opts.Stdin,
|
||||
Stdout: opts.Stdout,
|
||||
Stderr: opts.Stderr,
|
||||
}
|
||||
if err = r.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
82
file.go
82
file.go
@@ -1,82 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-zglob"
|
||||
)
|
||||
|
||||
func minTime(a, b time.Time) time.Time {
|
||||
if !a.IsZero() && a.Before(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
func maxTime(a, b time.Time) time.Time {
|
||||
if a.After(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func getPatternsMinTime(patterns []string) (m time.Time, err error) {
|
||||
for _, p := range patterns {
|
||||
mp, err := getPatternMinTime(p)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
m = minTime(m, mp)
|
||||
}
|
||||
return
|
||||
}
|
||||
func getPatternsMaxTime(patterns []string) (m time.Time, err error) {
|
||||
for _, p := range patterns {
|
||||
mp, err := getPatternMaxTime(p)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
m = maxTime(m, mp)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getPatternMinTime(pattern string) (minTime time.Time, err error) {
|
||||
files, err := zglob.Glob(pattern)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
modTime := info.ModTime()
|
||||
if minTime.IsZero() || modTime.Before(minTime) {
|
||||
minTime = modTime
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getPatternMaxTime(pattern string) (maxTime time.Time, err error) {
|
||||
files, err := zglob.Glob(pattern)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
modTime := info.ModTime()
|
||||
if modTime.After(maxTime) {
|
||||
maxTime = modTime
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
17
go.mod
Normal file
17
go.mod
Normal file
@@ -0,0 +1,17 @@
|
||||
module github.com/go-task/task/v3
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.7.0
|
||||
github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/mattn/go-zglob v0.0.1
|
||||
github.com/radovskyb/watcher v1.0.5
|
||||
github.com/spf13/pflag v1.0.3
|
||||
github.com/stretchr/testify v1.5.1
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
mvdan.cc/sh/v3 v3.1.2
|
||||
)
|
||||
|
||||
go 1.13
|
||||
68
go.sum
Normal file
68
go.sum
Normal file
@@ -0,0 +1,68 @@
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb h1:/qbv1F67s6ehqX9mG23cJOeca3FWpOVKgtPfPUMAi0k=
|
||||
github.com/go-task/slim-sprig v0.0.0-20200516131648-f9bac4e523eb/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/pkg/diff v0.0.0-20190930165518-531926345625/go.mod h1:kFj35MyHn14a6pIgWhm46KVjJr5CHys3eEYxkuKD1EI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/radovskyb/watcher v1.0.5 h1:wqt7gb+HjGacvFoLTKeT44C+XVPxu7bvHvKT1IvZ7rw=
|
||||
github.com/radovskyb/watcher v1.0.5/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
|
||||
github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867 h1:JoRuNIf+rpHl+VhScRQQvzbHed86tKkqwPMV34T8myw=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407 h1:5zh5atpUEdIc478E/ebrIaHLKcfVvG6dL/fGv7BcMoM=
|
||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/editorconfig v0.1.1-0.20200121172147-e40951bde157/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8=
|
||||
mvdan.cc/sh/v3 v3.1.2 h1:PG5BYlwtrkZTbJXUy25r0/q9shB5ObttCaknkOIB1XQ=
|
||||
mvdan.cc/sh/v3 v3.1.2/go.mod h1:F+Vm4ZxPJxDKExMLhvjuI50oPnedVXpfjNSrusiTOno=
|
||||
@@ -1,23 +0,0 @@
|
||||
build:
|
||||
binary: task
|
||||
main: cmd/task/task.go
|
||||
goos:
|
||||
- windows
|
||||
- darwin
|
||||
- linux
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
ignore:
|
||||
- goos: darwin
|
||||
goarch: 386
|
||||
|
||||
archive:
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
|
||||
replacements:
|
||||
amd64: x64
|
||||
386: x86
|
||||
darwin: macOS
|
||||
27
help.go
27
help.go
@@ -2,32 +2,41 @@ package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
func (e *Executor) printExistingTasksHelp() {
|
||||
// PrintTasksHelp prints help os tasks that have a description
|
||||
func (e *Executor) PrintTasksHelp() {
|
||||
tasks := e.tasksWithDesc()
|
||||
if len(tasks) == 0 {
|
||||
e.Logger.Outf(logger.Yellow, "task: No tasks with description available")
|
||||
return
|
||||
}
|
||||
fmt.Println("Available tasks for this project:")
|
||||
e.Logger.Outf(logger.Default, "task: Available tasks for this project:")
|
||||
|
||||
// Format in tab-separated columns with a tab stop of 8.
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
|
||||
w := tabwriter.NewWriter(e.Stdout, 0, 8, 0, '\t', 0)
|
||||
for _, task := range tasks {
|
||||
fmt.Fprintln(w, fmt.Sprintf("- %s:\t%s", task, e.Tasks[task].Desc))
|
||||
fmt.Fprintf(w, "* %s: \t%s\n", task.Name(), task.Desc)
|
||||
}
|
||||
w.Flush()
|
||||
}
|
||||
|
||||
func (e *Executor) tasksWithDesc() (tasks []string) {
|
||||
for name, task := range e.Tasks {
|
||||
func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) {
|
||||
tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks))
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
if task.Desc != "" {
|
||||
tasks = append(tasks, name)
|
||||
compiledTask, err := e.CompiledTask(taskfile.Call{Task: task.Task})
|
||||
if err == nil {
|
||||
task = compiledTask
|
||||
}
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
sort.Strings(tasks)
|
||||
sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task })
|
||||
return
|
||||
}
|
||||
|
||||
33
init.go
33
init.go
@@ -1,29 +1,38 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const defaultTaskfile = `# github.com/go-task/task
|
||||
const defaultTaskfile = `# https://taskfile.dev
|
||||
|
||||
default:
|
||||
cmds:
|
||||
- echo "Hello, World!"
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "{{.GREETING}}"
|
||||
silent: true
|
||||
`
|
||||
|
||||
// InitTaskfile Taskfile creates a new Taskfile
|
||||
func InitTaskfile() error {
|
||||
for _, f := range []string{"Taskfile.yml", "Taskfile.toml", "Taskfile.json"} {
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return ErrTaskfileAlreadyExists
|
||||
}
|
||||
func InitTaskfile(w io.Writer, dir string) error {
|
||||
f := filepath.Join(dir, "Taskfile.yml")
|
||||
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
return ErrTaskfileAlreadyExists
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("Taskfile.yml", []byte(defaultTaskfile), 0666); err != nil {
|
||||
if err := ioutil.WriteFile(f, []byte(defaultTaskfile), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Taskfile.yml created in the current directory")
|
||||
fmt.Fprintf(w, "Taskfile.yml created in the current directory\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
390
install-task.sh
Executable file
390
install-task.sh
Executable file
@@ -0,0 +1,390 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
# Code generated by godownloader on 2018-04-07T17:47:38Z. DO NOT EDIT.
|
||||
#
|
||||
|
||||
usage() {
|
||||
this=$1
|
||||
cat <<EOF
|
||||
$this: download go binaries for go-task/task
|
||||
|
||||
Usage: $this [-b] bindir [-d] [tag]
|
||||
-b sets bindir or installation directory, Defaults to ./bin
|
||||
-d turns on debug logging
|
||||
[tag] is a tag from
|
||||
https://github.com/go-task/task/releases
|
||||
If tag is missing, then the latest will be used.
|
||||
|
||||
Generated by godownloader
|
||||
https://github.com/goreleaser/godownloader
|
||||
|
||||
EOF
|
||||
exit 2
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
#BINDIR is ./bin unless set be ENV
|
||||
# over-ridden by flag below
|
||||
|
||||
BINDIR=${BINDIR:-./bin}
|
||||
while getopts "b:dh?" arg; do
|
||||
case "$arg" in
|
||||
b) BINDIR="$OPTARG" ;;
|
||||
d) log_set_priority 10 ;;
|
||||
h | \?) usage "$0" ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
TAG=$1
|
||||
}
|
||||
# this function wraps all the destructive operations
|
||||
# if a curl|bash cuts off the end of the script due to
|
||||
# network, either nothing will happen or will syntax error
|
||||
# out preventing half-done work
|
||||
execute() {
|
||||
tmpdir=$(mktmpdir)
|
||||
log_debug "downloading files into ${tmpdir}"
|
||||
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
|
||||
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
|
||||
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
|
||||
srcdir="${tmpdir}"
|
||||
(cd "${tmpdir}" && untar "${TARBALL}")
|
||||
install -d "${BINDIR}"
|
||||
for binexe in "task" ; do
|
||||
if [ "$OS" = "windows" ]; then
|
||||
binexe="${binexe}.exe"
|
||||
fi
|
||||
install "${srcdir}/${binexe}" "${BINDIR}/"
|
||||
log_info "installed ${BINDIR}/${binexe}"
|
||||
done
|
||||
}
|
||||
is_supported_platform() {
|
||||
platform=$1
|
||||
found=1
|
||||
case "$platform" in
|
||||
windows/386) found=0 ;;
|
||||
windows/amd64) found=0 ;;
|
||||
darwin/386) found=0 ;;
|
||||
darwin/amd64) found=0 ;;
|
||||
linux/386) found=0 ;;
|
||||
linux/amd64) found=0 ;;
|
||||
esac
|
||||
case "$platform" in
|
||||
darwin/386) found=1 ;;
|
||||
esac
|
||||
return $found
|
||||
}
|
||||
check_platform() {
|
||||
if is_supported_platform "$PLATFORM"; then
|
||||
# optional logging goes here
|
||||
true
|
||||
else
|
||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
tag_to_version() {
|
||||
if [ -z "${TAG}" ]; then
|
||||
log_info "checking GitHub for latest tag"
|
||||
else
|
||||
log_info "checking GitHub for tag '${TAG}'"
|
||||
fi
|
||||
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
|
||||
if test -z "$REALTAG"; then
|
||||
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
|
||||
exit 1
|
||||
fi
|
||||
# if version starts with 'v', remove it
|
||||
TAG="$REALTAG"
|
||||
VERSION=${TAG#v}
|
||||
}
|
||||
adjust_format() {
|
||||
# change format (tar.gz or zip) based on ARCH
|
||||
case ${ARCH} in
|
||||
windows) FORMAT=zip ;;
|
||||
esac
|
||||
true
|
||||
}
|
||||
adjust_os() {
|
||||
# adjust archive name based on OS
|
||||
true
|
||||
}
|
||||
adjust_arch() {
|
||||
# adjust archive name based on ARCH
|
||||
true
|
||||
}
|
||||
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
https://github.com/client9/shlib - portable posix shell functions
|
||||
Public domain - http://unlicense.org
|
||||
https://github.com/client9/shlib/blob/master/LICENSE.md
|
||||
but credit (and pull requests) appreciated.
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
is_command() {
|
||||
command -v "$1" >/dev/null
|
||||
}
|
||||
echoerr() {
|
||||
echo "$@" 1>&2
|
||||
}
|
||||
log_prefix() {
|
||||
echo "$0"
|
||||
}
|
||||
_logp=6
|
||||
log_set_priority() {
|
||||
_logp="$1"
|
||||
}
|
||||
log_priority() {
|
||||
if test -z "$1"; then
|
||||
echo "$_logp"
|
||||
return
|
||||
fi
|
||||
[ "$1" -le "$_logp" ]
|
||||
}
|
||||
log_tag() {
|
||||
case $1 in
|
||||
0) echo "emerg" ;;
|
||||
1) echo "alert" ;;
|
||||
2) echo "crit" ;;
|
||||
3) echo "err" ;;
|
||||
4) echo "warning" ;;
|
||||
5) echo "notice" ;;
|
||||
6) echo "info" ;;
|
||||
7) echo "debug" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
log_debug() {
|
||||
log_priority 7 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
|
||||
}
|
||||
log_info() {
|
||||
log_priority 6 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
|
||||
}
|
||||
log_err() {
|
||||
log_priority 3 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
|
||||
}
|
||||
log_crit() {
|
||||
log_priority 2 || return 0
|
||||
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
|
||||
}
|
||||
uname_os() {
|
||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||
case "$os" in
|
||||
msys_nt) os="windows" ;;
|
||||
esac
|
||||
echo "$os"
|
||||
}
|
||||
uname_arch() {
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64) arch="amd64" ;;
|
||||
x86) arch="386" ;;
|
||||
i686) arch="386" ;;
|
||||
i386) arch="386" ;;
|
||||
aarch64) arch="arm64" ;;
|
||||
armv5*) arch="arm5" ;;
|
||||
armv6*) arch="arm6" ;;
|
||||
armv7*) arch="arm7" ;;
|
||||
esac
|
||||
echo ${arch}
|
||||
}
|
||||
uname_os_check() {
|
||||
os=$(uname_os)
|
||||
case "$os" in
|
||||
darwin) return 0 ;;
|
||||
dragonfly) return 0 ;;
|
||||
freebsd) return 0 ;;
|
||||
linux) return 0 ;;
|
||||
android) return 0 ;;
|
||||
nacl) return 0 ;;
|
||||
netbsd) return 0 ;;
|
||||
openbsd) return 0 ;;
|
||||
plan9) return 0 ;;
|
||||
solaris) return 0 ;;
|
||||
windows) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
uname_arch_check() {
|
||||
arch=$(uname_arch)
|
||||
case "$arch" in
|
||||
386) return 0 ;;
|
||||
amd64) return 0 ;;
|
||||
arm64) return 0 ;;
|
||||
armv5) return 0 ;;
|
||||
armv6) return 0 ;;
|
||||
armv7) return 0 ;;
|
||||
ppc64) return 0 ;;
|
||||
ppc64le) return 0 ;;
|
||||
mips) return 0 ;;
|
||||
mipsle) return 0 ;;
|
||||
mips64) return 0 ;;
|
||||
mips64le) return 0 ;;
|
||||
s390x) return 0 ;;
|
||||
amd64p32) return 0 ;;
|
||||
esac
|
||||
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
||||
return 1
|
||||
}
|
||||
untar() {
|
||||
tarball=$1
|
||||
case "${tarball}" in
|
||||
*.tar.gz | *.tgz) tar -xzf "${tarball}" ;;
|
||||
*.tar) tar -xf "${tarball}" ;;
|
||||
*.zip) unzip "${tarball}" ;;
|
||||
*)
|
||||
log_err "untar unknown archive format for ${tarball}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
mktmpdir() {
|
||||
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"
|
||||
mkdir -p "${TMPDIR}"
|
||||
echo "${TMPDIR}"
|
||||
}
|
||||
http_download_curl() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
|
||||
else
|
||||
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
|
||||
fi
|
||||
if [ "$code" != "200" ]; then
|
||||
log_debug "http_download_curl received HTTP status $code"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
http_download_wget() {
|
||||
local_file=$1
|
||||
source_url=$2
|
||||
header=$3
|
||||
if [ -z "$header" ]; then
|
||||
wget -q -O "$local_file" "$source_url"
|
||||
else
|
||||
wget -q --header "$header" -O "$local_file" "$source_url"
|
||||
fi
|
||||
}
|
||||
http_download() {
|
||||
log_debug "http_download $2"
|
||||
if is_command curl; then
|
||||
http_download_curl "$@"
|
||||
return
|
||||
elif is_command wget; then
|
||||
http_download_wget "$@"
|
||||
return
|
||||
fi
|
||||
log_crit "http_download unable to find wget or curl"
|
||||
return 1
|
||||
}
|
||||
http_copy() {
|
||||
tmp=$(mktemp)
|
||||
http_download "${tmp}" "$1" "$2" || return 1
|
||||
body=$(cat "$tmp")
|
||||
rm -f "${tmp}"
|
||||
echo "$body"
|
||||
}
|
||||
github_release() {
|
||||
owner_repo=$1
|
||||
version=$2
|
||||
test -z "$version" && version="latest"
|
||||
giturl="https://github.com/${owner_repo}/releases/${version}"
|
||||
json=$(http_copy "$giturl" "Accept:application/json")
|
||||
test -z "$json" && return 1
|
||||
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
|
||||
test -z "$version" && return 1
|
||||
echo "$version"
|
||||
}
|
||||
hash_sha256() {
|
||||
TARGET=${1:-/dev/stdin}
|
||||
if is_command gsha256sum; then
|
||||
hash=$(gsha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command sha256sum; then
|
||||
hash=$(sha256sum "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command shasum; then
|
||||
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
||||
echo "$hash" | cut -d ' ' -f 1
|
||||
elif is_command openssl; then
|
||||
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
||||
echo "$hash" | cut -d ' ' -f a
|
||||
else
|
||||
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
hash_sha256_verify() {
|
||||
TARGET=$1
|
||||
checksums=$2
|
||||
if [ -z "$checksums" ]; then
|
||||
log_err "hash_sha256_verify checksum file not specified in arg2"
|
||||
return 1
|
||||
fi
|
||||
BASENAME=${TARGET##*/}
|
||||
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
||||
if [ -z "$want" ]; then
|
||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
||||
return 1
|
||||
fi
|
||||
got=$(hash_sha256 "$TARGET")
|
||||
if [ "$want" != "$got" ]; then
|
||||
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
cat /dev/null <<EOF
|
||||
------------------------------------------------------------------------
|
||||
End of functions from https://github.com/client9/shlib
|
||||
------------------------------------------------------------------------
|
||||
EOF
|
||||
|
||||
PROJECT_NAME="task"
|
||||
OWNER=go-task
|
||||
REPO="task"
|
||||
BINARY=task
|
||||
FORMAT=tar.gz
|
||||
OS=$(uname_os)
|
||||
ARCH=$(uname_arch)
|
||||
PREFIX="$OWNER/$REPO"
|
||||
|
||||
# use in logging routines
|
||||
log_prefix() {
|
||||
echo "$PREFIX"
|
||||
}
|
||||
PLATFORM="${OS}/${ARCH}"
|
||||
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
|
||||
|
||||
uname_os_check "$OS"
|
||||
uname_arch_check "$ARCH"
|
||||
|
||||
parse_args "$@"
|
||||
|
||||
check_platform
|
||||
|
||||
tag_to_version
|
||||
|
||||
adjust_format
|
||||
|
||||
adjust_os
|
||||
|
||||
adjust_arch
|
||||
|
||||
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
|
||||
|
||||
NAME=${BINARY}_${OS}_${ARCH}
|
||||
TARBALL=${NAME}.${FORMAT}
|
||||
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
|
||||
CHECKSUM=task_checksums.txt
|
||||
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
|
||||
|
||||
|
||||
execute
|
||||
70
internal/args/args.go
Normal file
70
internal/args/args.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
// ParseV3 parses command line argument: tasks and global variables
|
||||
func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) {
|
||||
var calls []taskfile.Call
|
||||
var globals *taskfile.Vars
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "=") {
|
||||
calls = append(calls, taskfile.Call{Task: arg})
|
||||
continue
|
||||
}
|
||||
|
||||
if globals == nil {
|
||||
globals = &taskfile.Vars{}
|
||||
}
|
||||
name, value := splitVar(arg)
|
||||
globals.Set(name, taskfile.Var{Static: value})
|
||||
}
|
||||
|
||||
if len(calls) == 0 {
|
||||
calls = append(calls, taskfile.Call{Task: "default"})
|
||||
}
|
||||
|
||||
return calls, globals
|
||||
}
|
||||
|
||||
// ParseV2 parses command line argument: tasks and vars of each task
|
||||
func ParseV2(args ...string) ([]taskfile.Call, *taskfile.Vars) {
|
||||
var calls []taskfile.Call
|
||||
var globals *taskfile.Vars
|
||||
|
||||
for _, arg := range args {
|
||||
if !strings.Contains(arg, "=") {
|
||||
calls = append(calls, taskfile.Call{Task: arg})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(calls) < 1 {
|
||||
if globals == nil {
|
||||
globals = &taskfile.Vars{}
|
||||
}
|
||||
name, value := splitVar(arg)
|
||||
globals.Set(name, taskfile.Var{Static: value})
|
||||
} else {
|
||||
if calls[len(calls)-1].Vars == nil {
|
||||
calls[len(calls)-1].Vars = &taskfile.Vars{}
|
||||
}
|
||||
name, value := splitVar(arg)
|
||||
calls[len(calls)-1].Vars.Set(name, taskfile.Var{Static: value})
|
||||
}
|
||||
}
|
||||
|
||||
if len(calls) == 0 {
|
||||
calls = append(calls, taskfile.Call{Task: "default"})
|
||||
}
|
||||
|
||||
return calls, globals
|
||||
}
|
||||
|
||||
func splitVar(s string) (string, string) {
|
||||
pair := strings.SplitN(s, "=", 2)
|
||||
return pair[0], pair[1]
|
||||
}
|
||||
204
internal/args/args_test.go
Normal file
204
internal/args/args_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package args_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/args"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestArgsV3(t *testing.T) {
|
||||
tests := []struct {
|
||||
Args []string
|
||||
ExpectedCalls []taskfile.Call
|
||||
ExpectedGlobals *taskfile.Vars
|
||||
}{
|
||||
{
|
||||
Args: []string{"task-a", "task-b", "task-c"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
{Task: "task-b"},
|
||||
{Task: "task-c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
{Task: "task-b"},
|
||||
{Task: "task-c"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"FOO", "BAR", "BAZ"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": taskfile.Var{Static: "bar"},
|
||||
"BAR": taskfile.Var{Static: "baz"},
|
||||
"BAZ": taskfile.Var{Static: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"task-a", "CONTENT=with some spaces"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"CONTENT"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"CONTENT": taskfile.Var{Static: "with some spaces"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"FOO=bar", "task-a", "task-b"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
{Task: "task-b"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"FOO"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": {Static: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: nil,
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"FOO=bar", "BAR=baz"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"FOO", "BAR"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": {Static: "bar"},
|
||||
"BAR": {Static: "baz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
|
||||
calls, globals := args.ParseV3(test.Args...)
|
||||
assert.Equal(t, test.ExpectedCalls, calls)
|
||||
assert.Equal(t, test.ExpectedGlobals, globals)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgsV2(t *testing.T) {
|
||||
tests := []struct {
|
||||
Args []string
|
||||
ExpectedCalls []taskfile.Call
|
||||
ExpectedGlobals *taskfile.Vars
|
||||
}{
|
||||
{
|
||||
Args: []string{"task-a", "task-b", "task-c"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
{Task: "task-b"},
|
||||
{Task: "task-c"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{
|
||||
Task: "task-a",
|
||||
Vars: &taskfile.Vars{
|
||||
Keys: []string{"FOO"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": taskfile.Var{Static: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{Task: "task-b"},
|
||||
{
|
||||
Task: "task-c",
|
||||
Vars: &taskfile.Vars{
|
||||
Keys: []string{"BAR", "BAZ"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"BAR": taskfile.Var{Static: "baz"},
|
||||
"BAZ": taskfile.Var{Static: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"task-a", "CONTENT=with some spaces"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{
|
||||
Task: "task-a",
|
||||
Vars: &taskfile.Vars{
|
||||
Keys: []string{"CONTENT"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"CONTENT": taskfile.Var{Static: "with some spaces"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"FOO=bar", "task-a", "task-b"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "task-a"},
|
||||
{Task: "task-b"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"FOO"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": {Static: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: nil,
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Args: []string{"FOO=bar", "BAR=baz"},
|
||||
ExpectedCalls: []taskfile.Call{
|
||||
{Task: "default"},
|
||||
},
|
||||
ExpectedGlobals: &taskfile.Vars{
|
||||
Keys: []string{"FOO", "BAR"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"FOO": {Static: "bar"},
|
||||
"BAR": {Static: "baz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
|
||||
calls, globals := args.ParseV2(test.Args...)
|
||||
assert.Equal(t, test.ExpectedCalls, calls)
|
||||
assert.Equal(t, test.ExpectedGlobals, globals)
|
||||
})
|
||||
}
|
||||
}
|
||||
12
internal/compiler/compiler.go
Normal file
12
internal/compiler/compiler.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
// Compiler handles compilation of a task before its execution.
|
||||
// E.g. variable merger, template processing, etc.
|
||||
type Compiler interface {
|
||||
GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error)
|
||||
HandleDynamicVar(v taskfile.Var) (string, error)
|
||||
}
|
||||
20
internal/compiler/env.go
Normal file
20
internal/compiler/env.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
// GetEnviron the all return all environment variables encapsulated on a
|
||||
// taskfile.Vars
|
||||
func GetEnviron() *taskfile.Vars {
|
||||
m := &taskfile.Vars{}
|
||||
for _, e := range os.Environ() {
|
||||
keyVal := strings.SplitN(e, "=", 2)
|
||||
key, val := keyVal[0], keyVal[1]
|
||||
m.Set(key, taskfile.Var{Static: val})
|
||||
}
|
||||
return m
|
||||
}
|
||||
111
internal/compiler/v2/compiler_v2.go
Normal file
111
internal/compiler/v2/compiler_v2.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
)
|
||||
|
||||
var _ compiler.Compiler = &CompilerV2{}
|
||||
|
||||
type CompilerV2 struct {
|
||||
Dir string
|
||||
|
||||
Taskvars *taskfile.Vars
|
||||
TaskfileVars *taskfile.Vars
|
||||
|
||||
Expansions int
|
||||
|
||||
Logger *logger.Logger
|
||||
|
||||
dynamicCache map[string]string
|
||||
muDynamicCache sync.Mutex
|
||||
}
|
||||
|
||||
// GetVariables returns fully resolved variables following the priority order:
|
||||
// 1. Task variables
|
||||
// 2. Call variables
|
||||
// 3. Taskfile variables
|
||||
// 4. Taskvars file variables
|
||||
// 5. Environment variables
|
||||
func (c *CompilerV2) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
|
||||
vr := varResolver{c: c, vars: compiler.GetEnviron()}
|
||||
vr.vars.Set("TASK", taskfile.Var{Static: t.Task})
|
||||
for _, vars := range []*taskfile.Vars{c.Taskvars, c.TaskfileVars, call.Vars, t.Vars} {
|
||||
for i := 0; i < c.Expansions; i++ {
|
||||
vr.merge(vars)
|
||||
}
|
||||
}
|
||||
return vr.vars, vr.err
|
||||
}
|
||||
|
||||
type varResolver struct {
|
||||
c *CompilerV2
|
||||
vars *taskfile.Vars
|
||||
err error
|
||||
}
|
||||
|
||||
func (vr *varResolver) merge(vars *taskfile.Vars) {
|
||||
if vr.err != nil {
|
||||
return
|
||||
}
|
||||
tr := templater.Templater{Vars: vr.vars}
|
||||
vars.Range(func(k string, v taskfile.Var) error {
|
||||
v = taskfile.Var{
|
||||
Static: tr.Replace(v.Static),
|
||||
Sh: tr.Replace(v.Sh),
|
||||
}
|
||||
static, err := vr.c.HandleDynamicVar(v)
|
||||
if err != nil {
|
||||
vr.err = err
|
||||
return err
|
||||
}
|
||||
vr.vars.Set(k, taskfile.Var{Static: static})
|
||||
return nil
|
||||
})
|
||||
vr.err = tr.Err()
|
||||
}
|
||||
|
||||
func (c *CompilerV2) HandleDynamicVar(v taskfile.Var) (string, error) {
|
||||
if v.Static != "" || v.Sh == "" {
|
||||
return v.Static, nil
|
||||
}
|
||||
|
||||
c.muDynamicCache.Lock()
|
||||
defer c.muDynamicCache.Unlock()
|
||||
|
||||
if c.dynamicCache == nil {
|
||||
c.dynamicCache = make(map[string]string, 30)
|
||||
}
|
||||
if result, ok := c.dynamicCache[v.Sh]; ok {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
opts := &execext.RunCommandOptions{
|
||||
Command: v.Sh,
|
||||
Dir: c.Dir,
|
||||
Stdout: &stdout,
|
||||
Stderr: c.Logger.Stderr,
|
||||
}
|
||||
if err := execext.RunCommand(context.Background(), opts); err != nil {
|
||||
return "", fmt.Errorf(`task: Command "%s" in taskvars file failed: %s`, opts.Command, err)
|
||||
}
|
||||
|
||||
// Trim a single trailing newline from the result to make most command
|
||||
// output easier to use in shell commands.
|
||||
result := strings.TrimSuffix(stdout.String(), "\n")
|
||||
|
||||
c.dynamicCache[v.Sh] = result
|
||||
c.Logger.VerboseErrf(logger.Magenta, `task: dynamic variable: '%s' result: '%s'`, v.Sh, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
98
internal/compiler/v3/compiler_v3.go
Normal file
98
internal/compiler/v3/compiler_v3.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package v3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
)
|
||||
|
||||
var _ compiler.Compiler = &CompilerV3{}
|
||||
|
||||
type CompilerV3 struct {
|
||||
Dir string
|
||||
|
||||
TaskfileVars *taskfile.Vars
|
||||
|
||||
Logger *logger.Logger
|
||||
|
||||
dynamicCache map[string]string
|
||||
muDynamicCache sync.Mutex
|
||||
}
|
||||
|
||||
func (c *CompilerV3) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
|
||||
result := compiler.GetEnviron()
|
||||
result.Set("TASK", taskfile.Var{Static: t.Task})
|
||||
|
||||
rangeFunc := func(k string, v taskfile.Var) error {
|
||||
tr := templater.Templater{Vars: result, RemoveNoValue: true}
|
||||
v = taskfile.Var{
|
||||
Static: tr.Replace(v.Static),
|
||||
Sh: tr.Replace(v.Sh),
|
||||
}
|
||||
if err := tr.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
static, err := c.HandleDynamicVar(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result.Set(k, taskfile.Var{Static: static})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := call.Vars.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := t.Vars.Range(rangeFunc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *CompilerV3) HandleDynamicVar(v taskfile.Var) (string, error) {
|
||||
if v.Static != "" || v.Sh == "" {
|
||||
return v.Static, nil
|
||||
}
|
||||
|
||||
c.muDynamicCache.Lock()
|
||||
defer c.muDynamicCache.Unlock()
|
||||
|
||||
if c.dynamicCache == nil {
|
||||
c.dynamicCache = make(map[string]string, 30)
|
||||
}
|
||||
if result, ok := c.dynamicCache[v.Sh]; ok {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
opts := &execext.RunCommandOptions{
|
||||
Command: v.Sh,
|
||||
Dir: c.Dir,
|
||||
Stdout: &stdout,
|
||||
Stderr: c.Logger.Stderr,
|
||||
}
|
||||
if err := execext.RunCommand(context.Background(), opts); err != nil {
|
||||
return "", fmt.Errorf(`task: Command "%s" in taskvars file failed: %s`, opts.Command, err)
|
||||
}
|
||||
|
||||
// Trim a single trailing newline from the result to make most command
|
||||
// output easier to use in shell commands.
|
||||
result := strings.TrimSuffix(stdout.String(), "\n")
|
||||
|
||||
c.dynamicCache[v.Sh] = result
|
||||
c.Logger.VerboseErrf(logger.Magenta, `task: dynamic variable: '%s' result: '%s'`, v.Sh, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
13
internal/execext/devnull.go
Normal file
13
internal/execext/devnull.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package execext
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
var _ io.ReadWriteCloser = devNull{}
|
||||
|
||||
type devNull struct{}
|
||||
|
||||
func (devNull) Read(p []byte) (int, error) { return 0, io.EOF }
|
||||
func (devNull) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (devNull) Close() error { return nil }
|
||||
88
internal/execext/exec.go
Normal file
88
internal/execext/exec.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package execext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"mvdan.cc/sh/v3/expand"
|
||||
"mvdan.cc/sh/v3/interp"
|
||||
"mvdan.cc/sh/v3/shell"
|
||||
"mvdan.cc/sh/v3/syntax"
|
||||
)
|
||||
|
||||
// RunCommandOptions is the options for the RunCommand func
|
||||
type RunCommandOptions struct {
|
||||
Command string
|
||||
Dir string
|
||||
Env []string
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNilOptions is returned when a nil options is given
|
||||
ErrNilOptions = errors.New("execext: nil options given")
|
||||
)
|
||||
|
||||
// RunCommand runs a shell command
|
||||
func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
|
||||
if opts == nil {
|
||||
return ErrNilOptions
|
||||
}
|
||||
|
||||
p, err := syntax.NewParser().Parse(strings.NewReader(opts.Command), "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environ := opts.Env
|
||||
if len(environ) == 0 {
|
||||
environ = os.Environ()
|
||||
}
|
||||
|
||||
r, err := interp.New(
|
||||
interp.Dir(opts.Dir),
|
||||
interp.Env(expand.ListEnviron(environ...)),
|
||||
|
||||
interp.OpenHandler(func(ctx context.Context, path string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) {
|
||||
if path == "/dev/null" {
|
||||
return devNull{}, nil
|
||||
}
|
||||
return interp.DefaultOpenHandler()(ctx, path, flag, perm)
|
||||
}),
|
||||
|
||||
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Run(ctx, p)
|
||||
}
|
||||
|
||||
// IsExitError returns true the given error is an exis status error
|
||||
func IsExitError(err error) bool {
|
||||
if _, ok := interp.IsExitStatus(err); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
|
||||
// if available.
|
||||
func Expand(s string) (string, error) {
|
||||
s = filepath.ToSlash(s)
|
||||
s = strings.Replace(s, " ", `\ `, -1)
|
||||
fields, err := shell.Fields(s, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(fields) > 0 {
|
||||
return fields[0], nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
64
internal/logger/logger.go
Normal file
64
internal/logger/logger.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type PrintFunc func(io.Writer, string, ...interface{})
|
||||
|
||||
var (
|
||||
Default PrintFunc = color.New(color.Reset).FprintfFunc()
|
||||
Blue PrintFunc = color.New(color.FgBlue).FprintfFunc()
|
||||
Green PrintFunc = color.New(color.FgGreen).FprintfFunc()
|
||||
Cyan PrintFunc = color.New(color.FgCyan).FprintfFunc()
|
||||
Yellow PrintFunc = color.New(color.FgYellow).FprintfFunc()
|
||||
Magenta PrintFunc = color.New(color.FgMagenta).FprintfFunc()
|
||||
Red PrintFunc = color.New(color.FgRed).FprintfFunc()
|
||||
)
|
||||
|
||||
// Logger is just a wrapper that prints stuff to STDOUT or STDERR,
|
||||
// with optional color.
|
||||
type Logger struct {
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
Verbose bool
|
||||
Color bool
|
||||
}
|
||||
|
||||
// Outf prints stuff to STDOUT.
|
||||
func (l *Logger) Outf(print PrintFunc, s string, args ...interface{}) {
|
||||
if len(args) == 0 {
|
||||
s, args = "%s", []interface{}{s}
|
||||
}
|
||||
if !l.Color {
|
||||
print = Default
|
||||
}
|
||||
print(l.Stdout, s+"\n", args...)
|
||||
}
|
||||
|
||||
// VerboseOutf prints stuff to STDOUT if verbose mode is enabled.
|
||||
func (l *Logger) VerboseOutf(print PrintFunc, s string, args ...interface{}) {
|
||||
if l.Verbose {
|
||||
l.Outf(print, s, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Errf prints stuff to STDERR.
|
||||
func (l *Logger) Errf(print PrintFunc, s string, args ...interface{}) {
|
||||
if len(args) == 0 {
|
||||
s, args = "%s", []interface{}{s}
|
||||
}
|
||||
if !l.Color {
|
||||
print = Default
|
||||
}
|
||||
print(l.Stderr, s+"\n", args...)
|
||||
}
|
||||
|
||||
// VerboseErrf prints stuff to STDERR if verbose mode is enabled.
|
||||
func (l *Logger) VerboseErrf(print PrintFunc, s string, args ...interface{}) {
|
||||
if l.Verbose {
|
||||
l.Errf(print, s, args...)
|
||||
}
|
||||
}
|
||||
26
internal/output/group.go
Normal file
26
internal/output/group.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Group struct{}
|
||||
|
||||
func (Group) WrapWriter(w io.Writer, _ string) io.Writer {
|
||||
return &groupWriter{writer: w}
|
||||
}
|
||||
|
||||
type groupWriter struct {
|
||||
writer io.Writer
|
||||
buff bytes.Buffer
|
||||
}
|
||||
|
||||
func (gw *groupWriter) Write(p []byte) (int, error) {
|
||||
return gw.buff.Write(p)
|
||||
}
|
||||
|
||||
func (gw *groupWriter) Close() error {
|
||||
_, err := io.Copy(gw.writer, &gw.buff)
|
||||
return err
|
||||
}
|
||||
11
internal/output/interleaved.go
Normal file
11
internal/output/interleaved.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Interleaved struct{}
|
||||
|
||||
func (Interleaved) WrapWriter(w io.Writer, _ string) io.Writer {
|
||||
return w
|
||||
}
|
||||
9
internal/output/output.go
Normal file
9
internal/output/output.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type Output interface {
|
||||
WrapWriter(w io.Writer, prefix string) io.Writer
|
||||
}
|
||||
63
internal/output/output_test.go
Normal file
63
internal/output/output_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package output_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInterleaved(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Interleaved{}
|
||||
var w = o.WrapWriter(&b, "")
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "foo\nbar\n", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "foo\nbar\nbaz\n", b.String())
|
||||
}
|
||||
|
||||
func TestGroup(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Group{}
|
||||
var w = o.WrapWriter(&b, "").(io.WriteCloser)
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "", b.String())
|
||||
assert.NoError(t, w.Close())
|
||||
assert.Equal(t, "foo\nbar\nbaz\n", b.String())
|
||||
}
|
||||
|
||||
func TestPrefixed(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
var o output.Output = output.Prefixed{}
|
||||
var w = o.WrapWriter(&b, "prefix").(io.WriteCloser)
|
||||
|
||||
t.Run("simple use cases", func(t *testing.T) {
|
||||
b.Reset()
|
||||
|
||||
fmt.Fprintln(w, "foo\nbar")
|
||||
assert.Equal(t, "[prefix] foo\n[prefix] bar\n", b.String())
|
||||
fmt.Fprintln(w, "baz")
|
||||
assert.Equal(t, "[prefix] foo\n[prefix] bar\n[prefix] baz\n", b.String())
|
||||
})
|
||||
|
||||
t.Run("multiple writes for a single line", func(t *testing.T) {
|
||||
b.Reset()
|
||||
|
||||
for _, char := range []string{"T", "e", "s", "t", "!"} {
|
||||
fmt.Fprint(w, char)
|
||||
assert.Equal(t, "", b.String())
|
||||
}
|
||||
|
||||
assert.NoError(t, w.Close())
|
||||
assert.Equal(t, "[prefix] Test!\n", b.String())
|
||||
})
|
||||
}
|
||||
65
internal/output/prefixed.go
Normal file
65
internal/output/prefixed.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Prefixed struct{}
|
||||
|
||||
func (Prefixed) WrapWriter(w io.Writer, prefix string) io.Writer {
|
||||
return &prefixWriter{writer: w, prefix: prefix}
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
writer io.Writer
|
||||
prefix string
|
||||
buff bytes.Buffer
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.buff.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
return n, pw.writeOutputLines(false)
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) Close() error {
|
||||
return pw.writeOutputLines(true)
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) writeOutputLines(force bool) error {
|
||||
for {
|
||||
switch line, err := pw.buff.ReadString('\n'); err {
|
||||
case nil:
|
||||
if err = pw.writeLine(line); err != nil {
|
||||
return err
|
||||
}
|
||||
case io.EOF:
|
||||
// if this line was not a complete line, re-add to the buffer
|
||||
if !force && !strings.HasSuffix(line, "\n") {
|
||||
_, err = pw.buff.WriteString(line)
|
||||
return err
|
||||
}
|
||||
|
||||
return pw.writeLine(line)
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) writeLine(line string) error {
|
||||
if line == "" {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(line, "\n") {
|
||||
line += "\n"
|
||||
}
|
||||
_, err := fmt.Fprintf(pw.writer, "[%s] %s", pw.prefix, line)
|
||||
return err
|
||||
}
|
||||
118
internal/status/checksum.go
Normal file
118
internal/status/checksum.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Checksum validades if a task is up to date by calculating its source
|
||||
// files checksum
|
||||
type Checksum struct {
|
||||
Dir string
|
||||
Task string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Dry bool
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (c *Checksum) IsUpToDate() (bool, error) {
|
||||
if len(c.Sources) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
checksumFile := c.checksumFilePath()
|
||||
|
||||
data, _ := ioutil.ReadFile(checksumFile)
|
||||
oldMd5 := strings.TrimSpace(string(data))
|
||||
|
||||
sources, err := globs(c.Dir, c.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
newMd5, err := c.checksum(sources...)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !c.Dry {
|
||||
_ = os.MkdirAll(filepath.Join(c.Dir, ".task", "checksum"), 0755)
|
||||
if err = ioutil.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.Generates) > 0 {
|
||||
// For each specified 'generates' field, check whether the files actually exist
|
||||
for _, g := range c.Generates {
|
||||
generates, err := glob(c.Dir, g)
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return oldMd5 == newMd5, nil
|
||||
}
|
||||
|
||||
func (c *Checksum) checksum(files ...string) (string, error) {
|
||||
h := md5.New()
|
||||
|
||||
for _, f := range files {
|
||||
// also sum the filename, so checksum changes for renaming a file
|
||||
if _, err := io.Copy(h, strings.NewReader(filepath.Base(f))); err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.Open(f)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = io.Copy(h, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (c *Checksum) Value() (interface{}, error) {
|
||||
return c.checksum()
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (c *Checksum) OnError() error {
|
||||
if len(c.Sources) == 0 {
|
||||
return nil
|
||||
}
|
||||
return os.Remove(c.checksumFilePath())
|
||||
}
|
||||
|
||||
// Kind implements the Checker Interface
|
||||
func (*Checksum) Kind() string {
|
||||
return "checksum"
|
||||
}
|
||||
|
||||
func (c *Checksum) checksumFilePath() string {
|
||||
return filepath.Join(c.Dir, ".task", "checksum", c.normalizeFilename(c.Task))
|
||||
}
|
||||
|
||||
var checksumFilenameRegexp = regexp.MustCompile("[^A-z0-9]")
|
||||
|
||||
// replaces invalid caracters on filenames with "-"
|
||||
func (*Checksum) normalizeFilename(f string) string {
|
||||
return checksumFilenameRegexp.ReplaceAllString(f, "-")
|
||||
}
|
||||
21
internal/status/checksum_test.go
Normal file
21
internal/status/checksum_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNormalizeFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
In, Out string
|
||||
}{
|
||||
{"foobarbaz", "foobarbaz"},
|
||||
{"foo/bar/baz", "foo-bar-baz"},
|
||||
{"foo@bar/baz", "foo-bar-baz"},
|
||||
{"foo1bar2baz3", "foo1bar2baz3"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, test.Out, (&Checksum{}).normalizeFilename(test.In))
|
||||
}
|
||||
}
|
||||
50
internal/status/glob.go
Normal file
50
internal/status/glob.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
|
||||
"github.com/mattn/go-zglob"
|
||||
)
|
||||
|
||||
func globs(dir string, globs []string) ([]string, error) {
|
||||
files := make([]string, 0)
|
||||
for _, g := range globs {
|
||||
f, err := glob(dir, g)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
files = append(files, f...)
|
||||
}
|
||||
sort.Strings(files)
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func glob(dir string, g string) ([]string, error) {
|
||||
files := make([]string, 0)
|
||||
if !filepath.IsAbs(g) {
|
||||
g = filepath.Join(dir, g)
|
||||
}
|
||||
g, err := execext.Expand(g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fs, err := zglob.Glob(g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range fs {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
continue
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
23
internal/status/none.go
Normal file
23
internal/status/none.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package status
|
||||
|
||||
// None is a no-op Checker
|
||||
type None struct{}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (None) IsUpToDate() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Value implements the Checker interface
|
||||
func (None) Value() (interface{}, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (None) Kind() string {
|
||||
return "none"
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (None) OnError() error {
|
||||
return nil
|
||||
}
|
||||
15
internal/status/status.go
Normal file
15
internal/status/status.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package status
|
||||
|
||||
var (
|
||||
_ Checker = &Timestamp{}
|
||||
_ Checker = &Checksum{}
|
||||
_ Checker = None{}
|
||||
)
|
||||
|
||||
// Checker is an interface that checks if the status is up-to-date
|
||||
type Checker interface {
|
||||
IsUpToDate() (bool, error)
|
||||
Value() (interface{}, error)
|
||||
OnError() error
|
||||
Kind() string
|
||||
}
|
||||
108
internal/status/timestamp.go
Normal file
108
internal/status/timestamp.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timestamp checks if any source change compared with the generated files,
|
||||
// using file modifications timestamps.
|
||||
type Timestamp struct {
|
||||
Dir string
|
||||
Sources []string
|
||||
Generates []string
|
||||
}
|
||||
|
||||
// IsUpToDate implements the Checker interface
|
||||
func (t *Timestamp) IsUpToDate() (bool, error) {
|
||||
if len(t.Sources) == 0 || len(t.Generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
generates, err := globs(t.Dir, t.Generates)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getMaxTime(sources...)
|
||||
if err != nil || sourcesMaxTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
generatesMinTime, err := getMinTime(generates...)
|
||||
if err != nil || generatesMinTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return !generatesMinTime.Before(sourcesMaxTime), nil
|
||||
}
|
||||
|
||||
func (t *Timestamp) Kind() string {
|
||||
return "timestamp"
|
||||
}
|
||||
|
||||
// Value implements the Checker Interface
|
||||
func (t *Timestamp) Value() (interface{}, error) {
|
||||
sources, err := globs(t.Dir, t.Sources)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getMaxTime(sources...)
|
||||
if err != nil {
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
if sourcesMaxTime.IsZero() {
|
||||
return time.Unix(0, 0), nil
|
||||
}
|
||||
|
||||
return sourcesMaxTime, nil
|
||||
}
|
||||
|
||||
func getMinTime(files ...string) (time.Time, error) {
|
||||
var t time.Time
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t = minTime(t, info.ModTime())
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func getMaxTime(files ...string) (time.Time, error) {
|
||||
var t time.Time
|
||||
for _, f := range files {
|
||||
info, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t = maxTime(t, info.ModTime())
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func minTime(a, b time.Time) time.Time {
|
||||
if !a.IsZero() && a.Before(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxTime(a, b time.Time) time.Time {
|
||||
if a.After(b) {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// OnError implements the Checker interface
|
||||
func (*Timestamp) OnError() error {
|
||||
return nil
|
||||
}
|
||||
103
internal/summary/summary.go
Normal file
103
internal/summary/summary.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package summary
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
func PrintTasks(l *logger.Logger, t *taskfile.Taskfile, c []taskfile.Call) {
|
||||
for i, call := range c {
|
||||
PrintSpaceBetweenSummaries(l, i)
|
||||
PrintTask(l, t.Tasks[call.Task])
|
||||
}
|
||||
}
|
||||
|
||||
func PrintSpaceBetweenSummaries(l *logger.Logger, i int) {
|
||||
spaceRequired := i > 0
|
||||
if !spaceRequired {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "")
|
||||
l.Outf(logger.Default, "")
|
||||
}
|
||||
|
||||
func PrintTask(l *logger.Logger, t *taskfile.Task) {
|
||||
printTaskName(l, t)
|
||||
printTaskDescribingText(t, l)
|
||||
printTaskDependencies(l, t)
|
||||
printTaskCommands(l, t)
|
||||
}
|
||||
|
||||
func printTaskDescribingText(t *taskfile.Task, l *logger.Logger) {
|
||||
if hasSummary(t) {
|
||||
printTaskSummary(l, t)
|
||||
} else if hasDescription(t) {
|
||||
printTaskDescription(l, t)
|
||||
} else {
|
||||
printNoDescriptionOrSummary(l)
|
||||
}
|
||||
}
|
||||
|
||||
func hasSummary(t *taskfile.Task) bool {
|
||||
return t.Summary != ""
|
||||
}
|
||||
|
||||
func printTaskSummary(l *logger.Logger, t *taskfile.Task) {
|
||||
lines := strings.Split(t.Summary, "\n")
|
||||
for i, line := range lines {
|
||||
notLastLine := i+1 < len(lines)
|
||||
if notLastLine || line != "" {
|
||||
l.Outf(logger.Default, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printTaskName(l *logger.Logger, t *taskfile.Task) {
|
||||
l.Outf(logger.Default, "task: %s", t.Name())
|
||||
l.Outf(logger.Default, "")
|
||||
}
|
||||
|
||||
func hasDescription(t *taskfile.Task) bool {
|
||||
return t.Desc != ""
|
||||
}
|
||||
|
||||
func printTaskDescription(l *logger.Logger, t *taskfile.Task) {
|
||||
l.Outf(logger.Default, t.Desc)
|
||||
}
|
||||
|
||||
func printNoDescriptionOrSummary(l *logger.Logger) {
|
||||
l.Outf(logger.Default, "(task does not have description or summary)")
|
||||
}
|
||||
|
||||
func printTaskDependencies(l *logger.Logger, t *taskfile.Task) {
|
||||
if len(t.Deps) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "")
|
||||
l.Outf(logger.Default, "dependencies:")
|
||||
|
||||
for _, d := range t.Deps {
|
||||
l.Outf(logger.Default, " - %s", d.Task)
|
||||
}
|
||||
}
|
||||
|
||||
func printTaskCommands(l *logger.Logger, t *taskfile.Task) {
|
||||
if len(t.Cmds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
l.Outf(logger.Default, "")
|
||||
l.Outf(logger.Default, "commands:")
|
||||
for _, c := range t.Cmds {
|
||||
isCommand := c.Cmd != ""
|
||||
if isCommand {
|
||||
l.Outf(logger.Default, " - %s", c.Cmd)
|
||||
} else {
|
||||
l.Outf(logger.Default, " - Task: %s", c.Task)
|
||||
}
|
||||
}
|
||||
}
|
||||
173
internal/summary/summary_test.go
Normal file
173
internal/summary/summary_test.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package summary_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/summary"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrintsDependenciesIfPresent(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Deps: []*taskfile.Dep{
|
||||
{Task: "dep1"},
|
||||
{Task: "dep2"},
|
||||
{Task: "dep3"},
|
||||
},
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.Contains(t, buffer.String(), "\ndependencies:\n - dep1\n - dep2\n - dep3\n")
|
||||
}
|
||||
|
||||
func createDummyLogger() (*bytes.Buffer, logger.Logger) {
|
||||
buffer := &bytes.Buffer{}
|
||||
l := logger.Logger{
|
||||
Stderr: buffer,
|
||||
Stdout: buffer,
|
||||
Verbose: false,
|
||||
}
|
||||
return buffer, l
|
||||
}
|
||||
|
||||
func TestDoesNotPrintDependenciesIfMissing(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Deps: []*taskfile.Dep{},
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.NotContains(t, buffer.String(), "dependencies:")
|
||||
}
|
||||
|
||||
func TestPrintTaskName(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Task: "my-task-name",
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.Contains(t, buffer.String(), "task: my-task-name\n")
|
||||
}
|
||||
|
||||
func TestPrintTaskCommandsIfPresent(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Cmds: []*taskfile.Cmd{
|
||||
{Cmd: "command-1"},
|
||||
{Cmd: "command-2"},
|
||||
{Task: "task-1"},
|
||||
},
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.Contains(t, buffer.String(), "\ncommands:\n")
|
||||
assert.Contains(t, buffer.String(), "\n - command-1\n")
|
||||
assert.Contains(t, buffer.String(), "\n - command-2\n")
|
||||
assert.Contains(t, buffer.String(), "\n - Task: task-1\n")
|
||||
}
|
||||
|
||||
func TestDoesNotPrintCommandIfMissing(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Cmds: []*taskfile.Cmd{},
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.NotContains(t, buffer.String(), "commands")
|
||||
}
|
||||
|
||||
func TestLayout(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
task := &taskfile.Task{
|
||||
Task: "sample-task",
|
||||
Summary: "line1\nline2\nline3\n",
|
||||
Deps: []*taskfile.Dep{
|
||||
{Task: "dependency"},
|
||||
},
|
||||
Cmds: []*taskfile.Cmd{
|
||||
{Cmd: "command"},
|
||||
},
|
||||
}
|
||||
|
||||
summary.PrintTask(&l, task)
|
||||
|
||||
assert.Equal(t, expectedOutput(), buffer.String())
|
||||
}
|
||||
|
||||
func expectedOutput() string {
|
||||
expected := `task: sample-task
|
||||
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
|
||||
dependencies:
|
||||
- dependency
|
||||
|
||||
commands:
|
||||
- command
|
||||
`
|
||||
return expected
|
||||
}
|
||||
|
||||
func TestPrintDescriptionAsFallback(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
taskWithoutSummary := &taskfile.Task{
|
||||
Desc: "description",
|
||||
}
|
||||
|
||||
taskWithSummary := &taskfile.Task{
|
||||
Desc: "description",
|
||||
Summary: "summary",
|
||||
}
|
||||
taskWithoutSummaryOrDescription := &taskfile.Task{}
|
||||
|
||||
summary.PrintTask(&l, taskWithoutSummary)
|
||||
|
||||
assert.Contains(t, buffer.String(), "description")
|
||||
|
||||
buffer.Reset()
|
||||
summary.PrintTask(&l, taskWithSummary)
|
||||
|
||||
assert.NotContains(t, buffer.String(), "description")
|
||||
|
||||
buffer.Reset()
|
||||
summary.PrintTask(&l, taskWithoutSummaryOrDescription)
|
||||
|
||||
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n")
|
||||
|
||||
}
|
||||
|
||||
func TestPrintAllWithSpaces(t *testing.T) {
|
||||
buffer, l := createDummyLogger()
|
||||
|
||||
t1 := &taskfile.Task{Task: "t1"}
|
||||
t2 := &taskfile.Task{Task: "t2"}
|
||||
t3 := &taskfile.Task{Task: "t3"}
|
||||
|
||||
tasks := make(taskfile.Tasks, 3)
|
||||
tasks["t1"] = t1
|
||||
tasks["t2"] = t2
|
||||
tasks["t3"] = t3
|
||||
|
||||
summary.PrintTasks(&l,
|
||||
&taskfile.Taskfile{Tasks: tasks},
|
||||
[]taskfile.Call{{Task: "t1"}, {Task: "t2"}, {Task: "t3"}})
|
||||
|
||||
assert.True(t, strings.HasPrefix(buffer.String(), "task: t1"))
|
||||
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t2")
|
||||
assert.Contains(t, buffer.String(), "\n(task does not have description or summary)\n\n\ntask: t3")
|
||||
|
||||
}
|
||||
7
internal/taskfile/call.go
Normal file
7
internal/taskfile/call.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package taskfile
|
||||
|
||||
// Call is the parameters to a task call
|
||||
type Call struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
81
internal/taskfile/cmd.go
Normal file
81
internal/taskfile/cmd.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Cmd is a task command
|
||||
type Cmd struct {
|
||||
Cmd string
|
||||
Silent bool
|
||||
Task string
|
||||
Vars *Vars
|
||||
IgnoreError bool
|
||||
}
|
||||
|
||||
// Dep is a task dependency
|
||||
type Dep struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrCantUnmarshalCmd is returned for invalid command YAML
|
||||
ErrCantUnmarshalCmd = errors.New("task: can't unmarshal cmd value")
|
||||
// ErrCantUnmarshalDep is returned for invalid dependency YAML
|
||||
ErrCantUnmarshalDep = errors.New("task: can't unmarshal dep value")
|
||||
)
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd string
|
||||
if err := unmarshal(&cmd); err == nil {
|
||||
if strings.HasPrefix(cmd, "^") {
|
||||
c.Task = strings.TrimPrefix(cmd, "^")
|
||||
} else {
|
||||
c.Cmd = cmd
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var cmdStruct struct {
|
||||
Cmd string
|
||||
Silent bool
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
}
|
||||
if err := unmarshal(&cmdStruct); err == nil && cmdStruct.Cmd != "" {
|
||||
c.Cmd = cmdStruct.Cmd
|
||||
c.Silent = cmdStruct.Silent
|
||||
c.IgnoreError = cmdStruct.IgnoreError
|
||||
return nil
|
||||
}
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := unmarshal(&taskCall); err == nil {
|
||||
c.Task = taskCall.Task
|
||||
c.Vars = taskCall.Vars
|
||||
return nil
|
||||
}
|
||||
return ErrCantUnmarshalCmd
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var task string
|
||||
if err := unmarshal(&task); err == nil {
|
||||
d.Task = task
|
||||
return nil
|
||||
}
|
||||
var taskCall struct {
|
||||
Task string
|
||||
Vars *Vars
|
||||
}
|
||||
if err := unmarshal(&taskCall); err == nil {
|
||||
d.Task = taskCall.Task
|
||||
d.Vars = taskCall.Vars
|
||||
return nil
|
||||
}
|
||||
return ErrCantUnmarshalDep
|
||||
}
|
||||
40
internal/taskfile/included_taskfile.go
Normal file
40
internal/taskfile/included_taskfile.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package taskfile
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrCantUnmarshalIncludedTaskfile is returned for invalid var YAML.
|
||||
ErrCantUnmarshalIncludedTaskfile = errors.New("task: can't unmarshal included value")
|
||||
)
|
||||
|
||||
// IncludedTaskfile represents information about included tasksfile
|
||||
type IncludedTaskfile struct {
|
||||
Taskfile string
|
||||
Dir string
|
||||
AdvancedImport bool
|
||||
}
|
||||
|
||||
// IncludedTaskfiles represents information about included tasksfiles
|
||||
type IncludedTaskfiles = map[string]IncludedTaskfile
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (it *IncludedTaskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var str string
|
||||
if err := unmarshal(&str); err == nil {
|
||||
it.Taskfile = str
|
||||
return nil
|
||||
}
|
||||
|
||||
var includedTaskfile struct {
|
||||
Taskfile string
|
||||
Dir string
|
||||
}
|
||||
if err := unmarshal(&includedTaskfile); err == nil {
|
||||
it.Dir = includedTaskfile.Dir
|
||||
it.Taskfile = includedTaskfile.Taskfile
|
||||
it.AdvancedImport = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrCantUnmarshalIncludedTaskfile
|
||||
}
|
||||
68
internal/taskfile/merge.go
Normal file
68
internal/taskfile/merge.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NamespaceSeparator contains the character that separates namescapes
|
||||
const NamespaceSeparator = ":"
|
||||
|
||||
// Merge merges the second Taskfile into the first
|
||||
func Merge(t1, t2 *Taskfile, namespaces ...string) error {
|
||||
if t1.Version != t2.Version {
|
||||
return fmt.Errorf(`Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
|
||||
}
|
||||
|
||||
if t2.Expansions != 0 && t2.Expansions != 2 {
|
||||
t1.Expansions = t2.Expansions
|
||||
}
|
||||
if t2.Output != "" {
|
||||
t1.Output = t2.Output
|
||||
}
|
||||
|
||||
if t1.Includes == nil {
|
||||
t1.Includes = make(IncludedTaskfiles)
|
||||
}
|
||||
for k, v := range t2.Includes {
|
||||
t1.Includes[k] = v
|
||||
}
|
||||
|
||||
if t1.Vars == nil {
|
||||
t1.Vars = &Vars{}
|
||||
}
|
||||
if t1.Env == nil {
|
||||
t1.Env = &Vars{}
|
||||
}
|
||||
t1.Vars.Merge(t2.Vars)
|
||||
t1.Env.Merge(t2.Env)
|
||||
|
||||
if t1.Tasks == nil {
|
||||
t1.Tasks = make(Tasks)
|
||||
}
|
||||
for k, v := range t2.Tasks {
|
||||
// FIXME(@andreynering): Refactor this block, otherwise we can
|
||||
// have serious side-effects in the future, since we're editing
|
||||
// the original references instead of deep copying them.
|
||||
|
||||
t1.Tasks[taskNameWithNamespace(k, namespaces...)] = v
|
||||
|
||||
for _, dep := range v.Deps {
|
||||
dep.Task = taskNameWithNamespace(dep.Task, namespaces...)
|
||||
}
|
||||
for _, cmd := range v.Cmds {
|
||||
if cmd.Task != "" {
|
||||
cmd.Task = taskNameWithNamespace(cmd.Task, namespaces...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func taskNameWithNamespace(taskName string, namespaces ...string) string {
|
||||
if strings.HasPrefix(taskName, ":") {
|
||||
return strings.TrimPrefix(taskName, ":")
|
||||
}
|
||||
return strings.Join(append(namespaces, taskName), NamespaceSeparator)
|
||||
}
|
||||
45
internal/taskfile/precondition.go
Normal file
45
internal/taskfile/precondition.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCantUnmarshalPrecondition is returned for invalid precond YAML.
|
||||
ErrCantUnmarshalPrecondition = errors.New("task: Can't unmarshal precondition value")
|
||||
)
|
||||
|
||||
// Precondition represents a precondition necessary for a task to run
|
||||
type Precondition struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (p *Precondition) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd string
|
||||
|
||||
if err := unmarshal(&cmd); err == nil {
|
||||
p.Sh = cmd
|
||||
p.Msg = fmt.Sprintf("`%s` failed", cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
var sh struct {
|
||||
Sh string
|
||||
Msg string
|
||||
}
|
||||
|
||||
if err := unmarshal(&sh); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Sh = sh.Sh
|
||||
p.Msg = sh.Msg
|
||||
if p.Msg == "" {
|
||||
p.Msg = fmt.Sprintf("%s failed", sh.Sh)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
48
internal/taskfile/precondition_test.go
Normal file
48
internal/taskfile/precondition_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package taskfile_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestPreconditionParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
content string
|
||||
v interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{
|
||||
"test -f foo.txt",
|
||||
&taskfile.Precondition{},
|
||||
&taskfile.Precondition{Sh: `test -f foo.txt`, Msg: "`test -f foo.txt` failed"},
|
||||
},
|
||||
{
|
||||
"sh: '[ 1 = 0 ]'",
|
||||
&taskfile.Precondition{},
|
||||
&taskfile.Precondition{Sh: "[ 1 = 0 ]", Msg: "[ 1 = 0 ] failed"},
|
||||
},
|
||||
{`
|
||||
sh: "[ 1 = 2 ]"
|
||||
msg: "1 is not 2"
|
||||
`,
|
||||
&taskfile.Precondition{},
|
||||
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
|
||||
},
|
||||
{`
|
||||
sh: "[ 1 = 2 ]"
|
||||
msg: "1 is not 2"
|
||||
`,
|
||||
&taskfile.Precondition{},
|
||||
&taskfile.Precondition{Sh: "[ 1 = 2 ]", Msg: "1 is not 2"},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
err := yaml.Unmarshal([]byte(test.content), test.v)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expected, test.v)
|
||||
}
|
||||
}
|
||||
135
internal/taskfile/read/taskfile.go
Normal file
135
internal/taskfile/read/taskfile.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package read
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrIncludedTaskfilesCantHaveIncludes is returned when a included Taskfile contains includes
|
||||
ErrIncludedTaskfilesCantHaveIncludes = errors.New("task: Included Taskfiles can't have includes. Please, move the include to the main Taskfile")
|
||||
// ErrIncludedTaskfilesCantHaveDotenvs is returned when a included Taskfile contains dotenvs
|
||||
ErrIncludedTaskfilesCantHaveDotenvs = errors.New("task: Included Taskfiles can't have dotenv declarations. Please, move the dotenv declaration to the main Taskfile")
|
||||
)
|
||||
|
||||
// Taskfile reads a Taskfile for a given directory
|
||||
func Taskfile(dir string, entrypoint string) (*taskfile.Taskfile, error) {
|
||||
path := filepath.Join(dir, entrypoint)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil, fmt.Errorf(`task: No Taskfile found on "%s". Use "task --init" to create a new one`, path)
|
||||
}
|
||||
t, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := t.ParsedVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v >= 3.0 && len(t.Dotenv) > 0 {
|
||||
for _, dotEnvPath := range t.Dotenv {
|
||||
if !filepath.IsAbs(dotEnvPath) {
|
||||
dotEnvPath = filepath.Join(dir, dotEnvPath)
|
||||
}
|
||||
envs, err := godotenv.Read(dotEnvPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, value := range envs {
|
||||
if _, ok := t.Env.Mapping[key]; !ok {
|
||||
t.Env.Set(key, taskfile.Var{Static: value})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for namespace, includedTask := range t.Includes {
|
||||
if v >= 3.0 {
|
||||
tr := templater.Templater{Vars: &taskfile.Vars{}, RemoveNoValue: true}
|
||||
includedTask = taskfile.IncludedTaskfile{
|
||||
Taskfile: tr.Replace(includedTask.Taskfile),
|
||||
Dir: tr.Replace(includedTask.Dir),
|
||||
AdvancedImport: includedTask.AdvancedImport,
|
||||
}
|
||||
if err := tr.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if filepath.IsAbs(includedTask.Taskfile) {
|
||||
path = includedTask.Taskfile
|
||||
} else {
|
||||
path = filepath.Join(dir, includedTask.Taskfile)
|
||||
}
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
path = filepath.Join(path, "Taskfile.yml")
|
||||
}
|
||||
includedTaskfile, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(includedTaskfile.Includes) > 0 {
|
||||
return nil, ErrIncludedTaskfilesCantHaveIncludes
|
||||
}
|
||||
|
||||
if v >= 3.0 && len(includedTaskfile.Dotenv) > 0 {
|
||||
return nil, ErrIncludedTaskfilesCantHaveDotenvs
|
||||
}
|
||||
|
||||
if includedTask.AdvancedImport {
|
||||
for _, task := range includedTaskfile.Tasks {
|
||||
if !filepath.IsAbs(task.Dir) {
|
||||
task.Dir = filepath.Join(includedTask.Dir, task.Dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err = taskfile.Merge(t, includedTaskfile, namespace); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3.0 {
|
||||
path = filepath.Join(dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
|
||||
if _, err = os.Stat(path); err == nil {
|
||||
osTaskfile, err := readTaskfile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = taskfile.Merge(t, osTaskfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for name, task := range t.Tasks {
|
||||
task.Task = name
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func readTaskfile(file string) (*taskfile.Taskfile, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var t taskfile.Taskfile
|
||||
return &t, yaml.NewDecoder(f).Decode(&t)
|
||||
}
|
||||
45
internal/taskfile/read/taskvars.go
Normal file
45
internal/taskfile/read/taskvars.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package read
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Taskvars reads a Taskvars for a given directory
|
||||
func Taskvars(dir string) (*taskfile.Vars, error) {
|
||||
vars := &taskfile.Vars{}
|
||||
|
||||
path := filepath.Join(dir, "Taskvars.yml")
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
vars, err = readTaskvars(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
path = filepath.Join(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS))
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
osVars, err := readTaskvars(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vars.Merge(osVars)
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
|
||||
func readTaskvars(file string) (*taskfile.Vars, error) {
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vars taskfile.Vars
|
||||
return &vars, yaml.NewDecoder(f).Decode(&vars)
|
||||
}
|
||||
96
internal/taskfile/task.go
Normal file
96
internal/taskfile/task.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Tasks represents a group of tasks
|
||||
type Tasks map[string]*Task
|
||||
|
||||
// Task represents a task
|
||||
type Task struct {
|
||||
Task string
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Summary string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Silent bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrCantUnmarshalTask is returned for invalid task YAML
|
||||
ErrCantUnmarshalTask = errors.New("task: can't unmarshal task value")
|
||||
)
|
||||
|
||||
func (t *Task) Name() string {
|
||||
if t.Label != "" {
|
||||
return t.Label
|
||||
}
|
||||
return t.Task
|
||||
}
|
||||
|
||||
func (t *Task) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var cmd Cmd
|
||||
if err := unmarshal(&cmd); err == nil && cmd.Cmd != "" {
|
||||
t.Cmds = append(t.Cmds, &cmd)
|
||||
return nil
|
||||
}
|
||||
|
||||
var cmds []*Cmd
|
||||
if err := unmarshal(&cmds); err == nil && len(cmds) > 0 {
|
||||
t.Cmds = cmds
|
||||
return nil
|
||||
}
|
||||
|
||||
var task struct {
|
||||
Cmds []*Cmd
|
||||
Deps []*Dep
|
||||
Label string
|
||||
Desc string
|
||||
Summary string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Status []string
|
||||
Preconditions []*Precondition
|
||||
Dir string
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Silent bool
|
||||
Method string
|
||||
Prefix string
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
}
|
||||
if err := unmarshal(&task); err == nil {
|
||||
t.Cmds = task.Cmds
|
||||
t.Deps = task.Deps
|
||||
t.Label = task.Label
|
||||
t.Desc = task.Desc
|
||||
t.Summary = task.Summary
|
||||
t.Sources = task.Sources
|
||||
t.Generates = task.Generates
|
||||
t.Status = task.Status
|
||||
t.Preconditions = task.Preconditions
|
||||
t.Dir = task.Dir
|
||||
t.Vars = task.Vars
|
||||
t.Env = task.Env
|
||||
t.Silent = task.Silent
|
||||
t.Method = task.Method
|
||||
t.Prefix = task.Prefix
|
||||
t.IgnoreError = task.IgnoreError
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrCantUnmarshalTask
|
||||
}
|
||||
68
internal/taskfile/taskfile.go
Normal file
68
internal/taskfile/taskfile.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Taskfile represents a Taskfile.yml
|
||||
type Taskfile struct {
|
||||
Version string
|
||||
Expansions int
|
||||
Output string
|
||||
Method string
|
||||
Includes IncludedTaskfiles
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface
|
||||
func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var taskfile struct {
|
||||
Version string
|
||||
Expansions int
|
||||
Output string
|
||||
Method string
|
||||
Includes IncludedTaskfiles
|
||||
Vars *Vars
|
||||
Env *Vars
|
||||
Tasks Tasks
|
||||
Silent bool
|
||||
Dotenv []string
|
||||
}
|
||||
if err := unmarshal(&taskfile); err != nil {
|
||||
return err
|
||||
}
|
||||
tf.Version = taskfile.Version
|
||||
tf.Expansions = taskfile.Expansions
|
||||
tf.Output = taskfile.Output
|
||||
tf.Method = taskfile.Method
|
||||
tf.Includes = taskfile.Includes
|
||||
tf.Vars = taskfile.Vars
|
||||
tf.Env = taskfile.Env
|
||||
tf.Tasks = taskfile.Tasks
|
||||
tf.Silent = taskfile.Silent
|
||||
tf.Dotenv = taskfile.Dotenv
|
||||
if tf.Expansions <= 0 {
|
||||
tf.Expansions = 2
|
||||
}
|
||||
if tf.Vars == nil {
|
||||
tf.Vars = &Vars{}
|
||||
}
|
||||
if tf.Env == nil {
|
||||
tf.Env = &Vars{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParsedVersion returns the version as a float64
|
||||
func (tf *Taskfile) ParsedVersion() (float64, error) {
|
||||
v, err := strconv.ParseFloat(tf.Version, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, tf.Version, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
66
internal/taskfile/taskfile_test.go
Normal file
66
internal/taskfile/taskfile_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package taskfile_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestCmdParse(t *testing.T) {
|
||||
const (
|
||||
yamlCmd = `echo "a string command"`
|
||||
yamlDep = `"task-name"`
|
||||
yamlTaskCall = `
|
||||
task: another-task
|
||||
vars:
|
||||
PARAM1: VALUE1
|
||||
PARAM2: VALUE2
|
||||
`
|
||||
)
|
||||
tests := []struct {
|
||||
content string
|
||||
v interface{}
|
||||
expected interface{}
|
||||
}{
|
||||
{
|
||||
yamlCmd,
|
||||
&taskfile.Cmd{},
|
||||
&taskfile.Cmd{Cmd: `echo "a string command"`},
|
||||
},
|
||||
{
|
||||
yamlTaskCall,
|
||||
&taskfile.Cmd{},
|
||||
&taskfile.Cmd{Task: "another-task", Vars: &taskfile.Vars{
|
||||
Keys: []string{"PARAM1", "PARAM2"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"PARAM1": taskfile.Var{Static: "VALUE1"},
|
||||
"PARAM2": taskfile.Var{Static: "VALUE2"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
yamlDep,
|
||||
&taskfile.Dep{},
|
||||
&taskfile.Dep{Task: "task-name"},
|
||||
},
|
||||
{
|
||||
yamlTaskCall,
|
||||
&taskfile.Dep{},
|
||||
&taskfile.Dep{Task: "another-task", Vars: &taskfile.Vars{
|
||||
Keys: []string{"PARAM1", "PARAM2"},
|
||||
Mapping: map[string]taskfile.Var{
|
||||
"PARAM1": taskfile.Var{Static: "VALUE1"},
|
||||
"PARAM2": taskfile.Var{Static: "VALUE2"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
err := yaml.Unmarshal([]byte(test.content), test.v)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.expected, test.v)
|
||||
}
|
||||
}
|
||||
133
internal/taskfile/var.go
Normal file
133
internal/taskfile/var.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package taskfile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCantUnmarshalVar is returned for invalid var YAML.
|
||||
ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value")
|
||||
)
|
||||
|
||||
// Vars is a string[string] variables map.
|
||||
type Vars struct {
|
||||
Keys []string
|
||||
Mapping map[string]Var
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements the yaml.Unmarshaler interface.
|
||||
func (vs *Vars) UnmarshalYAML(node *yaml.Node) error {
|
||||
if node.Kind != yaml.MappingNode {
|
||||
return errors.New("task: vars is not a map")
|
||||
}
|
||||
|
||||
// NOTE(@andreynering): on this style of custom unmarsheling,
|
||||
// even number contains the keys, while odd numbers contains
|
||||
// the values.
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
var v Var
|
||||
if err := valueNode.Decode(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
vs.Set(keyNode.Value, v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge merges the given Vars into the caller one
|
||||
func (vs *Vars) Merge(other *Vars) {
|
||||
other.Range(func(key string, value Var) error {
|
||||
vs.Set(key, value)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Set sets a value to a given key
|
||||
func (vs *Vars) Set(key string, value Var) {
|
||||
if vs.Mapping == nil {
|
||||
vs.Mapping = make(map[string]Var, 1)
|
||||
}
|
||||
if !strSliceContains(vs.Keys, key) {
|
||||
vs.Keys = append(vs.Keys, key)
|
||||
}
|
||||
vs.Mapping[key] = value
|
||||
}
|
||||
|
||||
func strSliceContains(s []string, str string) bool {
|
||||
for _, v := range s {
|
||||
if v == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Range allows you to loop into the vars in its right order
|
||||
func (vs *Vars) Range(yield func(key string, value Var) error) error {
|
||||
if vs == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range vs.Keys {
|
||||
if err := yield(k, vs.Mapping[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToCacheMap converts Vars to a map containing only the static
|
||||
// variables
|
||||
func (vs *Vars) ToCacheMap() (m map[string]interface{}) {
|
||||
m = make(map[string]interface{}, len(vs.Keys))
|
||||
vs.Range(func(k string, v Var) error {
|
||||
if v.Sh != "" {
|
||||
// Dynamic variable is not yet resolved; trigger
|
||||
// <no value> to be used in templates.
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Live != nil {
|
||||
m[k] = v.Live
|
||||
} else {
|
||||
m[k] = v.Static
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Var represents either a static or dynamic variable.
|
||||
type Var struct {
|
||||
Static string
|
||||
Live interface{}
|
||||
Sh string
|
||||
}
|
||||
|
||||
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||
func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
var str string
|
||||
if err := unmarshal(&str); err == nil {
|
||||
if strings.HasPrefix(str, "$") {
|
||||
v.Sh = strings.TrimPrefix(str, "$")
|
||||
} else {
|
||||
v.Static = str
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var sh struct {
|
||||
Sh string
|
||||
}
|
||||
if err := unmarshal(&sh); err == nil {
|
||||
v.Sh = sh.Sh
|
||||
return nil
|
||||
}
|
||||
|
||||
return ErrCantUnmarshalVar
|
||||
}
|
||||
52
internal/templater/funcs.go
Normal file
52
internal/templater/funcs.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package templater
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-task/slim-sprig"
|
||||
)
|
||||
|
||||
var (
|
||||
templateFuncs template.FuncMap
|
||||
)
|
||||
|
||||
func init() {
|
||||
taskFuncs := template.FuncMap{
|
||||
"OS": func() string { return runtime.GOOS },
|
||||
"ARCH": func() string { return runtime.GOARCH },
|
||||
"catLines": func(s string) string {
|
||||
s = strings.Replace(s, "\r\n", " ", -1)
|
||||
return strings.Replace(s, "\n", " ", -1)
|
||||
},
|
||||
"splitLines": func(s string) []string {
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
return strings.Split(s, "\n")
|
||||
},
|
||||
"fromSlash": func(path string) string {
|
||||
return filepath.FromSlash(path)
|
||||
},
|
||||
"toSlash": func(path string) string {
|
||||
return filepath.ToSlash(path)
|
||||
},
|
||||
"exeExt": func() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
// IsSH is deprecated.
|
||||
"IsSH": func() bool { return true },
|
||||
}
|
||||
// Deprecated aliases for renamed functions.
|
||||
taskFuncs["FromSlash"] = taskFuncs["fromSlash"]
|
||||
taskFuncs["ToSlash"] = taskFuncs["toSlash"]
|
||||
taskFuncs["ExeExt"] = taskFuncs["exeExt"]
|
||||
|
||||
templateFuncs = sprig.TxtFuncMap()
|
||||
for k, v := range taskFuncs {
|
||||
templateFuncs[k] = v
|
||||
}
|
||||
}
|
||||
85
internal/templater/templater.go
Normal file
85
internal/templater/templater.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package templater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
// Templater is a help struct that allow us to call "replaceX" funcs multiple
|
||||
// times, without having to check for error each time. The first error that
|
||||
// happen will be assigned to r.err, and consecutive calls to funcs will just
|
||||
// return the zero value.
|
||||
type Templater struct {
|
||||
Vars *taskfile.Vars
|
||||
RemoveNoValue bool
|
||||
|
||||
cacheMap map[string]interface{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Templater) ResetCache() {
|
||||
r.cacheMap = r.Vars.ToCacheMap()
|
||||
}
|
||||
|
||||
func (r *Templater) Replace(str string) string {
|
||||
if r.err != nil || str == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
templ, err := template.New("").Funcs(templateFuncs).Parse(str)
|
||||
if err != nil {
|
||||
r.err = err
|
||||
return ""
|
||||
}
|
||||
|
||||
if r.cacheMap == nil {
|
||||
r.cacheMap = r.Vars.ToCacheMap()
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err = templ.Execute(&b, r.cacheMap); err != nil {
|
||||
r.err = err
|
||||
return ""
|
||||
}
|
||||
if r.RemoveNoValue {
|
||||
return strings.ReplaceAll(b.String(), "<no value>", "")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (r *Templater) ReplaceSlice(strs []string) []string {
|
||||
if r.err != nil || len(strs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
new := make([]string, len(strs))
|
||||
for i, str := range strs {
|
||||
new[i] = r.Replace(str)
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars {
|
||||
if r.err != nil || vars == nil || len(vars.Keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var new taskfile.Vars
|
||||
vars.Range(func(k string, v taskfile.Var) error {
|
||||
new.Set(k, taskfile.Var{
|
||||
Static: r.Replace(v.Static),
|
||||
Live: v.Live,
|
||||
Sh: r.Replace(v.Sh),
|
||||
})
|
||||
return nil
|
||||
})
|
||||
|
||||
return &new
|
||||
}
|
||||
|
||||
func (r *Templater) Err() error {
|
||||
return r.err
|
||||
}
|
||||
32
precondition.go
Normal file
32
precondition.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPreconditionFailed is returned when a precondition fails
|
||||
ErrPreconditionFailed = errors.New("task: precondition not met")
|
||||
)
|
||||
|
||||
func (e *Executor) areTaskPreconditionsMet(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, p := range t.Preconditions {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: p.Sh,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
e.Logger.Errf(logger.Magenta, "task: %s", p.Msg)
|
||||
return false, ErrPreconditionFailed
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"runtime"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/imdario/mergo"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// ReadTaskfile parses Taskfile from the disk
|
||||
func (e *Executor) ReadTaskfile() error {
|
||||
var err error
|
||||
e.Tasks, err = e.readTaskfileData(TaskFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
osTasks, err := e.readTaskfileData(fmt.Sprintf("%s_%s", TaskFilePath, runtime.GOOS))
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case taskFileNotFound:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := mergo.MapWithOverwrite(&e.Tasks, osTasks); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) readTaskfileData(path string) (tasks map[string]*Task, err error) {
|
||||
if b, err := ioutil.ReadFile(path + ".yml"); err == nil {
|
||||
return tasks, yaml.Unmarshal(b, &tasks)
|
||||
}
|
||||
if b, err := ioutil.ReadFile(path + ".json"); err == nil {
|
||||
return tasks, json.Unmarshal(b, &tasks)
|
||||
}
|
||||
if b, err := ioutil.ReadFile(path + ".toml"); err == nil {
|
||||
return tasks, toml.Unmarshal(b, &tasks)
|
||||
}
|
||||
return nil, taskFileNotFound{path}
|
||||
}
|
||||
101
status.go
Normal file
101
status.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/status"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
)
|
||||
|
||||
// Status returns an error if any the of given tasks is not up-to-date
|
||||
func (e *Executor) Status(ctx context.Context, calls ...taskfile.Call) error {
|
||||
for _, call := range calls {
|
||||
t, err := e.CompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isUpToDate, err := e.isTaskUpToDate(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isUpToDate {
|
||||
return fmt.Errorf(`task: Task "%s" is not up-to-date`, t.Name())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
if len(t.Status) > 0 {
|
||||
return e.isTaskUpToDateStatus(ctx, t)
|
||||
}
|
||||
|
||||
checker, err := e.getStatusChecker(t)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return checker.IsUpToDate()
|
||||
}
|
||||
|
||||
func (e *Executor) statusOnError(t *taskfile.Task) error {
|
||||
checker, err := e.getStatusChecker(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return checker.OnError()
|
||||
}
|
||||
|
||||
func (e *Executor) getStatusChecker(t *taskfile.Task) (status.Checker, error) {
|
||||
method := t.Method
|
||||
if method == "" {
|
||||
method = e.Taskfile.Method
|
||||
}
|
||||
switch method {
|
||||
case "timestamp":
|
||||
return e.timestampChecker(t), nil
|
||||
case "checksum":
|
||||
return e.checksumChecker(t), nil
|
||||
case "none":
|
||||
return status.None{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`task: invalid method "%s"`, method)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) timestampChecker(t *taskfile.Task) status.Checker {
|
||||
return &status.Timestamp{
|
||||
Dir: t.Dir,
|
||||
Sources: t.Sources,
|
||||
Generates: t.Generates,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) checksumChecker(t *taskfile.Task) status.Checker {
|
||||
return &status.Checksum{
|
||||
Dir: t.Dir,
|
||||
Task: t.Task,
|
||||
Sources: t.Sources,
|
||||
Generates: t.Generates,
|
||||
Dry: e.Dry,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) {
|
||||
for _, s := range t.Status {
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
})
|
||||
if err != nil {
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited non-zero: %s", s, err)
|
||||
return false, nil
|
||||
}
|
||||
e.Logger.VerboseOutf(logger.Yellow, "task: status command %s exited zero", s)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
480
task.go
480
task.go
@@ -1,253 +1,401 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-task/task/execext"
|
||||
"github.com/go-task/task/v3/internal/compiler"
|
||||
compilerv2 "github.com/go-task/task/v3/internal/compiler/v2"
|
||||
compilerv3 "github.com/go-task/task/v3/internal/compiler/v3"
|
||||
"github.com/go-task/task/v3/internal/execext"
|
||||
"github.com/go-task/task/v3/internal/logger"
|
||||
"github.com/go-task/task/v3/internal/output"
|
||||
"github.com/go-task/task/v3/internal/summary"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
"github.com/go-task/task/v3/internal/taskfile/read"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const (
|
||||
// TaskFilePath is the default Taskfile
|
||||
TaskFilePath = "Taskfile"
|
||||
// MaximumTaskCall is the max number of times a task can be called.
|
||||
// This exists to prevent infinite loops on cyclic dependencies
|
||||
MaximumTaskCall = 100
|
||||
)
|
||||
|
||||
// Executor executes a Taskfile
|
||||
type Executor struct {
|
||||
Tasks Tasks
|
||||
Force bool
|
||||
Watch bool
|
||||
Taskfile *taskfile.Taskfile
|
||||
|
||||
watchingFiles map[string]struct{}
|
||||
}
|
||||
Dir string
|
||||
Entrypoint string
|
||||
Force bool
|
||||
Watch bool
|
||||
Verbose bool
|
||||
Silent bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
Parallel bool
|
||||
Color bool
|
||||
|
||||
// Tasks representas a group of tasks
|
||||
type Tasks map[string]*Task
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
// Task represents a task
|
||||
type Task struct {
|
||||
Cmds []string
|
||||
Deps []string
|
||||
Desc string
|
||||
Sources []string
|
||||
Generates []string
|
||||
Status []string
|
||||
Dir string
|
||||
Vars map[string]string
|
||||
Set string
|
||||
Env map[string]string
|
||||
Logger *logger.Logger
|
||||
Compiler compiler.Compiler
|
||||
Output output.Output
|
||||
OutputStyle string
|
||||
|
||||
taskvars *taskfile.Vars
|
||||
|
||||
taskCallCount map[string]*int32
|
||||
mkdirMutexMap map[string]*sync.Mutex
|
||||
}
|
||||
|
||||
// Run runs Task
|
||||
func (e *Executor) Run(args ...string) error {
|
||||
if e.HasCyclicDep() {
|
||||
return ErrCyclicDependencyDetected
|
||||
}
|
||||
|
||||
func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error {
|
||||
// check if given tasks exist
|
||||
for _, a := range args {
|
||||
if _, ok := e.Tasks[a]; !ok {
|
||||
for _, c := range calls {
|
||||
if _, ok := e.Taskfile.Tasks[c.Task]; !ok {
|
||||
// FIXME: move to the main package
|
||||
e.printExistingTasksHelp()
|
||||
return &taskNotFoundError{taskName: a}
|
||||
e.PrintTasksHelp()
|
||||
return &taskNotFoundError{taskName: c.Task}
|
||||
}
|
||||
}
|
||||
|
||||
if e.Watch {
|
||||
if err := e.watchTasks(args...); err != nil {
|
||||
return err
|
||||
if e.Summary {
|
||||
for i, c := range calls {
|
||||
compiledTask, err := e.CompiledTask(c)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
summary.PrintSpaceBetweenSummaries(e.Logger, i)
|
||||
summary.PrintTask(e.Logger, compiledTask)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, a := range args {
|
||||
if err := e.RunTask(context.Background(), a); err != nil {
|
||||
if e.Watch {
|
||||
return e.watchTasks(calls...)
|
||||
}
|
||||
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
for _, c := range calls {
|
||||
c := c
|
||||
if e.Parallel {
|
||||
g.Go(func() error { return e.RunTask(ctx, c) })
|
||||
} else {
|
||||
if err := e.RunTask(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
// Setup setups Executor's internal state
|
||||
func (e *Executor) Setup() error {
|
||||
if e.Entrypoint == "" {
|
||||
e.Entrypoint = "Taskfile.yml"
|
||||
}
|
||||
|
||||
var err error
|
||||
e.Taskfile, err = read.Taskfile(e.Dir, e.Entrypoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := e.Taskfile.ParsedVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if v < 3.0 {
|
||||
e.taskvars, err = read.Taskvars(e.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if e.Stdin == nil {
|
||||
e.Stdin = os.Stdin
|
||||
}
|
||||
if e.Stdout == nil {
|
||||
e.Stdout = os.Stdout
|
||||
}
|
||||
if e.Stderr == nil {
|
||||
e.Stderr = os.Stderr
|
||||
}
|
||||
e.Logger = &logger.Logger{
|
||||
Stdout: e.Stdout,
|
||||
Stderr: e.Stderr,
|
||||
Verbose: e.Verbose,
|
||||
Color: e.Color,
|
||||
}
|
||||
|
||||
if v < 2 {
|
||||
return fmt.Errorf(`task: Taskfile versions prior to v2 are not supported anymore`)
|
||||
}
|
||||
|
||||
// consider as equal to the greater version if round
|
||||
if v == 2.0 {
|
||||
v = 2.6
|
||||
}
|
||||
|
||||
if v > 3.0 {
|
||||
return fmt.Errorf(`task: Taskfile versions greater than v3.0 not implemented in the version of Task`)
|
||||
}
|
||||
|
||||
// Color available only on v3
|
||||
if v < 3 {
|
||||
e.Logger.Color = false
|
||||
}
|
||||
|
||||
if v < 3 {
|
||||
e.Compiler = &compilerv2.CompilerV2{
|
||||
Dir: e.Dir,
|
||||
Taskvars: e.taskvars,
|
||||
TaskfileVars: e.Taskfile.Vars,
|
||||
Expansions: e.Taskfile.Expansions,
|
||||
Logger: e.Logger,
|
||||
}
|
||||
} else {
|
||||
e.Compiler = &compilerv3.CompilerV3{
|
||||
Dir: e.Dir,
|
||||
TaskfileVars: e.Taskfile.Vars,
|
||||
Logger: e.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
if v < 2.1 && e.Taskfile.Output != "" {
|
||||
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
|
||||
}
|
||||
if v < 2.2 && len(e.Taskfile.Includes) > 0 {
|
||||
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
|
||||
}
|
||||
if v >= 3.0 && e.Taskfile.Expansions > 2 {
|
||||
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
|
||||
}
|
||||
|
||||
if e.OutputStyle != "" {
|
||||
e.Taskfile.Output = e.OutputStyle
|
||||
}
|
||||
switch e.Taskfile.Output {
|
||||
case "", "interleaved":
|
||||
e.Output = output.Interleaved{}
|
||||
case "group":
|
||||
e.Output = output.Group{}
|
||||
case "prefixed":
|
||||
e.Output = output.Prefixed{}
|
||||
default:
|
||||
return fmt.Errorf(`task: output option "%s" not recognized`, e.Taskfile.Output)
|
||||
}
|
||||
|
||||
if e.Taskfile.Method == "" {
|
||||
if v >= 3 {
|
||||
e.Taskfile.Method = "checksum"
|
||||
} else {
|
||||
e.Taskfile.Method = "timestamp"
|
||||
}
|
||||
}
|
||||
|
||||
if v <= 2.1 {
|
||||
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
|
||||
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
if task.IgnoreError {
|
||||
return err
|
||||
}
|
||||
for _, cmd := range task.Cmds {
|
||||
if cmd.IgnoreError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v < 2.6 {
|
||||
for _, task := range e.Taskfile.Tasks {
|
||||
if len(task.Preconditions) > 0 {
|
||||
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v < 3 {
|
||||
for _, taskfile := range e.Taskfile.Includes {
|
||||
if taskfile.AdvancedImport {
|
||||
return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks))
|
||||
e.mkdirMutexMap = make(map[string]*sync.Mutex, len(e.Taskfile.Tasks))
|
||||
for k := range e.Taskfile.Tasks {
|
||||
e.taskCallCount[k] = new(int32)
|
||||
e.mkdirMutexMap[k] = &sync.Mutex{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunTask runs a task by its name
|
||||
func (e *Executor) RunTask(ctx context.Context, name string) error {
|
||||
t, ok := e.Tasks[name]
|
||||
if !ok {
|
||||
return &taskNotFoundError{name}
|
||||
func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error {
|
||||
t, err := e.CompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !e.Watch && atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall {
|
||||
return &MaximumTaskCallExceededError{task: call.Task}
|
||||
}
|
||||
|
||||
if err := e.runDeps(ctx, name); err != nil {
|
||||
if err := e.runDeps(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !e.Force {
|
||||
upToDate, err := t.isUpToDate(ctx)
|
||||
preCondMet, err := e.areTaskPreconditionsMet(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if upToDate {
|
||||
log.Printf(`task: Task "%s" is up to date`, name)
|
||||
|
||||
upToDate, err := e.isTaskUpToDate(ctx, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if upToDate && preCondMet {
|
||||
if !e.Silent {
|
||||
e.Logger.Errf(logger.Magenta, `task: Task "%s" is up to date`, t.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.mkdir(t); err != nil {
|
||||
e.Logger.Errf(logger.Red, "task: cannot make directory %q: %v", t.Dir, err)
|
||||
}
|
||||
|
||||
for i := range t.Cmds {
|
||||
if err := e.runCommand(ctx, name, i); err != nil {
|
||||
return &taskRunError{name, err}
|
||||
if err := e.runCommand(ctx, t, call, i); err != nil {
|
||||
if err2 := e.statusOnError(t); err2 != nil {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: error cleaning status on error: %v", err2)
|
||||
}
|
||||
|
||||
if execext.IsExitError(err) && t.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: task error ignored: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
return &taskRunError{t.Task, err}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) runDeps(ctx context.Context, task string) error {
|
||||
func (e *Executor) mkdir(t *taskfile.Task) error {
|
||||
if t.Dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
mutex := e.mkdirMutexMap[t.Task]
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if _, err := os.Stat(t.Dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(t.Dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error {
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
t := e.Tasks[task]
|
||||
|
||||
for _, d := range t.Deps {
|
||||
dep := d
|
||||
d := d
|
||||
|
||||
g.Go(func() error {
|
||||
dep, err := t.ReplaceVariables(dep)
|
||||
err := e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = e.RunTask(ctx, dep); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return g.Wait()
|
||||
}
|
||||
|
||||
func (t *Task) isUpToDate(ctx context.Context) (bool, error) {
|
||||
if len(t.Status) > 0 {
|
||||
return t.isUpToDateStatus(ctx)
|
||||
}
|
||||
return t.isUpToDateTimestamp(ctx)
|
||||
}
|
||||
func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error {
|
||||
cmd := t.Cmds[i]
|
||||
|
||||
func (t *Task) isUpToDateStatus(ctx context.Context) (bool, error) {
|
||||
environ, err := t.getEnviron()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, s := range t.Status {
|
||||
err = execext.RunCommand(&execext.RunCommandOptions{
|
||||
Context: ctx,
|
||||
Command: s,
|
||||
Dir: t.Dir,
|
||||
Env: environ,
|
||||
})
|
||||
switch {
|
||||
case cmd.Task != "":
|
||||
err := e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars})
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (t *Task) isUpToDateTimestamp(ctx context.Context) (bool, error) {
|
||||
if len(t.Sources) == 0 || len(t.Generates) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sources, err := t.ReplaceSliceVariables(t.Sources)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
generates, err := t.ReplaceSliceVariables(t.Generates)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
sourcesMaxTime, err := getPatternsMaxTime(sources)
|
||||
if err != nil || sourcesMaxTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
generatesMinTime, err := getPatternsMinTime(generates)
|
||||
if err != nil || generatesMinTime.IsZero() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return generatesMinTime.After(sourcesMaxTime), nil
|
||||
}
|
||||
|
||||
func (e *Executor) runCommand(ctx context.Context, task string, i int) error {
|
||||
t := e.Tasks[task]
|
||||
|
||||
c, err := t.ReplaceVariables(t.Cmds[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(c, "^") {
|
||||
c = strings.TrimPrefix(c, "^")
|
||||
if err = e.RunTask(ctx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dir, err := t.ReplaceVariables(t.Dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
envs, err := t.getEnviron()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts := &execext.RunCommandOptions{
|
||||
Context: ctx,
|
||||
Command: c,
|
||||
Dir: dir,
|
||||
Env: envs,
|
||||
Stdin: os.Stdin,
|
||||
Stderr: os.Stderr,
|
||||
}
|
||||
|
||||
if t.Set == "" {
|
||||
log.Println(c)
|
||||
opts.Stdout = os.Stdout
|
||||
if err = execext.RunCommand(opts); err != nil {
|
||||
return err
|
||||
case cmd.Cmd != "":
|
||||
if e.Verbose || (!cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
|
||||
e.Logger.Errf(logger.Green, "task: %s", cmd.Cmd)
|
||||
}
|
||||
} else {
|
||||
buff := bytes.NewBuffer(nil)
|
||||
opts.Stdout = buff
|
||||
if err = execext.RunCommand(opts); err != nil {
|
||||
return err
|
||||
|
||||
if e.Dry {
|
||||
return nil
|
||||
}
|
||||
os.Setenv(t.Set, strings.TrimSpace(buff.String()))
|
||||
|
||||
stdOut := e.Output.WrapWriter(e.Stdout, t.Prefix)
|
||||
stdErr := e.Output.WrapWriter(e.Stderr, t.Prefix)
|
||||
defer func() {
|
||||
if _, ok := stdOut.(*os.File); !ok {
|
||||
if closer, ok := stdOut.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
if _, ok := stdErr.(*os.File); !ok {
|
||||
if closer, ok := stdErr.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := execext.RunCommand(ctx, &execext.RunCommandOptions{
|
||||
Command: cmd.Cmd,
|
||||
Dir: t.Dir,
|
||||
Env: getEnviron(t),
|
||||
Stdin: e.Stdin,
|
||||
Stdout: stdOut,
|
||||
Stderr: stdErr,
|
||||
})
|
||||
if execext.IsExitError(err) && cmd.IgnoreError {
|
||||
e.Logger.VerboseErrf(logger.Yellow, "task: command error ignored: %v", err)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Task) getEnviron() ([]string, error) {
|
||||
func getEnviron(t *taskfile.Task) []string {
|
||||
if t.Env == nil {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
envs := os.Environ()
|
||||
|
||||
for k, v := range t.Env {
|
||||
env, err := t.ReplaceVariables(fmt.Sprintf("%s=%s", k, v))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
environ := os.Environ()
|
||||
for k, v := range t.Env.ToCacheMap() {
|
||||
if s, ok := v.(string); ok {
|
||||
environ = append(environ, fmt.Sprintf("%s=%s", k, s))
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
return envs, nil
|
||||
return environ
|
||||
}
|
||||
|
||||
890
task_test.go
890
task_test.go
@@ -2,14 +2,185 @@ package task_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/internal/taskfile"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// fileContentTest provides a basic reusable test-case for running a Taskfile
|
||||
// and inspect generated files.
|
||||
type fileContentTest struct {
|
||||
Dir string
|
||||
Target string
|
||||
TrimSpace bool
|
||||
Files map[string]string
|
||||
}
|
||||
|
||||
func (fct fileContentTest) name(file string) string {
|
||||
return fmt.Sprintf("target=%q,file=%q", fct.Target, file)
|
||||
}
|
||||
|
||||
func (fct fileContentTest) Run(t *testing.T) {
|
||||
for f := range fct.Files {
|
||||
_ = os.Remove(filepath.Join(fct.Dir, f))
|
||||
}
|
||||
|
||||
e := &task.Executor{
|
||||
Dir: fct.Dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup(), "e.Setup()")
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: fct.Target}), "e.Run(target)")
|
||||
|
||||
for name, expectContent := range fct.Files {
|
||||
t.Run(fct.name(name), func(t *testing.T) {
|
||||
b, err := ioutil.ReadFile(filepath.Join(fct.Dir, name))
|
||||
assert.NoError(t, err, "Error reading file")
|
||||
s := string(b)
|
||||
if fct.TrimSpace {
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
assert.Equal(t, expectContent, s, "unexpected file content")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/env",
|
||||
Target: "default",
|
||||
TrimSpace: false,
|
||||
Files: map[string]string{
|
||||
"local.txt": "GOOS='linux' GOARCH='amd64' CGO_ENABLED='0'\n",
|
||||
"global.txt": "FOO='foo' BAR='overriden' BAZ='baz'\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestVarsV2(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/vars/v2",
|
||||
Target: "default",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"foo.txt": "foo",
|
||||
"bar.txt": "bar",
|
||||
"baz.txt": "baz",
|
||||
"tmpl_foo.txt": "foo",
|
||||
"tmpl_bar.txt": "bar",
|
||||
"tmpl_foo2.txt": "foo2",
|
||||
"tmpl_bar2.txt": "bar2",
|
||||
"shtmpl_foo.txt": "foo",
|
||||
"shtmpl_foo2.txt": "foo2",
|
||||
"nestedtmpl_foo.txt": "<no value>",
|
||||
"nestedtmpl_foo2.txt": "foo2",
|
||||
"foo2.txt": "foo2",
|
||||
"bar2.txt": "bar2",
|
||||
"baz2.txt": "baz2",
|
||||
"tmpl2_foo.txt": "<no value>",
|
||||
"tmpl2_foo2.txt": "foo2",
|
||||
"tmpl2_bar.txt": "<no value>",
|
||||
"tmpl2_bar2.txt": "bar2",
|
||||
"shtmpl2_foo.txt": "<no value>",
|
||||
"shtmpl2_foo2.txt": "foo2",
|
||||
"nestedtmpl2_foo2.txt": "<no value>",
|
||||
"override.txt": "bar",
|
||||
"nested.txt": "Taskvars-TaskfileVars-TaskVars",
|
||||
"task_name.txt": "hello",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
// Ensure identical results when running hello task directly.
|
||||
tt.Target = "hello"
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestVarsV3(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/vars/v3",
|
||||
Target: "default",
|
||||
Files: map[string]string{
|
||||
"missing-var.txt": "\n",
|
||||
"var-order.txt": "ABCDEF\n",
|
||||
"dependent-sh.txt": "123456\n",
|
||||
"with-call.txt": "Hi, ABC123!\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestMultilineVars(t *testing.T) {
|
||||
for _, dir := range []string{"testdata/vars/v2/multiline"} {
|
||||
tt := fileContentTest{
|
||||
Dir: dir,
|
||||
Target: "default",
|
||||
TrimSpace: false,
|
||||
Files: map[string]string{
|
||||
// Note:
|
||||
// - task does not strip a trailing newline from var entries
|
||||
// - task strips one trailing newline from shell output
|
||||
// - the cat command adds a trailing newline
|
||||
"echo_foobar.txt": "foo\nbar\n",
|
||||
"echo_n_foobar.txt": "foo\nbar\n",
|
||||
"echo_n_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n",
|
||||
"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
|
||||
"var_catlines.txt": " foo bar foobar baz \n",
|
||||
"var_enumfile.txt": "0:\n1:\n2:foo\n3: bar\n4:foobar\n5:\n6:baz\n7:\n8:\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVarsInvalidTmpl(t *testing.T) {
|
||||
const (
|
||||
dir = "testdata/vars/v2"
|
||||
target = "invalid-var-tmpl"
|
||||
expectError = "template: :1: unexpected EOF"
|
||||
)
|
||||
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup(), "e.Setup()")
|
||||
assert.EqualError(t, e.Run(context.Background(), taskfile.Call{Task: target}), expectError, "e.Run(target)")
|
||||
}
|
||||
|
||||
func TestParams(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/params",
|
||||
Target: "default",
|
||||
TrimSpace: false,
|
||||
Files: map[string]string{
|
||||
"hello.txt": "Hello\n",
|
||||
"world.txt": "World\n",
|
||||
"exclamation.txt": "!\n",
|
||||
"dep1.txt": "Dependence1\n",
|
||||
"dep2.txt": "Dependence2\n",
|
||||
"spanish.txt": "¡Holla mundo!\n",
|
||||
"spanish-dep.txt": "¡Holla dependencia!\n",
|
||||
"portuguese.txt": "Olá, mundo!\n",
|
||||
"portuguese2.txt": "Olá, mundo!\n",
|
||||
"german.txt": "Welt!\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
const dir = "testdata/deps"
|
||||
|
||||
@@ -32,84 +203,18 @@ func TestDeps(t *testing.T) {
|
||||
_ = os.Remove(filepath.Join(dir, f))
|
||||
}
|
||||
|
||||
c := exec.Command("task")
|
||||
c.Dir = dir
|
||||
if err := c.Run(); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||
|
||||
for _, f := range files {
|
||||
f = filepath.Join(dir, f)
|
||||
if _, err := os.Stat(f); err != nil {
|
||||
t.Errorf("File %s should exists", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVars(t *testing.T) {
|
||||
const dir = "testdata/vars"
|
||||
|
||||
files := []struct {
|
||||
file string
|
||||
content string
|
||||
}{
|
||||
{"foo.txt", "foo"},
|
||||
{"bar.txt", "bar"},
|
||||
{"foo2.txt", "foo2"},
|
||||
{"bar2.txt", "bar2"},
|
||||
{"equal.txt", "foo=bar"},
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
_ = os.Remove(filepath.Join(dir, f.file))
|
||||
}
|
||||
|
||||
c := exec.Command("task")
|
||||
c.Dir = dir
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
d, err := ioutil.ReadFile(filepath.Join(dir, f.file))
|
||||
if err != nil {
|
||||
t.Errorf("Error reading %s: %v", f.file, err)
|
||||
}
|
||||
s := string(d)
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
if s != f.content {
|
||||
t.Errorf("File content should be %s but is %s", f.content, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCall(t *testing.T) {
|
||||
const dir = "testdata/task_call"
|
||||
|
||||
files := []string{
|
||||
"foo.txt",
|
||||
"bar.txt",
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
_ = os.Remove(filepath.Join(dir, f))
|
||||
}
|
||||
|
||||
c := exec.Command("task")
|
||||
c.Dir = dir
|
||||
|
||||
if err := c.Run(); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
|
||||
t.Error(err)
|
||||
t.Errorf("File %s should exist", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -121,45 +226,640 @@ func TestStatus(t *testing.T) {
|
||||
_ = os.Remove(file)
|
||||
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
t.Errorf("File should not exists: %v", err)
|
||||
}
|
||||
c := exec.Command("task", "gen-foo")
|
||||
c.Dir = dir
|
||||
if err := c.Run(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
t.Errorf("File should exists: %v", err)
|
||||
t.Errorf("File should not exist: %v", err)
|
||||
}
|
||||
|
||||
buff := bytes.NewBuffer(nil)
|
||||
c = exec.Command("task", "gen-foo")
|
||||
c.Dir = dir
|
||||
c.Stderr = buff
|
||||
c.Stdout = buff
|
||||
if err := c.Run(); err != nil {
|
||||
t.Error(err)
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
|
||||
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
t.Errorf("File should exist: %v", err)
|
||||
}
|
||||
|
||||
e.Silent = false
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-foo"}))
|
||||
|
||||
if buff.String() != `task: Task "gen-foo" is up to date`+"\n" {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrecondition(t *testing.T) {
|
||||
const dir = "testdata/precondition"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
|
||||
// A precondition that has been met
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||
if buff.String() != "" {
|
||||
t.Errorf("Got Output when none was expected: %s", buff.String())
|
||||
}
|
||||
|
||||
// A precondition that was not met
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "impossible"}))
|
||||
|
||||
if buff.String() != "task: 1 != 0 obviously!\n" {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
buff.Reset()
|
||||
|
||||
// Calling a task with a precondition in a dependency fails the task
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "depends_on_impossible"}))
|
||||
|
||||
if buff.String() != "task: 1 != 0 obviously!\n" {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
buff.Reset()
|
||||
|
||||
// Calling a task with a precondition in a cmd fails the task
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "executes_failing_task_as_cmd"}))
|
||||
if buff.String() != "task: 1 != 0 obviously!\n" {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
buff.Reset()
|
||||
}
|
||||
|
||||
func TestGenerates(t *testing.T) {
|
||||
const (
|
||||
srcTask = "sub/src.txt"
|
||||
relTask = "rel.txt"
|
||||
absTask = "abs.txt"
|
||||
fileWithSpaces = "my text file.txt"
|
||||
)
|
||||
|
||||
// This test does not work with a relative dir.
|
||||
dir, err := filepath.Abs("testdata/generates")
|
||||
assert.NoError(t, err)
|
||||
var srcFile = filepath.Join(dir, srcTask)
|
||||
|
||||
for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} {
|
||||
path := filepath.Join(dir, task)
|
||||
_ = os.Remove(path)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Errorf("File should not exist: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
buff := bytes.NewBuffer(nil)
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: buff,
|
||||
Stderr: buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
for _, theTask := range []string{relTask, absTask, fileWithSpaces} {
|
||||
var destFile = filepath.Join(dir, theTask)
|
||||
var upToDate = fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) +
|
||||
fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask)
|
||||
|
||||
// Run task for the first time.
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
|
||||
|
||||
if _, err := os.Stat(srcFile); err != nil {
|
||||
t.Errorf("File should exist: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(destFile); err != nil {
|
||||
t.Errorf("File should exist: %v", err)
|
||||
}
|
||||
// Ensure task was not incorrectly found to be up-to-date on first run.
|
||||
if buff.String() == upToDate {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
buff.Reset()
|
||||
|
||||
// Re-run task to ensure it's now found to be up-to-date.
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: theTask}))
|
||||
if buff.String() != upToDate {
|
||||
t.Errorf("Wrong output message: %s", buff.String())
|
||||
}
|
||||
buff.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusChecksum(t *testing.T) {
|
||||
const dir = "testdata/checksum"
|
||||
|
||||
files := []string{
|
||||
"generated.txt",
|
||||
".task/checksum/build",
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
_ = os.Remove(filepath.Join(dir, f))
|
||||
|
||||
_, err := os.Stat(filepath.Join(dir, f))
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
for _, f := range files {
|
||||
_, err := os.Stat(filepath.Join(dir, f))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
buff.Reset()
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String())
|
||||
}
|
||||
|
||||
func TestLabelUpToDate(t *testing.T) {
|
||||
const dir = "testdata/label_uptodate"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||
assert.Contains(t, buff.String(), "foobar")
|
||||
}
|
||||
|
||||
func TestLabelSummary(t *testing.T) {
|
||||
const dir = "testdata/label_summary"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Summary: true,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||
assert.Contains(t, buff.String(), "foobar")
|
||||
}
|
||||
|
||||
func TestLabelInStatus(t *testing.T) {
|
||||
const dir = "testdata/label_status"
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
err := e.Status(context.Background(), taskfile.Call{Task: "foo"})
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "foobar")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelWithVariableExpansion(t *testing.T) {
|
||||
const dir = "testdata/label_var"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||
assert.Contains(t, buff.String(), "foobaz")
|
||||
}
|
||||
|
||||
func TestLabelInSummary(t *testing.T) {
|
||||
const dir = "testdata/label_summary"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "foo"}))
|
||||
assert.Contains(t, buff.String(), "foobar")
|
||||
}
|
||||
|
||||
func TestLabelInList(t *testing.T) {
|
||||
const dir = "testdata/label_list"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
e.PrintTasksHelp()
|
||||
assert.Contains(t, buff.String(), "foobar")
|
||||
}
|
||||
|
||||
func TestStatusVariables(t *testing.T) {
|
||||
const dir = "testdata/status_vars"
|
||||
|
||||
_ = os.RemoveAll(filepath.Join(dir, ".task"))
|
||||
_ = os.Remove(filepath.Join(dir, "generated.txt"))
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: false,
|
||||
Verbose: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
|
||||
assert.Contains(t, buff.String(), "d41d8cd98f00b204e9800998ecf8427e")
|
||||
|
||||
inf, err := os.Stat(filepath.Join(dir, "source.txt"))
|
||||
assert.NoError(t, err)
|
||||
ts := fmt.Sprintf("%d", inf.ModTime().Unix())
|
||||
tf := fmt.Sprintf("%s", inf.ModTime())
|
||||
|
||||
assert.Contains(t, buff.String(), ts)
|
||||
assert.Contains(t, buff.String(), tf)
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
const dir = "testdata/init"
|
||||
var file = filepath.Join(dir, "Taskfile.yml")
|
||||
|
||||
_ = os.Remove(file)
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
t.Errorf("Taskfile.yml should not exists")
|
||||
t.Errorf("Taskfile.yml should not exist")
|
||||
}
|
||||
|
||||
c := exec.Command("task", "--init")
|
||||
c.Dir = dir
|
||||
if err := c.Run(); err != nil {
|
||||
if err := task.InitTaskfile(ioutil.Discard, dir); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(file); err != nil {
|
||||
t.Errorf("Taskfile.yml should exists")
|
||||
t.Errorf("Taskfile.yml should exist")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCyclicDep(t *testing.T) {
|
||||
const dir = "testdata/cyclic"
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(context.Background(), taskfile.Call{Task: "task-1"}))
|
||||
}
|
||||
|
||||
func TestTaskVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
Dir string
|
||||
Version string
|
||||
}{
|
||||
{"testdata/version/v2", "2"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Dir, func(t *testing.T) {
|
||||
e := task.Executor{
|
||||
Dir: test.Dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.Equal(t, test.Version, e.Taskfile.Version)
|
||||
assert.Equal(t, 2, len(e.Taskfile.Tasks))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskIgnoreErrors(t *testing.T) {
|
||||
const dir = "testdata/ignore_errors"
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-should-pass"}))
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "task-should-fail"}))
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "cmd-should-pass"}))
|
||||
assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "cmd-should-fail"}))
|
||||
}
|
||||
|
||||
func TestExpand(t *testing.T) {
|
||||
const dir = "testdata/expand"
|
||||
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Errorf("Couldn't get $HOME: %v", err)
|
||||
}
|
||||
var buff bytes.Buffer
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "pwd"}))
|
||||
assert.Equal(t, home, strings.TrimSpace(buff.String()))
|
||||
}
|
||||
|
||||
func TestDry(t *testing.T) {
|
||||
const dir = "testdata/dry"
|
||||
|
||||
file := filepath.Join(dir, "file.txt")
|
||||
_ = os.Remove(file)
|
||||
|
||||
var buff bytes.Buffer
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Dry: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"}))
|
||||
|
||||
assert.Equal(t, "task: touch file.txt", strings.TrimSpace(buff.String()))
|
||||
if _, err := os.Stat(file); err == nil {
|
||||
t.Errorf("File should not exist %s", file)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDryChecksum tests if the checksum file is not being written to disk
|
||||
// if the dry mode is enabled.
|
||||
func TestDryChecksum(t *testing.T) {
|
||||
const dir = "testdata/dry_checksum"
|
||||
|
||||
checksumFile := filepath.Join(dir, ".task/checksum/default")
|
||||
_ = os.Remove(checksumFile)
|
||||
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
Dry: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||
|
||||
_, err := os.Stat(checksumFile)
|
||||
assert.Error(t, err, "checksum file should not exist")
|
||||
|
||||
e.Dry = false
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||
_, err = os.Stat(checksumFile)
|
||||
assert.NoError(t, err, "checksum file should exist")
|
||||
}
|
||||
|
||||
func TestIncludes(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes",
|
||||
Target: "default",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"main.txt": "main",
|
||||
"included_directory.txt": "included_directory",
|
||||
"included_directory_without_dir.txt": "included_directory_without_dir",
|
||||
"included_taskfile_without_dir.txt": "included_taskfile_without_dir",
|
||||
"./module2/included_directory_with_dir.txt": "included_directory_with_dir",
|
||||
"./module2/included_taskfile_with_dir.txt": "included_taskfile_with_dir",
|
||||
"os_include.txt": "os",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncorrectVersionIncludes(t *testing.T) {
|
||||
const dir = "testdata/incorrect_includes"
|
||||
expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
|
||||
assert.EqualError(t, e.Setup(), expectedError)
|
||||
}
|
||||
|
||||
func TestIncludesEmptyMain(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes_empty",
|
||||
Target: "included:default",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"file.txt": "default",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludesDependencies(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes_deps",
|
||||
Target: "default",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"default.txt": "default",
|
||||
"called_dep.txt": "called_dep",
|
||||
"called_task.txt": "called_task",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestIncludesCallingRoot(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/includes_call_root_task",
|
||||
Target: "included:call-root",
|
||||
TrimSpace: true,
|
||||
Files: map[string]string{
|
||||
"root_task.txt": "root task",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestSummary(t *testing.T) {
|
||||
const dir = "testdata/summary"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Summary: true,
|
||||
Silent: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"}))
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(dir, "task-with-summary.txt"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedOutput := string(data)
|
||||
if runtime.GOOS == "windows" {
|
||||
expectedOutput = strings.Replace(expectedOutput, "\r\n", "\n", -1)
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedOutput, buff.String())
|
||||
}
|
||||
|
||||
func TestWhenNoDirAttributeItRunsInSameDirAsTaskfile(t *testing.T) {
|
||||
const expected = "dir"
|
||||
const dir = "testdata/" + expected
|
||||
var out bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &out,
|
||||
Stderr: &out,
|
||||
}
|
||||
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
|
||||
|
||||
// got should be the "dir" part of "testdata/dir"
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
}
|
||||
|
||||
func TestWhenDirAttributeAndDirExistsItRunsInThatDir(t *testing.T) {
|
||||
const expected = "exists"
|
||||
const dir = "testdata/dir/explicit_exists"
|
||||
var out bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &out,
|
||||
Stderr: &out,
|
||||
}
|
||||
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "whereami"}))
|
||||
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
}
|
||||
|
||||
func TestWhenDirAttributeItCreatesMissingAndRunsInThatDir(t *testing.T) {
|
||||
const expected = "createme"
|
||||
const dir = "testdata/dir/explicit_doesnt_exist/"
|
||||
const toBeCreated = dir + expected
|
||||
const target = "whereami"
|
||||
var out bytes.Buffer
|
||||
e := &task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &out,
|
||||
Stderr: &out,
|
||||
}
|
||||
|
||||
// Ensure that the directory to be created doesn't actually exist.
|
||||
_ = os.RemoveAll(toBeCreated)
|
||||
if _, err := os.Stat(toBeCreated); err == nil {
|
||||
t.Errorf("Directory should not exist: %v", err)
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: target}))
|
||||
|
||||
got := strings.TrimSuffix(filepath.Base(out.String()), "\n")
|
||||
assert.Equal(t, expected, got, "Mismatch in the working directory")
|
||||
|
||||
// Clean-up after ourselves only if no error.
|
||||
_ = os.RemoveAll(toBeCreated)
|
||||
}
|
||||
|
||||
func TestDisplaysErrorOnUnsupportedVersion(t *testing.T) {
|
||||
e := task.Executor{
|
||||
Dir: "testdata/version/v1",
|
||||
Stdout: ioutil.Discard,
|
||||
Stderr: ioutil.Discard,
|
||||
}
|
||||
err := e.Setup()
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "task: Taskfile versions prior to v2 are not supported anymore", err.Error())
|
||||
|
||||
}
|
||||
|
||||
func TestShortTaskNotation(t *testing.T) {
|
||||
const dir = "testdata/short_task_notation"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
Silent: true,
|
||||
}
|
||||
assert.NoError(t, e.Setup())
|
||||
assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"}))
|
||||
assert.Equal(t, "string-slice-1\nstring-slice-2\nstring\n", buff.String())
|
||||
}
|
||||
|
||||
func TestDotenvShouldIncludeAllEnvFiles(t *testing.T) {
|
||||
tt := fileContentTest{
|
||||
Dir: "testdata/dotenv",
|
||||
Target: "default",
|
||||
TrimSpace: false,
|
||||
Files: map[string]string{
|
||||
"include.txt": "INCLUDE1='from_include1' INCLUDE2='from_include2'\n",
|
||||
},
|
||||
}
|
||||
tt.Run(t)
|
||||
}
|
||||
|
||||
func TestDotenvShouldErrorWithIncludeEnvPath(t *testing.T) {
|
||||
const dir = "testdata/dotenv"
|
||||
const entry = "Taskfile-errors1.yml"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Entrypoint: entry,
|
||||
Summary: true,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
err := e.Setup()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no such file")
|
||||
}
|
||||
|
||||
func TestDotenvShouldErrorWhenIncludingDependantDotenvs(t *testing.T) {
|
||||
const dir = "testdata/dotenv"
|
||||
const entry = "Taskfile-errors2.yml"
|
||||
|
||||
var buff bytes.Buffer
|
||||
e := task.Executor{
|
||||
Dir: dir,
|
||||
Entrypoint: entry,
|
||||
Summary: true,
|
||||
Stdout: &buff,
|
||||
Stderr: &buff,
|
||||
}
|
||||
|
||||
err := e.Setup()
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "move the dotenv")
|
||||
}
|
||||
|
||||
2
testdata/checksum/.gitignore
vendored
Normal file
2
testdata/checksum/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.task/
|
||||
generated.txt
|
||||
12
testdata/checksum/Taskfile.yml
vendored
Normal file
12
testdata/checksum/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
build:
|
||||
cmds:
|
||||
- cp ./source.txt ./generated.txt
|
||||
sources:
|
||||
- ./**/glob-with-inexistent-file.txt
|
||||
- ./source.txt
|
||||
generates:
|
||||
- ./generated.txt
|
||||
method: checksum
|
||||
1
testdata/checksum/source.txt
vendored
Normal file
1
testdata/checksum/source.txt
vendored
Normal file
@@ -0,0 +1 @@
|
||||
Hello, World!
|
||||
10
testdata/cyclic/Taskfile.yml
vendored
Normal file
10
testdata/cyclic/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
task-1:
|
||||
deps:
|
||||
- task: task-2
|
||||
|
||||
task-2:
|
||||
deps:
|
||||
- task: task-1
|
||||
85
testdata/deps/Taskfile.yml
vendored
85
testdata/deps/Taskfile.yml
vendored
@@ -1,53 +1,56 @@
|
||||
default:
|
||||
deps: [d1, d2, d3]
|
||||
version: '3'
|
||||
|
||||
d1:
|
||||
deps: [d11, d12, d13]
|
||||
cmds:
|
||||
- echo 'Text' > d1.txt
|
||||
tasks:
|
||||
default:
|
||||
deps: [d1, d2, d3]
|
||||
|
||||
d2:
|
||||
deps: [d21, d22, d23]
|
||||
cmds:
|
||||
- echo 'Text' > d2.txt
|
||||
d1:
|
||||
deps: [d11, d12, d13]
|
||||
cmds:
|
||||
- echo 'Text' > d1.txt
|
||||
|
||||
d3:
|
||||
deps: [d31, d32, d33]
|
||||
cmds:
|
||||
- echo 'Text' > d3.txt
|
||||
d2:
|
||||
deps: [d21, d22, d23]
|
||||
cmds:
|
||||
- echo 'Text' > d2.txt
|
||||
|
||||
d11:
|
||||
cmds:
|
||||
- echo 'Text' > d11.txt
|
||||
d3:
|
||||
deps: [d31, d32, d33]
|
||||
cmds:
|
||||
- echo 'Text' > d3.txt
|
||||
|
||||
d12:
|
||||
cmds:
|
||||
- echo 'Text' > d12.txt
|
||||
d11:
|
||||
cmds:
|
||||
- echo 'Text' > d11.txt
|
||||
|
||||
d13:
|
||||
cmds:
|
||||
- echo 'Text' > d13.txt
|
||||
d12:
|
||||
cmds:
|
||||
- echo 'Text' > d12.txt
|
||||
|
||||
d21:
|
||||
cmds:
|
||||
- echo 'Text' > d21.txt
|
||||
d13:
|
||||
cmds:
|
||||
- echo 'Text' > d13.txt
|
||||
|
||||
d22:
|
||||
cmds:
|
||||
- echo 'Text' > d22.txt
|
||||
d21:
|
||||
cmds:
|
||||
- echo 'Text' > d21.txt
|
||||
|
||||
d23:
|
||||
cmds:
|
||||
- echo 'Text' > d23.txt
|
||||
d22:
|
||||
cmds:
|
||||
- echo 'Text' > d22.txt
|
||||
|
||||
d31:
|
||||
cmds:
|
||||
- echo 'Text' > d31.txt
|
||||
d23:
|
||||
cmds:
|
||||
- echo 'Text' > d23.txt
|
||||
|
||||
d32:
|
||||
cmds:
|
||||
- echo 'Text' > d32.txt
|
||||
d31:
|
||||
cmds:
|
||||
- echo 'Text' > d31.txt
|
||||
|
||||
d33:
|
||||
cmds:
|
||||
- echo 'Text' > d33.txt
|
||||
d32:
|
||||
cmds:
|
||||
- echo 'Text' > d32.txt
|
||||
|
||||
d33:
|
||||
cmds:
|
||||
- echo 'Text' > d33.txt
|
||||
|
||||
7
testdata/dir/Taskfile.yml
vendored
Normal file
7
testdata/dir/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
whereami:
|
||||
cmds:
|
||||
- pwd
|
||||
silent: true
|
||||
8
testdata/dir/explicit_doesnt_exist/Taskfile.yml
vendored
Normal file
8
testdata/dir/explicit_doesnt_exist/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
whereami:
|
||||
dir: createme
|
||||
cmds:
|
||||
- pwd
|
||||
silent: true
|
||||
8
testdata/dir/explicit_exists/Taskfile.yml
vendored
Normal file
8
testdata/dir/explicit_exists/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
whereami:
|
||||
dir: exists
|
||||
cmds:
|
||||
- pwd
|
||||
silent: true
|
||||
0
testdata/dir/explicit_exists/exists/.keep
vendored
Normal file
0
testdata/dir/explicit_exists/exists/.keep
vendored
Normal file
8
testdata/dotenv/Taskfile-errors1.yml
vendored
Normal file
8
testdata/dotenv/Taskfile-errors1.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: '3'
|
||||
|
||||
dotenv: ['include1/.env', 'include1/envs/.env', 'file-does-not-exist']
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "INCLUDE1='$INCLUDE1' INCLUDE2='$INCLUDE2'" > include-errors1.txt
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user