diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21d4f54..d9c2ad3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,21 @@ on: - main jobs: Test: - runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} steps: - name: Check out repository code uses: actions/checkout@v4 - - run: ./tests/_run.sh + - name: Install newer bash on macOS + if: matrix.os == 'macos-latest' + run: brew install bash + - name: Run tests + run: | + if [[ "$(uname)" == "Darwin" ]]; then + /opt/homebrew/bin/bash ./tests/_run.sh + else + bash ./tests/_run.sh + fi - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/flex_ini.sh b/flex_ini.sh index 80f8294..8142e89 100644 --- a/flex_ini.sh +++ b/flex_ini.sh @@ -3,32 +3,43 @@ # You may change these here or overwrite them # within your scripts, whatever works best for # your use case. - auto_create_ini_on_load=true auto_save_on_changes=false back_up_changes_on_save=true back_up_changes_on_save_as=false reassign_file_permissions_when_possible=false tmp_directory="/tmp" +# Detect the operating system +if [[ "$(uname)" == "Darwin" ]]; then + _OS="macos" +elif [[ "$(uname)" == "Linux" ]]; then + _OS="linux" +else + _OS="unknown" +fi + +# Require bash 4.0+ for associative arrays +if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then + echo "Error: FlexIni requires bash 4.0 or higher for associative arrays" + echo "Current bash version: ${BASH_VERSION}" + echo "On macOS, install newer bash with: brew install bash" + exit 1 +fi declare -gA ini_associations declare -gA ini_unsaved_changes declare -gA ini_loaded - # Private Functions # -- # It's best to not call/modify these directly from your codebase # unless you know what you're doing! - # @private private_flex_ini_error # -- # Helper method to write consistent error logs private_flex_ini_error() { local msg="$1" - echo "[ FlexIni Error ] $msg" } - # @private private_flex_ini_mark_as_changed # -- # Marks an ini array as changed. @@ -36,7 +47,6 @@ private_flex_ini_mark_as_changed() { local ini_identifier=$(private_flex_ini_format_id "$1") ini_unsaved_changes["$ini_identifier"]=true } - # @private private_flex_ini_mark_as_unchanged # -- # Marks an ini array as unchanged. @@ -44,7 +54,6 @@ private_flex_ini_mark_as_unchanged() { local ini_identifier=$(private_flex_ini_format_id "$1") ini_unsaved_changes["$ini_identifier"]=false } - # @private private_flex_ini_required # -- # Helper function to identify required variables and return 1 @@ -52,26 +61,22 @@ private_flex_ini_mark_as_unchanged() { private_flex_ini_required() { local k="$1" local v="$2" - if [ -z "$v" ]; then private_flex_ini_error "value $k is required but was not provided" return 1 fi } - # @private private_flex_ini_require_loaded # -- # Function which returns 1 if the ini id has not # yet beenloaded. private_flex_ini_require_loaded() { local ini_identifier="$1" - if ! private_flex_ini_has_been_loaded "$ini_identifier"; then private_flex_ini_error "the ini id $ini_identifier has not-yet been loaded" return 1 fi } - # @private private_flex_ini_mark_as_loaded # -- # Marks an ini array as already loaded. @@ -79,14 +84,12 @@ private_flex_ini_mark_as_loaded() { local ini_identifier=$(private_flex_ini_format_id "$1") ini_loaded["$ini_identifier"]=true } - # @private private_flex_ini_has_been_loaded # -- # Returns 0 if the array has already been loaded, # returns 1 if it has not. private_flex_ini_has_been_loaded() { local ini_identifier=$(private_flex_ini_format_id "$1") - local loaded="${ini_loaded[$ini_identifier]}" if [ "$loaded" == "true" ]; then @@ -95,7 +98,6 @@ private_flex_ini_has_been_loaded() { return 1 fi } - # @private private_flex_ini_format_id # -- # Format the supplied ini id to make sure it's @@ -103,37 +105,29 @@ private_flex_ini_has_been_loaded() { private_flex_ini_format_id() { local default_ini_array_name="default" local ini_identifier="${1:-$default_ini_array_name}" - local formatted="${ini_identifier// /_}" echo "$formatted" | tr - _ } - # @private private_flex_ini_get_array_name # -- # Get the name of the array based on the ini id. private_flex_ini_get_array_name() { local ini_identifier=$(private_flex_ini_format_id "$1") local ini_name="${ini_identifier}_ini" - echo "$ini_name" } - # @private private_get_ini_file_path # -- # Get the file path for a specific ini id. private_get_ini_file_path() { local ini_identifier=$(private_flex_ini_format_id "$1") - local FILE_PATH="${ini_associations[$ini_identifier]}" - if [ -z "$FILE_PATH" ]; then log error "Error, no file associated with the ini id ${ini_identifier}" return 1 fi - echo "${FILE_PATH}" } - # @private private_flex_ini_init # -- # Initialize an ini file and load it into its array. This @@ -142,14 +136,10 @@ private_get_ini_file_path() { private_flex_ini_init() { local ini_file="$1" local ini_identifier=$(private_flex_ini_format_id "$2") - local ini=$(private_flex_ini_get_array_name "$ini_identifier") - declare -gA "$ini" - ini_associations["$ini_identifier"]="$ini_file" } - # @private private_flex_ini_create # -- # Create an ini file at the specified path. @@ -157,15 +147,12 @@ private_flex_ini_create() { local ini_file="$1" private_flex_ini_required "ini_file" "$ini_file" || return 1 - touch "$ini_file" || return 1 } - # Public Functions # -- # These functions are the ones you want to call from your scripts # since they are meant to work together and validate various items. - # @public flex_ini_load # -- # You'll need to call this method first in order to load up your @@ -174,37 +161,31 @@ flex_ini_load() { local ini_file="$1" local ini_identifier=$(private_flex_ini_format_id "$2") local force_reload="${3:-false}" - if [ "$force_reload" != "true" ] && private_flex_ini_has_been_loaded "$ini_identifier"; then return 0 fi - if [ "$force_reload" == "true" ] && private_flex_ini_has_been_loaded "$ini_identifier"; then flex_ini_clear "$ini_identifier" fi - if [ ! -f "$ini_file" ]; then if [ "$auto_create_ini_on_load" == "true" ]; then if ! private_flex_ini_create "$ini_file"; then - flex_ini_error "ini file could not be auto-created at ${ini_file}" + private_flex_ini_error "ini file could not be auto-created at ${ini_file}" return 1 fi else - flex_ini_error "ini file not found at ${ini_file} and auto_create_ini_on_load was not 'true' so we did not try to create it" + private_flex_ini_error "ini file not found at ${ini_file} and auto_create_ini_on_load was not 'true' so we did not try to create it" return 1 fi fi - private_flex_ini_init "$ini_file" "$ini_identifier" local ini=$(private_flex_ini_get_array_name "$ini_identifier") - local section="" local key="" local value="" local section_regex="^\[(.+)\]" local key_regex="^([^ =]+) *= *(.*) *$" local comment_regex="^;" - while IFS= read -r line; do if [[ $line =~ $comment_regex ]]; then continue @@ -218,11 +199,9 @@ flex_ini_load() { eval "$name_var" fi done <"$ini_file" - private_flex_ini_mark_as_unchanged "$ini_identifier" private_flex_ini_mark_as_loaded "$ini_identifier" } - # @public flex_ini_reload # -- # Reload an already-loaded ini ID. Please note, if you have not, in @@ -230,16 +209,13 @@ flex_ini_load() { flex_ini_reload() { local ini_identifier=$(private_flex_ini_format_id "$1") local destination_ini_path=$(private_get_ini_file_path "$ini_identifier") - if [ -z "$destination_ini_path" ]; then echo "[ Flex INI Error ] No INI filepath could be found from which to reload. Are you sure you loaded the config file for ${ini_identifier}?" + return 1 fi - flex_ini_clear "$ini_identifier" - flex_ini_load "$destination_ini_path" "$ini_identifier" } - # @public flex_ini_clear # -- # Clears an ini ID completely from our loaded configs. You @@ -247,16 +223,12 @@ flex_ini_reload() { # during debugging. flex_ini_clear() { local ini_identifier=$(private_flex_ini_format_id "$1") - private_flex_ini_require_loaded "$ini_identifier" || return 1 - unset ini_unsaved_changes["$ini_identifier"] unset ini_loaded["$ini_identifier"] - local current_array_name=$(private_flex_ini_get_array_name "$ini_identifier") unset "$current_array_name" } - # @public flex_ini_get # -- # Fetches a value from the specified ini array. @@ -264,27 +236,21 @@ flex_ini_get() { local key="$1" local ini_identifier=$(private_flex_ini_format_id "$2") local array_name=$(private_flex_ini_get_array_name "$ini_identifier") - private_flex_ini_required "key" "$key" || return 1 - local name_var='${'$array_name'['$key']}' local value=$(eval echo "$name_var") echo "$value" } - # @public flex_ini_has # -- # Returns 0 if the key exists, 1 if it does not. flex_ini_has() { local key="$1" local ini_identifier=$(private_flex_ini_format_id "$2") - private_flex_ini_required "key" "$key" || return 1 private_flex_ini_require_loaded "$ini_identifier" || return 1 - [[ $(flex_ini_get "$key" "$ini_identifier") ]] } - # @public flex_ini_update # -- # This creates or updates a value in your ini array. @@ -295,21 +261,17 @@ flex_ini_update() { local value="$2" local ini_identifier=$(private_flex_ini_format_id "$3") local array_name=$(private_flex_ini_get_array_name "$ini_identifier") - private_flex_ini_required "key" "$key" || return 1 private_flex_ini_require_loaded "$ini_identifier" || return 1 - local assign_var=$array_name'["'$key'"]="'"${value}"'"' eval "$assign_var" || return 1 - if [ "$auto_save_on_changes" == "true" ]; then flex_ini_save "$ini_identifier" else private_flex_ini_mark_as_changed "$ini_identifier" fi } - # @public flex_ini_delete # -- # Remove a value from your ini array. @@ -318,21 +280,17 @@ flex_ini_update() { flex_ini_delete() { local key="$1" local ini_identifier=$(private_flex_ini_format_id "$2") - private_flex_ini_required "key" "$key" || return 1 private_flex_ini_require_loaded "$ini_identifier" || return 1 - local array_name=$(private_flex_ini_get_array_name "$ini_identifier") local assign_var='unset '$array_name'["'$key'"]' eval "$assign_var" - - if [ "$auto_save_on_changes" == "true" ]; then + if [ "$auto_save_on_changes" == "true" ]; then flex_ini_save "$ini_identifier" else private_flex_ini_mark_as_changed "$ini_identifier" fi } - # @public flex_ini_save # -- # Save the values in the array back to the correct file. @@ -342,9 +300,7 @@ flex_ini_save() { local ini_identifier=$(private_flex_ini_format_id "$1") local override_path="$2" local default_destination_ini_path=$(private_get_ini_file_path "$ini_identifier") - private_flex_ini_require_loaded "$ini_identifier" || return 1 - # The save-as logic path first if [ -n "$override_path" ]; then # pre-create the file if it doesn't exist just to make sure we @@ -355,7 +311,6 @@ flex_ini_save() { return 1 fi fi - # set the destination ini path to whichever new path was specified local destination_ini_path="$override_path" # use the val from 'save as' to determine whether to back up @@ -368,33 +323,34 @@ flex_ini_save() { local should_back_up_before_save="${back_up_changes_on_save}" local is_save_as_operation=false fi - # If param is true, then fetch the user/group from the current ini # file and attempt to re-assign ownership when file is saved if # the current user is different from the owner of the file if [ "$reassign_file_permissions_when_possible" == "true" ]; then - local file_owner=$(stat --format '%U' "${destination_ini_path}") - local file_group=$(stat --format '%G' "${destination_ini_path}") + if [ "$_OS" == "macos" ]; then + local file_owner=$(stat -f '%Su' "${destination_ini_path}") + local file_group=$(stat -f '%Sg' "${destination_ini_path}") + elif [ "$_OS" == "linux" ]; then + local file_owner=$(stat --format '%U' "${destination_ini_path}") + local file_group=$(stat --format '%G' "${destination_ini_path}") + else + echo "[ FlexIni Warning ] File permission reassignment not supported on $_OS" + fi local current_user=$(echo "$USER") if [ "$file_owner" != "$current_user" ]; then local should_reassign_file_permissions=true fi fi - local current_section="" local has_free_keys=false - local ini_file=$(mktemp "${tmp_directory}/flexini.XXXXXX") - for key in $(flex_ini_keys "$ini_identifier"); do [[ $key == *.* ]] && continue has_free_keys=true local value=$(flex_ini_get "$key" "$ini_identifier") echo "$key = $value" >>"$ini_file" done - [[ "${has_free_keys}" == "true" ]] && echo >>"$ini_file" - # Collect all section names and sort them declare -A sections for key in $(flex_ini_keys "$ini_identifier"); do @@ -402,7 +358,6 @@ flex_ini_save() { IFS="." read -r section_name key_name <<<"$key" sections["$section_name"]=1 done - # Process sections in alphabetical order for section_name in $(printf '%s\n' "${!sections[@]}" | sort); do # Check if this is a new section @@ -411,7 +366,6 @@ flex_ini_save() { echo "[$section_name]" >>"$ini_file" current_section="$section_name" fi - # Collect and sort keys for this section only section_keys=() # Clear the array first for key in $(flex_ini_keys "$ini_identifier"); do @@ -421,7 +375,6 @@ flex_ini_save() { section_keys+=("$key") fi done - # Sort keys within this section and write them for key in $(printf '%s\n' "${section_keys[@]}" | sort); do IFS="." read -r key_section key_name <<<"$key" @@ -429,44 +382,36 @@ flex_ini_save() { echo "$key_name = $value" >>"$ini_file" done done - # Back up the destination ini file if specified if [ "$should_back_up_before_save" == "true" ]; then cp -f "$destination_ini_path" "${destination_ini_path}.bak" || private_flex_ini_error "could not make the backup copy of the ini file at ${destination_ini_path}" fi - # Replace the destination ini file with the tmp file if ! mv -f "$ini_file" "$destination_ini_path"; then private_flex_ini_error "could not move tmp ini file to its destination at ${destination_ini_path} -- data was not saved to disk" return 1 fi - # Try to update the permissions of the file, if set if [ "$should_reassign_file_permissions" == "true" ]; then chown "${file_owner}":"${file_group}" "${destination_ini_path}" || private_flex_ini_error "our attempt to reassign file permissions to '${file_owner}:${file_group} failed for file at ${destination_ini_path}" fi - # Only mark the ini as unchanged if we saved it to # the original file. if [ "$is_save_as_operation" != "true" ]; then private_flex_ini_mark_as_unchanged "$ini_identifier" fi } - # @public flex_ini_save_as # -- # A helper function to initiate a save-as. flex_ini_save_as() { local override_path="$1" local ini_identifier=$(private_flex_ini_format_id "$2") - private_flex_ini_require_loaded "$ini_identifier" || return 1 - flex_ini_save "$ini_identifier" "$override_path" || return 1 } - # @public flex_ini_has_unsaved # -- # Returns 0 if the array has unsaved changes, @@ -475,16 +420,13 @@ flex_ini_has_unsaved() { local ini_identifier=$(private_flex_ini_format_id "$1") private_flex_ini_require_loaded "$ini_identifier" || return 1 - local has_unsaved="${ini_unsaved_changes["$ini_identifier"]}" - if [ "$has_unsaved" == "true" ]; then return 0 else return 1 fi } - # @public flex_ini_reset # -- # Removes all data from the stores. Be careful with this one! @@ -494,36 +436,28 @@ flex_ini_reset() { unset "$array_name" unset ini_associations["$i"] done - for i in "${!ini_unsaved_changes[@]}"; do unset ini_unsaved_changes["$i"] done - for i in "${!ini_loaded[@]}"; do unset ini_loaded["$i"] done } - # @public flex_ini_show # -- # Show all loaded key-value pairs (whether or not they've been saved). flex_ini_show() { local ini_identifier=$(private_flex_ini_format_id "$1") local file_path=$(private_get_ini_file_path "$ini_identifier") - private_flex_ini_require_loaded "$ini_identifier" || return 1 - echo "[ ${ini_identifier} ]" echo "--" - for key in $(flex_ini_keys "$ini_identifier"); do local value=$(flex_ini_get "$key" "$ini_identifier") echo "$key = $value" done - echo "" } - # @public flex_ini_keys # -- # Get an array of all keys in your ini array (whether or not they) @@ -531,10 +465,8 @@ flex_ini_show() { flex_ini_keys() { local ini_identifier=$(private_flex_ini_format_id "$1") local array_name=$(private_flex_ini_get_array_name "$ini_identifier") - private_flex_ini_require_loaded "$ini_identifier" || return 1 - local name_var='${!'$array_name'[@]}' local keys=($(eval echo "$name_var")) for a in "${keys[@]}"; do echo "$a"; done | sort -} +} \ No newline at end of file diff --git a/tests/_run.sh b/tests/_run.sh index 4972ad6..bba4113 100755 --- a/tests/_run.sh +++ b/tests/_run.sh @@ -6,8 +6,8 @@ echo "" test_suites=() cd $test_parent_dir/src -for f in *; do - fn_name="${f/.sh/''}" +for f in *.sh; do + fn_name="${f%.sh}" test_suites+=("$fn_name") done cd ../.. diff --git a/tests/src/test_flex_ini_keys.sh b/tests/src/test_flex_ini_keys.sh index 10dcc12..f36bb41 100644 --- a/tests/src/test_flex_ini_keys.sh +++ b/tests/src/test_flex_ini_keys.sh @@ -5,7 +5,8 @@ test_flex_ini_keys() { flex_ini_update "testing" "two" flex_ini_update "tested" "three" - local res=($(flex_ini_keys)) + local res + readarray -t res < <(flex_ini_keys) expect "${#res[@]}" 3 expect_array_contains "test" "${res[@]}" diff --git a/tests/src/test_flex_ini_save_macos_compatibility.sh b/tests/src/test_flex_ini_save_macos_compatibility.sh new file mode 100644 index 0000000..01b400a --- /dev/null +++ b/tests/src/test_flex_ini_save_macos_compatibility.sh @@ -0,0 +1,53 @@ +test_flex_ini_save_macos_compatibility() { + local ini_one=$(create_ini) + + flex_ini_load "$ini_one" + flex_ini_update "test_key" "test_value" + + # Test that save works on both macOS and Linux + # This specifically tests the stat command compatibility + # The code should detect the OS and use appropriate stat flags + flex_ini_save + + # Verify the file was saved correctly + expect "$(flex_ini_get "test_key")" "test_value" + + # Test save_as functionality as well + local save_as_loc="${test_storage_dir}/macos_test_$$" + flex_ini_save_as "$save_as_loc" + + # Verify the save_as file exists and has correct content + if [ ! -f "$save_as_loc" ]; then + fail "Save as file was not created" + fi + + # Load the save_as file and verify content + flex_ini_load "$save_as_loc" "macos_test" + expect "$(flex_ini_get "test_key" "macos_test")" "test_value" + + # Test the specific stat commands used in the code + local test_file="${test_storage_dir}/stat_test_$$" + touch "$test_file" + + if [ "$_OS" == "macos" ]; then + # Test macOS stat commands + local owner=$(stat -f '%Su' "$test_file") + local group=$(stat -f '%Sg' "$test_file") + echo "macOS stat: owner=$owner, group=$group" + [[ -n "$owner" ]] || fail "macOS stat owner command failed" + [[ -n "$group" ]] || fail "macOS stat group command failed" + elif [ "$_OS" == "linux" ]; then + # Test Linux stat commands + local owner=$(stat --format '%U' "$test_file") + local group=$(stat --format '%G' "$test_file") + echo "Linux stat: owner=$owner, group=$group" + [[ -n "$owner" ]] || fail "Linux stat owner command failed" + [[ -n "$group" ]] || fail "Linux stat group command failed" + else + echo "Unknown OS: $_OS - skipping stat command tests" + fi + + # Clean up + flex_ini_reset + rm -f "$save_as_loc" "$test_file" +}