Skip to content

Commit

Permalink
Add new fun read_ini
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Solymos <psolymos@gmail.com>
  • Loading branch information
psolymos committed Jun 2, 2023
1 parent 565a7e1 commit 93f2a11
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 7 deletions.
4 changes: 2 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Package: rconfig
Type: Package
Title: Manage R Configuration at the Command Line
Version: 0.2.1
Date: 2023-03-18
Date: 2023-06-01
Authors@R: c(
person(given = "Peter",
family = "Solymos",
Expand All @@ -19,7 +19,7 @@ Description: Configuration management using files (JSON, YAML, separated text),
License: MIT + file LICENSE
LazyLoad: yes
Imports: yaml, jsonlite
RoxygenNote: 7.2.1
RoxygenNote: 7.2.3
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
BugReports: https://github.com/analythium/rconfig/issues
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ S3method(command,default)
S3method(value,default)
export(command)
export(rconfig)
export(read_ini)
export(value)
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Version 0.2.1

* Variable splitting did not consider the separator on the right hand side, now fixed.
* Added new function `read_ini` to read INI configuration files.

# Version 0.2.0

Expand Down
1 change: 0 additions & 1 deletion R/config.R
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@
#' value(CONFIG$test, FALSE) # unset
#'
#' @seealso [utils::modifyList()]
#' @keywords models regression
#' @name rconfig
NULL

Expand Down
144 changes: 142 additions & 2 deletions R/parsers.R
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ guess_ext <- function(x) {
"yml" = "yml",
"yaml" = "yml",
"json" = "json",
"ini" = "ini",
"txt")
}

Expand Down Expand Up @@ -117,13 +118,14 @@ parse_json <- function(x, ...) {
}
}

## Parse the file depending on file type (yml, json, txt)
## note: YAML, JSON, txt accept URLs or file names
## Parse the file depending on file type (yml, json, ini, txt)
## note: YAML, JSON, INI, txt accept URLs or file names
parse_file <- function(x, ...) {
x <- normalizePath(x, mustWork = FALSE)
out <- switch(guess_ext(x),
"yml" = parse_yml(x, ...),
"json" = parse_json(x, ...),
"ini" = parse_ini(x, ...),
"txt" = parse_txt(x, ...))
attr(out, "trace") <- list(
kind = "file",
Expand Down Expand Up @@ -260,3 +262,141 @@ config_list <- function(file = NULL, list = NULL, ...) {
attr(lists, "command") <- verbs
lists
}

#' Read INI Files
#'
#' Read INI (`.ini` file extension) configuration files.
#'
#' @details
#' An INI configuration file consists of sections, each led by a `[section]` header,
#' followed by key/value entries separated by a specific string (`=` or `:` by default).
#' By default, section names are case sensitive but keys are not.
#' Leading and trailing whitespace is removed from keys and values.
#' Values can be omitted if the parser is configured to allow it,
#' in which case the key/value delimiter may also be left out.
#' Values can also span multiple lines, as long as they are indented deeper than the first line of the value.
#' Depending on the parser's mode, blank lines may be treated as parts of multiline values or ignored.
#' By default, a valid section name can be any string that does not contain `\n` or `]`.
#' Configuration files may include comments, prefixed by specific characters (`#` and `;` by default).
#' Comments may appear on their own on an otherwise empty line, possibly indented.
#'
#' @param file The name and path of the INI configuration file.
#' @param ... Other arguments passed to the function (currently there is none).
#'
#' @return The configuration value a named list,
#' each element of the list being a section of the INI file.
#' Each element (section) containing the key-value pairs from the INI file.
#' When no value is provided in the file, the value is `""`.
#' By convention, all values returned by the function are of character type.
#' R expressions following `!expr` are evaluated according to the settings of
#' the `R_RCONFIG_EVAL` environment variable or the option `"rconfig.eval"`.
#'
#' @examples
#' inifile <- system.file("examples", "tox.ini", package = "rconfig")
#'
#' ## not evaluating R expressions
#' ini <- rconfig::read_ini(file = inifile)
#' str(ini)
#'
#' ## evaluating R expressions
#' op <- options("rconfig.eval" = TRUE)
#' ini <- rconfig::read_ini(file = inifile)
#' str(ini)
#'
#' # reset options
#' options(op)
#'
#' @name read_ini
#' @export
read_ini <- function(file, ...) {
parse_ini(file, ...)
}

## Parse INI file
## convert to YAML when !expr evaluation needed
## !expr evaluation is governed by do_eval()
parse_ini <- function(x, ...) {
z <- readLines(x)
l <- .parse_ini(z, ...)
if (do_eval()) {
y <- yaml::as.yaml(l)
yaml::yaml.load(y,
eval.expr = FALSE,
handlers = list(
expr = function(x)
eval(parse(text = x), envir = baseenv())))
} else {
l
}
}

## Workhorse function to parse contents of an INI file
## following rules specified at:
## https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
.parse_ini <- function(lines, ...) {

n <- length(lines)
nls <- nchar(lines) - nchar(trimws(lines, "left"))
v <- character(n)
ini <- list()
for (i in seq_len(n)) {
l <- trimws(lines[i])
# blanks
if (grepl("^\\s*$", l)) {
v[i] <- "b"
}
# comments
if (startsWith(l, "#") || startsWith(l, ";")) {
v[i] <- "c"
}
# section headers
if (grepl("^\\[(.*)\\]$", l)) {
section <- regmatches(l, regexec("^\\[(.*)\\]$", l))[[1L]][2L]
if (grepl("]", section) || grepl("\n", section))
stop("Section cannot contain `]` or `\\n`.")
ini[[section]] <- list()
v[i] <- "h"
}
# key-value pairs that are on the same line
if (grepl("^.*=.*$", l) || grepl("^.*:.*$", l)) {
key <- if (grepl("^.*=.*$", l))
"=" else ":"
kv <- strsplit(l, paste0("\\s*", key, "\\s*"))[[1L]]
if (length(kv) == 2L) {
if (identical(kv[1L], ""))
stop(paste0("Key missing on line ", i, "."))
ini[[section]][[kv[1L]]] <- kv[2L]
v[i] <- "k"
}
if (length(kv) == 1L) {
ini[[section]][[kv[1L]]] <- ""
v[i] <- "k"
}
}
if (i == 1L && v[i] != "h")
stop("The 1st line must be a section header.")
if (v[i] == "") {
# keys without value
if (nls[i] == 0L) {
ini[[section]][[l]] <- ""
v[i] <- "k"
} else {
if (nls[i] < nls[i-1L]) {
ini[[section]][[l]] <- ""
v[i] <- "k"
}
if (v[i-1L] == "m" && nls[i] == nls[i-1L]) {
# indented multi-line text
ini[[section]][[length(ini[[section]])]] <- c(ini[[section]][[length(ini[[section]])]], l)
v[i] <- "m"
}
if (v[i-1L] == "k" && nls[i] > nls[i-1L]) {
# indented multi-line text
ini[[section]][[length(ini[[section]])]] <- c(ini[[section]][[length(ini[[section]])]], l)
v[i] <- "m"
}
}
}
}
ini
}
2 changes: 2 additions & 0 deletions RELEASE.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ urlchecker::url_check()
devtools::document()
devtools::check()

devtools::install()

# multi-arch checks
library(rhub)
#validate_email("peter@analythium.io")
Expand Down
47 changes: 47 additions & 0 deletions inst/examples/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[Simple Values]
key=value
spaces in keys=allowed
spaces in values=allowed as well
spaces around the delimiter = obviously
you can also use : to delimit keys from values

[All Values Are Strings]
values like this: 1000000
or this: 3.14159265359
are they treated as numbers? : no
integers, floats and booleans are held as: strings
can use the API to get converted values directly: true

[Multiline Values]
chorus: I'm a lumberjack, and I'm okay
I sleep all night and I work all day

[No Values]
key_without_value
empty string value here =

[You can use comments]
# like this
; or this

# By default only in an empty line.
# Inline comments can be harmful because they prevent users
# from using the delimiting characters as parts of values.
# That being said, this can be customized.

[Sections Can Be Indented]
can_values_be_as_well = True
does_that_mean_anything_special = False
purpose = formatting for readability
multiline_values = are
handled just fine as
long as they are indented
deeper than the first line
of a value
# Did I mention we can indent comments, too?

[R specific pieces]
trials_chr = 5
trials_num = !expr 5
dataset = demo-data.csv
cores = !expr getOption("mc.cores", 1L)
2 changes: 0 additions & 2 deletions man/rconfig.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions man/read_ini.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 93f2a11

Please sign in to comment.