빌드노트
execFile에 넘긴 파이프와 별표가 그대로 인자가 될 때
Node.js execFile의 기본 직접 실행에서는 공백이 든 값도 인자 하나이고, 파이프·별표·변수 표기는 그대로 전달된다. 실제 argv와 명시한 Windows cmd 대조로 확인하고, 값·파일 목록을 직접 전달할 때와 소유한 고정 명령에 셸을 쓸 때를 구분한다.
터미널에서 쓰던 명령을 Node.js의 execFile()로 옮겼는데 |가 파이프를 만들지 않고 *.txt도 파일명으로 바뀌지 않는다면, 먼저 호출 코드의 실행 파일과 인자 배열을 확인한다. execFile()은 기본적으로 셸을 거치지 않는다. 배열에 넣은 |와 *.txt는 자식 프로그램이 받을 문자열이다.
Node.js 22.23.2의 execFile 문서 (새 탭에서 열림)는 기본 직접 실행에서 리다이렉션과 파일 패턴 확장을 제공하지 않는다고 설명한다. 값이나 파일명을 전달하려면 인자 배열을 유지하고, 셸 문법 자체가 필요한 고정 명령이라면 해석할 셸을 명시한다. 두 경우를 실제로 비교해 보자.
자식에게 도착한 인자를 확인한다
아래는 Windows 11 Pro, Node.js 22.23.2에서 별도로 실행한 합성 예제다. 시작 명령은 PowerShell 7.6.6에서 입력했다. 외부 패키지는 필요 없다. Node.js가 설치된 빈 폴더에 먼저 argv.cjs를 저장한다. 이 파일은 받은 추가 인자와 이번 예제의 환경변수 하나만 출력한다.
console.log(JSON.stringify({
argv: process.argv.slice(2),
env: process.env.LAB_EXECFILE_VALUE ?? null,
}));
slice(2)는 Node 실행 파일과 스크립트 경로를 제외한다. 이 두 항목의 의미는 process.argv 문서 (새 탭에서 열림)에서 확인할 수 있다. 같은 폴더에 alpha.txt와 two words.txt라는 일반 파일도 만든다. 내용은 아무 짧은 문자열이면 된다. 파일이 실제로 있는데도 별표가 그대로 전달되는지 확인하기 위한 준비다.
이제 direct.cjs에서 공백, 파이프, 별표와 세 종류의 변수 표기를 각각 하나의 인자로 넘긴다.
const { execFile } = require('node:child_process');
const env = { ...process.env, LAB_EXECFILE_VALUE: 'lab-value' };
const args = ['a b', '|', '*.txt', '%LAB_EXECFILE_VALUE%',
'$LAB_EXECFILE_VALUE', '$env:LAB_EXECFILE_VALUE'];
execFile(process.execPath, ['argv.cjs', ...args], {
cwd: __dirname, env, windowsHide: true,
}, (error, stdout) => {
if (error) { console.error(error); process.exitCode = 1; return; }
process.stdout.write(stdout);
});
process.execPath는 현재 Node 실행 파일의 경로다. 셸에서 찾은 node라는 이름 대신 이 파일을 자식으로 실행한다. cwd는 두 스크립트와 텍스트 파일이 있는 폴더로 정했다. 이 폴더에서 다음 명령을 실행하면 전달된 경계를 볼 수 있다.
node direct.cjs
직접 받은 출력은 다음과 같았고 종료 코드는 0이었다.
{"argv":["a b","|","*.txt","%LAB_EXECFILE_VALUE%","$LAB_EXECFILE_VALUE","$env:LAB_EXECFILE_VALUE"],"env":"lab-value"}
a b는 공백을 포함한 인자 하나다. |도 인자 하나이며 두 번째 명령을 시작하지 않았다. *.txt는 일치하는 파일 두 개가 있는데도 그대로 남았다. 세 변수 표기 역시 값으로 바뀌지 않았다. 반면 출력의 env는 lab-value다. 환경변수 전달은 성공했지만, 인자 문자열 안의 변수 표기를 해석하는 단계는 없었다.
공백을 보호하려고 배열 값 안에 따옴표를 추가할 필요도 없다. 같은 자식에 ['"a b"']를 넘긴 별도 대조에서는 따옴표 문자까지 포함한 인자가 도착했다. JavaScript 코드의 'a b'에서 바깥 작은따옴표는 문자열을 만드는 문법이고, '"a b"'의 큰따옴표는 그 문자열의 내용이다. 명령 전체를 'a b | *.txt'라는 요소 하나로 합친 대조도 문자열 한 개를 받았을 뿐이었다.
자기 코드에서 결과가 다르다면 shell 옵션, 실제 실행한 파일과 그 프로그램의 인자 처리부터 확인한다. 이 관찰은 Node 자식이 받은 값이다. 다른 CLI가 자체적으로 패턴을 확장하거나 |에 특별한 의미를 부여하는 것까지 막아 주지는 않는다.
변수값과 파일 목록을 부모에서 정한다
원하는 것이 변수 표기가 아니라 실제 값이라면 env.LAB_EXECFILE_VALUE를 배열에 넣으면 된다. 파일도 실제 이름을 골라 각각의 요소로 넣는다. 앞의 direct.cjs에서 const args = ... 선언을 다음으로 바꾼다. 두 텍스트 파일과 나머지 코드는 그대로 둔다.
const { readdirSync } = require('node:fs');
const files = readdirSync(__dirname, { withFileTypes: true })
.filter(entry => entry.isFile() && entry.name.endsWith('.txt'))
.map(entry => entry.name).sort();
const args = [env.LAB_EXECFILE_VALUE, ...files];
이 코드는 현재 예제 폴더의 일반 파일 중 이름이 소문자 .txt로 끝나는 것만 고른다. 하위 폴더는 검색하지 않는다. withFileTypes로 파일 종류를 함께 받는 동작은 readdirSync 문서 (새 탭에서 열림)에 설명돼 있다. 범용 glob 구현을 만든 것이 아니라 이 예제에 필요한 목록만 정한 것이다.
같은 node direct.cjs 명령으로 변경본을 실행했을 때 다음 값을 받았다.
{"argv":["lab-value","alpha.txt","two words.txt"],"env":"lab-value"}
파일명에 있는 공백도 유지됐다. 실제 CLI를 호출할 때는 이처럼 필요한 값과 파일명을 배열에 넣되, 그 CLI가 요구하는 옵션 위치와 파일명 규칙을 따른다. 파일 목록이 비었다면 *.txt를 다시 넣어 해결하지 말고, 검색 폴더·확장자 조건을 확인한 뒤 빈 입력을 허용할지 결정한다.
명령 언어가 필요할 때는 셸을 명시한다
환경변수 확장을 셸이 수행하는 경우도 비교했다. Windows에서 다음 내용을 별도 shell.cjs로 저장한다. 이 명령 문자열과 환경값은 모두 예제에서 정한 상수다.
const { execFile } = require('node:child_process');
const { join } = require('node:path');
const cmd = join(process.env.SystemRoot, 'System32', 'cmd.exe');
execFile(cmd, ['/d', '/c', 'echo %LAB_EXECFILE_VALUE%'], {
env: { ...process.env, LAB_EXECFILE_VALUE: 'lab-value' },
windowsHide: true,
}, (error, stdout) => {
if (error) { console.error(error); process.exitCode = 1; return; }
process.stdout.write(stdout);
});
같은 폴더에서 node shell.cjs를 실행하자 lab-value가 출력됐고 종료 코드는 0이었다. cmd 공식 문서 (새 탭에서 열림)에 따르면 /d는 AutoRun 명령 실행을 끄고, /c는 뒤의 명령을 수행한 뒤 종료한다. 여기서는 cmd.exe가 %LAB_EXECFILE_VALUE%를 해석했다. execFile()이라는 함수명만으로 셸 해석이 없다고 판단해서는 안 된다. 실행한 파일 자체가 셸이면 그 셸에 넘긴 명령은 해석된다.
대조에 사용한 셸은 C:\WINDOWS\System32\cmd.exe다. ver 출력은 10.0.26200.9457, 실행 파일의 파일 버전은 10.0.26100.8875였다. 앞에서 Node를 시작한 PowerShell이 이 명령을 해석한 것은 아니다. 또한 |로 한 명령의 출력을 다른 명령의 입력에 연결하는 것은 cmd의 명령 문법이다. 그런 고정 작업을 셸에 맡길 때는 명령 전체를 자신이 소유하고 검토할 수 있어야 한다.
기존 인자 배열에 곧바로 shell: true를 추가하는 방식은 피한다. Node.js 22 문서는 22.15.0부터 shell: true와 args를 함께 전달하는 사용을 폐기 예정으로 표시한다. 셸이 개입하면 인자 배열이 원래 보존하던 경계에 셸 해석이 추가되므로, 사용자 입력을 명령 문자열에 이어 붙이지 않는다. 위 대조는 고정된 무해 명령만 실행했으며 임의 입력을 안전하게 인용하는 방법을 검증한 것이 아니다.
Windows에서는 호출 대상의 확장자도 확인해야 한다. node.exe 같은 실행 파일과 .cmd·.bat 래퍼는 실행 조건이 다르다. Node의 Windows 배치 파일 설명 (새 탭에서 열림)은 배치 파일을 기본 직접 실행으로 시작할 수 없고, cmd.exe 같은 해석기가 필요하다고 명시한다. CLI 이름이 실제로 .cmd 래퍼를 가리킨다면 실제 실행 파일을 직접 호출할 수 있는지 확인하거나, 소유한 고정 배치 명령을 명시한 cmd로 실행한다.
실제로 확인한 범위는 Windows의 Node 자식 인자, 부모에서 만든 값·파일 목록, 명시한 cmd의 고정 명령이다. Linux·macOS의 셸이나 다른 CLI의 자체 glob 처리는 실행하지 않았다. .cmd는 명시한 cmd를 통한 고정 파이프 대조에 사용했지만, .cmd·.bat의 직접 실행 실패 코드와 공백이 있는 배치 파일 경로의 인용은 재현하지 않았다.