diff --git a/.gitignore b/.gitignore index 0c31bc1..72dba90 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,4 @@ host-edit host-clip host-info host-download +host-env diff --git a/Makefile b/Makefile index 8dcac1c..8dafc23 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas # SPDX-License-Identifier: GPL-3.0-or-later -TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download +TOOLS = host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env TEST_BINS = tests/test_host_common.out tests/test_host_command_builders.out tests/test_host_edit_command.out tests/test_host_download_command.out TEST_SCRIPTS = tests/test_package_layout.sh tests/test_ahi_driver_source.sh TESTS = $(TEST_BINS) $(TEST_SCRIPTS) -COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_base64.h src/host_clip_command.h src/host_download_command.h src/host_edit_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h src/uae_pragmas.h +COMMON_HEADERS = src/host_common.h src/host_path.h src/host_capture.h src/host_base64.h src/host_clip_command.h src/host_download_command.h src/host_edit_command.h src/host_env_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h src/uae_pragmas.h PACKAGE = Host-Tools-$(VERSION).lha PACKAGE_ROOT = Host-Tools PACKAGE_DIR ?= build/package @@ -91,10 +91,13 @@ host-info: src/host-info.c $(COMMON_HEADERS) host-download: src/host-download.c $(COMMON_HEADERS) $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-download.c -o $@ +host-env: src/host-env.c $(COMMON_HEADERS) + $(CC) $(CFLAGS) $(VERFLAGS) $(INCLUDES) src/host-env.c -o $@ + tests/test_host_common.out: tests/test_host_common.c src/host_common.h $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_common.c -o $@ -tests/test_host_command_builders.out: tests/test_host_command_builders.c src/host_base64.h src/host_clip_command.h src/host_common.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h +tests/test_host_command_builders.out: tests/test_host_command_builders.c src/host_base64.h src/host_clip_command.h src/host_common.h src/host_env_command.h src/host_info_command.h src/host_notify_command.h src/host_powershell.h src/host_reveal_command.h $(HOST_CC) $(HOST_NATIVE_FLAGS) $(HOST_CFLAGS) tests/test_host_command_builders.c -o $@ tests/test_host_edit_command.out: tests/test_host_edit_command.c src/host_edit_command.h src/host_common.h diff --git a/README.md b/README.md index 8a0b909..9350709 100644 --- a/README.md +++ b/README.md @@ -115,13 +115,27 @@ host-info host-download [] [FORCE] ``` +### 11. host-env +**Get and set host user environment variables.** +`host-env` reads, writes, removes, and lists environment variables on the host. On Windows it updates the user's persistent environment. On Linux and macOS it writes persistent values to `$HOME/.host-tools-env`; source that file from your host shell startup files if you want future host login shells to import those values automatically. +- **Persistent**: Changes are intended for future host processes. +- **Scoped Safely**: Existing host shells, desktop apps, and Amiberry's parent process cannot have their live environment changed by a child process. + +**Usage:** +```shell +host-env get NAME +host-env set NAME VALUE +host-env unset NAME +host-env list +``` + --- ## Requirements - **Amiberry v6.0+** (or a version with updated `uaelib` support). - "Native Code" execution must be enabled in Amiberry settings. -- All tools work on **Linux and macOS hosts**. On **Windows hosts**, `host-path`, `host-download`, `host-clip`, `host-reveal`, and `host-info` are supported with a current Amiberry (PowerShell and Explorer handle the Windows side; `curl.exe` ships with Windows 10 and later). The remaining tools (`host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify`) currently require a Linux or macOS host, since their host commands run through the POSIX shell. +- All tools work on **Linux and macOS hosts**. On **Windows hosts**, `host-path`, `host-download`, `host-clip`, `host-reveal`, `host-info`, and `host-env` are supported with a current Amiberry (PowerShell and Explorer handle the Windows side; `curl.exe` ships with Windows 10 and later). The remaining tools (`host-run`, `host-multiview`, `host-shell`, `host-edit`, `host-notify`) currently require a Linux or macOS host, since their host commands run through the POSIX shell. - For status-aware tools (`host-reveal`, `host-notify`, `host-clip`, and `host-info`), a newer Amiberry build with the `HostShell_Status` trap reports host command failures immediately. Older builds still work, but use timeout-based completion detection. - Linux desktop integration uses `xdg-utils` (`xdg-open`, `xdg-mime`) and GTK's `gtk-launch` when available. Notifications use `notify-send`; clipboard support uses `wl-clipboard`, `xclip`, or `xsel`; file selection in `host-reveal` uses `gdbus` when present. Character set conversion uses `iconv` when present. - `host-download` uses the host's `curl` or `wget` (`curl.exe` on Windows). Live streaming progress requires an Amiberry build with pipe-based HostShell sessions; on older Linux and macOS builds the tool falls back to a two-phase transfer that downloads on the host first. @@ -138,7 +152,7 @@ All tools follow AmigaDOS conventions: `0` on success, `10` (`RETURN_ERROR`) whe The installer shows a component checklist for the command tools, AmigaGuide documentation, and the UAE and UAESND AHI audio drivers. Before replacing an existing versioned file, it shows the installed and package versions and asks whether to replace or skip it. Files without version strings are still checked for existence and ask before overwrite. It does not edit startup files, system settings, AHI preferences, or existing driver configuration. -For manual installation, copy the binaries (`host-run`, `host-multiview`, `host-shell`, `host-path`, `host-reveal`, `host-notify`, `host-edit`, `host-clip`, `host-info`) from the package `C` drawer to `C:` or anywhere in your system path. To install the UAE AHI driver manually, copy `Devs/AHI/uae.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAE` to `DEVS:AudioModes/`. To install the UAESND AHI driver manually, copy `Devs/AHI/uaesnd.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAESND` to `DEVS:AudioModes/`. The UAESND driver plays each AHI channel through a hardware audio stream and requires the UAESND sound board to be enabled in the Amiberry configuration. +For manual installation, copy the binaries (`host-run`, `host-multiview`, `host-shell`, `host-path`, `host-reveal`, `host-notify`, `host-edit`, `host-clip`, `host-info`, `host-download`, `host-env`) from the package `C` drawer to `C:` or anywhere in your system path. To install the UAE AHI driver manually, copy `Devs/AHI/uae.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAE` to `DEVS:AudioModes/`. To install the UAESND AHI driver manually, copy `Devs/AHI/uaesnd.audio` to `DEVS:AHI/` and `Devs/AudioModes/UAESND` to `DEVS:AudioModes/`. The UAESND driver plays each AHI channel through a hardware audio stream and requires the UAESND sound board to be enabled in the Amiberry configuration. ## Examples diff --git a/package/Help/Host-Tools.guide b/package/Help/Host-Tools.guide index b1024b4..e5069be 100644 --- a/package/Help/Host-Tools.guide +++ b/package/Help/Host-Tools.guide @@ -28,6 +28,7 @@ The command tools are installed to C: when selected in the installer. @{"host-clip" link HostClip} exchanges text with the host clipboard. @{"host-info" link HostInfo} prints host integration details. @{"host-download" link HostDownload} downloads files through the host. +@{"host-env" link HostEnv} gets and sets host environment variables. @endnode @@ -180,6 +181,26 @@ host-download works on Linux, macOS, and Windows hosts. On Windows a current Ami @endnode +@node HostEnv "host-env" +host-env gets, sets, removes, and lists host user environment variables. + +Usage: + +host-env get NAME +host-env set NAME VALUE +host-env unset NAME +host-env list + +On Windows, host-env updates the user's persistent environment through PowerShell and the Windows user environment store. New host processes see the changed values. + +On Linux and macOS, host-env writes persistent values to $HOME/.host-tools-env. Source that file from your host shell startup files if you want future host login shells to import those values automatically. + +host-env cannot change the live environment of already-running host shells, desktop apps, or Amiberry's parent process. + +Variable names must use ASCII letters, digits, and underscores, and must not start with a digit. + +@endnode + @node InstallNotes "Installation Notes" The installer copies selected files only. It shows a component checklist first. When a versioned file already exists, Installer shows the installed and package versions and asks whether to replace or skip it. Files without version strings are still checked for existence and ask before overwrite. @@ -187,6 +208,6 @@ It does not edit startup files, system settings, or existing driver configuratio Native Code execution must be enabled in Amiberry for these tools to communicate with the host. -All tools work on Linux and macOS hosts. On Windows hosts, host-path, host-download, host-clip, host-reveal, and host-info are supported with a current Amiberry; the remaining tools require a Linux or macOS host. +All tools work on Linux and macOS hosts. On Windows hosts, host-path, host-download, host-clip, host-reveal, host-info, and host-env are supported with a current Amiberry; the remaining tools require a Linux or macOS host. @endnode diff --git a/package/Install b/package/Install index 7422ea6..1c3ff62 100644 --- a/package/Install +++ b/package/Install @@ -299,6 +299,7 @@ (P_InstallCommandTool "host-clip") (P_InstallCommandTool "host-info") (P_InstallCommandTool "host-download") + (P_InstallCommandTool "host-env") ) ) diff --git a/src/host-env.c b/src/host-env.c new file mode 100644 index 0000000..261e0aa --- /dev/null +++ b/src/host-env.c @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include "host_capture.h" +#include "host_env_command.h" + +static const char version[] = "$VER: Host-Env " VERSION_STR " (" DATE_STR ")"; + +static int print_usage(void) +{ + printf("Host-Env v%s\n", VERSION_STR); + printf("Host-Env gets and sets host user environment variables.\n"); + printf("%s\nUsage: host-env get \n" + " host-env set \n" + " host-env unset \n" + " host-env list\n", version); + return 0; +} + +static int require_name(const char *name) +{ + if (!host_env_valid_name(name)) { + printf("Invalid environment variable name\n"); + return 0; + } + return 1; +} + +int main(int argc, char *argv[]) +{ + static char command[HOST_MAX_COMMAND_LEN]; + int windows; + + command[0] = '\0'; + + if (!InitUAEResource()) + { + printf("UAEResource not found!\n"); + return 2; + } + + if (argc <= 1) + { + printf("Missing environment command\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + + if (strcmp(argv[1], "?") == 0) + { + return print_usage(); + } + + windows = (GetHostPlatform() == HOST_PLATFORM_WINDOWS); + + if (strcmp(argv[1], "get") == 0) { + if (argc != 3) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_get_command_windows(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_get_command(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "set") == 0) { + if (argc != 4) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_set_command_windows(command, sizeof(command), argv[2], argv[3])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_set_command(command, sizeof(command), argv[2], argv[3])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "unset") == 0) { + if (argc != 3) { + printf("Usage error\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (!require_name(argv[2])) { + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_unset_command_windows(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_unset_command(command, sizeof(command), argv[2])) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + if (strcmp(argv[1], "list") == 0) { + if (argc != 2) { + printf("Unexpected argument after list\n"); + print_usage(); + return HOST_RETURN_ERROR; + } + if (windows) { + if (!host_append_env_list_command_windows(command, sizeof(command))) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + } else if (!host_append_env_list_command(command, sizeof(command))) { + printf("Command is too long\n"); + return HOST_RETURN_ERROR; + } + return host_print_command_output(command); + } + + printf("Unknown environment command\n"); + print_usage(); + return HOST_RETURN_ERROR; +} diff --git a/src/host_env_command.h b/src/host_env_command.h new file mode 100644 index 0000000..7659e7a --- /dev/null +++ b/src/host_env_command.h @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: 2020-2026 Dimitris Panokostas + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef HOST_ENV_COMMAND_H +#define HOST_ENV_COMMAND_H + +#include +#include "host_common.h" +#include "host_powershell.h" + +static inline int host_env_is_alpha_or_underscore(char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; +} + +static inline int host_env_is_alnum_or_underscore(char c) +{ + return host_env_is_alpha_or_underscore(c) || (c >= '0' && c <= '9'); +} + +static inline int host_env_valid_name(const char *name) +{ + if (name == NULL || !host_env_is_alpha_or_underscore(name[0])) { + return 0; + } + + for (const char *p = name + 1; *p; p++) { + if (!host_env_is_alnum_or_underscore(*p)) { + return 0; + } + } + return 1; +} + +static inline int host_append_env_export_line(char *line, size_t line_size, + const char *name, const char *value) +{ + if (!host_append_literal(line, line_size, "export ") || + !host_append_literal(line, line_size, name) || + !host_append_literal(line, line_size, "='")) { + return 0; + } + + for (const char *p = value; *p; p++) { + char one[2]; + + if (*p == '\'') { + if (!host_append_literal(line, line_size, "'\\''")) { + return 0; + } + } else { + one[0] = *p; + one[1] = '\0'; + if (!host_append_literal(line, line_size, one)) { + return 0; + } + } + } + + return host_append_literal(line, line_size, "'"); +} + +static inline int host_append_env_file_prefix(char *command, size_t command_size) +{ + return host_append_literal(command, command_size, + "f=\"${HOME:?}/.host-tools-env\"; "); +} + +static inline int host_append_env_get_command(char *command, size_t command_size, + const char *name) +{ + if (!host_env_valid_name(name)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, "if [ \"${") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "+x}\" = x ]; then printf %s \"$") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "\"; elif [ -r \"$f\" ]; then . \"$f\"; if [ \"${") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "+x}\" = x ]; then printf %s \"$") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "\"; else exit 1; fi; else exit 1; fi"); +} + +static inline int host_append_env_set_command(char *command, size_t command_size, + const char *name, const char *value) +{ + static char line[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + line[0] = '\0'; + if (!host_append_env_export_line(line, sizeof(line), name, value)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "t=\"${f}.$$\"; touch \"$f\" || exit $?; grep -v '^export ") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "=' \"$f\" > \"$t\" 2>/dev/null || true; printf '%s\\n' ") && + host_append_shell_arg(command, command_size, line, 0) && + host_append_literal(command, command_size, " >> \"$t\" && mv \"$t\" \"$f\""); +} + +static inline int host_append_env_unset_command(char *command, size_t command_size, + const char *name) +{ + if (!host_env_valid_name(name)) { + return 0; + } + + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "t=\"${f}.$$\"; if [ -f \"$f\" ]; then grep -v '^export ") && + host_append_literal(command, command_size, name) && + host_append_literal(command, command_size, "=' \"$f\" > \"$t\" || true; mv \"$t\" \"$f\"; fi"); +} + +static inline int host_append_env_list_command(char *command, size_t command_size) +{ + return host_append_env_file_prefix(command, command_size) && + host_append_literal(command, command_size, + "if [ -r \"$f\" ]; then . \"$f\"; fi; env | sort"); +} + +static inline int host_append_env_get_command_windows(char *command, size_t command_size, + const char *name) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "$v=[Environment]::GetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",'User');" + "if($null -eq $v){exit 1};[Console]::Out.Write($v)") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_set_command_windows(char *command, size_t command_size, + const char *name, + const char *value) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Environment]::SetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",") && + host_append_ps_quoted(script, sizeof(script), value) && + host_append_literal(script, sizeof(script), ",'User')") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_unset_command_windows(char *command, size_t command_size, + const char *name) +{ + static char script[HOST_MAX_COMMAND_LEN]; + + if (!host_env_valid_name(name)) { + return 0; + } + + script[0] = '\0'; + return host_append_literal(script, sizeof(script), + "[Environment]::SetEnvironmentVariable(") && + host_append_ps_quoted(script, sizeof(script), name) && + host_append_literal(script, sizeof(script), ",$null,'User')") && + host_append_ps_encoded_command(command, command_size, script); +} + +static inline int host_append_env_list_command_windows(char *command, size_t command_size) +{ + return host_append_ps_encoded_command(command, command_size, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "[Environment]::GetEnvironmentVariables('User').GetEnumerator()|" + "Sort-Object Name|ForEach-Object{[Console]::Out.WriteLine(($_.Name)+'='+($_.Value))}"); +} + +#endif diff --git a/tests/test_host_command_builders.c b/tests/test_host_command_builders.c index 8fa0ab1..56eaaa2 100644 --- a/tests/test_host_command_builders.c +++ b/tests/test_host_command_builders.c @@ -8,6 +8,7 @@ #include #include "host_base64.h" #include "host_clip_command.h" +#include "host_env_command.h" #include "host_info_command.h" #include "host_notify_command.h" #include "host_powershell.h" @@ -142,6 +143,76 @@ static void test_info_command(void) require_shell_syntax(HOST_INFO_COMMAND); } +static void test_env_name_validation(void) +{ + require(host_env_valid_name("FOO"), "simple env name should be valid"); + require(host_env_valid_name("_FOO_1"), "underscore env name should be valid"); + require(!host_env_valid_name(""), "empty env name should be invalid"); + require(!host_env_valid_name("1FOO"), "env name must not start with a digit"); + require(!host_env_valid_name("BAD-NAME"), "env name must not contain hyphens"); + require(!host_env_valid_name("BAD=NAME"), "env name must not contain equals"); +} + +static void test_env_get_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_get_command(command, sizeof(command), "FOO"), + "env get command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "if [ \"${FOO+x}\" = x ]; then printf %s \"$FOO\""); + require_contains(command, ". \"$f\""); + require_contains(command, "else exit 1; fi"); + require_shell_syntax(command); +} + +static void test_env_set_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_set_command(command, sizeof(command), + "FOO", "a b's $HOME"), + "env set command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "grep -v '^export FOO=' \"$f\""); + require_contains(command, "printf '%s\\n' 'export FOO='\\''a b'\\''\\'\\'''\\''s $HOME'\\'''"); + require_contains(command, "mv \"$t\" \"$f\""); + require_shell_syntax(command); +} + +static void test_env_unset_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_unset_command(command, sizeof(command), "FOO"), + "env unset command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "grep -v '^export FOO=' \"$f\" > \"$t\""); + require_contains(command, "mv \"$t\" \"$f\""); + require_shell_syntax(command); +} + +static void test_env_list_command(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_list_command(command, sizeof(command)), + "env list command should build"); + + require_contains(command, "f=\"${HOME:?}/.host-tools-env\""); + require_contains(command, "env"); + require_contains(command, ". \"$f\""); + require_contains(command, "sort"); + require_shell_syntax(command); +} + static void require_ps_script(const char *command, const char *script) { static const char prefix[] = "powershell -NoProfile -EncodedCommand "; @@ -215,6 +286,40 @@ static void test_windows_info_command(void) require_ps_script(command, HOST_INFO_PS_SCRIPT); } +static void test_windows_env_commands(void) +{ + char command[HOST_MAX_COMMAND_LEN]; + + command[0] = '\0'; + require(host_append_env_get_command_windows(command, sizeof(command), "FOO"), + "windows env get command should build"); + require_ps_script(command, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "$v=[Environment]::GetEnvironmentVariable('FOO','User');" + "if($null -eq $v){exit 1};[Console]::Out.Write($v)"); + + command[0] = '\0'; + require(host_append_env_set_command_windows(command, sizeof(command), + "FOO", "a'b"), + "windows env set command should build"); + require_ps_script(command, + "[Environment]::SetEnvironmentVariable('FOO','a''b','User')"); + + command[0] = '\0'; + require(host_append_env_unset_command_windows(command, sizeof(command), "FOO"), + "windows env unset command should build"); + require_ps_script(command, + "[Environment]::SetEnvironmentVariable('FOO',$null,'User')"); + + command[0] = '\0'; + require(host_append_env_list_command_windows(command, sizeof(command)), + "windows env list command should build"); + require_ps_script(command, + "[Console]::OutputEncoding=[System.Text.Encoding]::GetEncoding(28591);" + "[Environment]::GetEnvironmentVariables('User').GetEnumerator()|" + "Sort-Object Name|ForEach-Object{[Console]::Out.WriteLine(($_.Name)+'='+($_.Value))}"); +} + static void test_small_buffer_failures(void) { char command[32]; @@ -230,6 +335,10 @@ static void test_small_buffer_failures(void) command[0] = '\0'; require(!host_append_clip_copy_command(command, sizeof(command), "Text"), "small clipboard copy command buffer should fail"); + + command[0] = '\0'; + require(!host_append_env_set_command(command, sizeof(command), "FOO", "Text"), + "small env set command buffer should fail"); } int main(void) @@ -241,10 +350,16 @@ int main(void) test_clip_copy_command_multiline(); test_clip_paste_command(); test_info_command(); + test_env_name_validation(); + test_env_get_command(); + test_env_set_command(); + test_env_unset_command(); + test_env_list_command(); test_ps_quoting(); test_windows_clip_commands(); test_windows_reveal_command(); test_windows_info_command(); + test_windows_env_commands(); test_small_buffer_failures(); return 0; } diff --git a/tests/test_package_layout.sh b/tests/test_package_layout.sh index cae1d63..0581bc4 100644 --- a/tests/test_package_layout.sh +++ b/tests/test_package_layout.sh @@ -102,7 +102,7 @@ if [ "$(drawer_window_hex "$PACKAGE_ROOT/Help.info")" != "0050002801900096" ]; t exit 1 fi -for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download; do +for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env; do test -f "$PACKAGE_ROOT/C/$tool" done grep -a -q '\$VER: Host-Run 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-run" @@ -115,6 +115,7 @@ grep -a -q '\$VER: Host-Edit 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-edit" grep -a -q '\$VER: Host-Clip 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-clip" grep -a -q '\$VER: Host-Info 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-info" grep -a -q '\$VER: Host-Download 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-download" +grep -a -q '\$VER: Host-Env 2.4 (2026-06-10)' "$PACKAGE_ROOT/C/host-env" if grep -a '\$VER: Host-[A-Za-z]* v[0-9]' "$PACKAGE_ROOT/C/"*; then echo "Host command \$VER strings must be parseable by Installer getversion, without a v prefix before the version number" >&2 exit 1 @@ -136,6 +137,7 @@ grep -q '^@database' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-run' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-shell' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'host-clip' "$PACKAGE_ROOT/Help/Host-Tools.guide" +grep -q 'host-env' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'UAE :16 bit HIFI Stereo++' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'uaesnd: Stereo' "$PACKAGE_ROOT/Help/Host-Tools.guide" grep -q 'uaesnd: HiFi Stereo' "$PACKAGE_ROOT/Help/Host-Tools.guide" @@ -217,7 +219,7 @@ if ! grep -q "^(exit)" "$PACKAGE_ROOT/Install"; then exit 1 fi -for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download; do +for tool in host-run host-multiview host-shell host-path host-reveal host-notify host-edit host-clip host-info host-download host-env; do grep -q "(P_InstallCommandTool \"$tool\")" "$PACKAGE_ROOT/Install" done