Skip to content

Instantly share code, notes, and snippets.

@beccasaurus
Last active September 20, 2019 05:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save beccasaurus/4b94f57e259f1947bd420e5811261a0a to your computer and use it in GitHub Desktop.
Save beccasaurus/4b94f57e259f1947bd420e5811261a0a to your computer and use it in GitHub Desktop.
.ini files
# This is a simple, clean configuration file.
#
# No errors were intentionally added to this file.
# For error case checking, see: edgecases.ini.
#
# ⚠️ If you change any of these values the tests will _surely_ explode! 💥
#
# Consult the README for tests on running the innie test suite.
[Parker]
breed = Pomeranian Dashund Mix
favorite toy = Duck
second favorite = Fox
favorite activity = pushing
[Lander]
breed = American Pitbull Terrier
favorite toy = Racquetball
second favorite = Kong
favorite activity = chewing
[Murdoch]
breed = Australian Shepherd Mix
favorite activity = licking
[Murphy]
breed = Golden Retriever
favorite toy = Rope
favorite activity = drooling
# Edge-cases!
#
# This file is meant to _test_ the parser.
#
# It should always be safe to add a new [section] to this file.
# We do not assert that the section names are _exactly_ equal to anything.
;
[ Hey, s’up? ]
not too much really =
;
source "https://rubygems.org"
gemspec
#! /usr/bin/env ruby
require_relative "./innie"
puts ".innie version #{Innie::VERSION.gsub '0', 'o'}"
Gem::Specification.new do |gem|
gem.name = "innie"
gem.version = "0.1.0"
gem.authors = ["Rebecca Taylor"]
gem.email = "bex@beccasaur.us"
gem.summary = "Manipulate INI files"
gem.description = "Manipulate INI files"
gem.license = "Apache-2.0"
gem.files = ["innie.rb"]
gem.require_path = "."
gem.bindir = "."
gem.executable = "innie"
gem.add_development_dependency "rake"
gem.add_development_dependency "rspec"
end
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
module Innie
VERSION = "0.1.0"
def self.load_file path
Innie::IniFile.new path
end
class IniFile
attr_reader :path
def initialize path = nil
@path = path
end
def section_names
ini_hash.keys
end
def section section_name
ini_hash[section_name]
end
alias [] section
def add_section section_name, properties
ini_hash[section_name] = properties
end
def remove_section section_name
ini_hash.delete section_name
end
def get section_name, property_name
ini_hash[section_name][property_name]
end
def set section_name, property_name, property_value
ini_hash[section_name][property_name] = property_value
end
def properties section_name
ini_hash[section_name].keys
end
def remove section_name, property_name = nil
if property_name
ini_hash[section_name].delete property_name
else
ini_hash.delete section_name
end
end
alias rm remove
def to_hash
ini_hash
end
alias to_h to_hash
def reload!
@ini_hash = IniParser.ini_to_hash path
self
end
# ! Sorry ! For now all comments and formatting is blown away 🌬
def save!
ini_text = StringIO.new
section_names.each do |section_name|
ini_text.puts "[#{section_name}]"
section(section_name).each do |property_name, property_value|
ini_text.puts "#{property_name} = #{property_value}"
end
end
ini_text = ini_text.string
File.write path, ini_text
self
end
private
def ini_hash
reload! if @ini_hash.nil?
@ini_hash
end
end
class IniParser
EMPTY_LINE_PATTERN = /^\s*$/
COMMENT_LINE_PATTERN = /^\s*[#;]/
SECTION_NAME_PATTERN = /^\[(?<section_name>.*)\]$/
PROPERTY_LINE_PATTERN = /^(?<property_name>[^=]+)=(?<property_value>.*)$/
def self.ini_to_hash path
hash = {}
current_section_name = nil
File.read(path).each_line.with_index do |line, line_number|
next if line =~ EMPTY_LINE_PATTERN
next if line =~ COMMENT_LINE_PATTERN
if line =~ SECTION_NAME_PATTERN
current_section_name = $~[:section_name].strip
hash[current_section_name] = {}
next
end
if current_section_name.nil?
raise ".ini must begin with a section (ignoring whitespace/comments)"
end
if line =~ PROPERTY_LINE_PATTERN
property_name = $~[:property_name].strip
property_value = $~[:property_value].strip
hash[current_section_name][property_name] = property_value
next
end
raise "Unrecognized .ini line: [#{line_number}] #{line.inspect}"
end
hash
end
end
end
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "innie"
module Innie
module CLI
def self.run args = ARGV
end
class Command
end
module Commands
class ShowSection < Command
end
end
class Result
attr_accessor :status, :stdout, :stderr
def output
stdout + stderr
end
end
end
end
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "innie_cli"
require "rspec"
describe "CLI for configuration using .ini files" do
example "calling the innie CLI" do
expect(`innie --version`).to eq ".innie version o.1.o\n"
end
# We'll use the commands from elsewhere, we won't really use this CLI.
example "calling the innie CLI using the library" do
result = Innie::CLI.run %w[ innie --version ]
expect(result.output).to eq ".innie version o.1.o\n"
end
# $ innie section[s] /path/to/config.ini [list]
#
# Alternatively, you can export the INI_FILE environment variable with a
# value of the path to your local .ini file. The command will run in that
# context to read/write your .ini file.
#
# $ export INI_FILE=/path/to/config
# $ innie sections
#
example "list section names"
# $ innie details
example "list sections with content"
# $ innie section foo
example "show section (including name)"
# $ innie get foo bar
example "get property value"
# $ innie set foo bar baz
example "set property value"
# $ innie rm foo bar
example "remove property"
# $ innie rm foo
example "remove section"
# $ innie file[s]
example "list configuration files available [in its default directory]"
end
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "innie"
require "rspec"
require "tempfile"
describe "Reading and writing .ini files" do
def load_ini filename, &block
raise "load_ini requires a block to run with provided .ini" if block.nil?
fixture_ini = File.expand_path filename, __dir__
ini_file = Tempfile.new "innie.test.ini"
ini_file.write File.read(fixture_ini)
ini_file.rewind
@ini = Innie.load_file(ini_file.path)
block.call
ini_file.close
ini_file.unlink
end
# Tests using the [dogs.ini] fixture file
context "dogs.ini" do
around {|example| load_ini("dogs.ini") { example.run }}
example "get section names" do
expect(@ini.section_names).to eq ["Parker", "Lander", "Murdoch", "Murphy"]
end
example "get section (as Hash)" do
parker = @ini["Parker"]
expect(parker.keys).to eq ["breed", "favorite toy", "second favorite",
"favorite activity"]
end
example "get entire ini as Hash" do
hash = @ini.to_hash
expect(hash.keys).to eq ["Parker", "Lander", "Murdoch", "Murphy"]
parker = hash["Parker"]
expect(parker.keys).to eq ["breed", "favorite toy", "second favorite",
"favorite activity"]
end
example "#get and #set and #remove #properties" do
# Change existing value with #set
expect(@ini.get "Parker", "breed").to eq "Pomeranian Dashund Mix"
@ini.set "Parker", "breed", "New value"
expect(@ini.get "Parker", "breed").to eq "New value"
# Add a new property with #set
expect(@ini.get "Parker", "favorite food").to be nil
@ini.set "Parker", "favorite food", "Cubes"
expect(@ini.get "Parker", "favorite food").to eq "Cubes"
# #remove properties
expect(@ini.properties "Parker").to include "breed"
expect(@ini.properties "Parker").to include "favorite food"
@ini.remove "Parker", "breed"
expect(@ini.properties "Parker").not_to include "breed"
expect(@ini.properties "Parker").to include "favorite food"
@ini.rm "Parker", "favorite food"
expect(@ini.properties "Parker").not_to include "breed"
expect(@ini.properties "Parker").not_to include "favorite food"
# #rm an entire section
expect(@ini.section_names).to include "Lander"
expect(@ini.section_names).to include "Parker"
@ini.rm "Lander"
expect(@ini.section_names).not_to include "Lander"
expect(@ini.section_names).to include "Parker"
end
example "change existing property value" do
@ini.reload!
expect(@ini["Parker"]["favorite toy"]).to eq "Duck"
# Confirm that we can change the values of @ini (Hash)
@ini["Parker"]["favorite toy"] = "Changed"
expect(@ini["Parker"]["favorite toy"]).to eq "Changed"
# Confirm that #reload blows away our changes
@ini.reload!
expect(@ini["Parker"]["favorite toy"]).to eq "Duck"
# Persist it!
@ini["Parker"]["favorite toy"] = "Changed"
@ini.save!
# Confirm that #save! didn't change the value
expect(@ini["Parker"]["favorite toy"]).to eq "Changed"
# Confirm that #reload loads our persisted changes
@ini.reload!
expect(@ini["Parker"]["favorite toy"]).to eq "Changed"
end
example "add new section" do
expect(@ini.section_names).to_not include "Cool new section"
@ini.add_section "Cool new section", "Foo" => "Bar", hi: "there"
expect(@ini.section_names).to include "Cool new section"
# If we reload it should disappear because add_section does not persist.
expect(@ini.reload!.section_names).not_to include "Cool new section"
@ini.add_section "Cool new section", "Foo" => "Bar", hi: "there"
# But if we save! first, we can reload! and the new section will be there.
expect(@ini.save!.reload!.section_names).to include "Cool new section"
expect(@ini["Cool new section"].keys).to eq ["Foo", "hi"]
# Check some of the contents of the actual .ini file
ini_text = File.read @ini.path
expect(ini_text).to include "[Cool new section]"
expect(ini_text).to include "Foo = Bar"
expect(ini_text).to include "hi = there"
end
example "delete section" do
expect(@ini.reload!.section_names).to include "Murphy"
@ini.remove_section "Murphy"
expect(@ini.section_names).not_to include "Cool new section"
# If we reload it should reappear because remove_section does not persist.
expect(@ini.reload!.section_names).to include "Murphy"
@ini.remove_section "Murphy"
# But if we save! first, we can reload! and the section will be gone.
expect(@ini.save!.reload!.section_names).not_to include "Murphy"
end
end
context "edgecases.ini" do
around {|example| load_ini("edgecases.ini") { example.run }}
example "Hey, s’up?" do
expect(@ini.section_names).to include "Hey, s’up?"
expect(@ini["Hey, s’up?"]).to eq({ "not too much really" => "" })
end
end
end
# Copyright 2019 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
begin
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec) do |task|
task.pattern = '*_spec.rb'
end
rescue LoadError
end
task default: :spec
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment