Source file opamArg.ml

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
(**************************************************************************)
(*                                                                        *)
(*    Copyright 2012-2020 OCamlPro                                        *)
(*    Copyright 2012 INRIA                                                *)
(*                                                                        *)
(*  All rights reserved. This file is distributed under the terms of the  *)
(*  GNU Lesser General Public License version 2.1, with the special       *)
(*  exception on linking described in the file LICENSE.                   *)
(*                                                                        *)
(**************************************************************************)

open OpamTypes
open OpamTypesBase
open Cmdliner
open OpamStd.Op

include OpamArgTools


(** Utils *)

let when_enum =
  [ "always", `Always; "never", `Never; "auto", `Auto ]
  |> List.map (fun (s,v) -> cli_original, s, v)

let confirm_enum = [
  cli_from cli2_1, "ask", `ask;
  cli_from cli2_1, "no", `all_no;
  cli_from cli2_1, "yes", `all_yes;
  cli_from cli2_1, "unsafe-yes", `unsafe_yes;
]

(* Windows directory separators need to be escaped for manpages *)
let dir_sep, escape_path =
  match Filename.dir_sep with
  | "\\" ->
    let esc = "\\\\" in
    esc,
    fun p ->
      OpamStd.List.concat_map esc (fun x -> x)
        (OpamStd.String.split_delim p '\\')
  | ds -> ds, fun x -> x


(** Opam environment variables *)

(* Environment variables that need to be initialised before config init, see
   [OpamCliMain.run]. *)
let preinit_environment_variables =
  let open OpamStd.Config in
  let core =
    let open OpamCoreConfig.E in [
      "DEBUG", (fun v -> DEBUG (env_int v)),
      "see options `--debug' and `--debug-level'.";
      "YES", (fun v -> YES (env_bool v)),
      "see options `--yes' and `--confirm-level`. \
       $(b,OPAMYES) has has priority over $(b,OPAMNO) and is \
       ignored if $(b,OPAMCONFIRMLEVEL) is set.";
    ] in
  let client =
    let open OpamClientConfig.E in [
      "CLI", (fun v -> CLI (env_string v)), "see option `--cli'.";
      "NOSELFUPGRADE",(fun v -> NOSELFUPGRADE (env_string v)),
      "see option `--no-self-upgrade'";
      "ROOTISOK", (fun v -> ROOTISOK (env_bool v)),
      "don't complain when running as root.";
    ] in
  core @ client

let preinit_opam_env_variables, doc_opam_env_variables_pre =
  let preinit () =
    OpamStd.Config.E.updates @@
    List.map (fun (var, cons, _doc) -> cons var)
      preinit_environment_variables
  in
  let doc =
    List.map (fun (var, _cons, doc) ->
        `P (Printf.sprintf "$(i,OPAM%s) %s" var doc))
      preinit_environment_variables
  in
  preinit, doc

(* Environment variables with their doc and their validity OPAMVAR_var and
   OPAMPACKAGE_var are defined and documented static in [help_sections].
*)
let environment_variables =
  let open OpamStd.Config in
  let core =
    let open OpamCoreConfig.E in [
      "COLOR", cli_original, (fun v -> COLOR (env_when v)),
      "when set to $(i,always) or $(i,never), sets a default value for the \
       `--color' option.";
      "CONFIRMLEVEL", cli_from cli2_1, (fun v -> CONFIRMLEVEL (env_answer v)),
      "see option `--confirm-level`. \
       $(b,OPAMCONFIRMLEVEL) has priority over $(b,OPAMYES) \
       and $(b,OPAMNO).";
      "DEBUGSECTIONS", cli_from cli2_1, (fun v -> DEBUGSECTIONS (env_sections v)),
      "if set, limits debug messages to the space-separated list of \
       sections. Sections can optionally have a specific debug level (for \
       example, $(b,CLIENT:2) or $(b,CLIENT CUDF:2)), but otherwise use \
       `--debug-level'.";
      "ERRLOGLEN", cli_original, (fun v -> ERRLOGLEN (env_int v)),
      "sets the number of log lines printed when a sub-process fails. 0 to \
       print all.";
      "KEEPLOGS", cli_original, (fun v -> KEEPLOGS (env_bool v)),
      "tells opam to not remove some temporary command logs and some \
       backups. This skips some finalisers and may also help to get more \
       reliable backtraces.";
      "LOGS", cli_original, (fun v -> LOGS (env_string v)),
      ("$(i,logdir) sets log directory, default is a temporary directory in \
       " ^ (if Sys.win32 then "%TEMP%" else "/tmp"));
      "MERGEOUT", cli_original, (fun v -> MERGEOUT (env_bool v)),
      "merge process outputs, stderr on stdout.";
      "NO", cli_original, (fun v -> NO (env_bool v)),
      "answer no to any question asked, see options `--no` and `--confirm-level`. \
       $(b,OPAMNO) is ignored if either $(b,OPAMCONFIRMLEVEL) or $(b,OPAMYES) \
       is set.";
      "PRECISETRACKING", cli_original, (fun v -> PRECISETRACKING (env_bool v)),
      "fine grain tracking of directories.";
      "SAFE", cli_original, (fun v -> SAFE (env_bool v)),
      "see option `--safe'.";
      "STATUSLINE", cli_original, (fun v -> STATUSLINE (env_when v)),
      ("display a dynamic status line showing what's currently going on on \
        the terminal. (one of "^string_of_enum when_enum^")");
      "USEOPENSSL", cli_original, (fun v -> USEOPENSSL (env_bool v)),
      "force openssl use for hash computing.";
      "UTF8", cli_original, (fun v -> UTF8 (env_when_ext v)),
      (Printf.sprintf "use UTF8 characters in output (one of %s). By default \
                       `auto', which is determined from the locale)."
         (string_of_enum when_enum));
      "UTF8MSGS", cli_original, (fun v -> UTF8MSGS (env_bool v)),
      "use extended UTF8 characters (camels) in opam messages. Implies \
       $(i,OPAMUTF8). This is set by default on OSX only.";
      "VERBOSE", cli_original, (fun v -> VERBOSE (env_level v)),
      "see option `--verbose'.";
    ] in
  let format =
    let open OpamFormatConfig.E in [
      "ALLPARENS", cli_original, (fun v -> ALLPARENS (env_bool v)),
      "surround all filters with parenthesis.";
      "SKIPVERSIONCHECKS", cli_original,
      (fun v -> SKIPVERSIONCHECKS (env_bool v)),
      "bypasses some version checks. Unsafe, for compatibility testing only.";
      "STRICT", cli_original, (fun v -> STRICT (env_bool v)),
      "fail on inconsistencies (file reading, switch import, etc.).";
    ] in
  let solver =
    let open OpamSolverConfig.E in [
      "BESTEFFORT", cli_original, (fun v -> BESTEFFORT (env_bool v)),
      "see option `--best-effort'.";
      "BESTEFFORTPREFIXCRITERIA", cli_original,
      (fun v -> BESTEFFORTPREFIXCRITERIA (env_string v)),
      "sets the string that must be prepended to the criteria when the \
       `--best-effort' option is set, and is expected to maximise the \
       `opam-query' property in the solution.";
      "CRITERIA", cli_original, (fun v -> CRITERIA (env_string v)),
      "specifies user $(i,preferences) for dependency solving. The default \
       value depends on the solver version, use `config report' to know the \
       current setting. See also option --criteria.";
      "CUDFFILE", cli_original, (fun v -> CUDFFILE (env_string v)),
      "save the cudf graph to $(i,file)-actions-explicit.dot.";
      "CUDFTRIM", cli_original, (fun v -> CUDFTRIM (env_string v)),
      "controls the filtering of unrelated packages during CUDF preprocessing.";
      "DIGDEPTH", cli_original, (fun v -> DIGDEPTH (env_int v)),
      "defines how aggressive the lookup for conflicts during CUDF \
       preprocessing is.";
      "EXTERNALSOLVER", cli_original, (fun v -> EXTERNALSOLVER (env_string v)),
      "see option `--solver'.";
      "FIXUPCRITERIA", cli_original, (fun v -> FIXUPCRITERIA (env_string v)),
      "same as $(i,OPAMUPGRADECRITERIA), but specific to fixup.";
      "NOASPCUD", cli_original, (fun v -> NOASPCUD (env_bool v)),
      "Deprecated.";
      "PREPRO", cli_original, (fun v -> PREPRO (env_bool v)),
      "set this to false to disable CUDF preprocessing. Less efficient, but \
       might help debugging solver issue.";
      "SOLVERALLOWSUBOPTIMAL", cli_from cli2_1,
      (fun v -> SOLVERALLOWSUBOPTIMAL (env_bool v)),
      "(default `true') allows some solvers to still return a solution when \
       they reach timeout; while the solution remains assured to be \
       consistent, there is no guarantee in this case that it fits the \
       expected optimisation criteria. If `true', opam willcontinue with a \
       warning, if `false' a timeout is an error. Currently only \
       the builtin-z3 backend handles this degraded case.";
      "SOLVERTIMEOUT", cli_original, (fun v -> SOLVERTIMEOUT (env_float v)),
      (Printf.sprintf
         "change the time allowance of the solver. Default is %.1f, set to 0 \
          for unlimited. Note that all solvers may not support this option."
         (OpamStd.Option.default 0. OpamSolverConfig.(default.solver_timeout)));
      "UPGRADECRITERIA", cli_original,
      (fun v -> UPGRADECRITERIA (env_string v)),
      "specifies user $(i,preferences) for dependency solving when performing \
       an upgrade. Overrides $(i,OPAMCRITERIA) in upgrades if both are set. \
       See also option --criteria.";
      "USEINTERNALSOLVER", cli_original,
      (fun v -> USEINTERNALSOLVER (env_bool v)),
      "see option `--use-internal-solver'.";
      "VERSIONLAGPOWER", cli_original, (fun v -> VERSIONLAGPOWER (env_int v)),
      "do not use.";
    ] in
  let repository =
    let open OpamRepositoryConfig.E in [
      "CURL", cli_original, (fun v -> CURL (env_string v)),
      "can be used to select a given 'curl' program. See $(i,OPAMFETCH) for \
       more options.";
      "FETCH", cli_original, (fun v -> FETCH (env_string v)),
      "specifies how to download files: either `wget', `curl' or a custom \
       command where variables $(b,%{url}%), $(b,%{out}%), $(b,%{retry}%), \
       $(b,%{compress}%) and $(b,%{checksum}%) will be replaced. Overrides the \
       'download-command' value from the main config file.";
      "NOCHECKSUMS", cli_original, (fun v -> NOCHECKSUMS (env_bool v)),
      "enables option --no-checksums when available.";
      "REQUIRECHECKSUMS", cli_original,
      (fun v -> REQUIRECHECKSUMS (env_bool v)),
      "Enables option `--require-checksums' when available \
       (e.g. for `opam install').";
      "RETRIES", cli_original, (fun v -> RETRIES (env_int v)),
      "sets the number of tries before failing downloads.";
      "VALIDATIONHOOK", cli_original, (fun v -> VALIDATIONHOOK (env_string v)),
      "if set, uses the `%{hook%}' command to validate \
       an opam repository update.";
    ] in
  let state =
    let open OpamStateConfig.E in [
      "BUILDDOC", cli_between cli2_0 cli2_1,
      (fun v -> BUILDDOC (env_bool v)), "see option `--build-doc'.";
      "BUILDTEST", cli_between cli2_0 cli2_1,
      (fun v -> BUILDTEST (env_bool v)), "see option `--build-test'.";
      "DOWNLOADJOBS", cli_original, (fun v -> DOWNLOADJOBS (env_int v)),
      "sets the maximum number of simultaneous downloads.";
      "DRYRUN", cli_original, (fun v -> DRYRUN (env_bool v)),
      "see option `--dry-run'.";
      "IGNORECONSTRAINTS", cli_original,
      (fun v -> IGNORECONSTRAINTS (env_string v)),
      "see install option `--ignore-constraints-on'.";
      "JOBS", cli_original, (fun v -> JOBS (env_int v)),
      "sets the maximum number of parallel workers to run.";
      "LOCKED", cli_original, (fun v -> LOCKED (env_string v)),
      "combination of `--locked' and `--lock-suffix' options.";
      "MAKECMD", cli_original, (fun v -> MAKECMD (env_string v)),
      "set the system make command to use.";
      "NODEPEXTS", cli_from cli2_1, (fun v -> NODEPEXTS (env_bool v)),
      "disables system dependencies handling, see option `--no-depexts'.";
      "NOENVNOTICE", cli_original, (fun v -> NOENVNOTICE (env_bool v)),
      "Internal.";
      "ROOT", cli_original, (fun v -> ROOT (env_string v)),
      "see option `--root'. This is automatically set \
       by `opam env --root=DIR --set-root'.";
      "SWITCH", cli_original, (fun v -> SWITCH (env_string v)),
      "see option `--switch'. Automatically set \
       by `opam env --switch=SWITCH --set-switch'.";
      "UNLOCKBASE", cli_original, (fun v -> UNLOCKBASE (env_bool v)),
      "see install option `--unlock-base'.";
      "WITHDOC", cli_original, (fun v -> WITHDOC (env_bool v)),
      "see install option `--with-doc'.";
      "WITHTEST", cli_original, (fun v -> WITHTEST (env_bool v)),
      "see install option `--with-test.";
    ] in
  let client =
    let open OpamClientConfig.E in [
      "ASSUMEDEPEXTS", cli_from cli2_1, (fun v -> ASSUMEDEPEXTS (env_bool v)),
      "see option `--assume-depexts'.";
      "AUTOREMOVE", cli_original, (fun v -> AUTOREMOVE (env_bool v)),
      "see remove option `--auto-remove'.";
      "DROPWORKINGDIR", cli_from cli2_1, (fun v -> DROPWORKINGDIR (env_bool v)),
      "overrides packages previously updated with $(b,--working-dir) on \
       update. Without this variable set, opam would keep them unchanged \
       unless explicitly named on the command-line.";
      "EDITOR", cli_original, (fun v -> EDITOR (env_string v)),
      "sets the editor to use for opam file editing, overrides $(i,\\$EDITOR) \
       and $(i,\\$VISUAL).";
      "FAKE", cli_original, (fun v -> FAKE (env_bool v)),
      "see option `--fake'.";
      "IGNOREPINDEPENDS", cli_original,
      (fun v -> IGNOREPINDEPENDS (env_bool v)),
      "see option `--ignore-pin-depends'.";
      "INPLACEBUILD", cli_original, (fun v -> INPLACEBUILD (env_bool v)),
      "see option `--inplace-build'.";
      "JSON", cli_original, (fun v -> JSON (env_string v)),
      "log json output to the given file \
       (use character `%' to index the files).";
      "KEEPBUILDDIR", cli_original, (fun v -> KEEPBUILDDIR (env_bool v)),
      "see install option `--keep-build-dir'.";
      "NOAUTOUPGRADE", cli_original, (fun v -> NOAUTOUPGRADE (env_bool v)),
      "disables automatic internal upgrade of repositories in an earlier \
       format to the current one, on 'update' or 'init'.";
      "NOAGGREGATE", cli_original, (fun v -> NOAGGREGATE (env_bool v)),
      "with `opam admin check', don't aggregate packages.";
      "PINKINDAUTO", cli_original, (fun v -> PINKINDAUTO (env_bool v)),
      "sets whether version control systems should be detected when pinning \
       to a local path. Enabled by default since 1.3.0.";
      "REUSEBUILDDIR", cli_original, (fun v -> REUSEBUILDDIR (env_bool v)),
      "see option `--reuse-build-dir'.";
      "SHOW", cli_original, (fun v -> SHOW (env_bool v)),
      "see option `--show'.";
      "SKIPUPDATE", cli_original, (fun v -> SKIPUPDATE (env_bool v)),
      "see option `--skip-updates'.";
      "STATS", cli_original, (fun v -> STATS (env_bool v)),
      "display stats at the end of command.";
      "WORKINGDIR", cli_original, (fun v -> WORKINGDIR (env_bool v)),
      "see option `--working-dir'.";
    ] in
  core @ format @ solver @ repository @ state @ client

let scrubbed_environment_variables =
  let f (name, validity, _, _) =
    if is_original_cli validity then
      None
    else
      Some ("OPAM" ^ name)
  in
    OpamStd.List.filter_map f environment_variables

let doc_opam_env_variables, init_opam_env_variabes =
  env_with_cli environment_variables

(** Help sections common to all commands *)

let global_option_section = Manpage.s_common_options
let help_sections cli =
  [
    `S global_option_section;
    `P "These options are common to all commands.";

    `S Manpage.s_environment;
    `P "Opam makes use of the environment variables listed here. Boolean \
        variables should be set to \"0\", \"no\", \"false\" or the empty \
        string to disable, \"1\", \"yes\" or \"true\" to enable.";
  ] @
  List.sort compare (doc_opam_env_variables_pre @ doc_opam_env_variables cli)
  @ [
    `P "$(i,OPAMVAR_var) overrides the contents of the variable $(i,var)  when \
        substituting `%{var}%` strings in `opam` files.";
    `P "$(i,OPAMVAR_package_var) overrides the contents of the variable \
        $(i,package:var) when substituting `%{package:var}%` strings in \
        `opam` files.";

    `S "CLI VERSION";
    `P "All scripts and programmatic invocations of opam should use `--cli' in \
        order to ensure that they work seamlessly with future versions of the \
        opam client. Additionally, blog posts or other documentation can \
        benefit, as it prevents information from becoming stale.";
    `P (Printf.sprintf
          "Although opam only supports roots ($(i,~%s.opam%s)) for the current \
           version, it does provide backwards compatibility for its \
           command-line interface." dir_sep dir_sep);
    `P "Since CLI version support was only added in opam 2.1, use $(i,OPAMCLI) \
        to select 2.0 support (as opam 2.0 will just ignore it), \
        and `--cli=2.1' for 2.1 (or later) versions, since an environment variable \
        controlling the parsing of syntax is brittle. To this end, opam \
        displays a warning if $(i,OPAMCLI) specifies a valid version other \
        than 2.0, and also if `--cli=2.0' is specified.";
    `P "The command-line version is selected by using the `--cli' option or \
        the $(i,OPAMCLI) environment variable. `--cli' may be specified more\
        than once, where the last instance takes precedence. $(i,OPAMCLI) is \
        only inspected if `--cli' is not given.";

    `S Manpage.s_exit_status;
    `P "As an exception to the following, the `exec' command returns 127 if the \
        command was not found or couldn't be executed, and the command's exit \
        value otherwise."
  ] @
  List.map (fun (reason, code) ->
      `I (string_of_int code, match reason with
        | `Success ->
          "Success, or true for boolean queries."
        | `False ->
          "False. Returned when a boolean return value is expected, e.g. when \
           running with $(b,--check), or for queries like $(b,opam lint)."
        | `Bad_arguments ->
          "Bad command-line arguments, or command-line arguments pointing to \
           an invalid context (e.g. file not following the expected format)."
        | `Not_found ->
          "Not found. You requested something (package, version, repository, \
           etc.) that couldn't be found."
        | `Aborted ->
          "Aborted. The operation required confirmation, which wasn't given."
        | `Locked ->
          "Could not acquire the locks required for the operation."
        | `No_solution ->
          "There is no solution to the user request. This can be caused by \
           asking to install two incompatible packages, for example."
        | `File_error ->
          "Error in package definition, or other metadata files. Using \
           $(b,--strict) raises this error more often."
        | `Package_operation_error ->
          "Package script error. Some package operations were unsuccessful. \
           This may be an error in the packages or an incompatibility with \
           your system. This can be a partial error."
        | `Sync_error ->
          "Sync error. Could not fetch some remotes from the network. This can \
           be a partial error."
        | `Configuration_error ->
          "Configuration error. Opam or system configuration doesn't allow \
           operation, and needs fixing."
        | `Solver_failure ->
          "Solver failure. The solver failed to return a sound answer. It can \
           be due to a broken external solver, or an error in solver \
           configuration."
        | `Internal_error ->
          "Internal error. Something went wrong, likely due to a bug in opam \
           itself."
        | `User_interrupt ->
          "User interrupt. SIGINT was received, generally due to the user \
           pressing Ctrl-C."
        ))
    OpamStd.Sys.exit_codes
  @ [
    `S "FURTHER DOCUMENTATION";
    `P (Printf.sprintf "See https://opam.ocaml.org/doc.");

    `S Manpage.s_authors;
    `P "Vincent Bernardoff <vb@luminar.eu.org>"; `Noblank;
    `P "Raja Boujbel       <raja.boujbel@ocamlpro.com>"; `Noblank;
    `P "Roberto Di Cosmo   <roberto@dicosmo.org>"; `Noblank;
    `P "Thomas Gazagnaire  <thomas@gazagnaire.org>"; `Noblank;
    `P "Louis Gesbert      <louis.gesbert@ocamlpro.com>"; `Noblank;
    `P "Fabrice Le Fessant <Fabrice.Le_fessant@inria.fr>"; `Noblank;
    `P "Anil Madhavapeddy  <anil@recoil.org>"; `Noblank;
    `P "Guillem Rieu       <guillem.rieu@ocamlpro.com>"; `Noblank;
    `P "Ralf Treinen       <ralf.treinen@pps.jussieu.fr>"; `Noblank;
    `P "Frederic Tuong     <tuong@users.gforge.inria.fr>";

    `S Manpage.s_bugs;
    `P "Check bug reports at https://github.com/ocaml/opam/issues.";
  ]


(** Global options *)

type global_options = {
  debug_level: int option;
  verbose: int;
  quiet : bool;
  color : OpamStd.Config.when_ option;
  opt_switch : string option;
  confirm_level : OpamStd.Config.answer option;
  yes: bool option;
  strict : bool;
  opt_root : dirname option;
  git_version : bool;
  external_solver : string option;
  use_internal_solver : bool;
  cudf_file : string option;
  solver_preferences : string option;
  best_effort : bool;
  safe_mode : bool;
  json : string option;
  no_auto_upgrade : bool;
  working_dir : bool;
  ignore_pin_depends : bool;
  cli : OpamCLIVersion.t;
}

(* The --cli passed by cmdliner is ignored (it's only there for --help) *)
let create_global_options
    git_version debug debug_level verbose quiet color opt_switch
    yes confirm_level
    strict
    opt_root external_solver use_internal_solver
    cudf_file solver_preferences best_effort safe_mode json no_auto_upgrade
    working_dir ignore_pin_depends
    d_no_aspcud _ =
  if d_no_aspcud then
    OpamConsole.warning
      "Option %s is deprecated, ignoring it."
      (OpamConsole.colorise `bold "--no-aspcud");
  let debug_level = OpamStd.Option.Op.(
      debug_level >>+ fun () -> if debug then Some 1 else None
    ) in
  let get_last l = match List.rev l with [] -> None | x::_ -> Some x in
  let yes = get_last yes in
  let confirm_level = get_last confirm_level in
  let verbose = List.length verbose in
  let cli = OpamCLIVersion.current in
  { git_version; debug_level; verbose; quiet; color; opt_switch; confirm_level; yes;
    strict; opt_root; external_solver; use_internal_solver;
    cudf_file; solver_preferences; best_effort; safe_mode; json;
    no_auto_upgrade; working_dir; ignore_pin_depends; cli }

let apply_global_options cli o =
  if o.git_version then (
    begin match OpamGitVersion.version with
      | None   -> ()
      | Some v -> OpamConsole.msg "%s\n" v
    end;
    OpamStd.Sys.exit_because `Success
  );
  let open OpamStd.Option.Op in
  let flag f = if f then Some true else None in
  let some x = match x with None -> None | some -> Some some in
  let solver =
    if o.use_internal_solver then
      Some (lazy (OpamCudfSolver.get_solver ~internal:true
                    OpamCudfSolver.default_solver_selection))
    else
      o.external_solver >>| fun s -> lazy (OpamCudfSolver.solver_of_string s)
  in
  let solver_prefs = o.solver_preferences >>| fun p -> lazy (Some p) in
  let yes = OpamStd.Option.(map some o.yes) in
  init_opam_env_variabes cli;
  OpamClientConfig.opam_init
    (* - format options - *)
    ?strict:(flag o.strict)
    (* ?skip_version_checks:bool *)
    (* ?all_parens:bool *)
    (* - core options - *)
    ?debug_level:(if o.safe_mode then Some 0 else o.debug_level)
    ?verbose_level:(if o.quiet then Some 0 else
                    if o.verbose = 0 then None else Some o.verbose)
    ?color:o.color
    (* ?utf8:[ `Extended | `Always | `Never | `Auto ] *)
    (* ?disp_status_line:[ `Always | `Never | `Auto ] *)
    ?confirm_level:o.confirm_level
    ?yes
    ?safe_mode:(flag o.safe_mode)
    (* ?lock_retries:int *)
    (* ?log_dir:OpamTypes.dirname *)
    (* ?keep_log_dir:bool *)
    (* - repository options - *)
    (* ?download_tool:(OpamTypes.arg list * dl_tool_kind) Lazy.t *)
    (* ?retries:int *)
    (* ?force_checksums:bool option *)
    (* - solver options *)
    ?cudf_file:(some o.cudf_file)
    ?solver
    ?best_effort:(flag o.best_effort)
    ?solver_preferences_default:solver_prefs
    ?solver_preferences_upgrade:solver_prefs
    ?solver_preferences_fixup:solver_prefs
    (* ?solver_preferences_best_effort_prefix: *)
    (* - state options - *)
    ?root_dir:o.opt_root
    ?current_switch:(o.opt_switch >>| OpamSwitch.of_string)
    ?switch_from:(o.opt_switch >>| fun _ -> `Command_line)
    (* ?jobs: int *)
    (* ?dl_jobs: int *)
    (* ?keep_build_dir:bool *)
    (* ?build_test:bool *)
    (* ?build_doc:bool *)
    (* ?show:bool *)
    (* ?dryrun:bool *)
    (* ?fake:bool *)
    (* ?makecmd:string Lazy.t *)
    (* ?ignore_constraints_on:name_set *)
    (* ?skip_dev_update:bool *)
    ?json_out:OpamStd.Option.Op.(o.json >>| function "" -> None | s -> Some s)
    (* ?root_is_ok:bool *)
    ?no_auto_upgrade:(flag o.no_auto_upgrade)
    (* - client options - *)
    ?working_dir:(flag o.working_dir)
    ?ignore_pin_depends:(flag o.ignore_pin_depends)
    (* ?print_stats:bool *)
    (* ?sync_archives:bool *)
    (* ?pin_kind_auto:bool *)
    (* ?autoremove:bool *)
    (* ?editor:string *)
    ~cli:o.cli
    ();
  if OpamClientConfig.(!r.json_out <> None) then (
    OpamJson.append "opam-version" (`String OpamVersion.(to_string (full ())));
    OpamJson.append "command-line"
      (`A (List.map (fun s -> `String s) (Array.to_list Sys.argv)))
  )


(** Build options *)

type build_options = {
  keep_build_dir: bool;
  reuse_build_dir: bool;
  inplace_build : bool;
  make          : string option;
  no_checksums  : bool;
  req_checksums : bool;
  build_test    : bool;
  build_doc     : bool;
  show          : bool;
  dryrun        : bool;
  fake          : bool;
  skip_update   : bool;
  jobs          : int option;
  ignore_constraints_on: name list option;
  unlock_base   : bool;
  locked        : bool;
  lock_suffix   : string;
  assume_depexts: bool;
  no_depexts    : bool;
}

let create_build_options
    keep_build_dir reuse_build_dir inplace_build make no_checksums
    req_checksums build_test build_doc show dryrun skip_update
    fake jobs ignore_constraints_on unlock_base locked lock_suffix
    assume_depexts no_depexts
    =
  {
    keep_build_dir; reuse_build_dir; inplace_build; make; no_checksums;
    req_checksums; build_test; build_doc; show; dryrun; skip_update; fake;
    jobs; ignore_constraints_on; unlock_base; locked; lock_suffix;
    assume_depexts; no_depexts;
  }

let apply_build_options cli b =
  let open OpamStd.Option.Op in
  let flag f = if f then Some true else None in
  OpamRepositoryConfig.update
    (* ?download_tool:(OpamTypes.arg list * dl_tool_kind) Lazy.t *)
    (* ?retries:int *)
    ?force_checksums:(if b.req_checksums then Some (Some true)
                      else if b.no_checksums then Some (Some false)
                      else None)
    ();
  OpamStateConfig.update
    (* ?root: -- handled globally *)
    ?jobs:(b.jobs >>| fun j -> lazy j)
    (* ?dl_jobs:int *)
    (* ?no_base_packages:(flag o.no_base_packages) -- handled globally *)
    ?build_test:(flag b.build_test)
    ?build_doc:(flag b.build_doc)
    ?dryrun:(flag b.dryrun)
    ?makecmd:(b.make >>| fun m -> lazy m)
    ?ignore_constraints_on:
      (b.ignore_constraints_on >>|
       OpamPackage.Name.Set.of_list)
    ?unlock_base:(flag b.unlock_base)
    ?locked:(if b.locked then Some (Some b.lock_suffix) else None)
    ?no_depexts:(flag (b.no_depexts || OpamCLIVersion.Op.(cli @= cli2_0)))
    ();
  OpamClientConfig.update
    ?keep_build_dir:(flag b.keep_build_dir)
    ?reuse_build_dir:(flag b.reuse_build_dir)
    ?inplace_build:(flag b.inplace_build)
    ?show:(flag b.show)
    ?fake:(flag b.fake)
    ?skip_dev_update:(flag b.skip_update)
    ?assume_depexts:(flag (b.assume_depexts || b.no_depexts))
    ~scrubbed_environment_variables
    ()


(** Converters *)

let pr_str = Format.pp_print_string

let repository_name =
  let parse str = `Ok (OpamRepositoryName.of_string str) in
  let print ppf name = pr_str ppf (OpamRepositoryName.to_string name) in
  parse, print

let url =
  let parse str =
    match OpamUrl.parse_opt ~from_file:false str with
    | Some url -> `Ok url
    | None -> `Error ("malformed url "^str)
  in
  let print ppf url = pr_str ppf (OpamUrl.to_string url) in
  parse, print

let filename =
  let parse str = `Ok (OpamFilename.of_string str) in
  let print ppf filename = pr_str ppf (OpamFilename.to_string filename) in
  parse, print

let existing_filename_or_dash =
  let parse str =
    if str = "-" then `Ok None
    else
      let f = OpamFilename.of_string str in
      if OpamFilename.exists f then `Ok (Some f)
      else
        `Error (Printf.sprintf "File %s not found" (OpamFilename.to_string f))
  in
  let print ppf filename =
    pr_str ppf OpamStd.Option.Op.((filename >>| OpamFilename.to_string) +! "-") in
  parse, print

let dirname =
  let parse str = `Ok (OpamFilename.Dir.of_string str) in
  let print ppf dir = pr_str ppf (escape_path (OpamFilename.prettify_dir dir)) in
  parse, print

let existing_filename_dirname_or_dash =
  let parse str =
    if str = "-" then `Ok None else
    match OpamFilename.opt_file (OpamFilename.of_string str) with
    | Some f -> `Ok (Some (OpamFilename.F f))
    | None -> match OpamFilename.opt_dir (OpamFilename.Dir.of_string str) with
      | Some d -> `Ok (Some (OpamFilename.D d))
      | None ->
        `Error (Printf.sprintf "File or directory %s not found" str)
  in
  let print ppf gf =
    pr_str ppf @@ match gf with
    | None -> "-"
    | Some (OpamFilename.D d) -> OpamFilename.Dir.to_string d
    | Some (OpamFilename.F f) -> OpamFilename.to_string f
  in
  parse, print

let _subpath_conv =
  (fun str -> `Ok (OpamStd.String.remove_prefix ~prefix:"./" str)), pr_str

let package_name =
  let parse str =
    try `Ok (OpamPackage.Name.of_string str)
    with Failure msg -> `Error msg
  in
  let print ppf pkg = pr_str ppf (OpamPackage.Name.to_string pkg) in
  parse, print

let package_version =
  let parse str =
    try `Ok (OpamPackage.Version.of_string str)
    with Failure msg -> `Error msg
  in
  let print ppf ver = pr_str ppf (OpamPackage.Version.to_string ver) in
  parse, print

let positive_integer : int Arg.converter =
  let (parser, printer) = Arg.int in
  let parser s =
    match parser s with
    | `Error _ -> `Error "expected a strictly positive integer"
    | `Ok n as r -> if n <= 0
      then `Error "expected a positive integer"
      else r in
  (parser, printer)

(* name * version option *)
let package =
  let parse str =
    let re = Re.(compile @@ seq [
        bos;
        group @@ rep1 @@ diff any (set ">=<.!");
        opt @@ seq [ set ".="; group @@ rep1 any ];
        eos;
      ]) in
    try
      let sub = Re.exec re str in
      let name = OpamPackage.Name.of_string (Re.Group.get sub 1) in
      let version_opt =
        try Some (OpamPackage.Version.of_string (Re.Group.get sub 2))
        with Not_found -> None in
      `Ok (name, version_opt)
    with Not_found | Failure _ -> `Error "bad package format"
  in
  let print ppf (name, version_opt) =
    match version_opt with
    | None -> pr_str ppf (OpamPackage.Name.to_string name)
    | Some v -> pr_str ppf (OpamPackage.Name.to_string name ^"."^
                            OpamPackage.Version.to_string v)
  in
  parse, print

let package_with_version =
  let parse str =
    match fst package str with
    | `Ok (n, Some v) -> `Ok (OpamPackage.create n v)
    | `Ok (_, None) -> `Error "missing package version"
    | `Error e -> `Error e
  in
  let print ppf nv = pr_str ppf (OpamPackage.to_string nv) in
  parse, print

(* name * version constraint *)
let atom =
  let parse str =
    try `Ok (OpamFormula.atom_of_string str)
    with Failure msg -> `Error msg
  in
  let print ppf atom =
    pr_str ppf (OpamFormula.short_string_of_atom atom) in
  parse, print

let atom_or_local =
  let parse str =
    if OpamStd.String.contains ~sub:Filename.dir_sep str ||
       OpamStd.String.starts_with ~prefix:"." str
    then
      if OpamFilename.(exists (of_string str)) then
        `Ok (`Filename (OpamFilename.of_string str))
      else if  OpamFilename.(exists_dir (Dir.of_string str)) then
        `Ok (`Dirname (OpamFilename.Dir.of_string str))
      else
        `Error (Printf.sprintf
                  "Not a valid package specification or existing file or \
                   directory: %s" str)
    else match fst atom str with
      | `Ok at -> `Ok (`Atom at)
      | `Error e -> `Error e
  in
  let print ppf = function
    | `Filename f -> pr_str ppf (OpamFilename.to_string f)
    | `Dirname d -> pr_str ppf (OpamFilename.Dir.to_string d)
    | `Atom a -> snd atom ppf a
  in
  parse, print

let atom_or_dir =
  let parse str = match fst atom_or_local str with
    | `Ok (`Filename _) ->
      `Error (Printf.sprintf
                "Not a valid package specification or existing directory: %s"
                str)
    | `Ok (`Atom _ | `Dirname _ as atom_or_dir) -> `Ok (atom_or_dir)
    | `Error e -> `Error e
  in
  let print ppf = snd atom_or_local ppf in
  parse, print

let dep_formula =
  let module OpamParser = OpamParser.FullPos in
  let module OpamPrinter = OpamPrinter.FullPos in
  let pp = OpamFormat.V.(package_formula `Conj (constraints version)) in
  let parse str =
    try
      let v = OpamParser.value_from_string str "<command-line>" in
      `Ok (OpamPp.parse pp ~pos:pos_null v)
    with e -> OpamStd.Exn.fatal e; `Error (Printexc.to_string e)
  in
  let print ppf f =
    pr_str ppf (OpamPrinter.value (OpamPp.print pp f))
  in
  parse, print

let variable_bindings =
  let parse str =
    try
      OpamStd.String.split str ',' |>
      List.map (fun s -> match OpamStd.String.cut_at s '=' with
          | Some (a, b) -> OpamVariable.of_string a, b
          | None -> Printf.ksprintf failwith "%S is not a binding" s) |>
      fun bnds -> `Ok bnds
    with Failure e -> `Error e
  in
  let print ppf x =
    List.map
      (fun (a,b) -> Printf.sprintf "%s=%s" (OpamVariable.to_string a) b) x |>
    String.concat "," |>
    pr_str ppf
  in
  parse, print

let warn_selector =
  let parse str =
    let sep = Re.(compile (set "+-")) in
    let sel = Re.(compile @@
                  seq [bos; group (rep1 digit);
                       opt @@ seq [str ".."; group (rep1 digit)];
                       eos]) in
    let rec seq i j =
      if i = j then [i]
      else if i < j then i :: seq (i+1) j
      else j :: seq (j+1) i
    in
    let rec aux acc = function
      | `Delim d :: `Text n :: r ->
        let nums =
          let g = Re.exec sel n in
          let i = int_of_string (Re.Group.get g 1) in
          try seq i (int_of_string (Re.Group.get g 2))
          with Not_found -> [i]
        in
        let enabled = Re.Group.get d 0 = "+" in
        let acc = List.fold_left (fun acc n -> (n, enabled) :: acc) acc nums in
        aux acc r
      | [] -> acc
      | _ -> raise Not_found
    in
    try `Ok (List.rev (aux [] (Re.split_full sep str)))
    with Not_found ->
      `Error "Expected a warning string, e.g. '--warn=-10..21+12-36'"
  in
  let print ppf warns =
    pr_str ppf @@
    OpamStd.List.concat_map "" (fun (num,enable) ->
        Printf.sprintf "%c%d" (if enable then '+' else '-') num)
      warns
  in
  parse, print

let _selector =
  let parse str =
    let r =
      List.fold_left (fun (plus, minus) elem ->
          match OpamStd.String.sub_at 1 elem with
          | "+" as prefix ->
            (OpamStd.String.remove_prefix ~prefix elem)::plus, minus
          | "-" as prefix ->
            plus, (OpamStd.String.remove_prefix ~prefix elem)::minus
          |  _ ->  elem::plus, minus)
        ([],[]) (OpamStd.String.split str ',')
    in
    `Ok r
  in
  let print ppf (plus,minus) =
    let concat c =
      OpamStd.List.concat_map ~nil:"" "," (fun x -> c^x)
    in
    pr_str ppf @@ Printf.sprintf "%s,%s" (concat "+" plus) (concat "-" minus)
  in
  parse, print

(* unused
let enum_with_default sl: 'a Arg.converter =
  let parse, print = Arg.enum sl in
  let parse s =
    match parse s with
    | `Ok _ as x -> x
    | _ -> `Ok (`default s) in
  parse, print
*)

let opamlist_column =
  let parse str =
    if OpamStd.String.ends_with ~suffix:":" str then
      let fld = OpamStd.String.remove_suffix ~suffix:":" str in
      `Ok (OpamListCommand.Field fld)
    else
    try
      List.find (function (OpamListCommand.Field _), _ -> false
                        | _, name -> name = str)
        OpamListCommand.field_names
      |> fun (f, _) -> `Ok f
    with Not_found ->
      `Error (Printf.sprintf
                "No known printer for column %s. If you meant an opam file \
                 field, use '%s:' instead (with a trailing colon)."
                str str)
  in
  let print ppf field =
    Format.pp_print_string ppf (OpamListCommand.string_of_field field)
  in
  parse, print

let opamlist_columns =
  let field_re =
    (* max paren nesting 1, obviously *)
    Re.(compile @@ seq [
        start;
        group @@ seq [
          rep @@ diff any (set ",(");
          opt @@ seq [char '('; rep (diff any (char ')')); char ')'];
        ];
        alt [char ','; stop];
      ])
  in
  let parse str =
    try
      let rec aux pos =
        if pos = String.length str then [] else
        let g = Re.exec ~pos field_re str in
        Re.Group.get g 1 :: aux (Re.Group.stop g 0)
      in
      let fields = aux 0 in
      List.fold_left (function
          | `Error _ as e -> fun _ -> e
          | `Ok acc -> fun str ->
            match fst opamlist_column str with
            | `Ok f -> `Ok (acc @ [f])
            | `Error _ as e -> e)
        (`Ok []) fields
    with Not_found ->
      `Error (Printf.sprintf "Invalid columns specification: '%s'." str)
  in
  let print ppf cols =
    let rec aux = function
      | x::(_::_) as r ->
        snd opamlist_column ppf x; Format.pp_print_char ppf ','; aux r
      | [x] -> snd opamlist_column ppf x
      | [] -> ()
    in
    aux cols
  in
  parse, print

let term_info ~cli title ~doc ~man =
  let man = man @ help_sections cli in
  Term.info ~sdocs:global_option_section ~docs:Manpage.s_commands
    ~doc ~man title

let mk_command ~cli validity name ~doc ~man =
  mk_command ~cli validity term_info name ~doc ~man

let mk_command_ret ~cli validity name ~doc ~man =
  mk_command_ret ~cli validity term_info name ~doc ~man

let make_command_alias ~cli cmd ?(options="") name =
  let term, info = cmd in
  let orig = Term.name info in
  let doc = Printf.sprintf "An alias for $(b,%s%s)." orig options in
  let man = [
    `S Manpage.s_description;
    `P (Printf.sprintf "$(mname)$(b, %s) is an alias for $(mname)$(b, %s%s)."
          name orig options);
    `P (Printf.sprintf "See $(mname)$(b, %s --help) for details."
          orig);
    `S Manpage.s_options;
  ] @ help_sections cli
  in
  term,
  Term.info name
    ~docs:"COMMAND ALIASES" ~sdocs:global_option_section
    ~doc ~man

let arg_list name doc kind =
  let doc = Arg.info ~docv:name ~doc [] in
  Arg.(value & pos_all kind [] & doc)

let nonempty_arg_list name doc kind =
  let doc = Arg.info ~docv:name ~doc [] in
  Arg.(non_empty & pos_all kind [] & doc)


(** Common flags *)

let print_short_flag cli validity =
  mk_flag ~cli validity ["s";"short"]
    "Output raw lists of names, one per line, skipping any details."

let shell_opt cli validity =
  let enum = [
    "bash",SH_bash;
    "sh",SH_sh;
    "csh",SH_csh;
    "zsh",SH_zsh;
    "fish",SH_fish;
  ] |> List.map (fun (s,v) -> cli_original, s, v)
  in
  mk_enum_opt ~cli validity ["shell"] "SHELL" enum
    (Printf.sprintf
       "Sets the configuration mode for opam environment appropriate for \
        $(docv). One of %s. Guessed from the parent processes and the \\$SHELL \
        variable by default."
       (string_of_enum enum))

let dot_profile_flag cli validity =
  mk_opt ~cli validity ["dot-profile"]
    "FILENAME"
    (Printf.sprintf
      "Name of the configuration file to update instead of \
       $(i,~%s.profile) or $(i,~%s.zshrc) based on shell detection."
      dir_sep dir_sep)
    (Arg.some filename) None

let repo_kind_flag cli validity =
  let main_kinds = [
    "http" , `http;
    "local", `rsync;
    "git"  , `git;
    "darcs", `darcs;
    "hg"   , `hg;
  ] |> List.map (fun (s,v) -> cli_original, s, v)
  in
  let aliases_kinds = [
    "wget" , `http;
    "curl" , `http;
    "rsync", `rsync;
  ] |> List.map (fun (s,v) -> cli_original, s, v)
  in
  mk_enum_opt ~cli validity ["k";"kind"] "KIND" (main_kinds @ aliases_kinds)
    (Printf.sprintf "Specify the kind of the repository to be used (%s)."
       (string_of_enum main_kinds))

let jobs_flag cli validity =
  mk_opt ~cli validity ["j";"jobs"] "JOBS"
    "Set the maximal number of concurrent jobs to use. The default value is \
    calculated from the number of cores. You can also set it using the \
    $(b,\\$OPAMJOBS) environment variable."
    Arg.(some positive_integer) None

let name_list =
  arg_list "PACKAGES" "List of package names." package_name

let atom_list =
  arg_list "PACKAGES"
    "List of package names, with an optional version or constraint, \
     e.g `pkg', `pkg.1.0' or `pkg>=0.5'."
    atom

let atom_or_local_list =
  arg_list "PACKAGES"
    (Printf.sprintf
      "List of package names, with an optional version or constraint, e.g `pkg', \
       `pkg.1.0' or `pkg>=0.5' ; or files or directory names containing package \
       description, with explicit directory (e.g. `.%sfoo.opam' or `.')" dir_sep)
    atom_or_local

let atom_or_dir_list =
  arg_list "PACKAGES"
    (Printf.sprintf
      "List of package names, with an optional version or constraint, e.g `pkg', \
       `pkg.1.0' or `pkg>=0.5' ; or directory names containing package \
       description, with explicit directory (e.g. `.%ssrcdir' or `.')" dir_sep)
    atom_or_dir

let nonempty_atom_list =
  nonempty_arg_list "PACKAGES"
    "List of package names, with an optional version or constraint, \
     e.g `pkg', `pkg.1.0' or `pkg>=0.5'."
    atom

let param_list =
  arg_list "PARAMS" "List of parameters." Arg.string

(* Options common to all commands *)
let global_options cli =
  let section = global_option_section in
  let git_version =
    mk_flag ~cli cli_original ~section ["git-version"]
      "Print the git version of opam, if set (i.e. you are using a development \
       version), and exit."
  in
  let debug =
    mk_flag ~cli cli_original ~section ["debug"]
      "Print debug message to stderr. \
       This is equivalent to setting $(b,\\$OPAMDEBUG) to \"true\"." in
  let debug_level =
    mk_opt ~cli cli_original ~section ["debug-level"] "LEVEL"
      "Like $(b,--debug), but allows specifying the debug level ($(b,--debug) \
       sets it to 1). Equivalent to setting $(b,\\$OPAMDEBUG) to a positive \
       integer."
      Arg.(some int) None in
  let verbose =
    Arg.(value & flag_all & info ~docs:section ["v";"verbose"] ~doc:
           "Be more verbose. One $(b,-v) shows all package commands, repeat to \
            also display commands called internally (e.g. $(i,tar), $(i,curl), \
            $(i,patch) etc.) Repeating $(i,n) times is equivalent to setting \
            $(b,\\$OPAMVERBOSE) to \"$(i,n)\".") in
  let quiet =
    mk_flag ~cli cli_original ~section ["q";"quiet"] "Disables $(b,--verbose)." in
  let color =
    mk_enum_opt ~cli cli_original ~section ["color"] "WHEN" when_enum
      (Printf.sprintf "Colorize the output. $(docv) must be %s."
         (string_of_enum when_enum)) in
  (* The --cli option is pre-processed, because it has to be able to appear
     before sub-commands. The one here is present only for --help. *)
  let cli_arg =
    mk_opt ~cli cli_original ~section ["cli"] "MAJOR.MINOR"
      "Use the command-line interface syntax and semantics of $(docv). \
       Intended for any persistent use of opam (scripts, blog posts, etc.), \
       any version of opam in the same MAJOR series will behave as for the \
       specified MINOR release. The flag was not available in opam 2.0, so to \
       select the 2.0 CLI, set the $(b,OPAMCLI) environment variable to \
       $(i,2.0) instead of using this parameter."
      Arg.string (OpamCLIVersion.to_string OpamCLIVersion.current) in
  let switch =
    mk_opt ~cli cli_original ~section ["switch"]
      "SWITCH" "Use $(docv) as the current compiler switch. \
                This is equivalent to setting $(b,\\$OPAMSWITCH) to $(i,SWITCH)."
      Arg.(some string) None in
  let yes =
    mk_vflag_all ~cli [
      cli_original, true, ["y";"yes"],
      "Answer yes to all opam yes/no questions without prompting. \
       See also $(b,--confirm-level). \
       This is equivalent to setting $(b,\\$OPAMYES) to \"true\".";
      cli_from cli2_1, false, ["no"],
      "Answer no to all opam yes/no questions without prompting. \
       See also $(b,--confirm-level). \
       This is equivalent to setting $(b,\\$OPAMNO) to \"true\".";
    ]
  in
  let confirm_level =
    mk_enum_opt_all ~cli (cli_from cli2_1) ~section ["confirm-level"] "LEVEL"
      confirm_enum
      (Printf.sprintf
         "Confirmation level, $(docv) must be %s. Can be specified more than \
          once. If $(b,--yes) or $(b,--no) are also given, the value of the \
          last $(b,--confirm-level) is taken into account. This is equivalent \
          to setting $(b, \\$OPAMCONFIRMLEVEL)`."
         (string_of_enum confirm_enum))
  in
  let strict =
    mk_flag ~cli cli_original ~section ["strict"]
      "Fail whenever an error is found in a package definition \
       or a configuration file. The default is to continue silently \
       if possible." in
  let root =
    mk_opt ~cli cli_original ~section ["root"]
      "ROOT" "Use $(docv) as the current root path. \
              This is equivalent to setting $(b,\\$OPAMROOT) to $(i,ROOT)."
      Arg.(some dirname) None in
  let d_no_aspcud =
    mk_flag ~cli (cli_between cli2_0 cli2_1) ~section ["no-aspcud"] "Deprecated"
  in
  let use_internal_solver =
    mk_flag ~cli cli_original ~section ["use-internal-solver"]
      "Disable any external solver, and use the built-in one (this requires \
       that opam has been compiled with a built-in solver). This is equivalent \
       to setting $(b,\\$OPAMNOASPCUD) or $(b,\\$OPAMUSEINTERNALSOLVER)." in
  let external_solver =
    mk_opt ~cli cli_original ~section ["solver"] "CMD"
      (Printf.sprintf
         "Specify the CUDF solver to use for resolving package installation \
          problems. This is either a predefined solver (this version of opam \
          supports %s), or a custom command that should contain the variables \
          %%{input}%%, %%{output}%%, %%{criteria}%%, and optionally \
          %%{timeout}%%. This is equivalent to setting $(b,\\$OPAMEXTERNALSOLVER)."
         (OpamStd.List.concat_map ", "
            (fun (module S : OpamCudfSolver.S) -> S.name)
            (OpamCudfSolver.default_solver_selection)))
      Arg.(some string) None in
  let solver_preferences =
    mk_opt ~cli cli_original ~section ["criteria"] "CRITERIA"
      ("Specify user $(i,preferences) for dependency solving for this run. \
        Overrides both $(b,\\$OPAMCRITERIA) and $(b,\\$OPAMUPGRADECRITERIA). \
        For details on the supported language, and the external solvers available, see \
        $(i, http://opam.ocaml.org/doc/External_solvers.html). \
        A general guide to using solver preferences can be found at \
        $(i,  http://www.dicosmo.org/Articles/usercriteria.pdf).")
      Arg.(some string) None in
  let cudf_file =
    mk_opt ~cli cli_original ~section ["cudf"] "FILENAME"
      "Debug option: Save the CUDF requests sent to the solver to \
       $(docv)-<n>.cudf."
      Arg.(some string) None in
  let best_effort =
    mk_flag ~cli cli_original ~section ["best-effort"]
      "Don't fail if all requested packages can't be installed: try to install \
       as many as possible. Note that not all external solvers may support \
       this option (recent versions of $(i,aspcud) or $(i,mccs) should). This \
       is equivalent to setting $(b,\\$OPAMBESTEFFORT) environment variable."
  in
  let safe_mode =
    mk_flag ~cli cli_original ~section ["readonly"; "safe"]
      "Make sure nothing will be automatically updated or rewritten. Useful \
       for calling from completion scripts, for example. Will fail whenever \
       such an operation is needed ; also avoids waiting for locks, skips \
       interactive questions and overrides the $(b,\\$OPAMDEBUG) variable. \
       This is equivalent to set environment variable $(b,\\$OPAMSAFE)."
  in
  let json_flag =
    mk_opt ~cli cli_original ~section ["json"] "FILENAME"
      "Save the results of the opam run in a computer-readable file. If the \
       filename contains the character `%', it will be replaced by an index \
       that doesn't overwrite an existing file. Similar to setting the \
       $(b,\\$OPAMJSON) variable."
      Arg.(some string) None
  in
  let no_auto_upgrade =
    mk_flag ~cli cli_original ~section ["no-auto-upgrade"]
      "When configuring or updating a repository that is written for an \
       earlier opam version (1.2), opam internally converts it to the current \
       format. This disables this behaviour. Note that repositories should \
       define their format version in a 'repo' file at their root, or they \
       will be assumed to be in the older format. It is, in any case, \
       preferable to upgrade the repositories manually using $(i,opam admin \
       upgrade [--mirror URL]) when possible."
  in
  let working_dir =
    mk_flag ~cli cli_original ~section ["working-dir"; "w"]
      "Whenever updating packages that are bound to a local, \
       version-controlled directory, update to the current working state of \
       their source instead of the last committed state, or the ref they are \
       pointing to. As source directory is copied as it is, if it isn't clean \
       it may result on a opam build failure.\
       This only affects packages explicitly listed on the command-line.\
       It can also be set with $(b,\\$OPAMWORKINGDIR). "
  in
  let ignore_pin_depends =
    mk_flag ~cli cli_original ~section ["ignore-pin-depends"]
      "Ignore extra pins required by packages that get pinned, either manually \
       through $(i,opam pin) or through $(i,opam install DIR). This is \
       equivalent to setting $(b,IGNOREPINDEPENDS=true)."
  in
  Term.(const create_global_options
        $git_version $debug $debug_level $verbose $quiet $color $switch
        $yes $confirm_level
        $strict $root $external_solver
        $use_internal_solver $cudf_file $solver_preferences $best_effort
        $safe_mode $json_flag $no_auto_upgrade $working_dir
        $ignore_pin_depends
        $d_no_aspcud $cli_arg)

(* lock options *)
let locked ?(section=Manpage.s_options) cli =
  mk_flag ~cli cli_original ~section ["locked"]
    "In commands that use opam files found from pinned sources, if a variant \
     of the file with an added .$(i,locked) extension is found (e.g. \
     $(b,foo.opam.locked) besides $(b,foo.opam)), that will be used instead. \
     This is typically useful to offer a more specific set of dependencies \
     and reproduce similar build contexts, hence the name. The $(i, lock)\
     option can be used to generate such files, based on the versions \
     of the dependencies currently installed on the host. This is equivalent \
     to setting the $(b,\\$OPAMLOCKED) environment variable. Note that this \
     option doesn't generally affect already pinned packages."
let lock_suffix ?(section=Manpage.s_options) cli =
  mk_opt ~cli (cli_from cli2_1) ~section ["lock-suffix"] "SUFFIX"
    "Set locked files suffix to $(i,SUFFIX)."
    Arg.(string) ("locked")

(* Options common to all build commands *)
let build_option_section = "PACKAGE BUILD OPTIONS"
let man_build_option_section =
  [
    `S build_option_section;
  ]
let build_options cli =
  let section = build_option_section in
  let keep_build_dir =
    mk_flag ~cli cli_original ~section ["b";"keep-build-dir"]
      "Keep the build directories after compiling packages. \
       This is equivalent to setting $(b,\\$OPAMKEEPBUILDDIR) to \"true\"." in
  let reuse_build_dir =
    mk_flag ~cli cli_original ~section ["reuse-build-dir"]
      "Reuse existing build directories (kept by using $(b,--keep-build-dir)), \
       instead of compiling from a fresh clone of the source. This can be \
       faster, but also lead to failures if the build systems of the packages \
       don't handle upgrades of dependencies well. This is equivalent to \
       setting $(b,\\$OPAMREUSEBUILDDIR) to \"true\"."
  in
  let inplace_build =
    mk_flag ~cli cli_original ~section ["inplace-build"]
      "When compiling a package which has its source bound to a local \
       directory, process the build and install actions directly in that \
       directory, rather than in a clean copy handled by opam. This only \
       affects packages that are explicitly listed on the command-line. \
       This is equivalent to setting $(b,\\$OPAMINPLACEBUILD) to \"true\"."
  in
  let no_checksums =
    mk_flag ~cli cli_original ~section ["no-checksums"]
      "Do not verify the checksum of downloaded archives.\
       This is equivalent to setting $(b,\\$OPAMNOCHECKSUMS) to \"true\"." in
  let req_checksums =
    mk_flag ~cli cli_original ~section ["require-checksums"]
      "Reject the installation of packages that don't provide a checksum for the upstream archives. \
       This is equivalent to setting $(b,\\$OPAMREQUIRECHECKSUMS) to \"true\"." in
  let build_test =
    mk_flag_replaced ~cli ~section [
      cli_between cli2_0 cli2_1 ~replaced:"--with-test", ["build-test"];
      cli_original, ["t";"with-test"];
    ] "Build and $(b,run) the package unit-tests. This only affects packages \
       listed on the command-line. This is equivalent to setting \
       $(b,\\$OPAMWITHTEST) (or the deprecated $(b,\\$OPAMBUILDTEST)) to \
       \"true\"."
  in
  let build_doc =
    mk_flag_replaced ~cli ~section [
      cli_between cli2_0 cli2_1 ~replaced:"--with-doc", ["build-doc"];
      cli_original, ["d";"with-doc"];
    ] "Build the package documentation. This only affects packages listed on \
       the command-line. This is equivalent to setting $(b,\\$OPAMWITHDOC) \
       (or the deprecated $(b,\\$OPAMBUILDDOC)) to \"true\"."
  in
  let make =
    mk_opt ~cli (cli_between cli2_0 cli2_1
                   ~replaced:"opam config set[-global] make MAKE")
      ~section ["m";"make"] "MAKE"
      "Use $(docv) as the default 'make' command. Has no effect if the \
       $(i,make) variable is defined."
      Arg.(some string) None
  in
  let show =
    mk_flag ~cli cli_original ~section ["show-actions"]
      "Call the solver and display the actions. Don't perform any changes. \
      This is equivalent to setting $(b,\\$OPAMSHOW)." in
  let dryrun =
    mk_flag ~cli cli_original ~section ["dry-run"]
      "Simulate the command, but don't actually perform any changes. This also \
       can be set with environment variable $(b,\\$OPAMDEBUG)." in
  let skip_update =
    mk_flag ~cli cli_original ~section ["skip-updates"]
      "When running an install, upgrade or reinstall on source-pinned \
       packages, they are normally updated from their origin first. This flag \
       disables that behaviour and will keep them to their version in cache. \
       This is equivalent to setting $(b,\\$OPAMSKIPUPDATE)."
  in
  let fake =
    mk_flag ~cli cli_original ~section ["fake"]
      "This option registers the actions into the opam database, without \
       actually performing them. \
       WARNING: This option is dangerous and likely to break your opam \
       environment. You probably want $(b,--dry-run). You've been $(i,warned)."
  in
  let ignore_constraints_on =
    mk_opt ~cli cli_original ~section ["ignore-constraints-on"] "PACKAGES"
      "Forces opam to ignore version constraints on all dependencies to the \
       listed packages. This can be used to test compatibility, but expect \
       builds to break when using this. Note that version constraints on \
       optional dependencies and conflicts are unaffected. This is equivalent \
       to setting $(b,\\$OPAMIGNORECONSTRAINTS)."
      Arg.(some (list package_name)) None ~vopt:(Some [])
  in
  let unlock_base =
    mk_flag_replaced ~cli ~section [
      cli_between cli2_0 cli2_1 ~replaced:"--update-invariant", ["unlock-base"];
      cli_from cli2_1, ["update-invariant"]
    ] "Allow changes to the packages set as switch base (typically, the main \
       compiler). Use with caution. This is equivalent to setting the \
       $(b,\\$OPAMUNLOCKBASE) environment variable"
  in
  let locked = locked cli ~section in
  let lock_suffix = lock_suffix cli ~section in
  let assume_depexts =
    mk_flag ~cli (cli_from cli2_1) ~section ["assume-depexts"]
      "Skip the installation step for any missing system packages, and attempt \
       to proceed with compilation of the opam packages anyway. If the \
       installation is successful, opam won't prompt again about these system \
       packages. Only meaningful if external dependency handling is enabled."
  in
  let no_depexts =
    mk_flag ~cli (cli_from cli2_1) ~section ["no-depexts"]
      "Temporarily disables handling of external dependencies. This can be \
       used if a package is not available on your system package manager, but \
       you installed the required dependency by hand. Implies \
       $(b,--assume-depexts), and stores the exceptions upon success as well."
  in
  Term.(const create_build_options
        $keep_build_dir $reuse_build_dir $inplace_build $make
        $no_checksums $req_checksums $build_test $build_doc $show $dryrun
        $skip_update $fake $jobs_flag cli cli_original $ignore_constraints_on
        $unlock_base $locked $lock_suffix $assume_depexts $no_depexts)

(* Option common to install commands *)
let assume_built cli =
  mk_flag ~cli cli_original  ["assume-built"]
    "For use on locally-pinned packages: assume they have already \
     been correctly built, and only run their installation \
     instructions, directly from their source directory. This \
     skips the build instructions and can be useful to install \
     packages that are being worked on. Implies $(i,--inplace-build). \
     No locally-pinned packages will be skipped."

(* Options common to all path based/related commands, e.g. (un)pin, upgrade,
   remove, (re)install.
   Disabled *)
let recurse _cli = Term.const false

(*
  mk_flag ~cli (cli_from cli2_2) ["recursive"]
    "Allow recursive lookups of (b,*.opam) files. Cf. $(i,--subpath) also."
*)

let subpath _cli = Term.const None
(*
  mk_opt ~cli (cli_from cli2_2) ["subpath"] "PATH"
    "$(b,*.opam) files are retrieved from the given sub directory instead of \
      top directory. Sources are then taken from the targeted sub directory, \
      internally only this subdirectory is copied/fetched.  It can be combined \
      with $(i,--recursive) to have a recursive lookup on the subpath."
    Arg.(some subpath_conv) None
*)

let package_selection_section = "PACKAGE SELECTION OPTIONS"

let package_selection cli =
  let section = package_selection_section in
  let depends_on =
    mk_opt_all ~cli cli_original ["depends-on"] "PACKAGES" ~section
      "List only packages that depend on one of (comma-separated) $(b,PACKAGES)."
      Arg.(list atom)
  in
  let required_by =
    mk_opt_all ~cli cli_original ["required-by"] "PACKAGES" ~section
      "List only the dependencies of (comma-separated) $(b,PACKAGES)."
      Arg.(list atom)
  in
  let conflicts_with =
    mk_opt_all ~cli cli_original ["conflicts-with"] "PACKAGES" ~section
      "List packages that have declared conflicts with at least one of the \
       given list. This includes conflicts defined from the packages in the \
       list, from the other package, or by a common $(b,conflict-class:) \
       field."
      Arg.(list package_with_version)
  in
  let coinstallable_with =
    mk_opt_all ~cli cli_original ["coinstallable-with"] "PACKAGES" ~section
      "Only list packages that are compatible with all of $(b,PACKAGES)."
      Arg.(list package_with_version)
  in
  let resolve =
    mk_opt_all ~cli cli_original ["resolve"] "PACKAGES" ~section
      "Restrict to a solution to install (comma-separated) $(docv), $(i,i.e.) \
       a consistent set of packages including those. This is subtly different \
       from `--required-by --recursive`, which is more predictable and can't \
       fail, but lists all dependencies independently without ensuring \
       consistency. \
       Without `--installed`, the answer is self-contained and independent of \
       the current installation. With `--installed', it's computed from the \
       set of currently installed packages. \
       `--no-switch` further makes the solution independent from the \
       currently pinned packages, architecture, and compiler version. \
       The combination with `--depopts' is not supported."
      Arg.(list atom)
  in
  let recursive =
    mk_flag ~cli cli_original ["recursive"] ~section
      "With `--depends-on' and `--required-by', display all transitive \
       dependencies rather than just direct dependencies." in
  let depopts =
    mk_flag ~cli cli_original ["depopts"]  ~section
      "Include optional dependencies in dependency requests."
  in
  let nobuild =
    mk_flag ~cli cli_original ["nobuild"]  ~section
      "Exclude build dependencies (they are included by default)."
  in
  let post =
    mk_flag ~cli cli_original ["post"]  ~section
      "Include dependencies tagged as $(i,post)."
  in
  let dev =
    mk_flag ~cli cli_original ["dev"]  ~section
      "Include development packages in dependencies."
  in
  let doc_flag =
    mk_flag ~cli cli_original ["doc";"with-doc"] ~section
      "Include doc-only dependencies."
  in
  let test =
    mk_flag ~cli cli_original ["t";"test";"with-test"] ~section
      "Include test-only dependencies."
  in
  let field_match =
    mk_opt_all ~cli cli_original ["field-match"] "FIELD:PATTERN" ~section
      "Filter packages with a match for $(i,PATTERN) on the given $(i,FIELD)"
      Arg.(pair ~sep:':' string string)
  in
  let has_flag =
    mk_opt_all ~cli cli_original ["has-flag"] "FLAG" ~section
      ("Only include packages which have the given flag set. \
        Package flags are one of: "^
       (OpamStd.List.concat_map " "
          (Printf.sprintf "$(b,%s)" @* string_of_pkg_flag)
          all_package_flags))
      ((fun s -> match pkg_flag_of_string s with
          | Pkgflag_Unknown s ->
            `Error ("Invalid package flag "^s^", must be one of "^
                    OpamStd.List.concat_map " " string_of_pkg_flag
                      all_package_flags)
          | f -> `Ok f),
       fun fmt flag ->
         Format.pp_print_string fmt (string_of_pkg_flag flag))
  in
  let has_tag =
    mk_opt_all ~cli cli_original ["has-tag"] "TAG" ~section
      "Only includes packages which have the given tag set"
      Arg.string
  in
  let filter
      depends_on required_by conflicts_with coinstallable_with resolve recursive
      depopts nobuild post dev doc_flag test field_match has_flag has_tag
    =
    let dependency_toggles = {
      OpamListCommand.
      recursive; depopts; build = not nobuild; post; test; doc = doc_flag;
      dev
    } in
    List.map (fun flag -> OpamListCommand.Flag flag) has_flag @
    List.map (fun tag -> OpamListCommand.Tag tag) has_tag @
    List.map (fun (field,patt) ->
        OpamListCommand.Pattern
          ({OpamListCommand.default_pattern_selector with
            OpamListCommand.fields = [field]},
           patt))
      field_match @
    List.map (fun deps ->
        OpamListCommand.Depends_on (dependency_toggles, deps))
      depends_on @
    List.map (fun rdeps ->
        OpamListCommand.Required_by (dependency_toggles, rdeps))
      required_by @
    List.map (fun pkgs ->
        OpamListCommand.Conflicts_with pkgs)
      conflicts_with @
    List.map (fun deps ->
        OpamListCommand.Solution (dependency_toggles, deps))
      resolve @
    List.map (fun pkgs ->
        OpamListCommand.Coinstallable_with (dependency_toggles, pkgs))
      coinstallable_with
  in
  Term.(const filter $
        depends_on $ required_by $ conflicts_with $ coinstallable_with $
        resolve $ recursive $ depopts $ nobuild $ post $ dev $ doc_flag $
        test $ field_match $ has_flag $ has_tag)

let package_listing_section = "OUTPUT FORMAT OPTIONS"

let package_listing cli =
  let section = package_listing_section in
  let all_versions = mk_flag ~cli cli_original ["all-versions";"V"]
      ~section
      "Normally, when multiple versions of a package match, only one is shown \
       in the output (the installed one, the pinned-to one, or, failing that, \
       the highest one available or the highest one). This flag disables this \
       behaviour and shows all matching versions. This also changes the \
       default display format to include package versions instead of just \
       package names (including when --short is set). This is automatically \
       turned on when a single non-pattern package name is provided on the \
       command-line."
  in
  let print_short =
    mk_flag ~cli cli_original ["short";"s"] ~section
      "Don't print a header, and sets the default columns to $(b,name) only. \
       If you need package versions included, use $(b,--columns=package) \
       instead"
  in
  let sort =
    mk_flag ~cli cli_original ["sort";"S"] ~section
      "Sort the packages in dependency order (i.e. an order in which they \
       could be individually installed.)"
  in
  let columns =
    mk_opt ~cli cli_original ["columns"] "COLUMNS" ~section
      (Printf.sprintf "Select the columns to display among: %s.\n\
                       The default is $(b,name) when $(i,--short) is present \
                       and %s otherwise."
         (OpamStd.List.concat_map ", " (fun (_,f) -> Printf.sprintf "$(b,%s)" f)
            OpamListCommand.raw_field_names)
         (OpamStd.List.concat_map ", "
            (fun f -> Printf.sprintf "$(b,%s)" (OpamListCommand.string_of_field f))
            OpamListCommand.default_list_format))
      Arg.(some & opamlist_columns) None
  in
  let normalise = mk_flag ~cli cli_original ["normalise"] ~section
      "Print the values of opam fields normalised"
  in
  let wrap = mk_flag ~cli cli_original ["wrap"] ~section
      "Wrap long lines, the default being to truncate when displaying on a \
       terminal, or to keep as is otherwise"
  in
  let separator =
    mk_opt ~cli cli_original ["separator"] "STRING" ~section
      "Set the column-separator string"
      Arg.string " "
  in
  let format all_versions short sort columns normalise wrap separator =
  fun ~force_all_versions ->
    let all_versions = force_all_versions || all_versions in
    let columns =
      match columns with
      | Some c -> c
      | None ->
        let cols =
          if short then [OpamListCommand.Name]
          else OpamListCommand.default_list_format
        in
        if all_versions then
          List.map (function
              | OpamListCommand.Name -> OpamListCommand.Package
              | c -> c)
            cols
        else cols
    in
    { OpamListCommand.
      short;
      header = not short;
      columns;
      all_versions;
      wrap = if wrap then Some (`Wrap "\\ ") else Some `Truncate;
      separator;
      value_printer = if normalise then `Normalised else `Normal;
      order = if sort then `Dependency else `Standard;
    }
  in
  Term.(const format $ all_versions $ print_short $ sort $ columns $ normalise $
        wrap $ separator)