Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/simulator-control-literals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eas-cli': patch
---

Preserve the input order of repeated simulator control arguments. Values after `--`, including `name=value`, are now positional literals; place named arguments before `--`.
26 changes: 22 additions & 4 deletions packages/eas-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2788,8 +2788,9 @@ USAGE
$ eas sim:control [ACTION...] [-h] [--id <value>] [--list] [--output <value>] [--json] [--non-interactive]

ARGUMENTS
[ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before
values that start with --.
[ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after
-- are positional literals, including text that starts with -- or name=. Shell quotes alone do not
disable name=value parsing.

FLAGS
-h, --help Show command help, or usage for the selected action from the session.
Expand Down Expand Up @@ -2817,6 +2818,14 @@ EXAMPLES

$ eas simulator:control clipboard.write -- --some-text

Write hello using the named text parameter (shell quotes do not change parsing):

$ eas simulator:control clipboard.write 'text=hello'

Write the literal text=hello using --:

$ eas simulator:control clipboard.write -- 'text=hello'

$ eas simulator:control hardwareKeyboard.set off

$ eas simulator:control permissions.set grant camera com.example.app
Expand Down Expand Up @@ -3263,8 +3272,9 @@ USAGE
$ eas simulator:control [ACTION...] [-h] [--id <value>] [--list] [--output <value>] [--json] [--non-interactive]

ARGUMENTS
[ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before
values that start with --.
[ACTION...] Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after
-- are positional literals, including text that starts with -- or name=. Shell quotes alone do not
disable name=value parsing.

FLAGS
-h, --help Show command help, or usage for the selected action from the session.
Expand Down Expand Up @@ -3292,6 +3302,14 @@ EXAMPLES

$ eas simulator:control clipboard.write -- --some-text

Write hello using the named text parameter (shell quotes do not change parsing):

$ eas simulator:control clipboard.write 'text=hello'

Write the literal text=hello using --:

$ eas simulator:control clipboard.write -- 'text=hello'

$ eas simulator:control hardwareKeyboard.set off

$ eas simulator:control permissions.set grant camera com.example.app
Expand Down
91 changes: 91 additions & 0 deletions packages/eas-cli/src/commands/simulator/__tests__/control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,91 @@ describe(SimulatorControl, () => {
}
});

it.each(['text=hello', '--', '--help', ''])(
'preserves the literal value after the separator: %s',
async value => {
served = {
...catalog,
'clipboard.write': {
type: 'object',
properties: { udid: { type: 'string' }, text: { type: 'string' } },
required: ['udid', 'text'],
},
};
await createCommand(['clipboard.write', '--id', 'session-123', '--', value]).runAsync();
expect(actionRequests()).toEqual([['clipboard.write', { text: value }]]);
}
);

it.each([
{
argv: ['permissions.set', '--id', 's', 'action=grant', '--', 's', 'camera'],
params: { action: 'grant', service: 's', bundleId: 'camera' },
},
{
argv: ['permissions.set', '--id', 's', '--', 'grant', '--', 'com.app'],
params: { action: 'grant', service: '--', bundleId: 'com.app' },
},
{
argv: ['permissions.set', '--id', 's', '--', 'grant', 'permissions.set', 'com.app'],
params: { action: 'grant', service: 'permissions.set', bundleId: 'com.app' },
},
{
argv: ['permissions.set', '--id', 'camera', 'grant', 'microphone', 'camera'],
params: { action: 'grant', service: 'microphone', bundleId: 'camera' },
},
])(
'preserves input order when positional tokens repeat earlier input: $argv',
async ({ argv, params }) => {
await createCommand(argv).runAsync();
expect(actionRequests()).toEqual([['permissions.set', params]]);
}
);

it('treats values as literal when the separator precedes the action', async () => {
served = {
...catalog,
'clipboard.write': {
type: 'object',
properties: { udid: { type: 'string' }, text: { type: 'string' } },
required: ['udid', 'text'],
},
};
await createCommand(['--id', 'session-123', '--', 'clipboard.write', 'text=hello']).runAsync();
expect(actionRequests()).toEqual([['clipboard.write', { text: 'text=hello' }]]);
});

it('does not treat a flag value of -- as the literal separator', async () => {
served = {
...catalog,
'clipboard.write': {
type: 'object',
properties: { udid: { type: 'string' }, text: { type: 'string' } },
required: ['udid', 'text'],
},
};
await createCommand(['clipboard.write', 'text=hello', '--id', '--']).runAsync();
expect(actionRequests()).toEqual([['clipboard.write', { text: 'hello' }]]);
});

it('keeps named arguments before the separator and positional values after it', async () => {
await createCommand([
'permissions.set',
'action=grant',
'--id',
'session-123',
'--',
'service=camera',
'bundleId=literal',
]).runAsync();
expect(actionRequests()).toEqual([
[
'permissions.set',
{ action: 'grant', service: 'service=camera', bundleId: 'bundleId=literal' },
],
]);
});

it('runs an action from the catalog with its values and prints the output', async () => {
await createCommand(['hardwareKeyboard.set', 'off', '--id', 'session-123']).runAsync();

Expand Down Expand Up @@ -212,6 +297,12 @@ describe(SimulatorControl, () => {
expect(jest.mocked(Log.log)).toHaveBeenCalledWith(
' eas simulator:control permissions.set <action> <service> <bundleId>'
);
const lines = jest.mocked(Log.log).mock.calls.map(([line]) => String(line));
const usage = lines.indexOf(
' eas simulator:control permissions.set <action> <service> <bundleId>'
);
expect(lines[usage + 1]).toBe(' [--id <session-id>] [--json]');
expect(lines.indexOf(' Values after -- are positional literals.')).toBeGreaterThan(usage + 1);
expect(actionRequests()).toEqual([]);
expect(close).toHaveBeenCalledTimes(1);
});
Expand Down
43 changes: 39 additions & 4 deletions packages/eas-cli/src/commands/simulator/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export default class SimulatorControl extends EasCommand {
'$ eas simulator:control --id <session-id>',
'$ eas simulator:control screenshot.capture --help',
'$ eas simulator:control clipboard.write -- --some-text',
{
description:
'Write hello using the named text parameter (shell quotes do not change parsing):',
command: "$ eas simulator:control clipboard.write 'text=hello'",
},
{
description: 'Write the literal text=hello using --:',
command: "$ eas simulator:control clipboard.write -- 'text=hello'",
},
'$ eas simulator:control hardwareKeyboard.set off',
'$ eas simulator:control permissions.set grant camera com.example.app',
'$ eas simulator:control app.install ./MyApp.ipa',
Expand All @@ -53,7 +62,7 @@ export default class SimulatorControl extends EasCommand {
static override args = {
action: Args.string({
description:
'Action to run, followed by its values in order or as name=value. Omit to list the actions. Put -- before values that start with --.',
'Action to run, followed by its values in order or as name=value. Omit to list the actions. Values after -- are positional literals, including text that starts with -- or name=. Shell quotes alone do not disable name=value parsing.',
}),
};

Expand Down Expand Up @@ -81,7 +90,28 @@ export default class SimulatorControl extends EasCommand {
};

async runAsync(): Promise<void> {
const { args, argv, flags } = await this.parse(SimulatorControl);
const { args, raw, flags } = await this.parse(SimulatorControl);
// oclif's argv sorts repeated values by their first occurrence in the input.
// Raw arg tokens preserve their actual order, including literals after --.
const values = raw
.filter(token => token.type === 'arg')
.map(token => token.input)
.slice(1);
let literalFrom = values.length;
for (let index = 0; index < this.argv.length; index++) {
const value = this.argv[index];
if (value === '--') {
literalFrom = Math.max(0, values.length - (this.argv.length - index - 1));
break;
}
// A standalone -- consumed as a flag value is not an argument separator.
const flag = value.startsWith('--')
? SimulatorControl.flags[value.slice(2) as keyof typeof SimulatorControl.flags]
: undefined;
if (flag?.type === 'option') {
index++;
}
}
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);

if (jsonFlag) {
Expand Down Expand Up @@ -137,7 +167,8 @@ export default class SimulatorControl extends EasCommand {
await this.runActionAsync(connection, entry, {
sessionId,
action,
values: argv.slice(1).map(String),
values,
literalFrom,
outputPath: flags.output,
jsonFlag,
});
Expand Down Expand Up @@ -181,6 +212,8 @@ export default class SimulatorControl extends EasCommand {
const outputFlag = entry.output === 'base64' ? '--output <path>' : '[--output <path>]';
Log.log(` ${entry.output ? `${outputFlag} ` : ''}[--id <session-id>] [--json]`);
Log.newLine();
Log.log(' Values after -- are positional literals.');
Log.newLine();
Log.log(` ${chalk.bold('OPTIONS')}`);
Log.log(` --id <session-id> Defaults to ${SIMULATOR_DOTENV_FILE_NAME}.`);
Log.log(' --json Print the action schema as JSON. Implies --non-interactive.');
Expand Down Expand Up @@ -208,17 +241,19 @@ export default class SimulatorControl extends EasCommand {
sessionId,
action,
values,
literalFrom,
outputPath,
jsonFlag,
}: {
sessionId: string;
action: string;
values: string[];
literalFrom: number;
outputPath: string | undefined;
jsonFlag: boolean;
}
): Promise<void> {
const params = buildHostActionParams(action, entry, values);
const params = buildHostActionParams(action, entry, values, literalFrom);
await assertHostActionOutputAsync(action, entry, outputPath);
const localFile = await findLocalUploadAsync(entry, params);

Expand Down
7 changes: 4 additions & 3 deletions packages/eas-cli/src/simulator/hostActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,13 +392,14 @@ export function formatHostActionUsage(name: string, entry: HostActionCatalogEntr
export function buildHostActionParams(
name: string,
entry: HostActionCatalogEntry,
values: string[]
values: string[],
literalFrom = values.length
): Record<string, unknown> {
const properties = hostActionProperties(entry);
const params: Record<string, unknown> = {};
const positional: string[] = [];
for (const value of values) {
const named = NAMED_VALUE.exec(value);
for (const [index, value] of values.entries()) {
const named = index < literalFrom ? NAMED_VALUE.exec(value) : null;
if (!named || !properties.has(named[1])) {
positional.push(value);
} else if (Object.hasOwn(params, named[1])) {
Expand Down
Loading