When One Loop Isn't Clearly Better

During a recent file reorganization task, I needed to process output from the Linux find command—splitting results into lines, trimming whitespace, filtering empties, and mapping each file to a move command. The complete script handled roughly 50,000 items. The implementation chained map, filter, and another map across the array.

const lines = execSync(`find "${searchPath}" -type f`).toString().split('\n')

const commands = lines
	.map((f) => f.trim())
	.filter(Boolean)
	.map((file) => {
		const destFile = getDestFile(file)
		const destFileDir = path.dirname(destFile)
		return `mkdir -p "${destFileDir}" && mv "${file}" "${destFile}"`
	})

commands.forEach((command) => execSync(command))

Several readers suggested switching to reduce, presumably for performance—to avoid multiple passes over the array. That reasoning misses the point for one-off scripts. The bottleneck wasn't array iteration; it was executing the filesystem commands. Performance concerns only matter when the operation itself is expensive or runs frequently, not when a script completes acceptably.

Suggestions to adopt Node-specific APIs or npm modules for cross-platform support were also off-target. Adding dependencies and abstraction to a one-off script that already works "fast enough" just increases complexity without practical benefit.

The reduce Rewrite

The same logic expressed with reduce becomes:

const commands = lines.reduce((accumulator, line) => {
	let file = line.trim()
	if (file) {
		const destFile = getDestFile(file)
		const destFileDir = path.dirname(destFile)
		accumulator.push(`mkdir -p "${destFileDir}" && mv "${file}" "${destFile}"`)
	}
	return accumulator
}, [])

While reduce isn't inherently problematic, the version here is arguably less readable than the original chained approach. The accumulation logic obscures the pipeline of transformations, making it harder to see at a glance what each stage does.

The Loop Alternative

Rewriting as a traditional loop after years of using array methods takes some thought. A classic for loop doesn't come out simpler either:

const commands = []
for (let index = 0; index < lines.length; index++) {
	const line = lines[index]
	const file = line.trim()
	if (file) {
		const destFile = getDestFile(file)
		const destFileDir = path.dirname(destFile)
		commands.push(`mkdir -p "${destFileDir}" && mv "${file}" "${destFile}"`)
	}
}

The for..of form, however, does improve on the traditional counter-based loop:

const commands = []
for (const line of lines) {
	const file = line.trim()
	if (!file) continue

	const destFile = getDestFile(file)
	const destFileDir = path.dirname(destFile)
	commands.push(`mkdir -p "${destFileDir}" && mv "${file}" "${destFile}"`)
}

That version is fairly clean, even if some developers dismiss loops as "imperative." Practical utility beats stylistic prejudice.

Choosing Between Approaches

The typical decision comes down to chaining versus for..of. If iterating multiple times is a genuine performance issue, for..of is the straightforward choice—single pass, no intermediate arrays, minimal ceremony.

reduce gets used less often, but trying it against other options before deciding is reasonable. Code style is subjective; the right tool depends on the specific array, the transformations, and the context—especially whether the code runs once or lives in a hot path.