1
2#' Read a DESCRIPTION file
3#'
4#' This is a convenience wrapper for \code{description$new()}.
5#' Very often you want to read an existing \code{DESCRIPTION}
6#' file, and to do this you can just supply the path to the file or its
7#' directory to \code{desc()}.
8#'
9#' @param cmd A command to create a description from scratch.
10#'   Currently only \code{"!new"} is implemented. If it does not start
11#'   with an exclamation mark, it will be interpreted as the \sQuote{file}
12#'   argument.
13#' @param file Name of the \code{DESCRIPTION} file to load. If all of
14#'   \sQuote{cmd}, \sQuote{file} and \sQuote{text} are \code{NULL} (the
15#'   default), then the \code{DESCRIPTION} file in the current working
16#'   directory is used. The file can also be an R package (source, or
17#'   binary), in which case the DESCRIPTION file is extracted from it, but
18#'   note that in this case \code{$write()} cannot write the file back in
19#'   the package archive.
20#' @param text A character scalar containing the full DESCRIPTION.
21#'   Character vectors are collapsed into a character scalar, with
22#'   newline as the separator.
23#' @param package If not NULL, then the name of an installed package
24#'     and the DESCRIPTION file of this package will be loaded.
25#'
26#' @export
27#' @examples
28#' desc(package = "desc")
29#' DESCRIPTION <- system.file("DESCRIPTION", package = "desc")
30#' desc(DESCRIPTION)
31
32desc <- function(cmd = NULL, file = NULL, text = NULL, package = NULL) {
33  description$new(cmd, file, text, package)
34}
35
36#' Read, write, update, validate DESCRIPTION files
37#'
38#' @section Constructors:
39#'
40#' There are two ways of creating a description object. The first
41#' is reading an already existing \code{DESCRIPTION} file; simply give
42#' the name of the file as an argument. The default is
43#' \code{DESCRIPTION}: \preformatted{  x <- description$new()
44#'   x2 <- description$new("path/to/DESCRIPTION")}
45#'
46#' The second way is creating a description object from scratch,
47#' supply \code{"!new"} as an argument to do this.
48#' \preformatted{  x3 <- description$new("!new")}
49#'
50#' The complete API reference:
51#' \preformatted{description$new(cmd = NULL, file = NULL, text = NULL,
52#'     package = NULL)}
53#' \describe{
54#'   \item{cmd:}{A command to create a description from scratch.
55#'     Currently only \code{"!new"} is implemented. If it does not start
56#'     with an exclamation mark, it will be interpreted as a \sQuote{file}
57#'     argument.}
58#'   \item{file:}{Name of the \code{DESCRIPTION} file to load. If it is
59#'     a directory, then we assume that it is inside an R package and
60#'     conduct a search for the package root directory, i.e. the first
61#'     directory up the tree that contains a \code{DESCRIPTION} file.
62#'     If \sQuote{cmd}, \sQuote{file}, \sQuote{text} and \sQuote{package}
63#'     are all \code{NULL} (the default), then the search is started from
64#'     the working directory. The file can also be an R package (source, or
65#'     binary), in which case the DESCRIPTION file is extracted from it,
66#'     but note that in this case \code{$write()} cannot write the file
67#'     back in the package archive.}
68#'   \item{text:}{A character scalar containing the full DESCRIPTION.
69#'     Character vectors are collapsed into a character scalar, with
70#'     newline as the separator.}
71#'   \item{package}{If not NULL, then the name of an installed package
72#'     and the DESCRIPTION file of this package will be loaded.}
73#' }
74#'
75#' @section Setting and Querying fields:
76#' Set a field with \code{$set} and query it with \code{$get}:
77#' \preformatted{  x <- description$new("!new")
78#'   x$get("Package)
79#'   x$set("Package", "foobar")
80#'   x$set(Title = "Example Package for 'description'")
81#'   x$get("Package")}
82#' Note that \code{$set} has two forms. You can either give the field name
83#' and new value as two arguments; or you can use a single named argument,
84#' the argument name is the field name, the argument value is the field
85#' value.
86#'
87#' The \code{$fields} method simply lists the fields in the object:
88#' \preformatted{  x$fields()}
89#'
90#' The \code{$has_fields} method checks if one or multiple fields are
91#' present in a description object: \preformatted{  x$has_fields("Package")
92#'   x$has_fields(c("Title", "foobar"))}
93#'
94#' The \code{$del} method removes the specified fields:
95#' \preformatted{  x$set(foo = "bar")
96#'   x$del("foo")}
97#'
98#' \code{$get_field} is similar to \code{$get}, but it queries a single
99#' field, it returns an unnamed vector if found, and returns the
100#' specified \code{default} value if not. By default it throws an error
101#' if the field is not found.
102#'
103#' The complete API reference:
104#' \preformatted{  description$get(keys)
105#'   description$get_field(key, default, trim_ws = TRUE, squish_ws = trim_ws)
106#'   description$set(...)
107#'   description$fields()
108#'   description$has_fields(keys)
109#'   description$del(keys)}
110#' \describe{
111#'   \item{key:}{A character string (length one), the key to query.}
112#'   \item{default:}{If specified and \code{key} is missing, this value
113#'     is returned. If not specified, an error is thrown.}
114#'   \item{trim_ws:}{Whether to trim leading and trailing whitespace
115#'     from the returned value.}
116#'   \item{squish_ws:}{Whether to reduce repeated whitespace in the
117#'     returned value.}
118#'   \item{keys:}{A character vector of keys to query, check or delete.}
119#'   \item{...:}{This must be either two unnamed arguments, the key and
120#'     and the value to set; or an arbitrary number of named arguments,
121#'     names are used as keys, values as values to set.}
122#' }
123#'
124#' @section Normalizing:
125#' Format DESCRIPTION in a standard way. \code{$str} formats each
126#' field in a standard way and returns them (it does not change the
127#' object itself), \code{$print} is used to print it to the
128#' screen. The \code{$normalize} function normalizes each field (i.e.
129#' it changes the object). Normalization means reformatting the fields,
130#' via \code{$reformat_fields()} and also reordering them via
131#' \code{$reorder_fields()}. The format of the various fields is
132#' opinionated and you might like it or not. Note that \code{desc} only
133#' re-formats fields that it updates, and only on demand, so if your
134#' formatting preferences differ, you can still manually edit
135#' \code{DESCRIPTION} and \code{desc} will respect your edits.
136#'
137#' \preformatted{  description$str(by_field = FALSE, normalize = TRUE,
138#'     mode = c("file", "screen"))
139#'   description$normalize()
140#'   description$reformat_fields()
141#'   description$reorder_fields()
142#'   description$print()
143#' }
144#' \describe{
145#'   \item{by_field:}{Whether to return the normalized format
146#'     by field, or collapsed into a character scalar.}
147#'   \item{normalize:}{Whether to reorder and reformat the fields.}
148#'   \item{mode:}{\sQuote{file} mode formats the fields as they are
149#'     written to a file with the \code{write} method. \sQuote{screen}
150#'     mode adds extra markup to some fields, e.g. formats the
151#'     \code{Authors@R} field in a readable way.}
152#' }
153#'
154#' @section Writing it to file:
155#' The \code{$write} method writes the description to a file.
156#' By default it writes it to the file it was created from, if it was
157#' created from a file. Otherwise giving a file name is compulsory:
158#' \preformatted{  x$write(file = "DESCRIPTION")}
159#'
160#' The \code{normalize} argument controls whether the fields are
161#' reformatted according to a standard style. By default they are not.
162#'
163#' The API:
164#' \preformatted{  description$write(file = NULL, normalize = NULL)}
165#' \describe{
166#'   \item{file:}{Path to write the description to. If it was created
167#'      from a file in the first place, then it is written to the same
168#'      file. Otherwise this argument must be specified.}
169#'   \item{normalize:}{Whether to reformat the fields in a standard way.}
170#' }
171#'
172#' @section Version numbers:
173#'
174#' \preformatted{  description$get_version()
175#'   description$set_version(version)
176#'   description$bump_version(which = c("patch", "minor", "major", "dev"))
177#' }
178#'
179#' \describe{
180#'   \item{version:}{A string or a \code{\link[base]{package_version}}
181#'     object.}
182#'   \item{which:}{Which component of the version number to increase.
183#'     See details just below.}
184#' }
185#'
186#' These functions are simple helpers to make it easier to query, set and
187#' increase the version number of a package.
188#'
189#' \code{$get_version()} returns the version number as a
190#' \code{\link[base]{package_version}} object. It throws an error if the
191#' package does not have a \sQuote{Version} field.
192#'
193#' \code{$set_version()} takes a string or a
194#' \code{\link[base]{package_version}} object and sets the \sQuote{Version}
195#' field to it.
196#'
197#' \code{$bump_version()} increases the version number. The \code{which}
198#' parameter specifies which component to increase.
199#' It can be a string referring to a component: \sQuote{major},
200#' \sQuote{minor}, \sQuote{patch} or \sQuote{dev}, or an integer
201#' scalar, for the latter components are counted from one, and the
202#' beginning. I.e. component one is equivalent to \sQuote{major}.
203#'
204#' If a component is bumped, then the ones after it are zeroed out.
205#' Trailing zero components are omitted from the new version number,
206#' but if the old version number had at least two or three components, then
207#' the one will also have two or three.
208#'
209#' The bumping of the \sQuote{dev} version (the fourth component) is
210#' special: if the original version number had less than four components,
211#' and the \sQuote{dev} version is bumped, then it is set to \code{9000}
212#' instead of \code{1}. This is a convention often used by R developers,
213#' it was originally invented by Winston Chang.
214#'
215#' Both \code{$set_version()} and \code{$bump_version()} use dots to
216#' separate the version number components.
217#'
218#' @section Dependencies:
219#' These functions handle the fields that define how the R package
220#' uses another R packages. See \code{\link{dep_types}} for the
221#' list of fields in this group.
222#'
223#' The \code{$get_deps} method returns all declared dependencies, in a
224#' data frame with columns: \code{type}, \code{package} and \code{version}.
225#' \code{type} is the name of the dependency field, \code{package} is the
226#' name of the R package, and \code{version} is the required version. If
227#' no specific versions are required, then this is a \code{"\*"}.
228#'
229#' The \code{$set_deps} method is the opposite of \code{$get_deps} and
230#' it sets all dependencies. The input is a data frame, with the same
231#' structure as the return value of \code{$get_deps}.
232#'
233#' The \code{$has_dep} method checks if a package is included in the
234#' dependencies. It returns a logical scalar. If \code{type} is not
235#' \sQuote{any}, then it has to match as well.
236#'
237#' The \code{$del_deps} method removes all declared dependencies.
238#'
239#' The \code{$set_dep} method adds or updates a single dependency. By
240#' default it adds the package to the \code{Imports} field.
241#'
242#' The API:
243#' \preformatted{  description$set_dep(package, type = dep_types, version = "\*")
244#'   description$set_deps(deps)
245#'   description$get_deps()
246#'   description$has_dep(package, type = c("any", dep_types))
247#'   description$del_dep(package, type = c("all", dep_types))
248#'   description$del_deps()
249#' }
250#' \describe{
251#'   \item{package:}{Name of the package to add to or remove from the
252#'     dependencies.}
253#'   \item{type:}{Dependency type, see \code{\link{dep_types}}. For
254#'     \code{$del_dep} it may also be \code{"all"}, and then the package
255#'     will be deleted from all dependency types.}
256#'   \item{version:}{Required version. Defaults to \code{"\*"}, which means
257#'     no explicit version requirements.}
258#'   \item{deps:}{A data frame with columns \code{type}, \code{package} and
259#'     \code{version}. \code{$get_deps} returns the same format.}
260#' }
261#'
262#' @section Collate fields:
263#' Collate fields contain lists of file names with R source code,
264#' and the package has a separate API for them. In brief, you can
265#' use \code{$add_to_collate} to add one or more files to the main or
266#' other collate field. You can use \code{$del_from_collate} to remove
267#' it from there.
268#'
269#' The API:
270#' \preformatted{  description$set_collate(files, which = c("main", "windows", "unix"))
271#'   description$get_collate(which = c("main", "windows", "unix"))
272#'   description$del_collate(which = c("all", "main", "windows", "unix"))
273#'   description$add_to_collate(files, which = c("default", "all", "main",
274#'     "windows", "unix"))
275#'   description$del_from_collate(files, which = c("all", "main",
276#'     "windows", "unix"))
277#' }
278#' \describe{
279#'   \item{files:}{The files to add or remove, in a character vector.}
280#'   \item{which:}{Which collate field to manipulate. \code{"default"} for
281#'   \code{$add_to_collate} means all existing collate fields, or the
282#'   main one if none exist.}
283#' }
284#'
285#' @section Authors:
286#' There is a specialized API for the \code{Authors@R} field,
287#' to add and remove authors, update their roles, change the maintainer,
288#' etc.
289#'
290#' The API:
291#' \preformatted{  description$get_authors()
292#'   description$set_authors(authors)
293#'   description$get_author(role)
294#'   description$get_maintainer()
295#'   description$coerce_authors_at_r()
296#' }
297#' \describe{
298#'    \item{authors:}{A \code{person} object, a list of authors.}
299#'    \item{role:}{The role to query. See \code{person} for details.}
300#' }
301#' \code{$get_authors} returns a \code{person} object, the parsed
302#' authors. See \code{\link[utils]{person}} for details.
303#'
304#' \code{$get_author} returns a \code{person} object, all authors with
305#' the specified role.
306#'
307#' \code{$get_maintainer} returns the maintainer of the package. It works
308#' with \code{Authors@R} fields and with traditional \code{Maintainer}
309#' fields as well.
310#'
311#' \code{$coerce_authors_at_r} converts an \code{Author} field to one with
312#' a \code{person} object. This coercion may be necessary for other
313#' functions such as \code{$get_authors}.
314#'
315#' \preformatted{  description$add_author(given = NULL, family = NULL, email = NULL,
316#'     role = NULL, comment = NULL, orcid = NULL)
317#'   description$add_me(role = "ctb", comment = NULL, orcid = NULL)
318#'   description$add_author_gh(username, role = "ctb", comment = NULL, orcid = NULL)
319#' }
320#' Add a new author. The arguments correspond to the arguments of the
321#' \code{\link[utils]{person}} function. \code{add_me} is a convenience
322#' function, it adds the current user as an author, and it needs the
323#' \code{whoami} package to be installed. It'll add your ORCID ID
324#' if you provide it as argument or save it as \code{ORCID_ID} environment
325#' variable in .Renviron.
326#' The full name is parsed by \code{add_me} and \code{add_author_gh} using
327#' \code{as.person} and collapsing the given name and the family name
328#' in order to e.g. have the first and middle names together as given
329#' name. This approach might be limited to some full name structures.
330#'
331#' \preformatted{  description$del_author(given = NULL, family = NULL, email = NULL,
332#'     role = NULL, comment = NULL, orcid = NULL)
333#' }
334#' Remove an author, or multiple authors. The author(s) to be removed
335#' can be specified via any field(s). All authors matching all
336#' specifications will be removed. E.g. if only \code{given = "Joe"}
337#' is supplied, then all authors whole given name matches \code{Joe} will
338#' be removed. The specifications can be (PCRE) regular expressions.
339#'
340#' \preformatted{  description$add_role(role, given = NULL, family = NULL, email = NULL,
341#'     comment = NULL, orcid = NULL)
342#'     description$add_orcid(orcid, given = NULL, family = NULL, email = NULL,
343#'     comment = NULL, role = NULL)
344#'   description$del_role(role, given = NULL, family = NULL, email = NULL,
345#'      comment = NULL, orcid = NULL)
346#'   description$change_maintainer(given = NULL, family = NULL,
347#'     email = NULL, comment = NULL, orcid = NULL)
348#' }
349#' \code{role} is the role to add or delete. The other arguments
350#' are used to select a subset of the authors, on which the operation
351#' is performed, similarly to \code{$del_author}.
352#'
353#' @section URLs:
354#'
355#' We provide helper functions for manipulating URLs in the \code{URL}
356#' field:
357#'
358#' \preformatted{  description$get_urls()
359#'   description$set_urls(urls)
360#'   description$add_urls(urls)
361#'   description$del_urls(pattern)
362#'   description$clear_urls()
363#' }
364#' \describe{
365#'   \item{urls:}{Character vector of URLs to set or add.}
366#'   \item{pattern:}{Perl compatible regular expression to specify the
367#'     URLs to be removed.}
368#' }
369#' \code{$get_urls()} returns all urls in a character vector. If no URL
370#' fields are present, a zero length vector is returned.
371#'
372#' \code{$set_urls()} sets the URL field to the URLs specified in the
373#' character vector argument.
374#'
375#' \code{$add_urls()} appends the specified URLs to the URL field. It
376#' creates the field if it does not exists. Duplicate URLs are removed.
377#'
378#' \code{$del_urls()} deletes the URLs that match the specified pattern.
379#'
380#' \code{$clear_urls()} deletes all URLs.
381#'
382#' @section Remotes:
383#'
384#' \code{devtools}, \code{remotes} and some other packages support the
385#' non-standard \code{Remotes} field in \code{DESCRIPTION}. This field
386#' can be used to specify locations of dependent packages: GitHub or
387#' BitBucket repositories, generic git repositories, etc. Please see the
388#' \sQuote{Package remotes} vignette in the \code{devtools} package.
389#'
390#' \code{desc} has helper functions for manipulating the \code{Remotes}
391#' field:
392#'
393#' \preformatted{  description$get_remotes()
394#'   description$get_remotes()
395#'   description$set_remotes(remotes)
396#'   description$add_remotes(remotes)
397#'   description$del_remotes(pattern)
398#'   description$clear_remotes()
399#' }
400#' \describe{
401#'   \item{remotes:}{Character vector of remote dependency locations to
402#'     set or add.}
403#'   \item{pattern:}{Perl compatible regular expression to specify the
404#'     remote dependency locations to remove.}
405#' }
406#' \code{$get_remotes()} returns all remotes in a character vector.
407#' If no URL fields are present, a zero length vector is returned.
408#'
409#' \code{$set_remotes()} sets the URL field to the Remotes specified in the
410#' character vector argument.
411#'
412#' \code{$add_remotes()} appends the specified remotes to the
413#' \code{Remotes} field. It creates the field if it does not exists.
414#' Duplicate remotes are removed.
415#'
416#' \code{$del_remotes()} deletes the remotes that match the specified
417#' pattern.
418#'
419#' \code{$clear_remotes()} deletes all remotes.
420#'
421#' @section Built:
422#'
423#' The \sQuote{Built} field is used in binary packages to store information
424#' about when and how a binary package was built.
425#'
426#' \code{$get_built()} returns the built information as a list with fields
427#' \sQuote{R}, \sQuote{Platform}, \sQuote{Date}, \sQuote{OStype}. It throws an
428#' error if the package does not have a \sQuote{Built} field.
429#'
430#' @section Encodings:
431#' When creating a `description` object, `desc` observes the `Encoding`
432#' field, if present, and uses the specified encoding to parse the file.
433#' Internally, it converts all fields to UTF-8.
434#'
435#' When writing a `description` object to a file, `desc` uses the
436#' `Encoding` field (if present), and converts all fields to the specified
437#' encoding.
438#'
439#' We suggest that whenever you need to use non-ASCII characters in your
440#' package, you use the UTF-8 encoding, for maximum portability.
441#'
442#' @export
443#' @importFrom R6 R6Class
444#' @docType class
445#' @format An R6 class.
446#'
447#' @examples
448#' ## Create a template
449#' desc <- description$new("!new")
450#' desc
451#'
452#' ## Read a file
453#' desc2 <- description$new(file = system.file("DESCRIPTION",
454#'                            package = "desc"))
455#' desc2
456#'
457#' ## Remove a field
458#' desc2$del("LazyData")
459#'
460#' ## Add another one
461#' desc2$set(VignetteBuilder = "knitr")
462#' desc2$get("VignetteBuilder")
463#' desc2
464
465description <- R6Class("description",
466  public = list(
467
468    ## Either from a file, or from a character vector
469    initialize = function(cmd = NULL, file = NULL, text = NULL, package = NULL)
470      idesc_create(self, private, cmd, file, text, package),
471
472    write = function(file = NULL)
473      idesc_write(self, private, file),
474
475    fields = function()
476      idesc_fields(self, private),
477
478    has_fields = function(keys)
479      idesc_has_fields(self, private, keys),
480
481    get = function(keys)
482      idesc_get(self, private, keys),
483
484    get_field = function(key, default = stop("Field '", key, "' not found"),
485                         trim_ws = TRUE, squish_ws = trim_ws)
486      idesc_get_field(self, private, key, default, trim_ws, squish_ws),
487
488    get_or_fail = function(keys)
489      idesc_get_or_fail(self, private, keys),
490
491    get_list = function(key, default = stop("Field '", key, "' not found"),
492                        sep = ",", trim_ws = TRUE, squish_ws = trim_ws)
493      idesc_get_list(self, private, key, default, sep, trim_ws, squish_ws),
494
495    set = function(...)
496      idesc_set(self, private, ...),
497
498    set_list = function(key, list_value, sep = ", ")
499      idesc_set_list(self, private, key, list_value, sep),
500
501    del = function(keys)
502      idesc_del(self, private, keys),
503
504    validate = function()
505      idesc_validate(self, private),
506
507    print = function()
508      idesc_print(self, private),
509
510    str = function(by_field = FALSE, normalize = TRUE,
511      mode = c("file", "screen"))
512      idesc_str(self, private, by_field, normalize, mode),
513
514    to_latex = function()
515      idesc_to_latex(self, private),
516
517    normalize = function()
518      idesc_normalize(self, private),
519
520    reformat_fields = function()
521      idesc_reformat_fields(self, private),
522
523    reorder_fields = function()
524      idesc_reorder_fields(self, private),
525
526    ## -----------------------------------------------------------------
527    ## Version numbers
528
529    get_version = function()
530      idesc_get_version(self, private),
531
532    set_version = function(version)
533      idesc_set_version(self, private, version),
534
535    bump_version = function(which)
536      idesc_bump_version(self, private, which),
537
538    ## -----------------------------------------------------------------
539    ## Package dependencies
540
541    set_dep = function(package, type = desc::dep_types, version = "*")
542      idesc_set_dep(self, private, package, match.arg(type), version),
543
544    set_deps = function(deps)
545      idesc_set_deps(self, private, deps),
546
547    get_deps = function()
548      idesc_get_deps(self, private),
549
550    del_dep = function(package, type = c("all", desc::dep_types))
551      idesc_del_dep(self, private, package, match.arg(type)),
552
553    del_deps = function()
554      idesc_del_deps(self, private),
555
556    has_dep = function(package, type = c("any", desc::dep_types))
557      idesc_has_dep(self, private, package, match.arg(type)),
558
559    ## -----------------------------------------------------------------
560    ## Collate fields
561
562    set_collate = function(files, which = c("main", "windows", "unix"))
563      idesc_set_collate(self, private, files, match.arg(which)),
564
565    get_collate = function(which = c("main", "windows", "unix"))
566      idesc_get_collate(self, private, match.arg(which)),
567
568    del_collate = function(which = c("all", "main", "windows", "unix"))
569      idesc_del_collate(self, private, match.arg(which)),
570
571    add_to_collate = function(files,
572      which = c("default", "all", "main", "windows", "unix"))
573      idesc_add_to_collate(self, private, files, match.arg(which)),
574
575    del_from_collate = function(files,
576      which = c("all", "main", "windows", "unix"))
577      idesc_del_from_collate(self, private, files, match.arg(which)),
578
579    ## -----------------------------------------------------------------
580    ## Authors@R
581
582    get_authors = function()
583      idesc_get_authors(self, private),
584
585    get_author = function(role = "cre")
586      idesc_get_author(self, private, role),
587
588    set_authors = function(authors)
589      idesc_set_authors(self, private, authors),
590
591    add_author = function(given = NULL, family = NULL, email = NULL,
592                          role = NULL, comment = NULL, orcid = NULL)
593      idesc_add_author(self, private, given, family, email, role, comment,
594                       orcid),
595
596    add_role = function(role, given = NULL, family = NULL, email = NULL,
597                        comment = NULL, orcid = NULL)
598      idesc_add_role(self, private, role, given, family, email, comment,
599                     orcid),
600
601    add_orcid = function(orcid, given = NULL, family = NULL, email = NULL,
602                        comment = NULL, role = NULL)
603      idesc_add_orcid(self, private, role = role, given = given, family = family,
604                     email = email, comment = comment,
605                     orcid = orcid),
606
607    del_author = function(given = NULL, family = NULL, email = NULL,
608                          role = NULL, comment = NULL, orcid = NULL)
609      idesc_del_author(self, private, given, family, email, role, comment,
610                       orcid),
611
612    del_role = function(role, given = NULL, family = NULL, email = NULL,
613                        comment = NULL, orcid = NULL)
614      idesc_del_role(self, private, role, given, family, email, comment,
615                     orcid),
616
617    change_maintainer = function(given = NULL, family = NULL, email = NULL,
618                                 comment = NULL, orcid = NULL)
619      idesc_change_maintainer(self, private, given, family, email, comment,
620                              orcid),
621
622    add_me = function(role = "ctb", comment = NULL, orcid = NULL)
623      idesc_add_me(self, private, role, comment, orcid),
624
625    add_author_gh = function(username, role = "ctb", comment = NULL, orcid = NULL)
626      idesc_add_author_gh(self, private, role = role,
627                   username = username,
628                   comment = comment, orcid = orcid),
629
630    get_maintainer = function()
631      idesc_get_maintainer(self, private),
632
633    coerce_authors_at_r = function()
634      idesc_coerce_authors_at_r(self, private),
635
636    ## -----------------------------------------------------------------
637    ## URL
638
639    get_urls = function()
640      idesc_get_urls(self, private),
641
642    set_urls = function(urls)
643      idesc_set_urls(self, private, urls),
644
645    add_urls = function(urls)
646      idesc_add_urls(self, private, urls),
647
648    del_urls = function(pattern)
649      idesc_del_urls(self, private, pattern),
650
651    clear_urls = function()
652      idesc_clear_urls(self, private),
653
654    ## -----------------------------------------------------------------
655    ## Remotes
656
657    get_remotes = function()
658      idesc_get_remotes(self, private),
659
660    set_remotes = function(remotes)
661      idesc_set_remotes(self, private, remotes),
662
663    add_remotes = function(remotes)
664      idesc_add_remotes(self, private, remotes),
665
666    del_remotes = function(pattern)
667      idesc_del_remotes(self, private, pattern),
668
669    clear_remotes = function()
670      idesc_clear_remotes(self, private),
671
672    ## -----------------------------------------------------------------
673    ## Built
674
675    get_built = function()
676      idesc_get_built(self, private)
677  ),
678
679  private = list(
680    data = NULL,
681    path = NULL,
682    notws = character()                   # entries without trailing ws
683  )
684)
685
686idesc_create <- function(self, private, cmd, file, text, package) {
687
688  if (!is.null(cmd) && substring(cmd, 1, 1) != "!") {
689    file <- cmd
690    cmd <- NULL
691  }
692
693  if (!is.null(cmd)) {
694    if (!is.null(file)) warning("file argument ignored")
695    if (!is.null(text)) warning("text argument ignored")
696    if (!is.null(package)) warning("package argument ignored")
697    idesc_create_cmd(self, private, cmd)
698
699  } else if (is.null(cmd) && is.null(file) && is.null(text) &&
700             is.null(package)) {
701    idesc_create_file(self, private, ".")
702
703  } else if (!is.null(file)) {
704    if (!is.null(text)) warning("text argument ignored")
705    if (!is.null(package)) warning("package argument ignored")
706    idesc_create_file(self, private, file)
707
708  } else if (!is.null(text)) {
709    if (!is.null(package)) warning("package argument ignored")
710    idesc_create_text(self, private, text)
711
712  } else {
713    idesc_create_package(self, private, package)
714  }
715
716  invisible(self)
717}
718
719idesc_create_cmd <- function(self, private, cmd = c("new")) {
720  stopifnot(is_constructor_cmd(cmd))
721
722  if (cmd == "!new") {
723    txt <-
724'Package: {{ Package }}
725Title: {{ Title }}
726Version: 1.0.0
727Authors@R:
728    c(person(given = "Jo", family = "Doe", email = "jodoe@dom.ain",
729      role = c("aut", "cre")))
730Maintainer: {{ Maintainer }}
731Description: {{ Description }}
732License: {{ License }}
733URL: {{ URL }}
734BugReports: {{ BugReports }}
735Encoding: UTF-8
736'
737    txt <- sub("Authors@R:", "Authors@R: ", txt)
738    idesc_create_text(self, private, text = txt)
739  }
740
741  invisible(self)
742}
743
744idesc_create_file <- function(self, private, file) {
745  stopifnot(is_path(file))
746
747  if (file.exists(file) && is_dir(file)) file <- find_description(file)
748  stopifnot(is_existing_file(file))
749
750  if (is_package_archive(file)) {
751    file <- get_description_from_package(file)
752
753  } else {
754    private$path <- normalizePath(file)
755  }
756
757  tryCatch(
758    lines <- readLines(file),
759    error = function(e) stop("Cannot read ", file, ": ", e$message)
760  )
761
762  idesc_create_text(self, private, lines)
763}
764
765idesc_create_text <- function(self, private, text) {
766  stopifnot(is.character(text))
767  con <- textConnection(text, local = TRUE, encoding = "bytes")
768  on.exit(close(con), add = TRUE)
769  dcf <- read_dcf(con)
770  private$notws <- dcf$notws
771  private$data <- dcf$dcf
772  check_encoding(self, private, NULL)
773}
774
775idesc_create_package <- function(self, private, package) {
776  stopifnot(is_string(package))
777  path <- system.file(package = package, "DESCRIPTION")
778  if (path == "") {
779    stop("Cannot find DESCRIPTION for installed package ", package)
780  }
781  idesc_create_file(self, private, path)
782}
783
784#' @importFrom crayon strip_style
785
786idesc_write <- function(self, private, file) {
787  if (is.null(file)) file <- private$path
788  if (is.null(file)) {
789    stop("Cannot write back DESCRIPTION. Note that it is not possible
790          to update DESCRIPTION files within package archives")
791  }
792
793  mat <- idesc_as_matrix(private$data)
794  if ("Encoding" %in% colnames(mat)) {
795    encoding <- mat[, "Encoding"]
796    mat[] <- iconv(mat[], from = "UTF-8", to = encoding)
797  }
798  # This is to avoid re-encoding
799  Encoding(mat) <- "unknown"
800
801  ## Need to write to a temp file first, to preserve absense of trailing ws
802  tmp <- tempfile()
803  on.exit(unlink(tmp), add = TRUE)
804  write.dcf(mat, file = tmp, keep.white = names(private$data))
805
806  removed <- ! names(private$notws) %in% colnames(mat)
807  if (any(removed)) private$notws <- private$notws[! removed]
808
809  postprocess_trailing_ws(tmp, names(private$notws))
810  if (file.exists(file) && is_dir(file)) file <- find_description(file)
811
812  ofile <- file(file, raw = TRUE, open = "wb+")
813  on.exit(close(ofile), add = TRUE)
814  writeLines(readLines(tmp), ofile)
815
816  invisible(self)
817}
818
819idesc_fields <- function(self, private) {
820  names(private$data)
821}
822
823idesc_has_fields <- function(self, private, keys) {
824  stopifnot(is.character(keys), has_no_na(keys))
825  keys %in% self$fields()
826}
827
828idesc_as_matrix <- function(data) {
829  matrix(
830    vapply(data, "[[", "", "value"),
831    nrow = 1,
832    dimnames = list(NULL, names(data))
833  )
834}
835
836idesc_get <- function(self, private, keys) {
837  stopifnot(is.character(keys), has_no_na(keys))
838  res <- lapply(private$data[keys], "[[", "value")
839  res[vapply(res, is.null, logical(1))] <- NA_character_
840  res <- as.character(unlist(res))
841  names(res) <- keys
842  res
843}
844
845idesc_get_field <- function(self, private, key, default, trim_ws, squish_ws) {
846  stopifnot(is_string(key))
847  stopifnot(is_flag(trim_ws))
848  val <- private$data[[key]]$value
849  if (!is.null(val)) {
850    if (trim_ws) val <- str_trim(val)
851    if (squish_ws) val <- str_squish(val)
852  }
853  val %||% default
854}
855
856idesc_get_or_fail <- function(self, private, keys) {
857  stopifnot(is.character(keys), has_no_na(keys))
858  res <- self$get(keys)
859  if (any(is.na(res))) {
860    w <- is.na(res)
861    msg <- paste0(
862      "Could not find DESCRIPTION ",
863      if (sum(w) == 1) "field: " else "fields: ",
864      paste(sQuote(keys[w]), collapse = ", "),
865      "."
866    )
867    stop(msg, call. = FALSE)
868  }
869  res
870}
871
872idesc_get_list <- function(self, private, key, default, sep, trim_ws, squish_ws) {
873  stopifnot(is_string(key), is_flag(trim_ws), is_flag(squish_ws))
874  val <- private$data[[key]]$value %||% default
875  val <- strsplit(val, sep, fixed = TRUE)[[1]]
876  if (trim_ws) val <- str_trim(val)
877  if (squish_ws) val <- str_squish(val)
878  val
879}
880
881## ... are either
882## - two unnamed arguments, key and value, or
883## - an arbitrary number of named arguments, the names are the keys,
884##   the values are the values
885
886idesc_set <- function(self, private, ...) {
887  args <- list(...)
888
889  if (is.null(names(args)) && length(args) == 2) {
890    keys <- as_string(args[[1]])
891    values <- as_string(args[[2]])
892
893  } else if (!is.null(names(args)) && all(names(args) != "")) {
894    keys <- names(args)
895    values <- unlist(args)
896
897  } else {
898    stop("$set needs two unnamed args, or all named args, see docs")
899  }
900
901  fields <- create_fields(keys, enc2utf8(values))
902  lapply(fields, check_field, warn = TRUE)
903  check_encoding(self, private, lapply(fields, "[[", "value"))
904  private$data[keys] <- fields
905
906  invisible(self)
907}
908
909idesc_set_list <- function(self, private, key, list_value, sep) {
910  stopifnot(is_string(key), is.character(list_value))
911  value <- paste(list_value, collapse = sep)
912  idesc_set(self, private, key, value)
913}
914
915idesc_del <- function(self, private, keys) {
916  stopifnot(is.character(keys), has_no_na(keys))
917  private$data <- private$data[setdiff(names(private$data), keys)]
918  invisible(self)
919}
920