Skip to content

Commit

Permalink
feat(cli/replace): replace substrings in values
Browse files Browse the repository at this point in the history
  • Loading branch information
dbohdan committed Jun 9, 2024
1 parent 2072ab8 commit 4779345
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 54 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The following commands are available:
- `get <filename> [<section> [<key> [-v|--value-only]]]` — retrieve data.
- `exists <filename> <section> [<key>]` — check if a section or a property exists.
- `set <filename> <section> <key> <value>` — set a property's value.
- `replace <filename> <section> <key> <old-value> <new-value>`set a property's value if it has a particular value.
- `replace <filename> <section> <key> <text> <replacement>`replace the first occurrence of `<text>` with `<replacement>` in the property's value. Empty `<text>` matches empty values.
- `delete <filename> <section> [<key>]` — delete a section or a property.
- `help` — print the help message.
- `version` — print the version number.
Expand All @@ -57,7 +57,7 @@ For `exists`, it reports whether the section or the property exists through its
An INI file consists of properties (`key=value` lines) and sections (designated with a `[section name]` header line).
A property can be at the "top level" of the file (before any section headers) or in a section (after a section header).
To do something with a property, you must give initool the correct section name.
Section names and keys are [case-sensitive](#case-sensitivity) by default, as are old values for the command `replace`.
Section names and keys are [case-sensitive](#case-sensitivity) by default, as is text for the command `replace`.
The global option `-i` or `--ignore-case` makes commands not distinguish between lower-case and upper-case [ASCII](https://en.wikipedia.org/wiki/ASCII) letters "A" through "Z" in section names and keys.

Do not include the square brackets in the section argument.
Expand Down Expand Up @@ -183,7 +183,7 @@ How nonexistent sections and properties are handled depends on the command.
- **Exit status:** 0.
- `replace`
- **Result:** Nothing from the input changes in the output.
- **Exit status:** 0 if the property exists and has the old value, 1 if it doesn't exist or has a different value.
- **Exit status:** 0 if the property exists and its value contains the text, 1 if it doesn't exist or the value doesn't contain the text.
- `delete`
- **Result:** Nothing is removed from the input in the output.
- **Exit status:** 0 if the section or property was deleted, 1 if it wasn't.
Expand Down Expand Up @@ -215,7 +215,7 @@ Initool is [case-sensitive](https://en.wikipedia.org/wiki/Case_sensitivity) by d
This means that it considers `[BOOT]` and `[boot]` different sections and `foo=5` and `FOO=5` properties with different keys.
The option `-i`/`--ignore-case` changes this behavior.
It makes initool treat ASCII letters "A" through "Z" and "a" through "z" as equal
when looking for sections and keys (every command) and values (`replace`).
when looking for sections and keys (every command) and text in values (`replace`).
The case of section names and keys is preserved in the output regardless of the `-i`/`--ignore-case` option.

### Repeated items
Expand Down
104 changes: 69 additions & 35 deletions ini.sml
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ struct
datatype operation =
Noop
| SelectSection of Id.id
| SelectProperty of {section: Id.id, key: Id.id}
| SelectProperty of {section: Id.id, key: Id.id, pattern: Id.id}
| RemoveSection of Id.id
| RemoveProperty of {section: Id.id, key: Id.id}
| UpdateProperty of
{section: Id.id, key: Id.id, oldValue: Id.id, newValue: string}
| ReplaceInValue of
{section: Id.id, key: Id.id, pattern: Id.id, replacement: string}

exception Tokenization of string

Expand Down Expand Up @@ -168,6 +168,41 @@ struct
if concat = "" then "" else concat ^ "\n"
end

fun findSubstring (matcher: string -> string -> bool) (needle: string)
(haystack: string) (start: int) : (int * int) option =
let
val needleSize = String.size needle
val haystackSize = String.size haystack
in
if needleSize = 0 andalso haystackSize = 0 then
SOME (0, 0)
else if needleSize = 0 orelse start + needleSize > haystackSize then
NONE
else if matcher needle (String.substring (haystack, start, needleSize)) then
SOME (start, needleSize)
else
findSubstring matcher needle haystack (start + 1)
end

fun hasSubstring (matcher: string -> string -> bool) (needle: string)
(haystack: string) : bool =
Option.isSome (findSubstring matcher needle haystack 0)

fun replace (matcher: string -> string -> bool) (pattern: Id.id)
(replacement: string) (haystack: string) : string =
case pattern of
Id.Wildcard => replacement
| Id.StrId needle =>
case findSubstring matcher needle haystack 0 of
NONE => haystack
| SOME (i, needleSize) =>
let
val before' = String.substring (haystack, 0, i)
val haystackSize = String.size haystack
val after = String.extract (haystack, i + needleSize, NONE)
in
before' ^ replacement ^ after
end

(* Say whether the item i in section sec should be returned under
* the operation opr.
Expand All @@ -177,17 +212,26 @@ struct
let
val sectionName = #name sec
val matches = Id.same opts
val matcher = (fn a => fn b => matches (Id.StrId a) (Id.StrId b))
in
case (opr, i) of
(Noop, _) => SOME i
| (SelectSection osn, _) =>
if matches osn sectionName then SOME i else NONE
| (SelectProperty {section = osn, key = okey}, Property {key, value = _}) =>
if matches osn sectionName andalso matches okey key then SOME i
| ( SelectProperty {section = osn, key = okey, pattern = pattern}
, Property {key, value}
) =>
if
matches osn sectionName andalso matches okey key
andalso
(case pattern of
Id.Wildcard => true
| Id.StrId substring => hasSubstring matcher substring value)
then SOME i
else NONE
| (SelectProperty {section = _, key = _}, Comment _) => NONE
| (SelectProperty {section = _, key = _}, Empty) => NONE
| (SelectProperty {section = _, key = _}, Verbatim _) => NONE
| (SelectProperty {section = _, key = _, pattern = _}, Comment _) => NONE
| (SelectProperty {section = _, key = _, pattern = _}, Empty) => NONE
| (SelectProperty {section = _, key = _, pattern = _}, Verbatim _) => NONE
| (RemoveSection osn, _) =>
if matches osn sectionName then NONE else SOME i
| (RemoveProperty {section = osn, key = okey}, Property {key, value = _}) =>
Expand All @@ -196,22 +240,26 @@ struct
| (RemoveProperty {section = _, key = _}, Comment _) => SOME i
| (RemoveProperty {section = _, key = _}, Empty) => SOME i
| (RemoveProperty {section = _, key = _}, Verbatim _) => SOME i
| ( UpdateProperty
{section = osn, key = okey, oldValue = ov, newValue = nv}
| ( ReplaceInValue
{ section = osn
, key = okey
, pattern = pattern
, replacement = replacement
}
, Property {key, value}
) =>
if
matches osn sectionName andalso matches okey key
andalso matches ov (Id.StrId value)
then SOME (Property {key = key, value = nv})
else SOME i
| ( UpdateProperty {section = _, key = _, oldValue = _, newValue = _}
if matches osn sectionName andalso matches okey key then
SOME (Property
{key = key, value = replace matcher pattern replacement value})
else
SOME i
| ( ReplaceInValue {section = _, key = _, pattern = _, replacement = _}
, Comment _
) => SOME i
| ( UpdateProperty {section = _, key = _, oldValue = _, newValue = _}
| ( ReplaceInValue {section = _, key = _, pattern = _, replacement = _}
, Empty
) => SOME i
| ( UpdateProperty {section = _, key = _, oldValue = _, newValue = _}
| ( ReplaceInValue {section = _, key = _, pattern = _, replacement = _}
, Verbatim _
) => SOME i
end
Expand All @@ -227,7 +275,7 @@ struct
case opr of
SelectSection osn =>
List.filter (fn sec => Id.same opts osn (#name sec)) ini
| SelectProperty {section = osn, key = _} =>
| SelectProperty {section = osn, key = _, pattern = _} =>
List.filter (fn sec => Id.same opts osn (#name sec)) ini
| RemoveSection osn =>
List.filter (fn sec => not (Id.same opts osn (#name sec))) ini
Expand Down Expand Up @@ -322,30 +370,16 @@ struct
end

fun propertyExists (opts: Id.options) (section: Id.id) (key: Id.id)
(ini: ini_data) =
(pattern: Id.id) (ini: ini_data) =
let
val q = SelectProperty {section = section, key = key}
val q = SelectProperty {section = section, key = key, pattern = pattern}
val sections = select opts q ini
in
List.exists
(fn {contents = (Property _ :: _), name = _} => true | _ => false)
sections
end

fun valueExists (opts: Id.options) (section: Id.id) (key: Id.id)
(value: Id.id) (ini: ini_data) =
let
val q = SelectProperty {section = section, key = key}
val sections = select opts q ini
in
List.exists
(fn {contents, name = _} =>
List.exists
(fn (Property {key = _, value = propValue}) =>
Id.same opts (Id.StrId propValue) value
| _ => false) contents) sections
end

fun removeEmptySections (sections: ini_data) =
List.filter (fn {contents = [], name = _} => false | _ => true) sections
end
34 changes: 19 additions & 15 deletions initool.sml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ val processFileQuiet = processFileCustom true
val getUsage = " <filename> [<section> [<key> [-v|--value-only]]]"
val existsUsage = " <filename> <section> [<key>]"
val setUsage = " <filename> <section> <key> <value>"
val replaceUsage = " <filename> <section> <key> <old-value> <new-value>"
val replaceUsage = " <filename> <section> <key> <text> <replacement>"
val deleteUsage = " <filename> <section> [<key>]"

val availableCommands =
Expand All @@ -101,7 +101,7 @@ val allUsage =
, "delete" ^ deleteUsage
]) ^ "\n\n help\n version\n\n"
^ "Each command can be abbreviated to its first letter. "
^ "<section>, <key>, and <old-value> can be '*' or '_' to match anything.")
^ "<section>, <key>, and <text> can be '*' or '_' to match anything.")

fun formatArgs (args: string list) =
let
Expand Down Expand Up @@ -147,8 +147,10 @@ fun getCommand (opts: options) [_, filename] =
val section = Id.fromStringWildcard section
val key = Id.fromStringWildcard key
val successFn = fn (_, filtered) =>
Ini.propertyExists (idOptions opts) section key filtered
val q = Ini.SelectProperty {section = section, key = key}
Ini.propertyExists (idOptions opts) section key Id.Wildcard filtered
val q =
Ini.SelectProperty
{section = section, key = key, pattern = Id.Wildcard}
val filterFn = fn sections =>
(Ini.removeEmptySections o (Ini.select (idOptions opts) q)) sections
in
Expand All @@ -162,8 +164,10 @@ fun getCommand (opts: options) [_, filename] =
val section = Id.fromStringWildcard section
val key = Id.fromStringWildcard key
val successFn = fn (_, filtered) =>
Ini.propertyExists (idOptions opts) section key filtered
val q = Ini.SelectProperty {section = section, key = key}
Ini.propertyExists (idOptions opts) section key Id.Wildcard filtered
val q =
Ini.SelectProperty
{section = section, key = key, pattern = Id.Wildcard}
val parsed =
((Ini.select (idOptions opts) q) o (Ini.parse (#passThrough opts))
o checkWrongEncoding o readLines) filename
Expand Down Expand Up @@ -199,7 +203,7 @@ fun existsCommand (opts: options) [_, filename, section] =
val section = Id.fromStringWildcard section
val key = Id.fromStringWildcard key
val successFn = fn (parsed, _) =>
Ini.propertyExists (idOptions opts) section key parsed
Ini.propertyExists (idOptions opts) section key Id.Wildcard parsed
in
processFileQuiet (#passThrough opts) successFn (fn x => x) filename
end
Expand Down Expand Up @@ -228,20 +232,20 @@ fun setCommand (opts: options) [_, filename, section, key, value] =
| setCommand opts [] = setCommand opts ["set"]

fun replaceCommand (opts: options)
[_, filename, section, key, oldValue, newValue] =
(* Replace old value with new *)
[_, filename, section, key, pattern, replacement] =
(* Replace pattern in value *)
let
val section = Id.fromStringWildcard section
val key = Id.fromStringWildcard key
val oldValue = Id.fromStringWildcard oldValue
val q = Ini.UpdateProperty
val pattern = Id.fromStringWildcard pattern
val q = Ini.ReplaceInValue
{ section = section
, key = key
, oldValue = oldValue
, newValue = newValue
, pattern = pattern
, replacement = replacement
}
val successFn = fn (parsed, _) =>
Ini.valueExists (idOptions opts) section key oldValue parsed
Ini.propertyExists (idOptions opts) section key pattern parsed
in
processFile (#passThrough opts) successFn
(Ini.select (idOptions opts) q) filename
Expand Down Expand Up @@ -270,7 +274,7 @@ fun deleteCommand (opts: options) [_, filename, section] =
val key = Id.fromStringWildcard key
val q = Ini.RemoveProperty {section = section, key = key}
val successFn = fn (parsed, _) =>
Ini.propertyExists (idOptions opts) section key parsed
Ini.propertyExists (idOptions opts) section key Id.Wildcard parsed
in
processFile (#passThrough opts) successFn
(Ini.select (idOptions opts) q) filename
Expand Down
29 changes: 29 additions & 0 deletions tests/replace-part.command
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
echo -- case-sensitive, match, beginning
"$INITOOL" r tests/replace-part.ini '' key A a && echo success || echo failure

echo -- case-sensitive, match, middle
"$INITOOL" r tests/replace-part.ini '' key 'longer ' '' && echo success || echo failure

echo -- case-sensitive, match, end
"$INITOOL" r tests/replace-part.ini '' key value. string && echo success || echo failure

echo -- case-sensitive, no match, end
"$INITOOL" r tests/replace-part.ini '' key value.. string && echo success || echo failure

echo -- case-insensitive, match, beginning
"$INITOOL" -i r tests/replace-part.ini '' key a a && echo success || echo failure

echo -- case-insensitive, match, middle
"$INITOOL" -i r tests/replace-part.ini '' key 'lOnGeR ' '' && echo success || echo failure

echo -- case-insensitive, match, end
"$INITOOL" -i r tests/replace-part.ini '' key Value. string && echo success || echo failure

echo -- only replace the first occurrence
"$INITOOL" -i r tests/replace-part.ini '' another-key AA GG && echo success || echo failure

echo -- empty text, match
"$INITOOL" -i r tests/replace-part.ini '' empty '' something && echo success || echo failure

echo -- empty text, no match
"$INITOOL" -i r tests/replace-part.ini '' key '' something && echo success || echo failure
3 changes: 3 additions & 0 deletions tests/replace-part.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
key=A longer value.
another-key=ABAABBAAABBB
empty=
50 changes: 50 additions & 0 deletions tests/replace-part.result
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-- case-sensitive, match, beginning
key=a longer value.
another-key=ABAABBAAABBB
empty=
success
-- case-sensitive, match, middle
key=A value.
another-key=ABAABBAAABBB
empty=
success
-- case-sensitive, match, end
key=A longer string
another-key=ABAABBAAABBB
empty=
success
-- case-sensitive, no match, end
key=A longer value.
another-key=ABAABBAAABBB
empty=
failure
-- case-insensitive, match, beginning
key=a longer value.
another-key=ABAABBAAABBB
empty=
success
-- case-insensitive, match, middle
key=A value.
another-key=ABAABBAAABBB
empty=
success
-- case-insensitive, match, end
key=A longer string
another-key=ABAABBAAABBB
empty=
success
-- only replace the first occurrence
key=A longer value.
another-key=ABGGBBAAABBB
empty=
success
-- empty text, match
key=A longer value.
another-key=ABAABBAAABBB
empty=something
success
-- empty text, no match
key=A longer value.
another-key=ABAABBAAABBB
empty=
failure
File renamed without changes.
File renamed without changes.

0 comments on commit 4779345

Please sign in to comment.