xfun/0000755000175000017500000000000014156202722011365 5ustar nileshnileshxfun/MD50000644000175000017500000001476614156202722011713 0ustar nileshnileshfde56b670e191e297b3883aa7dbe967a *DESCRIPTION 8250820174625f11b20c020347b7dcbf *LICENSE 098b5bb61b3f54dd972521a5e6972bf6 *NAMESPACE 294167efcf3160023e9a3ba08fa4bdcd *NEWS.md 7514edb7e95c22a89c7e128113c9e0a9 *R/api.R 8f724db3b8d1f08b73c26bdb6d8289dc *R/base64.R 2e6e13535e1a7e2e2b52650c012bc67b *R/cache.R c474de9a7511f4a62c4e3b27085c359b *R/command.R 65916e7b842743ea1c780e32a6fe8ba0 *R/cran.R da476e65f9c7e7777b710225920922f9 *R/data-structure.R 852ab016d104e500f36672c2986eea7d *R/encoding.R 806ee424ea2ebf901fd5c6dc06913351 *R/github.R d99ae3a077480364f92ebdbc13151eca *R/image.R 474072003ca8abf53a3beb5437db3e09 *R/io.R ad3ecfb8bcaaaf3f1b125f590f09bed4 *R/json.R 8957df6fb53638c994b66b23d2527e67 *R/markdown.R f6d758bc0c85eb8183e219887611615b *R/os.R d1f541f3e7d2da40e9f1b4d25a1263a2 *R/packages.R b1be6321fae32faaac497ee07dddbba8 *R/paths.R 5ac67fd766ae12bb6dcc2cd67a065708 *R/revcheck.R f1dd49b6d5a6050c1245f5235ea2f46f *R/rstudio.R b4fe11fe4d752389b91eb74b638dd08a *R/session.R 66762e0e52f73a47830c4d2897e9d121 *R/string.R 645758fdc4417b033b9033cf3bcc93a3 *R/utils.R f80b9875d6a99ec9764baac489b82c96 *build/vignette.rds d74d28ea060faa50eabc2cb51dcd7f5f *inst/doc/xfun.R 8d2df1b5212ae9cbf016b6b1dcafa591 *inst/doc/xfun.Rmd ad838d4e80ca9516be4fa10ec63e3e3c *inst/doc/xfun.html 9293b4398c636200df8bf31ce8e6ad3f *inst/scripts/call-fun.R 48b1848f9a4462dce14959981d3dd5ee *inst/scripts/child-pids.sh 4e4105a570be434f2780a252d213d2f0 *man/Rscript.Rd 2095a3404944f96680d87090a5eb4ae3 *man/Rscript_call.Rd 145823073737fb317192a88f96b0b1e8 *man/attr.Rd ca6c7788a264a72d2e3faf0f8841f041 *man/base64_encode.Rd db1d44246fbcf87868eba69b9658599f *man/base64_uri.Rd 6719fc46cae567295cc779acd23b1865 *man/base_pkgs.Rd 51fbefb2a2900335638689ea448e7529 *man/bg_process.Rd 489eabc96ce8fd2dda419985d45a19d4 *man/broken_packages.Rd f625fb2c2cf35d3af13c12c54f3e0ca6 *man/bump_version.Rd 48c8efea1328bb224584b1c1163cabe4 *man/cache_rds.Rd 1db89afb274e3242c8203311b053c486 *man/crandalf_check.Rd ae6faa59cb181830adb3fe5a709dc906 *man/del_empty_dir.Rd 17c8c03f01c5442c94c340c4f0f8dbba *man/dir_create.Rd a81979c659896b5ee7bb1cae68641244 *man/dir_exists.Rd 02fc26d7ef574043275531c603c7b0f7 *man/do_once.Rd 3f7d5ef05c3132da8cfcd64063d3f97a *man/download_file.Rd 0b28e0664be98878cee170bc2b5ce751 *man/embed_file.Rd 143c1962aa4b46f36b0eeaf6033cd151 *man/existing_files.Rd 983d5f39170b08de321bdce8f296906a *man/exit_call.Rd af6732d5714f63a5724c9f862ac00541 *man/file_ext.Rd 4e06ec02507a27ecafdcc920ca11f034 *man/file_string.Rd 9b2199d55eefe9f5fc89398107f7bc32 *man/format_bytes.Rd f87d3e3ea7ed4ee3d9fd1ff92d3f81d3 *man/from_root.Rd 3e3a695bf18c6e184be9a9fef9ffa894 *man/github_releases.Rd cac7f63f5b733f50418416994ea6a6df *man/grep_sub.Rd 09ac3348e9b66291e9ef038b1388d8d8 *man/gsub_file.Rd 12dfedf428830334f6fb5a68e01a34c0 *man/in_dir.Rd dce8624d399af10cae29915e8f1328eb *man/install_dir.Rd 92eb34f64cc68efcdaa6b306b24d9e08 *man/install_github.Rd 6b29efad62d48a40ea8ae985e615f208 *man/isFALSE.Rd 45e103ba007b5fb6d158046757bdeab4 *man/is_R_CMD_check.Rd 16d27d8341634f2bc29fd7dd90da848f *man/is_abs_path.Rd b27aa5dc7640437154c8c94236b1efbf *man/is_ascii.Rd f5b00a908de21bb882212acfd88bd4f0 *man/is_sub_path.Rd 0f3241f25dfd91aec6b8604c1bad452a *man/is_web_path.Rd 7dfd6f1893bc8efe6c5af489d20cf49a *man/magic_path.Rd 3dd4fec6fd5efef35e092a2172366510 *man/mark_dirs.Rd 4f93dc72505cbb2aec19a7608eaca29d *man/msg_cat.Rd 2673261a55dc25a250876ac35e47cca2 *man/native_encode.Rd 4908a401d737bfc0d7544706f5ade82b *man/news2md.Rd d14fd644e7f358e5a86bb8e150130409 *man/normalize_path.Rd 736537ffe052881a429024524db0b22d *man/numbers_to_words.Rd f5e104151d84e332e189c69572e4600d *man/optipng.Rd 172f886a4e06c47f9cb05f06781d3072 *man/os.Rd 7cbfa9aa2787f1abe314fcd39fd320c4 *man/parse_only.Rd 734421228bb595e8c6702bfdf9897b5c *man/pkg_attach.Rd c13f49676720646fd4bb8d6187f1dc1f *man/proc_kill.Rd c30c82ba0cb1bfc6b984cb27925edfea *man/process_file.Rd 52d2a5363ff48c1c6cfa3c5cef9d5f13 *man/proj_root.Rd beb2cf47c519361c8e3dbe13ca0f3b9e *man/prose_index.Rd 30ea18271d700ba35e246dfc4683d190 *man/protect_math.Rd 1a5280c440d309598289acac4358d325 *man/raw_string.Rd 6dbfe43344fbc9bbb3cd3a02709652e2 *man/read_all.Rd fb0fe349704b92dd4857fe9ff93c46cb *man/read_bin.Rd 4a9885ee1c7d6dc9b82547726b4e6a66 *man/read_utf8.Rd 7b8f8c176c529fc33ac10bf995448eaf *man/relative_path.Rd 93e07573f831ea574bbecf858abb36ad *man/rename_seq.Rd 71ea0a134abe22c69f2637a123c4b66a *man/rest_api.Rd fcd23b53a0ae6faba958317ead0ea294 *man/retry.Rd 2c191fa12748c7a2fa1254f404215a48 *man/rev_check.Rd e58fe40d97ec051d35da2ab08145b108 *man/rstudio_type.Rd ed767135afc65d66523e669608fae972 *man/same_path.Rd 21315a257e15b1c713c9f5d189155fc4 *man/session_info.Rd eafb96b44b0b6024ca5bf38fbee6a4e1 *man/set_envvar.Rd 15db5d87f4cc66894ea4b1e972ab22b7 *man/split_lines.Rd 756826889c63014ff14dd88c1ffb68dd *man/split_source.Rd 6b826a293afba42edddf52412f11aeb6 *man/strict_list.Rd 8779a107140a0f5caeba6f5b08087733 *man/stringsAsStrings.Rd a9b52e9187526c818b365eb7dfb854f1 *man/submit_cran.Rd 3260665d8d414b55805b7ca127f26dbd *man/system3.Rd f71cda431eef30f13eba86189f5595eb *man/tinify.Rd 4331e8a8ad2287858a3fb8654ea0a4f4 *man/tojson.Rd 8fb0d37a55e8a31c007f47d921b62b79 *man/tree.Rd c36d9132b3151a75d8a8439e66caad4e *man/try_error.Rd 1f21a2a55da87a2ba8ae5f053e133860 *man/try_silent.Rd adcdbea1761c1afde1b6fa203c0c7384 *man/upload_ftp.Rd de02426f6899317ea568ac93c1f98034 *man/url_accessible.Rd 4030df7964f5ccc19b8c8da7e9109ffa *man/url_filename.Rd 0254214fc4c894d20c7031e1367d886d *man/valid_syntax.Rd e3d3cb360fbfdb3c6974e14eb5f09870 *src/Makevars fa80cdcc801757b3bc591312ecaf1942 *src/base64.c 89a01103c4d3a20538970ed9ff993e4f *src/init.c 5c6caacc74f72def39c4cc64d980c71b *tests/test-ci.R fe60425d528bf3786b7f3c84eb595307 *tests/test-ci/test-cran.R 040b51b8b64bc6d060bfb339214a1b9c *tests/test-ci/test-revcheck.R 82604fe7a77a94208c71d3d2093044a6 *tests/test-cran.R d22e97c07da3639ddd1204ca0ccdbd7e *tests/test-cran/test-base64.R 5cfa000e0acd7844a06a1899db8f671e *tests/test-cran/test-command.R 64585c8d0560cadc3166b78501678284 *tests/test-cran/test-data-structure.R e61a29b8fe1cc7165d75e8c59c5864e7 *tests/test-cran/test-encoding.R 3a4db13fe05553a3752cd8cc65a4ac90 *tests/test-cran/test-io.R ba1eca1ad0cb05809de01075c324b84b *tests/test-cran/test-json.R 6fd6e9f776b294342ef714104d2ec7cf *tests/test-cran/test-markdown.R 6a92c86595dff18728b9be676a066d38 *tests/test-cran/test-packages.R 8fcf9f5c3eb9c835efc6689dfd549a64 *tests/test-cran/test-paths.R f11df567ae059fd1d364f8290a778757 *tests/test-cran/test-string.R 70e2184db79e0acd800d8cac64a5cd65 *tests/test-cran/test-utils.R 8d2df1b5212ae9cbf016b6b1dcafa591 *vignettes/xfun.Rmd xfun/NEWS.md0000644000175000017500000004062114156156426012477 0ustar nileshnilesh# CHANGES IN xfun VERSION 0.29 - `github_releases()` can fetch all releases (tags) of a Github repo now. - Added an argument `.error` to `download_file()` so that users can customize the error message when the download fails. - Added functions `rest_api_raw()` and `rest_api()` to get data from a REST API; also added the function `github_api()` to get data from the Github API based on `rest_api_raw()`. - Added a wrapper function `system3()` based on `system2()` to mark the character output of `system2()` as UTF-8 if appropriate. - Added a function `existing_files()` to return file paths that exist (a shorthand of `x[file.exists(x)]`). - Added a function `read_all()` to read multiple files and concatenate the content into a character vector. - `url_accessible()` uses `curlGetHeaders()` by default (instead of `download_file()`) to test if a URL is accessible when the **curl** package is not available. - When `options(xfun.rev_check.compare = FALSE)`, `rev_check()` will run `R CMD check` on reverse dependencies against a source package but not the CRAN version of this package. By default, this option is `TRUE`, meaning that `R CMD check` will run against both versions of the package. # CHANGES IN xfun VERSION 0.28 - Added a new function `url_accessible()` to test if a URL can be downloaded. - Added a new function `try_error()` to try an expression and see if it throws an error. # CHANGES IN xfun VERSION 0.27 - Exported and documented the function `xfun::base_pkgs()` (to return base R package names). - Changed the default value of the `status_only` argument of `compare_Rcheck()` from `FALSE` to `TRUE`. - Added new functions `crandalf_check()` and `crandalf_results()` for checking (especially large numbers of) reverse dependencies of packages via [**crandalf**](https://github.com/yihui/crandalf). - Added new functions `append_utf8()` and `append_unique()` based on `read_utf8()` and `write_utf8()` to append content to files or connections. # CHANGES IN xfun VERSION 0.26 - The `windows_only` argument of `native_encode()` has been removed. Now `native_encode()` only tries the conversion to native encoding on platforms where `l10n_info()[['UTF-8']]` does not return `TRUE`. - Added a `solaris` argument to `upload_win_builder()`. # CHANGES IN xfun VERSION 0.25 - Fixed a bug in `broken_packages()` (thanks, @PythonCoderUnicorn, rstudio/rmarkdown#1990). - Added a `files` argument to `optipng()` so that users can specify the list of PNG files instead of running `optipng` on a whole directory. # CHANGES IN xfun VERSION 0.24 - Exported the internal function `broken_packages()` to reinstall broken R packages. - Fixed the bug in `proj_root()` #54 (thanks, @clarkliming). # CHANGES IN xfun VERSION 0.23 ## NEW FEATURES - Added a `tinify()` function to compress PNG/JPEG images via [the Tinify API](https://tinypng.com/developers). - Added a `news2md()` function to convert package news to the Markdown format. This is mainly for converting the plain-text `NEWS` file and the `NEWS.Rd` file to `NEWS.md`. - Added a `format_bytes()` function to format numbers of bytes using a specified unit, e.g., `1024` can be formatted as `1 Kb`. - When using `pkg_load2()` in an **renv** project, it will use `renv::install()` to install missing packages by default to take advantage of **renv**'s caching feature (thanks, @chunyunma @cderv, #52). - `upload_win_builder()` no longer requires the system command `curl` to be available; if `curl` is not available, the R package **curl** will be used instead, which means this R package must be installed. In addition to uploading to the `ftp` server of win-builder, it's also possible to upload to : call `upload_win_builder(..., server = 'https')`. This change was made so that it would be possible to continue to upload to win-builder in case it should stop supporting `ftp` (CRAN has discouraged package authors from using `ftp://`). ## BUG FIXES - Backticks are added to math environments by mistake when `\begin{}` and `\end{}` do not match (thanks, @oliviergimenez, #51). ## MINOR CHANGES - The argument `src` was renamed to `pkg` in `install_dir()`. - The argument `file` of `upload_win_builder()` defaults to `pkg_build()` now, i.e., by default, it will build a source package and upload it, so you do not need to build the package separately. # CHANGES IN xfun VERSION 0.22 ## NEW FEATURES - `relative_path()` is vectorized now. - Added a new function `retry()` to retry calling a function for a number of times in case of errors. - Added a new function `sort_file()`, which is a shorthand for `process_file(fun = sort)` to sort the lines in a text file. ## MAJOR CHANGES - The argument `FUN` was renamed to `fun` in `process_file()`. ## MINOR CHANGES - Inside `download_file()`, the `timeout` option in `options()` is set to 3600 seconds when it takes the default value of 60 seconds, which may not be enough for downloading large files (thanks, @matthewgson, yihui/tinytex#286). # CHANGES IN xfun VERSION 0.21 ## NEW FEATURES - Added a new function `pkg_available()` to test if a package with a minimal version is available (thanks, @cderv, #45). - Added a new function `set_envvar()` to set environment variables and return their old values, so they could be restored later. - Added a new function `exit_call()` to call a function when a parent function exits. - Exported the internal function `read_bin()`. - Added an argument `verbose` to `bg_process()`. - `Rscript_call()` gains an `options` argument to pass command-line options to `Rscript` (thanks, @cderv, #48). # CHANGES IN xfun VERSION 0.20 ## NEW FEATURES - Added a function `msg_cat()` to generate a message with `cat()`. See the help page `?xfun::msg_cat` for more information. - Added a function `mark_dirs()` to mark directories with a trailing slash in a vector of paths to differentiate them from normal filenames (#44). ## BUG FIXES - `xfun::proc_kill()` failed to work on *nix. - `xfun::del_empty_dir()` failed to delete empty dirs. - `xfun::file_string()` preserves emptiness (thanks, @MichaelChirico, #38). - `xfun::raw_string()` preserves the class(es) of the input now (thanks, @MichaelChirico, #40). ## MINOR CHANGES - Exported the function `dir_create()`. # CHANGES IN xfun VERSION 0.19 ## NEW FEATURES - Added functions `bg_process()` to run a command in a background process, and `proc_kill()` to kill a process. - Added a function `del_empty_dir()` to delete a directory if it is empty. - Added functions `is_abs_path()` and `is_rel_path()` to check if paths are absolute or relative. - Added a function `is_sub_path()` to test if a path is under a directory. - Added a function `is_web_path()` to test if a path is a web path that starts with `http://` or `https://` or `ftp://` or `ftps://`. - Documented and exported the previously internal functions `dir_exists()` and `file_exists()` (thanks, @cderv, #36). - Added a function `dir_create()` to create a directory recursively by default when it does not exist. - Added an argument `fail` to `Rscript_call()` to allow users to customize the error message when an error occurred in calling the function in a new R session. ## MINOR CHANGES - `file_ext()`, `sans_ext()`, and `with_ext()` no longer use `tools::file_ext()` or `tools::file_path_sans_ext()`, but provide a slightly different implementation. They treat `tar.(gz|bz2|xz)` and `nb.html` as file extensions, and also allow extensions to contain a trailing `~` or `#`. # CHANGES IN xfun VERSION 0.18 ## NEW FEATURES - Added a function `grep_sub()` to perform replacement with `gsub()` on elements matched from `grep()`. - Added a function `github_releases()` to obtain the tags from the Github releases of a repo. - Added a function `bump_version()` to increase the last digit of version numbers by one. - Moved a function `process_file()` from the **blogdown** package to this package, and documented it. - Added a function `valid_syntax()` to check if an R code fragment is syntactically valid. This function was moved from the **highr** package. - Added a function `url_filename()` to extract filenames from URLs. This function is used by `download_file()` to determine the default output filename. - Added a function `do_once()` to perform a task once in an R session. - Added a function `proj_root()` to find the root directory of a project. Currently it only supports R package projects and RStudio projects by default. - Added a function `relative_path()` to calculate the relative path of a path relative to a directory. - Added a function `from_root()`, which is similar to `here::here()` but returns a relative path instead of an absolute path. - Added a function `magic_path()` that, given an incomplete input path, tries to find the actual path recursively under subdirectories of a root directory. For example, users may only provide a base filename, and `magic_path()` will look for this file under subdirectories and return the actual path if it is found. ## MINOR CHANGES - Now `download_file()` tries the download method `winnet` first (previously it was `libcurl`) on Windows (thanks, @cderv, #33). # CHANGES IN xfun VERSION 0.17 ## NEW FEATURES - Supports `xfun::pkg_attach(packages, install = "pak")`, i.e., use `pak::pkg_install()` to install a package when it is not installed (thanks, @GitHunter0, #32). - Added a new function `xfun::split_source()` to split lines of R source code into minimal complete expressions. This function was moved from the **highr** package. # CHANGES IN xfun VERSION 0.16 - Added a new function `base64_decode()` to decode data from the base64 encoding (thanks, @wush978, #31). # CHANGES IN xfun VERSION 0.15 ## NEW FEATURES - Added a new function `tree()`, which is based on `str()` in base R, but changes the output of `str()` into a tree diagram to make it easier to understand nested data structures. - Added a new function `base64_encode()` to encode data into the base64 encoding (thanks, @wush978, #27). - Added a new function `base64_uri()` to generate the Data URI (or Data URL) for a file. ## BUG FIXES - Fenced code blocks commented out in `` are not longer recognized as code blocks but prose (thanks, @jarauh, #25). # CHANGES IN xfun VERSION 0.14 ## NEW FEATURES - The `cache_rds()` function can invalidate the cache automatically when the code passed to its `expr` argument has changed. Two new arguments, `hash` and `clean` were added to this function to make it more useful and powerful. See the help page `?xfun::cache_rds()` for more information. # CHANGES IN xfun VERSION 0.13 ## NEW FEATURES - Added a new function `cache_rds()` to cache an R expression to a `*.rds` file. - Added a new function `Rscript_call()` to call a function (with arguments) in a new R session via the command `Rscript`. - The `recheck` argument of `rev_check()` can take a vector of package names, and only these packages will be checked. See `?xfun::rev_check` for more details. # CHANGES IN xfun VERSION 0.12 ## NEW FEATURES - Added a new function `split_lines()`. # CHANGES IN xfun VERSION 0.11 ## BUG FIXES - `read_utf8()` will read the file with `options(encoding = 'native.enc')` and ignore user's setting such as `options(encoding = 'UTF-8')` (#21). # CHANGES IN xfun VERSION 0.10 ## NEW FEATURES - Added the function `as_strict_list()` to convert an existing object to a strict list without wrapping it in another list if the object already is of type list (in contrast to how `strict_list()` behaves) (thanks, @salim-b, #20). # CHANGES IN xfun VERSION 0.9 ## NEW FEATURES - Added a function `rename_seq()` to rename files to add an incremental numeric prefix to the filenames, e.g., rename `a.txt`, `b.txt`, `c.txt` to `1-a.txt`, `2-b.txt`, `3-c.txt`. # CHANGES IN xfun VERSION 0.8 ## MINOR CHANGES - `xfun::write_utf8(NULL)` is equivalent to `xfun::write_utf8(character(0))` now (thanks, @schloerke, yihui/knitr#1714). # CHANGES IN xfun VERSION 0.7 ## MINOR CHANGES - `loadable()` is quiet with R 3.6.0 (https://stat.ethz.ch/pipermail/r-devel/2019-May/077774.html). # CHANGES IN xfun VERSION 0.6 ## NEW FEATURES - Added the `...` argument to `same_path()` to pass additional arguments to `normalize_path()`. ## BUG FIXES - The `warn` argument in `prose_index()` failed to suppress warnings. # CHANGES IN xfun VERSION 0.5 ## NEW FEATURES - Added functions `upload_ftp()` and `upload_win_builder()` to upload files to FTP servers. - Added a function `stringsAsStrings()` (see its help page for details). - Added an argument `warn` to `prose_index()` to suppress the warning when code fences are not balanced. ## BUG FIXES - Fixed the bug that `prose_index()` recognizes double backticks as code fences (thanks, @shrektan, #14 #15). # CHANGES IN xfun VERSION 0.4 ## NEW FEATURES - Added functions `embed_file()`, `embed_dir()`, and `embed_files()` to embed files in an HTML output file (e.g., from R Markdown), so that the files can be directly downloaded from the web browser. One use case is to call one of these functions in an R code chunk of an Rmd document to embed the Rmd source document or data files in the HTML output, so readers can download them. - Added a new argument `message` to `pkg_attach()`, so you can suppress package startup messages via `xfun::pkg_attach(..., message = FALSE)` or set the global option `options(xfun.pkg_attach.message = FALSE)` (thanks, @wch, yihui/knitr#1583). ## MINOR CHANGES - The argument `rw_error` was moved from `gsub_dir()` to `gsub_file()` (`gsub_dir(rw_error = ...)` will still work). - `is_ascii()` now returns `NA` for `NA_character_` (thanks, @shrektan, #8 #9). # CHANGES IN xfun VERSION 0.3 ## NEW FEATURES - Added a new functions `download_file()` to try various methods to download a file. - Added a new function `is_ascii()` to test if a character vector only consists of ASCII characters. - Added a new function `numbers_to_words()` to convert numbers to English words (thanks, @daijiang, #3). # CHANGES IN xfun VERSION 0.2 ## NEW FEATURES - Added a `new_session` argument to `loadable()`. - Added new functions `gsub_file()`, `gsub_files()`, `gsub_dir()`, and `gsub_ext()` to replace strings in files. - Added new functions `Rscript` and `Rcmd` as wrappers of `system2('Rscript')` and `system2('R', 'CMD')`, respectively. - Added a new function `install_dir()` to install a source package from a directory. - Added a new function `file_string()` to read a text file (encoded in UTF-8) and return its content a single character string (lines concatenated by `\n`). - Added a new function `raw_string()` to print a character vector in its "raw" form using `cat(..., sep = '\n')` instead of `print()`, because the latter may introduce `[1]`, "extra" double quotes, and escape sequences, which are not very human-readable. - Added a new function `session_info()` as an alternative to `sessionInfo()`. - Added a new function `rev_check()` to run `R CMD check` on the reverse dependencies of a package, and a corresponding helper function `compare_Rcheck()` for showing the differences in logs with the CRAN version and the current version of the package, respectively. - Added new functions for dealing with Markdown text: `prose_index()` returns the line indices of text that is prose (not code blocks), and `protect_math()` protects math expressions in Markdown in backticks. - Added an `error` argument to `read_utf8()` to signal an error if the file is not encoded in UTF-8. # CHANGES IN xfun VERSION 0.1 ## NEW FEATURES - `attr()` as an abbreviation of `base::attr(exact = TRUE)`. - `file_ext()`, `sans_ext()`, and `with_ext()` to manipulate extensions in filenames. - `in_dir()` to evaluate an R expression in a directory. - `isFALSE()` as an abbreviation of `identical(x, FALSE)`. - `is_windows()`, `is_macos()`, `is_linux()`, and `is_unix()` to test operating systems. - `native_encode()` to try to encode a character vector in the native encoding. - `normalize_path()` as an abbreviation of `normalizePath(winslash = '/', mustWork = FALSE)`. - `optipng()` to run the command `optipng` to optimize all PNG files under a directory. - `parse_only()` parses R code without keeping the source references. - `pkg_attach()` and `pkg_load()` to attach and load a vector of packages, respectively (and optionally, install the missing packages). - `read_utf8()` and `write_utf8()` to read and write UTF-8 files, respectively. - `same_path()` to test if two paths are the same. - `strict_list()` is a version of `list()` that disables partial matching of the `$` operator. - `tojson()` is a simple JSON serializer. - `try_silent()` is an abbreviation of `try(silent = TRUE)`. xfun/DESCRIPTION0000644000175000017500000000260214156202722013073 0ustar nileshnileshPackage: xfun Type: Package Title: Supporting Functions for Packages Maintained by 'Yihui Xie' Version: 0.29 Authors@R: c( person("Yihui", "Xie", role = c("aut", "cre", "cph"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666")), person("Wush", "Wu", role = "ctb"), person("Daijiang", "Li", role = "ctb"), person("Xianying", "Tan", role = "ctb"), person("Salim", "Brüggemann", role = "ctb", email = "salim-b@pm.me", comment = c(ORCID = "0000-0002-5329-5987")), person("Christophe", "Dervieux", role = "ctb"), person() ) Description: Miscellaneous functions commonly used in other packages maintained by 'Yihui Xie'. Imports: stats, tools Suggests: testit, parallel, codetools, rstudioapi, tinytex (>= 0.30), mime, markdown, knitr, htmltools, remotes, pak, rhub, renv, curl, jsonlite, rmarkdown License: MIT + file LICENSE URL: https://github.com/yihui/xfun BugReports: https://github.com/yihui/xfun/issues Encoding: UTF-8 RoxygenNote: 7.1.2 VignetteBuilder: knitr NeedsCompilation: yes Packaged: 2021-12-14 20:38:20 UTC; yihui Author: Yihui Xie [aut, cre, cph] (), Wush Wu [ctb], Daijiang Li [ctb], Xianying Tan [ctb], Salim Brüggemann [ctb] (), Christophe Dervieux [ctb] Maintainer: Yihui Xie Repository: CRAN Date/Publication: 2021-12-14 21:00:02 UTC xfun/man/0000755000175000017500000000000014145515322012141 5ustar nileshnileshxfun/man/url_accessible.Rd0000644000175000017500000000155114156162700015411 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{url_accessible} \alias{url_accessible} \title{Test if a URL is accessible} \usage{ url_accessible(x, use_curl = !capabilities("libcurl"), ...) } \arguments{ \item{x}{A URL as a character string.} \item{use_curl}{Whether to use the \pkg{curl} package or the \code{curlGetHeaders()} function in base R to send the request to the URL. By default, \pkg{curl} will be used when base R does not have the \command{libcurl} capability (which should be rare).} \item{...}{Arguments to be passed to \code{curlGetHeaders()}.} } \value{ \code{TRUE} or \code{FALSE}. } \description{ Try to send a \code{HEAD} request to a URL using \code{\link{curlGetHeaders}()} or the \pkg{curl} package, and see if it returns a successful status code. } \examples{ xfun::url_accessible("https://yihui.org") } xfun/man/dir_create.Rd0000644000175000017500000000123214156162700014527 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{dir_create} \alias{dir_create} \title{Create a directory recursively by default} \usage{ dir_create(x, recursive = TRUE, ...) } \arguments{ \item{x}{A path name.} \item{recursive}{Whether to create all directory components in the path.} \item{...}{Other arguments to be passed to \code{\link{dir.create}()}.} } \value{ A logical value indicating if the directory either exists or is successfully created. } \description{ First check if a directory exists. If it does, return \code{TRUE}, otherwise create it with \code{\link{dir.create}(recursive = TRUE)} by default. } xfun/man/base64_encode.Rd0000644000175000017500000000203314156162700015027 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/base64.R \name{base64_encode} \alias{base64_encode} \alias{base64_decode} \title{Encode/decode data into/from base64 encoding.} \usage{ base64_encode(x) base64_decode(x, from = NA) } \arguments{ \item{x}{For \code{base64_encode()}, a raw vector. If not raw, it is assumed to be a file or a connection to be read via \code{readBin()}. For \code{base64_decode()}, a string.} \item{from}{If provided (and \code{x} is not provided), a connection or file to be read via \code{readChar()}, and the result will be passed to the argument \code{x}.} } \value{ \code{base64_encode()} returns a character string. \code{base64_decode()} returns a raw vector. } \description{ The function \code{base64_encode()} encodes a file or a raw vector into the base64 encoding. The function \code{base64_decode()} decodes data from the base64 encoding. } \examples{ xfun::base64_encode(as.raw(1:10)) logo = xfun:::R_logo() xfun::base64_encode(logo) xfun::base64_decode("AQIDBAUGBwgJCg==") } xfun/man/session_info.Rd0000644000175000017500000000360214156162700015127 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/session.R \name{session_info} \alias{session_info} \title{An alternative to sessionInfo() to print session information} \usage{ session_info(packages = NULL, dependencies = TRUE) } \arguments{ \item{packages}{A character vector of package names, of which the versions will be printed. If not specified, it means all loaded and attached packages in the current R session.} \item{dependencies}{Whether to print out the versions of the recursive dependencies of packages.} } \value{ A character vector of the session information marked as \code{\link{raw_string}()}. } \description{ This function tweaks the output of \code{\link{sessionInfo}()}: (1) It adds the RStudio version information if running in the RStudio IDE; (2) It removes the information about matrix products, BLAS, and LAPACK; (3) It removes the names of base R packages; (4) It prints out package versions in a single group, and does not differentiate between loaded and attached packages. } \details{ It also allows you to only print out the versions of specified packages (via the \code{packages} argument) and optionally their recursive dependencies. For these specified packages (if provided), if a function \code{xfun_session_info()} exists in a package, it will be called and expected to return a character vector to be appended to the output of \code{session_info()}. This provides a mechanism for other packages to inject more information into the \code{session_info} output. For example, \pkg{rmarkdown} (>= 1.20.2) has a function \code{xfun_session_info()} that returns the version of Pandoc, which can be very useful information for diagnostics. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} xfun::session_info() if (xfun::loadable("MASS")) xfun::session_info("MASS") \dontshow{\}) # examplesIf} } xfun/man/in_dir.Rd0000644000175000017500000000074014156162700013675 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{in_dir} \alias{in_dir} \title{Evaluate an expression under a specified working directory} \usage{ in_dir(dir, expr) } \arguments{ \item{dir}{Path to a directory.} \item{expr}{An R expression.} } \description{ Change the working directory, evaluate the expression, and restore the working directory. } \examples{ library(xfun) in_dir(tempdir(), { print(getwd()) list.files() }) } xfun/man/strict_list.Rd0000644000175000017500000000341314156162700014774 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/data-structure.R \name{strict_list} \alias{strict_list} \alias{as_strict_list} \alias{$.xfun_strict_list} \alias{print.xfun_strict_list} \title{Strict lists} \usage{ strict_list(...) as_strict_list(x) \method{$}{xfun_strict_list}(x, name) \method{print}{xfun_strict_list}(x, ...) } \arguments{ \item{...}{Objects (list elements), possibly named. Ignored in the \code{print()} method.} \item{x}{For \code{as_strict_list()}, the object to be coerced to a strict list. For \code{print()}, a strict list.} \item{name}{The name (a character string) of the list element.} } \value{ Both \code{strict_list()} and \code{as_strict_list()} return a list with the class \code{xfun_strict_list}. Whereas \code{as_strict_list()} attempts to coerce its argument \code{x} to a list if necessary, \code{strict_list()} just wraps its argument \code{...} in a list, i.e., it will add another list level regardless if \code{...} already is of type list. } \description{ A strict list is essentially a normal \code{\link{list}()} but it does not allow partial matching with \code{$}. } \details{ To me, partial matching is often more annoying and surprising than convenient. It can lead to bugs that are very hard to discover, and I have been bitten by it many times. When I write \code{x$name}, I always mean precisely \code{name}. You should use a modern code editor to autocomplete the \code{name} if it is too long to type, instead of using partial names. } \examples{ library(xfun) (z = strict_list(aaa = "I am aaa", b = 1:5)) z$a # NULL! z$aaa # I am aaa z$b z$c = "create a new element" z2 = unclass(z) # a normal list z2$a # partial matching z3 = as_strict_list(z2) # a strict list again z3$a # NULL again! } xfun/man/gsub_file.Rd0000644000175000017500000000325014156162700014367 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{gsub_file} \alias{gsub_file} \alias{gsub_files} \alias{gsub_dir} \alias{gsub_ext} \title{Search and replace strings in files} \usage{ gsub_file(file, ..., rw_error = TRUE) gsub_files(files, ...) gsub_dir(..., dir = ".", recursive = TRUE, ext = NULL, mimetype = ".*") gsub_ext(ext, ..., dir = ".", recursive = TRUE) } \arguments{ \item{file}{Path of a single file.} \item{...}{For \code{gsub_file()}, arguments passed to \code{gsub()}. For other functions, arguments passed to \code{gsub_file()}. Note that the argument \code{x} of \code{gsub()} is the content of the file.} \item{rw_error}{Whether to signal an error if the file cannot be read or written. If \code{FALSE}, the file will be ignored (with a warning).} \item{files}{A vector of file paths.} \item{dir}{Path to a directory (all files under this directory will be replaced).} \item{recursive}{Whether to find files recursively under a directory.} \item{ext}{A vector of filename extensions (without the leading periods).} \item{mimetype}{A regular expression to filter files based on their MIME types, e.g., \code{'^text/'} for plain text files. This requires the \pkg{mime} package.} } \description{ These functions provide the "file" version of \code{\link{gsub}()}, i.e., they perform searching and replacement in files via \code{gsub()}. } \note{ These functions perform in-place replacement, i.e., the files will be overwritten. Make sure you backup your files in advance, or use version control! } \examples{ library(xfun) f = tempfile() writeLines(c("hello", "world"), f) gsub_file(f, "world", "woRld", fixed = TRUE) readLines(f) } xfun/man/prose_index.Rd0000644000175000017500000000155114156162700014751 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/markdown.R \name{prose_index} \alias{prose_index} \title{Find the indices of lines in Markdown that are prose (not code blocks)} \usage{ prose_index(x, warn = TRUE) } \arguments{ \item{x}{A character vector of text in Markdown.} \item{warn}{Whether to emit a warning when code fences are not balanced.} } \value{ An integer vector of indices of lines that are prose in Markdown. } \description{ Filter out the indices of lines between code block fences such as \verb{```} (could be three or four or more backticks). } \note{ If the code fences are not balanced (e.g., a starting fence without an ending fence), this function will treat all lines as prose. } \examples{ library(xfun) prose_index(c("a", "```", "b", "```", "c")) prose_index(c("a", "````", "```r", "1+1", "```", "````", "c")) } xfun/man/numbers_to_words.Rd0000644000175000017500000000236314156162700016027 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string.R \name{numbers_to_words} \alias{numbers_to_words} \alias{n2w} \title{Convert numbers to English words} \usage{ numbers_to_words(x, cap = FALSE, hyphen = TRUE, and = FALSE) n2w(x, cap = FALSE, hyphen = TRUE, and = FALSE) } \arguments{ \item{x}{A numeric vector. Values should be integers. The absolute values should be less than \code{1e15}.} \item{cap}{Whether to capitalize the first letter of the word. This can be useful when the word is at the beginning of a sentence. Default is \code{FALSE}.} \item{hyphen}{Whether to insert hyphen (-) when the number is between 21 and 99 (except 30, 40, etc.).} \item{and}{Whether to insert \code{and} between hundreds and tens, e.g., write 110 as \dQuote{one hundred and ten} if \code{TRUE} instead of \dQuote{one hundred ten}.} } \value{ A character vector. } \description{ This can be helpful when writing reports with \pkg{knitr}/\pkg{rmarkdown} if we want to print numbers as English words in the output. The function \code{n2w()} is an alias of \code{numbers_to_words()}. } \examples{ library(xfun) n2w(0, cap = TRUE) n2w(0:121, and = TRUE) n2w(1e+06) n2w(1e+11 + 12345678) n2w(-987654321) n2w(1e+15 - 1) } \author{ Daijiang Li } xfun/man/tojson.Rd0000644000175000017500000000222214156162700013742 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/json.R \name{tojson} \alias{tojson} \alias{json_vector} \title{A simple JSON serializer} \usage{ tojson(x) json_vector(x, to_array = FALSE, quote = TRUE) } \arguments{ \item{x}{An R object.} \item{to_array}{Whether to convert a vector to a JSON array (use \code{[]}).} \item{quote}{Whether to double quote the elements.} } \value{ A character string. } \description{ A JSON serializer that only works on a limited types of R data (\code{NULL}, lists, logical scalars, character/numeric vectors). A character string of the class \code{JS_EVAL} is treated as raw JavaScript, so will not be quoted. The function \code{json_vector()} converts an atomic R vector to JSON. } \examples{ library(xfun) tojson(NULL) tojson(1:10) tojson(TRUE) tojson(FALSE) cat(tojson(list(a = 1, b = list(c = 1:3, d = "abc")))) cat(tojson(list(c("a", "b"), 1:5, TRUE))) # the class JS_EVAL is originally from htmlwidgets::JS() JS = function(x) structure(x, class = "JS_EVAL") cat(tojson(list(a = 1:5, b = JS("function() {return true;}")))) } \seealso{ The \pkg{jsonlite} package provides a full JSON serializer. } xfun/man/grep_sub.Rd0000644000175000017500000000124114156162700014234 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{grep_sub} \alias{grep_sub} \title{Perform replacement with \code{gsub()} on elements matched from \code{grep()}} \usage{ grep_sub(pattern, replacement, x, ...) } \arguments{ \item{pattern, replacement, x, ...}{Passed to \code{\link{grep}()} and \code{gsub()}.} } \value{ A character vector. } \description{ This function is a shorthand of \code{gsub(pattern, replacement, grep(pattern, x, value = TRUE))}. } \examples{ # find elements that matches 'a[b]+c' and capitalize 'b' with perl regex xfun::grep_sub("a([b]+)c", "a\\\\U\\\\1c", c("abc", "abbbc", "addc", "123"), perl = TRUE) } xfun/man/bump_version.Rd0000644000175000017500000000111314156162700015134 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string.R \name{bump_version} \alias{bump_version} \title{Bump version numbers} \usage{ bump_version(x) } \arguments{ \item{x}{A vector of version numbers (of the class \code{"numeric_version"}), or values that can be coerced to version numbers via \code{as.numeric_version()}.} } \value{ A vector of new version numbers. } \description{ Increase the last digit of version numbers, e.g., from \code{0.1} to \code{0.2}, or \code{7.23.9} to \code{7.23.10}. } \examples{ xfun::bump_version(c("0.1", "91.2.14")) } xfun/man/optipng.Rd0000644000175000017500000000121114156162700014103 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{optipng} \alias{optipng} \title{Run OptiPNG on all PNG files under a directory} \usage{ optipng( dir = ".", files = list.files(dir, "[.]png$", recursive = TRUE, full.names = TRUE), ... ) } \arguments{ \item{dir}{Path to a directory.} \item{files}{Alternatively, you can choose the specific files to optimize.} \item{...}{Arguments to be passed to \code{system2()}.} } \description{ Call the command \command{optipng} via \code{system2()} to optimize all PNG files under a directory. } \references{ OptiPNG: \url{http://optipng.sourceforge.net}. } xfun/man/Rscript.Rd0000644000175000017500000000113314156162700014054 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{Rscript} \alias{Rscript} \alias{Rcmd} \title{Run the commands \command{Rscript} and \command{R CMD}} \usage{ Rscript(args, ...) Rcmd(args, ...) } \arguments{ \item{args}{A character vector of command-line arguments.} \item{...}{Other arguments to be passed to \code{\link{system2}()}.} } \value{ A value returned by \code{system2()}. } \description{ Wrapper functions to run the commands \command{Rscript} and \command{R CMD}. } \examples{ library(xfun) Rscript(c("-e", "1+1")) Rcmd(c("build", "--help")) } xfun/man/del_empty_dir.Rd0000644000175000017500000000071514156162700015253 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{del_empty_dir} \alias{del_empty_dir} \title{Delete an empty directory} \usage{ del_empty_dir(dir) } \arguments{ \item{dir}{Path to a directory. If \code{NULL} or the directory does not exist, no action will be performed.} } \description{ Use \code{list.file()} to check if there are any files or subdirectories under a directory. If not, delete this empty directory. } xfun/man/native_encode.Rd0000644000175000017500000000141014156162700015227 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/encoding.R \name{native_encode} \alias{native_encode} \title{Try to use the system native encoding to represent a character vector} \usage{ native_encode(x) } \arguments{ \item{x}{A character vector.} } \description{ Apply \code{enc2native()} to the character vector, and check if \code{enc2utf8()} can convert it back without a loss. If it does, return \code{enc2native(x)}, otherwise return the original vector with a warning. } \note{ On platforms that supports UTF-8 as the native encoding (\code{\link{l10n_info}()[['UTF-8']]} returns \code{TRUE}), the conversion will be skipped. } \examples{ library(xfun) s = intToUtf8(c(20320, 22909)) Encoding(s) s2 = native_encode(s) Encoding(s2) } xfun/man/read_utf8.Rd0000644000175000017500000000273514156162700014320 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{read_utf8} \alias{read_utf8} \alias{write_utf8} \alias{append_utf8} \alias{append_unique} \title{Read / write files encoded in UTF-8} \usage{ read_utf8(con, error = FALSE) write_utf8(text, con, ...) append_utf8(text, con, sort = TRUE) append_unique(text, con, sort = function(x) base::sort(unique(x))) } \arguments{ \item{con}{A connection or a file path.} \item{error}{Whether to signal an error when non-UTF8 characters are detected (if \code{FALSE}, only a warning message is issued).} \item{text}{A character vector (will be converted to UTF-8 via \code{\link{enc2utf8}()}).} \item{...}{Other arguments passed to \code{\link{writeLines}()} (except \code{useBytes}, which is \code{TRUE} in \code{write_utf8()}).} \item{sort}{Logical (\code{FALSE} means not to sort the content) or a function to sort the content; \code{TRUE} is equivalent to \code{base::sort}.} } \description{ Read or write files, assuming they are encoded in UTF-8. \code{read_utf8()} is roughly \code{readLines(encoding = 'UTF-8')} (a warning will be issued if non-UTF8 lines are found), and \code{write_utf8()} calls \code{writeLines(enc2utf8(text), useBytes = TRUE)}. } \details{ The function \code{append_utf8()} appends UTF-8 content to a file or connection based on \code{read_utf8()} and \code{write_utf8()}, and optionally sort the content. The function \code{append_unique()} appends unique lines to a file or connection. } xfun/man/parse_only.Rd0000644000175000017500000000074514156162700014611 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{parse_only} \alias{parse_only} \title{Parse R code and do not keep the source} \usage{ parse_only(code) } \arguments{ \item{code}{A character vector of the R source code.} } \value{ R \code{\link{expression}}s. } \description{ An abbreviation of \code{parse(keep.source = FALSE)}. } \examples{ library(xfun) parse_only("1+1") parse_only(c("y~x", "1:5 # a comment")) parse_only(character(0)) } xfun/man/format_bytes.Rd0000644000175000017500000000120014156162700015117 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{format_bytes} \alias{format_bytes} \title{Format numbers of bytes using a specified unit} \usage{ format_bytes(x, units = "auto", ...) } \arguments{ \item{x}{A numeric vector (each element represents a number of bytes).} \item{units, ...}{Passed to \code{\link[=format.object_size]{format}()}.} } \value{ A character vector. } \description{ Call the S3 method \code{format.object_size()} to format numbers of bytes. } \examples{ xfun::format_bytes(c(1, 1024, 2000, 1e+06, 2e+08)) xfun::format_bytes(c(1, 1024, 2000, 1e+06, 2e+08), units = "KB") } xfun/man/split_lines.Rd0000644000175000017500000000141214156162700014753 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string.R \name{split_lines} \alias{split_lines} \title{Split a character vector by line breaks} \usage{ split_lines(x) } \arguments{ \item{x}{A character vector.} } \value{ All elements of the character vector are split by \code{'\n'} into lines. } \description{ Call \code{unlist(strsplit(x, '\n'))} on the character vector \code{x} and make sure it works in a few edge cases: \code{split_lines('')} returns \code{''} instead of \code{character(0)} (which is the returned value of \code{strsplit('', '\n')}); \code{split_lines('a\n')} returns \code{c('a', '')} instead of \code{c('a')} (which is the returned value of \code{strsplit('a\n', '\n')}. } \examples{ xfun::split_lines(c("a", "b\nc")) } xfun/man/msg_cat.Rd0000644000175000017500000000224114156162700014044 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{msg_cat} \alias{msg_cat} \title{Generate a message with \code{cat()}} \usage{ msg_cat(...) } \arguments{ \item{...}{Character strings of messages, which will be concatenated into one string via \code{paste(c(...), collapse = '')}.} } \value{ Invisible \code{NULL}, with the side-effect of printing the message. } \description{ This function is similar to \code{\link{message}()}, and the difference is that \code{msg_cat()} uses \code{\link{cat}()} to write out the message, which is sent to \code{\link{stdout}} instead of \code{\link{stderr}}. The message can be suppressed by \code{\link{suppressMessages}()}. } \note{ By default, a newline will not be appended to the message. If you need a newline, you have to explicitly add it to the message (see \sQuote{Examples}). } \examples{ { # a message without a newline at the end xfun::msg_cat("Hello world!") # add a newline at the end xfun::msg_cat(" This message appears right after the previous one.\n") } suppressMessages(xfun::msg_cat("Hello world!")) } \seealso{ This function was inspired by \code{rlang::inform()}. } xfun/man/bg_process.Rd0000644000175000017500000000310114156162700014551 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{bg_process} \alias{bg_process} \title{Start a background process} \usage{ bg_process( command, args = character(), verbose = getOption("xfun.bg_process.verbose", FALSE) ) } \arguments{ \item{command, args}{The system command and its arguments. They do not need to be quoted, since they will be quoted via \code{\link{shQuote}()} internally.} \item{verbose}{If \code{FALSE}, suppress the output from \verb{stdout} (and also \verb{stderr} on Windows). The default value of this argument can be set via a global option, e.g., \code{options(xfun.bg_process.verbose = TRUE)}.} } \value{ The process ID as a character string. } \description{ Start a background process using the PowerShell cmdlet \command{Start-Process -PassThru} on Windows or the ampersand \command{&} on Unix, and return the process ID. } \note{ On Windows, if PowerShell is not available, try to use \code{\link{system2}(wait = FALSE)} to start the background process instead. The process ID will be identified from the output of the command \command{tasklist}. This method of looking for the process ID may not be reliable. If the search is not successful in 30 seconds, it will throw an error (timeout). If a longer time is needed, you may set \code{options(xfun.bg_process.timeout)} to a larger value, but it should be very rare that a process cannot be started in 30 seconds. When you reach the timeout, it is more likely that the command actually failed. } \seealso{ \code{\link{proc_kill}()} to kill a process. } xfun/man/read_all.Rd0000644000175000017500000000201714156162700014173 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{read_all} \alias{read_all} \title{Read all text files and concatenate their content} \usage{ read_all(files, before = function(f) NULL, after = function(f) NULL) } \arguments{ \item{files}{A vector of file paths.} \item{before, after}{A function that takes one file path as the input and returns values to be added before or after the content of the file. Alternatively, they can be constant values to be added.} } \value{ A character vector. } \description{ Read files one by one, and optionally add text before/after the content. Then combine all content into one character vector. } \examples{ # two files in this package fs = system.file("scripts", c("call-fun.R", "child-pids.sh"), package = "xfun") xfun::read_all(fs) # add file paths before file content and an empty line after content xfun::read_all(fs, before = function(f) paste("#-----", f, "-----"), after = "") # add constants xfun::read_all(fs, before = "/*", after = c("*/", "")) } xfun/man/try_error.Rd0000644000175000017500000000073114156162700014460 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{try_error} \alias{try_error} \title{Try an expression and see if it throws an error} \usage{ try_error(expr) } \arguments{ \item{expr}{An R expression.} } \value{ \code{TRUE} (error) or \code{FALSE} (success). } \description{ Use \code{\link{tryCatch}()} to check if an expression throws an error. } \examples{ xfun::try_error(stop("foo")) # TRUE xfun::try_error(1:10) # FALSE } xfun/man/embed_file.Rd0000644000175000017500000000455514156162700014514 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/markdown.R \name{embed_file} \alias{embed_file} \alias{embed_dir} \alias{embed_files} \title{Embed a file, multiple files, or directory on an HTML page} \usage{ embed_file(path, name = basename(path), text = paste("Download", name), ...) embed_dir(path, name = paste0(normalize_path(path), ".zip"), ...) embed_files(path, name = with_ext(basename(path[1]), ".zip"), ...) } \arguments{ \item{path}{Path to the file(s) or directory.} \item{name}{The default filename to use when downloading the file. Note that for \code{embed_dir()}, only the base name (of the zip filename) will be used.} \item{text}{The text for the hyperlink.} \item{...}{For \code{embed_file()}, additional arguments to be passed to \code{htmltools::a()} (e.g., \code{class = 'foo'}). For \code{embed_dir()} and \code{embed_files()}, arguments passed to \code{embed_file()}.} } \value{ An HTML tag \samp{} with the appropriate attributes. } \description{ For a file, first encode it into base64 data (a character string). Then generate a hyperlink of the form \samp{Download filename}. The file can be downloaded when the link is clicked in modern web browsers. For a directory, it will be compressed as a zip archive first, and the zip file is passed to \code{embed_file()}. For multiple files, they are also compressed to a zip file first. } \details{ These functions can be called in R code chunks in R Markdown documents with HTML output formats. You may embed an arbitrary file or directory in the HTML output file, so that readers of the HTML page can download it from the browser. A common use case is to embed data files for readers to download. } \note{ Windows users may need to install Rtools to obtain the \command{zip} command to use \code{embed_dir()} and \code{embed_files()}. These functions require R packages \pkg{mime} and \pkg{htmltools}. If you have installed the \pkg{rmarkdown} package, these packages should be available, otherwise you need to install them separately. Currently Internet Explorer does not support downloading embedded files (\url{https://caniuse.com/#feat=download}). Chrome has a 2MB limit on the file size. } \examples{ logo = xfun:::R_logo() link = xfun::embed_file(logo, text = "Download R logo") link if (interactive()) htmltools::browsable(link) } xfun/man/Rscript_call.Rd0000644000175000017500000000260414156162700015053 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{Rscript_call} \alias{Rscript_call} \title{Call a function in a new R session via \code{Rscript()}} \usage{ Rscript_call( fun, args = list(), options = NULL, ..., wait = TRUE, fail = sprintf("Failed to run '\%s' in a new R session.", deparse(substitute(fun))[1]) ) } \arguments{ \item{fun}{A function, or a character string that can be parsed and evaluated to a function.} \item{args}{A list of argument values.} \item{options}{A character vector of options to passed to \code{\link{Rscript}}, e.g., \code{"--vanilla"}.} \item{..., wait}{Arguments to be passed to \code{\link{system2}()}.} \item{fail}{The desired error message when an error occurred in calling the function.} } \value{ The returned value of the function in the new R session. } \description{ Save the argument values of a function in a temporary RDS file, open a new R session via \code{\link{Rscript}()}, read the argument values, call the function, and read the returned value back to the current R session. } \examples{ factorial(10) # should return the same value xfun::Rscript_call("factorial", list(10)) # the first argument can be either a character string or a function xfun::Rscript_call(factorial, list(10)) # Run Rscript starting a vanilla R session xfun::Rscript_call(factorial, list(10), options = c("--vanilla")) } xfun/man/download_file.Rd0000644000175000017500000000263514156162700015244 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{download_file} \alias{download_file} \title{Try various methods to download a file} \usage{ download_file( url, output = url_filename(url), ..., .error = "No download method works (auto/wininet/wget/curl/lynx)" ) } \arguments{ \item{url}{The URL of the file.} \item{output}{Path to the output file. By default, it is determined by \code{\link{url_filename}()}.} \item{...}{Other arguments to be passed to \code{\link{download.file}()} (except \code{method}).} \item{.error}{An error message to signal when the download fails.} } \value{ The integer code \code{0} for success, or an error if none of the methods work. } \description{ Try all possible methods in \code{\link{download.file}()} (e.g., \code{libcurl}, \code{curl}, \code{wget}, and \code{wininet}) and see if any method can succeed. The reason to enumerate all methods is that sometimes the default method does not work, e.g., \url{https://stat.ethz.ch/pipermail/r-devel/2016-June/072852.html}. } \note{ To allow downloading large files, the \code{timeout} option in \code{\link{options}()} will be temporarily set to one hour (3600 seconds) inside this function when this option has the default value of 60 seconds. If you want a different \code{timeout} value, you may set it via \code{options(timeout = N)}, where \code{N} is the number of seconds (not 60). } xfun/man/base64_uri.Rd0000644000175000017500000000120414156162700014370 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/base64.R \name{base64_uri} \alias{base64_uri} \title{Generate the Data URI for a file} \usage{ base64_uri(x) } \arguments{ \item{x}{A file path.} } \value{ A string of the form \verb{data:;base64,}. } \description{ Encode the file in the base64 encoding, and add the media type. The data URI can be used to embed data in HTML documents, e.g., in the \code{src} attribute of the \verb{} tag. } \examples{ logo = xfun:::R_logo() img = htmltools::img(src = xfun::base64_uri(logo), alt = "R logo") if (interactive()) htmltools::browsable(img) } xfun/man/is_sub_path.Rd0000644000175000017500000000143514156162700014733 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{is_sub_path} \alias{is_sub_path} \title{Test if a path is a subpath of a dir} \usage{ is_sub_path(x, dir, n = nchar(dir)) } \arguments{ \item{x}{A vector of paths.} \item{dir}{A vector of directory paths.} \item{n}{The length of \code{dir} paths.} } \value{ A logical vector. } \description{ Check if the path starts with the dir path. } \note{ You may want to normalize the values of the \code{x} and \code{dir} arguments first (with \code{xfun::\link{normalize_path}()}), to make sure the path separators are consistent. } \examples{ xfun::is_sub_path("a/b/c.txt", "a/b") # TRUE xfun::is_sub_path("a/b/c.txt", "d/b") # FALSE xfun::is_sub_path("a/b/c.txt", "a\\\\b") # FALSE (even on Windows) } xfun/man/magic_path.Rd0000644000175000017500000000304114156162700014522 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{magic_path} \alias{magic_path} \title{Find a file or directory under a root directory} \usage{ magic_path( ..., root = proj_root(), relative = TRUE, error = TRUE, message = getOption("xfun.magic_path.message", TRUE), n_dirs = getOption("xfun.magic_path.n_dirs", 10000) ) } \arguments{ \item{...}{A character vector of path components.} \item{root}{The root directory under which to search for the path. If \code{NULL}, the current working directory is used.} \item{relative}{Whether to return a relative path.} \item{error}{Whether to signal an error if the path is not found, or multiple paths are found.} \item{message}{Whether to emit a message when multiple paths are found and \code{error = FALSE}.} \item{n_dirs}{The number of subdirectories to recursively search. The recursive search may be time-consuming when there are a large number of subdirectories under the root directory. If you really want to search for all subdirectories, you may try \code{n_dirs = Inf}.} } \value{ The path found under the root directory, or an error when \code{error = TRUE} and the path is not found (or multiple paths are found). } \description{ Given a path, try to find it recursively under a root directory. The input path can be an incomplete path, e.g., it can be a base filename, and \code{magic_path()} will try to find this file under subdirectories. } \examples{ \dontrun{ xfun::magic_path("mtcars.csv") # find any file that has the base name mtcars.csv } } xfun/man/tinify.Rd0000644000175000017500000000577214156162700013745 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/image.R \name{tinify} \alias{tinify} \title{Use the Tinify API to compress PNG and JPEG images} \usage{ tinify( input, output, quiet = FALSE, force = FALSE, key = getOption("xfun.tinify.key", Sys.getenv("R_XFUN_TINIFY_KEY")), history = getOption("xfun.tinify.history", Sys.getenv("R_XFUN_TINIFY_HISTORY")) ) } \arguments{ \item{input}{A vector of input paths of images.} \item{output}{A vector of output paths or a function that takes \code{input} and returns a vector of output paths (e.g., \code{output = \link{identity}} means \code{output = input}). By default, if the \code{history} argument is not a provided, \code{output} is \code{input} with a suffix \code{-min} (e.g., when \code{input = 'foo.png'}, \code{output = 'foo-min.png'}), otherwise \code{output} is the same as \code{input}, which means the original image files will be overwritten.} \item{quiet}{Whether to suppress detailed information about the compression, which is of the form \samp{input.png (10 Kb) ==> output.png (5 Kb, 50\%); compression count: 42}. The percentage after \code{output.png} stands for the compression ratio, and the compression count shows the number of compressions used for the current month.} \item{force}{Whether to compress an image again when it appears to have been compressed before. This argument only makes sense when the \code{history} argument is provided.} \item{key}{The Tinify API key. It can be set via either the global option \code{xfun.tinify.key} (you may set it in \file{~/.Rprofile}) or the environment variable \code{R_XFUN_TINIFY_KEY} (you may set it in \file{~/.Renviron}).} \item{history}{Path to a history file to record the MD5 checksum of compressed images. If the checksum of an expected output image exists in this file and \code{force = FALSE}, the compression will be skipped. This can help you avoid unnecessary API calls.} } \value{ The output file paths. } \description{ Compress PNG/JPEG images with \samp{api.tinify.com}, and download the compressed images. This function requires R packages \pkg{curl} and \pkg{jsonlite}. } \details{ You are recommended to set the API key in \file{.Rprofile} or \file{.Renviron}. After that, the only required argument of this function is \code{input}. If the original images can be overwritten by the compressed images, you may either use \code{output = identity}, or set the value of the \code{history} argument in \file{.Rprofile} or \file{.Renviron}. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} f = file.path(R.home("doc"), "html", "logo.jpg") xfun::tinify(f) # remember to set the API key before trying this \dontshow{\}) # examplesIf} } \references{ Tinify API: \url{https://tinypng.com/developers}. } \seealso{ The \pkg{tinieR} package (\url{https://github.com/jmablog/tinieR/}) is a more comprehensive implementation of the Tinify API, whereas \code{xfun::tinify()} has only implemented the feature of shrinking images. } xfun/man/attr.Rd0000644000175000017500000000077614156162700013414 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{attr} \alias{attr} \title{Obtain an attribute of an object without partial matching} \usage{ attr(...) } \arguments{ \item{...}{Passed to \code{base::\link[base]{attr}()} (without the \code{exact} argument).} } \description{ An abbreviation of \code{base::\link[base]{attr}(exact = TRUE)}. } \examples{ z = structure(list(a = 1), foo = 2) base::attr(z, "f") # 2 xfun::attr(z, "f") # NULL xfun::attr(z, "foo") # 2 } xfun/man/os.Rd0000644000175000017500000000105614156162700013053 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/os.R \name{is_windows} \alias{is_windows} \alias{is_unix} \alias{is_macos} \alias{is_linux} \title{Test for types of operating systems} \usage{ is_windows() is_unix() is_macos() is_linux() } \description{ Functions based on \code{.Platform$OS.type} and \code{Sys.info()} to test if the current operating system is Windows, macOS, Unix, or Linux. } \examples{ library(xfun) # only one of the following statements should be true is_windows() is_unix() && is_macos() is_linux() } xfun/man/valid_syntax.Rd0000644000175000017500000000121614156162701015136 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string.R \name{valid_syntax} \alias{valid_syntax} \title{Check if the syntax of the code is valid} \usage{ valid_syntax(code, silent = TRUE) } \arguments{ \item{code}{A character vector of R source code.} \item{silent}{Whether to suppress the error message when the code is not valid.} } \value{ \code{TRUE} if the code could be parsed, otherwise \code{FALSE}. } \description{ Try to \code{\link{parse}()} the code and see if an error occurs. } \examples{ xfun::valid_syntax("1+1") xfun::valid_syntax("1+") xfun::valid_syntax(c("if(T){1+1}", "else {2+2}"), silent = FALSE) } xfun/man/set_envvar.Rd0000644000175000017500000000155414156162700014611 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{set_envvar} \alias{set_envvar} \title{Set environment variables} \usage{ set_envvar(vars) } \arguments{ \item{vars}{A named character vector of the form \code{c(VARIABLE = VALUE)}. If any value is \code{NA}, this function will try to unset the variable.} } \value{ Old values of the variables (if not set, \code{NA}). } \description{ Set environment variables from a named character vector, and return the old values of the variables, so they could be restored later. } \details{ The motivation of this function is that \code{\link{Sys.setenv}()} does not return the old values of the environment variables, so it is not straightforward to restore the variables later. } \examples{ vars = xfun::set_envvar(c(FOO = "1234")) Sys.getenv("FOO") xfun::set_envvar(vars) Sys.getenv("FOO") } xfun/man/rev_check.Rd0000644000175000017500000001561114156162700014365 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/revcheck.R \name{rev_check} \alias{rev_check} \alias{compare_Rcheck} \title{Run \command{R CMD check} on the reverse dependencies of a package} \usage{ rev_check( pkg, which = "all", recheck = NULL, ignore = NULL, update = TRUE, timeout = getOption("xfun.rev_check.timeout", 15 * 60), src = file.path(src_dir, pkg), src_dir = getOption("xfun.rev_check.src_dir") ) compare_Rcheck(status_only = TRUE, output = "00check_diffs.md") } \arguments{ \item{pkg}{The package name.} \item{which}{Which types of reverse dependencies to check. See \code{tools::\link[tools]{package_dependencies}()} for possible values. The special value \code{'hard'} means the hard dependencies, i.e., \code{c('Depends', 'Imports', 'LinkingTo')}.} \item{recheck}{A vector of package names to be (re)checked. If not provided and there are any \file{*.Rcheck} directories left by certain packages (this often means these packages failed the last time), \code{recheck} will be these packages; if there are no \file{*.Rcheck} directories but a text file \file{recheck} exists, \code{recheck} will be the character vector read from this file. This provides a way for you to manually specify the packages to be checked. If there are no packages to be rechecked, all reverse dependencies will be checked.} \item{ignore}{A vector of package names to be ignored in \command{R CMD check}. If this argument is missing and a file \file{00ignore} exists, the file will be read as a character vector and passed to this argument.} \item{update}{Whether to update all packages before the check.} \item{timeout}{Timeout in seconds for \command{R CMD check} to check each package. The (approximate) total time can be limited by the global option \code{xfun.rev_check.timeout_total}.} \item{src}{The path of the source package directory.} \item{src_dir}{The parent directory of the source package directory. This can be set in a global option if all your source packages are under a common parent directory.} \item{status_only}{If \code{TRUE}, only compare the final statuses of the checks (the last line of \file{00check.log}), and delete \file{*.Rcheck} and \file{*.Rcheck2} if the statuses are identical, otherwise write out the full diffs of the logs. If \code{FALSE}, compare the full logs under \file{*.Rcheck} and \file{*.Rcheck2}.} \item{output}{The output Markdown file to which the diffs in check logs will be written. If the \pkg{markdown} package is available, the Markdown file will be converted to HTML, so you can see the diffs more clearly.} } \value{ A named numeric vector with the names being package names of reverse dependencies; \code{0} indicates check success, \code{1} indicates failure, and \code{2} indicates that a package was not checked due to global timeout. } \description{ Install the source package, figure out the reverse dependencies on CRAN, download all of their source packages, and run \command{R CMD check} on them in parallel. } \details{ Everything occurs under the current working directory, and you are recommended to call this function under a designated directory, especially when the number of reverse dependencies is large, because all source packages will be downloaded to this directory, and all \file{*.Rcheck} directories will be generated under this directory, too. If a source tarball of the expected version has been downloaded before (under the \file{tarball} directory), it will not be downloaded again (to save time and bandwidth). After a package has been checked, the associated \file{*.Rcheck} directory will be deleted if the check was successful (no warnings or errors or notes), which means if you see a \file{*.Rcheck} directory, it means the check failed, and you need to take a look at the log files under that directory. The time to finish the check is recorded for each package. As the check goes on, the total remaining time will be roughly estimated via \code{n * mean(times)}, where \code{n} is the number of packages remaining to be checked, and \code{times} is a vector of elapsed time of packages that have been checked. If a check on a reverse dependency failed, its \file{*.Rcheck} directory will be renamed to \file{*.Rcheck2}, and another check will be run against the CRAN version of the package unless \code{options(xfun.rev_check.compare = FALSE)} is set. If the logs of the two checks are the same, it means no new problems were introduced in the package, and you can probably ignore this particular reverse dependency. The function \code{compare_Rcheck()} can be used to create a summary of all the differences in the check logs under \file{*.Rcheck} and \file{*.Rcheck2}. This will be done automatically if \code{options(xfun.rev_check.summary = TRUE)} has been set. A recommended workflow is to use a special directory to run \code{rev_check()}, set the global \code{\link{options}} \code{xfun.rev_check.src_dir} and \code{repos} in the R startup (see \code{?\link{Startup}}) profile file \code{.Rprofile} under this directory, and (optionally) set \code{R_LIBS_USER} in \file{.Renviron} to use a special library path (so that your usual library will not be cluttered). Then run \code{xfun::rev_check(pkg)} once, investigate and fix the problems or (if you believe it was not your fault) ignore broken packages in the file \file{00ignore}, and run \code{xfun::rev_check(pkg)} again to recheck the failed packages. Repeat this process until all \file{*.Rcheck} directories are gone. As an example, I set \code{options(repos = c(CRAN = 'https://cran.rstudio.com'), xfun.rev_check.src_dir = '~/Dropbox/repo')} in \file{.Rprofile}, and \code{R_LIBS_USER=~/R-tmp} in \file{.Renviron}. Then I can run, for example, \code{xfun::rev_check('knitr')} repeatedly under a special directory \file{~/Downloads/revcheck}. Reverse dependencies and their dependencies will be installed to \file{~/R-tmp}, and \pkg{knitr} will be installed from \file{~/Dropbox/repo/kintr}. } \seealso{ \code{devtools::revdep_check()} is more sophisticated, but currently has a few major issues that affect me: (1) It always deletes the \file{*.Rcheck} directories (\url{https://github.com/r-lib/devtools/issues/1395}), which makes it difficult to know more information about the failures; (2) It does not fully install the source package before checking its reverse dependencies (\url{https://github.com/r-lib/devtools/pull/1397}); (3) I feel it is fairly difficult to iterate the check (ignore the successful packages and only check the failed packages); by comparison, \code{xfun::rev_check()} only requires you to run a short command repeatedly (failed packages are indicated by the existing \file{*.Rcheck} directories, and automatically checked again the next time). \code{xfun::rev_check()} borrowed a very nice feature from \code{devtools::revdep_check()}: estimating and displaying the remaining time. This is particularly useful for packages with huge numbers of reverse dependencies. } xfun/man/file_string.Rd0000644000175000017500000000100714156162700014733 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{file_string} \alias{file_string} \title{Read a text file and concatenate the lines by \code{'\n'}} \usage{ file_string(file) } \arguments{ \item{file}{Path to a text file (should be encoded in UTF-8).} } \value{ A character string of text lines concatenated by \code{'\n'}. } \description{ The source code of this function should be self-explanatory. } \examples{ xfun::file_string(system.file("DESCRIPTION", package = "xfun")) } xfun/man/is_R_CMD_check.Rd0000644000175000017500000000075514156162700015153 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cran.R \name{is_R_CMD_check} \alias{is_R_CMD_check} \alias{is_CRAN_incoming} \alias{check_package_name} \alias{check_old_package} \title{Some utility functions for checking packages} \usage{ is_R_CMD_check() is_CRAN_incoming() check_package_name() check_old_package(name, version) } \description{ Miscellaneous utility functions to obtain information about the package checking environment. } \keyword{internal} xfun/man/rename_seq.Rd0000644000175000017500000000332314156162700014550 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{rename_seq} \alias{rename_seq} \title{Rename files with a sequential numeric prefix} \usage{ rename_seq( pattern = "^[0-9]+-.+[.]Rmd$", format = "auto", replace = TRUE, start = 1, dry_run = TRUE ) } \arguments{ \item{pattern}{A regular expression for \code{\link{list.files}()} to obtain the files to be renamed. For example, to rename \code{.jpeg} files, use \code{pattern = "[.]jpeg$"}.} \item{format}{The format for the numeric prefix. This is passed to \code{\link{sprintf}()}. The default format is \code{"\%0Nd"} where \code{N = floor(log10(n)) + 1} and \code{n} is the number of files, which means the prefix may be padded with zeros. For example, if there are 150 files to be renamed, the format will be \code{"\%03d"} and the prefixes will be \code{001}, \code{002}, ..., \code{150}.} \item{replace}{Whether to remove existing numeric prefixes in filenames.} \item{start}{The starting number for the prefix (it can start from 0).} \item{dry_run}{Whether to not really rename files. To be safe, the default is \code{TRUE}. If you have looked at the new filenames and are sure the new names are what you want, you may rerun \code{rename_seq()} with \code{dry_run = FALSE)} to actually rename files.} } \value{ A named character vector. The names are original filenames, and the vector itself is the new filenames. } \description{ Rename a series of files and add an incremental numeric prefix to the filenames. For example, files \file{a.txt}, \file{b.txt}, and \file{c.txt} can be renamed to \file{1-a.txt}, \file{2-b.txt}, and \file{3-c.txt}. } \examples{ xfun::rename_seq() xfun::rename_seq("[.](jpeg|png)$", format = "\%04d") } xfun/man/news2md.Rd0000644000175000017500000000175714156162700014021 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{news2md} \alias{news2md} \title{Convert package news to the Markdown format} \usage{ news2md(package, ..., output = "NEWS.md", category = TRUE) } \arguments{ \item{package, ...}{Arguments to be passed to \code{\link{news}()}.} \item{output}{The output file path.} \item{category}{Whether to keep the category names.} } \value{ If \code{output = NA}, returns the Markdown content as a character vector, otherwise the content is written to the output file. } \description{ Read the package news with \code{\link{news}()}, convert the result to Markdown, and write to an output file (e.g., \file{NEWS.md}). Each package version appears in a first-level header, each category (e.g., \samp{NEW FEATURES} or \samp{BUG FIXES}) is in a second-level header, and the news items are written into bullet lists. } \examples{ # news for the current version of R xfun::news2md("R", Version == getRversion(), output = NA) } xfun/man/tree.Rd0000644000175000017500000000174614156162700013377 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{tree} \alias{tree} \title{Turn the output of \code{\link{str}()} into a tree diagram} \usage{ tree(...) } \arguments{ \item{...}{Arguments to be passed to \code{\link{str}()} (note that the \code{comp.str} is hardcoded inside this function, and it is the only argument that you cannot customize).} } \value{ A character string as a \code{\link{raw_string}()}. } \description{ The super useful function \code{str()} uses \verb{..} to indicate the level of sub-elements of an object, which may be difficult to read. This function uses vertical pipes to connect all sub-elements on the same level, so it is clearer which elements belong to the same parent element in an object with a nested structure (such as a nested list). } \examples{ fit = lsfit(1:9, 1:9) str(fit) xfun::tree(fit) fit = lm(dist ~ speed, data = cars) str(fit) xfun::tree(fit) # some trivial examples xfun::tree(1:10) xfun::tree(iris) } xfun/man/broken_packages.Rd0000644000175000017500000000133714156162700015552 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{broken_packages} \alias{broken_packages} \title{Find out broken packages and reinstall them} \usage{ broken_packages(reinstall = TRUE) } \arguments{ \item{reinstall}{Whether to reinstall the broken packages, or only list their names.} } \value{ A character vector of names of broken package. } \description{ If a package is broken (i.e., not \code{\link{loadable}()}), reinstall it. } \details{ Installed R packages could be broken for several reasons. One common reason is that you have upgraded R to a newer \code{x.y} version, e.g., from \code{4.0.5} to \code{4.1.0}, in which case you need to reinstall previously installed packages. } xfun/man/base_pkgs.Rd0000644000175000017500000000100114156162700014356 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{base_pkgs} \alias{base_pkgs} \title{Get base R package names} \usage{ base_pkgs() } \value{ A character vector of base R package names. } \description{ Return names of packages from \code{\link{installed.packages}()} of which the priority is \code{"base"}. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} xfun::base_pkgs() \dontshow{\}) # examplesIf} } xfun/man/relative_path.Rd0000644000175000017500000000231214156162700015255 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{relative_path} \alias{relative_path} \title{Get the relative path of a path relative to a directory} \usage{ relative_path(x, dir = ".", use.. = TRUE, error = TRUE) } \arguments{ \item{x}{A vector of paths to be converted to relative paths.} \item{dir}{Path to a directory.} \item{use..}{Whether to use double-dots (\file{..}) in the relative path. A double-dot indicates the parent directory (starting from the directory provided by the \code{dir} argument).} \item{error}{Whether to signal an error if a path cannot be converted to a relative path.} } \value{ A vector of relative paths if the conversion succeeded; otherwise the original paths when \code{error = FALSE}, and an error when \code{error = TRUE}. } \description{ Given a directory, return the relative path that is relative to this directory. For example, the path \file{foo/bar.txt} relative to the directory \file{foo/} is \file{bar.txt}, and the path \file{/a/b/c.txt} relative to \file{/d/e/} is \file{../../a/b/c.txt}. } \examples{ xfun::relative_path("foo/bar.txt", "foo/") xfun::relative_path("foo/bar/a.txt", "foo/haha/") xfun::relative_path(getwd()) } xfun/man/isFALSE.Rd0000644000175000017500000000062614156162700013622 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{isFALSE} \alias{isFALSE} \title{Test if an object is identical to \code{FALSE}} \usage{ isFALSE(x) } \arguments{ \item{x}{An R object.} } \description{ A simple abbreviation of \code{identical(x, FALSE)}. } \examples{ library(xfun) isFALSE(TRUE) # false isFALSE(FALSE) # true isFALSE(c(FALSE, FALSE)) # false } xfun/man/rest_api.Rd0000644000175000017500000000572114156162700014243 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/api.R, R/github.R \name{rest_api} \alias{rest_api} \alias{rest_api_raw} \alias{github_api} \title{Get data from a REST API} \usage{ rest_api(...) rest_api_raw(root, endpoint, token = "", params = list(), headers = NULL) github_api( endpoint, token = "", params = list(), headers = NULL, raw = !loadable("jsonlite") ) } \arguments{ \item{...}{Arguments to be passed to \code{rest_api_raw()}.} \item{root}{The API root URL.} \item{endpoint}{The API endpoint.} \item{token}{A named character string (e.g., \code{c(token = "xxxx")}), which will be used to create an authorization header of the form \samp{Authorization: NAME TOKEN} for the API call, where \samp{NAME} is the name of the string and \samp{TOKEN} is the string. If the string does not have a name, \samp{Basic} will be used as the default name.} \item{params}{A list of query parameters to be sent with the API call.} \item{headers}{A named character vector of HTTP headers, e.g., \code{c(Accept = "application/vnd.github.v3+json")}.} \item{raw}{Whether to return the raw response or parse the response with \pkg{jsonlite}.} } \value{ A character vector (the raw JSON response) or an R object parsed from the JSON text. } \description{ Read data from a REST API and optionally with an authorization token in the request header. The function \code{rest_api_raw()} returns the raw text of the response, and \code{rest_api()} will parse the response with \code{jsonlite::fromJSON()} (assuming that the response is in the JSON format). } \details{ These functions are simple wrappers based on \code{\link{url}()} and \code{\link{read_utf8}()}. Specifically, the \code{headers} argument is passed to \code{url()}, and \code{read_utf8()} will send a \samp{GET} request to the API server. This means these functions only support the \samp{GET} method. If you need to use other HTTP methods (such as \samp{POST}), you have to use other packages such as \pkg{curl} and \pkg{httr}. \code{github_api()} is a wrapper function based on \code{rest_api_raw()} to obtain data from the Github API: \url{https://docs.github.com/en/rest}. You can provide a personal access token (PAT) via the \code{token} argument, or via one of the environment variables \var{GITHUB_PAT}, \var{GITHUB_TOKEN}, \var{GH_TOKEN}. A PAT allows for a much higher rate limit in API calls. Without a token, you can only make 60 calls in an hour. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # a normal GET request xfun::rest_api("https://httpbin.org", "/get") xfun::rest_api_raw("https://httpbin.org", "/get") # send the request with an auth header xfun::rest_api("https://httpbin.org", "/headers", "OPEN SESAME!") # with query parameters xfun::rest_api("https://httpbin.org", "/response-headers", params = list(foo = "bar")) # get the rate limit info from Github xfun::github_api("/rate_limit") \dontshow{\}) # examplesIf} } xfun/man/url_filename.Rd0000644000175000017500000000124714156162701015077 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{url_filename} \alias{url_filename} \title{Extract filenames from a URLs} \usage{ url_filename(x) } \arguments{ \item{x}{A character vector of URLs.} } \value{ A character vector of filenames at the end of URLs. } \description{ Get the base names of URLs via \code{\link{basename}()}, and remove the possible query parameters or hash from the names. } \examples{ xfun::url_filename("https://yihui.org/images/logo.png") xfun::url_filename("https://yihui.org/index.html") xfun::url_filename("https://yihui.org/index.html?foo=bar") xfun::url_filename("https://yihui.org/index.html#about") } xfun/man/proj_root.Rd0000644000175000017500000000400614156162700014445 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \docType{data} \name{proj_root} \alias{proj_root} \alias{root_rules} \title{Return the (possible) root directory of a project} \format{ An object of class \code{matrix} (inherits from \code{array}) with 2 rows and 2 columns. } \usage{ proj_root(path = "./", rules = root_rules) root_rules } \arguments{ \item{path}{The initial path to start the search. If it is a file path, its parent directory will be used.} \item{rules}{A matrix of character strings of two columns: the first column contains regular expressions to look for filenames that match the patterns, and the second column contains regular expressions to match the content of the matched files. The regular expression can be an empty string, meaning that it will match anything.} } \value{ Path to the root directory if found, otherwise \code{NULL}. } \description{ Given a path of a file (or dir) in a potential project (e.g., an R package or an RStudio project), return the path to the project root directory. } \details{ The search for the root directory is performed by a series of tests, currently including looking for a \file{DESCRIPTION} file that contains \code{Package: *} (which usually indicates an R package), and a \file{*.Rproj} file that contains \code{Version: *} (which usually indicates an RStudio project). If files with the expected patterns are not found in the initial directory, the search will be performed recursively in upper-level directories. } \note{ This function was inspired by the \pkg{rprojroot} package, but is much less sophisticated. It is a rather simple function designed to be used in some of packages that I maintain, and may not meet the need of general users until this note is removed in the future (which should be unlikely). If you are sure that you are working on the types of projects mentioned in the \sQuote{Details} section, this function may be helpful to you, otherwise please consider using \pkg{rprojroot} instead. } \keyword{datasets} xfun/man/protect_math.Rd0000644000175000017500000000324314156162700015123 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/markdown.R \name{protect_math} \alias{protect_math} \title{Protect math expressions in pairs of backticks in Markdown} \usage{ protect_math(x) } \arguments{ \item{x}{A character vector of text in Markdown.} } \value{ A character vector with math expressions in backticks. } \description{ For Markdown renderers that do not support LaTeX math, we need to protect math expressions as verbatim code (in a pair of backticks), because some characters in the math expressions may be interpreted as Markdown syntax (e.g., a pair of underscores may make text italic). This function detects math expressions in Markdown (by heuristics), and wrap them in backticks. } \details{ Expressions in pairs of dollar signs or double dollar signs are treated as math, if there are no spaces after the starting dollar sign, or before the ending dollar sign. There should be spaces before the starting dollar sign, unless the math expression starts from the very beginning of a line. For a pair of single dollar signs, the ending dollar sign should not be followed by a number. With these assumptions, there should not be too many false positives when detecing math expressions. Besides, LaTeX environments (\verb{\begin{*}} and \verb{\end{*}}) are also protected in backticks. } \note{ If you are using Pandoc or the \pkg{rmarkdown} package, there is no need to use this function, because Pandoc's Markdown can recognize math expressions. } \examples{ library(xfun) protect_math(c("hi $a+b$", "hello $$\\\\alpha$$", "no math here: $x is $10 dollars")) protect_math(c("hi $$", "\\\\begin{equation}", "x + y = z", "\\\\end{equation}")) } xfun/man/same_path.Rd0000644000175000017500000000100614156162700014366 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{same_path} \alias{same_path} \title{Test if two paths are the same after they are normalized} \usage{ same_path(p1, p2, ...) } \arguments{ \item{p1, p2}{Two vectors of paths.} \item{...}{Arguments to be passed to \code{\link{normalize_path}()}.} } \description{ Compare two paths after normalizing them with the same separator (\code{/}). } \examples{ library(xfun) same_path("~/foo", file.path(Sys.getenv("HOME"), "foo")) } xfun/man/read_bin.Rd0000644000175000017500000000130314156162700014170 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{read_bin} \alias{read_bin} \title{Read all records of a binary file as a raw vector by default} \usage{ read_bin(file, what = "raw", n = file.info(file)$size, ...) } \arguments{ \item{file, what, n, ...}{Arguments to be passed to \code{readBin()}.} } \value{ A vector returned from \code{readBin()}. } \description{ This is a wrapper function of \code{\link{readBin}()} with default arguments \code{what = "raw"} and \code{n = \link{file.size}(file)}, which means it will read the full content of a binary file as a raw vector by default. } \examples{ f = tempfile() cat("abc", file = f) xfun::read_bin(f) unlink(f) } xfun/man/github_releases.Rd0000644000175000017500000000205114156162700015573 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/github.R \name{github_releases} \alias{github_releases} \title{Get the tags of Github releases of a repository} \usage{ github_releases( repo, tag = "", pattern = "v[0-9.]+", use_jsonlite = loadable("jsonlite") ) } \arguments{ \item{repo}{The repository name of the form \code{user/repo}, e.g., \code{"yihui/xfun"}.} \item{tag}{A tag as a character string. If provided, it will be returned if the tag exists. If \code{tag = "latest"}, the tag of the latest release is returned.} \item{pattern}{A regular expression to match the tags.} \item{use_jsonlite}{Whether to use \pkg{jsonlite} to parse the releases info.} } \value{ A character vector of (GIT) tags. } \description{ Use the Github API (\code{\link{github_api}()}) to obtain the tags of the releases. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} xfun::github_releases("yihui/xfun") xfun::github_releases("gohugoio/hugo") \dontshow{\}) # examplesIf} } xfun/man/exit_call.Rd0000644000175000017500000000224114156162700014373 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{exit_call} \alias{exit_call} \title{Call \code{on.exit()} in a parent function} \usage{ exit_call(fun, n = 2, ...) } \arguments{ \item{fun}{A function to be called when the parent function exits.} \item{n}{The parent frame number. For \code{n = 1}, \code{exit_call(fun)} is the same as \code{on.exit(fun())}; \code{n = 2} means adding \code{on.exit(fun())} in the parent function; \code{n = 3} means the grandparent, etc.} \item{...}{Other arguments to be passed to \code{on.exit()}.} } \description{ The function \code{\link{on.exit}()} is often used to perform tasks when the current function exits. This \code{exit_call()} function allows calling a function when a parent function exits (thinking of it as inserting an \code{on.exit()} call into the parent function). } \examples{ f = function(x) { print(x) xfun::exit_call(function() print("The parent function is exiting!")) } g = function(y) { f(y) print("f() has been called!") } g("An argument of g()!") } \references{ This function was inspired by Kevin Ushey: \url{https://yihui.org/en/2017/12/on-exit-parent/} } xfun/man/is_ascii.Rd0000644000175000017500000000105614156162700014215 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/encoding.R \name{is_ascii} \alias{is_ascii} \title{Check if a character vector consists of entirely ASCII characters} \usage{ is_ascii(x) } \arguments{ \item{x}{A character vector.} } \value{ A logical vector indicating whether each element of the character vector is ASCII. } \description{ Converts the encoding of a character vector to \code{'ascii'}, and check if the result is \code{NA}. } \examples{ library(xfun) is_ascii(letters) # yes is_ascii(intToUtf8(8212)) # no } xfun/man/existing_files.Rd0000644000175000017500000000120014156162700015435 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{existing_files} \alias{existing_files} \title{Find file paths that exist} \usage{ existing_files(x, first = FALSE) } \arguments{ \item{x}{A vector of file paths.} \item{first}{Whether to return the first existing path. If \code{TRUE} and no specified files exist, it will signal an error.} } \value{ A vector of existing file paths. } \description{ This is a shorthand of \code{x[file.exists(x)]}, and optionally returns the first existing file path. } \examples{ xfun::existing_files(c("foo.txt", system.file("DESCRIPTION", package = "xfun"))) } xfun/man/rstudio_type.Rd0000644000175000017500000000212114156162700015156 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/rstudio.R \name{rstudio_type} \alias{rstudio_type} \title{Type a character vector into the RStudio source editor} \usage{ rstudio_type(x, pause = function() 0.1, mistake = 0, save = 0) } \arguments{ \item{x}{A character vector.} \item{pause}{A function to return a number in seconds to pause after typing each character.} \item{mistake}{The probability of making random mistakes when typing the next character. A random mistake is a random string typed into the editor and deleted immediately.} \item{save}{The probability of saving the document after typing each character. Note that If a document is not opened from a file, it will never be saved.} } \description{ Use the \pkg{rstudioapi} package to insert characters one by one into the RStudio source editor, as if they were typed by a human. } \examples{ library(xfun) if (loadable("rstudioapi") && rstudioapi::isAvailable()) { rstudio_type("Hello, RStudio! xfun::rstudio_type() looks pretty cool!", pause = function() runif(1, 0, 0.5), mistake = 0.1) } } xfun/man/dir_exists.Rd0000644000175000017500000000127714156162700014614 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{dir_exists} \alias{dir_exists} \alias{file_exists} \title{Test the existence of files and directories} \usage{ dir_exists(x) file_exists(x) } \arguments{ \item{x}{A vector of paths.} } \value{ A logical vector. } \description{ These are wrapper functions of \code{utils::\link{file_test}()} to test the existence of directories and files. Note that \code{file_exists()} only tests files but not directories, which is the main difference between \code{\link{file.exists}()} in base R. If you use are using the R version 3.2.0 or above, \code{dir_exists()} is the same as \code{\link{dir.exists}()} in base R. } xfun/man/submit_cran.Rd0000644000175000017500000000203414156162700014735 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cran.R \name{submit_cran} \alias{submit_cran} \title{Submit a source package to CRAN} \usage{ submit_cran(file = pkg_build(), comment = "") } \arguments{ \item{file}{The path to the source package tarball. By default, the current working directory is treated as the package root directory, and automatically built into a tarball, which is deleted after submission. This means you should run \code{xfun::submit_cran()} in the root directory of a package project, unless you want to pass a path explicitly to the \code{file} argument.} \item{comment}{Submission comments for CRAN. By default, if a file \file{cran-comments.md} exists, its content will be read and used as the comment.} } \description{ Build a source package and submit it to CRAN with the \pkg{curl} package. } \seealso{ \code{devtools::submit_cran()} does the same job, with a few more dependencies in addition to \pkg{curl} (such as \pkg{cli}); \code{xfun::submit_cran()} only depends on \pkg{curl}. } xfun/man/install_github.Rd0000644000175000017500000000133114156162700015436 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{install_github} \alias{install_github} \title{An alias of \code{remotes::install_github()}} \usage{ install_github(...) } \arguments{ \item{...}{Arguments to be passed to \code{remotes::\link[remotes]{install_github}()}.} } \description{ This alias is to make autocomplete faster via \code{xfun::install_github}, because most \code{remotes::install_*} functions are never what I want. I only use \code{install_github} and it is inconvenient to autocomplete it, e.g. \code{install_git} always comes before \code{install_github}, but I never use it. In RStudio, I only need to type \code{xfun::ig} to get \code{xfun::install_github}. } xfun/man/system3.Rd0000644000175000017500000000200414156162700014033 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{system3} \alias{system3} \title{Run \code{system2()} and mark its character output as UTF-8 if appropriate} \usage{ system3(...) } \arguments{ \item{...}{Passed to \code{\link{system2}()}.} } \value{ The value returned by \code{system2()}. } \description{ This is a wrapper function based on \code{system2()}. If \code{system2()} returns character output (e.g., with the argument \code{stdout = TRUE}), check if the output is encoded in UTF-8. If it is, mark it with UTF-8 explicitly. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} a = shQuote(c("-e", "print(intToUtf8(c(20320, 22909)))")) x2 = system2("Rscript", a, stdout = TRUE) Encoding(x2) # unknown x3 = xfun::system3("Rscript", a, stdout = TRUE) # encoding of x3 should be UTF-8 if the current locale is UTF-8 !l10n_info()[["UTF-8"]] || Encoding(x3) == "UTF-8" # should be TRUE \dontshow{\}) # examplesIf} } xfun/man/pkg_attach.Rd0000644000175000017500000000714614156162700014545 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{pkg_attach} \alias{pkg_attach} \alias{pkg_load} \alias{loadable} \alias{pkg_available} \alias{pkg_attach2} \alias{pkg_load2} \title{Attach or load packages, and automatically install missing packages if requested} \usage{ pkg_attach( ..., install = FALSE, message = getOption("xfun.pkg_attach.message", TRUE) ) pkg_load(..., error = TRUE, install = FALSE) loadable(pkg, strict = TRUE, new_session = FALSE) pkg_available(pkg, version = NULL) pkg_attach2(...) pkg_load2(...) } \arguments{ \item{...}{Package names (character vectors, and must always be quoted).} \item{install}{Whether to automatically install packages that are not available using \code{\link{install.packages}()}. Besides \code{TRUE} and \code{FALSE}, the value of this argument can also be a function to install packages (\code{install = TRUE} is equivalent to \code{install = install.packages}), or a character string \code{"pak"} (equivalent to \code{install = pak::pkg_install}, which requires the \pkg{pak} package). You are recommended to set a CRAN mirror in the global option \code{repos} via \code{\link{options}()} if you want to automatically install packages.} \item{message}{Whether to show the package startup messages (if any startup messages are provided in a package).} \item{error}{Whether to signal an error when certain packages cannot be loaded.} \item{pkg}{A single package name.} \item{strict}{If \code{TRUE}, use \code{\link{requireNamespace}()} to test if a package is loadable; otherwise only check if the package is in \code{\link{.packages}(TRUE)} (this does not really load the package, so it is less rigorous but on the other hand, it can keep the current R session clean).} \item{new_session}{Whether to test if a package is loadable in a new R session. Note that \code{new_session = TRUE} implies \code{strict = TRUE}.} \item{version}{A minimal version number. If \code{NULL}, only test if a package is available and do not check its version.} } \value{ \code{pkg_attach()} returns \code{NULL} invisibly. \code{pkg_load()} returns a logical vector, indicating whether the packages can be loaded. } \description{ \code{pkg_attach()} is a vectorized version of \code{\link{library}()} over the \code{package} argument to attach multiple packages in a single function call. \code{pkg_load()} is a vectorized version of \code{\link{requireNamespace}()} to load packages (without attaching them). The functions \code{pkg_attach2()} and \code{pkg_load2()} are wrappers of \code{pkg_attach(install = TRUE)} and \code{pkg_load(install = TRUE)}, respectively. \code{loadable()} is an abbreviation of \code{requireNamespace(quietly = TRUE)}. \code{pkg_available()} tests if a package with a minimal version is available. } \details{ These are convenience functions that aim to solve these common problems: (1) We often need to attach or load multiple packages, and it is tedious to type several \code{library()} calls; (2) We are likely to want to install the packages when attaching/loading them but they have not been installed. } \examples{ library(xfun) pkg_attach("stats", "graphics") # pkg_attach2('servr') # automatically install servr if it is not installed (pkg_load("stats", "graphics")) } \seealso{ \code{pkg_attach2()} is similar to \code{pacman::p_load()}, but does not allow non-standard evaluation (NSE) of the \code{...} argument, i.e., you must pass a real character vector of package names to it, and all names must be quoted. Allowing NSE adds too much complexity with too little gain (the only gain is that it saves your effort in typing two quotes). } xfun/man/crandalf_check.Rd0000644000175000017500000000452614156162700015346 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/revcheck.R \name{crandalf_check} \alias{crandalf_check} \alias{crandalf_results} \title{Submit check jobs to crandalf} \usage{ crandalf_check(pkg, size = 400, jobs = Inf, which = "all") crandalf_results(pkg, repo = NA, limit = 200, wait = 5 * 60) } \arguments{ \item{pkg}{The package name of which the reverse dependencies are to be checked.} \item{size}{The number of reverse dependencies to be checked in each job.} \item{jobs}{The number of jobs to run in Github Actions (by default, all jobs are submitted, but you can choose to submit the first few jobs).} \item{which}{The type of dependencies (see \code{\link{rev_check}()}).} \item{repo}{The crandalf repo on Github (of the form \code{user/repo} such as \code{"yihui/crandalf"}). Usually you do not need to specify it, unless you are not calling this function inside the crandalf project, because \command{gh} should be able to figure out the repo automatically.} \item{limit}{The maximum of records for \command{gh run list} to retrieve. You only need a larger number if the check results are very early in the Github Action history.} \item{wait}{Number of seconds to wait if not all jobs have been completed on Github. By default, this function checks the status every 5 minutes until all jobs are completed. Set \code{wait} to 0 to disable waiting (and throw an error immediately when any jobs are not completed).} } \description{ Check the reverse dependencies of a package using the crandalf service: \url{https://github.com/yihui/crandalf}. If the number of reverse dependencies is large, they will be split into batches and pushed to crandalf one by one. } \details{ Due to the time limit of a single job on Github Actions (6 hours), you will have to split the large number of reverse dependencies into batches and check them sequentially on Github (at most 5 jobs in parallel). The function \code{crandalf_check()} does this automatically when necessary. It requires the \command{git} command to be available. The function \code{crandalf_results()} fetches check results from Github after all checks are completed, merge the results, and show a full summary of check results. It requires \code{gh} (Github CLI: \url{https://cli.github.com/manual/}) to be installed and you also need to authenticate with your Github account beforehand. } xfun/man/mark_dirs.Rd0000644000175000017500000000126514156162700014407 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{mark_dirs} \alias{mark_dirs} \title{Mark some paths as directories} \usage{ mark_dirs(x) } \arguments{ \item{x}{Character vector of paths to files and directories.} } \description{ Add a trailing backlash to a file path if this is a directory. This is useful in messages to the console for example to quickly identify directories from files. } \details{ If \code{x} is a vector of relative paths, directory test is done with path relative to the current working dir. Use \code{xfun::\link{in_dir}()} or use absolute paths. } \examples{ mark_dirs(list.files(find.package("xfun"), full.names = TRUE)) } xfun/man/split_source.Rd0000644000175000017500000000105214156162700015141 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/string.R \name{split_source} \alias{split_source} \title{Split source lines into complete expressions} \usage{ split_source(x) } \arguments{ \item{x}{A character vector of R source code.} } \value{ A list of character vectors, and each vector contains a complete R expression. } \description{ Parse the lines of code one by one to find complete expressions in the code, and put them in a list. } \examples{ xfun::split_source(c("if (TRUE) {", "1 + 1", "}", "print(1:5)")) } xfun/man/install_dir.Rd0000644000175000017500000000146614156162700014743 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/packages.R \name{install_dir} \alias{install_dir} \title{Install a source package from a directory} \usage{ install_dir(pkg, build = TRUE, build_opts = NULL, install_opts = NULL) } \arguments{ \item{pkg}{The package source directory.} \item{build}{Whether to build a tarball from the source directory. If \code{FALSE}, run \command{R CMD INSTALL} on the directory directly (note that vignettes will not be automatically built).} \item{build_opts}{The options for \command{R CMD build}.} \item{install_opts}{The options for \command{R CMD INSTALL}.} } \value{ Invisible status from \command{R CMD INSTALL}. } \description{ Run \command{R CMD build} to build a tarball from a source directory, and run \command{R CMD INSTALL} to install it. } xfun/man/upload_ftp.Rd0000644000175000017500000000266014156162700014571 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{upload_ftp} \alias{upload_ftp} \alias{upload_win_builder} \title{Upload to an FTP server via \command{curl}} \usage{ upload_ftp(file, server, dir = "") upload_win_builder( file = pkg_build(), version = c("R-devel", "R-release", "R-oldrelease"), server = c("ftp", "https"), solaris = pkg_available("rhub") ) } \arguments{ \item{file}{Path to a local file.} \item{server}{The address of the FTP server. For \code{upload_win_builder()}, \code{server = 'https'} means uploading to \code{'https://win-builder.r-project.org/upload.aspx'}.} \item{dir}{The remote directory to which the file should be uploaded.} \item{version}{The R version(s) on win-builder.} \item{solaris}{Whether to also upload the package to the Rhub server to check it on Solaris.} } \value{ Status code returned from \code{\link{system2}()} or \code{curl::curl_fetch_memory()}. } \description{ The function \code{upload_ftp()} runs the command \command{curl -T file server} to upload a file to an FTP server if the system command \command{curl} is available, otherwise it uses the R package \pkg{curl}. The function \code{upload_win_builder()} uses \code{upload_ftp()} to upload packages to the win-builder server. } \details{ These functions were written mainly to save package developers the trouble of going to the win-builder web page and uploading packages there manually. } xfun/man/file_ext.Rd0000644000175000017500000000234014156162700014226 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{file_ext} \alias{file_ext} \alias{sans_ext} \alias{with_ext} \title{Manipulate filename extensions} \usage{ file_ext(x) sans_ext(x) with_ext(x, ext) } \arguments{ \item{x}{A character of file paths.} \item{ext}{A vector of new extensions. It must be either of length 1, or the same length as \code{x}.} } \value{ A character vector of the same length as \code{x}. } \description{ Functions to obtain (\code{file_ext()}), remove (\code{sans_ext()}), and change (\code{with_ext()}) extensions in filenames. } \details{ \code{file_ext()} is similar to \code{tools::\link{file_ext}()}, and \code{sans_ext()} is similar to \code{tools::\link{file_path_sans_ext}()}. The main differences are that they treat \code{tar.(gz|bz2|xz)} and \code{nb.html} as extensions (but functions in the \pkg{tools} package doesn't allow double extensions by default), and allow characters \code{~} and \code{#} to be present at the end of a filename. } \examples{ library(xfun) p = c("abc.doc", "def123.tex", "path/to/foo.Rmd", "backup.ppt~", "pkg.tar.xz") file_ext(p) sans_ext(p) with_ext(p, ".txt") with_ext(p, c(".ppt", ".sty", ".Rnw", "doc", "zip")) with_ext(p, "html") } xfun/man/raw_string.Rd0000644000175000017500000000174114156162700014612 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/data-structure.R \name{raw_string} \alias{raw_string} \alias{print.xfun_raw_string} \title{Print a character vector in its raw form} \usage{ raw_string(x) \method{print}{xfun_raw_string}(x, ...) } \arguments{ \item{x}{For \code{raw_string()}, a character vector. For the print method, the \code{raw_string()} object.} \item{...}{Other arguments (currently ignored).} } \description{ The function \code{raw_string()} assigns the class \code{xfun_raw_string} to the character vector, and the corresponding printing function \code{print.xfun_raw_string()} uses \code{cat(x, sep = '\n')} to write the character vector to the console, which will suppress the leading indices (such as \code{[1]}) and double quotes, and it may be easier to read the characters in the raw form (especially when there are escape sequences). } \examples{ library(xfun) raw_string(head(LETTERS)) raw_string(c("a \"b\"", "hello\tworld!")) } xfun/man/process_file.Rd0000644000175000017500000000224514156162700015110 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/io.R \name{process_file} \alias{process_file} \alias{sort_file} \title{Read a text file, process the text with a function, and write the text back} \usage{ process_file(file, fun = identity, x = read_utf8(file)) sort_file(..., fun = sort) } \arguments{ \item{file}{Path to a text file.} \item{fun}{A function to process the text.} \item{x}{The content of the file.} \item{...}{Arguments to be passed to \code{process_file()}.} } \value{ If \code{file} is provided, invisible \code{NULL} (the file is updated as a side effect), otherwise the processed content (as a character vector). } \description{ Read a text file with the UTF-8 encoding, apply a function to the text, and write back to the original file. } \details{ \code{sort_file()} is an application of \code{process_file()}, with the processing function being \code{\link{sort}()}, i.e., it sorts the text lines in a file and write back the sorted text. } \examples{ f = tempfile() xfun::write_utf8("Hello World", f) xfun::process_file(f, function(x) gsub("World", "woRld", x)) xfun::read_utf8(f) # see if it has been updated file.remove(f) } xfun/man/stringsAsStrings.Rd0000644000175000017500000000132014156162700015753 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{stringsAsStrings} \alias{stringsAsStrings} \alias{strings_please} \title{Set the global option \code{\link{options}(stringsAsFactors = FALSE)} inside a parent function and restore the option after the parent function exits} \usage{ stringsAsStrings() strings_please() } \description{ This is a shorthand of \code{opts = options(stringsAsFactors = FALSE); on.exit(options(opts), add = TRUE)}; \code{strings_please()} is an alias of \code{stringsAsStrings()}. } \examples{ f = function() { xfun::strings_please() data.frame(x = letters[1:4], y = factor(letters[1:4])) } str(f()) # the first column should be character } xfun/man/is_abs_path.Rd0000644000175000017500000000132414156162700014704 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{is_abs_path} \alias{is_abs_path} \alias{is_rel_path} \title{Test if paths are relative or absolute} \usage{ is_abs_path(x) is_rel_path(x) } \arguments{ \item{x}{A vector of paths.} } \value{ A logical vector. } \description{ On Unix, check if the paths start with \file{/} or \file{~} (if they do, they are absolute paths). On Windows, check if a path remains the same (via \code{xfun::\link{same_path}()}) if it is prepended with \file{./} (if it does, it is a relative path). } \examples{ xfun::is_abs_path(c("C:/foo", "foo.txt", "/Users/john/", tempdir())) xfun::is_rel_path(c("C:/foo", "foo.txt", "/Users/john/", tempdir())) } xfun/man/cache_rds.Rd0000644000175000017500000001421714156162700014350 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/cache.R \name{cache_rds} \alias{cache_rds} \title{Cache the value of an R expression to an RDS file} \usage{ cache_rds( expr = { }, rerun = FALSE, file = "cache.rds", dir = "cache/", hash = NULL, clean = getOption("xfun.cache_rds.clean", TRUE), ... ) } \arguments{ \item{expr}{An R expression.} \item{rerun}{Whether to delete the RDS file, rerun the expression, and save the result again (i.e., invalidate the cache if it exists).} \item{file}{The \emph{base} (see Details) cache filename under the directory specified by the \code{dir} argument. If not specified and this function is called inside a code chunk of a \pkg{knitr} document (e.g., an R Markdown document), the default is the current chunk label plus the extension \file{.rds}.} \item{dir}{The path of the RDS file is partially determined by \code{paste0(dir, file)}. If not specified and the \pkg{knitr} package is available, the default value of \code{dir} is the \pkg{knitr} chunk option \code{cache.path} (so if you are compiling a \pkg{knitr} document, you do not need to provide this \code{dir} argument explicitly), otherwise the default is \file{cache/}. If you do not want to provide a \code{dir} but simply a valid path to the \code{file} argument, you may use \code{dir = ""}.} \item{hash}{A \code{list} object that contributes to the MD5 hash of the cache filename (see Details). It can also take a special character value \code{"auto"}. Other types of objects are ignored.} \item{clean}{Whether to clean up the old cache files automatically when \code{expr} has changed.} \item{...}{Other arguments to be passed to \code{\link{saveRDS}()}.} } \value{ If the cache file does not exist, run the expression and save the result to the file, otherwise read the cache file and return the value. } \description{ Save the value of an expression to a cache file (of the RDS format). Next time the value is loaded from the file if it exists. } \details{ Note that the \code{file} argument does not provide the full cache filename. The actual name of the cache file is of the form \file{BASENAME_HASH.rds}, where \file{BASENAME} is the base name provided via the \file{file} argument (e.g., if \code{file = 'foo.rds'}, \code{BASENAME} would be \file{foo}), and \file{HASH} is the MD5 hash (also called the \sQuote{checksum}) calculated from the R code provided to the \code{expr} argument and the value of the \code{hash} argument, which means when the code or the \code{hash} argument changes, the \file{HASH} string may also change, and the old cache will be invalidated (if it exists). If you want to find the cache file, look for \file{.rds} files that contain 32 hexadecimal digits (consisting of 0-9 and a-z) at the end of the filename. The possible ways to invalidate the cache are: 1) change the code in \code{expr} argument; 2) delete the cache file manually or automatically through the argument \code{rerun = TRUE}; and 3) change the value of the \code{hash} argument. The first two ways should be obvious. For the third way, it makes it possible to automatically invalidate the cache based on changes in certain R objects. For example, when you run \code{cache_rds({ x + y })}, you may want to invalidate the cache to rerun \code{{ x + y }} when the value of \code{x} or \code{y} has been changed, and you can tell \code{cache_rds()} to do so by \code{cache_rds({ x + y }, hash = list(x, y))}. The value of the argument \code{hash} is expected to be a list, but it can also take a special value, \code{"auto"}, which means \code{cache_rds(expr)} will try to automatically figure out the global variables in \code{expr}, return a list of their values, and use this list as the actual value of \code{hash}. This behavior is most likely to be what you really want: if the code in \code{expr} uses an external global variable, you may want to invalidate the cache if the value of the global variable has changed. Here a \dQuote{global variable} means a variable not created locally in \code{expr}, e.g., for \code{cache_rds({ x <- 1; x + y })}, \code{x} is a local variable, and \code{y} is (most likely to be) a global variable, so changes in \code{y} should invalidate the cache. However, you know your own code the best. If you want to be completely sure when to invalidate the cache, you can always provide a list of objects explicitly rather than relying on \code{hash = "auto"}. By default (the argument \code{clean = TRUE}), old cache files will be automatically cleaned up. Sometimes you may want to use \code{clean = FALSE} (set the R global option \code{options(xfun.cache_rds.clean = FALSE)} if you want \code{FALSE} to be the default). For example, you may not have decided which version of code to use, and you can keep the cache of both versions with \code{clean = FALSE}, so when you switch between the two versions of code, it will still be fast to run the code. } \note{ Changes in the code in the \code{expr} argument do not necessarily always invalidate the cache, if the changed code is \code{\link{parse}d} to the same expression as the previous version of the code. For example, if you have run \code{cache_rds({Sys.sleep(5);1+1})} before, running \code{cache_rds({ Sys.sleep( 5 ) ; 1 + 1 })} will use the cache, because the two expressions are essentially the same (they only differ in white spaces). Usually you can add/delete white spaces or comments to your code in \code{expr} without invalidating the cache. See the package vignette \code{vignette('xfun', package = 'xfun')} for more examples. When this function is called in a code chunk of a \pkg{knitr} document, you may not want to provide the filename or directory of the cache file, because they have reasonable defaults. Side-effects (such as plots or printed output) will not be cached. The cache only stores the last value of the expression in \code{expr}. } \examples{ f = tempfile() # the cache file compute = function(...) { res = xfun::cache_rds({ Sys.sleep(1) 1:10 }, file = f, dir = "", ...) res } compute() # takes one second compute() # returns 1:10 immediately compute() # fast again compute(rerun = TRUE) # one second to rerun compute() file.remove(f) } xfun/man/from_root.Rd0000644000175000017500000000223514156162700014440 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{from_root} \alias{from_root} \title{Get the relative path of a path in a project relative to the current working directory} \usage{ from_root(..., root = proj_root(), error = TRUE) } \arguments{ \item{...}{A character vector of path components \emph{relative to the root directory of the project}.} \item{root}{The root directory of the project.} \item{error}{Whether to signal an error if the path cannot be converted to a relative path.} } \value{ A relative path, or an error when the project root directory cannot be determined or the conversion failed and \code{error = TRUE}. } \description{ First compose an absolute path using the project root directory and the relative path components, i.e., \code{\link{file.path}(root, ...)}. Then convert it to a relative path with \code{\link{relative_path}()}, which is relative to the current working directory. } \details{ This function was inspired by \code{here::here()}, and the major difference is that it returns a relative path by default, which is more portable. } \examples{ \dontrun{ xfun::from_root("data", "mtcars.csv") } } xfun/man/try_silent.Rd0000644000175000017500000000060214156162700014622 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{try_silent} \alias{try_silent} \title{Try to evaluate an expression silently} \usage{ try_silent(expr) } \arguments{ \item{expr}{An R expression.} } \description{ An abbreviation of \code{try(silent = TRUE)}. } \examples{ library(xfun) z = try_silent(stop("Wrong!")) inherits(z, "try-error") } xfun/man/normalize_path.Rd0000644000175000017500000000067714156162700015456 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{normalize_path} \alias{normalize_path} \title{Normalize paths} \usage{ normalize_path(x, winslash = "/", must_work = FALSE) } \arguments{ \item{x, winslash, must_work}{Arguments passed to \code{\link{normalizePath}()}.} } \description{ A wrapper function of \code{normalizePath()} with different defaults. } \examples{ library(xfun) normalize_path("~") } xfun/man/proc_kill.Rd0000644000175000017500000000120014156162700014377 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/command.R \name{proc_kill} \alias{proc_kill} \title{Kill a process and (optionally) all its child processes} \usage{ proc_kill(pid, recursive = TRUE, ...) } \arguments{ \item{pid}{The process ID.} \item{recursive}{Whether to kill the child processes of the process.} \item{...}{Arguments to be passed to \code{\link{system2}()} to run the command to kill the process.} } \value{ The status code returned from \code{system2()}. } \description{ Run the command \command{taskkill /f /pid} on Windows and \command{kill} on Unix, respectively, to kill a process. } xfun/man/retry.Rd0000644000175000017500000000166714156162700013607 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/utils.R \name{retry} \alias{retry} \title{Retry calling a function for a number of times} \usage{ retry(fun, ..., .times = 3, .pause = 5) } \arguments{ \item{fun}{A function.} \item{...}{Arguments to be passed to the function.} \item{.times}{The number of times.} \item{.pause}{The number of seconds to wait before the next attempt.} } \description{ If the function returns an error, retry it for the specified number of times, with a pause between attempts. } \details{ One application of this function is to download a web resource. Since the download might fail sometimes, you may want to retry it for a few more times. } \examples{\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} # read the Github releases info of the repo yihui/xfun xfun::retry(xfun::github_releases, "yihui/xfun") \dontshow{\}) # examplesIf} } xfun/man/do_once.Rd0000644000175000017500000000313214156162700014035 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/session.R \name{do_once} \alias{do_once} \title{Perform a task once in an R session} \usage{ do_once( task, option, hint = c("You will not see this message again in this R session.", "If you never want to see this message,", sprintf("you may set options(\%s = FALSE) in your .Rprofile.", option)) ) } \arguments{ \item{task}{Any R code expression to be evaluated once to perform a task, e.g., \code{warning('Danger!')} or \code{message('Today is ', Sys.Date())}.} \item{option}{An R option name. This name should be as unique as possible in \code{\link{options}()}. After the task has been successfully performed, this option will be set to \code{FALSE} in the current R session, to prevent the task from being performed again the next time when \code{do_once()} is called.} \item{hint}{A character vector to provide a hint to users on how not to perform the task or see the message again in the current R session. Set \code{hint = ""} if you do not want to provide the hint.} } \value{ The value returned by the \code{task}, invisibly. } \description{ Perform a task once in an R session, e.g., emit a message or warning. Then give users an optional hint on how not to perform this task at all. } \examples{ do_once(message("Today's date is ", Sys.Date()), "xfun.date.reminder") # if you run it again, it will not emit the message again do_once(message("Today's date is ", Sys.Date()), "xfun.date.reminder") do_once({ Sys.sleep(2) 1 + 1 }, "xfun.task.1plus1") do_once({ Sys.sleep(2) 1 + 1 }, "xfun.task.1plus1") } xfun/man/is_web_path.Rd0000644000175000017500000000075014156162700014716 0ustar nileshnilesh% Generated by roxygen2: do not edit by hand % Please edit documentation in R/paths.R \name{is_web_path} \alias{is_web_path} \title{Test if a path is a web path} \usage{ is_web_path(x) } \arguments{ \item{x}{A vector of paths.} } \value{ A logical vector. } \description{ Check if a path starts with \file{http://} or \file{https://} or \file{ftp://} or \file{ftps://}. } \examples{ xfun::is_web_path("https://www.r-project.org") # TRUE xfun::is_web_path("www.r-project.org") # FALSE } xfun/src/0000755000175000017500000000000014156200274012154 5ustar nileshnileshxfun/src/Makevars0000644000175000017500000000002013673475145013656 0ustar nileshnileshCXX_STD = CXX11 xfun/src/base64.c0000644000175000017500000001432213732647320013414 0ustar nileshnilesh#include #include #include #include const char base64_table[65] = { 'A','B','C','D','E','F','G','H', 'I','J','K','L','M','N','O','P', 'Q','R','S','T','U','V','W','X', 'Y','Z','a','b','c','d','e','f', 'g','h','i','j','k','l','m','n', 'o','p','q','r','s','t','u','v', 'w','x','y','z','0','1','2','3', '4','5','6','7','8','9','+','/', '%' }; const char padding = '='; const unsigned char upadding = '='; void base64_encode_impl( const unsigned char* input, const R_xlen_t input_len, char* output, const R_xlen_t output_len ) { R_xlen_t input_len_left = input_len; R_xlen_t i1 = 0; R_xlen_t i2 = 0; while(input_len_left > 2) { output[i2++] = base64_table[input[i1] / 4]; output[i2++] = base64_table[16 * (input[i1] % 4) + input[ i1 + 1 ] / 16]; output[i2++] = base64_table[4 * (input [ i1 + 1] % 16) + input[i1 + 2] / 64]; output[i2++] = base64_table[input[i1 + 2] % 64]; i1 += 3; input_len_left -= 3; } if (input_len_left) { output[i2++] = base64_table[input[i1] / 4]; if (input_len_left > 1) { output[i2++] = base64_table[16 * (input[i1] % 4) + input[ i1 + 1 ] / 16]; output[i2++] = base64_table[4 * (input [i1 + 1] % 16)]; output[i2++] = padding; } else { output[i2++] = base64_table[16 * (input[i1] % 4)]; output[i2++] = padding; output[i2++] = padding; } } } static const short base64_reverse_table[256] = { -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -2, -2, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, 62, -2, -2, -2, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -2, -2, -2, -2, 0, 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, -2, -2, -2, -2, -2, -2, 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, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }; int base64_decode_impl( const unsigned char* input, R_xlen_t input_len, unsigned char* output, R_xlen_t* poutput_len ) { R_xlen_t output_len = *poutput_len; for (R_xlen_t i = 0;i < output_len;i++) { output[i] = 0; } int ch = 0; R_xlen_t i = 0, j = 0, k = 0; while(input_len-- > 0) { ch = *input++; if (ch == padding) { if (*input != upadding && (i % 4) == 1) { return 1; } continue; } ch = base64_reverse_table[ch]; if (ch == -1) { continue; } else if (ch == -2) { return 1; } switch (i % 4) { case 0 : { output[j] = ch << 2; break; } case 1 : { output[j++] |= ch >> 4; if (j < output_len) output[j] = (ch & 0x0f) << 4; break; } case 2 : { output[j++] |= ch >> 2; if (j < output_len) output[j] = (ch & 0x03) << 6; break; } case 3 : { output[j++] |= ch; break; } } i++; } k = j; if (ch == padding ) { switch (i % 4) { case 1 : { return 1; } case 2 : k++; case 3 : if (k < output_len) output[k] = 0; } } *poutput_len = j; return 0; } SEXP base64_enc(SEXP input) { int rv = 0; // get input R_xlen_t input_len; #if defined(R_VERSION) && R_VERSION >= R_Version(3,0,0) input_len = XLENGTH(input); #else input_len = LENGTH(input); #endif R_xlen_t output_len = input_len / 3 * 4; if (input_len % 3) { output_len += 4; } unsigned char *input_content = (unsigned char*) RAW(input); // declare output // allocate memory SEXP result = PROTECT(NEW_CHARACTER(1)); if (result) { // check if memory is allocated successfully char* result_content = (char*) malloc(output_len + 1); if (result_content) { // check if memory is allocated successfully base64_encode_impl(input_content, input_len, result_content, output_len); result_content[output_len] = 0; SET_STRING_ELT(result, 0, mkChar(result_content)); free(result_content); } else { rv = 2; } } else { rv = 1; } UNPROTECT(1); switch (rv) { case 1 : { error("Failed to allocate memory for result"); break; } case 2 : { error("Failed to allocate memory for result_content"); break; } default : { } } return result; } SEXP base64_dec(SEXP input) { int rv = 0; R_xlen_t input_len; SEXP result = R_NilValue; #if defined(R_VERSION) && R_VERSION >= R_Version(3,0,0) input_len = XLENGTH(input); #else input_len = LENGTH(input); #endif if (input_len != 1 || TYPEOF(input) != STRSXP) { rv = 1; } else { SEXP input_char = STRING_ELT(input, 0); const unsigned char* input_p = (unsigned char*) CHAR(input_char); const R_xlen_t input_str_size = strlen((const char*) input_p); if (input_str_size % 4 != 0) { rv = 2; } else { R_xlen_t output_len = input_str_size; unsigned char* result_content = malloc(output_len); if (result_content) { if (base64_decode_impl( input_p, input_str_size, result_content, &output_len ) != 0) { rv = 2; } else { result = PROTECT(NEW_RAW(output_len)); unsigned char* presult = RAW_POINTER(result); for (R_xlen_t i = 0;i < output_len;i++) { presult[i] = result_content[i]; } UNPROTECT(1); } free(result_content); } else { rv = 3; result = R_NilValue; } } } switch (rv) { case 1 : { error("The input should be a character vector with length 1"); break; } case 2 : { error("The input string is not a valid base64 encoded string"); break; } case 3 : { error("Failed to allocate memory for result"); break; } } return result; } xfun/src/init.c0000644000175000017500000000072013706122175013265 0ustar nileshnilesh#include #include #include // for NULL #include /* .Call calls */ extern SEXP base64_enc(SEXP); extern SEXP base64_dec(SEXP); static const R_CallMethodDef CallEntries[] = { {"base64_enc", (DL_FUNC) &base64_enc, 1}, {"base64_dec", (DL_FUNC) &base64_dec, 1}, {NULL, NULL, 0} }; void R_init_xfun(DllInfo *dll) { R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); R_useDynamicSymbols(dll, FALSE); } xfun/vignettes/0000755000175000017500000000000014156200274013375 5ustar nileshnileshxfun/vignettes/xfun.Rmd0000644000175000017500000003336214143234661015033 0ustar nileshnilesh--- title: An Introduction to xfun subtitle: A Collection of Miscellaneous Functions author: "Yihui Xie" date: "`r Sys.Date()`" slug: xfun githubEditURL: https://github.com/yihui/xfun/edit/master/vignettes/xfun.Rmd output: knitr:::html_vignette: toc: yes vignette: > %\VignetteIndexEntry{An Introduction to xfun} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} library(xfun) ``` After writing about 20 R packages, I found I had accumulated several utility functions that I used across different packages, so I decided to extract them into a separate package. Previously I had been using the evil triple-colon `:::` to access these internal utility functions. Now with **xfun**, these functions have been exported, and more importantly, documented. It should be better to use them under the sun instead of in the dark. This page shows examples of a subset of functions in this package. For a full list of functions, see the help page `help(package = 'xfun')`. The source package is available on Github: https://github.com/yihui/xfun. ## No more partial matching for lists! I have been bitten many times by partial matching in lists, e.g., when I want `x$a` but the element `a` does not exist in the list `x`, it returns the value `x$abc` if `abc` exists in `x`. A strict list is a list for which the partial matching of the `$` operator is disabled. The functions `xfun::strict_list()` and `xfun::as_strict_list()` are the equivalents to `base::list()` and `base::as.list()` respectively which always return as strict list, e.g., ```{r} library(xfun) (z = strict_list(aaa = "I am aaa", b = 1:5)) z$a # NULL (strict matching) z$aaa # I am aaa z$b z$c = "you can create a new element" z2 = unclass(z) # a normal list z2$a # partial matching z3 = as_strict_list(z2) # a strict list again z3$a # NULL (strict matching) again! ``` Similarly, the default partial matching in `attr()` can be annoying, too. The function `xfun::attr()` is simply a shorthand of `attr(..., exact = TRUE)`. I want it, or I do not want. There is no "I probably want". ## Output character vectors for human eyes When R prints a character vector, your eyes may be distracted by the indices like `[1]`, double quotes, and escape sequences. To see a character vector in its "raw" form, you can use `cat(..., sep = '\n')`. The function `raw_string()` marks a character vector as "raw", and the corresponding printing function will call `cat(sep = '\n')` to print the character vector to the console. ```{r comment=''} library(xfun) raw_string(head(LETTERS)) (x = c("a \"b\"", "hello\tworld!")) raw_string(x) # this is more likely to be what you want to see ``` ## Print the content of a text file I have used `paste(readLines('foo'), collapse = '\n')` many times before I decided to write a simple wrapper function `xfun::file_string()`. This function also makes use of `raw_string()`, so you can see the content of a file in the console as a side-effect, e.g., ```{r comment=''} f = system.file("LICENSE", package = "xfun") xfun::file_string(f) as.character(xfun::file_string(f)) # essentially a character string ``` ## Get the data URI of a file Files can be encoded into base64 strings via `base64_uri()`. This is a common technique to embed arbitrary files in HTML documents (which is [what `xfun::embed_file()` does](https://bookdown.org/yihui/rmarkdown-cookbook/embed-file.html) and it is based on `base64_uri()`). ```{r} f = system.file("LICENSE", package = "xfun") xfun::base64_uri(f) ``` ## Match strings and do substitutions After typing the code `x = grep(pattern, x, value = TRUE); gsub(pattern, '\\1', x)` many times, I combined them into a single function `xfun::grep_sub()`. ```{r} xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE) ``` ## Search and replace strings in files I can never remember how to properly use `grep` or `sed` to search and replace strings in multiple files. My favorite IDE, RStudio, has not provided this feature yet (you can only search and replace in the currently opened file). Therefore I did a quick and dirty implementation in R, including functions `gsub_files()`, `gsub_dir()`, and `gsub_ext()`, to search and replace strings in multiple files under a directory. Note that the files are assumed to be encoded in UTF-8. If you do not use UTF-8, we cannot be friends. Seriously. All functions are based on `gsub_file()`, which performs searching and replacing in a single file, e.g., ```{r comment=''} library(xfun) f = tempfile() writeLines(c("hello", "world"), f) gsub_file(f, "world", "woRld", fixed = TRUE) file_string(f) ``` The function `gsub_dir()` is very flexible: you can limit the list of files by MIME types, or extensions. For example, if you want to do substitution in text files, you may use `gsub_dir(..., mimetype = '^text/')`. The function `process_file()` is a more general way to process files. Basically it reads a file, process the content with a function that you pass to it, and writes back the text, e.g., ```{r, comment=''} process_file(f, function(x) { rep(x, 3) # repeat the content 3 times }) file_string(f) ``` **WARNING**: Before using these functions, make sure that you have backed up your files, or version control your files. The files will be modified in-place. If you do not back up or use version control, there is no chance to regret. ## Manipulate filename extensions Functions `file_ext()` and `sans_ext()` are based on functions in **tools**. The function `with_ext()` adds or replaces extensions of filenames, and it is vectorized. ```{r} library(xfun) p = c("abc.doc", "def123.tex", "path/to/foo.Rmd") file_ext(p) sans_ext(p) with_ext(p, ".txt") with_ext(p, c(".ppt", ".sty", ".Rnw")) with_ext(p, "html") ``` ## Find files (in a project) without the pain of thinking about absolute/relative paths The function `proj_root()` was inspired by the **rprojroot** package, and tries to find the root directory of a project. Currently it only supports R package projects and RStudio projects by default. It is much less sophisticated than **rprojroot**. The function `from_root()` was inspired by `here::here()`, but returns a relative path (relative to the project's root directory found by `proj_root()`) instead of an absolute path. For example, `xfun::from_root('data', 'cars.csv')` in a code chunk of `docs/foo.Rmd` will return `../data/cars.csv` when `docs/` and `data/` directories are under the root directory of a project. ``` root/ |-- data/ | |-- cars.csv | |-- docs/ |-- foo.Rmd ``` If file paths are too much pain for you to think about, you can just pass an incomplete path to the function `magic_path()`, and it will try to find the actual path recursively under subdirectories of a root directory. For example, you may only provide a base filename, and `magic_path()` will look for this file under subdirectories and return the actual path if it is found. By default, it returns a relative path, which is relative to the current working directory. With the above example, `xfun::magic_path('cars.csv')` in a code chunk of `docs/foo.Rmd` will return `../data/cars.csv`, if `cars.csv` is a unique filename in the project. You can freely move it to any folders of this project, and `magic_path()` will still find it. If you are not using a project to manage files, `magic_path()` will look for the file under subdirectories of the current working directory. ## Types of operating systems The series of functions `is_linux()`, `is_macos()`, `is_unix()`, and `is_windows()` test the types of the OS, using the information from `.Platform` and `Sys.info()`, e.g., ```{r} xfun::is_macos() xfun::is_unix() xfun::is_linux() xfun::is_windows() ``` ## Loading and attaching packages Oftentimes I see users attach a series of packages in the beginning of their scripts by repeating `library()` multiple times. This could be easily vectorized, and the function `xfun::pkg_attach()` does this job. For example, ```{r eval=FALSE} library(testit) library(parallel) library(tinytex) library(mime) ``` is equivalent to ```{r eval=FALSE} xfun::pkg_attach(c('testit', 'parallel', 'tinytex', 'mime')) ``` I also see scripts that contain code to install a package if it is not available, e.g., ```{r eval=FALSE} if (!requireNamespace('tinytex')) install.packages('tinytex') library(tinytex) ``` This could be done via ```{r eval=FALSE} xfun::pkg_attach2('tinytex') ``` The function `pkg_attach2()` is a shorthand of `pkg_attach(..., install = TRUE)`, which means if a package is not available, install it. This function can also deal with multiple packages. The function `loadable()` tests if a package is loadable. ## Read/write files in UTF-8 Functions `read_utf8()` and `write_utf8()` can be used to read/write files in UTF-8. They are simple wrappers of `readLines()` and `writeLines()`. ## Convert numbers to English words The function `numbers_to_words()` (or `n2w()` for short) converts numbers to English words. ```{r} n2w(0, cap = TRUE) n2w(seq(0, 121, 11), and = TRUE) n2w(1e+06) n2w(1e+11 + 12345678) n2w(-987654321) n2w(1e+15 - 1) ``` ## Cache an R expression to an RDS file The function `cache_rds()` provides a simple caching mechanism: the first time an expression is passed to it, it saves the result to an RDS file; the next time it will read the RDS file and return the value instead of evaluating the expression again. If you want to invalidate the cache, you can use the argument `rerun = TRUE`. ```{r, eval=FALSE} res = xfun::cache_rds({ # pretend the computing here is a time-consuming Sys.sleep(2) 1:10 }) ``` When the function is used in a code chunk in a **knitr** document, the RDS cache file is saved to a path determined by the chunk label (the base filename) and the chunk option `cache.path` (the cache directory), so you do not have to provide the `file` and `dir` arguments of `cache_rds()`. This caching mechanism is much simpler than **knitr**'s caching. Cache invalidation is often tricky (see [this post](https://yihui.org/en/2018/06/cache-invalidation/)), so this function may be helpful if you want more transparency and control over when to invalidate the cache (for `cache_rds()`, the cache is invalidated when the cache file is deleted, which can be achieved via the argument `rerun = TRUE`). As documented on the help page of `cache_rds()`, there are two common cases in which you may want to invalidate the cache: 1. The code in the expression has changed, e.g., if you changed the code from `cache_rds({x + 1})` to `cache_rds({x + 2})`, the cache will be automatically invalidated and the expression will be re-evaluated. However, please note that changes in white spaces or comments do not matter. Or generally speaking, as long as the change does not affect the parsed expression, the cache will not be invalidated, e.g., the two expressions below are essentially identical (hence if you have executed `cache_rds()` on the first expression, the second expression will be able to take advantage of the cache): ```r res = xfun::cache_rds({ Sys.sleep(3 ); x=1:10; # semi-colons won't matter x+1; }) res = xfun::cache_rds({ Sys.sleep(3) x = 1:10 # a comment x + 1 # feel free to make any changes in white spaces }) ``` 1. The value of a global variable in the expression has changed, e.g., if `y` has changed, you are most likely to want to invalidate the cache and rerun the expression below: ```r res = xfun::cache_rds({ x = 1:10 x + y }) ``` This is because `x` is a local variable in the expression, and `y` is an external global variable (not created locally like `x`). To invalidate the cache when `y` has changed, you may let `cache_rds()` know through the `hash` argument that `y` needs to be considered when deciding if the cache should be invalidated: ```r res = xfun::cache_rds({ x = 1:10 x + y }, hash = list(y)) ``` If you do not want to provide this list of value(s) to the `hash` argument, you may try `hash = "auto"` instead, which asks `cache_rds()` to try to figure out all global variables automatically and use a list of their values as the value for the `hash` argument. ```r res = xfun::cache_rds({ x = 1:10 x + y }, hash = "auto") ``` ## Check reverse dependencies of a package Running `R CMD check` on the reverse dependencies of **knitr** and **rmarkdown** is my least favorite thing in developing R packages, because the numbers of their reverse dependencies are huge. The function `rev_check()` reflects some of my past experience in this process. I think I have automated it as much as possible, and made it as easy as possible to discover possible new problems introduced by the current version of the package (compared to the CRAN version). Finally I can just sit back and let it run. ## Input a character vector into the RStudio source editor The function `rstudio_type()` inputs characters in the RStudio source editor as if they were typed by a human. I came up with the idea when preparing my talk for rstudio::conf 2018 ([see this post](https://yihui.org/en/2018/03/blogdown-video-rstudio-conf/) for more details). ## Print session information Since I have never been fully satisfied by the output of `sessionInfo()`, I tweaked it to make it more useful in my use cases. For example, it is rarely useful to print out the names of base R packages, or information about the matrix products / BLAS / LAPACK. Oftentimes I want additional information in the session information, such as the Pandoc version when **rmarkdown** is used. The function `session_info()` tweaks the output of `sessionInfo()`, and makes it possible for other packages to append information in the output of `session_info()`. You can choose to print out the versions of only the packages you specify, e.g., ```{r} xfun::session_info(c('xfun', 'rmarkdown', 'knitr', 'tinytex'), dependencies = FALSE) ``` xfun/build/0000755000175000017500000000000014156200274012464 5ustar nileshnileshxfun/build/vignette.rds0000644000175000017500000000032314156200274015021 0ustar nileshnileshb```b`add`b2 1# 'H+ MAwS+)O)M.S(W)ES ֞Q&1a(DXT%iewI-HK î?/S+`zP԰Aհe ,s\ܠL t7`~΢r=xA$Gs=ʕXVr7Lxfun/tests/0000755000175000017500000000000014120530315012520 5ustar nileshnileshxfun/tests/test-cran/0000755000175000017500000000000014156202722014427 5ustar nileshnileshxfun/tests/test-cran/test-encoding.R0000644000175000017500000000222414120530315017306 0ustar nileshnileshlibrary(testit) assert('native_encode() warns against characters that cannot be represented in native encoding', { latin_str = 'fa\u00e7ile' # Latin-1 cn_str = '\u4e2d\u6587\u5b57\u7b26' # UTF-8 Chinese # when the locale is not UTF-8, the above strings cannot be converted to native encoding (isTRUE(l10n_info()[['UTF-8']]) || has_warning({native_encode(cn_str); native_encode(latin_str)})) gb2312_raw = as.raw(c(0xd6, 0xd0, 0xce, 0xc4, 0xd7, 0xd6, 0xb7, 0xfb)) is_gb2312_native = identical(gb2312_raw, charToRaw(enc2native(cn_str))) no_need_test = !(is_windows() && is_gb2312_native) cn_str_native = native_encode(cn_str) (no_need_test || Encoding(cn_str_native) %==% 'unknown') (no_need_test || charToRaw(cn_str_native) %==% gb2312_raw) }) assert('is_ascii() can identify ascii strings', { ascii_str = c('aaa', 'bbb', 'ccc') latin_str = 'fa\u00e7ile' cn_str = '\u4e2d\u6587\u5b57\u7b26' mixed_str = c(ascii_str, latin_str) (is_ascii(ascii_str) %==% c(TRUE, TRUE, TRUE)) (!is_ascii(latin_str)) (!is_ascii(cn_str)) (is_ascii(mixed_str) %==% c(TRUE, TRUE, TRUE, FALSE)) (is_ascii(c(NA_character_, 'a')) %==% c(NA, TRUE)) }) xfun/tests/test-cran/test-io.R0000644000175000017500000000330114120530315016124 0ustar nileshnileshlibrary(testit) assert('invalid_utf8() works and respects NA', { (invalid_utf8(character()) %==% integer()) x = 'fa\xE7ile'; Encoding(x) = 'latin1' (invalid_utf8(c('aaa', x, NA_character_, '', '\u4e2d\u6587')) %==% 2L) }) assert('read/write_utf8() works', { ascii_txt = c('aa', 'bb', 'cc') utf8_txt = c('\u4e2d\u6587', '\u5927\u5bb6\u597d', '\u65e9\u996d') latin_txt = local({x = 'fa\xE7ile'; Encoding(x) = 'latin1'; x}) mixed_txt = c(ascii_txt, latin_txt, utf8_txt) write_utf8(ascii_txt, con = (ascii_file = tempfile())) write_utf8(utf8_txt, con = (utf8_file = tempfile())) write_utf8(latin_txt, con = (latin_file = tempfile())) write_utf8(mixed_txt, con = (mixed_file = tempfile())) (read_utf8(ascii_file) %==% ascii_txt) (read_utf8(utf8_file) %==% utf8_txt) (read_utf8(latin_file) %==% latin_txt) # identical will not compare Encoding (Encoding(read_utf8(latin_file)) %==% 'UTF-8') (read_utf8(mixed_file) %==% mixed_txt) (Encoding(read_utf8(mixed_file)) %==% c(rep('unknown', 3), rep('UTF-8', 4))) mixed_file2 = tempfile() local({ opts = options(encoding = 'native.enc'); on.exit(options(opts), add = TRUE) writeLines(mixed_txt, con = mixed_file2, useBytes = TRUE) }) (suppressWarnings(read_utf8(mixed_file2)[4] != mixed_txt[4])) has_warning(read_utf8(mixed_file2)) has_error(read_utf8(mixed_file2, error = TRUE)) }) assert('empty files produce character() via file_string()', { tmp = tempfile() writeLines(character(), tmp) (file_string(tmp) %==% raw_string(character())) }) assert('grep_sub() matches elements and do substitution on them', { (grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE) %==% c('aBc', 'aBBBc')) }) xfun/tests/test-cran/test-markdown.R0000644000175000017500000000363314120530315017347 0ustar nileshnileshlibrary(testit) assert('prose_index() works', { x = c('a', '```', 'b', '```', 'c') out = c(1L, 5L) (prose_index(x) %==% out) x = c('a', '````', '```r', '1+1', '```', '````', 'c') out = c(1L, 7L) (prose_index(x) %==% out) x = c('a', '``', 'b', '``', 'c') out = seq_along(x) (prose_index(x) %==% out) # a character vector of length zero x = character() out = integer() (prose_index(x) %==% out) # one backbrick x = c('`', 'a', '`') out = seq_along(x) (prose_index(x) %==% out) # two backbrick x = c('``', 'a', '``') out = seq_along(x) (prose_index(x) %==% out) # no code fences x = c('a', 'b') out = c(1L, 2L) (prose_index(x) %==% out) # two code fences x = c('```', 'b', '```', '```', 'd', '```') out = integer() (prose_index(x) %==% out) # code fences commented out x = c('```', 'b', '```', '') (prose_index(x) %==% 4:6) # if the code fences are not balanced x = c('a', '```', 'b', '``', 'c') out = seq_along(x) (has_warning(prose_index(x))) (prose_index(x) %==% out) }) assert('protect_math() puts inline math expressions in backticks', { (protect_math('$x$') %==% '`\\(x\\)`') (protect_math('hi $x$ a') %==% 'hi `\\(x\\)` a') (protect_math('$ a $') %==% '$ a $') (protect_math(' $a$') %==% ' `\\(a\\)`') (protect_math('$ x$') %==% '$ x$') (protect_math('$x $') %==% '$x $') (protect_math('b$a$') %==% 'b$a$') # no space before $; unlikely to be math (protect_math('`$a$`') %==% '`$a$`') (protect_math('hi $x$9') %==% 'hi $x$9') (protect_math('$500 $600') %==% '$500 $600') (protect_math('$$a$$') %==% '`$$a$$`') (protect_math('$$a$') %==% '$$a$') (protect_math('hi $$\alpha$$') %==% 'hi `$$\alpha$$`') (protect_math('hi $$\alpha $$') %==% 'hi $$\alpha $$') (protect_math('hi $$ \alpha$$') %==% 'hi $$ \alpha$$') (protect_math('hi $$\alpha$$ and $$ \alpha$$') %==% 'hi `$$\alpha$$` and $$ \alpha$$') }) xfun/tests/test-cran/test-data-structure.R0000644000175000017500000000374414120530315020477 0ustar nileshnileshlibrary(testit) assert('strict_list() is really strict', { s_list = strict_list(aaa = 1:3, bbb = c('hey', 'mom')) # the class name better not be changed in the future (inherits(s_list, 'xfun_strict_list')) # `$` returns the expected value if the name provided is complete (s_list$aaa %==% 1:3) # but partial match is prohibited (s_list$a %==% NULL) # `[` will return a list - will it be better if returns a strict list? (inherits(s_list['aaa'], 'list')) # and add an element won't change the class by accident s_list$ccc = 4 s_list$ddd = 'abcd' (inherits(s_list, 'xfun_strict_list')) }) assert('as_strict_list() converts a list to a strict list', { normal_list = list(aaa = 1:3, bbb = c('hey', 'mom')) s_list = as_strict_list(normal_list) # does the strict list have the same length as the normal list? (length(normal_list) %==% length(s_list)) # is the converted strict list equal to the same object created by `strict_list()`? s_list = strict_list(aaa = 1:3, bbb = c('hey', 'mom')) (as_strict_list(normal_list) %==% s_list) }) assert('print() returns the same output for strict and normal list', { normal_list = list(aaa = 1:2, bbb = c('hey', 'dad')) s_list = as_strict_list(normal_list) (capture.output(print(normal_list)) %==% capture.output(print(s_list))) }) assert('raw_string() prints as expected', { rs = raw_string(c('a "b"', 'hello\tworld!')) (inherits(rs, 'xfun_raw_string')) output = capture.output(rs) (output %==% c('a "b"', 'hello\tworld!')) }) assert('raw_string() returns 0-length xfun_raw_string when input is NULL', { rs1 = raw_string(NULL) (inherits(rs1, 'xfun_raw_string')) (length(rs1) == 0) (capture.output(rs1) %==% character(0)) # string 'NULL' is treated appropriately rs2 = raw_string('NULL') (inherits(rs2, 'xfun_raw_string')) (length(rs2) == 1) (capture.output(rs2) %==% 'NULL') }) assert('raw_string() inherits from character', { rs = raw_string(c('a "b"', 'hello\tworld!')) (inherits(rs, 'character')) }) xfun/tests/test-cran/test-json.R0000644000175000017500000000151514120530315016473 0ustar nileshnileshlibrary(testit) assert("tojson() works", { (tojson(NULL) %==% "null") (tojson(list()) %==% "{}") (has_error(tojson(Sys.Date()))) (has_error(tojson(NA))) (tojson(NA_character_) %==% '"NA"') (tojson(1:10) %==% "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]") (tojson(TRUE) %==% "true") (tojson(FALSE) %==% "false") x = list(a = 1, b = list(c = 1:3, d = "abc")) out = '{\n"a": 1,\n"b": {\n"c": [1, 2, 3],\n"d": "abc"\n}\n}' (tojson(x) %==% out) x = list(c("a", "b"), 1:5, TRUE) out = '[["a", "b"], [1, 2, 3, 4, 5], true]' (tojson(x) %==% out) (tojson(list('"a b"' = 'quotes "\'')) %==% '{\n"\\"a b\\"": "quotes \\"\'"\n}') JS = function(x) structure(x, class = "JS_EVAL") x = list(a = 1:5, b = JS("function() {return true;}")) out = '{\n"a": [1, 2, 3, 4, 5],\n"b": function() {return true;}\n}' (tojson(x) %==% out) }) xfun/tests/test-cran/test-paths.R0000644000175000017500000000627514120530315016651 0ustar nileshnileshlibrary(testit) assert('file_ext() and sans_ext() work', { p = c('abc.doc', 'def123.tex#', 'path/to/foo.Rmd', 'backup.ppt~', 'pkg.tar.xz') (file_ext(p) %==% c('doc', 'tex#', 'Rmd', 'ppt~', 'tar.xz')) (sans_ext(p) %==% c('abc', 'def123', 'path/to/foo', 'backup', 'pkg')) (file_ext(c('foo.bar.gz', 'foo', 'file.nb.html')) %==% c('gz', '', 'nb.html')) }) assert('with_ext() works for corner cases', { (with_ext(character(), 'abc') %==% character()) (with_ext('abc', character()) %==% 'abc') (with_ext(NA_character_, 'abc') %==% NA_character_) (has_error(with_ext('abc', NA_character_))) (with_ext('abc', c('d', 'e')) %==% c('abc.d', 'abc.e')) (has_error(with_ext(c('a', 'b'), c('d', 'e', 'f')))) (with_ext(c('a', 'b'), c('d', 'e')) %==% c('a.d', 'b.e')) (with_ext(c('a', 'b'), c('d')) %==% c('a.d', 'b.d')) (with_ext(c('a', 'b', 'c'), c('', '.d', 'e.e')) %==% c('a', 'b.d', 'c.e.e')) }) assert('same_path() works', { (is.na(same_path('~/foo', NA_character_))) (is.na(same_path(NA_character_, '~/foo'))) (same_path('~/foo', file.path(Sys.getenv('HOME'), 'foo'))) (!same_path(tempdir(), 'foo')) }) assert('url_filename() returns the file names in URLs', { (url_filename('https://yihui.org/images/logo.png') %==% 'logo.png') (url_filename(c( 'https://yihui.org/index.html', 'https://yihui.org/index.html?foo=bar', 'https://yihui.org/index.html#about' )) %==% rep('index.html', 3)) }) assert('is_abs_path() recognizes absolute paths on Windows and *nix', { (!is_abs_path('abc/def')) (is_abs_path(if (.Platform$OS.type == 'windows') { c('D:\\abc', '\\\\netdrive\\somewhere') } else '/abc/def')) }) assert('del_empty_dir() correctly deletes empty dirs', { # do nothing is NULL (del_empty_dir(NULL) %==% NULL) # remove if empty dir.create(temp_dir <- tempfile()) del_empty_dir(temp_dir) (!dir_exists(temp_dir)) # do not remove if not empty dir.create(temp_dir <- tempfile()) writeLines('test', tempfile(tmpdir = temp_dir)) (del_empty_dir(temp_dir) %==% NULL) (dir_exists(temp_dir)) unlink(temp_dir, recursive = TRUE) }) assert('mark_dirs add trailing / when necessary', { local({ dir.create(tmp_dir <- tempfile()) tmp_dir_slash = paste0(tmp_dir, "/") file.create(tmp_file <- tempfile(tmpdir = tmp_dir)) (mark_dirs(c(tmp_dir, tmp_file)) %==% c(tmp_dir_slash, tmp_file)) (mark_dirs(c(tmp_dir_slash, tmp_file)) %==% c(tmp_dir_slash, tmp_file)) unlink(tmp_dir, recursive = TRUE) }) }) assert("relative_path() works", { (relative_path(c('foo/bar.txt', 'foo/baz.txt'), 'foo/') %==% c("bar.txt", "baz.txt")) (relative_path('foo/bar.txt', 'foo') %==% "bar.txt") }) assert("proj_root() works", { # detect .Rproj root dir.create(tmp_dir <- tempfile()) tmp_dir_slash <- paste0(tmp_dir, "/") file.create(f1 <- file.path(tmp_dir, "test.Rproj")) writeLines(c("Version: 1.2.3", "test: 321"), f1) (same_path(proj_root(tmp_dir), tmp_dir) %==% TRUE) unlink(f1) # detect package root file.create(f2 <- file.path(tmp_dir, "DESCRIPTION")) writeLines(c("Package: abc", "test: 321"), f2) dir.create(tmp_dir_child <- tempfile(tmpdir = tmp_dir)) (same_path(proj_root(tmp_dir_child), tmp_dir) %==% TRUE) unlink(tmp_dir, recursive = TRUE) }) xfun/tests/test-cran/test-command.R0000644000175000017500000000054514120530315017142 0ustar nileshnileshlibrary(testit) assert("Rscript() correctly calls Rscript with system2()", { (Rscript(c("-e", "1+1"), stdout = TRUE) %==% "[1] 2") }) assert("Rscript_call() correctly calls Rscript()", { (Rscript_call(function() 1+1) %==% 2) loaded = Rscript_call( function() loadedNamespaces(), options = "--default-packages=xfun" ) ("xfun" %in% loaded) }) xfun/tests/test-cran/test-string.R0000644000175000017500000000366214120530315017035 0ustar nileshnileshlibrary(testit) assert('n2w converts numbers to words', { (n2w(0) %==% 'zero') # cap capitalizes the first letter (n2w(0, cap = TRUE) %==% 'Zero') # hyphen adds '-' for 21-99 (except 30, 40, ...) (n2w(21, cap = TRUE, hyphen = TRUE) %==% 'Twenty-one') (n2w(21, cap = TRUE, hyphen = FALSE) %==% 'Twenty one') # x can be negative integers (n2w(-21, cap = TRUE, hyphen = TRUE) %==% 'Minus twenty-one') (n2w(-21, cap = TRUE, hyphen = FALSE) %==% 'Minus twenty one') # and controls whether to have 'add' between hundreds and double digits (n2w(121, and = FALSE) %==% 'one hundred twenty-one') (n2w(121, and = TRUE) %==% 'one hundred and twenty-one') # x can be a vector with length > 1 (n2w(c(10, 13, 99, 1e6)) %==% c('ten', 'thirteen', 'ninety-nine', 'one million')) # the number should be less than 1e15 (has_error(n2w(1e15))) }) assert('split_lines() splits a character vector into lines', { (split_lines('a') %==% 'a') (split_lines('') %==% '') (split_lines(NULL) %==% NULL) (split_lines('a\n') %==% c('a', '')) (split_lines(c('a', 'b\nc')) %==% c('a', 'b', 'c')) (split_lines(c('', '\n')) %==% c('', '', '')) (split_lines('a\nb') %==% c('a', 'b')) (split_lines('a\nb\n\n') %==% c('a', 'b', '', '')) (split_lines(c('a\nb', '', ' ', 'c')) %==% c('a', 'b', '', ' ', 'c')) }) assert('split_source() puts lines of the same expression into a list element', { (split_source('1+1') %==% list('1+1')) (split_source(c('1+1+', '1')) %==% list(c('1+1+', '1'))) (split_source(c('1+1+', '1', 'TRUE')) %==% list(c('1+1+', '1'), 'TRUE')) }) assert('split_source() should signal an error for incomplete code', { (has_error(split_source('1+1+'))) (has_error(split_source(c('1+1', '1+1+')))) }) assert('valid_syntax() tells if a code fragment is syntactically valid', { (valid_syntax('1+1')) (!valid_syntax('1+1+')) (valid_syntax('if(TRUE)1')) (!valid_syntax(c('if(T){', 'F'))) (valid_syntax(c('if(T){', 'F}'))) }) xfun/tests/test-cran/test-utils.R0000644000175000017500000000074414120530315016665 0ustar nileshnileshlibrary(testit) assert('attr() is strict', { z = structure(list(a = 1), foo = 2) (attr(z, 'foo') %==% 2) (attr(z, 'f') %==% NULL) }) assert('in_dir() preserves the working directory', { owd = getwd() in_dir('.', setwd(tempdir())) (same_path(owd, getwd())) }) assert('stringsAsFactors() makes sure strings are not converted to factors', { f = function() { strings_please() data.frame(x = letters[1:4], y = factor(letters[1:4])) } is.character(f()[, 1]) }) xfun/tests/test-cran/test-packages.R0000644000175000017500000000104414120530315017275 0ustar nileshnileshlibrary(testit) assert("loadable() works", { (loadable("base")) # (loadable("base", new_session = TRUE)) (loadable("base", strict = FALSE)) (has_error(loadable(c("base", "base2")))) (has_error(loadable(character()))) (!loadable("#base")) # (!loadable("#base", new_session = TRUE)) (!loadable("#base", strict = FALSE)) }) assert("major_mninor_smaller() works", { (major_minor_smaller(as.numeric_version("4.1.0"), as.numeric_version("4.2.0"))) (!major_minor_smaller(as.numeric_version("4.1.0"), as.numeric_version("4.1.1"))) }) xfun/tests/test-cran/test-base64.R0000644000175000017500000000317514120530315016612 0ustar nileshnileshlibrary(testit) assert('base64_encode() encodes the string correctly', { ref = c( "AQ==", "AQI=", "AQID", "AQIDBA==", "AQIDBAU=", "AQIDBAUG", "AQIDBAUGBw==", "AQIDBAUGBwg=", "AQIDBAUGBwgJ", "AQIDBAUGBwgJCg==" ) (sapply(1:10, function(i) base64_encode(as.raw(1:i))) == ref) ref = c( "/w==", "//4=", "//79", "//79/A==", "//79/Ps=", "//79/Pv6", "//79/Pv6+Q==", "//79/Pv6+fg=", "//79/Pv6+fj3", "//79/Pv6+fj39g==" ) (sapply(255:246, function(i) base64_encode(as.raw(255:i))) == ref) }) assert('base64_decode() decodes the string correctly', { (sapply(c(1:10, 255:246), function(i) { input = as.raw(1:i) output = base64_encode(input) input2 = base64_decode(output) input %==% input2 })) }) assert('base64_decode() will not make R crash if the input is not valid', { (has_error(base64_decode("lz..??"))) }) assert('base64_encode_r() returns the same result as base64_encode()', { f = R_logo() (base64_encode_r(f) %==% base64_encode(f)) }) assert('base64_decode() does not accept a non-string input', { (has_error(base64_decode(x = 42))) }) assert('base64_decode() does not accept both string and file input', { f = tempfile() (has_error(base64_decode(x = 'Kg==', from = f))) }) assert('base64_decode() returns the same result when the same string is used as an input directly or from a file connection', { f = tempfile() writeLines(text = "Kg==", con = f, sep = "") (base64_decode(from = f) %==% base64_decode(x = 'Kg==')) }) assert('base64_uri() returns proper data type', { f = R_logo() (!grepl('[.]svg$', f) || strsplit(base64_uri(f), split = ';')[[1]][1] %==% 'data:image/svg+xml') }) xfun/tests/test-cran.R0000644000175000017500000000004614120530315014543 0ustar nileshnileshtestit::test_pkg('xfun', 'test-cran') xfun/tests/test-ci.R0000644000175000017500000000020014120530315014203 0ustar nileshnilesh# tests to run on CI servers (e.g. Github Actions) if (tolower(Sys.getenv('CI')) == 'true') testit::test_pkg('xfun', 'test-ci') xfun/tests/test-ci/0000755000175000017500000000000014156202722014077 5ustar nileshnileshxfun/tests/test-ci/test-cran.R0000644000175000017500000000032514120530315016113 0ustar nileshnileshlibrary(testit) assert("pkg_maintainers() works", { x = pkg_maintainers("xfun") (length(x) > 0) # %==% not working for some reason (x %==% c(xfun = packageDescription('xfun', fields = 'Maintainer'))) }) xfun/tests/test-ci/test-revcheck.R0000644000175000017500000000002014120530315016752 0ustar nileshnileshlibrary(testit) xfun/R/0000755000175000017500000000000014156156426011577 5ustar nileshnileshxfun/R/string.R0000644000175000017500000002003414142575665013234 0ustar nileshnilesh#' Convert numbers to English words #' #' This can be helpful when writing reports with \pkg{knitr}/\pkg{rmarkdown} if #' we want to print numbers as English words in the output. The function #' \code{n2w()} is an alias of \code{numbers_to_words()}. #' @param x A numeric vector. Values should be integers. The absolute values #' should be less than \code{1e15}. #' @param cap Whether to capitalize the first letter of the word. This can be #' useful when the word is at the beginning of a sentence. Default is #' \code{FALSE}. #' @param hyphen Whether to insert hyphen (-) when the number is between 21 and #' 99 (except 30, 40, etc.). #' @param and Whether to insert \code{and} between hundreds and tens, e.g., #' write 110 as \dQuote{one hundred and ten} if \code{TRUE} instead of #' \dQuote{one hundred ten}. #' @return A character vector. #' @author Daijiang Li #' @export #' @examples library(xfun) #' n2w(0, cap = TRUE) #' n2w(0:121, and = TRUE) #' n2w(1e6) #' n2w(1e11+12345678) #' n2w(-987654321) #' n2w(1e15-1) numbers_to_words = function(x, cap = FALSE, hyphen = TRUE, and = FALSE) { if (!is.numeric(x)) stop('The input is not numeric.') if (any(abs(x) >= 1e15)) stop('The absolute value must be less than 1e15.') opts = options(scipen = 15); on.exit(options(opts), add = TRUE) # avoid scientific notation if (any(x != floor(x))) stop('The numbers must be integer. ') zero_to_19 = c( 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', 'eleven', 'twelve', paste0(c('thir', 'four', 'fif', 'six', 'seven', 'eigh', 'nine'), 'teen') ) names(zero_to_19) = as.character(0:19) tens = c('twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety') names(tens) = as.character(seq(20, 90, 10)) marks = c('', 'thousand,', 'million,', 'billion,', 'trillion,') convert_1 = function(x_c) zero_to_19[x_c] # 0 - 9 # 10 - 99 convert_2 = function(x_c) { x_cs = strsplit(x_c, split = '')[[1]] if (x_cs[1] == 1) return(zero_to_19[x_c]) # 10 - 19 if (x_cs[2] == 0) return(tens[x_c]) # 20, 30, 40, ... # 21, 22, etc. paste(tens[as.integer(x_cs[1]) - 1], convert_1(x_cs[2]), sep = if (hyphen) '-' else ' ') } # 100 - 999 convert_3 = function(x_c) { x_cs = strsplit(x_c, split = '')[[1]] n_hundreds = paste(convert_1(x_cs[1]), 'hundred', sep = ' ') out = if (x_cs[2] == '0') { if (x_cs[3] == '0') return(n_hundreds) # x00 convert_1(x_cs[3]) # x0x } else { convert_2(paste(x_cs[2:3], collapse = '')) # xxx } paste(n_hundreds, out, sep = if (and) ' and ' else ' ') } convert_le3 = function(x_c) { x_c = gsub('^0+', '', x_c) # avoid something like 000, 001, 010; but also remove 0 n = nchar(x_c) if (n == 0) return('') if (n == 1) return(convert_1(x_c)) if (n == 2) return(convert_2(x_c)) if (n == 3) return(convert_3(x_c)) } convert_one = function(x) { minus = if (x >= 0) '' else { x = abs(x); 'minus ' } if (x == 0) { out = 'zero' # because convert_le3 removed all 0s } else { x_marks = strsplit(format(x, big.mark = ','), split = ',')[[1]] # e.g. 123,456,789 out = vapply(x_marks, convert_le3, character(1)) # group by 3 digits x_marks2 = marks[length(x_marks):1] # units? x_marks2[which(out == '')] = '' # e.g. 4,000,123, 000, remove millions out = paste(out, x_marks2, sep = ' ', collapse = ' ') # zip together } out = paste0(minus, out) out = gsub('^ *|,? *$', '', out) # trim heading/trailing space out = gsub(' {2,}', ' ', out) # remove multiple spaces if (cap) out = sub('^([a-z])', '\\U\\1', out, perl = TRUE) out } if (length(x) > 1) vapply(x, convert_one, character(1)) else convert_one(x) } #' @export #' @rdname numbers_to_words n2w = numbers_to_words # create a URL query string from named parameters query_params = function(..., .list = list()) { x = if (missing(.list)) list(...) else .list x = paste(names(x), x, sep = '=', collapse = '&') if (x != '') paste0('?', x) else x } #' Split a character vector by line breaks #' #' Call \code{unlist(strsplit(x, '\n'))} on the character vector \code{x} and #' make sure it works in a few edge cases: \code{split_lines('')} returns #' \code{''} instead of \code{character(0)} (which is the returned value of #' \code{strsplit('', '\n')}); \code{split_lines('a\n')} returns \code{c('a', #' '')} instead of \code{c('a')} (which is the returned value of #' \code{strsplit('a\n', '\n')}. #' @param x A character vector. #' @return All elements of the character vector are split by \code{'\n'} into #' lines. #' @export #' @examples xfun::split_lines(c('a', 'b\nc')) split_lines = function(x) { if (length(grep('\n', x)) == 0L) return(x) x = gsub('\n$', '\n\n', x) x[x == ''] = '\n' unlist(strsplit(x, '\n')) } #' Split source lines into complete expressions #' #' Parse the lines of code one by one to find complete expressions in the code, #' and put them in a list. #' @param x A character vector of R source code. #' @return A list of character vectors, and each vector contains a complete R #' expression. #' @export #' @examples xfun::split_source(c('if (TRUE) {', '1 + 1', '}', 'print(1:5)')) split_source = function(x) { if ((n <- length(x)) < 1) return(list(x)) i = i1 = i2 = 1 res = list() while (i2 <= n) { piece = x[i1:i2] if (valid_syntax(piece)) { res[[i]] = piece; i = i + 1 i1 = i2 + 1 # start from the next line } i2 = i2 + 1 } if (i1 <= n) parse(text = piece) # must be an error there res } #' Check if the syntax of the code is valid #' #' Try to \code{\link{parse}()} the code and see if an error occurs. #' @param code A character vector of R source code. #' @param silent Whether to suppress the error message when the code is not #' valid. #' @return \code{TRUE} if the code could be parsed, otherwise \code{FALSE}. #' @export #' @examples xfun::valid_syntax('1+1') #' xfun::valid_syntax('1+') #' xfun::valid_syntax(c('if(T){1+1}', 'else {2+2}'), silent = FALSE) valid_syntax = function(code, silent = TRUE) { !inherits(try(parse_only(code), silent = silent), 'try-error') } #' Bump version numbers #' #' Increase the last digit of version numbers, e.g., from \code{0.1} to #' \code{0.2}, or \code{7.23.9} to \code{7.23.10}. #' @param x A vector of version numbers (of the class \code{"numeric_version"}), #' or values that can be coerced to version numbers via #' \code{as.numeric_version()}. #' @return A vector of new version numbers. #' @export #' @examples xfun::bump_version(c('0.1', '91.2.14')) bump_version = function(x) { x = as.numeric_version(x) for (i in seq_along(x)) { v = x[i] n = length(unclass(v)[[1]]) v[[1, n]] = v[[1, n]] + 1 # bump the last digit x[i] = v } x } #' Fix pairs of characters in a file #' #' For example, the curly braces may be wrong (the opening and closing braces #' are swapped for some reason). #' @param x A character vector (by default, read from \code{file}). #' @param file Path to a text file. #' @param chars A vector of characters of length 2. By default, it is a pair of #' curly double quotes. #' @references \url{https://d.cosx.org/d/420794/5} #' @noRd #' @examples #' files = list.files('.', '[.]R?md$', recursive = TRUE, full.names = TRUE) #' for (f in files) { #' pair_chars(file = f) #' # curly single quotes #' pair_chars(file = f, chars = c('\U2018', '\U2019')) #' } pair_chars = function(x = read_utf8(file), file, chars = c('\U201c', '\U201d')) { if (length(chars) != 2) stop("'chars' must be of length 2 (i.e., a pair of characters)") is_file = !missing(file) r = paste(c('[', chars, ']'), collapse = '') k = gregexpr(r, x) m = regmatches(x, k) for (i in seq_along(m)) { n = length(m[[i]]) if (n %% 2 != 0) { warning( 'The characters do not appear in pairs in the text (', 'line: ', i, if (is_file) c('; file: ', file), '):\n', x[i], '\n' ) next } m[[i]] = rep(chars, length.out = n) } x2 = x regmatches(x, k) = m if (is_file) { if (!identical(x, x2)) xfun::write_utf8(x, file) invisible(x) } else x } xfun/R/cran.R0000644000175000017500000001156514140774145012652 0ustar nileshnilesh# retrieve the release dates of packages cran_pkg_dates = function(full = FALSE, maintainer = 'Yihui Xie') { info = tools::CRAN_package_db() pkgs = info[grep(maintainer, info$Maintainer), 'Package'] info = setNames(vector('list', length(pkgs)), pkgs) for (p in pkgs) { message('Processing ', p) x = readLines(u <- sprintf('https://cran.rstudio.com/web/packages/%s/', p)) i = which(x == 'Published:') if (length(i) == 0) stop('Cannot find the publishing date from ', u) d = as.Date(gsub('', '', x[i[1] + 1])) x = try(readLines(u <- sprintf('https://cran.r-project.org/src/contrib/Archive/%s/', p))) if (inherits(x, 'try-error')) { info[[p]] = d; next } r = '.+(\\d{4,}-\\d{2}-\\d{2}) .+' d = c(d, as.Date(gsub(r, '\\1', grep(r, x, value = TRUE)))) info[[p]] = sort(d, decreasing = TRUE) } if (full) info else sort(do.call(c, lapply(info, `[`, 1)), decreasing = TRUE) } # return packages that haven't been updated for X days, and can be updated on CRAN cran_updatable = function(days = 90, maintainer = 'Yihui Xie') { info = cran_pkg_dates(TRUE, maintainer) flag = unlist(lapply(info, function(d) { sum(d > Sys.Date() - 180) < 6 && d[1] < Sys.Date() - days })) names(which(flag)) } #' Some utility functions for checking packages #' #' Miscellaneous utility functions to obtain information about the package #' checking environment. #' @export #' @keywords internal is_R_CMD_check = function() { !is.na(check_package_name()) || tolower(Sys.getenv('_R_CHECK_LICENSE_')) == 'true' } #' @rdname is_R_CMD_check #' @export is_CRAN_incoming = function() { isTRUE(as.logical(Sys.getenv('_R_CHECK_CRAN_INCOMING_REMOTE_'))) } #' @rdname is_R_CMD_check #' @export check_package_name = function() { Sys.getenv('_R_CHECK_PACKAGE_NAME_', NA) } # is R CMD check running on a package that has a version lower or equal to `version`? #' @rdname is_R_CMD_check #' @export check_old_package = function(name, version) { if (is.na(pkg <- check_package_name()) || pkg != name) return(FALSE) tryCatch(packageVersion(name) <= version, error = function(e) FALSE) } # return package maintainers (with email addresses) pkg_maintainers = function(pkgs) { info = tools::CRAN_package_db() info = info[match(pkgs, info$Package), c('Package', 'Maintainer')] setNames(info$Maintainer, info$Package) } #' Submit a source package to CRAN #' #' Build a source package and submit it to CRAN with the \pkg{curl} package. #' @param file The path to the source package tarball. By default, the current #' working directory is treated as the package root directory, and #' automatically built into a tarball, which is deleted after submission. This #' means you should run \code{xfun::submit_cran()} in the root directory of a #' package project, unless you want to pass a path explicitly to the #' \code{file} argument. #' @param comment Submission comments for CRAN. By default, if a file #' \file{cran-comments.md} exists, its content will be read and used as the #' comment. #' @seealso \code{devtools::submit_cran()} does the same job, with a few more #' dependencies in addition to \pkg{curl} (such as \pkg{cli}); #' \code{xfun::submit_cran()} only depends on \pkg{curl}. #' @export submit_cran = function(file = pkg_build(), comment = '') { # if the tarball is automatically created, delete it after submission if (missing(file)) on.exit(file.remove(file), add = TRUE) # read the maintainer's name/email dir_create(d <- tempfile()) on.exit(unlink(d, recursive = TRUE), add = TRUE) desc = file.path(gsub('_.*', '', basename(file)), 'DESCRIPTION') untar(file, desc, exdir = d) info = read.dcf(file.path(d, desc), fields = 'Maintainer')[1, 1] info = unlist(strsplit(info, '( <|>)')) # read submission comments from cran-comments.md if exists if (missing(comment) && file_exists(f <- 'cran-comments.md')) { comment = file_string(f) } params = list( uploaded_file = curl::form_file(file), name = info[1], email = info[2], upload = 'Upload package' ) params$comment = if (length(comment)) comment server = 'https://xmpalantir.wu.ac.at/cransubmit/index2.php' # submit the form h = curl::new_handle() curl::handle_setform(h, .list = params) res = curl::curl_fetch_memory(server, h) # find the pkg_id from the response page id = grep_sub( '(.*Step 3<', rawToChar(res$content))) message( 'The package has been submitted. Please confirm the submission in email: ', params$email ) else message('The submission may be unsuccessful.') } xfun/R/paths.R0000644000175000017500000004706014145505647013051 0ustar nileshnilesh#' Manipulate filename extensions #' #' Functions to obtain (\code{file_ext()}), remove (\code{sans_ext()}), and #' change (\code{with_ext()}) extensions in filenames. #' #' \code{file_ext()} is similar to \code{tools::\link{file_ext}()}, and #' \code{sans_ext()} is similar to \code{tools::\link{file_path_sans_ext}()}. #' The main differences are that they treat \code{tar.(gz|bz2|xz)} and #' \code{nb.html} as extensions (but functions in the \pkg{tools} package #' doesn't allow double extensions by default), and allow characters \code{~} #' and \code{#} to be present at the end of a filename. #' @param x A character of file paths. #' @export #' @return A character vector of the same length as \code{x}. #' @examples library(xfun) #' p = c('abc.doc', 'def123.tex', 'path/to/foo.Rmd', 'backup.ppt~', 'pkg.tar.xz') #' file_ext(p); sans_ext(p); with_ext(p, '.txt') #' with_ext(p, c('.ppt', '.sty', '.Rnw', 'doc', 'zip')); with_ext(p, 'html') file_ext = function(x) { ext = character(length(x)) i = grep(reg_path, x) ext[i] = sub(reg_path, '\\3', x[i]) ext } #' @rdname file_ext #' @export sans_ext = function(x) { sub(reg_path, '\\1', x) } #' @param ext A vector of new extensions. It must be either of length 1, or the #' same length as \code{x}. #' @rdname file_ext #' @export with_ext = function(x, ext) { if (anyNA(ext)) stop("NA is not allowed in 'ext'") n1 = length(x); n2 = length(ext) if (n1 * n2 == 0) return(x) i = !grepl('^[.]', ext) & ext != '' ext[i] = paste0('.', ext[i]) if (all(ext == '')) ext = '' r = sub('[$]$', '?$', reg_ext) # make extensions in 'x' optional if (length(ext) == 1) return(sub(r, ext, x)) if (n1 > 1 && n1 != n2) stop("'ext' must be of the same length as 'x'") mapply(sub, r, ext, x, USE.NAMES = FALSE) } # regex to extract base path and extension from a file path reg_ext = '([.](([[:alnum:]]+|tar[.](gz|bz2|xz)|nb[.]html)[~#]?))$' reg_path = paste0('^(.*?)', reg_ext) #' Normalize paths #' #' A wrapper function of \code{normalizePath()} with different defaults. #' @param x,winslash,must_work Arguments passed to #' \code{\link{normalizePath}()}. #' @export #' @examples library(xfun) #' normalize_path('~') normalize_path = function(x, winslash = '/', must_work = FALSE) { res = normalizePath(x, winslash = winslash, mustWork = must_work) if (is_windows()) res[is.na(x)] = NA res } #' Test if two paths are the same after they are normalized #' #' Compare two paths after normalizing them with the same separator (\code{/}). #' @param p1,p2 Two vectors of paths. #' @param ... Arguments to be passed to \code{\link{normalize_path}()}. #' @export #' @examples library(xfun) #' same_path('~/foo', file.path(Sys.getenv('HOME'), 'foo')) same_path = function(p1, p2, ...) { normalize_path(p1, ...) == normalize_path(p2, ...) } #' Find file paths that exist #' #' This is a shorthand of \code{x[file.exists(x)]}, and optionally returns the #' first existing file path. #' @param x A vector of file paths. #' @param first Whether to return the first existing path. If \code{TRUE} and no #' specified files exist, it will signal an error. #' @return A vector of existing file paths. #' @export #' @examples #' xfun::existing_files(c('foo.txt', system.file('DESCRIPTION', package='xfun'))) existing_files = function(x, first = FALSE) { x = x[file_exists(x)] if (!first) return(x) x = head(x, 1) if (length(x) != 1) stop('None of the specified files exist.') x } #' Return the (possible) root directory of a project #' #' Given a path of a file (or dir) in a potential project (e.g., an R package or #' an RStudio project), return the path to the project root directory. #' #' The search for the root directory is performed by a series of tests, #' currently including looking for a \file{DESCRIPTION} file that contains #' \code{Package: *} (which usually indicates an R package), and a #' \file{*.Rproj} file that contains \code{Version: *} (which usually indicates #' an RStudio project). If files with the expected patterns are not found in the #' initial directory, the search will be performed recursively in upper-level #' directories. #' @param path The initial path to start the search. If it is a file path, its #' parent directory will be used. #' @param rules A matrix of character strings of two columns: the first column #' contains regular expressions to look for filenames that match the patterns, #' and the second column contains regular expressions to match the content of #' the matched files. The regular expression can be an empty string, meaning #' that it will match anything. #' @return Path to the root directory if found, otherwise \code{NULL}. #' @export #' @note This function was inspired by the \pkg{rprojroot} package, but is much #' less sophisticated. It is a rather simple function designed to be used in #' some of packages that I maintain, and may not meet the need of general #' users until this note is removed in the future (which should be unlikely). #' If you are sure that you are working on the types of projects mentioned in #' the \sQuote{Details} section, this function may be helpful to you, #' otherwise please consider using \pkg{rprojroot} instead. proj_root = function(path = './', rules = root_rules) { path = normalize_path(path) dir = if (dir_exists(path)) path else dirname(path) if (same_path(dir, file.path(dir, '..'))) return() if (is.null(dim(rules))) dim(rules) = c(1, length(rules)) for (i in seq_len(nrow(rules))) { file = rules[i, 1]; pattern = rules[i, 2] for (f in list.files(dir, file, full.names = TRUE)) { if (pattern == '' || length(grep(pattern, read_utf8(f)))) return(dir) } } proj_root(dirname(dir), rules) } #' @rdname proj_root #' @export root_rules = matrix(c( '^DESCRIPTION$', '^Package: ', '.+[.]Rproj$', '^Version: ' ), ncol = 2, byrow = TRUE, dimnames = list(NULL, c('file', 'pattern'))) #' Get the relative path of a path relative to a directory #' #' Given a directory, return the relative path that is relative to this #' directory. For example, the path \file{foo/bar.txt} relative to the directory #' \file{foo/} is \file{bar.txt}, and the path \file{/a/b/c.txt} relative to #' \file{/d/e/} is \file{../../a/b/c.txt}. #' @param dir Path to a directory. #' @param x A vector of paths to be converted to relative paths. #' @param use.. Whether to use double-dots (\file{..}) in the relative path. A #' double-dot indicates the parent directory (starting from the directory #' provided by the \code{dir} argument). #' @param error Whether to signal an error if a path cannot be converted to a #' relative path. #' @return A vector of relative paths if the conversion succeeded; otherwise the #' original paths when \code{error = FALSE}, and an error when \code{error = #' TRUE}. #' @export #' @examples #' xfun::relative_path('foo/bar.txt', 'foo/') #' xfun::relative_path('foo/bar/a.txt', 'foo/haha/') #' xfun::relative_path(getwd()) relative_path = function(x, dir = '.', use.. = TRUE, error = TRUE) { res = x for (i in seq_along(x)) res[i] = relative_path_one(x[i], dir, use.., error) res } relative_path_one = function(x, dir, use.., error) { # on Windows, if a relative path doesn't exist, normalizePath() will use # getwd() as its parent dir; however, normalizePath() just returns the # relative path on *nix, and we have to assume it's relative to getwd() abs_path = function(p) { if (!file.exists(p) && is_unix() && is_rel_path(p)) p = file.path(getwd(), p) normalize_path(p) } p = abs_path(x); n1 = nchar(p) if ((n1 <- nchar(p)) == 0) return(x) # not sure what you mean d = abs_path(dir); n2 = nchar(d) if (is_sub_path(p, d, n2)) { p2 = get_subpath(p, n1, n2) if (p2 == '') p2 = '.' # if the subpath is empty, it means the current dir return(p2) } if (!use..) { if (error) stop("When use.. = FALSE, the path 'x' must be under the 'dir'") return(x) } s = '../'; d1 = d while (!is_sub_path(p, d2 <- dirname(d1))) { if (same_path(d1, d2)) { if (error) stop( "The path 'x' cannot be converted to a relative path to 'dir'. ", "Perhaps they are on different volumes of the disk." ) return(x) } s = paste0('../', s) d1 = d2 # go to one level up } paste0(s, get_subpath(p, n1, nchar(d2))) } #' Test if a path is a subpath of a dir #' #' Check if the path starts with the dir path. #' @inheritParams is_abs_path #' @param dir A vector of directory paths. #' @param n The length of \code{dir} paths. #' @return A logical vector. #' @note You may want to normalize the values of the \code{x} and \code{dir} #' arguments first (with \code{xfun::\link{normalize_path}()}), to make sure #' the path separators are consistent. #' @export #' @examples #' xfun::is_sub_path('a/b/c.txt', 'a/b') # TRUE #' xfun::is_sub_path('a/b/c.txt', 'd/b') # FALSE #' xfun::is_sub_path('a/b/c.txt', 'a\\b') # FALSE (even on Windows) is_sub_path = function(x, dir, n = nchar(dir)) substr(x, 1, n) == dir # remove the first n2 characters and the possible / from the path get_subpath = function(p, n1, n2) { p = substr(p, n2 + 1, n1) sub('^/', '', p) } #' Test if paths are relative or absolute #' #' On Unix, check if the paths start with \file{/} or \file{~} (if they do, they #' are absolute paths). On Windows, check if a path remains the same (via #' \code{xfun::\link{same_path}()}) if it is prepended with \file{./} (if it #' does, it is a relative path). #' @param x A vector of paths. #' @return A logical vector. #' @export #' @examples #' xfun::is_abs_path(c('C:/foo', 'foo.txt', '/Users/john/', tempdir())) #' xfun::is_rel_path(c('C:/foo', 'foo.txt', '/Users/john/', tempdir())) is_abs_path = function(x) { if (is_unix()) grepl('^[/~]', x) else !same_path(x, file.path('.', x)) } #' @rdname is_abs_path #' @export is_rel_path = function(x) !is_abs_path(x) #' Test if a path is a web path #' #' Check if a path starts with \file{http://} or \file{https://} or #' \file{ftp://} or \file{ftps://}. #' @inheritParams is_abs_path #' @return A logical vector. #' @export #' @examples #' xfun::is_web_path('https://www.r-project.org') # TRUE #' xfun::is_web_path('www.r-project.org') # FALSE is_web_path = function(x) { grepl('^(f|ht)tps?://', x) } #' Get the relative path of a path in a project relative to the current working #' directory #' #' First compose an absolute path using the project root directory and the #' relative path components, i.e., \code{\link{file.path}(root, ...)}. Then #' convert it to a relative path with \code{\link{relative_path}()}, which is #' relative to the current working directory. #' #' This function was inspired by \code{here::here()}, and the major difference #' is that it returns a relative path by default, which is more portable. #' @param ... A character vector of path components \emph{relative to the root #' directory of the project}. #' @param root The root directory of the project. #' @param error Whether to signal an error if the path cannot be converted to a #' relative path. #' @return A relative path, or an error when the project root directory cannot #' be determined or the conversion failed and \code{error = TRUE}. #' @export #' @examples #' \dontrun{ #' xfun::from_root('data', 'mtcars.csv') #' } from_root = function(..., root = proj_root(), error = TRUE) { if (is.null(root)) stop('Cannot determin the root directory of the current project.') p = file.path(root, ..., fsep = '/') relative_path(p, error = error) } #' Find a file or directory under a root directory #' #' Given a path, try to find it recursively under a root directory. The input #' path can be an incomplete path, e.g., it can be a base filename, and #' \code{magic_path()} will try to find this file under subdirectories. #' @param ... A character vector of path components. #' @param root The root directory under which to search for the path. If #' \code{NULL}, the current working directory is used. #' @param relative Whether to return a relative path. #' @param error Whether to signal an error if the path is not found, or multiple #' paths are found. #' @param message Whether to emit a message when multiple paths are found and #' \code{error = FALSE}. #' @param n_dirs The number of subdirectories to recursively search. The #' recursive search may be time-consuming when there are a large number of #' subdirectories under the root directory. If you really want to search for #' all subdirectories, you may try \code{n_dirs = Inf}. #' @return The path found under the root directory, or an error when \code{error #' = TRUE} and the path is not found (or multiple paths are found). #' @export #' @examples #' \dontrun{ #' xfun::magic_path('mtcars.csv') # find any file that has the base name mtcars.csv #' } magic_path = function( ..., root = proj_root(), relative = TRUE, error = TRUE, message = getOption('xfun.magic_path.message', TRUE), n_dirs = getOption('xfun.magic_path.n_dirs', 10000) ) { if (file.exists(p <- file.path(...))) return(p) if (is.null(root)) root = getwd() nd = 0 # find a path 'f' recursively under a directory 'd' find_it = function(f, d) { if (nd > n_dirs) { if (error) stop( 'Failed to find the path under ', n_dirs, ' subdirectories. If you want ', 'to search for the path in more subdirectories, increase the value of ', "the 'n_dirs' argument of magic_path()." ) return(p) } ds = list.files(d, full.names = TRUE) ds = ds[dir_exists(ds)] if ((n1 <- length(ds)) == 0) return() nd <<- nd + n1 fs = file.path(ds, f) fs = fs[file.exists(fs)] if ((n2 <- length(fs)) == 1) return(fs) if (n2 > 1) { msg = c( 'Found more than one path containg the input path "', f, '":\n\n', paste('*', fs, collapse = '\n') ) if (error) stop(msg) if (message) base::message(msg, '\n\nReturned the first one.') return(fs[1]) } # look into subdirectories one by one for (i in seq_len(n1)) { fs = find_it(f, file.path(ds[i])) if (length(fs)) return(fs) } } f = find_it(p, root) if (is.null(f)) { if (error) stop('Could not find the path "', p, '" in any subdirectories.') p } else { if (relative) relative_path(f, error = error) else f } } #' Test the existence of files and directories #' #' These are wrapper functions of \code{utils::\link{file_test}()} to test the #' existence of directories and files. Note that \code{file_exists()} only tests #' files but not directories, which is the main difference between #' \code{\link{file.exists}()} in base R. If you use are using the R version #' 3.2.0 or above, \code{dir_exists()} is the same as \code{\link{dir.exists}()} #' in base R. #' @param x A vector of paths. #' @export #' @return A logical vector. dir_exists = function(x) file_test('-d', x) #' @rdname dir_exists #' @export file_exists = function(x) file_test('-f', x) #' Create a directory recursively by default #' #' First check if a directory exists. If it does, return \code{TRUE}, otherwise #' create it with \code{\link{dir.create}(recursive = TRUE)} by default. #' @param x A path name. #' @param recursive Whether to create all directory components in the path. #' @param ... Other arguments to be passed to \code{\link{dir.create}()}. #' @return A logical value indicating if the directory either exists or is #' successfully created. #' @export dir_create = function(x, recursive = TRUE, ...) { dir_exists(x) || dir.create(x, recursive = recursive) } #' Rename files with a sequential numeric prefix #' #' Rename a series of files and add an incremental numeric prefix to the #' filenames. For example, files \file{a.txt}, \file{b.txt}, and \file{c.txt} #' can be renamed to \file{1-a.txt}, \file{2-b.txt}, and \file{3-c.txt}. #' @param pattern A regular expression for \code{\link{list.files}()} to obtain #' the files to be renamed. For example, to rename \code{.jpeg} files, use #' \code{pattern = "[.]jpeg$"}. #' @param format The format for the numeric prefix. This is passed to #' \code{\link{sprintf}()}. The default format is \code{"\%0Nd"} where \code{N #' = floor(log10(n)) + 1} and \code{n} is the number of files, which means the #' prefix may be padded with zeros. For example, if there are 150 files to be #' renamed, the format will be \code{"\%03d"} and the prefixes will be #' \code{001}, \code{002}, ..., \code{150}. #' @param replace Whether to remove existing numeric prefixes in filenames. #' @param start The starting number for the prefix (it can start from 0). #' @param dry_run Whether to not really rename files. To be safe, the default is #' \code{TRUE}. If you have looked at the new filenames and are sure the new #' names are what you want, you may rerun \code{rename_seq()} with #' \code{dry_run = FALSE)} to actually rename files. #' @return A named character vector. The names are original filenames, and the #' vector itself is the new filenames. #' @export #' @examples xfun::rename_seq() #' xfun::rename_seq('[.](jpeg|png)$', format = '%04d') rename_seq = function( pattern = '^[0-9]+-.+[.]Rmd$', format = 'auto', replace = TRUE, start = 1, dry_run = TRUE ) { n = length(files <- list.files('.', pattern)) if (n == 0) return(files) files2 = if (replace) sub('^[0-9]+-*', '', files) else files if (format == 'auto') format = paste0('%0', floor(log10(n)) + 1, 'd') files2 = paste(sprintf(format, seq_len(n) + start - 1), files2, sep = '-') if (!dry_run) file.rename(files, files2) structure(setNames(files2, files), class = 'xfun_rename_seq') } #' @export print.xfun_rename_seq = function(x, ...) { x = unclass(x) tab = data.frame(original = names(x), ' ' = '->', new = unname(x), check.names = FALSE) if (loadable('knitr')) tab = knitr::kable(tab, 'simple') print(tab) } # return path to R's svg logo if it exists, otherwise return the jpg logo; or # specify a regex to match the logo path, e.g., ext = 'jpg$' R_logo = function(ext = NULL) { x = file.path(R.home('doc'), 'html', c('Rlogo.svg', 'logo.jpg')) if (!is.null(ext)) x = grep(ext, x, value = TRUE) existing_files(x, first = TRUE) } #' Extract filenames from a URLs #' #' Get the base names of URLs via \code{\link{basename}()}, and remove the #' possible query parameters or hash from the names. #' @param x A character vector of URLs. #' @return A character vector of filenames at the end of URLs. #' @export #' @examples #' xfun::url_filename('https://yihui.org/images/logo.png') #' xfun::url_filename('https://yihui.org/index.html') #' xfun::url_filename('https://yihui.org/index.html?foo=bar') #' xfun::url_filename('https://yihui.org/index.html#about') url_filename = function(x) { gsub('[?#].*$', '', basename(x)) # remove query/hash from url } #' Delete an empty directory #' #' Use \code{list.file()} to check if there are any files or subdirectories #' under a directory. If not, delete this empty directory. #' @param dir Path to a directory. If \code{NULL} or the directory does not #' exist, no action will be performed. #' @export del_empty_dir = function(dir) { if (is.null(dir) || !dir_exists(dir)) return() files = list.files(dir, all.files = TRUE, no.. = TRUE) if (length(files) == 0) unlink(dir, recursive = TRUE) } #' Mark some paths as directories #' #' Add a trailing backlash to a file path if this is a directory. This is useful #' in messages to the console for example to quickly identify directories from #' files. #' #' If \code{x} is a vector of relative paths, directory test is done with #' path relative to the current working dir. Use \code{xfun::\link{in_dir}()} or #' use absolute paths. #' #' @param x Character vector of paths to files and directories. #' @examples #' mark_dirs(list.files(find.package("xfun"), full.names = TRUE)) #' @export mark_dirs = function(x) { i = dir_exists(x) & !grepl("/$", x) x[i] = paste0(x[i], "/") x } xfun/R/encoding.R0000644000175000017500000000252014105542162013475 0ustar nileshnilesh#' Try to use the system native encoding to represent a character vector #' #' Apply \code{enc2native()} to the character vector, and check if #' \code{enc2utf8()} can convert it back without a loss. If it does, return #' \code{enc2native(x)}, otherwise return the original vector with a warning. #' @param x A character vector. #' @note On platforms that supports UTF-8 as the native encoding #' (\code{\link{l10n_info}()[['UTF-8']]} returns \code{TRUE}), the conversion #' will be skipped. #' @export #' @examples #' library(xfun) #' s = intToUtf8(c(20320, 22909)) #' Encoding(s) #' #' s2 = native_encode(s) #' Encoding(s2) native_encode = function(x) { if (isTRUE(l10n_info()[['UTF-8']])) return(x) if (identical(enc2utf8(x2 <- enc2native(x)), x)) return(x2) warning('The character vector cannot be represented in the native encoding') x } #' Check if a character vector consists of entirely ASCII characters #' #' Converts the encoding of a character vector to \code{'ascii'}, and check if #' the result is \code{NA}. #' @param x A character vector. #' @return A logical vector indicating whether each element of the character #' vector is ASCII. #' @export #' @examples library(xfun) #' is_ascii(letters) # yes #' is_ascii(intToUtf8(8212)) # no is_ascii = function(x) { out = !is.na(iconv(x, to = 'ascii')) out[is.na(x)] = NA out } xfun/R/image.R0000644000175000017500000001266314133423115012777 0ustar nileshnilesh# add a border to an image via ImageMagick add_border = function(input, pixels = 1, color = 'black', output) { input = normalizePath(input) if (missing(output)) output = paste0(sans_ext(input), '-output.', file_ext(input)) system2('convert', shQuote(c( input, '-shave', paste(pixels, pixels, sep = 'x'), '-bordercolor', color, '-border', pixels, output) )) optipng(dirname(output)) } #' Use the Tinify API to compress PNG and JPEG images #' #' Compress PNG/JPEG images with \samp{api.tinify.com}, and download the #' compressed images. This function requires R packages \pkg{curl} and #' \pkg{jsonlite}. #' #' You are recommended to set the API key in \file{.Rprofile} or #' \file{.Renviron}. After that, the only required argument of this function is #' \code{input}. If the original images can be overwritten by the compressed #' images, you may either use \code{output = identity}, or set the value of the #' \code{history} argument in \file{.Rprofile} or \file{.Renviron}. #' @param input A vector of input paths of images. #' @param output A vector of output paths or a function that takes \code{input} #' and returns a vector of output paths (e.g., \code{output = \link{identity}} #' means \code{output = input}). By default, if the \code{history} argument is #' not a provided, \code{output} is \code{input} with a suffix \code{-min} #' (e.g., when \code{input = 'foo.png'}, \code{output = 'foo-min.png'}), #' otherwise \code{output} is the same as \code{input}, which means the #' original image files will be overwritten. #' @param quiet Whether to suppress detailed information about the compression, #' which is of the form \samp{input.png (10 Kb) ==> output.png (5 Kb, 50\%); #' compression count: 42}. The percentage after \code{output.png} stands for #' the compression ratio, and the compression count shows the number of #' compressions used for the current month. #' @param force Whether to compress an image again when it appears to have been #' compressed before. This argument only makes sense when the \code{history} #' argument is provided. #' @param key The Tinify API key. It can be set via either the global option #' \code{xfun.tinify.key} (you may set it in \file{~/.Rprofile}) or the #' environment variable \code{R_XFUN_TINIFY_KEY} (you may set it in #' \file{~/.Renviron}). #' @param history Path to a history file to record the MD5 checksum of #' compressed images. If the checksum of an expected output image exists in #' this file and \code{force = FALSE}, the compression will be skipped. This #' can help you avoid unnecessary API calls. #' @return The output file paths. #' @references Tinify API: \url{https://tinypng.com/developers}. #' @seealso The \pkg{tinieR} package (\url{https://github.com/jmablog/tinieR/}) #' is a more comprehensive implementation of the Tinify API, whereas #' \code{xfun::tinify()} has only implemented the feature of shrinking images. #' @export #' @examplesIf interactive() #' f = file.path(R.home('doc'), 'html', 'logo.jpg') #' xfun::tinify(f) # remember to set the API key before trying this tinify = function( input, output, quiet = FALSE, force = FALSE, key = getOption('xfun.tinify.key', Sys.getenv('R_XFUN_TINIFY_KEY')), history = getOption('xfun.tinify.history', Sys.getenv('R_XFUN_TINIFY_HISTORY')) ) { if (!(is.character(key) && length(key) == 1 && key != '')) stop( "The value of the 'key' argument must be a single non-empty character string." ) if (any(i <- !file_exists(input))) stop( 'Input file(s) not found: ', paste(input[i], collapse = ', ') ) if (missing(output)) { output = if (is.character(history)) input else { paste0(sans_ext(input), '-min.', file_ext(input)) } } else if (is.function(output)) output = output(input) # avoid optimizing the input image if its md5 checksum exists in history save_history = function(file) { if (!is.character(history)) return() cat(paste0(tools::md5sum(file), '\n'), file = history, append = TRUE) } test_history = function(file) { is.character(history) && all(file_exists(c(history, file))) && (tools::md5sum(file) %in% readLines(history)) } auth = paste('Authorization: Basic', base64_encode(charToRaw(paste0('api:', key)))) mapply(input, output, FUN = function(i, o) { if (!force && test_history(o)) { if (!quiet) message( 'The image ', o, ' has been compressed before. ', 'To compress it again, call tinify() with force = TRUE.' ) return() } if (grepl('[.]png$', i, ignore.case = TRUE)) optipng(files = i, stdout = if (quiet) FALSE else '') res = curl::curl_upload(i, 'https://api.tinify.com/shrink', httpheader = auth, verbose = FALSE) cnt = curl::parse_headers_list(res$headers)[['compression-count']] res = jsonlite::fromJSON(rawToChar(res$content)) if (!is.character(u <- res$output$url)) stop2( "Failed to shrink '", i, "'", sprintf(': %s (%s)', res$error, res$message) ) if (!quiet) message(sprintf( '%s (%s) ==> %s (%s, %.01f%%); compression count: %s', i, format_bytes(res$input$size), o, format_bytes(res$output$size), res$output$ratio * 100, if (length(cnt)) cnt else NA )) # back up the original image and restore it if download failed if (i == o) { b = paste0(i, '~') file.rename(i, b) on.exit(if (file_exists(o)) file.remove(b) else file.rename(b, i), add = TRUE) } curl::curl_download(u, o) save_history(o) }) invisible(output) } xfun/R/api.R0000644000175000017500000000523014143024424012457 0ustar nileshnilesh#' Get data from a REST API #' #' Read data from a REST API and optionally with an authorization token in the #' request header. The function \code{rest_api_raw()} returns the raw text of #' the response, and \code{rest_api()} will parse the response with #' \code{jsonlite::fromJSON()} (assuming that the response is in the JSON #' format). #' #' These functions are simple wrappers based on \code{\link{url}()} and #' \code{\link{read_utf8}()}. Specifically, the \code{headers} argument is #' passed to \code{url()}, and \code{read_utf8()} will send a \samp{GET} request #' to the API server. This means these functions only support the \samp{GET} #' method. If you need to use other HTTP methods (such as \samp{POST}), you have #' to use other packages such as \pkg{curl} and \pkg{httr}. #' @param ... Arguments to be passed to \code{rest_api_raw()}. #' @return A character vector (the raw JSON response) or an R object parsed from #' the JSON text. #' @export #' @examplesIf interactive() #' # a normal GET request #' xfun::rest_api('https://httpbin.org', '/get') #' xfun::rest_api_raw('https://httpbin.org', '/get') #' #' # send the request with an auth header #' xfun::rest_api('https://httpbin.org', '/headers', 'OPEN SESAME!') #' #' # with query parameters #' xfun::rest_api('https://httpbin.org', '/response-headers', params = list(foo = 'bar')) #' #' # get the rate limit info from Github #' xfun::github_api('/rate_limit') rest_api = function(...) { res = rest_api_raw(...) jsonlite::fromJSON(res, simplifyVector = FALSE) } #' @param root The API root URL. #' @param endpoint The API endpoint. #' @param token A named character string (e.g., \code{c(token = "xxxx")}), which #' will be used to create an authorization header of the form #' \samp{Authorization: NAME TOKEN} for the API call, where \samp{NAME} is the #' name of the string and \samp{TOKEN} is the string. If the string does not #' have a name, \samp{Basic} will be used as the default name. #' @param params A list of query parameters to be sent with the API call. #' @param headers A named character vector of HTTP headers, e.g., \code{c(Accept #' = "application/vnd.github.v3+json")}. #' @rdname rest_api #' @export rest_api_raw = function(root, endpoint, token = '', params = list(), headers = NULL) { if (is.null(names(token))) names(token) = 'Basic' endpoint = sub('^/?', '/', endpoint) # make sure it has a leading / con = url( paste0(root, endpoint, query_params(.list = params)), encoding = 'UTF-8', headers = c( headers, if (token != '') c(Authorization = sprintf('%s %s', names(token), token)) ) ) on.exit(close(con), add = TRUE) raw_string(suppressWarnings(read_utf8(con))) } xfun/R/os.R0000644000175000017500000000123313311015222012316 0ustar nileshnilesh#' Test for types of operating systems #' #' Functions based on \code{.Platform$OS.type} and \code{Sys.info()} to test if #' the current operating system is Windows, macOS, Unix, or Linux. #' @rdname os #' @export #' @examples #' library(xfun) #' # only one of the following statements should be true #' is_windows() #' is_unix() && is_macos() #' is_linux() is_windows = function() .Platform$OS.type == 'windows' #' @rdname os #' @export is_unix = function() .Platform$OS.type == 'unix' #' @rdname os #' @export is_macos = function() unname(Sys.info()['sysname'] == 'Darwin') #' @rdname os #' @export is_linux = function() unname(Sys.info()['sysname'] == 'Linux') xfun/R/json.R0000644000175000017500000000444014143240043012656 0ustar nileshnilesh#' A simple JSON serializer #' #' A JSON serializer that only works on a limited types of R data (\code{NULL}, #' lists, logical scalars, character/numeric vectors). A character string of the #' class \code{JS_EVAL} is treated as raw JavaScript, so will not be quoted. The #' function \code{json_vector()} converts an atomic R vector to JSON. #' @param x An R object. #' @export #' @return A character string. #' @seealso The \pkg{jsonlite} package provides a full JSON serializer. #' @examples library(xfun) #' tojson(NULL); tojson(1:10); tojson(TRUE); tojson(FALSE) #' cat(tojson(list(a = 1, b = list(c = 1:3, d = 'abc')))) #' cat(tojson(list(c('a', 'b'), 1:5, TRUE))) #' #' # the class JS_EVAL is originally from htmlwidgets::JS() #' JS = function(x) structure(x, class = 'JS_EVAL') #' cat(tojson(list(a = 1:5, b = JS('function() {return true;}')))) tojson = function(x) { if (is.null(x)) return('null') if (is.logical(x)) { if (length(x) != 1 || any(is.na(x))) stop('Logical values of length > 1 and NA are not supported') return(tolower(as.character(x))) } if (is.character(x) && inherits(x, 'JS_EVAL')) return(paste(x, collapse = '\n')) if (is.character(x) || is.numeric(x)) { return(json_vector(x, length(x) != 1 || inherits(x, 'AsIs'), is.character(x))) } if (is.list(x)) { if (length(x) == 0) return('{}') return(if (is.null(names(x))) { json_vector(unlist(lapply(x, tojson)), TRUE, quote = FALSE) } else { nms = quote_string(names(x)) paste0('{\n', paste(nms, unlist(lapply(x, tojson)), sep = ': ', collapse = ',\n'), '\n}') }) } stop('The class of x is not supported: ', paste(class(x), collapse = ', ')) } #' @param to_array Whether to convert a vector to a JSON array (use \code{[]}). #' @param quote Whether to double quote the elements. #' @rdname tojson #' @export json_vector = function(x, to_array = FALSE, quote = TRUE) { if (quote) { x = quote_string(x) x = gsub('\n', '\\\\n', x) x = gsub('\b', '\\\\b', x) x = gsub('\f', '\\\\f', x) x = gsub('\r', '\\\\r', x) x = gsub('\t', '\\\\t', x) } if (to_array) paste0('[', paste(x, collapse = ', '), ']') else x } # escape \ and " in strings, and quote them quote_string = function(x) { x = gsub('(["\\])', "\\\\\\1", x) if (length(x)) x = paste0('"', x, '"') x } xfun/R/data-structure.R0000644000175000017500000000562113753064224014671 0ustar nileshnilesh#' Strict lists #' #' A strict list is essentially a normal \code{\link{list}()} but it does not #' allow partial matching with \code{$}. #' #' To me, partial matching is often more annoying and surprising than #' convenient. It can lead to bugs that are very hard to discover, and I have #' been bitten by it many times. When I write \code{x$name}, I always mean #' precisely \code{name}. You should use a modern code editor to autocomplete #' the \code{name} if it is too long to type, instead of using partial names. #' @param ... Objects (list elements), possibly named. Ignored in the #' \code{print()} method. #' @export #' @return Both \code{strict_list()} and \code{as_strict_list()} return a list #' with the class \code{xfun_strict_list}. Whereas \code{as_strict_list()} #' attempts to coerce its argument \code{x} to a list if necessary, #' \code{strict_list()} just wraps its argument \code{...} in a list, i.e., it #' will add another list level regardless if \code{...} already is of type #' list. #' @examples library(xfun) #' (z = strict_list(aaa = 'I am aaa', b = 1:5)) #' z$a # NULL! #' z$aaa # I am aaa #' z$b #' z$c = 'create a new element' #' #' z2 = unclass(z) # a normal list #' z2$a # partial matching #' #' z3 = as_strict_list(z2) # a strict list again #' z3$a # NULL again! strict_list = function(...) { as_strict_list(list(...)) } # https://twitter.com/xieyihui/status/782462926862954496 #' @param x For \code{as_strict_list()}, the object to be coerced to a strict #' list. #' #' For \code{print()}, a strict list. #' @rdname strict_list #' @export as_strict_list = function(x) { structure(as.list(x), class = 'xfun_strict_list') } #' @param name The name (a character string) of the list element. #' @rdname strict_list #' @export `$.xfun_strict_list` = function(x, name) x[[name]] #' @rdname strict_list #' @export print.xfun_strict_list = function(x, ...) { print(unclass(x)) } #' Print a character vector in its raw form #' #' The function \code{raw_string()} assigns the class \code{xfun_raw_string} to #' the character vector, and the corresponding printing function #' \code{print.xfun_raw_string()} uses \code{cat(x, sep = '\n')} to write the #' character vector to the console, which will suppress the leading indices #' (such as \code{[1]}) and double quotes, and it may be easier to read the #' characters in the raw form (especially when there are escape sequences). #' @param x For \code{raw_string()}, a character vector. For the print method, #' the \code{raw_string()} object. #' @export #' @examples library(xfun) #' raw_string(head(LETTERS)) #' raw_string(c('a "b"', 'hello\tworld!')) raw_string = function(x) { if (is.null(x)) x = as.character(x) class(x) = c('xfun_raw_string', class(x)) x } #' @param ... Other arguments (currently ignored). #' @rdname raw_string #' @export print.xfun_raw_string = function(x, ...) { if (length(x)) cat(x, sep = '\n') invisible(x) } xfun/R/io.R0000644000175000017500000003360314153512256012330 0ustar nileshnilesh#' Read / write files encoded in UTF-8 #' #' Read or write files, assuming they are encoded in UTF-8. \code{read_utf8()} #' is roughly \code{readLines(encoding = 'UTF-8')} (a warning will be issued if #' non-UTF8 lines are found), and \code{write_utf8()} calls #' \code{writeLines(enc2utf8(text), useBytes = TRUE)}. #' #' The function \code{append_utf8()} appends UTF-8 content to a file or #' connection based on \code{read_utf8()} and \code{write_utf8()}, and #' optionally sort the content. The function \code{append_unique()} appends #' unique lines to a file or connection. #' @param con A connection or a file path. #' @param error Whether to signal an error when non-UTF8 characters are detected #' (if \code{FALSE}, only a warning message is issued). #' @param text A character vector (will be converted to UTF-8 via #' \code{\link{enc2utf8}()}). #' @param ... Other arguments passed to \code{\link{writeLines}()} (except #' \code{useBytes}, which is \code{TRUE} in \code{write_utf8()}). #' @export read_utf8 = function(con, error = FALSE) { # users may have set options(encoding = 'UTF-8'), which usually won't help but # will bring more trouble than good, so we reset this option temporarily opts = options(encoding = 'native.enc'); on.exit(options(opts), add = TRUE) x = readLines(con, encoding = 'UTF-8', warn = FALSE) i = invalid_utf8(x) n = length(i) if (n > 0) (if (error) stop else warning)( if (is.character(con)) c('The file ', con, ' is not encoded in UTF-8. '), 'These lines contain invalid UTF-8 characters: ', paste(c(head(i), if (n > 6) '...'), collapse = ', ') ) x } #' @rdname read_utf8 #' @export write_utf8 = function(text, con, ...) { if (is.null(text)) text = character(0) if (identical(con, '')) { cat(text, sep = '\n', file = con) } else { # prevent re-encoding the text in the file() connection in writeLines() # https://kevinushey.github.io/blog/2018/02/21/string-encoding-and-r/ opts = options(encoding = 'native.enc'); on.exit(options(opts), add = TRUE) writeLines(enc2utf8(text), con, ..., useBytes = TRUE) } } #' @param sort Logical (\code{FALSE} means not to sort the content) or a #' function to sort the content; \code{TRUE} is equivalent to #' \code{base::sort}. #' @rdname read_utf8 #' @export append_utf8 = function(text, con, sort = TRUE) { x = read_utf8(con, error = TRUE) x = c(x, text) if (is.logical(sort)) sort = if (sort) base::sort else identity if (is.function(sort)) x = sort(x) write_utf8(x, con) } #' @rdname read_utf8 #' @export append_unique = function(text, con, sort = function(x) base::sort(unique(x))) { append_utf8(text, con, sort) } # which lines are invalid UTF-8 invalid_utf8 = function(x) { which(!is_utf8(x)) } test_utf8 = function(x) { is.na(x) | !is.na(iconv(x, 'UTF-8', 'UTF-8')) } # validUTF8() was added to base R 3.3.0 is_utf8 = function(x) { if ('validUTF8' %in% ls(baseenv())) validUTF8(x) else test_utf8(x) } #' Read a text file and concatenate the lines by \code{'\n'} #' #' The source code of this function should be self-explanatory. #' @param file Path to a text file (should be encoded in UTF-8). #' @return A character string of text lines concatenated by \code{'\n'}. #' @export #' @examples #' xfun::file_string(system.file('DESCRIPTION', package = 'xfun')) file_string = function(file) { x = read_utf8(file) # paste converts 0-length character() into 1-length "" if (length(x)) x = paste(x, collapse = '\n') raw_string(x) } #' Read all records of a binary file as a raw vector by default #' #' This is a wrapper function of \code{\link{readBin}()} with default arguments #' \code{what = "raw"} and \code{n = \link{file.size}(file)}, which means it #' will read the full content of a binary file as a raw vector by default. #' @param file,what,n,... Arguments to be passed to \code{readBin()}. #' @return A vector returned from \code{readBin()}. #' @export #' @examples #' f = tempfile() #' cat('abc', file = f) #' xfun::read_bin(f) #' unlink(f) read_bin = function(file, what = 'raw', n = file.info(file)$size, ...) { readBin(file, what, n, ...) } #' Read all text files and concatenate their content #' #' Read files one by one, and optionally add text before/after the content. Then #' combine all content into one character vector. #' @param files A vector of file paths. #' @param before,after A function that takes one file path as the input and #' returns values to be added before or after the content of the file. #' Alternatively, they can be constant values to be added. #' @return A character vector. #' @export #' @examples #' # two files in this package #' fs = system.file('scripts', c('call-fun.R', 'child-pids.sh'), package = 'xfun') #' xfun::read_all(fs) #' #' # add file paths before file content and an empty line after content #' xfun::read_all(fs, before = function(f) paste('#-----', f, '-----'), after = '') #' #' # add constants #' xfun::read_all(fs, before = '/*', after = c('*/', '')) read_all = function(files, before = function(f) NULL, after = function(f) NULL) { b = before; a = after x = unlist(lapply(files, function(f) { c(if (is.function(b)) b(f) else b, read_utf8(f), if (is.function(a)) a(f) else a) })) raw_string(x) } #' Read a text file, process the text with a function, and write the text back #' #' Read a text file with the UTF-8 encoding, apply a function to the text, and #' write back to the original file. #' #' \code{sort_file()} is an application of \code{process_file()}, with the #' processing function being \code{\link{sort}()}, i.e., it sorts the text lines #' in a file and write back the sorted text. #' @param file Path to a text file. #' @param fun A function to process the text. #' @param x The content of the file. #' @param ... Arguments to be passed to \code{process_file()}. #' @return If \code{file} is provided, invisible \code{NULL} (the file is #' updated as a side effect), otherwise the processed content (as a character #' vector). #' @export #' @examples f = tempfile() #' xfun::write_utf8('Hello World', f) #' xfun::process_file(f, function(x) gsub('World', 'woRld', x)) #' xfun::read_utf8(f) # see if it has been updated #' file.remove(f) process_file = function(file, fun = identity, x = read_utf8(file)) { x = fun(x) if (missing(file)) x else write_utf8(x, file) } #' @rdname process_file #' @export sort_file = function(..., fun = sort) { process_file(fun = fun, ...) } #' Search and replace strings in files #' #' These functions provide the "file" version of \code{\link{gsub}()}, i.e., #' they perform searching and replacement in files via \code{gsub()}. #' @param file Path of a single file. #' @param ... For \code{gsub_file()}, arguments passed to \code{gsub()}. For #' other functions, arguments passed to \code{gsub_file()}. Note that the #' argument \code{x} of \code{gsub()} is the content of the file. #' @param rw_error Whether to signal an error if the file cannot be read or #' written. If \code{FALSE}, the file will be ignored (with a warning). #' @param files A vector of file paths. #' @param dir Path to a directory (all files under this directory will be #' replaced). #' @param recursive Whether to find files recursively under a directory. #' @param ext A vector of filename extensions (without the leading periods). #' @param mimetype A regular expression to filter files based on their MIME #' types, e.g., \code{'^text/'} for plain text files. This requires the #' \pkg{mime} package. #' @note These functions perform in-place replacement, i.e., the files will be #' overwritten. Make sure you backup your files in advance, or use version #' control! #' @export #' @examples library(xfun) #' f = tempfile() #' writeLines(c('hello', 'world'), f) #' gsub_file(f, 'world', 'woRld', fixed = TRUE) #' readLines(f) gsub_file = function(file, ..., rw_error = TRUE) { if (!(file.access(file, 2) == 0 && file.access(file, 4) == 0)) { (if (rw_error) stop else warning)('Unable to read or write to ', file) if (!rw_error) return(invisible()) } x1 = tryCatch(read_utf8(file, error = TRUE), error = function(e) if (rw_error) stop(e)) if (is.null(x1)) return(invisible()) x2 = gsub(x = x1, ...) if (!identical(x1, x2)) write_utf8(x2, file) } #' @rdname gsub_file #' @export gsub_files = function(files, ...) { for (f in files) gsub_file(f, ...) } #' @rdname gsub_file #' @export gsub_dir = function(..., dir = '.', recursive = TRUE, ext = NULL, mimetype = '.*') { files = list.files(dir, full.names = TRUE, recursive = recursive) if (length(ext)) files = files[file_ext(files) %in% ext] if (mimetype != '.*') files = files[grep(mimetype, mime::guess_type(files))] gsub_files(files, ...) } #' @rdname gsub_file #' @export gsub_ext = function(ext, ..., dir = '.', recursive = TRUE) { gsub_dir(..., dir = dir, recursive = recursive, ext = ext) } #' Perform replacement with \code{gsub()} on elements matched from \code{grep()} #' #' This function is a shorthand of \code{gsub(pattern, replacement, #' grep(pattern, x, value = TRUE))}. #' @param pattern,replacement,x,... Passed to \code{\link{grep}()} and #' \code{gsub()}. #' @return A character vector. #' @export #' @examples # find elements that matches 'a[b]+c' and capitalize 'b' with perl regex #' xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE) grep_sub = function(pattern, replacement, x, ...) { x = grep(pattern, x, value = TRUE, ...) gsub(pattern, replacement, x, ...) } #' Try various methods to download a file #' #' Try all possible methods in \code{\link{download.file}()} (e.g., #' \code{libcurl}, \code{curl}, \code{wget}, and \code{wininet}) and see if any #' method can succeed. The reason to enumerate all methods is that sometimes the #' default method does not work, e.g., #' \url{https://stat.ethz.ch/pipermail/r-devel/2016-June/072852.html}. #' @param url The URL of the file. #' @param output Path to the output file. By default, it is determined by #' \code{\link{url_filename}()}. #' @param ... Other arguments to be passed to \code{\link{download.file}()} #' (except \code{method}). #' @param .error An error message to signal when the download fails. #' @note To allow downloading large files, the \code{timeout} option in #' \code{\link{options}()} will be temporarily set to one hour (3600 seconds) #' inside this function when this option has the default value of 60 seconds. #' If you want a different \code{timeout} value, you may set it via #' \code{options(timeout = N)}, where \code{N} is the number of seconds (not #' 60). #' @return The integer code \code{0} for success, or an error if none of the #' methods work. #' @export download_file = function( url, output = url_filename(url), ..., .error = 'No download method works (auto/wininet/wget/curl/lynx)' ) { if (getOption('timeout') == 60L) { opts = options(timeout = 3600) # one hour on.exit(options(opts), add = TRUE) } download = function(method = 'auto') suppressWarnings({ tryCatch(download.file(url, output, ..., method = method), error = function(e) 1L) }) for (method in c(if (is_windows()) 'wininet', 'libcurl', 'auto')) { if (download(method = method) == 0) return(0L) } # check for libcurl/curl/wget/lynx, call download.file with appropriate method if (Sys.which('curl') != '') { # curl needs to add a -L option to follow redirects opts2 = if (is.null(getOption('download.file.extra'))) options(download.file.extra = c('-L', '--fail')) res = download(method = 'curl') options(opts2) if (res == 0) return(res) } if (Sys.which('wget') != '') { if ((res <- download(method = 'wget')) == 0) return(res) } if (Sys.which('lynx') != '') { if ((res <- download(method = 'lynx')) == 0) return(res) } stop(.error) } #' Test if a URL is accessible #' #' Try to send a \code{HEAD} request to a URL using #' \code{\link{curlGetHeaders}()} or the \pkg{curl} package, and see if it #' returns a successful status code. #' @param x A URL as a character string. #' @param use_curl Whether to use the \pkg{curl} package or the #' \code{curlGetHeaders()} function in base R to send the request to the URL. #' By default, \pkg{curl} will be used when base R does not have the #' \command{libcurl} capability (which should be rare). #' @param ... Arguments to be passed to \code{curlGetHeaders()}. #' @return \code{TRUE} or \code{FALSE}. #' @export #' @examples xfun::url_accessible('https://yihui.org') url_accessible = function(x, use_curl = !capabilities('libcurl'), ...) { try_status = function(code) tryCatch(code < 400, error = function(e) FALSE) if (use_curl) { h = curl::new_handle() curl::handle_setopt(h, customrequest = 'HEAD', nobody = TRUE) try_status(curl::curl_fetch_memory(x, h)$status_code) } else { # use curlGetHeaders() instead try_status(attr(curlGetHeaders(x, ...), 'status')) } } #' Generate a message with \code{cat()} #' #' This function is similar to \code{\link{message}()}, and the difference is #' that \code{msg_cat()} uses \code{\link{cat}()} to write out the message, #' which is sent to \code{\link{stdout}} instead of \code{\link{stderr}}. The #' message can be suppressed by \code{\link{suppressMessages}()}. #' @param ... Character strings of messages, which will be concatenated into one #' string via \code{paste(c(...), collapse = '')}. #' @note By default, a newline will not be appended to the message. If you need #' a newline, you have to explicitly add it to the message (see #' \sQuote{Examples}). #' @return Invisible \code{NULL}, with the side-effect of printing the message. #' @seealso This function was inspired by \code{rlang::inform()}. #' @export #' @examples #' { #' # a message without a newline at the end #' xfun::msg_cat('Hello world!') #' # add a newline at the end #' xfun::msg_cat(' This message appears right after the previous one.\n') #' } #' suppressMessages(xfun::msg_cat('Hello world!')) msg_cat = function(...) { x = paste(c(...), collapse = '') withRestarts({ signalCondition(simpleMessage(x)) cat(x) }, muffleMessage = function() invisible(NULL)) } xfun/R/cache.R0000644000175000017500000002046114132441073012755 0ustar nileshnilesh#' Cache the value of an R expression to an RDS file #' #' Save the value of an expression to a cache file (of the RDS format). Next #' time the value is loaded from the file if it exists. #' #' Note that the \code{file} argument does not provide the full cache filename. #' The actual name of the cache file is of the form \file{BASENAME_HASH.rds}, #' where \file{BASENAME} is the base name provided via the \file{file} argument #' (e.g., if \code{file = 'foo.rds'}, \code{BASENAME} would be \file{foo}), and #' \file{HASH} is the MD5 hash (also called the \sQuote{checksum}) calculated #' from the R code provided to the \code{expr} argument and the value of the #' \code{hash} argument, which means when the code or the \code{hash} argument #' changes, the \file{HASH} string may also change, and the old cache will be #' invalidated (if it exists). If you want to find the cache file, look for #' \file{.rds} files that contain 32 hexadecimal digits (consisting of 0-9 and #' a-z) at the end of the filename. #' #' The possible ways to invalidate the cache are: 1) change the code in #' \code{expr} argument; 2) delete the cache file manually or automatically #' through the argument \code{rerun = TRUE}; and 3) change the value of the #' \code{hash} argument. The first two ways should be obvious. For the third #' way, it makes it possible to automatically invalidate the cache based on #' changes in certain R objects. For example, when you run \code{cache_rds({ x + #' y })}, you may want to invalidate the cache to rerun \code{{ x + y }} when #' the value of \code{x} or \code{y} has been changed, and you can tell #' \code{cache_rds()} to do so by \code{cache_rds({ x + y }, hash = list(x, #' y))}. The value of the argument \code{hash} is expected to be a list, but it #' can also take a special value, \code{"auto"}, which means #' \code{cache_rds(expr)} will try to automatically figure out the global #' variables in \code{expr}, return a list of their values, and use this list as #' the actual value of \code{hash}. This behavior is most likely to be what you #' really want: if the code in \code{expr} uses an external global variable, you #' may want to invalidate the cache if the value of the global variable has #' changed. Here a \dQuote{global variable} means a variable not created locally #' in \code{expr}, e.g., for \code{cache_rds({ x <- 1; x + y })}, \code{x} is a #' local variable, and \code{y} is (most likely to be) a global variable, so #' changes in \code{y} should invalidate the cache. However, you know your own #' code the best. If you want to be completely sure when to invalidate the #' cache, you can always provide a list of objects explicitly rather than #' relying on \code{hash = "auto"}. #' #' By default (the argument \code{clean = TRUE}), old cache files will be #' automatically cleaned up. Sometimes you may want to use \code{clean = FALSE} #' (set the R global option \code{options(xfun.cache_rds.clean = FALSE)} if you #' want \code{FALSE} to be the default). For example, you may not have decided #' which version of code to use, and you can keep the cache of both versions #' with \code{clean = FALSE}, so when you switch between the two versions of #' code, it will still be fast to run the code. #' @param expr An R expression. #' @param rerun Whether to delete the RDS file, rerun the expression, and save #' the result again (i.e., invalidate the cache if it exists). #' @param file The \emph{base} (see Details) cache filename under the directory #' specified by the \code{dir} argument. If not specified and this function is #' called inside a code chunk of a \pkg{knitr} document (e.g., an R Markdown #' document), the default is the current chunk label plus the extension #' \file{.rds}. #' @param dir The path of the RDS file is partially determined by #' \code{paste0(dir, file)}. If not specified and the \pkg{knitr} package is #' available, the default value of \code{dir} is the \pkg{knitr} chunk option #' \code{cache.path} (so if you are compiling a \pkg{knitr} document, you do #' not need to provide this \code{dir} argument explicitly), otherwise the #' default is \file{cache/}. If you do not want to provide a \code{dir} but #' simply a valid path to the \code{file} argument, you may use \code{dir = #' ""}. #' @param hash A \code{list} object that contributes to the MD5 hash of the #' cache filename (see Details). It can also take a special character value #' \code{"auto"}. Other types of objects are ignored. #' @param clean Whether to clean up the old cache files automatically when #' \code{expr} has changed. #' @param ... Other arguments to be passed to \code{\link{saveRDS}()}. #' @note Changes in the code in the \code{expr} argument do not necessarily #' always invalidate the cache, if the changed code is \code{\link{parse}d} to #' the same expression as the previous version of the code. For example, if #' you have run \code{cache_rds({Sys.sleep(5);1+1})} before, running #' \code{cache_rds({ Sys.sleep( 5 ) ; 1 + 1 })} will use the cache, because #' the two expressions are essentially the same (they only differ in white #' spaces). Usually you can add/delete white spaces or comments to your code #' in \code{expr} without invalidating the cache. See the package vignette #' \code{vignette('xfun', package = 'xfun')} for more examples. #' #' When this function is called in a code chunk of a \pkg{knitr} document, you #' may not want to provide the filename or directory of the cache file, #' because they have reasonable defaults. #' #' Side-effects (such as plots or printed output) will not be cached. The #' cache only stores the last value of the expression in \code{expr}. #' @return If the cache file does not exist, run the expression and save the #' result to the file, otherwise read the cache file and return the value. #' @export #' @examples #' f = tempfile() # the cache file #' compute = function(...) { #' res = xfun::cache_rds({ #' Sys.sleep(1) #' 1:10 #' }, file = f, dir = '', ...) #' res #' } #' compute() # takes one second #' compute() # returns 1:10 immediately #' compute() # fast again #' compute(rerun = TRUE) # one second to rerun #' compute() #' file.remove(f) cache_rds = function( expr = {}, rerun = FALSE, file = 'cache.rds', dir = 'cache/', hash = NULL, clean = getOption('xfun.cache_rds.clean', TRUE), ... ) { if (loadable('knitr')) { if (missing(file) && !is.null(lab <- knitr::opts_current$get('label'))) file = paste0(lab, '.rds') if (missing(dir) && !is.null(d <- knitr::opts_current$get('cache.path'))) dir = d } path = paste0(dir, file) if (!grepl(r <- '([.]rds)$', path)) path = paste0(path, '.rds') code = deparse(substitute(expr)) md5 = md5sum_obj(code) if (identical(hash, 'auto')) hash = global_vars(code, parent.frame(2)) if (is.list(hash)) md5 = md5sum_obj(c(md5, md5sum_obj(hash))) path = sub(r, paste0('_', md5, '\\1'), path) if (rerun) unlink(path) if (clean) clean_cache(path) if (file_exists(path)) readRDS(path) else { obj = expr # lazy evaluation dir.create(dirname(path), recursive = TRUE, showWarnings = FALSE) saveRDS(obj, path, ...) obj } } # write an object to a file and return the md5 sum md5sum_obj = function(x) { f = tempfile(); on.exit(unlink(f), add = TRUE) if (is.character(x)) writeLines(x, f) else saveRDS(x, f) tools::md5sum(f) } # clean up old cache files (those with the same base names as the new cache # file, e.g., if the new file is FOO_0123abc...z.rds, then FOO_9876def...x.rds # should be deleted) clean_cache = function(path) { olds = list.files(dirname(path), '_[0-9a-f]{32}[.]rds$', full.names = TRUE) olds = c(olds, path) # `path` may not exist; make sure it is in target paths base = basename(olds) keep = basename(path) == base # keep this file (will cache to this file) base = substr(base, 1, nchar(base) - 37) # 37 = 1 (_) + 32 (md5 sum) + 4 (.rds) unlink(olds[(base == base[keep][1]) & !keep]) } # analyze code and find out global variables find_globals = function(code) { fun = eval(parse_only(c('function(){', code, '}'))) setdiff(codetools::findGlobals(fun), known_globals) } known_globals = c( '{', '[', '(', ':', '<-', '=', '+', '-', '*', '/', '%%', '%/%', '%*%', '%o%', '%in%' ) # return a list of values of global variables in code global_vars = function(code, env) { if (length(vars <- find_globals(code)) > 0) mget(vars, env) } xfun/R/command.R0000644000175000017500000003302714145515224013336 0ustar nileshnilesh#' Run \code{system2()} and mark its character output as UTF-8 if appropriate #' #' This is a wrapper function based on \code{system2()}. If \code{system2()} #' returns character output (e.g., with the argument \code{stdout = TRUE}), #' check if the output is encoded in UTF-8. If it is, mark it with UTF-8 #' explicitly. #' @param ... Passed to \code{\link{system2}()}. #' @return The value returned by \code{system2()}. #' @export #' @examplesIf interactive() #' a = shQuote(c('-e', 'print(intToUtf8(c(20320, 22909)))')) #' x2 = system2('Rscript', a, stdout = TRUE) #' Encoding(x2) # unknown #' #' x3 = xfun::system3('Rscript', a, stdout = TRUE) #' # encoding of x3 should be UTF-8 if the current locale is UTF-8 #' !l10n_info()[['UTF-8']] || Encoding(x3) == 'UTF-8' # should be TRUE system3 = function(...) { res = system2(...) if (is.character(res)) { if (all(is_utf8(res))) Encoding(res) = 'UTF-8' } if (is.integer(res) && res == 0) invisible(res) else res } #' Run OptiPNG on all PNG files under a directory #' #' Call the command \command{optipng} via \code{system2()} to optimize all PNG #' files under a directory. #' @param dir Path to a directory. #' @param files Alternatively, you can choose the specific files to optimize. #' @param ... Arguments to be passed to \code{system2()}. #' @references OptiPNG: \url{http://optipng.sourceforge.net}. #' @export optipng = function( dir = '.', files = list.files(dir, '[.]png$', recursive = TRUE, full.names = TRUE), ... ) { if (Sys.which('optipng') != '') for (f in files) system2('optipng', shQuote(f), ...) } #' Run the commands \command{Rscript} and \command{R CMD} #' #' Wrapper functions to run the commands \command{Rscript} and \command{R CMD}. #' @param args A character vector of command-line arguments. #' @param ... Other arguments to be passed to \code{\link{system2}()}. #' @export #' @return A value returned by \code{system2()}. #' @examples library(xfun) #' Rscript(c('-e', '1+1')) #' Rcmd(c('build', '--help')) Rscript = function(args, ...) { # unset R_TESTS for the new R session: https://stackoverflow.com/a/27994299 if (is_R_CMD_check()) { v = set_envvar(c(R_TESTS = NA)); on.exit(set_envvar(v), add = TRUE) } system2(file.path(R.home('bin'), 'Rscript'), args, ...) } #' @rdname Rscript #' @export Rcmd = function(args, ...) { system2(file.path(R.home('bin'), 'R'), c('CMD', args), ...) } #' Call a function in a new R session via \code{Rscript()} #' #' Save the argument values of a function in a temporary RDS file, open a new R #' session via \code{\link{Rscript}()}, read the argument values, call the #' function, and read the returned value back to the current R session. #' @param fun A function, or a character string that can be parsed and evaluated #' to a function. #' @param args A list of argument values. #' @param options A character vector of options to passed to #' \code{\link{Rscript}}, e.g., \code{"--vanilla"}. #' @param ...,wait Arguments to be passed to \code{\link{system2}()}. #' @param fail The desired error message when an error occurred in calling the #' function. #' @export #' @return The returned value of the function in the new R session. #' @examples factorial(10) #' # should return the same value #' xfun::Rscript_call('factorial', list(10)) #' #' # the first argument can be either a character string or a function #' xfun::Rscript_call(factorial, list(10)) #' #' # Run Rscript starting a vanilla R session #' xfun::Rscript_call(factorial, list(10), options = c("--vanilla")) Rscript_call = function( fun, args = list(), options = NULL, ..., wait = TRUE, fail = sprintf("Failed to run '%s' in a new R session.", deparse(substitute(fun))[1]) ) { f = replicate(2, tempfile(fileext = '.rds')) on.exit(unlink(if (wait) f else f[2]), add = TRUE) saveRDS(list(fun, args), f[1]) Rscript( c(options, shQuote(c(pkg_file('scripts', 'call-fun.R'), f))) ,..., wait = wait ) if (wait) if (file_exists(f[2])) readRDS(f[2]) else stop(fail, call. = FALSE) } # call a function in a background process Rscript_bg = function(fun, args = list(), timeout = 10) { pid = tempfile() # to store the process ID of the new R session saveRDS(NULL, pid) Rscript_call(function() { saveRDS(Sys.getpid(), pid) # remove this pid file when the function finishes on.exit(unlink(pid), add = TRUE) do.call(fun, args) }, wait = FALSE) id = NULL # read the above process ID into this R session res = list(pid = id, is_alive = function() FALSE) # check if the pid file still exists; if not, the process has ended if (!file_exists(pid)) return(res) t0 = Sys.time() while (difftime(Sys.time(), t0, units = 'secs') < timeout) { Sys.sleep(.1) if (!file_exists(pid)) return(res) if (length(id <- readRDS(pid)) == 1) break } if (length(id) == 0) stop( 'Failed to launch the background process in ', timeout, ' seconds (timeout).' ) list(pid = id, is_alive = function() file_exists(pid)) } #' Kill a process and (optionally) all its child processes #' #' Run the command \command{taskkill /f /pid} on Windows and \command{kill} on #' Unix, respectively, to kill a process. #' @param pid The process ID. #' @param recursive Whether to kill the child processes of the process. #' @param ... Arguments to be passed to \code{\link{system2}()} to run the #' command to kill the process. #' @return The status code returned from \code{system2()}. #' @export proc_kill = function(pid, recursive = TRUE, ...) { if (is_windows()) { system2('taskkill', c(if (recursive) '/t', '/f', '/pid', pid), ...) } else { system2('kill', c(pid, if (recursive) child_pids(pid)), ...) } } # obtain pids of all child processes (recursively) child_pids = function(id) { x = system2('sh', shQuote(c(pkg_file('scripts', 'child-pids.sh'), id)), stdout = TRUE) grep('^[0-9]+$', x, value = TRUE) } powershell = function(command) { if (Sys.which('powershell') == '') return() command = paste(command, collapse = ' ') system2('powershell', c('-Command', shQuote(command)), stdout = TRUE) } # start a background process via the PowerShell cmdlet and return its pid ps_process = function(command, args = character(), verbose = FALSE) { powershell(c( 'echo (Start-Process', '-FilePath', shQuote(command), '-ArgumentList', ps_quote(args), '-PassThru', '-WindowStyle', sprintf('%s).ID', if (verbose) 'Normal' else 'Hidden') )) } # quote PowerShell arguments properly ps_quote = function(x) { x = gsub('"', '""', x) # '""' mean a literal '"' # if an argument contains a space, surround it with escaped double quotes `"`" i = grep('\\s', x) x[i] = sprintf('`"%s`"', x[i]) sprintf('"%s"', paste(x, collapse = ' ')) } #' Start a background process #' #' Start a background process using the PowerShell cmdlet \command{Start-Process #' -PassThru} on Windows or the ampersand \command{&} on Unix, and return the #' process ID. #' @param command,args The system command and its arguments. They do not need to #' be quoted, since they will be quoted via \code{\link{shQuote}()} #' internally. #' @param verbose If \code{FALSE}, suppress the output from \verb{stdout} (and #' also \verb{stderr} on Windows). The default value of this argument can be #' set via a global option, e.g., \code{options(xfun.bg_process.verbose = #' TRUE)}. #' @return The process ID as a character string. #' @note On Windows, if PowerShell is not available, try to use #' \code{\link{system2}(wait = FALSE)} to start the background process #' instead. The process ID will be identified from the output of the command #' \command{tasklist}. This method of looking for the process ID may not be #' reliable. If the search is not successful in 30 seconds, it will throw an #' error (timeout). If a longer time is needed, you may set #' \code{options(xfun.bg_process.timeout)} to a larger value, but it should be #' very rare that a process cannot be started in 30 seconds. When you reach #' the timeout, it is more likely that the command actually failed. #' @export #' @seealso \code{\link{proc_kill}()} to kill a process. bg_process = function( command, args = character(), verbose = getOption('xfun.bg_process.verbose', FALSE) ) { throw_error = function(...) stop( 'Failed to run the command', ..., ' in the background: ', paste(shQuote(c(command, args)), collapse = ' '), call. = FALSE ) # check the possible pid returned from system2() check_pid = function(res) { if (is.null(res)) return(res) if (!is.null(attr(res, 'status'))) throw_error() if (length(res) == 1 && grepl('^[0-9]+$', res)) return(res) throw_error() } if (is_windows()) { # first try 'Start-Process -PassThrough' to start a background process; if # PowerShell is unavailable, fall back to system2(wait = FALSE), and the # method to find out the pid is not 100% reliable if (length(pid <- check_pid(ps_process(command, args, verbose))) == 1) return(pid) message( 'It seems you do not have PowerShell installed. The process ID may be inaccurate.' ) # format of task list: hugo.exe 4592 Console 1 35,188 K tasklist = function() system2('tasklist', stdout = TRUE) pid1 = tasklist() system2(command, shQuote(args), wait = FALSE) get_pid = function() { # make sure the command points to an actual executable (e.g., resolve 'R' # to 'R.exe') if (!file_exists(command)) { if (Sys.which(command) != '') command = Sys.which(command) } cmd = basename(command) pid2 = setdiff(tasklist(), pid1) # the process's info should start with the command name pid2 = pid2[substr(pid2, 1, nchar(cmd)) == cmd] if (length(pid2) == 0) return() m = regexec('\\s+([0-9]+)\\s+', pid2) for (v in regmatches(pid2, m)) if (length(v) >= 2) return(v[2]) } t0 = Sys.time(); id = NULL; timeout = getOption('xfun.bg_process.timeout', 30) while (difftime(Sys.time(), t0, units = 'secs') < timeout) { if (length(id <- get_pid()) > 0) break } if (length(id) > 0) return(id) system2(command, args, timeout = timeout) # see what the error is throw_error(' in ', timeout, ' second(s)') } else { pid = tempfile(); on.exit(unlink(pid), add = TRUE) code = paste(c( shQuote(c(command, args)), if (!verbose) '> /dev/null', '& echo $! >', shQuote(pid) ), collapse = ' ') system2('sh', c('-c', shQuote(code))) return(check_pid(readLines(pid))) } } #' Upload to an FTP server via \command{curl} #' #' The function \code{upload_ftp()} runs the command \command{curl -T file #' server} to upload a file to an FTP server if the system command #' \command{curl} is available, otherwise it uses the R package \pkg{curl}. The #' function \code{upload_win_builder()} uses \code{upload_ftp()} to upload #' packages to the win-builder server. #' #' These functions were written mainly to save package developers the trouble of #' going to the win-builder web page and uploading packages there manually. #' @param file Path to a local file. #' @param server The address of the FTP server. For \code{upload_win_builder()}, #' \code{server = 'https'} means uploading to #' \code{'https://win-builder.r-project.org/upload.aspx'}. #' @param dir The remote directory to which the file should be uploaded. #' @param version The R version(s) on win-builder. #' @return Status code returned from \code{\link{system2}()} or #' \code{curl::curl_fetch_memory()}. #' @export upload_ftp = function(file, server, dir = '') { if (dir != '') dir = gsub('/*$', '/', dir) server = paste0(server, dir) if (Sys.which('curl') == '') { curl::curl_upload(file, server)$status_code } else { system2('curl', shQuote(c('-T', file, server))) } } #' @param solaris Whether to also upload the package to the Rhub server to check #' it on Solaris. #' @rdname upload_ftp #' @export upload_win_builder = function( file = pkg_build(), version = c("R-devel", "R-release", "R-oldrelease"), server = c('ftp', 'https'), solaris = pkg_available('rhub') ) { if (missing(file)) on.exit(file.remove(file), add = TRUE) if (system2('git', 'status', stderr = FALSE) == 0) system2('git', 'pull') server = server[1] server = switch( server, 'ftp' = paste0(server, '://win-builder.r-project.org/'), 'https' = paste0(server, '://win-builder.r-project.org/upload.aspx'), server ) res = if (grepl('^ftp://', server)) { lapply(version, upload_ftp, file = file, server = server) } else { vers = c('R-devel' = 2, 'R-release' = 1, 'R-oldrelease' = 3) params = list( FileUpload = file, Button = 'Upload File', # perhaps we should read these tokens dynamically from # https://win-builder.r-project.org/upload.aspx `__VIEWSTATE` = '/wEPDwULLTE0OTY5NTg0MTUPZBYCAgIPFgIeB2VuY3R5cGUFE211bHRpcGFydC9mb3JtLWRhdGFkZFHMrNH6JjHTyJ00T0dAADGf4oa0', `__VIEWSTATEGENERATOR` = '69164837', `__EVENTVALIDATION` = '/wEWBQKksYbrBgKM54rGBgK7q7GGCAKF2fXbAwLWlM+bAqR2dARbCNfKVu0vDawqWYgB5kKI' ) lapply(version, function(i) { names(params)[1:2] = paste0(names(params)[1:2], vers[i]) if (Sys.which('curl') == '') { h = curl::new_handle() params[[1]] = curl::form_file(params[[1]]) curl::handle_setform(h, .list = params) curl::curl_fetch_memory(server, h)$status_code } else { params[1] = paste0('@', params[1]) system2('curl', shQuote(c( rbind('-F', paste(names(params), params, sep = '=')), server )), stdout = FALSE) } }) } if (solaris) rhub::check_on_solaris( file, check_args = '--no-manual', show_status = FALSE, env_vars = c(`_R_CHECK_FORCE_SUGGESTS_` = 'false') ) setNames(unlist(res), version) } xfun/R/base64.R0000644000175000017500000000602214057724152013003 0ustar nileshnilesh#' Encode/decode data into/from base64 encoding. #' #' The function \code{base64_encode()} encodes a file or a raw vector into the #' base64 encoding. The function \code{base64_decode()} decodes data from the #' base64 encoding. #' @param x For \code{base64_encode()}, a raw vector. If not raw, it is assumed #' to be a file or a connection to be read via \code{readBin()}. For #' \code{base64_decode()}, a string. #' @param from If provided (and \code{x} is not provided), a connection or file #' to be read via \code{readChar()}, and the result will be passed to the #' argument \code{x}. #' @return \code{base64_encode()} returns a character string. #' \code{base64_decode()} returns a raw vector. #' @useDynLib xfun, .registration = TRUE #' @export #' @examples xfun::base64_encode(as.raw(1:10)) #' logo = xfun:::R_logo() #' xfun::base64_encode(logo) base64_encode = function(x) { if (!is.raw(x)) x = read_bin(x) .Call('base64_enc', x, PACKAGE = 'xfun') } #' @export #' @rdname base64_encode #' @examples xfun::base64_decode("AQIDBAUGBwgJCg==") base64_decode = function(x, from = NA) { if (!is.na(from)) { if (!missing(x)) stop("Please provide either 'x' or 'from', but not both.") x = readChar(from, file.size(from), TRUE) } if (!is.character(x) || length(x) != 1) stop("'x' must be a single character string.") .Call('base64_dec', x, PACKAGE = 'xfun') } # an R implementation of base64 encoding by Wush Wu moved from knitr (of # historic interest only): https://github.com/yihui/knitr/pull/324 base64_encode_r = function(x) { if (!is.raw(x)) x = read_bin(x) chars = c(LETTERS, letters, 0:9, '+', '/') n = length(s <- as.integer(x)) res = rep(NA, (n + 2) / 3 * 4) i = 0L # index of res vector j = 1L # index of base64_table while (n > 2L) { res[i <- i + 1L] = chars[s[j] %/% 4L + 1L] res[i <- i + 1L] = chars[16 * (s[j] %% 4L) + s[j + 1L] %/% 16 + 1L] res[i <- i + 1L] = chars[4L * (s[j + 1L] %% 16) + s[j + 2L] %/% 64L + 1L] res[i <- i + 1L] = chars[s[j + 2L] %% 64L + 1L] j = j + 3L n = n - 3L } if (n) { res[i <- i + 1L] = chars[s[j] %/% 4L + 1L] if (n > 1L) { res[i <- i + 1L] = chars[16 * (s[j] %% 4L) + s[j + 1L] %/% 16 + 1L] res[i <- i + 1L] = chars[4L * (s[j + 1L] %% 16) + 1L] res[i <- i + 1L] = '=' } else { res[i <- i + 1L] = chars[16 * (s[j] %% 4L) + 1L] res[i <- i + 1L] = '=' res[i <- i + 1L] = '=' } } paste(res[!is.na(res)], collapse = '') } #' Generate the Data URI for a file #' #' Encode the file in the base64 encoding, and add the media type. The data URI #' can be used to embed data in HTML documents, e.g., in the \code{src} #' attribute of the \verb{} tag. #' @param x A file path. #' @return A string of the form \verb{data:;base64,}. #' @export #' @examples #' logo = xfun:::R_logo() #' img = htmltools::img(src = xfun::base64_uri(logo), alt = 'R logo') #' if (interactive()) htmltools::browsable(img) base64_uri = function(x) { paste0("data:", mime::guess_type(x), ";base64,", base64_encode(x)) } xfun/R/github.R0000644000175000017500000000756714156156616013224 0ustar nileshnilesh#' Get the tags of Github releases of a repository #' #' Use the Github API (\code{\link{github_api}()}) to obtain the tags of the #' releases. #' @param repo The repository name of the form \code{user/repo}, e.g., #' \code{"yihui/xfun"}. #' @param tag A tag as a character string. If provided, it will be returned if #' the tag exists. If \code{tag = "latest"}, the tag of the latest release is #' returned. #' @param pattern A regular expression to match the tags. #' @param use_jsonlite Whether to use \pkg{jsonlite} to parse the releases info. #' @export #' @return A character vector of (GIT) tags. #' @examplesIf interactive() #' xfun::github_releases('yihui/xfun') #' xfun::github_releases('gohugoio/hugo') github_releases = function( repo, tag = '', pattern = 'v[0-9.]+', use_jsonlite = loadable('jsonlite') ) { if (tag != '') return(github_releases2(repo, tag, pattern)) i = 1; v = character() repeat { res = github_api( sprintf('/repos/%s/tags', repo), NULL, list(per_page = 100, page = i), raw = !use_jsonlite ) v2 = unlist(if (use_jsonlite) { lapply(res, `[[`, 'name') } else { m = gregexec('\\{"name":"([^"]+)",', res) lapply(regmatches(res, m), function(x) x[2, ]) }) if (length(v2) == 0) break v = c(v, v2) if (length(v2) < 100) break # not enough items for the next page i = i + 1 } grep(sprintf('^%s$', pattern), unique(v), value = TRUE) } # the fallback method to retrieve release tags (read HTML source) github_releases2 = function(repo, tag = '', pattern = '[^"&]+') { read = function() suppressWarnings( read_utf8(sprintf('https://github.com/%s/releases/%s', repo, tag)) ) h = if (tag == '') read() else tryCatch(read(), error = function(e) '') r = sprintf('^.*?%s/releases/tag/(%s)".*', repo, pattern) unique(grep_sub(r, '\\1', h)) } #' @details \code{github_api()} is a wrapper function based on #' \code{rest_api_raw()} to obtain data from the Github API: #' \url{https://docs.github.com/en/rest}. You can provide a personal access #' token (PAT) via the \code{token} argument, or via one of the environment #' variables \var{GITHUB_PAT}, \var{GITHUB_TOKEN}, \var{GH_TOKEN}. A PAT #' allows for a much higher rate limit in API calls. Without a token, you can #' only make 60 calls in an hour. #' @param raw Whether to return the raw response or parse the response with #' \pkg{jsonlite}. #' @rdname rest_api #' @export github_api = function( endpoint, token = '', params = list(), headers = NULL, raw = !loadable('jsonlite') ) { token = c(token, unname(Sys.getenv(envs <- c('GITHUB_PAT', 'GITHUB_TOKEN', 'GH_TOKEN')))) token = if (length(token <- token[token != ''])) token[1] else '' names(token) = 'token' error = TRUE on.exit(if (error && token == '') message( 'You may need to save a Github personal access token in one of the ', 'environment variables: ', paste(envs, collapse = ', ') )) res = rest_api_raw('https://api.github.com', endpoint, token, params, headers) error = FALSE if (raw) res else jsonlite::fromJSON(res, FALSE) } git = function(...) { if (Sys.which('git') == '') stop('git is not available') # R's HOME var is different from the system's HOME on Windows: # https://github.com/yihui/crandalf/issues/24 if (is_windows()) { env = set_envvar(c(HOME = Sys.getenv('USERPROFILE'))) on.exit(set_envvar(env), add = TRUE) } system2('git', ...) } git_co = function(args = NULL, ...) { git(c('checkout', args), ...) } git_test_branch = function() { if (length(d <- git(c('diff', '--name-only'), stdout = TRUE))) stop( 'The current branch has changes not stated for commit:\n', paste(d, collapse = '\n') ) } gh = function(...) { if (Sys.which('gh') == '') stop('Github CLI not found: https://cli.github.com') system2('gh', ...) } gh_run = function(..., repo = NA) { gh(c(if (!is.na(repo)) c('-R', repo), 'run', ...), stdout = TRUE) } xfun/R/markdown.R0000644000175000017500000001635314133346110013536 0ustar nileshnilesh#' Find the indices of lines in Markdown that are prose (not code blocks) #' #' Filter out the indices of lines between code block fences such as \verb{```} #' (could be three or four or more backticks). #' @param x A character vector of text in Markdown. #' @param warn Whether to emit a warning when code fences are not balanced. #' @note If the code fences are not balanced (e.g., a starting fence without an #' ending fence), this function will treat all lines as prose. #' @return An integer vector of indices of lines that are prose in Markdown. #' @export #' @examples library(xfun) #' prose_index(c('a', '```', 'b', '```', 'c')) #' prose_index(c('a', '````', '```r', '1+1', '```', '````', 'c')) prose_index = function(x, warn = TRUE) { idx = NULL; r = '^(\\s*```+).*'; s = '' for (i in setdiff(grep(r, x), grep('-->\\s*$', x))) { if (s == '') { s = gsub(r, '\\1', x[i]); idx = c(idx, i); next } # look for the next line with the same amount of backticks (end of block) if (grepl(paste0('^', s), x[i])) { idx = c(idx, i); s = '' } } xi = seq_along(x); n = length(idx) if (n == 0) return(xi) if (n %% 2 != 0) { if (warn) warning('Code fences are not balanced') # treat all lines as prose return(xi) } idx2 = matrix(idx, nrow = 2) idx2 = unlist(mapply(seq, idx2[1, ], idx2[2, ], SIMPLIFY = FALSE)) xi[-idx2] } #' Protect math expressions in pairs of backticks in Markdown #' #' For Markdown renderers that do not support LaTeX math, we need to protect #' math expressions as verbatim code (in a pair of backticks), because some #' characters in the math expressions may be interpreted as Markdown syntax #' (e.g., a pair of underscores may make text italic). This function detects #' math expressions in Markdown (by heuristics), and wrap them in backticks. #' #' Expressions in pairs of dollar signs or double dollar signs are treated as #' math, if there are no spaces after the starting dollar sign, or before the #' ending dollar sign. There should be spaces before the starting dollar sign, #' unless the math expression starts from the very beginning of a line. For a #' pair of single dollar signs, the ending dollar sign should not be followed by #' a number. With these assumptions, there should not be too many false #' positives when detecing math expressions. #' #' Besides, LaTeX environments (\verb{\begin{*}} and \verb{\end{*}}) are also #' protected in backticks. #' @param x A character vector of text in Markdown. #' @return A character vector with math expressions in backticks. #' @note If you are using Pandoc or the \pkg{rmarkdown} package, there is no #' need to use this function, because Pandoc's Markdown can recognize math #' expressions. #' @export #' @examples library(xfun) #' protect_math(c('hi $a+b$', 'hello $$\\alpha$$', 'no math here: $x is $10 dollars')) #' protect_math(c('hi $$', '\\begin{equation}', 'x + y = z', '\\end{equation}')) protect_math = function(x) { i = prose_index(x) if (length(i)) x[i] = escape_math(x[i]) x } escape_math = function(x) { # replace $x$ with `\(x\)` (protect inline math in ) m = gregexpr('(?<=^|[\\s])[$](?! )[^$]+?(?Download filename}. The file can be downloaded when #' the link is clicked in modern web browsers. For a directory, it will be #' compressed as a zip archive first, and the zip file is passed to #' \code{embed_file()}. For multiple files, they are also compressed to a zip #' file first. #' #' These functions can be called in R code chunks in R Markdown documents with #' HTML output formats. You may embed an arbitrary file or directory in the HTML #' output file, so that readers of the HTML page can download it from the #' browser. A common use case is to embed data files for readers to download. #' @param path Path to the file(s) or directory. #' @param name The default filename to use when downloading the file. Note that #' for \code{embed_dir()}, only the base name (of the zip filename) will be #' used. #' @param text The text for the hyperlink. #' @param ... For \code{embed_file()}, additional arguments to be passed to #' \code{htmltools::a()} (e.g., \code{class = 'foo'}). For \code{embed_dir()} #' and \code{embed_files()}, arguments passed to \code{embed_file()}. #' @note Windows users may need to install Rtools to obtain the \command{zip} #' command to use \code{embed_dir()} and \code{embed_files()}. #' #' These functions require R packages \pkg{mime} and \pkg{htmltools}. If you #' have installed the \pkg{rmarkdown} package, these packages should be #' available, otherwise you need to install them separately. #' #' Currently Internet Explorer does not support downloading embedded files #' (\url{https://caniuse.com/#feat=download}). Chrome has a 2MB limit on the #' file size. #' @return An HTML tag \samp{} with the appropriate attributes. #' @export #' @examples #' logo = xfun:::R_logo() #' link = xfun::embed_file(logo, text = 'Download R logo') #' link #' if (interactive()) htmltools::browsable(link) embed_file = function(path, name = basename(path), text = paste('Download', name), ...) { h = paste0("data:", mime::guess_type(path), ";base64,", base64_encode(path)) htmltools::a(text, href = h, download = name, ...) } #' @rdname embed_file #' @export embed_dir = function(path, name = paste0(normalize_path(path), '.zip'), ...) { name = gsub('/', '', basename(name)) in_dir(path, { name = file.path(tempdir(), name); on.exit(file.remove(name), add = TRUE) zip(name, '.'); embed_file(name, ...) }) } #' @rdname embed_file #' @export embed_files = function(path, name = with_ext(basename(path[1]), '.zip'), ...) { name = file.path(tempdir(), basename(name)) on.exit(file.remove(name), add = TRUE) zip(name, path) embed_file(name, ...) } zip = function(name, ...) { if (utils::zip(name, ...) != 0) stop('Failed to create the zip archive ', name) invisible(0) } xfun/R/session.R0000644000175000017500000001220214133423044013366 0ustar nileshnilesh#' An alternative to sessionInfo() to print session information #' #' This function tweaks the output of \code{\link{sessionInfo}()}: (1) It adds #' the RStudio version information if running in the RStudio IDE; (2) It removes #' the information about matrix products, BLAS, and LAPACK; (3) It removes the #' names of base R packages; (4) It prints out package versions in a single #' group, and does not differentiate between loaded and attached packages. #' #' It also allows you to only print out the versions of specified packages (via #' the \code{packages} argument) and optionally their recursive dependencies. #' For these specified packages (if provided), if a function #' \code{xfun_session_info()} exists in a package, it will be called and #' expected to return a character vector to be appended to the output of #' \code{session_info()}. This provides a mechanism for other packages to inject #' more information into the \code{session_info} output. For example, #' \pkg{rmarkdown} (>= 1.20.2) has a function \code{xfun_session_info()} that #' returns the version of Pandoc, which can be very useful information for #' diagnostics. #' @param packages A character vector of package names, of which the versions #' will be printed. If not specified, it means all loaded and attached #' packages in the current R session. #' @param dependencies Whether to print out the versions of the recursive #' dependencies of packages. #' @return A character vector of the session information marked as #' \code{\link{raw_string}()}. #' @export #' @examplesIf interactive() #' xfun::session_info() #' if (xfun::loadable('MASS')) xfun::session_info('MASS') session_info = function(packages = NULL, dependencies = TRUE) { res = sessionInfo() res$matprod = res$BLAS = res$LAPACK = NULL if (loadable('rstudioapi') && rstudioapi::isAvailable()) { res$running = paste0(res$running, ', RStudio ', rstudioapi::getVersion()) } tweak_info = function(obj, extra = NULL) { res = capture.output(print(obj)) i = grep('^(attached base packages|Matrix products):\\s*$', res, ignore.case = TRUE) if (length(i)) res = res[-c(i, i + 1)] res = gsubi('^\\s*locale:\\s*$', 'Locale:', res) res = gsub('^\\s*\\[[0-9]+]\\s*', ' ', res) # remove vector indices like [1] res = gsubi('^\\s*other attached packages:\\s*$', 'Package version:', res) # print the locale info on a single line if possible if (length(i <- which(res == 'Locale:')) == 1 && res[i + 2] == '') { res[i] = paste(res[i], gsub('\\s*/\\s*', ' / ', gsub('^\\s+', '', res[i + 1]))) res = res[-(i + 1)] } raw_string(c(res, extra)) } version_info = function(pkgs) { res = lapply(pkgs, function(p) { list(Version = as.character(packageVersion(p)), Package = p) }) as.list(setNames(res, pkgs)) } res$basePkgs = raw_string(list()) info = c(res$otherPkgs, res$loadedOnly) if (length(packages) > 0) { info = info[intersect(names(info), packages)] info = c(info, version_info(setdiff(packages, names(info)))) } res$loadedOnly = NULL if (dependencies) { deps = pkg_dep(names(info), installed.packages(), recursive = TRUE) deps = sort(setdiff(deps, names(info))) info = c(info, version_info(deps)) } if (length(packages) > 0 || dependencies) info = info[sort(names(info))] res$otherPkgs = info extra = unlist(lapply(packages, function(p) tryCatch( c('', getFromNamespace('xfun_session_info', p)()), error = function(e) NULL) )) tweak_info(res, extra) } #' Perform a task once in an R session #' #' Perform a task once in an R session, e.g., emit a message or warning. Then #' give users an optional hint on how not to perform this task at all. #' @param task Any R code expression to be evaluated once to perform a task, #' e.g., \code{warning('Danger!')} or \code{message('Today is ', Sys.Date())}. #' @param option An R option name. This name should be as unique as possible in #' \code{\link{options}()}. After the task has been successfully performed, #' this option will be set to \code{FALSE} in the current R session, to #' prevent the task from being performed again the next time when #' \code{do_once()} is called. #' @param hint A character vector to provide a hint to users on how not to #' perform the task or see the message again in the current R session. Set #' \code{hint = ""} if you do not want to provide the hint. #' @return The value returned by the \code{task}, invisibly. #' @export #' @examples #' do_once(message("Today's date is ", Sys.Date()), "xfun.date.reminder") #' # if you run it again, it will not emit the message again #' do_once(message("Today's date is ", Sys.Date()), "xfun.date.reminder") #' #' do_once({Sys.sleep(2); 1 + 1}, "xfun.task.1plus1") #' do_once({Sys.sleep(2); 1 + 1}, "xfun.task.1plus1") do_once = function(task, option, hint = c( 'You will not see this message again in this R session.', 'If you never want to see this message,', sprintf('you may set options(%s = FALSE) in your .Rprofile.', option) )) { if (isFALSE(getOption(option))) return(invisible()) task hint = paste(hint, collapse = ' ') if (hint != '') message(hint) options(setNames(list(FALSE), option)) invisible(task) } xfun/R/packages.R0000644000175000017500000003362414133362673013506 0ustar nileshnilesh#' Attach or load packages, and automatically install missing packages if #' requested #' #' \code{pkg_attach()} is a vectorized version of \code{\link{library}()} over #' the \code{package} argument to attach multiple packages in a single function #' call. \code{pkg_load()} is a vectorized version of #' \code{\link{requireNamespace}()} to load packages (without attaching them). #' The functions \code{pkg_attach2()} and \code{pkg_load2()} are wrappers of #' \code{pkg_attach(install = TRUE)} and \code{pkg_load(install = TRUE)}, #' respectively. \code{loadable()} is an abbreviation of #' \code{requireNamespace(quietly = TRUE)}. \code{pkg_available()} tests if a #' package with a minimal version is available. #' #' These are convenience functions that aim to solve these common problems: (1) #' We often need to attach or load multiple packages, and it is tedious to type #' several \code{library()} calls; (2) We are likely to want to install the #' packages when attaching/loading them but they have not been installed. #' @param ... Package names (character vectors, and must always be quoted). #' @param install Whether to automatically install packages that are not #' available using \code{\link{install.packages}()}. Besides \code{TRUE} and #' \code{FALSE}, the value of this argument can also be a function to install #' packages (\code{install = TRUE} is equivalent to \code{install = #' install.packages}), or a character string \code{"pak"} (equivalent to #' \code{install = pak::pkg_install}, which requires the \pkg{pak} package). #' You are recommended to set a CRAN mirror in the global option \code{repos} #' via \code{\link{options}()} if you want to automatically install packages. #' @param message Whether to show the package startup messages (if any startup #' messages are provided in a package). #' @return \code{pkg_attach()} returns \code{NULL} invisibly. \code{pkg_load()} #' returns a logical vector, indicating whether the packages can be loaded. #' @seealso \code{pkg_attach2()} is similar to \code{pacman::p_load()}, but does #' not allow non-standard evaluation (NSE) of the \code{...} argument, i.e., #' you must pass a real character vector of package names to it, and all names #' must be quoted. Allowing NSE adds too much complexity with too little gain #' (the only gain is that it saves your effort in typing two quotes). #' @import utils #' @export #' @examples library(xfun) #' pkg_attach('stats', 'graphics') #' # pkg_attach2('servr') # automatically install servr if it is not installed #' #' (pkg_load('stats', 'graphics')) pkg_attach = function( ..., install = FALSE, message = getOption('xfun.pkg_attach.message', TRUE) ) { if (!message) library = function(...) { suppressPackageStartupMessages(base::library(...)) } for (i in c(...)) { if (!isFALSE(install) && !loadable(i)) pkg_install(i, install) library(i, character.only = TRUE) } } #' @param error Whether to signal an error when certain packages cannot be loaded. #' @rdname pkg_attach #' @export pkg_load = function(..., error = TRUE, install = FALSE) { n = length(pkg <- c(...)); res = logical(n) if (n == 0) return(invisible(res)) for (i in seq_len(n)) { res[i] = loadable(p <- pkg[i]) if (!isFALSE(install) && !res[i]) { pkg_install(p, install); res[i] = loadable(p) } } if (error && any(!res)) stop('Package(s) not loadable: ', paste(pkg[!res], collapse = ' ')) invisible(res) } #' @param pkg A single package name. #' @param strict If \code{TRUE}, use \code{\link{requireNamespace}()} to test if #' a package is loadable; otherwise only check if the package is in #' \code{\link{.packages}(TRUE)} (this does not really load the package, so it #' is less rigorous but on the other hand, it can keep the current R session #' clean). #' @param new_session Whether to test if a package is loadable in a new R #' session. Note that \code{new_session = TRUE} implies \code{strict = TRUE}. #' @rdname pkg_attach #' @export loadable = function(pkg, strict = TRUE, new_session = FALSE) { if (length(pkg) != 1L) stop("'pkg' must be a character vector of length one") if (new_session) { Rscript(c('-e', shQuote(sprintf('library("%s")', pkg))), stdout = FALSE, stderr = FALSE) == 0 } else { if (strict) { suppressPackageStartupMessages(requireNamespace(pkg, quietly = TRUE)) } else pkg %in% .packages(TRUE) } } #' @param version A minimal version number. If \code{NULL}, only test if a #' package is available and do not check its version. #' @rdname pkg_attach #' @export pkg_available = function(pkg, version = NULL) { loadable(pkg) && (is.null(version) || packageVersion(pkg) >= version) } #' @rdname pkg_attach #' @export pkg_attach2 = function(...) pkg_attach(..., install = TRUE) #' @rdname pkg_attach #' @export pkg_load2 = function(...) pkg_load(..., install = TRUE) pkg_update = function(...) { update.packages(ask = FALSE, checkBuilt = TRUE, ...) } # allow users to specify a custom install.packages() function via the global # option xfun.install.packages pkg_install = function(pkgs, install = TRUE, ...) { if (length(pkgs) == 0) return() # in case the CRAN repo is not set up repos = getOption('repos') if (length(repos) == 0 || identical(repos, c(CRAN = '@CRAN@'))) { opts = options(repos = c(CRAN = 'https://cran.rstudio.com')) on.exit(options(opts), add = TRUE) } if (length(pkgs) > 1) message('Installing ', length(pkgs), ' packages: ', paste(pkgs, collapse = ' ')) if (isTRUE(install)) install = getOption( 'xfun.install.packages', if (is.na(Sys.getenv('RENV_PROJECT', NA)) || !loadable('renv')) install.packages else { function(pkgs, lib = NULL, ...) renv::install(pkgs, library = lib, ...) } ) if (identical(install, 'pak')) install = pak::pkg_install retry(install, pkgs, ..., .pause = 0) } #' Find out broken packages and reinstall them #' #' If a package is broken (i.e., not \code{\link{loadable}()}), reinstall it. #' #' Installed R packages could be broken for several reasons. One common reason #' is that you have upgraded R to a newer \code{x.y} version, e.g., from #' \code{4.0.5} to \code{4.1.0}, in which case you need to reinstall previously #' installed packages. #' @param reinstall Whether to reinstall the broken packages, or only list their #' names. #' @return A character vector of names of broken package. #' @export broken_packages = function(reinstall = TRUE) { libs = .libPaths() pkgs = unlist(lapply(libs, function(lib) { p = unlist(lapply(.packages(TRUE, lib), function(p) { if (!loadable(p, new_session = TRUE)) p })) if (length(p) && reinstall) { remove.packages(p, lib); pkg_install(p, lib = lib) } p })) if(reinstall) invisible(pkgs) else pkgs } # remove (binary) packages that were built with a previous major-minor version of R check_built = function(dir = '.', dry_run = TRUE) { ext = if (xfun::is_macos()) 'tgz' else if (xfun::is_windows()) 'zip' else 'tar.gz' r = paste0('_[-.0-9]+[.]', ext, '$') pkgs = list.files(dir, r, full.names = TRUE) meta = file.path(dir, 'PACKAGES') info = if (file_exists(meta)) read.dcf(meta) extract = if (grepl('gz$', ext)) untar else unzip for (f in pkgs) { d = file.path(gsub(r, '', basename(f)), 'DESCRIPTION') extract(f, d) if (is.na(b <- read.dcf(d, 'Built')[1, 1])) next unlink(dirname(d), recursive = TRUE) v = as.numeric_version(gsub('^\\s*R ([^;]+);.*', '\\1', b)) if (major_minor_smaller(v, getRversion())) { message('The package ', f, ' was built with R ', v) if (!dry_run) file.remove(f) } } if (!is.null(info) && !dry_run) tools::write_PACKAGES(dir) } # is one version smaller than the other in major.minor? e.g., 4.1.0 is smaller # than 4.2.0, but not smaller than 4.1.1 major_minor_smaller = function(v1, v2) { v1 = unclass(v1)[[1]] v2 = unclass(v2)[[1]] if (length(v1) < 3 || length(v2) < 3) return(TRUE) # should return NA v1[1] < v2[1] || v1[2] < v2[2] } #' Install a source package from a directory #' #' Run \command{R CMD build} to build a tarball from a source directory, and run #' \command{R CMD INSTALL} to install it. #' @param pkg The package source directory. #' @param build Whether to build a tarball from the source directory. If #' \code{FALSE}, run \command{R CMD INSTALL} on the directory directly (note #' that vignettes will not be automatically built). #' @param build_opts The options for \command{R CMD build}. #' @param install_opts The options for \command{R CMD INSTALL}. #' @export #' @return Invisible status from \command{R CMD INSTALL}. install_dir = function(pkg, build = TRUE, build_opts = NULL, install_opts = NULL) { if (build) { pkg = pkg_build(pkg, build_opts) on.exit(unlink(pkg), add = TRUE) } res = Rcmd(c('INSTALL', install_opts, pkg)) if (res != 0) stop('Failed to install the package ', pkg) invisible(res) } pkg_build = function(dir = '.', opts = NULL) { desc = file.path(dir, 'DESCRIPTION') pv = read.dcf(desc, fields = c('Package', 'Version')) # delete existing tarballs unlink(sprintf('%s_*.tar.gz', pv[1, 1])) Rcmd(c('build', opts, shQuote(dir))) pkg = sprintf('%s_%s.tar.gz', pv[1, 1], pv[1, 2]) if (!file_exists(pkg)) stop('Failed to build the package ', pkg) pkg } # query the Homebrew dependencies of an R package brew_dep = function(pkg) { u = sprintf('https://sysreqs.r-hub.io/pkg/%s/osx-x86_64-clang', pkg) x = retry(readLines, u, warn = FALSE) x = gsub('^\\s*\\[|\\]\\s*$', '', x) x = unlist(strsplit(gsub('"', '', x), '[, ]+')) x = setdiff(x, 'null') if (length(x)) message('Package ', pkg, ' requires Homebrew packages: ', paste(x, collapse = ' ')) x } brew_deps = function(pkgs) { if (length(pkgs) == 0) return() deps = pkg_brew_deps() unlist(lapply(pkgs, function(p) { if (is.null(deps[[p]])) brew_dep(p) else deps[[p]] })) } pkg_brew_deps = function() { con = url('https://macos.rbind.io/bin/macosx/sysreqsdb.rds') on.exit(close(con), add = TRUE) readRDS(con) } install_brew_deps = function(pkg = .packages(TRUE)) { inst = installed.packages() pkg = intersect(pkg, pkg_needs_compilation(inst)) deps = pkg_brew_deps() deps = deps[c(pkg, pkg_dep(pkg, inst, recursive = TRUE))] deps = paste(na.omit(unique(unlist(deps))), collapse = ' ') if (deps != '') system(paste('brew install', deps)) } pkg_needs_compilation = function(db = installed.packages()) { pkgs = unname(db[tolower(db[, 'NeedsCompilation']) == 'yes', 'Package']) pkgs[!is.na(pkgs)] } #' An alias of \code{remotes::install_github()} #' #' This alias is to make autocomplete faster via \code{xfun::install_github}, #' because most \code{remotes::install_*} functions are never what I want. I #' only use \code{install_github} and it is inconvenient to autocomplete it, #' e.g. \code{install_git} always comes before \code{install_github}, but I #' never use it. In RStudio, I only need to type \code{xfun::ig} to get #' \code{xfun::install_github}. #' @param ... Arguments to be passed to #' \code{remotes::\link[remotes]{install_github}()}. #' @export install_github = function(...) remotes::install_github(...) # Remove packages not installed from CRAN reinstall_from_cran = function(dry_run = TRUE, skip_github = TRUE) { r = paste(c('Repository', if (skip_github) 'GithubRepo'), collapse = '|') r = paste0('^(', r, '): ') for (lib in .libPaths()) { pkgs = .packages(TRUE, lib) pkgs = setdiff(pkgs, c('xfun', 'rstudio', base_pkgs())) for (p in pkgs) { desc = read_utf8(system.file('DESCRIPTION', package = p, lib.loc = lib)) if (!any(grepl(r, desc))) { if (dry_run) message(p, ': ', lib) else install.packages(p, lib = lib) } } } } #' Convert package news to the Markdown format #' #' Read the package news with \code{\link{news}()}, convert the result to #' Markdown, and write to an output file (e.g., \file{NEWS.md}). Each package #' version appears in a first-level header, each category (e.g., \samp{NEW #' FEATURES} or \samp{BUG FIXES}) is in a second-level header, and the news #' items are written into bullet lists. #' @param package,... Arguments to be passed to \code{\link{news}()}. #' @param output The output file path. #' @param category Whether to keep the category names. #' @return If \code{output = NA}, returns the Markdown content as a character #' vector, otherwise the content is written to the output file. #' @export #' @examples #' # news for the current version of R #' xfun::news2md('R', Version == getRversion(), output = NA) news2md = function(package, ..., output = 'NEWS.md', category = TRUE) { db = news(package = package, ...) k = db[, 'Category'] db[is.na(k), 'Category'] = '' # replace NA category with '' res = unlist(lapply(unique(db[, 'Version']), function(v) { d1 = db[db[, 'Version'] == v, ] res = unlist(lapply(unique(d1[, 'Category']), function(k) { txt = d1[d1[, 'Category'] == k, 'Text'] txt = txt[txt != ''] if (k == '' && length(txt) == 0) return() txt = gsub('\n *', ' ', txt) c(if (category && k != '') paste('##', k), if (length(txt)) paste('-', txt)) })) if (is.na(dt <- d1[1, 'Date'])) dt = '' else dt = paste0(' (', dt, ')') c(sprintf('# CHANGES IN %s VERSION %s%s', package, v, dt), res) })) res = c(rbind(res, '')) # add a blank line after each line if (is.na(output)) raw_string(res) else write_utf8(res, output) } #' Get base R package names #' #' Return names of packages from \code{\link{installed.packages}()} of which the #' priority is \code{"base"}. #' @return A character vector of base R package names. #' @export #' @examplesIf interactive() #' xfun::base_pkgs() base_pkgs = function() rownames(installed.packages(priority = 'base')) # update one package (from source by default) pkg_update_one = function(pkg, type = 'source') { opts = options(repos = c(CRAN = 'https://cran.r-project.org')) on.exit(options(opts), add = TRUE) if (is.null(pkgs <- old.packages(type = type)) || !pkg %in% rownames(pkgs)) return() install.packages(pkg, pkgs[pkg, 'LibPath'], type = type, INSTALL_opts = '--no-staged-install') NULL } xfun/R/revcheck.R0000644000175000017500000007125314156156426013524 0ustar nileshnilesh#' Run \command{R CMD check} on the reverse dependencies of a package #' #' Install the source package, figure out the reverse dependencies on CRAN, #' download all of their source packages, and run \command{R CMD check} on them #' in parallel. #' #' Everything occurs under the current working directory, and you are #' recommended to call this function under a designated directory, especially #' when the number of reverse dependencies is large, because all source packages #' will be downloaded to this directory, and all \file{*.Rcheck} directories #' will be generated under this directory, too. #' #' If a source tarball of the expected version has been downloaded before (under #' the \file{tarball} directory), it will not be downloaded again (to save time #' and bandwidth). #' #' After a package has been checked, the associated \file{*.Rcheck} directory #' will be deleted if the check was successful (no warnings or errors or notes), #' which means if you see a \file{*.Rcheck} directory, it means the check #' failed, and you need to take a look at the log files under that directory. #' #' The time to finish the check is recorded for each package. As the check goes #' on, the total remaining time will be roughly estimated via \code{n * #' mean(times)}, where \code{n} is the number of packages remaining to be #' checked, and \code{times} is a vector of elapsed time of packages that have #' been checked. #' #' If a check on a reverse dependency failed, its \file{*.Rcheck} directory will #' be renamed to \file{*.Rcheck2}, and another check will be run against the #' CRAN version of the package unless \code{options(xfun.rev_check.compare = #' FALSE)} is set. If the logs of the two checks are the same, it means no new #' problems were introduced in the package, and you can probably ignore this #' particular reverse dependency. The function \code{compare_Rcheck()} can be #' used to create a summary of all the differences in the check logs under #' \file{*.Rcheck} and \file{*.Rcheck2}. This will be done automatically if #' \code{options(xfun.rev_check.summary = TRUE)} has been set. #' #' A recommended workflow is to use a special directory to run #' \code{rev_check()}, set the global \code{\link{options}} #' \code{xfun.rev_check.src_dir} and \code{repos} in the R startup (see #' \code{?\link{Startup}}) profile file \code{.Rprofile} under this directory, #' and (optionally) set \code{R_LIBS_USER} in \file{.Renviron} to use a special #' library path (so that your usual library will not be cluttered). Then run #' \code{xfun::rev_check(pkg)} once, investigate and fix the problems or (if you #' believe it was not your fault) ignore broken packages in the file #' \file{00ignore}, and run \code{xfun::rev_check(pkg)} again to recheck the #' failed packages. Repeat this process until all \file{*.Rcheck} directories #' are gone. #' #' As an example, I set \code{options(repos = c(CRAN = #' 'https://cran.rstudio.com'), xfun.rev_check.src_dir = '~/Dropbox/repo')} in #' \file{.Rprofile}, and \code{R_LIBS_USER=~/R-tmp} in \file{.Renviron}. Then I #' can run, for example, \code{xfun::rev_check('knitr')} repeatedly under a #' special directory \file{~/Downloads/revcheck}. Reverse dependencies and their #' dependencies will be installed to \file{~/R-tmp}, and \pkg{knitr} will be #' installed from \file{~/Dropbox/repo/kintr}. #' @param pkg The package name. #' @param which Which types of reverse dependencies to check. See #' \code{tools::\link[tools]{package_dependencies}()} for possible values. The #' special value \code{'hard'} means the hard dependencies, i.e., #' \code{c('Depends', 'Imports', 'LinkingTo')}. #' @param recheck A vector of package names to be (re)checked. If not provided #' and there are any \file{*.Rcheck} directories left by certain packages #' (this often means these packages failed the last time), \code{recheck} will #' be these packages; if there are no \file{*.Rcheck} directories but a text #' file \file{recheck} exists, \code{recheck} will be the character vector #' read from this file. This provides a way for you to manually specify the #' packages to be checked. If there are no packages to be rechecked, all #' reverse dependencies will be checked. #' @param ignore A vector of package names to be ignored in \command{R CMD #' check}. If this argument is missing and a file \file{00ignore} exists, the #' file will be read as a character vector and passed to this argument. #' @param update Whether to update all packages before the check. #' @param src The path of the source package directory. #' @param src_dir The parent directory of the source package directory. This can #' be set in a global option if all your source packages are under a common #' parent directory. #' @param timeout Timeout in seconds for \command{R CMD check} to check each #' package. The (approximate) total time can be limited by the global option #' \code{xfun.rev_check.timeout_total}. #' @return A named numeric vector with the names being package names of reverse #' dependencies; \code{0} indicates check success, \code{1} indicates failure, #' and \code{2} indicates that a package was not checked due to global #' timeout. #' @seealso \code{devtools::revdep_check()} is more sophisticated, but currently #' has a few major issues that affect me: (1) It always deletes the #' \file{*.Rcheck} directories #' (\url{https://github.com/r-lib/devtools/issues/1395}), which makes it #' difficult to know more information about the failures; (2) It does not #' fully install the source package before checking its reverse dependencies #' (\url{https://github.com/r-lib/devtools/pull/1397}); (3) I feel it is #' fairly difficult to iterate the check (ignore the successful packages and #' only check the failed packages); by comparison, \code{xfun::rev_check()} #' only requires you to run a short command repeatedly (failed packages are #' indicated by the existing \file{*.Rcheck} directories, and automatically #' checked again the next time). #' #' \code{xfun::rev_check()} borrowed a very nice feature from #' \code{devtools::revdep_check()}: estimating and displaying the remaining #' time. This is particularly useful for packages with huge numbers of reverse #' dependencies. #' @export rev_check = function( pkg, which = 'all', recheck = NULL, ignore = NULL, update = TRUE, timeout = getOption('xfun.rev_check.timeout', 15 * 60), src = file.path(src_dir, pkg), src_dir = getOption('xfun.rev_check.src_dir') ) { if (length(src) != 1 || !dir_exists(src)) stop( 'The package source dir (the "src" argument) must be an existing directory' ) message('Installing the source package ', src) install_dir(path.expand(src)) db = available.packages(type = 'source') # install packages that are not loadable (testing in parallel) p_install = function(pkgs) { pkgs_up = NULL if (update) { message('Updating all R packages...') pkgs_up = intersect(old.packages(checkBuilt = TRUE)[, 'Package'], pkgs) pkg_install(pkgs_up) } pkgs = setdiff(pkgs, pkgs_up) # don't install pkgs that were just updated print(system.time( pkg_install(unlist(plapply(pkgs, function(p) if (!loadable(p, new_session = TRUE)) p))) )) } unlink('*.Rcheck2', recursive = TRUE) if (missing(recheck)) { dirs = list.files('.', '.+[.]Rcheck$') pkgs = gsub('.Rcheck$', '', dirs) recheck = if (length(pkgs) == 0 && file_exists('recheck')) { scan('recheck', 'character') } else pkgs } pkgs = if (length(recheck)) { p_install(pkg_dep(recheck, db, which = 'all')) recheck } else { res = check_deps(pkg, db, which) message('Installing dependencies of reverse dependencies') res$install = setdiff(res$install, ignore_deps()) print(system.time(p_install(res$install))) res$check } pkgs = intersect(pkgs, rownames(db)) # make sure the pkgs are on CRAN lib_cran = './library-cran' on.exit(unlink(lib_cran, recursive = TRUE), add = TRUE) dir.create(lib_cran, showWarnings = FALSE) pkg_install(pkg, lib = lib_cran) # the CRAN version of the package f = tempfile('check-done', fileext = '.rds') l = tempfile('check-lock'); on.exit(unlink(c(f, l)), add = TRUE) n = length(pkgs) if (n == 0) { message('No reverse dependencies to be checked for the package ', pkg); return() } if (missing(ignore) && file_exists('00ignore')) ignore = scan('00ignore', 'character') if (length(ignore)) { message('Ignoring packages: ', paste(ignore, collapse = ' ')) unlink(sprintf('%s.Rcheck', ignore), recursive = TRUE) pkgs = setdiff(pkgs, ignore) if ((n <- length(pkgs)) == 0) { message('No packages left to be checked'); return() } } message('Downloading tarballs') tars = download_tarball(pkgs, db, dir = 'tarball') tars = setNames(tars, pkgs) t0 = Sys.time() tt = getOption('xfun.rev_check.timeout_total', Inf) message('Checking ', n, ' packages: ', paste(pkgs, collapse = ' ')) res = plapply(pkgs, function(p) { d = sprintf('%s.Rcheck', p) if (!p %in% rownames(db)) { message('Checking ', p, ' (aborted since it is no longer on CRAN') unlink(d, recursive = TRUE) return() } timing = function() { # in case two packages finish at exactly the same time while (file_exists(l)) Sys.sleep(.1) file.create(l); on.exit(unlink(l), add = TRUE) done = c(if (file_exists(f)) readRDS(f), p) saveRDS(done, f) n2 = length(setdiff(pkgs, done)) # remaining packages t1 = Sys.time(); t2 = Sys.time() + n2 * (t1 - t0) / (n - n2) message( 'Packages remaining: ', n2, '/', n, '; Expect to finish at ', t2, ' (', format(round(difftime(t2, t1))), ')' ) # 0 (FALSE): success; 1: failure setNames(as.integer(dir_exists(d)), p) } if (!file_exists(z <- tars[p])) { dir.create(d, showWarnings = FALSE) return(timing()) } # timeout; package not checked if (difftime(Sys.time(), t0, units = 'secs') > tt) { return(setNames(2L, p)) } check_it = function(args = NULL, ...) { system2( file.path(R.home('bin'), 'R'), c(args, 'CMD', 'check', '--no-manual', shQuote(z)), stdout = FALSE, stderr = FALSE, timeout = timeout, ... ) } check_it() if (!clean_Rcheck(d)) { if (!dir_exists(d)) {dir.create(d); return(timing())} # try to install missing LaTeX packages for vignettes if possible, then recheck vigs = list.files( file.path(d, 'vign_test', p, 'vignettes'), '[.](Rnw|Rmd)$', ignore.case = TRUE, full.names = TRUE ) pkg_load2('tinytex') if (length(vigs) && any(file_exists(with_ext(vigs, 'log')))) { if (tinytex::is_tinytex()) for (vig in vigs) in_dir(dirname(vig), { Rscript(shQuote(c('-e', 'if (grepl("[.]Rnw$", f <- commandArgs(T), ignore.case = T)) knitr::knit2pdf(f) else rmarkdown::render(f)', basename(vig)))) }) check_it() if (clean_Rcheck(d)) return(timing()) } # if there are still missing LaTeX packages, install them and recheck l0 = tinytex::tl_pkgs() lapply( list.files(d, '[.]log$', full.names = TRUE, recursive = TRUE), tinytex::parse_install, quiet = TRUE ) if (!identical(l0, tinytex::tl_pkgs())) { check_it() if (clean_Rcheck(d)) return(timing()) } # clean up the check log, and recheck with the current CRAN version of pkg cleanup = function() in_dir(d, { clean_log() # so that I can easily preview it in the Finder on macOS file_exists('00install.out') && file.rename('00install.out', '00install.log') }) # ignore vignettes that failed to build for unknown reasons cleanup() if (clean_Rcheck(d)) return(timing()) # whether to check the package against the CRAN version? if (!getOption('xfun.rev_check.compare', TRUE)) return(timing()) file.rename(d, d2 <- paste0(d, '2')) check_it('--no-environ', env = tweak_r_libs(lib_cran)) if (!dir_exists(d)) file.rename(d2, d) else { cleanup() if (identical_logs(c(d, d2))) unlink(c(d, d2), recursive = TRUE) } } timing() }) if (getOption('xfun.rev_check.summary', FALSE)) { html = compare_Rcheck(); if (isTRUE(grepl('[.]html$', html))) browseURL(html) } unlist(res) } # remove the OK lines in the check log clean_log = function() { if (!file_exists(l <- '00check.log')) return() x = grep('^[*].+OK$', read_utf8(l), invert = TRUE, value = TRUE) # don't want diffs in random tempdir/tempfile paths when comparing check logs x[grep(dirname(tempdir()), x, fixed = TRUE)] = 'RANDOM TEMPDIR/TEMPFILE PATH DELETED' # delete the download progress x = grep('^\\s*\\[\\d+%] Downloaded \\d+ bytes...\\s*$', x, invert = TRUE, value = TRUE) # delete lines of the form "address 0x1067143eb, cause 'illegal opcode'" x = grep("address 0x[[:xdigit:]]+, cause '[^']+'", x, invert = TRUE, value = TRUE) x = recheck_vig(x) x = tail(x, -2) writeLines(x, l) # remove the first 2 lines (log dir name and R version) x } # sometimes R CMD check fails to build vignettes for unknown reasons; try to # recheck the package in this case recheck_vig = function(x) { if (!any(i1 <- (x == '* checking re-building of vignette outputs ... WARNING'))) return(x) i1 = which(i1)[1] i2 = which(x == 'Execution halted') i2 = i2[i2 > i1] if (length(i2) == 0) return(x) i3 = grep('^[*] checking ', x) # next checking item i3 = i3[i3 > i1] if (length(i3)) { i2 = i2[i2 < i3[1]] # 'Execution halted' needs to appear before next '* checking' if (length(i2) == 0) return(x) } # if no explicit errors were found in processing vignettes, remove the relevant log i2 = tail(i2, 1) if (length(grep('Error: processing vignette .+ failed with diagnostics:', x[i1:i2])) == 0) x = x[-(i1:i2)] x } # are the check logs identical under a series of *.Rcheck directories? identical_logs = function(dirs) { if (length(dirs) < 2) return(FALSE) if (!all(file_exists(logs <- file.path(dirs, '00check.log')))) return(FALSE) x = read_utf8(logs[1]) for (i in 2:length(dirs)) if (!identical(x, read_utf8(logs[i]))) return(FALSE) TRUE } # delete files/dirs that are usually not helpful clean_Rcheck2 = function(dir = '.') { owd = setwd(dir); on.exit(setwd(owd), add = TRUE) ds = list.files('.', '.+[.]Rcheck$') for (d in c(ds, paste0(ds, '2'))) { f1 = list.files(d, full.names = TRUE) f2 = file.path(d, c('00_pkg_src', '00check.log', '00install.log'), fsep = '/') unlink(setdiff(f1, f2), recursive = TRUE) } } # add a new library path to R_LIBS_USER tweak_r_libs = function(new) { x = read_all(existing_files(c('~/.Renviron', '.Renviron'))) x = grep('^\\s*#', x, invert = TRUE, value = TRUE) x = gsub('^\\s+|\\s+$', '', x) x = x[x != ''] i = grep('^R_LIBS_USER=.+', x) if (length(i)) { x[i[1]] = sub('(="?)', path_sep('\\1', new), x[i[1]]) x } else { v = Sys.getenv('R_LIBS_USER') v = if (v == '') new else path_sep(new, v) c(paste0('R_LIBS_USER=', v), x) } } # separate paths by the path separator on a specific platform path_sep = function(...) paste(..., sep = .Platform$path.sep) # a shorthand of tools::package_dependencies() pkg_dep = function(x, ...) { if (length(x)) unique(unlist(tools::package_dependencies(x, ...))) } # calculate the packages required to check a package check_deps = function(x, db = available.packages(), which = 'all') { if (identical(which, 'hard')) which = c('Depends', 'Imports', 'LinkingTo') x0 = db[, 'Package'] # all available packages # packages that reverse depend on me x1 = pkg_dep(x, db, which, reverse = TRUE) x1 = intersect(x1, x0) # only check a sample of soft reverse dependencies (useful if there are too many) if (identical(which, 'all') && (n <- getOption('xfun.rev_check.sample', 100)) >= 0) { x2 = pkg_dep(x, db, c('Suggests', 'Enhances'), reverse = TRUE) x2 = intersect(x2, x0) if (n < length(x2)) x1 = c(setdiff(x1, x2), sample(x2, n)) } # to R CMD check x1, I have to install all their dependencies x2 = pkg_dep(x1, db, 'all') # and for those dependencies, I have to install the default dependencies x3 = pkg_dep(x2, db, recursive = TRUE) list(check = x1, install = intersect(c(x1, x2, x3), x0)) } #' Submit check jobs to crandalf #' #' Check the reverse dependencies of a package using the crandalf service: #' \url{https://github.com/yihui/crandalf}. If the number of reverse #' dependencies is large, they will be split into batches and pushed to crandalf #' one by one. #' #' Due to the time limit of a single job on Github Actions (6 hours), you will #' have to split the large number of reverse dependencies into batches and check #' them sequentially on Github (at most 5 jobs in parallel). The function #' \code{crandalf_check()} does this automatically when necessary. It requires #' the \command{git} command to be available. #' #' The function \code{crandalf_results()} fetches check results from Github #' after all checks are completed, merge the results, and show a full summary of #' check results. It requires \code{gh} (Github CLI: #' \url{https://cli.github.com/manual/}) to be installed and you also need to #' authenticate with your Github account beforehand. #' @param pkg The package name of which the reverse dependencies are to be #' checked. #' @param size The number of reverse dependencies to be checked in each job. #' @param jobs The number of jobs to run in Github Actions (by default, all jobs #' are submitted, but you can choose to submit the first few jobs). #' @param which The type of dependencies (see \code{\link{rev_check}()}). #' @export crandalf_check = function(pkg, size = 400, jobs = Inf, which = 'all') { git_test_branch() git_co('main') on.exit(git_co('main'), add = TRUE) git_test_branch() # do everything inside the check-pkg branch b = paste0('check-', pkg) if (git_co(b, stderr = FALSE) != 0) { git_co(c('-b', b)) writeLines('# placeholder', 'recheck') git(c('add', 'recheck')) git(c('commit', '-m', shQuote(paste('Revcheck', pkg)))) git('push') message( 'Please create a pull request from the branch ', b, ' on Github and re-run xfun::crandalf_check("', pkg, '").' ) return(invisible()) } git(c('merge', 'main')) x = check_deps(pkg, which = which)$check n = length(x) if (n <= size) { message('No need to split ', n, ' reverse dependencies into batches of size ', size, '.') if (any(grepl('Your branch is ahead of ', git('status', stdout = TRUE)))) { git('push') } else if (Sys.which('gh') != '') { gh(c('workflow', 'run', 'rev-check.yaml', '--ref', b)) message('Triggering rev-check.yaml job against ', b, ' branch in crandalf repo on Github.') } else { message('Remember to re-run the last job for the package ', pkg, ' on Github.') } return(invisible()) } b = ceiling(n/size) i = rep(seq_len(b), each = size)[seq_len(n)] k = 1 # use an id in the commit so that I know which jobs are for the same pkg id = format(Sys.time(), '%Y%m%d%H%M') for (p in head(split(x, i), jobs)) { message('Batch ', k) writeLines(p, 'recheck') git(c('add', 'recheck')) git(c('commit', '-m', shQuote(paste( c(id, 'checking:', head(p, 3), '...'), collapse = ' ' )))) git('push') Sys.sleep(10) k = k + 1 } } #' @param repo The crandalf repo on Github (of the form \code{user/repo} such as #' \code{"yihui/crandalf"}). Usually you do not need to specify it, unless you #' are not calling this function inside the crandalf project, because #' \command{gh} should be able to figure out the repo automatically. #' @param limit The maximum of records for \command{gh run list} to retrieve. #' You only need a larger number if the check results are very early in the #' Github Action history. #' @param wait Number of seconds to wait if not all jobs have been completed on #' Github. By default, this function checks the status every 5 minutes until #' all jobs are completed. Set \code{wait} to 0 to disable waiting (and throw #' an error immediately when any jobs are not completed). #' @rdname crandalf_check #' @export crandalf_results = function(pkg, repo = NA, limit = 200, wait = 5 * 60) { res = crandalf_jobs(pkg, repo, limit) if (NROW(res) == 0) { stop('Did not find check results for ', pkg, ' from Github Actions.') } if (any(res[, 1] != 'completed')) { if (wait <= 0) stop('Please wait till all jobs have been completed on Github Actions.') status = NULL repeat { res = crandalf_jobs(pkg, repo, limit) if (all(res[, 1] == 'completed')) break if (is.null(status) || !identical(status, table(res[, 1]))) { status = table(res[, 1]) timestamp() print(status) } Sys.sleep(wait) } } ids = grep_sub('^(\\d+) checking: .+', '\\1', res[, 3]) i = if (length(ids) > 0) grep(sprintf('^%s checking: ', ids[1]), res[, 3]) else 1 res = res[i, , drop = FALSE] res = res[res[, 2] == 'failure', , drop = FALSE] if (NROW(res) == 0) { stop('Did not find any failed results on Github Actions.') } for (i in seq_len(nrow(res))) { message('Downloading check results (', i, '/', nrow(res), ')') gh_run('download', res[i, 7], '-D', tempfile('crandalf-', '.'), repo = repo) } if (interactive()) browseURL(crandalf_merge(pkg)) } # retrieve the first N jobs info crandalf_jobs = function(pkg, repo = NA, limit = 200) { res = gh_run('list', '-L', limit, '-w', 'rev-check', repo = repo) res = res[grep(paste0('rev-check\tcheck-', pkg), res)] do.call(rbind, strsplit(res, '\t')) } crandalf_merge = function(pkg) { unlink(list.files('.', '[.]Rcheck2?$'), recursive = TRUE) x1 = x2 = x3 = NULL f1 = '00check_diffs.html'; f3 = 'latex.txt' for (d in list.files('.', '^crandalf-.+')) { if (!dir_exists(d)) next p = file.path(d, 'macOS-rev-check-results') if (file_exists(f <- file.path(p, f1))) { x = read_utf8(f) x1 = if (length(x1) == 0) x else { i1 = grep('', x)[1] i2 = tail(grep('', x), 1) i3 = tail(grep('', x1), 1) append(x1, x[(i1 + 1):(i2 - 1)], i3 - 1) } file.remove(f) } if (file_exists(f <- file.path(p, 'recheck2'))) { x2 = c(x2, read_utf8(f)) file.remove(f) } cs = list.files(p, '[.]Rcheck[2]?$', full.names = TRUE) file.rename(cs, basename(cs)) if (file_exists(f <- file.path(p, f3))) { x3 = c(x3, read_utf8(f)) file.remove(f) } unlink(d, recursive = TRUE) } write_utf8(x1, f1) # the full summary # store newly detected missing latex packages in latex.txt and commit/push git_co('main') append_unique(x3, f3) find_missing_latex() git(c('commit', '-m', shQuote('add more latex packages'), f3)) git('push') git_co(paste0('check-', pkg)) r = '[.]Rcheck2$' write_utf8(sort(unique(c(x2, gsub(r, '', list.files('.', r))))), 'recheck') f1 } # mclapply() with a different default for mc.cores and disable prescheduling plapply = function(X, FUN, ...) { parallel::mclapply( X, FUN, ..., mc.cores = getOption('mc.cores', parallel::detectCores()), mc.preschedule = FALSE ) } # download the source package from CRAN download_tarball = function(p, db = available.packages(type = 'source'), dir = '.', retry = 3) { if (!dir_exists(dir)) dir.create(dir, recursive = TRUE) z = file.path(dir, sprintf('%s_%s.tar.gz', p, db[p, 'Version'])) mapply(function(p, z) { # remove other versions of the package tarball unlink(setdiff(list.files(dir, sprintf('^%s_.+.tar.gz', p), full.names = TRUE), z)) for (i in seq_len(retry)) { if (file_exists(z)) break try(download.file(paste(db[p, 'Repository'], basename(z), sep = '/'), z, mode = 'wb')) } }, p, z, SIMPLIFY = FALSE) z } # clean up *.Rcheck if there are no warnings, errors, or notes in the log clean_Rcheck = function(dir, log = read_utf8(file.path(dir, '00check.log'))) { # do not check the status line if (length(grep('^Status: ', tail(log, 1)))) log = head(log, -1) if (length(grep('(WARNING|ERROR|NOTE)$', log)) == 0) unlink(dir, recursive = TRUE) !dir_exists(dir) } #' @rdname rev_check #' @param status_only If \code{TRUE}, only compare the final statuses of the #' checks (the last line of \file{00check.log}), and delete \file{*.Rcheck} #' and \file{*.Rcheck2} if the statuses are identical, otherwise write out the #' full diffs of the logs. If \code{FALSE}, compare the full logs under #' \file{*.Rcheck} and \file{*.Rcheck2}. #' @param output The output Markdown file to which the diffs in check logs will #' be written. If the \pkg{markdown} package is available, the Markdown file #' will be converted to HTML, so you can see the diffs more clearly. #' @export compare_Rcheck = function(status_only = TRUE, output = '00check_diffs.md') { if (length(dirs <- list.files('.', '.+[.]Rcheck$')) == 0) { # clean up the `recheck` file if (file_exists('recheck')) writeLines(character(), 'recheck') return() } d2 = function(d) c(d, paste0(d, '2')) logs = function(d) file.path(d2(d), '00check.log') res = NULL if (!status_only && Sys.which('diff') == '') warning("The command 'diff' is not available; will not calculate exact diffs in logs.") for (d in dirs) { f = existing_files(logs(d)) if (status_only && length(f) == 2) { status_line = function(file) { x = tail(read_utf8(file), 1) if (grepl('^Status: ', x)) x else { warning('The last line of ', file, ' is not the status.') NULL } } # if the check with current CRAN version of package also failed, or the # two statues are the same, chances are we are good to go s1 = status_line(f[1]) if (length(grep('Status: .*\\d+ ERROR', s1)) || identical(s1, status_line(f[2]))) { unlink(d2(d), recursive = TRUE); next } } res = c( res, paste('##', p <- sans_ext(d)), '', sprintf('[CRAN version](https://cran.rstudio.com/package=%s) (-) vs current version (+):\n', p), '```diff', file_diff(f), '```', '' ) if (length(res2 <- cran_check_page(p, NULL))) res = c( res, 'CRAN check logs:\n\n```', head_tail(unique(unlist(strsplit(res2, '\n')))), '```\n' ) } if (length(res) == 0) return() xfun::write_utf8(res, output) if (!loadable('markdown')) return(output) markdown::markdownToHTML( text = gsub('>', '+', gsub('^<', '-', res)), output = html_file <- with_ext(output, 'html'), header = c( "", "", "" ), encoding = 'UTF-8' ) if (!getOption('xfun.rev_check.keep_md', FALSE)) unlink(output) html_file } # keep the first and last n elements in x, and omit the middle head_tail = function(x, n = 10) { if (length(x) <= 2 * n) return(x) c(head(x, n), '....', tail(x, n)) } # compute the diffs of two files; if diffs too large, dedup them file_diff = function(files, len = 200, use_diff = Sys.which('diff') != '') { n = length(files) if (n == 0) return() if (n == 1) { f = tempfile(); on.exit(unlink(f), add = TRUE); file.create(f) files = c(f, files) } d = if (use_diff) { suppressWarnings(system2('diff', shQuote(files), stdout = TRUE)) } else { c(paste('<', read_utf8(files[1])), '---', paste('>', read_utf8(files[2]))) } if (length(d) >= len) unique(d) else d } # specify a list of package names to be ignored when installing all dependencies ignore_deps = function() { if (file_exists('00ignore_deps')) scan('00ignore_deps', 'character') } # download a check summary of a package from CRAN cran_check_page = function(pkg, con = '00check-cran.log') { u = sprintf('https://cran.rstudio.com/web/checks/check_results_%s.html', pkg) x = read_utf8(u) if (length(i <- grep('Check Details', x, ignore.case = TRUE)) == 0) return() x = x[i[1]:length(x)] x = gsub('<[^>]+>', '', x) x = gsub(' ', ' ', x) x = gsub('>', '>', x) x = gsub('<', '<', x) x = gsub('\\s+', ' ', x) x = paste(trimws(x), collapse = '\n') x = gsub('\n\n+', '\n\n', x) if (length(con) == 1) writeLines(x, con) else x } # download CRAN check summaries of all failed packages cran_check_pages = function() { dirs = list.files('.', '[.]Rcheck$') for (d in dirs) { if (dir_exists(d)) in_dir(d, cran_check_page(gsub('[.]Rcheck$', '', d))) } } # parse the check log for missing LaTeX packages and install them find_missing_latex = function() { dirs = list.files('.', '[.]Rcheck2?$') pkgs = NULL for (d in dirs) { if (dir_exists(d)) pkgs = c(pkgs, in_dir( d, tinytex::parse_packages('00check.log', quiet = c(TRUE, FALSE, FALSE)) )) } pkgs = unique(pkgs) if (file_exists(f <- 'latex.txt')) append_unique(pkgs, f) pkgs } xfun/R/utils.R0000644000175000017500000002231314134060133013044 0ustar nileshnileshstop2 = function(...) stop(..., call. = FALSE) warning2 = function(...) warning(..., call. = FALSE) #' Obtain an attribute of an object without partial matching #' #' An abbreviation of \code{base::\link[base]{attr}(exact = TRUE)}. #' @param ... Passed to \code{base::\link[base]{attr}()} (without the #' \code{exact} argument). #' @export #' @examples #' z = structure(list(a = 1), foo = 2) #' base::attr(z, 'f') # 2 #' xfun::attr(z, 'f') # NULL #' xfun::attr(z, 'foo') # 2 attr = function(...) base::attr(..., exact = TRUE) #' Set environment variables #' #' Set environment variables from a named character vector, and return the old #' values of the variables, so they could be restored later. #' #' The motivation of this function is that \code{\link{Sys.setenv}()} does not #' return the old values of the environment variables, so it is not #' straightforward to restore the variables later. #' @param vars A named character vector of the form \code{c(VARIABLE = VALUE)}. #' If any value is \code{NA}, this function will try to unset the variable. #' @return Old values of the variables (if not set, \code{NA}). #' @export #' @examples #' vars = xfun::set_envvar(c(FOO = '1234')) #' Sys.getenv('FOO') #' xfun::set_envvar(vars) #' Sys.getenv('FOO') set_envvar = function(vars) { if (is.null(nms <- names(vars)) || any(nms == '')) stop( "The 'vars' argument must take a named character vector." ) vals = Sys.getenv(nms, NA, names = TRUE) i = is.na(vars) suppressWarnings(Sys.unsetenv(nms[i])) if (length(vars <- vars[!i])) do.call(Sys.setenv, as.list(vars)) invisible(vals) } #' Call \code{on.exit()} in a parent function #' #' The function \code{\link{on.exit}()} is often used to perform tasks when the #' current function exits. This \code{exit_call()} function allows calling a #' function when a parent function exits (thinking of it as inserting an #' \code{on.exit()} call into the parent function). #' @param fun A function to be called when the parent function exits. #' @param n The parent frame number. For \code{n = 1}, \code{exit_call(fun)} is #' the same as \code{on.exit(fun())}; \code{n = 2} means adding #' \code{on.exit(fun())} in the parent function; \code{n = 3} means the #' grandparent, etc. #' @param ... Other arguments to be passed to \code{on.exit()}. #' @references This function was inspired by Kevin Ushey: #' \url{https://yihui.org/en/2017/12/on-exit-parent/} #' @export #' @examples #' f = function(x) { #' print(x) #' xfun::exit_call(function() print('The parent function is exiting!')) #' } #' g = function(y) { #' f(y) #' print('f() has been called!') #' } #' g('An argument of g()!') exit_call = function(fun, n = 2, ...) { do.call( on.exit, list(substitute(fun(), list(fun = fun)), add = TRUE, ...), envir = parent.frame(n) ) } #' Set the global option \code{\link{options}(stringsAsFactors = FALSE)} inside #' a parent function and restore the option after the parent function exits #' #' This is a shorthand of \code{opts = options(stringsAsFactors = FALSE); #' on.exit(options(opts), add = TRUE)}; \code{strings_please()} is an alias of #' \code{stringsAsStrings()}. #' @export #' @examples #' f = function() { #' xfun::strings_please() #' data.frame(x = letters[1:4], y = factor(letters[1:4])) #' } #' str(f()) # the first column should be character stringsAsStrings = function() { # TODO: remove this function in the future since stringsAsFactors starts to # default to FALSE since R 4.0.0 if (isFALSE(getOption('stringsAsFactors'))) return(invisible()) opts = options(stringsAsFactors = FALSE) exit_call(function() options(opts)) } #' @rdname stringsAsStrings #' @export strings_please = stringsAsStrings #' Evaluate an expression under a specified working directory #' #' Change the working directory, evaluate the expression, and restore the #' working directory. #' @param dir Path to a directory. #' @param expr An R expression. #' @export #' @examples #' library(xfun) #' in_dir(tempdir(), {print(getwd()); list.files()}) in_dir = function(dir, expr) { owd = setwd(dir); on.exit(setwd(owd)) expr } #' Test if an object is identical to \code{FALSE} #' #' A simple abbreviation of \code{identical(x, FALSE)}. #' @param x An R object. #' @export #' @examples #' library(xfun) #' isFALSE(TRUE) # false #' isFALSE(FALSE) # true #' isFALSE(c(FALSE, FALSE)) # false isFALSE = function(x) identical(x, FALSE) #' Parse R code and do not keep the source #' #' An abbreviation of \code{parse(keep.source = FALSE)}. #' @param code A character vector of the R source code. #' @export #' @return R \code{\link{expression}}s. #' @examples library(xfun) #' parse_only('1+1'); parse_only(c('y~x', '1:5 # a comment')) #' parse_only(character(0)) parse_only = function(code) { if (length(code) == 0) return(expression()) parse(text = code, keep.source = FALSE) } #' Try to evaluate an expression silently #' #' An abbreviation of \code{try(silent = TRUE)}. #' @param expr An R expression. #' @export #' @examples library(xfun) #' z = try_silent(stop('Wrong!')) #' inherits(z, 'try-error') try_silent = function(expr) try(expr, silent = TRUE) #' Try an expression and see if it throws an error #' #' Use \code{\link{tryCatch}()} to check if an expression throws an error. #' @inheritParams try_silent #' @return \code{TRUE} (error) or \code{FALSE} (success). #' @export #' @examples #' xfun::try_error(stop('foo')) # TRUE #' xfun::try_error(1:10) # FALSE try_error = function(expr) { err = FALSE tryCatch(expr, error = function(e) err <<- TRUE) err } #' Retry calling a function for a number of times #' #' If the function returns an error, retry it for the specified number of #' times, with a pause between attempts. #' #' One application of this function is to download a web resource. Since the #' download might fail sometimes, you may want to retry it for a few more times. #' @param fun A function. #' @param ... Arguments to be passed to the function. #' @param .times The number of times. #' @param .pause The number of seconds to wait before the next attempt. #' @export #' @examplesIf interactive() #' # read the Github releases info of the repo yihui/xfun #' xfun::retry(xfun::github_releases, 'yihui/xfun') retry = function(fun, ..., .times = 3, .pause = 5) { for (i in seq_len(.times)) { if (!inherits(res <- tryCatch(fun(...), error = identity), 'error')) return(res) Sys.sleep(.pause) } stop(res$message, call. = FALSE) } gsubi = function(...) gsub(..., ignore.case = TRUE) #' Turn the output of \code{\link{str}()} into a tree diagram #' #' The super useful function \code{str()} uses \verb{..} to indicate the level #' of sub-elements of an object, which may be difficult to read. This function #' uses vertical pipes to connect all sub-elements on the same level, so it is #' clearer which elements belong to the same parent element in an object with a #' nested structure (such as a nested list). #' @param ... Arguments to be passed to \code{\link{str}()} (note that the #' \code{comp.str} is hardcoded inside this function, and it is the only #' argument that you cannot customize). #' @return A character string as a \code{\link{raw_string}()}. #' @export #' @examples fit = lsfit(1:9, 1:9) #' str(fit) #' xfun::tree(fit) #' #' fit = lm(dist ~ speed, data = cars) #' str(fit) #' xfun::tree(fit) #' #' # some trivial examples #' xfun::tree(1:10) #' xfun::tree(iris) tree = function(...) { x = capture.output(str(..., comp.str = '$ ')) r = '^([^$-]+[$-] )(.*)$' x1 = gsub(r, '\\1', x) x2 = gsub(r, '\\2', x) x1 = gsub('[.][.]', ' ', x1) x1 = gsub('[$] $', '|-', x1) x1 = connect_pipes(x1) x3 = paste(x1, x2, sep = '') i = !grepl(r, x) x3[i] = x[i] raw_string(x3) } # for a tree diagram, connect the pipes on the same level, e.g., change # |- .. # |- .. # # |- .. # to # |- .. # |- .. # | # |- .. # this task is not complicated, but just boring nested for-loops connect_pipes = function(x) { ns = nchar(x); n = max(ns); m = length(x) if (n < 2 || m < 3) return(x) A = matrix('', nrow = m, ncol = n) x = strsplit(x, '') for (i in seq_len(m)) { A[i, seq_len(ns[i])] = x[[i]] } k = NULL for (j in seq_len(n - 1)) { for (i in seq_len(m - 2)) { if (!all(A[i, j + 0:1] == c('|', '-'))) next for (l in (i + 1):m) { cells = A[l, j + 0:1] if (all(cells == ' ')) { if (l == m) { k = NULL; break } else k = c(k, l) } else if (all(cells == c('|', '-'))) { break } else { k = NULL; break } } if (length(k) > 0) A[k, j] = '|' k = NULL } } apply(A, 1, paste, collapse = '') } pkg_file = function(...) system.file(..., package = 'xfun', mustWork = TRUE) #' Format numbers of bytes using a specified unit #' #' Call the S3 method \code{format.object_size()} to format numbers of bytes. #' @param x A numeric vector (each element represents a number of bytes). #' @param units,... Passed to \code{\link[=format.object_size]{format}()}. #' @return A character vector. #' @export #' @examples #' xfun::format_bytes(c(1, 1024, 2000, 1e6, 2e8)) #' xfun::format_bytes(c(1, 1024, 2000, 1e6, 2e8), units = 'KB') format_bytes = function(x, units = 'auto', ...) { vapply(x, function(b) { format(structure(b, class = 'object_size'), units = units, ...) }, character(1)) } xfun/R/rstudio.R0000644000175000017500000000425713701776020013414 0ustar nileshnilesh#' Type a character vector into the RStudio source editor #' #' Use the \pkg{rstudioapi} package to insert characters one by one into the #' RStudio source editor, as if they were typed by a human. #' @param x A character vector. #' @param pause A function to return a number in seconds to pause after typing #' each character. #' @param mistake The probability of making random mistakes when typing the next #' character. A random mistake is a random string typed into the editor and #' deleted immediately. #' @param save The probability of saving the document after typing each #' character. Note that If a document is not opened from a file, it will never #' be saved. #' @export #' @import stats #' @examples library(xfun) #' if (loadable('rstudioapi') && rstudioapi::isAvailable()) { #' rstudio_type('Hello, RStudio! xfun::rstudio_type() looks pretty cool!', #' pause = function() runif(1, 0, .5), mistake = .1) #' } rstudio_type = function(x, pause = function() .1, mistake = 0, save = 0) { get_ctx = function() rstudioapi::getSourceEditorContext() ctx = get_ctx() if (is.null(id <- ctx$id)) { message('Please make sure an RStudio editor tab is open') return() } save_it = function(prob = 1) { if (ctx$path == '' || (rbinom(1, 1, prob) == 0)) return() ctx = get_ctx() # in case a new line is automatically added at the end when saving the doc on.exit(rstudioapi::setSelectionRanges(ctx$selection[[1]]$range, id), add = TRUE) rstudioapi::documentSave(id) } type_one = function(x) { rstudioapi::insertText(text = x, id = id) Sys.sleep(pause()) } type_mistake = function() { n = sample(1:10, 1) x = sample(ascii_chars, n, replace = TRUE) for (i in x) type_one(i) Sys.sleep(.5) ctx = rstudioapi::getSourceEditorContext() r = ctx$selection[[1]]$range r$start[2] = r$start[2] - n rstudioapi::modifyRange(r, '', id) Sys.sleep(.5) } x = paste(x, collapse = '\n') for (i in unlist(strsplit(x, ''))) { type_one(i); save_it(save) if (runif(1) < mistake) type_mistake() } save_it(as.integer(save > 0)) # if prob is non-zero, save it finally invisible() } ascii_chars = intToUtf8(32:126, TRUE) xfun/LICENSE0000644000175000017500000000005414020527026012366 0ustar nileshnileshYEAR: 2018-2021 COPYRIGHT HOLDER: Yihui Xie xfun/inst/0000755000175000017500000000000014156200274012342 5ustar nileshnileshxfun/inst/scripts/0000755000175000017500000000000014145515221014030 5ustar nileshnileshxfun/inst/scripts/child-pids.sh0000644000175000017500000000030013741412323016375 0ustar nileshnilesh# given a PID, output all its child PIDs recursively list_children() { for j in $(pgrep -P $1); do echo $j echo $(list_children $j) done } for i in $@; do list_children $i done xfun/inst/scripts/call-fun.R0000644000175000017500000000111513736233377015670 0ustar nileshnilesh# This script is executed via the command line `Rscript call-fun.R arg1 arg2`, # where arg1 is a path to an .rds file, which contains the function and its # arguments saved as a list, and arg2 is a path to an .rds file to which the # returned value of the function call is saved. local({ if (length(a <- commandArgs(TRUE)) != 2) stop('The number of arguments passed to Rscript should be 2.') x = readRDS(a[1]) # list(fun, args) f = x[[1]] if (is.character(f)) f = eval(parse(text = f), envir = globalenv()) r = do.call(f, x[[2]], envir = globalenv()) saveRDS(r, a[2]) }) xfun/inst/doc/0000755000175000017500000000000014156200274013107 5ustar nileshnileshxfun/inst/doc/xfun.html0000644000175000017500000014346714156200274014774 0ustar nileshnilesh An Introduction to xfun

An Introduction to xfun

A Collection of Miscellaneous Functions

Yihui Xie

2021-12-14

After writing about 20 R packages, I found I had accumulated several utility functions that I used across different packages, so I decided to extract them into a separate package. Previously I had been using the evil triple-colon ::: to access these internal utility functions. Now with xfun, these functions have been exported, and more importantly, documented. It should be better to use them under the sun instead of in the dark.

This page shows examples of a subset of functions in this package. For a full list of functions, see the help page help(package = 'xfun'). The source package is available on Github: https://github.com/yihui/xfun.

No more partial matching for lists!

I have been bitten many times by partial matching in lists, e.g., when I want x$a but the element a does not exist in the list x, it returns the value x$abc if abc exists in x. A strict list is a list for which the partial matching of the $ operator is disabled. The functions xfun::strict_list() and xfun::as_strict_list() are the equivalents to base::list() and base::as.list() respectively which always return as strict list, e.g.,

library(xfun)
(z = strict_list(aaa = "I am aaa", b = 1:5))
## $aaa
## [1] "I am aaa"
## 
## $b
## [1] 1 2 3 4 5
z$a  # NULL (strict matching)
## NULL
z$aaa  # I am aaa
## [1] "I am aaa"
z$b
## [1] 1 2 3 4 5
z$c = "you can create a new element"

z2 = unclass(z)  # a normal list
z2$a  # partial matching
## [1] "I am aaa"
z3 = as_strict_list(z2)  # a strict list again
z3$a  # NULL (strict matching) again!
## NULL

Similarly, the default partial matching in attr() can be annoying, too. The function xfun::attr() is simply a shorthand of attr(..., exact = TRUE).

I want it, or I do not want. There is no “I probably want”.

Output character vectors for human eyes

When R prints a character vector, your eyes may be distracted by the indices like [1], double quotes, and escape sequences. To see a character vector in its “raw” form, you can use cat(..., sep = '\n'). The function raw_string() marks a character vector as “raw”, and the corresponding printing function will call cat(sep = '\n') to print the character vector to the console.

library(xfun)
raw_string(head(LETTERS))
A
B
C
D
E
F
(x = c("a \"b\"", "hello\tworld!"))
[1] "a \"b\""       "hello\tworld!"
raw_string(x)  # this is more likely to be what you want to see
a "b"
hello   world!

Get the data URI of a file

Files can be encoded into base64 strings via base64_uri(). This is a common technique to embed arbitrary files in HTML documents (which is what xfun::embed_file() does and it is based on base64_uri()).

f = system.file("LICENSE", package = "xfun")
xfun::base64_uri(f)
## [1] "data:text/plain;base64,WUVBUjogMjAxOC0yMDIxCkNPUFlSSUdIVCBIT0xERVI6IFlpaHVpIFhpZQo="

Match strings and do substitutions

After typing the code x = grep(pattern, x, value = TRUE); gsub(pattern, '\\1', x) many times, I combined them into a single function xfun::grep_sub().

xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE)
## [1] "aBc"   "aBBBc"

Search and replace strings in files

I can never remember how to properly use grep or sed to search and replace strings in multiple files. My favorite IDE, RStudio, has not provided this feature yet (you can only search and replace in the currently opened file). Therefore I did a quick and dirty implementation in R, including functions gsub_files(), gsub_dir(), and gsub_ext(), to search and replace strings in multiple files under a directory. Note that the files are assumed to be encoded in UTF-8. If you do not use UTF-8, we cannot be friends. Seriously.

All functions are based on gsub_file(), which performs searching and replacing in a single file, e.g.,

library(xfun)
f = tempfile()
writeLines(c("hello", "world"), f)
gsub_file(f, "world", "woRld", fixed = TRUE)
file_string(f)
hello
woRld

The function gsub_dir() is very flexible: you can limit the list of files by MIME types, or extensions. For example, if you want to do substitution in text files, you may use gsub_dir(..., mimetype = '^text/').

The function process_file() is a more general way to process files. Basically it reads a file, process the content with a function that you pass to it, and writes back the text, e.g.,

process_file(f, function(x) {
  rep(x, 3)  # repeat the content 3 times
})
file_string(f)
hello
woRld
hello
woRld
hello
woRld

WARNING: Before using these functions, make sure that you have backed up your files, or version control your files. The files will be modified in-place. If you do not back up or use version control, there is no chance to regret.

Manipulate filename extensions

Functions file_ext() and sans_ext() are based on functions in tools. The function with_ext() adds or replaces extensions of filenames, and it is vectorized.

library(xfun)
p = c("abc.doc", "def123.tex", "path/to/foo.Rmd")
file_ext(p)
## [1] "doc" "tex" "Rmd"
sans_ext(p)
## [1] "abc"         "def123"      "path/to/foo"
with_ext(p, ".txt")
## [1] "abc.txt"         "def123.txt"      "path/to/foo.txt"
with_ext(p, c(".ppt", ".sty", ".Rnw"))
## [1] "abc.ppt"         "def123.sty"      "path/to/foo.Rnw"
with_ext(p, "html")
## [1] "abc.html"         "def123.html"      "path/to/foo.html"

Find files (in a project) without the pain of thinking about absolute/relative paths

The function proj_root() was inspired by the rprojroot package, and tries to find the root directory of a project. Currently it only supports R package projects and RStudio projects by default. It is much less sophisticated than rprojroot.

The function from_root() was inspired by here::here(), but returns a relative path (relative to the project’s root directory found by proj_root()) instead of an absolute path. For example, xfun::from_root('data', 'cars.csv') in a code chunk of docs/foo.Rmd will return ../data/cars.csv when docs/ and data/ directories are under the root directory of a project.

root/
  |-- data/
  |   |-- cars.csv
  |
  |-- docs/
      |-- foo.Rmd

If file paths are too much pain for you to think about, you can just pass an incomplete path to the function magic_path(), and it will try to find the actual path recursively under subdirectories of a root directory. For example, you may only provide a base filename, and magic_path() will look for this file under subdirectories and return the actual path if it is found. By default, it returns a relative path, which is relative to the current working directory. With the above example, xfun::magic_path('cars.csv') in a code chunk of docs/foo.Rmd will return ../data/cars.csv, if cars.csv is a unique filename in the project. You can freely move it to any folders of this project, and magic_path() will still find it. If you are not using a project to manage files, magic_path() will look for the file under subdirectories of the current working directory.

Types of operating systems

The series of functions is_linux(), is_macos(), is_unix(), and is_windows() test the types of the OS, using the information from .Platform and Sys.info(), e.g.,

xfun::is_macos()
## [1] TRUE
xfun::is_unix()
## [1] TRUE
xfun::is_linux()
## [1] FALSE
xfun::is_windows()
## [1] FALSE

Loading and attaching packages

Oftentimes I see users attach a series of packages in the beginning of their scripts by repeating library() multiple times. This could be easily vectorized, and the function xfun::pkg_attach() does this job. For example,

library(testit)
library(parallel)
library(tinytex)
library(mime)

is equivalent to

xfun::pkg_attach(c('testit', 'parallel', 'tinytex', 'mime'))

I also see scripts that contain code to install a package if it is not available, e.g.,

if (!requireNamespace('tinytex')) install.packages('tinytex')
library(tinytex)

This could be done via

xfun::pkg_attach2('tinytex')

The function pkg_attach2() is a shorthand of pkg_attach(..., install = TRUE), which means if a package is not available, install it. This function can also deal with multiple packages.

The function loadable() tests if a package is loadable.

Read/write files in UTF-8

Functions read_utf8() and write_utf8() can be used to read/write files in UTF-8. They are simple wrappers of readLines() and writeLines().

Convert numbers to English words

The function numbers_to_words() (or n2w() for short) converts numbers to English words.

n2w(0, cap = TRUE)
## [1] "Zero"
n2w(seq(0, 121, 11), and = TRUE)
##  [1] "zero"                       "eleven"                    
##  [3] "twenty-two"                 "thirty-three"              
##  [5] "forty-four"                 "fifty-five"                
##  [7] "sixty-six"                  "seventy-seven"             
##  [9] "eighty-eight"               "ninety-nine"               
## [11] "one hundred and ten"        "one hundred and twenty-one"
n2w(1e+06)
## [1] "one million"
n2w(1e+11 + 12345678)
## [1] "one hundred billion, twelve million, three hundred forty-five thousand, six hundred seventy-eight"
n2w(-987654321)
## [1] "minus nine hundred eighty-seven million, six hundred fifty-four thousand, three hundred twenty-one"
n2w(1e+15 - 1)
## [1] "nine hundred ninety-nine trillion, nine hundred ninety-nine billion, nine hundred ninety-nine million, nine hundred ninety-nine thousand, nine hundred ninety-nine"

Cache an R expression to an RDS file

The function cache_rds() provides a simple caching mechanism: the first time an expression is passed to it, it saves the result to an RDS file; the next time it will read the RDS file and return the value instead of evaluating the expression again. If you want to invalidate the cache, you can use the argument rerun = TRUE.

res = xfun::cache_rds({
  # pretend the computing here is a time-consuming
  Sys.sleep(2)
  1:10
})

When the function is used in a code chunk in a knitr document, the RDS cache file is saved to a path determined by the chunk label (the base filename) and the chunk option cache.path (the cache directory), so you do not have to provide the file and dir arguments of cache_rds().

This caching mechanism is much simpler than knitr’s caching. Cache invalidation is often tricky (see this post), so this function may be helpful if you want more transparency and control over when to invalidate the cache (for cache_rds(), the cache is invalidated when the cache file is deleted, which can be achieved via the argument rerun = TRUE).

As documented on the help page of cache_rds(), there are two common cases in which you may want to invalidate the cache:

  1. The code in the expression has changed, e.g., if you changed the code from cache_rds({x + 1}) to cache_rds({x + 2}), the cache will be automatically invalidated and the expression will be re-evaluated. However, please note that changes in white spaces or comments do not matter. Or generally speaking, as long as the change does not affect the parsed expression, the cache will not be invalidated, e.g., the two expressions below are essentially identical (hence if you have executed cache_rds() on the first expression, the second expression will be able to take advantage of the cache):

    res = xfun::cache_rds({
      Sys.sleep(3  );
      x=1:10;  # semi-colons won't matter
      x+1;
    })
    
    res = xfun::cache_rds({
      Sys.sleep(3)
      x = 1:10  # a comment
      x +
        1  # feel free to make any changes in white spaces
    })
  2. The value of a global variable in the expression has changed, e.g., if y has changed, you are most likely to want to invalidate the cache and rerun the expression below:

    res = xfun::cache_rds({
      x = 1:10
      x + y
    })

    This is because x is a local variable in the expression, and y is an external global variable (not created locally like x). To invalidate the cache when y has changed, you may let cache_rds() know through the hash argument that y needs to be considered when deciding if the cache should be invalidated:

    res = xfun::cache_rds({
      x = 1:10
      x + y
    }, hash = list(y))

    If you do not want to provide this list of value(s) to the hash argument, you may try hash = "auto" instead, which asks cache_rds() to try to figure out all global variables automatically and use a list of their values as the value for the hash argument.

    res = xfun::cache_rds({
      x = 1:10
      x + y
    }, hash = "auto")

Check reverse dependencies of a package

Running R CMD check on the reverse dependencies of knitr and rmarkdown is my least favorite thing in developing R packages, because the numbers of their reverse dependencies are huge. The function rev_check() reflects some of my past experience in this process. I think I have automated it as much as possible, and made it as easy as possible to discover possible new problems introduced by the current version of the package (compared to the CRAN version). Finally I can just sit back and let it run.

Input a character vector into the RStudio source editor

The function rstudio_type() inputs characters in the RStudio source editor as if they were typed by a human. I came up with the idea when preparing my talk for rstudio::conf 2018 (see this post for more details).

xfun/inst/doc/xfun.R0000644000175000017500000000611714156200273014216 0ustar nileshnilesh## ----setup, include=FALSE----------------------------------------------------- library(xfun) ## ----------------------------------------------------------------------------- library(xfun) (z = strict_list(aaa = "I am aaa", b = 1:5)) z$a # NULL (strict matching) z$aaa # I am aaa z$b z$c = "you can create a new element" z2 = unclass(z) # a normal list z2$a # partial matching z3 = as_strict_list(z2) # a strict list again z3$a # NULL (strict matching) again! ## ----comment=''--------------------------------------------------------------- library(xfun) raw_string(head(LETTERS)) (x = c("a \"b\"", "hello\tworld!")) raw_string(x) # this is more likely to be what you want to see ## ----comment=''--------------------------------------------------------------- f = system.file("LICENSE", package = "xfun") xfun::file_string(f) as.character(xfun::file_string(f)) # essentially a character string ## ----------------------------------------------------------------------------- f = system.file("LICENSE", package = "xfun") xfun::base64_uri(f) ## ----------------------------------------------------------------------------- xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE) ## ----comment=''--------------------------------------------------------------- library(xfun) f = tempfile() writeLines(c("hello", "world"), f) gsub_file(f, "world", "woRld", fixed = TRUE) file_string(f) ## ---- comment=''-------------------------------------------------------------- process_file(f, function(x) { rep(x, 3) # repeat the content 3 times }) file_string(f) ## ----------------------------------------------------------------------------- library(xfun) p = c("abc.doc", "def123.tex", "path/to/foo.Rmd") file_ext(p) sans_ext(p) with_ext(p, ".txt") with_ext(p, c(".ppt", ".sty", ".Rnw")) with_ext(p, "html") ## ----------------------------------------------------------------------------- xfun::is_macos() xfun::is_unix() xfun::is_linux() xfun::is_windows() ## ----eval=FALSE--------------------------------------------------------------- # library(testit) # library(parallel) # library(tinytex) # library(mime) ## ----eval=FALSE--------------------------------------------------------------- # xfun::pkg_attach(c('testit', 'parallel', 'tinytex', 'mime')) ## ----eval=FALSE--------------------------------------------------------------- # if (!requireNamespace('tinytex')) install.packages('tinytex') # library(tinytex) ## ----eval=FALSE--------------------------------------------------------------- # xfun::pkg_attach2('tinytex') ## ----------------------------------------------------------------------------- n2w(0, cap = TRUE) n2w(seq(0, 121, 11), and = TRUE) n2w(1e+06) n2w(1e+11 + 12345678) n2w(-987654321) n2w(1e+15 - 1) ## ---- eval=FALSE-------------------------------------------------------------- # res = xfun::cache_rds({ # # pretend the computing here is a time-consuming # Sys.sleep(2) # 1:10 # }) ## ----------------------------------------------------------------------------- xfun::session_info(c('xfun', 'rmarkdown', 'knitr', 'tinytex'), dependencies = FALSE) xfun/inst/doc/xfun.Rmd0000644000175000017500000003336214143234661014545 0ustar nileshnilesh--- title: An Introduction to xfun subtitle: A Collection of Miscellaneous Functions author: "Yihui Xie" date: "`r Sys.Date()`" slug: xfun githubEditURL: https://github.com/yihui/xfun/edit/master/vignettes/xfun.Rmd output: knitr:::html_vignette: toc: yes vignette: > %\VignetteIndexEntry{An Introduction to xfun} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} library(xfun) ``` After writing about 20 R packages, I found I had accumulated several utility functions that I used across different packages, so I decided to extract them into a separate package. Previously I had been using the evil triple-colon `:::` to access these internal utility functions. Now with **xfun**, these functions have been exported, and more importantly, documented. It should be better to use them under the sun instead of in the dark. This page shows examples of a subset of functions in this package. For a full list of functions, see the help page `help(package = 'xfun')`. The source package is available on Github: https://github.com/yihui/xfun. ## No more partial matching for lists! I have been bitten many times by partial matching in lists, e.g., when I want `x$a` but the element `a` does not exist in the list `x`, it returns the value `x$abc` if `abc` exists in `x`. A strict list is a list for which the partial matching of the `$` operator is disabled. The functions `xfun::strict_list()` and `xfun::as_strict_list()` are the equivalents to `base::list()` and `base::as.list()` respectively which always return as strict list, e.g., ```{r} library(xfun) (z = strict_list(aaa = "I am aaa", b = 1:5)) z$a # NULL (strict matching) z$aaa # I am aaa z$b z$c = "you can create a new element" z2 = unclass(z) # a normal list z2$a # partial matching z3 = as_strict_list(z2) # a strict list again z3$a # NULL (strict matching) again! ``` Similarly, the default partial matching in `attr()` can be annoying, too. The function `xfun::attr()` is simply a shorthand of `attr(..., exact = TRUE)`. I want it, or I do not want. There is no "I probably want". ## Output character vectors for human eyes When R prints a character vector, your eyes may be distracted by the indices like `[1]`, double quotes, and escape sequences. To see a character vector in its "raw" form, you can use `cat(..., sep = '\n')`. The function `raw_string()` marks a character vector as "raw", and the corresponding printing function will call `cat(sep = '\n')` to print the character vector to the console. ```{r comment=''} library(xfun) raw_string(head(LETTERS)) (x = c("a \"b\"", "hello\tworld!")) raw_string(x) # this is more likely to be what you want to see ``` ## Print the content of a text file I have used `paste(readLines('foo'), collapse = '\n')` many times before I decided to write a simple wrapper function `xfun::file_string()`. This function also makes use of `raw_string()`, so you can see the content of a file in the console as a side-effect, e.g., ```{r comment=''} f = system.file("LICENSE", package = "xfun") xfun::file_string(f) as.character(xfun::file_string(f)) # essentially a character string ``` ## Get the data URI of a file Files can be encoded into base64 strings via `base64_uri()`. This is a common technique to embed arbitrary files in HTML documents (which is [what `xfun::embed_file()` does](https://bookdown.org/yihui/rmarkdown-cookbook/embed-file.html) and it is based on `base64_uri()`). ```{r} f = system.file("LICENSE", package = "xfun") xfun::base64_uri(f) ``` ## Match strings and do substitutions After typing the code `x = grep(pattern, x, value = TRUE); gsub(pattern, '\\1', x)` many times, I combined them into a single function `xfun::grep_sub()`. ```{r} xfun::grep_sub('a([b]+)c', 'a\\U\\1c', c('abc', 'abbbc', 'addc', '123'), perl = TRUE) ``` ## Search and replace strings in files I can never remember how to properly use `grep` or `sed` to search and replace strings in multiple files. My favorite IDE, RStudio, has not provided this feature yet (you can only search and replace in the currently opened file). Therefore I did a quick and dirty implementation in R, including functions `gsub_files()`, `gsub_dir()`, and `gsub_ext()`, to search and replace strings in multiple files under a directory. Note that the files are assumed to be encoded in UTF-8. If you do not use UTF-8, we cannot be friends. Seriously. All functions are based on `gsub_file()`, which performs searching and replacing in a single file, e.g., ```{r comment=''} library(xfun) f = tempfile() writeLines(c("hello", "world"), f) gsub_file(f, "world", "woRld", fixed = TRUE) file_string(f) ``` The function `gsub_dir()` is very flexible: you can limit the list of files by MIME types, or extensions. For example, if you want to do substitution in text files, you may use `gsub_dir(..., mimetype = '^text/')`. The function `process_file()` is a more general way to process files. Basically it reads a file, process the content with a function that you pass to it, and writes back the text, e.g., ```{r, comment=''} process_file(f, function(x) { rep(x, 3) # repeat the content 3 times }) file_string(f) ``` **WARNING**: Before using these functions, make sure that you have backed up your files, or version control your files. The files will be modified in-place. If you do not back up or use version control, there is no chance to regret. ## Manipulate filename extensions Functions `file_ext()` and `sans_ext()` are based on functions in **tools**. The function `with_ext()` adds or replaces extensions of filenames, and it is vectorized. ```{r} library(xfun) p = c("abc.doc", "def123.tex", "path/to/foo.Rmd") file_ext(p) sans_ext(p) with_ext(p, ".txt") with_ext(p, c(".ppt", ".sty", ".Rnw")) with_ext(p, "html") ``` ## Find files (in a project) without the pain of thinking about absolute/relative paths The function `proj_root()` was inspired by the **rprojroot** package, and tries to find the root directory of a project. Currently it only supports R package projects and RStudio projects by default. It is much less sophisticated than **rprojroot**. The function `from_root()` was inspired by `here::here()`, but returns a relative path (relative to the project's root directory found by `proj_root()`) instead of an absolute path. For example, `xfun::from_root('data', 'cars.csv')` in a code chunk of `docs/foo.Rmd` will return `../data/cars.csv` when `docs/` and `data/` directories are under the root directory of a project. ``` root/ |-- data/ | |-- cars.csv | |-- docs/ |-- foo.Rmd ``` If file paths are too much pain for you to think about, you can just pass an incomplete path to the function `magic_path()`, and it will try to find the actual path recursively under subdirectories of a root directory. For example, you may only provide a base filename, and `magic_path()` will look for this file under subdirectories and return the actual path if it is found. By default, it returns a relative path, which is relative to the current working directory. With the above example, `xfun::magic_path('cars.csv')` in a code chunk of `docs/foo.Rmd` will return `../data/cars.csv`, if `cars.csv` is a unique filename in the project. You can freely move it to any folders of this project, and `magic_path()` will still find it. If you are not using a project to manage files, `magic_path()` will look for the file under subdirectories of the current working directory. ## Types of operating systems The series of functions `is_linux()`, `is_macos()`, `is_unix()`, and `is_windows()` test the types of the OS, using the information from `.Platform` and `Sys.info()`, e.g., ```{r} xfun::is_macos() xfun::is_unix() xfun::is_linux() xfun::is_windows() ``` ## Loading and attaching packages Oftentimes I see users attach a series of packages in the beginning of their scripts by repeating `library()` multiple times. This could be easily vectorized, and the function `xfun::pkg_attach()` does this job. For example, ```{r eval=FALSE} library(testit) library(parallel) library(tinytex) library(mime) ``` is equivalent to ```{r eval=FALSE} xfun::pkg_attach(c('testit', 'parallel', 'tinytex', 'mime')) ``` I also see scripts that contain code to install a package if it is not available, e.g., ```{r eval=FALSE} if (!requireNamespace('tinytex')) install.packages('tinytex') library(tinytex) ``` This could be done via ```{r eval=FALSE} xfun::pkg_attach2('tinytex') ``` The function `pkg_attach2()` is a shorthand of `pkg_attach(..., install = TRUE)`, which means if a package is not available, install it. This function can also deal with multiple packages. The function `loadable()` tests if a package is loadable. ## Read/write files in UTF-8 Functions `read_utf8()` and `write_utf8()` can be used to read/write files in UTF-8. They are simple wrappers of `readLines()` and `writeLines()`. ## Convert numbers to English words The function `numbers_to_words()` (or `n2w()` for short) converts numbers to English words. ```{r} n2w(0, cap = TRUE) n2w(seq(0, 121, 11), and = TRUE) n2w(1e+06) n2w(1e+11 + 12345678) n2w(-987654321) n2w(1e+15 - 1) ``` ## Cache an R expression to an RDS file The function `cache_rds()` provides a simple caching mechanism: the first time an expression is passed to it, it saves the result to an RDS file; the next time it will read the RDS file and return the value instead of evaluating the expression again. If you want to invalidate the cache, you can use the argument `rerun = TRUE`. ```{r, eval=FALSE} res = xfun::cache_rds({ # pretend the computing here is a time-consuming Sys.sleep(2) 1:10 }) ``` When the function is used in a code chunk in a **knitr** document, the RDS cache file is saved to a path determined by the chunk label (the base filename) and the chunk option `cache.path` (the cache directory), so you do not have to provide the `file` and `dir` arguments of `cache_rds()`. This caching mechanism is much simpler than **knitr**'s caching. Cache invalidation is often tricky (see [this post](https://yihui.org/en/2018/06/cache-invalidation/)), so this function may be helpful if you want more transparency and control over when to invalidate the cache (for `cache_rds()`, the cache is invalidated when the cache file is deleted, which can be achieved via the argument `rerun = TRUE`). As documented on the help page of `cache_rds()`, there are two common cases in which you may want to invalidate the cache: 1. The code in the expression has changed, e.g., if you changed the code from `cache_rds({x + 1})` to `cache_rds({x + 2})`, the cache will be automatically invalidated and the expression will be re-evaluated. However, please note that changes in white spaces or comments do not matter. Or generally speaking, as long as the change does not affect the parsed expression, the cache will not be invalidated, e.g., the two expressions below are essentially identical (hence if you have executed `cache_rds()` on the first expression, the second expression will be able to take advantage of the cache): ```r res = xfun::cache_rds({ Sys.sleep(3 ); x=1:10; # semi-colons won't matter x+1; }) res = xfun::cache_rds({ Sys.sleep(3) x = 1:10 # a comment x + 1 # feel free to make any changes in white spaces }) ``` 1. The value of a global variable in the expression has changed, e.g., if `y` has changed, you are most likely to want to invalidate the cache and rerun the expression below: ```r res = xfun::cache_rds({ x = 1:10 x + y }) ``` This is because `x` is a local variable in the expression, and `y` is an external global variable (not created locally like `x`). To invalidate the cache when `y` has changed, you may let `cache_rds()` know through the `hash` argument that `y` needs to be considered when deciding if the cache should be invalidated: ```r res = xfun::cache_rds({ x = 1:10 x + y }, hash = list(y)) ``` If you do not want to provide this list of value(s) to the `hash` argument, you may try `hash = "auto"` instead, which asks `cache_rds()` to try to figure out all global variables automatically and use a list of their values as the value for the `hash` argument. ```r res = xfun::cache_rds({ x = 1:10 x + y }, hash = "auto") ``` ## Check reverse dependencies of a package Running `R CMD check` on the reverse dependencies of **knitr** and **rmarkdown** is my least favorite thing in developing R packages, because the numbers of their reverse dependencies are huge. The function `rev_check()` reflects some of my past experience in this process. I think I have automated it as much as possible, and made it as easy as possible to discover possible new problems introduced by the current version of the package (compared to the CRAN version). Finally I can just sit back and let it run. ## Input a character vector into the RStudio source editor The function `rstudio_type()` inputs characters in the RStudio source editor as if they were typed by a human. I came up with the idea when preparing my talk for rstudio::conf 2018 ([see this post](https://yihui.org/en/2018/03/blogdown-video-rstudio-conf/) for more details). ## Print session information Since I have never been fully satisfied by the output of `sessionInfo()`, I tweaked it to make it more useful in my use cases. For example, it is rarely useful to print out the names of base R packages, or information about the matrix products / BLAS / LAPACK. Oftentimes I want additional information in the session information, such as the Pandoc version when **rmarkdown** is used. The function `session_info()` tweaks the output of `sessionInfo()`, and makes it possible for other packages to append information in the output of `session_info()`. You can choose to print out the versions of only the packages you specify, e.g., ```{r} xfun::session_info(c('xfun', 'rmarkdown', 'knitr', 'tinytex'), dependencies = FALSE) ``` xfun/NAMESPACE0000644000175000017500000000464614156162700012617 0ustar nileshnilesh# Generated by roxygen2: do not edit by hand S3method("$",xfun_strict_list) S3method(print,xfun_raw_string) S3method(print,xfun_rename_seq) S3method(print,xfun_strict_list) export(Rcmd) export(Rscript) export(Rscript_call) export(append_unique) export(append_utf8) export(as_strict_list) export(attr) export(base64_decode) export(base64_encode) export(base64_uri) export(base_pkgs) export(bg_process) export(broken_packages) export(bump_version) export(cache_rds) export(check_old_package) export(check_package_name) export(compare_Rcheck) export(crandalf_check) export(crandalf_results) export(del_empty_dir) export(dir_create) export(dir_exists) export(do_once) export(download_file) export(embed_dir) export(embed_file) export(embed_files) export(existing_files) export(exit_call) export(file_exists) export(file_ext) export(file_string) export(format_bytes) export(from_root) export(github_api) export(github_releases) export(grep_sub) export(gsub_dir) export(gsub_ext) export(gsub_file) export(gsub_files) export(in_dir) export(install_dir) export(install_github) export(isFALSE) export(is_CRAN_incoming) export(is_R_CMD_check) export(is_abs_path) export(is_ascii) export(is_linux) export(is_macos) export(is_rel_path) export(is_sub_path) export(is_unix) export(is_web_path) export(is_windows) export(json_vector) export(loadable) export(magic_path) export(mark_dirs) export(msg_cat) export(n2w) export(native_encode) export(news2md) export(normalize_path) export(numbers_to_words) export(optipng) export(parse_only) export(pkg_attach) export(pkg_attach2) export(pkg_available) export(pkg_load) export(pkg_load2) export(proc_kill) export(process_file) export(proj_root) export(prose_index) export(protect_math) export(raw_string) export(read_all) export(read_bin) export(read_utf8) export(relative_path) export(rename_seq) export(rest_api) export(rest_api_raw) export(retry) export(rev_check) export(root_rules) export(rstudio_type) export(same_path) export(sans_ext) export(session_info) export(set_envvar) export(sort_file) export(split_lines) export(split_source) export(strict_list) export(stringsAsStrings) export(strings_please) export(submit_cran) export(system3) export(tinify) export(tojson) export(tree) export(try_error) export(try_silent) export(upload_ftp) export(upload_win_builder) export(url_accessible) export(url_filename) export(valid_syntax) export(with_ext) export(write_utf8) import(stats) import(utils) useDynLib(xfun, .registration = TRUE)