diff --git a/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs
index da3d181..881601a 100644
--- a/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs
+++ b/OnePassword.NET.Tests/OnePasswordManagerCommandTests.cs
@@ -14,6 +14,8 @@ namespace OnePassword;
[TestFixture]
public class OnePasswordManagerCommandTests
{
+ private static readonly string[] EnvironmentVariableNames = ["API_URL", "CONNECTION", "EMPTY"];
+ private static readonly string[] EnvironmentVariableValues = ["https://example.com", "Server=db;Password=secret=", ""];
private static readonly string[] ParsedRecipients = ["one@example.com", "two@example.com"];
[Test]
@@ -166,6 +168,17 @@ public void GetSecretUsesTrimmedReference()
Assert.That(fakeCli.LastArguments, Does.StartWith("read op://vault/item/field --no-newline"));
}
+ [Test]
+ public void GetSecretPassesReferenceWithSpacesAsSingleArgument()
+ {
+ using var fakeCli = new FakeCli();
+ var manager = fakeCli.CreateManager();
+
+ manager.GetSecret("op://vault/item/field with spaces");
+
+ Assert.That(fakeCli.LastArgumentLines, Does.Contain("op://vault/item/field with spaces"));
+ }
+
[Test]
public void SaveSecretUsesTrimmedReference()
{
@@ -187,6 +200,46 @@ public void SaveSecretUsesTrimmedReference()
}
}
+ [Test]
+ public void GetEnvironmentVariablesUsesTrimmedEnvironmentIdAndParsesOutput()
+ {
+ using var fakeCli = new FakeCli(nextOutput: "API_URL=https://example.com\nCONNECTION=Server=db;Password=secret=\nEMPTY=\n");
+ var manager = fakeCli.CreateManager();
+
+ var variables = manager.GetEnvironmentVariables(" env-id ");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(fakeCli.LastArguments, Is.EqualTo("environment read env-id"));
+ Assert.That(variables.Select(x => x.Name), Is.EqualTo(EnvironmentVariableNames));
+ Assert.That(variables.Select(x => x.Value), Is.EqualTo(EnvironmentVariableValues));
+ });
+ }
+
+ [Test]
+ public void SaveEnvironmentVariablesUsesTrimmedEnvironmentIdAndWritesOutput()
+ {
+ using var fakeCli = new FakeCli(nextOutput: "API_URL=https://example.com\n");
+ var manager = fakeCli.CreateManager();
+ var outputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+
+ try
+ {
+ manager.SaveEnvironmentVariables(" env-id ", outputPath);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(fakeCli.LastArguments, Is.EqualTo("environment read env-id"));
+ Assert.That(File.ReadAllText(outputPath), Is.EqualTo("API_URL=https://example.com\n"));
+ });
+ }
+ finally
+ {
+ if (File.Exists(outputPath))
+ File.Delete(outputPath);
+ }
+ }
+
[Test]
public void RevokeGroupPermissionsUsesVaultGroupCommand()
{
@@ -374,6 +427,19 @@ public void ShareItemWithMultipleEmailsUsesCommaSeparatedEmails()
Assert.That(fakeCli.LastArguments, Does.Contain("--emails one@example.com,two@example.com"));
}
+ [Test]
+ public void ShareItemPassesEmailValueWithSpacesAsSingleArgument()
+ {
+ using var fakeCli = new FakeCli();
+ var manager = fakeCli.CreateManager();
+
+ manager.ShareItem("item-id", "vault-id", ["recipient@example.com --view-once"]);
+
+ var emailFlagIndex = Array.IndexOf(fakeCli.LastArgumentLines, "--emails");
+ Assert.That(emailFlagIndex, Is.GreaterThanOrEqualTo(0));
+ Assert.That(fakeCli.LastArgumentLines[emailFlagIndex + 1], Is.EqualTo("recipient@example.com --view-once"));
+ }
+
[Test]
public void ShareItemWithEmptyEmailCollectionOmitsEmailsFlag()
{
@@ -439,6 +505,7 @@ public void ShareItemParsesStructuredShareResult()
private sealed class FakeCli : IDisposable
{
private readonly string _argumentsPath;
+ private readonly string _argumentLinesPath;
private readonly string _directoryPath;
private readonly string _errorOutputPath;
private readonly string _inputPath;
@@ -452,6 +519,7 @@ public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}", stri
{
_directoryPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
_argumentsPath = Path.Combine(_directoryPath, "last-arguments.txt");
+ _argumentLinesPath = Path.Combine(_directoryPath, "last-argument-lines.txt");
_errorOutputPath = Path.Combine(_directoryPath, "error-output.txt");
_inputPath = Path.Combine(_directoryPath, "last-input.txt");
_nextOutputPath = Path.Combine(_directoryPath, "next-output.txt");
@@ -485,7 +553,9 @@ public FakeCli(string versionOutput = "2.32.1\n", string nextOutput = "{}", stri
public string ExecutableName { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "op.cmd" : "op";
- public string LastArguments => File.Exists(_argumentsPath) ? File.ReadAllText(_argumentsPath) : "";
+ public string LastArguments => File.Exists(_argumentsPath) ? File.ReadAllText(_argumentsPath).TrimEnd('\r', '\n') : "";
+
+ public string[] LastArgumentLines => File.Exists(_argumentLinesPath) ? File.ReadAllLines(_argumentLinesPath) : [];
public string LastInput => File.Exists(_inputPath) ? File.ReadAllText(_inputPath) : "";
@@ -529,6 +599,16 @@ private static string GetScript(string versionOutputFileName)
@echo off
setlocal
> "%~dp0last-arguments.txt" echo %*
+ break > "%~dp0last-argument-lines.txt"
+ if not "%~1"=="" >> "%~dp0last-argument-lines.txt" echo(%~1
+ if not "%~2"=="" >> "%~dp0last-argument-lines.txt" echo(%~2
+ if not "%~3"=="" >> "%~dp0last-argument-lines.txt" echo(%~3
+ if not "%~4"=="" >> "%~dp0last-argument-lines.txt" echo(%~4
+ if not "%~5"=="" >> "%~dp0last-argument-lines.txt" echo(%~5
+ if not "%~6"=="" >> "%~dp0last-argument-lines.txt" echo(%~6
+ if not "%~7"=="" >> "%~dp0last-argument-lines.txt" echo(%~7
+ if not "%~8"=="" >> "%~dp0last-argument-lines.txt" echo(%~8
+ if not "%~9"=="" >> "%~dp0last-argument-lines.txt" echo(%~9
> "%~dp0last-input.txt" more
if "%~1"=="update" (
if exist "%~dp0update-payload.zip" copy /y "%~dp0update-payload.zip" "%~3\update-payload.zip" > nul
@@ -549,6 +629,7 @@ @echo off
#!/bin/sh
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
printf '%s' "$*" > "$script_dir/last-arguments.txt"
+ printf '%s\n' "$@" > "$script_dir/last-argument-lines.txt"
cat > "$script_dir/last-input.txt"
if [ "$1" = "update" ]; then
if [ -f "$script_dir/update-payload.zip" ]; then
diff --git a/OnePassword.NET/Environments/EnvironmentVariable.cs b/OnePassword.NET/Environments/EnvironmentVariable.cs
new file mode 100644
index 0000000..87b7efb
--- /dev/null
+++ b/OnePassword.NET/Environments/EnvironmentVariable.cs
@@ -0,0 +1,28 @@
+namespace OnePassword.Environments;
+
+/// Represents a variable from a 1Password Environment.
+public sealed class EnvironmentVariable
+{
+ /// The variable name.
+ public string Name { get; internal set; } = "";
+
+ /// The variable value.
+ public string Value { get; internal set; } = "";
+
+ /// Initializes a new instance of .
+ public EnvironmentVariable()
+ {
+ }
+
+ /// Initializes a new instance of with the specified name and value.
+ /// The variable name.
+ /// The variable value.
+ public EnvironmentVariable(string name, string value)
+ {
+ Name = name ?? "";
+ Value = value ?? "";
+ }
+
+ ///
+ public override string ToString() => $"{Name}={Value}";
+}
diff --git a/OnePassword.NET/IOnePasswordManager.Environments.cs b/OnePassword.NET/IOnePasswordManager.Environments.cs
new file mode 100644
index 0000000..65b3606
--- /dev/null
+++ b/OnePassword.NET/IOnePasswordManager.Environments.cs
@@ -0,0 +1,19 @@
+using OnePassword.Environments;
+
+namespace OnePassword;
+
+public partial interface IOnePasswordManager
+{
+ /// Gets the environment variables for a 1Password Environment.
+ /// The Environment ID.
+ /// The environment variables.
+ /// Thrown when there is an invalid argument.
+ public ImmutableList GetEnvironmentVariables(string environmentId);
+
+ /// Saves the environment variables for a 1Password Environment to disk.
+ /// The Environment ID.
+ /// The output file path.
+ /// The file mode to use when creating the file.
+ /// Thrown when there is an invalid argument.
+ public void SaveEnvironmentVariables(string environmentId, string filePath, string? fileMode = null);
+}
diff --git a/OnePassword.NET/OnePassword.NET.csproj b/OnePassword.NET/OnePassword.NET.csproj
index 1e75cae..fc7b37c 100644
--- a/OnePassword.NET/OnePassword.NET.csproj
+++ b/OnePassword.NET/OnePassword.NET.csproj
@@ -9,7 +9,7 @@
Jean-Sebastien Carle
OnePassword.NET
1Password CLI Wrapper
- 2.5.0
+ 2.5.0-daydream.1
2.5.0.0
2.5.0.0
Copyright © Jean-Sebastien Carle 2021-2025
@@ -65,6 +65,9 @@
OnePasswordManager.Documents.cs
+
+ OnePasswordManager.Environments.cs
+
OnePasswordManager.Groups.cs
diff --git a/OnePassword.NET/OnePasswordManager.Accounts.cs b/OnePassword.NET/OnePasswordManager.Accounts.cs
index e9d39cc..ddaca55 100644
--- a/OnePassword.NET/OnePasswordManager.Accounts.cs
+++ b/OnePassword.NET/OnePasswordManager.Accounts.cs
@@ -20,7 +20,7 @@ public ImmutableList GetAccounts()
if (_mode == Mode.ServiceAccount)
throw new InvalidOperationException($"{nameof(GetAccounts)} is not supported when using service accounts.");
- const string command = "account list";
+ var command = new OpCommand("account", "list");
return Op(JsonContext.Default.ImmutableListAccount, command);
}
@@ -32,7 +32,9 @@ public AccountDetails GetAccount(string account = "")
var trimmedAccount = account?.Trim() ?? "";
- var command = trimmedAccount.Length > 0 ? $"account get --account \"{trimmedAccount}\"" : "account get";
+ var command = new OpCommand("account", "get");
+ if (trimmedAccount.Length > 0)
+ command.Add("--account", trimmedAccount);
return Op(JsonContext.Default.AccountDetails, command);
}
@@ -68,9 +70,9 @@ public void AddAccount(string address, string email, string secretKey, string pa
var trimmedShorthand = shorthand?.Trim() ?? "";
- var command = $"account add --address \"{trimmedAddress}\" --email \"{trimmedEmail}\" --secret-key \"{trimmedSecretKey}\"";
+ var command = new OpCommand("account", "add", "--address", trimmedAddress, "--email", trimmedEmail, "--secret-key", trimmedSecretKey);
if (trimmedShorthand.Length > 0)
- command += $" --shorthand \"{trimmedShorthand}\"";
+ command.Add("--shorthand", trimmedShorthand);
var result = Op(command, trimmedPassword, true);
if (result.Contains("No saved device ID.", StringComparison.Ordinal))
@@ -115,7 +117,7 @@ public void SignIn(string? password = null)
throw new ArgumentException($"{nameof(password)} cannot be empty.", nameof(password));
}
- const string command = "signin --force --raw";
+ var command = new OpCommand("signin", "--force", "--raw");
var result = Op(command, password?.Trim());
_session = result.Trim();
}
@@ -126,11 +128,11 @@ public void SignOut(bool all = false)
if (_mode == Mode.ServiceAccount)
throw new InvalidOperationException($"{nameof(SignOut)} is not supported when using service accounts.");
- var command = "signout";
+ var command = new OpCommand("signout");
if (all)
- command += " --all";
+ command.Add("--all");
else if (_account.Length > 0)
- command += $" --account \"{_account}\"";
+ command.Add("--account", _account);
Op(command);
_session = "";
}
@@ -146,8 +148,8 @@ public ImmutableList ForgetAccount(bool all = false)
if (_session.Length > 0)
SignOut(all);
- var command = "account forget";
- command += all ? " --all" : $" \"{_account}\"";
+ var command = new OpCommand("account", "forget");
+ command.Add(all ? "--all" : _account);
var result = Op(command);
diff --git a/OnePassword.NET/OnePasswordManager.Documents.cs b/OnePassword.NET/OnePasswordManager.Documents.cs
index dc2e122..d80ce9b 100644
--- a/OnePassword.NET/OnePasswordManager.Documents.cs
+++ b/OnePassword.NET/OnePasswordManager.Documents.cs
@@ -21,7 +21,7 @@ public ImmutableList GetDocuments(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"document list --vault {vaultId}";
+ var command = new OpCommand("document", "list", "--vault", vaultId);
return Op(JsonContext.Default.ImmutableListDocumentDetails, command);
}
@@ -40,11 +40,11 @@ public ImmutableList SearchForDocuments(string? vaultId = null,
if (vaultId is not null && vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = "document list";
+ var command = new OpCommand("document", "list");
if (vaultId is not null)
- command += $" --vault {vaultId}";
+ command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
- command += " --include-archive";
+ command.Add("--include-archive");
return Op(JsonContext.Default.ImmutableListDocumentDetails, command);
}
@@ -80,9 +80,9 @@ public void GetDocument(string documentId, string vaultId, string filePath, stri
var trimmedFileMode = fileMode?.Trim();
// Not specifying --force will hang waiting for user input if the file exists.
- var command = $"document get {documentId} --out-file \"{trimmedFilePath}\" --force --vault {vaultId}";
+ var command = new OpCommand("document", "get", documentId, "--out-file", trimmedFilePath, "--force", "--vault", vaultId);
if (trimmedFileMode is not null)
- command += $" --file-mode {trimmedFileMode}";
+ command.Add("--file-mode", trimmedFileMode);
Op(command);
}
@@ -119,13 +119,13 @@ public void SearchForDocument(string documentId, string filePath, string? vaultI
var trimmedFileMode = fileMode?.Trim();
// Not specifying --force will hang waiting for user input if the file exists.
- var command = $"document get {documentId} --out-file \"{trimmedFilePath}\" --force";
+ var command = new OpCommand("document", "get", documentId, "--out-file", trimmedFilePath, "--force");
if (vaultId is not null)
- command += $" --vault {vaultId}";
+ command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
- command += " --include-archive";
+ command.Add("--include-archive");
if (trimmedFileMode is not null)
- command += $" --file-mode {trimmedFileMode}";
+ command.Add("--file-mode", trimmedFileMode);
Op(command);
}
@@ -177,13 +177,13 @@ public Document CreateDocument(string vaultId, string filePath, string? fileName
var trimmedFileName = fileName?.Trim();
var trimmedTitle = title?.Trim();
- var command = $"document create \"{trimmedFilePath}\" --vault {vaultId}";
+ var command = new OpCommand("document", "create", trimmedFilePath, "--vault", vaultId);
if (trimmedFileName is not null)
- command += $" --file-name \"{trimmedFileName}\"";
+ command.Add("--file-name", trimmedFileName);
if (trimmedTitle is not null)
- command += $" --title \"{trimmedTitle}\"";
+ command.Add("--title", trimmedTitle);
if (tags is not null && tags.Count > 0)
- command += $" --tags \"{tags.ToCommaSeparated()}\"";
+ command.Add("--tags", tags.ToCommaSeparated());
return Op(JsonContext.Default.Document, command);
}
@@ -223,13 +223,13 @@ public void ReplaceDocument(string documentId, string vaultId, string filePath,
var trimmedFileName = fileName?.Trim();
var trimmedTitle = title?.Trim();
- var command = $"document edit {documentId} \"{trimmedFilePath}\" --vault {vaultId}";
+ var command = new OpCommand("document", "edit", documentId, trimmedFilePath, "--vault", vaultId);
if (trimmedFileName is not null)
- command += $" --file-name \"{trimmedFileName}\"";
+ command.Add("--file-name", trimmedFileName);
if (trimmedTitle is not null)
- command += $" --title \"{trimmedTitle}\"";
+ command.Add("--title", trimmedTitle);
if (tags is not null && tags.Count > 0)
- command += $" --tags \"{tags.ToCommaSeparated()}\"";
+ command.Add("--tags", tags.ToCommaSeparated());
Op(command);
}
@@ -252,7 +252,7 @@ public void ArchiveDocument(string documentId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"document delete {documentId} --vault {vaultId} --archive";
+ var command = new OpCommand("document", "delete", documentId, "--vault", vaultId, "--archive");
Op(command);
}
@@ -275,7 +275,7 @@ public void DeleteDocument(string documentId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"document delete {documentId} --vault {vaultId}";
+ var command = new OpCommand("document", "delete", documentId, "--vault", vaultId);
Op(command);
}
}
diff --git a/OnePassword.NET/OnePasswordManager.Environments.cs b/OnePassword.NET/OnePasswordManager.Environments.cs
new file mode 100644
index 0000000..d6713ca
--- /dev/null
+++ b/OnePassword.NET/OnePasswordManager.Environments.cs
@@ -0,0 +1,69 @@
+using OnePassword.Environments;
+
+namespace OnePassword;
+
+public sealed partial class OnePasswordManager
+{
+ ///
+ public ImmutableList GetEnvironmentVariables(string environmentId)
+ {
+ ValidateEnvironmentId(environmentId);
+ var result = ReadEnvironmentVariables(environmentId);
+ return ParseEnvironmentVariables(result);
+ }
+
+ ///
+ public void SaveEnvironmentVariables(string environmentId, string filePath, string? fileMode = null)
+ {
+ ValidateEnvironmentId(environmentId);
+ if (filePath is null || filePath.Length == 0)
+ throw new ArgumentException($"{nameof(filePath)} cannot be empty.", nameof(filePath));
+ var trimmedFilePath = filePath.Trim();
+ if (trimmedFilePath.Length == 0)
+ throw new ArgumentException($"{nameof(trimmedFilePath)} cannot be empty.", nameof(filePath));
+
+ var trimmedFileMode = fileMode?.Trim();
+ var result = ReadEnvironmentVariables(environmentId);
+
+ File.WriteAllText(trimmedFilePath, result);
+ if (trimmedFileMode is not null && trimmedFileMode.Length > 0)
+ ApplyFileMode(trimmedFileMode, trimmedFilePath);
+ }
+
+ private string ReadEnvironmentVariables(string environmentId)
+ {
+ ValidateEnvironmentId(environmentId);
+ var trimmedEnvironmentId = environmentId.Trim();
+ var command = new OpCommand("environment", "read", trimmedEnvironmentId);
+ return Op(command, Array.Empty(), false, false);
+ }
+
+ private static ImmutableList ParseEnvironmentVariables(string result)
+ {
+ var variables = ImmutableList.CreateBuilder();
+ using var reader = new StringReader(result);
+ string? line;
+ while ((line = reader.ReadLine()) is not null)
+ {
+ if (line.Length == 0)
+ continue;
+
+ var separatorIndex = line.IndexOf('=', StringComparison.Ordinal);
+ if (separatorIndex <= 0)
+ throw new SerializationException("Could not deserialize the command result.");
+
+ variables.Add(new EnvironmentVariable(line[..separatorIndex], line[(separatorIndex + 1)..]));
+ }
+
+ return variables.ToImmutable();
+ }
+
+ private static void ValidateEnvironmentId(string environmentId)
+ {
+ if (environmentId is null || environmentId.Length == 0)
+ throw new ArgumentException($"{nameof(environmentId)} cannot be empty.", nameof(environmentId));
+ var trimmedEnvironmentId = environmentId.Trim();
+ if (trimmedEnvironmentId.Length == 0)
+ throw new ArgumentException($"{nameof(trimmedEnvironmentId)} cannot be empty.", nameof(environmentId));
+ }
+}
diff --git a/OnePassword.NET/OnePasswordManager.Groups.cs b/OnePassword.NET/OnePasswordManager.Groups.cs
index ca43c66..9fdec6c 100644
--- a/OnePassword.NET/OnePasswordManager.Groups.cs
+++ b/OnePassword.NET/OnePasswordManager.Groups.cs
@@ -10,7 +10,7 @@ public sealed partial class OnePasswordManager
///
public ImmutableList GetGroups()
{
- const string command = "group list";
+ var command = new OpCommand("group", "list");
return Op(JsonContext.Default.ImmutableListGroup, command);
}
@@ -38,7 +38,7 @@ public ImmutableList GetVaultGroups(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"vault group list {vaultId}";
+ var command = new OpCommand("vault", "group", "list", vaultId);
return Op(JsonContext.Default.ImmutableListVaultGroup, command);
}
@@ -48,7 +48,7 @@ public ImmutableList GetUserGroups(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"group list --user {userId}";
+ var command = new OpCommand("group", "list", "--user", userId);
return Op(JsonContext.Default.ImmutableListUserGroup, command);
}
@@ -67,7 +67,7 @@ public GroupDetails GetGroup(string groupId)
if (groupId is null || groupId.Length == 0)
throw new ArgumentException($"{nameof(groupId)} cannot be empty.", nameof(groupId));
- var command = $"group get {groupId}";
+ var command = new OpCommand("group", "get", groupId);
return Op(JsonContext.Default.GroupDetails, command);
}
@@ -83,9 +83,9 @@ public GroupDetails CreateGroup(string name, string? description = null)
var trimmedDescription = description?.Trim();
- var command = $"group create \"{trimmedName}\"";
+ var command = new OpCommand("group", "create", trimmedName);
if (trimmedDescription is not null)
- command += $" --description \"{trimmedDescription}\"";
+ command.Add("--description", trimmedDescription);
return Op(JsonContext.Default.GroupDetails, command);
}
@@ -120,11 +120,11 @@ public void EditGroup(string groupId, string? name = null, string? description =
if (name is null && description is null)
throw new InvalidOperationException("Nothing to edit.");
- var command = $"group edit {groupId}";
+ var command = new OpCommand("group", "edit", groupId);
if (trimmedName is not null)
- command += $" --name \"{trimmedName}\"";
+ command.Add("--name", trimmedName);
if (trimmedDescription is not null)
- command += $" --description \"{trimmedDescription}\"";
+ command.Add("--description", trimmedDescription);
Op(command);
}
@@ -143,7 +143,7 @@ public void DeleteGroup(string groupId)
if (groupId is null || groupId.Length == 0)
throw new ArgumentException($"{nameof(groupId)} cannot be empty.", nameof(groupId));
- var command = $"group delete {groupId}";
+ var command = new OpCommand("group", "delete", groupId);
Op(command);
}
@@ -170,7 +170,7 @@ public void GrantAccess(string groupId, string userId, UserRole userRole = UserR
if (userRole != UserRole.Member && userRole != UserRole.Manager)
throw new ArgumentException($"{nameof(userRole)} must be {nameof(UserRole.Member)} or {nameof(UserRole.Manager)}.", nameof(userRole));
- var command = $"group user grant --group {groupId} --user {userId} --role \"{userRole.ToEnumString()}\"";
+ var command = new OpCommand("group", "user", "grant", "--group", groupId, "--user", userId, "--role", userRole.ToEnumString());
Op(command);
}
@@ -193,7 +193,7 @@ public void RevokeAccess(string groupId, string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"group user revoke --group {groupId} --user {userId}";
+ var command = new OpCommand("group", "user", "revoke", "--group", groupId, "--user", userId);
Op(command);
}
}
diff --git a/OnePassword.NET/OnePasswordManager.Items.cs b/OnePassword.NET/OnePasswordManager.Items.cs
index 01391a8..81e2c6f 100644
--- a/OnePassword.NET/OnePasswordManager.Items.cs
+++ b/OnePassword.NET/OnePasswordManager.Items.cs
@@ -24,7 +24,7 @@ public ImmutableList- GetItems(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item list --vault {vaultId}";
+ var command = new OpCommand("item", "list", "--vault", vaultId);
return Op(JsonContext.Default.ImmutableListItem, command);
}
@@ -44,17 +44,17 @@ public ImmutableList
- SearchForItems(string? vaultId = null, bool? includeA
if (vaultId is not null && vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = "item list";
+ var command = new OpCommand("item", "list");
if (vaultId is not null)
- command += $" --vault {vaultId}";
+ command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
- command += " --include-archive";
+ command.Add("--include-archive");
if (favorite is not null && favorite.Value)
- command += " --favorite";
+ command.Add("--favorite");
if (categories is not null && categories.Count > 0)
- command += $" --categories \"{categories.ToCommaSeparated(true)}\"";
+ command.Add("--categories", categories.ToCommaSeparated(true));
if (tags is not null && tags.Count > 0)
- command += $" --tags \"{tags.ToCommaSeparated()}\"";
+ command.Add("--tags", tags.ToCommaSeparated());
return Op(JsonContext.Default.ImmutableListItem, command);
}
@@ -77,7 +77,7 @@ public Item GetItem(string itemId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item get {itemId} --vault {vaultId}";
+ var command = new OpCommand("item", "get", itemId, "--vault", vaultId);
return Op(JsonContext.Default.Item, command);
}
@@ -100,11 +100,11 @@ public Item SearchForItem(string itemId, string? vaultId = null, bool? includeAr
if (vaultId is not null && vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item get {itemId}";
+ var command = new OpCommand("item", "get", itemId);
if (vaultId is not null)
- command += $" --vault {vaultId}";
+ command.Add("--vault", vaultId);
if (includeArchive is not null && includeArchive.Value)
- command += " --include-archive";
+ command.Add("--include-archive");
return Op(JsonContext.Default.Item, command);
}
@@ -125,9 +125,9 @@ public Item CreateItem(Template template, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item create --vault {vaultId} -";
+ var command = new OpCommand("item", "create", "--vault", vaultId, "-");
foreach (var assignmentStatement in GetFileAttachmentAssignmentStatements(template.Fields, template.FileAttachments))
- command += $" {QuoteArgument(assignmentStatement)}";
+ command.Add(assignmentStatement);
var json = SerializeTemplateForItemCommand(template);
var createdItem = Op(JsonContext.Default.Item, command, json);
@@ -154,20 +154,20 @@ public Item EditItem(Item item, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item edit {item.Id} --vault {vaultId}";
+ var command = new OpCommand("item", "edit", item.Id, "--vault", vaultId);
foreach (var assignmentStatement in GetFileAttachmentAssignmentStatements(item.Fields, item.FileAttachments)
.Concat(GetDeletedFileAttachmentAssignmentStatements(item.FileAttachments)))
- command += $" {QuoteArgument(assignmentStatement)}";
+ command.Add(assignmentStatement);
var json = SerializeItemForEditCommand(item);
if (item.TitleChanged)
- command += $" --title \"{item.Title}\"";
+ command.Add("--title", item.Title);
if (((ITracked)item.Tags).Changed)
- command += $" --tags \"{item.Tags.ToCommaSeparated()}\"";
+ command.Add("--tags", item.Tags.ToCommaSeparated());
if (((ITracked)item.Urls).Changed)
{
var changedUrl = item.Urls.FirstOrDefault(url => url.Primary && ((ITracked)url).Changed);
- command += $" --url \"{changedUrl}\"";
+ command.Add("--url", $"{changedUrl}");
}
var editedItem = Op(JsonContext.Default.Item, command, json);
((ITracked)item).AcceptChanges();
@@ -233,7 +233,7 @@ public void ArchiveItem(string itemId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item delete {itemId} --vault {vaultId} --archive";
+ var command = new OpCommand("item", "delete", itemId, "--vault", vaultId, "--archive");
Op(command);
}
@@ -256,7 +256,7 @@ public void DeleteItem(string itemId, string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"item delete {itemId} --vault {vaultId}";
+ var command = new OpCommand("item", "delete", itemId, "--vault", vaultId);
Op(command);
}
@@ -283,7 +283,7 @@ public void MoveItem(string itemId, string currentVaultId, string destinationVau
if (destinationVaultId is null || destinationVaultId.Length == 0)
throw new ArgumentException($"{nameof(destinationVaultId)} cannot be empty.", nameof(destinationVaultId));
- var command = $"item move {itemId} --current-vault {currentVaultId} --destination-vault {destinationVaultId}";
+ var command = new OpCommand("item", "move", itemId, "--current-vault", currentVaultId, "--destination-vault", destinationVaultId);
Op(command);
}
@@ -338,14 +338,14 @@ public ItemShare ShareItem(string itemId, string vaultId, IReadOnlyCollection 0)
- command += $" --emails {string.Join(',', normalizedEmailAddresses)}";
+ command.Add("--emails", string.Join(',', normalizedEmailAddresses));
if (expiresIn is not null)
- command += $" --expires-in {expiresIn.Value.ToHumanReadable()}";
+ command.Add("--expires-in", expiresIn.Value.ToHumanReadable());
if (viewOnce is not null && viewOnce.Value)
- command += " --view-once";
+ command.Add("--view-once");
return ParseItemShare(Op(command));
}
@@ -531,11 +531,6 @@ private static string BuildFileAttachmentTarget(string? name, Section? section,
return trimmedSection.Length == 0 ? trimmedName : $"{trimmedSection}.{trimmedName}";
}
- private static string QuoteArgument(string argument)
- {
- return $"\"{argument.Replace("\"", "\\\"", StringComparison.InvariantCulture)}\"";
- }
-
private static ItemShare ParseItemShare(string result)
{
var trimmedResult = result.Trim();
diff --git a/OnePassword.NET/OnePasswordManager.Templates.cs b/OnePassword.NET/OnePasswordManager.Templates.cs
index 078c185..609e649 100644
--- a/OnePassword.NET/OnePasswordManager.Templates.cs
+++ b/OnePassword.NET/OnePasswordManager.Templates.cs
@@ -9,7 +9,7 @@ public sealed partial class OnePasswordManager
///
public ImmutableList GetTemplates()
{
- const string command = "item template list";
+ var command = new OpCommand("item", "template", "list");
return Op(JsonContext.Default.ImmutableListTemplateInfo, command);
}
@@ -19,7 +19,7 @@ public Template GetTemplate(ITemplate template)
if (template is null || template.Name.Length == 0)
throw new ArgumentException($"{nameof(template.Name)} cannot be empty.", nameof(template));
- var command = $"item template get \"{template.Name}\"";
+ var command = new OpCommand("item", "template", "get", template.Name);
var result = Op(JsonContext.Default.Template, command);
result.Name = template.Name;
@@ -33,7 +33,7 @@ public Template GetTemplate(string name)
if (string.IsNullOrEmpty(name))
throw new ArgumentException($"{nameof(name)} cannot be empty.", nameof(name));
- var command = $"item template get \"{name}\"";
+ var command = new OpCommand("item", "template", "get", name);
var result = Op(JsonContext.Default.Template, command);
result.Name = name;
@@ -49,7 +49,7 @@ public Template GetTemplate(Category category)
var templateName = category.ToEnumString();
- var command = $"item template get \"{templateName}\"";
+ var command = new OpCommand("item", "template", "get", templateName);
var result = Op(JsonContext.Default.Template, command);
result.Name = templateName;
diff --git a/OnePassword.NET/OnePasswordManager.Users.cs b/OnePassword.NET/OnePasswordManager.Users.cs
index 5b66d3f..3dfd6cb 100644
--- a/OnePassword.NET/OnePasswordManager.Users.cs
+++ b/OnePassword.NET/OnePasswordManager.Users.cs
@@ -10,7 +10,7 @@ public sealed partial class OnePasswordManager
///
public ImmutableList GetUsers()
{
- const string command = "user list";
+ var command = new OpCommand("user", "list");
return Op(JsonContext.Default.ImmutableListUser, command);
}
@@ -38,7 +38,7 @@ public ImmutableList GetGroupUsers(string groupId)
if (groupId is null || groupId.Length == 0)
throw new ArgumentException($"{nameof(groupId)} cannot be empty.", nameof(groupId));
- var command = $"group user list {groupId}";
+ var command = new OpCommand("group", "user", "list", groupId);
return Op(JsonContext.Default.ImmutableListGroupUser, command);
}
@@ -48,7 +48,7 @@ public ImmutableList GetVaultUsers(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"vault user list {vaultId}";
+ var command = new OpCommand("vault", "user", "list", vaultId);
return Op(JsonContext.Default.ImmutableListVaultUser, command);
}
@@ -67,7 +67,7 @@ public UserDetails GetUser(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"user get {userId}";
+ var command = new OpCommand("user", "get", userId);
return Op(JsonContext.Default.UserDetails, command);
}
@@ -87,9 +87,9 @@ public UserDetails ProvisionUser(string name, string emailAddress, Language lang
if (trimmedEmailAddress.Length == 0)
throw new ArgumentException($"{nameof(emailAddress)} cannot be empty.", nameof(emailAddress));
- var command = $"user provision --name \"{trimmedName}\" --email \"{trimmedEmailAddress}\"";
+ var command = new OpCommand("user", "provision", "--name", trimmedName, "--email", trimmedEmailAddress);
if (language != Language.Default)
- command += $" --language \"{language.ToEnumString()}\"";
+ command.Add("--language", language.ToEnumString());
return Op(JsonContext.Default.UserDetails, command);
}
@@ -108,14 +108,14 @@ public void ConfirmUser(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"user confirm {userId}";
+ var command = new OpCommand("user", "confirm", userId);
Op(command);
}
///
public void ConfirmAllUsers()
{
- const string command = "user confirm --all";
+ var command = new OpCommand("user", "confirm", "--all");
Op(command);
}
@@ -148,11 +148,11 @@ public void EditUser(string userId, string? name = null, bool? travelMode = null
if (name is null && travelMode is null)
throw new InvalidOperationException("Nothing to edit.");
- var command = $"user edit {userId}";
+ var command = new OpCommand("user", "edit", userId);
if (trimmedName is not null)
- command += $" --name \"{trimmedName}\"";
+ command.Add("--name", trimmedName);
if (travelMode.HasValue)
- command += $" --travel-mode {(travelMode.Value ? "on" : "off")}";
+ command.Add("--travel-mode", travelMode.Value ? "on" : "off");
Op(command);
}
@@ -171,7 +171,7 @@ public void DeleteUser(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"user delete {userId}";
+ var command = new OpCommand("user", "delete", userId);
Op(command);
}
@@ -190,9 +190,9 @@ public void SuspendUser(string userId, int? deauthorizeDevicesDelay = null)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"user suspend {userId}";
+ var command = new OpCommand("user", "suspend", userId);
if (deauthorizeDevicesDelay is not null)
- command += $" --deauthorize-devices-after {deauthorizeDevicesDelay.Value}s";
+ command.Add("--deauthorize-devices-after", $"{deauthorizeDevicesDelay.Value}s");
Op(command);
}
@@ -211,7 +211,7 @@ public void ReactivateUser(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"user reactivate {userId}";
+ var command = new OpCommand("user", "reactivate", userId);
Op(command);
}
}
diff --git a/OnePassword.NET/OnePasswordManager.Vaults.cs b/OnePassword.NET/OnePasswordManager.Vaults.cs
index ed24fb8..4746f4c 100644
--- a/OnePassword.NET/OnePasswordManager.Vaults.cs
+++ b/OnePassword.NET/OnePasswordManager.Vaults.cs
@@ -10,7 +10,7 @@ public sealed partial class OnePasswordManager
///
public ImmutableList GetVaults()
{
- const string command = "vault list";
+ var command = new OpCommand("vault", "list");
return Op(JsonContext.Default.ImmutableListVault, command);
}
@@ -38,7 +38,7 @@ public ImmutableList GetGroupVaults(string groupId)
if (groupId is null || groupId.Length == 0)
throw new ArgumentException($"{nameof(groupId)} cannot be empty.", nameof(groupId));
- var command = $"vault list --group {groupId}";
+ var command = new OpCommand("vault", "list", "--group", groupId);
return Op(JsonContext.Default.ImmutableListVault, command);
}
@@ -48,7 +48,7 @@ public ImmutableList GetUserVaults(string userId)
if (userId is null || userId.Length == 0)
throw new ArgumentException($"{nameof(userId)} cannot be empty.", nameof(userId));
- var command = $"vault list --user {userId}";
+ var command = new OpCommand("vault", "list", "--user", userId);
return Op(JsonContext.Default.ImmutableListVault, command);
}
@@ -67,7 +67,7 @@ public VaultDetails GetVault(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"vault get {vaultId}";
+ var command = new OpCommand("vault", "get", vaultId);
return Op(JsonContext.Default.VaultDetails, command);
}
@@ -83,13 +83,13 @@ public VaultDetails CreateVault(string name, string? description = null, VaultIc
var trimmedDescription = description?.Trim();
- var command = $"vault create \"{trimmedName}\"";
+ var command = new OpCommand("vault", "create", trimmedName);
if (trimmedDescription is not null)
- command += $" --description \"{trimmedDescription}\"";
+ command.Add("--description", trimmedDescription);
if (icon != VaultIcon.Default && icon != VaultIcon.Unknown)
- command += $" --icon \"{icon.ToEnumString()}\"";
+ command.Add("--icon", icon.ToEnumString());
if (allowAdminsToManage.HasValue)
- command += $" --allow-admins-to-manage {(allowAdminsToManage.Value ? "true" : "false")}";
+ command.Add("--allow-admins-to-manage", allowAdminsToManage.Value ? "true" : "false");
return Op(JsonContext.Default.VaultDetails, command);
}
@@ -124,15 +124,15 @@ public void EditVault(string vaultId, string? name = null, string? description =
if (name is null && description is null && icon is VaultIcon.Default or VaultIcon.Unknown && travelMode is null)
throw new InvalidOperationException("Nothing to edit.");
- var command = $"vault edit {vaultId}";
+ var command = new OpCommand("vault", "edit", vaultId);
if (trimmedName is not null)
- command += $" --name \"{trimmedName}\"";
+ command.Add("--name", trimmedName);
if (trimmedDescription is not null)
- command += $" --description \"{trimmedDescription}\"";
+ command.Add("--description", trimmedDescription);
if (icon != VaultIcon.Default && icon != VaultIcon.Unknown)
- command += $" --icon \"{icon.ToEnumString()}\"";
+ command.Add("--icon", icon.ToEnumString());
if (travelMode.HasValue)
- command += $" --travel-mode {(travelMode.Value ? "on" : "off")}";
+ command.Add("--travel-mode", travelMode.Value ? "on" : "off");
Op(command);
}
@@ -151,7 +151,7 @@ public void DeleteVault(string vaultId)
if (vaultId is null || vaultId.Length == 0)
throw new ArgumentException($"{nameof(vaultId)} cannot be empty.", nameof(vaultId));
- var command = $"vault delete {vaultId}";
+ var command = new OpCommand("vault", "delete", vaultId);
Op(command);
}
@@ -191,7 +191,7 @@ public void GrantGroupPermissions(string vaultId, string groupId, IReadOnlyColle
if (permissions is null || permissions.Count == 0)
throw new ArgumentException($"{nameof(permissions)} cannot be empty.", nameof(permissions));
- var command = $"vault group grant --vault {vaultId} --group {groupId} --permissions \"{permissions.ToCommaSeparated()}\"";
+ var command = new OpCommand("vault", "group", "grant", "--vault", vaultId, "--group", groupId, "--permissions", permissions.ToCommaSeparated());
Op(command);
}
@@ -205,7 +205,7 @@ public void GrantUserPermissions(string vaultId, string userId, IReadOnlyCollect
if (permissions is null || permissions.Count == 0)
throw new ArgumentException($"{nameof(permissions)} cannot be empty.", nameof(permissions));
- var command = $"vault user grant --vault {vaultId} --user {userId} --permissions \"{permissions.ToCommaSeparated()}\"";
+ var command = new OpCommand("vault", "user", "grant", "--vault", vaultId, "--user", userId, "--permissions", permissions.ToCommaSeparated());
Op(command);
}
@@ -245,7 +245,7 @@ public void RevokeGroupPermissions(string vaultId, string groupId, IReadOnlyColl
if (permissions is null || permissions.Count == 0)
throw new ArgumentException($"{nameof(permissions)} cannot be empty.", nameof(permissions));
- var command = $"vault group revoke --vault {vaultId} --group {groupId} --permissions \"{permissions.ToCommaSeparated()}\"";
+ var command = new OpCommand("vault", "group", "revoke", "--vault", vaultId, "--group", groupId, "--permissions", permissions.ToCommaSeparated());
Op(command);
}
@@ -259,7 +259,7 @@ public void RevokeUserPermissions(string vaultId, string userId, IReadOnlyCollec
if (permissions is null || permissions.Count == 0)
throw new ArgumentException($"{nameof(permissions)} cannot be empty.", nameof(permissions));
- var command = $"vault user revoke --vault {vaultId} --user {userId} --permissions \"{permissions.ToCommaSeparated()}\"";
+ var command = new OpCommand("vault", "user", "revoke", "--vault", vaultId, "--user", userId, "--permissions", permissions.ToCommaSeparated());
Op(command);
}
}
diff --git a/OnePassword.NET/OnePasswordManager.cs b/OnePassword.NET/OnePasswordManager.cs
index 9c4a42f..3f7d900 100644
--- a/OnePassword.NET/OnePasswordManager.cs
+++ b/OnePassword.NET/OnePasswordManager.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using System.Collections;
+using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
@@ -20,12 +21,38 @@ public sealed partial class OnePasswordManager : IOnePasswordManager
#else
private static readonly Regex VersionRegex = new (@"Version ([^\s]+) is now available\.", RegexOptions.Compiled);
#endif
- private readonly string[] _excludedAccountCommands = ["--version", "update", "account list", "account add", "account forget", "signout --all"];
- private readonly string[] _excludedSessionCommands = ["--version", "update", "account list", "account add", "account forget", "signin", "signout", "signout --all"];
+ private static readonly string[][] ExcludedAccountCommands =
+ [
+ ["--version"],
+ ["update"],
+ ["account", "list"],
+ ["account", "add"],
+ ["account", "forget"],
+ ["signout", "--all"]
+ ];
+
+ private static readonly string[][] ExcludedSessionCommands =
+ [
+ ["--version"],
+ ["update"],
+ ["account", "list"],
+ ["account", "add"],
+ ["account", "forget"],
+ ["signin"],
+ ["signout"],
+ ["signout", "--all"]
+ ];
+
+ private static readonly string[][] ServiceAccountUnsupportedCommands =
+ [
+ ["events-api"],
+ ["group"],
+ ["user"]
+ ];
+
private readonly Mode _mode = Mode.Interactive;
private readonly string _opPath;
private readonly string _serviceAccountToken;
- private readonly string[] _serviceAccountUnsupportedCommands = ["events-api", "group", "user"];
private readonly bool _verbose;
private string _account = "";
private string _session = "";
@@ -70,7 +97,7 @@ public bool Update()
var tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
- var command = $"update --directory \"{tempDirectory}\"";
+ var command = new OpCommand("update", "--directory", tempDirectory);
var result = Op(command);
var match = VersionRegex.Match(result);
@@ -106,7 +133,7 @@ public string GetSecret(string reference)
if (trimmedReference.Length == 0)
throw new ArgumentException($"{nameof(trimmedReference)} cannot be empty.", nameof(reference));
- var command = $"read {trimmedReference} --no-newline";
+ var command = new OpCommand("read", trimmedReference, "--no-newline");
return Op(command);
}
@@ -125,9 +152,9 @@ public void SaveSecret(string reference, string filePath, string? fileMode = nul
throw new ArgumentException($"{nameof(trimmedFilePath)} cannot be empty.", nameof(filePath));
var trimmedFileMode = fileMode?.Trim();
- var command = $"read {trimmedReference} --no-newline --force --out-file \"{trimmedFilePath}\"";
+ var command = new OpCommand("read", trimmedReference, "--no-newline", "--force", "--out-file", trimmedFilePath);
if (trimmedFileMode is not null && trimmedFileMode.Length > 0)
- command += $" --file-mode {trimmedFileMode}";
+ command.Add("--file-mode", trimmedFileMode);
Op(command);
}
@@ -150,7 +177,7 @@ private static OnePasswordManagerOptions ValidateOptions(OnePasswordManagerOptio
private string GetVersion()
{
- const string command = "--version";
+ var command = new OpCommand("--version");
return Op(command).Trim();
}
@@ -169,6 +196,21 @@ private static void EnsureExecutablePermissions(string executablePath)
chmod?.WaitForExit();
}
+ private static void ApplyFileMode(string fileMode, string filePath)
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ return;
+
+ using var chmod = Process.Start(new ProcessStartInfo("chmod", $"{fileMode} \"{filePath}\"")
+ {
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true
+ });
+ chmod?.WaitForExit();
+ }
+
private static string GetStandardError(Process process)
{
var error = new StringBuilder();
@@ -185,7 +227,7 @@ private static string GetStandardOutput(Process process)
return output.ToString();
}
- private TResult Op(JsonTypeInfo jsonTypeInfo, string command, string? input = null, bool returnError = false) where TResult : class
+ private TResult Op(JsonTypeInfo jsonTypeInfo, OpCommand command, string? input = null, bool returnError = false) where TResult : class
{
var result = Op(command, input is null ? Array.Empty() : [input], returnError);
var obj = JsonSerializer.Deserialize(result, jsonTypeInfo) ?? throw new SerializationException("Could not deserialize the command result.");
@@ -194,44 +236,44 @@ private TResult Op(JsonTypeInfo jsonTypeInfo, string command,
return obj;
}
- private string Op(string command, string? input = null, bool returnError = false) => Op(command, input is null ? Array.Empty() : [input], returnError);
+ private string Op(OpCommand command, string? input = null, bool returnError = false, bool formatOutput = true) => Op(command, input is null ? Array.Empty() : [input], returnError, formatOutput);
- private string Op(string command, IEnumerable input, bool returnError)
+ private string Op(OpCommand command, IEnumerable input, bool returnError, bool formatOutput = true)
{
- var arguments = command;
- if (command != "--version")
- arguments += " --format json --no-color";
+ var arguments = command.Clone();
+ if (!command.StartsWith(["--version"]) && formatOutput)
+ arguments.Add("--format", "json", "--no-color");
switch (_mode)
{
case Mode.ServiceAccount:
- if (IsUnsupportedCommand(command, _serviceAccountUnsupportedCommands))
+ if (IsCommandMatch(command, ServiceAccountUnsupportedCommands))
throw new InvalidOperationException($"Unsupported command {command} when using ServiceAccount");
break;
case Mode.Interactive:
case Mode.AppIntegrated:
default:
- var excluded = IsExcludedCommand(command, _excludedAccountCommands);
+ var excluded = IsCommandMatch(command, ExcludedAccountCommands);
var requireAccount = _mode != Mode.AppIntegrated && !excluded;
var passAccount = _account.Length != 0 && !excluded;
if (requireAccount && !passAccount)
throw new InvalidOperationException("Cannot execute command because account has not been set.");
- var passSession = !(_mode == Mode.AppIntegrated || IsExcludedCommand(command, _excludedSessionCommands));
+ var passSession = !(_mode == Mode.AppIntegrated || IsCommandMatch(command, ExcludedSessionCommands));
if (passSession && _session.Length == 0)
throw new InvalidOperationException("Cannot execute command because account has not been signed in.");
if (passAccount)
- arguments += $" --account {_account}";
+ arguments.Add("--account", _account);
if (passSession)
- arguments += $" --session {_session}";
+ arguments.Add("--session", _session);
break;
}
if (_verbose)
Console.WriteLine($"{Path.GetDirectoryName(_opPath)}>op {arguments}");
- var startInfo = new ProcessStartInfo(_opPath, arguments)
+ var startInfo = new ProcessStartInfo(_opPath)
{
CreateNoWindow = true,
UseShellExecute = false,
@@ -241,6 +283,15 @@ private string Op(string command, IEnumerable input, bool returnError)
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
+ if (IsWindowsCommandScript(_opPath))
+ {
+ startInfo.Arguments = arguments.ToString();
+ }
+ else
+ {
+ foreach (var argument in arguments)
+ startInfo.ArgumentList.Add(argument);
+ }
if (_mode == Mode.ServiceAccount)
startInfo.EnvironmentVariables["OP_SERVICE_ACCOUNT_TOKEN"] = _serviceAccountToken;
@@ -279,14 +330,59 @@ private string Op(string command, IEnumerable input, bool returnError)
throw new InvalidOperationException(error.Length > 28 ? error[28..].Trim() : error);
}
- private static bool IsExcludedCommand(string command, IEnumerable excludedCommands)
+ private static bool IsCommandMatch(OpCommand command, IEnumerable> commandPrefixes)
+ {
+ return commandPrefixes.Any(command.StartsWith);
+ }
+
+ private static bool IsWindowsCommandScript(string filePath)
{
- return excludedCommands.Any(x => command.StartsWith(x, StringComparison.InvariantCulture));
+ var extension = Path.GetExtension(filePath);
+ return RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
+ && (string.Equals(extension, ".cmd", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(extension, ".bat", StringComparison.OrdinalIgnoreCase));
}
- private static bool IsUnsupportedCommand(string command, IEnumerable unsupportedCommands)
+ private sealed class OpCommand : IEnumerable
{
- return unsupportedCommands.Any(x => command.StartsWith(x, StringComparison.InvariantCulture));
+ private readonly List _arguments;
+
+ public OpCommand(params string[] arguments)
+ {
+ _arguments = [.. arguments];
+ }
+
+ private OpCommand(IEnumerable arguments)
+ {
+ _arguments = [.. arguments];
+ }
+
+ public OpCommand Add(params string[] arguments)
+ {
+ _arguments.AddRange(arguments);
+ return this;
+ }
+
+ public OpCommand Clone() => new(_arguments);
+
+ public IEnumerator GetEnumerator() => _arguments.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+
+ public bool StartsWith(IReadOnlyList prefix)
+ {
+ return prefix.Count <= _arguments.Count
+ && prefix.Select((argument, index) => string.Equals(argument, _arguments[index], StringComparison.Ordinal)).All(static matches => matches);
+ }
+
+ public override string ToString() => string.Join(" ", _arguments.Select(FormatArgument));
+
+ private static string FormatArgument(string argument)
+ {
+ return argument.Any(char.IsWhiteSpace) || argument.Contains('"', StringComparison.Ordinal)
+ ? $"\"{argument.Replace("\"", "\\\"", StringComparison.Ordinal)}\""
+ : argument;
+ }
}
#if NET7_0_OR_GREATER
diff --git a/README.md b/README.md
index c85d976..57f763b 100644
--- a/README.md
+++ b/README.md
@@ -162,6 +162,25 @@ var attachment = itemWithAttachments.FileAttachments.First();
onePassword.SaveFileAttachmentContent(attachment, itemWithAttachments, vault, @"C:\Files\Production.env");
```
+### Reading variables from a 1Password Environment
+
+This feature requires the beta 1Password CLI `2.33.0-beta.02` or later.
+
+```csharp
+var variables = onePassword.GetEnvironmentVariables("environment-id");
+
+foreach (var variable in variables)
+ Console.WriteLine($"{variable.Name}={variable.Value}");
+```
+
+### Saving variables from a 1Password Environment
+
+This writes the Environment output to disk using the same `KEY=value` format returned by the CLI.
+
+```csharp
+onePassword.SaveEnvironmentVariables("environment-id", @".env");
+```
+
### Sharing an item without email restrictions
```csharp
diff --git a/docfx/docs/quick-start.md b/docfx/docs/quick-start.md
index 9f426e3..84aaec2 100644
--- a/docfx/docs/quick-start.md
+++ b/docfx/docs/quick-start.md
@@ -141,6 +141,25 @@ var attachment = itemWithAttachments.FileAttachments.First();
onePassword.SaveFileAttachmentContent(attachment, itemWithAttachments, vault, @"C:\Files\Production.env");
```
+### Reading variables from a 1Password Environment
+
+This feature requires the beta 1Password CLI `2.33.0-beta.02` or later.
+
+```csharp
+var variables = onePassword.GetEnvironmentVariables("environment-id");
+
+foreach (var variable in variables)
+ Console.WriteLine($"{variable.Name}={variable.Value}");
+```
+
+### Saving variables from a 1Password Environment
+
+This writes the Environment output to disk using the same `KEY=value` format returned by the CLI.
+
+```csharp
+onePassword.SaveEnvironmentVariables("environment-id", @".env");
+```
+
### Sharing an item without email restrictions
```csharp