Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 32 additions & 139 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,179 +106,72 @@ but I do not want this project to live in "pre-1.0" forever.

## Command Line Usage

The `@chrisoakman/standard-clojure-style` npm package exposes a command-line
tool to help format your Clojure projects. You may wish to run this as a git
hook, via continuous integration, an editor integration, etc.

If you have Node.js installed on your system, you can try out Standard Clojure
Style with the `npx` command:
Use `list` to preview files, `check` to verify formatting, and `fix` to format
files in place. `check` does not modify files; `fix` does.

```sh
## NOTE: the "fix" command will change your files on disk!
## Please ensure a clean git working tree or new branch as necessary

# formats the file located at src/com/example/foo.clj
npx @chrisoakman/standard-clojure-style fix src/com/example/foo.clj
standard-clj list src/ test/
standard-clj check src/ test/

# formats all .clj, .cljs, .cljc, .jank, .edn files found in the src/ directory
# and subdirectories (ie: recursive)
npx @chrisoakman/standard-clojure-style fix src/
# NOTE: "fix" writes to your files on disk and cannot undo its changes.
# Please ensure a clean git working tree or new branch as necessary.
standard-clj fix src/ test/
```

If you plan to use the library frequently you may wish to install it globally:
Directories are searched recursively. By default, Standard Clojure Style finds
`.clj`, `.cljs`, `.cljc`, `.jank`, and `.edn` files. You can also pass individual
files:

```sh
# Installs "standard-clj" globally onto your system via npm
npm install --global @chrisoakman/standard-clojure-style
standard-clj fix src/my_app/core.clj deps.edn
```

#### Quick Reference
For more control, use `--include` and `--ignore`:

```sh
# use the "list" command to see which files standard-clj will analyze
standard-clj list src/

# use the "check" command to see which files need formatting
standard-clj check src-clj/ src-cljs/

## use the "fix" command to format files with Standard Clojure Style
standard-clj fix src/ test/ project.clj

## you can pass a glob pattern for more control over which files are formatted
standard-clj fix --include "src/**/*.{clj,cljs,cljc}"

## ignore files or folders with the --ignore flag
standard-clj fix --include "src/**/*.{clj,cljs,cljc}" --ignore "src/com/example/some_weird_file.clj"

## standard-clj will look for a .standard-clj.edn or .standard-clj.json file in the directory where
## the command is run from (likely the root directory for your project)
echo '{:include ["src-clj/**/*.clj" "src-cljs/**/*.cljs"]}' > .standard-clj.edn
standard-clj fix

## or pass a config file explicitly using the --config argument
standard-clj list --config /home/user1/my-project/my-standard-cfg.json

## pipe code directly to the fix command using "-"
echo '(ns my.company.core (:require [clojure.string :as str]))' | standard-clj fix -
```

#### `list` command

Use `standard-clj list` to see which files will be effected by the `check` and
`fix` commands. This command is useful in order to test your `--include`
glob patterns or `.standard-clj.edn` config files.

```sh
# prints each filename that will be effected by the "check" and "fix" commands
standard-clj list src/

# output the same file list in various data formats
standard-clj list src/ --output json
standard-clj list src/ --output json-pretty
standard-clj list src/ --output edn
standard-clj list src/ --output edn-pretty
standard-clj check \
--include "src/**/*.{clj,cljs,cljc}" \
--ignore "src/generated/**/*"
```

#### `check` command

Use `standard-clj check` to see if files are already formatted with Standard
Clojure Style. Useful for continuous integration. This command will **not** write
to any files on disk.

Returns exit code 0 if all files are already formatted, 1 otherwise.
Most projects that use Standard Clojure Style regularly should commit a
`.standard-clj.edn` file:

```sh
# check to see if files are already formatted with Standard Clojure Style
standard-clj check src-clj/ src-cljs/ test/

# runs the same check, but only prints files that need fixing
standard-clj check src-clj/ src-cljs/ test/ --log-level=ignore-already-formatted
```clojure
{:include ["src/" "test/"]
:ignore ["src/generated/"]}
```

#### `fix` command

Use `standard-clj fix` to format files according to Standard Clojure Style.
This command **will** write to files on disk, so please ensure a clean git
working tree or new branch as necessary. The changes made by this command
cannot be undone by this program.

Returns exit code 0 if all files have been formatted, 1 otherwise.
Then run:

```sh
# format files according to Standard Clojure Style
standard-clj fix src/ test/ deps.edn
standard-clj check
```

#### `fix -` command (stdin / stdout)

Use `standard-clj fix -` to pipe code directly via stdin.
Use `standard-clj list` whenever you want to confirm which files were selected.

Prints the formatted code to stdout with error code 0 if successful. Prints an
error message to stderr with error code 1 otherwise.
Run the package without installation:

```sh
echo '(ns my.company.core (:require [clojure.string :as str]))' | standard-clj fix -
npx @chrisoakman/standard-clojure-style check src/ test/
```

#### Which files will be formatted?

`standard-clj` accepts several ways to know which files to format:

* pass filenames directly as arguments
* pass directories directly as arguments
* pass a [glob pattern] with the `--include` option
Or install the CLI globally:

```sh
# will fix:
# - dev/user.clj (single file argument)
# - project.clj (single file argument)
# - all .clj, .cljs, .cljc, .edn files in the src-clj/ directory and subdirectories (directory argument)
# - all .edn files in the resources/ directory and subdirectories (glob pattern argument)
standard-clj fix dev/user.clj project.clj src-clj/ test/ --include "resources/**/*.edn"
npm install --global @chrisoakman/standard-clojure-style
```

`--include` or `--ignore` arguments passed via command line will supercede any
`--include` or `--ignore` arguments found via config file.

You can always use the `list` command to see which files will be formatted by `standard-clj`.

#### Other options

- `--config` or `-c` - pass a filepath of a config file to use for options to the `standard-clj` program.
- `--ignore` or `-ig` - exclude files from `list`, `check`, or `fix` commands. Accepts individual files or directories.
- `--include` or `-in` - include files for the `list`, `check`, or `fix` commands. Accepts a [glob pattern].
- `--log-level` or `-l` - specify a logging level
- `"everything"` or `0` - prints everything to either stdout or stderr. This is the default.
- `"ignore-already-formatted"` or `1`
- For the `check` command, will only print files that need formatting.
- For the `fix` command, will only print files that were formatted or have errors.
- This option can be less noisy in your terminal if you have a project with many files and only
want to see the ones that need formatting.
- `"quiet"` or `5` - will not print anything to stdout or stderr for the `check` or `fix` commands

[glob pattern]:https://github.com/isaacs/node-glob?tab=readme-ov-file#glob-primer

#### Options via config file

By default, `standard-clj` will look for a `.standard-clj.edn` or
`.standard-clj.json` file located in the directory where the command is run.
Most projects that use `standard-clj` regularly will want to commit this file
to their git repo for convenience.
The `fix` command also supports stdin:

```sh
# create a .standard-clj.edn file
echo '{:include ["src-clj/**/*.clj" "src-cljs/**/*.cljs"]}' > .standard-clj.edn

# run the "fix" command with options from that file
standard-clj fix
echo '(ns my.company.core)' | standard-clj fix -
```

You can use the `--config` or `-c` flag to specify a different file location:
See the [CLI docs] for config-file formats, glob syntax, option precedence,
custom file extensions, and additional examples.

```sh
# run the "fix" command with options from ./my-config-file.edn
standard-clj fix --config ./my-config-file.edn
```
[CLI docs]:https://github.com/oakmac/standard-clojure-style-js/blob/master/docs/cli.md

## Ignore a file or form

Expand Down
Binary file modified bun.lockb
Binary file not shown.
116 changes: 33 additions & 83 deletions cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import process from 'process'

// npm imports
import { parseEDNString, toEDNStringFromSimpleObject } from 'edn-data'
import { globSync } from 'glob'
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import yocto from 'yoctocolors'
Expand All @@ -28,7 +27,8 @@ import yocto from 'yoctocolors'
// script before publishing to npm
import standardClj from './lib/standard-clojure-style.js' // 7b323d1c-2984-4bd1-9304-d62d8dee9a1f

// Pure helper functions (no side effects, tested separately)
// CLI helpers
import cliFileDiscovery from './cli_file_discovery.mjs'
import cliUtil from './cli_util.js'

const scriptStartTime = performance.now()
Expand Down Expand Up @@ -110,10 +110,6 @@ async function readStream (stream) {
return Buffer.concat(chunks).toString('utf8')
}

function alwaysTrue () {
return true
}

function setLogLevel (level) {
logLevel = cliUtil.normalizeLogLevel(level)
}
Expand Down Expand Up @@ -176,94 +172,48 @@ function injectConfigFile (argv) {
// =============================================================================

// returns a Set of files from the args passed to the "list", "check", or "fix" commands
function getFilesFromArgv (argv, cmd) {
function getFilesFromArgv (argv) {
// remove the first item, which is the command
argv._.shift()
const directArgs = argv._
const fileExtensionsSet = argv['file-ext']
let includeFiles = []

// process the direct arguments
directArgs.forEach(arg => {
let possibleFileOrDir = arg
if (!fs.isAbsolute(arg)) {
// if the argument is not an absolute path, assume it is relative to the
// directory where the script is being run from
possibleFileOrDir = path.join(rootDir, arg)
}

if (fs.isFileSync(possibleFileOrDir)) {
includeFiles.push(possibleFileOrDir)
} else if (fs.isDirectorySync(possibleFileOrDir)) {
fs.traverseTreeSync(possibleFileOrDir, (f) => {
const fileExt = path.extname(f)
if (fileExtensionsSet.has(fileExt)) {
includeFiles.push(f)
}
return true
}, alwaysTrue)
} else {
printToStderr(yocto.bold(yocto.yellow('WARN')) + ' Could not find a file or directory at "' + arg + '"')
}
})

// process the --include glob patterns
if (cliUtil.isArray(argv.include)) {
argv.include.forEach(includeStr => {
const filesFromGlob = globSync(includeStr)
includeFiles = includeFiles.concat(filesFromGlob)
})
let cliIncludePatterns = []
const cliIncludeWasPassed = cliUtil.isArray(argv.include)
if (cliIncludeWasPassed) {
cliIncludePatterns = argv.include
}

// load --include files via config file if the user did not pass any direct arguments
const anyDirectArgsPassed = directArgs.length > 0
if (!anyDirectArgsPassed && argv._optionsLoadedViaConfigFile && cliUtil.isArray(argv.includeFromConfig)) {
argv.includeFromConfig.forEach(includeStr => {
const filesFromGlob = globSync(includeStr)
includeFiles = includeFiles.concat(filesFromGlob)
})
const anyCliFileSelection = directArgs.length > 0 || cliIncludeWasPassed
let includePatterns = cliIncludePatterns
if (!anyCliFileSelection &&
argv._optionsLoadedViaConfigFile &&
cliUtil.isArray(argv.includeFromConfig)) {
includePatterns = argv.includeFromConfig
}

// exclude files if necessary
const ignoreFiles = []
let ignorePatterns = null
// use --ignore from CLI argument
let ignorePatterns = []
if (cliUtil.isArray(argv.ignore)) {
ignorePatterns = argv.ignore
// or from config file if present
} else if (argv._optionsLoadedViaConfigFile && cliUtil.isArray(argv.ignoreFromConfig)) {
ignorePatterns = argv.ignoreFromConfig
}

if (ignorePatterns) {
ignorePatterns.forEach(ignoreStr => {
let possibleFileOrDir = ignoreStr
if (!fs.isAbsolute(ignoreStr)) {
// if the argument is not an absolute path, assume it is relative to the
// directory where the script is being run from
possibleFileOrDir = path.join(rootDir, ignoreStr)
return cliFileDiscovery.discoverFiles({
rootDir,
directArgs,
includePatterns,
ignorePatterns,
fileExtensions: argv['file-ext'],
onMissingPath: (kind, filename) => {
let ignoreText = ''
if (kind === 'ignore') {
ignoreText = ' to ignore'
}

if (fs.isFileSync(possibleFileOrDir)) {
ignoreFiles.push(possibleFileOrDir)
} else if (fs.isDirectorySync(possibleFileOrDir)) {
fs.traverseTreeSync(possibleFileOrDir, (f) => {
const fileExt = path.extname(f)
if (fileExtensionsSet.has(fileExt)) {
ignoreFiles.push(f)
}
return true
}, alwaysTrue)
} else {
printToStderr(yocto.bold(yocto.yellow('WARN')) + ' Could not find a file or directory to ignore at "' + ignoreStr + '"')
}
})
}

const includeFilesSet = new Set(includeFiles)
const ignoreFileSet = new Set(ignoreFiles)

return cliUtil.setDifference(includeFilesSet, ignoreFileSet)
printToStderr(
yocto.bold(yocto.yellow('WARN')) +
' Could not find a file or directory' + ignoreText + ' at "' + filename + '"'
)
}
})
}

// =============================================================================
Expand Down Expand Up @@ -366,7 +316,7 @@ function processCheckCmd (argv) {

printProgramInfo({ command: 'check' })

const filesToProcess = getFilesFromArgv(argv, 'check')
const filesToProcess = getFilesFromArgv(argv)

if (filesToProcess.size === 0) {
exitSad('No files were passed to the "check" command. Please pass a filename, directory, or --include glob pattern.')
Expand Down Expand Up @@ -413,7 +363,7 @@ function processFixCmdNotStdin (argv) {

printProgramInfo({ command: 'fix' })

const filesToProcess = getFilesFromArgv(argv, 'fix')
const filesToProcess = getFilesFromArgv(argv)

if (filesToProcess.size === 0) {
exitSad('No files were passed to the "fix" command. Please pass a filename, directory, or --include glob pattern.')
Expand Down Expand Up @@ -487,7 +437,7 @@ function processFixCmd (argv) {
}

function processListCmd (argv) {
const filesSet = getFilesFromArgv(argv, 'list')
const filesSet = getFilesFromArgv(argv)
const sortedFiles = setToArray(filesSet).sort()

if (argv.output === 'json') {
Expand Down
Loading
Loading