bin.mjs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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 { 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. all: {
  35. short: 'A',
  36. description: `By default, the glob cli command will not expand any
  37. arguments that are an exact match to a file on disk.
  38. This prevents double-expanding, in case the shell expands
  39. an argument whose filename is a glob expression.
  40. For example, if 'app/*.ts' would match 'app/[id].ts', then
  41. on Windows powershell or cmd.exe, 'glob app/*.ts' will
  42. expand to 'app/[id].ts', as expected. However, in posix
  43. shells such as bash or zsh, the shell will first expand
  44. 'app/*.ts' to a list of filenames. Then glob will look
  45. for a file matching 'app/[id].ts' (ie, 'app/i.ts' or
  46. 'app/d.ts'), which is unexpected.
  47. Setting '--all' prevents this behavior, causing glob
  48. to treat ALL patterns as glob expressions to be expanded,
  49. even if they are an exact match to a file on disk.
  50. When setting this option, be sure to enquote arguments
  51. so that the shell will not expand them prior to passing
  52. them to the glob command process.
  53. `,
  54. },
  55. absolute: {
  56. short: 'a',
  57. description: 'Expand to absolute paths',
  58. },
  59. 'dot-relative': {
  60. short: 'd',
  61. description: `Prepend './' on relative matches`,
  62. },
  63. mark: {
  64. short: 'm',
  65. description: `Append a / on any directories matched`,
  66. },
  67. posix: {
  68. short: 'x',
  69. description: `Always resolve to posix style paths, using '/' as the
  70. directory separator, even on Windows. Drive letter
  71. absolute matches on Windows will be expanded to their
  72. full resolved UNC maths, eg instead of 'C:\\foo\\bar',
  73. it will expand to '//?/C:/foo/bar'.
  74. `,
  75. },
  76. follow: {
  77. short: 'f',
  78. description: `Follow symlinked directories when expanding '**'`,
  79. },
  80. realpath: {
  81. short: 'R',
  82. description: `Call 'fs.realpath' on all of the results. In the case
  83. of an entry that cannot be resolved, the entry is
  84. omitted. This incurs a slight performance penalty, of
  85. course, because of the added system calls.`,
  86. },
  87. stat: {
  88. short: 's',
  89. description: `Call 'fs.lstat' on all entries, whether required or not
  90. to determine if it's a valid match.`,
  91. },
  92. 'match-base': {
  93. short: 'b',
  94. description: `Perform a basename-only match if the pattern does not
  95. contain any slash characters. That is, '*.js' would be
  96. treated as equivalent to '**/*.js', matching js files
  97. in all directories.
  98. `,
  99. },
  100. dot: {
  101. description: `Allow patterns to match files/directories that start
  102. with '.', even if the pattern does not start with '.'
  103. `,
  104. },
  105. nobrace: {
  106. description: 'Do not expand {...} patterns',
  107. },
  108. nocase: {
  109. description: `Perform a case-insensitive match. This defaults to
  110. 'true' on macOS and Windows platforms, and false on
  111. all others.
  112. Note: 'nocase' should only be explicitly set when it is
  113. known that the filesystem's case sensitivity differs
  114. from the platform default. If set 'true' on
  115. case-insensitive file systems, then the walk may return
  116. more or less results than expected.
  117. `,
  118. },
  119. nodir: {
  120. description: `Do not match directories, only files.
  121. Note: to *only* match directories, append a '/' at the
  122. end of the pattern.
  123. `,
  124. },
  125. noext: {
  126. description: `Do not expand extglob patterns, such as '+(a|b)'`,
  127. },
  128. noglobstar: {
  129. description: `Do not expand '**' against multiple path portions.
  130. Ie, treat it as a normal '*' instead.`,
  131. },
  132. 'windows-path-no-escape': {
  133. description: `Use '\\' as a path separator *only*, and *never* as an
  134. escape character. If set, all '\\' characters are
  135. replaced with '/' in the pattern.`,
  136. },
  137. })
  138. .num({
  139. 'max-depth': {
  140. short: 'D',
  141. description: `Maximum depth to traverse from the current
  142. working directory`,
  143. },
  144. })
  145. .opt({
  146. cwd: {
  147. short: 'C',
  148. description: 'Current working directory to execute/match in',
  149. default: process.cwd(),
  150. },
  151. root: {
  152. short: 'r',
  153. description: `A string path resolved against the 'cwd', which is
  154. used as the starting point for absolute patterns that
  155. start with '/' (but not drive letters or UNC paths
  156. on Windows).
  157. Note that this *doesn't* necessarily limit the walk to
  158. the 'root' directory, and doesn't affect the cwd
  159. starting point for non-absolute patterns. A pattern
  160. containing '..' will still be able to traverse out of
  161. the root directory, if it is not an actual root directory
  162. on the filesystem, and any non-absolute patterns will
  163. still be matched in the 'cwd'.
  164. To start absolute and non-absolute patterns in the same
  165. path, you can use '--root=' to set it to the empty
  166. string. However, be aware that on Windows systems, a
  167. pattern like 'x:/*' or '//host/share/*' will *always*
  168. start in the 'x:/' or '//host/share/' directory,
  169. regardless of the --root setting.
  170. `,
  171. },
  172. platform: {
  173. description: `Defaults to the value of 'process.platform' if
  174. available, or 'linux' if not. Setting --platform=win32
  175. on non-Windows systems may cause strange behavior!`,
  176. validOptions: [
  177. 'aix',
  178. 'android',
  179. 'darwin',
  180. 'freebsd',
  181. 'haiku',
  182. 'linux',
  183. 'openbsd',
  184. 'sunos',
  185. 'win32',
  186. 'cygwin',
  187. 'netbsd',
  188. ],
  189. },
  190. })
  191. .optList({
  192. ignore: {
  193. short: 'i',
  194. description: `Glob patterns to ignore`,
  195. },
  196. })
  197. .flag({
  198. debug: {
  199. short: 'v',
  200. description: `Output a huge amount of noisy debug information about
  201. patterns as they are parsed and used to match files.`,
  202. },
  203. })
  204. .flag({
  205. help: {
  206. short: 'h',
  207. description: 'Show this usage information',
  208. },
  209. });
  210. try {
  211. const { positionals, values } = j.parse();
  212. if (values.help) {
  213. console.log(j.usage());
  214. process.exit(0);
  215. }
  216. if (positionals.length === 0 && !values.default)
  217. throw 'No patterns provided';
  218. if (positionals.length === 0 && values.default)
  219. positionals.push(values.default);
  220. const patterns = values.all ? positionals : positionals.filter(p => !existsSync(p));
  221. const matches = values.all ?
  222. []
  223. : positionals.filter(p => existsSync(p)).map(p => join(p));
  224. const stream = globStream(patterns, {
  225. absolute: values.absolute,
  226. cwd: values.cwd,
  227. dot: values.dot,
  228. dotRelative: values['dot-relative'],
  229. follow: values.follow,
  230. ignore: values.ignore,
  231. mark: values.mark,
  232. matchBase: values['match-base'],
  233. maxDepth: values['max-depth'],
  234. nobrace: values.nobrace,
  235. nocase: values.nocase,
  236. nodir: values.nodir,
  237. noext: values.noext,
  238. noglobstar: values.noglobstar,
  239. platform: values.platform,
  240. realpath: values.realpath,
  241. root: values.root,
  242. stat: values.stat,
  243. debug: values.debug,
  244. posix: values.posix,
  245. });
  246. const cmd = values.cmd;
  247. if (!cmd) {
  248. matches.forEach(m => console.log(m));
  249. stream.on('data', f => console.log(f));
  250. }
  251. else {
  252. stream.on('data', f => matches.push(f));
  253. stream.on('end', () => foregroundChild(cmd, matches, { shell: true }));
  254. }
  255. }
  256. catch (e) {
  257. console.error(j.usage());
  258. console.error(e instanceof Error ? e.message : String(e));
  259. process.exit(1);
  260. }
  261. //# sourceMappingURL=bin.mjs.map