appium-adb 16.1.2 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/build/lib/adb.d.ts +2 -0
  4. package/build/lib/adb.d.ts.map +1 -1
  5. package/build/lib/adb.js +5 -1
  6. package/build/lib/adb.js.map +1 -1
  7. package/build/lib/tools/android-manifest.js +1 -1
  8. package/build/lib/tools/android-manifest.js.map +1 -1
  9. package/build/lib/tools/apk-utils.js +3 -3
  10. package/build/lib/tools/apk-utils.js.map +1 -1
  11. package/build/lib/tools/app-commands.d.ts +4 -4
  12. package/build/lib/tools/app-commands.d.ts.map +1 -1
  13. package/build/lib/tools/app-commands.js +35 -36
  14. package/build/lib/tools/app-commands.js.map +1 -1
  15. package/build/lib/tools/device-settings.js +2 -2
  16. package/build/lib/tools/device-settings.js.map +1 -1
  17. package/build/lib/tools/fs-commands.js +2 -2
  18. package/build/lib/tools/fs-commands.js.map +1 -1
  19. package/build/lib/tools/lockmgmt.d.ts.map +1 -1
  20. package/build/lib/tools/lockmgmt.js +6 -2
  21. package/build/lib/tools/lockmgmt.js.map +1 -1
  22. package/build/lib/tools/system-calls.d.ts +14 -3
  23. package/build/lib/tools/system-calls.d.ts.map +1 -1
  24. package/build/lib/tools/system-calls.js +12 -6
  25. package/build/lib/tools/system-calls.js.map +1 -1
  26. package/build/lib/tools/types.d.ts +8 -0
  27. package/build/lib/tools/types.d.ts.map +1 -1
  28. package/build/lib/types.d.ts +2 -0
  29. package/build/lib/types.d.ts.map +1 -1
  30. package/lib/adb.ts +7 -1
  31. package/lib/tools/android-manifest.ts +1 -1
  32. package/lib/tools/apk-utils.ts +3 -3
  33. package/lib/tools/app-commands.ts +52 -50
  34. package/lib/tools/device-settings.ts +2 -2
  35. package/lib/tools/fs-commands.ts +2 -2
  36. package/lib/tools/lockmgmt.ts +6 -2
  37. package/lib/tools/system-calls.ts +29 -9
  38. package/lib/tools/types.ts +8 -0
  39. package/lib/types.ts +3 -0
  40. package/package.json +3 -3
@@ -80,7 +80,7 @@ export async function resolveLaunchableActivity(
80
80
  ): Promise<string> {
81
81
  const {preferCmd = true} = opts;
82
82
  if (!preferCmd || (await this.getApiLevel()) < 24) {
83
- const stdout = await this.shell(['dumpsys', 'package', pkg]);
83
+ const stdout = await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true});
84
84
  const names = parseLaunchableActivityNames(stdout);
85
85
  if (util.isEmpty(names)) {
86
86
  log.debug(stdout);
@@ -110,6 +110,7 @@ export async function resolveLaunchableActivity(
110
110
  }
111
111
  const {stdout, stderr} = await this.shell(['cmd', 'package', 'resolve-activity', '--brief', pkg], {
112
112
  outputFormat: this.EXEC_OUTPUT_FORMAT.FULL,
113
+ quoteArgs: true,
113
114
  });
114
115
  for (const line of (stdout || '').split('\n').map((x) => x.trim())) {
115
116
  if (this.isValidClass(line)) {
@@ -132,7 +133,7 @@ export async function resolveLaunchableActivity(
132
133
  */
133
134
  export async function forceStop(this: ADB, pkg: string): Promise<string> {
134
135
  assertSafeComponentName(pkg, 'application package name');
135
- return await this.shell(['am', 'force-stop', util.quote(pkg)]);
136
+ return await this.shell(['am', 'force-stop', pkg], {quoteArgs: true});
136
137
  }
137
138
 
138
139
  /**
@@ -147,7 +148,7 @@ export async function forceStop(this: ADB, pkg: string): Promise<string> {
147
148
  * @returns The output of the corresponding adb command.
148
149
  */
149
150
  export async function killPackage(this: ADB, pkg: string): Promise<string> {
150
- return await this.shell(['am', 'kill', pkg]);
151
+ return await this.shell(['am', 'kill', pkg], {quoteArgs: true});
151
152
  }
152
153
 
153
154
  /**
@@ -158,7 +159,7 @@ export async function killPackage(this: ADB, pkg: string): Promise<string> {
158
159
  * @returns The output of the corresponding adb command.
159
160
  */
160
161
  export async function clear(this: ADB, pkg: string): Promise<string> {
161
- return await this.shell(['pm', 'clear', pkg]);
162
+ return await this.shell(['pm', 'clear', pkg], {quoteArgs: true});
162
163
  }
163
164
 
164
165
  /**
@@ -180,7 +181,7 @@ export async function grantAllPermissions(this: ADB, pkg: string, apk?: string):
180
181
  * If apk not provided, considering apk already installed on the device
181
182
  * and fetching targetSdk using package name.
182
183
  */
183
- dumpsysOutput = await this.shell(['dumpsys', 'package', pkg]);
184
+ dumpsysOutput = await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true});
184
185
  targetSdk = await this.targetSdkVersionUsingPKG(pkg, dumpsysOutput);
185
186
  } else {
186
187
  targetSdk = await this.targetSdkVersionFromManifest(apk);
@@ -195,7 +196,7 @@ export async function grantAllPermissions(this: ADB, pkg: string, apk?: string):
195
196
  * The app has to list the permissions in the manifest.
196
197
  * refer: https://developer.android.com/training/permissions/requesting.html
197
198
  */
198
- dumpsysOutput = dumpsysOutput || (await this.shell(['dumpsys', 'package', pkg]));
199
+ dumpsysOutput = dumpsysOutput || (await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true}));
199
200
  const requestedPermissions = await this.getReqPermissions(pkg, dumpsysOutput);
200
201
  const grantedPermissions = await this.getGrantedPermissions(pkg, dumpsysOutput);
201
202
  const permissionsToGrant = requestedPermissions.filter((x) => !grantedPermissions.includes(x));
@@ -234,7 +235,7 @@ export async function grantPermissions(this: ADB, pkg: string, permissions: stri
234
235
  // adb shell buffer is limited
235
236
  log.debug(`Granting permissions ${JSON.stringify(permissions)} to '${pkg}'`);
236
237
  try {
237
- await this.shellChunks((perm) => ['pm', 'grant', pkg, perm], permissions);
238
+ await this.shellChunks((perm) => ['pm', 'grant', pkg, perm], permissions, {quoteArgs: true});
238
239
  } catch (e) {
239
240
  const err = e as ExecError;
240
241
  if (!IGNORED_PERM_ERRORS.some((pattern) => pattern.test(err.stderr || err.message))) {
@@ -252,7 +253,7 @@ export async function grantPermissions(this: ADB, pkg: string, permissions: stri
252
253
  */
253
254
  export async function grantPermission(this: ADB, pkg: string, permission: string): Promise<void> {
254
255
  try {
255
- await this.shell(['pm', 'grant', pkg, permission]);
256
+ await this.shell(['pm', 'grant', pkg, permission], {quoteArgs: true});
256
257
  } catch (e) {
257
258
  const err = e as ExecError;
258
259
  if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
@@ -270,7 +271,7 @@ export async function grantPermission(this: ADB, pkg: string, permission: string
270
271
  */
271
272
  export async function revokePermission(this: ADB, pkg: string, permission: string): Promise<void> {
272
273
  try {
273
- await this.shell(['pm', 'revoke', pkg, permission]);
274
+ await this.shell(['pm', 'revoke', pkg, permission], {quoteArgs: true});
274
275
  } catch (e) {
275
276
  const err = e as ExecError;
276
277
  if (!NOT_CHANGEABLE_PERM_ERROR.test(err.stderr || err.message)) {
@@ -294,7 +295,7 @@ export async function getGrantedPermissions(
294
295
  cmdOutput: string | null = null,
295
296
  ): Promise<string[]> {
296
297
  log.debug('Retrieving granted permissions');
297
- const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg]));
298
+ const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true}));
298
299
  return extractMatchingPermissions(stdout, ['install', 'runtime'], true);
299
300
  }
300
301
 
@@ -308,7 +309,7 @@ export async function getGrantedPermissions(
308
309
  */
309
310
  export async function getDeniedPermissions(this: ADB, pkg: string, cmdOutput: string | null = null): Promise<string[]> {
310
311
  log.debug('Retrieving denied permissions');
311
- const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg]));
312
+ const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true}));
312
313
  return extractMatchingPermissions(stdout, ['install', 'runtime'], false);
313
314
  }
314
315
 
@@ -322,7 +323,7 @@ export async function getDeniedPermissions(this: ADB, pkg: string, cmdOutput: st
322
323
  */
323
324
  export async function getReqPermissions(this: ADB, pkg: string, cmdOutput: string | null = null): Promise<string[]> {
324
325
  log.debug('Retrieving requested permissions');
325
- const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg]));
326
+ const stdout = cmdOutput || (await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true}));
326
327
  return extractMatchingPermissions(stdout, ['requested']);
327
328
  }
328
329
 
@@ -352,7 +353,7 @@ export async function getPackageInfo(this: ADB, pkg: string): Promise<AppInfo> {
352
353
  const result: AppInfo = {name: pkg};
353
354
  let stdout: string;
354
355
  try {
355
- stdout = await this.shell(['dumpsys', 'package', pkg]);
356
+ stdout = await this.shell(['dumpsys', 'package', pkg], {quoteArgs: true});
356
357
  } catch (err) {
357
358
  const error = err as Error;
358
359
  log.debug(error.stack);
@@ -386,7 +387,7 @@ export async function getPackageInfo(this: ADB, pkg: string): Promise<AppInfo> {
386
387
  * @throws {Error} If there was an error while fetching the .apk
387
388
  */
388
389
  export async function pullApk(this: ADB, pkg: string, tmpDir: string): Promise<string> {
389
- const stdout = (await this.shell(['pm', 'path', pkg])).trim();
390
+ const stdout = (await this.shell(['pm', 'path', pkg], {quoteArgs: true})).trim();
390
391
  const packageMarker = 'package:';
391
392
  if (!stdout.startsWith(packageMarker)) {
392
393
  throw new Error(`Cannot pull the .apk package for '${pkg}'. Original error: ${stdout}`);
@@ -417,7 +418,7 @@ export async function activateApp(this: ADB, appId: string): Promise<void> {
417
418
  const cmd = ['monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'];
418
419
  let output: string;
419
420
  try {
420
- output = await this.shell(cmd);
421
+ output = await this.shell(cmd, {quoteArgs: true});
421
422
  log.debug(`Command stdout: ${output}`);
422
423
  } catch (e) {
423
424
  const error = e as Error;
@@ -439,21 +440,24 @@ export async function activateApp(this: ADB, appId: string): Promise<void> {
439
440
  activityName = await this.resolveLaunchableActivity(appId, {preferCmd: false});
440
441
  }
441
442
 
442
- const stdout = await this.shell([
443
- 'am',
444
- apiLevel < 26 ? 'start' : 'start-activity',
445
- '-a',
446
- 'android.intent.action.MAIN',
447
- '-c',
448
- 'android.intent.category.LAUNCHER',
449
- // FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
450
- // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
451
- // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
452
- '-f',
453
- '0x10200000',
454
- '-n',
455
- activityName,
456
- ]);
443
+ const stdout = await this.shell(
444
+ [
445
+ 'am',
446
+ apiLevel < 26 ? 'start' : 'start-activity',
447
+ '-a',
448
+ 'android.intent.action.MAIN',
449
+ '-c',
450
+ 'android.intent.category.LAUNCHER',
451
+ // FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
452
+ // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_NEW_TASK
453
+ // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
454
+ '-f',
455
+ '0x10200000',
456
+ '-n',
457
+ activityName,
458
+ ],
459
+ {quoteArgs: true},
460
+ );
457
461
  log.debug(stdout);
458
462
  if (/^error:/im.test(stdout)) {
459
463
  throw new Error(`Cannot activate '${appId}'. Original error: ${stdout}`);
@@ -479,7 +483,7 @@ export async function isAppInstalled(this: ADB, pkg: string, opts: IsAppInstalle
479
483
  cmd.push('--user', user);
480
484
  }
481
485
  cmd.push(pkg);
482
- const stdout = await this.shell(cmd);
486
+ const stdout = await this.shell(cmd, {quoteArgs: true});
483
487
  isInstalled = /^package:/m.test(stdout);
484
488
  } catch {
485
489
  isInstalled = false;
@@ -519,12 +523,12 @@ export async function listInstalledPackages(
519
523
 
520
524
  let stdout: string;
521
525
  try {
522
- stdout = await this.shell(cmd);
526
+ stdout = await this.shell(cmd, {quoteArgs: true});
523
527
  } catch (e) {
524
528
  const error = e as ExecError;
525
529
  // https://github.com/appium/appium-uiautomator2-driver/issues/810
526
530
  if ((error.stderr || error.stdout || error.message)?.includes('access user') && util.isEmpty(user)) {
527
- stdout = await this.shell([...cmd, '--user', '0']);
531
+ stdout = await this.shell([...cmd, '--user', '0'], {quoteArgs: true});
528
532
  } else {
529
533
  throw e;
530
534
  }
@@ -561,16 +565,14 @@ export async function startUri(
561
565
  if (waitForLaunch) {
562
566
  args.push('-W');
563
567
  }
564
- // adb joins these arguments into a command parsed by the device's shell,
565
- // regardless of the host operating system.
566
- args.push('-a', 'android.intent.action.VIEW', '-d', util.quote(uri));
568
+ args.push('-a', 'android.intent.action.VIEW', '-d', uri);
567
569
  if (pkg) {
568
570
  assertSafeComponentName(pkg, 'application package name');
569
- args.push(util.quote(pkg));
571
+ args.push(pkg);
570
572
  }
571
573
 
572
574
  try {
573
- const res = await this.shell(args);
575
+ const res = await this.shell(args, {quoteArgs: true});
574
576
  if (res.toLowerCase().includes('unable to resolve intent')) {
575
577
  throw new Error(res);
576
578
  }
@@ -616,7 +618,7 @@ export async function startApp(this: ADB, startAppOptions: StartAppOptions): Pro
616
618
  if (options.waitDuration !== undefined && Number.isInteger(options.waitDuration) && options.waitDuration >= 0) {
617
619
  shellOpts.timeout = options.waitDuration;
618
620
  }
619
- const stdout = await this.shell(cmd, shellOpts);
621
+ const stdout = await this.shell(cmd, {...shellOpts, quoteArgs: true});
620
622
  if (stdout.includes('Error: Activity class') && stdout.includes('does not exist')) {
621
623
  if (options.retry && options.activity && !options.activity.startsWith('.')) {
622
624
  log.debug(
@@ -886,41 +888,41 @@ export async function waitForNotActivity(this: ADB, pkg: string, act: string, wa
886
888
  }
887
889
 
888
890
  /**
889
- * Builds command line representation for the given
890
- * application startup options
891
+ * Builds literal arguments for the given application startup options.
892
+ * Execute the result with `shell` with `quoteArgs: true`, not `shell`.
891
893
  *
892
894
  * @param startAppOptions - Application options mapping
893
895
  * @param apiLevel - The actual OS API level
894
- * @returns The actual command line array
896
+ * @returns Unquoted command arguments
895
897
  */
896
898
  export function buildStartCmd(startAppOptions: StartCmdOptions, apiLevel: number): string[] {
897
899
  const {user, waitForLaunch, pkg, activity, action, category, stopApp, flags, optionalIntentArguments} =
898
900
  startAppOptions;
899
901
  const cmd = ['am', apiLevel < 26 ? 'start' : 'start-activity'];
900
902
  if (util.hasValue(user)) {
901
- cmd.push('--user', util.quote(`${user}`));
903
+ cmd.push('--user', `${user}`);
902
904
  }
903
905
  if (waitForLaunch) {
904
906
  cmd.push('-W');
905
907
  }
906
908
  if (activity && pkg) {
907
909
  const component = activity.startsWith(`${pkg}/`) ? activity : `${pkg}/${activity}`;
908
- cmd.push('-n', util.quote(component));
910
+ cmd.push('-n', component);
909
911
  }
910
912
  if (stopApp && apiLevel >= 15) {
911
913
  cmd.push('-S');
912
914
  }
913
915
  if (action) {
914
- cmd.push('-a', util.quote(action));
916
+ cmd.push('-a', action);
915
917
  }
916
918
  if (category) {
917
- cmd.push('-c', util.quote(category));
919
+ cmd.push('-c', category);
918
920
  }
919
921
  if (flags) {
920
- cmd.push('-f', util.quote(flags));
922
+ cmd.push('-f', flags);
921
923
  }
922
924
  if (optionalIntentArguments) {
923
- cmd.push(...parseOptionalIntentArguments(optionalIntentArguments).map((arg) => util.quote(arg)));
925
+ cmd.push(...parseOptionalIntentArguments(optionalIntentArguments));
924
926
  }
925
927
  return cmd;
926
928
  }
@@ -1023,7 +1025,7 @@ export function matchComponentName(classString: string): RegExpExecArray | null
1023
1025
  /**
1024
1026
  * Asserts a package/activity name has only legal component characters, rejecting shell
1025
1027
  * metacharacters to prevent command injection. The permitted `$` and `*` are still
1026
- * shell-expansion characters and are escaped separately in `buildStartCmd`.
1028
+ * shell-expansion characters and are quoted at execution time by `shell` with `quoteArgs: true`.
1027
1029
  *
1028
1030
  * @param value - The package or activity name to verify
1029
1031
  * @param kind - A human-readable label for the value used in the error message
@@ -1112,7 +1114,7 @@ export async function broadcast(this: ADB, intent: string): Promise<void> {
1112
1114
  throw new Error(`Invalid intent ${intent}`);
1113
1115
  }
1114
1116
  log.debug(`Broadcasting: ${intent}`);
1115
- await this.shell(['am', 'broadcast', '-a', intent]);
1117
+ await this.shell(['am', 'broadcast', '-a', intent], {quoteArgs: true});
1116
1118
  }
1117
1119
 
1118
1120
  /**
@@ -195,7 +195,7 @@ export async function setSetting(
195
195
  setting: string,
196
196
  value: string | number,
197
197
  ): Promise<string> {
198
- return await this.shell(['settings', 'put', util.quote(namespace), util.quote(setting), util.quote(`${value}`)]);
198
+ return await this.shell(['settings', 'put', namespace, setting, `${value}`], {quoteArgs: true});
199
199
  }
200
200
 
201
201
  /**
@@ -514,7 +514,7 @@ export async function addToDeviceIdleWhitelist(this: ADB, ...packages: string[])
514
514
  }
515
515
 
516
516
  log.info(`Adding ${util.pluralize('package', packages.length)} ${JSON.stringify(packages)} to Doze whitelist`);
517
- await this.shellChunks((pkg) => ['dumpsys', 'deviceidle', 'whitelist', `+${pkg}`], packages);
517
+ await this.shellChunks((pkg) => ['dumpsys', 'deviceidle', 'whitelist', `+${pkg}`], packages, {quoteArgs: true});
518
518
  return true;
519
519
  }
520
520
 
@@ -32,7 +32,7 @@ export async function fileExists(this: ADB, remotePath: string): Promise<boolean
32
32
  export async function ls(this: ADB, remotePath: string, opts: string[] = []): Promise<string[]> {
33
33
  try {
34
34
  const args = ['ls', ...opts, remotePath];
35
- const stdout = await this.shell(args);
35
+ const stdout = await this.shell(args, {quoteArgs: true});
36
36
  const lines = stdout.split('\n');
37
37
  return lines
38
38
  .map((l) => l.trim())
@@ -79,7 +79,7 @@ export async function fileSize(this: ADB, remotePath: string): Promise<number> {
79
79
  * @param path - The path to be removed recursively.
80
80
  */
81
81
  export async function rimraf(this: ADB, path: string): Promise<void> {
82
- await this.shell(['rm', '-rf', path]);
82
+ await this.shell(['rm', '-rf', path], {quoteArgs: true});
83
83
  }
84
84
 
85
85
  /**
@@ -41,6 +41,7 @@ export async function verifyLockCredential(this: ADB, credential: string | null
41
41
  try {
42
42
  const {stdout, stderr} = await this.shell(buildCommand('verify', credential), {
43
43
  outputFormat: this.EXEC_OUTPUT_FORMAT.FULL,
44
+ quoteArgs: true,
44
45
  });
45
46
  if (stdout?.includes('verified successfully')) {
46
47
  return true;
@@ -78,6 +79,7 @@ export async function clearLockCredential(this: ADB, credential: string | null =
78
79
  try {
79
80
  const {stdout, stderr} = await this.shell(buildCommand('clear', credential), {
80
81
  outputFormat: this.EXEC_OUTPUT_FORMAT.FULL,
82
+ quoteArgs: true,
81
83
  });
82
84
  if (!['user has no password', 'Lock credential cleared'].some((x) => (stderr || stdout)?.includes(x))) {
83
85
  throw new Error(stderr || stdout);
@@ -106,6 +108,7 @@ export async function isLockEnabled(this: ADB): Promise<boolean> {
106
108
  try {
107
109
  const {stdout, stderr} = await this.shell(buildCommand('get-disabled'), {
108
110
  outputFormat: this.EXEC_OUTPUT_FORMAT.FULL,
111
+ quoteArgs: true,
109
112
  });
110
113
  if (
111
114
  /\bfalse\b/.test(stdout) ||
@@ -160,6 +163,7 @@ export async function setLockCredential(
160
163
  try {
161
164
  const {stdout, stderr} = await this.shell(cmd, {
162
165
  outputFormat: this.EXEC_OUTPUT_FORMAT.FULL,
166
+ quoteArgs: true,
163
167
  });
164
168
  if (!stdout?.includes('set to')) {
165
169
  throw new Error(stderr || stdout);
@@ -318,10 +322,10 @@ export function isScreenOnFully(dumpsys: string): boolean {
318
322
  function buildCommand(verb: string, oldCredential: string | null = null, ...args: string[]): string[] {
319
323
  const cmd = ['locksettings', verb];
320
324
  if (oldCredential && !util.isEmpty(oldCredential)) {
321
- cmd.push('--old', util.quote(`${oldCredential}`));
325
+ cmd.push('--old', `${oldCredential}`);
322
326
  }
323
327
  if (args.length > 0) {
324
- cmd.push(...args.map((arg) => util.quote(`${arg}`)));
328
+ cmd.push(...args.map((arg) => `${arg}`));
325
329
  }
326
330
  return cmd;
327
331
  }
@@ -465,7 +465,12 @@ export const EXEC_OUTPUT_FORMAT = {
465
465
  } as const;
466
466
 
467
467
  /**
468
- * Execute the given adb command.
468
+ * Execute the given adb command without automatically quoting its arguments.
469
+ *
470
+ * When invoking `adb shell`, the device shell interprets the command.
471
+ * Unquoted values may be expanded or interpreted as shell syntax.
472
+ * For literal shell arguments, prefer `shell(args, {quoteArgs: true})`
473
+ * and pass unquoted argument values. `adbExec` itself does not apply `quoteArgs`.
469
474
  *
470
475
  * @param cmd - Command string or array of command arguments
471
476
  * @param opts - Execution options with `outputFormat: 'full'`
@@ -478,7 +483,12 @@ export async function adbExec(
478
483
  opts: AdbExecOptions & {outputFormat: 'full'},
479
484
  ): Promise<ExecResult>;
480
485
  /**
481
- * Execute the given adb command.
486
+ * Execute the given adb command without automatically quoting its arguments.
487
+ *
488
+ * When invoking `adb shell`, the device shell interprets the command.
489
+ * Unquoted values may be expanded or interpreted as shell syntax.
490
+ * For literal shell arguments, prefer `shell(args, {quoteArgs: true})`
491
+ * and pass unquoted argument values. `adbExec` itself does not apply `quoteArgs`.
482
492
  *
483
493
  * @param cmd - Command string or array of command arguments
484
494
  * @param opts - Execution options
@@ -593,9 +603,10 @@ export async function shell(
593
603
  */
594
604
  export async function shell(this: ADB, cmd: string | string[], opts?: ShellExecOptions): Promise<string>;
595
605
  export async function shell(this: ADB, cmd: string | string[], opts?: ShellExecOptions): Promise<string | ExecResult> {
596
- const {privileged} = opts ?? {};
597
-
598
- const cmdArr = Array.isArray(cmd) ? cmd : [cmd];
606
+ const {quoteArgs = false, ...execOpts} = opts ?? {};
607
+ const {privileged} = execOpts;
608
+ const args = Array.isArray(cmd) ? cmd : [cmd];
609
+ const cmdArr = quoteArgs ? args.map((arg) => util.quote(arg)) : args;
599
610
  const fullCmd: string[] = ['shell'];
600
611
  if (privileged) {
601
612
  log.info(`'adb shell ${util.quote(redactSecrets(cmdArr))}' requires root access`);
@@ -608,7 +619,7 @@ export async function shell(this: ADB, cmd: string | string[], opts?: ShellExecO
608
619
  } else {
609
620
  fullCmd.push(...cmdArr);
610
621
  }
611
- return await this.adbExec(fullCmd, opts);
622
+ return await this.adbExec(fullCmd, opts === undefined ? undefined : execOpts);
612
623
  }
613
624
 
614
625
  /**
@@ -1368,16 +1379,25 @@ export async function isMitmCertificateInstalled(this: ADB, cert: Buffer | strin
1368
1379
  *
1369
1380
  * @param argTransformer - Function to transform each argument into command array
1370
1381
  * @param args - Array of arguments to process
1382
+ * @param opts - Set quoteArgs to true for unquoted literal command arguments (default false)
1371
1383
  * @throws {Error} If argument transformer returns invalid result or command execution fails
1372
1384
  */
1373
- export async function shellChunks(this: ADB, argTransformer: (x: string) => string[], args: string[]): Promise<void> {
1385
+ export async function shellChunks(
1386
+ this: ADB,
1387
+ argTransformer: (x: string) => string[],
1388
+ args: string[],
1389
+ opts: Pick<ShellExecOptions, 'quoteArgs'> = {},
1390
+ ): Promise<void> {
1374
1391
  const commands: string[][] = [];
1375
1392
  let cmdChunk: string[] = [];
1376
1393
  for (const arg of args) {
1377
- const nextCmd = argTransformer(arg);
1378
- if (!Array.isArray(nextCmd)) {
1394
+ const command = argTransformer(arg);
1395
+ if (!Array.isArray(command)) {
1379
1396
  throw new Error('Argument transformer must result in an array');
1380
1397
  }
1398
+ // Quote before measuring chunks and adding separators. A caller-supplied
1399
+ // semicolon is literal data when quoting is enabled.
1400
+ const nextCmd = opts.quoteArgs ? command.map((value) => util.quote(value)) : command;
1381
1401
  if (nextCmd.at(-1) !== ';') {
1382
1402
  nextCmd.push(';');
1383
1403
  }
@@ -637,6 +637,14 @@ export interface SpecialAdbExecOptions {
637
637
  }
638
638
 
639
639
  export interface ShellExecOptions {
640
+ /**
641
+ * Quote each command argument as literal data for the device shell.
642
+ * Pass unquoted arguments when enabled. A string command is treated as one
643
+ * literal argument; use an array for an executable and its arguments.
644
+ * Defaults to false for compatibility with existing shell expressions.
645
+ * TODO: Enable by default in the next breaking release.
646
+ */
647
+ quoteArgs?: boolean;
640
648
  /**
641
649
  * The name of the corresponding Appium's timeout capability
642
650
  * (used in the error messages).
package/lib/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type {AppiumLoggerLevel} from '@appium/types';
2
+
1
3
  import type {Logcat} from './logcat.js';
2
4
  import type {StringRecord} from './tools/types.js';
3
5
 
@@ -27,6 +29,7 @@ export interface ADBOptions {
27
29
  remoteAdbPort?: number;
28
30
  clearDeviceLogsOnStart?: boolean;
29
31
  listenAllNetwork?: boolean;
32
+ logLevel?: AppiumLoggerLevel;
30
33
  }
31
34
 
32
35
  export interface ADBExecutable {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appium-adb",
3
- "version": "16.1.2",
3
+ "version": "16.3.0",
4
4
  "description": "Android Debug Bridge interface",
5
5
  "homepage": "https://appium.io",
6
6
  "bugs": {
@@ -57,12 +57,12 @@
57
57
  "@types/ini": "^4.1.0",
58
58
  "@types/node": "^26.6.5",
59
59
  "@types/semver": "^7.5.0",
60
- "@types/sinon": "^22.0.0",
60
+ "@types/sinon": "^22.0.2",
61
61
  "sinon": "^22.0.0"
62
62
  },
63
63
  "engines": {
64
64
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
65
65
  "npm": ">=10"
66
66
  },
67
- "gitHead": "dd7fac6576be570d54de8ff2874b88426b4446dd"
67
+ "gitHead": "0cfdd5ff3c91b690c3f07a9efc400415c7bd24bb"
68
68
  }