welp 0.3.1

Fixes the command you just typed wrong. A drop-in replacement for thefuck: every rule, every shell, no Python.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
//! More language and tool rules ported from thefuck: adb, aws, az, cargo,
//! composer, conda, docker, fab, gem, go, gradle, grunt, gulp, heroku, lein,
//! mvn, npm, pip, python, rails, react-native, terraform, tsuru, vagrant,
//! workon and yarn.
//!
//! Where thefuck runs a tool to learn the valid names, the rule here runs the
//! same help or listing command through `listing` (read-only, 3 s timeout),
//! or reads the file the information lives in.

use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::sync::LazyLock;

use regex::Regex;

use super::{Rule, listing, replace_arg, replace_program};
use crate::command::Command;

/// Rules in this file that are off unless the user enables them.
pub const DISABLED: &[&str] = &[
    // Its fix force-removes a container the user never mentioned.
    "docker_image_being_used_by_container",
    // `--delete-ghost-migrations` deletes migration records from the database;
    // South's own message says "this may not be good".
    "django_south_ghost",
];

pub fn rules() -> Vec<Rule> {
    vec![
        Rule {
            name: "adb_unknown_command",
            description: "Fixes a mistyped adb command",
            priority: 1100,
            matches: |c| {
                is_app(c, &["adb"]) && c.output.starts_with("Android Debug Bridge version")
            },
            fixes: adb_unknown_command,
        },
        Rule {
            name: "ag_literal",
            description: "Searches ag for a literal string with -Q after a bad regex",
            priority: 1000,
            matches: |c| is_app(c, &["ag"]) && c.output.trim_end().ends_with("run ag with -Q"),
            fixes: |c| vec![replace_program(&c.script, "ag -Q")],
        },
        Rule {
            name: "aws_cli",
            description: "Takes the aws command the cli suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["aws"])
                    && c.output.contains("usage:")
                    && c.output.contains("maybe you meant:")
            },
            fixes: aws_cli,
        },
        Rule {
            name: "az_cli",
            description: "Takes the az command the cli suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["az"])
                    && c.output.contains("is not in the")
                    && c.output.contains("command group")
            },
            fixes: az_cli,
        },
        Rule {
            name: "cargo",
            description: "Builds when cargo is run without a command",
            priority: 1000,
            matches: |c| c.script == "cargo",
            fixes: |_| vec!["cargo build".to_string()],
        },
        Rule {
            name: "cargo_no_command",
            description: "Takes the cargo command it suggests",
            priority: 1000,
            matches: |c| {
                let output = c.output_lower();
                is_app(c, &["cargo"])
                    && c.parts.len() > 1
                    && (output.contains("no such subcommand") || output.contains("no such command"))
                    && c.output.contains("Did you mean")
            },
            fixes: cargo_no_command,
        },
        Rule {
            name: "composer_not_command",
            description: "Takes the composer command it suggests",
            priority: 1000,
            matches: composer_not_command_matches,
            fixes: composer_not_command,
        },
        Rule {
            name: "conda_mistype",
            description: "Takes the conda command it suggests",
            priority: 1000,
            matches: |c| is_app(c, &["conda"]) && c.output.contains("Did you mean 'conda"),
            fixes: conda_mistype,
        },
        Rule {
            name: "cpp11",
            description: "Adds -std=c++11 when the compiler needs it",
            priority: 1000,
            matches: |c| {
                is_app(c, &["g++", "clang++"])
                    && (c.output.contains(
                        "This file requires compiler and library support for the ISO C++ 2011 standard.",
                    ) || c.output.contains("-Wc++11-extensions"))
            },
            fixes: |c| vec![format!("{} -std=c++11", c.script)],
        },
        Rule {
            name: "django_south_ghost",
            description: "Deletes ghost migrations from the database with --delete-ghost-migrations",
            priority: 1000,
            matches: |c| {
                c.script.contains("manage.py")
                    && c.script.contains("migrate")
                    && c.output.contains("or pass --delete-ghost-migrations")
            },
            fixes: |c| vec![format!("{} --delete-ghost-migrations", c.script)],
        },
        Rule {
            name: "django_south_merge",
            description: "Adds --merge to a South migration that ran out of order",
            priority: 1000,
            matches: |c| {
                c.script.contains("manage.py")
                    && c.script.contains("migrate")
                    && c.output
                        .contains("--merge: will just attempt the migration")
            },
            fixes: |c| vec![format!("{} --merge", c.script)],
        },
        Rule {
            name: "docker_image_being_used_by_container",
            description: "Force-removes the container that uses the image, then removes it",
            priority: 1000,
            matches: |c| {
                is_app(c, &["docker"])
                    && c.output
                        .contains("image is being used by running container")
            },
            fixes: |c| {
                c.output
                    .split_whitespace()
                    .last()
                    .map(|id| {
                        format!(
                            "docker container rm -f {} && {}",
                            crate::command::quote(id),
                            c.script
                        )
                    })
                    .into_iter()
                    .collect()
            },
        },
        Rule {
            name: "docker_login",
            description: "Logs in to docker first",
            priority: 1000,
            matches: |c| {
                is_app(c, &["docker"])
                    && c.script.contains("docker")
                    && c.output.contains("access denied")
                    && c.output.contains("may require 'docker login'")
            },
            fixes: |c| vec![format!("docker login && {}", c.script)],
        },
        Rule {
            name: "docker_not_command",
            description: "Fixes a mistyped docker command",
            priority: 1100,
            matches: docker_not_command_matches,
            fixes: |c| with_sudo(c, docker_not_command),
        },
        Rule {
            name: "fab_command_not_found",
            description: "Fixes a mistyped fabric task",
            priority: 1100,
            matches: |c| is_app(c, &["fab"]) && c.output.contains("Warning: Command(s) not found:"),
            fixes: fab_command_not_found,
        },
        Rule {
            name: "gem_unknown_command",
            description: "Fixes a mistyped gem command",
            priority: 1100,
            matches: |c| {
                is_app(c, &["gem"])
                    && c.output
                        .contains("ERROR:  While executing gem ... (Gem::CommandLineError)")
                    && c.output.contains("Unknown command")
            },
            fixes: |c| gem_unknown_command(c, listing("gem", &["help", "commands"]).as_deref()),
        },
        Rule {
            name: "go_unknown_command",
            description: "Fixes a mistyped go command",
            priority: 1100,
            matches: |c| {
                is_app(c, &["go"]) && c.parts.len() > 1 && c.output.contains("unknown command")
            },
            fixes: |c| go_unknown_command(c, listing("go", &[]).as_deref()),
        },
        Rule {
            name: "gradle_no_task",
            description: "Fixes a mistyped gradle task",
            priority: 1100,
            matches: |c| is_app(c, &["gradle", "gradlew"]) && GRADLE_TASK.is_match(&c.output),
            fixes: gradle_no_task,
        },
        Rule {
            name: "gradle_wrapper",
            description: "Uses ./gradlew when gradle itself is not installed",
            priority: 1000,
            matches: |c| {
                is_app(c, &["gradle"])
                    && gradle_wrapper_applies(c, on_path("gradle"), Path::new("gradlew").is_file())
            },
            fixes: |c| {
                vec![format!(
                    "./gradlew {}",
                    c.parts
                        .iter()
                        .skip(1)
                        .cloned()
                        .collect::<Vec<_>>()
                        .join(" ")
                )]
            },
        },
        Rule {
            name: "grunt_task_not_found",
            description: "Fixes a mistyped grunt task",
            priority: 1100,
            matches: |c| is_app(c, &["grunt"]) && GRUNT_TASK.is_match(&c.output),
            fixes: |c| grunt_task_not_found(c, listing("grunt", &["--help"]).as_deref()),
        },
        Rule {
            name: "gulp_not_task",
            description: "Fixes a mistyped gulp task",
            priority: 1100,
            matches: |c| is_app(c, &["gulp"]) && c.output.contains("is not in your gulpfile"),
            fixes: |c| gulp_not_task(c, listing("gulp", &["--tasks-simple"]).as_deref()),
        },
        Rule {
            name: "heroku_multiple_apps",
            description: "Names one of the apps in the git remotes with --app",
            priority: 1000,
            matches: |c| {
                is_app(c, &["heroku"])
                    && c.output
                        .contains("https://devcenter.heroku.com/articles/multiple-environments")
            },
            fixes: |c| {
                HEROKU_APP
                    .captures_iter(&c.output)
                    .map(|found| format!("{} --app {}", c.script, &found[1]))
                    .collect()
            },
        },
        Rule {
            name: "heroku_not_command",
            description: "Runs the command heroku says it meant",
            priority: 1000,
            matches: |c| is_app(c, &["heroku"]) && c.output.contains("Run heroku _ to run"),
            fixes: |c| {
                HEROKU_RUN
                    .captures(&c.output)
                    .map(|found| found[1].to_string())
                    .into_iter()
                    .collect()
            },
        },
        Rule {
            name: "lein_not_task",
            description: "Takes the lein task it suggests",
            priority: 1000,
            matches: |c| is_app(c, &["lein"]) && lein_not_task_matches(&unsudo(c).1),
            fixes: |c| with_sudo(c, lein_not_task),
        },
        Rule {
            name: "mvn_no_command",
            description: "Gives mvn a goal to run",
            priority: 1000,
            matches: |c| {
                is_app(c, &["mvn"])
                    && c.output
                        .contains("No goals have been specified for this build")
            },
            fixes: |c| {
                vec![
                    format!("{} clean package", c.script),
                    format!("{} clean install", c.script),
                ]
            },
        },
        Rule {
            name: "mvn_unknown_lifecycle_phase",
            description: "Fixes a mistyped mvn lifecycle phase",
            priority: 1100,
            matches: |c| {
                is_app(c, &["mvn"])
                    && MVN_FAILED_PHASE.is_match(&c.output)
                    && MVN_PHASES.is_match(&c.output)
            },
            fixes: mvn_unknown_lifecycle_phase,
        },
        Rule {
            name: "npm_missing_script",
            description: "Fixes a mistyped script name for npm run",
            priority: 1100,
            matches: |c| {
                is_app(c, &["npm"])
                    && c.parts.iter().any(|part| part.starts_with("ru"))
                    && NPM_MISSING_SCRIPT.is_match(&c.output)
            },
            fixes: |c| npm_missing_script(c, &package_scripts(&read_package_json(Path::new(".")))),
        },
        Rule {
            name: "npm_wrong_command",
            description: "Fixes a mistyped npm command",
            priority: 1100,
            matches: |c| is_app(c, &["npm"]) && npm_wrong_command_matches(&unsudo(c).1),
            fixes: |c| with_sudo(c, npm_wrong_command),
        },
        Rule {
            name: "php_s",
            description: "Uses -S, the built-in server flag, in place of -s",
            priority: 1000,
            matches: |c| {
                is_app(c, &["php"])
                    && c.parts.len() > 2
                    && c.parts.iter().any(|part| part == "-s")
                    && c.parts.last().is_some_and(|last| last != "-s")
            },
            fixes: |c| vec![replace_arg(&c.script, "-s", "-S")],
        },
        Rule {
            name: "pip_unknown_command",
            description: "Takes the pip command it suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["pip", "pip2", "pip3"])
                    && c.output.contains("unknown command")
                    && c.output.contains("maybe you meant")
            },
            fixes: pip_unknown_command,
        },
        Rule {
            name: "python_command",
            description: "Runs a .py script with python",
            priority: 1000,
            matches: |c| c.script.contains(".py") && python_command_matches(&unsudo(c).1),
            fixes: |c| {
                let (sudo, inner) = unsudo(c);
                vec![format!("{sudo}python {}", inner.script)]
            },
        },
        Rule {
            name: "python_module_error",
            description: "Installs the missing Python module with pip, then runs the command",
            priority: 1000,
            matches: |c| c.output.contains("ModuleNotFoundError: No module named '"),
            fixes: |c| {
                MISSING_MODULE
                    .captures(&c.output)
                    .map(|found| {
                        format!(
                            "pip install {} && {}",
                            crate::command::quote(&found[1]),
                            c.script
                        )
                    })
                    .into_iter()
                    .collect()
            },
        },
        Rule {
            name: "rails_migrations_pending",
            description: "Runs the pending migrations rails asks for, then the command",
            priority: 1000,
            matches: |c| {
                c.output
                    .contains("Migrations are pending. To resolve this issue, run:")
            },
            fixes: |c| {
                RAILS_MIGRATE
                    .captures(&c.output)
                    .map(|found| format!("{} && {}", &found[1], c.script))
                    .into_iter()
                    .collect()
            },
        },
        Rule {
            name: "react_native_command_unrecognized",
            description: "Fixes a mistyped react-native command",
            priority: 1100,
            matches: |c| is_app(c, &["react-native"]) && REACT_NATIVE_UNKNOWN.is_match(&c.output),
            fixes: |c| {
                react_native_command_unrecognized(
                    c,
                    listing("react-native", &["--help"]).as_deref(),
                )
            },
        },
        Rule {
            name: "terraform_init",
            description: "Runs terraform init first",
            priority: 1000,
            matches: |c| {
                let output = c.output_lower();
                is_app(c, &["terraform"])
                    && (output.contains("this module is not yet installed")
                        || output.contains("initialization required"))
            },
            fixes: |c| vec![format!("terraform init && {}", c.script)],
        },
        Rule {
            name: "terraform_no_command",
            description: "Takes the terraform command it suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["terraform"])
                    && TERRAFORM_MISTAKE.is_match(&c.output)
                    && TERRAFORM_FIX.is_match(&c.output)
            },
            fixes: terraform_no_command,
        },
        Rule {
            name: "tsuru_login",
            description: "Logs in to tsuru first",
            priority: 1000,
            matches: |c| {
                is_app(c, &["tsuru"])
                    && c.output.contains("not authenticated")
                    && c.output.contains("session has expired")
            },
            fixes: |c| vec![format!("tsuru login && {}", c.script)],
        },
        Rule {
            name: "tsuru_not_command",
            description: "Takes the tsuru command it suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["tsuru"])
                    && c.output
                        .contains(" is not a tsuru command. See \"tsuru help\".")
                    && c.output.contains("\nDid you mean?\n\t")
            },
            fixes: tsuru_not_command,
        },
        Rule {
            name: "vagrant_up",
            description: "Starts the machine with vagrant up first",
            priority: 1000,
            matches: |c| is_app(c, &["vagrant"]) && c.output_lower().contains("run `vagrant up`"),
            fixes: vagrant_up,
        },
        Rule {
            name: "workon_doesnt_exists",
            description: "Fixes a mistyped virtualenv name, or creates it with mkvirtualenv",
            priority: 1100,
            matches: |c| is_app(c, &["workon"]) && workon_applies(c, &workon_envs()),
            fixes: |c| workon_fixes(c, &workon_envs()),
        },
        Rule {
            name: "yarn_alias",
            description: "Takes the yarn command it suggests",
            priority: 1000,
            matches: |c| {
                is_app(c, &["yarn"]) && c.parts.len() > 1 && c.output.contains("Did you mean")
            },
            fixes: yarn_alias,
        },
        Rule {
            name: "yarn_command_replaced",
            description: "Runs the command yarn says replaced the old one",
            priority: 1000,
            matches: |c| {
                is_app(c, &["yarn"]) && c.parts.len() > 1 && YARN_REPLACED.is_match(&c.output)
            },
            fixes: |c| {
                YARN_REPLACED
                    .captures(&c.output)
                    .map(|found| found[1].to_string())
                    .into_iter()
                    .collect()
            },
        },
        Rule {
            name: "yarn_help",
            description: "Opens the documentation page yarn help points to",
            priority: 1000,
            matches: |c| {
                is_app(c, &["yarn"])
                    && c.parts.len() > 2
                    && c.parts[1] == "help"
                    && c.output.contains("for documentation about this command.")
            },
            fixes: |c| {
                YARN_DOCS
                    .captures(&c.output)
                    .map(|found| open_command(&found[1], on_path("xdg-open")))
                    .into_iter()
                    .collect()
            },
        },
    ]
}

// helpers shared with more_pkg

/// thefuck's `is_app`: the program, by its base name, is one of `names`. A
/// leading `sudo` is looked through.
pub(super) fn is_app(command: &Command, names: &[&str]) -> bool {
    let program = command.program();
    names.contains(&program.rsplit('/').next().unwrap_or(program))
}

/// Whether `program` is an executable file in a directory on PATH.
pub(super) fn on_path(program: &str) -> bool {
    std::env::var_os("PATH")
        .is_some_and(|path| std::env::split_paths(&path).any(|dir| dir.join(program).is_file()))
}

/// thefuck's `sudo_support`: a rule sees the command without its leading
/// `sudo`, and what it fixes gets the `sudo` back.
pub(super) fn unsudo(command: &Command) -> (&'static str, Command) {
    match command.script.strip_prefix("sudo ") {
        Some(rest) => ("sudo ", Command::new(rest.trim_start(), &command.output)),
        None => ("", command.clone()),
    }
}

pub(super) fn with_sudo(command: &Command, fixes: fn(&Command) -> Vec<String>) -> Vec<String> {
    let (sudo, inner) = unsudo(command);
    fixes(&inner)
        .into_iter()
        .map(|fix| format!("{sudo}{fix}"))
        .collect()
}

/// Python's `SequenceMatcher(None, a, b).ratio()`, so the order of the
/// suggestions is the one thefuck gives. Python tracks "junk" only for
/// strings of 200 or more characters; nothing here is that long.
fn ratio(a: &[char], b: &[char]) -> f64 {
    let total = a.len() + b.len();
    if total == 0 {
        return 1.0;
    }
    let mut matched = 0;
    let mut pending = vec![(0, a.len(), 0, b.len())];
    while let Some((alo, ahi, blo, bhi)) = pending.pop() {
        let (i, j, size) = longest_match(a, b, alo, ahi, blo, bhi);
        if size > 0 {
            matched += size;
            if alo < i && blo < j {
                pending.push((alo, i, blo, j));
            }
            if i + size < ahi && j + size < bhi {
                pending.push((i + size, ahi, j + size, bhi));
            }
        }
    }
    2.0 * matched as f64 / total as f64
}

/// The longest run that `a[alo..ahi]` and `b[blo..bhi]` share: where it
/// starts in each and how long it is, the earliest one when several tie.
fn longest_match(
    a: &[char],
    b: &[char],
    alo: usize,
    ahi: usize,
    blo: usize,
    bhi: usize,
) -> (usize, usize, usize) {
    let (mut best_i, mut best_j, mut best_size) = (alo, blo, 0);
    let mut lengths: HashMap<usize, usize> = HashMap::new();
    for (i, ai) in a.iter().enumerate().take(ahi).skip(alo) {
        let mut next = HashMap::new();
        for (j, bj) in b.iter().enumerate().take(bhi).skip(blo) {
            if ai != bj {
                continue;
            }
            let before = if j > 0 {
                lengths.get(&(j - 1)).copied().unwrap_or(0)
            } else {
                0
            };
            let size = before + 1;
            next.insert(j, size);
            if size > best_size {
                best_i = i + 1 - size;
                best_j = j + 1 - size;
                best_size = size;
            }
        }
        lengths = next;
    }
    (best_i, best_j, best_size)
}

/// Python's `difflib.get_close_matches`: up to `n` of the `possibilities`
/// that look like `word` (at least `cutoff` out of 1), best first.
fn close_matches<S: AsRef<str>>(
    word: &str,
    possibilities: &[S],
    n: usize,
    cutoff: f64,
) -> Vec<String> {
    let word: Vec<char> = word.chars().collect();
    let mut scored: Vec<(f64, &str)> = possibilities
        .iter()
        .map(AsRef::as_ref)
        .filter_map(|candidate| {
            let chars: Vec<char> = candidate.chars().collect();
            let score = ratio(&chars, &word);
            (score >= cutoff).then_some((score, candidate))
        })
        .collect();
    // Python picks the largest (score, name) pairs.
    scored.sort_by(|x, y| y.0.total_cmp(&x.0).then_with(|| y.1.cmp(x.1)));
    scored
        .into_iter()
        .take(n)
        .map(|(_, name)| name.to_string())
        .collect()
}

/// thefuck's `get_closest`. Unlike thefuck it does not fall back to the
/// first possibility when nothing is close: that offered fixes like
/// `adb backup` for `adb xyzzy`.
fn closest<S: AsRef<str>>(word: &str, possibilities: &[S]) -> Option<String> {
    close_matches(word, possibilities, 1, 0.6)
        .into_iter()
        .next()
}

/// thefuck's `replace_command`: the script once per name that looks like
/// `broken`, with `broken` swapped for it. thefuck shows three.
pub(super) fn replace_command<S: AsRef<str>>(
    script: &str,
    broken: &str,
    matched: &[S],
) -> Vec<String> {
    close_matches(broken, matched, 3, 0.1)
        .iter()
        .map(|new| replace_arg(script, broken, new.trim()))
        .collect()
}

/// thefuck's `get_all_matched_commands`: the non-empty lines after one that
/// holds a separator.
fn lines_after(output: &str, separators: &[&str]) -> Vec<String> {
    let mut found = false;
    let mut lines = Vec::new();
    for line in output.split('\n') {
        if separators.iter().any(|separator| line.contains(separator)) {
            found = true;
        } else if found && !line.trim().is_empty() {
            lines.push(line.trim().to_string());
        }
    }
    lines
}

fn first_word(line: &str) -> Option<String> {
    line.split(' ')
        .next()
        .filter(|word| !word.is_empty())
        .map(String::from)
}

// adb

const ADB_COMMANDS: &[&str] = &[
    "backup",
    "bugreport",
    "connect",
    "devices",
    "disable-verity",
    "disconnect",
    "enable-verity",
    "emu",
    "forward",
    "get-devpath",
    "get-serialno",
    "get-state",
    "install",
    "install-multiple",
    "jdwp",
    "keygen",
    "kill-server",
    "logcat",
    "pull",
    "push",
    "reboot",
    "reconnect",
    "restore",
    "reverse",
    "root",
    "run-as",
    "shell",
    "sideload",
    "start-server",
    "sync",
    "tcpip",
    "uninstall",
    "unroot",
    "usb",
    "wait-for",
];

fn adb_unknown_command(command: &Command) -> Vec<String> {
    for (index, arg) in command.parts.iter().enumerate().skip(1) {
        // -s, -H, -P and -L take a value, which is not the command
        let takes_value = matches!(command.parts[index - 1].as_str(), "-s" | "-H" | "-P" | "-L");
        if !arg.starts_with('-') && !takes_value {
            return closest(arg, ADB_COMMANDS)
                .map(|right| replace_arg(&command.script, arg, &right))
                .into_iter()
                .collect();
        }
    }
    Vec::new()
}

// aws and az

static AWS_INVALID_CHOICE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Invalid choice: '(.*)', maybe you meant:").unwrap());
static AWS_OPTION: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?m)^\s*\*\s(.*)").unwrap());

fn aws_cli(command: &Command) -> Vec<String> {
    let Some(mistake) = AWS_INVALID_CHOICE.captures(&command.output) else {
        return Vec::new();
    };
    AWS_OPTION
        .captures_iter(&command.output)
        .map(|option| replace_arg(&command.script, &mistake[1], option[1].trim()))
        .collect()
}

static AZ_INVALID_CHOICE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"az.*: '(.*)' is not in the '.*' command group\.").unwrap());
static AZ_OPTION: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?m)^The most similar choice to '.*' is:\n\s*(.*)$").unwrap());

fn az_cli(command: &Command) -> Vec<String> {
    let Some(mistake) = AZ_INVALID_CHOICE.captures(&command.output) else {
        return Vec::new();
    };
    AZ_OPTION
        .captures_iter(&command.output)
        .map(|option| replace_arg(&command.script, &mistake[1], option[1].trim()))
        .collect()
}

// cargo, composer and conda

static CARGO_SUGGESTION: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Did you mean `([^`]*)`").unwrap());

fn cargo_no_command(command: &Command) -> Vec<String> {
    let (Some(broken), Some(found)) = (
        command.parts.get(1),
        CARGO_SUGGESTION.captures(&command.output),
    ) else {
        return Vec::new();
    };
    vec![replace_arg(&command.script, broken, &found[1])]
}

static COMPOSER_UNDEFINED: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Command "([^"]*)" is not defined"#).unwrap());
static COMPOSER_THIS: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Did you mean this\?[^\n]*\n\s*([^\n]*)").unwrap());
static COMPOSER_ONE_OF: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Did you mean one of these\?[^\n]*\n\s*([^\n]*)").unwrap());

/// composer says to use `require` when `install` was given a package.
fn composer_wants_require(command: &Command) -> bool {
    command.parts.iter().any(|part| part == "install")
        && command.output_lower().contains("composer require")
}

fn composer_not_command_matches(command: &Command) -> bool {
    let output = command.output_lower();
    is_app(command, &["composer"])
        && (output.contains("did you mean this?")
            || output.contains("did you mean one of these?")
            || composer_wants_require(command))
}

fn composer_not_command(command: &Command) -> Vec<String> {
    if composer_wants_require(command) {
        return vec![replace_arg(&command.script, "install", "require")];
    }
    let Some(broken) = COMPOSER_UNDEFINED.captures(&command.output) else {
        return Vec::new();
    };
    let suggestion = COMPOSER_THIS
        .captures(&command.output)
        .or_else(|| COMPOSER_ONE_OF.captures(&command.output));
    suggestion
        .map(|found| replace_arg(&command.script, &broken[1], found[1].trim()))
        .into_iter()
        .collect()
}

static CONDA_COMMAND: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"'conda ([^']*)'").unwrap());

fn conda_mistype(command: &Command) -> Vec<String> {
    let names: Vec<String> = CONDA_COMMAND
        .captures_iter(&command.output)
        .map(|found| found[1].to_string())
        .collect();
    match names.as_slice() {
        [broken, correct, ..] => {
            replace_command(&command.script, broken, std::slice::from_ref(correct))
        }
        _ => Vec::new(),
    }
}

// docker

static DOCKER_UNKNOWN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"docker: '(\w+)' is not a docker command.").unwrap());

fn docker_not_command_matches(command: &Command) -> bool {
    is_app(command, &["docker"])
        && (command.output.contains("is not a docker command")
            || command.output.contains("Usage:\tdocker"))
}

/// The names listed under a heading such as `Commands:` in docker's help.
fn docker_commands_under(lines: &[&str], heading: &str) -> Vec<String> {
    lines
        .iter()
        .skip_while(|line| !line.starts_with(heading))
        .skip(1)
        .take_while(|line| !line.trim().is_empty())
        .filter_map(|line| first_word(line.trim()))
        .collect()
}

/// Every command in `docker`'s own help: management commands first.
fn docker_commands(help: &str) -> Vec<String> {
    let lines: Vec<&str> = help.lines().collect();
    let mut commands = if lines.contains(&"Management Commands:") {
        docker_commands_under(&lines, "Management Commands:")
    } else {
        Vec::new()
    };
    commands.extend(docker_commands_under(&lines, "Commands:"));
    commands
}

fn docker_not_command(command: &Command) -> Vec<String> {
    docker_not_command_with(command, || listing("docker", &[]))
}

/// `help` is asked for only when the command needs docker's own list.
fn docker_not_command_with(
    command: &Command,
    help: impl FnOnce() -> Option<String>,
) -> Vec<String> {
    // `docker image la`: docker printed the help of `docker image`
    if command.output.contains("Usage:") && command.parts.len() > 1 {
        let lines: Vec<&str> = command.output.split('\n').collect();
        let subcommands = docker_commands_under(&lines, "Commands:");
        return match command.parts.get(2) {
            Some(wrong) => replace_command(&command.script, wrong, &subcommands),
            None => Vec::new(),
        };
    }
    let Some(wrong) = DOCKER_UNKNOWN.captures(&command.output) else {
        return Vec::new();
    };
    let help = help().unwrap_or_default();
    replace_command(&command.script, &wrong[1], &docker_commands(&help))
}

// fab

/// Names in fab's output after the line holding `start`, up to the line
/// holding `end` if there is one.
fn fab_between(output: &str, start: &str, end: Option<&str>) -> Vec<String> {
    let mut found = false;
    let mut names = Vec::new();
    for line in output.split('\n') {
        if line.contains(start) {
            found = true;
            continue;
        }
        if end.is_some_and(|end| line.contains(end)) {
            break;
        }
        if found && !line.trim().is_empty() {
            names.extend(first_word(line.trim()));
        }
    }
    names
}

fn fab_command_not_found(command: &Command) -> Vec<String> {
    let missing = fab_between(
        &command.output,
        "Warning: Command(s) not found:",
        Some("Available commands:"),
    );
    let available = fab_between(&command.output, "Available commands:", None);
    let mut script = command.script.clone();
    for name in missing {
        if let Some(right) = closest(&name, &available) {
            script = script.replace(&format!(" {name}"), &format!(" {right}"));
        }
    }
    vec![script]
}

// gem and go

static GEM_UNKNOWN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"(?m)Unknown command (.*)$").unwrap());

/// The commands in `gem help commands`: indented four spaces, not more.
fn gem_commands(help: &str) -> Vec<String> {
    help.lines()
        .filter_map(|line| line.strip_prefix("    "))
        .filter(|rest| !rest.starts_with(' '))
        .filter_map(first_word)
        .collect()
}

fn gem_unknown_command(command: &Command, help: Option<&str>) -> Vec<String> {
    let (Some(unknown), Some(help)) = (GEM_UNKNOWN.captures(&command.output), help) else {
        return Vec::new();
    };
    replace_command(&command.script, unknown[1].trim(), &gem_commands(help))
}

/// The commands `go` prints under "The commands are:".
fn go_commands(help: &str) -> Vec<String> {
    help.lines()
        .map(str::trim)
        .skip_while(|line| *line != "The commands are:")
        .skip(2)
        .take_while(|line| !line.is_empty())
        .filter_map(first_word)
        .collect()
}

fn go_unknown_command(command: &Command, help: Option<&str>) -> Vec<String> {
    let (Some(wrong), Some(help)) = (command.parts.get(1), help) else {
        return Vec::new();
    };
    closest(wrong, &go_commands(help))
        .map(|right| replace_arg(&command.script, wrong, &right))
        .into_iter()
        .collect()
}

// gradle

static GRADLE_TASK: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Task '(.*)' (is ambiguous|not found)").unwrap());

/// The tasks in `gradle tasks`: the lines under a `----` rule.
fn gradle_tasks(help: &str) -> Vec<String> {
    let mut listed = false;
    let mut tasks = Vec::new();
    for line in help.lines().map(str::trim) {
        if line.starts_with("----") {
            listed = true;
        } else if line.is_empty() {
            listed = false;
        } else if listed
            && !line.starts_with("All tasks runnable from root project")
            && !line.starts_with("Tasks runnable from root project")
        {
            tasks.extend(first_word(line));
        }
    }
    tasks
}

fn gradle_no_task(command: &Command) -> Vec<String> {
    let Some(wrong) = GRADLE_TASK.captures(&command.output) else {
        return Vec::new();
    };
    let program = command.program();
    // listing only finds programs on PATH or given as an absolute path
    let program = if program.contains('/') && !Path::new(program).is_absolute() {
        std::env::current_dir()
            .map(|dir| dir.join(program).to_string_lossy().into_owned())
            .unwrap_or_else(|_| program.to_string())
    } else {
        program.to_string()
    };
    let Some(help) = listing(&program, &["tasks"]) else {
        return Vec::new();
    };
    replace_command(&command.script, &wrong[1], &gradle_tasks(&help))
}

fn gradle_wrapper_applies(command: &Command, gradle_installed: bool, has_wrapper: bool) -> bool {
    is_app(command, &["gradle"])
        && !gradle_installed
        && command.output.contains("not found")
        && has_wrapper
}

// grunt and gulp

static GRUNT_TASK: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Warning: Task "(.*)" not found."#).unwrap());

/// The tasks under "Available tasks" in `grunt --help`.
fn grunt_tasks(help: &str) -> Vec<String> {
    let mut listed = false;
    let mut tasks = Vec::new();
    for line in help.lines().map(str::trim) {
        if line.contains("Available tasks") {
            listed = true;
        } else if listed && line.is_empty() {
            break;
        } else if listed && line.contains("  ") {
            tasks.extend(first_word(line));
        }
    }
    tasks
}

fn grunt_task_not_found(command: &Command, help: Option<&str>) -> Vec<String> {
    let (Some(task), Some(help)) = (GRUNT_TASK.captures(&command.output), help) else {
        return Vec::new();
    };
    // `grunt cmpass:all`: the task is the part before the colon
    let wrong = task[1].split(':').next().unwrap_or_default();
    closest(wrong, &grunt_tasks(help))
        .map(|right| {
            command
                .script
                .replace(&format!(" {wrong}"), &format!(" {right}"))
        })
        .into_iter()
        .collect()
}

static GULP_TASK: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Task '(\w+)' is not in your gulpfile").unwrap());

fn gulp_not_task(command: &Command, tasks: Option<&str>) -> Vec<String> {
    let (Some(wrong), Some(tasks)) = (GULP_TASK.captures(&command.output), tasks) else {
        return Vec::new();
    };
    let tasks: Vec<&str> = tasks
        .lines()
        .map(str::trim)
        .filter(|task| !task.is_empty())
        .collect();
    replace_command(&command.script, &wrong[1], &tasks)
}

// heroku

static HEROKU_APP: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([^\s]+) \([^)]*\)").unwrap());
static HEROKU_RUN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Run heroku _ to run ([^.]*)").unwrap());

// lein

static LEIN_NOT_TASK: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"'([^']*)' is not a task").unwrap());

fn lein_not_task_matches(command: &Command) -> bool {
    is_app(command, &["lein"])
        && command.script.starts_with("lein")
        && command.output.contains("is not a task. See 'lein help'")
        && command.output.contains("Did you mean this?")
}

fn lein_not_task(command: &Command) -> Vec<String> {
    let Some(broken) = LEIN_NOT_TASK.captures(&command.output) else {
        return Vec::new();
    };
    replace_command(
        &command.script,
        &broken[1],
        &lines_after(&command.output, &["Did you mean this?"]),
    )
}

// mvn

static MVN_FAILED_PHASE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"\[ERROR\] Unknown lifecycle phase "([^"]+)""#).unwrap());
static MVN_PHASES: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Available lifecycle phases are: (.+) -> \[Help 1\]").unwrap());

fn mvn_unknown_lifecycle_phase(command: &Command) -> Vec<String> {
    let (Some(failed), Some(available)) = (
        MVN_FAILED_PHASE.captures(&command.output),
        MVN_PHASES.captures(&command.output),
    ) else {
        return Vec::new();
    };
    let phases: Vec<&str> = available[1].split(", ").collect();
    let selected = close_matches(&failed[1], &phases, 3, 0.6);
    replace_command(&command.script, &failed[1], &selected)
}

// npm

static NPM_MISSING_SCRIPT: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r#"(?i)npm (?:ERR!|error) missing script: "?([^"\n]*)"?"#).unwrap()
});

fn npm_missing_script(command: &Command, scripts: &[String]) -> Vec<String> {
    NPM_MISSING_SCRIPT
        .captures(&command.output)
        .map(|missing| replace_command(&command.script, missing[1].trim(), scripts))
        .unwrap_or_default()
}

fn read_package_json(dir: &Path) -> String {
    fs::read_to_string(dir.join("package.json")).unwrap_or_default()
}

static SCRIPTS_KEY: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#""scripts"\s*:\s*\{"#).unwrap());

/// The names under `"scripts"` in a package.json, which is where `npm run`
/// lists them from. A scripts block is only string pairs, so a small
/// scanner does.
fn package_scripts(json: &str) -> Vec<String> {
    let mut names = Vec::new();
    let Some(start) = SCRIPTS_KEY.find(json) else {
        return names;
    };
    let mut chars = json[start.end()..].chars().peekable();
    loop {
        while chars.next_if(|c| c.is_whitespace() || *c == ',').is_some() {}
        let Some(name) = json_string(&mut chars) else {
            break;
        };
        while chars.next_if(|c| c.is_whitespace()).is_some() {}
        if chars.next() != Some(':') {
            break;
        }
        while chars.next_if(|c| c.is_whitespace()).is_some() {}
        if json_string(&mut chars).is_none() {
            break;
        }
        names.push(name);
    }
    names
}

fn json_string(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<String> {
    chars.next_if_eq(&'"')?;
    let mut text = String::new();
    loop {
        match chars.next()? {
            '"' => return Some(text),
            '\\' => text.push(chars.next()?),
            c => text.push(c),
        }
    }
}

/// The command npm was given: the first word that is not a flag.
fn npm_wrong_word(command: &Command) -> Option<&String> {
    command
        .parts
        .iter()
        .skip(1)
        .find(|part| !part.starts_with('-'))
}

fn npm_wrong_command_matches(command: &Command) -> bool {
    is_app(command, &["npm"])
        && command.parts.first().is_some_and(|first| first == "npm")
        && command.output.contains("where <command> is one of:")
        && npm_wrong_word(command).is_some()
}

/// The commands npm lists after "where <command> is one of:".
fn npm_commands(output: &str) -> Vec<String> {
    output
        .split('\n')
        .skip_while(|line| !line.starts_with("where <command> is one of:"))
        .skip(1)
        .take_while(|line| !line.is_empty())
        .flat_map(|line| line.split(", "))
        .map(str::trim)
        .filter(|name| !name.is_empty())
        .map(String::from)
        .collect()
}

fn npm_wrong_command(command: &Command) -> Vec<String> {
    let Some(wrong) = npm_wrong_word(command) else {
        return Vec::new();
    };
    closest(wrong, &npm_commands(&command.output))
        .map(|right| replace_arg(&command.script, wrong, &right))
        .into_iter()
        .collect()
}

// pip and python

static PIP_BROKEN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"ERROR: unknown command "([^"]+)""#).unwrap());
static PIP_SUGGESTED: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"maybe you meant "([^"]+)""#).unwrap());

fn pip_unknown_command(command: &Command) -> Vec<String> {
    match (
        PIP_BROKEN.captures(&command.output),
        PIP_SUGGESTED.captures(&command.output),
    ) {
        (Some(broken), Some(suggested)) => {
            vec![replace_arg(&command.script, &broken[1], &suggested[1])]
        }
        _ => Vec::new(),
    }
}

fn python_command_matches(command: &Command) -> bool {
    command
        .parts
        .first()
        .is_some_and(|first| first.ends_with(".py"))
        && (command.output.contains("Permission denied")
            || command.output.contains("command not found"))
}

static MISSING_MODULE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"ModuleNotFoundError: No module named '([^']+)'").unwrap());

// rails and react-native

static RAILS_MIGRATE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"To resolve this issue, run:\s+(.*?)\n").unwrap());

static REACT_NATIVE_UNKNOWN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Unrecognized command '(.*)'").unwrap());

/// The commands under "Commands:" in `react-native --help`.
fn react_native_commands(help: &str) -> Vec<String> {
    help.lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .skip_while(|line| !line.contains("Commands:"))
        .skip(1)
        .filter_map(first_word)
        .collect()
}

fn react_native_command_unrecognized(command: &Command, help: Option<&str>) -> Vec<String> {
    let (Some(wrong), Some(help)) = (REACT_NATIVE_UNKNOWN.captures(&command.output), help) else {
        return Vec::new();
    };
    replace_command(&command.script, &wrong[1], &react_native_commands(help))
}

// terraform

static TERRAFORM_MISTAKE: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Terraform has no command named "([^"]+)"\."#).unwrap());
static TERRAFORM_FIX: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Did you mean "([^"]+)"\?"#).unwrap());

fn terraform_no_command(command: &Command) -> Vec<String> {
    match (
        TERRAFORM_MISTAKE.captures(&command.output),
        TERRAFORM_FIX.captures(&command.output),
    ) {
        (Some(mistake), Some(fix)) => vec![replace_arg(&command.script, &mistake[1], &fix[1])],
        _ => Vec::new(),
    }
}

// tsuru

static TSURU_BROKEN: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"tsuru: "([^"]*)" is not a tsuru command"#).unwrap());

fn tsuru_not_command(command: &Command) -> Vec<String> {
    let Some(broken) = TSURU_BROKEN.captures(&command.output) else {
        return Vec::new();
    };
    replace_command(
        &command.script,
        &broken[1],
        &lines_after(&command.output, &["Did you mean"]),
    )
}

// vagrant and workon

fn vagrant_up(command: &Command) -> Vec<String> {
    let everything = format!("vagrant up && {}", command.script);
    match command.parts.get(2) {
        Some(machine) => vec![
            format!(
                "vagrant up {} && {}",
                crate::command::quote(machine),
                command.script
            ),
            everything,
        ],
        None => vec![everything],
    }
}

/// The virtualenvs in a directory such as ~/.virtualenvs.
fn virtualenvs(root: &Path) -> Vec<String> {
    let mut names: Vec<String> = fs::read_dir(root)
        .into_iter()
        .flatten()
        .flatten()
        .filter(|entry| entry.path().is_dir())
        .filter_map(|entry| entry.file_name().into_string().ok())
        .collect();
    names.sort();
    names
}

fn workon_envs() -> Vec<String> {
    dirs::home_dir()
        .map(|home| virtualenvs(&home.join(".virtualenvs")))
        .unwrap_or_default()
}

fn workon_applies(command: &Command, envs: &[String]) -> bool {
    is_app(command, &["workon"])
        && command
            .parts
            .get(1)
            .is_some_and(|name| !envs.contains(name))
}

fn workon_fixes(command: &Command, envs: &[String]) -> Vec<String> {
    let Some(misspelled) = command.parts.get(1) else {
        return Vec::new();
    };
    let mut fixes = replace_command(&command.script, misspelled, envs);
    fixes.push(format!(
        "mkvirtualenv {}",
        crate::command::quote(misspelled)
    ));
    fixes
}

// yarn

static YARN_SUGGESTION: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Did you mean [`"](?:yarn )?([^`"]*)[`"]"#).unwrap());
static YARN_REPLACED: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r#"Run "(.*)" instead"#).unwrap());
static YARN_DOCS: LazyLock<Regex> =
    LazyLock::new(|| Regex::new(r"Visit ([^ ]*) for documentation about this command\.").unwrap());

fn yarn_alias(command: &Command) -> Vec<String> {
    let (Some(broken), Some(fix)) = (
        command.parts.get(1),
        YARN_SUGGESTION.captures(&command.output),
    ) else {
        return Vec::new();
    };
    vec![replace_arg(&command.script, broken, &fix[1])]
}

/// thefuck's `open_command`: xdg-open where there is one, `open` (macOS) otherwise.
fn open_command(url: &str, has_xdg_open: bool) -> String {
    let opener = if has_xdg_open { "xdg-open" } else { "open" };
    format!("{opener} {}", crate::command::quote(url))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rules::{all, rule_matches};

    const DOCKER_SWARM_HELP: &str = r#"
Usage:	docker swarm COMMAND

Manage Swarm

Commands:
  ca          Display and rotate the root CA
  init        Initialize a swarm
  join        Join a swarm as a node and/or manager
  join-token  Manage join tokens
  leave       Leave the swarm
  unlock      Unlock swarm
  unlock-key  Manage the unlock key
  update      Update the swarm

Run 'docker swarm COMMAND --help' for more information on a command.
"#;
    const DOCKER_IMAGE_HELP: &str = r#"
Usage:	docker image COMMAND

Manage images

Commands:
  build       Build an image from a Dockerfile
  history     Show the history of an image
  import      Import the contents from a tarball to create a filesystem image
  inspect     Display detailed information on one or more images
  load        Load an image from a tar archive or STDIN
  ls          List images
  prune       Remove unused images
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rm          Remove one or more images
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE

Run 'docker image COMMAND --help' for more information on a command.
"#;
    const DOCKER_HELP: &str = r#"
Usage:	docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
      --config string      Location of client config files (default "/Users/ik1ne/.docker")
  -c, --context string     Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var
                           and default context set with "docker context use")
  -D, --debug              Enable debug mode
  -H, --host list          Daemon socket(s) to connect to
  -l, --log-level string   Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
      --tls                Use TLS; implied by --tlsverify
      --tlscacert string   Trust certs signed only by this CA (default "/Users/ik1ne/.docker/ca.pem")
      --tlscert string     Path to TLS certificate file (default "/Users/ik1ne/.docker/cert.pem")
      --tlskey string      Path to TLS key file (default "/Users/ik1ne/.docker/key.pem")
      --tlsverify          Use TLS and verify the remote
  -v, --version            Print version information and quit

Management Commands:
  builder     Manage builds
  config      Manage Docker configs
  container   Manage containers
  context     Manage contexts
  image       Manage images
  network     Manage networks
  node        Manage Swarm nodes
  plugin      Manage plugins
  secret      Manage Docker secrets
  service     Manage services
  stack       Manage Docker stacks
  swarm       Manage Swarm
  system      Manage Docker
  trust       Manage trust on Docker images
  volume      Manage volumes

Commands:
  attach      Attach local standard input, output, and error streams to a running container
  build       Build an image from a Dockerfile
  commit      Create a new image from a container's changes
  cp          Copy files/folders between a container and the local filesystem
  create      Create a new container
  diff        Inspect changes to files or directories on a container's filesystem
  events      Get real time events from the server
  exec        Run a command in a running container
  export      Export a container's filesystem as a tar archive
  history     Show the history of an image
  images      List images
  import      Import the contents from a tarball to create a filesystem image
  info        Display system-wide information
  inspect     Return low-level information on Docker objects
  kill        Kill one or more running containers
  load        Load an image from a tar archive or STDIN
  login       Log in to a Docker registry
  logout      Log out from a Docker registry
  logs        Fetch the logs of a container
  pause       Pause all processes within one or more containers
  port        List port mappings or a specific mapping for the container
  ps          List containers
  pull        Pull an image or a repository from a registry
  push        Push an image or a repository to a registry
  rename      Rename a container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more images
  run         Run a command in a new container
  save        Save one or more images to a tar archive (streamed to STDOUT by default)
  search      Search the Docker Hub for images
  start       Start one or more stopped containers
  stats       Display a live stream of container(s) resource usage statistics
  stop        Stop one or more running containers
  tag         Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
  top         Display the running processes of a container
  unpause     Unpause all processes within one or more containers
  update      Update configuration of one or more containers
  version     Show the Docker version information
  wait        Block until one or more containers stop, then print their exit codes

Run 'docker COMMAND --help' for more information on a command.
"#;
    const GEM_HELP: &str = r#"
GEM commands are:

    build             Build a gem from a gemspec
    cert              Manage RubyGems certificates and signing settings
    check             Check a gem repository for added or missing files
    cleanup           Clean up old versions of installed gems
    contents          Display the contents of the installed gems
    dependency        Show the dependencies of an installed gem
    environment       Display information about the RubyGems environment
    fetch             Download a gem and place it in the current directory
    generate_index    Generates the index files for a gem server directory
    help              Provide help on the 'gem' command
    install           Install a gem into the local repository
    list              Display local gems whose name matches REGEXP
    lock              Generate a lockdown list of gems
    mirror            Mirror all gem files (requires rubygems-mirror)
    open              Open gem sources in editor
    outdated          Display all gems that need updates
    owner             Manage gem owners of a gem on the push server
    pristine          Restores installed gems to pristine condition from files
                      located in the gem cache
    push              Push a gem up to the gem server
    query             Query gem information in local or remote repositories
    rdoc              Generates RDoc for pre-installed gems
    search            Display remote gems whose name matches REGEXP
    server            Documentation and gem repository HTTP server
    sources           Manage the sources and cache file RubyGems uses to search
                      for gems
    specification     Display gem specification (in yaml)
    stale             List gems along with access times
    uninstall         Uninstall gems from the local repository
    unpack            Unpack an installed gem to the current directory
    update            Update installed gems to the latest version
    which             Find the location of a library file you can require
    yank              Remove a pushed gem from the index

For help on a particular command, use 'gem help COMMAND'.

Commands may be abbreviated, so long as they are unambiguous.
e.g. 'gem i rake' is short for 'gem install rake'.

"#;
    const GO_HELP: &str = r#"Go is a tool for managing Go source code.

Usage:

	go <command> [arguments]

The commands are:

	bug         start a bug report
	build       compile packages and dependencies
	clean       remove object files and cached files
	doc         show documentation for package or symbol
	env         print Go environment information
	fix         update packages to use new APIs
	fmt         gofmt (reformat) package sources
	generate    generate Go files by processing source
	get         add dependencies to current module and install them
	install     compile and install packages and dependencies
	list        list packages or modules
	mod         module maintenance
	run         compile and run Go program
	test        test packages
	tool        run specified go tool
	version     print Go version
	vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

	buildconstraint build constraints
	buildmode       build modes
	c               calling between Go and C
	cache           build and test caching
	environment     environment variables
	filetype        file types
	go.mod          the go.mod file
	gopath          GOPATH environment variable
	gopath-get      legacy GOPATH go get
	goproxy         module proxy protocol
	importpath      import path syntax
	modules         modules, module versions, and more
	module-get      module-aware go get
	module-auth     module authentication using go.sum
	module-private  module configuration for non-public modules
	packages        package lists and patterns
	testflag        testing flags
	testfunc        testing functions

Use "go help <topic>" for more information about that topic.

"#;
    const GRUNT_HELP: &str = r#"
Grunt: The JavaScript Task Runner (v0.4.5)

Usage
 grunt [options] [task [task ...]]

Options
    --help, -h  Display this help text.
        --base  Specify an alternate base path. By default, all file paths are
                relative to the Gruntfile. (grunt.file.setBase) *
    --no-color  Disable colored output.
   --gruntfile  Specify an alternate Gruntfile. By default, grunt looks in the
                current or parent directories for the nearest Gruntfile.js or
                Gruntfile.coffee file.
   --debug, -d  Enable debugging mode for tasks that support it.
       --stack  Print a stack trace when exiting with a warning or fatal error.
   --force, -f  A way to force your way past warnings. Want a suggestion? Don't
                use this option, fix your code.
       --tasks  Additional directory paths to scan for task and "extra" files.
                (grunt.loadTasks) *
         --npm  Npm-installed grunt plugins to scan for task and "extra" files.
                (grunt.loadNpmTasks) *
    --no-write  Disable writing files (dry run).
 --verbose, -v  Verbose mode. A lot more information output.
 --version, -V  Print the grunt version. Combine with --verbose for more info.
  --completion  Output shell auto-completion rules. See the grunt-cli
                documentation for more information.

Options marked with * have methods exposed via the grunt API and should instead
be specified inside the Gruntfile wherever possible.

Available tasks
  autoprefixer  Prefix CSS files. *
    concurrent  Run grunt tasks concurrently *
         clean  Clean files and folders. *
       compass  Compile Sass to CSS using Compass *
        concat  Concatenate files. *
       connect  Start a connect web server. *
          copy  Copy files. *
        cssmin  Minify CSS *
       htmlmin  Minify HTML *
      imagemin  Minify PNG, JPEG, GIF and SVG images *
        jshint  Validate files with JSHint. *
        uglify  Minify files with UglifyJS. *
         watch  Run predefined tasks whenever watched files change.
       filerev  File revisioning based on content hashing *
        cdnify  Replace scripts with refs to the Google CDN *
         karma  run karma. *
         newer  Run a task with only those source files that have been modified
                since the last successful run.
     any-newer  DEPRECATED TASK.  Use the "newer" task instead
 newer-postrun  Internal task.
   newer-clean  Remove cached timestamps.
    ngAnnotate  Add, remove and rebuild AngularJS dependency injection
                annotations *
    ngconstant  Dynamic angular constant generator task. *
        svgmin  Minify SVG *
        usemin  Replaces references to non-minified scripts / stylesheets *
 useminPrepare  Using HTML markup as the primary source of information *
       wiredep  Inject Bower components into your source code. *
         serve  Compile then start a connect web server
        server  DEPRECATED TASK. Use the "serve" task instead
          test  Alias for "clean:server", "ngconstant:test", "wiredep",
                "concurrent:test", "autoprefixer", "connect:test", "karma"
                tasks.
         build  Alias for "ngconstant:production", "clean:dist", "wiredep",
                "useminPrepare", "concurrent:dist", "autoprefixer", "concat",
                "ngAnnotate", "copy:dist", "cdnify", "cssmin", "uglify",
                "filerev", "usemin", "htmlmin" tasks.
       default  Alias for "newer:jshint", "test", "build" tasks.

Tasks run in the order specified. Arguments may be passed to tasks that accept
them by using colons, like "lint:files". Tasks marked with * are "multi tasks"
and will iterate over all sub-targets if no argument is specified.

The list of available tasks may change based on tasks directories or grunt
plugins specified in the Gruntfile or via command-line options.

For more information, see http://gruntjs.com/
"#;
    const REACT_NATIVE_HELP: &str = r#"
Scanning 615 folders for symlinks in /home/nvbn/work/zcho/BookkaWebView/node_modules (6ms)

  Usage: react-native [options] [command]


  Options:

    -V, --version  output the version number
    -h, --help     output usage information


  Commands:

    start [options]                    starts the webserver
    run-ios [options]                  builds your app and starts it on iOS simulator
    run-android [options]              builds your app and starts it on a connected Android emulator or device
    new-library [options]              generates a native library bridge
    bundle [options]                   builds the javascript bundle for offline use
    unbundle [options]                 builds javascript as "unbundle" for offline use
    eject [options]                    Re-create the iOS and Android folders and native code
    link [options] [packageName]       links all native dependencies (updates native build files)
    unlink [options] <packageName>     unlink native dependency
    install [options] <packageName>    install and link native dependencies
    uninstall [options] <packageName>  uninstall and unlink native dependencies
    upgrade [options]                  upgrade your app's template files to the latest version; run this after updating the react-native version in your package.json and running npm install
    log-android [options]              starts adb logcat
    log-ios [options]                  starts iOS device syslog tail
"#;
    const NPM_USAGE: &str = r#"
Usage: npm <command>

where <command> is one of:
    access, add-user, adduser, apihelp, author, bin, bugs, c,
    cache, completion, config, ddp, dedupe, deprecate, dist-tag,
    dist-tags, docs, edit, explore, faq, find, find-dupes, get,
    help, help-search, home, i, info, init, install, issues, la,
    link, list, ll, ln, login, logout, ls, outdated, owner,
    pack, ping, prefix, prune, publish, r, rb, rebuild, remove,
    repo, restart, rm, root, run-script, s, se, search, set,
    show, shrinkwrap, star, stars, start, stop, t, tag, team,
    test, tst, un, uninstall, unlink, unpublish, unstar, up,
    update, upgrade, v, verison, version, view, whoami

npm <cmd> -h     quick help on <cmd>
npm -l           display full usage info
npm faq          commonly asked questions
npm help <term>  search for help on <term>
npm help npm     involved overview

Specify configs in the ini-formatted file:
    /home/nvbn/.npmrc
or on the command line via: npm <command> --key value
Config info can be viewed via: npm help config

npm@2.14.7 /opt/node/lib/node_modules/npm
"#;

    /// What the named rule alone offers: nothing unless it matches. Only for
    /// rules whose fixes don't run a program; the others are tested through
    /// the function that takes the program's output.
    fn fixes_of(name: &str, script: &str, output: &str) -> Vec<String> {
        let rule = all()
            .into_iter()
            .find(|rule| rule.name == name)
            .unwrap_or_else(|| panic!("no rule named {name}"));
        let command = Command::new(script, output);
        if (rule.matches)(&command) {
            (rule.fixes)(&command)
        } else {
            Vec::new()
        }
    }

    #[test]
    fn close_matches_order_like_python() {
        // the example in Python's difflib documentation
        assert_eq!(
            close_matches("appel", &["ape", "apple", "peach", "puppy"], 3, 0.6),
            ["apple", "ape"]
        );
        assert_eq!(
            close_matches(
                "pes",
                &["ps", "push", "pause", "attach", "tag", "stats", "images"],
                3,
                0.1
            ),
            ["ps", "push", "pause"]
        );
        // nothing close: no guess, where thefuck would take the first name
        assert_eq!(closest("xyzzy", ADB_COMMANDS), None);
        assert_eq!(
            close_matches::<&str>("a", &[], 3, 0.1),
            Vec::<String>::new()
        );
    }

    #[test]
    fn the_risky_rules_are_the_ones_that_are_off() {
        assert_eq!(
            DISABLED,
            ["docker_image_being_used_by_container", "django_south_ghost"]
        );
        let output = "south.exceptions.GhostMigrations: \n\n ! These migrations are in the database but not on disk:\n ! with the south_migrationhistory table, or pass --delete-ghost-migrations\n";
        assert!(
            crate::rules::fixes_for("./manage.py migrate", output)
                .iter()
                .all(|fix| !fix.contains("--delete-ghost-migrations"))
        );
        let err = "Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1";
        assert!(
            crate::rules::fixes_for("docker image rm -f cd809b04b6ff", err)
                .iter()
                .all(|fix| !fix.contains("container rm"))
        );
    }

    const ADB_USAGE: &str = "Android Debug Bridge version 1.0.31\n\n -d                            - directs command to the only connected USB device\n                                 returns an error if more than one USB device is present.\n -e                            - directs command to the only running emulator.\n                                 returns an error if more than one emulator is running.\n -s <specific device>          - directs command to the device or emulator with the given\n                                 serial number or qualifier. Overrides ANDROID_SERIAL\n                                 environment variable.\n";

    #[test]
    fn adb_fixes_the_command_and_not_a_flag_value() {
        for (script, fix) in [
            (
                "adb puhs test.bin /sdcard/test.bin",
                "adb push test.bin /sdcard/test.bin",
            ),
            ("adb -s 1111 logcta", "adb -s 1111 logcat"),
            (
                "adb -P 666 pulll /sdcard/test.bin",
                "adb -P 666 pull /sdcard/test.bin",
            ),
            ("adb -d logcatt", "adb -d logcat"),
            ("adb -e reboott", "adb -e reboot"),
        ] {
            assert_eq!(
                fixes_of("adb_unknown_command", script, ADB_USAGE),
                [fix],
                "{script}"
            );
        }
        assert!(rule_matches("adb_unknown_command", "adb lgcat", ADB_USAGE));
        assert!(!rule_matches("adb_unknown_command", "git branch foo", ""));
        assert!(!rule_matches("adb_unknown_command", "abd push", ""));
        // no command is close: no guess
        assert!(fixes_of("adb_unknown_command", "adb xyzzy", ADB_USAGE).is_empty());
    }

    #[test]
    fn ag_searches_for_a_literal() {
        let output = "ERR: Bad regex! pcre_compile() failed at position 1: missing )\nIf you meant to search for a literal string, run ag with -Q\n";
        assert_eq!(fixes_of("ag_literal", "ag \\(", output), ["ag -Q \\("]);
        assert!(!rule_matches("ag_literal", "ag foo", ""));
    }

    const AWS_HEAD: &str = "usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]\nTo see help text, you can run:\n\n  aws help\n  aws <command> help\n  aws <command> <subcommand> help\n";

    #[test]
    fn aws_takes_every_suggestion() {
        let misspelled_command = format!(
            "{AWS_HEAD}aws: error: argument command: Invalid choice, valid choices are:\n\ndynamodb                                 | dynamodbstreams\nec2                                      | ecr\n\n\nInvalid choice: 'dynamdb', maybe you meant:\n\n  * dynamodb\n"
        );
        let multiple = format!(
            "{AWS_HEAD}aws: error: argument operation: Invalid choice, valid choices are:\n\ndescribe-table                           | get-item\nlist-tables                              | put-item\n\n\nInvalid choice: 't-item', maybe you meant:\n\n  * put-item\n  * get-item\n"
        );
        let no_suggestions = format!(
            "{AWS_HEAD}aws: error: argument command: Invalid choice, valid choices are:\n\ndynamodb                                 | dynamodbstreams\nec2                                      | ecr\n"
        );
        assert_eq!(
            fixes_of("aws_cli", "aws dynamdb scan", &misspelled_command),
            ["aws dynamodb scan"]
        );
        assert_eq!(
            fixes_of("aws_cli", "aws dynamodb t-item", &multiple),
            ["aws dynamodb put-item", "aws dynamodb get-item"]
        );
        assert!(!rule_matches(
            "aws_cli",
            "aws dynamodb invalid",
            &no_suggestions
        ));
    }

    #[test]
    fn az_takes_the_suggestion() {
        let command = "az: 'providers' is not in the 'az' command group. See 'az --help'.\n\nThe most similar choice to 'providers' is:\n    provider\n";
        let subcommand = "az provider: 'lis' is not in the 'az provider' command group. See 'az provider --help'.\n\nThe most similar choice to 'lis' is:\n    list\n";
        assert_eq!(
            fixes_of("az_cli", "az providers list", command),
            ["az provider list"]
        );
        assert_eq!(
            fixes_of("az_cli", "az provider lis", subcommand),
            ["az provider list"]
        );
        let none = "az provider: error: the following arguments are required: _subcommand\nusage: az provider [-h] {list,show,register,unregister,operation} ...\n";
        assert!(!rule_matches("az_cli", "az provider", none));
    }

    #[test]
    fn cargo_alone_builds() {
        assert_eq!(fixes_of("cargo", "cargo", ""), ["cargo build"]);
        assert!(!rule_matches("cargo", "cargo build", ""));
        assert!(!rule_matches("cargo", "cargo test", ""));
    }

    #[test]
    fn cargo_takes_the_suggestion() {
        let old = "No such subcommand\n\n        Did you mean `build`?\n";
        let new = "error: no such subcommand\n\n\tDid you mean `build`?\n";
        // cargo now says "command"
        let newest = "error: no such command: `buils`\n\n\tDid you mean `build`?\n";
        assert_eq!(
            fixes_of("cargo_no_command", "cargo buid", old),
            ["cargo build"]
        );
        assert_eq!(
            fixes_of("cargo_no_command", "cargo buils", new),
            ["cargo build"]
        );
        assert_eq!(
            fixes_of("cargo_no_command", "cargo buils --release", newest),
            ["cargo build --release"]
        );
        assert!(!rule_matches("cargo_no_command", "cargo build", ""));
        assert!(!rule_matches("cargo_no_command", "npm buid", old));
    }

    #[test]
    fn composer_takes_the_suggestion() {
        let this = "\n\n                                    \n  [InvalidArgumentException]        \n  Command \"udpate\" is not defined.  \n  Did you mean this?                \n      update                        \n                                    \n\n\n";
        let one_of = "\n\n                                   \n  [InvalidArgumentException]       \n  Command \"pdate\" is not defined.  \n  Did you mean one of these?       \n      selfupdate                   \n      self-update                  \n      update                       \n                                   \n\n\n";
        let require = "Invalid argument package. Use \"composer require package\" instead to add packages to your composer.json.";
        assert_eq!(
            fixes_of("composer_not_command", "composer udpate", this),
            ["composer update"]
        );
        assert_eq!(
            fixes_of("composer_not_command", "composer pdate", one_of),
            ["composer selfupdate"]
        );
        assert_eq!(
            fixes_of("composer_not_command", "composer install package", require),
            ["composer require package"]
        );
        assert!(!rule_matches("composer_not_command", "ls update", this));
    }

    #[test]
    fn conda_takes_the_suggestion() {
        let output =
            "\n\nCommandNotFoundError: No command 'conda lst'.\nDid you mean 'conda list'?\n\n    ";
        assert_eq!(
            fixes_of("conda_mistype", "conda lst", output),
            ["conda list"]
        );
        assert!(!rule_matches(
            "conda_mistype",
            "codna list",
            "bash: codna: command not found"
        ));
    }

    #[test]
    fn cpp11_adds_the_standard() {
        // thefuck has no test for this rule: these are the messages it looks for
        let gcc = "In file included from foo.cpp:1:\n/usr/include/c++/4.6/bits/c++0x_warning.h:31:2: error: #error This file requires compiler and library support for the ISO C++ 2011 standard. This support is currently experimental, and must be enabled with the -std=c++11 or -std=gnu++11 compiler options.\n";
        let clang = "foo.cpp:3:5: warning: 'auto' type specifier is a C++11 extension [-Wc++11-extensions]\n";
        assert_eq!(
            fixes_of("cpp11", "g++ foo.cpp", gcc),
            ["g++ foo.cpp -std=c++11"]
        );
        assert_eq!(
            fixes_of("cpp11", "clang++ foo.cpp -o foo", clang),
            ["clang++ foo.cpp -o foo -std=c++11"]
        );
        assert!(!rule_matches("cpp11", "g++ foo.cpp", ""));
        assert!(!rule_matches("cpp11", "gcc foo.c", gcc));
    }

    #[test]
    fn django_south_flags() {
        let ghost = "south.exceptions.GhostMigrations: \n\n ! These migrations are in the database but not on disk:\n    <app1: 0033_auto__...>\n ! I'm not trusting myself; either fix this yourself by fiddling\n ! with the south_migrationhistory table, or pass --delete-ghost-migrations\n ! to South to have it delete ALL of these records (this may not be good).\n";
        let merge = "Running migrations for app:\n ! Migration app:0003_auto... should not have been applied before app:0002_auto__add_field_query_due_date_ but was.\nsouth.exceptions.InconsistentMigrationHistory: Inconsistent migration history\nThe following options are available:\n    --merge: will just attempt the migration ignoring any potential dependency conflicts.\n";
        assert_eq!(
            fixes_of("django_south_ghost", "./manage.py migrate auth", ghost),
            ["./manage.py migrate auth --delete-ghost-migrations"]
        );
        assert_eq!(
            fixes_of("django_south_merge", "./manage.py migrate auth", merge),
            ["./manage.py migrate auth --merge"]
        );
        assert!(rule_matches(
            "django_south_ghost",
            "python manage.py migrate",
            ghost
        ));
        for name in ["django_south_ghost", "django_south_merge"] {
            let output = if name == "django_south_ghost" {
                ghost
            } else {
                merge
            };
            assert!(!rule_matches(name, "./manage.py migrate", ""), "{name}");
            assert!(!rule_matches(name, "app migrate", output), "{name}");
            assert!(!rule_matches(name, "./manage.py test", output), "{name}");
        }
    }

    #[test]
    fn docker_removes_the_container_first() {
        let output = "Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image is being used by running container e5e2591040d1";
        assert_eq!(
            fixes_of(
                "docker_image_being_used_by_container",
                "docker image rm -f cd809b04b6ff",
                output
            ),
            ["docker container rm -f e5e2591040d1 && docker image rm -f cd809b04b6ff"]
        );
        // the id is the last word, wherever the line was wrapped
        let wrapped = "\n        Error response from daemon: conflict: unable to delete cd809b04b6ff (cannot be forced) - image\n        is being used by running container e5e2591040d1\n        ";
        assert!(
            (all()
                .into_iter()
                .find(|rule| rule.name == "docker_image_being_used_by_container")
                .unwrap()
                .fixes)(&Command::new("docker image rm -f cd809b04b6ff", wrapped))
            .iter()
            .all(|fix| fix.starts_with("docker container rm -f e5e2591040d1 && "))
        );
        let name = "docker_image_being_used_by_container";
        assert!(!rule_matches(
            name,
            "docker image rm -f cd809b04b6ff",
            "bash: docker: command not found"
        ));
        assert!(!rule_matches(name, "git image rm -f cd809b04b6ff", output));
    }

    #[test]
    fn docker_logs_in_first() {
        let pull = "Sending build context to Docker daemon  118.8kB\nStep 1/6 : FROM foo/bar:fdb7c6d\npull access denied for foo/bar, repository does not exist or may require 'docker login'\n";
        let push = "The push refers to repository [artifactory:9090/foo/bar]\npush access denied for foo/bar, repository does not exist or may require 'docker login'\n";
        assert_eq!(
            fixes_of(
                "docker_login",
                "docker build -t artifactory:9090/foo/bar:fdb7c6d .",
                pull
            ),
            ["docker login && docker build -t artifactory:9090/foo/bar:fdb7c6d ."]
        );
        assert_eq!(
            fixes_of(
                "docker_login",
                "docker push artifactory:9090/foo/bar:fdb7c6d",
                push
            ),
            ["docker login && docker push artifactory:9090/foo/bar:fdb7c6d"]
        );
        let pushed = "The push refers to repository [artifactory:9090/foo/bar]\n9c29c7ad209d: Preparing\n71f3ad53dfe0: Pushed\ncb95fa0faeb1: Layer already exists\n";
        assert!(!rule_matches(
            "docker_login",
            "docker push artifactory:9090/foo/bar:fdb7c6d",
            pushed
        ));
    }

    fn docker_unknown(command: &str) -> String {
        format!("docker: '{command}' is not a docker command.\nSee 'docker --help'.")
    }

    #[test]
    fn docker_fixes_a_mistyped_command() {
        let fixed = |script: &str, output: &str| {
            docker_not_command_with(&Command::new(script, output), || {
                Some(DOCKER_HELP.to_string())
            })
        };
        assert_eq!(
            fixed("docker swarn", &docker_unknown("swarn")),
            ["docker swarm", "docker start", "docker search"]
        );
        assert_eq!(
            fixed("docker inage", &docker_unknown("inage")),
            ["docker image", "docker images", "docker rename"]
        );
        // `docker swarm int`: docker printed the help of `docker swarm`
        assert_eq!(
            fixed("docker swarm int", DOCKER_SWARM_HELP),
            [
                "docker swarm init",
                "docker swarm join",
                "docker swarm join-token"
            ]
        );
        assert_eq!(
            fixed("docker image la", DOCKER_IMAGE_HELP),
            ["docker image load", "docker image ls", "docker image tag"]
        );
        // `docker swarm` alone has no mistyped subcommand to fix
        assert!(fixed("docker swarm", DOCKER_SWARM_HELP).is_empty());
        // and a plain docker help is not a mistake
        assert!(!rule_matches("docker_not_command", "docker ps", ""));
        assert!(!rule_matches(
            "docker_not_command",
            "cat pes",
            &docker_unknown("pes")
        ));
        assert!(rule_matches(
            "docker_not_command",
            "sudo docker pes",
            &docker_unknown("pes")
        ));
        assert!(rule_matches(
            "docker_not_command",
            "docker swarn",
            &docker_unknown("swarn")
        ));
        assert!(rule_matches(
            "docker_not_command",
            "docker swarm int",
            DOCKER_SWARM_HELP
        ));
    }

    #[test]
    fn docker_help_lists_management_commands_then_the_rest() {
        let commands = docker_commands(DOCKER_HELP);
        assert_eq!(commands[0], "builder");
        assert!(commands.contains(&"swarm".to_string()));
        assert!(commands.contains(&"attach".to_string()));
        assert!(commands.contains(&"wait".to_string()));
        assert!(!commands.contains(&"Options:".to_string()));
    }

    #[test]
    fn fab_fixes_every_task_it_does_not_know() {
        let output = "\nWarning: Command(s) not found:\n    extenson\n    deloyp\n\nAvailable commands:\n\n    update_config\n    prepare_extension\n    Template               A string class for supporting $-substitutions.\n    deploy\n    glob                   Return a list of paths matching a pathname pattern.\n    install_web\n    set_version\n";
        for (script, fix) in [
            ("fab extenson", "fab prepare_extension"),
            (
                "fab extenson:version=2016",
                "fab prepare_extension:version=2016",
            ),
            (
                "fab extenson:version=2016 install_web set_version:val=0.5.0",
                "fab prepare_extension:version=2016 install_web set_version:val=0.5.0",
            ),
            (
                "fab extenson:version=2016 deloyp:beta=true -H the.fuck",
                "fab prepare_extension:version=2016 deploy:beta=true -H the.fuck",
            ),
        ] {
            assert_eq!(fixes_of("fab_command_not_found", script, output), [fix]);
        }
        assert!(rule_matches(
            "fab_command_not_found",
            "fab extenson deloyp",
            output
        ));
        assert!(!rule_matches(
            "fab_command_not_found",
            "gulp extenson",
            output
        ));
        assert!(!rule_matches("fab_command_not_found", "fab deloyp", ""));
    }

    #[test]
    fn gem_fixes_a_mistyped_command() {
        let output = |command: &str| {
            format!(
                "\nERROR:  While executing gem ... (Gem::CommandLineError)\n    Unknown command {command}\n"
            )
        };
        let first = |script: &str, command: &str| {
            gem_unknown_command(&Command::new(script, &output(command)), Some(GEM_HELP))
        };
        assert_eq!(
            first("gem isntall jekyll", "isntall")[0],
            "gem install jekyll"
        );
        assert_eq!(first("gem last --local", "last")[0], "gem list --local");
        // a wrapped description line is not a command
        assert!(!gem_commands(GEM_HELP).contains(&"located".to_string()));
        assert!(gem_commands(GEM_HELP).contains(&"pristine".to_string()));
        assert!(rule_matches(
            "gem_unknown_command",
            "gem isntall jekyll",
            &output("isntall")
        ));
        assert!(!rule_matches(
            "gem_unknown_command",
            "gem install jekyll",
            ""
        ));
        assert!(!rule_matches(
            "gem_unknown_command",
            "git log",
            &output("log")
        ));
        // without the list of commands there is nothing to fix with
        assert!(
            gem_unknown_command(&Command::new("gem isntall x", &output("isntall")), None)
                .is_empty()
        );
    }

    #[test]
    fn go_fixes_a_mistyped_command() {
        let output = "go bulid: unknown command\nRun 'go help' for usage.";
        let fixes = go_unknown_command(&Command::new("go bulid", output), Some(GO_HELP));
        assert_eq!(fixes, ["go build"]);
        let commands = go_commands(GO_HELP);
        assert_eq!(commands.first().map(String::as_str), Some("bug"));
        assert_eq!(commands.last().map(String::as_str), Some("vet"));
        assert!(rule_matches("go_unknown_command", "go bulid", output));
        assert!(!rule_matches(
            "go_unknown_command",
            "go run",
            "go run: no go files listed"
        ));
    }

    #[test]
    fn gradle_fixes_a_mistyped_task() {
        // thefuck has no test for these two rules; this is what gradle prints
        let tasks = "\n> Task :tasks\n\n------------------------------------------------------------\nTasks runnable from root project 'demo'\n------------------------------------------------------------\n\nBuild tasks\n-----------\nassemble - Assembles the outputs of this project.\nbuild - Assembles and tests this project.\nclean - Deletes the build directory.\n\nVerification tasks\n------------------\ncheck - Runs all checks.\ntest - Runs the test suite.\n\nBUILD SUCCESSFUL in 1s\n";
        assert_eq!(
            gradle_tasks(tasks),
            ["assemble", "build", "clean", "check", "test"]
        );
        let output = "FAILURE: Build failed with an exception.\n\n* What went wrong:\nTask 'tets' not found in root project 'demo'.\n";
        let wrong = GRADLE_TASK.captures(output).unwrap();
        assert_eq!(&wrong[1], "tets");
        assert_eq!(
            replace_command("gradle tets", &wrong[1], &gradle_tasks(tasks))[0],
            "gradle test"
        );
        assert!(rule_matches("gradle_no_task", "gradle tets", output));
        assert!(rule_matches("gradle_no_task", "./gradlew tets", output));
        assert!(!rule_matches(
            "gradle_no_task",
            "gradle test",
            "BUILD SUCCESSFUL"
        ));
        assert!(!rule_matches("gradle_no_task", "make tets", output));
    }

    #[test]
    fn gradle_wrapper_stands_in_for_a_missing_gradle() {
        let not_found = Command::new("gradle tasks", "gradle: not found");
        assert!(gradle_wrapper_applies(&not_found, false, true));
        assert!(!gradle_wrapper_applies(&not_found, false, false));
        assert!(!gradle_wrapper_applies(&not_found, true, true));
        assert!(!gradle_wrapper_applies(
            &Command::new("npm tasks", "npm: not found"),
            false,
            true
        ));
        assert!(!gradle_wrapper_applies(
            &Command::new("gradle tasks", "command not found"),
            true,
            true
        ));
    }

    #[test]
    fn grunt_fixes_a_mistyped_task() {
        let output = |task: &str| {
            format!(
                "\nWarning: Task \"{task}\" not found. Use --force to continue.\n\nAborted due to warnings.\n"
            )
        };
        let first = |script: &str, task: &str| {
            grunt_task_not_found(&Command::new(script, &output(task)), Some(GRUNT_HELP))
        };
        assert_eq!(first("grunt defualt", "defualt"), ["grunt default"]);
        assert_eq!(
            first("grunt cmpass:all", "cmpass:all"),
            ["grunt compass:all"]
        );
        assert_eq!(
            first("grunt cmpass:all --color", "cmpass:all"),
            ["grunt compass:all --color"]
        );
        assert!(grunt_tasks(GRUNT_HELP).contains(&"autoprefixer".to_string()));
        assert!(grunt_tasks(GRUNT_HELP).contains(&"default".to_string()));
        // the options listed before the tasks are not tasks
        assert!(!grunt_tasks(GRUNT_HELP).contains(&"--help,".to_string()));
        assert!(rule_matches(
            "grunt_task_not_found",
            "grunt defualt",
            &output("defualt")
        ));
        assert!(rule_matches(
            "grunt_task_not_found",
            "grunt buld:css",
            &output("buld:css")
        ));
        assert!(!rule_matches(
            "grunt_task_not_found",
            "npm nuild",
            &output("nuild")
        ));
        assert!(!rule_matches("grunt_task_not_found", "grunt rm", ""));
    }

    #[test]
    fn gulp_fixes_a_mistyped_task() {
        let output = "[00:41:11] Using gulpfile gulpfile.js\n[00:41:11] Task 'srve' is not in your gulpfile\n[00:41:11] Please check the documentation for proper gulpfile formatting\n";
        assert_eq!(
            gulp_not_task(
                &Command::new("gulp srve", output),
                Some("serve \nbuild \ndefault \n")
            ),
            ["gulp serve", "gulp default"]
        );
        assert!(rule_matches("gulp_not_task", "gulp srve", output));
        assert!(!rule_matches("gulp_not_task", "gulp serve", ""));
        assert!(!rule_matches("gulp_not_task", "cat srve", output));
    }

    #[test]
    fn heroku_names_the_app() {
        let output = "\n ▸    Multiple apps in git remotes\n ▸    Usage: --remote heroku-dev\n ▸    or: --app myapp-dev\n ▸    Your local git repository has more than 1 app referenced in git remotes.\n ▸    Because of this, we can't determine which app you want to run this command against.\n ▸    Specify the app you want with --app or --remote.\n ▸    Heroku remotes in repo:\n ▸    myapp (heroku)\n ▸    myapp-dev (heroku-dev)\n ▸\n ▸    https://devcenter.heroku.com/articles/multiple-environments\n";
        assert_eq!(
            fixes_of("heroku_multiple_apps", "heroku pg", output),
            ["heroku pg --app myapp", "heroku pg --app myapp-dev"]
        );
        let pg = "\n=== HEROKU_POSTGRESQL_TEAL_URL, DATABASE_URL\nPlan:                  Hobby-basic\nStatus:                Available\nRows:                  12345/10000000 (In compliance)\n";
        assert!(!rule_matches("heroku_multiple_apps", "heroku pg", pg));
    }

    #[test]
    fn heroku_runs_the_command_it_meant() {
        let output = "\n ▸    log is not a heroku command.\n ▸    Perhaps you meant logs?\n ▸    Run heroku _ to run heroku logs.\n ▸    Run heroku help for a list of available commands.";
        assert_eq!(
            fixes_of("heroku_not_command", "heroku log", output),
            ["heroku logs"]
        );
        assert!(!rule_matches("heroku_not_command", "cat log", output));
    }

    #[test]
    fn lein_takes_the_suggestions() {
        let output = "'rpl' is not a task. See 'lein help'.\n\nDid you mean this?\n         repl\n         jar\n";
        assert_eq!(
            fixes_of("lein_not_task", "lein rpl --help", output),
            ["lein repl --help", "lein jar --help"]
        );
        assert_eq!(
            fixes_of("lein_not_task", "sudo lein rpl", output),
            ["sudo lein repl", "sudo lein jar"]
        );
        assert!(!rule_matches("lein_not_task", "ls", output));
    }

    const MVN_PHASE_LIST: &str = "Available lifecycle phases are: validate, initialize, generate-sources, process-sources, generate-resources, process-resources, compile, process-classes, generate-test-sources, process-test-sources, generate-test-resources, process-test-resources, test-compile, process-test-classes, test, prepare-package, package, pre-integration-test, integration-test, post-integration-test, verify, install, deploy, pre-clean, clean, post-clean, pre-site, site, post-site, site-deploy. -> [Help 1]";
    const MVN_FORMAT: &str = "You must specify a valid lifecycle phase or a goal in the format <plugin-prefix>:<goal> or <plugin-group-id>:<plugin-artifact-id>[:<plugin-version>]:<goal>.";

    #[test]
    fn mvn_gives_a_goal_to_a_bare_command() {
        let output = format!(
            "[ERROR] No goals have been specified for this build. {MVN_FORMAT} {MVN_PHASE_LIST}"
        );
        assert_eq!(
            fixes_of("mvn_no_command", "mvn", &output),
            ["mvn clean package", "mvn clean install"]
        );
        assert_eq!(
            fixes_of("mvn_no_command", "mvn -N", &output),
            ["mvn -N clean package", "mvn -N clean install"]
        );
        let built =
            "[INFO] Scanning for projects...\n[INFO] BUILD SUCCESS\n[INFO] Total time: 0.477s\n";
        assert!(!rule_matches("mvn_no_command", "mvn clean", built));
        assert!(!rule_matches("mvn_no_command", "mvn --help", ""));
        assert!(!rule_matches("mvn_no_command", "mvn -v", ""));
    }

    #[test]
    fn mvn_fixes_a_mistyped_phase() {
        let output = |phase: &str| {
            format!("[ERROR] Unknown lifecycle phase \"{phase}\". {MVN_FORMAT} {MVN_PHASE_LIST}")
        };
        assert_eq!(
            fixes_of("mvn_unknown_lifecycle_phase", "mvn cle", &output("cle")),
            ["mvn clean", "mvn compile"]
        );
        assert_eq!(
            fixes_of(
                "mvn_unknown_lifecycle_phase",
                "mvn claen package",
                &output("claen")
            ),
            ["mvn clean package"]
        );
        assert!(!rule_matches(
            "mvn_unknown_lifecycle_phase",
            "mvn clean",
            "[INFO] BUILD SUCCESS"
        ));
    }

    #[test]
    fn npm_fixes_a_mistyped_script_name() {
        let output = |script: &str| {
            format!(
                "\nnpm ERR! Linux 4.4.0-31-generic\nnpm ERR! argv \"/opt/node/bin/node\" \"/opt/node/bin/npm\" \"run\" \"dvelop\"\nnpm ERR! node v4.4.7\nnpm ERR! npm  v2.15.8\n\nnpm ERR! missing script: {script}\nnpm ERR!\nnpm ERR! If you need help, you may report this error at:\n"
            )
        };
        let json = "{\n  \"name\": \"code-view-web\",\n  \"scripts\": {\n    \"test\": \"jest\",\n    \"build\": \"cp node_modules/ace-builds/src-min/ -a resources/ace/ && webpack\",\n    \"develop\": \"webpack-dev-server --progress --colors\",\n    \"watch-test\": \"jest --verbose --watch\"\n  }\n}";
        let scripts = package_scripts(json);
        assert_eq!(scripts, ["test", "build", "develop", "watch-test"]);
        let first = |script: &str, wrong: &str| {
            npm_missing_script(&Command::new(script, &output(wrong)), &scripts)[0].clone()
        };
        assert_eq!(
            first("npm ru wach-tests", "wach-tests"),
            "npm ru watch-test"
        );
        assert_eq!(
            first("npm -i run-script dvelop", "dvelop"),
            "npm -i run-script develop"
        );
        assert_eq!(
            first("npm -i run-script buld -X POST", "buld"),
            "npm -i run-script build -X POST"
        );
        // npm 7 and later
        let modern = "npm error Missing script: \"dvelop\"\nnpm error\nnpm error To see a list of scripts, run:\nnpm error   npm run\n";
        assert_eq!(
            npm_missing_script(&Command::new("npm run dvelop", modern), &scripts)[0],
            "npm run develop"
        );
        assert!(rule_matches(
            "npm_missing_script",
            "npm ru wach",
            &output("wach")
        ));
        assert!(rule_matches(
            "npm_missing_script",
            "npm run live-tes",
            &output("live-tes")
        ));
        assert!(rule_matches(
            "npm_missing_script",
            "npm run-script sahare",
            &output("sahare")
        ));
        assert!(!rule_matches(
            "npm_missing_script",
            "npm wach",
            &output("wach")
        ));
        assert!(!rule_matches(
            "npm_missing_script",
            "vim live-tes",
            &output("live-tes")
        ));
        assert!(!rule_matches(
            "npm_missing_script",
            "npm run-script sahare",
            ""
        ));
    }

    #[test]
    fn npm_fixes_a_mistyped_command() {
        for (script, fix) in [
            ("npm urgrade", "npm upgrade"),
            ("npm -g isntall gulp", "npm -g install gulp"),
            ("npm isntall -g gulp", "npm install -g gulp"),
            ("sudo npm isntall -g gulp", "sudo npm install -g gulp"),
        ] {
            assert_eq!(
                fixes_of("npm_wrong_command", script, NPM_USAGE),
                [fix],
                "{script}"
            );
        }
        for script in [
            "npm urgrdae",
            "npm urgrade -g",
            "npm -f urgrade -g",
            "npm urg",
        ] {
            assert!(
                rule_matches("npm_wrong_command", script, NPM_USAGE),
                "{script}"
            );
        }
        for (script, output) in [
            ("npm urgrade", ""),
            ("npm", NPM_USAGE),
            ("test urgrade", NPM_USAGE),
            ("npm -e", NPM_USAGE),
        ] {
            assert!(
                !rule_matches("npm_wrong_command", script, output),
                "{script}"
            );
        }
        // nothing is close to it: no guess
        assert!(fixes_of("npm_wrong_command", "npm xyzzyx", NPM_USAGE).is_empty());
    }

    #[test]
    fn php_serves_with_a_capital_s() {
        assert_eq!(
            fixes_of("php_s", "php -s localhost:8000", ""),
            ["php -S localhost:8000"]
        );
        assert_eq!(
            fixes_of("php_s", "php -t pub -s 0.0.0.0:8080", ""),
            ["php -t pub -S 0.0.0.0:8080"]
        );
        assert!(!rule_matches("php_s", "php -S localhost:8000", ""));
        assert!(!rule_matches("php_s", "vim php -s", ""));
    }

    #[test]
    fn pip_takes_the_suggestion() {
        let output = "ERROR: unknown command \"instatl\" - maybe you meant \"install\"";
        assert_eq!(
            fixes_of("pip_unknown_command", "pip instatl", output),
            ["pip install"]
        );
        let plus = "ERROR: unknown command \"un+install\" - maybe you meant \"uninstall\"";
        assert_eq!(
            fixes_of("pip_unknown_command", "pip un+install thefuck", plus),
            ["pip uninstall thefuck"]
        );
        assert!(!rule_matches(
            "pip_unknown_command",
            "pip i",
            "ERROR: unknown command \"i\""
        ));
    }

    #[test]
    fn python_runs_a_script_with_python() {
        assert_eq!(
            fixes_of("python_command", "./test_sudo.py", "Permission denied"),
            ["python ./test_sudo.py"]
        );
        assert_eq!(
            fixes_of(
                "python_command",
                "sudo temp.py",
                "sh: temp.py: command not found"
            ),
            ["sudo python temp.py"]
        );
        assert!(rule_matches(
            "python_command",
            "temp.py",
            "Permission denied"
        ));
        assert!(!rule_matches("python_command", "temp.py", ""));
        assert!(!rule_matches("python_command", "ls", "Permission denied"));
    }

    #[test]
    fn python_installs_a_missing_module() {
        let output = |file: &str, module: &str| {
            format!(
                "Traceback (most recent call last):\n  File \"{file}\", line 1, in <module>\n    import {module}\nModuleNotFoundError: No module named '{module}'"
            )
        };
        assert_eq!(
            fixes_of(
                "python_module_error",
                "python some_script.py",
                &output("some_script.py", "more_itertools")
            ),
            ["pip install more_itertools && python some_script.py"]
        );
        assert_eq!(
            fixes_of(
                "python_module_error",
                "./some_other_script.py",
                &output("some_other_script.py", "a_module")
            ),
            ["pip install a_module && ./some_other_script.py"]
        );
        assert!(!rule_matches(
            "python_module_error",
            "python hello_world.py",
            "Hello World"
        ));
        let name_error = "Traceback (most recent call last):\n  File \"hello_world.py\", line 1, in <module>\n    pritn(\"Hello World\")\nNameError: name 'pritn' is not defined";
        assert!(!rule_matches(
            "python_module_error",
            "./hello_world.py",
            name_error
        ));
    }

    #[test]
    fn rails_runs_the_pending_migrations() {
        let development = "\nMigrations are pending. To resolve this issue, run:\n\n        rails db:migrate RAILS_ENV=development\n";
        let test = "\nMigrations are pending. To resolve this issue, run:\n\n        bin/rails db:migrate RAILS_ENV=test\n";
        assert_eq!(
            fixes_of("rails_migrations_pending", "bin/rspec", development),
            ["rails db:migrate RAILS_ENV=development && bin/rspec"]
        );
        assert_eq!(
            fixes_of("rails_migrations_pending", "bin/rspec", test),
            ["bin/rails db:migrate RAILS_ENV=test && bin/rspec"]
        );
        assert!(!rule_matches(
            "rails_migrations_pending",
            "bin/rspec",
            "Environment data not found in the schema. To resolve this issue, run: \n\n"
        ));
    }

    #[test]
    fn react_native_fixes_a_mistyped_command() {
        let output = |command: &str| format!("Unrecognized command '{command}'");
        let first = |script: &str, wrong: &str| {
            react_native_command_unrecognized(
                &Command::new(script, &output(wrong)),
                Some(REACT_NATIVE_HELP),
            )[0]
            .clone()
        };
        assert_eq!(first("react-native star", "star"), "react-native start");
        assert_eq!(
            first("react-native logsandroid -f", "logsandroid"),
            "react-native log-android -f"
        );
        assert!(react_native_commands(REACT_NATIVE_HELP).contains(&"run-ios".to_string()));
        assert!(rule_matches(
            "react_native_command_unrecognized",
            "react-native star",
            &output("star")
        ));
        assert!(rule_matches(
            "react_native_command_unrecognized",
            "react-native android-logs",
            &output("android-logs")
        ));
        assert!(!rule_matches(
            "react_native_command_unrecognized",
            "gradle star",
            &output("star")
        ));
        assert!(!rule_matches(
            "react_native_command_unrecognized",
            "react-native start",
            ""
        ));
    }

    #[test]
    fn terraform_inits_first() {
        for (script, output) in [
            (
                "terraform plan",
                "Error: Initialization required. Please see the error message above.",
            ),
            (
                "terraform plan",
                "This module is not yet installed. Run \"terraform init\" to install all modules required by this configuration.",
            ),
            (
                "terraform apply",
                "Error: Initialization required. Please see the error message above.",
            ),
        ] {
            assert_eq!(
                fixes_of("terraform_init", script, output),
                [format!("terraform init && {script}")]
            );
        }
        for (script, output) in [
            ("terraform --version", "Terraform v0.12.2"),
            (
                "terraform plan",
                "No changes. Infrastructure is up-to-date.",
            ),
            (
                "terraform apply",
                "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.",
            ),
        ] {
            assert!(!rule_matches("terraform_init", script, output));
            assert!(!rule_matches("terraform_no_command", script, output));
        }
    }

    #[test]
    fn terraform_takes_the_suggestion() {
        assert_eq!(
            fixes_of(
                "terraform_no_command",
                "terraform appyl",
                "Terraform has no command named \"appyl\". Did you mean \"apply\"?"
            ),
            ["terraform apply"]
        );
        assert_eq!(
            fixes_of(
                "terraform_no_command",
                "terraform destory --some-other-option",
                "Terraform has no command named \"destory\". Did you mean \"destroy\"?"
            ),
            ["terraform destroy --some-other-option"]
        );
    }

    #[test]
    fn tsuru_logs_in_first() {
        let expired = "Error: you're not authenticated or your session has expired.";
        let again = "You're not authenticated or your session has expired. Please use \"login\" command for authentication.";
        assert_eq!(
            fixes_of("tsuru_login", "tsuru app-shell", expired),
            ["tsuru login && tsuru app-shell"]
        );
        assert_eq!(
            fixes_of("tsuru_login", "tsuru app-log -f", again),
            ["tsuru login && tsuru app-log -f"]
        );
        assert!(!rule_matches("tsuru_login", "tsuru", ""));
        assert!(!rule_matches(
            "tsuru_login",
            "tsuru app-restart",
            "Error: unauthorized"
        ));
        assert!(!rule_matches(
            "tsuru_login",
            "tsuru app-log -f",
            "Error: unparseable data"
        ));
    }

    #[test]
    fn tsuru_takes_the_suggestions() {
        let output = |command: &str, names: &[&str]| {
            let mut text = format!(
                "tsuru: \"{command}\" is not a tsuru command. See \"tsuru help\".\n\nDid you mean?\n"
            );
            for name in names {
                text.push_str(&format!("\t{name}\n"));
            }
            text
        };
        assert_eq!(
            fixes_of(
                "tsuru_not_command",
                "tsuru log",
                &output("log", &["app-log", "login", "logout"])
            ),
            ["tsuru login", "tsuru logout", "tsuru app-log"]
        );
        assert_eq!(
            fixes_of(
                "tsuru_not_command",
                "tsuru app-l",
                &output("app-l", &["app-list", "app-log"])
            ),
            ["tsuru app-log", "tsuru app-list"]
        );
        assert_eq!(
            fixes_of(
                "tsuru_not_command",
                "tsuru user-list",
                &output("user-list", &["team-user-list"])
            ),
            ["tsuru team-user-list"]
        );
        // no suggestions, or not a tsuru command at all
        assert!(!rule_matches(
            "tsuru_not_command",
            "tsuru tchururu",
            &output("tchururu", &[])
        ));
        assert!(!rule_matches(
            "tsuru_not_command",
            "tsuru version",
            "tsuru version 0.16.0."
        ));
        assert!(!rule_matches(
            "tsuru_not_command",
            "tsuru env-get",
            "Error: App thefuck not found."
        ));
    }

    #[test]
    fn vagrant_starts_the_machine_first() {
        let ssh = "VM must be running to open SSH connection. Run `vagrant up`\nto start the virtual machine.";
        let rdp = "VM must be created before running this command. Run `vagrant up` first.";
        assert_eq!(
            fixes_of("vagrant_up", "vagrant ssh", ssh),
            ["vagrant up && vagrant ssh"]
        );
        assert_eq!(
            fixes_of("vagrant_up", "vagrant ssh devbox", ssh),
            [
                "vagrant up devbox && vagrant ssh devbox",
                "vagrant up && vagrant ssh devbox"
            ]
        );
        assert_eq!(
            fixes_of("vagrant_up", "vagrant rdp", rdp),
            ["vagrant up && vagrant rdp"]
        );
        assert!(!rule_matches("vagrant_up", "vagrant ssh", ""));
        assert!(!rule_matches(
            "vagrant_up",
            "vagrant ssh jeff",
            "The machine with the name 'jeff' was not found configured for this Vagrant environment."
        ));
    }

    #[test]
    fn workon_fixes_a_mistyped_environment_or_creates_it() {
        let envs = vec!["thefuck".to_string(), "code_view".to_string()];
        let first = |script: &str| workon_fixes(&Command::new(script, ""), &envs)[0].clone();
        assert_eq!(first("workon tehfuck"), "workon thefuck");
        assert_eq!(first("workon code-view"), "workon code_view");
        assert_eq!(first("workon zzzz"), "mkvirtualenv zzzz");
        // with no environments at all the only offer is to create it
        assert_eq!(
            workon_fixes(&Command::new("workon web", ""), &[]),
            ["mkvirtualenv web"]
        );
        for script in ["workon tehfuck", "workon code-view", "workon new-env"] {
            assert!(workon_applies(&Command::new(script, ""), &envs), "{script}");
        }
        for script in [
            "workon thefuck",
            "workon code_view",
            "work on tehfuck",
            "workon",
        ] {
            assert!(
                !workon_applies(&Command::new(script, ""), &envs),
                "{script}"
            );
        }
    }

    #[test]
    fn virtualenvs_are_the_directories() {
        let dir = tempfile::tempdir().unwrap();
        std::fs::create_dir(dir.path().join("web")).unwrap();
        std::fs::create_dir(dir.path().join("api")).unwrap();
        std::fs::write(dir.path().join("postactivate"), "").unwrap();
        assert_eq!(virtualenvs(dir.path()), ["api", "web"]);
        assert!(virtualenvs(&dir.path().join("missing")).is_empty());
    }

    #[test]
    fn yarn_takes_the_suggestion() {
        for (script, output, fix) in [
            (
                "yarn rm",
                "error Did you mean `yarn remove`?",
                "yarn remove",
            ),
            (
                "yarn etil",
                "error Command \"etil\" not found. Did you mean \"etl\"?",
                "yarn etl",
            ),
            ("yarn ls", "error Did you mean `yarn list`?", "yarn list"),
        ] {
            assert_eq!(fixes_of("yarn_alias", script, output), [fix], "{script}");
        }
        assert!(!rule_matches(
            "yarn_alias",
            "yarn",
            "error Did you mean `yarn list`?"
        ));
        assert!(!rule_matches("yarn_alias", "yarn list", ""));
    }

    #[test]
    fn yarn_runs_the_command_that_replaced_the_old_one() {
        let output = |package: &str| {
            format!(
                "error `install` has been replaced with `add` to add new dependencies. Run \"yarn add {package}\" instead."
            )
        };
        assert_eq!(
            fixes_of(
                "yarn_command_replaced",
                "yarn install redux",
                &output("redux")
            ),
            ["yarn add redux"]
        );
        assert_eq!(
            fixes_of(
                "yarn_command_replaced",
                "yarn install lodash",
                &output("lodash")
            ),
            ["yarn add lodash"]
        );
        assert!(!rule_matches("yarn_command_replaced", "yarn install", ""));
    }

    #[test]
    fn yarn_help_opens_the_docs() {
        let output = "\n  Usage: yarn [command] [flags]\n\n  Options:\n\n    -h, --help                      output usage information\n    --network-concurrency <number>  maximum number of concurrent network requests\n\n  Visit https://yarnpkg.com/en/docs/cli/clean for documentation about this command.\n";
        let url = "https://yarnpkg.com/en/docs/cli/clean";
        let fixes = fixes_of("yarn_help", "yarn help clean", output);
        assert!(fixes == [format!("xdg-open {url}")] || fixes == [format!("open {url}")]);
        assert_eq!(open_command(url, false), format!("open {url}"));
        assert_eq!(open_command(url, true), format!("xdg-open {url}"));
        assert!(!rule_matches("yarn_help", "yarn help", output));
        assert!(!rule_matches("yarn_help", "yarn help clean", ""));
    }
}