| Crates.io | nu-lint |
| lib.rs | nu-lint |
| version | 0.0.142 |
| created_at | 2025-10-17 17:09:35.457081+00 |
| updated_at | 2026-01-25 23:30:49.496202+00 |
| description | Linter for Nu shell scripts that helpfully suggests improvements |
| homepage | |
| repository | https://codeberg.org/wvhulle/nu-lint |
| max_upload_size | |
| id | 1887938 |
| size | 2,256,001 |
Linter for the innovative Nu shell.
Learning to use a new shell is a radical change that can use some assistance. This project is aimed at helping new and intermediate users of the Nu shell. Nu shell has a lot of useful features not found in other scripting languages. This linter will give you hints to use all of them and even offer automatic fixes.
Lint all Nu files in working directory with:
nu-lint
To see all options and get help:
nu-lint --help
Following screenshots were taking in Helix with nu-lint set-up as LSP (nu-lint --lsp).
space a in Helix): 
space k in Helix): 

To have nu-lint in your terminal directly (not just scripts), use nushell-lsp:


The rule turn_positional_into_stream_input recommends to use pipelines instead of positional arguments:
def filter-positive [numbers] {
$numbers | where $it > 0
}
def filter-positive [] {
where $it > 0
}
This encourages lazy pipeline input: a positional list argument loads all data into memory at once, while implicit pipeline input processes elements one at a time.
All rules are optional and can be disabled with a configuration file or comment. The rule definitions are compatible with:
Some of the rules need further testing and improvement. Please make an issue on the issue tracker to report any bugs. In early stages of development some rules may be replaced or renamed.
+150 rules are defined and most have automatic fixes available (list may be out-of-date):
idioms - Simplifications unique to the Nu language.
not_is_empty_to_is_not_empty (auto-fix): Simplify not ... is-empty to is-not-emptycolumns_in_to_has (auto-fix): Use 'has' operator instead of 'in ($record | columns)'columns_not_in_to_not_has (auto-fix): Simplify not-in columns to not-hasdispatch_with_subcommands: Match dispatch replaceable with subcommandsget_optional_to_has (auto-fix): Simplify get -o | is-not-empty to hasget_optional_to_not_has (auto-fix): Simplify get -o | is-empty to not-hashardcoded_math_constants (auto-fix): Hardcoded mathematical constants should use std/math constants insteadtranspose_items (auto-fix): Simplify transpose | each to itemsmerge_get_cell_path (auto-fix): Combine chained 'get' commands into cell pathsmerge_multiline_print (auto-fix): Consecutive prints mergeable into onepositional_to_pipeline (auto-fix): Data parameter convertible to pipeline inputsource_to_use: source replaceable with usecompound_assignment (auto-fix): Compound assignment operators simplify simple arithmetic.contains_to_regex_op (auto-fix): Use =~ and !~ operators instead of verbose 'str contains' checksansi_over_escape_codes (auto-fix): Raw ANSI escape replaceable with ansiappend_to_concat_assign (auto-fix): Use ++= operator instead of verbose append in assignmentcustom_log_command (auto-fix): Custom log command shadows stdlib. Use use std/log insteadchained_append (auto-fix): Use spread syntax instead of chained 'append' commandsmerge_flat_upsert (auto-fix): Merge consecutive flat field assignments with upsertmerge_nested_upsert (auto-fix): Merge consecutive nested field assignments with upsertuse_load_env (auto-fix): Use load-env for multiple $env assignmentsremove_hat_not_builtin (auto-fix): Detect unnecessary '^' prefix on external commandsparsing - Better ways to parse and transform text data.
lines_instead_of_split (auto-fix): Use 'lines' command for splitting by newlinesnever_space_split (auto-fix): Unnecessary quotes around variablelines_each_to_parse (auto-fix): Remove redundant 'each' wrapper around 'parse'simplify_regex_parse (auto-fix): Simplify 'parse --regex' to 'parse' with pattern syntaxsplit_row_get_multistatement (auto-fix): Extract field directly with 'parse' instead of storing split resultsplit_first_to_parse (auto-fix): Extract first field with 'parse' patternsplit_row_get_inline (auto-fix): Extract field by name with 'parse' patternsplit_row_space_to_split_words (auto-fix): Use 'split words' for whitespace splittingfilesystem - Simplify file and path operations.
from_after_parsed_open (auto-fix): open already parses known formats into structured dataopen_raw_from_to_open (auto-fix): Simplify open --raw | from to openstring_param_as_path (auto-fix): Parameter typed as string but used as filesystem pathdead-code - Remove unused or redundant code
self_import: Circular import: script imports itselfunnecessary_accumulate: Redundant accumulator pattern: can be simplifiedassign_then_return (auto-fix): Redundant variable before returndo_not_compare_booleans (auto-fix): Redundant comparison with boolean literalif_null_to_default (auto-fix): Simplify if-null pattern to | defaultredundant_ignore (auto-fix): Commands producing output that is discarded with '| ignore'unnecessary_mut (auto-fix): Variable marked mut but never reassignedunused_helper_functions (auto-fix): Function unreachable from entry pointsunused_parameter (auto-fix): Function parameter declared but never usedunused_variable (auto-fix): Variable declared but never usedscript_export_main (auto-fix): In scripts, 'def main' is the entry point and doesn't need 'export'string_may_be_bare (auto-fix): Quoted string can be bare wordsingle_call_command (auto-fix): Single-line command called only onceappend_to_concat_assign (auto-fix): Use ++= operator instead of verbose append in assignmentposix - Replace common bash/POSIX patterns.
ignore_over_dev_null (auto-fix): Use '| ignore' instead of redirecting to /dev/nullawk_to_pipeline (auto-fix): awk replaceable with structured pipelinebat_to_open (auto-fix): bat replaceable with open for file viewingcat_to_open (auto-fix): External cat replaceable with opendate_to_date_now (auto-fix): External date replaceable with date nowdf_to_sys_disks (auto-fix): df replaceable with sys disksredundant_echo (auto-fix): Redundant echo (identity function)find_to_glob (auto-fix): find replaceable with glob or lsfree_to_sys_mem (auto-fix): free replaceable with sys mem for memory infogrep_to_find_or_where (auto-fix): grep replaceable with find or wherehead_to_first (auto-fix): head replaceable with firsthostname_to_sys_host (auto-fix): hostname replaceable with sys hostexternal_cd_to_builtin (auto-fix): External cd replaceable with built-in cdexternal_ls_to_builtin (auto-fix): External ls replaceable with built-inpager_to_explore (auto-fix): Pager replaceable with exploreread_to_input (auto-fix): read replaceable with inputsed_to_str_transform (auto-fix): sed replaceable with str replaceexternal_sort_to_builtin (auto-fix): External sort replaceable with built-intac_to_reverse (auto-fix): tac replaceable with lines | reversetail_to_last (auto-fix): tail replaceable with lastuname_to_sys_host (auto-fix): uname replaceable with sys hostexternal_uniq_to_builtin (auto-fix): External uniq replaceable with built-inuptime_to_sys_host (auto-fix): uptime replaceable with sys hostusers_to_sys_users (auto-fix): users replaceable with sys usersw_to_sys_users (auto-fix): w replaceable with sys userswc_to_length (auto-fix): wc replaceable with lengthwho_to_sys_users (auto-fix): who replaceable with sys usersiteration - Better patterns for loops and iteration.
loop_counter_to_range: Loop counter to range iterationwhile_counter_to_range: Counter while-loop to range iterationruntime-errors - Preventing unexpected runtime behaviour.
add_hat_external_commands (auto-fix): Always use the '^' prefix on external commandsfragile_last_exit_code (auto-fix): Fragile LAST_EXIT_CODE checkcheck_complete_exit_code: Unchecked exit code after completedescriptive_error_messages: Error messages should be descriptive and actionableunescaped_interpolation: Unescaped braces in string interpolationexit_only_in_main: Avoid using 'exit' in functions other than 'main'check_typed_flag_before_use: Typed flag used without null checknon_final_failure_check: Non-final pipeline command exit code ignorederror_make_for_non_fatal (auto-fix): Use 'error make' for catchable errors in functions and try blockstry_instead_of_do: Use 'try' blocks instead of 'do' blocks for error-prone operationsunsafe_dynamic_record_access (auto-fix): Use 'get -o' for dynamic keys to handle missing keys safelymissing_stdin_in_shebang (auto-fix): Shebang missing --stdin for inputdynamic_script_import: Dynamic import path not statically validatedcatch_builtin_error_try: Catch runtime errors from built-in commands using 'try' blocksunchecked_cell_path_index (auto-fix): Cell path numeric index access may panic on empty listsunchecked_get_index (auto-fix): Prefer optional cell path $list.0? over get for index accesswrap_external_with_complete: External command missing complete wrappersource_to_use: source replaceable with usespread_list_to_external (auto-fix): List variables passed to external commands should be spread with ...glob_may_drop_quotes (auto-fix): Quoted glob pattern treated as literalrequire_main_with_stdin: Scripts using $in must define a main functionfiltering - Better patterns for filtering and selecting data.
each_if_to_where (auto-fix): Use 'where' for filtering instead of 'each' with 'if'for_filter_to_where: Use 'where' filter instead of for loop with if and appendomit_it_in_row_condition (auto-fix): Field names in 'where' row conditions don't need $it. prefixslice_to_drop (auto-fix): Use 'drop' instead of 'slice ..-N' to drop last N-1 elementsslice_to_last (auto-fix): Use 'last' instead of 'slice (-N)..' to get last N elementsslice_to_skip (auto-fix): Use 'skip' instead of 'slice N..' to skip first N elementsslice_to_take (auto-fix): Use 'take' instead of 'slice 0..N' to take first N elementswhere_closure_drop_parameter (auto-fix): You can drop the closure and its parameter in 'where' and 'filter'.remove_redundant_in (auto-fix): Redundant $in at pipeline startperformance - Rules with potential performance impact
redundant_nu_subprocess: Redundant nu -c subprocess calldispatch_with_subcommands: Match dispatch replaceable with subcommandsself_import: Circular import: script imports itselfpositional_to_pipeline (auto-fix): Data parameter convertible to pipeline inputunnecessary_accumulate: Redundant accumulator pattern: can be simplifiedmerge_multiline_print (auto-fix): Consecutive prints mergeable into onechained_str_transform (auto-fix): Consecutive str replace combinablestreaming_hidden_by_complete (auto-fix): Streaming commands should not be wrapped with 'complete'chained_append (auto-fix): Use spread syntax instead of chained 'append' commandstype-safety - Annotate with type hints where possible.
external_script_as_argument: Script path passed as command argumentnothing_outside_signature (auto-fix): nothing type used outside signatureadd_type_hints_arguments (auto-fix): Arguments of custom commands should have type annotationsstring_param_as_path (auto-fix): Parameter typed as string but used as filesystem pathmissing_output_type (auto-fix): Command missing output type annotationmissing_in_type (auto-fix): Command using $in missing input typeredundant_nu_subprocess: Redundant nu -c subprocess calldynamic_script_import: Dynamic import path not statically validateddocumentation - Improve usefullness user-facing messages.
add_doc_comment_exported_fn: Exported functions should have documentation commentsdescriptive_error_messages: Error messages should be descriptive and actionableadd_label_to_error: error make should include 'label'add_help_to_error: error make missing help fieldadd_span_to_label: labels should include 'span' to highlight error location in user codeadd_url_to_error: error make should include 'url' field to link to documentationmain_positional_args_docs: Missing docs on main positional parametermain_named_args_docs: Missing docs on main flag parametermax_positional_params: Custom commands should have ≤ 2 positional parametersexplicit_long_flags (auto-fix): Replace short flags (-f) with long flags (--flag)list_param_to_variadic (auto-fix): Use variadic ...args instead of a single list parametereffects - Handle built-in and external commands with side-effects.
dangerous_file_operations: File operation on dangerous system patherrors_to_stderr: Error messages should go to stderr, not stdoutdont_mix_different_effects: Functions should not mix different types of I/O operations or effects.print_and_return_data: Function prints and returns dataeach_nothing_to_for_loop (auto-fix): each mappings with no output should be written as for loops.silence_stderr_data: External commands that write data to stderr should not be silencedexternal - Replace common external CLI tools.
curl_to_http (auto-fix): curl replaceable with http commandsfd_to_glob (auto-fix): fd replaceable with glob or lsjq_to_nu_pipeline (auto-fix): Simple jq filter replaceable with pipelinewget_to_http_get (auto-fix): wget replaceable with http getexternal_which_to_builtin (auto-fix): External which replaceable with built-instructured_data_to_csv_tool (auto-fix): Table piped to CSV tool without to csvstructured_data_to_json_tool (auto-fix): Data piped to JSON tool without to jsonformatting - Formatting according to style-guide.
ansi_over_escape_codes (auto-fix): Raw ANSI escape replaceable with ansicollapsible_if (auto-fix): Nested if-statements collapsible with andforbid_excessive_nesting: Avoid excessive nesting (more than 4 levels deep)max_function_body_length: Function bodies should be short to maintain readabilityif_else_chain_to_match (auto-fix): Use 'match' for value-based branching instead of if-else-if chainsblock_brace_spacing (auto-fix): Block body needs spaces inside braces: { body } not {body}closure_brace_pipe_spacing (auto-fix): Space between { and | in closureclosure_pipe_body_spacing (auto-fix): Closure body needs spaces: {|x| body } not {|x|body}no_trailing_spaces (auto-fix): Eliminate trailing spaces at the end of linesomit_list_commas (auto-fix): Omit commas between list items.pipe_spacing (auto-fix): Inconsistent spacing around |record_brace_spacing (auto-fix): Record braces should touch content: {a: 1} not { a: 1 }reflow_wide_pipelines (auto-fix): Pipeline exceeds line length limitreflow_wide_lists (auto-fix): Wrap wide lists vertically across multiple lines.wrap_wide_records (auto-fix): Wrap records exceeding 80 chars or with deeply nested structuresnaming - Follow official naming conventions
kebab_case_commands: Custom commands should use kebab-case naming conventionscreaming_snake_constants: Constants should use SCREAMING_SNAKE_CASE naming conventionsnake_case_variables (auto-fix): Variables should use snake_case naming conventionadd_label_to_error: error make should include 'label'upstream - Forward warnings and errors of the Nu parser.
dynamic_script_import: Dynamic import path not statically validatednu_deprecated (auto-fix): Parser detected deprecated command or flag usagenu_parse_error: Parser encountered a syntax errorThe type installation that will always work on your system. From crates.io:
cargo install nu-lint
No system dependencies are required.
Build from source:
cargo install --path .
From latest git main branch:
cargo install --git https://codeberg.org/wvhulle/nu-lint
Run without installing permanently (using flakes):
nix run git+https://codeberg.org/wvhulle/nu-lint
Download the appropriate binary from the releases subpage.
Available at VS Code Marketplace.
Add to your ~/.config/helix/languages.toml:
[language-server.nu-lint]
command = "nu-lint"
args = ["--lsp"]
[[language]]
name = "nu"
language-servers = ["nu-lint"]
The official Nu LSP server offers completion and some other hints. It should be configured out of the box for new Helix installations and environments that have Nushell installed.
Add to your Neovim configuration (Lua):
vim.lsp.config['nu-lint'] = {
cmd = { 'nu-lint', '--lsp' },
filetypes = { 'nu' },
root_markers = { '.git' }
}
vim.lsp.enable('nu-lint')
You may want to configure official Nu language server in addition to this linter, see nu --lsp command.
Add to your Emacs configuration (with Eglot, built-in since Emacs 29):
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'(nushell-mode "nu-lint" "--lsp")))
Add to your ~/.config/kate/lspclient/settings.json:
{
"servers": {
"nushell": {
"command": ["nu-lint", "--lsp"],
"highlightingModeRegex": "^Nushell$"
}
}
}
You can also implement your own editor extensions using the --lsp flag as in: nu-lint --lsp. This will spawn a language server compliant with the Language Server Protocol.
Some rules are deactivated by default. Usually because they are too noisy or annoy people. You should activate them with the config file and a level override.
A configuration file at the top of the workspace is optional and should be named .nu-lint.toml in your project root. It may look like this:
# Some rules are configurable
max_pipeline_length = 80
pipeline_placement = "start"
explicit_optional_access = true
# Set lint level of a set of rules at once.
[groups]
performance = "warning"
type-safety = "error"
# Override a single rule level
[rules]
dispatch_with_subcommands = "hint"
unchecked_cell_path_index = "off"
In this particular case, the user overrides the 'level' of certain groups and individual rules.
You can also turn of a rule violation on a particular lien by appending a comment to the line:
$data | each {|row|
$row.values.0 # nu-lint-ignore: unchecked_cell_path_index
}
There is a shortcut to do this by selecting the "ignore line violation" code action in the code action menu of your editor.
For any setting you don't set in the optional workspace configuration file, the defaults set in ./src/config.rs will be used. If you specify the option in the configuration file, it will override the defaults.