bin.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #!/usr/bin/env node
  2. import { foregroundChild } from 'foreground-child';
  3. import { existsSync } from 'fs';
  4. import { jack } from 'jackspeak';
  5. import { loadPackageJson } from 'package-json-from-dist';
  6. import { basename, join } from 'path';
  7. import { globStream } from './index.js';
  8. const { version } = loadPackageJson(import.meta.url, '../package.json');
  9. const j = jack({
  10. usage: 'glob [options] [<pattern> [<pattern> ...]]',
  11. })
  12. .description(`
  13. Glob v${version}
  14. Expand the positional glob expression arguments into any matching file
  15. system paths found.
  16. `)
  17. .opt({
  18. cmd: {
  19. short: 'c',
  20. hint: 'command',
  21. description: `Run the command provided, passing the glob expression
  22. matches as arguments.`,
  23. },
  24. })
  25. .opt({
  26. default: {
  27. short: 'p',
  28. hint: 'pattern',
  29. description: `If no positional arguments are provided, glob will use
  30. this pattern`,
  31. },
  32. })
  33. .flag({
  34. shell: {
  35. default: false,
  36. description: `Interpret the command as a shell command by passing it
  37. to the shell, with all matched filesystem paths appended,
  38. **even if this cannot be done safely**.
  39. This is **not** unsafe (and usually unnecessary) when using
  40. the known Unix shells sh, bash, zsh, and fish, as these can
  41. all be executed in such a way as to pass positional
  42. arguments safely.
  43. **Note**: THIS IS UNSAFE IF THE FILE PATHS ARE UNTRUSTED,
  44. because a path like \`'some/path/\\$\\(cmd)'\` will be
  45. executed by the shell.
  46. If you do have positional arguments that you wish to pass to
  47. the command ahead of the glob pattern matches, use the
  48. \`--cmd-arg\`/\`-g\` option instead.
  49. The next major release of glob will fully remove the ability
  50. to use this option unsafely.`,
  51. },
  52. })
  53. .optList({
  54. 'cmd-arg': {
  55. short: 'g',
  56. hint: 'arg',
  57. default: [],
  58. description: `Pass the provided values to the supplied command, ahead of
  59. the glob matches.
  60. For example, the command:
  61. glob -c echo -g"hello" -g"world" *.txt
  62. might output:
  63. hello world a.txt b.txt
  64. This is a safer (and future-proof) alternative than putting
  65. positional arguments in the \`-c\`/\`--cmd\` option.`,
  66. },
  67. })
  68. .flag({
  69. all: {
  70. short: 'A',
  71. description: `By default, the glob cli command will not expand any
  72. arguments that are an exact match to a file on disk.
  73. This prevents double-expanding, in case the shell expands
  74. an argument whose filename is a glob expression.
  75. For example, if 'app/*.ts' would match 'app/[id].ts', then
  76. on Windows powershell or cmd.exe, 'glob app/*.ts' will
  77. expand to 'app/[id].ts', as expected. However, in posix
  78. shells such as bash or zsh, the shell will first expand
  79. 'app/*.ts' to a list of filenames. Then glob will look
  80. for a file matching 'app/[id].ts' (ie, 'app/i.ts' or
  81. 'app/d.ts'), which is unexpected.
  82. Setting '--all' prevents this behavior, causing glob
  83. to treat ALL patterns as glob expressions to be expanded,
  84. even if they are an exact match to a file on disk.
  85. When setting this option, be sure to enquote arguments
  86. so that the shell will not expand them prior to passing
  87. them to the glob command process.
  88. `,
  89. },
  90. absolute: {
  91. short: 'a',
  92. description: 'Expand to absolute paths',
  93. },
  94. 'dot-relative': {
  95. short: 'd',
  96. description: `Prepend './' on relative matches`,
  97. },
  98. mark: {
  99. short: 'm',
  100. description: `Append a / on any directories matched`,
  101. },
  102. posix: {
  103. short: 'x',
  104. description: `Always resolve to posix style paths, using '/' as the
  105. directory separator, even on Windows. Drive letter
  106. absolute matches on Windows will be expanded to their
  107. full resolved UNC paths, eg instead of 'C:\\foo\\bar',
  108. it will expand to '//?/C:/foo/bar'.
  109. `,
  110. },
  111. follow: {
  112. short: 'f',
  113. description: `Follow symlinked directories when expanding '**'`,
  114. },
  115. realpath: {
  116. short: 'R',
  117. description: `Call 'fs.realpath' on all of the results. In the case
  118. of an entry that cannot be resolved, the entry is
  119. omitted. This incurs a slight performance penalty, of
  120. course, because of the added system calls.`,
  121. },
  122. stat: {
  123. short: 's',
  124. description: `Call 'fs.lstat' on all entries, whether required or not
  125. to determine if it's a valid match.`,
  126. },
  127. 'match-base': {
  128. short: 'b',
  129. description: `Perform a basename-only match if the pattern does not
  130. contain any slash characters. That is, '*.js' would be
  131. treated as equivalent to '**/*.js', matching js files
  132. in all directories.
  133. `,
  134. },
  135. dot: {
  136. description: `Allow patterns to match files/directories that start
  137. with '.', even if the pattern does not start with '.'
  138. `,
  139. },
  140. nobrace: {
  141. description: 'Do not expand {...} patterns',
  142. },
  143. nocase: {
  144. description: `Perform a case-insensitive match. This defaults to
  145. 'true' on macOS and Windows platforms, and false on
  146. all others.
  147. Note: 'nocase' should only be explicitly set when it is
  148. known that the filesystem's case sensitivity differs
  149. from the platform default. If set 'true' on
  150. case-insensitive file systems, then the walk may return
  151. more or less results than expected.
  152. `,
  153. },
  154. nodir: {
  155. description: `Do not match directories, only files.
  156. Note: to *only* match directories, append a '/' at the
  157. end of the pattern.
  158. `,
  159. },
  160. noext: {
  161. description: `Do not expand extglob patterns, such as '+(a|b)'`,
  162. },
  163. noglobstar: {
  164. description: `Do not expand '**' against multiple path portions.
  165. Ie, treat it as a normal '*' instead.`,
  166. },
  167. 'windows-path-no-escape': {
  168. description: `Use '\\' as a path separator *only*, and *never* as an
  169. escape character. If set, all '\\' characters are
  170. replaced with '/' in the pattern.`,
  171. },
  172. })
  173. .num({
  174. 'max-depth': {
  175. short: 'D',
  176. description: `Maximum depth to traverse from the current
  177. working directory`,
  178. },
  179. })
  180. .opt({
  181. cwd: {
  182. short: 'C',
  183. description: 'Current working directory to execute/match in',
  184. default: process.cwd(),
  185. },
  186. root: {
  187. short: 'r',
  188. description: `A string path resolved against the 'cwd', which is
  189. used as the starting point for absolute patterns that
  190. start with '/' (but not drive letters or UNC paths
  191. on Windows).
  192. Note that this *doesn't* necessarily limit the walk to
  193. the 'root' directory, and doesn't affect the cwd
  194. starting point for non-absolute patterns. A pattern
  195. containing '..' will still be able to traverse out of
  196. the root directory, if it is not an actual root directory
  197. on the filesystem, and any non-absolute patterns will
  198. still be matched in the 'cwd'.
  199. To start absolute and non-absolute patterns in the same
  200. path, you can use '--root=' to set it to the empty
  201. string. However, be aware that on Windows systems, a
  202. pattern like 'x:/*' or '//host/share/*' will *always*
  203. start in the 'x:/' or '//host/share/' directory,
  204. regardless of the --root setting.
  205. `,
  206. },
  207. platform: {
  208. description: `Defaults to the value of 'process.platform' if
  209. available, or 'linux' if not. Setting --platform=win32
  210. on non-Windows systems may cause strange behavior!`,
  211. validOptions: [
  212. 'aix',
  213. 'android',
  214. 'darwin',
  215. 'freebsd',
  216. 'haiku',
  217. 'linux',
  218. 'openbsd',
  219. 'sunos',
  220. 'win32',
  221. 'cygwin',
  222. 'netbsd',
  223. ],
  224. },
  225. })
  226. .optList({
  227. ignore: {
  228. short: 'i',
  229. description: `Glob patterns to ignore`,
  230. },
  231. })
  232. .flag({
  233. debug: {
  234. short: 'v',
  235. description: `Output a huge amount of noisy debug information about
  236. patterns as they are parsed and used to match files.`,
  237. },
  238. version: {
  239. short: 'V',
  240. description: `Output the version (${version})`,
  241. },
  242. help: {
  243. short: 'h',
  244. description: 'Show this usage information',
  245. },
  246. });
  247. try {
  248. const { positionals, values } = j.parse();
  249. const { cmd, shell, all, default: def, version: showVersion, help, absolute, cwd, dot, 'dot-relative': dotRelative, follow, ignore, 'match-base': matchBase, 'max-depth': maxDepth, mark, nobrace, nocase, nodir, noext, noglobstar, platform, realpath, root, stat, debug, posix, 'cmd-arg': cmdArg, } = values;
  250. if (showVersion) {
  251. console.log(version);
  252. process.exit(0);
  253. }
  254. if (help) {
  255. console.log(j.usage());
  256. process.exit(0);
  257. }
  258. //const { shell, help } = values
  259. if (positionals.length === 0 && !def)
  260. throw 'No patterns provided';
  261. if (positionals.length === 0 && def)
  262. positionals.push(def);
  263. const patterns = all ? positionals : positionals.filter(p => !existsSync(p));
  264. const matches = all ? [] : positionals.filter(p => existsSync(p)).map(p => join(p));
  265. const stream = globStream(patterns, {
  266. absolute,
  267. cwd,
  268. dot,
  269. dotRelative,
  270. follow,
  271. ignore,
  272. mark,
  273. matchBase,
  274. maxDepth,
  275. nobrace,
  276. nocase,
  277. nodir,
  278. noext,
  279. noglobstar,
  280. platform: platform,
  281. realpath,
  282. root,
  283. stat,
  284. debug,
  285. posix,
  286. });
  287. if (!cmd) {
  288. matches.forEach(m => console.log(m));
  289. stream.on('data', f => console.log(f));
  290. }
  291. else {
  292. cmdArg.push(...matches);
  293. stream.on('data', f => cmdArg.push(f));
  294. // Attempt to support commands that contain spaces and otherwise require
  295. // shell interpretation, but do NOT shell-interpret the arguments, to avoid
  296. // injections via filenames. This affordance can only be done on known Unix
  297. // shells, unfortunately.
  298. //
  299. // 'bash', ['-c', cmd + ' "$@"', 'bash', ...matches]
  300. // 'zsh', ['-c', cmd + ' "$@"', 'zsh', ...matches]
  301. // 'fish', ['-c', cmd + ' "$argv"', ...matches]
  302. const { SHELL = 'unknown' } = process.env;
  303. const shellBase = basename(SHELL);
  304. const knownShells = ['sh', 'ksh', 'zsh', 'bash', 'fish'];
  305. if ((shell || /[ "']/.test(cmd)) &&
  306. knownShells.includes(shellBase)) {
  307. const cmdWithArgs = `${cmd} "\$${shellBase === 'fish' ? 'argv' : '@'}"`;
  308. if (shellBase !== 'fish') {
  309. cmdArg.unshift(SHELL);
  310. }
  311. cmdArg.unshift('-c', cmdWithArgs);
  312. stream.on('end', () => foregroundChild(SHELL, cmdArg));
  313. }
  314. else {
  315. if (shell) {
  316. process.emitWarning('The --shell option is unsafe, and will be removed. To pass ' +
  317. 'positional arguments to the subprocess, use -g/--cmd-arg instead.', 'DeprecationWarning', 'GLOB_SHELL');
  318. }
  319. stream.on('end', () => foregroundChild(cmd, cmdArg, { shell }));
  320. }
  321. }
  322. }
  323. catch (e) {
  324. console.error(j.usage());
  325. console.error(e instanceof Error ? e.message : String(e));
  326. process.exit(1);
  327. }
  328. //# sourceMappingURL=bin.mjs.map