diff --git a/README.md b/README.md index f070116..55620fd 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The following commands are available: - `get [
[ [-v|--value-only]]]` — retrieve data. - `exists
[]` — check if a section or a property exists. - `set
` — set a property's value. -- `replace
` — set a property's value if it has a particular value. +- `replace
` — replace the first occurrence of `` with `` in the property's value. Empty `` matches empty values. - `delete
[]` — delete a section or a property. - `help` — print the help message. - `version` — print the version number. @@ -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. @@ -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. @@ -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 diff --git a/ini.sml b/ini.sml index 7ff7df7..a0cf41a 100644 --- a/ini.sml +++ b/ini.sml @@ -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 @@ -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. @@ -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 = _}) => @@ -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 @@ -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 @@ -322,9 +370,9 @@ 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 @@ -332,20 +380,6 @@ struct 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 diff --git a/initool.sml b/initool.sml index 4e87fbb..eb24af9 100644 --- a/initool.sml +++ b/initool.sml @@ -79,7 +79,7 @@ val processFileQuiet = processFileCustom true val getUsage = " [
[ [-v|--value-only]]]" val existsUsage = "
[]" val setUsage = "
" -val replaceUsage = "
" +val replaceUsage = "
" val deleteUsage = "
[]" val availableCommands = @@ -101,7 +101,8 @@ val allUsage = , "delete" ^ deleteUsage ]) ^ "\n\n help\n version\n\n" ^ "Each command can be abbreviated to its first letter. " - ^ "
, , and can be '*' or '_' to match anything.") + ^ "
, , and can be '*' or '_' to match anything. " + ^ "Empty matches empty values.") fun formatArgs (args: string list) = let @@ -147,8 +148,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 @@ -162,8 +165,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 @@ -199,7 +204,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 @@ -228,20 +233,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 @@ -270,7 +275,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 diff --git a/tests/replace-part.command b/tests/replace-part.command new file mode 100644 index 0000000..2982a0f --- /dev/null +++ b/tests/replace-part.command @@ -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 diff --git a/tests/replace-part.ini b/tests/replace-part.ini new file mode 100644 index 0000000..5472f33 --- /dev/null +++ b/tests/replace-part.ini @@ -0,0 +1,3 @@ +key=A longer value. +another-key=ABAABBAAABBB +empty= diff --git a/tests/replace-part.result b/tests/replace-part.result new file mode 100644 index 0000000..b45273f --- /dev/null +++ b/tests/replace-part.result @@ -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 diff --git a/tests/replace.command b/tests/replace-whole.command similarity index 100% rename from tests/replace.command rename to tests/replace-whole.command diff --git a/tests/replace.result b/tests/replace-whole.result similarity index 100% rename from tests/replace.result rename to tests/replace-whole.result